-
Notifications
You must be signed in to change notification settings - Fork 0
Control Flow Analysis
The control-flow graph is a Zig port of ESLint's CodePathAnalysis
(code_path.zig:3), and it runs off the same event stream as the rest of the
semantic phase. The parser's branch_*, loop_*, try_*, switch_*,
logical_*, cond_*, and terminator events drive the builder directly — there
is no separate AST walk. The result lets a lint rule answer "can control reach
this node?" and "does this loop body always exit on the first iteration?"
The graph is built from segments — maximal straight-line runs — stored as a
MultiArrayList and addressed by seg_id used as a direct index
(code_path.zig:360). A segment is nine u32 columns:
Segment (36 bytes = 9 × u32) (code_path.zig:28)
codepath
all_prev_start / all_prev_end all predecessors
prev_start / prev_end reachable predecessors only
looped_prev_start / looped_prev_end loop back-edges
collapsed_prev_start / collapsed_prev_end reachable-ancestor closure
Three of the four prev-edge ranges are written once at creation and never change.
looped_prev is the exception: markLooped writes the back-edge from a loop's
end to its head after the head segment already exists (code_path.zig:747). Forward
adjacency (next) lives in a separate 16-byte SegNextInfo sidecar so a forward
walk does not drag the whole segment through cache (code_path.zig:49).
The builder mirrors ESLint's state objects (code_path.zig:8); each maps to a syntactic construct:
CodePath a function / program / static-block / field-initializer body (code_path.zig:68)
ForkContext parallel segment lists during a fork (inline cap 2) (code_path.zig:109)
ChoiceContext if/else, &&, ||, ??, reused for loop & switch tests (code_path.zig:299)
LoopContext loop continue/break targets (code_path.zig:337)
TryContext try / catch / finally lanes (code_path.zig:316)
SwitchContext switch case bodies (code_path.zig:310)
ChoiceKind is test_kind, logical_and, logical_or, nullish, loop,
switch_kind (code_path.zig:297). The builder keeps the live choice / loop / try
/ switch contexts plus the current fork context and code path
(code_path.zig:404).
The resolver's per-event switch (under do_cfg) maps each event to a builder
call (event_resolver.zig):
terminator → makeReturn / makeThrow / makeBreak[Labeled] / makeContinue[Labeled]
and sets cfg_alive = false for the code after it (:690)
loop_open / loop_test_end / loop_body_end / loop_close
→ pushLoopContext / setLoopContinueDest / makeLoopBackEdge /
popLoopContext (:994)
try_open / try_catch_start / try_finally_start / try_close
→ pushTryContext / makeCatchBlock / makeFinallyBlock /
popTryContext (:1007)
switch_open / switch_case_start / switch_close
→ pushSwitchContext / makeSwitchCaseBody / popSwitchContext (:1028)
logical_* / cond_*
→ pushChoiceContext / makeLogicalRight |
makeConsequent · makeConditionalAlternate / popChoiceContext (:1040)
The branch_* events themselves maintain the resolver's cfg_alive liveness; the
actual if/else fork is driven by the choice-context arms.
Each segment carries one reachability byte in a hot sidecar. The entry segment is
seeded reachable (code_path.zig:574); every other segment is reachable iff any
predecessor is — newNextSegment ORs over the prevs and createSegment records
the result (code_path.zig:586, :650). This is one forward computation, not a
fixpoint: it is exact because segments are created in forward order, so a
segment's predecessors are finalized before it (a loop head's reachability is
fixed by its entry edge before markLooped adds the back-edge). A terminator
creates an explicit unreachable successor for the code after it, and for an
unreachable segment buildCollapsedPrev inherits each predecessor's
already-computed reachable-ancestor set (code_path.zig:633). Separately, the
resolver marks dead AST nodes through node_reachable (event_resolver.zig:695).
CodePathBuilder.Result (code_path.zig:1772) is a flat SoA snapshot — the
per-segment columns, the adjacency target pools, the code-path pools, and the
emitted event list — plus the builder's ArenaAllocator moved in wholesale.
Moving the arena instead of copying each array avoids a large per-array memcpy;
Result.deinit just frees the arena. finish first calls flattenCpPools to
interleave each code path's return/throw lists into contiguous ranges; its only
failure mode is arena OOM, surfaced as a panic because the caller already had to
handle OOM while building (code_path.zig:1890).
es-parser — MIT licensed. This wiki documents the implementation under src/; when a detail matters, the source is authoritative.