Skip to content

Grammar

The following EBNF describes wick v0.3, derived from the syntax reference and the reference compiler’s front end. Terminals are quoted; { } means zero or more repetitions, [ ] means optional, and | separates alternatives. Comments and whitespace are handled by the lexer and are not shown. The precedence of the expression rules, from orExpr (loosest) down to postfix (tightest), matches the table in Chapter 3.

program        = { toplevel } ;
toplevel       = recordDecl | fnDecl | statement ;
recordDecl     = "record" ident "{" field { "," field } "}" ;
field          = ident ":" scalar ;

fnDecl         = "fn" ident "(" [ params ] ")" [ ":" type ] block ;
params         = param { "," param } ;
param          = ident ":" type ;
block          = "{" { statement } "}" ;

statement      = letStmt | assign | callStmt | ifStmt | whileStmt
               | forStmt | "break" | "continue" | returnStmt ;
letStmt        = "let" ident [ ":" type ] "=" expr ;
assign         = lvalue "=" expr ;
lvalue         = ident [ "[" expr "]" ] [ "." ident ] ;
callStmt       = call ;
ifStmt         = "if" expr block { "elif" expr block } [ "else" block ] ;
whileStmt      = "while" expr block ;
forStmt        = "for" ident "in" expr ".." expr block ;
returnStmt     = "return" [ expr ] ;

type           = element [ "?" ]
               | "list" "<" element ">" [ "?" ]
               | "map"  "<" element ">" [ "?" ] ;
scalar         = "num" | "bool" | "str" ;
element        = scalar | ident ;  (* ident: declared record type *)

expr           = orExpr ;
orExpr         = andExpr    { "or"  andExpr } ;
andExpr        = coalesce   { "and" coalesce } ;
coalesce       = equality   { "??"  equality } ;
equality       = comparison { ( "==" | "!=" ) comparison } ;
comparison     = addition   { ( "<" | "<=" | ">" | ">=" ) addition } ;
addition       = term       { ( "+" | "-" ) term } ;
term           = unary      { ( "*" | "/" | "%" ) unary } ;
unary          = ( "-" | "not" ) unary | postfix ;
postfix        = primary { "(" [ args ] ")" | "[" expr "]" | "." ident } ;
primary        = number | string | "true" | "false" | "nil"
               | qualified | ident | "(" expr ")"
               | listLit | mapLit | recordLit ;
recordLit      = ident "{" ident ":" expr { "," ident ":" expr } "}" ;
qualified      = ident "." ident ;         (* e.g. lt.draw *)
call           = ( ident | qualified ) "(" [ args ] ")" ;
args           = expr { "," expr } ;

listLit        = "[" [ expr { "," expr } ] "]" ;
mapLit         = "[" string ":" expr { "," string ":" expr } "]" ;

ident          = letter { letter | digit } ;    (* letter includes _ *)
number         = digit { digit } [ "." digit { digit } ]
               | ( "0x" | "0X" ) hexDigit { hexDigit }
               | ( "0b" | "0B" ) bit { bit } ;
hexDigit       = digit | "a".."f" | "A".."F" ;
bit            = "0" | "1" ;
string         = '"' { char | escape } '"' ;
escape         = "\n" | "\t" | '\"' | "\\" ;

Notes on the grammar. An empty listLit ([]) is legal syntactically but requires a type annotation on its enclosing let, since the element type cannot be inferred. A mapLit requires string-literal keys. The .. in forStmt is a single token; the lexer guarantees that 1..5 tokenises as 1, .., 5. Type names are contextual, recognised only in the type position, so they are not reserved words.