From f8d33645e24ee1b115a50def0f0a0e7e8f2072e5 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Mon, 20 Jul 2026 23:32:26 +0100 Subject: [PATCH 1/4] Add Claude Code badge to README Credits Claude Code as a development tool, matching the existing "Powered by LLVM" badge style. Co-Authored-By: Claude Sonnet 5 --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 727cc1458..c1f3ae14a 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ Powered by [![LLVM|MLIR](https://llvm.org/img/LLVM-Logo-Derivative-1.png)](https://llvm.org/) +Developed by [![Claude Code](https://img.shields.io/badge/Claude-Code-D97757?logo=claude&logoColor=white)](https://claude.com/claude-code) + [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/donate/?hosted_button_id=BBJ4SQYLA6D2L) A native ahead-of-time (AOT) and JIT compiler for **TypeScript**, built on **LLVM/MLIR**. From b3ee9247a56ce7091a9b228cd9d623b96a05bb3b Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Mon, 20 Jul 2026 23:35:39 +0100 Subject: [PATCH 2/4] Extend interface extends+optional coverage to cross-module cast Adds a cross-module test pair for `interface Derived extends Base` where Base declares an optional field, cast from imported exported objects both with and without the optional field provided. Confirms the fix from #271 (same-module only) also covers the cross-module path, which has historically been more fragile due to vtable cloning and GC-heap boxing - no additional bug found; full interface test suite (69 tests) passes 100%. Co-Authored-By: Claude Sonnet 5 --- tslang/test/tester/CMakeLists.txt | 2 ++ ...ctural_typed_extends_interface_optional.ts | 28 +++++++++++++++++++ ...ctural_typed_extends_interface_optional.ts | 17 +++++++++++ 3 files changed, 47 insertions(+) create mode 100644 tslang/test/tester/tests/export_object_literal_structural_typed_extends_interface_optional.ts create mode 100644 tslang/test/tester/tests/import_object_literal_structural_typed_extends_interface_optional.ts diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index fd7352fb3..9a27998e2 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -871,6 +871,7 @@ add_test(NAME test-compile-shared-export-import-object-literal-structural-typed- add_test(NAME test-compile-shared-export-import-object-literal-structural-typed-extends-interface-multilevel COMMAND test-runner -shared -gctors-as-method "${PROJECT_SOURCE_DIR}/test/tester/tests/import_object_literal_structural_typed_extends_interface_multilevel.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_object_literal_structural_typed_extends_interface_multilevel.ts") add_test(NAME test-compile-shared-export-import-object-literal-structural-typed-extends-interface-diamond COMMAND test-runner -shared -gctors-as-method "${PROJECT_SOURCE_DIR}/test/tester/tests/import_object_literal_structural_typed_extends_interface_diamond.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_object_literal_structural_typed_extends_interface_diamond.ts") add_test(NAME test-compile-shared-export-import-object-literal-structural-typed-extends-interface-triple COMMAND test-runner -shared -gctors-as-method "${PROJECT_SOURCE_DIR}/test/tester/tests/import_object_literal_structural_typed_extends_interface_triple.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_object_literal_structural_typed_extends_interface_triple.ts") +add_test(NAME test-compile-shared-export-import-object-literal-structural-typed-extends-interface-optional COMMAND test-runner -shared -gctors-as-method "${PROJECT_SOURCE_DIR}/test/tester/tests/import_object_literal_structural_typed_extends_interface_optional.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_object_literal_structural_typed_extends_interface_optional.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") @@ -896,6 +897,7 @@ add_test(NAME test-jit-shared-export-import-object-literal-structural-typed-exte add_test(NAME test-jit-shared-export-import-object-literal-structural-typed-extends-interface-multilevel COMMAND test-runner -jit -shared -gctors-as-method "${PROJECT_SOURCE_DIR}/test/tester/tests/import_object_literal_structural_typed_extends_interface_multilevel.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_object_literal_structural_typed_extends_interface_multilevel.ts") add_test(NAME test-jit-shared-export-import-object-literal-structural-typed-extends-interface-diamond COMMAND test-runner -jit -shared -gctors-as-method "${PROJECT_SOURCE_DIR}/test/tester/tests/import_object_literal_structural_typed_extends_interface_diamond.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_object_literal_structural_typed_extends_interface_diamond.ts") add_test(NAME test-jit-shared-export-import-object-literal-structural-typed-extends-interface-triple COMMAND test-runner -jit -shared -gctors-as-method "${PROJECT_SOURCE_DIR}/test/tester/tests/import_object_literal_structural_typed_extends_interface_triple.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_object_literal_structural_typed_extends_interface_triple.ts") +add_test(NAME test-jit-shared-export-import-object-literal-structural-typed-extends-interface-optional COMMAND test-runner -jit -shared -gctors-as-method "${PROJECT_SOURCE_DIR}/test/tester/tests/import_object_literal_structural_typed_extends_interface_optional.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_object_literal_structural_typed_extends_interface_optional.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/export_object_literal_structural_typed_extends_interface_optional.ts b/tslang/test/tester/tests/export_object_literal_structural_typed_extends_interface_optional.ts new file mode 100644 index 000000000..55a3808f7 --- /dev/null +++ b/tslang/test/tester/tests/export_object_literal_structural_typed_extends_interface_optional.ts @@ -0,0 +1,28 @@ +namespace M4 { + export interface Base { + base: number; + opt?: number; + } + + export interface Derived extends Base { + derived: number; + } + + export var rawPresent: { + base: number; + opt: number; + derived: number; + } = { + base: 1.0, + opt: 5.0, + derived: 10.0, + }; + + export var rawMissing: { + base: number; + derived: number; + } = { + base: 2.0, + derived: 20.0, + }; +} diff --git a/tslang/test/tester/tests/import_object_literal_structural_typed_extends_interface_optional.ts b/tslang/test/tester/tests/import_object_literal_structural_typed_extends_interface_optional.ts new file mode 100644 index 000000000..8bae842bc --- /dev/null +++ b/tslang/test/tester/tests/import_object_literal_structural_typed_extends_interface_optional.ts @@ -0,0 +1,17 @@ +import './export_object_literal_structural_typed_extends_interface_optional' + +let present: M4.Derived = M4.rawPresent; +let missing: M4.Derived = M4.rawMissing; + +assert(present.base == 1); +assert(present.opt == 5); +assert(present.derived == 10); + +assert(missing.base == 2); +assert(missing.opt == undefined); +assert(missing.derived == 20); + +print(present.opt); +assert(present.opt == 5); + +print("done."); From e6a989c93f1398738ea7dfbafb511b184eebd735 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Tue, 21 Jul 2026 00:06:20 +0100 Subject: [PATCH 3/4] Fix compiler crash casting object literal that omits an extends-inherited optional method An object literal omitting an optional METHOD inherited via `extends` (e.g. `interface Base { opt?(n: number): number }`, `interface Derived extends Base`) crashed the compiler outright with llvm_unreachable("not implemented yet") in mlirGenObjectVirtualTableDefinitionForInterface. The equivalent missing FIELD case already had a placeholder pattern (a -1 sentinel cast to the field's ref type, inserted into the vtable slot), but the missing-method case had never been implemented at all - this was simply unsupported, not merely broken for extends specifically. Fix: mirror the missing-field's -1-sentinel placeholder for a missing method's vtable slot, cast to the method's function-pointer-ref type instead of a field type. New test 00interface_optional_method_extends.ts locks this in (compile+jit, registered in CMakeLists.txt). Note: comparing an optional interface method against `undefined` (`x.optMethod == undefined`) is a separate, deeper, pre-existing bug (hardcoded to a compile-time-constant result in UndefLogicHelper.h, unrelated to extends) - deliberately left unfixed and documented in the test's comments after an attempted fix caused an unresolved control-flow crash; deferred for a future session. Co-Authored-By: Claude Sonnet 5 --- tslang/lib/TypeScript/MLIRGenInterfaces.cpp | 21 ++++++ tslang/test/tester/CMakeLists.txt | 2 + .../00interface_optional_method_extends.ts | 65 +++++++++++++++++++ 3 files changed, 88 insertions(+) create mode 100644 tslang/test/tester/tests/00interface_optional_method_extends.ts diff --git a/tslang/lib/TypeScript/MLIRGenInterfaces.cpp b/tslang/lib/TypeScript/MLIRGenInterfaces.cpp index d7cf37bc7..065bc6479 100644 --- a/tslang/lib/TypeScript/MLIRGenInterfaces.cpp +++ b/tslang/lib/TypeScript/MLIRGenInterfaces.cpp @@ -449,8 +449,29 @@ namespace mlirgen MLIRHelper::getStructIndex(builder, fieldIndex)); } } + else if (methodOrField.isMissing) + { + // an optional (`?`) interface METHOD the object literal doesn't + // provide - same "null value, as missing field/method" placeholder + // as the isField branch above (getStructIndex + -1 sentinel), just + // cast to the method's function-pointer-ref type instead of a field + // type. Reachable via extends (see 00interface_optional_method_extends.ts): + // a required method is always compile-time-resolvable through + // lookupObjectLiteralMethodSymbol/findMethod above and never reaches + // isMissing here; only a genuinely-absent optional method does. + auto negative1 = builder.create(location, builder.getI64Type(), + mth.getI64AttrValue(-1)); + auto castedPtr = cast(location, mlir_ts::RefType::get(methodOrField.methodInfo.funcType), + negative1, genContext); + vtableValue = builder.create( + location, virtTuple, castedPtr, vtableValue, + MLIRHelper::getStructIndex(builder, fieldIndex)); + } else { + // a real, present, non-object-literal method (e.g. a class + // implementing the interface) reaching the METHOD (not + // methodsAsFields) branch - not yet exercised by any test. llvm_unreachable("not implemented yet"); /* auto methodConstName = builder.create( diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 9a27998e2..cf5ca67f8 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -293,6 +293,7 @@ add_test(NAME test-compile-00-object-annotated-method-interleaved COMMAND test-r 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-optional-method-extends COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_optional_method_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") @@ -653,6 +654,7 @@ add_test(NAME test-jit-00-object-annotated-method-interleaved COMMAND test-runne 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-optional-method-extends COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_optional_method_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_method_extends.ts b/tslang/test/tester/tests/00interface_optional_method_extends.ts new file mode 100644 index 000000000..82d29142f --- /dev/null +++ b/tslang/test/tester/tests/00interface_optional_method_extends.ts @@ -0,0 +1,65 @@ +// Sibling of 00interface_optional_extends.ts (optional FIELD inherited via +// extends), but for an optional METHOD instead. Methods go through a +// different vtable-patch path than data fields +// (mlirGenCreateInterfaceVTableForObject clones/patches function pointers, +// not byte offsets), so a fix proven for optional fields isn't automatically +// proven for optional methods. +// +// Found and fixed: casting an object literal that OMITS an optional method +// inherited via extends used to crash the compiler outright +// (llvm_unreachable("not implemented yet") in +// mlirGenObjectVirtualTableDefinitionForInterface - building the placeholder +// vtable for a missing METHOD had never been implemented at all, only for a +// missing FIELD). Fixed by mirroring the missing-field's -1-sentinel +// placeholder pattern for a missing method's vtable slot. +// +// KNOWN LIMITATION (not fixed here, deferred): comparing an optional +// interface METHOD against `undefined` (`someObj.optMethod == undefined`) +// does not work correctly for ANY interface, extends or not - it's hardcoded +// to a compile-time-constant result in UndefLogicHelper.h's +// processUndefVale, which only special-cases InterfaceType/ClassType against +// undefined, not BoundFunctionType. A fix was attempted (zeroing the +// bound_func's `this` pointer as a missing-sentinel, checking it in the +// undefined-comparison lowering) but caused a control-flow crash deep in +// LLVM dialect conversion (execution ran off the end of a JIT-compiled +// block) that wasn't resolved before this test landed. Do not use +// `== undefined` / `!= undefined` on an optional interface method in a test +// until that's fixed - only test presence via calling it or (like here) +// simply never calling an omitted one. + +function main() { + interface Base { + base: number; + opt?(n: number): number; + } + + interface Derived extends Base { + derived: number; + } + + let present: Derived = { + base: 1.0, + opt(n: number) { return this.base + n; }, + derived: 10.0, + }; + let missing: Derived = { base: 2.0, derived: 20.0 }; + + assert(present.base == 1); + assert(present.opt(4) == 5); + assert(present.derived == 10); + + // does not call missing.opt (per the known limitation above, there is no + // reliable way yet to check it's absent before calling it) - just + // exercises that constructing/casting this object at all doesn't crash + // building its vtable, which is the bug that was actually fixed here. + assert(missing.base == 2); + assert(missing.derived == 20); + + // re-read the providing object's method after the non-providing one was + // cast, to catch the shared-virtualIndex-clobber bug class if it + // resurfaces here + print(present.opt(9)); + assert(present.opt(9) == 10); + + print("done."); +} From ad30ad9de41fb822c32b2681ffcb9dcadec181c0 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Tue, 21 Jul 2026 00:32:31 +0100 Subject: [PATCH 4/4] Fix optional interface method vs undefined comparison always returning wrong constant Comparing an optional interface method against `undefined` (`obj.optMethod == undefined`) never worked for any interface, extends or not: UndefLogicHelper.h's processUndefVale only special-cased InterfaceType/ClassType against undefined, so a BoundFunctionType fell through to a hardcoded compile-time-constant false/true regardless of whether the method was actually present. Fix required two coordinated changes: - InterfaceSymbolRefOpLowering (LowerToLLVM.cpp) now branchlessly selects a null `this` pointer (LLVM::SelectOp) when an optional method's vtable slot holds the "missing member" -1 sentinel - a real bound method's `this` is never null, giving the comparison something concrete to check. - UndefLogicHelper.h's new BoundFunctionType branch checks that null-or-not via a directly-emitted LLVM::ICmpOp, not the shared LogicOp helper: v1 is a template parameter baked in from the outer comparison operator that triggered the whole call (e.g. arith::CmpIPredicate::ne for a source-level `!=`), so passing a different SyntaxKind at the call site was silently ignored and inverted the result. An earlier attempt using CodeLogicHelper's conditionalExpressionLowering (block-splitting control flow) caused an unresolved crash; WinDbg dump analysis showed execution running off the end of a JIT-compiled block. Replacing it with the branchless select (already used successfully elsewhere in this file) resolved it cleanly. Restores the `== undefined`/`!= undefined` assertions in 00interface_optional_method_extends.ts now that they pass correctly. Full test suite: 758/758 passing. Co-Authored-By: Claude Sonnet 5 --- .../TypeScript/LowerToLLVM/UndefLogicHelper.h | 53 +++++++++++++++++++ tslang/lib/TypeScript/LowerToLLVM.cpp | 30 ++++++++++- .../00interface_optional_method_extends.ts | 40 +++++++------- 3 files changed, 103 insertions(+), 20 deletions(-) diff --git a/tslang/include/TypeScript/LowerToLLVM/UndefLogicHelper.h b/tslang/include/TypeScript/LowerToLLVM/UndefLogicHelper.h index 5b9e7f7c0..735caee80 100644 --- a/tslang/include/TypeScript/LowerToLLVM/UndefLogicHelper.h +++ b/tslang/include/TypeScript/LowerToLLVM/UndefLogicHelper.h @@ -118,6 +118,59 @@ class UndefLogicHelper return LogicOp(binOp, opCmpCode, val2, val2.getType(), casted, casted.getType(), rewriter, typeConverter, compileOptions); } + else if (isa(t2)) + { + // an optional (`?`) interface/class METHOD compared against + // `undefined` (e.g. `missing.opt == undefined` - + // 00interface_optional_method_extends.ts). Unlike an optional + // FIELD (OptionalType + HasValueOp runtime check), a bound_func has + // no such wrapper, so this used to fall to whenOneValueIsUndef below + // and return a compile-time-constant false/true regardless of the + // actual value - meaning the comparison never reflected whether the + // method was really present. InterfaceSymbolRefOpLowering now selects + // a null `this` pointer when the vtable slot holds the "missing + // optional member" -1 sentinel (a real bound method's `this` is + // never null), so check that instead. + // + // NOTE: deliberately NOT using LogicOp here to + // compute "is this null" - v1 is a template parameter baked in from + // the ORIGINAL comparison operator that triggered this whole call + // (e.g. arith::CmpIPredicate::ne for a source-level `!=`), not + // something the `op`/SyntaxKind argument can override at the call + // site (LogicOp's isIntOrIndex branch ignores `op` entirely and uses + // `v1` directly) - passing SyntaxKind::EqualsEqualsToken here while v1 + // is still `ne` silently computed "this != null" instead of "this == + // null", inverting the result. Emit the LLVM::ICmpOp directly instead, + // pointer-converting both sides the same way LogicOp's + // isNullableTypeNoUnion branch would. + auto thisVal = rewriter.create(loc, mlir_ts::OpaqueType::get(rewriter.getContext()), val2); + auto nullVal = rewriter.create(loc, mlir_ts::NullType::get(rewriter.getContext())); + LLVMTypeConverterHelper llvmtch(&typeConverter); + auto intPtrType = llvmtch.getIntPtrType(0); + auto thisValAsLLVMType = rewriter.create(loc, typeConverter.convertType(thisVal.getType()), thisVal); + auto nullValAsLLVMType = rewriter.create(loc, typeConverter.convertType(nullVal.getType()), nullVal); + auto thisPtrValue = rewriter.create(loc, intPtrType, thisValAsLLVMType); + auto nullPtrValue = rewriter.create(loc, intPtrType, nullValAsLLVMType); + mlir::Value isNull = rewriter.create(loc, LLVM::ICmpPredicate::eq, thisPtrValue, nullPtrValue); + + switch (opCmpCode) + { + case SyntaxKind::EqualsEqualsToken: + case SyntaxKind::EqualsEqualsEqualsToken: + return isNull; + case SyntaxKind::ExclamationEqualsToken: + case SyntaxKind::ExclamationEqualsEqualsToken: + { + auto trueVal = clh.createI1ConstantOf(true); + return (mlir::Value)rewriter.create(loc, isNull, trueVal); + } + default: + // ordering comparisons against undefined aren't meaningful for a + // callable - same "result is false already" fallback as any + // other unhandled type below. + return whenOneValueIsUndef(rewriter, loc); + } + } else { // result is false already diff --git a/tslang/lib/TypeScript/LowerToLLVM.cpp b/tslang/lib/TypeScript/LowerToLLVM.cpp index 03aad3f23..f8d13a562 100644 --- a/tslang/lib/TypeScript/LowerToLLVM.cpp +++ b/tslang/lib/TypeScript/LowerToLLVM.cpp @@ -4880,8 +4880,36 @@ struct InterfaceSymbolRefOpLowering : public TsLlvmPattern(interfaceSymbolRefOp.getType())) { + mlir::Value thisValEffective = thisVal; + if (isOptional) + { + // an optional (`?`) interface method the object literal doesn't provide + // is patched with a -1 sentinel in the vtable slot (see + // mlirGenObjectVirtualTableDefinitionForInterface's missing-method + // branch), same convention as the FIELD case below. Select a null + // `this` when the slot is that sentinel, so the resulting bound_func + // has a null `this` pointer - a real bound method's `this` is never + // null - giving a later undefined-comparison something concrete to + // check instead of always reading as "present". A branchless + // LLVM::SelectOp is used here rather than CodeLogicHelper's + // conditionalExpressionLowering (block-splitting control flow) since + // this pattern - used successfully elsewhere in this file, e.g. + // ValueOrDefaultOpLowering - is simpler and carries no risk of a + // malformed/dangling basic block. + LLVMTypeConverterHelper llvmtch(static_cast(getTypeConverter())); + auto intPtrType = llvmtch.getIntPtrType(0); + auto negative1 = tsLlvmContext->compileOptions.sizeBits == 32 + ? clh.createI32ConstantOf(-1) + : clh.createI64ConstantOf(-1); + auto methodOrFieldIntPtrValue = rewriter.create(loc, intPtrType, methodOrFieldPtr); + auto isMissing = + rewriter.create(loc, LLVM::ICmpPredicate::eq, methodOrFieldIntPtrValue, negative1); + auto nullThisVal = rewriter.create(loc, th.getPtrType()); + thisValEffective = rewriter.create(loc, isMissing, nullThisVal, thisVal); + } + auto thisOpaque = - rewriter.create(loc, mlir_ts::OpaqueType::get(rewriter.getContext()), thisVal); + rewriter.create(loc, mlir_ts::OpaqueType::get(rewriter.getContext()), thisValEffective); auto methodTypedPtr = rewriter.create( loc, mlir_ts::FunctionType::get(rewriter.getContext(), boundFunc.getInputs(), boundFunc.getResults()), methodOrFieldPtr); diff --git a/tslang/test/tester/tests/00interface_optional_method_extends.ts b/tslang/test/tester/tests/00interface_optional_method_extends.ts index 82d29142f..507bf7be4 100644 --- a/tslang/test/tester/tests/00interface_optional_method_extends.ts +++ b/tslang/test/tester/tests/00interface_optional_method_extends.ts @@ -5,27 +5,30 @@ // not byte offsets), so a fix proven for optional fields isn't automatically // proven for optional methods. // -// Found and fixed: casting an object literal that OMITS an optional method -// inherited via extends used to crash the compiler outright +// Two bugs found and fixed here: +// +// 1. Casting an object literal that OMITS an optional method inherited via +// extends used to crash the compiler outright // (llvm_unreachable("not implemented yet") in // mlirGenObjectVirtualTableDefinitionForInterface - building the placeholder // vtable for a missing METHOD had never been implemented at all, only for a // missing FIELD). Fixed by mirroring the missing-field's -1-sentinel // placeholder pattern for a missing method's vtable slot. // -// KNOWN LIMITATION (not fixed here, deferred): comparing an optional -// interface METHOD against `undefined` (`someObj.optMethod == undefined`) -// does not work correctly for ANY interface, extends or not - it's hardcoded -// to a compile-time-constant result in UndefLogicHelper.h's -// processUndefVale, which only special-cases InterfaceType/ClassType against -// undefined, not BoundFunctionType. A fix was attempted (zeroing the -// bound_func's `this` pointer as a missing-sentinel, checking it in the -// undefined-comparison lowering) but caused a control-flow crash deep in -// LLVM dialect conversion (execution ran off the end of a JIT-compiled -// block) that wasn't resolved before this test landed. Do not use -// `== undefined` / `!= undefined` on an optional interface method in a test -// until that's fixed - only test presence via calling it or (like here) -// simply never calling an omitted one. +// 2. Comparing an optional interface METHOD against `undefined` +// (`someObj.optMethod == undefined`) never worked for ANY interface, extends +// or not - hardcoded to a compile-time-constant result in +// UndefLogicHelper.h's processUndefVale, which only special-cased +// InterfaceType/ClassType against undefined, not BoundFunctionType. Fixed by: +// (a) InterfaceSymbolRefOpLowering now selects a null `this` pointer +// (branchless LLVM::SelectOp) when an optional method's vtable slot holds the +// "missing" -1 sentinel - a real bound method's `this` is never null; (b) +// UndefLogicHelper.h's new BoundFunctionType branch checks that null-or-not +// via a directly-emitted LLVM::ICmpOp - NOT the shared LogicOp<...> helper, +// whose comparison predicate is a template parameter baked in from the +// OUTER comparison operator that triggered the whole call (passing a +// different SyntaxKind at the call site is silently ignored and inverts the +// result). function main() { interface Base { @@ -45,14 +48,12 @@ function main() { let missing: Derived = { base: 2.0, derived: 20.0 }; assert(present.base == 1); + assert(present.opt != undefined); assert(present.opt(4) == 5); assert(present.derived == 10); - // does not call missing.opt (per the known limitation above, there is no - // reliable way yet to check it's absent before calling it) - just - // exercises that constructing/casting this object at all doesn't crash - // building its vtable, which is the bug that was actually fixed here. assert(missing.base == 2); + assert(missing.opt == undefined); assert(missing.derived == 20); // re-read the providing object's method after the non-providing one was @@ -60,6 +61,7 @@ function main() { // resurfaces here print(present.opt(9)); assert(present.opt(9) == 10); + assert(present.opt != undefined); print("done."); }