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:

Input Invariants

Output Guarantees

After this pass completes:

  1. Each ReactiveScope.dependencies contains 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)
  2. Each ReactiveScope.declarations contains identifiers that:

    • Are assigned within the scope
    • Are used outside the scope (need to be exposed as scope outputs)
  3. Property load chains are resolved to their root identifiers with paths (e.g., props.user.name becomes {identifier: props, path: ["user", "name"]})

  4. Optional chains are handled correctly, distinguishing between a?.b and a.b access types

Algorithm

Phase 1: Build Sidemaps

  1. findTemporariesUsedOutsideDeclaringScope: Identifies temporaries that are used outside the scope where they were declared (cannot be hoisted/reordered safely)

  2. collectTemporariesSidemap: Creates a mapping from temporary IdentifierIds to their source ReactiveScopeDependency. For example:

    $0 = LoadLocal 'a'
    $1 = PropertyLoad $0.'b'
    

    Maps $1.id to {identifier: a, path: [{property: 'b', optional: false}]}

  3. collectOptionalChainSidemap: Traverses optional chain blocks to map temporaries within optional chains to their full optional dependency path

  4. 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:

  1. Scope Entry/Exit: When entering a scope terminal, push a new dependency array. When exiting, propagate collected dependencies to parent scopes if valid.

  2. 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.
  3. Dependency Validation (#checkValidDependency):

    • Skip ref values (isRefValueType)
    • Skip object methods (isObjectMethodType)
    • Only include if declared before scope start

Phase 3: Derive Minimal Dependencies

For each scope, use ReactiveScopeDependencyTreeHIR to:

  1. Build a tree from hoistable property loads
  2. Add all collected dependencies to the tree
  3. Truncate dependencies at their maximal safe-to-evaluate subpath
  4. 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:

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

  1. 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: