Skip to content

Performance and Concurrency

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

Performance

This page is the set of techniques the implementation uses to keep allocation and cache misses down, each traceable to source. They are design choices rather than benchmarked outcomes: es-parser ships no benchmark harness, so the page makes no comparative speed claim.

Struct-of-arrays everywhere

Nodes, tokens, scopes, symbols, and references are each a std.MultiArrayList, so every field is its own contiguous column. A pass that needs one field — every reference's symbol_id, every symbol's name, every token's has_newline_before — streams a dense array and pulls nothing else through cache. The AST's children live in a single flat extra_data: []u32 rather than per-node heap lists. See AST and Memory Layout.

One event stream, no second walk

The semantic phase does not walk the AST; it replays a flat array of 8-byte events the parser emitted, eight to a cache line, skipping every literal and operator that has no scoping meaning. This is the single biggest structural decision and it is covered in Architecture.

Pre-sized buffers

The parser sizes nodes and extra_data to tokens.len * 3/4 before parsing (parser.zig:657) — roughly 0.75 nodes per token — so the common case writes through cached raw pointers with no growth. It is an estimate, not a hard bound: an overflow takes a cold path that doubles the buffer and refreshes the cached pointers (refreshNodePtrs). The token columns are the exception — see below.

Mutable, cached column pointers

peek and friends read tokens through cached raw column pointers — tags_ptr and tok_starts_ptr (parser.zig:224, :232) — rather than re-deriving the MultiArrayList slices each time. These stay valid because the token columns are the lexer's finalized output — the parser borrows them and never grows them (the node and event pointers are re-cached if their buffers grow). Those stable token pointers are what let the TypeScript layer rewrite >> into > in place (O(1), visible to the next peek); see TypeScript and JSX.

SIMD where it pays

The lexer vectorizes the inner scans that dominate its time — identifier runs, line-terminator hunts, comment and template bodies — all 16 bytes wide with a scalar tail (scalar_lexer.zig:223). Whitespace skipping stays scalar. Line-start computation for diagnostics is the same 16-wide scan (span.zig:85). Details in Lexer and Tokens.

Keyword recognition

Keyword recognition gates on a comptime first-char bitset and a packed-u64 compare (no hash, no table), so an identifier exits in a couple of comparisons and the hot path skips the lookup entirely after . / ?. (lexer.zig:210, :178). See Lexer and Tokens.

Lazy work

Several costs are paid only when asked for:

line starts     built on the first diagnostic, via span.LineIndex
parent links    built only when build_parents is set (semantic.zig:146)
CFG + reachability   skipped when need_cfg = false
comment trivia  collected only when a CommentSink is supplied

Resolver hot structures

Reference resolution fronts the name-hash map (scope_map) with a 512-entry direct-mapped L1 cache — {hash: u64, sym: SymbolId}, 8 KB total (event_resolver.zig:409) — so the common "resolve to an already-visible binding" case is a single indexed probe. Two-phase resolution (immediate, then a retry over the unresolved queue) means the resolver never re-walks the tree to close forward references. See Semantic Analysis.

The zeroing-allocator contract

The binder and CFG builder read sentinel-initialized buffers where 0 means "none", so they assume fresh memory is zeroed. The analyzer therefore requires a zero-filling allocator — an arena over page_allocator, or the bundled ZeroingAllocator (semantic.zig:94). This is a deliberate trade: skipping explicit initialization of those buffers in exchange for a caller contract. See Semantic Analysis.

Threading

Every path es-parser executes is single-threaded. The stable-token-pointer property above is the load-bearing invariant on the parse path; nothing the library executes spawns a thread.

Clone this wiki locally