From d8239714207f007c06d055c983b966425a1df000 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Mon, 14 Sep 2026 20:53:19 +0100 Subject: [PATCH] Skip statically dead typeof branches at MLIR generation `typeof x === "name"` on a value whose type is known at compile time emits a TypeDescriptor, whose run-time name (typeOfAsString of its type) is compared with the literal by content, so the result is known while generating MLIR. It was not folded, so a branch that can never run was generated anyway. In a generic specialised for an array, such a branch can hold casts an array cannot take: `print("s: ", x)` under `typeof x === "string"` emitted an array-to-string cast that reached llvm_unreachable at CastLogicHelper.h:815 in debug builds (01symbol, 00funcs_generic_with_typeof) and undefined behaviour in release. Two changes: - The ==, ===, != and !== comparison between a TypeDescriptor and a string literal (either order) now produces a boolean literal, which `if` already uses to skip a branch. `any`, union tags and stored typeof values keep their run-time check; names compare exactly as at run time, so no result changes. - `if` added its narrowing (safe-cast) before deciding whether to generate a branch, so a skipped branch still got a cast of the tested value. Unused, it was removed as dead code, but under --di the narrowed variable's debug record kept it alive into LLVM lowering. Narrowing is now added only to a branch that is generated, for both `then` and `else`. Adds 00typeof_static_fold.ts (compile, jit, rc/none corpus). Co-Authored-By: Claude Opus 5 --- tslang/lib/TypeScript/MLIRGenExpressions.cpp | 9 ++++ tslang/lib/TypeScript/MLIRGenImpl.h | 51 +++++++++++++++++++ tslang/lib/TypeScript/MLIRGenStatements.cpp | 20 +++++--- tslang/test/tester/CMakeLists.txt | 3 ++ .../test/tester/tests/00typeof_static_fold.ts | 35 +++++++++++++ 5 files changed, 111 insertions(+), 7 deletions(-) create mode 100644 tslang/test/tester/tests/00typeof_static_fold.ts diff --git a/tslang/lib/TypeScript/MLIRGenExpressions.cpp b/tslang/lib/TypeScript/MLIRGenExpressions.cpp index 4b6c8aadd..959c0d22a 100644 --- a/tslang/lib/TypeScript/MLIRGenExpressions.cpp +++ b/tslang/lib/TypeScript/MLIRGenExpressions.cpp @@ -779,6 +779,15 @@ namespace mlirgen EXIT_IF_FAILED_OR_NO_VALUE(result2) auto rightExpressionValue = V(result2); + // `typeof x === "name"` where x has a type known at compile time: the descriptor's name is + // known now, and the run-time compare is a plain string compare, so fold it. The boolean + // literal lets `if` skip a branch that can never run - in a generic specialised for an array, + // such a branch can hold casts an array cannot take, which crash LLVM lowering. + if (auto folded = foldStaticTypeOfCompare(location, opCode, leftExpressionValue, rightExpressionValue)) + { + return *folded; + } + // check if const expr. if (genContext.allowConstEval) { diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index 8e7e88889..f2c21d6eb 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -7698,6 +7698,57 @@ class MLIRGenImpl return V(builder.create(location, literalType, literalType.getValue())); } + // `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 + // known here. Returns nothing for any other comparison (an `any`, a union tag, a stored typeof). + std::optional foldStaticTypeOfCompare(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 stringLiteralOf = [](mlir::Value value) -> std::optional { + if (auto constOp = value.getDefiningOp()) + { + if (auto strAttr = dyn_cast(constOp.getValue())) + { + return strAttr.getValue(); + } + } + + return std::nullopt; + }; + + auto descriptorOp = leftValue.getDefiningOp(); + auto literal = stringLiteralOf(rightValue); + if (!descriptorOp) + { + descriptorOp = rightValue.getDefiningOp(); + literal = stringLiteralOf(leftValue); + } + + if (!descriptorOp || !literal) + { + return std::nullopt; + } + + TypeOfOpHelper toh(builder); + auto namesMatch = toh.typeOfAsString(descriptorOp.getDescriptorType()) == *literal; + + // the typeof value was built for this comparison only + if (descriptorOp->use_empty()) + { + descriptorOp->erase(); + } + + return mlirGenBooleanValue(location, isEquals ? namesMatch : !namesMatch); + } + ValueOrLogicalResult mlirGen(TrueLiteral trueLiteral, const GenContext &genContext); ValueOrLogicalResult mlirGen(FalseLiteral falseLiteral, const GenContext &genContext); diff --git a/tslang/lib/TypeScript/MLIRGenStatements.cpp b/tslang/lib/TypeScript/MLIRGenStatements.cpp index 75308e97b..25c13039d 100644 --- a/tslang/lib/TypeScript/MLIRGenStatements.cpp +++ b/tslang/lib/TypeScript/MLIRGenStatements.cpp @@ -520,16 +520,21 @@ namespace mlirgen builder.setInsertionPointToStart(&ifOp.getThenRegion().front()); + // Narrowing (safe-cast) is added only to a branch that is generated: it emits a cast of the tested + // value, and for a statically decided test that cast can be one the value cannot take - an array + // narrowed to `string` by `typeof x === "string"`. Under --di the narrowed variable's debug record + // keeps such a cast alive into LLVM lowering even though the branch body was skipped. ElseSafeCase elseSafeCase{}; { - // check if we do safe-cast here SymbolTableScopeT varScope(symbolTable); SafeTypesMapScopeT safeTypesMapScope(safeTypesMap); - checkSafeCast(ifStatementAST->expression, V(result), hasElse ? &elseSafeCase : nullptr, genContext); auto processIf = !literalValue.has_value() || literalValue.value(); if (processIf) { + // check if we do safe-cast here + checkSafeCast(ifStatementAST->expression, V(result), hasElse ? &elseSafeCase : nullptr, genContext); + auto result = mlirGen(ifStatementAST->thenStatement, genContext); EXIT_IF_FAILED(result) } @@ -539,15 +544,16 @@ namespace mlirgen { builder.setInsertionPointToStart(&ifOp.getElseRegion().front()); SymbolTableScopeT varScope(symbolTable); - if (elseSafeCase.safeType) - { - // add case statement - addSafeCastStatement(elseSafeCase.expr, elseSafeCase.safeType, false, nullptr, genContext); - } auto processIf = !literalValue.has_value() || !literalValue.value(); if (processIf) { + if (elseSafeCase.safeType) + { + // add case statement + addSafeCastStatement(elseSafeCase.expr, elseSafeCase.safeType, false, nullptr, genContext); + } + auto result = mlirGen(ifStatementAST->elseStatement, genContext); EXIT_IF_FAILED(result) } diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index c32144524..5fc759079 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -170,6 +170,7 @@ add_test(NAME test-compile-00-funcs-nesting COMMAND test-runner "${PROJECT_SOURC add_test(NAME test-compile-00-funcs-nesting-generic COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00funcs_nesting_generic.ts") 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-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") add_test(NAME test-compile-00-arrow-generic COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00arrow_generic.ts") @@ -585,6 +586,7 @@ add_test(NAME test-jit-00-funcs-nesting COMMAND test-runner -jit "${PROJECT_SOUR add_test(NAME test-jit-00-funcs-nesting-generic COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00funcs_nesting_generic.ts") 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-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") add_test(NAME test-jit-00-arrow-generic COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00arrow_generic.ts") @@ -1569,6 +1571,7 @@ set(TSLANG_CORPUS 00type_aliases_in_generics.ts 00type_guard_function.ts 00typed_array.ts + 00typeof_static_fold.ts 00types_indexedaccesstype.ts 00types_keyof_enum.ts 00types_mappedtype.ts diff --git a/tslang/test/tester/tests/00typeof_static_fold.ts b/tslang/test/tester/tests/00typeof_static_fold.ts new file mode 100644 index 000000000..927fe26a4 --- /dev/null +++ b/tslang/test/tester/tests/00typeof_static_fold.ts @@ -0,0 +1,35 @@ +// `typeof x === ""` on a value whose type is known at compile time is folded to a +// constant, so a branch that can never run is not generated. Before the fold, the branch +// below that prints an array as a string reached LLVM lowering and crashed the compiler. +function describe(x: T) { + if (typeof x === "string") { + print("string: ", x); + return 1; + } + + if ("array" === typeof x) { + return 2; + } + + if (typeof x !== "boolean") { + return 3; + } + + return 4; +} + +function main() { + assert(describe("abc") == 1, "string"); + assert(describe([]) == 2, "array"); + assert(describe(["a", "b"]) == 2, "string array"); + assert(describe(2.5) == 3, "number"); + assert(describe(true) == 4, "boolean"); + + // `any` is only known at run time and stays a run-time check + let a: any = "text"; + assert(typeof a === "string", "any holding a string"); + a = 2.5; + assert(typeof a !== "string", "any holding a number"); + + print("done."); +}