| 1 | # validatePreservedManualMemoization |
| 2 | |
| 3 | ## File |
| 4 | `src/Validation/ValidatePreservedManualMemoization.ts` |
| 5 | |
| 6 | ## Purpose |
| 7 | Validates that all explicit manual memoization (`useMemo`/`useCallback`) from the original source code is accurately preserved in the compiled output. This ensures that values the developer intended to be memoized remain memoized after compilation. |
| 8 | |
| 9 | ## Input Invariants |
| 10 | - Operates on ReactiveFunction (post-reactive scope inference) |
| 11 | - Manual memoization markers (`StartMemoize`/`FinishMemoize`) are present from earlier passes |
| 12 | - Scopes have been assigned and merged as appropriate |
| 13 | |
| 14 | ## Validation Rules |
| 15 | This pass validates three conditions: |
| 16 | |
| 17 | ### 1. Dependencies not mutated later |
| 18 | Validates that dependencies of manual memoization are not mutated after the memoization call: |
| 19 | ``` |
| 20 | Existing memoization could not be preserved. This dependency may be modified later |
| 21 | ``` |
| 22 | |
| 23 | ### 2. Inferred dependencies match source |
| 24 | Validates that the compiler's inferred dependencies match the manually specified dependencies: |
| 25 | ``` |
| 26 | Existing memoization could not be preserved. The inferred dependencies did not match |
| 27 | the manually specified dependencies, which could cause the value to change more or |
| 28 | less frequently than expected. The inferred dependency was `X`, but the source |
| 29 | dependencies were [Y, Z]. |
| 30 | ``` |
| 31 | |
| 32 | ### 3. Output value is memoized |
| 33 | Validates that the memoized value actually ends up in a reactive scope: |
| 34 | ``` |
| 35 | Existing memoization could not be preserved. This value was memoized in source but |
| 36 | not in compilation output |
| 37 | ``` |
| 38 | |
| 39 | ## Algorithm |
| 40 | |
| 41 | ### State Management |
| 42 | The visitor tracks: |
| 43 | - `scopes: Set<ScopeId>` - All completed reactive scopes |
| 44 | - `prunedScopes: Set<ScopeId>` - Scopes that were pruned |
| 45 | - `temporaries: Map<IdentifierId, ManualMemoDependency>` - Temporary variable mappings |
| 46 | - `manualMemoState: ManualMemoBlockState | null` - Current manual memoization context |
| 47 | |
| 48 | ### ManualMemoBlockState |
| 49 | ```typescript |
| 50 | type ManualMemoBlockState = { |
| 51 | reassignments: Map<DeclarationId, Set<Identifier>>; // Track inlined useMemo reassignments |
| 52 | loc: SourceLocation; // Source location for errors |
| 53 | decls: Set<DeclarationId>; // Declarations within the memo block |
| 54 | depsFromSource: Array<ManualMemoDependency> | null; // Original deps from source |
| 55 | manualMemoId: number; // Unique ID for this memoization |
| 56 | }; |
| 57 | ``` |
| 58 | |
| 59 | ### Processing Flow |
| 60 | |
| 61 | 1. **On `StartMemoize` instruction:** |
| 62 | - Validate that dependencies' scopes have completed (not mutated later) |
| 63 | - Initialize `manualMemoState` with source dependencies |
| 64 | - Push error if any dependency's scope hasn't completed yet |
| 65 | |
| 66 | 2. **During memo block (between Start/Finish):** |
| 67 | - Track all declarations made within the block |
| 68 | - Track reassignments for inlined useMemo handling |
| 69 | - Record property loads and temporaries |
| 70 | |
| 71 | 3. **On scope completion:** |
| 72 | - Validate each scope dependency against source dependencies using `compareDeps()` |
| 73 | - An inferred dependency matches if: |
| 74 | - Root identifiers are the same (same named variable) |
| 75 | - Paths are identical, OR |
| 76 | - Inferred path is more specific (not involving `.current` refs) |
| 77 | |
| 78 | 4. **On `FinishMemoize` instruction:** |
| 79 | - Validate that the memoized value is in a completed scope |
| 80 | - Handle inlined useMemo with reassignment tracking |
| 81 | - Push error if value is unmemoized |
| 82 | |
| 83 | ### Dependency Comparison Results |
| 84 | ```typescript |
| 85 | enum CompareDependencyResult { |
| 86 | Ok = 0, // Dependencies match |
| 87 | RootDifference = 1, // Different root variables |
| 88 | PathDifference = 2, // Different property paths |
| 89 | Subpath = 3, // Inferred is less specific |
| 90 | RefAccessDifference = 4, // ref.current access differs |
| 91 | } |
| 92 | ``` |
| 93 | |
| 94 | ## Edge Cases |
| 95 | |
| 96 | ### Inlined useMemo Handling |
| 97 | When useMemo is inlined, it produces `let` declarations followed by reassignments. The pass tracks these reassignments to ensure all code paths produce memoized values. |
| 98 | |
| 99 | ### Ref Access |
| 100 | Special handling for `.current` property access on refs. Since `ref_prev === ref_new` does not imply `ref_prev.current === ref_new.current`, the pass is strict about ref access differences. |
| 101 | |
| 102 | ### More Specific Dependencies |
| 103 | If the compiler infers a more specific dependency (e.g., `obj.prop.value` instead of `obj`), this is acceptable as long as it doesn't involve ref access. |
| 104 | |
| 105 | ## TODOs |
| 106 | None found in the source. |
| 107 | |
| 108 | ## Example |
| 109 | |
| 110 | ### Fixture: `error.preserve-use-memo-ref-missing-reactive.ts` |
| 111 | |
| 112 | **Input:** |
| 113 | ```javascript |
| 114 | // @validatePreserveExistingMemoizationGuarantees |
| 115 | import {useCallback, useRef} from 'react'; |
| 116 | |
| 117 | function useFoo({cond}) { |
| 118 | const ref1 = useRef<undefined | (() => undefined)>(); |
| 119 | const ref2 = useRef<undefined | (() => undefined)>(); |
| 120 | const ref = cond ? ref1 : ref2; |
| 121 | |
| 122 | return useCallback(() => { |
| 123 | if (ref != null) { |
| 124 | ref.current(); |
| 125 | } |
| 126 | }, []); |
| 127 | } |
| 128 | ``` |
| 129 | |
| 130 | **Error:** |
| 131 | ``` |
| 132 | Found 1 error: |
| 133 | |
| 134 | Compilation Skipped: Existing memoization could not be preserved |
| 135 | |
| 136 | React Compiler has skipped optimizing this component because the existing manual |
| 137 | memoization could not be preserved. The inferred dependencies did not match the |
| 138 | manually specified dependencies, which could cause the value to change more or |
| 139 | less frequently than expected. The inferred dependency was `ref`, but the source |
| 140 | dependencies were []. Inferred dependency not present in source. |
| 141 | |
| 142 | error.preserve-use-memo-ref-missing-reactive.ts:9:21 |
| 143 | > 9 | return useCallback(() => { |
| 144 | | ^^^^^^^ |
| 145 | > 10 | if (ref != null) { |
| 146 | > 11 | ref.current(); |
| 147 | > 12 | } |
| 148 | > 13 | }, []); |
| 149 | | ^^^^ Could not preserve existing manual memoization |
| 150 | ``` |
| 151 | |
| 152 | **Why it fails:** The callback uses `ref` which is conditionally assigned based on `cond`. The compiler infers `ref` as a dependency, but the source specifies an empty dependency array `[]`. This mismatch means the memoization cannot be preserved as-is. |