RFC 016: loop and break <value> (Loop Expressions)¶
Status: Planned Created: 2025-12-24
Summary¶
Add a loop: keyword for explicit infinite loops, and extend break to optionally carry a value: break <expr>.
This enables treating loop: as an expression that can produce a value (similar to Rust’s loop { ... }), while
keeping while as the general conditional loop construct.
Motivation¶
Today, users express infinite loops as while True:. The compiler may emit Rust loop {} for this pattern, but the
source language has no explicit infinite-loop construct and cannot express “break with a value”.
Adding loop: and break <value> provides:
- Clearer intent in source (
loop:reads as “infinite loop”). - A foundation for expression-oriented control flow without “initialize then mutate” patterns.
- A natural home for “search until found” patterns that return a value.
Goals¶
- Introduce
loop:as an explicit infinite loop construct. - Allow
breakto optionally carry a value:break <expr>. - Allow
loop:to be used in expression position (e.g., assignment RHS). - Keep existing
break(no value) valid and well-defined. - Keep
while True:valid (and optionally desugarloop:towhile True:or vice-versa).
Non-goals¶
- Labeled
break/continuesyntax changes (may be addressed in a follow-up RFC). breakwith multiple values / tuple sugar (users can return tuples explicitly).- Making
whilean expression (this RFC keeps “value-yielding loops” scoped toloop:).
Proposed syntax¶
loop:¶
loop:
# body
...
break (with optional value)¶
break
break some_expr
Semantics¶
Loop execution¶
loop:executes its body repeatedly until it is exited viabreak(or an error/abort).continueskips to the next iteration (existing behavior).
break values¶
breakexits the innermostloop:. If it includes a value, that value becomes the value produced by theloop:expression.breakwithout a value is equivalent tobreak ()(i.e., producesUnit).
Expression result type¶
loop: is an expression with a single result type:
- If every reachable
breakin the loop isbreak(no value), the loop’s type isUnit. - If one or more
breakstatements include values, the loop’s type is the least upper bound (LUB) / unification result of allbreakvalue types.- If the compiler cannot unify the break value types, it is a type error.
- If a
loop:has no reachablebreak, it is considered non-terminating.- Initial implementation may treat this as a type error unless we also introduce a
Never/!type.
- Initial implementation may treat this as a type error unless we also introduce a
Interaction with generators (yield)¶
break and yield are different control-flow concepts:
breakexits a loop.yieldproduces one element of anIterator[T]and suspends execution (RFC 006).
Inside a generator function, loop: behaves like any other loop construct:
yield exprproduces a value and suspends the generator.breakexits the loop; the generator may continue after the loop, or terminate if nothing follows.
Open design question:
- If
loop:is allowed as an expression inside generator bodies,break <value>would produce a value for theloop:expression, but this value is distinct from generator output (which is produced only byyield). We can either allow this (it is orthogonal), or restrict “loop-as-expression” in generators initially for simplicity.
Interaction with async/await¶
await is an async suspension point: it pauses an async def until a Future is ready.
It does not replace loops.
You typically combine them:
async def wait_until_done() -> None:
loop:
if await done():
break
If you need polling/backoff, await controls waiting between iterations:
async def wait_with_backoff() -> None:
loop:
if done():
break
await sleep(0.01)
return
Examples¶
Example 1: Compute a value without external mutation¶
answer = loop:
if some_condition():
break 42
Equivalent with while True: (today):
mut answer = 0
while True:
# Without `break <value>`, you typically compute a result via external mutation.
if some_condition():
answer = 42
break
Example 2: Search until found¶
found = loop:
item = next_item()
if item.is_ok():
break item
Equivalent with while True: (today):
mut found = None
while True:
item = next_item()
if item.is_ok():
found = item
break
Alternative with a conditional while (works when you can express the loop as “repeat until condition”):
item = next_item()
while not item.is_ok():
item = next_item()
# Here: item.is_ok() == true
found = item
Why keep loop: / break <value> anyway?
loop:supports multiple exit points naturally (success, timeout, error), without extra state variables.break <value>makes the loop expression-oriented, so you can writefound = loop: ... break valuedirectly.- A conditional
whileoften forces pre-initialization (or ado-whileconstruct) to compute the first value.
Example 3: break without value¶
loop:
if done():
break
Lowering / codegen strategy (Rust backend)¶
Desugaring options¶
The compiler may implement loop: in one of two ways:
- AST-level sugar: desugar
loop:towhile True:early, and keep codegen optimizations. - Dedicated IR node: lower
loop:directly to an IRLoopstatement/expression, and emit Rustloop {}.
If break <value> is introduced, a dedicated IR representation is recommended, because Rust requires:
loop { ... break expr; }for value-yielding loops, and- the
loopconstruct (notwhile) to yield a value.
IR changes¶
If we treat loop: as an expression, IR likely needs:
- An expression form (e.g.,
IrExprKind::Loop { body: Vec<IrStmt>, result_ty: IrType }), or - A block-expression convention where a
Loopstatement plusbreak valuecomposes into an expression value.
Additionally, IrStmtKind::Break should be extended to carry an optional value expression
(and still optionally support labels in the future).
Backwards compatibility¶
- Existing programs remain valid.
while True:remains valid and may continue to codegen to Rustloop {}.breakwithout value remains valid.
Alternatives considered¶
Implementation notes (current crate layout)¶
This RFC introduces new syntax and vocabulary:
- A new keyword:
loop - An extended form of
break(break <value>)
In the current workspace, those changes should be implemented in:
crates/incan_core/src/lang/keywords.rs: addloopto the keyword registry (with correct RFC provenance)crates/incan_syntax: lexer emitsTokenKind::Keyword(KeywordId::Loop)and parser handlesloop:andbreak <expr>crates/incan_coredocgen/tests: update reference docs + add parity/guardrail tests so registry ↔ lexer stay aligned
Alternative A: Only keep while True:¶
Pros:
- No new keyword.
Cons:
- Harder to justify
break <value>onwhile. - Less clear intent in source.
Alternative B: Make while an expression too¶
Pros:
- Fewer constructs.
Cons:
- More semantic surface and surprises (“
whileyields a value?”). - Less aligned with Rust codegen constraints.
Open questions¶
- Do we want a
Never/!type for non-terminatingloop:expressions? - Do we want labeled loops (and labeled
break/continue) in the same RFC or separately? - Should
loop:be allowed in statement position only initially, with expression usage added later?
Possible future syntax sugar: loop ... until ...¶
We may add a compact, statement-level sugar for “repeat an action until a condition holds”:
loop item.next() until item.is_ok()
Desugaring (action first, then test, then break):
loop:
item.next()
if item.is_ok():
break
Notes:
until <expr>must typecheck tobool.- This form is intended as a statement (it does not yield a value by itself).