Skip to content

Commit 7bb4fd5

Browse files
Fix accessor vtable dispatch through base-typed references
Closes the "much bigger gap" flagged alongside super-accessor-crash-fix (PR #278): overriding a get/set accessor pair in a subclass and accessing it through a BASE-TYPED reference (an ordinary upcast, not `super`) always resolved statically to the base's own accessor instead of dispatching virtually to the override - a real correctness gap (this is standard OOP virtual dispatch, not an edge case), unlike ordinary methods which already dispatch through the vtable correctly. Root cause: an accessor's backing get/set functions are ALREADY registered as ordinary MethodInfo entries in ClassInfo::methods (same list ordinary methods use) with a real vtable slot assigned via registerClassMethodMember/getVirtualTable - accessors are virtual by default exactly like methods. But ClassAccessorAccess never checked isVirtual at all: it unconditionally built ThisAccessorOp with a hardcoded FlatSymbolRefAttr naming THIS class's own get/set symbol, completely bypassing the vtable that was already being correctly built. Fix: mirror ClassMethodAccess's own isVirtual/isStorageType check (isStorageType is true for `super.<accessor>`'s raw-struct thisValue, so super access is naturally excluded from virtual dispatch, same as methods) and route through the vtable via VirtualSymbolRefOp (the same op static virtual method access already uses) feeding into the existing "indirect" (function-value) ThisIndirectAccessorOp, instead of the hardcoded-symbol ThisAccessorOp. Checked before the isDynamicImport branch, matching ClassMethodAccess's ordering - the vtable's own construction already resolves dynamic-import-owned slots, so virtual dispatch through it is correct regardless of module boundaries. One regression found and fixed during verification: getThisRefOfClass only narrows `thisValue` for the isSuperClass case, so an ordinary access whose static type is still a wider union (e.g. `T | null` narrowed only at the type-info level, never cast at the value level - hit by 00iterator_bug.ts's spread-operator-generated `.length` access) reached the new vtable lookup with an un-narrowed union type, which mlirGenPropertyAccessExpression's PropertyRefOp rejects (unlike the old AnyType-typed ThisAccessorOp it replaces). Fixed by explicitly casting to classInfo->classType before the vtable lookup. New same-file regression test 00class_accessor_virtual.ts (compile + jit tiers). Also closed the historical NOTE in import_class_accessor.ts documenting this exact gap as deliberately untested - now asserts virtual dispatch through a base-typed reference works, cross-module, across all 3 tiers (plain, -shared compile, -jit -shared). Full suite: 810/810 green. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 4949eac commit 7bb4fd5

4 files changed

Lines changed: 146 additions & 6 deletions

File tree

tslang/lib/TypeScript/MLIRGenAccessCall.cpp

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,100 @@ namespace mlirgen
592592
{
593593
auto effectiveThisValue = getThisRefOfClass(location, classInfo->classType, thisValue, isSuperClass, genContext);
594594

595+
// Accessor vtable dispatch (see accessor-vtable-dispatch-fix memory): overriding
596+
// a get/set pair in a subclass and accessing it through a BASE-TYPED reference
597+
// (an ordinary upcast, not `super`) used to always resolve statically to the
598+
// base's own accessor - accessors were never part of the vtable dispatch
599+
// decision at all, even though their backing get/set functions are ALREADY
600+
// registered as ordinary (virtual-by-default) MethodInfo entries with real
601+
// vtable slots via registerClassMethodMember/getVirtualTable, exactly like
602+
// regular methods. Mirror ClassMethodAccess's own isVirtual/isStorageType check
603+
// (isStorageType is true for `super.<accessor>`'s raw-struct thisValue, so this
604+
// naturally excludes super access from virtual dispatch, same as methods) and
605+
// route through the vtable via VirtualSymbolRefOp + the existing "indirect"
606+
// (function-value) ThisIndirectAccessorOp, instead of hardcoding this class's
607+
// own get/set symbol names. Checked BEFORE the isDynamicImport branch below,
608+
// same ordering as ClassMethodAccess - the vtable's own construction already
609+
// resolves dynamic-import-owned slots (mlirGenClassVirtualTableDefinition's
610+
// isOwnedByDynamicImport handling), so virtual dispatch through it is correct
611+
// regardless of module boundaries, without needing separate dlsym resolution.
612+
auto isStorageType = isa<mlir_ts::ClassStorageType>(thisValue.getType());
613+
614+
auto findMethodInfo = [&](FunctionEntry funcEntry) -> const MethodInfo * {
615+
if (!funcEntry)
616+
{
617+
return nullptr;
618+
}
619+
620+
auto it = llvm::find_if(classInfo->methods,
621+
[&](auto &m) { return m.funcName == funcEntry.name; });
622+
return it != classInfo->methods.end() ? &*it : nullptr;
623+
};
624+
625+
auto getMethodInfo = findMethodInfo(getFunc);
626+
auto setMethodInfo = findMethodInfo(setFunc);
627+
628+
auto isVirtualAccessor = !isStorageType &&
629+
((getMethodInfo && (getMethodInfo->isAbstract || getMethodInfo->isVirtual)) ||
630+
(setMethodInfo && (setMethodInfo->isAbstract || setMethodInfo->isVirtual)));
631+
632+
if (isVirtualAccessor)
633+
{
634+
// effectiveThisValue can still carry a wider static type here (e.g. a
635+
// union like `T | null` that the caller narrowed at the type-info level to
636+
// find classInfo, but never actually cast the VALUE to) - getThisRefOfClass
637+
// only narrows for the isSuperClass branch, a no-op otherwise.
638+
// mlirGenPropertyAccessExpression's vtable lookup needs a genuine class
639+
// reference, unlike the plain-AnyType ThisAccessorOp this branch replaces,
640+
// so cast explicitly first (same narrowing getThisRefOfClass already does
641+
// for super).
642+
CAST_A(vtableThisValue, location, classInfo->classType, effectiveThisValue, genContext);
643+
644+
auto vtableAccess =
645+
mlirGenPropertyAccessExpression(location, vtableThisValue, VTABLE_NAME, genContext);
646+
647+
if (!vtableAccess)
648+
{
649+
emitError(location, "") << "class '" << classInfo->fullName << "' missing 'virtual table'";
650+
}
651+
652+
EXIT_IF_FAILED_OR_NO_VALUE(vtableAccess)
653+
654+
auto resolveVirtualAccessorFunc = [&](FunctionEntry funcEntry, const MethodInfo *methodInfo) -> mlir::Value {
655+
if (!funcEntry)
656+
{
657+
return mlir::Value();
658+
}
659+
660+
assert(genContext.allowPartialResolve || methodInfo->virtualIndex >= 0);
661+
662+
return builder.create<mlir_ts::VirtualSymbolRefOp>(
663+
location, funcEntry.funcType, vtableAccess,
664+
builder.getI32IntegerAttr(methodInfo->virtualIndex),
665+
mlir::FlatSymbolRefAttr::get(builder.getContext(), funcEntry.name));
666+
};
667+
668+
auto getterValue = resolveVirtualAccessorFunc(getFunc, getMethodInfo);
669+
auto setterValue = resolveVirtualAccessorFunc(setFunc, setMethodInfo);
670+
671+
auto opaqueThisType = mlir_ts::OpaqueType::get(builder.getContext());
672+
if (!getterValue)
673+
{
674+
getterValue = builder.create<mlir_ts::UndefOp>(
675+
location, mlir_ts::FunctionType::get(builder.getContext(), {opaqueThisType}, {accessorResultType}, false));
676+
}
677+
678+
if (!setterValue)
679+
{
680+
setterValue = builder.create<mlir_ts::UndefOp>(
681+
location, mlir_ts::FunctionType::get(builder.getContext(), {opaqueThisType, accessorResultType}, {}, false));
682+
}
683+
684+
auto thisIndirectAccessorOp = builder.create<mlir_ts::ThisIndirectAccessorOp>(
685+
location, accessorResultType, vtableThisValue, getterValue, setterValue, mlir::Value());
686+
return thisIndirectAccessorOp.getResult(0);
687+
}
688+
595689
if (classInfo->isDynamicImport)
596690
{
597691
// A plain FlatSymbolRefAttr (the path below) lowers to a call the static linker

tslang/test/tester/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ add_test(NAME test-compile-00-class-discover-types COMMAND test-runner "${PROJEC
236236
add_test(NAME test-compile-00-class-accessor COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_accessor.ts")
237237
add_test(NAME test-compile-00-class-accessor-2 COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_accessor2.ts")
238238
add_test(NAME test-compile-00-class-accessor-super COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_accessor_super.ts")
239+
add_test(NAME test-compile-00-class-accessor-virtual COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_accessor_virtual.ts")
239240
add_test(NAME test-compile-00-class-super COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_super.ts")
240241
add_test(NAME test-compile-00-class-super-static COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_super_static.ts")
241242
add_test(NAME test-compile-00-class-default-constructor COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_def_constr.ts")
@@ -599,6 +600,7 @@ add_test(NAME test-jit-00-class-discover-types COMMAND test-runner -jit "${PROJE
599600
add_test(NAME test-jit-00-class-accessor COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_accessor.ts")
600601
add_test(NAME test-jit-00-class-accessor-2 COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_accessor2.ts")
601602
add_test(NAME test-jit-00-class-accessor-super COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_accessor_super.ts")
603+
add_test(NAME test-jit-00-class-accessor-virtual COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_accessor_virtual.ts")
602604
add_test(NAME test-jit-00-class-super COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_super.ts")
603605
add_test(NAME test-jit-00-class-super-static COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_super_static.ts")
604606
add_test(NAME test-jit-00-class-default-constructor COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_def_constr.ts")
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Regression test for the accessor-vtable-dispatch-fix: overriding a
2+
// get/set accessor pair in a subclass and accessing it through a
3+
// BASE-TYPED reference (an ordinary upcast, not `super`) used to always
4+
// resolve statically to the base's own accessor - accessors were never
5+
// part of the vtable dispatch decision at all, unlike ordinary methods.
6+
class Base {
7+
protected _val: number = 10;
8+
9+
get val(): number {
10+
return this._val;
11+
}
12+
13+
set val(v: number) {
14+
this._val = v;
15+
}
16+
}
17+
18+
class Derived extends Base {
19+
get val(): number {
20+
return this._val * 2;
21+
}
22+
23+
set val(v: number) {
24+
this._val = v + 1;
25+
}
26+
}
27+
28+
function main() {
29+
const d = new Derived();
30+
const asBase: Base = d;
31+
32+
asBase.val = 5;
33+
assert(asBase.val == 12);
34+
35+
// and through the derived-typed reference directly, for good measure
36+
assert(d.val == 12);
37+
38+
print("done.");
39+
}

tslang/test/tester/tests/import_class_accessor.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,17 @@ function main() {
2020
assert(t.celsius == 100);
2121
assert(t.fahrenheit == 212);
2222

23-
// NOTE: accessing an overridden accessor through a base-typed reference
24-
// (`const asBase: M.Temperature = t; asBase.celsius`) is a known,
25-
// pre-existing, non-cross-module-specific gap: get/set accessors are not
26-
// part of the vtable, so such access resolves statically to the base's
27-
// own accessor rather than dispatching virtually. Not exercised here -
28-
// this test only covers the super-accessor-call crash fix.
23+
// Accessing an overridden accessor through a base-typed reference used to
24+
// be a known, pre-existing, non-cross-module-specific gap: get/set
25+
// accessors were not part of the vtable, so such access resolved
26+
// statically to the base's own accessor rather than dispatching
27+
// virtually (see accessor-vtable-dispatch-fix memory). Fixed - this now
28+
// dispatches to ClampedTemperature's override even through the
29+
// M.Temperature-typed reference, cross-module.
30+
const asBase: M.Temperature = t;
31+
asBase.celsius = -5;
32+
assert(asBase.celsius == 0);
33+
assert(asBase.fahrenheit == 32);
2934

3035
print("done.");
3136
}

0 commit comments

Comments
 (0)