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."); +}