#Control Flow
Status: language specification draft.
This chapter defines suites, conditionals, loops, match, return, defer,
and runtime panics.
- flow.suiteshd-lang control-flow constructs use indentation-delimited suites and may produce values.
- flow.condition.boolConditions are always
bool: there is no truthiness conversion from numbers, strings, collections, or optional values.
fn sign(n: i32) -> i32:
if n > 0: +1
else if n < 0: -1
else: +0#Blocks And Completion
A suite evaluates its statements in order, and its final expression may give it a value.
#Suite Values
- flow.block.orderA suite evaluates statements in source order.
- flow.block.valueIf control reaches the final expression statement normally, that expression is the suite's value.
- flow.block.voidA suite whose final statement is not a value expression has type
void. - flow.block.abrupt
return,break,continue, optional or result propagation with?, and a runtime panic complete the current control path abruptly. They do not produce the suite's ordinary final value. - flow.block.non-emptyAn indented suite must contain at least one statement.
- flow.block.pass
passsupplies an explicit no-op expression when a body is intentionally empty.
fn demo(flag: bool) -> i32:
x := if flag:
+1
else:
+2
x#Must-Use Values
- flow.must-use.discardAn expression whose value is discarded and whose type is
Result[T, E],T?, ormut Suspend[T]is a compile-time error. Error:discarded-must-use-value. - flow.must-use.statementThis includes a non-final expression statement.
- flow.must-use.discarded-suiteIt also includes the final expression of any suite whose value is discarded:
- a loop body;
- an
ifwithoutelse; - a statement-position
matcharm; - a
defersuite; - a module's top-level script.
- flow.must-use.handledSuch a value must be propagated with
?, inspected bymatch, returned, stored for later use, or explicitly discarded with_ := expressionorlet _ = expression. - flow.must-use.underscoreThe
_spelling does not bind a local name.
fn save() -> Result[i32, SaveError]:
.Err(SaveError { message: "failed" })
fn save_all(values: List[i32]) -> void:
for value in values:
save() # error: discarded-must-use-value
_ := save() # valid: an explicit discard
let _ = save() # valid: the same discardWhy. The
_spelling makes the discard visible in review.
#Unused Bindings
- flow.unused.lintAn ordinary local binding that is never read produces a lint finding. Warning:
unused-local-binding. - flow.unused.underscoreNames beginning with
_suppress that warning. - flow.unused.must-useThe exception is an unread binding of a suspension, which is an error. A suspension counts however it is bound, with
:=,let, orlet mut. Error:discarded-must-use-value. An unreadResultorT?binding is the ordinaryunused-local-bindingwarning, because binding a value to a name counts as using it. - flow.unused.discard-formsThe two discard forms,
_ := expressionandlet _ = expression, each explicitly discard a must-use value without binding it. No other form discards one.
fn demo() -> i32:
_unused := +1 # names that begin with _ need no read
+2#Conditional Expressions
if, else if, and else select exactly one suite:
label := if score >= 90:
"excellent"
else if score >= 70:
"passing"
else:
"needs work"- flow.if.select
if,else if, andelseselect exactly one suite. - flow.if.orderConditions are evaluated from top to bottom until one is
true. - flow.if.lazyLater conditions and unselected suites are not evaluated.
- flow.if.else-if
else ifis one conditional-chain form. It is not parsed as anelsesuite containing an unrelated nestedif.
#Conditional Values
- flow.if.value.elseWhen an
ifis used where a value is required, it must have anelse. - flow.if.value.branchesEvery reachable branch must then produce a value, and those values must have one compatible result type.
- flow.if.value.least-commonWithout an expected type, that type is the least common type of the branch values.
- flow.if.statementIn statement position,
elsemay be omitted, and the conditional then has typevoid.
fn fee(rush: bool) -> i32:
base := if rush: +10 else: +5
base * 2#Same-Line Conditionals
- flow.if.same-lineA same-line
ifcannot be written directly inside another same-line suite. - flow.if.same-line.error
if a: if b: 1 else: 2 else: 3andfn sign(x: i32) -> i32: if x < 0: -1 else: 1are errors. Error:syntax-error.
fn pick(a: bool, b: bool) -> i32:
v := if a: if b: 1 else: 2 else: 3 # error: syntax-error
v
fn sign(x: i32) -> i32: if x < 0: -1 else: 1 # error: syntax-errorWhy. Each
elseon a line then has one owner.
Note. Nest the inner conditional in parentheses, as in
if a: (if b: 1 else: 2) else: 3, or give the outer suite an indented body.
See also: Statements.
#For Loops
A for loop obtains an iterator from its iterable expression and repeatedly
advances that iterator:
for value in values:
println(value)- flow.for.iterable-onceThe iterable expression is evaluated exactly once.
- flow.for.fresh-iteratorA new iterator is obtained for each execution of the loop.
- flow.for.bindingThe binding pattern receives each yielded value before the body executes.
- flow.for.patternThe pattern of a
forloop or a comprehensionforclause matches each yielded value of typeT. It matches as aletpattern matches an initializer of typeT, under Let Patterns. - flow.for.pattern.irrefutableThe pattern must be irrefutable for
T. A refutable pattern is an error, and a loop'selsedoes not handle it. Error:refutable-let-pattern. - flow.for.binding.freshEach iteration makes a new binding of every name in the pattern. A closure made in the body captures that iteration's binding, and the closure's writes to that name reach that iteration only.
struct Point:
x: i32
y: i32
fn total(points: List[Point], scores: Map[string, i32]) -> i32:
let sum = +0
for Point { x, y } in points:
sum = sum + x + y
for (_, score) in scores:
sum = sum + score
sumfn bad(values: List[i32]) -> void:
for (left, right) in values: # error: type-mismatch
pass
fn present(found: List[i32?]) -> void:
for .Some(value) in found: # error: refutable-let-pattern
passNote. A map yields
(K, V)tuples, sofor (key, value) in entriesis a tuple pattern, andfor entry in entriesbinds the whole tuple.
#Iteration Protocols
A for loop uses the prelude traits Iterable[T] and Iterator[T]:
trait Iterator[T]:
fn next(&mut self) -> T?
trait Iterable[T]:
type IntoIter < Iterator[T]
fn iter(self) -> Self::IntoIter- flow.for.iterator.trait
Iterator[T]is a prelude trait declaringfn next(&mut self) -> T?. - flow.for.iterator.user-implA user-defined type may implement
Iterator[T]with its own traversal state. - flow.for.collection-cursor.private
std.iter.FromFn[T]is a concrete cursor with one privatestep: fn() -> T?field. - flow.for.iterator.next.traitCalling
nextadvances the cursor through its implementation ofIterator[T]. - flow.for.iterator.value-stateIterator values follow ordinary value semantics; copying a mutable reference shares the referenced cursor and its progress.
- flow.for.iter-independentAn ordinary iterable creates an independent iterator on every
iter()call. - flow.for.iterator-exhaustedThe iterator stores traversal progress. Once
nexthas returned.None, the iterator is exhausted. - flow.for.iterator-after-noneWhat
nextreturns when it is called again on an exhausted iterator is unspecified. - flow.for.iterable-collections
List[T]andMap[K, V]implementIterable: a list yieldsT, and a map(K, V). - flow.for.iterator.iterable-not-impliedImplementing
Iterator[T]does not implyIterable[T]. - flow.for.string-not-iterable
stringdoes not implementIterable, so a loop over a string is an error. Error:unsatisfied-trait-bound. - flow.for.string-explicitA loop over a string's contents iterates
chars(),char_indices(), orbytes(), which String Methods defines. - flow.for.accepts-owned-cursor
foraccepts anIterable[T], a value implementingIterator[T], or a mutable reference to such a cursor. - flow.for.iterable.cursorWhen the iterable expression's type implements
Iterable[T]and is not aListor aMap, the loop callsiteronce and advances the resulting cursor. A list or a map is iterated as Snapshot Iteration states. - flow.for.iterable.into-iter
Iterable[T]declares the associated cursor typeIntoIter, bounded byIterator[T], anditerreturns a value of it. Each implementation names its cursor type, as intype IntoIter = ListIter[T]. - flow.for.iterable.static-cursorThe loop holds the
IntoItervalueiterreturned as its own writable copy, as it holds an iterator source, and calls itsnextthrough that type'sIterator[T]implementation, with no trait value. - flow.for.iterator.owned-sourceWhen the source is an iterator, the loop calls
nexton its owned binding without callingiter. - flow.for.iterator.owned-positionIteration starts at the copied cursor's current position and exhausts that copy unless the loop exits early.
- flow.for.iterator.shared-referenceA loop cannot advance a cursor through a shared reference. Error:
not-writable. - flow.for.iterator.owned-bindingA loop copies its cursor value into a hidden writable binding, regardless of whether the source expression is a writable place.
- flow.for.protocol-requiredA source implementing neither
Iterable[T]norIterator[T]is an error. Error:unsatisfied-trait-bound. - flow.for.comprehensionComprehension
forclauses accept the same values by the same rules. - flow.for.iterator.iterable-boundAn iterator without an
Iterable[T]implementation does not satisfy anIterable[T]bound. Error:unsatisfied-trait-bound. - flow.for.iterator.evaluate-onceA loop evaluates its source expression once before iteration starts.
- flow.for.iterator.copy-preserves-sourceAdvancing the loop's owned cursor value does not change the original value.
- flow.for.iterator.reference-progressIterating a mutable cursor reference advances the referenced cursor's progress.
Why. The cursor type is the implementation's own, as Rust's
IntoIterator::IntoIteris. So a loop or an adapter chain over a list or a range advances a concrete cursor and calls itsnextdirectly, never through a trait value.
Why.
Iterable[T]promises a fresh, independent traversal on everyiter()call. A function bounded by it may loop over its argument twice, or zip it with itself, and see every element each time. An iterator is a single cursor and cannot keep that promise. If it implementedIterable, a second pass would silently see only what the first pass left, or nothing, as with Python generators. So iterators stay outsideIterable.
forstill consumes an iterator directly, byflow.for.iterator.owned-source: the loop owns the copied cursor. To pass an iterator where anIterableis expected, collect it first (let rest: List[T] = it.collect()) or make it the receiver (it.zip(other)).
struct Countdown:
left: i32
impl Iterator[i32] for Countdown:
fn next(&mut self) -> i32?:
if self.left <= 0:
return .None
self.left -= 1
.Some(self.left + 1)
fn countdown(start: i32) -> Countdown:
Countdown { left: start }fn drain(source: &mut dyn Iterator[i32]) -> i32:
let total = +0
for value in source:
total = total + value
total
fn twice(values: List[i32]) -> i32:
let total = +0
for value in values.iter():
total = total + value
for value in values.iter():
total = total + value
totaluse std.iter.FromFn
struct Countdown:
left: i32
impl Iterator[i32] for Countdown:
fn next(&mut self) -> i32?: .None
fn main(source: Countdown) -> List[i32]:
[for value in source => value]
fn bad() -> void:
for value in 1: # error: unsatisfied-trait-bound
pass
fn letters(text: string) -> void:
for letter in text: # error: unsatisfied-trait-bound
pass
fn peek(source: FromFn[i32]) -> fn() -> i32?:
source.step # error: private-member
fn count[I < Iterable[i32]](source: I) -> i32:
let total = +0
for item in source:
total = total + item
total
fn from_iterator(source: Countdown) -> i32:
count(source) # error: unsatisfied-trait-boundOpen in playgroundNote. An iterator is single-pass, and there is no
cloneorteeof an iterator. To traverse the items twice, calliter()on the collection again, astwicedoes, or collect the iterator into a list first (What Collect Builds).
Why.
nextchanges its cursor through&mut self. A direct loop therefore requires a writable cursor place or a mutable reference. Named adapters store their source cursor as an ordinary value, andby_refsupplies a value cursor that borrows an existing cursor.
See also: Iterators, the stdlib-tier chapter that specifies
the iterator adapters, collect, and FromIterator.
#Built-In Collection Iteration
- flow.for.listThe built-in
List[T]iterable yields each element asT, whether the list is reached directly or through a reference. - flow.for.map.entriesThe built-in
Map[K, V]iterable yields(K, V)tuples in the map's iteration order. - flow.for.map.valueDestructuring each entry yields the value as
V, whether the map is reached directly or through a reference. - flow.for.no-root-grantA loop binding is not writable, even when the list is in a writable place or behind a
&mutreference. Changing a field of an element it binds is an error. Writing through an element of a*mut Utype is valid. Error:not-writable. - flow.for.libraryLibraries may provide additional mutable-iteration APIs with separate aliasing rules.
- flow.for.collection.referenceOutside Element Reference Loops, a
forover a reference to aListorMapiterates the referenced value.
fn total(items: List[i32]) -> i32:
let sum = +0
for item in items:
sum = sum + item
sum
fn map_total(items: &Map[string, i32]) -> i32:
let sum = +0
for (_, item) in items:
sum = sum + item
sum#Snapshot Iteration
A for loop over a list or a map iterates the collection as it was when
the loop began.
fn doubled(values: List[i32]) -> List[i32]:
let mut out = values
for value in out:
out.push(value) # the loop still ends after the old elements
out- flow.for.snapshotA
forloop over aListorMapvalue iterates the collection as it was when the loop began. - flow.for.snapshot.changeA change to the collection while the loop runs, in its body or through a call, changes the collection but not the loop. The loop neither visits an added element nor panics.
- flow.for.snapshot.costBeginning the loop copies no element. A change during the loop copies the collection's elements once, as a change to any copy does (
types.coll.copy-on-write).
Why. A loop over a value sees one value. A body that changes the list it walks gets the result it wrote, and no rule asks the author to know whether an iterator is live.
#Element Reference Loops
for p in &mut xs and for p in &xs bind a reference to each element of
a list in turn.
fn raised(prices: List[i32]) -> List[i32]:
let mut out = prices
for price in &mut out:
*price += 1
out- flow.for.ref
for p in &mut xs, wherexsis a list, binds the pattern to a&mut Telement reference to each slot ofxsin order, andfor p in &xsto a&Tone. - flow.for.ref.slots
xsis evaluated once. The loop visits the slots from 0 up to the list's length when the loop began. - flow.for.ref.accessThe loop is one access to
xs, as an element reference's call is (ref.elem.access). It begins when the loop begins and ends when the loop ends or is left. - flow.for.ref.conflictWhen
xsis unaliased, the body may readxsand write its elements, but an expression in it that may change the length ofxs, as the table underref.elem.conflictlists, is an error. Error:conflicting-access. - flow.for.ref.pinnedThe loop pins
xswhile it runs, as an access does, so any other change of the list's length, through a reference or a call, panics. Panic:collection-pinned. - flow.for.ref.bindingThe loop binding refers to its slot for one step. The body may read and write through it and pass it as an argument for a reference parameter (
ref.elem.callee-copy.declared). Binding it to another name, storing it, capturing it in a closure, or returning it is an error. Error:element-reference-outlives-call. - flow.for.ref.writable
for p in &mut xsneedsxsin a writable place, as&mut xs[i]does. Error:not-writable. - flow.for.ref.mapA loop over element references takes a list, so a loop over
&mor&mut mwith a mapmis an error. Error:map-value-not-addressable. - flow.for.ref.through
xsmay be a reference to a list:for p in &mut randfor p in &rloop over the slots of the listrrefers to, as if*rwere the place. Through&T,for p in &mut ris an error. Error:not-writable.
fn invalid(prices: Map[string, i32]) -> void:
fixed := [+1, 2]
for x in &mut fixed: # error: not-writable
*x = 0
for price in &prices: # error: map-value-not-addressable
pass
let mut scores = [+1, 2]
for score in &mut scores:
*score += scores[0] # reads and element writes are allowed
scores.push(*score) # error: conflicting-access
let mut kept: List[*i32] = []
for score in &scores:
kept.push(score) # error: element-reference-outlives-callWhy. A loop that changes each element in place needs a place per step. Keeping the length still keeps the body from growing the list under the loop, so the loop never skips or repeats a slot.
#Range Iteration
A for loop iterates the integers of a range:
fn total(n: i32) -> i32:
let sum = 0
for i in 0..n:
sum = sum + i
for i in 1..=3:
sum = sum + i
sum
fn first_square_over(limit: i32) -> i32:
let found = 0
for i in 1..:
if i * i > limit:
found = i
break
found- flow.for.range.iterable-twoFor each integer type
T,Range[T]andRangeFrom[T]implementIterable[T]. - flow.for.range.cursorThe cursor type of
Range[T]isstd.ops.RangeIter[T], and that ofRangeFrom[T]isstd.ops.RangeFromIter[T]. - flow.for.range.half-openIterating
a..byieldsa,a + 1, and so on, up to but not includingb. It yields nothing whena >= b. - flow.for.range.inclusiveIterating
a..=byieldsathroughb, and yieldsbeven when it is the type's largest value. It yields nothing whena > b. - flow.for.range.fromIterating
a..yieldsa,a + 1, and so on, with no end, sofor i in 0..:runs until the loop exits another way. - flow.for.range.from-overflowAn iterator over
a..has no end. Asking it for the item after the type's largest value is a checked runtime panic in a debug or test build, asa + 1is. It wraps in a release build, again asa + 1does. Panic:integer-overflow. - flow.for.range.freshEach
iter()call on a range starts from its start bound, and iterating never changes the range value. - flow.for.range.to
RangeTo[T]does not implementIterable, since it has no start, so a loop over..bis an error. Error:unsatisfied-trait-bound. - flow.for.range.to-through
..=bis aRangeTo[T]too, so a loop over it is an error. Error:unsatisfied-trait-bound. - flow.for.range.full
RangeFulldoes not implementIterableeither, so a loop over..is an error. Error:unsatisfied-trait-bound. - flow.for.range.floatA range of a floating-point type does not implement
Iterable, so a loop over0.0..1.0is an error. Error:unsatisfied-trait-bound.
fn invalid(n: i32) -> void:
for i in ..n: # error: unsatisfied-trait-bound
pass
for i in ..: # error: unsatisfied-trait-bound
pass
for x in 0.0..1.0: # error: unsatisfied-trait-bound
passNote.
stdwrites one implementation overstd.num.Integer, so a range of literals whose type is still open, as in(1..4).iter(), already has its cursor type.
#Iterator Snapshots
An iterator that iter() returns over a list or a map iterates a snapshot,
as a for loop over one does (Snapshot Iteration).
- flow.for.cursor.snapshotAn iterator that
iter()returns over aListor aMapyields the elements or entries the collection held wheniter()ran. A later push, removal, or replacement changes the collection but not the iterator, and does not make it panic. - flow.for.user-definedUser-defined iterables must document equivalent mutation behavior in their own contract.
- flow.for.collection-cursor.copyA copy of a list or map cursor has its own position and iterates the same snapshot.
fn demo() -> void:
let mut items: List[i32] = [+1, +2]
let mut cursor = items.iter()
_ := cursor.next()
items.push(+3)
_ := cursor.next() # .Some(+2): the list as iter() found it
_ := cursor.next() # .None: the pushed element is not in the snapshotWhy. A cursor is a value made from a value. A change to the list after
iter()changes the list, as a change to any copy of it does (types.coll.copy-on-write), and no rule asks the author to know whether a cursor is live.
#While Loops
A while loop repeats its body under a bool condition:
while index < names.len():
index = index + 1- flow.while.conditionA
whileloop evaluates itsboolcondition before every iteration. - flow.while.initially-falseIf the condition is initially
false, the body does not execute.
#Infinite Loops
A while true loop runs until its body leaves it, so a function may return
from inside it with nothing after it:
fn try_connect(attempt: i32) -> bool:
attempt >= 3
fn connect() -> i32:
let attempt = +1
while true:
if try_connect(attempt):
return attempt
attempt = attempt + 1- flow.while.infiniteA
whileloop whose condition is the literaltrue, alone or in grouping parentheses, is an infinite loop. - flow.while.infinite.literalNo other condition makes a loop infinite: not a named constant, not
!false, and not a variable that holdstrue. - flow.while.infinite.exitAn infinite loop completes normally only through a
breakthat targets it. - flow.while.infinite.neverAn infinite loop that no
breaktargets has typenever, with or withoutelse. - flow.while.infinite.elseThe
elsesuite of an infinite loop is never evaluated. It is still type-checked, and it still joins the loop result type, asflow.loop.else.typestates.
Why. hd has no
loopkeyword;while truetakes its place, with the rule Java and Go use for reachability. Only the literal counts, so the loop head alone shows that the loop is infinite. A constant would make reachability depend on a value defined elsewhere.
Note. A
breakexits the nearest enclosing loop, byflow.break. So abreakinside a nested loop targets that nested loop, and leaves the outer infinite loop running.
See also: Unreachable Code,
Bodies And Control Paths,
The never Type.
#Break, Continue, And Loop Else
break and continue leave the current loop body, and a loop with else
produces a value.
#Break And Continue
- flow.continue
continueskips the remainder of the current loop body and begins the next iteration. - flow.break
breakexits the nearest enclosing loop. - flow.break.boundaryNeither operation targets a loop across a function or closure boundary.
- flow.break.outside-loop
breakandcontinueoutside a loop are compile-time errors, including at module top level and directly in a test body. Error:break-outside-loop.
#Loop Values
A for or while loop with else is value-producing:
found := for value in values:
if value > 100:
break value
else:
.None- flow.loop.void.typeA loop without
elsehas typevoid, unless it is an infinite loop that nobreaktargets, which has typeneverbyflow.while.infinite.never. - flow.loop.void.breakA loop without
elsepermits plainbreakbut notbreak value. Abreak valuethere is an error. Error:break-value-context. - flow.loop.else.valueA
fororwhileloop withelseis value-producing. - flow.loop.else.break-value
break valueterminates the loop and supplies its value. - flow.loop.else.exhaustionNormal exhaustion of a
forloop, or afalsewhilecondition, evaluates theelsesuite and uses that suite's value. - flow.loop.else.plain-breakPlain
breakin a value-producing loop is an error. Error:break-value-context. - flow.loop.else.typeEvery reachable
break valueand theelsesuite join by the least common type, which is the loop result type. - flow.loop.else.continue
continuedoes not produce a loop value. - flow.loop.else.skippedThe
elsesuite is not executed after anybreak, abrupt function return, propagation with?, cancellation, or runtime panic.
fn invalid(values: List[i32]) -> void:
for value in values:
break value # error: break-value-context
fn main() -> i32:
while true:
break # error: break-value-context
else:
1Open in playground#Match Expressions
match evaluates its subject once and compares arms in source order:
message := match status:
JobStatus.Queued => "waiting"
JobStatus.Running => "working"
_ => "unknown"#Arms And Guards
- flow.match.subject
matchevaluates its subject once and compares arms in source order. - flow.match.guardAn arm may add
iffollowed by aboolguard. - flow.match.guard.evalAfter its pattern matches, the guard is evaluated using that arm's pattern bindings.
- flow.match.guard.resultIf the guard is false, matching continues with the next arm; if it is true, that arm is selected.
- flow.match.guard.skippedA guard is not evaluated when its pattern fails.
- flow.match.bodyOnly the selected arm's body is evaluated.
- flow.match.resultAll arm results must have one compatible type when the match value is used.
- flow.match.result.least-commonWithout an expected type, that type is the least common type of the arm results.
fn grade(score: i32) -> string:
match score:
90 => "top"
s if s >= 70 => "passing"
_ => "needs work"#Exhaustiveness
- flow.match.exhaustiveA match must be exhaustive. Coverage is checked as the table below states. Error:
nonexhaustive-match.
| Rule | Coverage |
|---|---|
| flow.match.cover.enumEnum | A nominal enum, including Option[T] (written T?), requires every variant, accounting for payload constraints, or a catch-all pattern. A variant whose payload has type never still needs an arm, as .Err(e) => e. |
| flow.match.cover.boolBool | bool is covered by both true and false, or by a catch-all pattern. |
| flow.match.cover.tupleTuple | A tuple pattern covers the tuple values covered recursively by its element patterns. |
| flow.match.cover.unitUnit | void is covered by the unit pattern (), or by a catch-all pattern. |
| flow.match.cover.dataData | A data pattern covers its nominal struct type when every listed field pattern is irrefutable, while unlisted fields are unconstrained. |
| flow.match.cover.integerInteger | An integer type is covered when its literal and range patterns together hold every value of the type, or by a catch-all pattern. |
| flow.match.cover.other-valuesOther values | Floating-point, char, string, and other value spaces require an irrefutable catch-all after any literal cases. |
| flow.match.cover.referenceReference | A subject of type &T or &mut T is covered as a subject of type T, since its patterns read through it. |
- flow.match.guard.coverageA guarded arm contributes no coverage to exhaustiveness, even when its pattern would be irrefutable without the guard.
- flow.match.guard.later-armA later unguarded arm must cover its values.
- flow.match.catch-all.finalAn unguarded irrefutable catch-all must be the final arm.
- flow.match.catch-all.guardedA guarded catch-all may be followed by other arms.
- flow.match.duplicate.guardedDuplicate patterns are permitted when earlier occurrences are guarded.
- flow.match.duplicate.coveredAn arm already covered by an earlier unguarded arm is unreachable.
- flow.match.unreachableDuplicate unguarded literals, duplicate fully covered variants, arms after an unguarded catch-all, and other statically provable unreachable arms are errors. Error:
unreachable-match-arm. - flow.match.unreachable.armThe
unreachable-match-armerror is reported on the unreachable arm. - flow.match.limitAn implementation may limit the work of checking one match's coverage and reachability. The limit is implementation-defined.
- flow.match.limit.errorA match whose check exceeds that limit is an error, reported on the match. Error:
match-too-complex.
enum Status:
Ready
Waiting
enum Flag:
On(value: i32)
Off
fn invalid(status: Status) -> string:
match status: # error: nonexhaustive-match
Status.Ready => "ready"
fn choose(flag: Flag) -> i32:
match flag:
_ => 1
Flag.Off => 0 # error: unreachable-match-armWhy. An unguarded irrefutable catch-all must be the final arm because every later arm would be unreachable.
#Catch-All And Variant Patterns
- flow.match.catch-all
_and a bare binding identifier are catch-all patterns for the subject type. - flow.match.bare-variantIf a bare identifier resolves to a variant of the subject enum, it is an error rather than a new catch-all binding. Write
.Variantor a qualified variant name. Error:bare-variant-pattern. - flow.match.bare-payloadAn unqualified identifier followed by a payload list, such as
Some(value)orOk(value), is also an error. Error:bare-variant-pattern. - flow.match.optionalAn optional is matched like any other enum.
- flow.match.optional.someFor a subject of type
T?,.Some(value)matches only the present case and bindsvalueasT. - flow.match.optional.none
.Nonematches only absence. - flow.match.optional.qualified
Option.Some(value)andOption.Noneare the qualified forms. - flow.match.optional.bareA bare
valuebinds the entireT?. - flow.match.optional.bare-variantA bare
NoneorSome(value)is an error. Error:bare-variant-pattern. - flow.match.optional.nestedMatching
T??with.Some(value)removes only the outer layer, sovaluehas typeT?..Some(.Some(value))reaches the inner value. - flow.match.optional.no-patternThere is no optional-specific pattern:
value?is not a pattern. - flow.match.contextualA
.Variantpattern whose expected subject or nested payload type is not an enum is an error, as for.Some(value)against ani32subject. Error:missing-contextual-enum-type.
fn value_or_zero(value: i32?) -> i32:
match value:
Some(number) => number # error: bare-variant-pattern
.None => 0
fn nonoptional(value: i32) -> i32:
match value:
.Some(present) => present # error: missing-contextual-enum-type
_ => 0#Payload Patterns
Enum payload patterns use call-style parentheses. Positional patterns come
first, and field=pattern names a payload field:
match expr:
Expr.Add(l, r) => l + r
Expr.Sub(left=l, right=r) => l - r
Expr.Scale(left, factor=2) => left * 2- flow.match.payload.parenthesesEnum payload patterns use call-style parentheses.
- flow.match.payload.orderPositional patterns come first, and
field=patternnames a payload field. - flow.match.payload.requiredA pattern for a variant that carries a payload must list that payload, with one pattern per payload field.
- flow.match.payload.arityA bare variant pattern for such a variant, or a payload list of a different length, is an error. Error:
pattern-arity. - flow.match.payload.unknownA named pattern that names no payload field of the variant is an error. Error:
unknown-field. - flow.match.payload.bindingA bare identifier in payload position binds a new arm-local name. It does not need to match the payload field's declaration name.
- flow.match.payload.name-mismatch.lintA positional bare binding equal to another payload fieldโs name in the same variant produces a lint finding. Warning:
variant-binding-name-mismatch. - flow.match.payload.name-mismatch.exampleFor example, binding
else_valuein thethen_valueposition emitsvariant-binding-name-mismatch. - flow.match.literalA literal pattern requires an equal value.
- flow.match.variant.shorthandAn enum variant pattern may use
.Variantwhen its subject or enclosing payload position fixes one enum type; otherwise it uses the qualifiedEnum.Variantform. - flow.match.variant.shorthand-exhaustiveThe shorthand has the same exhaustiveness rules as the qualified form.
enum Flag:
On(value: i32)
Off
enum Choice:
Pair(left: i32, right: i32)
fn choose(flag: Flag) -> i32:
match flag:
Flag.On => 1 # error: pattern-arity
Flag.Off => 0
fn bad(choice: Choice) -> i32:
match choice:
Choice.Pair(missing=value, right=other) => value # error: unknown-fieldNote. Use named patterns to make an intentional reorder explicit.
#Payload Bindings
- flow.match.no-ownershipMatching does not transfer ownership.
- flow.match.bind.primitivePrimitive payloads bind by value.
- flow.match.bind.genericA payload declared with a generic parameter binds its substituted type.
- flow.match.bind.tupleTuple pattern elements retain their declared types.
- flow.match.literal-payloadA literal-constrained payload pattern such as
Expr.Scale(value, factor=2)covers only that subset of the variant. - flow.match.literal-payload.restAnother arm must therefore cover the remaining
Scalevalues unless a later catch-all does.
enum Event:
Click(x: i32, y: i32)
Key(key: string)
fn describe(event: Event) -> string:
match event:
Event.Click(x, y) => "click at $x,$y"
Event.Key(key) => "key $key"#Nested And Data Patterns
A data pattern destructures a struct value by field:
match value:
Shape.Dot(Point { x: 0, y }) => y
Shape.Dot(Point { x: px, y: _ }) => px
Shape.Line(start=Point { x: 0 }, end=_) => 0
Shape.Line(start=_, end=_) => 1- flow.match.nestedNested variant and tuple patterns are permitted by the grammar.
- flow.match.dataData destructuring patterns are also permitted.
- flow.match.data.bind
User { name }binds thenamefield, andUser { name: alias }binds it asalias. - flow.match.data.literal
User { age: 18 }matches only values with that field value. - flow.match.data.unlistedUnlisted fields are ignored.
- flow.match.data.labelAs in a data expression, a field label inside the braces is followed by
:. The=label belongs to payload patterns in parentheses. - flow.match.data.fieldsListed fields must be distinct and exist on the named struct type.
- flow.match.data.fields.unknownA listed field that the struct type does not declare is an error. Error:
unknown-field. - flow.match.data.fields.duplicateA field listed twice is an error, in a data pattern and in an enum payload pattern alike. In
.Rect(w, w=1)the positional pattern andw=both name the payload fieldw. Error:duplicate-data-pattern-field. - flow.match.data.publicA cross-module pattern may name only public fields.
- flow.match.data.emptyAn empty data pattern is irrefutable for that struct type.
- flow.match.data.nominalA named data pattern must match the subject's nominal type. It does not structurally match a different struct type with the same fields.
- flow.match.data.bind-primitivePrimitive fields bind by value.
- flow.match.data.genericA generic field declared
field: Pbinds as substitutedP.
struct User:
name: string
age: i32
fn read(user: User) -> i32:
match user:
User { missing } => 1 # error: unknown-field
fn invalid(user: User) -> string:
match user:
User { name, name: alias } => alias # error: duplicate-data-pattern-field#Spread Patterns
A spread pattern ends a tuple pattern and binds the rest element of a tuple to its list:
fn total(t: (usize, usize, List[i32]...)) -> usize:
let (a, b, xs...) = t
a + b + xs.len()
fn lengths(rows: List[(string, List[i32]...)]) -> List[usize]:
[for (_, xs...) in rows => xs.len()]
fn describe(t: (i32, List[i32]...)) -> string:
match t:
(0, _...) => "zero"
(n, xs...) => "$n and ${xs.len()} more"- flow.match.spread.restA tuple pattern that ends in a spread pattern matches a tuple type with a rest element
List[T].... Its other subpatterns match the fixed elements one each, in order. - flow.match.spread.bindThe spread pattern
xs...bindsxsto the rest element'sList[T], soxsabove is aList[i32]. The spread pattern_...binds nothing. - flow.match.spread.arityThe subpatterns before a spread pattern must be as many as the type's fixed elements. Error:
type-mismatch. - flow.match.spread.fixed-tupleA spread pattern against a tuple type without a rest element, or against any other type, is an error. Error:
type-mismatch. - flow.match.spread.requiredA tuple pattern without a spread pattern against a tuple type with a rest element is an error. Error:
type-mismatch. - flow.match.spread.coverA spread pattern covers every list, so a tuple pattern that ends in one is irrefutable when its other subpatterns are.
fn fixed(pair: (i32, i32)) -> i32:
let (first, rest...) = pair # error: type-mismatch
first
fn plain(t: (i32, List[i32]...)) -> i32:
let (first, rest) = t # error: type-mismatch
first
fn short(t: (i32, i32, List[i32]...)) -> i32:
let (first, rest...) = t # error: type-mismatch
firstNote. A spread pattern stands wherever a tuple pattern does: in a
let, aforloop or comprehension clause, and amatcharm. There are no list patterns, so there is no[a, rest...]form.
Why. The pattern mirrors the type
(i32, i32, List[i32]...)and the expression(a, b, xs...), so a rest tuple is built and taken apart in one shape.
#Unit Pattern
The unit pattern () matches the unit value, so a void success is matched
as it is built:
fn save(ready: bool) -> Result[void, string]:
if ready:
return .Ok(())
.Err("not ready")
fn report(ready: bool) -> string:
match save(ready):
.Ok(()) => "saved"
.Err(message) => message- flow.match.unitThe unit pattern
()matches the unit value(), the one value ofvoid. - flow.match.unit.irrefutableIt is irrefutable for
void, so.Ok(())covers every success of aResult[void, E]. - flow.match.unit.bindsIt binds no name.
- flow.match.unit.typeA unit pattern against a value whose type is not
voidis an error. Error:type-mismatch.
fn read() -> Result[i32, string]:
.Ok(1)
fn report() -> i32:
match read():
.Ok(()) => 0 # error: type-mismatch
.Err(_) => 1Why. Rust and Swift match
()with(). Construction and matching then read alike, and.Ok(())needs no_that hides what it matches.
#Range Patterns
A range pattern matches the integers that the range of the same form holds:
fn bucket(n: u8) -> string:
match n:
0 => "zero"
1..10 => "small"
10..=99 => "medium"
100.. => "large"
fn sign(n: i8) -> i32:
match n:
..=-1 => -1
0 => 0
1.. => 1| Rule | Pattern | Matches |
|---|---|---|
| flow.match.range.half-openHalf-open | a..b |
the integers from a up to, not including, b |
| flow.match.range.inclusiveInclusive | a..=b |
the integers from a through b |
| flow.match.range.fromFrom | a.. |
the integers from a through the type's largest value |
| flow.match.range.to-inclusiveTo inclusive | ..=b |
the integers from the type's smallest value through b |
| flow.match.range.toTo | ..b |
the integers from the type's smallest value up to, not including, b |
- flow.match.range.subjectA range pattern's subject must have an integer type. A range pattern against any other type is an error. Error:
type-mismatch. - flow.match.range.bound-typeEach bound is checked with the subject's type as its expected type, so a bound outside that type is an error. Error:
integer-literal-range. - flow.match.range.no-bindA range pattern binds no name and builds no range value.
- flow.match.range.coverA range pattern covers exactly the values it matches, by
flow.match.cover.integer, so..=-1,0, and1..coveri8. - flow.match.range.emptyA range pattern that matches no value, as
5..5or3..=1, is an unreachable arm. Error:unreachable-match-arm. - flow.match.range.coveredAn arm whose every value earlier unguarded arms cover is unreachable, by
flow.match.duplicate.covered. Error:unreachable-match-arm. - flow.match.range.overlapArms that overlap only in part are not an error, and the first matching arm is selected.
fn invalid(name: string) -> string:
match name:
0..=9 => "digit" # error: type-mismatch
_ => "other"
fn too_wide(n: u8) -> string:
match n:
0..=300 => "byte" # error: integer-literal-range
fn missing(n: u8) -> string:
match n: # error: nonexhaustive-match
0..=127 => "low"
129.. => "high"
fn covered(n: i32) -> string:
match n:
0..=9 => "digit"
3..5 => "never" # error: unreachable-match-arm
_ => "other"Note. A pattern names no constant, so each bound is a literal.
Why. An integer type is a finite range of values, so ranges can cover it without a catch-all, as Rust's checker does.
#Let Patterns
A let pattern matches its initializer as a match arm's pattern would,
and a let-else block handles the values it does not match:
struct Point:
x: i32
y: i32
fn find(id: i32) -> Point?:
if id == 0: .Some(Point { x: 1, y: 2 }) else: .None
fn read(id: i32) -> i32:
let .Some(point) = find(id) else: return 0
let Point { x, y } = point
x + y- flow.let.matchA
letpattern matches the initializer's value under the pattern rules of Match Expressions, as one unguarded arm would. - flow.let.bindIts names bind as that arm's names would, as Payload Bindings and Nested And Data Patterns state.
- flow.let.irrefutableA pattern is irrefutable when it alone covers the initializer's type, under the coverage rules of Exhaustiveness. Any other pattern is refutable.
- flow.let.irrefutable.no-elseA
letwith an irrefutable pattern needs noelse. - flow.let.refutable.elseA
letwith a refutable pattern, such as a literal, an enum variant, or.Some(v), must have anelseblock. A refutable pattern without one is an error. Error:refutable-let-pattern. - flow.let.else.orderA let-else evaluates its initializer once. When the pattern matches, it binds the names; otherwise the
elseblock runs. - flow.let.else.divergeThe
elseblock must diverge: control never reaches its end. - flow.let.else.diverge.formsA block diverges when its final statement has type
never, such asreturn,break,continue, or a call topanic. It also diverges when the final statement is aniformatchwhose every branch diverges. - flow.let.else.falls-throughAn
elseblock that may complete normally is an error. Error:let-else-falls-through. - flow.let.else.unreachableAn
elseblock after an irrefutable pattern could never run, so it is an error. Error:unreachable-match-arm. - flow.let.tuple-arityA tuple pattern needs a tuple of the same arity. Against any other value it is an error. Error:
type-mismatch.
fn find(id: i32) -> i32?:
if id == 0: .Some(7) else: .None
fn missing_else(id: i32) -> i32:
let .Some(value) = find(id) # error: refutable-let-pattern
value
fn falls_through(id: i32) -> i32:
let .Some(value) = find(id) else: # error: let-else-falls-through
println("missing")
value
fn arity() -> i32:
let (first, second) = (1, 2, 3) # error: type-mismatch
first
fn always(pair: (i32, i32)) -> i32:
let (low, high) = pair else: return 0 # error: unreachable-match-arm
low + highWhy. An
elseblock that fell through would reach code that reads names the pattern never bound. Rust's let-else and Swift'sguard letrequire the same divergence. Anelsethat can never run tells the reader the pattern may fail. That is false, so it is rejected as an unreachable arm would be.
See also: Let-Else Statements, and Let-Else Scope for where the names are visible.
#Return
return completes the nearest enclosing named function or closure.
- flow.return.value
return expressionimmediately completes the nearest enclosing named function or closure with that value. - flow.return.typeThe value must be assignable to the declared or inferred return type.
- flow.return.bareBare
returnis valid only for avoid-returning function or closure. - flow.return.outside
returnoutside a named function or closure is a compile-time error. Error:return-outside-function. - flow.return.script-onlyA module script is not an implicit return target.
- flow.return.fallthroughFalling through a function body evaluates its final expression as the return value.
- flow.return.void-fallthroughA function declared
-> voidmay fall through after a statement whose result isvoid. - flow.return.callbackInside a trailing callback block,
returncompletes the generated callback, not the function containing the call. - flow.return.non-localhd-lang has no non-local return from a closure.
return 1 # error: return-outside-function#Deferred Cleanup
defer: registers a cleanup suite:
fn read_first!(path: string) -> Result[string, FileError] $ Files:
let handle: &mut dyn FileHandle = $.use(Files).open!(path)?
defer:
_ := handle.close()
handle.read!()#Registration And Order
- flow.defer.register
defer:registers a synchronous cleanup suite on the innermost executing lexical block. - flow.defer.not-runReaching the statement does not run its suite.
- flow.defer.runRegistered suites run once in last-in, first-out order when that block exits normally, through
return,break, orcontinue, or because postfix?propagates. - flow.defer.loopEach loop iteration has its own body block, so its registered suites run before the next iteration begins.
- flow.defer.result-savedA value-producing block evaluates and saves its result before running its registered suites.
- flow.defer.operandsLikewise, a
returnorbreakoperand and a value being propagated by?are evaluated before cleanup begins. - flow.defer.capturesA cleanup suite observes captured lexical storage at cleanup time.
fn first() -> void: pass
fn second() -> void: pass
fn work() -> void: pass
fn demo() -> void:
defer: first()
defer: second()
work() # work, then second, then first#Cleanup Scopes
- flow.defer.scopesCleanup scopes are function and closure bodies, loop bodies, each selected
iforelsesuite, and match arms. Provider scopes and trailing callback blocks, including test bodies, are cleanup scopes too. - flow.defer.not-scopesModule top level and declaration bodies that do not execute are not cleanup scopes.
- flow.defer.outsideA
deferthere is an error. Error:defer-outside-cleanup-scope.
defer: # error: defer-outside-cleanup-scope
pass#Cleanup Suite Restrictions
- flow.defer.voidA
defersuite must producevoid. - flow.defer.restrictedIt cannot bang-call, otherwise suspend, propagate with
?, or transfer control withreturn,break, orcontinue. - flow.defer.suspendA bang call or other suspending operation in the suite is an error. Error:
suspension-forbidden-context. - flow.defer.controlPropagation with
?, or areturn,break, orcontinuethat would leave the suite, is an error. Error:defer-control-flow. - flow.defer.block-on.directA call of
std.task.block_onorprintlnwritten directly in the suite is an error. Error:suspension-forbidden-context. - flow.defer.block-on.indirectSuch a call reached through another call while the suite runs panics, as
req.drive.block-on.indirectsays. Panic:suspension-forbidden-context.
defer: # error: defer-outside-cleanup-scope
pass
fn cleanup!() -> void:
pass
fn invalid!() -> void:
defer:
cleanup!() # error: suspension-forbidden-context
pass
fn run() -> i32:
defer:
return 1 # error: defer-control-flow
0See also: Suspending Functions.
#Panics And Escaping Aliases
- flow.defer.panicA runtime panic does not run pending
defersuites. - flow.defer.cleanup-panicIf a cleanup suite itself panics, the instance is poisoned and no remaining cleanup suite is guaranteed to run.
- flow.defer.alias-escape
deferdoes not stop an alias to a closed handle from escaping.
fn cleanup() -> void: pass
fn demo() -> void:
defer: cleanup() # not run: a panic skips pending defer suites
panic("boom")Why. Core hd-lang has no panic unwinding.
Note. Ownership and alias-escape prevention remain open design work.
#Using Bindings
A using binding binds a value that is disposed when its scope ends:
use std.ops.Disposable
struct Lock:
name: string
held: *mut List[string]
impl Disposable for Lock:
fn dispose!(&mut self) -> void:
self.held.push("released ${self.name}")
fn acquire(name: string, held: *mut List[string]) -> *mut Lock:
held.push("acquired ${name}")
&mut Lock { name: name, held: held }
fn transfer!(held: *mut List[string]) -> void:
using first = acquire("a", held)
using second = acquire("b", held)
held.push("work") # then b is released, then a#Disposable
pub trait Disposable:
fn dispose!(&mut self) -> void- flow.using.disposable-ref
std.opsdeclares the traitDisposableas above, sodispose!changes the value in place. Code imports it to name it, as inuse std.ops.Disposable; ausingbinding needs no import. - flow.using.suspend
dispose!is a bang method, so it may suspend. A type whose release never suspends still writesdispose!, and its body makes no suspending call. - flow.using.best-effort
dispose!returnsvoid, so a release that fails reports nothing. A type whose release can fail offers a separate method that returns aResult, which code calls before the scope ends.
#Binding And Registration
- flow.using.form
using name = expressionandusing name: T = expressionbindnameto the value ofexpressionfor the rest of the suite. - flow.using.bindingA
usingbinding is not reassignable. Error:non-reassignable-binding. - flow.using.disposable-required-refThe binding's type must implement
Disposable. For a binding of a reference&mut T,Tmust implement it instead, or, for&mut dyn Tr,Trmust have it as a supertrait. Error:unsatisfied-trait-bound. - flow.using.accessThe binding has its annotation's type, or else its initializer's type.
- flow.using.placeA binding that holds its value is the place that
dispose!changes: the registered call takes the binding's address, as a&mut selfcall on alet mutbinding does. No other&mut selfcall may change the binding. Error:not-writable. - flow.using.referenceA binding of a reference
&mut Tdisposes the value it refers to: the registered call passes the reference, so every other reference to that value sees it disposed. A trait value reference&mut dyn Trdisposes through its trait value, bytrait.dyn.ref.member. - flow.using.reference.read-onlyA binding of a read-only reference,
&Tor&dyn Tr, is an error, sincedispose!changes the value. Error:not-writable. - flow.using.registerOnce the initializer is evaluated and bound, the statement registers a cleanup suite that calls
name.dispose!(), exactly as adeferat that point would. - flow.using.disposeThat suite runs by the rules of Registration And Order. So a value is disposed when its block exits normally, through
return,break, orcontinue, or because postfix?propagates. - flow.using.orderDisposes and
defersuites of one block run in one last-in, first-out order. - flow.using.loopEach loop iteration disposes the values that its body bound with
usingbefore the next iteration begins. - flow.using.unboundA statement whose initializer does not complete, because it propagates with
?or panics, registers nothing. - flow.using.scopesA
usingbinding follows the cleanup scope rules ofdefer, so one at module top level is an error. Error:defer-outside-cleanup-scope. - flow.using.bangThe registered call is a bang call written at the
usingstatement. So ausingbinding outside a driver context is an error. Error:bang-call-outside-suspension. - flow.using.in-deferA
usingbinding inside adefersuite is a suspension in that suite. Error:suspension-forbidden-context. - flow.using.suite-suspendsUnlike a written
defersuite, a dispose suite may suspend, and the scope exit goes on oncedispose!returns. - flow.using.not-forbiddenA dispose suite is not a forbidden context of
req.drive.block-on.indirect. - flow.using.panicA runtime panic does not run pending disposes, as
flow.defer.panicsays ofdefersuites. - flow.using.cancelCancellation unwinds a task as any exit does: its registered disposes run, in last-in, first-out order with its
defersuites. - flow.using.cancel.shieldedA
dispose!that suspends is shielded from the cancellation, and it takes no cancellation of its own. So each dispose runs to its end, unless it is cut short as the next rule says. - flow.using.cancel.cut-shortOnly
abandon()and an await of a suspension cancelled elsewhere cut a dispose short, byreq.abandon.disposeandreq.abandon.dependency. - flow.using.cancel.waitThe cancellation of a task completes only once its disposes have. A suspension whose cancellation reaches a child waits for the child's disposes before it runs its own.
use std.ops.Disposable
struct Lock:
name: string
impl Disposable for Lock:
fn dispose!(&mut self) -> void:
pass
struct Note:
text: string
fn swap!() -> void:
using lock = Lock { name: "a" }
lock = Lock { name: "b" } # error: non-reassignable-binding
fn read!(note: Note) -> string:
using copy = Note { ...note } # error: unsatisfied-trait-bound
copy.text
fn peek!(lock: &Lock) -> void:
using held = lock # error: not-writable
fn plain(id: i32) -> void:
using session = Lock { name: "s" } # error: bang-call-outside-suspensionWhy. A release often waits, as a socket flushes or a server answers, so one trait whose
dispose!may suspend covers every handle. The registration is thedeferthat Go code writes by hand after each open, made impossible to forget. Cancellation follows JavaScript'sawait usingand C#'sawait usingwithDisposeAsync: an abandoned task still awaits each disposal, andDisposeAsynctakes no cancellation token.
Note.
cancel!()on a stored suspension waits for adispose!that suspends, byreq.cancel.wait, and a cancel that arrives during a dispose takes it over, byreq.cancel.takeover.abandon()stops a waiting dispose instead, and a handle the dispose owns needs adeferin the dispose that closes it.usingdoes not stop an alias of the value from outliving its scope.
#Unreachable Code
This section defines unreachable statements.
- flow.unreachable.defStatements following an unconditional
return,break, orcontinuein the same suite are unreachable. - flow.unreachable.infinite-loopStatements following an infinite loop that no
breaktargets, in the same suite, are unreachable. - flow.unreachable.lintUnreachable code produces a lint finding without changing program validity. Warning:
unreachable-code. - flow.unreachable.checkedUnreachable code is still parsed and type-checked.
- flow.unreachable.policyTooling may provide a package policy that promotes this warning to an error without changing language semantics.
fn accept(port: i32) -> void:
pass
fn serve(port: i32) -> void:
while true:
accept(port)
println("server stopped") # warning: unreachable-code#Runtime Panics
This section defines runtime panics.
- flow.panic.defA runtime panic is an abrupt, unrecoverable failure of the current program instance.
- flow.panic.not-resultIt is distinct from a recoverable
Resulterror and is not catchable by core hd-lang source code. - flow.panic.sourcesInteger overflow, division errors, invalid shifts, out-of-bounds indexing, and invalidated built-in iterators panic when their owning chapters require a checked runtime failure. Integer overflow and invalid shifts panic in a debug or test build only. Exhausting the call stack, the heap, or a time limit also panics, by the rules below.
- flow.panic.stack-exhaustedExhausting the call stack panics. The report gives a source location when one is available, as
flow.panic.reportsays. Panic:stack-exhausted. - flow.panic.heap-exhaustedAn allocation that the heap cannot satisfy, within any heap limit the host sets, panics. Panic:
heap-exhausted. - flow.panic.time-limitA program instance that runs past a time limit the host sets panics. Panic:
time-limit.
fn demo(a: i32, b: i32) -> i32:
a / b # b == 0 panics with integer-division-by-zero#Panic Behavior
- flow.panic.stopWhen a panic occurs, ordinary evaluation stops immediately.
- flow.panic.nothing-runsNo enclosing
elsesuite, remaining expression, or caller statement executes. - flow.panic.runtime-cleanupRuntime and library mechanisms may observe the complete exit and perform mandatory runtime cleanup.
- flow.panic.no-handlerCore source has no panic handler or unwinding construct.
- flow.panic.reportThe runtime must report at least a stable failure category and source location when one is available.
- flow.panic.encodingIts Wasm trap, host error, and diagnostic encoding are ABI details.
- flow.panic.report.fallbackWhen an operation that panics with
integer-overflowhas a type from theusizedefault, the report must say so. The category staysinteger-overflow. - flow.panic.report.fallback.fixThat report must suggest a signed literal or a type annotation, and should name the binding whose type fell back when the operation reads one.
fn must(n: i32) -> i32:
if n < 0: panic("negative")
nNote. The report text is not normative. For
let balance = 100followed bybalance = balance - 150, one such message is "balancefell back tousize(no signed literal); write+100orlet balance: i32 = 100".
#Program Instances
- flow.panic.instanceOne program instance is the instantiated module graph and execution state that Module Initialization defines.
- flow.panic.poisonA panic poisons that instance: the host must not invoke it again and must discard it after reporting the failure.
- flow.panic.isolationHosts that require invocation isolation create a separate instance per invocation.
tests:
it("one case"):
_ := +1
it("another"):
_ := +2 # a fresh program instance; nothing carries over#Panic Categories
- flow.panic.known-categoriesStable panic categories are exactly
assertion-failed,collection-pinned,explicit-panic,fact-evaluation-failed,heap-exhausted,host-contract,integer-overflow,integer-division-by-zero,invalid-shift,index-out-of-bounds,iterator-invalidated,stack-exhausted,structure-variant-mismatch,suspension-competing-driver,suspension-deadlock,suspension-forbidden-context,suspension-invalid-state,suspension-reentrant-poll, andtime-limit. - flow.panic.explicitThe prelude function
panic(message: string) -> neverexplicitly causes anexplicit-panicfailure. - flow.panic.neverA panic or other abrupt expression is valid in any value-producing arm without affecting the compatible result type of reachable normal arms.
fn must(n: i32) -> i32:
if n < 0: panic("negative")
nWhy.
neveris assignable to every type.