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 ConditionStatus: 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
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.
| 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.
| 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 |
The unconditional loop
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.
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.
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.
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.
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.
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.
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.
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
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.
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
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.
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.
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).
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.
break x stays additive.Every program that would use it is refused today, so admitting it later breaks none.
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.
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.
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.
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.
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.
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.
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
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.
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.
An if that begins a line is a statement of its own, as a [ that begins one
is a list (ADR-135 D3).
if already guards a match arm (Part I 3.4); it means only when in both
places.
A call that never comes back
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.
while or for; the unconditional one is while true, emitted as
loop, and a function that ends in one that no break leaves needs no
return after it. NK1104 still fires on a while whose condition is a
name, and on a while true a break leaves.break and continue cost no program a name: the words were reserved before
they were constructs. loop is free for a program to use.NK1132 (D16) and NK1133 (D13, D14,
D22), in Part III C.3.=> throw NotFound and ?? throw Missing
need no braces; a ?? whose fallback jumps is a match below, not a closure.return 250 if speed > 250.return; a value
out of a loop is a let or a return; a while true the walk wrongly thinks
a jump leaves still asks for the return (D8).break with a value, and with it a loop keyword and a while as an
expression (D2, D15).while true
nothing leaves is unreachable; that wants its own diagnostic, not silence.panic (D27). Nothing reads a
call graph, and a !-valued call is not in the ledger’s type language.return. It stays legal; taking
it back would break programs (ADR-298 D1’s polarity).never a program can write in a declaration.unless, or a negated guard - if !cond says it - and a guard after
anything but the four jumps (D24).