Syntax
Comments
Section titled “Comments”Line comments start with # and run to the end of the line:
# this is a commentlet x = 1 # trailing comments work tooVariables
Section titled “Variables”int age = 25float rate = 1.5bool active = truestring name = "Pluvial"auto count = 10 # inferred as intauto title = "Rain" # inferred as string-
Syntax:
<type> <name> = <initializer>. The initializer is required — there is no default initialization. -
With an explicit type, the initializer’s static type must match exactly.
int x = 1.5is a compile error. -
autoinfers the type from the initializer and then fixes it permanently.autois not dynamic:auto x = 10x = 20 # OKx = "hello" # compile error — type is fixed as int -
Reassignment requires a matching type. Assignment is a statement, not an expression:
y = x = 1is invalid. -
Redeclaring a name in the same scope is an error. Shadowing in nested scopes is allowed.
Operators
Section titled “Operators”Arithmetic: + - * / % **
Section titled “Arithmetic: + - * / % **”- Both operands must be the same numeric type (
int OP int → int,float OP float → float). - Mixing types (
int + float) is a compile error. No implicit promotion. string + stringconcatenates. Other arithmetic onstringis an error.- Integer division truncates:
10 / 4 → 2. Float division follows IEEE-754. - Integer division by zero is a runtime error (exit 70). Integer
%by zero is a runtime error. int ** intis repeated multiplication; a negative exponent is a runtime error.float ** floatcallspow().**is right-associative:2 ** 3 ** 2 == 2 ** 9 == 512.- Unary
-binds tighter than**, so-2 ** 4 == 16(this differs from Python).
Bitwise: & | ^ ~ << >> (int only)
Section titled “Bitwise: & | ^ ~ << >> (int only)”- All bitwise operators require
intoperands. Mixing withfloat/bool/string/nullable/etc. is a compile error:"bitwise '<op>' requires int operands". - Shifts are arithmetic (the sign of the left operand is preserved on right shift).
- Shift count
< 0is a runtime error. Shift count>= 49is a runtime error: Pluvial’sintis a 49-bit payload (see Types).
Comparison and equality
Section titled “Comparison and equality”< > <= >=require both operands to be the same numeric type. Mixed types,string, andboolare compile errors. Result isbool.== !=require the same type on both sides. Cross-type comparisons (int == float,"a" == 1) are compile errors.string == stringcompares contents.x == null/x != nullis allowed only whenxis nullable. Comparing a non-nullable value againstnullis a compile error ("'int' is never null; comparison is meaningless").
Logical: && || !
Section titled “Logical: && || !”- Operands and result are
bool. There is no truthiness conversion. &&and||short-circuit.- Logical and bitwise are distinct:
&&/||/!are bool-only,& | ~are int-only.
Unary: ! - ~
Section titled “Unary: ! - ~”!bool → bool,-int → int/-float → float,~int → int.
Result-state check: is
Section titled “Result-state check: is”r is okandr is errevaluate tobool.- The right-hand side is restricted to the keywords
okanderr. - The left-hand side must be a
Result. Anything else is a compile error. - See Types — Result for narrowing rules.
Precedence (low → high)
Section titled “Precedence (low → high)”|| → && → | → ^ → & → == != is →< > <= >= → << >> → + - → * / % → ** (right-assoc) →unary ! - ~ → . / [i] → grouping ( )Augmented assignment
Section titled “Augmented assignment”+= -= *= /= %= **= &= |= ^= <<= >>= are pure syntactic sugar for x = x op e. The base operator’s type rules and error messages apply unchanged.
There are three legal left-hand sides:
- A variable —
x += 1. The readonly check is the same as=, sofor-loop variables,matchbindings, andselfare still not assignable. - An array element —
a[i] += 1. The receiver and index are evaluated once —a[get_idx()] += 100callsget_idx()exactly once. - A struct field —
obj.field += 1, including nested paths likeo.inner.v += 1.
Augmented assignment on a map index (m[k] op= e) and chained forms (x += y += 1) are not currently supported.
Control flow
Section titled “Control flow”Braces are mandatory; conditions live in parentheses.
if (cond) { ...} else if (cond2) { ...} else { ...}
while (cond) { ...}
for item in arr { ...}
break # exit the innermost for/whilecontinue # next iteration of the innermost loop- Conditions must be
bool. There is no truthiness conversion. - Single-statement bodies without braces are not allowed.
for-in
Section titled “for-in”for-in walks arrays and maps:
array<int> a = [10, 20, 30]for x in a { println(x) # 10, 20, 30}
for (i, x) in a { # enumerate form: i is the 0-based index println(to_string(i) + ":" + to_string(x))}
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)) }}- The iterable is evaluated once.
- The loop variables are new read-only locals — assigning to them is a compile error.
- For arrays, iteration is index-ordered (
0toarr.length - 1). - For maps, iteration walks live hash-table slots; the order is not insertion order. The iteration takes a snapshot of
m.capacityat the start, so inserting or deleting inside the loop does not crash; deleted keys are skipped, and new keys placed outside the snapshot range are not visited. - The tuple form
for (X, Y) in ...dispatches on the static type of the subject: array → enumerate (int + T), map → key + value (K + V?).
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.
match expr { pattern => { body } pattern => { body } _ => { body } # wildcard, only as the last arm}The four pattern forms (16d):
- Literal — an
int,float,bool, orstringliteral. The subject must have the same base type. Duplicate literals are a compile error. ok(v)/err(e)— the subject must be aResult. ForResult<T>,eis bound asstring; forResult<T, E>,eis bound as the structE.nullor a bare identifier — the subject must beT?.nullmatches the absent case; a bare identifier is a binding pattern that matches when the value is present and binds the narrowed (non-null) value to a new read-only local._— wildcard. Must be the last arm.
The compiler enforces exhaustiveness:
| Subject | Required arms |
|---|---|
Result<T> / Result<T,E> | ok(_) AND err(_) (or _) |
bool | true AND false (or _) |
T? | null AND a catch-all (binding or _), or just _ |
int / float / string | _ required (infinite domain) |
struct / array / map | _ required (no destructuring yet) |
A non-exhaustive match is a compile error.
Flow typing (narrowing)
Section titled “Flow typing (narrowing)”Nullable values must be narrowed before they can be used as their base type. Pluvial recognises exactly two condition shapes (level B):
if (x != null) { ... }—xis narrowed toTinside thethenbranch.if (x == null) { ... } else { ... }—xis narrowed toTinside theelsebranch.
Result narrowing uses the same machinery, triggered by is ok / is err:
if (r is ok) { THEN } else { ELSE }—r.valueis accessible inTHEN,r.errorinELSE.if (r is err) { ... } else { ... }is the mirror.
Unlike nullable narrowing, Result is narrowed on both branches, since ok and err are the only two states.
Reassigning a narrowed variable cancels the narrowing. This is the soundness rule:
int? a = 5if (a != null) { int c = a + 1 # OK: a is int here a = null int d = a + 1 # compile error — a is int? again}