Skip to content

Internals and Contributing

Eric San edited this page Jun 15, 2026 · 2 revisions

Internals and Contributing

This page is for people changing es-parser's internals — the file-level mechanics the conceptual pages summarize. Every item is anchored to source; when prose and source disagree, the source wins.

AST buffer ownership and the *_cap fields

The parser builds nodes / extra_data / scope_events / node_end_toks / parent_fixups into over-sized buffers (pre-sized to tokens.len * 3/4, parser.zig:657) and hands them to the Ast without a shrinking realloc. Each buffer's true backing capacity is recorded in a paired *_cap field (extra_data_cap, scope_events_cap, …, ast.zig:728) so deinit frees the real allocation, not the logical length. Consumers never touch this — they call Ast.deinit — but a contributor who adds a transferred buffer must add its _cap and free it the same way. Tokens are the exception: the Ast borrows them and frees nothing.

The ref_event_idx sentinel

ref_event_idx maps a node to the reference event most recently emitted for it, so a speculative reference can be cancelled (an identifier that turns out to be an arrow parameter). It uses 0 as the "none" sentinel with a +1 bias: the array is @memset to 0 (parser.zig:703), a write stores event_idx + 1 (parser.zig:1335), and a read treats 0 as "none", otherwise using idx - 1 (parser.zig:1681, :1747). The bias is what lets 0 be both the zero-initialized state and the "no event" sentinel.

The two JavaScript-only expression guards

Both live in parseBinaryExpression under if (!p.is_ts):

?? mixing (expressions.zig:5968, :5975, :6024)
  reject  ??  whose left  is logical_or / logical_and
  reject  || / &&  whose left is nullish_coalesce
  reject  ??  whose right is logical_or / logical_and
** unary base (expressions.zig:5988)
  reject  **  whose left ∈ { delete_expr, typeof_expr, void_expr,
                             logical_not, bitwise_not, unary_plus,
                             unary_minus, await_expr }
  (prefix_inc / prefix_dec are intentionally NOT in the set)

The token-rewrite log

recordTokMut (parser.zig:8675) logs a (idx, tag, start) triple only while record_tok_muts is set — which it is during speculative type-argument parsing. The in-place rewrite (tags_ptr[idx] = …) is unconditional; the flag controls only whether it is journalled for undoTokMuts. A rewrite on a committed path is permanent and deliberately unlogged. If you add a speculative parse that mutates tokens, set and restore record_tok_muts around it (see tryParseTsTypeArguments, expressions.zig:7524) or your rewrite won't be undone on backtrack.

Resolver event-order invariant

The resolver's reference cache and undo machinery assume events arrive in a fixed order per scope: scope_open before the declares and references inside it, each matched by exactly one scope_close. undo_stacks[depth] restores scope_map and ref_cache on close, so an emitter that opens a scope without closing it — or emits a reference outside any open scope — corrupts visibility. Pair every emit when adding a construct that opens a scope.

checkRedeclarations

checkRedeclarations (event_resolver.zig:1398) runs only when diagnose_redeclare is set and returns immediately for TypeScript input (if (ast.is_ts) return &.{};, event_resolver.zig:1411). That bail is how TS declaration merging avoids false duplicates. On JavaScript it runs several passes: same-scope lexical duplicates, lexical-vs-var across the block/var boundary, duplicate block functions with the Annex B B.3.3.4 exemption, parameter-vs-body, and catch-param-vs-var per B.3.5.

CFG Result layout

CodePathBuilder.Result (code_path.zig:1772) is flat SoA plus a moved-in arena — finish transfers the arena instead of copying each column, and deinit frees just the arena. A subtlety for anyone touching segment edges: of the prev-edge ranges, all_prev / prev / collapsed_prev are set at createSegment and never change, but looped_prev is written later by markLooped (code_path.zig:747) when a loop's back-edge is added. Segment is 36 bytes (9 × u32), SoA-stored.

Contributing notes

  • The parser is split across expressions.zig, typescript.zig, and jsx.zig, which call back into Parser methods marked pub only to cross Zig's file boundary. A pub fn on Parser is not public API; the supported surface is Parser.parse / parseWithOptions / parseWithLanguage* and the semantic.SemanticAnalyzer facade.
  • This page's file:line anchors are its contract with the source; update them when you move the definitions they point at.

Clone this wiki locally