Skip to content
12 changes: 7 additions & 5 deletions .claude/SCHEMA_DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,13 @@ found not to). Expose `--precision {rta,0-cfa,0-1-cfa}`. Coarse heap precision
conservative but sound-leaning semantic `ddg`.

### D7 — L4 summary edges: own summary pass
Compute `summary` (actual_in→actual_out) edges via a dedicated pass — hammock regions
composed bottom-up over the SCC-condensation DAG (Tarjan), k-limited to a monotone
fixpoint — mirroring `codeanalyzer-python` (`summaries.py`/`scc.py`). WALA's HRB
summaries are lazily computed inside its Slicer and not cleanly exposable. Heaviest
L4 unit; lands last.
Compute `summary` (actual_in→actual_out) edges via a dedicated pass — composed
bottom-up over the SCC-condensation DAG (Tarjan), k-limited to a monotone fixpoint.
The Python pilot operates at statement granularity, not via region decomposition;
region decomposition remains an open refinement for either analyzer. Both persist
`cfg` and `cdg` on the callable and compute post-dominators; what remains is the
region decomposition itself. WALA's HRB summaries are lazily computed inside its
Slicer and not cleanly exposable. Heaviest L4 unit; lands last.

### D8 — Identity: `can://java/<app>/<file>/<type>/<signature>`
Java analog of the pilot's `can://python/…`; built from the existing `signatureOf()`.
Expand Down
359 changes: 352 additions & 7 deletions docs/design/plans/2026-08-28-l3-entry-defs.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions docs/design/specs/schema-v2-l3-l4-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ Recorded in [`.claude/SCHEMA_DECISIONS.md`](../../../.claude/SCHEMA_DECISIONS.md
| D4 | Type kinds | **Single `kind`** ∈ `class\|interface\|enum\|record\|annotation` + `nesting:{parent?,is_local?}` | Replaces the `is_interface/is_enum/is_record/is_nested/...` boolean pile. |
| D5 | L3 CFG engine & granularity | **WALA engine → project to source-statement `line:col` nodes** | WALA computes SSACFG/dominance/def-use (heap-ready for L4); project each SSA instruction to its enclosing source statement via `IMethod.getSourcePosition` + JavaParser statement spans. **Fallback:** if source-fidelity proves unresolvable, revisit hand-building the CFG from the JavaParser AST (how Python/TS/Go do it). |
| D6 | L4 points-to precision | **RTA default + `--precision {rta,0-cfa,0-1-cfa}`** | RTA is proven to scale (0-1-CFA was found not to); coarse heap precision ⇒ conservative semantic `ddg`. Precision tunable per project. |
| D7 | L4 summary edges | **Own summary pass** (region/bottom-up over the SCC condensation, k-limited) | Parity with `codeanalyzer-python` (`summaries.py`/`scc.py`); keystone-conformant. Heaviest L4 unit; lands last. |
| D7 | L4 summary edges | **Own summary pass** (bottom-up over the SCC condensation, k-limited, monotone fixpoint) | Bottom-up composition with monotone fixpoint over SCC-condensation DAG, mirroring the Python pilot's approach (which operates at statement granularity, not region decomposition); keystone-conformant. Heaviest L4 unit; lands last. |
| D8 | `can://` scheme for Java | `can://java/<app>/<file>/<type>/<signature>` | Java analog of the pilot's `can://python/…`; built from the existing `signatureOf()`. |
| D9 | Neo4j namespace | Keep the **`J_`** relationship prefix | Existing convention (`J_CALLS`, …); dual-label `JSymbol` merge pattern already present. |

Expand Down Expand Up @@ -148,7 +148,7 @@ Global/static state modeled as **extra** formal/actual vertices (rides the same

**Semantic DDG:** `DataDependenceOptions.FULL` + `ModRef` yields alias/heap-derived def-use, emitted as **additional** `ddg` edges tagged `prov:["points-to"]`. L3's `prov:["ssa"]` edges are untouched — this preserves `L3 ⊆ L4` (weak-update / over-approximate posture; no strong updates that would remove an edge).

**Summary pass (D7):** a dedicated pass mirroring `codeanalyzer-python` — hammock-region summaries composed bottom-up over the SCC-condensation DAG (Tarjan), k-limited, iterated to a monotone fixpoint within each SCC. Produces the `summary` (actual_in→actual_out) edges that make later SDK slicing/taint context-sensitive without re-descending into callees. Heaviest unit; sequenced last.
**Summary pass (D7):** a dedicated pass composing bottom-up over the SCC-condensation DAG (Tarjan), k-limited, iterated to a monotone fixpoint within each SCC. The Python pilot reaches its transfer relation at statement granularity rather than via region decomposition; region decomposition remains an open refinement for either analyzer. Both analyzers persist `cfg` and `cdg` on the callable and compute post-dominators; what remains to implement is the region decomposition itself. Produces the `summary` (actual_in→actual_out) edges that make later SDK slicing/taint context-sensitive without re-descending into callees. Heaviest unit; sequenced last.

**Cost controls:** flag-gated (nothing at L4 runs unless `-a 4`); k-limiting mandatory for termination; summaries content-hashed/cached with recorded dependency metadata (incremental re-analysis aspirational); parallel-by-construction wavefront over the SCC DAG, `-j N` byte-identical to `-j 1`.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,10 @@ public JCallable build(
// cfg/cdg/ddg overlays. The BlockStmt and symbol solver are live only here.
// Skipped under the wala engine — L3WalaOverlays handles that path post-build.
if (ctx.getAnalysisLevel() >= 3 && "ast".equals(ctx.getL3Engine())) {
L3Overlays.L3Result l3 = L3Overlays.build(b, callable.getBody(), ctx, ctx.getGraphFieldDepth());
L3Overlays.L3Result l3 = L3Overlays.build(b, callable.getBody(), ctx,
ctx.getGraphFieldDepth(),
callable.getParameters().stream().map(JParameter::getName)
.collect(Collectors.toList()));
callable.setBody(l3.body());
callable.setCfg(l3.cfg());
callable.setCdg(l3.cdg());
Expand Down
13 changes: 12 additions & 1 deletion src/main/java/com/ibm/cldk/syntactic_analysis/L3Overlays.java
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,22 @@ public List<JDdgEdge> ddg() {
}
}

/** Existing callers keep working; a callable with no declared formals behaves exactly as before. */
public static L3Result build(BlockStmt body, Map<String, JBodyNode> existingBody, L1BuildContext ctx,
int fieldDepth) {
return build(body, existingBody, ctx, fieldDepth, List.of());
}

/**
* @param formals the callable's declared parameter names, in declaration order — passed through to
* {@link DdgBuilder#build(ControlFlowGraph, int, List)} so a formal is defined at {@code @entry}
* and its own dataflow can root a dependence at a parameter.
*/
public static L3Result build(BlockStmt body, Map<String, JBodyNode> existingBody, L1BuildContext ctx,
int fieldDepth, List<String> formals) {
ControlFlowGraph cfg = CfgBuilder.build(body, existingBody, ctx);
List<JCdgEdge> cdg = CdgBuilder.build(cfg);
List<JDdgEdge> ddg = DdgBuilder.build(cfg, fieldDepth);
List<JDdgEdge> ddg = DdgBuilder.build(cfg, fieldDepth, formals);
return new L3Result(cfg.nodes(), cfg.toCfgEdges(), cdg, ddg);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ public int hashCode() {
}
}

/** Existing callers keep working; a callable with no declared formals behaves exactly as before. */
public static List<JDdgEdge> build(ControlFlowGraph cfg, int fieldDepth) {
return build(cfg, fieldDepth, List.of());
}

/**
* Compute the data-dependence edges for one callable, in three phases:
*
Expand All @@ -85,8 +90,13 @@ public int hashCode() {
*
* Edges are deduped and sorted for determinism; each is one def→use pair for a {@code fieldDepth}-
* limited access path.
*
* @param formals the callable's declared parameter names, in declaration order. They are defined
* at {@code @entry}: a formal has no defining statement, but it is live on entry, and without
* that def no dependence can root at a parameter — which is what forced downstream consumers
* to recover parameter flow by matching source text.
*/
public static List<JDdgEdge> build(ControlFlowGraph cfg, int fieldDepth) {
public static List<JDdgEdge> build(ControlFlowGraph cfg, int fieldDepth, List<String> formals) {
// Phase 1: per-node gen sets (defs) and the paths each node reads (uses).
List<String> nodes = reachableFromEntry(cfg);
Map<String, Set<String>> defs = new HashMap<>();
Expand All @@ -95,6 +105,11 @@ public static List<JDdgEdge> build(ControlFlowGraph cfg, int fieldDepth) {
Set<String> d = new LinkedHashSet<>();
Set<String> u = new LinkedHashSet<>();
collect(cfg.astNode(n), d, u, fieldDepth);
if (ControlFlowGraph.ENTRY.equals(n)) {
// A formal's access path is its bare name — a base segment, which AccessPath never
// truncates, so this is k-independent by construction.
d.addAll(formals);
}
defs.put(n, d);
uses.put(n, u);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import com.ibm.cldk.schema.JParameter;
import com.ibm.cldk.schema.JType;
import com.ibm.cldk.schema.Span;
import com.ibm.cldk.syntactic_analysis.controlflow.ControlFlowGraph;
import java.nio.charset.StandardCharsets;
import java.util.ArrayDeque;
import java.util.ArrayList;
Expand Down Expand Up @@ -36,13 +37,15 @@
* <p>The relation is derived <em>syntactically</em>, in the weak-update (may-flow, never-drop)
* posture the L4 design takes: a parameter reaches the return if the callable's {@code ddg} carries
* it there, if a call site passes it on and that callee's own summary returns it, or if the
* parameter is simply <em>named</em> in a body node's source text. That last rule is what makes the
* pass useful at all on ordinary code, because {@link DdgBuilder} gives a parameter no defining node
* — nothing in the {@code ddg} is ever rooted at one. Without it {@code int c(int z) { return z - 3;
* }} has no def-use to follow at all, and {@code int m(int q) { int t = q; return t; }} has only
* {@code (decl → ret, var "t")}, which no seed reaches. The cost is counting a parameter merely
* parameter is simply <em>named</em> in a body node's source text. That last rule is what made the
* pass useful at all on ordinary code back when {@link DdgBuilder} gave a parameter no defining node,
* so nothing in the {@code ddg} could be rooted at one: {@code int c(int z) { return z - 3; }} had no
* def-use to follow at all, and {@code int m(int q) { int t = q; return t; }} had only
* {@code (decl → ret, var "t")}, which no seed reached. The cost is counting a parameter merely
* <em>mentioned</em> in a statement as flowing out of it. Over-approximating there is the accepted
* trade.
* trade. {@code DdgBuilder} now defines the formals at {@code @entry}, so the ddg rule reaches these
* shapes on its own and the two text rules have been measured redundant on both corpora; retiring
* them is a separate change.
*/
public final class SummaryPass {

Expand Down Expand Up @@ -204,11 +207,12 @@ private static Fn facts(JCallable c, byte[] source) {
}

/**
* Where a parameter's value is visible, syntactically: the def end of any {@code ddg} edge whose
* access path is rooted at it, any call-site {@code actual_in} vertex whose argument text names
* it, and <em>every</em> body node whose source text names it (see the class javadoc — a parameter
* has no defining node in the {@code ddg}, so text is the only thing that can put it on the map at
* all).
* Where a parameter's value is visible, syntactically: an end of any {@code ddg} edge whose
* access path is rooted at it — the def end for an ordinary edge, the <em>use</em> end for one
* rooted at {@code @entry} (see the comment on that rule) — any call-site {@code actual_in}
* vertex whose argument text names it, and <em>every</em> body node whose source text names it
* (see the class javadoc for why the two text rules exist and why they now have nothing left to
* add).
*
* <p>Every kind with a span is seedable, containers included. A {@code branch}/{@code loop}/
* {@code switch} span swallows the statements nested inside it, so seeding one on a name mentioned
Expand Down Expand Up @@ -241,7 +245,37 @@ private static Set<String> seedsFor(Fn fn, String name, Map<String, String> span
if (ddg != null) {
for (JDdgEdge e : ddg) {
if (name.equals(base(e.getVar()))) {
seeds.add(e.getSrc());
// For an entry-rooted edge, the *use* end — the obvious `e.getSrc()` is wrong
// there. A seed is a node id, and the graph `facts` builds is keyed on node
// identity alone (it discards `var`), so seeding a def node also hands this
// parameter the out-edges of every *other* variable defined at that node.
// `@entry` is where that bites hardest: `DdgBuilder` defines all of a callable's
// formals at that one synthetic node, so `getSrc()` collapses every parameter's
// seed set to `{@entry}` and lets one parameter's reach credit them all — 43
// spurious summary edges on daytrader8, every one of them on a callable of arity
// greater than 1. The dst is where *this* parameter's value is observed, which
// is exactly what the `var`-filtered edge licenses and no more.
//
// A real def site keeps its `src`. The same conflation happens there in
// miniature, but it is the bounded may-flow over-approximation this pass already
// accepts. Doing this unconditionally was measured against the pre-change
// baseline and found to cost three daytrader8 edges at
// `TradeDirect.completeOrder(Connection, Integer)`, all under the `var` label
// `orderID` — but that label is nominal, not semantic: those edges are rooted at
// the node defining `orderData` and reach a `getOrderID()` call on it, so `var`
// names the *field* `OrderDataBean.orderID`, not this callable's own parameter of
// the same spelling. The parameter reaches the return, if at all, only through
// JDBC that neither this engine nor WALA models, so the three are a conflation of
// the same class this fix removes, not a real flow it would drop.
//
// The scoping still stops at `@entry`, though — for a narrower reason than "it
// would cost something real". This branch is a regression fix: its job is
// restoring the baseline it perturbed, not auditing every other instance of this
// same over-seeding. And the L4 posture forbids under-approximation on evidence
// this thin — widening `src` to `dst` for every rule-1 edge, not just entry-rooted
// ones, is measured safe on only two corpora, not enough to rule out dropping a
// real flow somewhere neither has been run. That burden belongs to a follow-on.
seeds.add(ControlFlowGraph.ENTRY.equals(e.getSrc()) ? e.getDst() : e.getSrc());
}
}
}
Expand Down
15 changes: 14 additions & 1 deletion src/test/java/com/ibm/cldk/schema/L3DifferentialGateTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import com.github.javaparser.StaticJavaParser;
import com.github.javaparser.ast.body.MethodDeclaration;
import com.github.javaparser.ast.stmt.BlockStmt;
import com.github.javaparser.ast.stmt.Statement;
import com.ibm.cldk.CodeAnalyzer;
Expand Down Expand Up @@ -214,7 +216,18 @@ void differentialGateCfgCdgAgreeDdgDeltaReported(@TempDir Path tmp) throws Excep
// ---- AST engine (reference oracle) --------------------------------------------------
ControlFlowGraph astG = CfgBuilder.build(body, new LinkedHashMap<>(), ctx);
List<JCdgEdge> astCdg = CdgBuilder.build(astG);
List<JDdgEdge> astDdg = DdgBuilder.build(astG, 3);
// Every TARGET_METHODS entry takes at least one parameter, so building the reference
// oracle's ddg without formals (the legacy two-arg overload) would leave it blind to the
// @entry-rooted edges the shipped engine now emits — exactly the divergence this gate
// exists to compare against WALA. Extract the formals the same way
// DdgBuilderEntryDefsTest.ddgOf does and use the three-arg overload instead.
MethodDeclaration astMethod = StaticJavaParser.parse(FIXTURE)
.findAll(MethodDeclaration.class).stream()
.filter(m -> m.getNameAsString().equals(methodName))
.findFirst().orElseThrow();
List<String> formals = astMethod.getParameters().stream()
.map(p -> p.getNameAsString()).collect(Collectors.toList());
List<JDdgEdge> astDdg = DdgBuilder.build(astG, 3, formals);

// ---- WALA engine -------------------------------------------------------------------
WalaAnalysis.MethodIr mir = findMethod(wala, methodName);
Expand Down
Loading