fix(dataflow): connect the L4 port lattice to the statement ddg - #169
Merged
Conversation
Measured on dataflow-app at -a 4 (33 callables; 24 formal_in, 20 actual_in, 21
actual_out vertices), the ddg edges touching a port were:
statement -> formal_out 31
formal_in -> * 0
* -> actual_in 0
actual_out -> * 0
One of four binding classes. The port lattice was connected ACROSS callables
(param_in 21, param_out 22) but not INTO the statement-level dataflow on either
side of a call, so an end-to-end walk could cross a call only on the return leg:
a caller's definition never reached actual_in, actual_out never reached a use, and
a walk entering through param_in dead-ended at formal_in. #81 named the three
missing classes; #80 asked for the absence to be verified on a larger corpus first
-- done, and categorical.
Python fixed its twin (#115) by emitting the extra_edges its SDG assembler already
computed. Ours computes none, so the three classes are derived at emit time from
what is present: `formal_in:n -> first-use` re-sources every param-sourced intra
ddg edge from the port (the L3 `@entry -> use` edge stays); `def stmt ->
actual_in:k` binds a reaching variable to argument k when its head is a whole
word in that argument's text at the call site (the callable's own call_sites,
still present during the pass); `actual_out -> callsite` sends the return value
into the call statement, whose existing `L -> use` edges carry it on. All tagged
prov ["reaching-defs"], deduped, sorted.
After:
formal_in -> statement 31 statement -> actual_in 5
formal_in -> actual_in 8 actual_out -> statement 11
formal_in -> call 1 actual_out -> call 6
statement -> formal_out 31 (unchanged)
tagged, every endpoint in body; a pass-through parameter binds from its own port;
L3 ddg ⊆ L4 ddg. Mutation-checked: dropping the formal_in re-sourcing and breaking
the argument match each fail it. #80's side question -- are TS call vertices on
the CFG spine? -- is answered by the measurement: statement/entry/call edges exist,
so unlike python's they are not cfg-orphaned.
rahlk
force-pushed
the
fix/issue-81-l4-port-binding
branch
from
September 6, 2026 13:01
799f2f0 to
83cc71b
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #81, closes #80 (its twin; the "verify on a larger corpus first" it asked for is below). Python parity with codeanalyzer-python #115.
The gap, measured
dataflow-appat-a 4— 33 callables, 24formal_in, 20actual_in, 21actual_outvertices. ddg edges touching a port, before:statement → formal_outformal_in → ** → actual_inactual_out → *One of four binding classes. The lattice was connected across callables (
param_in21,param_out22) but not into the statement-level ddg on either side of a call. So an end-to-endflows_towalk crossed a call only on the return leg: a caller's definition never reachedactual_in,actual_outnever reached a use, and a walk entering throughparam_indead-ended atformal_in. Categorical on 33 callables, not a one-call-site artefact.What changes
Python's fix emits
extra_edgesits SDG assembler already computed. Ours computes none, so the three classes are derived at emit time inemitL4from facts that are present — the intra-callable ddg, thePARAM_IN/PARAM_OUTedges, CFG node spans, and the callable's owncall_sites(INTERNAL, still on the tree during the pass):formal_in:n → first-use— every param-sourced intra ddg edge, re-sourced from the port. The L3@entry → useedge stays (a param folds onto@entrybelow L4), so L3 ⊆ L4 holds.def stmt → actual_in:k— a variable reaching the call statement binds to argumentkwhen its head is a whole word in that argument's text. A parameter passed straight through binds from its own@formal_in:n.actual_out → callsite— python's class exactly: the return value flows into the statement that made the call; the existingL → useedges carry it onward.All
prov: ["reaching-defs"](the label both analyzers share), deduped on(src, dst, var, prov), sorted.After:
formal_in → statement/→ actual_in/→ callstatement → actual_inactual_out → statement/→ callstatement → formal_outVerification
y = build(x)all four classes exist,reaching-defstagged, every ddg endpoint names a body node; a pass-through parameter binds from its own port; L3 ddg ⊆ L4 ddgformal_inre-sourcing and breaking the argument match each fail the testbun testgreen; container tests run (TS_DDGrows changed); no schema, Neo4j label orSCHEMA_VERSIONchange; decisions recorded in.claude/SCHEMA_DECISIONS.mdThe ceiling, stated
def → actual_in:kbinds by whole-word text match on the argument — there is no per-argument AST at this stage. A name that is both a local and a property (f(o.v)with a localv) over-binds; a value threaded through a helper inside the argument still binds, which is the reaching-defs reading. Python has the AST and doesn't pay this; noted so it isn't mistaken for precision it doesn't have.#80's side question — are TS
callvertices on the CFG spine? — is answered by the measurement:statement → call,entry → call,call → statementedges exist, so unlike python's they are not cfg-orphaned.