Skip to content

Commit f53e5fc

Browse files
A global whose initializer branches becomes a constructor (#462) (#474)
`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 <noreply@anthropic.com>
1 parent ee08124 commit f53e5fc

5 files changed

Lines changed: 127 additions & 1 deletion

File tree

‎tslang/include/TypeScript/TypeScriptOps.td‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2215,8 +2215,11 @@ def TypeScript_AddressOfOp : TypeScript_Op<"AddressOf"> {
22152215
}];
22162216
}
22172217

2218+
// The initializer is one block as MLIRGen builds it, ending in ts.GlobalResult; lowering a branch
2219+
// in it (a conditional, an optional widened into a union) adds blocks, and GlobalOpLowering then
2220+
// makes the global a constructor (#462).
22182221
def TypeScript_GlobalOp : TypeScript_Op<"Global",
2219-
[IsolatedFromAbove, SingleBlockImplicitTerminator<"GlobalResultOp">, Symbol]> {
2222+
[IsolatedFromAbove, Symbol]> {
22202223
let arguments = (ins
22212224
TypeAttr:$type,
22222225
UnitAttr:$constant,

‎tslang/lib/TypeScript/LowerToLLVM.cpp‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4472,6 +4472,13 @@ struct GlobalOpLowering : public TsLlvmPattern<mlir_ts::GlobalOp>
44724472

44734473
globalOp.getInitializerRegion().walk(visitorAllOps);
44744474

4475+
// a branch in the initializer (a conditional, an optional widened into a union) has left
4476+
// it more than one block, which only a function can hold (#462)
4477+
if (!globalOp.getInitializerRegion().empty() && !globalOp.getInitializerRegion().hasOneBlock())
4478+
{
4479+
createAsGlobalConstructor = true;
4480+
}
4481+
44754482
auto linkage = globalOp.getLinkage();
44764483
LLVM::GlobalOp llvmGlobalOp;
44774484
if (createAsGlobalConstructor)

‎tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp‎

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,77 @@ class OwnedReturnConsumptionPass
304304
return;
305305
}
306306

307+
// An optional widened into a union (`let g: C | null | undefined = mkU()`, #462) is a
308+
// ts.If over the call made before it: each branch yields that call's value through
309+
// views, or a value that holds nothing (the empty optional). The global takes the call's
310+
// count, as it would take the call's straight.
311+
if (auto ifOp = value.getDefiningOp<mlir_ts::IfOp>())
312+
{
313+
auto throughViews = [](mlir::Value yielded) {
314+
while (auto *definingOp = yielded.getDefiningOp())
315+
{
316+
if (auto castOp = mlir::dyn_cast<mlir_ts::CastOp>(definingOp))
317+
{
318+
yielded = castOp.getIn();
319+
}
320+
else if (auto optionalValueOp = mlir::dyn_cast<mlir_ts::OptionalValueOp>(definingOp))
321+
{
322+
yielded = optionalValueOp.getIn();
323+
}
324+
else if (auto createUnionInstanceOp = mlir::dyn_cast<mlir_ts::CreateUnionInstanceOp>(definingOp))
325+
{
326+
yielded = createUnionInstanceOp.getIn();
327+
}
328+
else if (auto valueOp = mlir::dyn_cast<mlir_ts::ValueOp>(definingOp))
329+
{
330+
yielded = valueOp.getIn();
331+
}
332+
else
333+
{
334+
break;
335+
}
336+
}
337+
338+
return yielded;
339+
};
340+
341+
mlir::Value producer;
342+
auto agrees = true;
343+
for (auto &region : ifOp->getRegions())
344+
{
345+
auto resultOp = region.empty() ? mlir_ts::ResultOp()
346+
: mlir::dyn_cast<mlir_ts::ResultOp>(region.back().getTerminator());
347+
if (!resultOp || resultOp->getNumOperands() != 1)
348+
{
349+
agrees = false;
350+
break;
351+
}
352+
353+
auto yielded = throughViews(resultOp->getOperand(0));
354+
auto *yieldedOp = yielded.getDefiningOp();
355+
if (yieldedOp && mlir::isa<mlir_ts::OptionalUndefOp, mlir_ts::NullOp, mlir_ts::UndefOp>(yieldedOp))
356+
{
357+
continue;
358+
}
359+
360+
if (producer && producer != yielded)
361+
{
362+
agrees = false;
363+
break;
364+
}
365+
366+
producer = yielded;
367+
}
368+
369+
if (agrees && producer && producesOwnedResult(producer) &&
370+
!ifOp->isAncestor(producer.getDefiningOp()))
371+
{
372+
producer.getDefiningOp()->setAttr(OWNED_RESULT_CONSUMED_ATTR_NAME,
373+
mlir::UnitAttr::get(&getContext()));
374+
return;
375+
}
376+
}
377+
307378
// An object literal (`let o = { s: mk() }`) is built in a temporary slot and read out
308379
// whole: what its fields were given goes into the global with it (#461), nested
309380
// literals included.

‎tslang/test/tester/CMakeLists.txt‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,7 @@ tslang_add_test(NAME test-compile-00-throw-f32 COMMAND test-runner "${PROJECT_SO
430430
tslang_add_test(NAME test-compile-00-throw-u32 COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00throw_u32.ts")
431431
tslang_add_test(NAME test-compile-00-cast-to-f32 COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00cast_to_f32.ts")
432432
tslang_add_test(NAME test-compile-00-instanceof-namespace-narrowing COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00instanceof_namespace_narrowing.ts")
433+
tslang_add_test(NAME test-compile-00-global-initializer-branches COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00global_initializer_branches.ts")
433434
tslang_add_test(NAME test-compile-00-using-nested-scopes COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00using_nested_scopes.ts")
434435
tslang_add_test(NAME test-compile-00-using-unwind-progress COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00using_unwind_progress.ts")
435436
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")
@@ -919,6 +920,7 @@ tslang_add_test(NAME test-jit-00-throw-f32 COMMAND test-runner -jit "${PROJECT_S
919920
tslang_add_test(NAME test-jit-00-throw-u32 COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00throw_u32.ts")
920921
tslang_add_test(NAME test-jit-00-cast-to-f32 COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00cast_to_f32.ts")
921922
tslang_add_test(NAME test-jit-00-instanceof-namespace-narrowing COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00instanceof_namespace_narrowing.ts")
923+
tslang_add_test(NAME test-jit-00-global-initializer-branches COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00global_initializer_branches.ts")
922924
tslang_add_test(NAME test-jit-00-using-nested-scopes COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00using_nested_scopes.ts")
923925
tslang_add_test(NAME test-jit-00-using-unwind-progress COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00using_unwind_progress.ts")
924926
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")
@@ -1785,6 +1787,7 @@ set(TSLANG_CORPUS
17851787
00global_constant_concat.ts
17861788
00const_of_let.ts
17871789
00instanceof_namespace_narrowing.ts
1790+
00global_initializer_branches.ts
17881791
00instanceof_union.ts
17891792
00nullable_to_any.ts
17901793
00ternary_new_or_null.ts
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// A module-level variable whose initializer branches: an optional widened into a union with null
2+
// (#462), and a conditional expression. Lowered, the initializer had more than one block, which a
3+
// global's region could not hold: "'ts.Global' op expects region #0 to have 0 or 1 blocks".
4+
class Node {
5+
v = 0;
6+
tag = "";
7+
}
8+
9+
function mk(v: number) {
10+
const n = new Node();
11+
n.v = v;
12+
n.tag = "t" + v;
13+
return n;
14+
}
15+
16+
function mkU(): Node | undefined {
17+
return mk(7);
18+
}
19+
20+
function none(): Node | undefined {
21+
return undefined;
22+
}
23+
24+
function on() {
25+
return true;
26+
}
27+
28+
let g: Node | null | undefined = mkU();
29+
let h: Node | null | undefined = none();
30+
let x = on() ? 1 : 2;
31+
const label = on() ? "yes" : "no";
32+
let pick: Node | null = on() ? mk(3) : null;
33+
34+
function main() {
35+
assert(g !== undefined && g !== null && g.tag == "t7", "an optional widened into a union");
36+
assert(h === undefined, "an empty optional widened into a union");
37+
assert(x == 1, "a conditional number");
38+
assert(label == "yes", "a conditional string");
39+
assert(pick !== null && pick.v == 3, "a conditional object");
40+
41+
print("done.");
42+
}

0 commit comments

Comments
 (0)