-
Notifications
You must be signed in to change notification settings - Fork 0
Performance and Concurrency
Who this is for: performance engineers. These are design facts drawn from the source — not benchmark numbers. File-level hot-path mechanics are in Internals.
This page collects the performance-relevant design decisions in one place.
-
Struct-of-arrays throughout. Nodes, tokens, scopes, symbols, and references
are all
std.MultiArrayList. A pass that needs one field streams a dense column without dragging the others through cache — e.g.SymbolTable.allNames()/allFlags()return single columns, andReferenceTablecan scan allsymbol_ids to count unresolved refs without touching node/scope data. See AST and Memory Layout. -
Flat side tables, index identity. Variable-arity children live in one
extra_data: []u32; nodes reference children byu32index, never by pointer. No per-node heap allocation; the tree is contiguous and trivially serializable. -
Cached column pointers on hot paths. The parser caches raw pointers into
the token and node columns (
tags_ptr,tok_starts_ptr,node_tags_ptr, …) sopeek()/nodeTagskipMultiArrayList.items(...)reconstruction; the lexer caches token columns similarly. The resolver hoists the event write cursor (ev_ptr/ev_len). -
Packed events. Each semantic event is a
packed struct(u64)— eight per 64-byte line. The CFG splits the hot "next" adjacency into a 16-byteSegNextInfosidecar somarkUsed/markLoopedtouch a quarter cache line.
The parser sizes nodes and extra_data to @max(sizing_count * 3 / 4, 1)
(sizing_count = tokens.len, or the caller's capacity_hint in streaming
mode) — about 0.75 entries per token — so the hot addNode/event-push paths hit
appendAssumeCapacity. The lexer pre-sizes the token list to
@max(src.len / 2 + 16, 64) and grows geometrically (cap*2 + 16). max_nodes
is a hard ceiling of sizing_count * 16, checked in addNode. Recursion is
bounded at max_recursion_depth = 400 to turn deep nesting into a diagnostic
rather than a stack overflow.
The lexer and line-index code use 16-wide byte vectors (@Vector(16, u8)):
-
asciiIdentEndscans an identifier run 16 bytes at a time, classifying[A-Za-z0-9_$]with a lowercase fold and range compares, and jumping to the first non-match with@ctz. Pure-ASCII identifiers (the common case) never decode UTF-8. -
lineTerminatorScan(comment/regex line scanning) andcomputeLineStarts(the lazy line index) skip 16-byte windows that contain no\n/\r/0xE2candidate, falling to scalar handling only on windows that do.
keywordLookup is a hand-rolled perfect hash, not a hash map: a length gate, an
O(1) comptime first-char bitset (KW_FC_MASK[len]), then switch(len) →
switch(first_char) with each candidate compared as one packed u64 word — at
most two comparisons. After ./?. the keyword test is skipped entirely
(the next identifier is a property name).
-
Line starts are not produced by the lexer.
span.LineIndexbuilds the line-start table on first location lookup and caches it, so a clean file that emits no diagnostics never pays for it. -
Comments are opt-in (a
CommentSinkmust be supplied). -
Parents are on demand (
build_parents), built in one forward pass. -
CFG is toggleable (
need_cfg) — skipping it removes a large fraction of semantic work when no flow-dependent rule is active. -
Ref ranges are toggleable (
build_ref_ranges); when on, grouping is a counting sort (O(n + k)), not a comparison sort. -
Redeclaration checks are opt-in (
diagnose_redeclare).
Reference resolution fronts a scope_map HashMap with a 512-entry direct-mapped
ref_cache (hash & 511) to absorb repeated lookups of the same identifier;
hoist_map gives O(1) per-level ancestor probing in the unresolved retry pass;
undo_stacks restore visibility in O(1) per shadowed name on scope close. See
Semantic Analysis.
Status: the streaming machinery below is designed but not driven by any in-tree code. Parsing and analysis are single-threaded in every path this library ships. Verify before relying on it: the lexer never publishes a token count (
TokenizeOptions.publish_to/publish_batch_maskhave zero consumers inscalar_lexer.zig), nothing constructsStreamingHooks, and the onlystd.Thread.spawnin the library is the resolver's scope/CFG split — which is itself opt-in and uncalled byanalyze(see below). Treat this section as a description of the plumbing a downstream driver could use, not of a working default.
The parser side of a lex→parse pipeline is implemented:
ParseOptions.streaming (StreamingHooks) carries published_len /
lex_done atomics, and the parser's pump (refreshParsedLen, the
peekSlow/peekAtSlow slow paths) blocks until more tokens are published or EOF
is signaled. The 3-stage hooks (events_publish_to, ast_view_out,
ast_ready) and the parser-side publish at statement boundaries
(event stream PUBLISH_BATCH = 4096 in scope_events.zig) are also present, as
is the lexer-side token batch size (PUBLISH_BATCH = 1024 in
lexer_helpers.zig). What is missing is a producer: no in-tree code spawns a
concurrent lexer or wires publish_to, so the pump never sees a concurrently
growing token stream.
The design constraint these hooks are built around is real and worth knowing:
shared buffers (nodes, scope_events, …) are pre-sized to safe upper bounds and
must not realloc mid-parse, because a consumer thread would hold raw pointers
into them — which is why streaming would size from capacity_hint and the parser
uses appendAssumeCapacity throughout.
The resolver also exposes a scope/CFG parallel split
(ScopeCfgParallel.start/.join + combineParts) — the library's only actual
Thread.spawn. It too is opt-in: the default analyze/analyzeWithOptions path
calls the single-threaded resolveFull (or resolveFullScope when need_cfg is
off), never ScopeCfgParallel. So even the implemented parallelism is not on by
default.
The analyzer reads sentinel-initialized buffers assuming fresh memory is zero.
Use an ArenaAllocator over std.heap.page_allocator (zeroed OS pages) or wrap
with semantic.ZeroingAllocator. A raw GPA, or an arena over c_allocator, can
crash in ReleaseFast. This is a caller contract (no runtime probe is possible —
see Semantic Analysis).
Next: API Reference · Architecture
es-parser — MIT licensed. This wiki documents the implementation under src/; when a detail matters, the source is authoritative.