From 9df02ea848075f3eea1504bfe76d1b00d1ba070a Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Mon, 20 Jul 2026 23:13:12 +0100 Subject: [PATCH] Fix casting object literal that omits an extends-inherited optional field An object literal omitting an optional field inherited via `extends` (e.g. `interface Derived extends Base` where `Base` declares `opt?: number`) failed to compile when cast to the derived interface, even though the identical scenario works when the interface declares the optional field directly. Root cause: getInterfaceCloneFields (the fallback clone path used when a tuple-to-interface cast needs field-type coercion) tried to backfill every interface field into the clone's tuple, including ones the source object genuinely lacks. The codebase's actual convention for a genuinely-absent optional field is a shorter storage tuple with the slot omitted entirely - InterfaceSymbolRefOp's `optional` attribute handles the absence via a runtime slot-count check, not a placeholder value in a same-width tuple. Fix: skip appending an absent conditional field in getInterfaceCloneFields's append loop instead of backfilling it, matching the existing convention. Also fixes getTupleTypeFields to propagate field.isConditional instead of hardcoding false (methods already did this correctly). Co-Authored-By: Claude Sonnet 5 --- .../TypeScript/MLIRLogic/MLIRGenStore.h | 2 +- tslang/lib/TypeScript/MLIRGenCast.cpp | 16 ++++++++ tslang/test/tester/CMakeLists.txt | 2 + .../tests/00interface_optional_extends.ts | 38 +++++++++++++++++++ 4 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 tslang/test/tester/tests/00interface_optional_extends.ts diff --git a/tslang/include/TypeScript/MLIRLogic/MLIRGenStore.h b/tslang/include/TypeScript/MLIRLogic/MLIRGenStore.h index 5e50064e6..b250c8257 100644 --- a/tslang/include/TypeScript/MLIRLogic/MLIRGenStore.h +++ b/tslang/include/TypeScript/MLIRLogic/MLIRGenStore.h @@ -348,7 +348,7 @@ struct InterfaceInfo for (auto &field : fields) { - tupleFields.push_back({field.id, field.type, false, mlir_ts::AccessLevel::Public}); + tupleFields.push_back({field.id, field.type, field.isConditional, mlir_ts::AccessLevel::Public}); } return mlir::success(); diff --git a/tslang/lib/TypeScript/MLIRGenCast.cpp b/tslang/lib/TypeScript/MLIRGenCast.cpp index d78553977..43cb19960 100644 --- a/tslang/lib/TypeScript/MLIRGenCast.cpp +++ b/tslang/lib/TypeScript/MLIRGenCast.cpp @@ -40,6 +40,22 @@ namespace mlirgen if (std::find_if(fields.begin(), fields.end(), [&](auto &item) { return item.id == interfaceField.id; }) == fields.end()) { + // the source has no field of this name at all - only reachable for a + // conditional (`?`) interface member (getVirtualTable's resolveField + // already rejects a missing non-conditional one, failing + // canCastTupleToInterface before this clone path even runs). Match the + // convention InterfaceSymbolRefOp's `optional` slot-count check already + // relies on for the non-clone path (see 00interface_optional_cast_order.ts): + // the field is simply ABSENT from the storage tuple, not present with a + // placeholder value - so skip it here rather than appending it. Appending it + // with any value/type would desync the clone's field COUNT from what + // interfaceVTableNameForObject's slot-index math and the interface's runtime + // "is this slot within the actual object's tuple size" check expect. + if (interfaceField.isConditional) + { + continue; + } + fields.push_back(interfaceField); } } diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 8fa42a214..fd7352fb3 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -292,6 +292,7 @@ add_test(NAME test-compile-00-object-annotated-method-params COMMAND test-runner add_test(NAME test-compile-00-object-annotated-method-interleaved COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00object_annotated_method_interleaved.ts") add_test(NAME test-compile-00-object-annotated-method-extends-interface COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00object_annotated_method_extends_interface.ts") add_test(NAME test-compile-00-object-annotated-method-extends-interface-multilevel COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00object_annotated_method_extends_interface_multilevel.ts") +add_test(NAME test-compile-00-interface-optional-extends COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_optional_extends.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") @@ -651,6 +652,7 @@ add_test(NAME test-jit-00-object-annotated-method-params COMMAND test-runner -ji add_test(NAME test-jit-00-object-annotated-method-interleaved COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00object_annotated_method_interleaved.ts") add_test(NAME test-jit-00-object-annotated-method-extends-interface COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00object_annotated_method_extends_interface.ts") add_test(NAME test-jit-00-object-annotated-method-extends-interface-multilevel COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00object_annotated_method_extends_interface_multilevel.ts") +add_test(NAME test-jit-00-interface-optional-extends COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_optional_extends.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") diff --git a/tslang/test/tester/tests/00interface_optional_extends.ts b/tslang/test/tester/tests/00interface_optional_extends.ts new file mode 100644 index 000000000..fad05dd7d --- /dev/null +++ b/tslang/test/tester/tests/00interface_optional_extends.ts @@ -0,0 +1,38 @@ +// Combines two previously-separate axes of interface coverage that had never +// been tested together: an optional member (00interface_optional_cast_order.ts +// - optional member's virtualIndex can get clobbered to -1 on the SHARED +// InterfaceInfo by whichever cast ran most recently) and `extends` +// (00object_annotated_method_extends_interface*.ts - inherited members need +// vtableOffset added to their own-interface-relative virtualIndex). The +// optional member here is declared on the BASE interface, so resolving it +// through the DERIVED interface's combined vtable exercises both the +// extends-offset math and the optional-member vtable-patch path at once. + +function main() { + interface Base { + base: number; + opt?: number; + } + + interface Derived extends Base { + derived: number; + } + + let present: Derived = { base: 1, opt: 5, derived: 10 }; + let missing: Derived = { base: 2, derived: 20 }; + + assert(present.base == 1); + assert(present.opt == 5); + assert(present.derived == 10); + + assert(missing.base == 2); + assert(missing.opt == undefined); + assert(missing.derived == 20); + + // re-read the providing object after the non-providing one was cast, to + // catch the shared-virtualIndex-clobber bug class if it resurfaces here + print(present.opt); + assert(present.opt == 5); + + print("done."); +}