From e7f44ae6da8113a3f69a7f49ba97b0bd9043551e Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sun, 11 Oct 2026 12:58:25 +0100 Subject: [PATCH 1/2] Comparing an Opaque with `Opaque | null` no longer overflows the stack `next != this.current`, with `current: Opaque | null`, crashed the compiler with 0xC00000FD and no message. LogicOp, for a nullable left operand and a union without a tag on the right, called itself again with the union's base type as the LEFT type and the union still as the right one - the same branch, forever. The base type now replaces the right type, as for a union on the left. Reported in docs/bugs/native-ts-graphics-findings.md (issue 1). Co-Authored-By: Claude Opus 5.5 --- .../LowerToLLVM/UnaryBinLogicalOrHelper.h | 5 +- tslang/test/tester/CMakeLists.txt | 2 + .../tester/tests/00opaque_nullable_compare.ts | 61 +++++++++++++++++++ 3 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 tslang/test/tester/tests/00opaque_nullable_compare.ts diff --git a/tslang/include/TypeScript/LowerToLLVM/UnaryBinLogicalOrHelper.h b/tslang/include/TypeScript/LowerToLLVM/UnaryBinLogicalOrHelper.h index 4d0b119b3..a5dc95d2a 100644 --- a/tslang/include/TypeScript/LowerToLLVM/UnaryBinLogicalOrHelper.h +++ b/tslang/include/TypeScript/LowerToLLVM/UnaryBinLogicalOrHelper.h @@ -229,7 +229,10 @@ mlir::Value LogicOp(Operation *binOp, SyntaxKind op, mlir::Value left, mlir::Typ return cmpOp; } - return LogicOp(binOp, op, left, baseType, right, rightType, builder, typeConverter, compileOptions); + // the union is the right operand, so it is the right type that becomes its base type: + // passing the base type as the left type and the union again as the right one came + // straight back here, until the compiler's stack ran out (`opaque != (opaque | null)`) + return LogicOp(binOp, op, left, leftType, right, baseType, builder, typeConverter, compileOptions); } // excluded string diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 7a9a4d06e..0df195d72 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -412,6 +412,7 @@ tslang_add_test(NAME test-compile-00-optional-number-compare COMMAND test-runner tslang_add_test(NAME test-compile-00-optional-loose-null COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00optional_loose_null.ts") tslang_add_test(NAME test-compile-00-union-nullable-array COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00union_nullable_array.ts") tslang_add_test(NAME test-compile-00-union-nullable-value COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00union_nullable_value.ts") +tslang_add_test(NAME test-compile-00-opaque-nullable-compare COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00opaque_nullable_compare.ts") tslang_add_test(NAME test-compile-00-union-value-equality COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00union_value_equality.ts") tslang_add_test(NAME test-compile-00-strict-equals-mismatched-types COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00strict_equals_mismatched_types.ts") tslang_add_test(NAME test-compile-00-length-with-negative-merge COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00length_with_negative_merge.ts") @@ -932,6 +933,7 @@ tslang_add_test(NAME test-jit-00-optional-number-compare COMMAND test-runner -ji tslang_add_test(NAME test-jit-00-optional-loose-null COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00optional_loose_null.ts") tslang_add_test(NAME test-jit-00-union-nullable-array COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00union_nullable_array.ts") tslang_add_test(NAME test-jit-00-union-nullable-value COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00union_nullable_value.ts") +tslang_add_test(NAME test-jit-00-opaque-nullable-compare COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00opaque_nullable_compare.ts") tslang_add_test(NAME test-jit-00-union-value-equality COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00union_value_equality.ts") tslang_add_test(NAME test-jit-00-strict-equals-mismatched-types COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00strict_equals_mismatched_types.ts") tslang_add_test(NAME test-jit-00-length-with-negative-merge COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00length_with_negative_merge.ts") diff --git a/tslang/test/tester/tests/00opaque_nullable_compare.ts b/tslang/test/tester/tests/00opaque_nullable_compare.ts new file mode 100644 index 000000000..f94441f29 --- /dev/null +++ b/tslang/test/tester/tests/00opaque_nullable_compare.ts @@ -0,0 +1,61 @@ +// Comparing an Opaque with an `Opaque | null` overflowed the compiler's stack (0xC00000FD, no +// message): the comparison's lowering kept calling itself with the same operand types. + +class State { + current: Opaque | null; + + constructor() { + this.current = null; + } + + set(next: Opaque): boolean { + if (next != this.current) { + this.current = next; + return true; + } + + return false; + } +} + +class Item { + id = 1; +} + +function sameOpaque(a: Opaque, b: Opaque | null) { + return a == b; +} + +function sameOpaqueReversed(a: Opaque | null, b: Opaque) { + return a == b; +} + +function sameItem(a: Item, b: Item | null) { + return a === b; +} + +function differentItem(a: Item, b: Item | null) { + return a !== b; +} + +function main() { + const s = new State(); + const h = new Item(); + const g = new Item(); + + assert(s.set(h), "first set binds"); + assert(!s.set(h), "same handle again is skipped"); + assert(s.set(g), "another handle binds"); + + assert(sameOpaque(h, h), "h == h"); + assert(!sameOpaque(h, g), "h == g"); + assert(!sameOpaque(h, null), "h == null"); + assert(sameOpaqueReversed(h, h), "h == h (union on the left)"); + + const item = new Item(); + assert(sameItem(item, item), "item === item"); + assert(!sameItem(item, null), "item === null"); + assert(differentItem(item, new Item()), "item !== other"); + + print("done."); +} From 5222a8ac4b0cb177be09fd6917aff9b200927e2c Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sun, 11 Oct 2026 13:03:14 +0100 Subject: [PATCH 2/2] List the new test in TSLANG_CORPUS, so rc and none run it too Co-Authored-By: Claude Opus 5.5 --- tslang/test/tester/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 0df195d72..a99ec5609 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -2168,6 +2168,7 @@ set(TSLANG_CORPUS typeGuardOfFormThisMember.ts typeGuardOfFormTypeOfBoolean.ts types_vs_interfaces.ts + 00opaque_nullable_compare.ts ) # the default model runs these ahead of time only, so the other models do too