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
38 changes: 38 additions & 0 deletions tslang/docs/interface-vtable-simplification-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -435,3 +435,41 @@ not-yet-investigated bug. No regression test was added for the
now-fixed crash (a full end-to-end test would still fail, on this new bug,
not the old one); this section records the fix and the newly-found blocker
for whoever picks up the clone-bug investigation next.

### Clone field-order bug FIXED (2026-07-19, follow-up to the above)

Root cause of the reversed clone: both clone-building blocks (in
`castTupleToInterface` and `castObjectToInterface`, MLIRGenCast.cpp) built
the clone's field list from `InterfaceInfo::getTupleTypeFields`, which
emits **methods first, then fields** (MLIRGenStore.h) - so for
`Counter {count; inc()}` the clone's layout became `[inc@0, count@8]`,
reversed from the source tuple's `[count@0, inc@8]`. Two consumers still
addressed fields by the source layout: the interface vtable's field-offset
slots and - fatally, unfixable from the importer - the exporting module's
already-compiled `inc` body, which hard-codes `count` at offset 0. Hence
`c.count` read the funcptr slot and `inc()` incremented it.

Fix: new file-local helper `getInterfaceCloneFields` (MLIRGenCast.cpp),
used by both clone sites - the clone now preserves the SOURCE tuple's field
order, taking only the field TYPES from the interface (the type-coercion
purpose of the clone, e.g. si32 -> number or func-typed field vs method
funcType), with interface-only members appended after the source fields.
All member lookups downstream are name-based, so only the byte layout was
at stake.

With this plus PR #256, the full cross-module scenario finally works:
regression test pair added -
`export_object_literal_structural_typed.ts` (structurally-typed
method-bearing export, NOT interface-typed at the definition site) /
`import_object_literal_structural_typed.ts` (casts the imported value to
the interface in the importer, asserts count 0 -> 1 -> 2 through the
interface), wired as
`test-{compile,jit}-shared-export-import-object-literal-structural-typed`.
Note the cast still clones (warning stands): mutations through the
interface don't write back to the exporter's global - that's the
documented value-semantics of this cast, not a bug in this arc. Known
remaining sharp edge (pre-existing, unchanged): a clone whose field TYPES
are size-changing coercions (e.g. si32 -> f64 number) still shifts offsets
relative to already-compiled method bodies expecting the original layout;
that can only bite literals whose inferred field types differ in size from
the interface's, and is out of scope here. 722/722 suite (720 + 2 new).
72 changes: 43 additions & 29 deletions tslang/lib/TypeScript/MLIRGenCast.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,47 @@ namespace typescript
namespace mlirgen
{

ValueOrLogicalResult MLIRGenImpl::selectFieldsValues(mlir::Location location, SmallVector<mlir::Value> &values, mlir::Value value,
// Field list for the clone made when a tuple/object value can't be cast to an
// interface as-is (field types need coercion, e.g. si32 -> number, or a
// func-typed field vs a method's funcType). The clone MUST keep the SOURCE
// tuple's field order: the interface vtable's field-offset slots and any
// methods compiled against the original object layout (in particular in
// another module) address fields by byte offset, so reordering to interface
// member order (methods-first, per InterfaceInfo::getTupleTypeFields)
// silently reads/writes the wrong slots at runtime. Only the TYPES are taken
// from the interface; interface-only members are appended after the source
// fields.
static mlir::LogicalResult getInterfaceCloneFields(mlir::ArrayRef<mlir_ts::FieldInfo> srcFields,
InterfaceInfo::TypePtr interfaceInfo, mlir::MLIRContext *context,
SmallVector<mlir_ts::FieldInfo> &fields)
{
SmallVector<mlir_ts::FieldInfo> interfaceFields;
if (mlir::failed(interfaceInfo->getTupleTypeFields(interfaceFields, context)))
{
return mlir::failure();
}

for (auto &origField : srcFields)
{
auto interfaceField =
std::find_if(interfaceFields.begin(), interfaceFields.end(),
[&](auto &item) { return item.id == origField.id; });
fields.push_back(interfaceField != interfaceFields.end() ? *interfaceField : origField);
}

for (auto &interfaceField : interfaceFields)
{
if (std::find_if(fields.begin(), fields.end(),
[&](auto &item) { return item.id == interfaceField.id; }) == fields.end())
{
fields.push_back(interfaceField);
}
}

return mlir::success();
}

ValueOrLogicalResult MLIRGenImpl::selectFieldsValues(mlir::Location location, SmallVector<mlir::Value> &values, mlir::Value value,
::llvm::ArrayRef<::mlir::typescript::FieldInfo> fields, bool filterSpecialCases, const GenContext &genContext, bool errorAsWarning)
{
auto count = 0;
Expand Down Expand Up @@ -1579,24 +1619,11 @@ namespace mlirgen
if (mlir::failed(mth.canCastTupleToInterface(location, srcTuple, interfaceInfo, true)))
{
SmallVector<mlir_ts::FieldInfo> fields;
if (mlir::failed(interfaceInfo->getTupleTypeFields(fields, builder.getContext())))
if (mlir::failed(getInterfaceCloneFields(srcTuple.getFields(), interfaceInfo, builder.getContext(), fields)))
{
return mlir::failure();
}

// append all fields from original tuple
for (auto origField : srcTuple.getFields()) {
if (std::find_if(
fields.begin(),
fields.end(),
[&] (auto& item) {
return item.id == origField.id;
}) == fields.end())
{
fields.push_back(origField);
}
}

auto newInterfaceTupleType = getTupleType(fields);
CAST(inEffective, location, newInterfaceTupleType, inEffective, genContext);
tupleType = newInterfaceTupleType;
Expand Down Expand Up @@ -1638,24 +1665,11 @@ namespace mlirgen
if (mlir::failed(mth.canCastTupleToInterface(location, storageTuple, interfaceInfo, true)))
{
SmallVector<mlir_ts::FieldInfo> fields;
if (mlir::failed(interfaceInfo->getTupleTypeFields(fields, builder.getContext())))
if (mlir::failed(getInterfaceCloneFields(storageTuple.getFields(), interfaceInfo, builder.getContext(), fields)))
{
return mlir::failure();
}

// append all fields from original tuple
for (auto origField : storageTuple.getFields()) {
if (std::find_if(
fields.begin(),
fields.end(),
[&] (auto& item) {
return item.id == origField.id;
}) == fields.end())
{
fields.push_back(origField);
}
}

auto newInterfaceTupleType = getTupleType(fields);

CAST_A(unboxed, location, newInterfaceTupleType, in, genContext);
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 @@ -847,6 +847,7 @@ add_test(NAME test-compile-shared-decl-emit-class COMMAND test-runner -shared "$
add_test(NAME test-compile-shared-export-import-class-interface COMMAND test-runner -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_interface.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_interface.ts")
add_test(NAME test-compile-shared-export-import-object-literal-with-class-types COMMAND test-runner -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_object_literal_with_class_types.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_object_literal_with_class_types.ts")
add_test(NAME test-compile-shared-export-import-object-literal-with-interface COMMAND test-runner -shared -gctors-as-method "${PROJECT_SOURCE_DIR}/test/tester/tests/import_object_literal_with_interface.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_object_literal_with_interface.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-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 @@ -862,6 +863,7 @@ add_test(NAME test-jit-shared-decl-emit-class COMMAND test-runner -jit -shared "
add_test(NAME test-jit-shared-export-import-class-interface COMMAND test-runner -jit -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_interface.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_interface.ts")
add_test(NAME test-jit-shared-export-import-object-literal-with-class-types COMMAND test-runner -jit -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_object_literal_with_class_types.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_object_literal_with_class_types.ts")
add_test(NAME test-jit-shared-export-import-object-literal-with-interface COMMAND test-runner -jit -shared -gctors-as-method "${PROJECT_SOURCE_DIR}/test/tester/tests/import_object_literal_with_interface.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_object_literal_with_interface.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-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")
14 changes: 14 additions & 0 deletions tslang/test/tester/tests/export_object_literal_structural_typed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace A {

export interface Counter {
count: number;
inc(): void;
}

// deliberately typed with a STRUCTURAL type, not the interface: the
// interface cast then happens in the IMPORTING module against the
// @dllimport-reconstructed tuple type (see
// import_object_literal_structural_typed.ts), unlike
// export_object_literal_with_interface.ts where the cast happens here.
export var counterObj: { count: number; inc(): void } = { count: 0, inc() { this.count = this.count + 1; } };
}
20 changes: 20 additions & 0 deletions tslang/test/tester/tests/import_object_literal_structural_typed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import './export_object_literal_structural_typed'

// Casting the imported structurally-typed VALUE to a method-bearing interface
// in the importing module. Regression coverage for two bugs:
// - the hybrid_func -> func cast crash in CastLogicHelper (PR #256);
// - the interface-cast clone reordering fields to interface (methods-first)
// order, which silently misread `count` through the vtable's field offset
// and made inc() corrupt the funcptr slot instead of incrementing.
// Note: this cast clones the object (see the compiler warning), so mutations
// through `c` are visible via `c` but not via A.counterObj.
var c: A.Counter = <A.Counter>A.counterObj;
print(c.count);
assert(c.count == 0);
c.inc();
print(c.count);
assert(c.count == 1);
c.inc();
print(c.count);
assert(c.count == 2);
print("done.");
Loading