Control & Errors
Statements & Control Flow
Every statement form the parser accepts: declarations, blocks, branches,
and the four loop shapes. Conditions require bool — there is
no truthiness anywhere in the grammar.
Type name = expr; var name = expr; // declaration (var/let infer)
Type name; // auto-constructed
using Type name = expr; // deterministic cleanup: see Errors & Resources
expr; // expression statement
{ stmt* } // block
return expr; => expr; // return (arrow form)
if (cond) stmt [else stmt]
while (cond) stmt
do stmt while (cond); // post-test: body runs before the first check
for (init; cond; step) stmt
for (T x in iterable) stmt // ranges, arrays, maps (Pair entries), any IIterable<T>
break; continue; // loop control
throw expr; // see Errors & Resources
try stmt catch (Type name?) stmt [catch ...] // see Errors & Resources
bind ...; uses NS; use NS::name (as alias)?; // imports: see Namespaces & Injection
; // emptyDeclarations & assignment
A declaration names a type and binds it: Type name = expr; for
an explicit type, or var/let to infer the type
from the initializer. var and let are pure
inference markers, not types — they differ only in mutability.
let is sugar for const var: a single-assignment
inferred binding. var stays freely reassignable.
int count = 0; // explicit type
var name = "Ada"; // inferred, reassignable
let id = nextId(); // inferred, single-assignment (const var)
A bare declaration with no initializer — Type name; —
auto-constructs to that type's default: string s;
is "", Array<T> a; is [],
int i; is 0. Auto-construction of arbitrary
object types is currently partial — primitives and arrays are covered.
string s; // ""
int i; // 0
Array<int> a; // []
const composes with either form (const int max = 50;,
const var limit = 100;) and fixes the slot after its
initialization window closes. See
Types & Generics for the full
const/readonly story.
Blocks & expression statements
Any expression followed by ; is a statement — most often a
call or an assignment. A block { stmt* } groups statements
and introduces its own scope; it is itself a valid statement anywhere one
is expected, including as the body of an if, loop, or
function.
log.info("starting"); // expression statement (a call)
count = count + 1; // expression statement (an assignment)
{
var tmp = compute();
use(tmp);
} // block: tmp is out of scope after thisA return exits the enclosing function, either as a full statement or the arrow shorthand used in expression-bodied members:
int square(int x) { return x * x; }
int square2(int x) => x * x;if / else
if (cond) stmt [else stmt]. cond must be
bool — Leviathan has no truthiness: an
int, a string, or an optional value cannot
stand in for a condition. Chained conditions are just an else
whose statement is another if.
if (n < 0) {
sign = -1;
} else if (n == 0) {
sign = 0;
} else {
sign = 1;
}Every construct that branches on a condition — if,
while, the do-while test, and the
C-style for middle clause — takes exactly a bool
expression. There is no implicit conversion from any other type, so
if (x) is only legal when x is already
bool. An if/while condition also
narrows a union tested with
!= None/== None/is in its
branches.
while
while (cond) stmt — a pre-test loop: cond is
checked before every iteration, including the first, so the body may run
zero times.
int i = 0;
while (i < 10) {
print(i);
i = i + 1;
}do-while
do stmt while (cond); — a post-test loop: the body runs once
unconditionally, then the loop continues while cond holds.
Unlike while, the body always executes at least once.
int attempts = 0;
do {
attempts = attempts + 1;
} while (attempts < 3 && !succeeded());
continue inside a do-while body
jumps to the condition check, not back to the top of the body — the same
target while uses, just reached from mid-body.
C-style for
for (init; cond; step) stmt — the classic three-clause loop.
init runs once before the loop; cond (must be
bool) is checked before every iteration; step
runs after each iteration, before the next cond check.
for (int i = 0; i < 10; i = i + 1) {
print(i);
}
continue in a C-style for jumps to step
(then re-checks cond) rather than skipping straight to the top
of the body.
for-in
for (T x in iterable) stmt iterates a Range, an
Array, a Map (yielding Pair entries),
or any type implementing IIterable<T>. for (var x
in e) infers the loop variable's type from what e
yields.
for (int i in 1..5) { print(i); } // range
for (string s in names) { print(s); } // array
for (Pair e in scores) { // map: entries as Pair<K,V>
print("${e.first} = ${e.second}");
}
The two prelude interfaces that make a user type for-in-able:
interface IIterator<T> { bool hasNext(); T next(); }
interface IIterable<T> { IIterator<T> iterator(); }
When e's static type implements IIterable<T>,
for (T x in e) stmt desugars to:
var __it = e.iterator();
while (__it.hasNext()) { T x = __it.next(); stmt }
break/continue behave exactly as in that
hand-written while — a continue re-checks
hasNext(). A type implementing neither a built-in collection
nor IIterable<T> is a compile error naming the protocol.
The checker actually picks a loop's dispatch path statically — a range
literal is a counted loop with no object allocated, and
Array/Map/Range take a fast
IterLen/IterAt path rather than routing through
the protocol — see
Collections & Iteration
for the full dispatch-order contract.
match
match (subject) { pattern => body; ... } dispatches on a
type pattern (Type =>, narrows the
subject in that arm), a value or range pattern
(0 =>, 1..9 =>), or else.
Arms are tried first-match-wins; a closed union is exhaustive without
else, while an open class hierarchy requires one.
match (m) {
Method::GET => handleGet();
Method::HEAD => handleHead();
Method::POST => handlePost();
} // exhaustive: closed union, no else needed
match (status) {
Status::OK => "ok";
else => "other";
}
match is a statement when its arms end
without a trailing ; after the block form, and an
expression when its arms yield a value — the same
match syntax works in either position. Bodies are either
=> expr; or => { block }. Under the hood
match lowers to the same is/type-dispatch test
used by catch and is — one dispatch path for the
whole language. Full arm-matching semantics and match as a
value-producing expression are covered in
Expressions & Operators;
this page only covers its shape as a control-flow statement.
match does not create a loop-nesting scope. A
break/continue inside a match arm
that itself sits inside a loop targets that enclosing loop, exactly as
if the match weren't there.
break & continue
break; exits the nearest enclosing loop
(while, do-while, C-style
for, for..in). continue; skips to
the next iteration — for while/do-while,
to the condition; for C-style for, to the step (then the
condition); for for..in, to the next element.
for (int i = 0; i < 100; i = i + 1) {
if (i % 2 == 0) { continue; } // next i
if (i > 50) { break; } // stop the loop
print(i);
}Both are unlabeled only — they always target the innermost enclosing loop, and using either outside of any loop is a compile error.
A bare break/continue inside a lambda never
reaches a loop in the enclosing function — it is legal only when
the lambda's own body has a loop of its own.
for (int i in 0..9) {
items.forEach((x) => {
// break; here would be a compile error —
// this lambda's body has no loop of its own.
});
}
match arms do not count as loop nesting either — see the
callout above.
Exceptions & using (preview)
Two more statement forms exist for control transfer and cleanup, but get full treatment on their own page:
throw expr; // expr must implement IException
try stmt catch (Type name?) stmt [catch ...] // first assignable clause wins
using Type name = expr; // deterministic cleanup on every exit path
throw is a control-transfer statement like return;
try/catch selects the first clause whose declared
type the thrown value is assignable to; using declares a
scope-owned IDisposable resource whose close()
runs on every way the declaring block can exit. See
Errors & Resources for the exception
hierarchy, catch-clause selection, and the full using
cleanup contract.