Skip to content
Eric San edited this page Jun 14, 2026 · 3 revisions

es-parser

A fast JavaScript / TypeScript / JSX parser written in Zig.

es-parser is a recursive-descent parser with a Pratt expression core, a MultiArrayList-backed struct-of-arrays (SoA) AST, a single-pass scalar lexer with a SIMD ASCII fast path, and an event-driven semantic layer (lexical scope tree, symbol table, reference resolution, and an ESLint-style control-flow graph). It produces an ESTree-compatible node taxonomy and emits a four-tier diagnostic stream (error / warning / info / hint). It was extracted from the Ez linter and is maintained as a standalone library.

When to reach for it: you want a single, fast, allocation-lean parse-to-semantics library embeddable in a Zig toolchain (linter, bundler, codemod, editor tooling) and you want scope/symbol/reference/CFG data out of the box, not just a syntax tree. It is a single-file syntactic parser — it does no type checking and no cross-file resolution.

Maturity: pre-1.0 (v0.2.13), MIT-licensed, extracted from and still backing the Ez linter. The API is stable in shape but versioned 0.x; pin a tag. CI runs on Linux and macOS against a pinned Zig dev build (see Building and Integration).

This wiki is written for engineers who build and maintain parsers. It documents the actual data structures, algorithms, and performance trade-offs in the source tree — not a tutorial. Every claim is drawn from the code under src/.


At a glance

Property Value
Implementation language Zig (minimum_zig_version = 0.17.0-dev.607+456b2ec07)
Parsing strategy Hand-written recursive descent + Pratt (precedence-climbing) expressions
AST storage std.MultiArrayList(Node) (SoA) + flat extra_data: []const u32 side table
Node identity NodeIndex = enum(u32) (root = 0, none = maxInt(u32))
Lexer Single-pass scalar tokenizer (scalar_lexer.zig), 16-byte SIMD ID/line-terminator scans
Semantic model Linear event stream emitted during parse, replayed in one pass (event_resolver.zig)
CFG Port of ESLint CodePathAnalysis (code_path.zig)
Languages .js .mjs .cjs .jsx .ts .mts .cts .tsx .d.ts .d.mts .d.cts
Concurrency Optional 2-stage (lex‖parse) and 3-stage (lex‖parse‖sem) streaming pipelines
License MIT

Conformance (from README.md)

Suite Result
tc39/test262-parser-tests must-parse 3,966 / 3,966 · must-reject 1,389 / 1,389
TypeScript compiler tests (tests/cases) 19,120 / 19,136
Babel parser fixtures — valid 1,928 / 1,928
Babel parser fixtures — invalid (correctly rejected) 1,548 / 1,548

The residual TypeScript failures require cross-file type analysis or transpile-level error recovery, neither of which a single-file syntactic parser performs. The Babel rows are over es-parser's supported-feature subset (~1,740 of the 5,216 parser fixtures are skipped — Flow, the pipeline operator, record/tuple, and other proposals it does not target); the test262-parser-tests rows are the full suite. CI gates only test262-parser-tests plus a semantic robustness sweep — the Babel/TypeScript/full-test262 numbers are measured manually. See Conformance and Testing.


The three-stage pipeline

source bytes ──▶ Lexer.tokenize* ──▶ TokenList (SoA)
                                         │
                                         ▼
                 Parser.parse* ──▶ Ast { nodes (SoA), extra_data, errors,
                                         scope_events, node_end_toks, … }
                                         │  (scope/declare/reference/CFG events
                                         │   are emitted inline during the parse)
                                         ▼
       semantic.SemanticAnalyzer.analyze* ──▶ SemanticResult { scopes, symbols,
                                              references, code_path_result,
                                              node_reachable, diagnostics, … }

The semantic phase does not walk the AST. The parser emits a packed 8-byte event per scope/binding/reference/control-flow boundary; the analyzer consumes that linear stream in a single pass. See Semantic Analysis.

Minimal usage

const es = @import("es_parser");

var lr = try es.Lexer.tokenize(allocator, source);   // .js, script mode
defer lr.deinit(allocator);

var tree = try es.Parser.parse(allocator, source, lr.tokens.slice());
defer tree.deinit(allocator);

for (tree.errors) |d|
    if (d.severity == .@"error")
        std.debug.print("error: {s}\n", .{d.message});

var sem = try es.semantic.SemanticAnalyzer.analyze(allocator, &tree);
defer sem.deinit(allocator);

Parser.parse enables event emission by default, so the returned Ast is ready for the fast-path analyzer. See API Reference for TS/JSX, module mode, and the options structs.


Find your path

Wiki map

  • Architecture — module layout, pipeline, design philosophy.
  • Lexer and Tokens — the scalar lexer, SIMD paths, regex/division, templates, JSX lexing, keyword perfect-hash, token representation.
  • AST and Memory LayoutNode, extra_data, NodeIndex, the SoA design, ESTree mapping, span recovery.
  • Parser — statement grammar, the Pratt expression engine, precedence table, arrow disambiguation, error recovery, buffer pre-sizing.
  • TypeScript and JSX — type grammar, speculative parsing, the <</>> token-rewrite log, JSX/type-argument ambiguity.
  • Semantic Analysis — the event stream, the resolver, scope tree / symbol table / reference table, hoisting, TDZ, redeclaration early-errors.
  • Control-Flow Analysis — the code_path CFG, segments/forks/joins, reachability.
  • Performance and Concurrency — SoA rationale, SIMD, the streaming pipelines, the zeroing-allocator contract, sizing heuristics.
  • API Reference — every public entry point and options field.
  • Building and Integrationzig fetch, build.zig, requirements.
  • Conformance and Testing — suites, runners, CI, fuzzing.
  • Internals and Contributing — file-level mechanics for people patching es-parser.

Clone this wiki locally