From 83e0775886b3d05647c24d080ec29ffc2c1dc34e Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sun, 11 Oct 2026 12:53:31 +0100 Subject: [PATCH 1/2] Numbers in `any` compare by value, whatever types they were boxed with `===` and `!==` between two `any`s compared the boxes' bytes, and when the sizes differed (a `number` and an `s32`) both answered false. So `isEq(x: any, y: any) { if (x !== y) fail(); }`, the DefaultLib tests' helper, could never fail on a computed double compared with an integer literal, which is how `Math.max(-1, -7)` returning 2.2e-308 got past DefaultLib's tests/math.ts. Loose `==` had no case for two numbers with different tags either (`1.0 == 1` was false), and orderings compared bytes. Two numeric `any`s now compare as f64 for every operator: the tag's first letter says float, unsigned or signed, and the box's data size says the width. Values of different kinds with different sizes are unequal under `!==` too, not just under `!=`. Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/LowerToLLVM.cpp | 164 +++++++++++++----- tslang/test/tester/CMakeLists.txt | 2 + .../test/tester/tests/00any_number_compare.ts | 84 +++++++++ 3 files changed, 211 insertions(+), 39 deletions(-) create mode 100644 tslang/test/tester/tests/00any_number_compare.ts diff --git a/tslang/lib/TypeScript/LowerToLLVM.cpp b/tslang/lib/TypeScript/LowerToLLVM.cpp index 31ac76fdd..4802704f0 100644 --- a/tslang/lib/TypeScript/LowerToLLVM.cpp +++ b/tslang/lib/TypeScript/LowerToLLVM.cpp @@ -1272,17 +1272,115 @@ class AnyCompareOpLowering : public TsLlvmPattern return bodyCmpResult; }, [&](OpBuilder &builder, Location loc) { - // sizes (and therefore kinds) differ: == is false, != is true; ordering - // ops and === / !== fall back to their pre-existing "false" behavior. - if (code == SyntaxKind::ExclamationEqualsToken) - { - return (mlir::Value)rewriter.create(loc, LLVM::ICmpPredicate::ne, sizeAny1, sizeAny2); - } + // sizes (and therefore kinds) differ: == and === are false, != and !== are true, + // and an ordering is false + return (mlir::Value)clh.createI1ConstantOf(code == SyntaxKind::ExclamationEqualsToken || + code == SyntaxKind::ExclamationEqualsEqualsToken); + }); + } + + // A numeric `any` as an f64, whatever width and signedness it was boxed with. Its tag names + // the type ("number", "f32", "s64", "u8", "index", ...): the first letter says float ("f" and + // "number"), unsigned ("u") or signed (the rest), and the box's data size says the width. + mlir::Value unboxNumericAsF64(mlir::Value anyValue, mlir::Value tag, mlir::Location loc, AnyLogic &al, + CodeLogicHelper &clh, TypeHelper &th, LLVMTypeConverterHelper &llvmtch, + ConversionPatternRewriter &rewriter) const + { + auto f64Type = th.getF64Type(); + auto llvmIndexType = llvmtch.typeConverter->convertType(th.getIndexType()); - return (mlir::Value)sizesEqual; + auto firstLetter = rewriter.create(loc, th.getI8Type(), tag); + auto size = al.getDataSizeOfAny(anyValue); + + auto letterIs = [&](char letter) { + return (mlir::Value)rewriter.create(loc, LLVM::ICmpPredicate::eq, firstLetter, + clh.createI8ConstantOf(letter)); + }; + auto sizeIs = [&](int64_t bytes) { + return (mlir::Value)rewriter.create(loc, LLVM::ICmpPredicate::eq, size, + clh.createIndexConstantOf(llvmIndexType, bytes)); + }; + + auto readInteger = [&](bool isSigned) { + auto read = [&](unsigned bits) -> mlir::Value { + auto raw = al.UnboxAny(anyValue, rewriter.getIntegerType(bits)); + return isSigned ? (mlir::Value)rewriter.create(loc, f64Type, raw) + : (mlir::Value)rewriter.create(loc, f64Type, raw); + }; + + return clh.conditionalExpressionLowering( + loc, f64Type, sizeIs(8), [&](OpBuilder &, Location) { return read(64); }, + [&](OpBuilder &, Location) { + return clh.conditionalExpressionLowering( + loc, f64Type, sizeIs(4), [&](OpBuilder &, Location) { return read(32); }, + [&](OpBuilder &, Location) { + return clh.conditionalExpressionLowering( + loc, f64Type, sizeIs(2), [&](OpBuilder &, Location) { return read(16); }, + [&](OpBuilder &, Location) { return read(8); }); + }); + }); + }; + + auto isFloat = rewriter.create(loc, letterIs('f'), letterIs('n')); + return clh.conditionalExpressionLowering( + loc, f64Type, isFloat, + [&](OpBuilder &, Location) { + return clh.conditionalExpressionLowering( + loc, f64Type, sizeIs(4), + [&](OpBuilder &, Location) { + return (mlir::Value)rewriter.create(loc, f64Type, + al.UnboxAny(anyValue, th.getF32Type())); + }, + [&](OpBuilder &, Location) { return al.UnboxAny(anyValue, f64Type); }); + }, + [&](OpBuilder &, Location) { + return clh.conditionalExpressionLowering( + loc, f64Type, letterIs('u'), [&](OpBuilder &, Location) { return readInteger(false); }, + [&](OpBuilder &, Location) { return readInteger(true); }); }); } + // Two numeric `any`s compare by value, as JavaScript numbers: `1.0 === 1`, whatever widths they + // were boxed with. Comparing their bytes made a number and an integer never equal - and never + // unequal either, `!==` included - and ordered them by their bytes. + mlir::Value compareNumbers(mlir::Value op1, mlir::Value tag1, mlir::Value op2, mlir::Value tag2, SyntaxKind code, + mlir::Location loc, AnyLogic &al, CodeLogicHelper &clh, TypeHelper &th, + LLVMTypeConverterHelper &llvmtch, ConversionPatternRewriter &rewriter) const + { + auto value1 = unboxNumericAsF64(op1, tag1, loc, al, clh, th, llvmtch, rewriter); + auto value2 = unboxNumericAsF64(op2, tag2, loc, al, clh, th, llvmtch, rewriter); + + LLVM::FCmpPredicate predicate; + switch (code) + { + case SyntaxKind::EqualsEqualsToken: + case SyntaxKind::EqualsEqualsEqualsToken: + predicate = LLVM::FCmpPredicate::oeq; + break; + case SyntaxKind::ExclamationEqualsToken: + case SyntaxKind::ExclamationEqualsEqualsToken: + predicate = LLVM::FCmpPredicate::une; + break; + case SyntaxKind::GreaterThanToken: + predicate = LLVM::FCmpPredicate::ogt; + break; + case SyntaxKind::GreaterThanEqualsToken: + predicate = LLVM::FCmpPredicate::oge; + break; + case SyntaxKind::LessThanToken: + predicate = LLVM::FCmpPredicate::olt; + break; + case SyntaxKind::LessThanEqualsToken: + predicate = LLVM::FCmpPredicate::ole; + break; + default: + emitError(loc, "unsupported 'any' comparison operator"); + return mlir::Value(); + } + + return rewriter.create(loc, predicate, value1, value2); + } + // JS loose equality across differing `any` payload kinds: number<->string, // number<->boolean, string<->boolean each coerce (number.toString()/parseFloat(), // boolean->0|1, boolean->"true"|"false") rather than comparing raw bytes. Only @@ -1298,14 +1396,6 @@ class AnyCompareOpLowering : public TsLlvmPattern auto stringTy = mlir_ts::StringType::get(rewriter.getContext()); auto booleanTy = mlir_ts::BooleanType::get(rewriter.getContext()); - auto isTag = [&](mlir::Value tag, const char *name) { - auto tagLiteral = ch.getOrCreateGlobalString(name, std::string(name)); - auto strcmpFuncOp = ch.getOrInsertFunction("strcmp", th.getFunctionType(th.getI32Type(), {th.getPtrType(), th.getPtrType()})); - auto cmp = rewriter.create(loc, strcmpFuncOp, ValueRange{tag, tagLiteral}); - auto const0 = clh.createI32ConstantOf(0); - return (mlir::Value)rewriter.create(loc, LLVM::ICmpPredicate::eq, cmp.getResult(), const0); - }; - // typeOfAsString reports concrete-width tags ("s32"/"s64"/...) for integer // literals and only uses "number" for float-typed values (see // TypeOfOpHelper::typeOfAsString), so "is this any numeric" cannot just test the @@ -1315,26 +1405,9 @@ class AnyCompareOpLowering : public TsLlvmPattern TypeDescriptorLogic tdl(rewriter, tch, loc); auto isKind = [&](mlir::Value tag, int kind) { return tdl.isKind(tag, kind); }; - // unbox a numeric `any` (whatever its concrete boxed width/signedness) into - // a normalized f64 for comparison, dispatching on the exact tag reported at - // box time so we read back the same width that was stored. This one stays - // name-based: the descriptor's kind says "numeric", not which width, and the - // width is what decides how many bytes to read back. + LLVMTypeConverterHelper llvmtch(static_cast(getTypeConverter())); auto unboxNumericAsF64 = [&](mlir::Value numberSideAny, mlir::Value numberTag) { - auto asF64 = [&](mlir::Type storedTy) { - auto raw = al.UnboxAny(numberSideAny, tch.convertType(storedTy)); - return storedTy == numberTy ? raw : castLogic.cast(raw, storedTy, numberTy); - }; - - return clh.conditionalExpressionLowering( - loc, th.getF64Type(), isTag(numberTag, "number"), - [&](OpBuilder &, Location) { return asF64(numberTy); }, - [&](OpBuilder &, Location) { - return clh.conditionalExpressionLowering( - loc, th.getF64Type(), isTag(numberTag, "s64"), - [&](OpBuilder &, Location) { return asF64(mlir::IntegerType::get(rewriter.getContext(), 64, mlir::IntegerType::Signed)); }, - [&](OpBuilder &, Location) { return asF64(mlir::IntegerType::get(rewriter.getContext(), 32, mlir::IntegerType::Signed)); }); - }); + return this->unboxNumericAsF64(numberSideAny, numberTag, loc, al, clh, th, llvmtch, rewriter); }; auto compareAsNumber = [&](mlir::Value numVal1, mlir::Value numVal2) { @@ -1440,17 +1513,14 @@ class AnyCompareOpLowering : public TsLlvmPattern auto op1 = transformed.getOp1(); auto op2 = transformed.getOp2(); - auto compareBoxes = [&]() -> mlir::Value { + auto compareOtherBoxes = [&](mlir::Value tag1, mlir::Value tag2) -> mlir::Value { // only loose equality (==/!=) coerces across differing payload kinds; strict - // equality and ordering compare the underlying bytes as before. + // equality and ordering compare the underlying bytes. if (code != SyntaxKind::EqualsEqualsToken && code != SyntaxKind::ExclamationEqualsToken) { return sameKindCompare(op, op1, op2, code, loc, al, clh, th, ch, llvmtch, rewriter); } - auto tag1 = al.getTypeOfAny(op1); - auto tag2 = al.getTypeOfAny(op2); - auto strcmpFuncOp = ch.getOrInsertFunction("strcmp", th.getFunctionType(th.getI32Type(), {th.getPtrType(), th.getPtrType()})); auto tagCmp = rewriter.create(loc, strcmpFuncOp, ValueRange{tag1, tag2}); auto const0 = clh.createI32ConstantOf(0); @@ -1462,6 +1532,22 @@ class AnyCompareOpLowering : public TsLlvmPattern [&](OpBuilder &builder, Location loc) { return coerceAndCompareMixedKinds(op1, op2, tag1, tag2, code, loc, al, castLogic, clh, th, ch, tch, rewriter); }); }; + auto compareBoxes = [&]() -> mlir::Value { + auto tag1 = al.getTypeOfAny(op1); + auto tag2 = al.getTypeOfAny(op2); + + // two numbers compare by value for every operator, whichever widths they were boxed with + TypeDescriptorLogic tdl(rewriter, tch, loc); + auto bothNumbers = rewriter.create(loc, tdl.isKind(tag1, TYPE_KIND_NUMBER), + tdl.isKind(tag2, TYPE_KIND_NUMBER)); + return clh.conditionalExpressionLowering( + loc, th.getLLVMBoolType(), bothNumbers, + [&](OpBuilder &, Location loc) { + return compareNumbers(op1, tag1, op2, tag2, code, loc, al, clh, th, llvmtch, rewriter); + }, + [&](OpBuilder &, Location loc) { return compareOtherBoxes(tag1, tag2); }); + }; + // `undefined` and `null` in an `any`, answered before any box is opened: `u == undefined` // of an `any` holding undefined was false - the box's tag and bytes were compared against // `undefined` or `null` turned into an `any`, which is a null pointer rather than a box, diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 7a9a4d06e..e3d6547dd 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -414,6 +414,7 @@ tslang_add_test(NAME test-compile-00-union-nullable-array COMMAND test-runner "$ 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-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-any-number-compare COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00any_number_compare.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") tslang_add_test(NAME test-compile-00-length-signed-arithmetic COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00length_signed_arithmetic.ts") tslang_add_test(NAME test-compile-00-optional-fields COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00optional_fields.ts") @@ -934,6 +935,7 @@ tslang_add_test(NAME test-jit-00-union-nullable-array COMMAND test-runner -jit " 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-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-any-number-compare COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00any_number_compare.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") tslang_add_test(NAME test-jit-00-length-signed-arithmetic COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00length_signed_arithmetic.ts") tslang_add_test(NAME test-jit-00-optional-fields COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00optional_fields.ts") diff --git a/tslang/test/tester/tests/00any_number_compare.ts b/tslang/test/tester/tests/00any_number_compare.ts new file mode 100644 index 000000000..a1b20e5fa --- /dev/null +++ b/tslang/test/tester/tests/00any_number_compare.ts @@ -0,0 +1,84 @@ +// Two `any`s holding numbers compare by value, as JavaScript numbers do, whatever types they were +// boxed with: a `number` and an integer were neither `===` nor `!==` each other, so a check like +// `isEq(x: any, y: any) { if (x !== y) fail(); }` could never fail on a computed double compared +// with an integer literal. + +function isNe(x: any, y: any): boolean { + return x !== y; +} + +function isStrictEq(x: any, y: any): boolean { + return x === y; +} + +function mixedWidths() { + const n: any = 2.5; + const i: any = 1; + assert(n !== i, "2.5 !== 1"); + assert(!(n === i), "2.5 === 1"); + assert(n != i, "2.5 != 1"); + assert(!(n == i), "2.5 == 1"); + + const whole: any = 1.0; + assert(whole === i, "1.0 === 1"); + assert(!(whole !== i), "1.0 !== 1"); + assert(whole == i, "1.0 == 1"); + + assert(isNe(2.2e-308, -1), "isNe(2.2e-308, -1)"); + assert(!isNe(-1.0, -1), "isNe(-1.0, -1)"); + assert(isStrictEq(3, 3.0), "isStrictEq(3, 3.0)"); +} + +function widths() { + const a: any = 5; + const b: any = 5; + const c: any = 5; + const d: any = 5.0; + const e: any = -3; + const f: any = -3.0; + assert(a === b, "s64 === u8"); + assert(b === c, "u8 === f32"); + assert(c === d, "f32 === number"); + assert(e === f, "s16 === number (negative)"); + assert(a !== e, "s64 !== s16"); +} + +function ordering() { + const big: any = 2.5; + const small: any = 1; + const neg: any = -7; + assert(big > small, "2.5 > 1"); + assert(small < big, "1 < 2.5"); + assert(neg < small, "-7 < 1"); + assert(big >= 2.5, "2.5 >= 2.5"); + assert(!(neg > small), "-7 > 1"); +} + +function nan() { + const x: any = NaN; + const y: any = NaN; + assert(x !== y, "NaN !== NaN"); + assert(!(x === y), "NaN === NaN"); +} + +function otherKinds() { + // strings, and values of different kinds, are unaffected + const s: any = "a"; + const t: any = "a"; + const n: any = 1; + assert(s === t, "\"a\" === \"a\""); + assert(s !== n, "\"a\" !== 1"); + assert(!(s === n), "\"a\" === 1"); + const b: any = true; + assert(b !== n, "true !== 1"); + assert(b == n, "true == 1"); +} + +function main() { + mixedWidths(); + widths(); + ordering(); + nan(); + otherKinds(); + print("done."); +} From dacae917f13448b7208edcba7c2bbebe3b2dc782 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sun, 11 Oct 2026 13:04:19 +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 e3d6547dd..09e13d613 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 + 00any_number_compare.ts ) # the default model runs these ahead of time only, so the other models do too