Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 29 additions & 7 deletions tslang/docs/interface-vtable-simplification-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
33 changes: 30 additions & 3 deletions tslang/include/TypeScript/MLIRLogic/MLIRPrinter.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 <typename TPL>
bool isObjectShapedTuple(TPL t)
{
auto fields = t.getFields();
return !fields.empty() && llvm::all_of(fields, [](auto &field) { return (bool)field.id; });
}

template <typename T, typename TPL>
void printTupleOrObjectType(T &out, TPL t)
{
if (isObjectShapedTuple(t))
{
printObjectType(out, t);
}
else
{
printTupleType(out, t);
}
}

template <typename T, typename U>
void printUnionType(T &out, U t, const char *S)
{
Expand Down Expand Up @@ -303,7 +330,7 @@ class MLIRPrinter
out << "[]";
})
.template Case<mlir_ts::ConstTupleType>([&](auto t) {
printTupleType(out, t);
printTupleOrObjectType(out, t);
})
.template Case<mlir_ts::EnumType>([&](auto t) {
//printType(out, t.getElementType());
Expand Down Expand Up @@ -340,7 +367,7 @@ class MLIRPrinter
out << ">";
})
.template Case<mlir_ts::TupleType>([&](auto t) {
printTupleType(out, t);
printTupleOrObjectType(out, t);
})
.template Case<mlir_ts::UnionType>([&](auto t) {
printUnionType(out, t, " | ");
Expand Down Expand Up @@ -429,7 +456,7 @@ class MLIRPrinter
}
})
.template Case<mlir_ts::ObjectStorageType>([&](auto t) {
printTupleType(out, t);
printObjectType(out, t);
})
.template Case<mlir_ts::NeverType>([&](auto) {
out << "never";
Expand Down
2 changes: 2 additions & 0 deletions tslang/test/tester/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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")
Original file line number Diff line number Diff line change
@@ -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; },
};
}
Original file line number Diff line number Diff line change
@@ -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.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.");
Loading