From c728f11b05cb45d3d672b4002a4cfe0773c4b87d Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Mon, 14 Sep 2026 23:44:42 +0100 Subject: [PATCH] Narrow a union to its only member of the typeof kind `typeof u === "function"` (also "class", "interface", "object", "array") names a kind, not a type, so it narrowed `u` to Opaque (Opaque[] for "array"). For a union that threw the member type away: - `let f: (() => number) | string; if (typeof f === "function") f()` failed with "not a function to call"; - `let c: Box | number; if (typeof c === "class") c.value` failed with "Can't resolve property 'value' of type Opaque". A union already lists its members of that kind: the members whose runtime tag, typeOfAsString, is the tested name. When there is exactly one, checkSafeCastTypeOf now narrows the union to that member. With several members of the kind the value stays Opaque. The cast helper castFromUnion generates (`if (typeof t == 'function') return t;`) relies on it: narrowed to a smaller union, `return t` is a union cast again and instantiates the helper without end (01union_type and logicalAssignment5_3 overflowed the stack, typeGuardFunction threw at run time). Co-Authored-By: Claude Opus 5 --- tslang/lib/TypeScript/MLIRGenImpl.h | 37 ++++++++++++++++ tslang/test/tester/CMakeLists.txt | 3 ++ .../tester/tests/00typeof_union_narrowing.ts | 44 +++++++++++++++++++ 3 files changed, 84 insertions(+) create mode 100644 tslang/test/tester/tests/00typeof_union_narrowing.ts diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index b0a120c70..5c545e4b7 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -3385,6 +3385,14 @@ class MLIRGenImpl if (typeToken) { + if (text == S("function") || text == S("class") || text == S("interface") || text == S("object") || text == S("array")) + { + if (auto membersType = getUnionMembersOfTypeOf(expr, wstos(text), genContext)) + { + return addSafeCastStatement(expr, membersType, inverse, elseSafeCase, genContext); + } + } + return addSafeCastStatement(expr, typeToken, inverse, elseSafeCase, genContext); } @@ -3395,6 +3403,35 @@ class MLIRGenImpl return mlir::failure(); } + // "function", "class", "interface", "object" and "array" name a kind, not a type, so they narrow to + // Opaque (or Opaque[]). A union already lists its members of that kind - the members whose runtime + // tag, typeOfAsString, is that name - and when there is exactly one, the union is narrowed to it: + // through Opaque a function could not be called and a class field not resolved. + // With several members of the kind it stays Opaque. The union cast helper castFromUnion generates + // (`if (typeof t == 'function') return t;`) depends on that: narrowed to a smaller union, `return t` + // would be a union cast again and instantiate the helper without end. + // Returns no type when the value is not a union or does not have exactly one member of the kind. + mlir::Type getUnionMembersOfTypeOf(Expression expr, llvm::StringRef typeOfName, const GenContext &genContext) + { + auto unionType = dyn_cast_or_null(evaluate(expr, genContext)); + if (!unionType) + { + return mlir::Type(); + } + + TypeOfOpHelper toh(builder); + SmallVector members; + for (auto member : unionType.getTypes()) + { + if (toh.typeOfAsString(member) == typeOfName) + { + members.push_back(member); + } + } + + return members.size() == 1 ? members.front() : mlir::Type(); + } + mlir::LogicalResult checkSafeCastUndefined(Expression optVal, Expression undefVal, bool inverse, ElseSafeCase *elseSafeCase, const GenContext &genContext) { auto expr = stripParentheses(undefVal); diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 1f5ae95e3..442430b45 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -172,6 +172,7 @@ add_test(NAME test-compile-00-funcs-nesting-capture COMMAND test-runner "${PROJE 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-typeof-union-narrowing COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00typeof_union_narrowing.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") @@ -590,6 +591,7 @@ add_test(NAME test-jit-00-funcs-nesting-capture COMMAND test-runner -jit "${PROJ 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-typeof-union-narrowing COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00typeof_union_narrowing.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") @@ -1578,6 +1580,7 @@ set(TSLANG_CORPUS 00typed_array.ts 00typeof_static_fold.ts 00typeof_static_fold_conditions.ts + 00typeof_union_narrowing.ts 00types_indexedaccesstype.ts 00types_keyof_enum.ts 00types_mappedtype.ts diff --git a/tslang/test/tester/tests/00typeof_union_narrowing.ts b/tslang/test/tester/tests/00typeof_union_narrowing.ts new file mode 100644 index 000000000..06d264ab7 --- /dev/null +++ b/tslang/test/tester/tests/00typeof_union_narrowing.ts @@ -0,0 +1,44 @@ +// `typeof u === "function"` (also "class", "array") names a kind, not a type. For a union the +// members of that kind are known, so `u` is narrowed to them. It was narrowed to Opaque instead: +// a function could not be called and a class field could not be resolved. +class Box { + value = 5; +} + +function one(): number { + return 1; +} + +function main() { + let f: (() => number) | string = one; + if (typeof f === "function") { + assert(f() == 1, "function member is callable"); + } else { + assert(false, "function: wrong branch"); + } + + f = "abc"; + if (typeof f !== "function") { + assert(f.length == 3, "string member after !=="); + } else { + assert(false, "string: wrong branch"); + } + + let c: Box | number = new Box(); + if (typeof c === "class") { + assert(c.value == 5, "class member field"); + } else { + assert(false, "class: wrong branch"); + } + + const numbers: number[] = [1, 2, 3]; + let a: number[] | string = numbers; + if (typeof a === "array") { + assert(a.length == 3, "array member length"); + assert(a[2] == 3, "array member element"); + } else { + assert(false, "array: wrong branch"); + } + + print("done."); +}