You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
L4 port lattice is disconnected from the statement ddg: a formal_in has out-degree zero #227
The level-4 port vertices (formal_in, actual_in, formal_out, actual_out) are emitted, and the lattice edges between them are emitted, but no dependence edge ever touches a port. A formal_in — a callable's entry point for a parameter, and the natural seed for "what does this argument reach" — has out-degree zero.
Measured on a full-depth Neo4j projection holding daytrader8 and ThingsBoard (3.0.1, contract 2.0.0):
Zero of 134,742 J_DDG edges and zero of 46,936 J_CDG edges have a port at either end. The lattice's only edges are J_PARAM_IN (actual_in→formal_in), J_PARAM_OUT (formal_out→actual_out) and J_SUMMARY (actual_in→actual_out), so the port layer is an island: reachable from the statement graph in neither direction.
This is not a projection artefact. The same gap is in analysis.json, on the committed -a 4 fixture and on a whole-application -a 4 run.
The sibling analyzers fixed this
codeanalyzer-python#115 — closed. Its graph now has 262,582PY_DDG edges leaving a port.
Java appears to be the last of the three with the layer disconnected.
Why it matters downstream
Interprocedural value questions are exactly what L4 exists to answer, and each of them silently degenerates. In the SDK, slice_forward from a parameter returns the seed alone, paths_between returns no paths, and flows_to_call/flows_to_argument return False — for every input, whatever the program does. An empty answer that is indistinguishable from "no flow exists" is worse than an error, so the SDK now refuses those four on a Java graph rather than answering, via a probe for whether any dependence edge touches a port. When this is fixed they will answer with no SDK change, and the probe will pass on its own.
slice_backward is unaffected and is not gated: it walks J_PARAM_IN in reverse and reaches the argument vertex at each call site.
Repro
java -jar codeanalyzer-3.0.1.jar -i <any project> -a 4 -o out
then, on out/analysis.json, count ddg edges whose src or dst is a formal_in/actual_in/formal_out/actual_out key. Zero on every project tried.
Definition of done
A parameter's formal_in has outgoing ddg to the statements that read it, and the statements that compute a returned value have ddg into formal_out — the shape codeanalyzer-python#115 landed.
Re-measured the lattice while building SDK coverage for the fix, and the definition of done above is missing two of the four crossings. Flagging before 3.0.3 goes out, because a fix that lands only the two named directions leaves the interprocedural accessors exactly as degenerate as they are today — but silently, since the SDK's probe would start passing.
What 3.0.2 emits. Every edge that touches a port is port-to-port; nothing crosses between the lattice and the statement graph. Neo4j projection holding daytrader8 and ThingsBoard (181,355 port vertices):
src kind
rel
dst kind
edges
actual_in
J_PARAM_IN
formal_in
76,810
formal_out
J_PARAM_OUT
actual_out
44,961
actual_in
J_SUMMARY
actual_out
22,222
Which gives formal_in out-degree 0, actual_inin-degree 0, formal_outin-degree 0, actual_out out-degree 0. The same four numbers hold on the committed -a 4analysis.json fixture, so it is not a projection artefact.
What the fixed sibling emits. codeanalyzer-python#115's graph carries all four crossings, not two:
Why the two omitted directions are the ones that matter for interprocedural queries. A forward walk from a parameter of A into a call A → B has to leave A's statement graph through B's actual_in vertex to cross J_PARAM_IN. With no edge into an actual_in, that vertex is unreachable, so the walk stops inside A no matter how well formal_in → statement works. Concretely, in the SDK:
slice_forward from a parameter — fixed by formal_in → statement alone. ✅
flows_to_call, flows_to_argument, paths_between — still False / [] for every input, because their target set is a callee's formal_in vertices and no path can reach one. ❌
And the probe stops catching it. The SDK refuses those accessors on a measurement of the data — "does any formal_in have an outgoing dependence edge". A formal_in-only fix satisfies that probe, so the refusal lifts and three accessors go back to answering a uniform False that is indistinguishable from a proved absence of flow. That is the failure this issue was filed to end, so it is worth closing all four crossings in one release rather than two now and two later.
Suggested addition to the definition of done:
the value computed for a call argument has ddg into that call's actual_in, and a call's actual_out has ddg into the statements that read the result — the other half of the shape codeanalyzer-python#115 landed.
Happy to re-run both measurements against 3.0.3 and post the table.
Activity
fix(l4): join the SDG port lattice to the statement ddg (#230)
rahlk commentedon Sep 7, 2026
Re-measured the lattice while building SDK coverage for the fix, and the definition of done above is missing two of the four crossings. Flagging before 3.0.3 goes out, because a fix that lands only the two named directions leaves the interprocedural accessors exactly as degenerate as they are today — but silently, since the SDK's probe would start passing.
What 3.0.2 emits. Every edge that touches a port is port-to-port; nothing crosses between the lattice and the statement graph. Neo4j projection holding daytrader8 and ThingsBoard (181,355 port vertices):
actual_inJ_PARAM_INformal_informal_outJ_PARAM_OUTactual_outactual_inJ_SUMMARYactual_outWhich gives
formal_inout-degree 0,actual_inin-degree 0,formal_outin-degree 0,actual_outout-degree 0. The same four numbers hold on the committed-a 4analysis.jsonfixture, so it is not a projection artefact.What the fixed sibling emits. codeanalyzer-python#115's graph carries all four crossings, not two:
actual_informal_in→ statement/call/return/branch/raise/loopformal_outactual_out→ statement/call/branch/return/loopWhy the two omitted directions are the ones that matter for interprocedural queries. A forward walk from a parameter of
Ainto a callA → Bhas to leaveA's statement graph throughB'sactual_invertex to crossJ_PARAM_IN. With no edge into anactual_in, that vertex is unreachable, so the walk stops insideAno matter how wellformal_in → statementworks. Concretely, in the SDK:slice_forwardfrom a parameter — fixed byformal_in → statementalone. ✅flows_to_call,flows_to_argument,paths_between— stillFalse/[]for every input, because their target set is a callee'sformal_invertices and no path can reach one. ❌And the probe stops catching it. The SDK refuses those accessors on a measurement of the data — "does any
formal_inhave an outgoing dependence edge". Aformal_in-only fix satisfies that probe, so the refusal lifts and three accessors go back to answering a uniformFalsethat is indistinguishable from a proved absence of flow. That is the failure this issue was filed to end, so it is worth closing all four crossings in one release rather than two now and two later.Suggested addition to the definition of done:
ddginto that call'sactual_in, and a call'sactual_outhasddginto the statements that read the result — the other half of the shape codeanalyzer-python#115 landed.Happy to re-run both measurements against 3.0.3 and post the table.