Repository navigation
platform: the dispatch envelope is a core table in every language, not a Java-only one - #474
Merged
Merged
Conversation
…t a Java-only one
`call_edges` says what the engine CONCLUDED. Nothing in the bundle said what the hierarchy
ADMITTED — the set those edges were narrowed from — except in Java, and the two tables that
carried it were empty by construction in the other two front ends:
overrides java 7751 / 2541 / 163 rows on real projects typescript 0 python 0
type_instantiated java 3906 / 306 / 263 typescript 0 python 207
`schema_notes` declared the emptiness in prose, so nothing lied. But bundle-test.sh asserts
the DDL of every non-ext table byte-identical across the three languages, and that assertion
actively hid this: the contract was identical in SHAPE and not in CONTENT. A consumer opening
two bundles and running one query got a full answer from one and silence from the other, with
no error. Recovering the fan meant dropping to ext_implementors (2 cols, type-level),
ext_structural_implementor (2 cols, type-level) or ext_mro_position (4 cols, positional) —
three different queries for one question, none of which reaches a method.
None of this was missing analysis. It was computed and not projected. So:
dispatch_candidates(base_method_id, candidate_method_id, basis)
java virtual_override, as-is basis nominal
typescript implementors / structural_implementor x declared_method,
joined on the member name basis nominal, structural
python type_ancestor reversed x mro_lookup basis mro
and TypeScript's missing RTA set, one projection over a relation that already held it:
type_instantiated(t, "new") :- new_expression_type(_, _, t).
`overrides` stays as the Java-shaped alias it is, documented as dispatch_candidates filtered
to basis = nominal. `basis` is in the vocabulary per language, so a consumer that trusts only
declarations filters `structural` out; `dispatch_envelope_of(:qualified_name)` joins the two
new tables and returns, per candidate, whether its owner is in the RTA set — the column that
lets a consumer narrow the envelope itself.
READING mro_lookup, NOT type_defines_method, is what makes the Python set honest. On the
diamond fixture (A; B(A); C(A); D(B,C)) it yields `C.shared -> B.shared`: a call resolving to
C.shared runs B.shared on a D receiver, because D linearises [D, B, C, A]. B is not a subtype
of C, so no "the subtype declares this name" rule reaches that pair.
A CONSTRUCTOR IS NOT DISPATCHED, and the first TypeScript rule paired them anyway — a subclass
constructor is a member of the subclass with the same escaped name. MEASURED on
01-class-dispatch-and-super: 2 of 5 pairs were `Shape.<constructor> -> Circle.<constructor>`
and `-> Square.<constructor>`, edges no run can take. Guarded on both sides. Java's
virtual_override does not have this defect (checked on two hierarchy cases); Python's
__init__/__new__ are excluded for the same reason, since to Python's attribute machinery
__init__ IS an ordinary MRO attribute and mro_lookup cannot know the constructed class is
written at the site.
The issue also proposed exporting mro_lookup. It is not exported: once dispatch_candidates
exists nothing would query it, and it is (types x visible names x depth) — an unmeasured
bundle-size cost. ext_mro_position still carries the raw linearisation.
`basis` is a string literal in a rule, which Python's literal gate refuses unless it is a
declared language fact. It is not one — it is an output vocabulary term, written and never
joined on, exactly the category of call_unresolvable and site_reason — so
method_dispatch_candidate joins REASON_HEADS rather than the allowlist being widened.
TESTS. Nothing downstream consumes this relation, so a rule that stopped emitting it would
move no other golden and no test would notice — which is how it stayed Java-only. Two layers
now read it:
- a per-case `.envelope` golden in all three suites (16 java, 18 typescript, 3 python),
hash-free so it is portable, with the case's library IR loaded so a library-declared base
is named rather than printing as an anonymous `lib:?`, and with the declaration line
appended where a qualified name is shared (Java's enum-constant fan prints three distinct
methods as one name);
- bundle-test.sh asserts the envelope and the RTA set are NON-EMPTY IN EVERY LANGUAGE,
inside the per-language loop rather than once outside it — an assertion that runs once
passes on Java and never looks. It discriminates: reverting the TypeScript and Python
halves produces 12 failures, and Java passes either way, which is the asymmetry itself.
THE TIERS ARE CONSISTENT IN SHAPE AND WERE NOT IN THEIR CONTRACT. All three front ends assign
the tier by identical logic (target count 1 -> known_edge, >=2 -> multi_inferred, 0 -> boundary
or ambiguous), and the four shared tiers exist everywhere. Two things were nonetheless wrong in
the schema, both found while wiring this table up:
- `multi_inferred` was described as "virtual dispatch over INSTANTIATED subtypes". That is RTA
language, and type_instantiated is read in a rule body in PYTHON ONLY -- Java and TypeScript
compute and export it and never consume it, so their fans are CHA-wide, bounded by the
dispatch cap. The shared description now says only what is true everywhere, and three
per-language notes say how wide the fan actually is. The engine-side question is #473.
- The three language-specific tiers sit on OPPOSITE sides of the trust line and neither is in
the shared set: Java's ambiguous_anon is a blind spot, TypeScript's ambient_terminal and
intrinsic_terminal are RESOLVED single-target edges. So `tier IN (known_edge,
multi_inferred)` -- the obvious filter, and the one the shipped blast_radius query used --
silently drops resolved edges in TypeScript and nowhere else. A note on call_edges states
the resolved set and the blind-spot set per language, and blast_radius now traverses the
resolved terminals too.
Fixes #471.
swapnilpaliwal-sd
added a commit
that referenced
this pull request
Sep 14, 2026
Rename detection carried every edited file from src/ to graph/ on its own. One conflict, in the bundle test: reshape renamed the bundle's CSV output directory graph/ -> csv/ (the repository now HAS a top-level graph/), and #474 added dispatch_candidates to the same core-table loop. Both, not either. The new files — 37 .envelope goldens and test/tools/envelope_report.py — needed placing by hand: git can infer where an EDITED file went when its directory was renamed, but not where a file created on the other branch belongs. Verified on the merged tree: bundle-test.sh passes (3 languages, one schema across them, canonical queries run, schema doc current), the Python literal gate passes, all 37 goldens are under graph/test/*/expected, and the three rule files carry method_dispatch_candidate.
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 #471. Stacks on nothing, based directly on
main(d54837c9, which is #453 merged).The gap
call_edgessays what the engine concluded. Nothing in the bundle said what the hierarchyadmitted, the set those edges were narrowed from, except in Java. The two core tables
that carry it were empty by construction in the other two front ends, measured on real runs:
overridestype_instantiatedschema_notesdeclared the emptiness in prose, so nothing lied. Butbundle-test.shassertsthe DDL of every non-
exttable byte-identical across the three languages, and that assertionactively hid this: the contract was identical in shape and not in content. Recovering the
fan meant dropping to
ext_implementors(2 cols, type-level),ext_structural_implementor(2 cols, type-level) or
ext_mro_position(4 cols, positional), three different queries forone question, none of which reaches a method.
None of it was missing analysis. It was computed and not projected.
What this adds
virtual_override, as-isnominalimplementors/structural_implementor×declared_method, joined on the member namenominal,structuraltype_ancestorreversed ×mro_lookupmroplus TypeScript's missing RTA set, one projection over a relation that already held the answer:
type_instantiated(t, "new") :- new_expression_type(_, _, t).overridesstays as the Java-shaped alias it is, documented asdispatch_candidatesfilteredto
basis = nominal. A new canonical querydispatch_envelope_of(:qualified_name)joins thetwo tables and returns, per candidate, whether its owner is in the RTA set, the column that
lets a consumer narrow the envelope itself.
Reading
mro_lookup, nottype_defines_methodOn the diamond fixture (
A;B(A);C(A);D(B, C)) the Python rule yields:A call resolving to
C.sharedrunsB.sharedon aDreceiver, becauseDlinearises[D, B, C, A].Bis not a subtype ofC, no "the subtype declares this name" rulereaches that pair. That row is the whole reason the basis is called
mro.A constructor is not dispatched, and my first rule paired them
new Shape()runs Shape's constructor and never Circle's, however Circle extends Shape. Thefirst TypeScript rule paired them anyway, because a subclass constructor is a member of the
subclass with the same escaped name. Measured on
01-class-dispatch-and-super: 2 of 5 pairswere
Shape.<constructor> -> Circle.<constructor>and-> Square.<constructor>, edges norun can take. Guarded. That case now reads:
three constructors declared in the file, none of them present. Java's
virtual_overridedoesnot have this defect (checked on two hierarchy cases). Python's
__init__/__new__areexcluded for the same reason: to Python's attribute machinery
__init__is an ordinary MROattribute, and
mro_lookupcannot know the constructed class is written at the site.Two departures from the issue
mro_lookupis not exported. The issue proposed it; oncedispatch_candidatesexistsnothing would query it, and it is (types × visible names × depth), an unmeasured bundle-size
cost.
ext_mro_positionstill carries the raw linearisation.basisis a string literal in a rule, which Python's literal gate refuses unless it is adeclared language fact. It is not one, it is an output vocabulary term, written and never
joined on, exactly the category of
call_unresolvableandsite_reason, somethod_dispatch_candidatejoinsREASON_HEADSrather than the allowlist being widened.Tests
Nothing downstream consumes this relation, so a rule that stopped emitting it would move no
other golden and no test would notice, which is how it stayed Java-only. Two layers read it now.
A per-case
.envelopegolden in all three suites (16 java, 18 typescript, 3 python).Hash-free, so it is portable; the case's library IR is loaded so a library-declared base is
named (
lib:dep.Strategy.run -> app.ExtImplA.run) rather than printing as an anonymouslib:?; and the declaration line is appended where a qualified name is shared, because Java'senum-constant fan prints three distinct methods under one name.
bundle-test.shasserts the envelope and the RTA set are non-empty in every language,inside the per-language loop rather than once outside it, an assertion that runs once passes
on Java and never looks. It discriminates: reverting the TypeScript and Python halves produces
12 failures, and Java passes either way, which is the asymmetry itself. TypeScript's fixture
also carries a duck type nothing constructs, so
owner_instantiatedis asserted to return bothvalues rather than being constant.
Verification
bundle: ok (3 languages, one schema across them, canonical queries run, schema doc current)virtual_overriderow-for-row on every case, it is a projectionreshape(reshape: parser/ (subtree) + graph/ + bin/axiom-graph. One repository for the whole pipeline #469): every file lands ingraph/by rename detection, onetrivial conflict (reshape renamed the bundle's CSV dir
graph/→csv/), and the bundletest passes there