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
engine: a function placed in an array or table and invoked by iteration has no caller, so everything downstream of it loses its callers too #967
A pipeline registers its steps as data and then calls them by position:
const steps = [stepA, stepB, stepC] // the registration
for (const s of steps) s(node, ctx) // the invocation
Neither line produces a call edge. The registration is a reference, not a call; the invocation names an element, not a declaration. So stepB has no caller, and — this is the part that costs — every declaration stepB reaches inherits that silence. The graph holds both outer edges of a chain and not the one joining them.
Measured on a compiler codebase. A change to a helper, and three test files in the fixing commit:
test -> transform — resolved
transform -> the pipeline step — ABSENT, the step is an element of a nodeTransforms array called by index
the step -> the changed helper — resolved
impact <helper> --tests therefore answers "0 of 3419 test methods reach the change" while naming, correctly, the two source files that call it. A test-selection built on that answer picks one of the three files it needed. The tests are indexed — the file has 68 symbols and 602 resolved outgoing edges — so this is not a coverage or indexing gap, it is the one hop.
Worth stressing that the silence is load-bearing in the wrong direction: an empty answer reads as "nothing depends on this", which is the most dangerous thing a change-impact tool can say incorrectly.
Shapes this covers, all of which register callables as data and dispatch by iteration or lookup: transform and visitor pipelines, middleware chains, plugin lists, reducer and handler maps, command registries, and anything a decorator inserts into a module-level table.
Idea. A callable named in an array or object literal that is itself iterated or indexed is a call from whoever performs the iteration, at lower certainty than a direct call. The existing certainty ladder already has a rung for this kind of claim — it does not have to be reported as resolved to be reported at all. Adjacent work exists for a table read with a computed key; this is the same family with the key being a position rather than a name.
Until an edge exists, a consumer cannot distinguish "nothing reaches this" from "the one hop that would is not modelled", which is worth a distinguishing signal on the answer itself.
A minimal reproduction is on skill/test-impact as tests/cases/typescript/pipeline-array-dispatch — three files, and the two symptoms are exact: impact measure --tests answers "0 of 1 test method(s)" and path run widenRange answers "no chain of resolved calls".
A second language shows the same shape: on a 23-file Python framework service, a dict of handlers dispatched by lookup, a TRANSFORMS list invoked by iteration, and a signal emit loop each lose their tests the same way, measured against behavioural truth rather than gold files.
Sharper statement of the boundary, from Python data on a framework service plus three Java fixtures — the line is not "dynamic dispatch", it is whether a key is written at both ends.
Recovered, because the registration carries a name the dispatch also writes:
@receiver("order_created") paired with emit("order_created") — joined on the key
@exporter("csv") paired with export(order, "csv") — joined on the key
@MethodSource("cases") in JUnit — the provider is named by a string in the annotation, and it resolves
a fixture in an abstract base reaching the change on a subclass test's behalf
Not recovered, because the registration carries no key at all:
TRANSFORMS = [strip_blank_lines, normalise_skus] iterated by clean() — the function is an ELEMENT, and its position is the only thing identifying it
nodeTransforms: [transformFor, …] called by index
a dict subscript such as EXPORTERS[kind](order) — recoverable only when a DECORATOR wrote the key; the subscript itself is silent
So register-by-NAME works today and register-as-DATA does not, and the difference is that a name survives into the dispatch site while a position does not. That is a narrower and more actionable target than the issue title implies: the missing edge is from whoever performs the iteration to every callable the literal holds.
The three passing Java cases are now regression guards on the skills branch (#975), so the boundary is asserted in both directions rather than only described.
Reproduced as a case in this repository, scored against the TypeScript compiler's own
answer (checker.getResolvedSignature, through tools/tsc_oracle_case.mjs), so the
truth side is an independent authority and not the engine restating itself.
graph/test/typescript/cases/64-array-dispatch holds four dispatch shapes over the
same four functions:
oracle=7 engine=4 agree=4 extra=0, and the three missing edges are:
dispatch
reaches
resolved
for..of over an array literal
widenRange
no
steps[0](node), indexed read
widenRange
no
byName.square(node), keyed by name
squareRange
no
called by name
narrowRange
yes
Each step's own call to measure resolves, so the far half of every chain is present
and only the dispatch hop is missing. Nothing is over-resolved.
Two of these do not match the boundary stated earlier in this thread.
An indexed read is not recovered, and this engine has nothing that could recover
it. The clause for a COMPUTED_CALL whose receiver is an array, reading elem_value, exists only in the JavaScript engine, at graph/javascript/engine/expression-resolution/callee-resolution.dl:61. The
TypeScript callee-resolution.dl carries no COMPUTED_CALL clause and no elem_value at all. Porting it is not sufficient on its own: the golden records steps[0](node) at pipeline.ts:24 as a METHOD_CALL, so the classification has to
move with the rule.
A name-keyed object literal is not recovered either. The earlier comment put the
line at "whether a key is written at both ends", with register-by-NAME on the working
side. That holds for the decorator and annotation registries it lists, where the key
is an argument the parser records. It does not hold for a plain object literal read by
property: byName.square(node) writes the key at both ends and still produces
nothing. So the boundary is narrower than "a name survives into the dispatch site".
The case is committed with the three edges pinned in expected/64-array-dispatch.known-missing, so the gap is declared rather than silent
and any of the three starting to work will fail the suite until the list is updated. runDirect and the measure hops are the controls in the same case.
No rule change yet. A rule that resolves a callable through the collection holding it
carries real precision risk, and the acceptance bar here wants dev and held-out corpus
numbers behind it rather than a fixture.
Activity
swapnilpaliwal-sd commentedon Sep 18, 2026
A minimal reproduction is on skill/test-impact as tests/cases/typescript/pipeline-array-dispatch — three files, and the two symptoms are exact:
impact measure --testsanswers "0 of 1 test method(s)" andpath run widenRangeanswers "no chain of resolved calls".A second language shows the same shape: on a 23-file Python framework service, a dict of handlers dispatched by lookup, a TRANSFORMS list invoked by iteration, and a signal emit loop each lose their tests the same way, measured against behavioural truth rather than gold files.
swapnilpaliwal-sd commentedon Sep 19, 2026
Sharper statement of the boundary, from Python data on a framework service plus three Java fixtures — the line is not "dynamic dispatch", it is whether a key is written at both ends.
Recovered, because the registration carries a name the dispatch also writes:
@receiver("order_created")paired withemit("order_created")— joined on the key@exporter("csv")paired withexport(order, "csv")— joined on the key@MethodSource("cases")in JUnit — the provider is named by a string in the annotation, and it resolvesNot recovered, because the registration carries no key at all:
TRANSFORMS = [strip_blank_lines, normalise_skus]iterated byclean()— the function is an ELEMENT, and its position is the only thing identifying itnodeTransforms: [transformFor, …]called by indexEXPORTERS[kind](order)— recoverable only when a DECORATOR wrote the key; the subscript itself is silentSo register-by-NAME works today and register-as-DATA does not, and the difference is that a name survives into the dispatch site while a position does not. That is a narrower and more actionable target than the issue title implies: the missing edge is from whoever performs the iteration to every callable the literal holds.
The three passing Java cases are now regression guards on the skills branch (#975), so the boundary is asserted in both directions rather than only described.
skill: one module decides what a registration is, and the suite gets …
skill: a parameterised test is a test, and a site named once is not t…
test/typescript: a callable reached through the collection holding it…
test/typescript: a callable reached through the collection holding it…
swapnilpaliwal-sd commentedon Sep 19, 2026
Reproduced as a case in this repository, scored against the TypeScript compiler's own
answer (
checker.getResolvedSignature, throughtools/tsc_oracle_case.mjs), so thetruth side is an independent authority and not the engine restating itself.
graph/test/typescript/cases/64-array-dispatchholds four dispatch shapes over thesame four functions:
oracle=7 engine=4 agree=4 extra=0, and the three missing edges are:for..ofover an array literalwidenRangesteps[0](node), indexed readwidenRangebyName.square(node), keyed by namesquareRangenarrowRangeEach step's own call to
measureresolves, so the far half of every chain is presentand only the dispatch hop is missing. Nothing is over-resolved.
Two of these do not match the boundary stated earlier in this thread.
An indexed read is not recovered, and this engine has nothing that could recover
it. The clause for a
COMPUTED_CALLwhose receiver is an array, readingelem_value, exists only in the JavaScript engine, atgraph/javascript/engine/expression-resolution/callee-resolution.dl:61. TheTypeScript
callee-resolution.dlcarries noCOMPUTED_CALLclause and noelem_valueat all. Porting it is not sufficient on its own: the golden recordssteps[0](node)atpipeline.ts:24as aMETHOD_CALL, so the classification has tomove with the rule.
A name-keyed object literal is not recovered either. The earlier comment put the
line at "whether a key is written at both ends", with register-by-NAME on the working
side. That holds for the decorator and annotation registries it lists, where the key
is an argument the parser records. It does not hold for a plain object literal read by
property:
byName.square(node)writes the key at both ends and still producesnothing. So the boundary is narrower than "a name survives into the dispatch site".
The case is committed with the three edges pinned in
expected/64-array-dispatch.known-missing, so the gap is declared rather than silentand any of the three starting to work will fail the suite until the list is updated.
runDirectand themeasurehops are the controls in the same case.No rule change yet. A rule that resolves a callable through the collection holding it
carries real precision risk, and the acceptance bar here wants dev and held-out corpus
numbers behind it rather than a fixture.
impact: a dispatch base is a contract, not a callable the change reac…
plugin: the query frontend ships from main (#1153)
java: a class header does not see the class's own member types (#1244) (
typescript: an object literal implements the interface it is written …
typescript: a function held in an array or table is run by the call t…
typescript: a function held in an array or table is run by the call t…
Merge pull request #1657 from AxiomCodeAI/loop/ty-is967
swapnilpaliwal-sd commentedon Sep 28, 2026
Fixed by #1657 (merged into 0.1.5).