ADR-276: while true Is the Unconditional Loop; break and continue Leave the Innermost Loop and Carry Nothing; a Jump Is an Expression of the Never Type and May Carry Its Condition

Status: Accepted Date: October 3, 2026 History: the records this one replaces are archived in docs/history/adr; read them only for how the decision was reached Answers: whether the language has a third loop form (the loop { … } entry the roadmap carried); whether break, continue and loop stay available as names; whether break and continue are in the language; the while_true warning about the emitted file (#227); a loop that cannot end still has to be followed by a return (#183); => throw NotFound being a parse error (#262); whether break carries a value; and the owner’s ask for return 250 if the speed is over 250 Related: ADR-298 D1 (a keyword is a reserved word; reserving costs nothing before programs exist), ADR-292 D7 (un-reserving is the free direction), ADR-298 D6 (loop is a name, and its message is the undeclared-name help), ADR-296 D17 (name for name), ADR-292 D1 (a handler that diverts is not overlapped), ADR-281 (a lock door’s block is a lambda), ADR-025 D2 (a failure leaves without a sigil), ADR-301 (what a loop’s head may hold), ADR-308 D3 (the safe direction of an over-approximating walk), ADR-135 D3 (a token that begins a line begins something new), ADR-279 (a borrowed left side of ?? beside a jump), ADR-269 D9 (the prover reads a jump’s guard), Part I 2.1, 3.2, 3.3, 3.4 and 7.1, Part III C.1 and C.3

1. Question

How is a loop left, and what is a jump? Three loop forms or two; whether break and continue exist, which loop they leave and whether they carry a value; where a jump may not go; what type a jump has where it stands as an expression; and whether its condition may follow it.

Each answer leans on another: a loop keyword is worth a word only if break carries a value; whether while true can end a function that declares a type depends on whether a break can leave it and on what the language below makes of it; and a jump as an expression is safe only because a value after a break is refused. So they are one record.

2. How others do it

Language Its answer What it costs there Source
Go no while at all: for { … }, for cond { … } and for … range are one word in three forms nothing: one word carries every loop shape not re-checked
Rust loop { … } is an expression; break value hands its value back; a loop with no break diverges and is !; labels name an outer loop; while true draws the while_true lint a second loop keyword and a second naming scheme for labels the reference and its lint
Swift guard cond else { return } a negated condition and a new word not re-checked

What transfers is Go’s: one keyword per loop shape is not needed. What does not is Rust’s case for loop, because it rests on break carrying a value (D2, D13). Rust’s types do transfer below the language: while true { } is () there and loop { } is !, which decides D3-D5.

3. Options, weighed

Option What is written Why it lost Decided in
a loop keyword loop { … } a second spelling for a loop the language already has; its only argument is a break with a value, which D13 refuses D1, D2
no break or continue a flag, or return a for cannot be stopped: the flag stops the body while the loop turns on - 2,000,000 turns for 1,415 turns’ work, unbounded in n D11
labelled jumps break 'outer a second naming scheme, invisible to every analysis that asks where a value goes; reopened only by a measurement on a real program D12
break x while true { … break x } a loop that is sometimes a value is the ambiguity if was spared; additive later D13, D15
an #![allow(while_true)] in the preamble - a blanket over every file, hiding a warning that was right D4
a jump stays a statement => { throw NotFound } braces that hold nothing together and produce nothing D20
guard … else guard speed <= 250 else { return 250 } negates the condition and adds a word D23, D26
a trailing condition on any statement x = 1 if c a reader would have to read every line to its end to learn whether it runs D24

4. Decision

The unconditional loop

D1 — while true { … } is the unconditional loop, and there is no second spelling for it.

There is no loop keyword. One fewer keyword puts the whole of control flow on one page; a keyword is the most expensive thing a language adds and the hardest it removes.

D2 — A loop keyword is reopened only by a break that carries a value.

Rust’s loop earns its word because break value makes it an expression. Here break carries nothing (D13), so the case is empty. Should that change, the question comes back, and while true { … } whose break hands back a value would read as a lie: the head says the loop does not end and the body says what it ends with.

D3 — A while whose condition is the literal true is emitted as loop { … }.

This is name for name, not an exception to it: D1 says what the construct is, and Rust’s name for the unconditional loop is loop. The literal only - never a name that happens to be true; while running { … } stays a while.

D4 — The emitted preamble gets no allow; where the language below complains about emitted code, the first question is whether it is right.

A suppression in the preamble says nobody will be shown this again, about every file, including cases nobody has looked at. An allow is the answer only once the warning has been asked about and found wrong, and then it stands next to the emitted construct.

D5 — The two forms differ in type below, so D3 is a prerequisite and not a nicety.

while true { } is () in Rust and loop { } is !. A function that never returns can be built only out of the second, so no checker rule (D6) could have stood while the emitter wrote the first.

D6 — A body whose last statement is a loop nothing leaves does not answer for its type.

fn forever() -> i32 { while true { … } } is not NK1104. The tail check is skipped rather than satisfied: there is no value, and inventing one would claim something about a program that has none. An accept loop, an event loop or a supervisor needs no unreachable return. That return stays legal.

D7 — The test is the literal true, and no break bound to that loop.

The same literal D3 turns into loop, so the checker claims divergence in exactly the cases the emitter delivers it.

D8 — The walk over-approximates a jump leaves this loop, on purpose.

It descends into a lambda, where a break is not bound to this loop. A false yes costs the unreachable return every program already had; a false no would hand rustc a body that falls off its end. So it leans to yes.

The words

D9 — break and continue are reserved words; loop is a name.

A program may not name anything break or continue. loop is reserved for no construct (D1), so it is an ordinary name, and a loop { … } meets the undeclared-name help, which says write while true. Reserving narrows what parses and un-reserving widens it, so a word is reserved for a construct the language may want and for nothing else.

D10 — The reserved words are three alternations in the grammar, not one widened.

The alternation the grammar backend generates is a tuple with a maximum width; the jump words are a third part (RESERVED_C) rather than an addition to the end of the second, which is at that width.

The jumps

D11 — break leaves the innermost loop around it; continue starts that loop’s next turn.

Without them a for cannot be stopped: a flag stops the body while the loop turns on - 2,000,000 turns against 1,415 with a break, unbounded in n.

D12 — There is no label.

A label is a second naming scheme, scoped to a construct and invisible to every analysis that asks where a value goes: the six analyses that walk statements each needed an arm for the two jumps, and every arm does nothing. Leaving an outer loop is a flag or a return. Reopened by a measurement - the shape, written out against the labelled form, on a program someone has.

D13 — A loop is a statement and hands back nothing, so neither jump carries a value, and break x is refused rather than read as two statements.

Without the refusal, break i parses as break and then a statement i: the program compiles and the value is dropped. NK1133 says what is true - nothing after a break or continue in the same block is reached. A construct that is sometimes a value and sometimes not is the ambiguity if was spared by being an expression always (Part I 3.1).

D14 — NK1133’s help names the two shapes that carry a value out of a loop.

A let before the loop that the loop assigns, or a return where the function has nothing left to do - the second is what a search loop is usually written as.

D15 — Admitting break x stays additive.

Every program that would use it is refused today, so admitting it later breaks none.

D16 — A jump does not leave a function; a break or continue whose loop is outside a lambda, a task, an overlap branch or a DSL fold’s init, step or merge is NK1132.

Changed by ADR-async-5 D10: a task an async form starts is such a boundary; overlap is removed.

Each of the four is a separate function below (a closure, an async move block, an async block per branch, a closure handed to the fold driver), and a branch cannot go to a label in another function. The message names which of the four stands in the way; the help says decide inside, act outside. The same rule is why a ?? whose fallback jumps is lowered to a match and not to unwrap_or_else, which takes a closure.

D17 — A catch handler is not one of them.

A handler lowers to a match arm, so fs::read_to_string(p) catch { break } leaves the loop around it. A handler that jumps diverts, as one that returns does, so the ordering analysis does not overlap the statement after it (ADR-292) - except where the jump is bound to a loop written inside the handler.

D18 — The refusal is in the checker and in the emitter, and the emitter’s is the guarantee.

Flow::in_loop is true in a loop’s body and false where a lambda, a task, an overlap branch or a function body begins; every statement is emitted through one place, so a jump with no loop cannot be written whatever a walk reached.

D19 — The jump rules stand last in the statement alternation.

A PEG alternation is an ordered list. Next to return_stmt, every statement that is not a jump paid for two failed matches (+1.28 % compile time); last, only a block’s closing } reaches them (+0.13 % to +0.32 %). Both words are reserved, so no earlier arm can take one.

D20 — throw, return, break and continue are expressions, and their type is never.

They may stand wherever an expression may: find(id) ?? throw NotFound(id), else => throw OutOfRange(score). Never fits every expected type without widening anything, so an arm that jumps sits beside an arm that hands back a &str and the match is a &str.

D21 — What a jump does is unchanged, and the statement forms remain.

A throw leaves the function and makes it throws; a return returns; a break and a continue need a loop and do not cross a function boundary (D16). A bare jump in statement position is the statement, as it was, so every analysis that asks about a jump asks about the statement.

D22 — NK1133 is unchanged, and it is what makes D20 safe.

A value written after a break is still refused, one position over, so D20 does not turn break x into a dropped value.

A jump’s condition

D23 — return, throw, break and continue may carry their condition after them.

return 250 if speed > 250, throw TooFast(speed) if speed > 250, continue if n % 2 != 0, break if done. Each is if cond { jump } and the tree the parser builds is that if, so every rule of if holds, and a statement after break if done is reachable - NK1133 does not apply.

D24 — Only after a jump.

After a jump the line says where control goes and the condition says when; nothing else is guarded this way. return if c { a } else { b } is still a return of an if expression: where a block follows the condition, the if is the value. return if done is the bare return, guarded.

D25 — The condition stands on the jump’s line.

An if that begins a line is a statement of its own, as a [ that begins one is a list (ADR-135 D3).

D26 — No new word.

if already guards a match arm (Part I 3.4); it means only when in both places.

A call that never comes back

D27 — panic(…) has the type never, as a jump has.

m[k] ?? panic(f"{k} was a key a moment ago") is what the map holds, and an arm else => panic("…") is not one of the answers that have to agree. It is the prelude’s panic: a function or a binding the program names panic is an ordinary call. A panic leaves the function as a throw does, so what a branch took before it is not read after it. The language below has the same rule (panic! is ! in Rust).

Rejected: a never in the ledger’s type language for every function that does not return. Only panic is in the prelude, and the rest of §6’s case (a call that aborts, infinite recursion) needs a call graph nothing reads.

5. Consequences

6. What this does not decide

This site is open source. Improve this page.