From 906778cbf5d4f322679f2b1bfbdeac381c202689 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 3 Oct 2026 21:12:06 +0100 Subject: [PATCH] A global whose initializer branches becomes a constructor (#462) `let g: Node | null | undefined = mkU()`, with mkU returning `Node | undefined`, failed in every model with "'ts.Global' op expects region #0 to have 0 or 1 blocks", and so did any conditional global initializer (`let x = on() ? 1 : 2`). The optional's widening into the union, like a conditional, is a ts.If; the affine lowering turns it into blocks, and ts.Global's SingleBlockImplicitTerminator trait rejected them. The trait is dropped (MLIRGen still builds one block ending in ts.GlobalResult, and nothing reads the initializer as one block after the affine lowering), and GlobalOpLowering makes a global whose initializer has more than one block a constructor: only a function can hold the branches. Under rc the widened optional then lost its object: the call runs before the ts.If, and the branch yields it through views (ts.Value, a cast, ts.OptionalValue), so the global never took its reference over and the call's was given back at the end of the initializer. claimGlobalInitializers now follows a ts.If whose branches all yield one owned producer from before it through views, or a value that holds nothing (the empty optional, null, undefined), and the global takes the count. Test: 00global_initializer_branches (compile and JIT, and in the rc/none corpus): an optional widened into a union (set and empty), and conditional number, string and object globals. It failed to compile before the change. The own corpus is unchanged. Fixes #462. Co-Authored-By: Claude Opus 5.5 --- tslang/include/TypeScript/TypeScriptOps.td | 5 +- tslang/lib/TypeScript/LowerToLLVM.cpp | 7 ++ .../TypeScript/OwnedReturnConsumptionPass.cpp | 71 +++++++++++++++++++ tslang/test/tester/CMakeLists.txt | 3 + .../tests/00global_initializer_branches.ts | 42 +++++++++++ 5 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 tslang/test/tester/tests/00global_initializer_branches.ts diff --git a/tslang/include/TypeScript/TypeScriptOps.td b/tslang/include/TypeScript/TypeScriptOps.td index 818fdf6d5..92cb2a11d 100644 --- a/tslang/include/TypeScript/TypeScriptOps.td +++ b/tslang/include/TypeScript/TypeScriptOps.td @@ -2215,8 +2215,11 @@ def TypeScript_AddressOfOp : TypeScript_Op<"AddressOf"> { }]; } +// The initializer is one block as MLIRGen builds it, ending in ts.GlobalResult; lowering a branch +// in it (a conditional, an optional widened into a union) adds blocks, and GlobalOpLowering then +// makes the global a constructor (#462). def TypeScript_GlobalOp : TypeScript_Op<"Global", - [IsolatedFromAbove, SingleBlockImplicitTerminator<"GlobalResultOp">, Symbol]> { + [IsolatedFromAbove, Symbol]> { let arguments = (ins TypeAttr:$type, UnitAttr:$constant, diff --git a/tslang/lib/TypeScript/LowerToLLVM.cpp b/tslang/lib/TypeScript/LowerToLLVM.cpp index a01c87f55..7b3d5c128 100644 --- a/tslang/lib/TypeScript/LowerToLLVM.cpp +++ b/tslang/lib/TypeScript/LowerToLLVM.cpp @@ -4454,6 +4454,13 @@ struct GlobalOpLowering : public TsLlvmPattern globalOp.getInitializerRegion().walk(visitorAllOps); + // a branch in the initializer (a conditional, an optional widened into a union) has left + // it more than one block, which only a function can hold (#462) + if (!globalOp.getInitializerRegion().empty() && !globalOp.getInitializerRegion().hasOneBlock()) + { + createAsGlobalConstructor = true; + } + auto linkage = globalOp.getLinkage(); LLVM::GlobalOp llvmGlobalOp; if (createAsGlobalConstructor) diff --git a/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp b/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp index f2f4e559b..acf10bb3e 100644 --- a/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp +++ b/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp @@ -304,6 +304,77 @@ class OwnedReturnConsumptionPass return; } + // An optional widened into a union (`let g: C | null | undefined = mkU()`, #462) is a + // ts.If over the call made before it: each branch yields that call's value through + // views, or a value that holds nothing (the empty optional). The global takes the call's + // count, as it would take the call's straight. + if (auto ifOp = value.getDefiningOp()) + { + auto throughViews = [](mlir::Value yielded) { + while (auto *definingOp = yielded.getDefiningOp()) + { + if (auto castOp = mlir::dyn_cast(definingOp)) + { + yielded = castOp.getIn(); + } + else if (auto optionalValueOp = mlir::dyn_cast(definingOp)) + { + yielded = optionalValueOp.getIn(); + } + else if (auto createUnionInstanceOp = mlir::dyn_cast(definingOp)) + { + yielded = createUnionInstanceOp.getIn(); + } + else if (auto valueOp = mlir::dyn_cast(definingOp)) + { + yielded = valueOp.getIn(); + } + else + { + break; + } + } + + return yielded; + }; + + mlir::Value producer; + auto agrees = true; + for (auto ®ion : ifOp->getRegions()) + { + auto resultOp = region.empty() ? mlir_ts::ResultOp() + : mlir::dyn_cast(region.back().getTerminator()); + if (!resultOp || resultOp->getNumOperands() != 1) + { + agrees = false; + break; + } + + auto yielded = throughViews(resultOp->getOperand(0)); + auto *yieldedOp = yielded.getDefiningOp(); + if (yieldedOp && mlir::isa(yieldedOp)) + { + continue; + } + + if (producer && producer != yielded) + { + agrees = false; + break; + } + + producer = yielded; + } + + if (agrees && producer && producesOwnedResult(producer) && + !ifOp->isAncestor(producer.getDefiningOp())) + { + producer.getDefiningOp()->setAttr(OWNED_RESULT_CONSUMED_ATTR_NAME, + mlir::UnitAttr::get(&getContext())); + return; + } + } + // An object literal (`let o = { s: mk() }`) is built in a temporary slot and read out // whole: what its fields were given goes into the global with it (#461), nested // literals included. diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 0f53f727a..3e750c2e4 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -430,6 +430,7 @@ tslang_add_test(NAME test-compile-00-throw-f32 COMMAND test-runner "${PROJECT_SO tslang_add_test(NAME test-compile-00-throw-u32 COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00throw_u32.ts") tslang_add_test(NAME test-compile-00-cast-to-f32 COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00cast_to_f32.ts") tslang_add_test(NAME test-compile-00-instanceof-namespace-narrowing COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00instanceof_namespace_narrowing.ts") +tslang_add_test(NAME test-compile-00-global-initializer-branches COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00global_initializer_branches.ts") tslang_add_test(NAME test-compile-00-using-nested-scopes COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00using_nested_scopes.ts") tslang_add_test(NAME test-compile-00-using-unwind-progress COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00using_unwind_progress.ts") tslang_add_test(NAME test-compile-00-break-continue-scope-exit COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00break_continue_scope_exit.ts") @@ -917,6 +918,7 @@ tslang_add_test(NAME test-jit-00-throw-f32 COMMAND test-runner -jit "${PROJECT_S tslang_add_test(NAME test-jit-00-throw-u32 COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00throw_u32.ts") tslang_add_test(NAME test-jit-00-cast-to-f32 COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00cast_to_f32.ts") tslang_add_test(NAME test-jit-00-instanceof-namespace-narrowing COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00instanceof_namespace_narrowing.ts") +tslang_add_test(NAME test-jit-00-global-initializer-branches COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00global_initializer_branches.ts") tslang_add_test(NAME test-jit-00-using-nested-scopes COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00using_nested_scopes.ts") tslang_add_test(NAME test-jit-00-using-unwind-progress COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00using_unwind_progress.ts") tslang_add_test(NAME test-jit-00-break-continue-scope-exit COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00break_continue_scope_exit.ts") @@ -1779,6 +1781,7 @@ set(TSLANG_CORPUS 00instanceof.ts 00const_of_let.ts 00instanceof_namespace_narrowing.ts + 00global_initializer_branches.ts 00instanceof_union.ts 00nullable_to_any.ts 00ternary_new_or_null.ts diff --git a/tslang/test/tester/tests/00global_initializer_branches.ts b/tslang/test/tester/tests/00global_initializer_branches.ts new file mode 100644 index 000000000..4a5390aaa --- /dev/null +++ b/tslang/test/tester/tests/00global_initializer_branches.ts @@ -0,0 +1,42 @@ +// A module-level variable whose initializer branches: an optional widened into a union with null +// (#462), and a conditional expression. Lowered, the initializer had more than one block, which a +// global's region could not hold: "'ts.Global' op expects region #0 to have 0 or 1 blocks". +class Node { + v = 0; + tag = ""; +} + +function mk(v: number) { + const n = new Node(); + n.v = v; + n.tag = "t" + v; + return n; +} + +function mkU(): Node | undefined { + return mk(7); +} + +function none(): Node | undefined { + return undefined; +} + +function on() { + return true; +} + +let g: Node | null | undefined = mkU(); +let h: Node | null | undefined = none(); +let x = on() ? 1 : 2; +const label = on() ? "yes" : "no"; +let pick: Node | null = on() ? mk(3) : null; + +function main() { + assert(g !== undefined && g !== null && g.tag == "t7", "an optional widened into a union"); + assert(h === undefined, "an empty optional widened into a union"); + assert(x == 1, "a conditional number"); + assert(label == "yes", "a conditional string"); + assert(pick !== null && pick.v == 3, "a conditional object"); + + print("done."); +}