main
md 286 lines 8.08 KB
Rendered Raw
1 # codegenReactiveFunction
2
3 ## File
4 `src/ReactiveScopes/CodegenReactiveFunction.ts`
5
6 ## Purpose
7 This is the final pass that converts the ReactiveFunction representation back into a Babel AST. It generates the memoization code that makes React components and hooks efficient by:
8 1. Creating the `useMemoCache` call to allocate cache slots
9 2. Generating dependency comparisons to check if values have changed
10 3. Emitting conditional blocks that skip computation when cached values are valid
11 4. Storing computed values in the cache
12 5. Loading cached values when dependencies haven't changed
13
14 ## Input Invariants
15 - The ReactiveFunction has been through all prior passes
16 - All identifiers that need names have been promoted and renamed
17 - Reactive scopes have finalized `dependencies`, `declarations`, and `reassignments`
18 - Early returns have been transformed with sentinel values (via `propagateEarlyReturns`)
19 - Pruned scopes are marked with `kind: 'pruned-scope'`
20 - Unique identifiers set is available to avoid naming conflicts
21
22 ## Output Guarantees
23 - Returns a `CodegenFunction` with Babel AST `body`
24 - All reactive scopes become if-else blocks checking dependencies
25 - The `$` cache array is properly sized with `useMemoCache(n)`
26 - Each dependency and output gets its own cache slot
27 - Pruned scopes emit their instructions inline without memoization
28 - Early returns use the sentinel pattern with post-scope checks
29 - Statistics are collected: `memoSlotsUsed`, `memoBlocks`, `memoValues`, etc.
30
31 ## Algorithm
32
33 ### Entry Point: codegenFunction
34 ```typescript
35 export function codegenFunction(fn: ReactiveFunction): Result<CodegenFunction, CompilerError> {
36 const cx = new Context(...);
37
38 // Optional: Fast Refresh source hash tracking
39 if (enableResetCacheOnSourceFileChanges) {
40 fastRefreshState = { cacheIndex: cx.nextCacheIndex, hash: sha256(source) };
41 }
42
43 const compiled = codegenReactiveFunction(cx, fn);
44
45 // Prepend useMemoCache call if any cache slots used
46 if (cacheCount !== 0) {
47 body.unshift(
48 t.variableDeclaration('const', [
49 t.variableDeclarator(
50 t.identifier('$'),
51 t.callExpression(t.identifier('useMemoCache'), [t.numericLiteral(cacheCount)])
52 )
53 ])
54 );
55 }
56
57 return compiled;
58 }
59 ```
60
61 ### Context Class
62 Tracks state during codegen:
63 ```typescript
64 class Context {
65 #nextCacheIndex: number = 0; // Allocates cache slots
66 #declarations: Set<DeclarationId> = new Set(); // Tracks declared variables
67 temp: Temporaries; // Maps identifiers to their expressions
68 errors: CompilerError;
69
70 get nextCacheIndex(): number {
71 return this.#nextCacheIndex++; // Returns and increments
72 }
73 }
74 ```
75
76 ### codegenReactiveScope
77 The core of memoization code generation:
78
79 ```typescript
80 function codegenReactiveScope(cx: Context, statements: Array<t.Statement>,
81 scope: ReactiveScope, block: ReactiveBlock): void {
82 const changeExpressions: Array<t.Expression> = [];
83 const cacheStoreStatements: Array<t.Statement> = [];
84 const cacheLoadStatements: Array<t.Statement> = [];
85
86 // 1. Generate dependency checks
87 for (const dep of scope.dependencies) {
88 const index = cx.nextCacheIndex;
89 changeExpressions.push(
90 t.binaryExpression('!==',
91 t.memberExpression(t.identifier('$'), t.numericLiteral(index), true),
92 codegenDependency(cx, dep)
93 )
94 );
95 cacheStoreStatements.push(
96 t.assignmentExpression('=', $[index], dep)
97 );
98 }
99
100 // 2. Generate output cache slots
101 for (const {identifier} of scope.declarations) {
102 const index = cx.nextCacheIndex;
103 // Declare variable if not already declared
104 if (!cx.hasDeclared(identifier)) {
105 statements.push(t.variableDeclaration('let', [t.variableDeclarator(name, null)]));
106 }
107 cacheLoads.push({name, index, value: name});
108 }
109
110 // 3. Build test condition
111 let testCondition = changeExpressions.reduce((acc, expr) =>
112 t.logicalExpression('||', acc, expr)
113 );
114
115 // 4. If no dependencies, use sentinel check
116 if (testCondition === null) {
117 testCondition = t.binaryExpression('===',
118 $[firstOutputIndex],
119 t.callExpression(Symbol.for, ['react.memo_cache_sentinel'])
120 );
121 }
122
123 // 5. Generate the memoization if-else
124 statements.push(
125 t.ifStatement(
126 testCondition,
127 computationBlock, // Compute + store in cache
128 cacheLoadBlock // Load from cache
129 )
130 );
131 }
132 ```
133
134 ### Generated Structure
135 For a scope with dependencies `[a, b]` and output `result`:
136
137 ```javascript
138 let result;
139 if ($[0] !== a || $[1] !== b) {
140 // Computation block
141 result = compute(a, b);
142
143 // Store dependencies
144 $[0] = a;
145 $[1] = b;
146
147 // Store output
148 $[2] = result;
149 } else {
150 // Load from cache
151 result = $[2];
152 }
153 ```
154
155 ### Early Return Handling
156 When a scope has an early return (from `propagateEarlyReturns`):
157
158 ```typescript
159 // Before scope: initialize sentinel
160 t0 = Symbol.for("react.early_return_sentinel");
161
162 // Scope generates labeled block
163 bb0: {
164 // ... computation ...
165 if (cond) {
166 t0 = returnValue;
167 break bb0;
168 }
169 }
170
171 // After scope: check for early return
172 if (t0 !== Symbol.for("react.early_return_sentinel")) {
173 return t0;
174 }
175 ```
176
177 ### Pruned Scopes
178 Pruned scopes emit their instructions inline without memoization:
179 ```typescript
180 case 'pruned-scope': {
181 const scopeBlock = codegenBlockNoReset(cx, item.instructions);
182 statements.push(...scopeBlock.body); // Inline, no memoization
183 break;
184 }
185 ```
186
187 ## Edge Cases
188
189 ### Zero Dependencies
190 Scopes with no dependencies use a sentinel value check instead:
191 ```javascript
192 if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
193 // First render only
194 }
195 ```
196
197 ### Fast Refresh / HMR
198 When `enableResetCacheOnSourceFileChanges` is enabled, the generated code includes a source hash check that resets the cache when the source changes:
199 ```javascript
200 if ($[0] !== "source_hash_abc123") {
201 for (let $i = 0; $i < cacheCount; $i++) {
202 $[$i] = Symbol.for("react.memo_cache_sentinel");
203 }
204 $[0] = "source_hash_abc123";
205 }
206 ```
207
208
209 ### Labeled Breaks
210 Control flow with labeled breaks (for early returns or loop exits) uses `codegenLabel` to generate consistent label names:
211 ```typescript
212 function codegenLabel(id: BlockId): string {
213 return `bb${id}`; // e.g., "bb0", "bb1"
214 }
215 ```
216
217 ### Nested Functions
218 Function expressions and object methods are recursively processed with their own contexts.
219
220 ### FBT/Internationalization
221 Special handling for FBT operands ensures they're memoized in the same scope for correct internationalization behavior.
222
223 ## Statistics Collected
224 ```typescript
225 type CodegenFunction = {
226 memoSlotsUsed: number; // Total cache slots allocated
227 memoBlocks: number; // Number of reactive scopes
228 memoValues: number; // Total memoized values
229 prunedMemoBlocks: number; // Scopes that were pruned
230 prunedMemoValues: number; // Values in pruned scopes
231 hasInferredEffect: boolean;
232 };
233 ```
234
235 ## TODOs
236 None in the source file.
237
238 ## Example
239
240 ### Fixture: `simple.js`
241
242 **Input:**
243 ```javascript
244 export default function foo(x, y) {
245 if (x) {
246 return foo(false, y);
247 }
248 return [y * 10];
249 }
250 ```
251
252 **Generated Code:**
253 ```javascript
254 import { c as _c } from "react/compiler-runtime";
255 export default function foo(x, y) {
256 const $ = _c(4); // Allocate 4 cache slots
257 if (x) {
258 let t0;
259 if ($[0] !== y) { // Check dependency
260 t0 = foo(false, y); // Compute
261 $[0] = y; // Store dependency
262 $[1] = t0; // Store output
263 } else {
264 t0 = $[1]; // Load from cache
265 }
266 return t0;
267 }
268 const t0 = y * 10;
269 let t1;
270 if ($[2] !== t0) { // Check dependency
271 t1 = [t0]; // Compute
272 $[2] = t0; // Store dependency
273 $[3] = t1; // Store output
274 } else {
275 t1 = $[3]; // Load from cache
276 }
277 return t1;
278 }
279 ```
280
281 Key observations:
282 - `_c(4)` allocates 4 cache slots total
283 - First scope uses slots 0-1: slot 0 for `y` dependency, slot 1 for `t0` output
284 - Second scope uses slots 2-3: slot 2 for `t0` (the computed `y * 10`), slot 3 for `t1` (the array)
285 - Each scope has an if-else structure: compute/store vs load
286 - The memoization ensures referential equality of the returned array when `y` hasn't changed