Statements
Grammar
Section titled “Grammar”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 destructureAssignment = 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 bindingRaise = "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.
In this specification
Section titled “In this specification”- Bindings and assignment specifies local binding forms,
name: Type = expr, mutation, lvalues, compound assignment, and assignment evaluation order.
Bindings and mutation
Section titled “Bindings and mutation”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.EnvironmentcurrentEnv = "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-else
Section titled “let-else”let user = db.find(id) else { return error(.notFound) }// user is non-nil from here onIf 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.
Control flow
Section titled “Control flow”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, anyIEnumerable<T>); - a range
a..b, which is half-open —for i in 0..nruns0 … n-1(Expressions → ranges); - a channel (
for v in ch), draining until the channel completes (Concurrency → channels).
A for target may destructure a tuple — for (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 leftdefer { 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 .return and definite return
Section titled “return and definite return”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
Section titled “try / catch / throw”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 + boundcatch (_: FormatException) { … } // typed, value discardedcatch (e) { … } // bound, any exceptioncatch { … } // bare — any exception, no bindingThere 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.