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
22 changes: 22 additions & 0 deletions tslang/docs/object-literal-boxing-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,28 @@ Things that consume the literal's *value type* and must tolerate

## 6. Risks / open items

- **FIXED (2026-07-19): global interface from a method-bearing literal**. A
top-level binding whose declared type is an interface, initialized from an
object literal with a method (`const c: Counter = { count: 0, inc(){...} }`),
crashed the JIT (0xC0000005) on first access. Casting the literal to the
interface (`mlirGenCreateInterfaceVTableForObject`) builds a *per-object*
vtable patched with the method's function pointer; that vtable was a stack
`VariableOp` (alloca). A local binding's alloca outlives its uses, but a
global binding's initializer lowers to a `__cctor` function, so the alloca
dangled once `__cctor` returned and the interface's vtable pointer
referenced freed stack memory. Fix: heap-allocate the patched vtable
(`NewOp` + `StoreOp` + `CastOp`), same footing as the object it describes.
Regression test `00interface_global_method.ts`. Note the method-*less*
interface case (`Point`) was always fine — it points `NewInterface`
directly at the shared global vtable, no per-object patched copy.
- **STILL OPEN: cross-module (`-shared`) method-bearing interface**. Exporting
such a global and reading it from an importing module still crashes the JIT
(null function-pointer call in the importer's `__mlir_gctors`); the
`-shared` AOT path has its own separate issues too. This is a distinct
problem in the shared-lib DLL-load / `gctors-as-method` symbol-resolution
subsystem, not the vtable-lifetime bug fixed above (which was
single-module). Reproduces via the `export/import_object_literal_with_interface.ts`
pair with an added method-bearing `export var`. Not yet root-caused.
- **Annotated tuple types re-open a small value-semantics window**: after
gap 3, `const x: { m(): void } = { m() {} }` copies the object into a
tuple at the annotation boundary, losing aliasing for that binding. The
Expand Down
13 changes: 10 additions & 3 deletions tslang/lib/TypeScript/MLIRGenInterfaces.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -213,11 +213,18 @@ namespace mlirgen
}

// match VTable
// 1) clone vtable
// 1) clone vtable onto the GC heap (NOT a stack VariableOp): this per-object
// patched vtable is pointed at by the resulting interface value, and that
// interface can be stored into a global whose initializer lowers to a
// __cctor function -- a stack alloca would dangle once the __cctor returns,
// crashing on the first field/method access through the interface. Heap
// allocation puts the vtable on the same footing as the object itself
// (already `NewOp`-allocated). See docs/object-literal-boxing-design.md.
auto vtableType = mlir::cast<mlir_ts::TupleType>(mlir::cast<mlir_ts::RefType>(globalVTableRefValue.getType()).getElementType());
auto valueVTable = builder.create<mlir_ts::LoadOp>(location, vtableType, globalVTableRefValue);
auto varVTable = builder.create<mlir_ts::VariableOp>(location, globalVTableRefValue.getType(), valueVTable,
builder.getBoolAttr(false), builder.getIndexAttr(0));
auto heapVTable = builder.create<mlir_ts::NewOp>(location, mlir_ts::ValueRefType::get(vtableType), builder.getBoolAttr(false));
builder.create<mlir_ts::StoreOp>(location, valueVTable, heapVTable);
auto varVTable = builder.create<mlir_ts::CastOp>(location, globalVTableRefValue.getType(), heapVTable);

for (auto& method : newInterfacePtr->methods)
{
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 @@ -280,6 +280,7 @@ add_test(NAME test-compile-00-interface-object-2 COMMAND test-runner "${PROJECT_
add_test(NAME test-compile-00-interface-object-3 COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_object3.ts")
add_test(NAME test-compile-00-interface-object-4 COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_object4.ts")
add_test(NAME test-compile-00-interface-object-5 COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_object5.ts")
add_test(NAME test-compile-00-interface-global-method COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_global_method.ts")
add_test(NAME test-compile-00-interface-conjunction COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_conjunction.ts")
add_test(NAME test-compile-00-interface-partial COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_partial.ts")
add_test(NAME test-compile-00-interface-optional COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_optional.ts")
Expand Down Expand Up @@ -629,6 +630,7 @@ add_test(NAME test-jit-00-interface-object-2 COMMAND test-runner -jit "${PROJECT
add_test(NAME test-jit-00-interface-object-3 COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_object3.ts")
add_test(NAME test-jit-00-interface-object-4 COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_object4.ts")
add_test(NAME test-jit-00-interface-object-5 COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_object5.ts")
add_test(NAME test-jit-00-interface-global-method COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_global_method.ts")
add_test(NAME test-jit-00-interface-conjunction COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_conjunction.ts")
add_test(NAME test-jit-00-interface-partial COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_partial.ts")
add_test(NAME test-jit-00-interface-optional COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_optional.ts")
Expand Down
40 changes: 40 additions & 0 deletions tslang/test/tester/tests/00interface_global_method.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// regression test: a top-level (global) binding whose declared type is an interface,
// initialized from an object literal that has a method, used to crash the JIT with
// 0xC0000005 on the first field/method access.
// Root cause: casting the (boxed) object literal to the interface builds a per-object
// vtable patched with the method's function pointer; that vtable was a stack VariableOp
// (alloca). For a local binding the alloca outlives its uses, but a global binding's
// initializer lowers to a __cctor function, so the alloca dangled once the __cctor
// returned -- the interface's vtable pointer then referenced freed stack memory.
// Fix: heap-allocate the patched vtable (mlirGenCreateInterfaceVTableForObject), same
// footing as the object it describes.

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

const counter: Counter = {
count: 0,
inc() { this.count = this.count + 1; },
};

// a second same-shape global to make sure two independent per-object vtables both survive
const other: Counter = {
count: 100,
inc() { this.count = this.count + 10; },
};

function main() {
assert(counter.count == 0);
counter.inc();
counter.inc();
assert(counter.count == 2);

other.inc();
assert(other.count == 110);
// the two globals must not share state
assert(counter.count == 2);

print("done.");
}
Loading