From c34a620454ce429a57071fcbb2c7891626809978 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Wed, 30 Sep 2026 23:21:47 +0100 Subject: [PATCH] An optional number compares as the wider type, and only when it holds a value `let n: number | undefined = 0.5; n === 0` was true. The numeric sync in adjustTypesForBinaryOp looked for the first promotion type either operand had exactly; an optional never is one, so the other operand's type won - an integer literal's si32 - and the optional was cast out to it, 0.5 becoming 0. The same cast read an empty optional as 0: `undefined === 0`, `undefined < 1` were true. A comparison with one optional numeric side now widens both to the wider of the optional's element type and the other operand's type, and keeps the optional, so the lowering compares only when it holds a value (OptionalLogicHelper). `-` and `*`, which share the case, are left as they were. Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/MLIRGenImpl.h | 63 +++++++++++++++++++ tslang/test/tester/CMakeLists.txt | 2 + .../tester/tests/00optional_number_compare.ts | 56 +++++++++++++++++ 3 files changed, 121 insertions(+) create mode 100644 tslang/test/tester/tests/00optional_number_compare.ts diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index 1ae1e43c0..207b34e28 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -5861,6 +5861,54 @@ class MLIRGenImpl return mlir::success(); } + // An optional number compared with a number. The lowering compares them only when the optional + // holds a value (OptionalLogicHelper), so it stays optional here: synced as a plain value it was + // cast out of the optional first, an empty one reading as 0 - and the promotion saw only the other + // operand's type, so against an integer literal 0.5 became an s32 and `0.5 === 0` was true. Both + // sides take the wider of the optional's element type and the other operand's type. + ValueOrLogicalResult syncOptionalNumericTypes(mlir::Location location, mlir::Value &leftExpressionValue, mlir::Value &rightExpressionValue, const GenContext &genContext) + { + auto leftOptType = dyn_cast(leftExpressionValue.getType()); + auto rightOptType = dyn_cast(rightExpressionValue.getType()); + if (!leftOptType == !rightOptType) + { + return mlir::success(); + } + + auto &optValue = leftOptType ? leftExpressionValue : rightExpressionValue; + auto &otherValue = leftOptType ? rightExpressionValue : leftExpressionValue; + auto elementType = (leftOptType ? leftOptType : rightOptType).getElementType(); + auto otherType = otherValue.getType(); + if (auto literalType = dyn_cast(otherType)) + { + otherType = literalType.getElementType(); + } + + if (!isPromotableNumeric(elementType) || !isPromotableNumeric(otherType)) + { + return mlir::success(); + } + + auto types = numericPromotionOrder(); + auto commonType = llvm::find_if(types, [&](mlir::Type type) { return type == elementType || type == otherType; }); + if (commonType == types.end()) + { + return mlir::success(); + } + + if (elementType != *commonType) + { + CAST(optValue, location, getOptionalType(*commonType), optValue, genContext); + } + + if (otherValue.getType() != *commonType) + { + CAST(otherValue, location, *commonType, otherValue, genContext); + } + + return optValue; + } + // The order the arithmetic operators promote in, widest first: the first type either // operand already has is the one both are cast to. Shared by `+` and by the general // arithmetic/comparison path below, which is the point -- they disagreed, and `+` was @@ -6169,6 +6217,21 @@ class MLIRGenImpl // TODO: do we need to sync type for all Ops? auto types = numericPromotionOrder(); + // `-` and `*` fall into this case too: arithmetic on an optional is left as it was + if (opCode != SyntaxKind::MinusToken && opCode != SyntaxKind::AsteriskToken) + { + auto o = syncOptionalNumericTypes(location, leftExpressionValue, rightExpressionValue, genContext); + if (o.value) + { + break; + } + + if (mlir::failed(o.result)) + { + return mlir::failure(); + } + } + auto r = syncUnionTypes(location, leftExpressionValue, rightExpressionValue, genContext); if (r.value) { diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 2ed05fbf1..e6d8c70b0 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -385,6 +385,7 @@ tslang_add_test(NAME test-compile-00-union-errors COMMAND test-runner "${PROJECT tslang_add_test(NAME test-compile-00-union-null-to-string COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00union_null_to_string.ts") tslang_add_test(NAME test-compile-00-union-null-undefined COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00union_null_undefined.ts") tslang_add_test(NAME test-compile-00-union-null-undefined-nonstrict COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00union_null_undefined_nonstrict.ts") +tslang_add_test(NAME test-compile-00-optional-number-compare COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00optional_number_compare.ts") tslang_add_test(NAME test-compile-00-intersection-type COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00intersection_type.ts") tslang_add_test(NAME test-compile-00-intersection-type-generic COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00intersection_type_generic.ts") tslang_add_test(NAME test-compile-00-length COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00length.ts") @@ -853,6 +854,7 @@ tslang_add_test(NAME test-jit-00-union-errors COMMAND test-runner -jit "${PROJEC tslang_add_test(NAME test-jit-00-union-null-to-string COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00union_null_to_string.ts") tslang_add_test(NAME test-jit-00-union-null-undefined COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00union_null_undefined.ts") tslang_add_test(NAME test-jit-00-union-null-undefined-nonstrict COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00union_null_undefined_nonstrict.ts") +tslang_add_test(NAME test-jit-00-optional-number-compare COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00optional_number_compare.ts") tslang_add_test(NAME test-jit-00-union-global-init COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00union_global_init.ts") tslang_add_test(NAME test-jit-00-intersection-type COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00intersection_type.ts") tslang_add_test(NAME test-jit-00-intersection-type-generic COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00intersection_type_generic.ts") diff --git a/tslang/test/tester/tests/00optional_number_compare.ts b/tslang/test/tester/tests/00optional_number_compare.ts new file mode 100644 index 000000000..fa2cd3a88 --- /dev/null +++ b/tslang/test/tester/tests/00optional_number_compare.ts @@ -0,0 +1,56 @@ +// An optional number compares as the wider of its own type and the other operand's - not as the +// other operand's alone, which made 0.5 an s32 against an integer literal - and only when it holds a +// value: an empty one is not 0, it is less than, greater than and equal to nothing +function holdsFraction() { + let n: number | undefined = 0.5; + assert(!(n === 0), "0.5 === 0"); + assert(n === 0.5, "0.5 === 0.5"); + assert(n !== 0, "0.5 !== 0"); + assert(n < 1, "0.5 < 1"); + assert(n > 0, "0.5 > 0"); + assert(!(0 === n), "0 === 0.5"); + assert(0.5 == n, "0.5 == 0.5"); + + let zero: number = 0; + let half: number = 0.5; + assert(!(n === zero), "0.5 === zero"); + assert(n === half, "0.5 === half"); + assert(n > zero, "0.5 > zero"); +} + +function empty() { + let n: number | undefined = undefined; + assert(!(n === 0), "undefined === 0"); + assert(!(n == 0), "undefined == 0"); + assert(n !== 0, "undefined !== 0"); + assert(!(n < 1), "undefined < 1"); + assert(!(n >= 0), "undefined >= 0"); + assert(!(0 === n), "0 === undefined"); + + let zero: number = 0; + assert(!(n === zero), "undefined === zero"); + assert(!(n <= zero), "undefined <= zero"); +} + +function integers() { + let i: s32 | undefined = 3; + assert(i === 3, "s32 3 === 3"); + assert(!(i === 0), "s32 3 === 0"); + assert(i > 2, "s32 3 > 2"); + assert(i < 3.5, "s32 3 < 3.5"); + + i = undefined; + assert(!(i === 0), "s32 undefined === 0"); + assert(!(i < 1), "s32 undefined < 1"); + + let f: number | undefined = 3; + assert(f === 3, "3.0 === 3"); + assert(f > 2.5, "3.0 > 2.5"); +} + +function main() { + holdsFraction(); + empty(); + integers(); + print("done."); +}