-
Notifications
You must be signed in to change notification settings - Fork 0
Parser
The parser is hand-written recursive descent for statements and declarations, with a Pratt (precedence-climbing) engine for expressions. It builds the SoA AST and, by default, the semantic event stream in the same pass, and it never throws on malformed input — it records a diagnostic, recovers, and keeps going.
Parser.parse(alloc, src, tokens) // emit_events on (parser.zig:458)
Parser.parseWithOptions(alloc, src, tokens, opts) // (parser.zig:526)
Parser.parseWithLanguage(alloc, src, tokens, lang, is_module) // (parser.zig:534)
parse is the common path and turns event emission on for you. parseWithOptions
takes the full options:
// src/parser.zig:466
pub const ParseOptions = struct {
language: Language = .js, // .js .jsx .ts .tsx .dts
is_module: bool = false, // import/export + always-strict
global_return: bool = false,
is_strict: ?bool = null, // defaults to is_module
annex_b: bool = true,
experimental_decorators: bool = false,
events_out: ?*ScopeEventStream = null,
emit_events: bool = false, // parse() sets this true
};Events are emitted when either emit_events is set or an events_out sink is
supplied (parser.zig:613).
parseProgram (parser.zig:1987) reserves the root at index 0, then loops
while (!isAtEnd()), parsing one statement at a time into a scratch list and
catching error.ParseError per statement so a single bad statement doesn't sink
the parse (parser.zig:2033). The collected statements become the root's
SubRange.
Expressions go through parseExpressionPrec(p, min_prec) (expressions.zig:112):
parse a prefix, then loop, consuming any infix operator whose precedence is at
least min_prec. Precedence is one table lookup per token:
Precedence enum(u8), 0..20 (expressions.zig:53)
none=0 comma=1 assignment=2 conditional=3 nullish_coalesce=4
logical_or=5 … exponentiation=15 unary=16 postfix=17 call=18
new_expr=19 primary=20
prec_table: [256]Precedence (expressions.zig:5857) tag → precedence
Associativity lives in Precedence.next() (expressions.zig:83): left-associative
operators recurse with level + 1, forcing the right operand to bind tighter,
while the two right-associative levels — assignment and exponentiation —
recurse at their own level. That is why a = b = c and 2 ** 3 ** 2 group to the
right and everything else to the left.
Member access, calls, and ?. are all .call-precedence and run in the same
postfix loop. Optional chaining uses exactly three tags —
optional_member_expr, optional_computed_member_expr, optional_call_expr
(expressions.zig:6513, :6477, :6466) — and ?.#priv reuses optional_member_expr
(:6494).
(a, b) => … versus a parenthesized expression is decided after the ) is
consumed: the parser collects the parenthesized items into scratch, and if the
next token is => on the same line (and arrows are allowed), reinterprets them as
parameters; otherwise they stay a grouping or sequence expression
(expressions.zig:3402, :3625). Empty () forces an arrow. TS-typed arrows
((x: T): R =>) are caught earlier with speculative backtracking.
An assignment LHS or arrow parameter list is parsed as an expression first, then
rewritten into a pattern by reinterpretAsPattern (expressions.zig:7156):
array_literal → array_pattern, object_literal → object_pattern,
spread_element → rest_element, assign → assignment_pattern, recursing through
properties and sequences. This is the cover-grammar trick — parse the permissive
form, retype on commit.
expectSemicolon (parser.zig:1814) eats a ; if present; otherwise it inserts
one before } or EOF, or when the next token starts a new line (isOnNewLine,
parser.zig:1831, reading the token's precomputed newline flag). The
no-line-terminator restrictions for return / throw / break / continue and
postfix ++ / -- are enforced at their own sites.
On error.ParseError the driver records a Diagnostic, calls synchronize to
skip to the next statement boundary, and drops an error_node placeholder in
place (parser.zig:1842, :1889). synchronize advances with a 16-wide SIMD scan
for the structural stops (;, }, EOF, statement-start keywords). A runaway
guard bails the whole parse after more than 100 consecutive errors — i.e. on the
101st (parser.zig:2039) — and a forward-progress check guarantees at least one
token is consumed per recovery, so it cannot loop.
Nesting is capped at max_recursion_depth = 400 (parser.zig:1915); at the limit
enterRecursion emits a diagnostic and returns error.ParseError rather than
overflowing the native stack. Node and extra_data buffers are pre-sized to
tokens.len * 3/4 (≈0.75 nodes per token, parser.zig:657), so addNode and event
pushes hit appendAssumeCapacity.
Two operator-mixing rules are enforced in parseBinaryExpression, both gated on
if (!p.is_ts) — TypeScript defers them to its own checker:
-
??mixing.a || b ?? c,a && b ?? c, anda ?? b || c(on either side) are rejected:??may not mix with||/&&without parentheses (expressions.zig:5967, :6024). -
**unary base. The left operand of**may not be an unparenthesized unary expression; the rejected set is exactlydelete typeof void ! ~ +x -x await(expressions.zig:5988).prefix_inc/prefix_decare intentionally allowed.
es-parser — MIT licensed. This wiki documents the implementation under src/; when a detail matters, the source is authoritative.