main
md 130 lines 4.65 KB
Rendered Raw
1 # pruneNonEscapingScopes
2
3 ## File
4 `src/ReactiveScopes/PruneNonEscapingScopes.ts`
5
6 ## Purpose
7 This pass prunes (removes) reactive scopes whose outputs do not "escape" the component and therefore do not need to be memoized. A value "escapes" in two ways:
8
9 1. **Returned from the function** - The value is directly returned or transitively aliased by a return value
10 2. **Passed to a hook** - Any value passed as an argument to a hook may be stored by React internally (e.g., the closure passed to `useEffect`)
11
12 The key insight is that values which never escape the component boundary can be safely recreated on each render without affecting the behavior of consumers.
13
14 ## Input Invariants
15 - The input is a `ReactiveFunction` after scope blocks have been identified
16 - Reactive scopes have been assigned to instructions
17 - The pass runs after `BuildReactiveFunction` and `PruneUnusedLabels`, before `PruneNonReactiveDependencies`
18
19 ## Output Guarantees
20 - **Scopes with non-escaping outputs are removed** - Their instructions are inlined back into the parent scope/function body
21 - **Scopes with escaping outputs are retained** - Values that escape via return or hook arguments remain memoized
22 - **Transitive dependencies of escaping scopes are preserved** - If an escaping scope depends on a non-escaping value, that value's scope is also retained to prevent unnecessary invalidation
23 - **`FinishMemoize` instructions are marked `pruned=true`** - When a scope is pruned, the associated memoization instructions are flagged
24
25 ## Algorithm
26
27 ### Phase 1: Build the Dependency Graph
28 Using `CollectDependenciesVisitor`, build:
29 - **Identifier nodes** - Each node tracks memoization level, dependencies, scopes, and whether ultimately memoized
30 - **Scope nodes** - Each scope tracks its dependencies
31 - **Escaping values** - Identifiers that escape via return or hook arguments
32
33 ### Phase 2: Classify Memoization Levels
34 Each instruction value is classified:
35 - `Memoized`: Arrays, objects, function calls, `new` expressions - always potentially aliasing
36 - `Conditional`: Conditional/logical expressions, property loads - memoized only if dependencies are memoized
37 - `Unmemoized`: JSX elements (when `memoizeJsxElements` is false), DeclareLocal
38 - `Never`: Primitives, LoadGlobal, binary/unary expressions - can be cheaply compared
39
40 ### Phase 3: Compute Memoized Identifiers
41 `computeMemoizedIdentifiers()` performs a graph traversal starting from escaping values:
42 - For each escaping value, recursively visit its dependencies
43 - Mark values and their scopes based on memoization level
44 - When marking a scope, force-memoize all its dependencies
45
46 ### Phase 4: Prune Scopes
47 `PruneScopesTransform` visits each scope block:
48 - If any scope output is in the memoized set, keep the scope
49 - If no outputs are memoized, replace the scope block with its inlined instructions
50
51 ## Edge Cases
52
53 ### Interleaved Mutations
54 ```javascript
55 const a = [props.a]; // independently memoizable, non-escaping
56 const b = [];
57 const c = {};
58 c.a = a; // c captures a, but c doesn't escape
59 b.push(props.b); // b escapes via return
60 return b;
61 ```
62 Here `a` does not directly escape, but it is a dependency of the scope containing `b`. The algorithm correctly identifies that `a`'s scope must be preserved.
63
64 ### Hook Arguments Escape
65 Values passed to hooks are treated as escaping because hooks may store references internally.
66
67 ### JSX Special Handling
68 JSX elements are marked as `Unmemoized` by default because React.memo() can handle dynamic memoization.
69
70 ### noAlias Functions
71 If a function signature indicates `noAlias === true`, its arguments are not treated as escaping.
72
73 ### Reassignments
74 When a scope reassigns a variable, the scope is added as a dependency of that variable.
75
76 ## TODOs
77 None explicitly in the source file.
78
79 ## Example
80
81 ### Fixture: `escape-analysis-non-escaping-interleaved-allocating-dependency.js`
82
83 **Input:**
84 ```javascript
85 function Component(props) {
86 const a = [props.a];
87
88 const b = [];
89 const c = {};
90 c.a = a;
91 b.push(props.b);
92
93 return b;
94 }
95 ```
96
97 **Output:**
98 ```javascript
99 function Component(props) {
100 const $ = _c(5);
101 let t0;
102 if ($[0] !== props.a) {
103 t0 = [props.a];
104 $[0] = props.a;
105 $[1] = t0;
106 } else {
107 t0 = $[1];
108 }
109 const a = t0; // a is memoized even though it doesn't escape directly
110
111 let b;
112 if ($[2] !== a || $[3] !== props.b) {
113 b = [];
114 const c = {}; // c is NOT memoized - it doesn't escape
115 c.a = a;
116 b.push(props.b);
117 $[2] = a;
118 $[3] = props.b;
119 $[4] = b;
120 } else {
121 b = $[4];
122 }
123 return b;
124 }
125 ```
126
127 Key observations:
128 - `a` is memoized because it's a dependency of the scope containing `b`
129 - `c` is not separately memoized because it doesn't escape
130 - `b` is memoized because it's returned