Skip to content

Statements

Block = "{" { Statement } "}" .
Statement = Binding | Assignment | If | While | For | Match
| Defer | Return | Break | Continue | Try | Raise | ExprStmt
| Region . // see /spec/comptime/
Binding = ( "let" | "var" ) BindTarget [ ":" Type ] "=" Expr
| identifier ":" Type "=" Expr // typed mutable local
| "let" BindTarget "=" Expr "else" Block // let-else
| "async" "let" identifier [ ":" Type ] "=" Expr
| ConstDecl .
BindTarget = identifier | "(" identifier { "," identifier } ")" . // tuple destructure
Assignment = Lvalue ( "=" | "+=" | "-=" | "*=" | "/=" | "%=" | "&=" | "|=" | "^="
| "<<=" | ">>=" | ">>>=" ) Expr .
Lvalue = identifier { "." identifier | "[" Expr "]" } .
If = "if" Expr Block { "else" "if" Expr Block } [ "else" Block ] .
While = "while" Expr Block .
For = [ "await" ] "for" BindTarget "in" Expr Block .
Defer = "defer" Block .
Return = "return" [ Expr ] .
Try = "try" Block CatchClause { CatchClause } .
CatchClause = "catch" [ "(" identifier [ ":" Type ] ")" ] Block . // name-first binding
Raise = "raise" identifier "(" [ ArgList ] ")" .
ExprStmt = Expr .

Conditions take no parentheses, and braces are mandatory on every block — there is no single-statement form. This removes the dangling-else and accidental-single-statement classes of bug at the grammar level.

A delimited region in statement position splices: the statements its template produced are peers of the ones around them, not a nested block. A name the template binds is therefore in scope for the statements that follow it.

  • Bindings and assignment specifies local binding forms, name: Type = expr, mutation, lvalues, compound assignment, and assignment evaluation order.

let binds an immutable name; var a mutable one; const a compile-time literal (folding rules and ES1011 in Declarations). The immutability of let is enforced by the compiler, not the CLR — a write to a let is a binding error, and a closure over a let may read but not assign it. A binding may destructure a tuple: let (a, b) = pair projects .Item1 / .Item2. The optional : Type ascription pins the binding’s type; without it the type is the initializer’s.

When a mutable local’s type is the point of the declaration, the field-ordered form is equivalent to var name: Type = Expr:

currentEnv: string = AppConfig.Environment
currentEnv = "development"

The colon distinguishes this declaration from assignment. The form is local-only, requires an explicit type, and declares a mutable binding; it does not change namespace let/var syntax.

Assignment targets a local, parameter, member chain (p.x, self.total), or index expression (xs[i]). The compound forms += -= *= /= %= &= |= ^= <<= >>= >>>= apply the corresponding binary operator to the current value and store back, on any assignable target.

async let name = init starts init concurrently at the declaration and awaits it at first reference; its shape and rules are in Concurrency → async let.

let user = db.find(id) else { return error(.notFound) }
// user is non-nil from here on

If the initializer evaluates to nil, the else block runs and shall leave the scope (return, break, continue, throw, or an infinite loop). After the statement the bound name is non-nil, so the happy path continues without a nesting level. An else block that falls through is ill-formed.

if / else if / else chains test boolean conditions; each arm is a block. while loops while its condition holds.

for … in iterates:

  • a collection (for v in xs, any IEnumerable<T>);
  • a range a..b, which is half-openfor i in 0..n runs 0 … n-1 (Expressions → ranges);
  • a channel (for v in ch), draining until the channel completes (Concurrency → channels).

A for target may destructure a tuplefor (k, v) in entries, for (items, errors) in results. break and continue apply to the nearest enclosing loop.

defer { … } registers its block to run on scope exit, in LIFO order — the last defer reached runs first. It runs on every exit path: normal completion, an early return, a break out of the scope, or an exception unwinding through it. It compiles to a CIL .try / .finally handler region over the rest of the block, and is E#‘s cleanup mechanism — there is no finally keyword. See Exceptions & cleanup → defer for the full semantics, including behavior under async:

defer { conn.close() } // runs however the scope is left
defer { log("done") } // runs first (LIFO), before conn.close()

match dispatches over a union, ref union, enum, or literal value, and is both a statement and an expression. Its grammar, payload binding, exhaustiveness, and lowering are specified completely in Pattern matching:

Match = "match" ( Expr | "(" Expr ":" Type ")" ) "{" { Arm } "}" .
Arm = Pattern Block | Pattern Expr .

A non-void function shall return on every path; a fall-through is ES2140. The analysis is terminator-aware: return, throw, and an infinite loop with no break are terminators, and a composite statement counts as returning when all of its exits do — an if/else whose every arm terminates, or an exhaustive match whose every arm terminates, satisfies the rule with no redundant trailing return. A non-exhaustive match (a literal match, or a case match missing variants) does not satisfy it on its own; it needs a default arm or a following return (Pattern matching → definite return).

try / catch / throw handle exceptions — the CLR’s mechanism for exceptional conditions, the complement to value-based Result flow: model expected failure as a Result, let exceptional conditions throw. The BCL throws, so the common pattern is to catch at that seam and translate into a Result. The binding is name-first, like every other E# declaration (name: Type):

catch (e: FormatException) { … } // typed + bound
catch (_: FormatException) { … } // typed, value discarded
catch (e) { … } // bound, any exception
catch { … } // bare — any exception, no binding

There is no finally (use defer inside the try). throw e raises an exception; throw with no operand rethrows the current exception and is valid only inside a catch.

raise Name(args) fires the field-style event Name declared on the enclosing class. It lowers to a thread-safe capture-then-invoke and is null-safe — a no-op when there are no subscribers, never a NullReferenceException. raise naming an event not declared on the enclosing type is ES2142. The full event surface — declaration, += / -=, and the emitted CLR shape — is in Delegates & events.