From cbf3afc9736d776b2aeeec3496c521c353d3b8a7 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sun, 19 Jul 2026 23:15:00 +0100 Subject: [PATCH 1/2] Extend test coverage for object-literal/interface casts and method dispatch Adds four new test cases exercising variations not covered by the recent interface-vtable-cast arc (#256-258), which only ever tested zero-argument, single-method interfaces: - 00object_annotated_method_params.ts: type-literal-annotated object methods with PARAMETERS (that arc's fix only had zero-arg method coverage), including a same-module chained sibling-method call. - 00interface_object_array.ts: several distinct object literals (each its own per-type constant vtable) collected into a single interface-typed array and dispatched through the same call site in a loop - would catch a shared/aliased vtable or wrong-`this` bug that a single-object test can't. - export/import_object_literal_structural_typed_params.ts: extends export/import_object_literal_structural_typed.ts's cross-module coverage along the "zero-arg -> parameterized method" axis. Along the way, found and documented (not fixed) two new, real gaps in docs/interface-vtable-simplification-design.md: - a cross-module cast to a MULTI-method interface breaks for every vtable slot past 0 (wrong value at slot 1, crash at slot 2+) - every prior test in this arc only used single-method interfaces, so this was never exercised before. The committed cross-module test deliberately stays single-method to avoid committing a failing case. - (same-module only, unrelated) a value-returning method can't `return this.siblingMethod(...)` within the same type-literal-annotated object literal - calling the sibling as a bare statement works fine. Avoided in the committed tests. Full ctest suite: 730/730 passed (722 + 8 new). Co-Authored-By: Claude Fable 5 --- .../interface-vtable-simplification-design.md | 57 +++++++++++++++++++ tslang/test/tester/CMakeLists.txt | 6 ++ .../tester/tests/00interface_object_array.ts | 46 +++++++++++++++ .../tests/00object_annotated_method_params.ts | 37 ++++++++++++ ..._object_literal_structural_typed_params.ts | 23 ++++++++ ..._object_literal_structural_typed_params.ts | 17 ++++++ 6 files changed, 186 insertions(+) create mode 100644 tslang/test/tester/tests/00interface_object_array.ts create mode 100644 tslang/test/tester/tests/00object_annotated_method_params.ts create mode 100644 tslang/test/tester/tests/export_object_literal_structural_typed_params.ts create mode 100644 tslang/test/tester/tests/import_object_literal_structural_typed_params.ts diff --git a/tslang/docs/interface-vtable-simplification-design.md b/tslang/docs/interface-vtable-simplification-design.md index a8215cd03..ea4fb3c23 100644 --- a/tslang/docs/interface-vtable-simplification-design.md +++ b/tslang/docs/interface-vtable-simplification-design.md @@ -473,3 +473,60 @@ 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). + +### Newly found: multi-method cross-module vtable slot bug (2026-07-19) + +Found while extending test coverage beyond this arc's fixes - every prior +test/fix in #256-#258 only ever exercised a **single-method** interface +cast cross-module (`Counter {count; inc()}`). Trying a genuinely +multi-method interface (`Accumulator {total; add(n); addTwice(n); +scaled(factor): number}`, canonical vtable order after +`assignCanonicalVirtualIndexes` = methods-first-in-declaration-order then +fields = `add`@0, `addTwice`@1, `scaled`@2, `total`@3) surfaced a clean, +reproducible pattern when casting a cross-module structurally-typed VALUE +to it and calling each method **in isolation** (bisected one at a time via +a temporary `test-runner.cpp` stdout-surfacing patch, same technique as +earlier bugs in this file - reverted before commit): + +| method (canonical slot) | isolated result | +|---|---| +| `add(n)` (slot 0) | correct - mutates `total` as expected | +| `addTwice(n)` (slot 1) | WRONG VALUE, no crash - `total` ends up incorrect but the process completes and reports the mismatch cleanly | +| `scaled(factor)` (slot 2) | CRASH - silent, no assert/error text reaches output at all (raw access violation with buffered stdout lost, unlike the controlled assert failures elsewhere in this file) | + +Slot 0 works, slot 1 is wrong-but-survives, slot 2 crashes outright - +consistent with SOMETHING going wrong specifically in how slots beyond 0 +are constructed or addressed for a cross-module structurally-typed cast +(as opposed to the field-order bug from earlier in this file, which was a +uniform reversal affecting all slots equally and is already fixed). Not +yet root-caused - candidates worth checking first: whether +`getInterfaceCloneFields`/the vtable-patch loop in +`mlirGenCreateInterfaceVTableForObject` (MLIRGenInterfaces.cpp) iterates +methods needing patching in the right order relative to the CANONICAL +`virtualIndex` for interfaces with >1 method (an off-by-one or +wrong-iteration-source bug would explain "slot 0 fine, slot 1+ broken"); +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. + +Also worth noting for whoever investigates: a completely SEPARATE, +same-module-only finding surfaced while building the initial (broken) +version of this test - a value-returning method cannot `return +this.siblingMethod(...)` (using a sibling call's return value directly in +a `return` statement) within the same type-literal-annotated object +literal; calling the sibling as a bare statement (discarding its return +value) works fine. Confirmed same-module, unrelated to cross-module +casting at all - likely a self-referential type-inference ordering gap +(the caller's return type depends on resolving the callee's return type, +which depends on `this`, which is still being constructed). Not +investigated further; avoided in the committed tests +(`00object_annotated_method_params.ts` uses `setBase`/re-`scale`, not a +chained-return pattern). diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 216da22ba..4eedcd7e7 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -288,6 +288,8 @@ add_test(NAME test-compile-00-interface-optional-cast-order COMMAND test-runner 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-object-annotated-method COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00object_annotated_method.ts") +add_test(NAME test-compile-00-object-annotated-method-params COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00object_annotated_method_params.ts") +add_test(NAME test-compile-00-interface-object-array COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_object_array.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") @@ -642,6 +644,8 @@ add_test(NAME test-jit-00-interface-optional-cast-order COMMAND test-runner -jit 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-object-annotated-method COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00object_annotated_method.ts") +add_test(NAME test-jit-00-object-annotated-method-params COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00object_annotated_method_params.ts") +add_test(NAME test-jit-00-interface-object-array COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_object_array.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") @@ -850,6 +854,7 @@ add_test(NAME test-compile-shared-export-import-class-interface COMMAND test-run 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-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-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") @@ -866,6 +871,7 @@ add_test(NAME test-jit-shared-export-import-class-interface COMMAND test-runner 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-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-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/00interface_object_array.ts b/tslang/test/tester/tests/00interface_object_array.ts new file mode 100644 index 000000000..f8d7422e6 --- /dev/null +++ b/tslang/test/tester/tests/00interface_object_array.ts @@ -0,0 +1,46 @@ +// Array-of-interface coverage: several DISTINCT object literals (each its own +// location-hashed storage type and its own lifted method, per +// docs/interface-vtable-simplification-design.md section 3) collected into a +// single Shape[]-typed array and dispatched through the SAME interface at a +// single call site in a loop. Exercises that each element's own vtable +// (constant, per-type) is independently correct - a bug here would typically +// show up as every element calling the FIRST element's method (a shared/ +// aliased vtable) or a wrong `this` binding once mixed in a homogeneous +// array. + +interface Shape { + area(): number; +} + +function main() { + const square = { + side: 4.0, + area() { return this.side * this.side; }, + }; + + const rectangle = { + width: 3.0, + height: 5.0, + area() { return this.width * this.height; }, + }; + + const circleLike = { + radius: 2.0, + area() { return this.radius * this.radius * 3.0; }, + }; + + let shapes: Shape[] = [square, rectangle, circleLike]; + + let total = 0.0; + for (let i = 0; i < shapes.length; i++) { + total = total + shapes[i].area(); + } + + print(total); + assert(square.area() == 16.0); + assert(rectangle.area() == 15.0); + assert(circleLike.area() == 12.0); + assert(total == 43.0); + + print("done."); +} diff --git a/tslang/test/tester/tests/00object_annotated_method_params.ts b/tslang/test/tester/tests/00object_annotated_method_params.ts new file mode 100644 index 000000000..82a1c2ee9 --- /dev/null +++ b/tslang/test/tester/tests/00object_annotated_method_params.ts @@ -0,0 +1,37 @@ +// Extends 00object_annotated_method.ts's coverage: that test only exercised +// zero-argument methods (inc(): void, twice(): number). Here the type-literal +// method members take parameters and one method calls ANOTHER method on +// `this` (chained dispatch through the same implicit-this-param mechanism +// fixed for MethodSignature tuple members). + +function main() { + let acc: { total: number; add(n: number): void; addTwice(n: number): void } = { + total: 0, + add(n: number) { this.total = this.total + n; }, + addTwice(n: number) { this.add(n); this.add(n); }, + }; + + acc.add(3); + assert(acc.total == 3); + + acc.addTwice(4); + assert(acc.total == 11); + print(acc.total); + + let calc: { base: number; scale(factor: number): number; setBase(value: number): void } = { + base: 5, + scale(factor: number) { return this.base * factor; }, + setBase(value: number) { this.base = value; }, + }; + + const scaled = calc.scale(3); + assert(scaled == 15); + print(scaled); + + calc.setBase(10); + const rescaled = calc.scale(3); + assert(rescaled == 30); + print(rescaled); + + print("done."); +} diff --git a/tslang/test/tester/tests/export_object_literal_structural_typed_params.ts b/tslang/test/tester/tests/export_object_literal_structural_typed_params.ts new file mode 100644 index 000000000..532f68d88 --- /dev/null +++ b/tslang/test/tester/tests/export_object_literal_structural_typed_params.ts @@ -0,0 +1,23 @@ +namespace A { + + export interface Accumulator { + total: number; + add(n: number): void; + } + + // structurally-typed (not interface-typed) export, like + // export_object_literal_structural_typed.ts, but extends that test's + // coverage: the method takes a PARAMETER (that test's inc() took none). + // + // NOTE: deliberately kept to ONE method. A multi-method version of this + // (total; add(n); addTwice(n); scaled(factor): number) was tried and + // found broken cross-module for any method beyond vtable slot 0 - see + // docs/interface-vtable-simplification-design.md's "multi-method + // cross-module vtable slot bug" section. That's a distinct, deeper, + // not-yet-fixed bug; this test intentionally stays within the + // currently-working single-method shape. + export var acc: { total: number; add(n: number): void } = { + total: 0.0, + add(n: number) { this.total = this.total + n; }, + }; +} diff --git a/tslang/test/tester/tests/import_object_literal_structural_typed_params.ts b/tslang/test/tester/tests/import_object_literal_structural_typed_params.ts new file mode 100644 index 000000000..82998026c --- /dev/null +++ b/tslang/test/tester/tests/import_object_literal_structural_typed_params.ts @@ -0,0 +1,17 @@ +import './export_object_literal_structural_typed_params' + +// Casts the imported structurally-typed VALUE to a single-method interface in +// the importer, exercising a PARAMETERIZED (not zero-arg) method - extends +// export/import_object_literal_structural_typed.ts's coverage (that test's +// inc() took no arguments). +var acc: A.Accumulator = A.acc; + +acc.add(3); +print(acc.total); +assert(acc.total == 3); + +acc.add(4); +print(acc.total); +assert(acc.total == 7); + +print("done."); From 1f40bab5490fcf20bcf308411fd524c76921380b Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sun, 19 Jul 2026 23:15:43 +0100 Subject: [PATCH 2/2] Update CMake configuration for Visual Studio 18 and adjust ROOT_PATH in typescript.cmake --- docs/how/cmake_vulkan/CMakeLists.txt | 2 +- docs/how/cmake_vulkan/config_debug.bat | 2 +- docs/how/cmake_vulkan/config_release.bat | 2 +- docs/how/cmake_vulkan/typescript.cmake | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/how/cmake_vulkan/CMakeLists.txt b/docs/how/cmake_vulkan/CMakeLists.txt index ced8b2285..a3daf2b6e 100644 --- a/docs/how/cmake_vulkan/CMakeLists.txt +++ b/docs/how/cmake_vulkan/CMakeLists.txt @@ -50,7 +50,7 @@ target_link_directories( ${GCLIBPATH} ) -set (LIBS "gcmt-lib") +set (LIBS "gc") target_link_libraries( ${PROJECT_NAME} diff --git a/docs/how/cmake_vulkan/config_debug.bat b/docs/how/cmake_vulkan/config_debug.bat index 8c960b0f9..1624885bc 100644 --- a/docs/how/cmake_vulkan/config_debug.bat +++ b/docs/how/cmake_vulkan/config_debug.bat @@ -1,6 +1,6 @@ pushd mkdir "__build/debug" cd "__build/debug" -cmake ../.. -G "Visual Studio 16 2019" -A x64 -DCMAKE_BUILD_TYPE=Debug -Wno-dev +cmake ../.. -G "Visual Studio 18 2026" -A x64 -DCMAKE_BUILD_TYPE=Debug -Wno-dev cmake --build . --config Debug -j 1 popd diff --git a/docs/how/cmake_vulkan/config_release.bat b/docs/how/cmake_vulkan/config_release.bat index a47512ced..fe8efab7a 100644 --- a/docs/how/cmake_vulkan/config_release.bat +++ b/docs/how/cmake_vulkan/config_release.bat @@ -1,6 +1,6 @@ pushd mkdir "__build/release" cd "__build/release" -cmake ../.. -G "Visual Studio 16 2019" -A x64 -DCMAKE_BUILD_TYPE=Release -Wno-dev +cmake ../.. -G "Visual Studio 18 2026" -A x64 -DCMAKE_BUILD_TYPE=Release -Wno-dev cmake --build . --config Release -j 1 popd diff --git a/docs/how/cmake_vulkan/typescript.cmake b/docs/how/cmake_vulkan/typescript.cmake index e37101694..ad35fa7ba 100644 --- a/docs/how/cmake_vulkan/typescript.cmake +++ b/docs/how/cmake_vulkan/typescript.cmake @@ -1,4 +1,4 @@ -set (ROOT_PATH "I:\\tslang\\57") +set (ROOT_PATH "I:\\tslang") set (_3RD_PARTY_PATH "${ROOT_PATH}") set (BUILD_PATH "${ROOT_PATH}") set (TSLANGPATH "${BUILD_PATH}")