From 21cd9a746be183b5788138175b88648c8db76f3f Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Wed, 30 Sep 2026 16:51:05 +0100 Subject: [PATCH] An `any` compares with undefined, null and strings by what it holds Four ways comparing an `any` gave the wrong answer: - With undefined, UndefLogicHelper answered at compile time: `==` false, `!=` true, whatever the `any` held. It now hands an `any` to the run-time comparison (AnyCompareOp), as it already did for a class or an interface. - LogicOp boxed the other operand only when its type differed from the `any`'s - but the operands are lowered already, and an `any`, a string and null are all a `ptr`. The string (or null) itself went in as the box, and its "tag" was read from whatever it pointed at (address 8 for null): `let t: any = "text"; t == "text"` was false. It is boxed now by its TypeScript type, on the stack - the box lives for the comparison only, and a heap one was never freed under reference counting (96 MB for 2M comparisons at -O0). - AnyCompareOp answers undefined and null before opening any box: a null pointer is undefined (a zeroed `any` slot is one), otherwise the tag's kind says. Loosely undefined and null equal each other and nothing else; strictly each equals only itself; an ordering stays false. - Two string boxes compared the bytes of their payload, a pointer: equal text built at run time was unequal. They compare with strcmp now. And `if (a)` / `!a` of an `any` holding undefined or null threw "Can't cast from any type" from the generated ___unbox; it returns false for both, as JavaScript does. Test 00any_equality_undefined.ts. Without the change it fails in every model, compile and JIT. Co-Authored-By: Claude Opus 5.5 --- .../LowerToLLVM/UnaryBinLogicalOrHelper.h | 35 ++++- .../TypeScript/LowerToLLVM/UndefLogicHelper.h | 4 +- tslang/lib/TypeScript/LowerToLLVM.cpp | 132 +++++++++++++++--- tslang/lib/TypeScript/MLIRGenCast.cpp | 7 + tslang/test/tester/CMakeLists.txt | 3 + .../tester/tests/00any_equality_undefined.ts | 65 +++++++++ 6 files changed, 227 insertions(+), 19 deletions(-) create mode 100644 tslang/test/tester/tests/00any_equality_undefined.ts diff --git a/tslang/include/TypeScript/LowerToLLVM/UnaryBinLogicalOrHelper.h b/tslang/include/TypeScript/LowerToLLVM/UnaryBinLogicalOrHelper.h index 3400eb9ff..057654f66 100644 --- a/tslang/include/TypeScript/LowerToLLVM/UnaryBinLogicalOrHelper.h +++ b/tslang/include/TypeScript/LowerToLLVM/UnaryBinLogicalOrHelper.h @@ -8,6 +8,8 @@ #include "TypeScript/TypeScriptOps.h" #include "TypeScript/LowerToLLVM/LLVMTypeConverterHelper.h" +#include "TypeScript/MLIRLogic/TypeOfOpHelper.h" +#include "TypeScript/LowerToLLVM/LLVMCodeHelperBase.h" #include "scanner_enums.h" @@ -156,7 +158,38 @@ mlir::Value LogicOp(Operation *binOp, SyntaxKind op, mlir::Value left, mlir::Typ } else if (isa(leftType)) { - if (left.getType() != right.getType()) + // the operands are lowered already, so an `any` and a string (or null) are both a `ptr` + // and comparing their types said nothing: the string itself was handed over as the box, + // and its bytes read as one - `let t: any = "text"; t == "text"` was false. Box it by + // its TypeScript type, as a cast to `any` does - on the stack: the box lives for this + // comparison only, and one on the heap was never freed under reference counting. + auto rightElementType = isa(rightType) ? cast(rightType).getElementType() : rightType; + if (!isa(rightElementType) && !isa(rightElementType)) + { + auto ctx = builder.getContext(); + auto ptrTy = LLVM::LLVMPointerType::get(ctx); + auto llvmIndexType = typeConverter.convertType(builder.getIndexType()); + auto boxType = LLVM::LLVMStructType::getLiteral(ctx, {llvmIndexType, ptrTy, right.getType()}, false); + + LLVMCodeHelperBase ch(binOp, builder, &typeConverter, compileOptions); + auto box = ch.Alloca(boxType, 1); + + auto sizeMLIR = builder.create(loc, builder.getIndexType(), right.getType()); + auto size = builder.create(loc, llvmIndexType, sizeMLIR); + + TypeOfOpHelper toh(builder); + auto typeOfValue = builder.create(loc, ptrTy, toh.typeOfLogic(loc, rightElementType)); + + auto fieldAddress = [&](int index) { + return builder.create(loc, ptrTy, boxType, box, ArrayRef{0, index}); + }; + builder.create(loc, size, fieldAddress(ANY_SIZE)); + builder.create(loc, typeOfValue, fieldAddress(ANY_TYPE)); + builder.create(loc, right, fieldAddress(ANY_DATA)); + + right = box; + } + else if (left.getType() != right.getType()) { right = builder.create(loc, left.getType(), right); } diff --git a/tslang/include/TypeScript/LowerToLLVM/UndefLogicHelper.h b/tslang/include/TypeScript/LowerToLLVM/UndefLogicHelper.h index 071bfbf9c..0e7e43941 100644 --- a/tslang/include/TypeScript/LowerToLLVM/UndefLogicHelper.h +++ b/tslang/include/TypeScript/LowerToLLVM/UndefLogicHelper.h @@ -117,7 +117,9 @@ class UndefLogicHelper auto processUndefVale = [&](OpBuilder &builder, Location loc, mlir::Type t1, mlir::Value val1, mlir::Type t2, mlir::Value val2) { - if (isa(t2) || isa(t2)) + // an `any` may hold undefined, so it is asked at run time (AnyCompareOp) rather + // than answered here: `u == undefined` of an `any` holding undefined was false + if (isa(t2) || isa(t2) || isa(t2)) { auto casted = rewriter.create(loc, t2, val1); return LogicOp(binOp, opCmpCode, val2, val2.getType(), casted, casted.getType(), diff --git a/tslang/lib/TypeScript/LowerToLLVM.cpp b/tslang/lib/TypeScript/LowerToLLVM.cpp index 0d27a513c..aabbca330 100644 --- a/tslang/lib/TypeScript/LowerToLLVM.cpp +++ b/tslang/lib/TypeScript/LowerToLLVM.cpp @@ -1027,6 +1027,55 @@ class AnyCompareOpLowering : public TsLlvmPattern ConversionPatternRewriter &rewriter) const { auto i8PtrTy = th.getPtrType(); + + auto compareResultWith0 = [&](mlir::Value compareResult) -> mlir::Value { + auto const0 = clh.createI32ConstantOf(0); + switch (code) + { + case SyntaxKind::EqualsEqualsToken: + case SyntaxKind::EqualsEqualsEqualsToken: + return rewriter.create(loc, LLVM::ICmpPredicate::eq, compareResult, const0); + case SyntaxKind::ExclamationEqualsToken: + case SyntaxKind::ExclamationEqualsEqualsToken: + return rewriter.create(loc, LLVM::ICmpPredicate::ne, compareResult, const0); + case SyntaxKind::GreaterThanToken: + return rewriter.create(loc, LLVM::ICmpPredicate::sgt, compareResult, const0); + case SyntaxKind::GreaterThanEqualsToken: + return rewriter.create(loc, LLVM::ICmpPredicate::sge, compareResult, const0); + case SyntaxKind::LessThanToken: + return rewriter.create(loc, LLVM::ICmpPredicate::slt, compareResult, const0); + case SyntaxKind::LessThanEqualsToken: + return rewriter.create(loc, LLVM::ICmpPredicate::sle, compareResult, const0); + default: + op->emitError("unsupported 'any' comparison operator"); + return mlir::Value(); + } + }; + + // two strings compare by their text: their payload is a pointer, and comparing its bytes + // made two equal strings unequal unless they were the same literal + TypeConverterHelper tch(getTypeConverter()); + TypeDescriptorLogic tdl(rewriter, tch, loc); + auto bothStrings = rewriter.create(loc, tdl.isKind(al.getTypeOfAny(op1), TYPE_KIND_STRING), + tdl.isKind(al.getTypeOfAny(op2), TYPE_KIND_STRING)); + return clh.conditionalExpressionLowering( + loc, th.getLLVMBoolType(), bothStrings, + [&](OpBuilder &, Location) { + auto strcmpFuncOp = ch.getOrInsertFunction("strcmp", th.getFunctionType(th.getI32Type(), {i8PtrTy, i8PtrTy})); + auto compareResult = rewriter.create( + loc, strcmpFuncOp, ValueRange{al.UnboxAny(op1, i8PtrTy), al.UnboxAny(op2, i8PtrTy)}); + return compareResultWith0(compareResult.getResult()); + }, + [&](OpBuilder &, Location) { return compareBytes(op, op1, op2, code, loc, al, clh, th, ch, llvmtch, rewriter); }); + } + + // the payloads' bytes: for everything but a string, whose payload is a pointer + mlir::Value compareBytes(mlir_ts::AnyCompareOp op, mlir::Value op1, mlir::Value op2, SyntaxKind code, + mlir::Location loc, AnyLogic &al, CodeLogicHelper &clh, TypeHelper &th, + LLVMCodeHelper &ch, LLVMTypeConverterHelper &llvmtch, + ConversionPatternRewriter &rewriter) const + { + auto i8PtrTy = th.getPtrType(); auto llvmIndexType = llvmtch.typeConverter->convertType(th.getIndexType()); auto memcmpFuncOp = ch.getOrInsertFunction("memcmp", th.getFunctionType(th.getI32Type(), {i8PtrTy, i8PtrTy, llvmIndexType})); @@ -1256,27 +1305,76 @@ class AnyCompareOpLowering : public TsLlvmPattern auto op1 = transformed.getOp1(); auto op2 = transformed.getOp2(); - // only loose equality (==/!=) coerces across differing payload kinds; strict - // equality and ordering compare the underlying bytes as before. - if (code != SyntaxKind::EqualsEqualsToken && code != SyntaxKind::ExclamationEqualsToken) - { - auto result = sameKindCompare(op, op1, op2, code, loc, al, clh, th, ch, llvmtch, rewriter); - rewriter.replaceOp(op, result); - return success(); - } + auto compareBoxes = [&]() -> mlir::Value { + // only loose equality (==/!=) coerces across differing payload kinds; strict + // equality and ordering compare the underlying bytes as before. + 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 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); - auto tagsEqual = rewriter.create(loc, LLVM::ICmpPredicate::eq, tagCmp.getResult(), const0); + 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); + auto tagsEqual = rewriter.create(loc, LLVM::ICmpPredicate::eq, tagCmp.getResult(), const0); + + return clh.conditionalExpressionLowering( + loc, th.getLLVMBoolType(), tagsEqual, + [&](OpBuilder &builder, Location loc) { return sameKindCompare(op, op1, op2, code, loc, al, clh, th, ch, llvmtch, rewriter); }, + [&](OpBuilder &builder, Location loc) { return coerceAndCompareMixedKinds(op1, op2, tag1, tag2, code, loc, al, castLogic, clh, th, ch, tch, rewriter); }); + }; + + // `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, + // so its tag was read from address 8 - and a zeroed `any` slot is a null pointer too. A + // null pointer is undefined; otherwise the tag's kind says. Loosely, undefined and null + // equal each other and nothing else; strictly, each equals only itself. + auto nullPtr = rewriter.create(loc, th.getPtrType()); + TypeDescriptorLogic tdl(rewriter, tch, loc); + auto kindOf = [&](mlir::Value anyValue) { + auto isNullPtr = rewriter.create(loc, LLVM::ICmpPredicate::eq, anyValue, nullPtr); + return clh.conditionalExpressionLowering( + loc, th.getI32Type(), isNullPtr, + [&](OpBuilder &, Location) { return (mlir::Value)clh.createI32ConstantOf(TYPE_KIND_UNDEFINED); }, + [&](OpBuilder &, Location) { return tdl.getKindFromTag(al.getTypeOfAny(anyValue)); }); + }; + + auto isNullish = [&](mlir::Value kind) { + auto isUndefined = rewriter.create(loc, LLVM::ICmpPredicate::eq, kind, clh.createI32ConstantOf(TYPE_KIND_UNDEFINED)); + auto isNull = rewriter.create(loc, LLVM::ICmpPredicate::eq, kind, clh.createI32ConstantOf(TYPE_KIND_NULL)); + return (mlir::Value)rewriter.create(loc, isUndefined, isNull); + }; + + auto kind1 = kindOf(op1); + auto kind2 = kindOf(op2); + auto nullish1 = isNullish(kind1); + auto nullish2 = isNullish(kind2); + auto eitherNullish = rewriter.create(loc, nullish1, nullish2); auto result = clh.conditionalExpressionLowering( - loc, th.getLLVMBoolType(), tagsEqual, - [&](OpBuilder &builder, Location loc) { return sameKindCompare(op, op1, op2, code, loc, al, clh, th, ch, llvmtch, rewriter); }, - [&](OpBuilder &builder, Location loc) { return coerceAndCompareMixedKinds(op1, op2, tag1, tag2, code, loc, al, castLogic, clh, th, ch, tch, rewriter); }); + loc, th.getLLVMBoolType(), eitherNullish, + [&](OpBuilder &, Location) -> mlir::Value { + switch (code) + { + case SyntaxKind::EqualsEqualsToken: + return rewriter.create(loc, nullish1, nullish2); + case SyntaxKind::ExclamationEqualsToken: + return rewriter.create(loc, rewriter.create(loc, nullish1, nullish2), + clh.createI1ConstantOf(true)); + case SyntaxKind::EqualsEqualsEqualsToken: + return rewriter.create(loc, LLVM::ICmpPredicate::eq, kind1, kind2); + case SyntaxKind::ExclamationEqualsEqualsToken: + return rewriter.create(loc, LLVM::ICmpPredicate::ne, kind1, kind2); + default: + // an ordering against undefined or null: false, as before, without reading a box + return clh.createI1ConstantOf(false); + } + }, + [&](OpBuilder &, Location) { return compareBoxes(); }); rewriter.replaceOp(op, result); diff --git a/tslang/lib/TypeScript/MLIRGenCast.cpp b/tslang/lib/TypeScript/MLIRGenCast.cpp index 0b647d579..58df7e216 100644 --- a/tslang/lib/TypeScript/MLIRGenCast.cpp +++ b/tslang/lib/TypeScript/MLIRGenCast.cpp @@ -1881,6 +1881,13 @@ namespace mlirgen ss << "\nif (typeof a == 'undefined') return 'undefined';"; ss << "\nif (typeof a == 'null') return 'null';"; } + + // `if (a)` and `!a` of an `any` holding undefined or null: false, as in JavaScript - + // it threw "Can't cast from any type" + if (mlir::isa(type) || mlir::isa(type)) { + ss << "\nif (typeof a == 'undefined') return false;"; + ss << "\nif (typeof a == 'null') return false;"; + } } ss << "\nthrow \"Can't cast from any type\";\n"; diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index dd11a13ba..5ca97d9a2 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -435,6 +435,7 @@ tslang_add_test(NAME test-compile-00-rest-args-pack-owned COMMAND test-runner "$ tslang_add_test(NAME test-compile-00-switch-char-string-cases COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00switch_char_string_cases.ts") tslang_add_test(NAME test-compile-00-generic-class-constructor-inference COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00generic_class_constructor_inference.ts") tslang_add_test(NAME test-compile-00-generator-done-value-zero COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00generator_done_value_zero.ts") +tslang_add_test(NAME test-compile-00-any-equality-undefined COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00any_equality_undefined.ts") tslang_add_test(NAME test-compile-00-owned-generators COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00owned_generators.ts") tslang_add_test(NAME test-compile-00-owned-globals COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00owned_globals.ts") tslang_add_test(NAME test-compile-00-owned-generator-locals COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00owned_generator_locals.ts") @@ -897,6 +898,7 @@ tslang_add_test(NAME test-jit-00-rest-args-pack-owned COMMAND test-runner -jit " tslang_add_test(NAME test-jit-00-switch-char-string-cases COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00switch_char_string_cases.ts") tslang_add_test(NAME test-jit-00-generic-class-constructor-inference COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00generic_class_constructor_inference.ts") tslang_add_test(NAME test-jit-00-generator-done-value-zero COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00generator_done_value_zero.ts") +tslang_add_test(NAME test-jit-00-any-equality-undefined COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00any_equality_undefined.ts") tslang_add_test(NAME test-jit-00-owned-generators COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00owned_generators.ts") tslang_add_test(NAME test-jit-00-owned-globals COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00owned_globals.ts") tslang_add_test(NAME test-jit-00-owned-generator-locals COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00owned_generator_locals.ts") @@ -1583,6 +1585,7 @@ set(TSLANG_CORPUS 00any_generic_equals.ts 00any_types.ts 00any.ts + 00any_equality_undefined.ts 00array_assignment5.ts 00array_cond_access.ts 00array_length_grow_zeroed.ts diff --git a/tslang/test/tester/tests/00any_equality_undefined.ts b/tslang/test/tester/tests/00any_equality_undefined.ts new file mode 100644 index 000000000..bd375df7f --- /dev/null +++ b/tslang/test/tester/tests/00any_equality_undefined.ts @@ -0,0 +1,65 @@ +// Comparing an `any`: +// - with undefined or null was answered at compile time (== false, != true) whatever it held; +// - `null` (and a string) turned into an `any` for the comparison was a pointer, not a box, and +// its "tag" was read from whatever it pointed at; +// - two strings compared the bytes of their pointers, so equal text built at run time differed; +// - `if (a)` / `!a` of an `any` holding undefined or null threw "Can't cast from any type". +function main() { + const u: any = undefined; + assert(u == undefined); + assert(u === undefined); + assert(u == null); + assert(!(u === null)); + assert(!(u != undefined)); + assert(!u); + if (u) { + assert(false); + } + + const n: any = null; + assert(n == null); + assert(n === null); + assert(n == undefined); + assert(!(n === undefined)); + assert(!n); + + const s: any = "text"; + assert(!(s == undefined)); + assert(!(s == null)); + assert(s != undefined); + assert(s); + + // a `let` is not folded into its initializer: the comparison boxes "text" itself + let t: any = "te"; + assert(!(t == "text")); + t = "text"; + assert(t == "text"); + assert(t === "text"); + assert(!(t != "text")); + + // equal text, built at run time, in two boxes + const built = "te" + "xt"; + const a: any = built; + const b: any = "text"; + assert(a == b); + assert(a === b); + assert(!(a != b)); + const c: any = "other"; + assert(!(a == c)); + + // an `any` holding undefined against another holding null + assert(u == n); + assert(!(u === n)); + + // many comparisons: the box made for the right-hand side is on the stack + let hits = 0; + const words = ["text", "nope"]; + for (let i = 0; i < 100000; i++) { + if (t == words[i % 2]) { + hits++; + } + } + assert(hits == 50000); + + print("done."); +}