inferReactivePlaces

File

src/Inference/InferReactivePlaces.ts

Purpose

Determines which Places (identifiers and temporaries) in the HIR are reactive - meaning they may semantically change over the course of the component or hook's lifetime. This information is critical for memoization: reactive places form the dependencies that, when changed, should invalidate cached values.

A place is reactive if it derives from any source of reactivity:

  1. Props - Component parameters may change between renders
  2. Hooks - Hooks can access state or context which can change
  3. use operator - Can access context which may change
  4. Mutation with reactive operands - Values mutated in instructions that have reactive operands become reactive themselves
  5. Conditional assignment based on reactive control flow - Values assigned in branches controlled by reactive conditions become reactive

Input Invariants

Output Guarantees

Algorithm

The algorithm uses fixpoint iteration to propagate reactivity forward through the control-flow graph:

Initialization

  1. Create a ReactivityMap backed by disjoint sets of mutably-aliased identifiers
  2. Mark all function parameters as reactive (props are reactive by definition)
  3. Create a ControlDominators helper to identify blocks controlled by reactive conditions

Fixpoint Loop

Iterate until no changes occur:

For each block:

  1. Phi Nodes: Mark phi nodes reactive if:

    • Any operand is reactive, OR
    • Any predecessor block is controlled by a reactive condition (control-flow dependency)
  2. Instructions: For each instruction:

    • Track stable identifier sources (for hooks like useRef, useState dispatch)
    • Check if any operand is reactive
    • Hook calls and use operator are sources of reactivity
    • If instruction has reactive input:
      • Mark lvalues reactive (unless they are known-stable like setState functions)
    • If instruction has reactive input OR is in reactive-controlled block:
      • Mark mutable operands (Capture, Store, Mutate effects) as reactive
  3. Terminals: Check terminal operands for reactivity

Post-processing

Propagate reactivity to inner functions (nested FunctionExpression and ObjectMethod).

Key Data Structures

ReactivityMap

class ReactivityMap {
  hasChanges: boolean = false;           // Tracks if fixpoint changed
  reactive: Set<IdentifierId> = new Set(); // Set of reactive identifiers
  aliasedIdentifiers: DisjointSet<Identifier>; // Mutable alias groups
}

StableSidemap

class StableSidemap {
  map: Map<IdentifierId, {isStable: boolean}> = new Map();
}

Tracks sources of stability (e.g., useState()[1] dispatch function). Forward data-flow analysis that:

ControlDominators

Uses post-dominator frontier analysis to determine which blocks are controlled by reactive branch conditions.

Edge Cases

Backward Reactivity Propagation via Mutable Aliasing

const x = [];
const z = [x];
x.push(props.input);
return <div>{z}</div>;

Here z aliases x which is later mutated with reactive data. The disjoint set ensures z becomes reactive even though the mutation happens after its creation.

Stable Types Are Not Reactive

const [state, setState] = useState();
// setState is stable - not marked reactive despite coming from reactive hook

The StableSidemap tracks these and skips marking them reactive.

Ternary with Stable Values Still Reactive

props.cond ? setState1 : setState2

Even though both branches are stable types, the result depends on reactive control flow, so it cannot be marked non-reactive just based on type.

Phi Nodes with Reactive Predecessors

When a phi's predecessor block is controlled by a reactive condition, the phi becomes reactive even if its operands are all non-reactive constants.

TODOs

No explicit TODO comments are present in the source file. However, comments note:

Example

Fixture: reactive-dependency-fixpoint.js

Input:

function Component(props) {
  let x = 0;
  let y = 0;
  while (x === 0) {
    x = y;
    y = props.value;
  }
  return [x];
}

Before InferReactivePlaces:

bb1 (loop):
  store x$26:TPhi:TPhi: phi(bb0: read x$21:TPrimitive, bb3: read x$32:TPhi)
  store y$30:TPhi:TPhi: phi(bb0: read y$24:TPrimitive, bb3: read y$37)
  ...
bb3 (block):
  [12] mutate? $35 = LoadLocal read props$19
  [13] mutate? $36 = PropertyLoad read $35.value
  [14] mutate? $38 = StoreLocal Reassign mutate? y$37 = read $36

After InferReactivePlaces:

bb1 (loop):
  store x$26:TPhi{reactive}:TPhi: phi(bb0: read x$21:TPrimitive, bb3: read x$32:TPhi{reactive})
  store y$30:TPhi{reactive}:TPhi: phi(bb0: read y$24:TPrimitive, bb3: read y$37{reactive})
  [6] mutate? $27:TPhi{reactive} = LoadLocal read x$26:TPhi{reactive}
  ...
bb3 (block):
  [12] mutate? $35{reactive} = LoadLocal read props$19{reactive}
  [13] mutate? $36{reactive} = PropertyLoad read $35{reactive}.value
  [14] mutate? $38{reactive} = StoreLocal Reassign mutate? y$37{reactive} = read $36{reactive}

Key observations: