Documentation
Language reference
The precise rules behind the language guide. This documents the language as implemented by the toolchain today.
Lexical structure
Identifiers
- Lower names (
model,viewItem2): lowercase letter, then letters/digits/underscores. Bind values, functions, fields, type variables. - Upper names (
Model,Increment): uppercase start. Name modules, types, and constructors. - Qualified names:
String.fromInt(module + value),Maybe.Just(module + constructor). Resolved by the module system. _alone is the wildcard pattern.
Keywords
module exposing import as type alias let in case of if then else
Literals
| Form | Examples | Notes |
|---|---|---|
| Int | 42, 0xFF | 64-bit |
| Float | 3.14, 6.02e23, 1.0e-9 | 64-bit IEEE |
| String | "hi\n", """multi line""" | escapes: \n \t \r \" \\ \u{XXXX}; single-quoted strings cannot span lines |
| Char | 'a', '\n', '\u{1F330}' | one extended grapheme cluster |
| Unit | () |
Comments
-- to end of line; {- ... -} nesting block comments.
Field access vs accessor functions
A . glued to the end of an expression is field access (model.count, (f x).name); a . starting a token is an accessor function (.count, usable anywhere a function is). The distinction is made lexically by adjacency.
Layout (the offside rule)
There are no braces or semicolons. The parser enforces:
- Top level: every declaration starts at column 1; every continuation line of a declaration is indented past column 1.
- Expressions: a token at column ≤ the governing indentation ends the current expression.
case e of: the first token afteroffixes the branch column. Every branch pattern must start exactly at that column; every branch body must stay right of it.let: the first binding fixes the binding column; sibling bindings align with it;inends the block.- Inside
( ),[ ],{ }the layout rule is suspended — line up multiline lists and records however you like.
Expressions
Grammar sketch (EBNF-ish; lower/upper are identifier classes):
expr ::= lambda | ifExpr | letExpr | caseExpr | opExpr
lambda ::= '\' pattern+ '->' expr
ifExpr ::= 'if' expr 'then' expr 'else' expr
letExpr ::= 'let' (lower pattern* '=' expr)+ 'in' expr
caseExpr ::= 'case' expr 'of' (pattern '->' expr)+
opExpr ::= app (operator app)* -- by precedence table
app ::= atomSuffix+ -- left-assoc application
atomSuffix ::= atom ('.' lower)* -- field access
atom ::= literal | lower | upper | qualified | accessor
| '-' atomSuffix -- negation
| '(' ')' | '(' operator ')' | '(' expr (',' expr)* ')'
| '[' (expr (',' expr)*)? ']'
| record
record ::= '{' '}'
| '{' lower '|' (lower '=' expr) (',' lower '=' expr)* '}'
| '{' (lower '=' expr) (',' lower '=' expr)* '}'
Operator precedence
Higher binds tighter:
| Prec | Operators | Associativity |
|---|---|---|
| 9 | << | right |
| 9 | >> | left |
| 8 | ^ | right |
| 7 | * / // | left |
| 6 | + - | left |
| 5 | ++ :: | right |
| 4 | == /= < > <= >= | none |
| 3 | && | right |
| 2 | || | right |
| 1 | |> | left |
| 1 | <| | right |
Function application binds tighter than all operators. && and || short-circuit.
Semantics notes
- Currying:
f a bis(f a) b. Partial application is a value. - Negation:
-xin expression position negates; between expressions-is subtraction. Writef (-1)to pass a negative literal. /vs//:/always produces a Float;//requires Ints and truncates toward zero;n // 0 == 0.- Equality is structural. Comparing functions is a runtime error.
- Ordering (
<etc.) works on numbers, strings, chars, and lists/tuples of comparables. ifrequires aBoolcondition — no truthiness.- Pattern match failure in
caseraises a runtime error naming the unmatched value (exhaustiveness checking arrives with the type checker).
Patterns
pattern ::= consPat ('as' lower)?
consPat ::= appPat ('::' consPat)?
appPat ::= upper patternAtom* | patternAtom
patternAtom ::= '_' | lower | upper | literal | '-'? number
| '(' ')' | '(' pattern (',' pattern)* ')'
| '[' (pattern (',' pattern)*)? ']'
| '{' lower (',' lower)* '}'
Record patterns bind the named fields: viewUser { name, age } = ....
Declarations
declaration ::= annotation? valueDecl | typeDecl | aliasDecl
annotation ::= lower ':' typeExpr
valueDecl ::= lower patternAtom* '=' expr
typeDecl ::= 'type' upper lower* '=' ctor ('|' ctor)*
ctor ::= upper typeAtom*
aliasDecl ::= 'type' 'alias' upper lower* '=' typeExpr
Constructor arity comes from the declaration: Banana Int makes Banana a 1-argument constructor. Applying too many arguments is a runtime error.
Type expressions:
typeExpr ::= typeApp ('->' typeExpr)?
typeApp ::= upper typeAtom* | typeAtom
typeAtom ::= lower | upper | '(' ')' | '(' typeExpr (',' typeExpr)* ')'
| '{' (lower ':' typeExpr (',' lower ':' typeExpr)*)? '}'
| '{' lower '|' fields '}'
Modules
module ::= header? import* declaration*
header ::= 'module' upper 'exposing' exposeList
import ::= 'import' upper ('as' upper)? ('exposing' exposeList)?
exposeList ::= '(' '..' ')' | '(' exposeItem (',' exposeItem)* ')'
exposeItem ::= lower | upper | upper '(..)' | '(' operator ')'
- A file without a header is module
Mainexposing everything. exposing (..)on a header exports every top-level name.Type(..)exposes a type’s constructors; a bareTypein Walnut’s export list currently also keeps its constructors reachable (this will tighten with the type checker).- Imports create qualified bindings (
Alias.name);exposingadditionally creates bare bindings. - Default scope in every module: unqualified
Basics, the constructorsTrue False Just Nothing Ok Err LT EQ GT, and qualified access to all stdlib modules (includingUiandWalnut).
Evaluation order
Top-level (and let) values are lazy, memoized thunks; functions are closures. Consequently declaration order never matters and mutual recursion works. A value whose definition depends on itself (x = x + 1) raises a cycle error.
Programs
An application module must expose main, built with:
Walnut.sandbox :
{ init : model
, update : msg -> model -> model
, view : model -> Ui.View msg
}
-> Walnut.Program model msg
The iOS runtime evaluates main, extracts the record, and runs the TEA loop against the UIKit renderer (architecture, views).
Runtime errors
All runtime errors carry a message in a clear “I was expecting…” spirit, e.g.
Runtime error: This `case` expression does not handle: Banana 3
Parse error at Main.walnut:14:9: Expected `->` after the pattern, but found `=`.
In an app, errors are rendered on-screen rather than crashing the process.