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

FormExamplesNotes
Int42, 0xFF64-bit
Float3.14, 6.02e23, 1.0e-964-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:

  1. Top level: every declaration starts at column 1; every continuation line of a declaration is indented past column 1.
  2. Expressions: a token at column ≤ the governing indentation ends the current expression.
  3. case e of: the first token after of fixes the branch column. Every branch pattern must start exactly at that column; every branch body must stay right of it.
  4. let: the first binding fixes the binding column; sibling bindings align with it; in ends the block.
  5. 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:

PrecOperatorsAssociativity
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 b is (f a) b. Partial application is a value.
  • Negation: -x in expression position negates; between expressions - is subtraction. Write f (-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.
  • if requires a Bool condition — no truthiness.
  • Pattern match failure in case raises 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 Main exposing everything.
  • exposing (..) on a header exports every top-level name.
  • Type(..) exposes a type’s constructors; a bare Type in Walnut’s export list currently also keeps its constructors reachable (this will tighten with the type checker).
  • Imports create qualified bindings (Alias.name); exposing additionally creates bare bindings.
  • Default scope in every module: unqualified Basics, the constructors True False Just Nothing Ok Err LT EQ GT, and qualified access to all stdlib modules (including Ui and Walnut).

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.