| 1 | # flattenScopesWithHooksOrUseHIR |
| 2 | |
| 3 | ## File |
| 4 | `src/ReactiveScopes/FlattenScopesWithHooksOrUseHIR.ts` |
| 5 | |
| 6 | ## Purpose |
| 7 | This pass removes (flattens) reactive scopes that transitively contain hook calls or `use()` operator calls. The key insight is that: |
| 8 | |
| 9 | 1. **Hooks cannot be called conditionally** - wrapping them in a memoized scope would make them conditionally called based on whether the cache is valid |
| 10 | 2. **The `use()` operator** - while it can be called conditionally in source code, React requires it to be called consistently if the component needs the returned value. Memoizing a scope containing `use()` would also make it conditionally called. |
| 11 | |
| 12 | By running reactive scope inference first (agnostic of hooks), the compiler knows which values "construct together" in the same scope. The pass then removes ALL memoization for scopes containing hook/use calls to ensure they are always executed unconditionally. |
| 13 | |
| 14 | ## Input Invariants |
| 15 | - HIR must have reactive scope terminals already built (pass runs after `BuildReactiveScopeTerminalsHIR`) |
| 16 | - Blocks are visited in order (the pass iterates through `fn.body.blocks`) |
| 17 | - Scope terminals have a `block` (body of the scope) and `fallthrough` (block after the scope) |
| 18 | - Type inference has run so that `getHookKind()` and `isUseOperator()` can identify hooks and use() calls |
| 19 | |
| 20 | ## Output Guarantees |
| 21 | - All scopes that transitively contained a hook or `use()` call are either: |
| 22 | - Converted to `LabelTerminal` - if the scope body is trivial (just the hook call and a goto) |
| 23 | - Converted to `PrunedScopeTerminal` - if the scope body contains other instructions besides the hook call |
| 24 | - The `PrunedScopeTerminal` still tracks the original scope information for downstream passes but will not generate memoization code |
| 25 | - The control flow structure is preserved (same blocks, same fallthroughs) |
| 26 | |
| 27 | ## Algorithm |
| 28 | |
| 29 | ### Phase 1: Identify Scopes Containing Hook/Use Calls |
| 30 | 1. Maintain a stack `activeScopes` of currently "open" reactive scopes |
| 31 | 2. Iterate through all blocks in order |
| 32 | 3. When entering a block: |
| 33 | - Remove any scopes from `activeScopes` whose fallthrough equals the current block (those scopes have ended) |
| 34 | 4. For each instruction in the block: |
| 35 | - If it's a `CallExpression` or `MethodCall` and the callee is a hook or use operator: |
| 36 | - Add all currently active scopes to the `prune` list |
| 37 | - Clear `activeScopes` (these scopes are now marked for pruning) |
| 38 | 5. If the block's terminal is a `scope`: |
| 39 | - Push it onto `activeScopes` |
| 40 | |
| 41 | ### Phase 2: Prune Identified Scopes |
| 42 | For each block ID in `prune`: |
| 43 | 1. Get the scope terminal |
| 44 | 2. Check if the scope body is trivial (single instruction + goto to fallthrough): |
| 45 | - If trivial: Convert to `LabelTerminal` (will be removed by `PruneUnusedLabels`) |
| 46 | - If non-trivial: Convert to `PrunedScopeTerminal` (preserves scope info but skips memoization) |
| 47 | |
| 48 | ## Key Data Structures |
| 49 | |
| 50 | ```typescript |
| 51 | // Stack tracking currently open scopes |
| 52 | activeScopes: Array<{block: BlockId; fallthrough: BlockId}> |
| 53 | |
| 54 | // List of block IDs whose scope terminals should be pruned |
| 55 | prune: Array<BlockId> |
| 56 | |
| 57 | // Terminal types used |
| 58 | LabelTerminal: {kind: 'label', block, fallthrough, id, loc} |
| 59 | PrunedScopeTerminal: {kind: 'pruned-scope', block, fallthrough, scope, id, loc} |
| 60 | ReactiveScopeTerminal: {kind: 'scope', block, fallthrough, scope, id, loc} |
| 61 | ``` |
| 62 | |
| 63 | ## Edge Cases |
| 64 | |
| 65 | ### Nested Scopes |
| 66 | When a hook is found in an inner scope, ALL enclosing scopes are also pruned (the hook call would become conditional if any outer scope were memoized). |
| 67 | |
| 68 | ### Method Call Hooks |
| 69 | Handles both `CallExpression` (e.g., `useHook(...)`) and `MethodCall` (e.g., `obj.useHook(...)`). |
| 70 | |
| 71 | ### Trivial Hook-Only Scopes |
| 72 | If a scope exists just for a hook call (single instruction + goto), it's converted to a `LabelTerminal` which is a simpler structure that gets cleaned up by later passes. |
| 73 | |
| 74 | ### Multiple Hooks in Sequence |
| 75 | Once the first hook is encountered, all active scopes are pruned and cleared, so subsequent hooks in outer scopes still work correctly. |
| 76 | |
| 77 | ## TODOs |
| 78 | None explicitly marked in the source file. |
| 79 | |
| 80 | ## Example |
| 81 | |
| 82 | ### Fixture: `nested-scopes-hook-call.js` |
| 83 | |
| 84 | **Input:** |
| 85 | ```javascript |
| 86 | function component(props) { |
| 87 | let x = []; |
| 88 | let y = []; |
| 89 | y.push(useHook(props.foo)); |
| 90 | x.push(y); |
| 91 | return x; |
| 92 | } |
| 93 | ``` |
| 94 | |
| 95 | **Before FlattenScopesWithHooksOrUseHIR:** |
| 96 | ``` |
| 97 | bb0: |
| 98 | [1] Scope @0 [1:22] block=bb6 fallthrough=bb7 // Outer scope for x |
| 99 | bb6: |
| 100 | [2] $22 = Array [] // x = [] |
| 101 | [3] StoreLocal x = $22 |
| 102 | [4] Scope @1 [4:17] block=bb8 fallthrough=bb9 // Inner scope for y |
| 103 | bb8: |
| 104 | [5] $25 = Array [] // y = [] |
| 105 | [6] StoreLocal y = $25 |
| 106 | ... |
| 107 | [10] $33 = Call useHook(...) // <-- Hook call here! |
| 108 | [11] MethodCall y.push($33) |
| 109 | ``` |
| 110 | |
| 111 | **After FlattenScopesWithHooksOrUseHIR:** |
| 112 | ``` |
| 113 | bb0: |
| 114 | [1] <pruned> Scope @0 [1:22] block=bb6 fallthrough=bb7 // PRUNED |
| 115 | bb6: |
| 116 | [2] $22 = Array [] |
| 117 | [3] StoreLocal x = $22 |
| 118 | [4] <pruned> Scope @1 [4:17] block=bb8 fallthrough=bb9 // PRUNED |
| 119 | bb8: |
| 120 | [5] $25 = Array [] |
| 121 | [6] StoreLocal y = $25 |
| 122 | ... |
| 123 | [12] Label block=bb10 fallthrough=bb11 // Hook call converted to label |
| 124 | bb10: |
| 125 | [13] $33 = Call useHook(...) |
| 126 | [14] Goto bb11 |
| 127 | ... |
| 128 | ``` |
| 129 | |
| 130 | **Final Output (no memoization):** |
| 131 | ```javascript |
| 132 | function component(props) { |
| 133 | const x = []; |
| 134 | const y = []; |
| 135 | y.push(useHook(props.foo)); |
| 136 | x.push(y); |
| 137 | return x; |
| 138 | } |
| 139 | ``` |
| 140 | |
| 141 | Notice that: |
| 142 | 1. Both scope @0 and scope @1 are marked as `<pruned>` because the hook call is inside scope @1, which is inside scope @0 |
| 143 | 2. The final output has no memoization wrappers - just the raw code |