diff --git a/tslang/lib/TypeScript/MLIRGenExpressions.cpp b/tslang/lib/TypeScript/MLIRGenExpressions.cpp index 959c0d22a..7be28fc83 100644 --- a/tslang/lib/TypeScript/MLIRGenExpressions.cpp +++ b/tslang/lib/TypeScript/MLIRGenExpressions.cpp @@ -641,6 +641,10 @@ namespace mlirgen EXIT_IF_FAILED_OR_NO_VALUE(result) auto condValue = V(result); + + // only the branch that runs is generated when the condition is known, see mlirGenSkippedBranch + auto staticCondition = getStaticBoolean(condValue); + if (condValue.getType() != getBooleanType()) { CAST(condValue, location, getBooleanType(), condValue, genContext); @@ -656,6 +660,7 @@ namespace mlirgen ElseSafeCase elseSafeCase; mlir::Value resultTrue; + if (staticCondition.value_or(true)) { // check if we do safe-cast here SymbolTableScopeT varScope(symbolTable); @@ -666,21 +671,28 @@ namespace mlirgen { EXIT_IF_FAILED_OR_NO_VALUE(result) } - + resultTrue = V(result); } + else + { + resultTrue = mlirGenSkippedBranch(location, whenTrueExpression, [&](const GenContext &evalGenContext) { + checkSafeCast(conditionalExpressionAST->condition, V(result), nullptr, evalGenContext); + }, genContext); + } builder.setInsertionPointToStart(&ifOp.getElseRegion().front()); auto whenFalseExpression = conditionalExpressionAST->whenFalse; mlir::Value resultFalse; + if (!staticCondition.value_or(false)) { SymbolTableScopeT varScope(symbolTable); if (elseSafeCase.safeType) { addSafeCastStatement(elseSafeCase.expr, elseSafeCase.safeType, false, nullptr, genContext); - } - + } + auto result2 = mlirGen(whenFalseExpression, genContext); if (!genContext.allowPartialResolve) { @@ -689,6 +701,15 @@ namespace mlirgen resultFalse = V(result2); } + else + { + resultFalse = mlirGenSkippedBranch(location, whenFalseExpression, [&](const GenContext &evalGenContext) { + if (elseSafeCase.safeType) + { + addSafeCastStatement(elseSafeCase.expr, elseSafeCase.safeType, false, nullptr, evalGenContext); + } + }, genContext); + } if (resultTrue && resultFalse) { diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index f2c21d6eb..b0a120c70 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -4345,6 +4345,24 @@ class MLIRGenImpl CAST_A(condValue, location, getBooleanType(), leftExpressionValue, genContext); + // `false && right`: right never runs and is not generated, see mlirGenSkippedBranch + auto rightSkipped = andOp && !getStaticBoolean(leftExpressionValue).value_or(true); + mlir::Type skippedRightType; + if (rightSkipped) + { + skippedRightType = evaluateSkippedBranch(rightExpression, [&](const GenContext &evalGenContext) { + checkSafeCast(leftExpression, leftExpressionValue, nullptr, evalGenContext); + }, genContext); + + // a boolean `false && right` is the constant false itself, so an enclosing `if` or `while` + // sees that its condition is known too + if (!saveResult && skippedRightType + && getUnionType(location, skippedRightType, leftExpressionValue.getType()) == getBooleanType()) + { + return condValue; + } + } + auto ifOp = builder.create(location, mlir::TypeRange{leftExpressionValue.getType()}, condValue, true); builder.setInsertionPointToStart(&ifOp.getThenRegion().front()); @@ -4352,7 +4370,12 @@ class MLIRGenImpl ElseSafeCase elseSafeCase; mlir::Value resultTrue; { - if (andOp) + if (rightSkipped) + { + resultTrue = builder.create( + location, skippedRightType ? skippedRightType : leftExpressionValue.getType()); + } + else if (andOp) { // check if we do safe-cast here SymbolTableScopeT varScope(symbolTable); @@ -7698,6 +7721,66 @@ class MLIRGenImpl return V(builder.create(location, literalType, literalType.getValue())); } + // The value of a condition known at compile time: a boolean literal, also seen through casts to + // boolean - a folded `typeof x === "name"`, or an `&&` whose left side folded to false. + std::optional getStaticBoolean(mlir::Value value) + { + while (value) + { + if (auto litType = dyn_cast(value.getType())) + { + if (auto boolVal = dyn_cast(litType.getValue())) + { + return boolVal.getValue(); + } + + return std::nullopt; + } + + auto castOp = value.getDefiningOp(); + if (!castOp || !isa(value.getType())) + { + return std::nullopt; + } + + value = castOp.getIn(); + } + + return std::nullopt; + } + + // A branch whose condition is known at compile time never runs and is not generated: its narrowing + // (see mlirGen(IfStatement)) would cast the tested value to a type the value cannot have. The branch + // still contributes its type to the expression, so it is evaluated, narrowing included, in the temporary + // module, and yields an undefined value of that type. Returns no value when the branch does not resolve. + mlir::Value mlirGenSkippedBranch(mlir::Location location, Expression expr, + std::function narrow, const GenContext &genContext) + { + auto type = evaluateSkippedBranch(expr, narrow, genContext); + if (!type) + { + return mlir::Value(); + } + + return builder.create(location, type); + } + + mlir::Type evaluateSkippedBranch(Expression expr, std::function narrow, + const GenContext &genContext) + { + TempModuleScope tempModuleScope(*this); + SymbolTableScopeT varScope(symbolTable); + SafeTypesMapScopeT safeTypesMapScope(safeTypesMap); + + GenContext evalGenContext(genContext); + evalGenContext.allowPartialResolve = true; + evalGenContext.funcOp = tempFuncOp; + narrow(evalGenContext); + auto result = mlirGen(expr, evalGenContext); + auto value = V(result); + return value ? value.getType() : mlir::Type(); + } + // `typeof x ==/===/!=/!== "name"` (either operand order) where `typeof x` is a TypeDescriptor, i.e. // x's type is known at compile time. At run time the descriptor's name - typeOfAsString of its // type, see TypeDescriptorOpLowering - is compared with the literal by content, so the result is diff --git a/tslang/lib/TypeScript/MLIRGenStatements.cpp b/tslang/lib/TypeScript/MLIRGenStatements.cpp index 25c13039d..112e89d0b 100644 --- a/tslang/lib/TypeScript/MLIRGenStatements.cpp +++ b/tslang/lib/TypeScript/MLIRGenStatements.cpp @@ -501,14 +501,7 @@ namespace mlirgen auto condValue = V(result); // special case: in case of LiteralValue do not process If value is False - std::optional literalValue; - if (auto litType = mlir::dyn_cast(condValue.getType())) - { - if (auto boolVal = mlir::dyn_cast(litType.getValue())) - { - literalValue = boolVal.getValue(); - } - } + auto literalValue = getStaticBoolean(condValue); // default implementation of IfOp if (condValue.getType() != getBooleanType()) @@ -639,6 +632,10 @@ namespace mlirgen EXIT_IF_FAILED_OR_NO_VALUE(result) auto conditionValue = V(result); + // a condition known to be false: the body never runs and, as in mlirGen(IfStatement), is not + // generated, so its narrowing cannot cast the tested value to a type it cannot have + auto processBody = getStaticBoolean(conditionValue).value_or(true); + if (conditionValue.getType() != getBooleanType()) { CAST(conditionValue, location, getBooleanType(), conditionValue, loopGenContext); @@ -649,13 +646,17 @@ namespace mlirgen // body builder.setInsertionPointToStart(&whileOp.getBody().front()); - // check if we do safe-cast here - SymbolTableScopeT varScopeBody(symbolTable); - SafeTypesMapScopeT safeTypesMapScope(safeTypesMap); - checkSafeCast(whileStatementAST->expression, conditionValue, nullptr, loopGenContext); + if (processBody) + { + // check if we do safe-cast here + SymbolTableScopeT varScopeBody(symbolTable); + SafeTypesMapScopeT safeTypesMapScope(safeTypesMap); + checkSafeCast(whileStatementAST->expression, conditionValue, nullptr, loopGenContext); + + auto result2 = mlirGen(whileStatementAST->statement, loopGenContext); + EXIT_IF_FAILED(result2) + } - auto result2 = mlirGen(whileStatementAST->statement, loopGenContext); - EXIT_IF_FAILED(result2) builder.create(location); builder.setInsertionPointAfter(whileOp); diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 065ac3c63..1f5ae95e3 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -171,6 +171,7 @@ add_test(NAME test-compile-00-funcs-nesting-generic COMMAND test-runner "${PROJE add_test(NAME test-compile-00-funcs-nesting-capture COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00funcs_nesting_capture.ts") add_test(NAME test-compile-00-funcs-hybrid-null-this COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00funcs_hybrid_null_this.ts") add_test(NAME test-compile-00-typeof-static-fold COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00typeof_static_fold.ts") +add_test(NAME test-compile-00-typeof-static-fold-conditions COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00typeof_static_fold_conditions.ts") add_test(NAME test-compile-00-array-move-wide-elements COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_move_wide_elements.ts") add_test(NAME test-compile-00-funcs-expression-generic COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00funcs_expression_generic.ts") add_test(NAME test-compile-00-funcs-expression-iterator COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00funcs_expression_iterator.ts") @@ -588,6 +589,7 @@ add_test(NAME test-jit-00-funcs-nesting-generic COMMAND test-runner -jit "${PROJ add_test(NAME test-jit-00-funcs-nesting-capture COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00funcs_nesting_capture.ts") add_test(NAME test-jit-00-funcs-hybrid-null-this COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00funcs_hybrid_null_this.ts") add_test(NAME test-jit-00-typeof-static-fold COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00typeof_static_fold.ts") +add_test(NAME test-jit-00-typeof-static-fold-conditions COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00typeof_static_fold_conditions.ts") add_test(NAME test-jit-00-array-move-wide-elements COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_move_wide_elements.ts") add_test(NAME test-jit-00-funcs-expression-generic COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00funcs_expression_generic.ts") add_test(NAME test-jit-00-funcs-expression-iterator COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00funcs_expression_iterator.ts") @@ -1575,6 +1577,7 @@ set(TSLANG_CORPUS 00type_guard_function.ts 00typed_array.ts 00typeof_static_fold.ts + 00typeof_static_fold_conditions.ts 00types_indexedaccesstype.ts 00types_keyof_enum.ts 00types_mappedtype.ts diff --git a/tslang/test/tester/tests/00typeof_static_fold_conditions.ts b/tslang/test/tester/tests/00typeof_static_fold_conditions.ts new file mode 100644 index 000000000..be5560fa6 --- /dev/null +++ b/tslang/test/tester/tests/00typeof_static_fold_conditions.ts @@ -0,0 +1,44 @@ +// A `?:`, `&&` or `while` whose condition folds at compile time (a `typeof` of a value whose type +// is known) does not generate the branch that never runs. That branch narrows the tested value, so +// for an array narrowed to `string` it cast the array to a string and crashed the compiler. +function conditional(x: T) { + return typeof x === "string" ? x.length : -1; +} + +function conditionalElse(x: T) { + return typeof x !== "string" ? -1 : x.length; +} + +function and(x: T) { + return typeof x === "string" && x.length > 1; +} + +function loop(x: T) { + let n = 0; + while (typeof x === "string" && n < x.length) { + n++; + } + + return n; +} + +function main() { + assert(conditional("abc") == 3, "?: string"); + assert(conditional([1, 2]) == -1, "?: array"); + assert(conditionalElse("abcd") == 4, "?: else string"); + assert(conditionalElse(["a"]) == -1, "?: else array"); + + assert(and("abc"), "&& string"); + assert(!and([1, 2]), "&& array"); + + assert(loop("ab") == 2, "while string"); + assert(loop([1, 2, 3]) == 0, "while array"); + + // the branch that is not generated still gives the expression its type + const text = "abc"; + let v = typeof text === "string" ? 1 : "one"; + v = "one"; + assert(v == "one", "?: keeps the type of both branches"); + + print("done."); +}