-
Notifications
You must be signed in to change notification settings - Fork 0
Control Flow Analysis
Who this is for: parser engineers and lint-rule authors who consume the CFG. The full
Result/Segmentfield layout is in Internals.
code_path.zig builds a full multi-segment control-flow graph during semantic
analysis. Its module doc states it is a Zig port of ESLint's
CodePathAnalysis, and the structures bear that out: the same fork/choice/
loop/try context machinery, segment graph, and event protocol. The CFG drives
per-node reachability (node_reachable), loop-exit reachability
(loop_exit_reachable), and per-reference segment IDs (references.seg_id).
In the default path the CFG is built single-threaded as part of the combined
resolveFull walk. There is also a resolveFullCfg "CFG half" that consumes the
same event stream and can run on a worker thread (opt-in via ScopeCfgParallel,
stitched by combineParts) — but analyze does not use it (see
Semantic Analysis).
| es-parser | ESLint equivalent | Role |
|---|---|---|
CodePathBuilder |
CodePathAnalyzer + CodePathState
|
drives the analysis |
Segment |
CodePathSegment |
a straight-line basic block |
CodePath |
CodePath |
one function / program / field-initializer / static-block |
ForkContext |
ForkContext |
manages parallel segment arrays at a branch |
ChoiceContext |
(choice state) |
if/else, &&, ||, ??, ?: (ChoiceKind: test_kind, logical_and, logical_or, nullish, loop, switch_kind) |
LoopContext |
(loop state) |
while / do-while / for / for-in / for-of
|
TryContext |
(try state) |
try / catch / finally
|
SwitchContext |
(switch state) |
switch / case / default (pushSwitchContext/popSwitchContext/makeSwitchCaseBody) |
SegmentId and CodePathId are u32 with NONE_SEG / NONE_CP =
maxInt(u32) sentinels. A CodePath has an Origin
(program, function, class_field_initializer, class_static_block), an
upper parent, an initial_segment, and ranges for its final/returned/
thrown segment sets.
A Segment is a straight-line basic block. Its predecessor adjacency comes in
four flavors — all_prev (every predecessor), prev (reachable only),
looped_prev (back-edges), and collapsed_prev (for unreachable segments: the
nearest reachable ancestors) — each stored as a [start,end) range into flat
target pools. all_prev/prev/collapsed_prev are set at creation and then
immutable; looped_prev is the exception — it is written after creation by
markLooped when a back-edge is added. The hot "next" adjacency is split into a
separate 16-byte SegNextInfo sidecar (4 per cache line) because it too is
written on every markUsed/markLooped. (Field-level layout:
Internals.)
To walk the graph from a segment s: successors are
seg_next[s].next_start..next_end indexed into next_targets (reachable only;
all_next_targets includes dead edges); predecessors are
seg_prev_start[s]..seg_prev_end[s] into prev_targets; back-edges live in
looped_targets. A reference's seg_id (on ReferenceTable) is a direct u32
index into these segment slices (NONE_SEG = maxInt(u32) means no active code
path), so references[i].seg_id → seg_*[seg_id] connects a reference to its
segment.
A new segment is reachable iff any of its predecessors is reachable
(newNextSegment); newUnreachableSegment creates an explicitly-dead segment.
For unreachable segments, buildCollapsedPrev runs a BFS back through
unreachable predecessors to collect the nearest reachable ancestors, using a
per-segment generation counter to dedup in O(work) rather than O(N²) — this lets
consumer rules (e.g. no-useless-return) avoid recursive walks at runtime.
ForkContext holds the parallel segment slices at a branch point. To avoid an
allocation per branch, it inlines the first FC_INLINE_CAP (= 2) slices and
spills the rest to a heap ArrayListUnmanaged — most fork contexts hold ≤ 2
entries before being discarded. reachable() reports whether any segment in the
head slice is reachable (consulted by try/catch to decide whether the catch
entry is live).
-
Choice (
if/else,&&/||/??,?:): aChoiceContextcarries atrue_forkandfalse_fork; the post-branch segment merges them. The short-circuit operators are driven by thelogical_*events and ternaries by thecond_*events. -
Loop (
LoopContext): records the continue destination and creates a back-edge from the body end (or continue path) back to the loop entry viamarkLooped. The loop "entry node" each iteration follows ESLint'sisLoopingTarget:while→ condition,do-while→ body,for→ update ‖ test ‖ body,for-in/for-of→ binding (loopingTargetNodeinevent_resolver.zig). -
Try (
TryContext): tracksreturned_fork,thrown_fork, andtry_end_fork; the finally block, when athrown_forkis non-empty, is built for both the normal and exception lanes. -
Switch (
SwitchContext):pushSwitchContext/popSwitchContextmodel the case/default fan-out and fall-through (makeSwitchCaseBody); a switch fork usesChoiceKind.switch_kind. (Relevant tono-fallthrough-style rules.)
Like ESLint, the builder emits a CFG event stream
(EventType: codepath_start/codepath_end, seg_start/seg_end,
unreachable_seg_start/unreachable_seg_end, seg_loop; with an EventPhase
of enter/exit/post/after_enter). A consumer can replay these to receive
the equivalent of ESLint's onCodePathStart / onCodePathSegmentStart / … rule
callbacks.
CodePathBuilder.Result is fully SoA: the segment fields are parallel slices,
plus seg_reachable, seg_next, the codepaths, the CFG events, and the flat
adjacency target pools that the per-segment ranges index into. Two performance
properties matter to a consumer: adjacency lives in flat target pools (no
per-segment lists), and finish() transfers the builder's arena into the
Result rather than copying the arrays out. With Options.cfg_pool_alloc, the
pools are bump-allocated from a caller buffer so a serializer can publish their
offsets without a copy. (Full field list:
Internals.)
During the CFG walk the resolver records, per .reference event, the current
segment ID and whether the path is alive (ref_event_seg_ids /
ref_event_alive). combineParts then writes each reference's seg_id and,
for dead references, sets node_reachable[node] = 0. The semantic post-pass
computeLoopBodyExitability consumes the result plus the AST to fill
loop_exit_reachable and to propagate statement-level deadness after terminators
and infinite empty loops (see
Semantic Analysis).
es-parser — MIT licensed. This wiki documents the implementation under src/; when a detail matters, the source is authoritative.