From be2c96200039210b15e0b53e88e0caf0cbbe43ab Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sun, 19 Jul 2026 11:16:35 +0100 Subject: [PATCH] Fix cross-module interface field/method vtable index mismatch Interface vtable slot numbers were assigned by two inconsistent algorithms: declaration-time assignment used raw interleaved declaration order, while cast-time assignment (getVirtualTable) always puts methods before fields. Whichever module performs an actual cast keeps the correct methods-first layout; a module that only reads an already-typed interface value it imported never runs the cast-time algorithm and keeps the stale declaration-order index, so a field read could land on a method's vtable slot (a function pointer misread as a byte offset) and crash the JIT with 0xC0000005. Add InterfaceInfo::assignCanonicalVirtualIndexes(), a pure function of the interface's own declaration, computed once when the declaration finishes resolving so all modules that re-parse it agree. Co-Authored-By: Claude Sonnet 5 --- .../TypeScript/MLIRLogic/MLIRGenStore.h | 32 +++++++++++++++++++ tslang/lib/TypeScript/MLIRGenInterfaces.cpp | 5 +++ .../export_object_literal_with_interface.ts | 7 ++++ .../import_object_literal_with_interface.ts | 2 ++ 4 files changed, 46 insertions(+) diff --git a/tslang/include/TypeScript/MLIRLogic/MLIRGenStore.h b/tslang/include/TypeScript/MLIRLogic/MLIRGenStore.h index 453c5b5e6..a52dd9170 100644 --- a/tslang/include/TypeScript/MLIRLogic/MLIRGenStore.h +++ b/tslang/include/TypeScript/MLIRLogic/MLIRGenStore.h @@ -539,6 +539,38 @@ struct InterfaceInfo return offset + methods.size() + fields.size(); } + // vtable slot numbers must be a pure function of the interface's own declaration + // (extends, then own methods in order, then own fields in order) - NOT of whichever + // object happens to be cast to it first. getVirtualTable() re-derives the same + // methods-then-fields order per-cast (needed to mark per-object optional members + // missing), but a module that only reads an already-typed interface value - without + // ever casting an object to it itself (e.g. importing an already-boxed global from + // another compilation unit) - never runs getVirtualTable() at all. Without this + // eagerly-computed, cast-independent pass, such a module falls back on the + // interleaved declaration-order index assigned at member-registration time + // (mlirGenInterfaceAddFieldMember / addInterfaceMethod), which disagrees with the + // methods-first layout whenever a field is declared before a method in source order - + // reading through the wrong vtable slot (e.g. a method's function pointer + // reinterpreted as a field offset) and crashing. + void assignCanonicalVirtualIndexes() + { + auto offset = 0; + for (auto &extent : extends) + { + offset += std::get<1>(extent)->getVTableSize(); + } + + for (auto &method : methods) + { + method.virtualIndex = offset++; + } + + for (auto &field : fields) + { + field.virtualIndex = offset++; + } + } + void recalcOffsets() { auto offset = 0; diff --git a/tslang/lib/TypeScript/MLIRGenInterfaces.cpp b/tslang/lib/TypeScript/MLIRGenInterfaces.cpp index 3859ad205..a98dc73c5 100644 --- a/tslang/lib/TypeScript/MLIRGenInterfaces.cpp +++ b/tslang/lib/TypeScript/MLIRGenInterfaces.cpp @@ -581,6 +581,11 @@ namespace mlirgen } while (notResolved > 0); + // fix up vtable slot numbers to the canonical methods-then-fields order now that + // all members are known - see assignCanonicalVirtualIndexes() for why this can't + // be left to getVirtualTable()'s per-cast assignment alone. + newInterfacePtr->assignCanonicalVirtualIndexes(); + // add to export if any if (auto hasExport = getExportModifier(interfaceDeclarationAST)) { diff --git a/tslang/test/tester/tests/export_object_literal_with_interface.ts b/tslang/test/tester/tests/export_object_literal_with_interface.ts index a385a5dc4..4321df47e 100644 --- a/tslang/test/tester/tests/export_object_literal_with_interface.ts +++ b/tslang/test/tester/tests/export_object_literal_with_interface.ts @@ -14,4 +14,11 @@ namespace A { // invalid Point3d is not exported export var Origin3d: Point3d = { x: 0, y: 0, z: 0 }; + + export interface Counter { + count: number; + inc(): void; + } + + export var counter: Counter = { count: 0, inc() { this.count = this.count + 1; } }; } diff --git a/tslang/test/tester/tests/import_object_literal_with_interface.ts b/tslang/test/tester/tests/import_object_literal_with_interface.ts index c405a3ca7..7d9c19ba8 100644 --- a/tslang/test/tester/tests/import_object_literal_with_interface.ts +++ b/tslang/test/tester/tests/import_object_literal_with_interface.ts @@ -10,4 +10,6 @@ if (A.Origin3d.x == 0) print("ok"); } +print(A.counter.count); + print("done.");