diff --git a/tslang/docs/interface-vtable-simplification-design.md b/tslang/docs/interface-vtable-simplification-design.md index db7eaa94c..fe7414c8d 100644 --- a/tslang/docs/interface-vtable-simplification-design.md +++ b/tslang/docs/interface-vtable-simplification-design.md @@ -653,13 +653,35 @@ or whether the heap-cloned vtable's allocated SIZE is computed from a stale/undersized type (a 2-slot allocation for a >2-slot vtable would also match this exact crash-only-past-slot-N shape). -**Not fixed.** The regression test actually added for this session -(`export/import_object_literal_structural_typed_params.ts`) deliberately -stays within the single-method shape that's known to work, to avoid -committing a failing test; it extends coverage only along the -"zero-arg -> parameterized method" axis, not the "single-method -> -multi-method" axis. A genuinely multi-method cross-module test is blocked -on this bug and is the natural next thing to add once it's fixed. +**FIXED (2026-07-20).** The actual root cause was upstream of all the +candidates above: it wasn't the vtable-patch loop or the heap-clone size at +all, both of which were correct. The bug was in the decl-text PRINTER +(`MLIRPrinter.h`) used to emit a `@dllimport` declaration's type for +cross-module round-tripping. A structurally-typed export's method fields are +physically stored as plain `FunctionType` (a raw 8-byte function pointer - +`this`/the object is implicit via the container, never a separate stored +pointer). But the printer described named-field (object-shaped) types using +POSITIONAL TUPLE syntax (`[name: (args) => result]`) instead of object syntax +(`{name(args): result}`) - and arrow-type syntax parses back as +`HybridFunctionType` (a 16-byte `{data,func}` runtime-tagged pair, meant for +call-boundary/variable contexts, never for object-literal field storage). That +ABI mismatch added a fixed +8-byte read-offset drift per field on reimport: +the first method field happened to still read correctly, the second read a +plausible-looking wrong value, and the third read 8 bytes past the end of the +real object entirely - matching this section's exact "slot 0 fine, slot 1 +wrong, slot 2 crash" bisection precisely, without the vtable-patch code being +at fault for any of it. + +Fix: `printType`'s dispatch for `TupleType`/`ConstTupleType`/`ObjectStorageType` +now calls `printObjectType` (not `printTupleType`) whenever the fields are all +named, via a new `isObjectShapedTuple`/`printTupleOrObjectType` helper pair - +this is exactly what `printFields`'s own pre-existing comment already +diagnosed, just not yet wired up. Verified via a genuinely multi-method +(`add`/`addTwice`/`scaled`) cross-module cast returning correct values +end-to-end; new regression test +`export/import_object_literal_structural_typed_multi_method.ts` (both +`-compile-shared` and `-jit-shared` variants) covers exactly the axis this +section originally left uncovered. Full 734-test suite: 100% pass. Also worth noting for whoever investigates: a completely SEPARATE, same-module-only finding surfaced while building the initial (broken) diff --git a/tslang/include/TypeScript/MLIRLogic/MLIRPrinter.h b/tslang/include/TypeScript/MLIRLogic/MLIRPrinter.h index 1bd78f423..bf956022a 100644 --- a/tslang/include/TypeScript/MLIRLogic/MLIRPrinter.h +++ b/tslang/include/TypeScript/MLIRLogic/MLIRPrinter.h @@ -218,6 +218,33 @@ class MLIRPrinter out << "}"; } + // a tuple whose fields are all named (real `id`, as opposed to a + // positional element with no id) is structurally an object, not a + // positional tuple - print it with printObjectType so method-shaped + // FunctionType fields round-trip via method-signature syntax instead of + // silently downgrading to arrow-type syntax (HybridFunctionType on + // reimport - see printFields' allowMethodSignature comment for the full + // mechanism and why this matters for cross-module @dllimport decls). + template + bool isObjectShapedTuple(TPL t) + { + auto fields = t.getFields(); + return !fields.empty() && llvm::all_of(fields, [](auto &field) { return (bool)field.id; }); + } + + template + void printTupleOrObjectType(T &out, TPL t) + { + if (isObjectShapedTuple(t)) + { + printObjectType(out, t); + } + else + { + printTupleType(out, t); + } + } + template void printUnionType(T &out, U t, const char *S) { @@ -303,7 +330,7 @@ class MLIRPrinter out << "[]"; }) .template Case([&](auto t) { - printTupleType(out, t); + printTupleOrObjectType(out, t); }) .template Case([&](auto t) { //printType(out, t.getElementType()); @@ -340,7 +367,7 @@ class MLIRPrinter out << ">"; }) .template Case([&](auto t) { - printTupleType(out, t); + printTupleOrObjectType(out, t); }) .template Case([&](auto t) { printUnionType(out, t, " | "); @@ -429,7 +456,7 @@ class MLIRPrinter } }) .template Case([&](auto t) { - printTupleType(out, t); + printObjectType(out, t); }) .template Case([&](auto) { out << "never"; diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index b9b1df192..1b5724478 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -856,6 +856,7 @@ add_test(NAME test-compile-shared-export-import-object-literal-with-interface CO add_test(NAME test-compile-shared-export-import-object-literal-untyped COMMAND test-runner -shared -gctors-as-method "${PROJECT_SOURCE_DIR}/test/tester/tests/import_object_literal_untyped.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_object_literal_untyped.ts") add_test(NAME test-compile-shared-export-import-object-literal-structural-typed COMMAND test-runner -shared -gctors-as-method "${PROJECT_SOURCE_DIR}/test/tester/tests/import_object_literal_structural_typed.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_object_literal_structural_typed.ts") add_test(NAME test-compile-shared-export-import-object-literal-structural-typed-params COMMAND test-runner -shared -gctors-as-method "${PROJECT_SOURCE_DIR}/test/tester/tests/import_object_literal_structural_typed_params.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_object_literal_structural_typed_params.ts") +add_test(NAME test-compile-shared-export-import-object-literal-structural-typed-multi-method COMMAND test-runner -shared -gctors-as-method "${PROJECT_SOURCE_DIR}/test/tester/tests/import_object_literal_structural_typed_multi_method.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_object_literal_structural_typed_multi_method.ts") add_test(NAME test-compile-shared-export-import-vars COMMAND test-runner -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_vars.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_vars.ts") add_test(NAME test-compile-shared-export-import-vars-2 COMMAND test-runner -shared -gctors-as-method "${PROJECT_SOURCE_DIR}/test/tester/tests/import_vars2.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_vars2.ts") add_test(NAME test-compile-shared-export-import-enum COMMAND test-runner -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_enum.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_enum.ts") @@ -874,6 +875,7 @@ add_test(NAME test-jit-shared-export-import-object-literal-with-interface COMMAN add_test(NAME test-jit-shared-export-import-object-literal-untyped COMMAND test-runner -jit -shared -gctors-as-method "${PROJECT_SOURCE_DIR}/test/tester/tests/import_object_literal_untyped.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_object_literal_untyped.ts") add_test(NAME test-jit-shared-export-import-object-literal-structural-typed COMMAND test-runner -jit -shared -gctors-as-method "${PROJECT_SOURCE_DIR}/test/tester/tests/import_object_literal_structural_typed.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_object_literal_structural_typed.ts") add_test(NAME test-jit-shared-export-import-object-literal-structural-typed-params COMMAND test-runner -jit -shared -gctors-as-method "${PROJECT_SOURCE_DIR}/test/tester/tests/import_object_literal_structural_typed_params.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_object_literal_structural_typed_params.ts") +add_test(NAME test-jit-shared-export-import-object-literal-structural-typed-multi-method COMMAND test-runner -jit -shared -gctors-as-method "${PROJECT_SOURCE_DIR}/test/tester/tests/import_object_literal_structural_typed_multi_method.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_object_literal_structural_typed_multi_method.ts") add_test(NAME test-jit-shared-export-import-vars COMMAND test-runner -jit -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_vars.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_vars.ts") add_test(NAME test-jit-shared-export-import-vars-2 COMMAND test-runner -jit -shared -gctors-as-method "${PROJECT_SOURCE_DIR}/test/tester/tests/import_vars2.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_vars2.ts") add_test(NAME test-jit-shared-export-import-enum COMMAND test-runner -jit -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_enum.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_enum.ts") diff --git a/tslang/test/tester/tests/export_object_literal_structural_typed_multi_method.ts b/tslang/test/tester/tests/export_object_literal_structural_typed_multi_method.ts new file mode 100644 index 000000000..74c76a59c --- /dev/null +++ b/tslang/test/tester/tests/export_object_literal_structural_typed_multi_method.ts @@ -0,0 +1,29 @@ +namespace A { + + export interface Accumulator { + total: number; + add(n: number): void; + addTwice(n: number): void; + scaled(factor: number): number; + } + + // extends export_object_literal_structural_typed_params.ts's coverage: that + // test deliberately stayed within a SINGLE-method shape because a genuinely + // multi-method structurally-typed export used to corrupt every method field + // after the first when reconstructed cross-module - see + // docs/interface-vtable-simplification-design.md's "multi-method + // cross-module vtable slot bug" section. Root cause turned out to be in the + // decl-text printer (MLIRPrinter.h's printType), not the vtable-patch code: + // a named-field tuple type was printed with positional tuple syntax + // ("[name: (args) => result]") instead of object syntax + // ("{name(args): result}"), so method fields re-imported as the wider + // HybridFunctionType (16-byte {data,func} pair) instead of the exporter's + // actual plain FunctionType (8-byte raw pointer) storage - misaligning + // every field after the first. + export var acc: { total: number; add(n: number): void; addTwice(n: number): void; scaled(factor: number): number } = { + total: 0.0, + add(n: number) { this.total = this.total + n; }, + addTwice(n: number) { this.add(n); this.add(n); }, + scaled(factor: number) { return this.total * factor; }, + }; +} diff --git a/tslang/test/tester/tests/import_object_literal_structural_typed_multi_method.ts b/tslang/test/tester/tests/import_object_literal_structural_typed_multi_method.ts new file mode 100644 index 000000000..aa202636b --- /dev/null +++ b/tslang/test/tester/tests/import_object_literal_structural_typed_multi_method.ts @@ -0,0 +1,19 @@ +import './export_object_literal_structural_typed_multi_method' + +// Casts the imported structurally-typed VALUE to a MULTI-method interface, +// calling each method to exercise every vtable slot - see +// export_object_literal_structural_typed_multi_method.ts for what this covers. +var acc: A.Accumulator = A.acc; + +acc.add(3); +print(acc.total); +assert(acc.total == 3); + +acc.addTwice(2); +print(acc.total); +assert(acc.total == 7); + +print(acc.scaled(2)); +assert(acc.scaled(2) == 14); + +print("done.");