main
md 143 lines 5.33 KB
Rendered Raw
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