pruneUnusedScopes
File
src/ReactiveScopes/PruneUnusedScopes.ts
Purpose
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.
Input Invariants
- The input is a
ReactiveFunctionthat has already been transformed into reactive scope form - Scopes have been created and have
declarations,reassignments, and potentiallyearlyReturnValuepopulated - The pass is called after:
pruneUnusedLabels- cleans up unnecessary labelspruneNonEscapingScopes- removes scopes whose outputs don't escapepruneNonReactiveDependencies- removes non-reactive dependencies from scopes
- Scopes may already be marked as pruned by earlier passes
Output Guarantees
Scopes that meet ALL of the following criteria are converted to pruned-scope:
- No return statement within the scope
- No reassignments (
scope.reassignments.size === 0) - Either no declarations (
scope.declarations.size === 0), OR all declarations "bubbled up" from inner scopes
Pruned scopes:
- Keep their original scope metadata (for debugging/tracking)
- Keep their instructions intact
- Will be executed unconditionally during codegen (no memoization check)
Algorithm
The pass uses the visitor pattern with ReactiveFunctionTransform:
-
State Tracking: A
Stateobject tracks whether a return statement was encountered:type State = { hasReturnStatement: boolean; }; -
Terminal Visitor (
visitTerminal): Checks if any terminal is areturnstatement -
Scope Transform (
transformScope): For each scope:- Creates a fresh state for this scope
- Recursively visits the scope's contents
- Checks pruning criteria:
!scopeState.hasReturnStatement- no early returnscope.reassignments.size === 0- no reassignmentsscope.declarations.size === 0OR!hasOwnDeclaration(scopeBlock)- no outputs
-
hasOwnDeclaration Helper: Determines if a scope has "own" declarations vs declarations propagated from nested scopes
Edge Cases
Return Statements
Scopes containing return statements are preserved because early returns need memoization to avoid re-executing the return check on every render.
Bubbled-Up Declarations
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.
Reassignments
Scopes with reassignments are kept because the reassignment represents a side effect that needs to be tracked for memoization.
Already-Pruned Scopes
The pass operates on ReactiveScopeBlock (kind: 'scope'), not PrunedReactiveScopeBlock. Scopes already pruned by earlier passes are not revisited.
Interaction with Subsequent Passes
The MergeReactiveScopesThatInvalidateTogether pass explicitly handles pruned scopes - it does not merge across them.
TODOs
None in the source file.
Example
Fixture: prune-scopes-whose-deps-invalidate-array.js
Input:
function Component(props) {
const x = [];
useHook();
x.push(props.value);
const y = [x];
return [y];
}
What happens:
- The scope for
xcannot be memoized becauseuseHook()is called inside it FlattenScopesWithHooksOrUseHIRmarks scope @0 aspruned-scopePruneUnusedScopesdoesn't change it further since it's already pruned
Output (no memoization for x):
function Component(props) {
const x = [];
useHook();
x.push(props.value);
const y = [x];
return [y];
}
Key Insight
The pruneUnusedScopes pass is part of a multi-pass pruning strategy:
FlattenScopesWithHooksOrUseHIR- Prunes scopes that contain hook/use callspruneNonEscapingScopes- Prunes scopes whose outputs don't escapepruneNonReactiveDependencies- Removes non-reactive dependenciespruneUnusedScopes- Prunes scopes with no remaining outputs
This pass acts as a cleanup for scopes that became "empty" after previous pruning passes removed their outputs.