Skip to content
Jennifer Programming Language

Grammar - EBNF

The declarative view of the grammar: what shapes the language contains, in ISO-style EBNF. For how the parser decides between alternatives (ordered choice, lookahead), see the PEG grammar - the two describe the same language and must be kept in sync; the parser (internal/parser/parser.go) is the source of truth for both. Semantic notes that no grammar expresses (precedence prose, scoping, value semantics) stay in grammar.md.

This grammar describes the token stream after preprocessing - file splices (include STRING ;) are expanded before the parser runs, so they don't appear here. Library imports (use IDENT ;) and module imports (import STRING [ "as" IDENT ] ;) do reach the parser: use becomes an ImportStmt, import a ModuleImportStmt.

Terminals in CAPITALS are token classes from the lexer (see Lexer > Token types); quoted strings are keywords or punctuation that match the corresponding token's lexeme.

ebnf
program     = { useStmt | moduleImport | exported | methodDef | structDef | enumDef | statement } EOF ;
exported    = "export" ( methodDef | structDef | enumDef | constDefine ) ;
                                       (* `export` publishes a name from a
                                          module; it may only precede a
                                          `func`, `def struct`, `def enum`, or
                                          `def const`.
                                          Whether a program may contain
                                          `export` at all (module vs script)
                                          is decided at load time, not by the
                                          grammar. *)
useStmt     = "use" IDENT [ "as" IDENT ] ";" ;      (* library import; the
                                                       optional "as ALIAS"
                                                       renames the namespace
                                                       at the use site *)
moduleImport = "import" STRING [ "as" IDENT ] ";" ;  (* module import; the
                                                       STRING path must end
                                                       in ".j". Top-level
                                                       only - a module is a
                                                       declaration, so an
                                                       import inside a block
                                                       is a parse error *)
methodDef   = "func" IDENT "(" [ paramList ] ")" block ;
paramList   = param { "," param } ;
param       = IDENT "as" type ;
block       = "{" { statement } "}" ;

structDef   = "def" "struct" IDENT "{" [ structField { "," structField } [ "," ] ] "}" ";" ;
                                       (* top-level only;
                                          IDENT names the struct type;
                                          zero fields parse. Hoisted
                                          before the first top-level
                                          statement runs. *)
enumDef     = "def" "enum" IDENT "{" enumVariant { "," enumVariant } [ "," ] "}" ";" ;
                                       (* top-level only; a sum type. IDENT
                                          names the enum; at least one variant.
                                          Hoisted like structDef. A value of the
                                          type is exactly one variant. *)
enumVariant = IDENT [ "{" structField { "," structField } [ "," ] "}" ] ;
                                       (* a payload-less tag, or a variant with
                                          named fields (a mini-struct). *)
structField = wordName "as" type ;     (* a field name may be any
                                          identifier-shaped word (see
                                          wordName below) - `from` / `to`
                                          are fine; camelCase (no `_`)
                                          is enforced as a check *)

statement   = defineStmt
            | assignStmt
            | indexAssign
            | fieldAssign
            | appendStmt
            | returnStmt
            | ifStmt
            | whileStmt
            | forStmt
            | forEachStmt
            | repeatStmt
            | matchStmt
            | breakStmt
            | continueStmt
            | exitStmt
            | tryStmt
            | throwStmt
            | deferStmt
            | exprStmt ;

tryStmt     = "try" block "catch" "(" IDENT ")" block ;
                                       (* IDENT is the catch
                                          binding, follows the
                                          iteration-variable name rule
                                          (letters only). No `finally`
                                          in v1. *)
throwStmt   = "throw" expr ";" ;
                                       (* expr may produce any
                                          value; convention is an
                                          `Error` struct. *)
deferStmt   = ( "defer" | "errdefer" ) deferCall ";" ;
deferCall   = "(" deferCall ")" | qualifiedCall | call ;
                                       (* exactly a call - a plain call
                                          (`cleanup()`) or a namespaced /
                                          module call (`fs.close($f)`);
                                          grouping parens are transparent
                                          (`defer (f());` is accepted),
                                          any other expr is a parse
                                          error. Args evaluate at the
                                          defer site; the call runs
                                          when the enclosing block
                                          exits, LIFO. `defer` runs on
                                          every exit path; `errdefer`
                                          only when the block exits
                                          with a propagating error (a
                                          throw / runtime error - not
                                          return, break, continue, or
                                          exit). Block-scoped. *)

returnStmt  = "return" [ expr ] ";" ;
breakStmt   = "break" ";" ;            (* exits the innermost loop *)
continueStmt = "continue" ";" ;        (* skips to the next iteration *)
exitStmt    = "exit" [ expr ] ";" ;    (* terminates the program; the
                                          optional int expr is the exit
                                          code (0 when omitted) *)

constDefine = "def" "const" IDENT "as" type "init" expr ";" ; (* the const
                                          form of defineStmt - the only `def`
                                          an `export` may mark *)
defineStmt  = "def" [ "const" ] IDENT "as" type [ "init" expr ] ";" ;
                                       (* constants require "init" and an
                                          uppercase name matching
                                          [A-Z]+(_[A-Z]+)* (uppercase
                                          chunks joined by single `_`;
                                          no leading, trailing or
                                          consecutive `_`); variables may
                                          omit "init" and get zero-value,
                                          and use the letters-only IDENT
                                          form *)

assignStmt  = VARREF "=" expr ";" ;

indexAssign = VARREF lvalueTail { lvalueTail } "[" expr "]" "=" expr ";" ;
                                       (* l-value chain ending in `[index]`;
                                          root is a VARREF. Tail
                                          steps may freely mix `[index]`
                                          and `.field`. *)

fieldAssign = VARREF lvalueTail { lvalueTail } "." wordName "=" expr ";" ;
                                       (* l-value chain ending in `.field`.
                                          Root is a VARREF; tail
                                          may mix `[index]` and `.field`. *)

lvalueTail  = "[" expr "]" | "." wordName ;

appendStmt  = VARREF "[" "]" "=" expr ";" ;
                                       (* append sugar: write-only
                                          target meaning "the position
                                          just past the end of the
                                          list"; read use `e[]` is a
                                          parse error. Only one bare
                                          VARREF root - chained forms
                                          like `$xs[0][]` are not supported
                                          (yet). *)

ifStmt      = "if" "(" expr ")" block
              { "elseif" "(" expr ")" block }
              [ "else" block ] ;

whileStmt   = "while" "(" expr ")" block ;

forStmt     = "for" "(" [ defineStmt | assignStmt | ";" ]
                        [ expr ] ";"
                        [ assignNoSemi ]
                  ")" block ;
assignNoSemi = VARREF "=" expr ;       (* same shape as assignStmt without trailing ";" *)

forEachStmt = "for" "(" "def" IDENT "in" expr ")" block ;
                                       (* iterates list elements (in order)
                                          or map keys (insertion order);
                                          the iteration variable is a fresh
                                          binding in the body's scope *)

repeatStmt  = "repeat" block "until" "(" expr ")" ";" ;
                                       (* post-test loop: the body runs at
                                          least once; exits when the
                                          condition is true *)

matchStmt   = "match" "(" expr ")" "{"
                  { "when" ( valueArm | patternArm ) block }
                  [ "else" block ]
              "}" ;
valueArm    = expr { "," expr } ;      (* one or more values compared to the
                                          subject by `==` *)
patternArm  = IDENT [ "(" IDENT ")" ] ; (* an enum-variant pattern: a variant
                                          name, optionally binding the payload
                                          into a fresh name *)
                                       (* Two arm forms, disambiguated at resolve
                                          time by the subject's type. When the
                                          subject is a variable / parameter of a
                                          known enum type, every arm is a
                                          patternArm (variant + optional binder),
                                          checked for exhaustiveness (cover every
                                          variant or add `else`). Otherwise arms
                                          are valueArms: the subject is compared
                                          to each value by `==`, first match wins,
                                          `else` optional and last, no
                                          fall-through. A bare `Name` value
                                          followed by `{` reads the `{` as the arm
                                          block - parenthesize a struct-literal
                                          value. *)

exprStmt    = expr ";" ;

type        = primType | listType | mapType | taskType | channelType | funcType | structType ;
primType    = "int" | "float" | "string" | "bool" | "null" | "bytes" ;
funcType    = "func" ;                 (* a first-class function value; the
                                          type carries no signature - arity
                                          and argument kinds are checked at
                                          the call site *)
channelType = "channel" "of" type ;    (* a CSP channel. `channel` is a
                                          CONTEXTUAL keyword - a type only in
                                          this `channel of T` position; the `of`
                                          disambiguates it from a struct name, so
                                          `channel` stays an ordinary identifier
                                          everywhere else *)
listType    = "list" "of" type ;
mapType     = "map" "of" type "to" type ;
taskType    = "task" "of" type ;       (* `task of T` - handle to
                                          a `spawn`ed computation. Same
                                          shape as `list of T`; recurses
                                          the same way (`task of list of
                                          int` is legal). *)
                                       (* recursive; nesting like
                                          `list of list of int` and
                                          `map of string to list of int`
                                          falls out naturally *)
structType  = IDENT [ "." IDENT ] ;    (* User-defined struct OR enum type
                                          (bare IDENT), or a library-/module-
                                          provided namespaced type
                                          (`IDENT.IDENT`). The parser cannot tell
                                          a struct name from an enum name here;
                                          both are resolved at runtime against
                                          the struct / enum / NSStructs tables.
                                          Unknown names are positioned errors. *)

expr        = rangeExpr ;
rangeExpr   = orExpr [ ".." orExpr ] ;
                                       (* half-open range `lo..hi` -> `[lo, hi)`.
                                          Non-associative (`a..b..c` is an
                                          error) and looser than every binary
                                          operator, so `1+1..2*3` parses as
                                          `(1+1)..(2*3)`. Both bounds int; a
                                          range materialises a `list of int`,
                                          or - as a for-each source - iterates
                                          lazily. `lo > hi` is a runtime error;
                                          `lo == hi` is empty. *)
orExpr      = andExpr { "or" andExpr } ;
andExpr     = notExpr { "and" notExpr } ;
notExpr     = "not" notExpr | compExpr ;
compExpr    = bitOrExpr { ("<" | ">" | "<=" | ">=" | "==" | "!=") bitOrExpr } ;
bitOrExpr   = bitXorExpr { "|" bitXorExpr } ;
bitXorExpr  = bitAndExpr { "^" bitAndExpr } ;
bitAndExpr  = shiftExpr { "&" shiftExpr } ;
shiftExpr   = addExpr { ("<<" | ">>") addExpr } ;
                                       (* the bitwise family (`|` `^` `&`
                                          `<<` `>>`, unary `~`) operates on
                                          int only; precedence sits between
                                          comparison and additive, tightest
                                          first: shift, then `&`, `^`, `|` *)
addExpr     = mulExpr { ("+" | "-") mulExpr } ;
mulExpr     = unaryExpr { ("*" | "/" | "//" | "%") unaryExpr } ;
unaryExpr   = ("-" | "~") unaryExpr | primary ;
primary     = ( INT | FLOAT | STRING | interpString | "true" | "false" | "null"
              | VARREF | qualifiedCall | qualifiedConstRef | taskCall
              | call | structLit | constRef | "(" expr ")"
              | listLit | mapLit | lenExpr | spawnExpr )
              { "[" ( expr | sliceTail ) "]" | "." wordName | callArgs } ;

interpString = STRING_INTERP ;         (* a cooked "..." string carrying one or
                                          more `{expr}` interpolation slots. The
                                          lexer emits it as one TOKEN_STRING_INTERP
                                          whose Parts alternate literal chunks and
                                          expression-source slots; the parser
                                          sub-parses each slot to a single `expr`
                                          and builds an InterpStringExpr (lowered
                                          to concat + convert.toString). A slot is
                                          one expr, not a statement; `\{`/`\}` are
                                          literal braces; a bare `}` is a lex
                                          error; a raw '...' string never
                                          interpolates. STRING is a cooked or raw
                                          literal with no slots. *)
                                       (* any primary can be index-,
                                          slice-, field-, or call-chained. A
                                          `[...]` holds either an index `expr`
                                          or a `sliceTail` (contains a `..`);
                                          a trailing `callArgs` is a call
                                          through a function value
                                          (CallValueExpr): `$f(x)`,
                                          `$fns[0](x)`, `makeAdder(1)(2)`. *)
callArgs    = "(" [ expr { "," expr } ] ")" ;
sliceTail   = orExpr ".." [ orExpr ]   (* `[a..]`, `[a..b]` *)
            | ".." [ orExpr ] ;        (* `[..]`, `[..b]` - endpoints parse at
                                          orExpr so a bool-keyed comparison
                                          index `$m[$a == $b]` stays an index,
                                          not a slice. Slice yields a fresh,
                                          value-semantic copy (never a view);
                                          open ends default to 0 / len; strict
                                          `0 <= lo <= hi <= len` bounds check;
                                          works on list / bytes / string
                                          (rune-indexed). Read-only:
                                          `$xs[a..b] = ...` is a parse error. *)
spawnExpr   = "spawn" block ;          (* launches the block as a
                                          goroutine and evaluates
                                          immediately to a `task of T`
                                          where T is the body's return
                                          type at the use site. Bare
                                          `return;` produces `task of
                                          null`. Value-semantics
                                          capture: every binding visible
                                          at the spawn site is
                                          deep-copied into a fresh frame
                                          at launch. *)
lenExpr     = "len" "(" expr ")" ;     (* polymorphic
                                          structural-length built-in
                                          (string / list / map /
                                          bytes). Reserved keyword,
                                          not a library function; the
                                          `core` library that once
                                          hosted it no longer exists. *)
structLit   = IDENT [ "." wordName [ "." wordName ] ] "{" [ structLitField { "," structLitField } [ "," ] ] "}" ;
                                       (* struct OR enum literal (same node).
                                          Bare IDENT names a user struct;
                                          `IDENT.IDENT` is a namespaced struct
                                          (`os.Result{...}`) OR a local enum
                                          variant (`Shape.Circle{...}`);
                                          `IDENT.IDENT.IDENT` is a cross-module
                                          enum variant (`mod.Shape.Circle{...}`).
                                          The enum-vs-struct decision is made at
                                          eval from the tables, not from
                                          capitalisation. A payload-less enum
                                          variant (`Shape.Empty`) has no `{...}`
                                          and reaches eval as a constRef the
                                          interpreter resolves against the enum.
                                          The `{` after IDENT in expression
                                          position is the tie-breaker against
                                          `constRef`. `P{}` parses; "every field
                                          present exactly once" is a post-parse
                                          check. *)
structLitField = wordName ":" expr ;
call        = IDENT "(" [ expr { "," expr } ] ")" ;
qualifiedCall      = IDENT "." wordName "(" [ expr { "," expr } ] ")" ;
qualifiedConstRef  = IDENT "." wordName ;
                                       (* qualifiedCall / qualifiedConstRef:
                                          IDENT "." wordName, then `(` decides
                                          which. Resolved against the
                                          namespaced-builtin / constant
                                          registry, gated by `use lib;`
                                          (or alias-aware equivalent). *)
taskCall    = "task" "." wordName "(" [ expr { "," expr } ] ")" ;
                                       (* the `task` library's namespace. `task`
                                          is a type keyword, not an IDENT, so
                                          `qualifiedCall` (IDENT-led) cannot
                                          match it - hence its own production.
                                          Expression position only, as the
                                          namespace prefix (`task.wait($t)`). *)
constRef    = IDENT ;                  (* bare-IDENT: constant reference; the
                                          parser disambiguates `call` vs
                                          `qualifiedCall` vs `constRef` by
                                          peeking for "." / "(". *)
wordName    = IDENT | KEYWORD ;        (* any identifier-shaped token: a plain
                                          IDENT or a keyword spelled like one
                                          (every Jennifer keyword is). Name
                                          positions - after a ".", and field
                                          names in struct definitions and
                                          literals - are contextually
                                          unambiguous, so reserved words are
                                          valid names there:
                                          `strings.repeat(...)`,
                                          `def struct Route { from as Point,
                                          to as Point };`, `$r.to`. *)
listLit     = "[" [ expr { "," expr } [ "," ] ] "]" ;
mapLit      = "{" [ expr ":" expr { "," expr ":" expr } [ "," ] ] "}" ;
                                       (* `{` is also a block opener; only
                                          legal as a map literal in
                                          expression position, where the
                                          parser is unambiguous *)

A type keyword (int, float, string, bool, bytes) has no expression-position meaning: the parser reports a positioned error pointing at the convert library (convert.toInt(v), ...; convert.bytesFromString(s, codec) for bytes). task is the one type keyword with an expression role - solely as the task. namespace prefix (task.wait($t)).