main
md 111 lines 4.21 KB
Rendered Raw
1 # pruneUnusedScopes
2
3 ## File
4 `src/ReactiveScopes/PruneUnusedScopes.ts`
5
6 ## Purpose
7 This pass converts reactive scopes that have no meaningful outputs into "pruned scopes". A pruned scope is no longer memoized - its instructions are executed unconditionally on every render. This optimization removes unnecessary memoization overhead for scopes that don't produce values that need to be cached.
8
9 ## Input Invariants
10 - The input is a `ReactiveFunction` that has already been transformed into reactive scope form
11 - Scopes have been created and have `declarations`, `reassignments`, and potentially `earlyReturnValue` populated
12 - The pass is called after:
13 - `pruneUnusedLabels` - cleans up unnecessary labels
14 - `pruneNonEscapingScopes` - removes scopes whose outputs don't escape
15 - `pruneNonReactiveDependencies` - removes non-reactive dependencies from scopes
16 - Scopes may already be marked as pruned by earlier passes
17
18 ## Output Guarantees
19 Scopes that meet ALL of the following criteria are converted to `pruned-scope`:
20 - No return statement within the scope
21 - No reassignments (`scope.reassignments.size === 0`)
22 - Either no declarations (`scope.declarations.size === 0`), OR all declarations "bubbled up" from inner scopes
23
24 Pruned scopes:
25 - Keep their original scope metadata (for debugging/tracking)
26 - Keep their instructions intact
27 - Will be executed unconditionally during codegen (no memoization check)
28
29 ## Algorithm
30
31 The pass uses the visitor pattern with `ReactiveFunctionTransform`:
32
33 1. **State Tracking**: A `State` object tracks whether a return statement was encountered:
34 ```typescript
35 type State = {
36 hasReturnStatement: boolean;
37 };
38 ```
39
40 2. **Terminal Visitor** (`visitTerminal`): Checks if any terminal is a `return` statement
41
42 3. **Scope Transform** (`transformScope`): For each scope:
43 - Creates a fresh state for this scope
44 - Recursively visits the scope's contents
45 - Checks pruning criteria:
46 - `!scopeState.hasReturnStatement` - no early return
47 - `scope.reassignments.size === 0` - no reassignments
48 - `scope.declarations.size === 0` OR `!hasOwnDeclaration(scopeBlock)` - no outputs
49
50 4. **hasOwnDeclaration Helper**: Determines if a scope has "own" declarations vs declarations propagated from nested scopes
51
52 ## Edge Cases
53
54 ### Return Statements
55 Scopes containing return statements are preserved because early returns need memoization to avoid re-executing the return check on every render.
56
57 ### Bubbled-Up Declarations
58 When nested scopes are flattened or merged, their declarations may be propagated to parent scopes. The `hasOwnDeclaration` check ensures that parent scopes with only inherited declarations can still be pruned.
59
60 ### Reassignments
61 Scopes with reassignments are kept because the reassignment represents a side effect that needs to be tracked for memoization.
62
63 ### Already-Pruned Scopes
64 The pass operates on `ReactiveScopeBlock` (kind: 'scope'), not `PrunedReactiveScopeBlock`. Scopes already pruned by earlier passes are not revisited.
65
66 ### Interaction with Subsequent Passes
67 The `MergeReactiveScopesThatInvalidateTogether` pass explicitly handles pruned scopes - it does not merge across them.
68
69 ## TODOs
70 None in the source file.
71
72 ## Example
73
74 ### Fixture: `prune-scopes-whose-deps-invalidate-array.js`
75
76 **Input:**
77 ```javascript
78 function Component(props) {
79 const x = [];
80 useHook();
81 x.push(props.value);
82 const y = [x];
83 return [y];
84 }
85 ```
86
87 What happens:
88 - The scope for `x` cannot be memoized because `useHook()` is called inside it
89 - `FlattenScopesWithHooksOrUseHIR` marks scope @0 as `pruned-scope`
90 - `PruneUnusedScopes` doesn't change it further since it's already pruned
91
92 **Output (no memoization for x):**
93 ```javascript
94 function Component(props) {
95 const x = [];
96 useHook();
97 x.push(props.value);
98 const y = [x];
99 return [y];
100 }
101 ```
102
103 ### Key Insight
104
105 The `pruneUnusedScopes` pass is part of a multi-pass pruning strategy:
106 1. `FlattenScopesWithHooksOrUseHIR` - Prunes scopes that contain hook/use calls
107 2. `pruneNonEscapingScopes` - Prunes scopes whose outputs don't escape
108 3. `pruneNonReactiveDependencies` - Removes non-reactive dependencies
109 4. **`pruneUnusedScopes`** - Prunes scopes with no remaining outputs
110
111 This pass acts as a cleanup for scopes that became "empty" after previous pruning passes removed their outputs.