propagateScopeDependenciesHIR
File
src/HIR/PropagateScopeDependenciesHIR.ts
Purpose
The propagateScopeDependenciesHIR pass is responsible for computing and assigning the dependencies for each reactive scope in the compiled function. Dependencies are the external values that a scope reads, which determine when the scope needs to re-execute. This is a critical step for memoization correctness - the compiler must track exactly which values a scope depends on so it can generate proper cache invalidation checks.
The pass also populates:
scope.dependencies- The set ofReactiveScopeDependencyobjects the scope readsscope.declarations- Values declared within the scope that are used outside it
Input Invariants
- Reactive scopes must be established (pass runs after
BuildReactiveScopeTerminalsHIR) - The function must be in SSA form
InferMutationAliasingRangesmust have run to establish when values are being mutatedInferReactivePlacesmarks which identifiers are reactive- Scope ranges have been aligned and normalized by earlier passes
Output Guarantees
After this pass completes:
-
Each
ReactiveScope.dependenciescontains the minimal set of dependencies that:- Were declared before the scope started
- Are read within the scope
- Are not ref values (which are always mutable)
- Are not object methods (which get codegen'd back into object literals)
-
Each
ReactiveScope.declarationscontains identifiers that:- Are assigned within the scope
- Are used outside the scope (need to be exposed as scope outputs)
-
Property load chains are resolved to their root identifiers with paths (e.g.,
props.user.namebecomes{identifier: props, path: ["user", "name"]}) -
Optional chains are handled correctly, distinguishing between
a?.banda.baccess types
Algorithm
Phase 1: Build Sidemaps
-
findTemporariesUsedOutsideDeclaringScope: Identifies temporaries that are used outside the scope where they were declared (cannot be hoisted/reordered safely)
-
collectTemporariesSidemap: Creates a mapping from temporary IdentifierIds to their source
ReactiveScopeDependency. For example:$0 = LoadLocal 'a' $1 = PropertyLoad $0.'b'Maps
$1.idto{identifier: a, path: [{property: 'b', optional: false}]} -
collectOptionalChainSidemap: Traverses optional chain blocks to map temporaries within optional chains to their full optional dependency path
-
collectHoistablePropertyLoads: Uses CFG analysis to determine which property loads can be safely hoisted
Phase 2: Collect Dependencies
The collectDependencies function traverses the HIR, maintaining a stack of active scopes:
-
Scope Entry/Exit: When entering a scope terminal, push a new dependency array. When exiting, propagate collected dependencies to parent scopes if valid.
-
Instruction Processing: For each instruction:
- Declare the lvalue with its instruction id and current scope
- Visit operands to record them as potential dependencies
- Handle special cases like
StoreLocal(tracks reassignments),Destructure,PropertyLoad, etc.
-
Dependency Validation (
#checkValidDependency):- Skip ref values (
isRefValueType) - Skip object methods (
isObjectMethodType) - Only include if declared before scope start
- Skip ref values (
Phase 3: Derive Minimal Dependencies
For each scope, use ReactiveScopeDependencyTreeHIR to:
- Build a tree from hoistable property loads
- Add all collected dependencies to the tree
- Truncate dependencies at their maximal safe-to-evaluate subpath
- Derive the minimal set (removing redundant nested dependencies)
Key Data Structures
ReactiveScopeDependency
type ReactiveScopeDependency = {
identifier: Identifier; // Root identifier
reactive: boolean; // Whether the value is reactive
path: DependencyPathEntry[]; // Chain of property accesses
}
DependencyPathEntry
type DependencyPathEntry = {
property: PropertyLiteral; // Property name
optional: boolean; // Is this `?.` access?
}
DependencyCollectionContext
Maintains:
#declarations: Map of DeclarationId to {id, scope} recording where each value was declared#reassignments: Map of Identifier to latest assignment info#scopes: Stack of currently active ReactiveScopes#dependencies: Stack of dependency arrays (one per active scope)#temporaries: Sidemap for resolving property loads
ReactiveScopeDependencyTreeHIR
A tree structure for efficient dependency deduplication that stores hoistable objects, tracks access types, and computes minimal dependencies.
Edge Cases
Values Used Outside Declaring Scope
If a temporary is used outside its declaring scope, it cannot be tracked in the sidemap because reordering the read would be invalid.
Ref.current Access
Accessing ref.current is treated specially - the dependency is truncated to just ref.
Optional Chains
Optional chains like a?.b?.c produce different dependency paths than a.b.c. The pass distinguishes them and may merge optional loads into unconditional ones when control flow proves the object is non-null.
Inner Functions
Dependencies from inner functions are collected recursively but with special handling for context variables.
Phi Nodes
When a value comes from multiple control flow paths, optional chain dependencies from phi operands are also visited.
TODOs
- Line 374-375:
// TODO(mofeiZ): understand optional chaining- More documentation needed for optional chain handling
Example
Fixture: reactive-control-dependency-if.js
Input:
function Component(props) {
let x;
if (props.cond) {
x = 1;
} else {
x = 2;
}
return [x];
}
Before PropagateScopeDependenciesHIR:
Scope scope @0 [12:15] dependencies=[] declarations=[] reassignments=[] block=bb9
After PropagateScopeDependenciesHIR:
Scope scope @0 [12:15] dependencies=[x$24:TPrimitive] declarations=[$26_@0] reassignments=[] block=bb9
The pass identified that:
- The scope at
[x]depends onx$24(the phi node result from the if/else branches) - Even though
xis assigned to constants (1 or 2), its value depends on the reactive control flow conditionprops.cond - The scope declares
$26_@0(the array output)