Commit 0a4f01c
authored
feat(l3): intraprocedural dataflow (CFG/CDG/DDG) — AST engine (#195)
* docs(design): detailed L3 two-engine dataflow spec; link from parent §8
Add docs/design/specs/l3-intraprocedural-dataflow-design.md — the
user-facing L3 design elaborating parent §8 into two interchangeable
engines (--l3-engine ast|wala, AST default) over one shared contract,
with a differential gate. Revises D5 (WALA-engine/AST-fallback) as D28
(AST-default/WALA-opt-in) and forward-links the parent section.
The WALA engine section spells out how line:col is recovered: WALA
supplies only a line, so the column is adopted from the matched AST
statement node (line-cover match -> content disambiguation ->
no-source-position handling -> innermost fallback), never synthesized.
§4.5.1 illustrates the limitation of intraprocedural DDG without
points-to (missed and spurious heap def-use through aliases), noting it
is a property of the level and affects both engines identically; L4
points-to adds the corrected edges as prov:[points-to].
§5.3 lays out the engine trade-offs and why AST is the default while
WALA is still built. §7.1 explains why the engines are used
alternatively rather than as an overlay: CFG/CDG shapes can't be unioned
without malforming the graph, a DDG union is redundant (same ssa tier,
not complementary like L2 declared/rta), and engine is an implementation
choice kept out of prov.
* feat(l3): type cfg/cdg/ddg schema edges + L3 schema oracle
Tighten the callable's cfg/cdg/ddg arrays from bare {type:array} to
typed cfgEdge/cdgEdge/ddgEdge $defs (endpoints are body-node local ids,
not can:// ids; ddg prov is the closed enum [ssa]). Add L3SchemaOracleTest
mirroring the L2 oracle: one accept per surface plus a rejection per way
a plausible L3 payload can be malformed.
* feat(l3): cfg/cdg/ddg edge models and callable fields
Add JCfgEdge{src,dst,kind}, JCdgEdge{src,dst}, JDdgEdge{src,dst,var,prov}
(endpoints are body-node local ids) and the null-until-L3 cfg/cdg/ddg
fields on JCallable. Gson (no serializeNulls) keeps them absent from the
payload below level 3; V2Emitter needs no change.
* feat(l3): control-flow core in controlflow package + L3 orchestrator
Organize L3 by concern: controlflow package (ControlFlowGraph, CfgBuilder,
and the coming CdgBuilder — control flow and control dependence are
cohesive, CDG is derived from the CFG), with data dependence to live in a
dataflow package. L3Overlays is the orchestrator, sibling to L2CallGraph,
returning the completed body + cfg/cdg/ddg.
CfgBuilder is the structured-CFG core: a recursive link wiring straight-line
sequences today (conditionals/loops/switch/exceptions follow), seeding L1
call nodes so the additive invariant holds. Deterministic edge ordering.
No records (Java 11).
* feat(l3): wire the parse-time L3 pass, CLI flags, and level gate
Thread analysisLevel + graphFieldDepth through L1BuildContext (new
overloaded ctor) and a new L1Extractor.extractAll overload; CallableBuilder
runs L3Overlays.build at parse time when level >= 3, merging the completed
body + cfg/cdg/ddg onto the callable. CodeAnalyzer: relax the v2 gate to
allow -a 3, add --l3-engine (ast default; wala rejected as not-yet-
implemented) and --graph-field-depth, and bypass the incremental cache at
level >= 3 (the AST a warm hit would skip is needed, and L3 overlays are
not an L1 cache artifact).
Invert the CLI gate test: -a 3 now emits dataflow overlays, -a 4 fails,
--l3-engine wala is rejected.
* feat(l3): CFG return/throw to exit + bare-call node reuse
return edges to @EXIT with kind 'return' (node kind 'return'); an uncaught
throw edges to @EXIT with kind 'exception' (handler routing follows with the
try-catch work). A bare-call statement is keyed at the call anchor (invoked
name / instantiated type), so it reuses the L1 call node rather than
duplicating it — kind stays 'call', honoring the additive invariant.
* feat(l3): CFG conditionals (if/else)
Refactor link into a type dispatch: blocks recurse via linkSequence, if
becomes a branch node with true/false edges into the arms (both rejoining
at the following statement; a missing else routes false straight to it).
Extract an ensure() helper; kinds are now passed per case.
* feat(l3): CFG loops (while/for/do/for-each)
Refactor CfgBuilder to an instance so the recursion carries graph+context,
and thread the terminal edge kind through link/linkSequence (loop bodies
pass loop_back). Top-tested loops (while/for/for-each) emit a loop node with
true into the body, a loop_back from the body tail, and false to the exit;
do/while enters the body first with the test looping back from the bottom.
for init/update are folded onto the loop node (no statement-level nodes).
* feat(l3): CFG break/continue and labeled targets
Add a frame stack of enclosing loop targets; break edges to the loop exit,
continue to the loop test. Labeled loops carry their label on the frame so
break/continue <label> resolve to the right enclosing loop; a labeled
non-loop gets a break-only frame. Loops push/pop their frame around the
body.
* feat(l3): CFG switch (classic fall-through + arrow)
A switch node with a switch_case edge per case entry; classic (colon) cases
fall through to the next case's entry, arrow (->) cases do not. break inside
a case exits to the join via the switch frame; a switch with no default gets
an implicit no-match switch_case edge to the join. Labeled switch carries
its label for labeled break.
* feat(l3): CFG exceptions, try/catch/finally, try-with-resources, synchronized
An exception-handler stack routes throwing statements (calls/allocations
and explicit throw) to the enclosing catch/finally, else the method exit;
outside a try no explicit edge is emitted (normal flow already reaches
exit). try/catch/finally: normal and exceptional paths both flow through a
single finally node (line:col identity precludes literal duplication); the
try body routes to every catch entry (type dispatch over-approximated).
try-with-resources analyses as a plain try (implicit close has no source
node); synchronized carries flow through its body. Infinite loops stay
well-formed via the loop's false edge, keeping @EXIT reachable.
* feat(l3): CDG via post-dominance
Compute post-dominators with Cooper-Harper-Kennedy on the reverse CFG rooted
at @EXIT (iterative DFS postorder to bound recursion), then derive control
dependence by the Ferrante-Ottenstein-Warren rule: for a CFG edge A->B where
B does not post-dominate A, every node from B up to ipdom(A) is control-
dependent on A. Wire cdg into L3Overlays; deterministic edge order.
Tests cover if/if-else, nested-branch chaining, loop body (and that code
after the branch/loop is not dependent), early return making following code
control-dependent, switch cases, and determinism.
* feat(l3): DDG reaching-defs over k-limited access paths
AccessPath derives base(.field|[*])* spellings (arrays index-insensitive,
truncated to k). DdgBuilder runs a monotone reaching-definitions fixpoint
over the CFG: a def prefix-kills its path and extensions, a use joins to
every reaching def whose path overlaps it, edges are prov:[ssa] (syntactic,
object-insensitive; aliasing is L4). ControlFlowGraph now records the AST
statement per node so defs/uses read off the source. Wire ddg into
L3Overlays.
Tests: local def-use, reassignment kill, field access paths, array [*]
collapse, loop-carried def-use, determinism.
* feat(l3): route abrupt exits through finally blocks in the CFG
Every exit from a try (normal, catch, return/break/continue, uncaught
throw) now runs the finally first. A finally is a single node (line:col
identity precludes per-path copies) whose completion fans out to the union
of all exits' continuations — a sound over-approximation that makes the
finally post-dominate the try body, so CDG is correct. Abrupt exits reroute
through the enclosing finally chain (innermost first, nested-aware) via a
unified scope stack; ControlFlowGraph.redirect fans the finally's completion
out from a temporary sentinel with no synthetic node.
Design doc: finally semantics, the WALA parity result (javac duplicates the
finally in bytecode but the copies collapse to the same single source node
under the line:col projection, so the engines converge), the pinned
exception-edge-density divergence, and the precise-duplication route as an
alternative if the one-node-per-line:col invariant is relaxed.
* test(l3): conformance gate — CFG well-formedness, PDG slice, L2 subset, schema
A dataflow-test fixture exercising loop-carried def-use, if/else, early
return, try/catch/finally (+ return through finally), and a shadowed
variable. L3DataflowGateTest asserts: every callable's cfg is well-formed
(single @entry/@EXIT, real spans, reachable from entry); try/catch yields an
exception edge; a return inside try/finally runs the finally; the PDG
backward slice of a variable includes its defs and controlling loop but not
unrelated variables; L2 output is a subset of L3; level-3 output is
deterministic and conforms to the canonical schema.
* test(l3): real-world conformance gate over the fixture apps
Add a realworld-tagged, parameterized L3 gate that runs the AST engine
(source-only, no build) over spring-petclinic, two quarkuscoffeeshop
modules, and commons-lang, asserting every callable's cfg is well-formed
(nested/anonymous types included) and the whole level-3 payload conforms to
the canonical schema. Extract validateSchema + a recursive well-formedness
walk shared with the fixture gate. All four apps pass.
* docs(l3): record D25-D28 in the decisions ledger; fix localId wording
Add the L3 decisions to .claude/SCHEMA_DECISIONS.md (D25 local-id endpoints,
D26 two engines, D27 syntactic DDG, D28 AST-default/WALA-opt-in + finally
single-node) and mark D5 as revised by D28. Correct the design doc's schema
section: localId already exists in the schema (broader pattern), so the edge
defs reuse it rather than adding one.
* docs(l3): report — AST-engine node/edge metrics over the 10 real-world fixtures
Baseline of the L3 AST engine's cfg/cdg/ddg output across cargotracker,
commons-lang, daytrader8, plantsbywebsphere, spring-petclinic, and the five
quarkuscoffeeshop modules: per-app structure/node/edge counts plus edge- and
body-node-kind breakdowns. Structured so a second
set of columns can be added when the WALA engine (#194) lands, with the
should-match vs pinned-divergence expectations noted. Batch runner and
metrics script live under the git-ignored output/l3/.
* docs(design): consolidate Neo4j projection into one post-L4 pass (#182)
Neo4j v2 projection — base relabel + L3/L4 overlays — is now one consolidated
pass after L4 (issue #182 widened), not a per-level in-PR overlay. So the JSON
levels (L3 here, L4 next) carry no Neo4j overlay, and the v2 default-output
flip (gated on the Neo4j projection) moves to post-L4; the analyzer major can
still be cut on the JSON levels first.
* docs(l3): document dataflow builders; drop stray NUL separators
Comment pass on the thin algorithm internals (DdgBuilder reaching-defs,
CdgBuilder post-dominance/intersect). Replace two raw NUL bytes in string
literals — the CDG dedup separator (-> space) and the finally sentinel
(-> '#finally-sentinel-'). Behavior-neutral: suite green, L3 metrics unchanged.1 parent 2f88349 commit 0a4f01c
31 files changed
Lines changed: 3164 additions & 15 deletions
File tree
- .claude
- docs/design
- notes
- specs
- src
- main/java/com/ibm/cldk
- schema
- syntactic_analysis
- controlflow
- dataflow
- test
- java/com/ibm/cldk
- schema
- syntactic_analysis
- controlflow
- dataflow
- resources
- schema
- test-applications/dataflow-test
- src/main/java/org/example
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
45 | 45 | | |
46 | 46 | | |
47 | 47 | | |
| 48 | + | |
48 | 49 | | |
49 | 50 | | |
50 | 51 | | |
51 | 52 | | |
52 | 53 | | |
53 | 54 | | |
| 55 | + | |
54 | 56 | | |
55 | 57 | | |
56 | 58 | | |
| |||
370 | 372 | | |
371 | 373 | | |
372 | 374 | | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
373 | 408 | | |
374 | 409 | | |
375 | 410 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
0 commit comments