| 1 | # pruneUnusedLabels |
| 2 | |
| 3 | ## File |
| 4 | `src/ReactiveScopes/PruneUnusedLabels.ts` |
| 5 | |
| 6 | ## Purpose |
| 7 | The `pruneUnusedLabels` pass optimizes control flow by: |
| 8 | |
| 9 | 1. **Flattening labeled terminals** where the label is not reachable via a `break` or `continue` statement |
| 10 | 2. **Marking labels as implicit** for terminals where the label exists but is never targeted |
| 11 | |
| 12 | This pass removes unnecessary labeled blocks that were introduced during compilation but serve no control flow purpose in the final output. JavaScript labeled statements are only needed when there is a corresponding `break label` or `continue label` that targets them. |
| 13 | |
| 14 | ## Input Invariants |
| 15 | - The input is a `ReactiveFunction` (after conversion from HIR) |
| 16 | - All `break` and `continue` terminals have: |
| 17 | - A `target` (BlockId) indicating which label they jump to |
| 18 | - A `targetKind` that is one of: `'implicit'`, `'labeled'`, or `'unlabeled'` |
| 19 | - Each `ReactiveTerminalStatement` has an optional `label` field containing `id` and `implicit` |
| 20 | - The pass runs after `assertWellFormedBreakTargets` which validates break/continue targets |
| 21 | |
| 22 | ## Output Guarantees |
| 23 | - Labeled terminals where the label is unreachable are flattened into their parent block |
| 24 | - When flattening, trailing unlabeled `break` statements (that would just fall through) are removed |
| 25 | - Labels that exist but are never targeted have their `implicit` flag set to `true` |
| 26 | - Control flow semantics are preserved - only structurally unnecessary labels are removed |
| 27 | |
| 28 | ## Algorithm |
| 29 | |
| 30 | The pass uses a two-phase approach with a single traversal: |
| 31 | |
| 32 | **Phase 1: Collect reachable labels** |
| 33 | ```typescript |
| 34 | if ((terminal.kind === 'break' || terminal.kind === 'continue') && |
| 35 | terminal.targetKind === 'labeled') { |
| 36 | state.add(terminal.target); // Mark this label as reachable |
| 37 | } |
| 38 | ``` |
| 39 | |
| 40 | **Phase 2: Transform terminals** |
| 41 | ```typescript |
| 42 | const isReachableLabel = stmt.label !== null && state.has(stmt.label.id); |
| 43 | |
| 44 | if (stmt.terminal.kind === 'label' && !isReachableLabel) { |
| 45 | // Flatten: extract block contents, removing trailing unlabeled break |
| 46 | const block = [...stmt.terminal.block]; |
| 47 | const last = block.at(-1); |
| 48 | if (last?.kind === 'terminal' && last.terminal.kind === 'break' && |
| 49 | last.terminal.target === null) { |
| 50 | block.pop(); // Remove trailing break |
| 51 | } |
| 52 | return {kind: 'replace-many', value: block}; |
| 53 | } else { |
| 54 | if (!isReachableLabel && stmt.label != null) { |
| 55 | stmt.label.implicit = true; // Mark as implicit |
| 56 | } |
| 57 | return {kind: 'keep'}; |
| 58 | } |
| 59 | ``` |
| 60 | |
| 61 | ## Edge Cases |
| 62 | |
| 63 | ### Trailing Break Removal |
| 64 | When flattening a labeled block, if the last statement is an unlabeled break (`target === null`), it is removed since it would just fall through anyway. |
| 65 | |
| 66 | ### Implicit vs Labeled Breaks |
| 67 | Only breaks with `targetKind === 'labeled'` count toward label reachability. Implicit breaks (fallthrough) and unlabeled breaks don't make a label "used". |
| 68 | |
| 69 | ### Continue Statements |
| 70 | Both `break` and `continue` with labeled targets mark the label as reachable. |
| 71 | |
| 72 | ### Non-Label Terminals with Labels |
| 73 | Other terminal types (like `if`, `while`, `for`) can also have labels. If unreachable, these labels are marked implicit but the terminal is not flattened. |
| 74 | |
| 75 | ## TODOs |
| 76 | None in the source file. |
| 77 | |
| 78 | ## Example |
| 79 | |
| 80 | ### Fixture: `unconditional-break-label.js` |
| 81 | |
| 82 | **Input:** |
| 83 | ```javascript |
| 84 | function foo(a) { |
| 85 | let x = 0; |
| 86 | bar: { |
| 87 | x = 1; |
| 88 | break bar; |
| 89 | } |
| 90 | return a + x; |
| 91 | } |
| 92 | ``` |
| 93 | |
| 94 | **Output (after full compilation):** |
| 95 | ```javascript |
| 96 | function foo(a) { |
| 97 | return a + 1; |
| 98 | } |
| 99 | ``` |
| 100 | |
| 101 | The labeled block `bar: { ... }` is removed because after the pass runs, constant propagation and dead code elimination further simplify the code. |
| 102 | |
| 103 | ### Fixture: `conditional-break-labeled.js` |
| 104 | |
| 105 | **Input:** |
| 106 | ```javascript |
| 107 | function Component(props) { |
| 108 | const a = []; |
| 109 | a.push(props.a); |
| 110 | label: { |
| 111 | if (props.b) { |
| 112 | break label; |
| 113 | } |
| 114 | a.push(props.c); |
| 115 | } |
| 116 | a.push(props.d); |
| 117 | return a; |
| 118 | } |
| 119 | ``` |
| 120 | |
| 121 | **Output:** |
| 122 | ```javascript |
| 123 | function Component(props) { |
| 124 | const $ = _c(5); |
| 125 | let a; |
| 126 | if ($[0] !== props.a || $[1] !== props.b || |
| 127 | $[2] !== props.c || $[3] !== props.d) { |
| 128 | a = []; |
| 129 | a.push(props.a); |
| 130 | bb0: { |
| 131 | if (props.b) { |
| 132 | break bb0; |
| 133 | } |
| 134 | a.push(props.c); |
| 135 | } |
| 136 | a.push(props.d); |
| 137 | // ... cache updates |
| 138 | } else { |
| 139 | a = $[4]; |
| 140 | } |
| 141 | return a; |
| 142 | } |
| 143 | ``` |
| 144 | |
| 145 | The labeled block `bb0: { ... }` is preserved because the `break bb0` inside the conditional targets this label. |