| 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 |