From 3822050f5a90dfb6885b03bb8f3787f205e1e651 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Wed, 22 Jul 2026 17:18:49 +0100 Subject: [PATCH 1/2] Fix class-implements-interface optional-member crash and add coverage class X implements Interface crashed the compiler at declaration time (no instantiation needed) whenever Interface had an optional member X didn't declare - mlirGenClassVirtualTableDefinitionForInterface never checked the isMissing flag that getVirtualTable already provides, unlike its object-literal sibling (mlirGenObjectVirtualTableDefinitionForInterface), which already handles this correctly. Fixed by mirroring that existing handling. Also adds cross-module (export/import) test coverage for class+interface combinations that had no prior coverage: multilevel interface extends, optional interface members, abstract class implementing an interface, and structural (no explicit `implements`) interface satisfaction. Several -shared tiers are added but left disabled in CMakeLists.txt, documented with the root cause: a class that is itself a dynamic import gets a constant (unresolvable) symbol reference for methods reachable only through its own interface vtable, when cast to that interface from the importing module - a real, separate, deeper bug left for a follow-up. --- tslang/lib/TypeScript/MLIRGenClasses.cpp | 23 ++++++++++- tslang/test/tester/CMakeLists.txt | 39 +++++++++++++++++++ ...ort_class_implements_interface_abstract.ts | 26 +++++++++++++ ...t_class_implements_interface_multilevel.ts | 28 +++++++++++++ ...ort_class_implements_interface_optional.ts | 26 +++++++++++++ .../export_class_structural_interface.ts | 30 ++++++++++++++ ...ort_class_implements_interface_abstract.ts | 24 ++++++++++++ ...t_class_implements_interface_multilevel.ts | 17 ++++++++ ...ort_class_implements_interface_optional.ts | 13 +++++++ .../import_class_structural_interface.ts | 38 ++++++++++++++++++ 10 files changed, 262 insertions(+), 2 deletions(-) create mode 100644 tslang/test/tester/tests/export_class_implements_interface_abstract.ts create mode 100644 tslang/test/tester/tests/export_class_implements_interface_multilevel.ts create mode 100644 tslang/test/tester/tests/export_class_implements_interface_optional.ts create mode 100644 tslang/test/tester/tests/export_class_structural_interface.ts create mode 100644 tslang/test/tester/tests/import_class_implements_interface_abstract.ts create mode 100644 tslang/test/tester/tests/import_class_implements_interface_multilevel.ts create mode 100644 tslang/test/tester/tests/import_class_implements_interface_optional.ts create mode 100644 tslang/test/tester/tests/import_class_structural_interface.ts diff --git a/tslang/lib/TypeScript/MLIRGenClasses.cpp b/tslang/lib/TypeScript/MLIRGenClasses.cpp index c25a44353..02f247078 100644 --- a/tslang/lib/TypeScript/MLIRGenClasses.cpp +++ b/tslang/lib/TypeScript/MLIRGenClasses.cpp @@ -1557,7 +1557,26 @@ genContext); auto fieldIndex = 0; for (auto methodOrField : virtualTable) { - if (methodOrField.isField) + if (methodOrField.isMissing) + { + // a genuinely-absent optional (`?`) interface field/method the class + // doesn't implement - mirrors the object-literal handling in + // mlirGenObjectVirtualTableDefinitionForInterface + // (MLIRGenInterfaces.cpp): querying the class for a field/method it + // doesn't have (via mlirGenPropertyAccessExpression, below) crashes + // internally instead of failing gracefully, so this case must be + // detected and handled up front rather than falling into the "present" + // branches - use the same -1 sentinel placeholder, cast to the slot's + // ref/func-pointer type. + auto negative1 = builder.create(location, builder.getI64Type(), + mth.getI64AttrValue(-1)); + auto slotType = methodOrField.isField ? methodOrField.fieldInfo.type : methodOrField.methodInfo.funcType; + auto castedPtr = cast(location, mlir_ts::RefType::get(slotType), negative1, genContext); + vtableValue = builder.create( + location, virtTuple, castedPtr, vtableValue, + MLIRHelper::getStructIndex(builder, fieldIndex)); + } + else if (methodOrField.isField) { auto nullObj = builder.create(location, getNullType()); auto classNull = cast(location, newClassPtr->classType, nullObj, genContext, true); @@ -1568,7 +1587,7 @@ genContext); emitError(location) << "can't find field (or it is inaccessible): " << methodOrField.fieldInfo.id << " in interface: " << newInterfacePtr->fullName << " for class: " << newClassPtr->fullName; - return TypeValueInitType{mlir::Type(), mlir::Value(), TypeProvided::No}; + return TypeValueInitType{mlir::Type(), mlir::Value(), TypeProvided::No}; } auto fieldRef = mcl.GetReferenceFromValue(location, fieldValue); diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index e6acb1a35..86d9f2678 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -869,6 +869,10 @@ add_test(NAME test-compile-export-import-class-static COMMAND test-runner "${PRO # import_class_generic.ts) for that follow-up. # add_test(NAME test-compile-export-import-class-generic COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_generic.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_generic.ts") add_test(NAME test-compile-export-import-class-accessor COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_accessor.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_accessor.ts") +add_test(NAME test-compile-export-import-class-implements-interface-multilevel COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_implements_interface_multilevel.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_implements_interface_multilevel.ts") +add_test(NAME test-compile-export-import-class-implements-interface-optional COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_implements_interface_optional.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_implements_interface_optional.ts") +add_test(NAME test-compile-export-import-class-implements-interface-abstract COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_implements_interface_abstract.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_implements_interface_abstract.ts") +add_test(NAME test-compile-export-import-class-structural-interface COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_structural_interface.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_structural_interface.ts") # shared libs tests (dlls/compile-time) add_test(NAME test-compile-time-shared-decl-emit-type COMMAND test-runner -shared -compile-time "${PROJECT_SOURCE_DIR}/test/tester/tests/emit_compiletime_func.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/decl_type.ts") @@ -899,6 +903,36 @@ add_test(NAME test-compile-shared-export-import-class-extends-implements-diamond add_test(NAME test-compile-shared-export-import-class-extends-multilevel COMMAND test-runner -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_extends_multilevel.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_extends_multilevel.ts") add_test(NAME test-compile-shared-export-import-class-abstract COMMAND test-runner -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_abstract.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_abstract.ts") add_test(NAME test-compile-shared-export-import-class-static COMMAND test-runner -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_static.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_static.ts") +# DISABLED: known issue found 2026-07-22. mlirGenClassVirtualTableDefinitionForInterface +# (a class's own per-interface vtable, built when it `implements` something) always emits a +# constant SymbolRefOp for the implementing method, unlike mlirGenClassVirtualTableDefinition +# (the class's ordinary vtable), which already special-cases a method owned by a dynamic-import +# class/base and resolves it at runtime instead (SearchForAddressOfSymbolOp) since a link-time +# constant address doesn't exist across a DLL boundary. Here the class ITSELF (M.Impl / M.Shape, +# not just an ancestor) is the dynamic import, and its interface-vtable is rebuilt from the +# importer's side, so the constant SymbolRefOp is unresolvable: lld fails with +# "undefined symbol: M.Impl.describe" (a method reachable only through the interface, e.g. +# `const asDerived: M.Derived = obj; asDerived.describe()`). A first attempt to mirror the +# working dynamic-import runtime-resolution pattern into the interface-vtable builder compiled, +# but broke DWARF debug-info generation ("DISubprogram attached to more than one function") and +# a runtime assert in the abstract-class variant - reverted rather than land a fix with +# unexamined side effects (see cross-module-class-coverage-2026-07 memory). The plain "compile" +# tier (no DLL boundary - both files link into one exe) and the jit-shared tier of the +# multilevel variant are unaffected and stay enabled below. +# add_test(NAME test-compile-shared-export-import-class-implements-interface-multilevel COMMAND test-runner -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_implements_interface_multilevel.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_implements_interface_multilevel.ts") +add_test(NAME test-compile-shared-export-import-class-implements-interface-optional COMMAND test-runner -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_implements_interface_optional.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_implements_interface_optional.ts") +# DISABLED: same dynamic-import interface-vtable gap as +# test-compile-shared-export-import-class-implements-interface-multilevel above (known issue, +# 2026-07-22) - M.Widget is itself the dynamic import and gets cast to its own interface +# (M.Printable) from the importer, so the same "undefined symbol: M.Widget.describe" link +# failure applies here too, reached via the structural/cast-triggered path +# (mlirGenCreateInterfaceVTableForClass) rather than the explicit-`implements` declaration-time +# path - confirms both paths share the same root cause. The LocalWidget half of this test (a +# class declared locally in the importer, cast to the cross-module interface) is unaffected and +# still exercised by the other 2 tiers below. +# add_test(NAME test-compile-shared-export-import-class-structural-interface COMMAND test-runner -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_structural_interface.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_structural_interface.ts") +# DISABLED: see the matching test-compile-shared-export-import-class-implements-interface-multilevel comment above (known issue, 2026-07-22). +# add_test(NAME test-compile-shared-export-import-class-implements-interface-abstract COMMAND test-runner -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_implements_interface_abstract.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_implements_interface_abstract.ts") # DISABLED: see the matching test-compile-export-import-class-generic comment above (known issue, 2026-07-22). # add_test(NAME test-compile-shared-export-import-class-generic COMMAND test-runner -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_generic.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_generic.ts") # DISABLED: known issue found 2026-07-22 (separate from the super-accessor-call crash fixed @@ -938,6 +972,11 @@ add_test(NAME test-jit-shared-export-import-class-extends-multilevel COMMAND tes add_test(NAME test-jit-shared-export-import-class-extends-implements-diamond COMMAND test-runner -jit -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_extends_implements_diamond.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_extends_implements_diamond.ts") add_test(NAME test-jit-shared-export-import-class-abstract COMMAND test-runner -jit -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_abstract.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_abstract.ts") add_test(NAME test-jit-shared-export-import-class-static COMMAND test-runner -jit -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_static.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_static.ts") +add_test(NAME test-jit-shared-export-import-class-implements-interface-multilevel COMMAND test-runner -jit -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_implements_interface_multilevel.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_implements_interface_multilevel.ts") +add_test(NAME test-jit-shared-export-import-class-implements-interface-optional COMMAND test-runner -jit -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_implements_interface_optional.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_implements_interface_optional.ts") +add_test(NAME test-jit-shared-export-import-class-structural-interface COMMAND test-runner -jit -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_structural_interface.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_structural_interface.ts") +# DISABLED: see the test-compile-shared-export-import-class-implements-interface-multilevel comment above (known issue, 2026-07-22) - the JIT tier of the abstract variant also crashes (unlike the multilevel variant's JIT tier, which passes). +# add_test(NAME test-jit-shared-export-import-class-implements-interface-abstract COMMAND test-runner -jit -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_implements_interface_abstract.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_implements_interface_abstract.ts") # DISABLED: see the matching test-compile-export-import-class-generic comment above (known issue, 2026-07-22). # add_test(NAME test-jit-shared-export-import-class-generic COMMAND test-runner -jit -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_generic.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_generic.ts") # DISABLED: see the matching test-compile-shared-export-import-class-accessor comment above (known issue, 2026-07-22). diff --git a/tslang/test/tester/tests/export_class_implements_interface_abstract.ts b/tslang/test/tester/tests/export_class_implements_interface_abstract.ts new file mode 100644 index 000000000..72cc4923b --- /dev/null +++ b/tslang/test/tester/tests/export_class_implements_interface_abstract.ts @@ -0,0 +1,26 @@ +namespace M { + + // An abstract class implementing an interface concretely (describe, + // which itself calls the still-abstract area()) - combines two things + // already proven to work separately cross-module (abstract classes: + // export_class_abstract.ts; class-implements-interface: + // export_class_interface.ts) in one type, which is exactly where + // cross-cutting vtable-layout bugs tend to hide: the concrete subclass's + // vtable must carry both the abstract-class-inherited slots AND the + // interface's slots, correctly interleaved, built across a module + // boundary. + + export interface Describable { + describe(): string; + } + + export abstract class Shape implements Describable { + color: string = "red"; + + abstract area(): number; + + describe(): string { + return `${this.color} area=${this.area()}`; + } + } +} diff --git a/tslang/test/tester/tests/export_class_implements_interface_multilevel.ts b/tslang/test/tester/tests/export_class_implements_interface_multilevel.ts new file mode 100644 index 000000000..f89f8de18 --- /dev/null +++ b/tslang/test/tester/tests/export_class_implements_interface_multilevel.ts @@ -0,0 +1,28 @@ +namespace M { + + // A class implementing an interface that itself `extends` another + // interface (multilevel), all defined in the exporting module - the + // class analogue of the object-literal multilevel-extends-interface + // coverage (export_object_literal_structural_typed_extends_interface_multilevel.ts), + // which found real cross-module vtable-offset bugs for the object-literal + // case. Exercises whether a real `class ... implements Derived` gets the + // same correct vtable slot assignment across a module boundary. + + export interface Base { + base: number; + describe(): string; + } + + export interface Derived extends Base { + derived: number; + } + + export class Impl implements Derived { + base: number = 1; + derived: number = 2; + + describe(): string { + return `base=${this.base},derived=${this.derived}`; + } + } +} diff --git a/tslang/test/tester/tests/export_class_implements_interface_optional.ts b/tslang/test/tester/tests/export_class_implements_interface_optional.ts new file mode 100644 index 000000000..4a1802ff4 --- /dev/null +++ b/tslang/test/tester/tests/export_class_implements_interface_optional.ts @@ -0,0 +1,26 @@ +namespace M { + + // A class implementing an interface with an optional field, one class + // providing it and one omitting it entirely - the class analogue of + // export_object_literal_structural_typed_extends_interface_optional.ts, + // whose object-literal equivalent (getInterfaceCloneFields backfilling + // instead of omitting a missing optional field) took 4 attempts to fix + // (see interface-extends-optional-field-clone-bug-fix memory). Exercises + // whether a real class that simply never declares the optional member + // gets the same correct "field genuinely absent" clone across a module + // boundary, rather than backfilled garbage. + + export interface Shape { + base: number; + opt?: number; + } + + export class WithOpt implements Shape { + base: number = 1; + opt: number = 5; + } + + export class WithoutOpt implements Shape { + base: number = 2; + } +} diff --git a/tslang/test/tester/tests/export_class_structural_interface.ts b/tslang/test/tester/tests/export_class_structural_interface.ts new file mode 100644 index 000000000..a9fd45277 --- /dev/null +++ b/tslang/test/tester/tests/export_class_structural_interface.ts @@ -0,0 +1,30 @@ +namespace M { + + // Printable/PrintableWithId are satisfied structurally, not declared via + // an explicit `implements` clause anywhere - casting to them triggers + // mlirGenCreateInterfaceVTableForClass (MLIRGenCast.cpp), the on-demand + // cast-time path, which is a DIFFERENT call site from + // mlirGenClassHeritageClauseImplements (the declaration-time path all of + // export_class_interface.ts / export_class_implements_interface_*.ts + // went through). `tag?: string` on Printable also re-exercises the + // isMissing fix (see class-implements-interface-optional-crash-fix + // memory) through this different path. + + export interface Printable { + describe(): string; + tag?: string; + } + + export interface PrintableWithId extends Printable { + id: number; + } + + export class Widget { + id: number = 42; + tag: string = "widget"; + + describe(): string { + return `Widget#${this.id}`; + } + } +} diff --git a/tslang/test/tester/tests/import_class_implements_interface_abstract.ts b/tslang/test/tester/tests/import_class_implements_interface_abstract.ts new file mode 100644 index 000000000..cf5728573 --- /dev/null +++ b/tslang/test/tester/tests/import_class_implements_interface_abstract.ts @@ -0,0 +1,24 @@ +import './export_class_implements_interface_abstract' + +class Square extends M.Shape { + side: number = 3; + + area(): number { + return this.side * this.side; + } +} + +function main() { + const s = new Square(); + assert(s.area() == 9); + assert(s.describe() == "red area=9"); + + const asDescribable: M.Describable = s; + assert(asDescribable.describe() == "red area=9"); + + const asShape: M.Shape = s; + assert(asShape.describe() == "red area=9"); + assert(asShape.area() == 9); + + print("done."); +} diff --git a/tslang/test/tester/tests/import_class_implements_interface_multilevel.ts b/tslang/test/tester/tests/import_class_implements_interface_multilevel.ts new file mode 100644 index 000000000..67ee65762 --- /dev/null +++ b/tslang/test/tester/tests/import_class_implements_interface_multilevel.ts @@ -0,0 +1,17 @@ +import './export_class_implements_interface_multilevel' + +function main() { + const obj = new M.Impl(); + assert(obj.describe() == "base=1,derived=2"); + + const asDerived: M.Derived = obj; + assert(asDerived.base == 1); + assert(asDerived.derived == 2); + assert(asDerived.describe() == "base=1,derived=2"); + + const asBase: M.Base = obj; + assert(asBase.base == 1); + assert(asBase.describe() == "base=1,derived=2"); + + print("done."); +} diff --git a/tslang/test/tester/tests/import_class_implements_interface_optional.ts b/tslang/test/tester/tests/import_class_implements_interface_optional.ts new file mode 100644 index 000000000..6951adaaf --- /dev/null +++ b/tslang/test/tester/tests/import_class_implements_interface_optional.ts @@ -0,0 +1,13 @@ +import './export_class_implements_interface_optional' + +function main() { + const withOpt: M.Shape = new M.WithOpt(); + assert(withOpt.base == 1); + assert(withOpt.opt == 5); + + const withoutOpt: M.Shape = new M.WithoutOpt(); + assert(withoutOpt.base == 2); + assert(withoutOpt.opt == undefined); + + print("done."); +} diff --git a/tslang/test/tester/tests/import_class_structural_interface.ts b/tslang/test/tester/tests/import_class_structural_interface.ts new file mode 100644 index 000000000..6bcb99c72 --- /dev/null +++ b/tslang/test/tester/tests/import_class_structural_interface.ts @@ -0,0 +1,38 @@ +import './export_class_structural_interface' + +// declared entirely in the importer, also with no `implements` clause - +// structurally cast to the EXPORTER's interfaces, and omits the optional +// `tag` field (unlike M.Widget, which provides it) +class LocalWidget { + id: number = 7; + + describe(): string { + return `Local#${this.id}`; + } +} + +function main() { + const w = new M.Widget(); + + const asPrintable: M.Printable = w; + assert(asPrintable.describe() == "Widget#42"); + assert(asPrintable.tag == "widget"); + + const asPrintableWithId: M.PrintableWithId = w; + assert(asPrintableWithId.id == 42); + assert(asPrintableWithId.describe() == "Widget#42"); + assert(asPrintableWithId.tag == "widget"); + + const lw = new LocalWidget(); + + const asLocalPrintable: M.Printable = lw; + assert(asLocalPrintable.describe() == "Local#7"); + assert(asLocalPrintable.tag == undefined); + + const asLocalPrintableWithId: M.PrintableWithId = lw; + assert(asLocalPrintableWithId.id == 7); + assert(asLocalPrintableWithId.describe() == "Local#7"); + assert(asLocalPrintableWithId.tag == undefined); + + print("done."); +} From c17b169373f8fad26d1305f45076ca96ec47566a Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Wed, 22 Jul 2026 18:07:29 +0100 Subject: [PATCH 2/2] Fix interface vtable handling for dynamic imports and resolve debug-info issues --- .../TypeScript/LowerToLLVM/LLVMCodeHelper.h | 10 ++- tslang/lib/TypeScript/LowerToLLVM.cpp | 12 ++- tslang/lib/TypeScript/MLIRGenClasses.cpp | 90 +++++++++++++++++-- tslang/test/tester/CMakeLists.txt | 60 +++++++------ 4 files changed, 136 insertions(+), 36 deletions(-) diff --git a/tslang/include/TypeScript/LowerToLLVM/LLVMCodeHelper.h b/tslang/include/TypeScript/LowerToLLVM/LLVMCodeHelper.h index ca1f03c36..7ed224904 100644 --- a/tslang/include/TypeScript/LowerToLLVM/LLVMCodeHelper.h +++ b/tslang/include/TypeScript/LowerToLLVM/LLVMCodeHelper.h @@ -279,7 +279,15 @@ class LLVMCodeHelper : public LLVMCodeHelperBase rewriter.create(location, value, addrToSave); } - rewriter.replaceOpWithNewOp(lastBlock->getTerminator()); + // don't let replaceOpWithNewOp inherit the replaced terminator's own location - that + // terminator (a GlobalResultOp from the global's original initializer) still carries + // whatever "live" source location the initializer happened to be built with (which, + // for a lazily-triggered global, can be deep inside an unrelated function's body - + // see the caller in LowerToLLVM.cpp for the full explanation). Use this function's + // own (already correct) `location` instead, matching newFuncOp's. + rewriter.setInsertionPoint(lastBlock->getTerminator()); + rewriter.create(location, mlir::ValueRange{}); + rewriter.eraseOp(lastBlock->getTerminator()); rewriter.eraseBlock(&newFuncOp.getBody().back()); diff --git a/tslang/lib/TypeScript/LowerToLLVM.cpp b/tslang/lib/TypeScript/LowerToLLVM.cpp index f8d13a562..d60056e11 100644 --- a/tslang/lib/TypeScript/LowerToLLVM.cpp +++ b/tslang/lib/TypeScript/LowerToLLVM.cpp @@ -3728,7 +3728,17 @@ struct GlobalOpLowering : public TsLlvmPattern auto name = globalOp.getSymName().str(); name.append("__cctor"); - lch.createFunctionFromRegion(loc, name, globalOp.getInitializerRegion(), globalOp.getSymName()); + // this synthesized per-global constructor function is compiler-internal glue, not + // user source - it must NOT be created with `loc` (the GlobalOp's own location): + // globalOp can be a lazily-triggered vtable/global whose location is wherever the + // triggering expression happened to be (e.g. a cast expression deep inside an + // unrelated function's body), and giving the new FuncOp that same "live" source + // location causes the debug-info pass to attach that enclosing function's + // DISubprogram to __cctor too - LLVM's verifier then rejects the module + // ("DISubprogram attached to more than one function") since a DISubprogram must + // identify exactly one function. Same "no debug info for this synthetic function" + // precedent as setDISubProgramTypesToFormOp's bodiless-declaration handling above. + lch.createFunctionFromRegion(mlir::UnknownLoc::get(rewriter.getContext()), name, globalOp.getInitializerRegion(), globalOp.getSymName()); { OpBuilder::InsertionGuard insertGuard(rewriter); diff --git a/tslang/lib/TypeScript/MLIRGenClasses.cpp b/tslang/lib/TypeScript/MLIRGenClasses.cpp index 02f247078..899c23391 100644 --- a/tslang/lib/TypeScript/MLIRGenClasses.cpp +++ b/tslang/lib/TypeScript/MLIRGenClasses.cpp @@ -1542,6 +1542,39 @@ genContext); return result; } + // a method implementing this interface can itself be owned by a dynamically imported + // class (not just a base of it) - such a slot can't be a link-time constant SymbolRefOp + // and must be resolved at runtime instead (see the dynamic-import handling in the method + // branch below, mirroring mlirGenClassVirtualTableDefinition's existing treatment of its + // own (non-interface) vtable slots). + std::function findMethodOwner = + [&](ClassInfo::TypePtr cls, mlir::StringRef funcName) -> ClassInfo::TypePtr { + if (llvm::any_of(cls->methods, [&](auto &m) { return m.funcName == funcName; })) + { + return cls; + } + + for (auto &base : cls->baseClasses) + { + if (auto owner = findMethodOwner(base, funcName)) + { + return owner; + } + } + + return ClassInfo::TypePtr(); + }; + + auto needsRuntimeSymbolResolution = llvm::any_of(virtualTable, [&](auto &methodOrField) { + if (methodOrField.isMissing || methodOrField.isField) + { + return false; + } + + auto owner = findMethodOwner(newClassPtr, methodOrField.methodInfo.funcName); + return owner && owner->isDynamicImport; + }); + // register global auto fullClassInterfaceVTableFieldName = interfaceVTableNameForClass(newClassPtr, newInterfacePtr); auto registeredType = registerVariable( @@ -1549,6 +1582,22 @@ genContext); [&](mlir::Location location, const GenContext &genContext) { // build vtable from names of methods + // this global's initializer can be rebuilt lazily from a call site deep inside + // an unrelated function (a cast expression in main(), via + // mlirGenCreateInterfaceVTableForClass); `location` still carries that call + // site's debug scope. Once any slot needs runtime symbol resolution, + // GlobalOpLowering promotes the WHOLE initializer into a synthesized __cctor + // function with its own DISubprogram (see the comment there) - so EVERY op + // built in this callback (not just the one slot that triggered the promotion) + // ends up living in __cctor, and must not carry a `!dbg` pointing at the + // calling site's (wrong) subprogram. This is compiler-synthesized glue, not + // user source worth stepping through, so drop location info entirely rather + // than try to keep it consistent per-op. + if (needsRuntimeSymbolResolution) + { + location = mlir::UnknownLoc::get(builder.getContext()); + } + MLIRCodeLogic mcl(builder, compileOptions); auto virtTuple = getVirtualTableType(virtualTable); @@ -1606,13 +1655,44 @@ genContext); } else { - auto methodConstName = builder.create( - location, methodOrField.methodInfo.funcType, - mlir::FlatSymbolRefAttr::get(builder.getContext(), - methodOrField.methodInfo.funcName)); + // the interface-implementing method can itself be owned by a + // dynamically imported class (not just a base of it - the class + // satisfying the interface may live in another module entirely) - same + // "not a link-time constant, resolve at runtime" situation + // mlirGenClassVirtualTableDefinition already handles for its own + // (non-interface) vtable slots, just not previously mirrored here. + auto owner = findMethodOwner(newClassPtr, methodOrField.methodInfo.funcName); + mlir::Value methodValueRef; + if (owner && owner->isDynamicImport) + { + // `location` is already UnknownLoc here (see + // needsRuntimeSymbolResolution above, computed from this same + // owner->isDynamicImport condition) - the resulting + // SearchForAddressOfSymbolOp forces this whole global's + // initializer into a synthesized __cctor function via + // GlobalOpLowering, and a "live" (main-scoped) location on any op + // living there would attach the wrong DISubprogram to it. + auto symbolNameValue = V(mlirGenStringValue(location, methodOrField.methodInfo.funcName, true)); + auto referenceToSymbolOpaque = builder.create( + location, getOpaqueType(), symbolNameValue); + auto castResult = cast(location, methodOrField.methodInfo.funcType, referenceToSymbolOpaque, genContext); + if (castResult.failed_or_no_value()) + { + return TypeValueInitType{mlir::Type(), mlir::Value(), TypeProvided::No}; + } + + methodValueRef = V(castResult); + } + else + { + methodValueRef = builder.create( + location, methodOrField.methodInfo.funcType, + mlir::FlatSymbolRefAttr::get(builder.getContext(), + methodOrField.methodInfo.funcName)); + } vtableValue = builder.create( - location, virtTuple, methodConstName, vtableValue, + location, virtTuple, methodValueRef, vtableValue, MLIRHelper::getStructIndex(builder, fieldIndex)); } diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 86d9f2678..eca6d87b9 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -903,35 +903,37 @@ add_test(NAME test-compile-shared-export-import-class-extends-implements-diamond add_test(NAME test-compile-shared-export-import-class-extends-multilevel COMMAND test-runner -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_extends_multilevel.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_extends_multilevel.ts") add_test(NAME test-compile-shared-export-import-class-abstract COMMAND test-runner -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_abstract.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_abstract.ts") add_test(NAME test-compile-shared-export-import-class-static COMMAND test-runner -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_static.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_static.ts") -# DISABLED: known issue found 2026-07-22. mlirGenClassVirtualTableDefinitionForInterface -# (a class's own per-interface vtable, built when it `implements` something) always emits a -# constant SymbolRefOp for the implementing method, unlike mlirGenClassVirtualTableDefinition -# (the class's ordinary vtable), which already special-cases a method owned by a dynamic-import -# class/base and resolves it at runtime instead (SearchForAddressOfSymbolOp) since a link-time -# constant address doesn't exist across a DLL boundary. Here the class ITSELF (M.Impl / M.Shape, -# not just an ancestor) is the dynamic import, and its interface-vtable is rebuilt from the -# importer's side, so the constant SymbolRefOp is unresolvable: lld fails with -# "undefined symbol: M.Impl.describe" (a method reachable only through the interface, e.g. -# `const asDerived: M.Derived = obj; asDerived.describe()`). A first attempt to mirror the -# working dynamic-import runtime-resolution pattern into the interface-vtable builder compiled, -# but broke DWARF debug-info generation ("DISubprogram attached to more than one function") and -# a runtime assert in the abstract-class variant - reverted rather than land a fix with -# unexamined side effects (see cross-module-class-coverage-2026-07 memory). The plain "compile" -# tier (no DLL boundary - both files link into one exe) and the jit-shared tier of the -# multilevel variant are unaffected and stay enabled below. -# add_test(NAME test-compile-shared-export-import-class-implements-interface-multilevel COMMAND test-runner -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_implements_interface_multilevel.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_implements_interface_multilevel.ts") +# FIXED 2026-07-22 (was disabled): mlirGenClassVirtualTableDefinitionForInterface (a class's own +# per-interface vtable, built when it `implements` something) used to always emit a constant +# SymbolRefOp for the implementing method, unlike mlirGenClassVirtualTableDefinition (the class's +# ordinary vtable), which already special-cased a method owned by a dynamic-import class/base and +# resolved it at runtime instead (SearchForAddressOfSymbolOp) since a link-time constant address +# doesn't exist across a DLL boundary. Mirrored that same runtime-resolution pattern into the +# interface-vtable builder's method branch. That alone broke DWARF debug-info generation +# ("DISubprogram attached to more than one function") because this global's initializer can be +# rebuilt lazily from a call site deep inside an unrelated function (a cast expression in main(), +# via mlirGenCreateInterfaceVTableForClass) - two further fixes were needed: (1) GlobalOpLowering +# (LowerToLLVM.cpp) was giving the synthesized __cctor constructor function the triggering call +# site's own "live" location instead of a neutral one, so the debug-info pass attached the wrong +# DISubprogram to it; (2) createFunctionFromRegion's replaceOpWithNewOp (LLVMCodeHelper.h) +# implicitly inherited the replaced terminator's (also wrong) location instead of the function's +# own. See class-implements-interface-dynamic-import-fix memory for the full account. +add_test(NAME test-compile-shared-export-import-class-implements-interface-multilevel COMMAND test-runner -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_implements_interface_multilevel.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_implements_interface_multilevel.ts") add_test(NAME test-compile-shared-export-import-class-implements-interface-optional COMMAND test-runner -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_implements_interface_optional.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_implements_interface_optional.ts") -# DISABLED: same dynamic-import interface-vtable gap as -# test-compile-shared-export-import-class-implements-interface-multilevel above (known issue, -# 2026-07-22) - M.Widget is itself the dynamic import and gets cast to its own interface -# (M.Printable) from the importer, so the same "undefined symbol: M.Widget.describe" link -# failure applies here too, reached via the structural/cast-triggered path -# (mlirGenCreateInterfaceVTableForClass) rather than the explicit-`implements` declaration-time -# path - confirms both paths share the same root cause. The LocalWidget half of this test (a -# class declared locally in the importer, cast to the cross-module interface) is unaffected and -# still exercised by the other 2 tiers below. -# add_test(NAME test-compile-shared-export-import-class-structural-interface COMMAND test-runner -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_structural_interface.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_structural_interface.ts") -# DISABLED: see the matching test-compile-shared-export-import-class-implements-interface-multilevel comment above (known issue, 2026-07-22). +add_test(NAME test-compile-shared-export-import-class-structural-interface COMMAND test-runner -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_structural_interface.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_structural_interface.ts") +# DISABLED: a separate, NOT fixed bug found 2026-07-22 while verifying the fix above. Unlike the +# multilevel/optional/structural variants (all pass now), this one calls describe() - a CONCRETE +# method inherited from the abstract dynamic-import base M.Shape, whose body internally calls the +# still-virtual this.area() - through an interface reference +# (`const asDescribable: M.Describable = s; asDescribable.describe()`). Confirmed via the real +# harness (instrumented with print instead of assert, see the session transcript): the DIRECT call +# `s.describe()` and the base-class-cast call `asShape.describe()` both return the correct +# "red area=9", but the SAME describe() reached through the interface returns "red area=0" - +# this.area() silently resolves to 0 instead of dispatching to Square's override, specifically +# when invoked via the interface's function-pointer slot. Root cause not yet isolated (likely a +# `this`-identity/binding gap specific to calling an inherited concrete method through an +# interface vtable slot, distinct from the link-time issue fixed above) - left for a dedicated +# follow-up. # add_test(NAME test-compile-shared-export-import-class-implements-interface-abstract COMMAND test-runner -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_implements_interface_abstract.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_implements_interface_abstract.ts") # DISABLED: see the matching test-compile-export-import-class-generic comment above (known issue, 2026-07-22). # add_test(NAME test-compile-shared-export-import-class-generic COMMAND test-runner -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_generic.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_generic.ts") @@ -975,7 +977,7 @@ add_test(NAME test-jit-shared-export-import-class-static COMMAND test-runner -ji add_test(NAME test-jit-shared-export-import-class-implements-interface-multilevel COMMAND test-runner -jit -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_implements_interface_multilevel.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_implements_interface_multilevel.ts") add_test(NAME test-jit-shared-export-import-class-implements-interface-optional COMMAND test-runner -jit -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_implements_interface_optional.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_implements_interface_optional.ts") add_test(NAME test-jit-shared-export-import-class-structural-interface COMMAND test-runner -jit -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_structural_interface.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_structural_interface.ts") -# DISABLED: see the test-compile-shared-export-import-class-implements-interface-multilevel comment above (known issue, 2026-07-22) - the JIT tier of the abstract variant also crashes (unlike the multilevel variant's JIT tier, which passes). +# DISABLED: see the matching test-compile-shared-export-import-class-implements-interface-abstract comment above (known issue, 2026-07-22, NOT fixed) - the JIT tier of the abstract variant hits the same this.area()-returns-0-via-interface bug. # add_test(NAME test-jit-shared-export-import-class-implements-interface-abstract COMMAND test-runner -jit -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_implements_interface_abstract.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_implements_interface_abstract.ts") # DISABLED: see the matching test-compile-export-import-class-generic comment above (known issue, 2026-07-22). # add_test(NAME test-jit-shared-export-import-class-generic COMMAND test-runner -jit -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_generic.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_generic.ts")