This is an informal BNF-ish grammar for pell. Tokens are spelled in
double quotes; non-terminals are angle-bracketed; [x] is optional,
{x}* is zero-or-more, x|y is alternation.
For the canonical implementation, see compiler/pell/parser.py.
Lexical structure
identifier = letter { letter | digit | "_" }*
number = digit { digit }* [ "." digit { digit }* ]
string = '"' { char | escape | interp }* '"'
rawstring = "`" { any-char }* "`"
regex = "/" { any-char | "\\" any-char }* "/"
sql_block = "sql!" "{" { any-char with brace tracking } "}"
jq_block = "jq!" "{" { any-char with brace tracking } "}"
comment = "//" { any-char until "\n" }
| "/*" { any-char } "*/"
escape = "\" ( "n" | "r" | "t" | "\"" | "\\" | "0" | other )
interp = "{" expr "}"
/regex/ is only recognized in regex-allowed token positions
(after operators, keywords, openers — see Regex tutorial
for the disambiguation rule).
Keywords
module import pub fn let return yield
if else for forall in match transaction
record error true false Some None Ok Err
unsafe finally and or not
type sealed aggregate case self
seq enum out inout
Top-level
module = "module" dotted_ident ";" { item }*
item = annotations? "pub"? "unsafe"?
( fn_def | record_def | error_def | sealed_type_def
| type_def | aggregate_def | seq_def | enum_def
| import_def )
dotted_ident = ident { "." ident }*
qualified_ident = ident { "::" ident }*
Functions
fn_def = "fn" ident "(" param_list? ")" [ "->" type_ref ]
block [ "finally" block ]
param_list = param { "," param }*
param = [ "out" | "inout" ] ident ":" type_ref
block = "{" { stmt }* "}"
Records and types
record_def = "record" ident "{" field_list? "}"
field_list = field { "," field }* [ "," ]
field = ident ":" type_ref
error_def = "error" ident "{" field_list? "}"
type_def = "type" ident "{" { field_decl | method_def }* "}"
field_decl = ident ":" type_ref ","
method_def = "fn" ident "(" param_list? ")" [ "->" type_ref ] block
sealed_type_def = "sealed" "type" ident "{" { case_decl | method_def }* "}"
case_decl = "case" ident "{" field_list? "}"
aggregate_def = "aggregate" ident "(" param_list ")" "->" type_ref
"{" "state" "{" field_list? "}"
"step" "(" param_list ")" block
[ "merge" "(" param_list ")" block ]
"finish" "(" ")" "->" type_ref block
"}"
seq_def = "seq" qualified_ident ";"
enum_def = "enum" ident "{" enum_variant { "," enum_variant }* [ "," ] "}"
enum_variant = ident [ "=" string ]
Type references
type_ref = prim_type | named_type | optional_type | generic_type
prim_type = "number" | "int" | "text" | "bigtext" | "bool"
| "date" | "timestamp" | "interval" | "bytes" | "json"
| "Unit" | "Never"
named_type = ident | qualified_ident
optional_type = "Option" "<" type_ref ">"
generic_type = ident "<" type_ref { "," type_ref }* ">"
Common generics: list<T>, map<K, V>, set<T>, cursor<T>,
stream<T>, rowtype<table_name>, Result<T, E>.
Statements
stmt = let_stmt | assign_stmt | expr_stmt | return_stmt
| yield_stmt | if_stmt | for_stmt | forall_stmt
| match_stmt | transaction_stmt
let_stmt = "let" ident [ ":" type_ref ] [ "=" expr ] ";"
assign_stmt = ident "=" expr ";"
| member_access "=" expr ";"
return_stmt = "return" [ expr ] ";"
yield_stmt = "yield" expr ";"
expr_stmt = expr ";"
if_stmt = "if" expr block [ "else" ( if_stmt | block ) ]
for_stmt = "for" ident "in" expr block
forall_stmt = "forall" ident "in" expr block
match_stmt = "match" expr "{" match_arm { "," match_arm }* "}"
match_arm = pattern "=>" ( expr | block )
transaction_stmt = "transaction" block
Expressions
expr = pipeline
pipeline = logical_or { "|>" logical_or }*
logical_or = logical_and { "||" logical_and }*
logical_and = equality { "&&" equality }*
equality = comparison { ( "==" | "!=" ) comparison }*
comparison = addition { ( "<" | "<=" | ">" | ">=" ) addition }*
addition = mult { ( "+" | "-" ) mult }*
mult = unary { ( "*" | "/" | "%" ) unary }*
unary = [ "!" | "-" ] postfix
postfix = primary { "(" arg_list? ")"
| "." ident
| "::" ident
| "::" "<" type_ref { "," type_ref }* ">"
| "[" expr "]"
| "?"
}*
primary = literal
| ident
| "(" expr ")"
| block_expr
| if_expr
| match_expr
| struct_lit
| list_lit
| "Ok" "(" expr ")"
| "Err" "(" expr ")"
| "Some" "(" expr ")"
| "None"
| sql_block
| jq_block
| regex
| rawstring
| string
literal = number | string | rawstring | regex
| "true" | "false"
arg_list = arg { "," arg }*
arg = expr | ident "=" expr (keyword arg)
struct_lit = ident "{" field_init_list? "}"
field_init = ident [ ":" expr ]
list_lit = "[" [ expr { "," expr }* ] "]"
Patterns (match)
pattern = wildcard_pattern
| binding_pattern
| variant_pattern
| literal_pattern
wildcard_pattern = "_"
binding_pattern = ident
variant_pattern = ident [ "{" field_pat { "," field_pat }* [ "," ".." ] "}" ]
[ "(" pattern { "," pattern }* ")" ]
field_pat = ident [ ":" pattern ]
Annotations
annotation = "@" ident [ "(" annotation_arg_list? ")" ]
annotation_arg_list = annotation_arg { "," annotation_arg }*
annotation_arg = expr | ident "=" expr
Method chains on sql!{}
sql_chain = sql_block { lock_modifier }* terminator
lock_modifier = ".for_update" "(" ")"
| ".nowait" "(" ")"
| ".skip_locked" "(" ")"
| ".wait" "(" expr ")"
| ".for_update_of" "(" ident { "," ident }* ")"
terminator = ".one" "(" ")"
| ".one_or_none" "(" ")"
| ".first" "(" ")"
| ".collect" "(" ")"
| ".rowcount" "(" ")"
| ".returning" "::" "<" type_ref ">" "(" ")" "." (...)
.if_empty(...) and .if_many(...) chains can be added after .one()
to override the default NotFound / TooManyRows handlers.
Reserved for future use
Pell reserves several keywords for features not yet in the surface:
var, transaction (transaction blocks are partially specced),
open, close, for_update. Don’t use these as identifiers in new
code.