Control flow & match
Control flow is conventional where it can be and expressive where it counts. No parentheses around conditions; braces are mandatory.
if / else
Section titled “if / else”if x < 0 { return 0 - x}
if a > b { return a} else if b > c { return b} else { return c}while i <= n { total += i i += 1}
for i in 0..n { // range buffer[i] = 0}
for item in collection { // any iterable process(item)}
for event in ch { // a channel, until it closes handle(event)}for..in also destructures tuples — and a Dictionary<K, V>, whose KeyValuePair<K, V>
elements unpack into the key and value (each typed, so key.Length resolves):
for (key, value) in scores { // scores: Dictionary<string, int> log("{key}: {value}")}match is where choice, ref choice, and enum pay off. Each arm names a variant with
.case, optionally binding its payload.
choice ConnState { disconnected connecting connected(sessionId: int) failed(reason: string)}
func describe(s: ConnState) -> string { match s { .disconnected { return "disconnected" } .connecting { return "connecting" } .connected(sid) { return "connected:{sid}" } .failed(reason) { return "failed:{reason}" } }}Multi-payload cases bind positionally (.message(lvl, txt)), and you can ignore a payload with
_. There’s also a single-binding case view: bind the whole case to one name and reach its
payloads by field — .pair(p) then p.a, p.b — which composes with positional binding so you
pick whichever reads better.
For a single-payload case the case view is transparent: the bound name is the payload
value, and name.field still resolves to the same thing — so .connected(sid) lets you use sid
as the int directly. This holds for value choice and ref choice alike:
ref choice Json { jbool(value: bool) jarr(items: List<Json>)}
match j { .jbool(b) { return b ? "true" : "false" } // b IS the bool .jarr(items) { return "[{items.Count}]" } // items IS the List}It’s pure sugar — .jbool(b) is exactly .jbool(c) reaching c.value, both lowering to the same
payload load. (A multi-payload case has no single value to unwrap to, so it keeps the named view:
.add(n) then n.left / n.right.)
ref choice matches the same way (it uses an isinst type check under the hood):
ref choice Expr { literal(value: int) add(left: Expr, right: Expr) neg(inner: Expr)}
func eval(e: Expr) -> int { match e { .literal(v) { return v } .add(l, r) { return eval(l) + eval(r) } .neg(inner) { return 0 - eval(inner) } }}match is exhaustive
Section titled “match is exhaustive”The compiler warns when a match over a choice/ref choice/enum misses a variant. Combined
with the every-path-must-return rule, an exhaustive match where every arm returns is a complete
function body — no trailing return needed. Add default { } as the catch-all when you want it.
Literal patterns
Section titled “Literal patterns”match also dispatches on int, string, and bool literals:
match status { 200 { return "ok" } 404 { return "not found" } default { return "unknown" }}(Literal universes are open, so default is how you stay total — exhaustiveness checking doesn’t
apply here.)
match is an expression
Section titled “match is an expression”A match can appear anywhere an expression can — a let, a return, an interpolation, an
expression-bodied function. Every arm must produce the same type.
let label = match status { 200 { "ok" } 404 { "not found" } default { "other" }}
func describe(r: Result<int, string>) -> string = match r { .ok(v) { "value={v}" } .err(e) { "err:{e}" }}defer { ... } schedules cleanup that runs when the scope exits (it lowers to try/finally).
Multiple defers run in LIFO order.
let handle = openFile(path)defer { handle.Close() } // runs on the way out, however the scope exitsreturn
Section titled “return”Standard return. The thing to remember is the definite-return rule: every path of a
non-void function must return, and the analysis understands exhaustive match and if/else
where both branches return, plus while true { ... } with no break. Correct exhaustive code
needs no filler trailing return.