Now I have a comprehensive understanding of the React Compiler's mutation and aliasing model. Let me now write the complete analysis based on all the files I've read.
The files I've read:
1. MUTABILITY_ALIASING_MODEL.md - The design document
2. AliasingEffects.ts - Type definitions
3. InferMutationAliasingEffects.ts - The abstract interpretation engine (almost 3000 lines)
4. InferMutationAliasingRanges.ts - Mutable range computation (840 lines)
5. HIR.ts - Type definitions for Effect, ValueKind, MutableRange, Place, Identifier
6. decompose.ml and decompose.mli - CloudLang's current decomposition logic
Let me now produce a comprehensive analysis document.
Now I have all the necessary information. Let me produce my complete analysis:
React Compiler Mutation and Aliasing Model Analysis
Overview
The React Compiler's mutation and aliasing model is a sophisticated abstract interpretation system that tracks how values flow through a program, how they mutate over time, and which operations are safe/unsafe. This analysis serves as the foundation for reactive scope inference, memoization, and validation of React rules.
Component: The Aliasing Model (MUTABILITY_ALIASING_MODEL.md)
File
/react/compiler/packages/babel-plugin-react-compiler/src/Inference/MUTABILITY_ALIASING_MODEL.md
Purpose
Design document explaining the fundamental concepts of the mutability and aliasing model. It defines the conceptual framework that both InferMutationAliasingEffects and InferMutationAliasingRanges implement.
Input Invariants
- HIR (High-level Intermediate Representation) is already built
- Control flow graph (CFG) is constructed with blocks and terminals
- Types are inferred for all identifiers
Output Guarantees
- Clear mental model for reasoning about mutation and aliasing
- Formal rules for effect transitivity
- Distinctions between different data flow mechanisms
Algorithm (detailed, step-by-step)
1. Fundamental Concepts
Values: Abstract entities with a "kind" (Mutable, Frozen, Primitive, Global, Context, MaybeFrozen)
References/Places: Pointers to values with an IdentifierId. Each place has:
identifier.id: Unique SSA identifier
identifier.declarationId: Original variable declaration
mutableRange: {start, end} instruction range where value is mutable
effect: How this reference uses the value (Read, Store, Mutate, Freeze, etc.)
Aliasing: When multiple places point to the same underlying value (e.g., b = a)
Capturing: When a value stores a reference to another value (e.g., arr.push(x), obj.prop = x)
Freezing: Marking a reference as immutable (passed to React, cannot be mutated further through that reference)
2. Key Distinctions Between Effects
| Effect |
Semantics |
Example |
Mutation Propagation |
| Assign |
Direct assignment, overwrites destination |
x = y → Mutate(y) ⇒ Mutate(x) |
|
| Alias |
Non-exclusive assignment (may be one of multiple values) |
c = identity(a) → Mutate(c) ⇒ Mutate(a) |
|
| Capture |
Storing a reference inside another value |
arr.push(x) → Mutate(arr) ≠> Mutate(x) |
|
| CreateFrom |
Extracting part of a value |
y = arr[0] → Mutate(y) ⇒ Mutate(arr) |
|
| MaybeAlias |
Potential aliasing (uncertain) |
c = unknown_fn(a) → Mutate(c) ⇒ MutateConditional(a) |
|
3. Transitivity Rules
Each effect is transitive with itself:
Assign b <- a
Assign c <- b
=> Assign c <- a
Alias b <- a
Alias c <- b
=> Alias c <- a
CreateFrom b <- a
CreateFrom c <- b
=> CreateFrom c <- a
Capture b <- a
Capture c <- b
=> Capture c <- a
Alias > Assign:
Assign b <- a
Alias c <- b
=> Alias c <- a
Alias b <- a
Assign c <- b
=> Alias c <- a
CreateFrom > Assign/Alias:
CreateFrom b <- a
Alias/Assign c <- b
=> CreateFrom c <- a
Alias/Assign b <- a
CreateFrom c <- b
=> CreateFrom c <- a
Capture > Assign/Alias:
Capture b <- a
Alias/Assign c <- b
=> Capture c <- a
Alias/Assign b <- a
Capture c <- b
=> Capture c <- a
Capture + CreateFrom = Alias:
Capture b <- a
CreateFrom c <- b
=> Alias c <- a
Intuition: Capturing then extracting returns the original value (or alias of it).
CreateFrom + Capture = Capture:
CreateFrom b <- a
Capture c <- b
=> Capture c <- a
Intuition: Extraction then capturing stores a copy, not an alias.
4. Mutation Propagation Rules
Mutate (non-transitive) propagates to:
- Direct aliases
- Assignments
- CreateFrom sources
- NOT captures
MutateTransitive propagates to:
- Everything above PLUS
- Captured values (recursive into nested structures)
MaybeAlias downgrades mutations to conditional:
- Known mutations become
MutateConditionally
- This prevents false positives on uncertain aliasing
5. How Freeze Works
Critical distinction: Freeze applies to the reference, not the value itself.
const x = {};
const y = [x];
y.pop(); // y no longer contains x
freeze(y); // Only freezes the reference y
mutate(x); // SAFE - x is a different reference
However, if there are multiple references to the same value:
const x = {};
const y = []; // Not same as above example
x.y = y;
freeze(y);
x.y.push(val); // ERROR if we track through captures - but we DON'T
Freeze effects on function expressions ARE transitive:
// Functions cannot change which variables they close over
const f = () => x;
freeze(f); // This freezes x transitively
6. MaybeAlias: When and Why
Used when the compiler cannot determine if aliasing actually occurs:
Case 1: Unknown function signatures
const c = foo(a); // Is foo the identity function? Unknown
// We must consider: c MAY be a, so mutations of c might affect a
// But we shouldn't error if we're unsure
Case 2: Conditional functions
const c = cond ? a : b; // c is definitely a OR b
// This uses Alias, not MaybeAlias
Case 3: Unknown functions with multiple args
const c = unknown(a, b);
// c might be a, might be b, or might be new value
// Record: MaybeAlias a -> c, MaybeAlias b -> c
// If c mutates: a and b are conditionally mutated
Key Data Structures
ValueKind enum:
Primitive: Numbers, strings, booleans, null, undefined (copy-on-write)
Global: Module-level variables (special rules)
Frozen: Immutable values (passed to React, hooks, etc.)
Mutable: Objects/arrays that can be modified
Context: Values from context (outside function scope)
MaybeFrozen: Union of frozen and mutable (uncertain)
ValueReason enum: Why a value has its kind
Global, JsxCaptured, HookCaptured, HookReturn, KnownReturnSignature, Context, ReactiveFunctionArgument, State, ReducerState, Other
Effect enum (legacy, on Places):
Unknown, Freeze, Read, Capture, ConditionallyMutate, ConditionallyMutateIterator, Mutate, Store
Edge Cases
- Hoisted functions: Functions declared after use create special freeze semantics
- Destructuring spreads:
{...rest} from frozen props can be treated as frozen
- Iterators: Known types (Array/Set/Map) have fresh iterators; unknown types may alias
- Ref objects: Special handling for
ref.current assignment hints
- Catch handlers: Values that throw into catch parameters have special aliasing
Mapping to CloudLang
Direct applicability:
- ValueKind.Mutable ↔
new Map(), new Set(), new Array() detection
- Capture/Alias ↔ Understanding which closures share state
- Freeze ↔ Values passed to React-like hooks (in CloudLang: exported function boundaries)
Key insight: CloudLang's captures_mutable field in closure_info is essentially the same as React's Capture effects on context variables.
Example
// React example
function Component(props) {
const a = {}; // Create: a = Mutable
const b = [a]; // Capture: a -> b
b[0] = props.value; // Mutate: b
const c = maybeIdentity(a); // MaybeAlias: a -> c
return <Foo c={c} />; // Freeze: c
}
// Effects computed:
// 1. Create a = Mutable
// 2. Capture a -> b
// 3. Mutate b (propagates to b only, NOT a)
// 4. CreateFrom a -> c (from maybeIdentity)
// 5. Freeze c (downgrades to conditional check for a)
Component: AliasingEffect Type (AliasingEffects.ts)
File
/react/compiler/packages/babel-plugin-react-compiler/src/Inference/AliasingEffects.ts
Purpose
Defines the 17+ effect variants that describe all possible operations on values. These effects are the atomic operations that the abstract interpreter processes.
Input Invariants
- HIR places are fully constructed with
Place and Identifier types
- Types are already inferred for values
Output Guarantees
- Type-safe effect definitions
- Hash function for effect internment
AliasingSignature type for function call composition
Algorithm (detailed, step-by-step)
The AliasingEffect type is a discriminated union with 17+ variants:
1. Creation Effects (3 variants)
Create:
{
kind: 'Create';
into: Place; // Where value is stored
value: ValueKind; // Kind of created value
reason: ValueReason; // Why it has this kind
}
- Creates a new value with given kind
- Used for: ObjectExpression, ArrayExpression, primitives
CreateFunction:
{
kind: 'CreateFunction';
captures: Array<Place>; // Captured variables
function: FunctionExpression | ObjectMethod;
into: Place;
}
- Tracks function creation with its captures
- Critical for: determining if function is mutable (has mutable captures)
Apply:
{
kind: 'Apply';
receiver: Place; // Method receiver or callee
function: Place; // The function being called
mutatesFunction: boolean; // Does call modify callee?
args: Array<Place | SpreadPattern | Hole>;
into: Place; // Result storage
signature: FunctionSignature | null; // Known effects, if available
loc: SourceLocation;
}
- Models function calls:
fn(), new C(), obj.method()
- Signature resolution priority:
- Local function with pre-computed
aliasingEffects
- Built-in with
aliasing property
- Legacy signature (fallback)
2. Aliasing Effects (5 variants)
Assign:
{ kind: 'Assign'; from: Place; into: Place; }
- Direct assignment:
into = from
- Overwrites previous aliases to
into
Alias:
{ kind: 'Alias'; from: Place; into: Place; }
- Non-exclusive assignment
- Used for: identity functions, conditional returns
- Does NOT initialize: must pair with Create/CreateFrom
Capture:
{ kind: 'Capture'; from: Place; into: Place; }
- Storing reference:
arr.push(x), obj.prop = x
- Mutation of
into does NOT mutate from
CreateFrom:
{ kind: 'CreateFrom'; from: Place; into: Place; }
- Extracting:
y = arr[0], y = obj.prop
- Mutation of
y propagates to from
ImmutableCapture:
{ kind: 'ImmutableCapture'; from: Place; into: Place; }
- Read-only data flow for future escape analysis
- Currently no-op for mutable range analysis
MaybeAlias:
{ kind: 'MaybeAlias'; from: Place; into: Place; }
- Uncertain aliasing from unknown function calls
- Mutations become conditional (no errors on frozen)
3. State-Changing Effects (4 variants)
Freeze:
{
kind: 'Freeze';
value: Place;
reason: ValueReason; // JsxCaptured, HookCaptured, etc.
}
- Marks reference as immutable
- Does NOT freeze value itself
- Special: Function expressions transitively freeze captures
Mutate:
{
kind: 'Mutate';
value: Place;
reason?: MutationReason | null; // {kind: 'AssignCurrentProperty'} for refs
}
- Mutates value and direct aliases
- Errors if value is frozen
- Reason field helps with ref detection hints
MutateConditionally:
{ kind: 'MutateConditionally'; value: Place; }
- May mutate if value is actually mutable
- Silently dropped for non-mutable values
MutateTransitive:
{ kind: 'MutateTransitive'; value: Place; }
- Mutates value, aliases, AND transitive captures
- For: unknown functions, arbitrary object mutation
MutateTransitiveConditionally:
{ kind: 'MutateTransitiveConditionally'; value: Place; }
- Transitive mutation, conditional on actual mutability
4. Side Effects (3 variants)
MutateFrozen:
{
kind: 'MutateFrozen';
place: Place;
error: CompilerDiagnostic;
}
- Always an error: mutating known-frozen value
MutateGlobal:
{
kind: 'MutateGlobal';
place: Place;
error: CompilerDiagnostic;
}
- Error during render, allowed in effects/events
- Used for: module-level mutation
Impure:
{
kind: 'Impure';
place: Place;
error: CompilerDiagnostic;
}
- Calling side-effecting function during render
Render:
{ kind: 'Render'; place: Place; }
- Marks value called during render
- Distinguishes: hooks vs event handlers
5. AliasingSignature
type AliasingSignature = {
receiver: IdentifierId; // @receiver placeholder
params: Array<IdentifierId>; // @param0, @param1, ...
rest: IdentifierId | null; // @rest placeholder
returns: IdentifierId; // @returns placeholder
effects: Array<AliasingEffect>; // Effects of call
temporaries: Array<Place>; // Internal placeholders
};
Encodes: What a function does when called, independent of specific arguments.
Used for:
- Built-in hooks (useState, useMemo, etc.)
- Local functions (constructed from their body)
- Function call composition
6. Effect Composition
Effects compose through function call signatures:
Apply(receiver: @r, function: @f, args: [@x, @y])
+ Signature: {
receiver: @r, params: [@x, @y], returns: @ret,
effects: [Mutate @x, Capture @y -> @ret]
}
=> Result effects:
- Mutate (argument in place of @x)
- Capture (argument in place of @y -> result)
- Create @ret (return value)
Key Data Structures
- MutationReason:
{kind: 'AssignCurrentProperty'} for ref.current assignment hints
- hashEffect(): Interns effects by string hashing for deduplication
Edge Cases
- Hole operands: Spread/rest arguments with no value
- SpreadPattern: Represents
...spread syntax (needs iterator mutation)
- Function signatures: Null signature = unknown function (use conservative fallback)
- Legacy signatures: Old-style effects need conversion to new AliasingEffect
Mapping to CloudLang
AliasingSignature is directly applicable:
- CloudLang functions can have signatures describing their effects
- This enables precise inter-function analysis
For service decomposition:
- Capture effects → Functions that share mutable state (same Worker group)
- MutateTransitive → Transitive mutation through shared state
- Freeze → Export boundaries (after function returns)
Example:
// CloudLang could define:
const fetchHandlerSignature: AliasingSignature = {
receiver: makeId(0),
params: [makeId(1)], // url
rest: null,
returns: makeId(2), // response
effects: [
{kind: 'MutateTransitiveConditionally', value: makePlaceholder(0)}, // fetcher may mutate
{kind: 'Create', into: makePlaceholder(2), value: ValueKind.Mutable, reason: ValueReason.Other}
]
};
Example
// TypeScript-style effect composition
const identitySig = {
receiver: makeId(0), // @receiver
params: [makeId(1)], // @param0
rest: null,
returns: makeId(2), // @returns
effects: [
{kind: 'Alias', from: makePlaceholder(1), into: makePlaceholder(2)}
]
};
// Applying: x = identity(y)
// Substitute: @param0 -> y, @returns -> x
// Result effect: {kind: 'Alias', from: y, into: x}
Component: Abstract Interpretation Engine (InferMutationAliasingEffects.ts)
File
/react/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts
Purpose
Two-phase abstract interpretation:
- Candidate effects: Compute potential effects per instruction based on semantics
- Abstract interpretation: Iteratively apply effects, track abstract value kinds, reach fixpoint
Determines precise mutation/aliasing effects by considering the abstract state of values.
Input Invariants
- HIR has CFG with blocks, instructions, terminals
- Types are inferred for all identifiers
- Phi nodes represent control flow joins
Output Guarantees
- Each instruction has
effects array with actual effects
- Effects are pruned/downgraded based on abstract state
- All function expressions have
aliasingEffects populated
Algorithm (detailed, step-by-step)
Phase 1: Initialization
// Create initial inference state
const initialState = InferenceState.empty(fn.env, isFunctionExpression);
// Context variables (from outer scope)
for (const ref of fn.context) {
initialState.initialize(refValue, {
kind: ValueKind.Context,
reason: new Set([ValueReason.Other])
});
initialState.define(ref, refValue);
}
// Parameters
const paramKind = isFunctionExpression
? {kind: ValueKind.Mutable, ...}
: {kind: ValueKind.Frozen, ...}; // Component params are frozen
for (const param of fn.params) {
inferParam(param, initialState, paramKind);
}
Phase 2: Main Interpretation Loop
const queuedStates = new Map<BlockId, InferenceState>();
queue(fn.body.entry, initialState);
let iterationCount = 0;
while (queuedStates.size !== 0) {
iterationCount++;
if (iterationCount > 100) throw "Potential infinite loop";
for (const [blockId, block] of fn.body.blocks) {
const incomingState = queuedStates.get(blockId);
queuedStates.delete(blockId);
if (!incomingState) continue;
statesByBlock.set(blockId, incomingState);
const state = incomingState.clone();
inferBlock(context, state, block);
// Queue successors (will be merged)
for (const nextBlockId of eachTerminalSuccessor(block.terminal)) {
queue(nextBlockId, state);
}
}
}
Key: queue() merges states from multiple predecessors using lattice join.
Phase 3: Per-Block Processing
function inferBlock(context: Context, state: InferenceState, block: BasicBlock) {
// Process phi nodes (control flow joins)
for (const phi of block.phis) {
state.inferPhi(phi);
}
// Process instructions
for (const instr of block.instructions) {
let instructionSignature = context.instructionSignatureCache.get(instr);
if (!instructionSignature) {
instructionSignature = computeSignatureForInstruction(context, state.env, instr);
context.instructionSignatureCache.set(instr, instructionSignature);
}
const effects = applySignature(context, state, instructionSignature, instr);
instr.effects = effects;
}
// Process terminal effects (return, try-catch)
if (terminal.kind === 'return' && !isFunctionExpression) {
terminal.effects = [{kind: 'Freeze', value: terminal.value, reason: ValueReason.JsxCaptured}];
}
// Handle try-catch aliasing...
}
Phase 4: Phi Node Handling
inferPhi(phi: Phi): void {
const values = new Set<InstructionValue>();
for (const [_, operand] of phi.operands) {
const operandValues = this.#variables.get(operand.identifier.id);
if (operandValues === undefined) continue; // Backedge, handled later
for (const v of operandValues) {
values.add(v);
}
}
if (values.size > 0) {
this.#variables.set(phi.place.identifier.id, values);
}
}
Key insight: Phi nodes collect possible values from different control flow paths.
Phase 5: Candidate Effect Computation
For each instruction kind, generate semantic effects:
ArrayExpression:
effects.push({
kind: 'Create',
into: lvalue,
value: ValueKind.Mutable,
reason: ValueReason.Other
});
for (const element of value.elements) {
if (element.kind === 'Identifier') {
effects.push({kind: 'Capture', from: element, into: lvalue});
} else if (element.kind === 'Spread') {
const mutateIterator = conditionallyMutateIterator(element.place);
if (mutateIterator) effects.push(mutateIterator);
effects.push({kind: 'Capture', from: element.place, into: lvalue});
}
}
PropertyStore (obj.prop = val):
const mutationReason = isRefCurrentAssignment ? {kind: 'AssignCurrentProperty'} : null;
effects.push({kind: 'Mutate', value: value.object, reason: mutationReason});
effects.push({kind: 'Capture', from: value.value, into: value.object});
effects.push({kind: 'Create', into: lvalue, value: ValueKind.Primitive, ...});
FunctionExpression:
effects.push({
kind: 'CreateFunction',
into: lvalue,
function: value,
captures: value.loweredFunc.func.context.filter(
operand => operand.effect === Effect.Capture
)
});
CallExpression (unknown signature fallback):
effects.push({kind: 'Create', into: lvalue, value: ValueKind.Mutable, ...});
for (const arg of [receiver, function, ...args]) {
// Mutate all args transitively (we don't know what function does)
effects.push({kind: 'MutateTransitiveConditionally', value: arg});
// MaybeAlias from each arg to result
effects.push({kind: 'MaybeAlias', from: arg, into: lvalue});
// Capture from each arg to every other arg
for (const otherArg of allArgs) {
effects.push({kind: 'Capture', from: arg, into: otherArg});
}
}
Phase 6: Apply Signature (Abstract Interpretation)
function applySignature(context, state, signature, instruction): Array<AliasingEffect> | null {
const effects: [];
// Validate function expression effects (no frozen mutation)
if (isFunctionExpression(instruction.value)) {
for (const effect of instruction.value.aliasingEffects) {
if ((effect.kind === 'Mutate' || 'MutateTransitive') &&
!context.has(effect.value)) {
// Check if captured variable is frozen
const value = state.kind(effect.value);
if (value.kind === ValueKind.Frozen) {
effects.push({kind: 'MutateFrozen', place: effect.value, error: ...});
}
}
}
}
const initialized = new Set<IdentifierId>();
for (const effect of signature.effects) {
applyEffect(context, state, effect, initialized, effects);
}
return effects.length !== 0 ? effects : null;
}
Phase 7: Effect Application
Create:
case 'Create': {
state.initialize(value, {kind: effect.value, reason: new Set([effect.reason])});
state.define(effect.into, value);
effects.push(effect);
}
Freeze:
case 'Freeze': {
const didFreeze = state.freeze(effect.value, effect.reason);
if (didFreeze) effects.push(effect);
// No effect if already frozen (idempotent)
}
Mutate variants:
case 'Mutate':
case 'MutateTransitive':
case 'MutateConditionally':
case 'MutateTransitiveConditionally': {
const mutationKind = state.mutate(effect.kind, effect.value);
if (mutationKind === 'mutate') {
effects.push(effect);
} else if (mutationKind === 'mutate-frozen') {
// Value is frozen, emit error
effects.push({kind: 'MutateFrozen', place: effect.value, error: ...});
} else if (mutationKind === 'mutate-global') {
effects.push({kind: 'MutateGlobal', place: effect.value, error: ...});
}
// 'none' or 'mutate-ref': no effect
}
Capture/Alias/Assign:
case 'Capture':
case 'Alias':
case 'Assign': {
const intoKind = state.kind(effect.into).kind;
const fromKind = state.kind(effect.from).kind;
// Source is frozen => use ImmutableCapture
if (fromKind === ValueKind.Frozen || fromKind === ValueKind.Primitive) {
applyEffect({kind: 'ImmutableCapture', from: effect.from, into: effect.into});
}
// Both mutable => keep effect
else if (fromKind === ValueKind.Mutable && intoKind === ValueKind.Mutable) {
effects.push(effect);
}
// Context involved => MaybeAlias (uncertain)
else if (sourceType === 'context' || destinationType === 'context') {
applyEffect({kind: 'MaybeAlias', from: effect.from, into: effect.into});
}
}
Apply:
case 'Apply': {
const functionValues = state.values(effect.function);
// Case 1: Local function with known aliasingEffects
if (isLocalFunctionWithSignature(functionValues[0])) {
const signature = buildSignatureFromFunctionExpression(functionExpr);
const signatureEffects = computeEffectsForSignature(state.env, signature, ...);
for (const signatureEffect of signatureEffects) {
applyEffect(context, state, signatureEffect, initialized, effects);
}
}
// Case 2: Known hook signature
else if (effect.signature?.aliasing) {
const signatureEffects = computeEffectsForSignature(state.env, signature.aliasing, ...);
for (const signatureEffect of signatureEffects) {
applyEffect(context, state, signatureEffect, initialized, effects);
}
}
// Case 3: Legacy signature
else if (effect.signature != null) {
const legacyEffects = computeEffectsForLegacySignature(state, effect.signature, ...);
for (const legacyEffect of legacyEffects) {
applyEffect(context, state, legacyEffect, initialized, effects);
}
}
// Case 4: Unknown function (conservative)
else {
effects.push({kind: 'Create', into: effect.into, value: ValueKind.Mutable, ...});
for (const arg of allArgs) {
effects.push({kind: 'MutateTransitiveConditionally', value: arg});
effects.push({kind: 'MaybeAlias', from: arg, into: effect.into});
effects.push({kind: 'Capture', from: arg, into: everyOtherArg});
}
}
}
Phase 8: State Operations
class InferenceState {
#values: Map<InstructionValue, AbstractValue>; // Value kinds
#variables: Map<IdentifierId, Set<InstructionValue>>; // Places -> values
// Get kind of place (join of all possible values)
kind(place: Place): AbstractValue {
const values = this.#variables.get(place.identifier.id);
let mergedKind: AbstractValue | null = null;
for (const value of values) {
const kind = this.#values.get(value);
mergedKind = mergedKind ? mergeAbstractValues(mergedKind, kind) : kind;
}
return mergedKind;
}
// Assignment: place now points to same values as from
assign(place: Place, from: Place): void {
const values = this.#variables.get(from.identifier.id);
this.#variables.set(place.identifier.id, new Set(values));
}
// Alias: place may point to additional values
appendAlias(place: Place, from: Place): void {
const prevValues = this.values(place);
const fromValues = this.#variables.get(from.identifier.id);
this.#variables.set(place.identifier.id, new Set([...prevValues, ...fromValues]));
}
// Freeze: mark all values as frozen
freeze(place: Place, reason: ValueReason): boolean {
const value = this.kind(place);
switch (value.kind) {
case ValueKind.Context:
case ValueKind.Mutable:
case ValueKind.MaybeFrozen: {
const values = this.values(place);
for (const instrValue of values) {
this.freezeValue(instrValue, reason);
}
return true;
}
case ValueKind.Frozen:
case ValueKind.Global:
case ValueKind.Primitive: {
return false; // Already immutable
}
}
}
// Merge two states (lattice join)
merge(other: InferenceState): InferenceState | null {
let nextValues, nextVariables;
// Join values (using mergeValueKinds lattice)
for (const [id, thisValue] of this.#values) {
const otherValue = other.#values.get(id);
if (otherValue) {
const mergedValue = mergeAbstractValues(thisValue, otherValue);
if (mergedValue !== thisValue) {
nextValues ??= new Map(this.#values);
nextValues.set(id, mergedValue);
}
}
}
// Union variable sets
for (const [id, thisValues] of this.#variables) {
const otherValues = other.#variables.get(id);
if (otherValues && !Set_equal(thisValues, otherValues)) {
nextVariables ??= new Map(this.#variables);
nextVariables.set(id, new Set([...thisValues, ...otherValues]));
}
}
return nextValues || nextVariables ? new InferenceState(...) : null;
}
}
Phase 9: Value Kind Lattice
function mergeValueKinds(a: ValueKind, b: ValueKind): ValueKind {
if (a === b) return a;
// MaybeFrozen absorbs everything
if (a === ValueKind.MaybeFrozen || b === ValueKind.MaybeFrozen) {
return ValueKind.MaybeFrozen;
}
// Mutable dominates Frozen, Primitive
if (a === ValueKind.Mutable || b === ValueKind.Mutable) {
if (a === ValueKind.Frozen || b === ValueKind.Frozen) {
return ValueKind.MaybeFrozen; // Frozen | Mutable
}
if (a === ValueKind.Context || b === ValueKind.Context) {
return ValueKind.Context;
}
return ValueKind.Mutable;
}
// Context dominates Frozen, Primitive
if (a === ValueKind.Context || b === ValueKind.Context) {
if (a === ValueKind.Frozen || b === ValueKind.Frozen) {
return ValueKind.MaybeFrozen;
}
return ValueKind.Context;
}
// Frozen dominates Primitive
if (a === ValueKind.Frozen || b === ValueKind.Frozen) {
return ValueKind.Frozen;
}
// Global is top
if (a === ValueKind.Global || b === ValueKind.Global) {
return ValueKind.Global;
}
// Primitive is bottom
return ValueKind.Primitive;
}
Lattice diagram (simplified):
Global
|
Mutable
/ \
Context MaybeFrozen
\ /
Frozen
|
Primitive
Key Data Structures
AbstractValue:
{
kind: ValueKind;
reason: ReadonlySet<ValueReason>;
context: ReadonlySet<Place>; // For context tracking
}
InferenceState:
{
#values: Map<InstructionValue, AbstractValue>;
#variables: Map<IdentifierId, Set<InstructionValue>>;
#isFunctionExpression: boolean;
env: Environment;
}
InstructionSignature (cached):
{
effects: ReadonlyArray<AliasingEffect>;
}
Context (global cache):
{
internedEffects: Map<string, AliasingEffect>;
instructionSignatureCache: Map<Instruction, InstructionSignature>;
functionSignatureCache: Map<FunctionExpression, AliasingSignature>;
applySignatureCache: Map<AliasingSignature, Map<AliasingEffect, Array<AliasingEffect> | null>>;
catchHandlers: Map<BlockId, Place>;
hoistedContextDeclarations: Map<DeclarationId, Place | null>;
nonMutatingSpreads: Set<IdentifierId>;
}
Edge Cases
- Hoisted functions: Variables used before declaration need special handling
- Non-mutating spreads: Props spreading
{...rest} can be frozen if source is frozen
- Iterator types:
GetIterator on Array/Set/Map knows fresh iterator; on unknown types may alias
- Ref detection:
ref.current assignment generates hint for user feedback
- Catch handlers: Values thrown into catch parameters have special aliasing semantics
- Multiple predecessors: State merging uses union of value sets + lattice join of kinds
- Self-referencing: Phi nodes with self-loops handled by backedge delay
Mapping to CloudLang
Direct applicability:
- Phi node handling → Join points for control flow in CloudLang
- State merging → How to combine abstract states from multiple branches
- ValueKind lattice → Similar hierarchy for CloudLang's "mutable" detection
For closure analysis:
captures_mutable in CloudLang ≈ Capture effects on context variables
- Abstract interpretation could determine which closures actually read/write mutable state
OCaml sketch (simplified):
type value_kind =
| Primitive (* numbers, strings, booleans *)
| Frozen (* immutable values *)
| Mutable (* objects that can be modified *)
| Context (* values from outer scope *)
type abstract_value = {
kind: value_kind;
reason: value_reason Set.t;
}
type inference_state = {
values : (instruction_value, abstract_value) Hashtbl.t;
variables : identifier_id list Hashtbl.t; (* identifier -> possible values *)
}
let merge_value_kinds (a : value_kind) (b : value_kind) : value_kind =
match a, b with
| k, k -> k
| MaybeFrozen, _ | _, MaybeFrozen -> MaybeFrozen
| Mutable, Frozen | Frozen, Mutable -> MaybeFrozen
| Mutable, Context | Context, Mutable -> Context
| Frozen, _ | _, Frozen -> Frozen
| Global, _ | _, Global -> Global
| Primitive, Primitive -> Primitive
| Mutable, Primitive | Primitive, Mutable -> Mutable
Example
// Simple function with control flow
function foo(x) {
let y;
if (x > 0) {
y = x;
} else {
y = 0;
}
return y;
}
// Initial: x = Mutable
// Block 1 (if): Phi y = {x, 0}
// - After merge: y = MaybeFrozen (Mutable | Primitive)
// Block 2 (return): Freeze y
// - y is MaybeFrozen, freeze succeeds, changes to Frozen
// - Return: Freeze effect on y
// Final effects:
// - Create x = Mutable (param)
// - Phi y = {x, 0} → y starts as MaybeFrozen
// - Freeze y (at return) → y becomes Frozen
Component: Mutable Range Computation (InferMutationAliasingRanges.ts)
File
/react/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingRanges.ts
Purpose
Build a data flow graph from effects, compute mutable ranges for each value, and extract externally-visible function effects.
The mutable range (start:end) defines which instructions a value is actively being mutated.
Input Invariants
- Effects are populated by
InferMutationAliasingEffects
- HIR has CFG with blocks, instructions, terminals
- Phi nodes are processed with operand relationships
Output Guarantees
- Each
Identifier.mutableRange is populated with {start, end}
- Each
Place.effect is populated with legacy Effect enum
- Function expressions have
aliasingEffects with externally visible effects
Render effects trigger validation of function calls
Algorithm (detailed, step-by-step)
Phase 1: Build Data Flow Graph
class AliasingState {
nodes: Map<Identifier, Node>;
Node = {
id: Identifier;
createdFrom: Map<Identifier, number>; // CreateFrom edges (backward)
captures: Map<Identifier, number>; // Capture edges (backward)
aliases: Map<Identifier, number>; // Assign/Alias edges (backward)
maybeAliases: Map<Identifier, number>; // MaybeAlias edges (backward)
edges: Array<{index, node, kind}>; // Forward edges (for forward traversal)
transitive: {kind, loc} | null; // Transitive mutation state
local: {kind, loc} | null; // Local mutation state
lastMutated: number; // Index of last mutation
mutationReason: MutationReason | null;
value: {kind: 'Object' | 'Phi' | 'Function'; ...}
};
}
Graph construction (ordered by instruction index):
let index = 0;
// Create nodes for params, context, return
for (const param of [...fn.params, ...fn.context, fn.returns]) {
state.create(param, {kind: 'Object'});
}
// Process each block
for (const block of fn.body.blocks.values()) {
// Phi nodes first
for (const phi of block.phis) {
state.create(phi.place, {kind: 'Phi'});
for (const [pred, operand] of phi.operands) {
if (!seenBlocks.has(pred)) {
// Backedge: defer processing
pendingPhis.get(pred).push({from: operand, into: phi.place, index});
} else {
state.assign(index++, operand, phi.place);
}
}
}
seenBlocks.add(block.id);
// Instructions
for (const instr of block.instructions) {
for (const effect of instr.effects) {
switch (effect.kind) {
case 'Create': {
state.create(effect.into, {kind: 'Object'});
break;
}
case 'CreateFrom': {
state.createFrom(index++, effect.from, effect.into);
break;
}
case 'Assign':
case 'Alias': {
state.assign(index++, effect.from, effect.into);
break;
}
case 'Capture': {
state.capture(index++, effect.from, effect.into);
break;
}
case 'Mutate': {
mutations.push({
index: index++,
place: effect.value,
transitive: false,
kind: MutationKind.Definite,
reason: effect.reason
});
break;
}
case 'MutateTransitive': {
mutations.push({
index: index++,
place: effect.value,
transitive: true,
kind: MutationKind.Definite
});
break;
}
case 'Freeze': {
// No graph effect, handled in mutation traversal
break;
}
case 'Render': {
renders.push({index: index++, place: effect.place});
break;
}
case 'MutateFrozen':
case 'MutateGlobal':
case 'Impure': {
errors.pushDiagnostic(effect.error);
functionEffects.push(effect);
break;
}
}
}
}
// Process pending phi edges after block
const blockPhis = pendingPhis.get(block.id);
if (blockPhis) {
for (const {from, into, index} of blockPhis) {
state.assign(index, from, into);
}
}
// Terminal effects
if (block.terminal.kind === 'return') {
state.assign(index++, block.terminal.value, fn.returns);
}
}
Phase 2: Process Mutations
for (const mutation of mutations) {
state.mutate(
mutation.index,
mutation.place.identifier,
makeInstructionId(mutation.id + 1), // end of mutation
mutation.transitive,
mutation.kind,
mutation.place.loc,
mutation.reason,
errors
);
}
for (const render of renders) {
state.render(render.index, render.place.identifier, errors);
}
Mutation traversal (bidirectional graph walk):
mutate(index, start, end, transitive, startKind, loc, reason, errors): void {
const seen = new Map<Identifier, MutationKind>();
const queue = [{
place: start,
transitive,
direction: 'backwards' | 'forwards',
kind: startKind
}];
while (queue.length !== 0) {
const {place: current, transitive, direction, kind} = queue.pop()!;
const previousKind = seen.get(current);
if (previousKind != null && previousKind >= kind) {
continue; // Already processed with stronger mutation
}
seen.set(current, kind);
const node = this.nodes.get(current);
node.lastMutated = Math.max(node.lastMutated, index);
if (end != null) {
node.id.mutableRange.end = Math.max(node.id.mutableRange.end, end);
}
// Set mutation state
if (transitive) {
if (node.transitive == null || node.transitive.kind < kind) {
node.transitive = {kind, loc};
}
} else {
if (node.local == null || node.local.kind < kind) {
node.local = {kind, loc};
}
}
// Forward edges: mutation affects successors
for (const edge of node.edges) {
if (edge.index >= index) break; // Edge after mutation
queue.push({
place: edge.node,
transitive,
direction: 'forwards',
kind: edge.kind === 'maybeAlias' ? MutationKind.Conditional : kind
});
}
// Backward edges: mutation affects predecessors
// Only for aliases/assigns/createfrom, not captures
for (const [alias, when] of node.createdFrom) {
if (when >= index) continue;
queue.push({place: alias, transitive: true, direction: 'backwards', kind});
}
for (const [alias, when] of node.aliases) {
if (when >= index) continue;
queue.push({place: alias, transitive, direction: 'backwards', kind});
}
for (const [alias, when] of node.maybeAliases) {
if (when >= index) continue;
queue.push({
place: alias,
transitive,
direction: 'backwards',
kind: MutationKind.Conditional // Downgrade for uncertainty
});
}
if (direction === 'backwards' || node.value.kind !== 'Phi') {
// Transitive mutation also affects captures
if (transitive) {
for (const [capture, when] of node.captures) {
if (when >= index) continue;
queue.push({place: capture, transitive, direction: 'backwards', kind});
}
}
}
}
}
Render validation:
render(index, start, errors): void {
const seen = new Set<Identifier>();
const queue = [start];
while (queue.length !== 0) {
const current = queue.pop()!;
if (seen.has(current)) continue;
seen.add(current);
const node = this.nodes.get(current);
if (node.transitive != null || node.local != null) continue;
if (node.value.kind === 'Function') {
// Check function for side effects
appendFunctionErrors(errors, node.value.function);
}
// Traverse reachable values
for (const [alias, when] of node.createdFrom) {
if (when >= index) continue;
queue.push(alias);
}
for (const [alias, when] of node.aliases) {
if (when >= index) continue;
queue.push(alias);
}
for (const [capture, when] of node.captures) {
if (when >= index) continue;
queue.push(capture);
}
}
}
Phase 3: Populate Function Effects
for (const param of [...fn.context, ...fn.params]) {
const place = param.kind === 'Identifier' ? param : param.place;
const node = state.nodes.get(place.identifier);
if (!node) continue;
// Local mutation
if (node.local?.kind === MutationKind.Conditional) {
functionEffects.push({
kind: 'MutateConditionally',
value: {...place, loc: node.local.loc}
});
} else if (node.local?.kind === MutationKind.Definite) {
functionEffects.push({
kind: 'Mutate',
value: {...place, loc: node.local.loc},
reason: node.mutationReason
});
}
// Transitive mutation
if (node.transitive?.kind === MutationKind.Conditional) {
functionEffects.push({
kind: 'MutateTransitiveConditionally',
value: {...place, loc: node.transitive.loc}
});
} else if (node.transitive?.kind === MutationKind.Definite) {
functionEffects.push({
kind: 'MutateTransitive',
value: {...place, loc: node.transitive.loc}
});
}
// Set legacy effect
if (node.local != null || node.transitive != null) {
place.effect = Effect.Capture;
}
}
Phase 4: Add Legacy Effects
for (const block of fn.body.blocks.values()) {
for (const phi of block.phis) {
phi.place.effect = Effect.Store;
const isMutated = phi.place.identifier.mutableRange.end > firstInstructionId;
for (const operand of phi.operands.values()) {
operand.effect = isMutated ? Effect.Capture : Effect.Read;
}
}
for (const instr of block.instructions) {
// Lvalues default to ConditionallyMutate
for (const lvalue of eachInstructionLValue(instr)) {
lvalue.effect = Effect.ConditionallyMutate;
if (lvalue.identifier.mutableRange.start === 0) {
lvalue.identifier.mutableRange.start = instr.id;
}
if (lvalue.identifier.mutableRange.end === 0) {
lvalue.identifier.mutableRange.end = makeInstructionId(instr.id + 1);
}
}
// Operands default to Read
for (const operand of eachInstructionValueOperand(instr.value)) {
operand.effect = Effect.Read;
}
// Override effects based on instruction effects
const operandEffects = new Map<IdentifierId, Effect>();
for (const effect of instr.effects) {
switch (effect.kind) {
case 'Assign':
case 'Alias':
case 'Capture':
case 'CreateFrom': {
const isMutated = effect.into.identifier.mutableRange.end > instr.id;
operandEffects.set(effect.from.identifier.id, isMutated ? Effect.Capture : Effect.Read);
operandEffects.set(effect.into.identifier.id, Effect.Store);
break;
}
case 'Mutate': {
operandEffects.set(effect.value.identifier.id, Effect.Store);
break;
}
case 'MutateTransitive':
case 'MutateConditionally':
case 'MutateTransitiveConditionally': {
operandEffects.set(effect.value.identifier.id, Effect.ConditionallyMutate);
break;
}
case 'Freeze': {
operandEffects.set(effect.value.identifier.id, Effect.Freeze);
break;
}
}
}
// Apply operand effects
for (const lvalue of eachInstructionLValue(instr)) {
const effect = operandEffects.get(lvalue.identifier.id) ?? Effect.ConditionallyMutate;
lvalue.effect = effect;
}
for (const operand of eachInstructionValueOperand(instr.value)) {
if (operand.identifier.mutableRange.end > instr.id &&
operand.identifier.mutableRange.start === 0) {
operand.identifier.mutableRange.start = instr.id;
}
const effect = operandEffects.get(operand.identifier.id) ?? Effect.Read;
operand.effect = effect;
}
// Handle hoisted functions
if (instr.value.kind === 'StoreContext' &&
instr.value.value.identifier.mutableRange.end <= instr.id) {
instr.value.value.identifier.mutableRange.end = makeInstructionId(instr.id + 1);
}
}
// Terminal effects
if (block.terminal.kind === 'return') {
block.terminal.value.effect = isFunctionExpression ? Effect.Read : Effect.Freeze;
}
}
Phase 5: Extract Function Signature
const returns = fn.returns.identifier;
functionEffects.push({
kind: 'Create',
into: fn.returns,
value: isPrimitiveType(returns) ? ValueKind.Primitive
: isJsxType(returns.type) ? ValueKind.Frozen
: ValueKind.Mutable,
reason: ValueReason.KnownReturnSignature
});
// Simulate transitive mutation of each param/context/return
const tracked = [...fn.params, ...fn.context, fn.returns].map(p => placeOf(p));
for (const into of tracked) {
state.mutate(mutationIndex++, into.identifier, null, true,
MutationKind.Conditional, into.loc, null, ignoredErrors);
for (const from of tracked) {
if (from.identifier.id === into.identifier.id ||
from.identifier.id === fn.returns.identifier.id) {
continue;
}
const fromNode = state.nodes.get(from.identifier);
if (fromNode.lastMutated === mutationIndex) {
if (into.identifier.id === fn.returns.identifier.id) {
// Return aliases to param/context
functionEffects.push({kind: 'Alias', from, into});
} else {
// Params/context capture each other
functionEffects.push({kind: 'Capture', from, into});
}
}
}
}
Key Data Structures
Node:
{
id: Identifier;
createdFrom: Map<Identifier, number>; // Backward CreateFrom edges
captures: Map<Identifier, number>; // Backward Capture edges
aliases: Map<Identifier, number>; // Backward Assign/Alias edges
maybeAliases: Map<Identifier, number>; // Backward MaybeAlias edges
edges: Array<{index, node, kind}>; // Forward edges (ordered)
transitive: {kind, loc} | null;
local: {kind, loc} | null;
lastMutated: number;
mutationReason: MutationReason | null;
value: {kind: 'Object' | 'Phi' | 'Function'; function?: HIRFunction};
}
MutableRange:
{
start: InstructionId; // Inclusive
end: InstructionId; // Exclusive
}
MutationKind (enum):
None = 0,
Conditional = 1, // Maybe mutated
Definite = 2, // Definitely mutated
Edge Cases
- Phi node ranges: Mutated after creation need non-zero start
- Hoisted functions: StoreContext extends mutable range to hoisting point
- Instruction effects: Overwrite default operand effects
- Return values: Frozen for components, Read for function expressions
- Self-referencing: Loop backedges handled correctly
- No mutations: Range may remain at 0:0 (immutable)
Mapping to CloudLang
Direct applicability:
- Mutable ranges → When a closure's captured state is live (for Worker group co-location)
- Function effects → Signature for inter-closure communication
For service decomposition:
(* Determine if closures need to be in same Worker *)
type closure_effect =
| Mutate of string (* Direct mutation *)
| MutateTransitive of string (* Transitive mutation *)
| MutateConditionally of string (* Conditional mutation *)
| Capture of string * string (* One closure captures another's state *)
(* Determine if two closures should share a Worker *)
let same_worker_required (c1 : closure_info) (c2 : closure_info) : bool =
(* Check if they share mutable state through captures *)
List.exists (fun (cap : string) ->
List.mem cap c1.captures_mutable &&
List.mem cap c2.captures_mutable)
|| (* Check if one mutates the other's state *)
List.exists (fun (effect : closure_effect) ->
match effect with
| Mutate state | MutateTransitive state ->
List.mem state c1.owned_state &&
List.mem state c2.captures_mutable
| Capture from * into ->
List.mem from c2.owned_state &&
List.mem into c1.captures_mutable
| _ -> false
) c1.effects
Example:
// React example
function Component(props) {
const [state, setState] = useState({count: 0});
const handler1 = () => {
setState({count: state.count + 1}); // Captures state
};
const handler2 = () => {
setState({count: state.count + 2}); // Also captures state
};
return <Button onClick={handler1} onOtherClick={handler2} />;
}
// Effects for handler1:
// - CreateFunction with captures: [state]
// - Capture state -> handler1 (via setState call)
// - MutateTransitive state (via setState)
//
// Function signature for Component:
// - MutateTransitive state (from both handlers)
// - Capture state -> Component.returns (implicit via useState)
Component: CloudLang Decomposition (decompose.ml)
File
/home/jm/data/ai/cloudlang-trees/react/lib/decompose.ml and decompose.mli
Purpose
Current CloudLang implementation of closure-to-Worker decomposition. Groups closures based on shared mutable state.
Input Invariants
Scope.analysis_result contains closures with captures_mutable and called_functions
- Each closure has name, captures_mutable list, called_functions list
Output Guarantees
worker_group list with: name, functions, owned_state, service_deps
- Each group is converted to IR Worker with KV bindings and service bindings
Algorithm (detailed, step-by-step)
type worker_group = {
name : string;
functions : string list;
owned_state : string list; (* Mutable state owned by this group *)
service_deps : string list; (* Other groups this group calls *)
}
Step 1: Build state → functions mapping
(* Find all mutable state variables *)
let all_mutable =
List.concat_map
(fun (c : Scope.closure_info) -> c.captures_mutable)
closures
|> String_list.dedup_preserve_order
(* For each mutable state, find all closures that capture it *)
let state_groups =
List.map
(fun (state_var : string) ->
let fns =
List.filter_map
(fun (c : Scope.closure_info) ->
if List.mem state_var c.captures_mutable
then Some c.name
else None)
closures
in
(state_var, String_list.dedup_preserve_order fns))
all_mutable
Step 2: Merge groups with overlapping functions
(* If a closure captures multiple state vars, merge those groups *)
let merged_groups =
List.fold_left
(fun acc (state_var, fns) ->
let overlapping, rest =
List.partition
(fun (_states, group_fns) ->
List.exists (fun f -> List.mem f group_fns) fns)
acc
in
match overlapping with
| [] -> ([state_var], fns) :: rest
| _ ->
let all_states =
String_list.dedup_preserve_order ([state_var] @ List.concat_map fst overlapping)
in
let all_fns =
String_list.dedup_preserve_order (fns @ List.concat_map snd overlapping)
in
(all_states, all_fns) :: rest)
) [] state_groups
Result: Groups may have multiple owned_state entries (merged closures).
Step 3: Add standalone functions
(* Functions with no mutable captures become their own group *)
let assigned_fns = List.concat_map (fun g -> g.functions) state_based_groups
let standalone_groups =
List.filter_map
(fun (c : Scope.closure_info) ->
if List.mem c.name assigned_fns
then None
else Some {
name = c.name;
functions = [c.name];
owned_state = [];
service_deps = [];
})
closures
Step 4: Compute cross-group dependencies
let to_ir (groups : worker_group list) : Ir.config =
List.map
(fun (group : worker_group) ->
let deps =
List.concat_map
(fun (fn_name : string) ->
match List.find_opt (fun c -> c.name = fn_name) closures with
| None -> []
| Some closure ->
List.filter_map
(fun (called : string) ->
match find_group_of_function groups called with
| Some target_group when target_group <> group.name -> Some target_group
| _ -> None)
closure.called_functions)
group.functions
in
{ group with service_deps = String_list.dedup_preserve_order deps })
groups
Step 5: Generate Worker IR
let generate_worker_script (group : worker_group) : string =
(* KV namespace for owned state *)
let kv_types =
List.map
(fun state_var ->
Printf.sprintf " %s: KVNamespace;" (String.uppercase_ascii state_var))
group.owned_state
@ (* Service bindings for dependencies *)
List.map
(fun dep ->
Printf.sprintf " %s: Fetcher;" (String.uppercase_ascii dep))
group.service_deps
in
(* Generate TypeScript Worker code *)
Printf.sprintf
{|
interface Env {
%s
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
%s
return new Response("ok");
}
}
|}
(String.concat "\n" kv_types)
(String.concat "\n" (
(* Access KV bindings *)
(List.map (fun s -> s) (List.map (fun state_var ->
Printf.sprintf " const %s = env.%s;" state_var (String.uppercase_ascii state_var)
) group.owned_state @
List.map (fun dep ->
Printf.sprintf " const %s = env.%s;" dep (String.uppercase_ascii dep)
) group.service_deps @
(* Placeholders for function handlers *)
List.map (fun fn -> Printf.sprintf " // handler: %s" fn) group.functions @
[ " return new Response(\"ok\");" ])))
Step 6: Durable Object inference
let do_fit_score (group : worker_group) : int =
let markers = infer_markers group in
(* Markers based on naming, structure *)
let marker_total =
List.fold_left (fun acc marker -> acc + marker_weight marker) 0 markers
in
let structure_bonus =
(if List.length group.functions > 1 then 5 else 0) +
(if group.service_deps <> [] then 5 else 0) +
(if has_exported_function group analysis then 5 else 0)
in
clamp_score (marker_total + structure_bonus) (* 0 to 100 *)
let infer_do_annotation (group : worker_group) : Ir.do_annotation option =
let markers = infer_markers group in
let score = do_fit_score_with_markers group markers in
if score < 60 then None
else
Some {
class_name = Printf.sprintf "%sCoordinatorDO" (pascal_case group.name);
binding_name = Printf.sprintf "%s_DO" (String.uppercase_ascii group.name);
markers;
intent = (* CoordinationIntent based on markers *);
if List.mem Ir.RealtimeFanout markers then Ir.RealtimeRoom {room_key_expr = "room_id"}
else if List.mem Ir.DurableTimerNeeded markers then Ir.ScheduledQueue {queue_key_expr}
else if group.owned_state <> [] then Ir.PerEntitySerial {key_expr}
else Ir.GlobalSingleton {name = group.name}
;
routing = (* RoutingPolicy based on score and deps *);
routing_policy ...
durability = (* DurabilityPolicy based on markers *)
durability_policy ...
;
do_fit_score = score;
reasons = List.map marker_reason markers;
}
Markers for Durable Object hints:
SerialConflictHotspot (score: 25)
StrictOrderingRequired (score: 20)
RealtimeFanout (score: 15)
DurableTimerNeeded (score: 15)
GlobalSingletonNeeded (score: 10)
EntityAffinityKeyed (score: 10)
ConsistencyBoundaryCrossed (score: 10)
HighFanInCoordinator (score: 5)
StatelessFastPathPreferWorker (score: -30)
Key Data Structures
closure_info (from Scope):
{
name : string;
captures_mutable : string list; (* Variables this closure captures that are mutable *)
called_functions : string list; (* Functions this closure calls *)
}
worker_group:
{
name : string;
functions : string list;
owned_state : string list;
service_deps : string list;
}
Edge Cases
- No mutable state: Closures with empty
captures_mutable become standalone groups
- Single function groups: May still be good Durable Object candidates (score threshold)
- No service deps: Stateless functions may not need Durable Objects
- Multiple state vars: Groups with multiple
owned_state entries
Mapping to React Compiler
Current CloudLang approach vs React Compiler precision:
| Aspect |
CloudLang (current) |
React Compiler |
Gap |
| Mutable state detection |
new Map/Set/Array |
Abstract interpretation + phi joins |
Major |
| Capture detection |
Direct syntax |
Capture effects |
Major |
| Mutation propagation |
None (assumes all mutate) |
Transitive rules |
Major |
| Transitive mutation |
Unknown (assumes worst) |
Precise tracking |
Major |
| Read-only closures |
Handled separately |
ImmutableCapture |
Gap |
| Conditional mutation |
Unknown |
MaybeAlias + MutateConditionally |
Major |
| Function signatures |
None |
AliasingSignature |
Major |
Key insight: CloudLang groups closures solely by captures_mutable which is essentially "do any closures share a mutable variable?" This is equivalent to checking if there are any Capture effects between their context variables.
Example
(* CloudLang example *)
let closures = [
{ name = "fetchHandler";
captures_mutable = ["cache"];
called_functions = ["fetchFromNetwork"];
};
{ name = "fetchFromNetwork";
captures_mutable = ["apiToken"];
called_functions = [];
};
{ name = "updateHandler";
captures_mutable = ["cache"];
called_functions = [];
};
] in
(* Step 1: State groups *)
let state_groups = [
("cache", ["fetchHandler", "updateHandler"]);
("apiToken", ["fetchFromNetwork"]);
]
(* Step 2: Standalone functions *)
let all_groups = state_groups @ [
{ name = "fetchFromNetwork"; functions = ["fetchFromNetwork"]; owned_state = []; service_deps = [] }
]
(* Step 3: Service deps *)
(* fetchFromNetwork is called by fetchHandler *)
let with_deps = [
{ name = "cache"; functions = ["fetchHandler", "updateHandler"];
service_deps = ["fetchFromNetwork"] };
{ name = "fetchFromNetwork"; functions = ["fetchFromNetwork"];
service_deps = [] };
]
CloudLang Specific Analysis
1. Which Closures Mutate Shared State (Worker Group Decomposition)
Current CloudLang approach:
(* Very conservative: if ANY shared mutable variable, same group *)
let same_worker_required (c1 : closure_info) (c2 : closure_info) : bool =
List.exists (fun cap ->
List.mem cap c1.captures_mutable &&
List.mem cap c2.captures_mutable
) c1.captures_mutable
React Compiler would track:
// For each closure, determine actual mutations
function computeClosureSignature(fn: HIRFunction): AliasingEffect[] {
const effects: AliasingEffect[] = [];
// Track all mutations of context variables
for (const instr of fn.body.instructions) {
for (const effect of instr.effects) {
// Check if effect mutates a context variable
if (isMutateEffect(effect)) {
for (const capturedVar of fn.context) {
if (affects(effect, capturedVar)) {
effects.push({
kind: isTransitiveMutation(effect) ? 'MutateTransitive' : 'Mutate',
value: capturedVar
});
}
}
}
}
}
// Add capture effects for return value
effects.push(...);
return effects;
}
Improvement for CloudLang:
type closure_mutation_analysis = {
closure_name : string;
directly_mutates : string list; (* state vars directly mutated *)
transitively_mutates : string list; (* state vars transitively mutated *)
captures_read_only : string list; (* state vars read but not mutated *)
calls_mutating_closures : string list; (* other closures that might mutate state *)
}
let same_worker_required_precise (c1 : closure_mutation_analysis)
(c2 : closure_mutation_analysis) : bool =
(* Case 1: Both mutate the same state variable *)
List.exists (fun state ->
List.mem state c1.directly_mutates ||
List.mem state c1.transitively_mutates ||
List.mem state c2.directly_mutates ||
List.mem state c2.transitively_mutates)
c1.captures_read_only
||
(* Case 2: One calls the other, and the other mutates state *)
(List.exists (fun called -> List.mem called c2.closes_state) c1.calls_mutating_closures &&
(List.exists (fun called -> List.mem called c1.closes_state) c2.calls_mutating_closures))
||
(* Case 3: Both capture same read-only state *)
(List.exists (fun state -> List.mem state c1.captures_read_only &&
List.mem state c2.captures_read_only)
c1.captures_read_only)
2. Transitive Mutations (KV vs Durable Object Decisions)
Current CloudLang: Assumes all closures with shared state need Durable Objects (serializable access).
React Compiler tracks precise transitivity:
// Non-transitive: mutation stays in container
array.push(x); // Mutate array, NOT x
// Transitive: mutation reaches nested values
obj.prop = x; // Mutate obj, but x is captured (so transitive)
For CloudLang decomposition:
(* After aliasing analysis, classify closures *)
type closure_kind =
| Stateless (* No mutable state *)
| ReadOnlyState (* Reads state, never mutates *)
| DirectMutator (* Directly mutates state *)
| TransitiveMutator (* Transitively mutates state *)
| ConcurrentMutator (* May race with other mutators *)
let classify_closure (analysis : closure_mutation_analysis) : closure_kind =
match analysis.directly_mutates, analysis.transitively_mutates with
| [], [] -> Stateless
| [], _ -> TransitiveMutator
| _, [] -> DirectMutator
| _, _ -> ConcurrentMutator
let decomposition_strategy (kind : closure_kind) : deployment_strategy =
match kind with
| Stateless -> StatelessWorker (* Regular Worker *)
| ReadOnlyState -> StatelessWorker (* Can be parallelized *)
| DirectMutator -> DurableObject (* Single DO per entity *)
| TransitiveMutator -> DurableObject (* Single DO per entity *)
| ConcurrentMutator -> DurableObject (* Single DO for serialization *)
KV vs DO decision:
let choose_storage_backend (group : worker_group) : storage_backend =
(* Check for concurrent mutations *)
let has_concurrent_mutators =
List.exists (fun closure ->
match classify_closure (get_mutation_analysis closure) with
| ConcurrentMutator -> true
| _ -> false
) group.functions
in
(* Check for transitive mutations *)
let has_transitive_mutations =
List.exists (fun closure ->
match classify_closure (get_mutation_analysis closure) with
| TransitiveMutator -> true
| _ -> false
) group.functions
in
match has_concurrent_mutators, has_transitive_mutations with
| true, _ -> DurableObjectSqlite (* Concurrent access needs DO *)
| _, true -> DurableObjectSqlite (* Transitive needs DO *)
| false, false ->
(* Only direct mutations or read-only *)
if group.owned_state = [] then DoSyncKv
else DoSyncKv (* KV is faster for simple cases *)
3. Aliased vs Read-Only Captures (Binding Type Decisions)
React Compiler distinction:
// Capture effect: data flow without mutation propagation
arr.push(x); // Capture x -> arr (Mutate arr !=> Mutate x)
// ImmutableCapture: read-only flow
const y = x.frozenField; // ImmutableCapture x -> y
For CloudLang binding types:
type binding_mode =
| ValueBinding (* Pass by value (copy) *)
| ReadOnlyReference (* Immutable reference *)
| MutableReference (* Mutable reference *)
| SharedBinding (* Shared mutable state *)
let determine_binding_mode (state_var : string)
(closure_analysis : closure_mutation_analysis)
(all_closures : string list) : binding_mode =
(* Count how closures interact with this state *)
let readers = List.filter (fun c ->
List.mem state_var c.captures_read_only
) all_closures in
let mutators = List.filter (fun c ->
List.mem state_var c.directly_mutates ||
List.mem state_var c.transitively_mutates
) all_closures in
in
match List.length readers, List.length mutators with
| 0, 0 -> ValueBinding (* Unused *)
| _, 0 -> ReadOnlyReference (* Read-only, can be immutable *)
| 0, _ -> SharedBinding (* Multiple mutators need coordination *)
| 1, 1 -> MutableReference (* Single mutator *)
| _, _ -> SharedBinding (* Multiple readers + mutator *)
4. Minimal Viable Aliasing Model for CloudLang
Core components needed:
- Value tracking (simplified from React):
type value_kind =
| Primitive (* Numbers, strings, booleans *)
| Immutable (* Explicitly frozen values *)
| MaybeImmutable (* Union of immutable and mutable *)
| Mutable (* Objects that can be modified *)
type abstract_value = {
kind : value_kind;
reason : reason list; (* Why it has this kind *)
}
- Effect tracking (subset of React's effects):
type aliasing_effect =
(* Data flow effects *)
| Create of {into : lvalue; kind : value_kind}
| Assign of {from : lvalue; into : lvalue}
| Capture of {from : lvalue; into : lvalue}
| CreateFrom of {from : lvalue; into : lvalue}
(* Mutation effects *)
| Mutate of {value : lvalue; transitive : bool}
| Freeze of {value : lvalue}
- Phi node handling:
type phi_node = {
result : lvalue;
operands : (string * lvalue) list; (* Control flow sources *)
}
let apply_phi (state : inference_state) (phi : phi_node) : inference_state =
(* Union of incoming value kinds *)
let merged_kind =
List.fold_left (fun acc (_pred, operand) ->
let operand_kind = get_value_kind state operand in
merge_value_kinds acc operand_kind
) value_kind.Primitive phi.operands
in
update_variable state phi.result merged_kind
- Abstract interpretation loop:
type inference_state = {
values : (string, abstract_value) Hashtbl.t;
worklist : string list; (* Variables to process *)
}
let compute_effects (hir : hir_function) : aliasing_effect list =
(* Worklist algorithm for fixpoint *)
let state = { values = Hashtbl.create 100; worklist = [] } in
(* Initialize parameters *)
List.iter (fun (param : parameter) ->
let initial_kind =
if param.is_exported then value_kind.Immutable
else value_kind.Mutable
in
state <- add_variable state param.name initial_kind
) hir.parameters;
(* Process until fixpoint *)
let rec loop () =
match state.worklist with
| [] -> ()
| var :: rest ->
state <- process_variable state var;
state.worklist <- rest;
(* Add variables that changed *)
List.iter (fun dep -> add_to_worklist_if_changed state dep)
(get_dependents var)
in loop ()
- Lattice join:
let merge_value_kinds (a : value_kind) (b : value_kind) : value_kind =
match a, b with
| k, k -> k
| MaybeImmutable, _ | _, MaybeImmutable -> MaybeImmutable
| Mutable, Immutable | Immutable, Mutable -> MaybeImmutable
| Immutable, _ | _, Immutable -> Immutable
| Primitive, Primitive -> Primitive
Simplification rationale:
- No need for
Context kind (CloudLang has flat module structure)
- No need for
MaybeAlias (use conservative Capture)
- No need for
Render effects (not React-specific)
- No need for legacy
Effect enum (can directly use new effects)
What's retained:
- Mutation transitivity: Critical for DO vs KV decisions
- Capture vs CreateFrom: Distinguishes reading vs writing
- Freeze semantics: For exported function boundaries
- Phi handling: For control flow joins
- Fixpoint iteration: For propagating information
Example
(* CloudLang program *)
let program = [
(* State variable *)
Decl("cache", MutableArray);
(* Closure 1: reads and writes *)
Closure("setCache",
params = ["key", "value"];
captures_mutable = ["cache"];
body = [
Call("push", [Var("cache"), Var("value")]) (* Mutate cache *)
]);
(* Closure 2: only reads *)
Closure("getCache",
params = ["key"];
captures_mutable = ["cache"];
body = [
Call("get", [Var("cache"), Var("key")]) (* CreateFrom cache *)
]);
(* Closure 3: reads, no mutation *)
Closure("hasKey",
params = ["key"];
captures_mutable = ["cache"];
body = [
Call("has", [Var("cache"), Var("key")]) (* CreateFrom cache, no mutation *)
]);
] in
(* Minimal aliasing analysis *)
let analysis = analyze_aliasing program in
(* Group by actual mutation patterns *)
let groups =
(* setCache: DirectMutator *)
{ name = "cacheManager";
functions = ["setCache"];
owned_state = ["cache"];
strategy = DurableObject }
::
(* getCache: ReadOnlyState *)
{ name = "cacheReader";
functions = ["getCache"];
owned_state = ["cache"];
strategy = DoSyncKv } (* Read-only can use KV *)
::
(* hasKey: ReadOnlyState *)
{ name = "cacheQuerier";
functions = ["hasKey"];
owned_state = ["cache"];
strategy = DoSyncKv }
Summary: Key Insights for CloudLang
1. The React Compiler's Strength
- Precise abstract interpretation: Tracks value kinds through control flow, merges at phi nodes
- Mutation transitivity: Distinguishes direct vs transitive mutations (critical for DO vs KV)
- Effect composition: Functions can have precise signatures describing their effects
- Conditional mutation:
MaybeAlias + MutateConditionally allows safe conservative analysis
- Freeze semantics: Reference-level freezing, not value-level (except for function captures)
2. Minimal CloudLang Aliasing Model
Core types (50 lines of OCaml):
type value_kind = Primitive | Immutable | MaybeImmutable | Mutable
type aliasing_effect =
| Create of {into: lvalue; kind: value_kind}
| Assign of {from: lvalue; into: lvalue}
| Capture of {from: lvalue; into: lvalue}
| CreateFrom of {from: lvalue; into: lvalue}
| Mutate of {value: lvalue; transitive: bool}
| Freeze of {value: lvalue}
type closure_signature = {
direct_mutates : string list;
transitive_mutates : string list;
captures_read_only : string list;
calls_closures : string list;
}
Core algorithm (100 lines of OCaml):
let analyze (hir : hir_function) : closure_signature =
let state = initial_state hir in
let rec loop worklist =
match worklist with
| [] -> state
| var :: rest ->
let effects = compute_effects_for_var state var in
let state' = apply_effects state var effects in
let worklist' =
rest @ (get_affected_vars effects) in
loop worklist'
in loop (get_all_vars hir)
let decompose (signatures : (string * closure_signature) list)
: worker_group list =
(* Group by: share mutable state OR call mutating closures *)
let rec merge_groups groups sig =
(* Find groups that should merge based on actual mutation patterns *)
in
List.fold_left merge_groups [] signatures
3. Priority Implementation Path
Phase 1 (1 week): Extend HIR with effect annotations
- Add
aliasing_effect field to HIR instructions
- Implement
compute_signature_for_instruction (subset of React's)
- Track value kinds (Primitive, Immutable, MaybeImmutable, Mutable)
Phase 2 (1 week): Abstract interpretation
- Implement fixpoint iteration over CFG
- Handle phi nodes with value kind merging
- Generate
Mutate effects with transitive flag
Phase 3 (1 week): Closure signature extraction
- For each closure, determine: direct_mutates, transitive_mutates, captures_read_only
- Identify calls to other closures
- Build call graph
Phase 4 (1 week): Enhanced decomposition
- Replace simple
captures_mutable check with precise mutation analysis
- Classify closures: Stateless, ReadOnly, DirectMutator, TransitiveMutator, ConcurrentMutator
- Choose storage backend based on classification
Phase 5 (1 week): Integration testing
- Update existing decompose.ml to use new analysis
- Validate DO vs KV decisions match intuition
- Add tests for transitive mutation detection
4. Expected Impact
Immediate benefits:
- Reduced Durable Objects: Read-only closures can use KV (10x faster, cheaper)
- Better parallelism: Stateless closures don't need coordination
- Fewer race conditions: Precise detection of concurrent mutations
Long-term benefits:
- Scalable architecture: Precise analysis enables better decomposition strategies
- Composability: Function signatures allow library-level effects
- Correctness: Avoid false positives (grouping unrelated closures)
This concludes the comprehensive analysis of the React Compiler's mutation and aliasing model and its applicability to CloudLang.