Skip to content

fix(l4): join the SDG port lattice to the statement ddg - #230

Merged
rahlk merged 1 commit into
mainfrom
fix/issue-227-connect-sdg-port-layer
Sep 7, 2026
Merged

fix(l4): join the SDG port lattice to the statement ddg#230
rahlk merged 1 commit into
mainfrom
fix/issue-227-connect-sdg-port-layer

Conversation

@rahlk

@rahlk rahlk commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

Closes #227

What

SdgPortEdges adds the four edge classes that were missing between the port lattice and the statement graph — the same shape codeanalyzer-python#115 and codeanalyzer-typescript#80/#81 landed:

edge meaning
@formal_in:k → use mirrors the @entry definition of parameter k
return → @formal_out the statements that hand a value back
statement → <call>/actual_in:i the statement that evaluates the arguments
<call>/actual_out → statement the statement that consumes the result

Each is a re-rooting of something the statement ddg already says, so the pass is pure and engine-free. It runs after SummaryPass, so summaries are still computed from the statement-level ddg alone.

A call's own body node carries no dependence edges, so actuals attach to the innermost statement containing the call, or to the call itself for a bare-call statement (foo(x);, where statement and call share an addressing anchor). The attachment is deliberately coarse — every actual of a site is fed by the one statement containing it, not by the reaching definition of that particular argument. L4 may add reach, never drop it, and the precise binding is recoverable from the statement's own incoming edges.

No contract change

No new node kind, edge type or field; prov stays the closed {ssa, points-to} enum. The graph contract holds at 2.0.0.

Verified — daytrader8 at -a 4

before after
ddg edges touching a port 0 5,108
formal_in with out-degree 0 1,176 of 1,176 140 of 1,176
total ddg 5,475 10,583

The 140 that remain are parameters no statement reads — investmentReturn, orderCompleted and pingTwoPhase are single-throw bodies, and removeHolding never reads orderID.

Output validates against analysis.v2.schema.json; two runs are byte-identical; every new endpoint is a body node of its own callable, so all 5,108 reach the Neo4j projection.

./gradlew test: 514/515 pass, 4 skipped. The one failure is CodeAnalyzerIntegrationTest ("Could not find a valid Docker environment"), which fails identically on main here.

Known gap

Only the AST L3 engine roots parameter flow at @entry; --l3-engine wala keeps NormalStatement endpoints only, so it has no such edge to mirror and its formal_in ports stay unattached. The other three classes are engine-independent.

Propagation

  • codeanalyzer-python / codeanalyzer-typescript — none: both already landed this shape; Java was the last of the three.
  • codeanalyzer-schema — none: emission only, contract unchanged.
  • python-sdk — follow-on when its codeanalyzer-java pin moves to the release carrying this: the port-edge probe passes on its own, so the gate on slice_forward/paths_between/flows_to_call/flows_to_argument can lift with no SDK code change, and any committed Java -a 4 fixtures need regenerating (ddg roughly doubles).
  • Stacking note: with fix(ddg): drop dependence edges whose endpoint is not a body node #229 (issue ddg edges reference endpoints that are not body nodes of their own callable #228) merged, the daytrader8 total is 10,493 rather than 10,583 — the 90 dangling points-to edges that PR drops.

The L4 ports and the param_in/param_out/summary edges between them were
emitted, but no dependence edge ever touched a port: a formal_in had
out-degree zero, so the SDG was two disjoint graphs. Every interprocedural
value question then degenerated to an empty answer indistinguishable from
'no flow exists', which is why the SDK refuses slice_forward, paths_between,
flows_to_call and flows_to_argument on a Java graph today.

SdgPortEdges adds the four missing edge classes, each a re-rooting of
something the statement graph already states, so the pass stays pure and
engine-free:

  @formal_in:k -> use          mirrors the @entry definition of parameter k
  return -> @formal_out        the statements that hand a value back
  statement -> actual_in:i     the statement that evaluates the arguments
  actual_out -> statement      the statement that consumes the result

A call's own body node carries no dependence edges, so the actuals attach to
the innermost statement containing the call — or to the call itself for a
bare-call statement, where the two share an addressing anchor. That
attachment is deliberately coarse: L4 may add reach, never drop it, and the
per-argument binding is recoverable from the statement's own incoming edges.

Runs after SummaryPass, so summaries are still computed from the
statement-level ddg alone.

No schema surface changes: no new node kind, edge type or field, and prov
stays the closed {ssa, points-to} enum, so the graph contract holds at 2.0.0.

On daytrader8 at -a 4: 0 -> 5,108 ddg edges touching a port, and formal_in
ports with out-degree zero fall from 1,176 to 140 — all 140 parameters that
no statement reads (methods whose body is a single throw, and one unread
argument). Output validates against analysis.v2.schema.json and two runs are
byte-identical.

Closes #227
@rahlk
rahlk merged commit c3ba6de into main Sep 7, 2026
@rahlk
rahlk deleted the fix/issue-227-connect-sdg-port-layer branch September 7, 2026 04:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

L4 port lattice is disconnected from the statement ddg: a formal_in has out-degree zero

1 participant