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