4. Grammar
This chapter defines the phrase grammar — the mapping from the token stream (§3) to program structure. Terminals are token kinds in UPPER_CASE (IDENT, INT, FLOAT, STR, CHAR, NEWLINE, INDENT, DEDENT) or the literal spellings of keywords and punctuation in double quotes. The notation is defined in §2.1.
The grammar defines *shape*, not *validity*: some productions accept forms that a later static rule rejects (for example, a value if without an else, §4.4). Such forms are flagged here and constrained in the semantic chapters. A program is valid only if it parses and satisfies every static-semantic rule.
4.1 Program and top-level declarations
A compilation input is a sequence of top-level declarations. Blank lines between them are insignificant.
Program ::= TopDecl*
TopDecl ::= PackageDecl | ImportDecl | ExternFn | ConstDecl
| Subscript | Struct | Enum | Handle | TypeDecl | Fn
The leader disambiguates: package, import, const, subscript (contextual identifiers, §3.7), extern (contextual, when not immediately followed by (), and the keywords struct, enum, handle, type each select their declaration; anything else begins a function.
PackageDecl ::= "package" IDENT NEWLINE
ImportDecl ::= "import" IDENT? STR NEWLINE
ConstDecl ::= "const" IDENT "=" ConstExpr NEWLINE
ImportDecl's optional IDENT is an alias for the imported package's prefix; the STR is the path (a core: prefix selects the corelib root). ConstExpr is an expression that folds to a single literal at compile time (integer arithmetic, bitwise, unary, and backward references to earlier top-level constants); its rules are given in §8 and §13. Package resolution, visibility, and merging are specified in §28.
4.1.1 Functions
Fn ::= "fn" IDENT "(" ParamList? ")" ( "->" Type )? WhereClause? ":" NEWLINE Block
ParamList ::= Param ( "," Param )*
Param ::= IDENT ":" ( "inout" | "sink" )? "..."? Type
A parameter is a name, a colon, an optional passing mode (inout for a copy-in/copy-out borrow, or the contextual sink for an owned/consumed parameter — §15), an optional ... marking a variadic parameter, and its type. A variadic parameter has type [T] (the call packs its trailing arguments into it) and MUST be the last parameter; it cannot also be inout or sink. A missing -> return type means the function returns nothing (void). A function MUST NOT return a handle (§25).
Type parameters are introduced *inside* Type by the $ sigil (§4.2); a function that mentions any $T type parameter or $N size parameter in its signature is generic. A WhereClause constrains a generic function:
WhereClause ::= "where" Constraint ( "," Constraint )*
Constraint ::= Predicate "(" IDENT ")"
| IDENT ":" Type ( "|" Type )*
Predicate ::= "numeric" | "comparable" | "has_str" | "hashable" | "defaultable"
The predicate set is closed (exactly the five names above; an unknown predicate is rejected) — see §7 for constraints. The type-set form T: A | B | … constrains T to one of a listed set (up to 16 types). A where clause requires a generic function; at most 8 constraints are allowed.
4.1.2 Structs, enums, newtypes, handles
Struct ::= "packed"? "struct" IDENT TypeParams? ":" NEWLINE INDENT FieldDecl+ DEDENT
FieldDecl ::= IDENT ":" Type NEWLINE
Enum ::= "enum" IDENT TypeParams? ":" NEWLINE INDENT VariantDecl+ DEDENT
VariantDecl::= IDENT ( "(" Type ( "," Type )* ")" )? NEWLINE
Handle ::= "handle" IDENT ":" NEWLINE INDENT "free" ":" IDENT NEWLINE DEDENT
TypeDecl ::= "type" IDENT "=" Type NEWLINE
TypeParams ::= "(" "$" IDENT ( "," "$" IDENT )* ")"
A struct has one or more fields; an enum has one or more variants, each with an optional payload of up to 8 types. A handle names an opaque FFI resource and its C free function (§25). A type declaration introduces a distinct newtype over an underlying type; the permitted underlying types are constrained in §5. Recursion through a struct field is permitted only via a container (e.g. [Node]), never as a direct by-value self-field (§17).
4.1.3 Extern functions and subscripts
ExternFn ::= "extern" STR? "fn" IDENT "(" ExternParamList? ")" ( "->" Type )? NEWLINE
Subscript ::= "subscript" IDENT "(" ParamList? ")" "->" "inout" Type ":" NEWLINE
INDENT "yield" "&" Place NEWLINE DEDENT
An ExternFn is bodyless and binds to a C symbol; its optional STR names a link library. Its parameter and return types are restricted to the FFI-crossable set — the scalars (int, char, float, bool), string, bytes, ptr, [int]/[float], typed handles, the first-class fixed-width numerics u8…u64 / i8…i64 / f32, plus inout scalar out-parameters (not string) and an Option(string) return (§24).
A Subscript declares a user-defined projection: it yields a place (an lvalue) rooted in one of its parameters. Its rules — the place must be rooted in a parameter, each parameter used at most once — are given in §18. Place is defined in §4.4.
4.2 Types
Type ::= "$" IDENT /* type parameter */
| "soa" "[" Type "]" /* struct-of-arrays */
| "bounded" "[" ( INT | IDENT ) "]" Type /* bounded[N]T / bounded[C]T, C an int const */
| "vector" "[" ( INT | IDENT ) "]" Type /* vector[N]T, N a power of two 2..64 */
| "fn" "(" ( Type ( "," Type )* )? ")" ( "->" Type )?
| "(" Type ( "," Type )+ ")" /* tuple, 2..8 elements */
| "[" ArrayOrMap
| "Option" "(" Type ")"
| "Result" "(" ( Type | "void" ) "," Type ")" /* void: ok payload only */
| "Channel" "(" Type ")"
| QualName TypeArgs? /* struct/enum/newtype/handle */
| PrimType
ArrayOrMap::= "$" IDENT "]" Type /* [$N]T size-parameter array */
| INT "]" Type /* [N]T fixed-size array */
| IDENT "]" Type /* [C]T fixed size named by an int const */
| Type "]" /* [T] dynamic array */
| Type ":" Type "]" /* [K:V] map */
TypeArgs ::= "(" Type ( "," Type )* ")"
QualName ::= IDENT ( "." IDENT )? /* Name or pkg.Name */
PrimType ::= "int" | "float" | "bool" | "string" | "ptr" | "bytes"
| "u32" | "u64" | "f32"
Notes (constrained further in §5–§7):
- A
$IDENTafter[denotes a size parameter ([$N]T, a const-generic array) — and is distinguished from a dynamic[$T](type-parameter element) by whether a *type* follows the]. A dynamic[$T]return followed by awhereclause is disambiguated becausewhereis not a type. - A
[N]Tor[C]T(integer literal, or anintconst name, before]followed by an element type) is a fixed-size array; a bare[T]is a dynamic array. bounded [N] Tis the inline fixed-capacity collection of §5.3.10. Its capacity MUST denote a positive integer: either an integer literal, or — exactly as a fixed-size[C]Tadmits — the name of anintconst(§5.3.10).boundedis *contextual*: it is a type constructor only when an[follows, and remains usable as an ordinary identifier elsewhere.vector [N] Tis the SIMD-lowered fixed array of §5.3.11. Its count MUST be a power of two between 2 and 64, written as an integer literal or the name of anintconst, and its element MUST beint,floatorf32.vectoris *contextual* in exactly the wayboundedis: it is a word only in type position and stays a legal identifier everywhere else.- A tuple type has 2–8 elements. A function type has up to 8 parameters. No element or parameter may be
void. QualName TypeArgsapplies a generic struct or enum to concrete type arguments (Box(int)); applying a generic to exactly its own type parameters is a recursive self-reference.- There is no
charorvoidtype spelling (§3.6).
4.3 Blocks and statements
A block is a newline-introduced, indentation-delimited sequence of statements:
Block ::= INDENT Stmt+ DEDENT
Stmt ::= ConstStmt | DeleteStmt | Return | Break | Continue | PassStmt
| Select | Match | If | ParallelFor | For
| MultiDecl | MultiAssign | SimulAssign
| Decl | TypedDecl | Assign | PlaceAssign | CompoundAssign
| ExprStmt
4.3.1 Simple statements
ConstStmt ::= "const" IDENT "=" ConstExpr NEWLINE
DeleteStmt ::= "delete" Postfix NEWLINE /* Postfix MUST be an index m[k] */
Return ::= "return" ( Expr ( "," Expr )* )? NEWLINE
| "return" ValueCtrl
Break ::= "break" NEWLINE
Continue ::= "continue" NEWLINE
PassStmt ::= "pass" NEWLINE /* the no-op; contextual, see App. B */
Decl ::= IDENT ":=" ( Expr | ValueCtrl ) NEWLINE
TypedDecl ::= IDENT ":" Type "=" ( Expr | ValueCtrl ) NEWLINE
Assign ::= IDENT "=" ( Expr | ValueCtrl ) NEWLINE
MultiDecl ::= IDENT ( "," IDENT )+ ":=" Expr NEWLINE
MultiAssign ::= IDENT ( "," IDENT )+ "=" Expr NEWLINE
SimulAssign ::= "(" Place ( "," Place )+ ")" "=" Expr NEWLINE
PlaceAssign ::= Place "=" ( Expr | ValueCtrl ) NEWLINE
CompoundAssign ::= Place CompoundOp "=" Expr NEWLINE
CompoundOp ::= "+" | "-" | "*" | "/" | "%" | "&" | "|" | "^" | "<<" | ">>"
ExprStmt ::= ( Call | Call "or_return" | Call OrElse ) NEWLINE /* or_return/or_else form: ok payload must be void */
Decl(x := e) declares a variable and infers its type;TypedDecl(x: T = e) declares with an explicit type;Assign(x = e) assigns to an existing variable.MultiDecl/MultiAssigndestructure a tuple-valued RHS into 2–8 names.SimulAssign((a, b) = (b, a)) assigns a tuple-valued RHS into 2–8 places, not just names, and every right-hand side is evaluated before any target is written (§17.5). It is a statement of its own: it MUST NOT appear as the init or post clause of a three-clausefor.PlaceAssignandCompoundAssignwrite through a place (§4.4): an index, a field, a tuple element, or a user-subscript call. InCompoundAssign, a side-effecting call inside the place is evaluated once (hoisted); a pure index sub-expression may be evaluated twice, as writing the form out longhand would (this single-vs-double evaluation is specified in §13).DeleteStmtremoves a map element; thePostfixMUST be an indexm[k].- The valid bare-expression statements are a call, and a call followed by
or_returnor anor_elseclause when the callee's ok payload isvoid(§5.3.6). A bare variable, index or field expression is rejected as having no effect, and so is anor_returnover any other payload type — that one would silently drop the value. The parser accepts the shape and the resolver enforces the payload type, because only the resolver knows it. ConstExprandValueCtrlare defined in §4.1 and §4.3.2.
4.3.2 Compound statements
If ::= "if" Expr ":" NEWLINE Block
( "elif" Expr ":" NEWLINE Block )*
( "else" ":" NEWLINE Block )?
Match ::= "match" Expr ":" NEWLINE INDENT MatchArm+ DEDENT
MatchArm ::= Pattern ":" NEWLINE Block
Pattern ::= VariantName ( "(" IDENT ( "," IDENT )* ")" )? /* variant, with 0..8 bindings */
| VariantName "(" SubPattern ")" /* Ok/Err/Some: one nested level */
| ScalarPattern /* int/char/bool subject */
| "_" /* wildcard */
VariantName ::= IDENT | IDENT "." IDENT /* Variant or pkg.Variant */
SubPattern ::= VariantName ( "(" IDENT ( "," IDENT )* ")" )?
ScalarPattern ::= ScalarElem ( ".." ScalarElem )? ( "|" ScalarElem ( ".." ScalarElem )? )*
ScalarElem ::= "-"? ( INT | CHAR | TRUE | FALSE ) | IDENT /* IDENT: an int constant */
For ::= "for" ForInit ";" Expr ";" ForPost ":" NEWLINE Block /* three-clause; all three REQUIRED */
| "for" IDENT "in" Expr ":" NEWLINE Block /* foreach */
| "for" Expr ":" NEWLINE Block /* condition */
| "for" ":" NEWLINE Block /* infinite */
ForInit ::= Decl | TypedDecl | Assign | CompoundAssign /* the ';' supplies the clause's NEWLINE */
ForPost ::= Assign | CompoundAssign /* the ':' supplies it; target MUST be a variable */
ParallelFor ::= "parallel" Width? "for" IDENT "in" "0" "..<" Expr ":" NEWLINE Block /* counting; the `0` is a literal */
| "parallel" Width? "for" IDENT "in" IDENT ":" NEWLINE Block /* foreach; the source MUST be a name */
Width ::= "(" Expr ")" /* fan-out width, 1..64; omitted = ncpu() */
Select ::= "select" ":" NEWLINE INDENT SelectArm+ DEDENT
SelectArm ::= "recv" "(" Expr "," IDENT ")" ":" NEWLINE Block
| "default" ":" NEWLINE Block
| "closed" ":" NEWLINE Block
ValueCtrl ::= If | Match /* value form: block branches ending in a value expression, tail position */
elifis sugar for anelseblock containing a single nestedif.- The four
forshapes are: three-clause (for init; cond; post:), foreach (for x in collection, over an array or a string's bytes), condition (for cond:, the while-loop form), and infinite (for:).break/continueare valid in every shape and only inside a loop. - In the three-clause form the
;and the block:stand in for theNEWLINEthatDecl/TypedDecl/Assign/CompoundAssignwould otherwise end with: the clauses are parsed by the ordinary statement parser, so a typed init (i: int = 0) and every compoundop=form come along without a second copy of the assignment grammar. All three clauses are required — this grammar has no empty-statement production to put in one — andfor:is the only degenerate form. Acontinueruns the post clause (§14.4). - There is no counting form.
for i in range(a[, b[, step]])and the contextual namerangewere removed on 2026-07-29 (§14.4); aforhead that namesrangeis refused with its replacements...<appears in exactly one production,ParallelFor— a sequentialfor i in 0..<N:is refused (§22). parallel forapplies only to the0..<Ncounting form or the foreach form, and its foreach source must be a bare name, not an expression (§22).matchis exhaustive; a bare variant name is a nullary-variant pattern, a parenthesized list binds a variant's payload,pkg.Variantmatches a qualified variant, and_is the wildcard (§19).ValueCtrlis the value-producing form ofif/match: it may appear only as the tail of a declaration (:=, typed=), an assignment, a place assignment, or areturn. Each branch/arm is an indented block whose final statement is a value expression (a bare value expression is the one-statement case), or a diverging call —die/exit, which produces no value because control leaves the program (§29.12.1); a valueifMUST have anelse; a valuematchMUST be exhaustive; all non-diverging branches MUST unify to one type, and at least one branch MUST be non-diverging. These rules are given in §13/§14.
4.4 Expressions
The expression grammar encodes precedence and associativity by nesting: each level parses the next-tighter level and then, left-associatively, its own operators (except the prefix levels, which are right-recursive).
Expr ::= OrExpr
OrExpr ::= AndExpr ( "or" AndExpr )*
AndExpr ::= NotExpr ( "and" NotExpr )*
NotExpr ::= "not" NotExpr | CmpExpr
CmpExpr ::= IsExpr ( ( "==" | "!=" | "<" | ">" | "<=" | ">=" | "in" ) IsExpr )*
IsExpr ::= AddExpr ( "is" VariantName )? /* variant test; does NOT chain */
AddExpr ::= MulExpr ( ( "+" | "-" | "|" | "^" ) MulExpr )*
MulExpr ::= UnaryExpr ( ( "*" | "/" | "%" | "<<" | ">>" | "&" ) UnaryExpr )*
UnaryExpr ::= ( "-" | "&" | "~" ) UnaryExpr | Postfix
Postfix ::= Primary PostfixOp* ( "or_return" | OrElse )? "..."?
OrElse ::= "or_else" IDENT ":" Expr /* IDENT or `_`; returns Expr on failure (§14.6.1) */
PostfixOp ::= "[" Expr "]" /* index */
| "[" Expr? ":" Expr? "]" /* slice (either bound optional) */
| "." IDENT /* field access */
| "." INT /* tuple index */
| "." "(" Component ( "," Component )* ")" /* swizzle, 2-8 components (§17.5) */
| "." IDENT CallTypeArgs? "(" ArgList? ")" /* qualified pkg call (on a bare identifier) */
| "." IDENT CallTypeArgs /* ... with no value arguments */
| "(" ArgList? ")" /* call */
ArgList ::= Expr ( "," Expr )*
Component ::= IDENT | INT /* a struct field, or a lane of an array / bounded / vector */
The place grammar (assignable lvalues, §4.3.1) is the subset of Postfix whose spine is an index, field, tuple-index, or subscript call rooted in a variable:
Place ::= IDENT ( "[" Expr "]" | "." IDENT | "." INT | "(" ArgList? ")" )*
Primary expressions:
Primary ::= INT | FLOAT | STR | CHAR
| "true" | "false" | "null"
| "(" Expr ")" /* grouping */
| "(" Expr ( "," Expr )+ ")" /* tuple literal, 2..8 */
| ArrayOrMapLit /* [ ... ] forms (§16, §18) */
| "soa" "[" "]" Type /* empty soa literal */
| "channel" "(" Type "," Expr ")" /* only as a declaration RHS */
| "spawn" Call /* spawn a direct call */
| Lambda
| "None" | "Some" "(" Expr ")" | "Ok" "(" Expr? ")" | "Err" "(" Expr ")"
| IDENT CallTypeArgs ( "(" ArgList? ")" )? /* explicit type args */
| IDENT "(" ArgList? ")" /* named call */
| IDENT /* variable, or nullary enum variant */
CallTypeArgs ::= "$" "(" Type ( "," Type )* ")" /* at least one; `f$()` is an error */
Lambda ::= "fn" "(" LambdaParams? ")" ( "->" Type )? ":" Expr
LambdaParams ::= IDENT ( ":" Type )? ( "," IDENT ( ":" Type )? )*
A lambda parameter's type is optional (inferred from the expected function type when omitted, §6.2), and a lambda parameter may not carry inout, sink, or ... — so a lambda uses LambdaParams, not the ParamList of §4.1.1. Call in ExprStmt and after spawn denotes a Postfix whose outermost operation is a call (...). A Subscript (§4.1.3) parameter is likewise plain IDENT ":" Type (no passing modes).
- The array/map literal forms (
[e, …],[k: v, …], empty typed[]T/[]K:V) and the pending bare[]/Nonegrounding are detailed in §16/§18
.
- A lambda's body is a single expression. A closure captures by deep copy at creation (§13).
IDENT $ ( Type … )supplies explicit type arguments to a generic call (e.g.zero$(int)); it is the only use of$in expression position.spawnandchannel(…)are restricted in where they may appear (§23):spawntakes a direct call;channel(T, cap)is legal only as the direct right-hand side of a declaration.- Assignment (
=), declare-and-infer (:=), and compound assignment are statement-level (§4.3.1); they are not expression operators and never appear insideExpr.
4.5 Operator precedence and associativity
From tightest-binding to loosest. Every binary level is left-associative; the prefix levels are right-associative.
| Precedence | Operators | Kind | Assoc | |
|---|---|---|---|---|
| 1 (tightest) | [] index/slice · . field/tuple-index · call · or_return · or_else · ... | postfix | left | |
| 2 | - & ~ | unary prefix | right | |
| 3 | * / % << >> & | binary | left | |
| 4 | + - `\ | ^` | binary | left |
| 5 | is | binary, non-associative | — | |
| 6 | == != < > <= >= in | binary | left | |
| 7 | not | unary prefix | right | |
| 8 | and | binary | left | |
| 9 (loosest) | or | binary | left |
This is Go's precedence: the shift operators (<<, >>) and bitwise-AND (&) bind at the multiplicative level, and bitwise-OR/XOR (|, ^) bind at the additive level — so every bitwise operator binds *tighter* than any comparison. Consequently a & b == c parses as (a & b) == c. The unary & (address-of / inout argument) and binary & (bitwise-AND) share a spelling but occupy different precedence levels (2 and 3). is is non-associative: its right operand is a variant name rather than an expression, so a is X is Y does not parse (§19.8). and/or short-circuit (§13). Evaluation order within a precedence level, and the order of argument and place sub-expression evaluation, is specified in §13 — the grammar fixes only associativity, not side-effect order.