Datafun Grammar

The concrete syntax of datafun, the statement and expression language of scripts (.dfs) and modules (.dfm), as EBNF. This is a reference: it states what the parser in crates/datalove-datafun-parser accepts, over the lexer and bracket matcher in crates/bcts. Written October 2026 from that code, with the cases that are not plainly read off the source run through the parser.

It builds on Datalit Grammar, whose type syntax it shares outright and whose value syntax it re-implements with differences. What a well-formed program means is in Datafun Typing Rules; the table of productions says which rule types each form.

Contents

Notation

The EBNF is as in the datalit grammar, over tokens rather than characters: whitespace and comments between tokens are not written. Two more notations carry what tokens alone cannot:

Brackets, ( [ { {| [| %{ #{ < (| <| and their partners, are matched before parsing (P050, P051). < and > are always brackets, never operators.

The parser's sources are cited by file and function: statement.rs, expr.rs, literal.rs, state.rs and lib.rs are in crates/datalove-datafun-parser/src/.

Grammar

(* ===== Top level  (lib.rs) ===== *)
script        = [ EOLS ], [ top_stmt, { EOLS, top_stmt }, [ EOLS ] ] ;
module        = [ EOLS ], [ mod_stmt, { EOLS, mod_stmt }, [ EOLS ] ] ;
mod_stmt      = decl_stmt | const_stmt ;   (* any other statement: P077 *)
interface     = [ EOLS ], [ iface_stmt, { EOLS, iface_stmt }, [ EOLS ] ] ;
iface_stmt    = native_fun | type_alias ;  (* a rider.dli; anything else: P084 *)
top_stmt      = decl_stmt | stmt ;
decl_stmt     = fun_def | native_fun | type_alias | require_stmt | import_stmt ;
stmt          = let_stmt | var_stmt | const_stmt | set_stmt | call_stmt
              | ret_stmt | if_stmt | match_stmt | loop_stmt | for_stmt
              | break_stmt | continue_stmt | debuglog_stmt ;
                (* chosen by the first word; anything else: P001;
                   tokens left on the line: P021 *)
block         = { ( stmt | decl_stmt ), EOLS } ;   (* decl_stmt in a block: P074 *)
end_line(k)   = "end", k ;         (* a line of its own; extra tokens: P033; missing: P034 *)

(* ===== Bindings  (statement.rs: parse_let, parse_var, parse_const, parse_binding) ===== *)
let_stmt      = "let", binding, [ ":", type ], "=", expr ;   (* P007; no "=": P083 *)
var_stmt      = "var", binding, [ ":", type ], "=", expr
              | "var", value_name, ":", type ;
                (* P008; pattern without value: P068; no type: P026 *)
const_stmt    = "const", value_name, [ ":", type ], "=", expr ; (* P024, P025 *)
binding       = value_name
              | "(", [ value_name, { ",", value_name }, [ "," ] ], ")"   (* (a): P070 *)
              | "{", [ field_pat, { ",", field_pat }, [ "," ] ], "}"
              | "atom", NAME
              | "term", NAME, value_name ;       (* one level deep; nested: P069 *)
field_pat     = NAME, [ "=", value_name ] ;      (* names unique: D038 *)

(* ===== Assignment  (statement.rs: parse_set, parse_place_steps) ===== *)
set_stmt      = "set", NAME, { place_step }, assign_op, expr ;  (* P009; no op: D025 *)
place_step    = ~".", field_sel
              | ~"[", expr, "]", [ ~"?" | ~"!" ] ;   (* spaced: P021, read anyway *)
assign_op     = "=" | "+=" | "-=" | "*=" | "/=" | "+!=" | "-!=" | "*!=" | "/!="
              | "+?=" | "-?=" | "*?=" | "/?=" ;

(* ===== Functions  (statement.rs: parse_fun, parse_native_fun, parse_type_alias) ===== *)
fun_def       = "fun", value_name, [ type_params ], param_list, [ ":", type ],
                [ where_clause ], EOLS,
                [ "inline", EOLS ],            (* elsewhere: P076 *)
                block, end_line("fun") ;     (* no name: P010; no "(": P002 *)
native_fun    = "native", "fun", value_name, [ type_params ], param_list,
                [ ":", type ], [ where_clause ] ;   (* P028, P029, P030 *)
type_params   = "<", [ type_name, { ",", type_name }, [ "," ] ], ">" ;
param_list    = "(", [ param, { ",", param }, [ "," ] ], ")" ;
param         = [ "const" ], [ "ref" | "mut" | "out" ], param_name, ":", type ;
                (* const with a mode: P082; no name: P011; no ":": P012 *)
where_clause  = "where", "{", [ bound, { ",", bound }, [ "," ] ], "}" ;  (* P061, P062 *)
bound         = NAME, "is", ( "ord" | "float" | "fixedint" ) ;
                (* P063, P060; a parameter bounded twice: P078 *)
type_alias    = "type", type_name, ":", type ;  (* P022, P023 *)

(* ===== Modules  (statement.rs: parse_require, parse_import) ===== *)
require_stmt  = "require", ( "module", lib_path
                           | "data", lib_path, [ ":", type ]
                           | "rider", value_name ) ;    (* P027; else P005 *)
lib_path      = NAME, "/", NAME, "/", value_name ;  (* P003, P004, P013-P016 *)
                (* the last segment is the alias or const the require binds *)
import_stmt   = "import", NAME, ".", NAME ;     (* P006, P017, P018 *)

(* ===== Control flow  (statement.rs: parse_if, parse_loop, parse_for, parse_match) ===== *)
if_stmt       = "if", if_arm, end_line("if") ;
if_arm        = expr, [ pipe_binding ], EOLS, block,
                [ "else", [ pipe_binding ],
                  ( "if", if_arm | EOLS, block ) ] ;   (* else-if shares one "end if" *)
pipe_binding  = "|", value_name, "|" ;          (* P019, P020 *)
loop_stmt     = "loop", [ "while", expr ], EOLS, block, end_line("loop") ;
for_stmt      = "for", value_name, [ ",", value_name ], "in", "ref", expr,
                EOLS, block, end_line("for") ;
                (* no name, or a third: P078; no "in": P078; no "ref": P079 *)
break_stmt    = "break" ;
continue_stmt = "continue" ;
match_stmt    = "match", expr, EOLS, { case_arm }, end_line("match") ;  (* P048 *)
case_arm      = "case", ( "atom", NAME | "term", NAME, value_name | "default" ),
                EOLS, block ;                   (* P044-P047; second default: P049 *)
ret_stmt      = "ret", [ expr ] ;
debuglog_stmt = "debuglog", expr ;
call_stmt     = "call", expr ;      (* a call node, then any "?" and "!": P031 *)

(* ===== Expressions  (expr.rs; precedence in binop_precedence) ===== *)
expr          = or_expr ;
or_expr       = and_expr, { ( "or" | "xor" ), and_expr } ;
and_expr      = cmp_expr, { "and", cmp_expr } ;
cmp_expr      = add_expr, [ cmp_op, add_expr ] ;    (* a second cmp_op: P067 *)
add_expr      = mul_expr, { add_op, mul_expr } ;
mul_expr      = unary, { mul_op, unary } ;
cmp_op        = "==" | "!=" | ".<" | ".>" | "<=" | ">=" ;
add_op        = "+" | "-" | "+!" | "-!" | "+?" | "-?" ;
mul_op        = "*" | "/" | "*!" | "/!" | "*?" | "/?" ;

unary         = prefix_op, unary | postfix_expr ;
postfix_expr  = primary, { postfix } ;          (* postfix binds tighter than prefix *)
prefix_op     = "-" | "-?" | "-!" | "not" ;
postfix       = ~"?" | ~"!" | ~"@"
              | ~".", field_sel
              | ~"[", expr, "]", [ ~"?" | ~"!" ]
              | ~"(", [ arg_list ], ")" ;       (* only after a single named field step: P072 *)
field_sel     = NAME | digit, { digit } ;       (* digits fit a u32; else, or nothing: P079 *)

primary       = hinted
              | "true" | "false" | "none"
              | ( "some" | "ok" | "er" | "data" | "error" ), payload
              | "atom", NAME                          (* P042 *)
              | "term", NAME, payload                 (* P043 *)
              | "enum", "{", expr, [ "," ], "}"       (* P071 *)
              | "icall", NAME, ~"(", [ arg_list ], ")"  (* P040, P041; spaced "(": P021 *)
              | dec_number | hex_number               (* no sign: "-" is prefix_op *)
              | string_lit                            (* D039 *)
              | NAME, [ ~"(", [ arg_list ], ")" ]     (* a call only when glued *)
              | paren | collection ;
                (* any other bracket, or nothing: P081; another token: P080 *)
payload       = unary ;                         (* a binary operator after it: P065 *)
arg_list      = arg, { ",", arg }, [ "," ] ;
arg           = [ "ref" | "mut" | "out" ], expr ;
paren         = "(", [ expr, { ",", expr }, [ "," ] ], ")" ;
                (* one element, no comma: grouping; else a tuple; () is unit *)

(* Hints  (literal.rs) *)
hinted        = ":", type, "/", ( hint_literal | unary ) ;   (* no "/": D049 *)
                (* a hinted directly under a hint is refused: P080 *)
hint_literal  = [ "-", ~ ], dec_number | hex_number | string_lit
              | "true" | "false" | "none"
              | ( "some" | "ok" | "er" | "data" | "error" ), payload
              | "atom", NAME | "term", NAME, payload     (* P042, P043 *)
              | "enum", "{", expr, [ "," ], "}"
              | paren | collection ;
                (* chosen by the first token; see "What a hint covers" below *)

(* Collections  (literal.rs) *)
collection    = list | set | map | struct | table | tensor ;
list          = "[", [ expr, { ",", expr }, [ "," ] ], "]" ;
set           = "#{", [ expr, { ",", expr }, [ "," ] ], "}" ;
map           = "%{", [ entry, { ",", entry }, [ "," ] ], "}" ;
entry         = expr, "=", expr ;               (* D017 *)
struct        = "{", [ sfield, { ",", sfield }, [ "," ] ], "}" ;
sfield        = NAME, "=", expr ;               (* D018, D038 *)
table         = "{|", [ ROWSEPS ], [ header_row, { ROWSEPS, data_row }, [ ROWSEPS ] ], "|}" ;
ROWSEPS       = ( newline | ";" ), { newline | ";" } ;
header_row    = NAME, { ",", NAME }, [ "," ] ;  (* D031, D034, D038 *)
data_row      = expr, { ",", expr }, [ "," ] ;  (* D030, D033 *)
tensor        = "[|", [ shape_header ], [ tensor_body ], "|]" ;   (* D040, D041 *)
shape_header  = digit_run, { digit_run }, "|" ;
tensor_body   = tensor_row, { comma_run, tensor_row } ;   (* D023, D033 *)
tensor_row    = expr, { expr } ;                (* elements end where fixity says *)
comma_run     = ",", { "," } ;

(* ===== Types ===== *)
type          = ? the datalit type grammar, datalit-ebnf.md, parsed by the same
                  code (datalove-datalit/src/parser/type_hint.rs) ? ;
                (* a type_name there is an alias or a type parameter here,
                   which F064 refuses when it names neither. There are no
                   function types and no type application. *)

(* ===== Lexical  (bcts lexer.rs, parser_util.rs) ===== *)
word          = wchar, { wchar } ;              (* [A-Za-z0-9_] or Unicode alphanumeric *)
NAME          = ? a word not beginning with an ASCII digit ? ;  (* non-ASCII: P075 *)
value_name    = NAME - ( "true" | "false" | "none" | "some" | "ok" | "er" | "data"
                       | "error" | "atom" | "term" | "enum" | "not" | "icall" ) ;
param_name    = value_name - ( "mut" | "out" | "ref" | "const" ) ;
type_name     = NAME - ( scalar type keyword | "atom" | "term" | "enum" ) ;
                (* each exclusion is P064, and the name is still taken *)
dec_number    = digit_run, [ ".", digit_run ], [ ( "e" | "E" ), [ "+" | "-" ], digit_run ] ;
                (* glued throughout; gaps: P052; letters on the end: P053 *)
hex_number    = "0", ( "x" | "X" ), hex_run ;
string_lit, digit_run, hex_run, comment, newline
              = ? as in the datalit grammar ? ;

Rules outside the grammar

Lines and blocks.

Spacing. The glue test decides fixity (spec Section 2.4):

Operators and payloads.

Bindings and signatures.

Recovery. P021 is also the code for any token left over after a complete statement, or after the last element of a bracket: debuglog 1 2 is P021 at the 2. It is suppressed after any earlier error in the same sub-parser, so fun f(a, b: u32): u32 extra reports the parameter and not extra.

Productions and their typing rules

Production Typing rules
hinted Syn-TypedExpr, Check-Hinted
literals, collections, constructors the datalit rules, inherited
add_op, mul_op, cmp_op, and, or, xor Pair, Syn-Arith, Syn-Ord, Syn-Eq, Syn-Logic, Check-BinOp
prefix_op Syn-Neg, Syn-NegChecked, Syn-NegOptional, Syn-Not, Check-Neg, Check-NegLit
postfix ?, ! Syn-TryOption, Syn-TryResult
postfix @ Syn-Adapt, Check-Adapt
postfix ., [ ] Syn-TupleProj, Syn-FieldProj, Syn-IndexOption, Syn-IndexResult
calls, icall Syn-Call, Syn-GenericCall, Check-GenericCall, Syn-Intrinsic
binding Pat-*, Let, Var-Uninit, Const
set_stmt Place-*, Set, Set-Compound
if_stmt, loop_stmt, for_stmt, match_stmt, ret_stmt If, If-Some, If-Ok, Loop, Loop-While, For, Match, Ret, Ret-Void
fun_def Fun

Differences from datalit

Datafun parses types with datalit's own code, and reuses its reserved-word tables, duplicate check (D038), string decoding and table and tensor splitting. Its value syntax is its own copy, in literal.rs and expr.rs, and differs:

Form Datalit Datafun
Payload of some, ok, er, data, error, term full_expr a unary expression; a binary operator after it is P065
A sign a glued - on a decimal literal; -0x10 is D050 part of the literal only right after a hint; elsewhere prefix negation, so -0x10 is legal
What a hint covers one datalit expression a literal form, or a unary expression wrapped in a Hinted node
(a, b) AnonTuple Tuple unhinted, AnonTuple under a hint
: T / (x) Group when under another hint Hinted(x)
Tensor elements and table cells datalit expressions datafun expressions, with fixity ending tensor elements
Unmatched brackets D054, D055 P050, P051

The same faults get different codes in the two parsers:

Fault Datalit Datafun
Malformed number; numeric suffix D019 P052; P053
enum literal without { D037 P071
atom, term without a name D035, D036 P042, P043
An unexpected token D020, D018 P080, P081
A non-ASCII name D043 P075

The rest are the same in both: a hint without its / (D049), a struct field without its name or = (D018), a map entry without = (D017), D030, D031, D033, D034, D038 to D041, tensor-shape D023, and every type diagnostic.

Divergences

Places the parser and the spec disagree, left as they are.