hd-lang

#Control Flow

Status: language specification draft.

This chapter defines suites, conditionals, loops, match, return, defer, and runtime panics.

  1. flow.suiteshd-lang control-flow constructs use indentation-delimited suites and may produce values.
  2. 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

  1. flow.block.orderA suite evaluates statements in source order.
  2. flow.block.valueIf control reaches the final expression statement normally, that expression is the suite's value.
  3. flow.block.voidA suite whose final statement is not a value expression has type void.
  4. flow.block.abruptreturn, 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.
  5. flow.block.non-emptyAn indented suite must contain at least one statement.
  6. flow.block.passpass supplies 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

  1. flow.must-use.discardAn expression whose value is discarded and whose type is Result[T, E], T?, or mut Suspend[T] is a compile-time error. Error: discarded-must-use-value.
  2. flow.must-use.statementThis includes a non-final expression statement.
  3. flow.must-use.discarded-suiteIt also includes the final expression of any suite whose value is discarded:
    • a loop body;
    • an if without else;
    • a statement-position match arm;
    • a defer suite;
    • a module's top-level script.
  4. flow.must-use.handledSuch a value must be propagated with ?, inspected by match, returned, stored for later use, or explicitly discarded with _ := expression or let _ = expression.
  5. flow.must-use.underscoreThe _ spelling does not bind a local name.
Error example
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 discard

Why. The _ spelling makes the discard visible in review.

#Unused Bindings

  1. flow.unused.lintAn ordinary local binding that is never read produces a lint finding. Warning: unused-local-binding.
  2. flow.unused.underscoreNames beginning with _ suppress that warning.
  3. 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, or let mut. Error: discarded-must-use-value. An unread Result or T? binding is the ordinary unused-local-binding warning, because binding a value to a name counts as using it.
  4. flow.unused.discard-formsThe two discard forms, _ := expression and let _ = 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"
  1. flow.if.selectif, else if, and else select exactly one suite.
  2. flow.if.orderConditions are evaluated from top to bottom until one is true.
  3. flow.if.lazyLater conditions and unselected suites are not evaluated.
  4. flow.if.else-ifelse if is one conditional-chain form. It is not parsed as an else suite containing an unrelated nested if.

#Conditional Values

  1. flow.if.value.elseWhen an if is used where a value is required, it must have an else.
  2. flow.if.value.branchesEvery reachable branch must then produce a value, and those values must have one compatible result type.
  3. flow.if.value.least-commonWithout an expected type, that type is the least common type of the branch values.
  4. flow.if.statementIn statement position, else may be omitted, and the conditional then has type void.
fn fee(rush: bool) -> i32:
    base := if rush: +10 else: +5
    base * 2

#Same-Line Conditionals

  1. flow.if.same-lineA same-line if cannot be written directly inside another same-line suite.
  2. flow.if.same-line.errorif a: if b: 1 else: 2 else: 3 and fn sign(x: i32) -> i32: if x < 0: -1 else: 1 are errors. Error: syntax-error.
Error example
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-error

Why. Each else on 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)
  1. flow.for.iterable-onceThe iterable expression is evaluated exactly once.
  2. flow.for.fresh-iteratorA new iterator is obtained for each execution of the loop.
  3. flow.for.bindingThe binding pattern receives each yielded value before the body executes.
  4. flow.for.patternThe pattern of a for loop or a comprehension for clause matches each yielded value of type T. It matches as a let pattern matches an initializer of type T, under Let Patterns.
  5. flow.for.pattern.irrefutableThe pattern must be irrefutable for T. A refutable pattern is an error, and a loop's else does not handle it. Error: refutable-let-pattern.
  6. 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
    sum
Error example
fn 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
        pass

Note. A map yields (K, V) tuples, so for (key, value) in entries is a tuple pattern, and for entry in entries binds 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
  1. flow.for.iterator.traitIterator[T] is a prelude trait declaring fn next(&mut self) -> T?.
  2. flow.for.iterator.user-implA user-defined type may implement Iterator[T] with its own traversal state.
  3. flow.for.collection-cursor.privatestd.iter.FromFn[T] is a concrete cursor with one private step: fn() -> T? field.
  4. flow.for.iterator.next.traitCalling next advances the cursor through its implementation of Iterator[T].
  5. flow.for.iterator.value-stateIterator values follow ordinary value semantics; copying a mutable reference shares the referenced cursor and its progress.
  6. flow.for.iter-independentAn ordinary iterable creates an independent iterator on every iter() call.
  7. flow.for.iterator-exhaustedThe iterator stores traversal progress. Once next has returned .None, the iterator is exhausted.
  8. flow.for.iterator-after-noneWhat next returns when it is called again on an exhausted iterator is unspecified.
  9. flow.for.iterable-collectionsList[T] and Map[K, V] implement Iterable: a list yields T, and a map (K, V).
  10. flow.for.iterator.iterable-not-impliedImplementing Iterator[T] does not imply Iterable[T].
  11. flow.for.string-not-iterablestring does not implement Iterable, so a loop over a string is an error. Error: unsatisfied-trait-bound.
  12. flow.for.string-explicitA loop over a string's contents iterates chars(), char_indices(), or bytes(), which String Methods defines.
  13. flow.for.accepts-owned-cursorfor accepts an Iterable[T], a value implementing Iterator[T], or a mutable reference to such a cursor.
  14. flow.for.iterable.cursorWhen the iterable expression's type implements Iterable[T] and is not a List or a Map, the loop calls iter once and advances the resulting cursor. A list or a map is iterated as Snapshot Iteration states.
  15. flow.for.iterable.into-iterIterable[T] declares the associated cursor type IntoIter, bounded by Iterator[T], and iter returns a value of it. Each implementation names its cursor type, as in type IntoIter = ListIter[T].
  16. flow.for.iterable.static-cursorThe loop holds the IntoIter value iter returned as its own writable copy, as it holds an iterator source, and calls its next through that type's Iterator[T] implementation, with no trait value.
  17. flow.for.iterator.owned-sourceWhen the source is an iterator, the loop calls next on its owned binding without calling iter.
  18. flow.for.iterator.owned-positionIteration starts at the copied cursor's current position and exhausts that copy unless the loop exits early.
  19. flow.for.iterator.shared-referenceA loop cannot advance a cursor through a shared reference. Error: not-writable.
  20. 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.
  21. flow.for.protocol-requiredA source implementing neither Iterable[T] nor Iterator[T] is an error. Error: unsatisfied-trait-bound.
  22. flow.for.comprehensionComprehension for clauses accept the same values by the same rules.
  23. flow.for.iterator.iterable-boundAn iterator without an Iterable[T] implementation does not satisfy an Iterable[T] bound. Error: unsatisfied-trait-bound.
  24. flow.for.iterator.evaluate-onceA loop evaluates its source expression once before iteration starts.
  25. flow.for.iterator.copy-preserves-sourceAdvancing the loop's owned cursor value does not change the original value.
  26. 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::IntoIter is. So a loop or an adapter chain over a list or a range advances a concrete cursor and calls its next directly, never through a trait value.

Why. Iterable[T] promises a fresh, independent traversal on every iter() 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 implemented Iterable, a second pass would silently see only what the first pass left, or nothing, as with Python generators. So iterators stay outside Iterable.

for still consumes an iterator directly, by flow.for.iterator.owned-source: the loop owns the copied cursor. To pass an iterator where an Iterable is 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
    total
Error example
use 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-bound
Open in playground

Note. An iterator is single-pass, and there is no clone or tee of an iterator. To traverse the items twice, call iter() on the collection again, as twice does, or collect the iterator into a list first (What Collect Builds).

Why. next changes 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, and by_ref supplies 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

  1. flow.for.listThe built-in List[T] iterable yields each element as T, whether the list is reached directly or through a reference.
  2. flow.for.map.entriesThe built-in Map[K, V] iterable yields (K, V) tuples in the map's iteration order.
  3. flow.for.map.valueDestructuring each entry yields the value as V, whether the map is reached directly or through a reference.
  4. flow.for.no-root-grantA loop binding is not writable, even when the list is in a writable place or behind a &mut reference. Changing a field of an element it binds is an error. Writing through an element of a *mut U type is valid. Error: not-writable.
  5. flow.for.libraryLibraries may provide additional mutable-iteration APIs with separate aliasing rules.
  6. flow.for.collection.referenceOutside Element Reference Loops, a for over a reference to a List or Map iterates 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
  1. flow.for.snapshotA for loop over a List or Map value iterates the collection as it was when the loop began.
  2. 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.
  3. 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
  1. flow.for.reffor p in &mut xs, where xs is a list, binds the pattern to a &mut T element reference to each slot of xs in order, and for p in &xs to a &T one.
  2. flow.for.ref.slotsxs is evaluated once. The loop visits the slots from 0 up to the list's length when the loop began.
  3. 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.
  4. flow.for.ref.conflictWhen xs is unaliased, the body may read xs and write its elements, but an expression in it that may change the length of xs, as the table under ref.elem.conflict lists, is an error. Error: conflicting-access.
  5. flow.for.ref.pinnedThe loop pins xs while 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.
  6. 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.
  7. flow.for.ref.writablefor p in &mut xs needs xs in a writable place, as &mut xs[i] does. Error: not-writable.
  8. flow.for.ref.mapA loop over element references takes a list, so a loop over &m or &mut m with a map m is an error. Error: map-value-not-addressable.
  9. flow.for.ref.throughxs may be a reference to a list: for p in &mut r and for p in &r loop over the slots of the list r refers to, as if *r were the place. Through &T, for p in &mut r is an error. Error: not-writable.
Error example
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-call

Why. 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
  1. flow.for.range.iterable-twoFor each integer type T, Range[T] and RangeFrom[T] implement Iterable[T].
  2. flow.for.range.cursorThe cursor type of Range[T] is std.ops.RangeIter[T], and that of RangeFrom[T] is std.ops.RangeFromIter[T].
  3. flow.for.range.half-openIterating a..b yields a, a + 1, and so on, up to but not including b. It yields nothing when a >= b.
  4. flow.for.range.inclusiveIterating a..=b yields a through b, and yields b even when it is the type's largest value. It yields nothing when a > b.
  5. flow.for.range.fromIterating a.. yields a, a + 1, and so on, with no end, so for i in 0..: runs until the loop exits another way.
  6. 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, as a + 1 is. It wraps in a release build, again as a + 1 does. Panic: integer-overflow.
  7. flow.for.range.freshEach iter() call on a range starts from its start bound, and iterating never changes the range value.
  8. flow.for.range.toRangeTo[T] does not implement Iterable, since it has no start, so a loop over ..b is an error. Error: unsatisfied-trait-bound.
  9. flow.for.range.to-through..=b is a RangeTo[T] too, so a loop over it is an error. Error: unsatisfied-trait-bound.
  10. flow.for.range.fullRangeFull does not implement Iterable either, so a loop over .. is an error. Error: unsatisfied-trait-bound.
  11. flow.for.range.floatA range of a floating-point type does not implement Iterable, so a loop over 0.0..1.0 is an error. Error: unsatisfied-trait-bound.
Error example
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
        pass

Note. std writes one implementation over std.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).

  1. flow.for.cursor.snapshotAn iterator that iter() returns over a List or a Map yields the elements or entries the collection held when iter() ran. A later push, removal, or replacement changes the collection but not the iterator, and does not make it panic.
  2. flow.for.user-definedUser-defined iterables must document equivalent mutation behavior in their own contract.
  3. 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 snapshot

Why. 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
  1. flow.while.conditionA while loop evaluates its bool condition before every iteration.
  2. 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
  1. flow.while.infiniteA while loop whose condition is the literal true, alone or in grouping parentheses, is an infinite loop.
  2. flow.while.infinite.literalNo other condition makes a loop infinite: not a named constant, not !false, and not a variable that holds true.
  3. flow.while.infinite.exitAn infinite loop completes normally only through a break that targets it.
  4. flow.while.infinite.neverAn infinite loop that no break targets has type never, with or without else.
  5. flow.while.infinite.elseThe else suite of an infinite loop is never evaluated. It is still type-checked, and it still joins the loop result type, as flow.loop.else.type states.

Why. hd has no loop keyword; while true takes 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 break exits the nearest enclosing loop, by flow.break. So a break inside 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

  1. flow.continuecontinue skips the remainder of the current loop body and begins the next iteration.
  2. flow.breakbreak exits the nearest enclosing loop.
  3. flow.break.boundaryNeither operation targets a loop across a function or closure boundary.
  4. flow.break.outside-loopbreak and continue outside a loop are compile-time errors, including at module top level and directly in a test body. Error: break-outside-loop.
Error example
fn main() -> void: break  # error: break-outside-loop
Open in playground

#Loop Values

A for or while loop with else is value-producing:

found := for value in values:
    if value > 100:
        break value
else:
    .None
  1. flow.loop.void.typeA loop without else has type void, unless it is an infinite loop that no break targets, which has type never by flow.while.infinite.never.
  2. flow.loop.void.breakA loop without else permits plain break but not break value. A break value there is an error. Error: break-value-context.
  3. flow.loop.else.valueA for or while loop with else is value-producing.
  4. flow.loop.else.break-valuebreak value terminates the loop and supplies its value.
  5. flow.loop.else.exhaustionNormal exhaustion of a for loop, or a false while condition, evaluates the else suite and uses that suite's value.
  6. flow.loop.else.plain-breakPlain break in a value-producing loop is an error. Error: break-value-context.
  7. flow.loop.else.typeEvery reachable break value and the else suite join by the least common type, which is the loop result type.
  8. flow.loop.else.continuecontinue does not produce a loop value.
  9. flow.loop.else.skippedThe else suite is not executed after any break, abrupt function return, propagation with ?, cancellation, or runtime panic.
Error example
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:
        1
Open 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

  1. flow.match.subjectmatch evaluates its subject once and compares arms in source order.
  2. flow.match.guardAn arm may add if followed by a bool guard.
  3. flow.match.guard.evalAfter its pattern matches, the guard is evaluated using that arm's pattern bindings.
  4. flow.match.guard.resultIf the guard is false, matching continues with the next arm; if it is true, that arm is selected.
  5. flow.match.guard.skippedA guard is not evaluated when its pattern fails.
  6. flow.match.bodyOnly the selected arm's body is evaluated.
  7. flow.match.resultAll arm results must have one compatible type when the match value is used.
  8. 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

  1. 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.
  1. flow.match.guard.coverageA guarded arm contributes no coverage to exhaustiveness, even when its pattern would be irrefutable without the guard.
  2. flow.match.guard.later-armA later unguarded arm must cover its values.
  3. flow.match.catch-all.finalAn unguarded irrefutable catch-all must be the final arm.
  4. flow.match.catch-all.guardedA guarded catch-all may be followed by other arms.
  5. flow.match.duplicate.guardedDuplicate patterns are permitted when earlier occurrences are guarded.
  6. flow.match.duplicate.coveredAn arm already covered by an earlier unguarded arm is unreachable.
  7. 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.
  8. flow.match.unreachable.armThe unreachable-match-arm error is reported on the unreachable arm.
  9. flow.match.limitAn implementation may limit the work of checking one match's coverage and reachability. The limit is implementation-defined.
  10. flow.match.limit.errorA match whose check exceeds that limit is an error, reported on the match. Error: match-too-complex.
Error example
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-arm

Why. An unguarded irrefutable catch-all must be the final arm because every later arm would be unreachable.

#Catch-All And Variant Patterns

  1. flow.match.catch-all_ and a bare binding identifier are catch-all patterns for the subject type.
  2. 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 .Variant or a qualified variant name. Error: bare-variant-pattern.
  3. flow.match.bare-payloadAn unqualified identifier followed by a payload list, such as Some(value) or Ok(value), is also an error. Error: bare-variant-pattern.
  4. flow.match.optionalAn optional is matched like any other enum.
  5. flow.match.optional.someFor a subject of type T?, .Some(value) matches only the present case and binds value as T.
  6. flow.match.optional.none.None matches only absence.
  7. flow.match.optional.qualifiedOption.Some(value) and Option.None are the qualified forms.
  8. flow.match.optional.bareA bare value binds the entire T?.
  9. flow.match.optional.bare-variantA bare None or Some(value) is an error. Error: bare-variant-pattern.
  10. flow.match.optional.nestedMatching T?? with .Some(value) removes only the outer layer, so value has type T?. .Some(.Some(value)) reaches the inner value.
  11. flow.match.optional.no-patternThere is no optional-specific pattern: value? is not a pattern.
  12. flow.match.contextualA .Variant pattern whose expected subject or nested payload type is not an enum is an error, as for .Some(value) against an i32 subject. Error: missing-contextual-enum-type.
Error example
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
  1. flow.match.payload.parenthesesEnum payload patterns use call-style parentheses.
  2. flow.match.payload.orderPositional patterns come first, and field=pattern names a payload field.
  3. flow.match.payload.requiredA pattern for a variant that carries a payload must list that payload, with one pattern per payload field.
  4. 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.
  5. flow.match.payload.unknownA named pattern that names no payload field of the variant is an error. Error: unknown-field.
  6. 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.
  7. 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.
  8. flow.match.payload.name-mismatch.exampleFor example, binding else_value in the then_value position emits variant-binding-name-mismatch.
  9. flow.match.literalA literal pattern requires an equal value.
  10. flow.match.variant.shorthandAn enum variant pattern may use .Variant when its subject or enclosing payload position fixes one enum type; otherwise it uses the qualified Enum.Variant form.
  11. flow.match.variant.shorthand-exhaustiveThe shorthand has the same exhaustiveness rules as the qualified form.
Error example
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-field

Note. Use named patterns to make an intentional reorder explicit.

#Payload Bindings

  1. flow.match.no-ownershipMatching does not transfer ownership.
  2. flow.match.bind.primitivePrimitive payloads bind by value.
  3. flow.match.bind.genericA payload declared with a generic parameter binds its substituted type.
  4. flow.match.bind.tupleTuple pattern elements retain their declared types.
  5. flow.match.literal-payloadA literal-constrained payload pattern such as Expr.Scale(value, factor=2) covers only that subset of the variant.
  6. flow.match.literal-payload.restAnother arm must therefore cover the remaining Scale values 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
  1. flow.match.nestedNested variant and tuple patterns are permitted by the grammar.
  2. flow.match.dataData destructuring patterns are also permitted.
  3. flow.match.data.bindUser { name } binds the name field, and User { name: alias } binds it as alias.
  4. flow.match.data.literalUser { age: 18 } matches only values with that field value.
  5. flow.match.data.unlistedUnlisted fields are ignored.
  6. 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.
  7. flow.match.data.fieldsListed fields must be distinct and exist on the named struct type.
  8. flow.match.data.fields.unknownA listed field that the struct type does not declare is an error. Error: unknown-field.
  9. 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 and w= both name the payload field w. Error: duplicate-data-pattern-field.
  10. flow.match.data.publicA cross-module pattern may name only public fields.
  11. flow.match.data.emptyAn empty data pattern is irrefutable for that struct type.
  12. 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.
  13. flow.match.data.bind-primitivePrimitive fields bind by value.
  14. flow.match.data.genericA generic field declared field: P binds as substituted P.
Error example
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"
  1. 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.
  2. flow.match.spread.bindThe spread pattern xs... binds xs to the rest element's List[T], so xs above is a List[i32]. The spread pattern _... binds nothing.
  3. flow.match.spread.arityThe subpatterns before a spread pattern must be as many as the type's fixed elements. Error: type-mismatch.
  4. 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.
  5. flow.match.spread.requiredA tuple pattern without a spread pattern against a tuple type with a rest element is an error. Error: type-mismatch.
  6. flow.match.spread.coverA spread pattern covers every list, so a tuple pattern that ends in one is irrefutable when its other subpatterns are.
Error example
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
    first

Note. A spread pattern stands wherever a tuple pattern does: in a let, a for loop or comprehension clause, and a match arm. 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
  1. flow.match.unitThe unit pattern () matches the unit value (), the one value of void.
  2. flow.match.unit.irrefutableIt is irrefutable for void, so .Ok(()) covers every success of a Result[void, E].
  3. flow.match.unit.bindsIt binds no name.
  4. flow.match.unit.typeA unit pattern against a value whose type is not void is an error. Error: type-mismatch.
Error example
fn read() -> Result[i32, string]:
    .Ok(1)

fn report() -> i32:
    match read():
        .Ok(()) => 0  # error: type-mismatch
        .Err(_) => 1

Why. 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
  1. 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.
  2. 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.
  3. flow.match.range.no-bindA range pattern binds no name and builds no range value.
  4. flow.match.range.coverA range pattern covers exactly the values it matches, by flow.match.cover.integer, so ..=-1, 0, and 1.. cover i8.
  5. flow.match.range.emptyA range pattern that matches no value, as 5..5 or 3..=1, is an unreachable arm. Error: unreachable-match-arm.
  6. flow.match.range.coveredAn arm whose every value earlier unguarded arms cover is unreachable, by flow.match.duplicate.covered. Error: unreachable-match-arm.
  7. flow.match.range.overlapArms that overlap only in part are not an error, and the first matching arm is selected.
Error example
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
  1. flow.let.matchA let pattern matches the initializer's value under the pattern rules of Match Expressions, as one unguarded arm would.
  2. flow.let.bindIts names bind as that arm's names would, as Payload Bindings and Nested And Data Patterns state.
  3. 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.
  4. flow.let.irrefutable.no-elseA let with an irrefutable pattern needs no else.
  5. flow.let.refutable.elseA let with a refutable pattern, such as a literal, an enum variant, or .Some(v), must have an else block. A refutable pattern without one is an error. Error: refutable-let-pattern.
  6. flow.let.else.orderA let-else evaluates its initializer once. When the pattern matches, it binds the names; otherwise the else block runs.
  7. flow.let.else.divergeThe else block must diverge: control never reaches its end.
  8. flow.let.else.diverge.formsA block diverges when its final statement has type never, such as return, break, continue, or a call to panic. It also diverges when the final statement is an if or match whose every branch diverges.
  9. flow.let.else.falls-throughAn else block that may complete normally is an error. Error: let-else-falls-through.
  10. flow.let.else.unreachableAn else block after an irrefutable pattern could never run, so it is an error. Error: unreachable-match-arm.
  11. flow.let.tuple-arityA tuple pattern needs a tuple of the same arity. Against any other value it is an error. Error: type-mismatch.
Error example
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 + high

Why. An else block that fell through would reach code that reads names the pattern never bound. Rust's let-else and Swift's guard let require the same divergence. An else that 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.

  1. flow.return.valuereturn expression immediately completes the nearest enclosing named function or closure with that value.
  2. flow.return.typeThe value must be assignable to the declared or inferred return type.
  3. flow.return.bareBare return is valid only for a void-returning function or closure.
  4. flow.return.outsidereturn outside a named function or closure is a compile-time error. Error: return-outside-function.
  5. flow.return.script-onlyA module script is not an implicit return target.
  6. flow.return.fallthroughFalling through a function body evaluates its final expression as the return value.
  7. flow.return.void-fallthroughA function declared -> void may fall through after a statement whose result is void.
  8. flow.return.callbackInside a trailing callback block, return completes the generated callback, not the function containing the call.
  9. flow.return.non-localhd-lang has no non-local return from a closure.
Error example
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

  1. flow.defer.registerdefer: registers a synchronous cleanup suite on the innermost executing lexical block.
  2. flow.defer.not-runReaching the statement does not run its suite.
  3. flow.defer.runRegistered suites run once in last-in, first-out order when that block exits normally, through return, break, or continue, or because postfix ? propagates.
  4. flow.defer.loopEach loop iteration has its own body block, so its registered suites run before the next iteration begins.
  5. flow.defer.result-savedA value-producing block evaluates and saves its result before running its registered suites.
  6. flow.defer.operandsLikewise, a return or break operand and a value being propagated by ? are evaluated before cleanup begins.
  7. 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

  1. flow.defer.scopesCleanup scopes are function and closure bodies, loop bodies, each selected if or else suite, and match arms. Provider scopes and trailing callback blocks, including test bodies, are cleanup scopes too.
  2. flow.defer.not-scopesModule top level and declaration bodies that do not execute are not cleanup scopes.
  3. flow.defer.outsideA defer there is an error. Error: defer-outside-cleanup-scope.
Error example
defer:   # error: defer-outside-cleanup-scope
    pass

#Cleanup Suite Restrictions

  1. flow.defer.voidA defer suite must produce void.
  2. flow.defer.restrictedIt cannot bang-call, otherwise suspend, propagate with ?, or transfer control with return, break, or continue.
  3. flow.defer.suspendA bang call or other suspending operation in the suite is an error. Error: suspension-forbidden-context.
  4. flow.defer.controlPropagation with ?, or a return, break, or continue that would leave the suite, is an error. Error: defer-control-flow.
  5. flow.defer.block-on.directA call of std.task.block_on or println written directly in the suite is an error. Error: suspension-forbidden-context.
  6. flow.defer.block-on.indirectSuch a call reached through another call while the suite runs panics, as req.drive.block-on.indirect says. Panic: suspension-forbidden-context.
Error example
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
    0

See also: Suspending Functions.

#Panics And Escaping Aliases

  1. flow.defer.panicA runtime panic does not run pending defer suites.
  2. flow.defer.cleanup-panicIf a cleanup suite itself panics, the instance is poisoned and no remaining cleanup suite is guaranteed to run.
  3. flow.defer.alias-escapedefer does 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
  1. flow.using.disposable-refstd.ops declares the trait Disposable as above, so dispose! changes the value in place. Code imports it to name it, as in use std.ops.Disposable; a using binding needs no import.
  2. flow.using.suspenddispose! is a bang method, so it may suspend. A type whose release never suspends still writes dispose!, and its body makes no suspending call.
  3. flow.using.best-effortdispose! returns void, so a release that fails reports nothing. A type whose release can fail offers a separate method that returns a Result, which code calls before the scope ends.

#Binding And Registration

  1. flow.using.formusing name = expression and using name: T = expression bind name to the value of expression for the rest of the suite.
  2. flow.using.bindingA using binding is not reassignable. Error: non-reassignable-binding.
  3. flow.using.disposable-required-refThe binding's type must implement Disposable. For a binding of a reference &mut T, T must implement it instead, or, for &mut dyn Tr, Tr must have it as a supertrait. Error: unsatisfied-trait-bound.
  4. flow.using.accessThe binding has its annotation's type, or else its initializer's type.
  5. 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 self call on a let mut binding does. No other &mut self call may change the binding. Error: not-writable.
  6. flow.using.referenceA binding of a reference &mut T disposes 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 Tr disposes through its trait value, by trait.dyn.ref.member.
  7. flow.using.reference.read-onlyA binding of a read-only reference, &T or &dyn Tr, is an error, since dispose! changes the value. Error: not-writable.
  8. flow.using.registerOnce the initializer is evaluated and bound, the statement registers a cleanup suite that calls name.dispose!(), exactly as a defer at that point would.
  9. 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, or continue, or because postfix ? propagates.
  10. flow.using.orderDisposes and defer suites of one block run in one last-in, first-out order.
  11. flow.using.loopEach loop iteration disposes the values that its body bound with using before the next iteration begins.
  12. flow.using.unboundA statement whose initializer does not complete, because it propagates with ? or panics, registers nothing.
  13. flow.using.scopesA using binding follows the cleanup scope rules of defer, so one at module top level is an error. Error: defer-outside-cleanup-scope.
  14. flow.using.bangThe registered call is a bang call written at the using statement. So a using binding outside a driver context is an error. Error: bang-call-outside-suspension.
  15. flow.using.in-deferA using binding inside a defer suite is a suspension in that suite. Error: suspension-forbidden-context.
  16. flow.using.suite-suspendsUnlike a written defer suite, a dispose suite may suspend, and the scope exit goes on once dispose! returns.
  17. flow.using.not-forbiddenA dispose suite is not a forbidden context of req.drive.block-on.indirect.
  18. flow.using.panicA runtime panic does not run pending disposes, as flow.defer.panic says of defer suites.
  19. flow.using.cancelCancellation unwinds a task as any exit does: its registered disposes run, in last-in, first-out order with its defer suites.
  20. 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.
  21. flow.using.cancel.cut-shortOnly abandon() and an await of a suspension cancelled elsewhere cut a dispose short, by req.abandon.dispose and req.abandon.dependency.
  22. 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.
Error example
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-suspension

Why. 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 the defer that Go code writes by hand after each open, made impossible to forget. Cancellation follows JavaScript's await using and C#'s await using with DisposeAsync: an abandoned task still awaits each disposal, and DisposeAsync takes no cancellation token.

Note. cancel!() on a stored suspension waits for a dispose! that suspends, by req.cancel.wait, and a cancel that arrives during a dispose takes it over, by req.cancel.takeover. abandon() stops a waiting dispose instead, and a handle the dispose owns needs a defer in the dispose that closes it. using does not stop an alias of the value from outliving its scope.

#Unreachable Code

This section defines unreachable statements.

  1. flow.unreachable.defStatements following an unconditional return, break, or continue in the same suite are unreachable.
  2. flow.unreachable.infinite-loopStatements following an infinite loop that no break targets, in the same suite, are unreachable.
  3. flow.unreachable.lintUnreachable code produces a lint finding without changing program validity. Warning: unreachable-code.
  4. flow.unreachable.checkedUnreachable code is still parsed and type-checked.
  5. 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.

  1. flow.panic.defA runtime panic is an abrupt, unrecoverable failure of the current program instance.
  2. flow.panic.not-resultIt is distinct from a recoverable Result error and is not catchable by core hd-lang source code.
  3. 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.
  4. flow.panic.stack-exhaustedExhausting the call stack panics. The report gives a source location when one is available, as flow.panic.report says. Panic: stack-exhausted.
  5. flow.panic.heap-exhaustedAn allocation that the heap cannot satisfy, within any heap limit the host sets, panics. Panic: heap-exhausted.
  6. 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

  1. flow.panic.stopWhen a panic occurs, ordinary evaluation stops immediately.
  2. flow.panic.nothing-runsNo enclosing else suite, remaining expression, or caller statement executes.
  3. flow.panic.runtime-cleanupRuntime and library mechanisms may observe the complete exit and perform mandatory runtime cleanup.
  4. flow.panic.no-handlerCore source has no panic handler or unwinding construct.
  5. flow.panic.reportThe runtime must report at least a stable failure category and source location when one is available.
  6. flow.panic.encodingIts Wasm trap, host error, and diagnostic encoding are ABI details.
  7. flow.panic.report.fallbackWhen an operation that panics with integer-overflow has a type from the usize default, the report must say so. The category stays integer-overflow.
  8. 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")
    n

Note. The report text is not normative. For let balance = 100 followed by balance = balance - 150, one such message is "balance fell back to usize (no signed literal); write +100 or let balance: i32 = 100".

#Program Instances

  1. flow.panic.instanceOne program instance is the instantiated module graph and execution state that Module Initialization defines.
  2. flow.panic.poisonA panic poisons that instance: the host must not invoke it again and must discard it after reporting the failure.
  3. 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

  1. 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, and time-limit.
  2. flow.panic.explicitThe prelude function panic(message: string) -> never explicitly causes an explicit-panic failure.
  3. 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")
    n

Why. never is assignable to every type.