diff --git a/tslang/lib/TypeScript/MLIRGenExpressions.cpp b/tslang/lib/TypeScript/MLIRGenExpressions.cpp index 6c90a8c14..dcde6624d 100644 --- a/tslang/lib/TypeScript/MLIRGenExpressions.cpp +++ b/tslang/lib/TypeScript/MLIRGenExpressions.cpp @@ -824,7 +824,19 @@ namespace mlirgen } } - auto resultReturnUnions = + return mlirGenBinaryValues(location, opCode, leftExpressionValue, rightExpressionValue, genContext, saveResult); + } + + ValueOrLogicalResult MLIRGenImpl::mlirGenBinaryValues(mlir::Location location, SyntaxKind opCode, mlir::Value leftExpressionValue, + mlir::Value rightExpressionValue, const GenContext &genContext, + bool saveResult) + { + if (auto folded = foldNullishCompare(location, opCode, leftExpressionValue, rightExpressionValue)) + { + return *folded; + } + + auto resultReturnUnions = binaryOpLogicForUnions(location, opCode, leftExpressionValue, rightExpressionValue, genContext); if (resultReturnUnions.value || resultReturnUnions.failed()) { diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index fd449c7d0..05cba451b 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -3055,6 +3055,11 @@ class MLIRGenImpl auto doneIdent = nf.createIdentifier(S("done")); NodeArray retObjectProperties; + if (stop && expr == SyntaxKind::Identifier && expr.as()->escapedText == S(UNDEFINED_NAME)) + { + expr->internalFlags |= InternalFlags::GeneratorDoneValue; + } + auto valueProp = nf.createPropertyAssignment(valueIdent, expr); retObjectProperties.push_back(valueProp); @@ -4757,6 +4762,122 @@ class MLIRGenImpl return resultFirst; } + // a type whose values are never null or undefined: a value type - a number, a boolean, a tuple - or a union + // of them only, such as `0 | 1 | 42` + bool isNeverNullish(mlir::Type type) + { + if (auto unionType = dyn_cast(type)) + { + return llvm::all_of(unionType.getTypes(), [&](mlir::Type subType) { return isNeverNullish(subType); }); + } + + return mth.isValueType(type); + } + + // the type without null and undefined: what is left once `?? b` did not take `b` + mlir::Type getNonNullishType(mlir::Type type) + { + if (auto optType = dyn_cast(type)) + { + return getNonNullishType(optType.getElementType()); + } + + if (auto unionType = dyn_cast(type)) + { + mlir::SmallVector types; + for (auto subType : unionType.getTypes()) + { + if (!isa(subType) && !isa(subType)) + { + types.push_back(subType); + } + } + + if (types.size() > 0 && types.size() < unionType.getTypes().size()) + { + return getUnionType(types); + } + } + + return type; + } + + // whether `value` is null or undefined, by its type: an optional only when it has no value or its value is, + // a value type never; anything else - a union, any, a reference - is compared with null and with undefined + mlir::Value mlirGenIsNullish(mlir::Location location, mlir::Value value, const GenContext &genContext) + { + auto toBoolean = [&](mlir::Value boolValue) -> mlir::Value { + if (boolValue && boolValue.getType() != getBooleanType()) + { + auto result = cast(location, getBooleanType(), boolValue, genContext); + return result.failed_or_no_value() ? mlir::Value() : V(result); + } + + return boolValue; + }; + + auto type = value.getType(); + if (isa(type) || isa(type)) + { + return toBoolean(V(mlirGenBooleanValue(location, true))); + } + + if (auto optType = dyn_cast(type)) + { + auto hasValue = builder.create(location, getBooleanType(), value); + auto elementType = optType.getElementType(); + + // the value is tested only when there is one + auto ifOp = builder.create(location, mlir::TypeRange{getBooleanType()}, hasValue, true); + builder.setInsertionPointToStart(&ifOp.getThenRegion().front()); + mlir::Value valueIsNullish; + if (isNeverNullish(elementType)) + { + valueIsNullish = toBoolean(V(mlirGenBooleanValue(location, false))); + } + else + { + auto innerValue = builder.create(location, elementType, value); + valueIsNullish = mlirGenIsNullish(location, innerValue, genContext); + if (!valueIsNullish) + { + return mlir::Value(); + } + } + + builder.create(location, mlir::ValueRange{valueIsNullish}); + + builder.setInsertionPointToStart(&ifOp.getElseRegion().front()); + builder.create(location, mlir::ValueRange{toBoolean(V(mlirGenBooleanValue(location, true)))}); + + builder.setInsertionPointAfter(ifOp); + return ifOp.getResult(0); + } + + if (isNeverNullish(type)) + { + return toBoolean(V(mlirGenBooleanValue(location, false))); + } + + auto isNull = mlirGenBinaryValues(location, SyntaxKind::EqualsEqualsEqualsToken, value, + builder.create(location, getNullType()), genContext); + if (isNull.failed_or_no_value()) + { + return mlir::Value(); + } + + auto isUndefined = mlirGenBinaryValues(location, SyntaxKind::EqualsEqualsEqualsToken, value, + builder.create(location, getUndefinedType()), genContext); + if (isUndefined.failed_or_no_value()) + { + return mlir::Value(); + } + + return builder.create( + location, getBooleanType(), builder.getI32IntegerAttr((int)SyntaxKind::BarToken), toBoolean(V(isNull)), + toBoolean(V(isUndefined))); + } + ValueOrLogicalResult mlirGenQuestionQuestionLogic(BinaryExpression binaryExpressionAST, bool saveResult, const GenContext &genContext) { @@ -4770,42 +4891,22 @@ class MLIRGenImpl EXIT_IF_FAILED_OR_NO_VALUE(result) auto leftExpressionValue = V(result); + // `a ?? b` is `b` only when `a` is null or undefined, so its type is `a`'s without them, or `b`'s + auto leftNonNullishType = getNonNullishType(leftExpressionValue.getType()); + // `a` that is only null or undefined - `x` narrowed so by `x = null` - always gives `b` + auto leftAlwaysNullish = + isa(leftNonNullishType) || isa(leftNonNullishType); auto resultWhenFalseType = evaluate(rightExpression, genContext); - auto defaultUnionType = getUnionType(location, leftExpressionValue.getType(), resultWhenFalseType); + auto defaultUnionType = getUnionType(location, leftNonNullishType, resultWhenFalseType); auto merged = false; - auto resultType = mth.findBaseType(resultWhenFalseType, leftExpressionValue.getType(), merged, defaultUnionType); + auto resultType = leftAlwaysNullish + ? resultWhenFalseType + : mth.findBaseType(resultWhenFalseType, leftNonNullishType, merged, defaultUnionType); - // extarct value from optional type - auto actualLeftValue = leftExpressionValue; - auto hasOptional = false; - if (auto optType = dyn_cast(actualLeftValue.getType())) + auto ifCond = mlirGenIsNullish(location, leftExpressionValue, genContext); + if (!ifCond) { - hasOptional = true; - CAST(actualLeftValue, location, optType.getElementType(), leftExpressionValue, genContext); - } - - CAST_A(opaqueValueOfLeftValue, location, getOpaqueType(), actualLeftValue, genContext); - - auto nullVal = builder.create(location, getNullType()); - - auto compareToNull = builder.create( - location, getBooleanType(), builder.getI32IntegerAttr((int)SyntaxKind::EqualsEqualsEqualsToken), opaqueValueOfLeftValue, - nullVal); - - mlir::Value ifCond = compareToNull; - if (hasOptional) - { - CAST_A(hasValue, location, getBooleanType(), leftExpressionValue, genContext); - CAST_A(isFalse, location, getBooleanType(), mlirGenBooleanValue(location, false), genContext); - auto compareToFalse = builder.create( - location, getBooleanType(), builder.getI32IntegerAttr((int)SyntaxKind::EqualsEqualsEqualsToken), isFalse, - hasValue); - - auto orOp = builder.create( - location, getBooleanType(), builder.getI32IntegerAttr((int)SyntaxKind::BarToken), compareToFalse, - compareToNull); - - ifCond = orOp; + return mlir::failure(); } auto ifOp = builder.create(location, mlir::TypeRange{resultType}, ifCond, true); @@ -4829,9 +4930,34 @@ class MLIRGenImpl builder.create(location, mlir::ValueRange{resultTrue}); builder.setInsertionPointToStart(&ifOp.getElseRegion().front()); - auto resultFalse = leftExpressionValue; + if (leftAlwaysNullish) + { + // never taken + builder.create(location, mlir::ValueRange{builder.create(location, resultType)}); + builder.setInsertionPointAfter(ifOp); + auto ifResult = ifOp.getResults().front(); + if (saveResult) + { + return mlirGenSaveLogicOneItem(location, leftExpressionValue, ifResult, genContext); + } - if (mlir::failed(instantiateGenericsForBinaryOp(location, leftExpressionValue, resultFalse, genContext))) + return ifResult; + } + + // here `a` is neither null nor undefined: take it as such + mlir::Value resultFalse = leftExpressionValue; + if (auto optType = dyn_cast(resultFalse.getType())) + { + resultFalse = builder.create(location, optType.getElementType(), resultFalse); + } + + if (isa(resultFalse.getType()) && resultFalse.getType() != leftNonNullishType) + { + resultFalse = castToNarrowedType(location, resultFalse, leftNonNullishType, genContext); + VALIDATE(resultFalse, location) + } + + if (mlir::failed(instantiateGenericsForBinaryOp(location, leftExpressionValue, resultFalse, genContext))) { return mlir::failure(); } @@ -8520,6 +8646,79 @@ class MLIRGenImpl return mlirGenBooleanValue(location, isEquals ? namesMatch : !namesMatch); } + // A compare with null or undefined whose answer the type gives: a value type - a number, a boolean, a tuple - + // is neither, and an optional of one holds undefined or a value, never null. Lowered as they were, `0 === null` + // compared the number with null read as 0, and was true. + std::optional foldNullishCompare(mlir::Location location, SyntaxKind opCode, + mlir::Value leftValue, mlir::Value rightValue) + { + auto isEquals = opCode == SyntaxKind::EqualsEqualsToken || opCode == SyntaxKind::EqualsEqualsEqualsToken; + auto isNotEquals = opCode == SyntaxKind::ExclamationEqualsToken || opCode == SyntaxKind::ExclamationEqualsEqualsToken; + if (!isEquals && !isNotEquals) + { + return std::nullopt; + } + + auto isLoose = opCode == SyntaxKind::EqualsEqualsToken || opCode == SyntaxKind::ExclamationEqualsToken; + auto isNullish = [](mlir::Type type) { return isa(type) || isa(type); }; + + mlir::Value otherValue; + mlir::Type nullishType; + if (isNullish(rightValue.getType()) && !isNullish(leftValue.getType())) + { + otherValue = leftValue; + nullishType = rightValue.getType(); + } + else if (isNullish(leftValue.getType()) && !isNullish(rightValue.getType())) + { + otherValue = rightValue; + nullishType = leftValue.getType(); + } + else + { + return std::nullopt; + } + + auto otherType = otherValue.getType(); + if (isNeverNullish(otherType)) + { + return mlirGenBooleanValue(location, isNotEquals); + } + + auto optType = dyn_cast(otherType); + if (!optType || !isNeverNullish(optType.getElementType())) + { + return std::nullopt; + } + + if (isa(nullishType)) + { + if (!isLoose) + { + return mlirGenBooleanValue(location, isNotEquals); + } + + // `== null` matches undefined too: the optional has no value + mlir::Value hasValue = builder.create(location, getBooleanType(), otherValue); + if (isNotEquals) + { + return hasValue; + } + + auto falseValue = builder.create(location, getBooleanType(), builder.getBoolAttr(false)); + return V(builder.create( + location, getBooleanType(), builder.getI32IntegerAttr((int)SyntaxKind::EqualsEqualsEqualsToken), hasValue, + falseValue)); + } + + // an optional compared with undefined is handled as it was + return std::nullopt; + } + + ValueOrLogicalResult mlirGenBinaryValues(mlir::Location location, SyntaxKind opCode, mlir::Value leftExpressionValue, + mlir::Value rightExpressionValue, const GenContext &genContext, + bool saveResult = false); + ValueOrLogicalResult mlirGen(TrueLiteral trueLiteral, const GenContext &genContext); ValueOrLogicalResult mlirGen(FalseLiteral falseLiteral, const GenContext &genContext); @@ -9557,6 +9756,16 @@ class MLIRGenImpl EXIT_IF_FAILED_OR_NO_VALUE(result) itemValue = V(result); + // a finished generator's value, of a value type - a number, a boolean, a tuple: its zero, as the cast + // of undefined to it lowers to (see UndefOpLowering), without that cast's warning in every generator + // of such a T. Other types keep the cast: to a string it is the text "undefined". + if (!!(propertyAssignment->initializer->internalFlags & InternalFlags::GeneratorDoneValue) + && receiverElementType && isa(itemValue.getType()) + && mth.isValueType(receiverElementType)) + { + itemValue = builder.create(location, receiverElementType); + } + // in case of Union type if (oli.receiverType && !receiverElementType) { diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index c77632fcc..aa0e701aa 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -397,6 +397,7 @@ tslang_add_test(NAME test-compile-00-optional-fields COMMAND test-runner "${PROJ tslang_add_test(NAME test-compile-00-generator-return COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00generator_return.ts") tslang_add_test(NAME test-compile-00-return-inference-undefined COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00return_inference_undefined.ts") tslang_add_test(NAME test-compile-00-return-falls-off-end COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00return_falls_off_end.ts") +tslang_add_test(NAME test-compile-00-nullish-values COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00nullish_values.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") @@ -877,6 +878,7 @@ tslang_add_test(NAME test-jit-00-optional-fields COMMAND test-runner -jit "${PRO tslang_add_test(NAME test-jit-00-generator-return COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00generator_return.ts") tslang_add_test(NAME test-jit-00-return-inference-undefined COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00return_inference_undefined.ts") tslang_add_test(NAME test-jit-00-return-falls-off-end COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00return_falls_off_end.ts") +tslang_add_test(NAME test-jit-00-nullish-values COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00nullish_values.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/00nullish_values.ts b/tslang/test/tester/tests/00nullish_values.ts new file mode 100644 index 000000000..4393801fc --- /dev/null +++ b/tslang/test/tester/tests/00nullish_values.ts @@ -0,0 +1,72 @@ +// `a ?? b` takes `b` only when `a` is null or undefined - not when it is 0 or "" - and its type is `a`'s +// without them; a number compared with null or undefined is not equal to it (null used to read as 0) +function orMinusOne(n?: number) { + return n ?? -1; +} + +function orDefault(s?: string) { + return s ?? "default"; +} + +function isNull(n: number) { + return n === null; +} + +function isNullOptional(n?: number) { + return n === null; +} + +function isNullishOptional(n?: number) { + return n == null; +} + +function isNotNullishOptional(n?: number) { + return n != null; +} + +function main() { + assert(orMinusOne() == -1, "no value"); + assert(orMinusOne(3) == 3, "value"); + assert(orMinusOne(0) == 0, "zero is a value"); + + const n = orMinusOne(0); + assert(typeof n == "number", "the result is a number"); + + assert(orDefault() == "default", "no string"); + assert(orDefault("a") == "a", "string"); + assert(orDefault("") == "", "empty string is a value"); + + let y: number | null = null; + assert((y ?? 8) == 8, "null"); + y = 0; + assert((y ?? 9) == 0, "zero of number | null"); + + let z: string | null | undefined = undefined; + assert((z ?? "u") == "u", "undefined of a union"); + z = null; + assert((z ?? "n") == "n", "null of a union"); + z = ""; + assert((z ?? "e") == "", "empty string of a union"); + + const five = 5; + assert((five ?? 3) == 5, "a number is never nullish"); + + let opt: number | undefined; + opt ??= 0; + assert(opt == 0, "??= assigns"); + opt ??= 7; + assert(opt == 0, "??= keeps zero"); + + let a: any = null; + assert((a ?? 4) == 4, "any null"); + + assert(!isNull(0), "0 === null"); + assert(!isNullOptional(0), "optional 0 === null"); + assert(!isNullOptional(), "optional undefined === null"); + assert(isNullishOptional(), "optional undefined == null"); + assert(!isNullishOptional(0), "optional 0 == null"); + assert(isNotNullishOptional(0), "optional 0 != null"); + assert(!isNotNullishOptional(), "optional undefined != null"); + + print("done."); +} diff --git a/tslang/ts-new-parser/enums.h b/tslang/ts-new-parser/enums.h index 8db18d967..f92036580 100644 --- a/tslang/ts-new-parser/enums.h +++ b/tslang/ts-new-parser/enums.h @@ -564,7 +564,10 @@ enum class InternalFlags : number // a `return` of a generator's own body: it finishes the generator (mlirGen(ReturnStatement)) GeneratorReturn = 1 << 12, // the next() a generator is rewritten into: it returns Iterator's {value: T, done} (processReturnType) - GeneratorNext = 1 << 13 + GeneratorNext = 1 << 13, + // the `undefined` of a finished generator's {value: undefined, done: true}: a `value` type that cannot hold + // undefined takes its zero (mlirGenObjectLiteralFields) + GeneratorDoneValue = 1 << 14 }; ENUM_OPS(InternalFlags)