Now I have all the information I need to produce a comprehensive analysis. Let me compile the complete documentation for each pass.
Now I have all the necessary information. Let me produce my complete analysis:
Complete Analysis: React Compiler Function Analysis and Effect Inference Passes
1. AnalyseFunctions
File
/home/jm/data/ai/cloudlang-trees/react/react/compiler/packages/babel-plugin-react-compiler/src/Inference/AnalyseFunctions.ts
Purpose
The key pass that performs recursive nested function analysis. It processes each inner function (function expressions and object methods) with a mini-pipeline of effect inference passes, then classifies context variables as either Capture or Read based on the function's behavior. This enables the outer function to understand what inner functions do without needing to re-analyze their bodies later.
Input Invariants
- Input function (
HIRFunction) must have its body.blocks map populated
instr.value.loweredFunc.func.context is populated for each FunctionExpression/ObjectMethod
- Each context variable has a
mutableRange and scope that can be reset
Output Guarantees
- Each
fn.aliasingEffects contains the externally-visible effects of that function
- Each context variable has its
operand.effect set to either Effect.Capture or Effect.Read
- Context variable
mutableRange is reset to {start: 0, end: 0} and scope is set to null
- Apply effects are replaced with more precise effects (assertion enforced)
Algorithm (detailed, step-by-step)
Outer Pass (analyseFunctions function):
- Iterate over all blocks in the function body
- For each instruction in each block:
- If instruction is a
FunctionExpression or ObjectMethod:
a. Call lowerWithMutationAliasing on the inner function
b. Reset mutableRange for each context variable to {start: 0, end: 0}
c. Reset scope for each context variable to null
Inner Mini-Pipeline (lowerWithMutationAliasing function):
Phase 1 - Effect Inference:
- Recursively call
analyseFunctions on the inner function (handles nested lambdas)
- Call
inferMutationAliasingEffects(fn, {isFunctionExpression: true})
- This performs abstract interpretation to infer mutation/aliasing effects
- Marks each instruction with an array of
AliasingEffect
- Call
deadCodeElimination(fn) - removes dead code
- Call
inferMutationAliasingRanges(fn, {isFunctionExpression: true}).unwrap()
- Builds an alias graph from effects
- Propagates mutations forward and backward through the graph
- Computes mutable ranges for all identifiers
- Returns externally-visible function effects
- Call
rewriteInstructionKindsBasedOnReassignment(fn) - rewrites instructions
- Call
inferReactiveScopeVariables(fn) - determines reactive scopes
- Store the effects in
fn.aliasingEffects
Phase 2 - Context Variable Classification:
Create a set capturedOrMutated of IdentifierId
Iterate over all functionEffects returned from Phase 1:
- For Capture effects (line 72):
capturedOrMutated.add(effect.from.identifier.id)
- For Alias effects (line 71):
capturedOrMutated.add(effect.from.identifier.id)
- For Assign effects (line 70):
capturedOrMutated.add(effect.from.identifier.id)
- For CreateFrom effects (line 73):
capturedOrMutated.add(effect.from.identifier.id)
- For MaybeAlias effects (line 74):
capturedOrMutated.add(effect.from.identifier.id)
- For Mutate effects (line 84-89):
capturedOrMutated.add(effect.value.identifier.id)
- For MutateConditionally/MutateTransitive/MutateTransitiveConditionally: same as Mutate
- For Apply effects (line 78-83): ASSERTION ERROR - should not exist
- For ImmutableCapture, Freeze, Create, Impure, Render, MutateFrozen, MutateGlobal, CreateFunction: no-op (not capture)
For each context variable operand:
- If
capturedOrMutated.has(operand.identifier.id) OR operand.effect === Effect.Capture:
- Set
operand.effect = Effect.Capture
- Else:
- Set
operand.effect = Effect.Read
Key Data Structures
HIRFunction:
{
context: Array<Place>, // Captured variables from outer scope
aliasingEffects: Array<AliasingEffect> | null,
// ... other fields
}
Place with Effect:
{
identifier: Identifier,
effect: Effect.Read | Effect.Capture | ...,
reactive: boolean
}
AliasingEffect Types (capture-inducing vs non-capture-inducing):
| Effect |
Captures? |
Why |
| Capture |
✓ |
Direct capture relationship |
| Alias |
✓ |
May alias the value |
| Assign |
✓ |
Assignment from source |
| MaybeAlias |
✓ |
Potential aliasing |
| CreateFrom |
✓ |
Created from source value |
| Mutate |
✓ |
Mutates the value |
| MutateConditionally |
✓ |
May mutate |
| MutateTransitive |
✓ |
Transitively mutates |
| MutateTransitiveConditionally |
✓ |
May transitively mutate |
| ImmutableCapture |
✗ |
Read-only capture |
| Freeze |
✗ |
Freezing doesn't capture |
| Create |
✗ |
Creates new value |
| Impure |
✗ |
Side effect, not capture |
| Render |
✗ |
Render-time access |
| MutateFrozen |
✗ |
Error state |
| MutateGlobal |
✗ |
Error state |
| CreateFunction |
✗ |
Function creation |
Edge Cases
- Apply effects assertion: If an
Apply effect exists in the function effects, it indicates a bug - Apply should have been replaced with more precise effects by the inference passes.
- Context variable mutable ranges: The comment notes that
inferReactiveScopeVariables makes identifiers point to the same mutableRange instance, so resetting start/end is insufficient - the entire range object must be replaced.
- Effect already set: The final classification checks if
operand.effect === Effect.Capture before setting it, meaning if it was already classified as Capture, it stays Capture regardless of other effects.
Mapping to CloudLang
CloudLang's scope.ml does a simpler version:
CloudLang:
type closure_info = {
name : string;
free_vars : string list; (* Free variables from outer scope *)
called_functions : string list; (* Functions called by this closure *)
captures_mutable : string list; (* Free vars that are mutable state *)
}
React's AnalyseFunctions:
free_vars ≈ All context variables (fn.context)
captures_mutable ≈ Context variables with Effect.Capture
- Called functions are tracked separately in React through effect signatures
Key Delta:
- React classifies all context variables as either Capture or Read based on fine-grained effect analysis
- CloudLang only separates
free_vars into captures_mutable (mutable state) vs regular free vars
- React tracks exact mutation patterns (Mutate vs MutateTransitive, conditional vs definite)
- React resets mutable ranges after analysis for use by outer function
Example (before/after)
Before:
function Component(props) {
const data = useData(); // Hook returns stable object
const handleClick = () => {
// Inner function capturing props.data
console.log(props.data);
};
return <Button onClick={handleClick}>Click</Button>;
}
After AnalyseFunctions (simplified HIR representation):
Inner function (handleClick):
context: [
{ identifier: "props", effect: Effect.Capture },
{ identifier: "data", effect: Effect.Read } // Not captured, just read
]
aliasingEffects: [
{ kind: "Render", place: <props> }
]
Outer function context classification:
{ identifier: "handleClick", effect: Effect.Capture }
// The inner function itself is captured
2. InferReactivePlaces
File
/home/jm/data/ai/cloudlang-trees/react/react/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReactivePlaces.ts
Purpose
Determines which values are "reactive" (may semantically change over the component/hook's lifetime). Reactive values include props, hook results, values mutated with reactive operands, and values conditionally assigned based on reactive conditions.
Input Invariants
fn.aliasingEffects is populated
fn.context is populated with classified effects
- Mutation ranges have been computed by previous passes
Output Guarantees
- Each
Place.reactive is set to true if the value is reactive
- The
ReactivityMap tracks reactive identifiers using a disjoint set for aliased values
- Fixpoint iteration ensures reactivity propagates through all dataflow paths
Algorithm (detailed, step-by-step)
Setup:
- Create
ReactivityMap with disjoint set of mutably-aliased identifiers
- Mark all function parameters as reactive (they're inputs that can change)
- Create
StableSidemap to track stable value sources
- Compute
isReactiveControlledBlock using dominator analysis
Fixpoint Iteration Loop:
do {
For each block:
1. Mark phis reactive if any operand is reactive
2. Mark phis reactive if any predecessor is reactive-controlled
3. For each instruction:
a. Update stability information
b. Check for reactive inputs
c. If inputs reactive:
- Mark all lvalues as reactive (unless stable-typed)
- If operands have mutation effects (Capture, Store, etc.):
Mark operands as reactive if they're mutable
} while (reactiveIdentifiers.snapshot() returns true - changes occurred)
**Propagation to Inner Functions:**
After fixpoint converges, recursively propagate reactivity for inner functions to account for hoisting/deduping of dependency instructions.
### Key Data Structures
**StableSidemap** - Tracks sources of stability:
```typescript
class StableSidemap {
map: Map<IdentifierId, {isStable: boolean}>
handleInstruction(instr: Instruction): void {
// CallExpression/MethodCall: Mark stable if evaluates to stable type/container
// PropertyLoad/Destructure: Propagate stability from source
// StoreLocal/LoadLocal: Propagate stability
}
}
ReactivityMap - Tracks reactivity with alias support:
class ReactivityMap {
hasChanges: boolean
reactive: Set<IdentifierId>
aliasedIdentifiers: DisjointSet<Identifier> // Unified alias groups
isReactive(place: Place): boolean {
const identifier = aliasedIdentifiers.find(place.identifier) ?? place.identifier
return reactive.has(identifier.id)
}
markReactive(place: Place): void {
const identifier = aliasedIdentifiers.find(place.identifier) ?? place.identifier
if (!reactive.has(identifier.id)) {
reactive.add(identifier.id)
hasChanges = true
}
}
}
Edge Cases
- Stable types override: Even if reactive inputs, values with
isStableType (like refs) are not marked reactive
- Conditional reactivity: Values assigned based on reactive conditions become reactive even if the assigned value is constant
- Transitive mutation:
z = [x] where x is later mutated makes z reactive due to transitive reactivity propagation
- Fixpoint required: Without iteration, we miss reactivity that propagates through aliases and conditionally-assigned values
Example
function Component(props) {
let x;
if (props.reactive) { // reactive condition
x = 1;
} else {
x = 2;
}
// x becomes reactive even though 1 and 2 are constants!
return x;
}
3. InferEffectDependencies
File
/home/jm/data/ai/cloudlang-trees/react/react/compiler/packages/babel-plugin-react-compiler/src/Inference/InferEffectDependencies.ts
Purpose
Infers reactive dependencies captured by useEffect lambdas and automatically adds them as the second argument if no dependency array is provided. This implements the "auto-dependencies" feature of the React Compiler.
Input Invariants
- Hook calls (
useEffect, useLayoutEffect, etc.) are configured for auto-dependencies
- Reactive scopes have been computed for the function body
fn.aliasingEffects exists for inner functions
Output Guarantees
useEffect calls without deps arrays get auto-generated deps
Place.reactive flags are retained for dependency inference
- Codegen receives proper dependency array information
Algorithm (detailed, step-by-step)
Phase 1 - Setup:
- Collect all
FunctionExpression instructions into fnExpressions map
- Build
autodepFnConfigs map from env.config.inferEffectDependencies
- Build
loadGlobals set of global function loads
Phase 2 - Process Instructions:
For each instruction in each block:
- If
FunctionExpression: Add to fnExpressions map
- If
PropertyLoad: Check if it's loading an auto-dep function from a module
- If
LoadGlobal: Track React imports and configured auto-dep functions
- If
CallExpression or MethodCall:
- Find
autodepsArgExpectedIndex from callee
- Check if call matches expected signature (no deps array provided)
- If matching:
a. Build effect deps array: effectDeps
b. Get the function expression from fnExpressions.get(value.args[0].identifier.id)
c. Case 1: Inline function:
- Get scope info or call
inferMinimalDependencies(fnExpr)
- For each dependency:
- Skip non-reactive refs/sets/fire functions
- Build dependency instructions using
buildDependencyInstructions
- Add dependency place to
effectDeps array
- Insert deps array instruction before the call
d. Case 2: Global function:
- Insert empty deps array
e. Mark location as having inferred effect
f. Replace the deps argument with a reference to the new deps array
Phase 3 - Minimal Dependency Inference (inferMinimalDependencies):
- Collect temporaries and optional chain sidemaps
- Collect hoistable property loads at function entry
- Call
inferDependencies with context tracking
- Build dependency tree and derive minimal dependencies
- Filter to only include context variables (free variables)
Phase 4 - Code Splicing:
Use rewriteSplices to insert new instructions into blocks:
- Split blocks at splice locations
- Insert dependency instructions
- Merge with existing code
- Fix up RPO, predecessors, and instruction IDs
Key Data Structures
ManualMemoDependency:
{
root: { kind: 'Global' | 'NamedLocal', ... },
path: Array<{property: string, optional: boolean}>,
loc: SourceLocation
}
ReactiveScopeDependency:
{
identifier: Identifier,
path: Array<{property: string, optional: boolean}>,
...
}
Edge Cases
- Non-reactive dependencies: Non-reactive refs, setters, and fire functions are excluded from auto-deps
- Optional chaining: Dependencies accessed through optional chains (
obj?.prop) require special handling
- Truncate at
.current: ref.current dependencies are truncated to just ref
- Global functions: Get empty dependency arrays
- Auto-deps eligible locations: Even manually-specified deps can be eligible for LSP decoration
Example
// Before
useEffect(() => {
console.log(props.count);
console.log(data.value);
}); // No deps array!
// After
useEffect(() => {
console.log(props.count);
console.log(data.value);
}, [props.count, data]); // Auto-inferred deps!
4. DropManualMemoization
File
/home/jm/data/ai/cloudlang-trees/react/react/compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts
Purpose
Removes useMemo and useCallback APIs by transforming them into direct function calls or references. This enables the compiler to take over memoization based on its own analysis.
Input Invariants
- Function is in pre-SSA form (before SSA transformation)
useMemo/useCallback calls are identified via global loads or property loads
Output Guarantees
useMemo(callback, deps) becomes callback() (direct call)
useCallback(callback, deps) becomes callback (direct reference)
- Optional validation markers inserted for validation modes
- Original deps arrays become dead code (removed by DCE)
Algorithm (detailed, step-by-step)
Phase 1 - Collection:
- Build
sidemap with:
functions: All FunctionExpression instructions
manualMemos: Identified useMemo/useCallback load instructions
react: Direct React global loads
maybeDeps: Tracked manual memo dependencies
maybeDepsLists: Array literal dependencies
optionals: Optional chain places
Phase 2 - Transform:
For each instruction:
- If it's a
useMemo/useCallback call:
- Extract function and deps using
extractManualMemoizationArgs
- Transform instruction:
useMemo: Replace with CallExpression where callee = fnArg, args = []
useCallback: Replace with LoadLocal of fnArg
- If validation enabled:
- Create
StartMemoize marker with deps list
- Create
FinishMemoize marker after the call
- Insert markers at appropriate positions
Phase 3 - Insert Markers:
Use queuedInserts map to add markers at specific instruction IDs:
StartMemoize after the hook load instruction
FinishMemoize after the transformed call
Key Data Structures
ManualMemoCallee:
{
kind: 'useMemo' | 'useCallback',
loadInstr: LoadGlobal | PropertyLoad
}
IdentifierSidemap:
{
functions: Map<IdentifierId, FunctionExpression>,
manualMemos: Map<IdentifierId, ManualMemoCallee>,
react: Set<IdentifierId>,
maybeDeps: Map<IdentifierId, ManualMemoDependency>,
maybeDepsLists: Map<IdentifierId, {loc, deps: Place[]}>
}
Edge Cases
- Non-inline functions: Validation requires inline functions (sketchy to use opaque function)
- Missing deps array: Works with null deps
- React namespace: Handles both
React.useMemo and direct useMemo imports
- useMemo void returns: Validates that
useMemo returns a value
Example
// Before
const memoized = useMemo(() => {
return expensiveCalc(props.data);
}, [props.data]);
// After
const memoized = (() => {
return expensiveCalc(props.data);
})(); // IIFE, will be inlined
5. InlineImmediatelyInvokedFunctionExpressions
File
/home/jm/data/ai/cloudlang-trees/react/react/compiler/packages/babel-plugin-react-compiler/src/Inference/InlineImmediatelyInvokedFunctionExpressions.ts
Purpose
Inlines IIFEs (Immediately Invoked Function Expressions) to allow more fine-grained memoization. This reduces function call overhead and enables better dependency tracking.
Input Invariants
- Functions have been lowered to HIR
- Block structure is intact
- IIFEs have no arguments
Output Guarantees
- IIFE bodies are spliced into the call site
- Return statements are replaced with assignments + gotos
- Control flow is preserved using labeled blocks or direct gotos
Algorithm (detailed, step-by-step)
Detection:
- Track all
FunctionExpression instructions assigned to temporaries
- For each
CallExpression:
- Check if callee is a local function expression
- Verify no arguments
- Verify no params, async, or generator
Inlining - Single Return (Simple Case):
- Split block: create continuation block with code after the call
- Replace original block's terminal with
goto to function entry
- Replace
return terminals with:
StoreLocal of return value to the IIFE's lvalue
goto to continuation block
- Add all function blocks to outer function
Inlining - Multiple Returns (Complex Case):
- Insert
LabelTerminal before the IIFE call
- Declare temporary variable for the result
- Replace
return terminals with:
StoreLocal of return value to result temporary
goto to label's fallthrough (continuation)
- Splice function blocks "between" the label and continuation
Cleanup:
- Remove inlined function definition instructions
- Reverse postorder blocks
- Renumber instructions
- Mark predecessors
- Merge consecutive blocks
Key Data Structures
- GotoVariant.Break: Used for labeled block gotos
- LabelTerminal: Controls control flow for inlined returns
Edge Cases
- Single return optimization: Avoids labeled block overhead
- Statement block requirement: Cannot inline IIFEs in expression blocks
- Nested IIFEs: Sequential IIFEs handled by pushing continuation blocks to queue
Example
// Before
const x = (() => {
const temp = [];
temp.push(props.value);
return temp;
})();
// After (inlined HIR)
let t0;
Label block=bb1 fallthrough=bb2
bb1:
const temp = [];
temp.push(props.value);
t0 = temp; // replaced return
Goto bb2 // instead of return
bb2:
const x = t0;
6. ControlDominators
File
/home/jm/data/ai/cloudlang-trees/react/react/compiler/packages/babel-plugin-react-compiler/src/Inference/ControlDominators.ts
Purpose
Computes which blocks are "controlled" by reactive values using post-dominator analysis. Used to determine if a block's execution depends on a reactive condition.
Input Invariants
- Post-dominator tree has been computed
isControlVariable predicate is provided
Output Guarantees
- Returns a function
ControlDominators that tests if a block ID is controlled by reactive values
- Results are cached in
postDominatorFrontierCache
Algorithm (detailed, step-by-step)
Post-Dominator Frontier Computation:
- Compute post-dominator tree for the function
- For each block:
- Find its post-dominator frontier
- Frontier = immediate successors of nodes that post-dominate the block
- These are the earliest blocks where execution may branch away
Control Block Test:
For a given block:
- Get its post-dominator frontier (cached)
- For each frontier block:
- Check if the terminal's test variable is reactive
- For
if/branch: check terminal.test
- For
switch: check terminal.test and case tests
- If any frontier block has a reactive test, the block is controlled
Key Data Structures
Post-Dominator Frontier:
- Set of blocks from which execution may or may not reach the target
- Computed by finding successors of post-dominating blocks
ControlDominators type:
type ControlDominators = (id: BlockId) => boolean
Edge Cases
- Exit node handling: Throws can be excluded from exit nodes
- Cache usage: Computation is expensive, so results are cached
- Multiple reactive tests: Any reactive test in the frontier makes the block controlled
Example
if (props.condition) { // reactive test
x = 1;
} else {
x = 2;
}
return x;
The block after the if is controlled by props.condition. Even though x is assigned constant values, it becomes reactive due to the reactive control flow.
CloudLang Comparison and Upgrade Path
Current CloudLang Scope Analysis
File: /home/jm/data/ai/cloudlang-trees/react/lib/scope.ml
Current Analysis:
type var_info = {
name : string;
is_mutable_state : bool; (* new Map(), new Set(), new Array() *)
is_function : bool (* arrow function *)
}
type closure_info = {
name : string;
free_vars : string list; (* All outer-scope references *)
called_functions : string list; (* Module functions called *)
captures_mutable : string list; (* Free vars that are mutable state *)
}
Algorithm:
- Two-pass analysis:
- Pass 1: Collect module-scope declarations
- Pass 2: For each arrow function, compute:
free_vars: References not locally declared
called_functions: Functions called as simple identifiers
captures_mutable: Subset of free_vars that are is_mutable_state
Delta: React vs CloudLang
| Aspect |
React |
CloudLang |
| Capture Classification |
Capture vs Read (fine-grained) |
captures_mutable vs free_vars |
| Effect Tracking |
Full AliasingEffects (16 types) |
Just mutable vs immutable |
| Mutation Analysis |
Conditional/Definite, Transitive |
Simple mutable flag |
| Reactivity |
Propagates through dataflow with fixpoint |
Not tracked |
| Mutable Ranges |
Start/end instruction IDs |
Not tracked |
| Function Signature |
Full aliasing effects |
Not modeled |
| Stability |
StableSidemap for refs/hooks |
Not tracked |
Proposed OCaml Module Signature
(** Effect classification for captured variables *)
type effect =
| Read (** Read-only access *)
| Capture (** Mutable capture - may mutate *)
(** Mutation kind *)
type mutation_kind =
| Definite (** Definitely mutates *)
| Conditional (** May mutate *)
(** Alias/capture relationship *)
type alias_effect =
| Alias of Place.t * Place.t (** a -> b, mutate(b) => mutate(a) *)
| Capture of Place.t * Place.t (** a captured into b, no transitive mutation *)
| MaybeAlias of Place.t * Place.t (** Potential alias *)
| ImmutableCapture of Place.t * Place.t (** Read-only capture *)
(** External function effects *)
type function_effects = {
mutations : (Place.t * mutation_kind) list; (** Params/context mutated *)
captures : alias_effect list; (** Data flow between params/return *)
impure : bool; (** Has side effects *)
}
(** Context variable with effect *)
type context_var = {
name : string;
effect : effect;
identifier : Identifier.t;
}
(** Closure analysis result *)
type closure_info = {
name : string;
params : Place.t list;
context : context_var list;
effects : function_effects;
aliasing_effects : alias_effect list;
reactive : Place.t list; (** Reactive free variables *)
}
(** Analysis result for program *)
type analysis_result = {
closures : closure_info list;
module_vars : (string * var_info) list;
exports : string list;
}
(** Main analysis function *)
val analyze : Ast.node -> (analysis_result, error) result
(** Analyze a single closure *)
val analyze_closure : string -> Ast.node list -> Ast.node ->
var_info list -> closure_info
(** Effect inference on HIR *)
val infer_effects : Hir.function -> function_effects result
(** Reactivity inference *)
val infer_reactive : Hir.function -> Place.t Set.t
Binding Inference Mapping
In CloudLang, inner functions become separate Workers. The effect signature determines what bindings each Worker needs:
React Context → CloudLang Binding:
| React Context |
CloudLang Binding |
| Effect.Capture of KV-type |
KV namespace binding |
| Effect.Read of KV-type |
KV read-only access |
| Effect.Capture of service-type |
Service binding |
| Effect.Capture of hook-result |
State binding |
| Render effect |
Durable object access |
| StableSidemap (ref) |
No binding needed (stable) |
Example:
// React
const db = useDB(); // hook returning stable reference
const getData = () => {
return db.get(props.id); // captures db (stable) and props.id (reactive)
};
// CloudLang generated bindings
worker getData {
bindings: {
db: useDB_result, // Effect.Read -> read-only
id: props_id, // Effect.Capture -> KV binding
}
}
Key insight: React's Capture vs Read classification directly maps to CloudLang's mutable vs immutable bindings. Captured mutable values require the binding to be passed to the Worker; read-only values can be accessed globally or via read-only references.