Control flow
Pluvial uses C-style control flow: conditions go in parentheses, bodies are wrapped in mandatory braces, and there are no brace-less single-statement forms.
if (cond) { ...} else if (cond2) { ...} else { ...}
while (cond) { ...}
for item in arr { ...}- Conditions must be of type
bool— there is no truthiness conversion (if (5)is a compile error). - Braces are required.
Block scope
Section titled “Block scope”- A block
{ }is a new lexical scope; variables declared inside are not visible outside. - Shadowing is allowed: an inner block may redeclare a name from an outer scope as a new variable that covers the outer one until the block ends.
- Redeclaring in the same scope is an error.
for-in
Section titled “for-in”for-in walks a collection’s elements in order. The iterables are array<T> and
map<K,V> (and strings, via for c in "hello").
array<int> a = [10, 20, 30]for x in a { println(x) # 10, 20, 30}
for (i, x) in a { # enumerate: i is the 0-based index println(to_string(i) + ":" + to_string(x))} # 0:10, 1:20, 2:30
map<string, int> m = {"x": 10, "y": 20}for k in m { println(k) # keys only}for (k, v) in m { if (v != null) { println(k + "=" + to_string(v)) }}Common rules:
- The iterable type is checked at compile time; a non-iterable (an
int, aResult, etc.) is a compile error. - Loop variables are new read-only locals whose types are inferred. Assigning to a
loop variable is a compile error (
"cannot assign to for-loop variable 'x'"). This applies to the enumerate indexitoo. - The iterable is evaluated exactly once, including any side effects.
For arrays, iteration runs from index 0 to arr.length - 1. The tuple form for (i, x) in arr is enumerate: i is a 0-based read-only int and x is the element.
For maps, iteration walks only live hash-table slots, in slot order (not insertion
order). The loop snapshots m.capacity at the start, so inserting or deleting inside the
body does not crash: deleted keys are skipped, and keys added beyond the snapshot range are
not visited.
The tuple form (X, Y) is interpreted by the static type of the subject, not by the
names: an array yields (index, element) and a map yields (key, value).
break and continue
Section titled “break and continue”breakimmediately exits the innermostfor/while.continuejumps to the next iteration of the innermost loop.- Using
break/continueoutside a loop is a compile error.
Labeled loops
Section titled “Labeled loops”A loop may carry a label, and break / continue may target it:
outer: while (cond) { while (inner) { break outer # exit the labeled loop continue outer # continue the labeled loop }}A break LABEL / continue LABEL that names no enclosing loop is a compile error.
match matches a value against patterns and runs the first matching arm. It is a
statement, not an expression; arms do not fall through, and bodies must use braces. The
subject is evaluated once.
match expr { pattern => { body } pattern => { body } _ => { body } # wildcard, only as the last arm}Pattern forms
Section titled “Pattern forms”- Literal — an
int,float,bool, orstringliteral. The subject must have the same base type; strings compare by content. A duplicate literal is a compile error. OR patterns are supported:1 | 2 => { ... }. ok(v)/err(e)— the subject must be aResult. ForResult<T>,v : Tande : string; forResult<T, E>,v : Tande : E(a struct, soe.fieldande.method()work).nullor a bare identifier — the subject must beT?.nullmatches the absent case; a bare identifier is a binding pattern that fires when the value is present and binds the narrowed non-nullTto a new read-only local._— wildcard, matches anything, must be the last arm.
Tuple patterns are also supported, including typed bindings and nesting:
match pair { (1, _) => { ... } (int x, string s) => { ... } # typed bindings (1, (int a, int b)) => { ... } # nested}Enum variants are matched too:
match shape { Shape.Circle(r) => { println(r) } Shape.Rectangle(w, h) => { println(w * h) } Shape.Point => { println("point") }}All bindings are read-only; reassigning one is a compile error.
Exhaustiveness
Section titled “Exhaustiveness”The compiler enforces exhaustiveness at compile time:
| Subject | Required arms |
|---|---|
Result<T> / Result<T,E> | ok(_) AND err(_) (or _) |
bool | true AND false (or _) |
T? (nullable) | null AND a catch-all (binding or _), or just _ |
int / float / string | _ required (infinite domain) |
| enum | every variant (or _) |
struct / array / map | _ required (no destructuring of these yet) |
A non-exhaustive match is a compile error (exit 65) — for example, forgetting the err
arm of a Result or the false case of a bool.
Flow typing (narrowing)
Section titled “Flow typing (narrowing)”A nullable value must be narrowed before it can be used as its base type. Pluvial recognises exactly two condition shapes:
if (x != null) { ... }—xis narrowed toTinside thethenbranch.if (x == null) { ... } else { ... }—xis narrowed toTinside theelsebranch.
int? a = 5if (a != null) { int b = a + 1 # OK: a is int inside this branch}# outside the block, a is int? againResult narrowing uses the same machinery, triggered by is ok / is err:
Result<int> r = parse(-3)if (r is ok) { int v = r.value # OK — narrowed to ok} else { string m = r.error # OK — else branch is the err state}Unlike nullable narrowing (which narrows only one branch), a Result is narrowed on
both branches, since ok and err are the only two states. .value is accessible only
under an ok-narrowing and .error only under an err-narrowing.
The recognised shapes are limited to x != null / x == null and r is ok / r is err
with a single variable — compound conditions, negation, and early-return patterns do not
narrow.
Reassignment cancels narrowing
Section titled “Reassignment cancels narrowing”Assigning to a narrowed variable returns it to its declared (nullable) type from that point on. This is the soundness rule that keeps null safety honest:
int? a = 5if (a != null) { int c = a + 1 # OK: a is int a = null int d = a + 1 # compile error — a is int? again}