Repository navigation
multi_inferred means a wider set in Java and TypeScript than in Python: the instantiation set is computed and never read #473
Description
What is inconsistent
multi_inferred is documented as one thing across the three front ends, "a sound SET of
possible targets", and the tier is assigned by identical logic everywhere (call_target_count
1 → known_edge, ≥2 → multi_inferred). What differs is how wide the set is, because only
one front end narrows it by the instantiation set:
python type_instantiated_reachable is read in 4 rule bodies:
resolution/dispatch.dl:58, :68
resolution/annotations.dl:82
expression-resolution/callee-resolution.dl:114
java type_instantiated — computed, exported, NEVER read by any rule
typescript type_instantiated — computed, exported, NEVER read by any rule
So a Java or TypeScript multi_inferred fan is CHA-wide: every override the hierarchy
admits, bounded only by dispatch_cap. A Python fan of the same shape is RTA-narrowed, a subtype the program never constructs is not in it.
Why it matters
The two are not the same claim. Given interface Handler with five implementors of which the
program constructs two, Java answers five and Python answers two. Both are sound; the Java
answer is three edges wider than the code can take, and a consumer comparing precision across
two bundles is comparing two different analyses under one label.
It also shows up as measurable imprecision. On a real Java project, type_instantiated holds
3,906 rows and virtual_override 7,751, the instantiation set exists, is the right size to
bite, and is discarded.
What the fix is not
This is NOT "make Java match Python". RTA narrowing is only sound when the instantiation set is
complete for the analysed scope, and the three front ends do not have the same guarantee:
- a Java type can be constructed reflectively, by a framework, or in a dependency whose IR is
not staged,type_instantiatedcoversnew, anonymous placements, enum constants and
ServiceLoaderproviders, and nothing else; - a TypeScript class can be constructed inside a
.d.ts-declared library whose body is absent; - Python's narrowing already carries an
ext_self_dispatch_assumes_closed_worldassumption row
for exactly this reason, and it is the honest precedent: narrow, and DECLARE the assumption.
So the question is per language: is the instantiation set complete enough to narrow with, and
if it is, what assumption row records the closed-world step?
Suggested shape
- Decide per front end whether to narrow, and gate it behind a flag rather than changing the
default silently, the goldens will move, and the movement is the measurement. - Where narrowing is applied, emit an assumption row in the same style as
self_dispatch_assumes_closed_world, so the bundle records that the fan was narrowed on a
closed-world premise rather than the consumer having to know. - A/B on every measured codebase: report edges lost, and how many of the lost edges the
language's oracle says are real. A narrowing that drops a real edge is not a precision win.
Meanwhile
The gap is now documented rather than silent: schema_notes on call_edges states per language
whether the fan is narrowed, and dispatch_candidates + type_instantiated (both populated in
all three since #471) let a consumer do the narrowing itself, that is what the
dispatch_envelope_of canonical query's owner_instantiated column is for. This issue is about
the ENGINE doing it, not the consumer.
Activity
platform: the dispatch envelope is a core table in every language, no…
typescript: a structurally conforming class the program constructs st…
swapnilpaliwal-sd commentedon Sep 19, 2026
Re-checked at
e0167dea. The asymmetry is unchanged, and one count in the filing has moved the wrong way, so recording the current state before anything is decided:So the label still covers two different analyses: a Java or TypeScript
multi_inferredfan is CHA-wide, a Python fan of the same shape is RTA-narrowed, anddispatch_capis the only thing bounding the first two.What is NOT being changed here, and why
This issue asks for the engine to narrow, per language, behind a flag, with an assumption row, and A/B'd on every measured codebase. I am leaving all four of those undone rather than doing them partially, for a reason that is worth writing down: the A/B is the deliverable, not the rule. RTA narrowing REMOVES targets from a set that is currently sound, so the measurement the issue asks for — "report edges lost, and how many of the lost edges the language's oracle says are real" — is the only thing that can distinguish a precision win from a silent recall loss, and it has to be run on both front ends. A rule landed ahead of that measurement is a guess with a flag on it.
One thing has changed, in the other direction
#428 is now fixed (PR #1043) by a rule that reads
type_instantiatedin a TypeScript rule BODY for the first time —structural_implementornow readmits a structurally conforming class when the program CONSTRUCTS it, where before any nominalimplementsin the codebase dropped every structural conformer entirely.Note the direction, because it is the opposite of this issue's: that reads the instantiation set to widen soundly (a class nothing constructs cannot receive a dispatched call, so requiring instantiation admits the real target and excludes nothing a run can take), where this issue asks about reading it to narrow, which needs the closed-world premise Python declares with
ext_self_dispatch_assumes_closed_worldand neither Java nor TypeScript currently has grounds for.It does mean the set is no longer wholly unread in TypeScript, so the "computed and never read" framing above is now true of Java only, and true of TypeScript only for the narrowing direction.
swapnilpaliwal-sd commentedon Sep 29, 2026
Fixed by #1788 (merged into 0.1.9).