diff --git a/docs/bugs/00generator_manual_next.ts b/docs/bugs/00generator_manual_next.ts new file mode 100644 index 000000000..69ead4ea6 --- /dev/null +++ b/docs/bugs/00generator_manual_next.ts @@ -0,0 +1,48 @@ +// regression test: calling .next() manually on a real `function*` generator (as opposed +// to driving it via `for...of`) used to never advance the generator's internal state. +// +// root cause: a `const` binding was stored in the symbol table as a bare SSA value with +// no backing stack storage. Each `g.next()` property access needed a ref to recover +// `this` for the bound ".next" method, and without real storage it fell back to +// allocating a fresh temporary copy of `g` -- re-seeded from the pristine, never-mutated +// original -- on every single call site. So every manual `.next()` call restarted the +// generator instead of resuming it. `for...of` happened to work because its lowering +// materializes the generator object into one persistent local up front and reuses it. +// +// fix: a const whose value is a tuple with a bound-method field (e.g. a generator or +// closure object) now gets real stack storage, matching what for...of already relied on. + +function* gen() { + for (let i = 0; i < 5; i++) { + yield i; + } +} + +function main() { + const g = gen(); + + let r = g.next(); + assert(!r.done); + assert(r.value == 0); + + r = g.next(); + assert(!r.done); + assert(r.value == 1); + + r = g.next(); + assert(!r.done); + assert(r.value == 2); + + r = g.next(); + assert(!r.done); + assert(r.value == 3); + + r = g.next(); + assert(!r.done); + assert(r.value == 4); + + r = g.next(); + assert(r.done); + + print("done."); +} diff --git a/tslang/include/TypeScript/LowerToLLVM/CodeLogicHelper.h b/tslang/include/TypeScript/LowerToLLVM/CodeLogicHelper.h index 9a29f42ff..5a11ebe39 100644 --- a/tslang/include/TypeScript/LowerToLLVM/CodeLogicHelper.h +++ b/tslang/include/TypeScript/LowerToLLVM/CodeLogicHelper.h @@ -114,19 +114,25 @@ class CodeLogicHelper // then block auto *thenBlock = rewriter.createBlock(continuationBlock); auto thenValue = thenBuilder(rewriter, loc); + // thenBuilder may itself branch into further blocks (e.g. a nested + // conditionalExpressionLowering call) -- always branch to the result block + // from wherever the insertion point actually ended up, not from the + // (possibly stale) block handle captured before the builder ran. + auto *thenEndBlock = rewriter.getInsertionBlock(); // else block auto *elseBlock = rewriter.createBlock(continuationBlock); auto elseValue = elseBuilder(rewriter, loc); + auto *elseEndBlock = rewriter.getInsertionBlock(); // result block auto *resultBlock = rewriter.createBlock(continuationBlock, TypeRange{type}, {loc}); rewriter.create(loc, ValueRange{}, continuationBlock); - rewriter.setInsertionPointToEnd(thenBlock); + rewriter.setInsertionPointToEnd(thenEndBlock); rewriter.create(loc, ValueRange{thenValue}, resultBlock); - rewriter.setInsertionPointToEnd(elseBlock); + rewriter.setInsertionPointToEnd(elseEndBlock); rewriter.create(loc, ValueRange{elseValue}, resultBlock); // Generate assertion test. diff --git a/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h b/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h index 37f23d2cf..28165d868 100644 --- a/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h +++ b/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h @@ -2275,6 +2275,8 @@ class MLIRTypeHelper { LLVM_DEBUG(llvm::dbgs() << "!! get type of field '" << fieldName << "' of '" << srcType << "'\n";); + srcType = stripLiteralType(srcType); + if (auto constTupleType = dyn_cast(srcType)) { auto index = constTupleType.getIndex(fieldName); diff --git a/tslang/lib/TypeScript/LowerToLLVM.cpp b/tslang/lib/TypeScript/LowerToLLVM.cpp index 50d976cdc..75459eaef 100644 --- a/tslang/lib/TypeScript/LowerToLLVM.cpp +++ b/tslang/lib/TypeScript/LowerToLLVM.cpp @@ -742,41 +742,29 @@ class AnyCompareOpLowering : public TsLlvmPattern public: using TsLlvmPattern::TsLlvmPattern; - LogicalResult matchAndRewrite(mlir_ts::AnyCompareOp op, Adaptor transformed, - ConversionPatternRewriter &rewriter) const final + // same-representation compare: valid when both operands are known to hold the + // same underlying kind (used both when the loose == / != tags actually match, and + // for ===/!==/relational ops, which never coerce across kinds). + mlir::Value sameKindCompare(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 { - - - TypeHelper th(rewriter); - CodeLogicHelper clh(op, rewriter); - LLVMCodeHelper ch(op, rewriter, getTypeConverter(), tsLlvmContext->compileOptions); - TypeConverterHelper tch(getTypeConverter()); - LLVMTypeConverterHelper llvmtch(static_cast(getTypeConverter())); - - auto loc = op->getLoc(); - - AnyLogic al(op, rewriter, tch, loc, tsLlvmContext->compileOptions); - //auto result = al.castToAny(in, transformed.getTypeInfo(), in.getType()); - auto i8PtrTy = th.getPtrType(); auto llvmIndexType = llvmtch.typeConverter->convertType(th.getIndexType()); - // compare bodies auto memcmpFuncOp = ch.getOrInsertFunction("memcmp", th.getFunctionType(th.getI32Type(), {i8PtrTy, i8PtrTy, llvmIndexType})); - // compare sizes of Any first - // TODO: finish it + auto sizeAny1 = al.getDataSizeOfAny(op1); + auto sizeAny2 = al.getDataSizeOfAny(op2); - auto sizeAny1 = al.getDataSizeOfAny(transformed.getOp1()); - auto sizeAny2 = al.getDataSizeOfAny(transformed.getOp2()); + auto sizesEqual = rewriter.create(loc, LLVM::ICmpPredicate::eq, sizeAny1, sizeAny2); - auto ptrCmpResult = rewriter.create(loc, LLVM::ICmpPredicate::eq, sizeAny1, sizeAny2); + auto dataPtr1 = al.getDataPtrOfAny(op1); + auto dataPtr2 = al.getDataPtrOfAny(op2); - auto dataPtr1 = al.getDataPtrOfAny(transformed.getOp1()); - auto dataPtr2 = al.getDataPtrOfAny(transformed.getOp2()); - - auto result = clh.conditionalExpressionLowering( - loc, th.getLLVMBoolType(), ptrCmpResult, + return clh.conditionalExpressionLowering( + loc, th.getLLVMBoolType(), sizesEqual, [&](OpBuilder &builder, Location loc) { auto const0 = clh.createI32ConstantOf(0); // sizeAny1 equals sizeAny2 @@ -785,7 +773,7 @@ class AnyCompareOpLowering : public TsLlvmPattern // else compare body mlir::Value bodyCmpResult; - switch ((SyntaxKind)op.getCode()) + switch (code) { case SyntaxKind::EqualsEqualsToken: case SyntaxKind::EqualsEqualsEqualsToken: @@ -821,8 +809,198 @@ class AnyCompareOpLowering : public TsLlvmPattern return bodyCmpResult; }, [&](OpBuilder &builder, Location loc) { - return ptrCmpResult; + // 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); + } + + return (mlir::Value)sizesEqual; }); + } + + // 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 + // reached for ==/!= once the runtime type tags are known to differ. + mlir::Value coerceAndCompareMixedKinds(mlir::Value op1, mlir::Value op2, mlir::Value tag1, mlir::Value tag2, + SyntaxKind code, mlir::Location loc, AnyLogic &al, CastLogicHelper &castLogic, + CodeLogicHelper &clh, TypeHelper &th, LLVMCodeHelper &ch, TypeConverterHelper &tch, + ConversionPatternRewriter &rewriter) const + { + auto isEquals = code == SyntaxKind::EqualsEqualsToken; + + auto numberTy = mlir_ts::NumberType::get(rewriter.getContext()); + 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" must check the + // realistic set of concrete tags a `number`-inferred literal can carry, not + // just the literal string "number". + auto isNumericTag = [&](mlir::Value tag) { + mlir::Value result = isTag(tag, "number"); + for (auto name : {"s32", "s64", "u32", "u64", "i32", "i64", "f32", "f64"}) + { + result = rewriter.create(loc, result, isTag(tag, name)); + } + return result; + }; + + // 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. + 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)); }); + }); + }; + + auto compareAsNumber = [&](mlir::Value numVal1, mlir::Value numVal2) { + return (mlir::Value)rewriter.create( + loc, isEquals ? LLVM::FCmpPredicate::oeq : LLVM::FCmpPredicate::une, numVal1, numVal2); + }; + + auto compareAsString = [&](mlir::Value strVal1, mlir::Value strVal2) { + auto strcmpFuncOp = ch.getOrInsertFunction("strcmp", th.getFunctionType(th.getI32Type(), {th.getPtrType(), th.getPtrType()})); + auto cmp = rewriter.create(loc, strcmpFuncOp, ValueRange{strVal1, strVal2}); + auto const0 = clh.createI32ConstantOf(0); + return (mlir::Value)rewriter.create( + loc, isEquals ? LLVM::ICmpPredicate::eq : LLVM::ICmpPredicate::ne, cmp.getResult(), const0); + }; + + // number <-> string: coerce the string side with parseFloat + auto numberStringCase = [&](mlir::Value numberSideAny, mlir::Value numberTag, mlir::Value stringSideAny) { + auto numVal = unboxNumericAsF64(numberSideAny, numberTag); + auto strVal = al.UnboxAny(stringSideAny, th.getPtrType()); + auto coercedNum = castLogic.cast(strVal, stringTy, numberTy); + return compareAsNumber(numVal, coercedNum); + }; + + // boolean <-> number: coerce the boolean side to 0.0/1.0 + auto booleanNumberCase = [&](mlir::Value booleanSideAny, mlir::Value numberSideAny, mlir::Value numberTag) { + auto boolVal = al.UnboxAny(booleanSideAny, th.getLLVMBoolType()); + auto numVal = unboxNumericAsF64(numberSideAny, numberTag); + auto coercedNum = castLogic.cast(boolVal, booleanTy, numberTy); + return compareAsNumber(coercedNum, numVal); + }; + + // boolean <-> string: coerce the boolean side to "true"/"false" + auto booleanStringCase = [&](mlir::Value booleanSideAny, mlir::Value stringSideAny) { + auto boolVal = al.UnboxAny(booleanSideAny, th.getLLVMBoolType()); + auto strVal = al.UnboxAny(stringSideAny, th.getPtrType()); + auto coercedStr = castLogic.cast(boolVal, booleanTy, stringTy); + return compareAsString(coercedStr, strVal); + }; + + auto tag1IsNumber = isNumericTag(tag1); + auto tag1IsString = isTag(tag1, "string"); + auto tag1IsBoolean = isTag(tag1, "boolean"); + auto tag2IsNumber = isNumericTag(tag2); + auto tag2IsString = isTag(tag2, "string"); + auto tag2IsBoolean = isTag(tag2, "boolean"); + + auto op1IsNumberOp2IsString = rewriter.create(loc, tag1IsNumber, tag2IsString); + auto op1IsStringOp2IsNumber = rewriter.create(loc, tag1IsString, tag2IsNumber); + auto op1IsBooleanOp2IsNumber = rewriter.create(loc, tag1IsBoolean, tag2IsNumber); + auto op1IsNumberOp2IsBoolean = rewriter.create(loc, tag1IsNumber, tag2IsBoolean); + auto op1IsBooleanOp2IsString = rewriter.create(loc, tag1IsBoolean, tag2IsString); + auto op1IsStringOp2IsBoolean = rewriter.create(loc, tag1IsString, tag2IsBoolean); + + return clh.conditionalExpressionLowering( + loc, th.getLLVMBoolType(), op1IsNumberOp2IsString, + [&](OpBuilder &, Location loc) { return numberStringCase(op1, tag1, op2); }, + [&](OpBuilder &builder, Location loc) { + return clh.conditionalExpressionLowering( + loc, th.getLLVMBoolType(), op1IsStringOp2IsNumber, + [&](OpBuilder &, Location loc) { return numberStringCase(op2, tag2, op1); }, + [&](OpBuilder &builder, Location loc) { + return clh.conditionalExpressionLowering( + loc, th.getLLVMBoolType(), op1IsBooleanOp2IsNumber, + [&](OpBuilder &, Location loc) { return booleanNumberCase(op1, op2, tag2); }, + [&](OpBuilder &builder, Location loc) { + return clh.conditionalExpressionLowering( + loc, th.getLLVMBoolType(), op1IsNumberOp2IsBoolean, + [&](OpBuilder &, Location loc) { return booleanNumberCase(op2, op1, tag1); }, + [&](OpBuilder &builder, Location loc) { + return clh.conditionalExpressionLowering( + loc, th.getLLVMBoolType(), op1IsBooleanOp2IsString, + [&](OpBuilder &, Location loc) { return booleanStringCase(op1, op2); }, + [&](OpBuilder &builder, Location loc) { + return clh.conditionalExpressionLowering( + loc, th.getLLVMBoolType(), op1IsStringOp2IsBoolean, + [&](OpBuilder &, Location loc) { return booleanStringCase(op2, op1); }, + [&](OpBuilder &builder, Location loc) { + // no known coercion: kinds differ and are unequal + return (mlir::Value)clh.createI1ConstantOf(!isEquals); + }); + }); + }); + }); + }); + }); + } + + LogicalResult matchAndRewrite(mlir_ts::AnyCompareOp op, Adaptor transformed, + ConversionPatternRewriter &rewriter) const final + { + TypeHelper th(rewriter); + CodeLogicHelper clh(op, rewriter); + LLVMCodeHelper ch(op, rewriter, getTypeConverter(), tsLlvmContext->compileOptions); + TypeConverterHelper tch(getTypeConverter()); + LLVMTypeConverterHelper llvmtch(static_cast(getTypeConverter())); + CastLogicHelper castLogic(op, rewriter, tch, tsLlvmContext->compileOptions); + + auto loc = op->getLoc(); + auto code = (SyntaxKind)op.getCode(); + + AnyLogic al(op, rewriter, tch, loc, tsLlvmContext->compileOptions); + + 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 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 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); }); rewriter.replaceOp(op, result); @@ -3039,6 +3217,16 @@ struct LogicalBinaryOpLowering : public TsLlvmPattern auto opType1 = logicalBinaryOp.getOperand1().getType(); auto opType2 = logicalBinaryOp.getOperand2().getType(); + // mlir_ts::BooleanType lowers to a signless i1, so isUnsignedInteger() (which + // only recognizes mlir::IntegerType's unsigned flavor) is false for it, and + // ordering comparisons fell to the signed predicate -- where i1 `true` (bit + // pattern 1) reads as -1, making `true > false` compare as `-1 > 0` (false). + // Booleans compare as unsigned 0/1, so treat them as such here explicitly. + auto isUnsignedOrBoolean = [](mlir::Type type) { + return type.isUnsignedInteger() || isa(type); + }; + auto useUnsignedCompare = isUnsignedOrBoolean(opType1) && isUnsignedOrBoolean(opType2); + // int and float mlir::Value value; switch (op) @@ -3058,7 +3246,7 @@ struct LogicalBinaryOpLowering : public TsLlvmPattern break; case SyntaxKind::GreaterThanToken: - if (opType1.isUnsignedInteger() && opType2.isUnsignedInteger()) + if (useUnsignedCompare) { value = logicOp( logicalBinaryOp, op, op1, opType1, op2, opType2, rewriter); @@ -3070,7 +3258,7 @@ struct LogicalBinaryOpLowering : public TsLlvmPattern } break; case SyntaxKind::GreaterThanEqualsToken: - if (opType1.isUnsignedInteger() && opType2.isUnsignedInteger()) + if (useUnsignedCompare) { value = logicOp( logicalBinaryOp, op, op1, opType1, op2, opType2, rewriter); @@ -3083,7 +3271,7 @@ struct LogicalBinaryOpLowering : public TsLlvmPattern break; case SyntaxKind::LessThanToken: - if (opType1.isUnsignedInteger() && opType2.isUnsignedInteger()) + if (useUnsignedCompare) { value = logicOp( logicalBinaryOp, op, op1, opType1, op2, opType2, rewriter); @@ -3096,7 +3284,7 @@ struct LogicalBinaryOpLowering : public TsLlvmPattern break; case SyntaxKind::LessThanEqualsToken: - if (opType1.isUnsignedInteger() && opType2.isUnsignedInteger()) + if (useUnsignedCompare) { value = logicOp( logicalBinaryOp, op, op1, opType1, op2, opType2, rewriter); diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index 283035c45..d1fe838cf 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -3647,7 +3647,15 @@ class MLIRGenImpl NodeFactory nf(NodeFactoryFlags::None); - if (auto hasLength = evaluateProperty(binaryExpressionAST->right, LENGTH_FIELD_NAME, genContext)) + // the length-based numeric-index rewrite below only makes sense when the left + // side is actually a number (e.g. `i in arr`); a string-literal left side (e.g. + // `"length" in arr` or `"push" in arr`) must fall through to the general + // field-lookup path further down instead, otherwise we'd cast a string to an + // index/int type and generate invalid IR. + auto leftIsStringLiteral = binaryExpressionAST->left == SyntaxKind::StringLiteral + || binaryExpressionAST->left == SyntaxKind::NoSubstitutionTemplateLiteral; + + if (!leftIsStringLiteral && evaluateProperty(binaryExpressionAST->right, LENGTH_FIELD_NAME, genContext)) { auto cond1 = nf.createBinaryExpression( binaryExpressionAST->left, nf.createToken(SyntaxKind::LessThanToken), @@ -4600,6 +4608,37 @@ class MLIRGenImpl return mlir::IntegerType::get(builder.getContext(), width, mlir::IntegerType::Signed); } + // JS/TS `===`/`!==` never coerce: operands of clearly different primitive kinds + // (boolean vs number, boolean vs string, string vs number) are simply unequal, + // full stop. Everything downstream of this function (adjustTypesForBinaryOp's + // numeric-sync loop, LogicalBinaryOpLowering) is shared with `==`/`!=`, which DO + // coerce, so without this check `1 === true` was being widened to a common + // numeric type just like `1 == true` and wrongly evaluated to `true`. + bool isDefinitelyMismatchedForStrictEquals(mlir::Type leftType, mlir::Type rightType) + { + auto isBoolean = [](mlir::Type type) { return isa(type); }; + auto isString = [](mlir::Type type) { return isa(type); }; + auto isNumeric = [](mlir::Type type) { return type.isIntOrIndexOrFloat() && !isa(type); }; + + auto leftIsBoolean = isBoolean(leftType); + auto rightIsBoolean = isBoolean(rightType); + auto leftIsString = isString(leftType); + auto rightIsString = isString(rightType); + auto leftIsNumeric = isNumeric(leftType); + auto rightIsNumeric = isNumeric(rightType); + + // only fire when BOTH sides are known, unambiguous primitives (not any/union/ + // object/etc., which may still need the general coercion/toPrimitive machinery) + auto leftIsKnownPrimitive = leftIsBoolean || leftIsString || leftIsNumeric; + auto rightIsKnownPrimitive = rightIsBoolean || rightIsString || rightIsNumeric; + if (!leftIsKnownPrimitive || !rightIsKnownPrimitive) + { + return false; + } + + return (leftIsBoolean != rightIsBoolean) || (leftIsString != rightIsString) || (leftIsNumeric != rightIsNumeric); + } + // TODO: review it, seems like big hack mlir::LogicalResult adjustTypesForBinaryOp(mlir::Location location, SyntaxKind opCode, mlir::Value &leftExpressionValue, mlir::Value &rightExpressionValue, const GenContext &genContext) @@ -4609,6 +4648,15 @@ class MLIRGenImpl return mlir::success(); } + if ((opCode == SyntaxKind::EqualsEqualsEqualsToken || opCode == SyntaxKind::ExclamationEqualsEqualsToken) + && isDefinitelyMismatchedForStrictEquals(leftExpressionValue.getType(), rightExpressionValue.getType())) + { + auto result = opCode == SyntaxKind::ExclamationEqualsEqualsToken; + leftExpressionValue = builder.create(location, getBooleanType(), builder.getBoolAttr(result)); + rightExpressionValue = builder.create(location, getBooleanType(), builder.getBoolAttr(true)); + return mlir::success(); + } + if (MLIRTypeCore::canHaveToPrimitiveMethod(leftExpressionValue.getType()) && evaluateProperty(location, leftExpressionValue, SYMBOL_TO_PRIMITIVE, genContext) && !isa(rightExpressionValue.getType()) @@ -4671,8 +4719,64 @@ class MLIRGenImpl } break; - case SyntaxKind::AsteriskToken: + case SyntaxKind::PlusToken: + { + // this is exactly the untyped default: case below (left/right type sync, + // string-preferring) -- PlusToken used to fall through to it unconditionally. + // Preserved as-is so string concat (`"fo" + 1`) and ordinary numeric-literal + // widening (`numberParam + 1`) keep working exactly like before. + auto leftType = leftExpressionValue.getType(); + if (isa(rightExpressionValue.getType())) + { + leftType = rightExpressionValue.getType(); + if (leftType != leftExpressionValue.getType()) + { + CAST(leftExpressionValue, location, leftType, leftExpressionValue, genContext); + } + } + + auto rightType = rightExpressionValue.getType(); + if (leftType != rightType) + { + CAST(rightExpressionValue, location, leftType, rightExpressionValue, genContext); + } + + // additionally: when neither side is a string (so this isn't concat) and + // both sides already had the SAME boolean type, the sync above was a no-op + // (leftType == rightType already), so booleans reached + // ArithmeticBinaryOpLowering as raw i1 and wrapped (`true + true` -> false + // instead of 2). Widen them to number in that case. + if (!isa(leftExpressionValue.getType()) && !isa(rightExpressionValue.getType())) + { + if (isa(leftExpressionValue.getType())) + { + CAST(leftExpressionValue, location, getNumberType(), leftExpressionValue, genContext); + } + + if (isa(rightExpressionValue.getType())) + { + CAST(rightExpressionValue, location, getNumberType(), rightExpressionValue, genContext); + } + } + + break; + } case SyntaxKind::MinusToken: + // unlike PlusToken, MinusToken never does string concat, so it's safe to + // widen booleans here and then fall through to the same cross-type sync + // used by the other arithmetic/comparison operators below (e.g. `any - number`). + if (isa(leftExpressionValue.getType())) + { + CAST(leftExpressionValue, location, getNumberType(), leftExpressionValue, genContext); + } + + if (isa(rightExpressionValue.getType())) + { + CAST(rightExpressionValue, location, getNumberType(), rightExpressionValue, genContext); + } + + [[fallthrough]]; + case SyntaxKind::AsteriskToken: case SyntaxKind::EqualsEqualsToken: case SyntaxKind::EqualsEqualsEqualsToken: case SyntaxKind::ExclamationEqualsToken: diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 7ceb86d35..1a5599f85 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -121,6 +121,8 @@ add_test(NAME test-compile-00-enums-multiple COMMAND test-runner "${PROJECT_SOUR add_test(NAME test-compile-01-enums COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/01enum.ts") add_test(NAME test-compile-00-numbers COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00numbers.ts") add_test(NAME test-compile-00-equals COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00equals.ts") +add_test(NAME test-compile-00-bool-arith-ops COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00bool_arith_ops.ts") +add_test(NAME test-compile-00-mixed-type-ops COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00mixed_type_ops.ts") add_test(NAME test-compile-00-funcs COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00funcs.ts") add_test(NAME test-compile-00-funcs-capture COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00funcs_capture.ts") add_test(NAME test-compile-00-funcs-vararg COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00funcs_vararg.ts") @@ -216,6 +218,8 @@ add_test(NAME test-compile-02-sizeof COMMAND test-runner "${PROJECT_SOURCE_DIR}/ add_test(NAME test-compile-00-new-delete COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00new_delete.ts") add_test(NAME test-compile-00-void COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00void.ts") add_test(NAME test-compile-00-in COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00in.ts") +add_test(NAME test-compile-00-in-method-names COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00in_method_names.ts") +add_test(NAME test-compile-00-class-structural-extends COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_structural_extends.ts") add_test(NAME test-compile-00-instanceof COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00instanceof.ts") add_test(NAME test-compile-00-class COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class.ts") add_test(NAME test-compile-00-class-new COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_new.ts") @@ -461,6 +465,8 @@ add_test(NAME test-jit-00-enums-multiple COMMAND test-runner -jit "${PROJECT_SOU add_test(NAME test-jit-01-enums COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/01enum.ts") add_test(NAME test-jit-00-numbers COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00numbers.ts") add_test(NAME test-jit-00-equals COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00equals.ts") +add_test(NAME test-jit-00-bool-arith-ops COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00bool_arith_ops.ts") +add_test(NAME test-jit-00-mixed-type-ops COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00mixed_type_ops.ts") add_test(NAME test-jit-00-funcs COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00funcs.ts") add_test(NAME test-jit-00-funcs-capture COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00funcs_capture.ts") add_test(NAME test-jit-00-funcs-vararg COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00funcs_vararg.ts") @@ -555,6 +561,8 @@ add_test(NAME test-jit-02-sizeof COMMAND test-runner -jit "${PROJECT_SOURCE_DIR} add_test(NAME test-jit-00-new-delete COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00new_delete.ts") add_test(NAME test-jit-00-void COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00void.ts") add_test(NAME test-jit-00-in COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00in.ts") +add_test(NAME test-jit-00-in-method-names COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00in_method_names.ts") +add_test(NAME test-jit-00-class-structural-extends COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_structural_extends.ts") add_test(NAME test-jit-00-instanceof COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00instanceof.ts") add_test(NAME test-jit-00-class COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class.ts") add_test(NAME test-jit-00-class-new COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_new.ts") diff --git a/tslang/test/tester/tests/00bool_arith_ops.ts b/tslang/test/tester/tests/00bool_arith_ops.ts new file mode 100644 index 000000000..b56b248d6 --- /dev/null +++ b/tslang/test/tester/tests/00bool_arith_ops.ts @@ -0,0 +1,42 @@ +// regression test: boolean operands in arithmetic/comparison binary ops must coerce to +// number the same way JS/TS does (true -> 1, false -> 0), not operate on the raw i1 +// representation. +// +// bugs found and fixed: +// - `+`/`-` had no case forcing numeric coercion when both operands were already the +// same type (boolean), unlike `/`/`%`/`**` which always force getNumberType(). So +// `true + true` computed 1+1 as a wrapping i1 add (result: false) instead of 2, and +// `true - false` similarly wrapped instead of giving 1. +// - ordering comparisons (`> >= < <=`) picked their icmp predicate via +// Type::isUnsignedInteger(), which is false for the custom BooleanType (a signless +// i1), so they fell to the *signed* predicate -- where i1 `true` (bit pattern 1) +// reads as -1, making `true > false` compare as `-1 > 0` (false) instead of true. + +function main() { + assert(true + true == 2); + assert(true + false == 1); + assert(false + false == 0); + + assert(true - false == 1); + assert(false - true == -1); + assert(true - true == 0); + + assert(true * 2 == 2); + assert(false * 2 == 0); + + assert(true > false); + assert(!(false > true)); + assert(false < true); + assert(!(true < false)); + assert(true >= true); + assert(true <= true); + assert(true >= false); + assert(!(false >= true)); + + assert(true == true); + assert(true != false); + assert(true === true); + assert(true !== false); + + print("done."); +} diff --git a/tslang/test/tester/tests/00class_structural_extends.ts b/tslang/test/tester/tests/00class_structural_extends.ts new file mode 100644 index 000000000..cc2ddc353 --- /dev/null +++ b/tslang/test/tester/tests/00class_structural_extends.ts @@ -0,0 +1,29 @@ +// regression test for #238: getFieldTypeByFieldName's ClassType branch looked up +// class field/method info in the *interface* registry (getInterfaceInfoByFullName) +// instead of the class registry (getClassInfoByFullName). Since classes and +// interfaces are registered in separate tables, this lookup silently failed for +// every field/method on a real class -- breaking both the `in` operator and +// structural generic constraints (`T extends { length: number }`) matched against +// a class instance. + +class Box { + length: number; + + constructor(length: number) { + this.length = length; + } +} + +function getLength(x: T): number { + return x.length; +} + +function main() { + const b = new Box(42); + + assert("length" in b); + + assert(getLength(b) == 42); + + print("done."); +} diff --git a/tslang/test/tester/tests/00in_method_names.ts b/tslang/test/tester/tests/00in_method_names.ts new file mode 100644 index 000000000..08f772b9e --- /dev/null +++ b/tslang/test/tester/tests/00in_method_names.ts @@ -0,0 +1,49 @@ +// regression test for #238: getFieldTypeByFieldName used to hit llvm_unreachable +// when the `in` operator (or structural type checks that reuse the same lookup) +// was asked about a name that is a method/extension-function name rather than a +// real data field -- e.g. "push" on an array, "fromCharCode"/"charAt" on a string, +// or a method name on a class instance. All five branches (Array, ConstArray, +// String, Interface, Class) must return "not found" instead of aborting. + +class Point { + x: number; + y: number; + + constructor(x: number, y: number) { + this.x = x; + this.y = y; + } + + dist(): number { + return this.x * this.x + this.y * this.y; + } +} + +function main() { + const trees = ["redwood", "bay", "cedar"]; + + // real data fields/indices are found + assert("length" in trees); + assert(0 in trees); + + // method/extension names are not data fields, must resolve to false, not abort + assert(!("push" in trees)); + assert(!("pop" in trees)); + assert(!("entries" in trees)); + assert(!("madeUpName" in trees)); + + const s = "hello"; + + assert("length" in s); + assert(!("charAt" in s)); + assert(!("fromCharCode" in s)); + + const p = new Point(3, 4); + + assert("x" in p); + assert("y" in p); + assert("dist" in p); + assert(!("madeUpField" in p)); + + print("done."); +} diff --git a/tslang/test/tester/tests/00mixed_type_ops.ts b/tslang/test/tester/tests/00mixed_type_ops.ts new file mode 100644 index 000000000..aba5fcde4 --- /dev/null +++ b/tslang/test/tester/tests/00mixed_type_ops.ts @@ -0,0 +1,103 @@ +// regression / coverage test: binary operations between operands of DIFFERENT static +// types (string+number, number+string, string+boolean, boolean+string, number+enum, +// any-typed mixed comparisons). These exercise the coercion rules in +// adjustTypesForBinaryOp / cast() (MLIRGenImpl.h, MLIRGenCast.cpp) that aren't covered +// by 00bool_arith_ops.ts (bool+number), 00strings.ts (string+number concat/compare) or +// arithmeticOperatorWithEnum.ts (enum+number). + +function stringNumber() { + // string + number -> string concat, number coerced via toString + assert("val=" + 42 == "val=42"); + assert(42 + "=val" == "42=val"); + assert("pi=" + 3.5 == "pi=3.5"); + assert(-1 + "x" == "-1x"); + + // compound assignment across types + let s = "n="; + s += 7; + assert(s == "n=7"); + + // relational compare: number coerced to string, then lexicographic compare + assert("9" + 0 > "8" + 9); // "90" > "89" + assert(!("2" + 0 < "1" + 9)); // "20" < "19" is false +} + +function stringBoolean() { + // string + boolean -> string concat, boolean coerced via toString + assert("flag=" + true == "flag=true"); + assert("flag=" + false == "flag=false"); + assert(true + "!" == "true!"); + assert(false + "!" == "false!"); + + let s = "b="; + s += true; + assert(s == "b=true"); +} + +function numberBooleanCompare() { + // number vs boolean relational/equality (boolean coerces to number: true->1, false->0) + assert(1 == true); + assert(0 == false); + assert(1 != false); + assert(2 > true); + assert(!(0 > false)); + assert(true >= 1); + assert(false <= 0); + + // strict equality does NOT coerce across types + assert(!(1 === true)); + assert(!(0 === false)); +} + +function enumNumber() { + enum Level { Low, Medium, High } + + let l = Level.Medium; + assert(l == 1); + assert(l + 1 == 2); + assert(l * 2 == 2); + assert(l < Level.High); + assert("level=" + l == "level=1"); +} + +function anyMixed() { + let a: any = "abc"; + let b: any = "abc"; + assert(a == b); + assert(a === b); + + // loose equality across differing `any` payload kinds coerces like JS: + // number<->string, boolean<->number, boolean<->string + a = 5; + b = "5"; + assert(a == b); + assert(b == a); + assert(!(a === b)); // strict equality checks type too, this direction works + + a = true; + b = 1; + assert(a == b); + assert(b == a); + + a = false; + b = 0; + assert(a == b); + + a = true; + b = "true"; + assert(a == b); + + a = 5; + b = "6"; + assert(a != b); +} + +function main() { + stringNumber(); + stringBoolean(); + numberBooleanCompare(); + enumNumber(); + anyMixed(); + + print("done."); +} diff --git a/tslang/test/tester/tests/01tuple.ts b/tslang/test/tester/tests/01tuple.ts index f92358b6d..787abdafe 100644 --- a/tslang/test/tester/tests/01tuple.ts +++ b/tslang/test/tester/tests/01tuple.ts @@ -37,7 +37,7 @@ function main() { const obj8 : IObj = { field1: 10 }; print(obj8.field1); - assert(obj8.field1 === 10); + assert(obj8.field1 === "10"); print("done."); }