Make interface vtable slot numbering a single source of truth#253
Merged
Conversation
InterfaceInfo::getVirtualTable() used to assign each field/method's
virtualIndex as a side effect of building the vtable for whichever
object was being cast at the time - correct only for the most
recently cast implementer. This broke two ways: an optional member's
compile-time -1 marker leaked into unrelated access sites compiled
afterward (silently returning undefined for a member that was
actually present), and for `extends`-derived interfaces a base
interface's slot position within the derived interface's combined
vtable clobbered its own standalone slot position.
Fix: InterfaceInfo::assignCanonicalVirtualIndexes() is now the sole
writer of virtualIndex, computed once from the interface's own
declaration. getVirtualTable() no longer mutates it.
findField()/findMethod() gained an offset-accumulating overload that
walks the extends chain so access sites can add the declaring
interface's slot-block offset to its standalone index.
InterfaceFieldAccess drops the compile-time optional-member bypass in
favor of the runtime slot==-1 check the lowering already has.
Also fixes two latent bugs this uncovered: mergeInterfaces() (used by
intersection type synthesis) had a mismatched aggregate initializer
that left virtualIndex zero for every field merged from a `{ ... }`
member, and the intersection-type synthesis path never ran the
canonical index pass at all.
See docs/interface-vtable-simplification-design.md for the full
design and PR1 implementation notes. New regression test:
00interface_optional_cast_order.ts. Full suite: 716/716.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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.
Summary
InterfaceInfo::getVirtualTable()used to assign each field/method'svirtualIndexas a side effect of building the vtable for whichever object was being cast at the time - correct only for the most recently cast implementer. Two concrete bugs fell out of this:-1marker (InterfaceFieldAccess) leaked into unrelated access sites compiled after a cast of a different object missing that member - silently returningundefinedfor a member that was actually present on the object actually being read.extends-derived interfaces, a base interface's slot position within the derived interface's combined vtable clobbered its own standalone slot position, breaking access through the base interface directly once it had also been used as a base.InterfaceInfo::assignCanonicalVirtualIndexes()(introduced in Fix cross-module interface field/method vtable index mismatch #252) is now the sole writer ofvirtualIndex, computed once from the interface's own declaration.getVirtualTable()no longer mutates it.findField()/findMethod()gained an offset-accumulating overload that walks theextendschain so access sites can add the declaring interface's slot-block offset to its standalone index.InterfaceFieldAccessdrops the compile-time optional-member bypass in favor of the runtimeslot == -1check the lowering (InterfaceSymbolRefOpLowering) already has.mergeInterfaces()(intersection type synthesis, e.g.type t = A & B & { c: number }) had a mismatched aggregate initializer that leftvirtualIndexzero for every field merged from a{ ... }member; and the intersection-type synthesis path never ran the canonical index pass at all, being a separate programmaticInterfaceInfobuilder rather than the AST-walk path.docs/interface-vtable-simplification-design.md(§4, §5, and the "Implementation notes" subsection).Test plan
00interface_optional_cast_order.ts(compile-time-optional-member-bypass bug), both AOT and JIT variantsctest -C Debug- 716/716 passed, no regressions (including00interface_object4.tsand00interface_conjunction.ts, which briefly regressed mid-implementation and are now exercising the extends-offset path for the first time)🤖 Generated with Claude Code