-
Notifications
You must be signed in to change notification settings - Fork 0
Home
A JavaScript, TypeScript, and JSX parser written in Zig. It does in one library what the JavaScript ecosystem usually splits across two: it produces an ESTree-shaped syntax tree, and it produces a full semantic model — lexical scopes, a symbol table, resolved references, and an ESLint-style control-flow graph — without ever walking the tree a second time. It was extracted from the Ez linter and still backs it.
A conventional toolchain parses with one library (acorn) and resolves scopes with another (eslint-scope) that walks the resulting tree again. es-parser folds that second walk into the first. While it parses, it emits a flat stream of scope, binding, reference, and control-flow events; the semantic phase replays that stream in a single linear pass. Many nodes in real source — literals, operators, punctuation — never emit an event, so the semantic phase touches only what actually affects scoping. That decision is the center of the design; the Architecture page works through it.
Language Zig (minimum 0.17.0-dev.607+456b2ec07)
Parsing hand-written recursive descent + Pratt expressions
AST struct-of-arrays (std.MultiArrayList) + a flat extra_data table
Semantics a linear event stream emitted at parse time, replayed in one pass
CFG a port of ESLint's CodePathAnalysis
Inputs .js .mjs .cjs .jsx .ts .mts .cts .tsx .d.ts .d.mts .d.cts
Threading single-threaded
License MIT
Pre-1.0 (v0.2.13). The API shape is stable but versioned 0.x — pin a tag. It is a per-file syntactic parser: no type checking, no cross-file resolution. CI runs on Linux and macOS against a pinned Zig dev build.
Conformance, reproduced locally via the conformance-* runners:
test262-parser-tests must-parse 3966/3966 · must-reject 1389/1389
TypeScript tests/cases 19120/19136
Babel fixtures valid 1928/1928 · invalid 1548/1548
The residual TypeScript cases need cross-file type analysis, which a single-file parser cannot do. The Babel rows cover es-parser's supported-feature subset — Flow, the pipeline operator, record/tuple, and other proposals are out of scope. Only test262-parser-tests and a semantic robustness sweep run in CI; the other numbers are measured by hand. See Conformance and Testing.
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 turns on event emission, so the returned Ast is ready for the
analyzer with no pass in between. One caller obligation worth flagging early: the
analyzer requires a zero-filling allocator (an arena over page_allocator, or
the bundled ZeroingAllocator). Full options, TS/JSX, and module mode are in the
API Reference.
Using it as a library: API Reference and Building and Integration, then skim AST and Memory Layout and Semantic Analysis for the shape of the output.
Studying or hacking on it: Architecture first, then Lexer → Parser → TypeScript and JSX → Semantic Analysis → Control-Flow Analysis, with Performance and Internals for the hot-path and file-level detail.
es-parser — MIT licensed. This wiki documents the implementation under src/; when a detail matters, the source is authoritative.