Skip to content
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
e5eb0b2
Lower spread-arm ternaries into per-branch blocks instead of hoisting
ctate Jul 19, 2026
797286a
Narrow optionals and union payloads after break/continue guards
ctate Jul 19, 2026
bbdc71e
Restore still-optional markers when scoped kind-narrowing exits
ctate Jul 19, 2026
0ec36f5
Type inferred locals from narrowed ternaries by the non-optional arm
ctate Jul 19, 2026
0f4ddfd
Pin shadowed-name kind guards leaving the subject's payload optional
ctate Jul 19, 2026
5ac9031
Keep reassigned let bindings out of const orelse fusion
ctate Jul 19, 2026
a921322
Repopulate switch-arm narrowing maps from the snapshot on exit
ctate Jul 19, 2026
769ea8b
Apply post-if narrowing on the else-if exit path from emitIf
ctate Jul 19, 2026
18e90e1
Keep branch-killed null narrowing dead past the merge
ctate Jul 19, 2026
deb48db
Bracket chain-substitution scopes with the narrowing kill-frame protocol
ctate Jul 20, 2026
c705a80
Drop an always-exiting branch's narrowing kills at the merge
ctate Jul 20, 2026
081a13c
Keep a lifted callback's trailing return inside its prefix's flow scope
ctate Jul 20, 2026
46972af
Merge a try body's narrowing kills when its catch can fall through
ctate Jul 20, 2026
760cf98
Decide narrowing-kill drops with an all-routes escape analysis
ctate Jul 20, 2026
d23f733
Treat never-completing loops as terminal in the narrowing analysis
ctate Jul 20, 2026
7554df1
Trace a caught throw into the code after its try when classifying routes
ctate Jul 20, 2026
873c257
Strip parentheses before matching narrowed targets in the ternary mac…
ctate Jul 20, 2026
21dc3a5
See through wrapped null literals in ternary and nullish decisions
ctate Jul 20, 2026
f96dc06
Let narrowing flow through plain lexical blocks
ctate Jul 20, 2026
6041135
Route caught-throw kills to the post-try continuation
ctate Jul 20, 2026
4a0c1a3
Join sibling arms' narrowing kills at the construct exit
ctate Jul 20, 2026
a61631d
Subtract joined branch kills from post-construct narrows
ctate Jul 20, 2026
31d8a92
Key the narrowing maps by declaration identity
ctate Jul 20, 2026
0a92608
Stage narrowing kills at the edges break, continue, and callback retu…
ctate Jul 20, 2026
0ab3e79
Canonicalize erased wrappers in the narrowing key
ctate Jul 20, 2026
c0b68eb
Treat the global undefined as an empty in ternary and nullish arm dec…
ctate Jul 20, 2026
cfa5914
Isolate probe and sibling-arm flow state in expression lowerings
ctate Jul 20, 2026
7834ede
Gate property-read detection on declaration-qualified keys, not sourc…
ctate Jul 20, 2026
3acc168
Stage finally-clause kills until the try construct's exit
ctate Jul 20, 2026
8b28776
Route pending exception kills through the catch's own control flow
ctate Jul 20, 2026
96129b4
Key element-access narrows by declaration and canonical index
ctate Jul 20, 2026
fec07cd
Keep narrowing through provably non-null reassignments
ctate Jul 20, 2026
37434fe
Emit a do-while's trailing test inside its body's narrowing scope
ctate Jul 20, 2026
2e6c2fc
Judge terminality through stacked case labels and statement labels
ctate Jul 20, 2026
c105a91
Judge exhaustive defaultless value switches terminal through the scru…
ctate Jul 20, 2026
76b2974
Judge value-switch exhaustiveness against a sound scrutinee type, nev…
ctate Jul 20, 2026
925dbb2
Count only tsc-reachable, same-scope assignments when widening a fina…
ctate Jul 20, 2026
71bd3cc
Gate narrowing captures on reads, not uses, in ternary arms and guard…
ctate Jul 20, 2026
116a4b6
Exclude tsc-unreachable branch arms from kill joins via one shared ju…
ctate Jul 20, 2026
59148de
Judge scrutinee flow trust by whether a nested assigner can have run yet
ctate Jul 20, 2026
2eec5a4
Stop break/continue-binding walks at function boundaries
ctate Jul 20, 2026
18f3f69
Give tsc-excluded arms no routes and extend the exclusion to for(;fal…
ctate Jul 20, 2026
a50cf45
Close a claimed-terminal plain switch's chain with an unreachable else
ctate Jul 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions changelog.d/ts-ternary-spread-arms.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
fix: **Ternaries with spread-literal arms compile from TypeScript cores**: `parsed === null ? q : { ...q, state: "ok", price: parsed }` — and the nested, `!==`, both-arms, argument, object-field, and `x === null ? { ...fallback } : x` spellings — no longer emit Zig that reads the null-narrowing capture before it binds (`use of undeclared identifier`) or evaluates both arms unconditionally; arms that build values statement-by-statement now lower into per-branch blocks feeding a typed temp, so exactly the taken arm runs, and pure-arm ternaries keep their tight `if`/`orelse` expression forms.
- **Optional switch payloads keep their optional through the capture**: reading a `number | null` payload (directly or via a `const` local) inside its `case` no longer mistypes the value as non-null, which routed `msg.parsed === null ? ... : ...` around the narrowing lowering and emitted `?f64` into `f64` slots.
- **Early-exit guards narrow like early returns**: `if (r === null) break;` in a parse loop (and the `continue`, labeled, multi-statement, `throw`-exit, and `if (x !== null) { ... } else { break/return }` spellings) now narrows the optional for the rest of the loop body the way tsc's flow analysis does, instead of emitting Zig field access on the still-optional value; `if (msg.kind !== "num") break;` narrows the union payload the same way.
- **Guard narrowing ends with its block**: a guard's captures no longer leak past the loop body or branch they narrow — reads after the construct see the unnarrowed value again (matching tsc, whose exit path may bypass the guard) instead of referencing an out-of-scope Zig capture.
- **Early switch-clause breaks stop the build**: an unlabeled `break` that exits a `switch` from inside a clause body now teaches at transpile time — Zig's `break` binds loops, so the old emission jumped past the enclosing loop instead of resuming after the switch.
- **A redundant kind guard no longer un-optionals a switch payload**: scoped kind-narrowing now restores the still-optional markers alongside the substitutions it snapshots, so `const marker = msg.kind === "got" ? 1 : 2;` inside `case "got":` no longer leaves a `number | null` payload typed non-null for the rest of the clause (which emitted `if (parsed != null) parsed + marker else 0` — invalid Zig operands on the `?f64`).
- **Inferred locals from narrowed ternaries value non-optional**: `const picked = q === null ? { ...fallback, price: 0 } : q;` (either polarity, no `: Quote` annotation) now types the local by the arm the condition narrows — `Quote`, exactly as tsc infers — instead of the raw optional, which declared a `?Quote` temporary that failed Zig compilation at its first non-optional use (`expected type 'Quote', found '?Quote'`).
- **A redundant nested `switch` on the same subject hands back the outer capture**: the arm cleanup now repopulates its narrowing maps from the snapshot instead of only deleting the arm's additions, so an inner arm's capture that OVERWROTE the outer arm's entry no longer leaks into the continuation after the inner switch (which emitted the inner capture name after its Zig block had closed — `use of undeclared identifier`).
- **Else-if chains keep post-if narrowing**: `if (x === null) return -1; else if (flag) { n = 2; } return x.v + n;` — and the else-if-else, chained else-if-else-if, and `!==`-polarity spellings — now narrow `x` after the statement like the plain-else form does; the else-if emission path returned before applying the post-if narrowing, so the fall-through read landed on the still-optional value (`optional type does not support field access`).
- **Reassigned `let` bindings never fuse into a `const`**: `let p = next(i); if (p === null) continue; p = { ...p, v: 10 };` no longer fuses the declaration and guard into `const p = next(i) orelse continue;` (Zig: `cannot assign to constant`); the binding stays a `var`, the guard keeps its plain null test, and later reads unwrap the live variable — which the assignment path keeps narrowed across provably non-null writes.
- **A branch that widens a narrowed optional stays widened past the merge**: `if (p === null) return -1; if (flag) { p = null; } if (p === null) return 0;` — the branch-exit restore that keeps narrowing CONTAINED (additions inside a branch die at its exit) no longer also resurrects a narrow the branch killed by assigning null (or a fresh optional-returning call); kills now re-apply after every branch, switch-arm, and kind-guard exit and propagate through nested blocks, so the post-merge re-check tests the live value instead of emitting `p.? == null` (Zig: `comparison of 'f64' with null`). The merge is conservative — a kill on any path that can reach the merge drops the narrow, and the re-check tsc demands anyway always compiles.
- **Compound-guard branches keep those kills dead too**: `if (r !== null && r > 0) { p = null; }` — where the branch emits under the chain's `.?` substitutions — no longer resurrects p's killed narrow when that substitution scope restores its snapshot (its restore ran after the branch re-applied the kill); the scope now rides the same kill-frame protocol as branch and switch-arm exits, and so does the chain-condition emitter, so every full-map narrowing restore in the emitter re-deletes killed entries on exit.
- **A kill on an always-exiting branch stays off the surviving flow**: `if (p.v < 0) { p = null; return -1; } return p.v;` inside a null guard — tsc keeps `p` narrowed at the second return because the killing branch left the function, and the emitter now agrees: a branch that always returns (or throws uncaught) drops its kills at the merge instead of deleting the narrow the surviving read depends on (which emitted field access straight onto the `?P`). Kills on paths that resume inside the function — fall-through arms, `break`/`continue` guards in loops, throws caught by an enclosing `try` — still merge outward and drive the post-merge re-check.
- **A guard in a lifted callback covers the trailing return it precedes**: `xs.map((p) => { if (p === null) throw bad; return p.v; })` lifts the callback as its statement prefix plus the trailing return's expression, and the prefix's narrowing scope closed before that expression emitted — the read landed on the raw `?P` (`optional type does not support field access`); prefix and trailing expression now share one flow scope, and the scope still closes before the callback's siblings in the emitted loop body.
Loading
Loading