返回资讯中心
外部精选
软件工程
#开发者实时流

CobaltC and Rust

x op= e is x = x op e with x evaluated once: the same types, the same overflow

Hacker News 最新SilentLambda2026/10/17 · 20 天前30 分钟阅读

以下正文同步自 Hacker News 最新,版权归原站所有,已转换为易读排版。

x op= e is x = x op e with x evaluated once: the same types, the same overflow check (x += 1 can overflow), the same write rules. Evaluating the place once matters only when it contains a call, as in *Vec::index_exclusive(&mut v, i) += 1, which calls index_exclusive once. There is no ++ or --; write += 1.

Blocks, if, while, for, foreach, break and continue; scope exit as the moment of destruction; and the fixed line between compile-time and run-time checks.

Control flow is C's if, while and for, without switch and goto, plus expression-valued if and match and a foreach over collections. What is new is how carefully the language ties scope to destruction, and the precise line between what the compiler proves and what the run time checks.

A block { … } opens a scope. Variables declared in it die when it ends, in reverse order; resources are destroyed at that point (§07); references held by those variables end too. A block is an expression: its value is its trailing expression, or () if there is none. Every statement is a scope of its own for temporaries: a value created during a statement and not stored anywhere is destroyed at the semicolon.

if (cond) { … } else { … } with mandatory parentheses and mandatory braces. else if chains as expected. An if without else has type void; an if used as a value needs both arms, of the same type.

The condition may also be a pattern test, if (Some(x) = e) { … } else { … }: it means match (e) { Some(x) : { … }, _ : { … } }, so x exists in the first block only, e is consumed exactly as that match would consume it, and the whole if can be a value. Any match pattern works — nested (Ok(Some(l))), a literal (if (0 = n)), a qualified variant — as long as it can fail: a bare name on the left of = is the assignment it always is, which is not a bool and is rejected, so if (x = e) never silently tests anything. A match stays the form when both cases do work, and the only form that checks every case is handled.

import std;

fn main() { HashMap<String, i32> ages = HashMap::new(); HashMap::insert(&mut ages, String::from_str("Ada"), 36); if (Some(a) = HashMap::get_str(&ages, "Ada")) { assert(*a == 36); // a: ref<i32, shared>, in scope here only } Option<i32> o = Some(4); i32 doubled = if (Some(x) = o) { x * 2 } else { 0 }; assert(doubled == 8); Result<Option<i32>, str> r = Ok(Some(7)); if (Ok(Some(n)) = r) { assert(n == 7); } else { assert(false); } if (Err(_) = r) { assert(false); } }

while, for, break, continue

while (cond) { … } loops while cond holds; for (init; cond; step) { … } is C's counted loop: init runs once (a declaration there belongs to the loop), cond is tested before each iteration, and step runs after each one. Any of the three may be left out; a missing cond is true, so for (;;) loops until a break. The body is a fresh scope each iteration, so anything declared inside it is destroyed before the next test. break exits the innermost loop; continue goes to its next iteration, running a for's step first. Both are typed never and must appear inside a loop. A body is always a block, so C's trap for (…); (an empty loop, then a block that runs once) is a syntax error here, and the message says to remove the ;.

A while condition may be a pattern test too: while (Some(x) = e) { … } is while (true) { match (e) { Some(x) : { … }, _ : break } } — e is evaluated again each time round, the body runs with x bound while the pattern matches, and the first value that does not match ends the loop. That last point matters for a Result: while (Ok(Some(line)) = read_line()) stops at the end of input and on a read error alike, which is the loop's written decision; a loop that must tell them apart keeps the match.

import std;

fn main() { Vec<String> stack = Vec::from_fn(3, [](usize i) : String { sprintf("job %d", i) }); usize done = 0; while (Some(job) = Vec::pop(&mut stack)) // each job moved out, destroyed at the body's end { if (String::ends_with(&job, "1")) { continue; } done += 1; } assert(done == 2 && Vec::len(&stack) == 0); u32 n = 10; while (Some(m) = checked_sub(n, 3)) // stops when the subtraction would go below zero { n = m; } assert(n == 1); }

Loopsruns

import std;

fn main() { i32 sum = 0; i32 i = 0; while (i < 10) { i += 1; if (i == 3) { continue; // skip 3 } if (i == 7) { break; // stop before adding 7 } sum += i; } assert(sum == 1 + 2 + 4 + 5 + 6); Vec<i32> v = Vec::new(); Vec::push(&mut v, 4); Vec::push(&mut v, 9); i32 total = 0; for (usize k = 0; k < Vec::len(&v); k += 1) // an index loop; foreach (below) is shorter { total += v[k]; } assert(total == 13); u32 odd = 0; for (u32 j = 0; j < 10; j += 1) { if (j % 2 == 0) { continue; // still runs j += 1 } odd += j; } assert(odd == 25); }

break outside a looprejected: diag.break-outside-loop

fn main() { break; // ✗ diag.break-outside-loop }

Loops that only end by returning. A while (true) that no break leaves can end only by return, a fault or exit, so nothing after it ever runs: its type is never, like return's (D-0175). A function may therefore end with such a loop and needs no value after it. A break inside a nested loop leaves only that loop and does not count; a break that leaves the while (true) itself makes it an ordinary loop again, after which the function must still give its value. Only the literal true has this meaning: the compiler does not look into a condition that merely happens to stay true. There is no separate loop keyword; while (true) is the way to say "forever".

import std;

// The first line that starts with #, read until one does; no value is // needed after the loop, because nothing after it can run. fn first_comment(ref<Vec<String>, shared> lines) : usize { usize i = 0; while (true) { if (String::starts_with(&lines[i], "#")) { return i; } i += 1; } }

fn main() { Vec<String> lines = Vec::new(); Vec::push(&mut lines, String::from_str("x = 1")); Vec::push(&mut lines, String::from_str("# note")); printf("line %v\n", first_comment(&lines)); }

outputline 1

foreach

foreach (x in c) { … } visits every element of a Vec, an array, a slice, a HashSet or a HashMap, in order: by index for a Vec or an array, in insertion order for a set or a map. How the loop holds the collection is written at the loop, the same three ways a function takes a value:

Use &c most of the time. A second name gives the position: foreach (i, x in &v) has i : usize. A HashMap is always looped over with two names, the key and the value: foreach (k, v in &m), or three to have the position too, foreach (i, k, v in &m). printf prints what a reference refers to, so printf("%d", x) works with x a ref<i32, shared>; elsewhere write *x for the number.

import std;

fn shout(String s) // takes ownership { printf("%s!\n", &s); }

fn main() { Vec<String> names = Vec::new(); Vec::push(&mut names, String::from_str("ada")); Vec::push(&mut names, String::from_str("lin")); foreach (i, name in &names) // read: names is still usable afterwards { printf("%d: %s\n", i, name); }

Vec<i32> numbers = Vec::new(); Vec::push(&mut numbers, 3); Vec::push(&mut numbers, 5); foreach (n in &mut numbers) // change in place { *n *= 2; } foreach (n in &numbers) { printf("%d ", n); // printf reads through the reference } printf("\n");

HashMap<str, u32> stock = HashMap::new(); HashMap::insert(&mut stock, "pens", 12); HashMap::insert(&mut stock, "ink", 3); foreach (item, count in &stock) { printf("%-5s %2d\n", item, count); }

foreach (name in names) // consume: each String moves into shout { shout(name); } // names is gone here; using it would be diag.stale-binding }

output0: ada 1: lin 6 10 pens 12 ink 3 ada! lin!

The loop's borrow of the collection lasts the whole loop, so a body that tries to change the collection itself (push to it, clear it) is rejected before the program runs, the same aliasing rule as anywhere else. There is no iterator invalidation to debug. A consuming loop left early, by break, return or a fault, destroys the elements it had not reached, exactly once.

import std;

fn main() { Vec<i32> v = Vec::new(); Vec::push(&mut v, 1); foreach (x in &v) { Vec::push(&mut v, *x); // ✗ diag.aliasing-conflict (static): v is borrowed by the loop } }

An array of plain values, such as array<i32, 5>, is copied by the by-value form: foreach (i, e in arr) walks a copy, so arr is neither borrowed nor used up, a write to arr[j] in the body is allowed (and not seen by e), and arr is still there afterwards. To change the elements themselves, loop over &mut arr and write through the reference: *e = …. While that loop runs, arr is borrowed by it, so reach the elements through e, not through arr[j].

import std;

fn main() { array<i32, 5> arr = [0, 0, 0, 0, 0];

foreach (i, e in &mut arr) { *e = narrow<i32>(i) * 10; // write through the element's reference } arr[4] += 1; // direct indexing works too, outside the loop

foreach (i, e in &arr) { printf("[%v] = %v\n", i, *e); } }

output[0] = 0 [1] = 10 [2] = 20 [3] = 30 [4] = 41

Counting with a range

foreach (x in lo..hi) counts from lo up to, not including, hi, the same way &a[lo..hi] takes elements lo up to hi. So 0..n runs exactly n times, and 1..10 stops at 9. A second name gives the position, counted from 0: foreach (i, x in lo..hi). Both bounds are evaluated once, lo first. If hi is not above lo, the body never runs.

x has the bounds' integer type. A plain number takes the other bound's type, so with u8 top, 0..top counts in u8. Two plain numbers are usize, so the counter can index an array or a Vec directly; for a negative range, give one bound a type: -3..0: i32. Bounds of two different types are rejected rather than converted. A range can end at its type's largest value without overflowing, because the counter never passes hi.

import std;

fn main() { foreach (v in 1..10) { printf("%v ", v); } printf("\n");

auto names = ["ada", "lin", "kay"]; foreach (i in 0..3) // i : usize, so it can index { printf("%v: %v\n", i, names[i]); }

u8 top = 255; foreach (i, b in 253..top) // b : u8; i is the position { printf("step %v is %v\n", i, b); } }

output1 2 3 4 5 6 7 8 9 0: ada 1: lin 2: kay step 0 is 253 step 1 is 254

A range to count over is written only in a foreach (a slice's lo..hi, §16, is the same notation for elements). It is not a value you can store, and it has no inclusive form or step. To count down or in steps, use a for loop.

return, break, continue, and a run-time fault all leave scopes the same way: every statement scope and block between here and the target is exited in order, destroying what each one owns. A return e first moves e out so it survives the unwinding. You never have to write cleanup code on an early exit.

Many rules in this guide say a program is "rejected" in one example and "faults at run time" in another. The boundary is fixed by the language, not left to the compiler's cleverness, so that every implementation accepts and rejects the same programs. A compiler may prove more than this checker does and leave out the run-time checks it has proved unnecessary (that is what cobc does; see the performance chapter), but it may not accept a program the language rejects or reject one it accepts: coby --check is the reference, and what it accepts is the language. Within one function body, the checker tracks a few facts about each variable: whether it has been moved from, whether it has been initialised, which references were visibly borrowed from it, and whether a reference to it has been handed somewhere the checker cannot follow (a call, a field, a thread). At each checked operation the outcome is one of three:

The analysis looks only at the current function and only at literals for value ranges. So 2147483647 + 1 is refuted statically, add(2147483647, 1) is checked when add runs, and borrowing x twice exclusively in one function is refuted while doing so through two reference parameters is che

正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。

打开原文
去社区讨论 →