From 1fd26cdcb5ea08be18068837126aca7836caadd2 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sun, 19 Jul 2026 14:36:44 +0100 Subject: [PATCH] Add captures-cast-to-interface test; document imported-object cast bugs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes out the two test-matrix gaps from docs/interface-vtable-simplification-design.md §7: - Captures-bearing object-literal method cast to an interface (00interface_captures.ts): confirms the constant-vtable path (§3) handles captures correctly - one shared function symbol per literal expression, per-instance state in a separate .captured field, no per-cast heap patch. - Casting an imported (cross-module) object value to an interface inside the importing module: attempting to write this test surfaced two separate pre-existing bugs, unrelated to this arc's changes, that make the scenario not work at all today regardless of how the value is exported - an mlir::cast assertion (MLIRGenInterfaces.cpp:312) when the export has no type annotation and degrades to a bare `object` type on reimport, and an llvm_unreachable("review usage") in the generic cast lowering (CastLogicHelper.h:765) when the export is given an explicit structural type instead. No test added (would just crash); both findings are documented in the design doc as a distinct, deeper bug area for future investigation, out of scope for this arc. 720/720 full suite green. Co-Authored-By: Claude Sonnet 5 --- .../interface-vtable-simplification-design.md | 66 +++++++++++++++++++ tslang/test/tester/CMakeLists.txt | 2 + .../test/tester/tests/00interface_captures.ts | 34 ++++++++++ 3 files changed, 102 insertions(+) create mode 100644 tslang/test/tester/tests/00interface_captures.ts diff --git a/tslang/docs/interface-vtable-simplification-design.md b/tslang/docs/interface-vtable-simplification-design.md index 3c680313b..1537c8249 100644 --- a/tslang/docs/interface-vtable-simplification-design.md +++ b/tslang/docs/interface-vtable-simplification-design.md @@ -333,3 +333,69 @@ Known risks: two-pass compilation (`Stages::Discovering`) — the side table must be populated consistently in whichever pass builds the vtable initializer; and `@dllimport` type reconstruction must keep *missing* from the side table (never a stale hit) so imported types deterministically take the fallback. + +### Test matrix follow-up (post-PR2, 2026-07-19) + +**Captures-bearing literal cast to an interface**: was genuinely uncovered: +neither `00funcs_capture.ts` (captures an outer var in an object-literal +method, but never cast to an interface) nor any of `00interface_object.ts` / +`00interface_global_method.ts` / `00interface_object5.ts` (cast to an +interface, but capture-free) combined both properties. Added +`00interface_captures.ts`. Works correctly, confirming §3's PR2 +implementation note that captures need no fallback: `--emit=mlir` shows the +vtable slot gets the constant `SymbolRefOp` (one lifted function shared by +every `make(x)` call) and exactly one `ts.New` (boxing the literal, which +carries the per-instance `.captured` field the shared function reads via +`this`) — no second heap allocation for a patched vtable. 720/720 with this +test added. + +**The imported-object-type fallback is not just untested - it's currently +broken, two different ways, independent of this arc's changes.** Attempting +to actually write the cross-module test surfaced two separate PRE-EXISTING +bugs (reproduced on `main@1db740c6`, i.e. before this arc's changes were ever +written into that call path in a way that matters here — both crash sites +predate PR2 and are unrelated to `objectLiteralMethodSymbolsMap`): + +1. `export var counterObj = { count: 0, inc() {...} };` (no explicit type + annotation, inferred boxed `ObjectType`) exports its declaration as `let + counterObj : object;` — the cross-module declaration-serialization + mechanism has no way to write out an inferred object-literal's structural + shape, degrading to the bare `object` type. Reimporting and casting that + to an interface in the importer hits `mlir::cast` + asserting false at `MLIRGenInterfaces.cpp:312` + (`mlirGenObjectVirtualTableDefinitionForInterface`), because + `objectType.getStorageType()` for the reconstructed bare-`object` type + isn't a `TupleType`/`ObjectStorageType` at all. Stack confirmed via + WinDbgX (`WinDbgX.exe -pv -p -c ".dump /ma " -c "qd"` attached + to the process while it was blocked on the assert's message box, then + `~*k` on the dump) - crash path: `MLIRGenCast.cpp:1621/1672` + (`castObjectToInterface`) → `MLIRGenInterfaces.cpp:193` + (`mlirGenCreateInterfaceVTableForObject`) → `:312`. +2. Giving the export an explicit structural type annotation + (`export var counterObj: { count: number; inc(): void } = {...}`) avoids + the `object`-degradation (the declaration now serializes as a real tuple + type, `let counterObj : [count:number, inc:() => void];`) but the literal + no longer gets boxed as `ObjectType` at all (plain tuple instead) — and + casting *that* cross-module tuple value to a method-bearing interface in + the importer hits `llvm_unreachable("review usage")` at + `CastLogicHelper.h:765`, a pre-existing dead/unimplemented branch in the + LOWERING-level cast dispatcher (`castLLVMTypes`'s "value to ref of value" + case). + +Neither is caused by or related to `objectLiteralMethodSymbolsMap`/§3's +side table — both crash before any vtable-slot content is decided, in the +object-type reconstruction and generic-cast layers respectively. This means +casting a cross-module method-bearing object VALUE to an interface inside +the importing module does not currently work at all, regardless of how it's +exported. **Test not added** (a test that's expected to crash doesn't +belong in the regression suite); the attempt and both crash sites are +recorded here instead. This is a distinct, deeper bug area (object-literal +type export/reimport across `@dllimport` boundaries, and the generic +cross-type-system cast lowering) than the vtable-slot work in §3-§5, and +is a candidate for its own separate investigation if it becomes worth +fixing - not part of this arc. `export_object_literal_with_interface.ts`'s +existing pattern (declare the export *already typed as the interface* at +its definition site, `export var counter: Counter = {...}`, so the cast +happens in the exporting module where a local `funcOp` genuinely exists) +remains the only currently-working way to share a method-bearing object +across modules, and is unaffected by any of the above. diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index b21619ae3..86923ab4c 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -286,6 +286,7 @@ add_test(NAME test-compile-00-interface-partial COMMAND test-runner "${PROJECT_S add_test(NAME test-compile-00-interface-optional COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_optional.ts") add_test(NAME test-compile-00-interface-optional-cast-order COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_optional_cast_order.ts") add_test(NAME test-compile-00-interface-function-typed-field COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_function_typed_field.ts") +add_test(NAME test-compile-00-interface-captures COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_captures.ts") add_test(NAME test-compile-00-interface-generic COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_generic.ts") add_test(NAME test-compile-00-interface-new COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_new.ts") add_test(NAME test-compile-00-interface-indexer COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_indexer.ts") @@ -638,6 +639,7 @@ add_test(NAME test-jit-00-interface-partial COMMAND test-runner -jit "${PROJECT_ add_test(NAME test-jit-00-interface-optional COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_optional.ts") add_test(NAME test-jit-00-interface-optional-cast-order COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_optional_cast_order.ts") add_test(NAME test-jit-00-interface-function-typed-field COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_function_typed_field.ts") +add_test(NAME test-jit-00-interface-captures COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_captures.ts") add_test(NAME test-jit-00-interface-generic COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_generic.ts") add_test(NAME test-jit-00-interface-new COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_new.ts") add_test(NAME test-jit-00-interface-indexer COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_indexer.ts") diff --git a/tslang/test/tester/tests/00interface_captures.ts b/tslang/test/tester/tests/00interface_captures.ts new file mode 100644 index 000000000..7109cfa31 --- /dev/null +++ b/tslang/test/tester/tests/00interface_captures.ts @@ -0,0 +1,34 @@ +// regression test: an object-literal method that captures an outer variable, +// cast to an interface and called through the interface reference. Confirms +// the constant-vtable optimization (see +// docs/interface-vtable-simplification-design.md §3) works correctly for +// captures-bearing methods, not just capture-free ones: the vtable slot gets +// a compile-time-constant SymbolRefOp (the lifted method is per-literal- +// expression, shared by every instance), while each instance's captured data +// lives in a separate per-object `.captured` field the method reads via +// `this` - so two independently-created instances must not share captured +// state. + +interface Getter { + get(): number; +} + +function make(x: number): Getter { + return { + get() { + return x; + } + }; +} + +function main() { + const g1: Getter = make(42); + const g2: Getter = make(100); + + assert(g1.get() == 42); + assert(g2.get() == 100); + // the two instances must not share captured state + assert(g1.get() == 42); + + print("done."); +}