-
Notifications
You must be signed in to change notification settings - Fork 0
Home
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/.
| 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 |
| 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.
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.
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.
- Using es-parser as a library? API Reference → Building and Integration → Conformance and Testing. Skim the shape of the output in AST and Memory Layout and Semantic Analysis.
- Studying or contributing to the parser? Architecture → Parser → TypeScript and JSX → Semantic Analysis → Control-Flow Analysis, then Internals and Contributing for file-level mechanics.
- Here for performance? Performance and Concurrency is the hub; Architecture explains the event-stream decision and Internals has the hot-path details.
- 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 Layout —
Node,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_pathCFG, 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 Integration —
zig fetch,build.zig, requirements. - Conformance and Testing — suites, runners, CI, fuzzing.
- Internals and Contributing — file-level mechanics for people patching es-parser.
es-parser — MIT licensed. This wiki documents the implementation under src/; when a detail matters, the source is authoritative.