main
md 152 lines 5.71 KB
Rendered Raw
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.