WP3: Mutation & aliasing model (@analyzer subagent)

/home/jm/data/ai/cloudlang-trees/react · 12 messages · 57 parts · · 180,570 tokens
Created Feb 10, 04:56:18 AM · Updated Feb 11, 04:54:04 AM · 2 syncs
userFeb 10, 04:56:18 AM

You are analyzing the React Compiler's mutation and aliasing model -- the heart of its analysis engine. This is RESEARCH ONLY -- do NOT edit or write any files. Return your complete analysis as your final message.

The React Compiler source is at: /home/jm/data/ai/cloudlang-trees/react/react/compiler/packages/babel-plugin-react-compiler/src/

Files to read and analyze:

  1. Inference/MUTABILITY_ALIASING_MODEL.md - Design document (READ THIS FIRST)
  2. Inference/AliasingEffects.ts - AliasingEffect type definitions and AliasingSignature
  3. Inference/InferMutationAliasingEffects.ts - The abstract interpretation engine (likely 1000+ lines -- read it all in chunks)
  4. Inference/InferMutationAliasingRanges.ts - Mutable range computation from effects

Also read for context:

  • HIR/HIR.ts - Reference types (Effect enum, ValueKind, MutableRange, Place, Identifier) - just the type definitions, not the whole file
  • /home/jm/data/ai/cloudlang-trees/react/lib/decompose.ml - CloudLang's current decomposition logic
  • /home/jm/data/ai/cloudlang-trees/react/lib/decompose.mli

Produce documentation using the template:

# Component Name

## File
## Purpose
## Input Invariants
## Output Guarantees
## Algorithm (detailed, step-by-step)
## Key Data Structures
## Edge Cases
## Mapping to CloudLang
## Example (before/after)

Specific questions to answer:

  1. The Aliasing Model (MUTABILITY_ALIASING_MODEL.md):

    • Fundamental concepts: values, references/places, aliasing, capturing, freezing
    • Alias vs Capture vs CreateFrom vs Assign distinctions
    • Transitivity rules (e.g., Capture then CreateFrom = Alias)
    • How Freeze works (freezes reference, not value)
    • MaybeAlias: when and why
  2. AliasingEffect Type (AliasingEffects.ts):

    • ALL 17+ variants with semantics
    • AliasingSignature: what it encodes
    • How effects compose
  3. Abstract Interpretation (InferMutationAliasingEffects.ts):

    • Abstract domain: AbstractValue = {kind: ValueKind, reason, context}
    • InferenceState: how it tracks pointer sets for phi nodes
    • Main loop: iteration to fixpoint
    • Candidate effects computation per instruction
    • applyEffect() walkthrough for each variant
    • Function call handling (Apply): local vs unknown vs built-in
    • state.freeze(), state.mutate(), state.assign(), state.merge()
    • ValueKind lattice ordering
  4. Mutable Range Computation (InferMutationAliasingRanges.ts):

    • Data flow graph construction from effects
    • MutableRange (start, end) computation
    • Extracting externally-visible effects for function expressions
    • Populating legacy Effect on Places
  5. For CloudLang specifically:

    • CloudLang detects "mutable state" by checking new Map/Set/Array. The React Compiler is vastly more precise. How to adapt for tracking:
      a) Which closures mutate shared state (Worker group decomposition)
      b) Transitive mutations (KV vs Durable Object decisions)
      c) Aliased vs read-only captures (binding type decisions)
    • What's the MINIMAL viable aliasing model for CloudLang?
    • OCaml type sketches for the core aliasing model

Return your COMPLETE analysis as a single structured markdown document in your final message. Be extremely thorough -- this is the most important work package. Do NOT write any files.

assistantFeb 10, 04:56:18 AM
glm-4.7 · zai-coding-plan
This is a complex research task where I need to analyze the React Compiler's mutation and aliasing model. Let me break this down: 1. I need to read multiple files from the React Compiler source 2. I need to understand CloudLang's current decomposition logic 3. I need to produce a comprehensive analysis using a specific template 4. This is RESEARCH ONLY - no file edits Let me start by reading all the required files. I'll use the batch tool to read them in parallel where possible. Files to read: 1. `Inference/MUTABILITY_ALIASING_MODEL.md` - Design document 2. `Inference/AliasingEffects.ts` - Type definitions 3. `Inference/InferMutationAliasingEffects.ts` - Abstract interpretation engine (likely large) 4. `Inference/InferMutationAliasingRanges.ts` - Mutable range computation 5. `HIR/HIR.ts` - Type definitions (Effect enum, ValueKind, MutableRange, Place, Identifier) 6. `/home/jm/data/ai/cloudlang-trees/react/lib/decompose.ml` 7. `/home/jm/data/ai/cloudlang-trees/react/lib/decompose.mli` Let me start by reading these files. Since InferMutationAliasingEffects.ts is likely 1000+ lines, I might need to read it in chunks.

I'll analyze the React Compiler's mutation and aliasing model by reading the source files and CloudLang's decomposition logic. Let me start by reading all the required files.

readcompleted
react/compiler/packages/babel-plugin-react-compiler/src/Inference/MUTABILITY_ALIASING_MODEL.md
Output
<file>
00001| # The Mutability & Aliasing Model
00002| 
00003| This document describes the new (as of June 2025) mutability and aliasing model powering React Compiler. The mutability and aliasing system is a conceptual subcomponent whose primary role is to determine minimal sets of values that mutate together, and the range of instructions over which those mutations occur. These minimal sets of values that mutate together, and the corresponding instructions doing those mutations, are ultimately grouped into reactive scopes, which then translate into memoization blocks in the output (after substantial additional processing described in the comments of those passes).
00004| 
00005| To build an intuition, consider the following example:
00006| 
00007| ```js
00008| function Component() {
00009|     // a is created and mutated over the course of these two instructions:
00010|     const a = {};
00011|     mutate(a);
00012| 
00013|     // b and c are created and mutated together — mutate might modify b via c
00014|     const b = {};
00015|     const c = {b};
00016|     mutate(c);
00017| 
00018|     // does not modify a/b/c
00019|     return <Foo a={a} c={c} />
00020| }
00021| ```
00022| 
00023| The goal of mutability and aliasing inference is to understand the set of instructions that create/modify a, b, and c.
00024| 
00025| In code, the mutability and aliasing model is compromised of the following phases:
00026| 
00027| * `InferMutationAliasingEffects`. Infers a set of mutation and aliasing effects for each instruction. The approach is to generate a set of candidate effects based purely on the semantics of each instruction and the types of the operands, then use abstract interpretation to determine the actual effects (or errros) that would apply. For example, an instruction that by default has a Capture effect might downgrade to an ImmutableCapture effect if the value is known to be frozen.
00028| * `InferMutationAliasingRanges`. Infers a mutable range (start:end instruc...
readcompleted
react/compiler/packages/babel-plugin-react-compiler/src/Inference/AliasingEffects.ts
Output
<file>
00001| /**
00002|  * Copyright (c) Meta Platforms, Inc. and affiliates.
00003|  *
00004|  * This source code is licensed under the MIT license found in the
00005|  * LICENSE file in the root directory of this source tree.
00006|  */
00007| 
00008| import {CompilerDiagnostic} from '../CompilerError';
00009| import {
00010|   FunctionExpression,
00011|   GeneratedSource,
00012|   Hole,
00013|   IdentifierId,
00014|   ObjectMethod,
00015|   Place,
00016|   SourceLocation,
00017|   SpreadPattern,
00018|   ValueKind,
00019|   ValueReason,
00020| } from '../HIR';
00021| import {FunctionSignature} from '../HIR/ObjectShape';
00022| import {printSourceLocation} from '../HIR/PrintHIR';
00023| 
00024| /**
00025|  * `AliasingEffect` describes a set of "effects" that an instruction/terminal has on one or
00026|  * more values in a program. These effects include mutation of values, freezing values,
00027|  * tracking data flow between values, and other specialized cases.
00028|  */
00029| export type AliasingEffect =
00030|   /**
00031|    * Marks the given value and its direct aliases as frozen.
00032|    *
00033|    * Captured values are *not* considered frozen, because we cannot be sure that a previously
00034|    * captured value will still be captured at the point of the freeze.
00035|    *
00036|    * For example:
00037|    * const x = {};
00038|    * const y = [x];
00039|    * y.pop(); // y dosn't contain x anymore!
00040|    * freeze(y);
00041|    * mutate(x); // safe to mutate!
00042|    *
00043|    * The exception to this is FunctionExpressions - since it is impossible to change which
00044|    * value a function closes over[1] we can transitively freeze functions and their captures.
00045|    *
00046|    * [1] Except for `let` values that are reassigned and closed over by a function, but we
00047|    * handle this explicitly with StoreContext/LoadContext.
00048|    */
00049|   | {kind: 'Freeze'; value: Place; reason: ValueReason}
00050|   /**
00051|    * Mutate ...
readcompleted
react/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingRanges.ts
Output
<file>
00001| /**
00002|  * Copyright (c) Meta Platforms, Inc. and affiliates.
00003|  *
00004|  * This source code is licensed under the MIT license found in the
00005|  * LICENSE file in the root directory of this source tree.
00006|  */
00007| 
00008| import {CompilerError, SourceLocation} from '..';
00009| import {
00010|   BlockId,
00011|   Effect,
00012|   HIRFunction,
00013|   Identifier,
00014|   IdentifierId,
00015|   InstructionId,
00016|   isJsxType,
00017|   makeInstructionId,
00018|   ValueKind,
00019|   ValueReason,
00020|   Place,
00021|   isPrimitiveType,
00022| } from '../HIR/HIR';
00023| import {
00024|   eachInstructionLValue,
00025|   eachInstructionValueOperand,
00026|   eachTerminalOperand,
00027| } from '../HIR/visitors';
00028| import {assertExhaustive, getOrInsertWith} from '../Utils/utils';
00029| import {Err, Ok, Result} from '../Utils/Result';
00030| import {AliasingEffect, MutationReason} from './AliasingEffects';
00031| 
00032| /**
00033|  * This pass builds an abstract model of the heap and interprets the effects of the
00034|  * given function in order to determine the following:
00035|  * - The mutable ranges of all identifiers in the function
00036|  * - The externally-visible effects of the function, such as mutations of params and
00037|  *   context-vars, aliasing between params/context-vars/return-value, and impure side
00038|  *   effects.
00039|  * - The legacy `Effect` to store on each Place.
00040|  *
00041|  * This pass builds a data flow graph using the effects, tracking an abstract notion
00042|  * of "when" each effect occurs relative to the others. It then walks each mutation
00043|  * effect against the graph, updating the range of each node that would be reachable
00044|  * at the "time" that the effect occurred.
00045|  *
00046|  * This pass also validates against invalid effects: any function that is reachable
00047|  * by being called, or via a Render effect, is validated against mutating globals
00048|  * or calli...
readcompleted
lib/decompose.ml
Output
<file>
00001| (** Closure decomposition: scope analysis -> Worker groups -> IR.
00002| 
00003|     Algorithm: 1. Seed groups from mutable state: functions that capture the
00004|     same mutable variable are placed in the same group. 2. Remaining functions
00005|     (no mutable captures) become their own group. 3. Cross-group function calls
00006|     become service binding dependencies. 4. Convert groups to IR Workers with KV
00007|     and service bindings. *)
00008| 
00009| type worker_group = {
00010|   name : string;
00011|   functions : string list;
00012|   owned_state : string list;
00013|   service_deps : string list;
00014| }
00015| 
00016| (** Find which group a function belongs to. *)
00017| let find_group_of_function (groups : worker_group list) (fn_name : string) :
00018|     string option =
00019|   List.find_map
00020|     (fun g -> if List.mem fn_name g.functions then Some g.name else None)
00021|     groups
00022| 
00023| (** Derive a group name from the mutable state variable name. e.g. "store" ->
00024|     "storage", "cache" -> "cache" *)
00025| let group_name_of_state (state_var : string) : string =
00026|   if state_var = "store" then "storage" else state_var
00027| 
00028| let clamp_score (score : int) : int = max 0 (min 100 score)
00029| 
00030| let contains_substr (s : string) (sub : string) : bool =
00031|   let len_s = String.length s in
00032|   let len_sub = String.length sub in
00033|   if len_sub = 0 then true
00034|   else if len_sub > len_s then false
00035|   else
00036|     let rec loop i =
00037|       if i > len_s - len_sub then false
00038|       else if String.sub s i len_sub = sub then true
00039|       else loop (i + 1)
00040|     in
00041|     loop 0
00042| 
00043| let any_name_matches (names : string list) (keywords : string list) : bool =
00044|   List.exists
00045|     (fun name ->
00046|       let lowered = String.lowercase_ascii name in
00047|       List.exists (fun keyword -> contains_substr lowered keyword) keyword...
readcompleted
lib/decompose.mli
Output
<file>
00001| (** Closure decomposition: scope analysis results -> IR config.
00002| 
00003|     Groups closures into Workers based on shared mutable state, generates KV
00004|     bindings for mutable state and service bindings for cross-group function
00005|     calls. *)
00006| 
00007| type worker_group = {
00008|   name : string;
00009|   functions : string list;
00010|   owned_state : string list;
00011|   service_deps : string list;
00012| }
00013| (** A logical group of functions that will become a single Worker. *)
00014| 
00015| val decompose : Scope.analysis_result -> worker_group list
00016| 
00017| val find_group_of_function : worker_group list -> string -> string option
00018| (** [find_group_of_function groups fn_name] returns the name of the group
00019|     that contains [fn_name], or [None]. *)
00020| 
00021| val do_fit_score : worker_group -> Scope.analysis_result -> int
00022| (** [do_fit_score group analysis] is a heuristic score in [0, 100] indicating
00023|     how well [group] fits Durable Object coordination semantics. *)
00024| 
00025| val infer_do_annotation :
00026|   worker_group -> Scope.analysis_result -> Ir.do_annotation option
00027| (** [infer_do_annotation group analysis] derives a Durable Object hint for
00028|     [group] when the fit score is high enough. *)
00029| 
00030| val to_ir : worker_group list -> Scope.analysis_result -> Ir.config
00031| 

(End of file - total 31 lines)
</file>
786in / 476out / 277reasoning
assistantFeb 10, 04:56:31 AM
glm-4.7 · zai-coding-plan
Good, I've read several key files. Now I need to read: 1. The HIR types (Effect enum, ValueKind, MutableRange, Place, Identifier) 2. The large InferMutationAliasingEffects.ts file Let me read these now.

Now let me read the HIR types and the large InferMutationAliasingEffects.ts file in chunks:

readcompleted
react/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts
Output
<file>
00001| /**
00002|  * Copyright (c) Meta Platforms, Inc. and affiliates.
00003|  *
00004|  * This source code is licensed under the MIT license found in the
00005|  * LICENSE file in the root directory of this source tree.
00006|  */
00007| 
00008| import {BindingKind} from '@babel/traverse';
00009| import * as t from '@babel/types';
00010| import {
00011|   CompilerDiagnostic,
00012|   CompilerError,
00013|   ErrorCategory,
00014| } from '../CompilerError';
00015| import {assertExhaustive} from '../Utils/utils';
00016| import {Environment, ReactFunctionType} from './Environment';
00017| import type {HookKind} from './ObjectShape';
00018| import {Type, makeType} from './Types';
00019| import {z} from 'zod/v4';
00020| import type {AliasingEffect} from '../Inference/AliasingEffects';
00021| import {isReservedWord} from '../Utils/Keyword';
00022| import {Err, Ok, Result} from '../Utils/Result';
00023| 
00024| /*
00025|  * *******************************************************************************************
00026|  * *******************************************************************************************
00027|  * ************************************* Core Data Model *************************************
00028|  * *******************************************************************************************
00029|  * *******************************************************************************************
00030|  */
00031| 
00032| // AST -> (lowering) -> HIR -> (analysis) -> Reactive Scopes -> (codegen) -> AST
00033| 
00034| /*
00035|  * A location in a source file, intended to be used for providing diagnostic information and
00036|  * transforming code while preserving source information (ie to emit source maps).
00037|  *
00038|  * `GeneratedSource` indicates that there is no single source location from which the code derives.
00039|  */
00040| export const GeneratedSource = Symbol();
00041| export type SourceLocation = t.SourceLocation | typeof Generate...
readcompleted
react/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts
Output
<file>
00001| /**
00002|  * Copyright (c) Meta Platforms, Inc. and affiliates.
00003|  *
00004|  * This source code is licensed under the MIT license found in the
00005|  * LICENSE file in the root directory of this source tree.
00006|  */
00007| 
00008| import {
00009|   CompilerDiagnostic,
00010|   CompilerError,
00011|   Effect,
00012|   SourceLocation,
00013|   ValueKind,
00014| } from '..';
00015| import {
00016|   BasicBlock,
00017|   BlockId,
00018|   DeclarationId,
00019|   Environment,
00020|   FunctionExpression,
00021|   GeneratedSource,
00022|   getHookKind,
00023|   HIRFunction,
00024|   Hole,
00025|   IdentifierId,
00026|   Instruction,
00027|   InstructionKind,
00028|   InstructionValue,
00029|   isArrayType,
00030|   isJsxType,
00031|   isMapType,
00032|   isPrimitiveType,
00033|   isRefOrRefValue,
00034|   isSetType,
00035|   makeIdentifierId,
00036|   Phi,
00037|   Place,
00038|   SpreadPattern,
00039|   Type,
00040|   ValueReason,
00041| } from '../HIR';
00042| import {
00043|   eachInstructionValueOperand,
00044|   eachPatternItem,
00045|   eachTerminalOperand,
00046|   eachTerminalSuccessor,
00047| } from '../HIR/visitors';
00048| import {Ok, Result} from '../Utils/Result';
00049| import {
00050|   assertExhaustive,
00051|   getOrInsertDefault,
00052|   getOrInsertWith,
00053|   Set_isSuperset,
00054| } from '../Utils/utils';
00055| import {
00056|   printAliasingEffect,
00057|   printAliasingSignature,
00058|   printIdentifier,
00059|   printInstruction,
00060|   printInstructionValue,
00061|   printPlace,
00062| } from '../HIR/PrintHIR';
00063| import {FunctionSignature} from '../HIR/ObjectShape';
00064| import prettyFormat from 'pretty-format';
00065| import {createTemporaryPlace} from '../HIR/HIRBuilder';
00066| import {
00067|   AliasingEffect,
00068|   AliasingSignature,
00069|   hashEffect,
00070|   MutationReason,
00071| } from './AliasingEffects';
00072| import {ErrorCategory} from '../CompilerError';
00073| 
00074| const DEBUG = false;...
26,036in / 162out / 53reasoning
assistantFeb 10, 04:56:37 AM
glm-4.7 · zai-coding-plan
I need to continue reading the InferMutationAliasingEffects.ts file. Let me read more of it in chunks.

Let me continue reading the InferMutationAliasingEffects.ts file:

readcompleted
react/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts
Output
<file>
00601|         switch (value.kind) {
00602|           case ValueKind.Frozen: {
00603|             const reason = getWriteErrorReason({
00604|               kind: value.kind,
00605|               reason: value.reason,
00606|             });
00607|             const variable =
00608|               effect.value.identifier.name !== null &&
00609|               effect.value.identifier.name.kind === 'named'
00610|                 ? `\`${effect.value.identifier.name.value}\``
00611|                 : 'value';
00612|             const diagnostic = CompilerDiagnostic.create({
00613|               category: ErrorCategory.Immutability,
00614|               reason: 'This value cannot be modified',
00615|               description: reason,
00616|             }).withDetails({
00617|               kind: 'error',
00618|               loc: effect.value.loc,
00619|               message: `${variable} cannot be modified`,
00620|             });
00621|             if (
00622|               effect.kind === 'Mutate' &&
00623|               effect.reason?.kind === 'AssignCurrentProperty'
00624|             ) {
00625|               diagnostic.withDetails({
00626|                 kind: 'hint',
00627|                 message: `Hint: If this value is a Ref (value returned by \`useRef()\`), rename the variable to end in "Ref".`,
00628|               });
00629|             }
00630|             effects.push({
00631|               kind: 'MutateFrozen',
00632|               place: effect.value,
00633|               error: diagnostic,
00634|             });
00635|           }
00636|         }
00637|       }
00638|     }
00639|   }
00640| 
00641|   /*
00642|    * Track which values we've already aliased once, so that we can switch to
00643|    * appendAlias() for subsequent aliases into the same value
00644|    */
00645|   const initialized = new Set<IdentifierId>();
00646| 
00647|   if (DEBUG) {
00648|     console.log(printInstruction(instruction));
00649|   }
00650| 
00651|   for (const eff...
11,596in / 90out / 23reasoning
assistantFeb 10, 04:56:41 AM
glm-4.7 · zai-coding-plan
I need to continue reading to get the rest of the InferMutationAliasingEffects.ts file. Let me read more.

Let me continue reading the rest of InferMutationAliasingEffects.ts and also get more of HIR.ts to see the type definitions:

readcompleted
react/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts
Output
<file>
01201|           console.log(`invalid mutation: ${printAliasingEffect(effect)}`);
01202|           console.log(prettyFormat(state.debugAbstractValue(value)));
01203|         }
01204| 
01205|         if (
01206|           mutationKind === 'mutate-frozen' &&
01207|           context.hoistedContextDeclarations.has(
01208|             effect.value.identifier.declarationId,
01209|           )
01210|         ) {
01211|           const variable =
01212|             effect.value.identifier.name !== null &&
01213|             effect.value.identifier.name.kind === 'named'
01214|               ? `\`${effect.value.identifier.name.value}\``
01215|               : null;
01216|           const hoistedAccess = context.hoistedContextDeclarations.get(
01217|             effect.value.identifier.declarationId,
01218|           );
01219|           const diagnostic = CompilerDiagnostic.create({
01220|             category: ErrorCategory.Immutability,
01221|             reason: 'Cannot access variable before it is declared',
01222|             description: `${variable ?? 'This variable'} is accessed before it is declared, which prevents the earlier access from updating when this value changes over time`,
01223|           });
01224|           if (hoistedAccess != null && hoistedAccess.loc != effect.value.loc) {
01225|             diagnostic.withDetails({
01226|               kind: 'error',
01227|               loc: hoistedAccess.loc,
01228|               message: `${variable ?? 'variable'} accessed before it is declared`,
01229|             });
01230|           }
01231|           diagnostic.withDetails({
01232|             kind: 'error',
01233|             loc: effect.value.loc,
01234|             message: `${variable ?? 'variable'} is declared here`,
01235|           });
01236| 
01237|           applyEffect(
01238|             context,
01239|             state,
01240|             {
01241|               kind: 'MutateFrozen',
01242|               place: effect.value,
01243|          ...
readcompleted
react/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts
Output
<file>
00501|   id: InstructionId;
00502|   loc: SourceLocation;
00503| };
00504| 
00505| export type DoWhileTerminal = {
00506|   kind: 'do-while';
00507|   loop: BlockId;
00508|   test: BlockId;
00509|   fallthrough: BlockId;
00510|   id: InstructionId;
00511|   loc: SourceLocation;
00512| };
00513| 
00514| export type WhileTerminal = {
00515|   kind: 'while';
00516|   loc: SourceLocation;
00517|   test: BlockId;
00518|   loop: BlockId;
00519|   fallthrough: BlockId;
00520|   id: InstructionId;
00521| };
00522| 
00523| export type ForTerminal = {
00524|   kind: 'for';
00525|   loc: SourceLocation;
00526|   init: BlockId;
00527|   test: BlockId;
00528|   update: BlockId | null;
00529|   loop: BlockId;
00530|   fallthrough: BlockId;
00531|   id: InstructionId;
00532| };
00533| 
00534| export type ForOfTerminal = {
00535|   kind: 'for-of';
00536|   loc: SourceLocation;
00537|   init: BlockId;
00538|   test: BlockId;
00539|   loop: BlockId;
00540|   fallthrough: BlockId;
00541|   id: InstructionId;
00542| };
00543| 
00544| export type ForInTerminal = {
00545|   kind: 'for-in';
00546|   loc: SourceLocation;
00547|   init: BlockId;
00548|   loop: BlockId;
00549|   fallthrough: BlockId;
00550|   id: InstructionId;
00551| };
00552| 
00553| export type LogicalTerminal = {
00554|   kind: 'logical';
00555|   operator: t.LogicalExpression['operator'];
00556|   test: BlockId;
00557|   fallthrough: BlockId;
00558|   id: InstructionId;
00559|   loc: SourceLocation;
00560| };
00561| 
00562| export type TernaryTerminal = {
00563|   kind: 'ternary';
00564|   test: BlockId;
00565|   fallthrough: BlockId;
00566|   id: InstructionId;
00567|   loc: SourceLocation;
00568| };
00569| 
00570| export type LabelTerminal = {
00571|   kind: 'label';
00572|   block: BlockId;
00573|   fallthrough: BlockId;
00574|   id: InstructionId;
00575|   loc: SourceLocation;
00576| };
00577| 
00578| export type OptionalTerminal = {
00579|   kind: 'optional';
00580|   /*
00581|    * Specifies whether this no...
5,941in / 152out / 24reasoning
assistantFeb 10, 04:56:46 AM
glm-4.7 · zai-coding-plan
I need to get more type definitions for ValueKind, MutableRange, Place, Identifier and Effect from HIR.ts. Let me continue reading.

Let me continue reading HIR.ts to get the type definitions:

readcompleted
react/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts
Output
<file>
00801|  * variable.
00802|  */
00803| export type ManualMemoDependency = {
00804|   root:
00805|     | {
00806|         kind: 'NamedLocal';
00807|         value: Place;
00808|         constant: boolean;
00809|       }
00810|     | {kind: 'Global'; identifierName: string};
00811|   path: DependencyPath;
00812|   loc: SourceLocation;
00813| };
00814| 
00815| export type StartMemoize = {
00816|   kind: 'StartMemoize';
00817|   // Start/FinishMemoize markers should have matching ids
00818|   manualMemoId: number;
00819|   /**
00820|    * deps-list from source code, or null if one was not provided
00821|    * (e.g. useMemo without a second arg)
00822|    */
00823|   deps: Array<ManualMemoDependency> | null;
00824|   /**
00825|    * The source location of the dependencies argument. Used for
00826|    * emitting diagnostics with a suggested replacement
00827|    */
00828|   depsLoc: SourceLocation | null;
00829|   loc: SourceLocation;
00830| };
00831| export type FinishMemoize = {
00832|   kind: 'FinishMemoize';
00833|   // Start/FinishMemoize markers should have matching ids
00834|   manualMemoId: number;
00835|   decl: Place;
00836|   pruned?: true;
00837|   loc: SourceLocation;
00838| };
00839| 
00840| /*
00841|  * Forget currently does not handle MethodCall correctly in
00842|  * all cases. Specifically, we do not bind the receiver and method property
00843|  * before calling to args. Until we add a SequenceExpression to inline all
00844|  * instructions generated when lowering args, we have a limited representation
00845|  * with some constraints.
00846|  *
00847|  * Forget currently makes these assumptions (checked in codegen):
00848|  *   - {@link MethodCall.property} is a temporary produced by a PropertyLoad or ComputedLoad
00849|  *     on {@link MethodCall.receiver}
00850|  *   - {@link MethodCall.property} remains an rval (i.e. never promoted to a
00851|  *     named identifier). We currently rely on this for codegen.
00852|  *
00853|  * Type inference do...
readcompleted
react/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts
Output
<file>
01801|         kind: 'Capture',
01802|         from: value.value,
01803|         into: lvalue,
01804|       });
01805|       break;
01806|     }
01807|     case 'NewExpression':
01808|     case 'CallExpression':
01809|     case 'MethodCall': {
01810|       let callee;
01811|       let receiver;
01812|       let mutatesCallee;
01813|       if (value.kind === 'NewExpression') {
01814|         callee = value.callee;
01815|         receiver = value.callee;
01816|         mutatesCallee = false;
01817|       } else if (value.kind === 'CallExpression') {
01818|         callee = value.callee;
01819|         receiver = value.callee;
01820|         mutatesCallee = true;
01821|       } else if (value.kind === 'MethodCall') {
01822|         callee = value.property;
01823|         receiver = value.receiver;
01824|         mutatesCallee = false;
01825|       } else {
01826|         assertExhaustive(
01827|           value,
01828|           `Unexpected value kind '${(value as any).kind}'`,
01829|         );
01830|       }
01831|       const signature = getFunctionCallSignature(env, callee.identifier.type);
01832|       effects.push({
01833|         kind: 'Apply',
01834|         receiver,
01835|         function: callee,
01836|         mutatesFunction: mutatesCallee,
01837|         args: value.args,
01838|         into: lvalue,
01839|         signature,
01840|         loc: value.loc,
01841|       });
01842|       break;
01843|     }
01844|     case 'PropertyDelete':
01845|     case 'ComputedDelete': {
01846|       effects.push({
01847|         kind: 'Create',
01848|         into: lvalue,
01849|         value: ValueKind.Primitive,
01850|         reason: ValueReason.Other,
01851|       });
01852|       // Mutates the object by removing the property, no aliasing
01853|       effects.push({kind: 'Mutate', value: value.object});
01854|       break;
01855|     }
01856|     case 'PropertyLoad':
01857|     case 'ComputedLoad': {
01858|       if (isPrimitiveType(lvalue.identifier)) {...
64,977in / 143out / 29reasoning
assistantFeb 10, 04:56:56 AM
glm-4.7 · zai-coding-plan
Let me continue reading to get the remaining parts and also look for the ValueKind, Effect, and MutableRange type definitions in HIR.ts.

Let me continue reading and find the type definitions:

readcompleted
react/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts
Output
<file>
01201| export type StoreGlobal = {
01202|   kind: 'StoreGlobal';
01203|   name: string;
01204|   value: Place;
01205|   loc: SourceLocation;
01206| };
01207| 
01208| export type BuiltinTag = {
01209|   kind: 'BuiltinTag';
01210|   name: string;
01211|   loc: SourceLocation;
01212| };
01213| 
01214| /*
01215|  * Range in which an identifier is mutable. Start and End refer to Instruction.id.
01216|  *
01217|  * Start is inclusive, End is exclusive (ie, end is the "first" instruction for which
01218|  * the value is not mutable).
01219|  */
01220| export type MutableRange = {
01221|   start: InstructionId;
01222|   end: InstructionId;
01223| };
01224| 
01225| export type VariableBinding =
01226|   // let, const, etc declared within the current component/hook
01227|   | {kind: 'Identifier'; identifier: Identifier; bindingKind: BindingKind}
01228|   // bindings declard outside the current component/hook
01229|   | NonLocalBinding;
01230| 
01231| // `import {bar as baz} from 'foo'`: name=baz, module=foo, imported=bar
01232| export type NonLocalImportSpecifier = {
01233|   kind: 'ImportSpecifier';
01234|   name: string;
01235|   module: string;
01236|   imported: string;
01237| };
01238| 
01239| export type NonLocalBinding =
01240|   // `import Foo from 'foo'`: name=Foo, module=foo
01241|   | {kind: 'ImportDefault'; name: string; module: string}
01242|   // `import * as Foo from 'foo'`: name=Foo, module=foo
01243|   | {kind: 'ImportNamespace'; name: string; module: string}
01244|   // `import {bar as baz} from 'foo'`
01245|   | NonLocalImportSpecifier
01246|   // let, const, function, etc declared in the module but outside the current component/hook
01247|   | {kind: 'ModuleLocal'; name: string}
01248|   // an unresolved binding
01249|   | {kind: 'Global'; name: string};
01250| 
01251| // Represents a user-defined variable (has a name) or a temporary variable (no name).
01252| export type Identifier = {
01253|   /**
01254|    * After EnterSSA, `id` uniquely identifi...
readcompleted
react/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts
Output
<file>
02201|       break;
02202|     }
02203|     case 'LoadLocal': {
02204|       effects.push({kind: 'Assign', from: value.place, into: lvalue});
02205|       break;
02206|     }
02207|     case 'StoreLocal': {
02208|       effects.push({
02209|         kind: 'Assign',
02210|         from: value.value,
02211|         into: value.lvalue.place,
02212|       });
02213|       effects.push({kind: 'Assign', from: value.value, into: lvalue});
02214|       break;
02215|     }
02216|     case 'PostfixUpdate':
02217|     case 'PrefixUpdate': {
02218|       effects.push({
02219|         kind: 'Create',
02220|         into: lvalue,
02221|         value: ValueKind.Primitive,
02222|         reason: ValueReason.Other,
02223|       });
02224|       effects.push({
02225|         kind: 'Create',
02226|         into: value.lvalue,
02227|         value: ValueKind.Primitive,
02228|         reason: ValueReason.Other,
02229|       });
02230|       break;
02231|     }
02232|     case 'StoreGlobal': {
02233|       const variable = `\`${value.name}\``;
02234|       effects.push({
02235|         kind: 'MutateGlobal',
02236|         place: value.value,
02237|         error: CompilerDiagnostic.create({
02238|           category: ErrorCategory.Globals,
02239|           reason:
02240|             'Cannot reassign variables declared outside of the component/hook',
02241|           description: `Variable ${variable} is declared outside of the component/hook. Reassigning this value during render is a form of side effect, which can cause unpredictable behavior depending on when the component happens to re-render. If this variable is used in rendering, use useState instead. Otherwise, consider updating it in an effect. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)`,
02242|         }).withDetails({
02243|           kind: 'error',
02244|           loc: instr.loc,
02245|           message: `${variable} cannot be reassigned`,
02246|       ...
18,085in / 141out / 29reasoning
assistantFeb 10, 04:57:08 AM
glm-4.7 · zai-coding-plan
I now have most of the essential files. Let me continue reading to get the rest of the InferMutationAliasingEffects.ts file and then I'll have enough to write a comprehensive analysis.

Let me read the remaining parts of InferMutationAliasingEffects.ts:

readcompleted
react/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts
Output
<file>
02501|  * Returns true if all of the arguments are both non-mutable (immutable or frozen)
02502|  * _and_ are not functions which might mutate their arguments. Note that function
02503|  * expressions count as frozen so long as they do not mutate free variables: this
02504|  * function checks that such functions also don't mutate their inputs.
02505|  */
02506| function areArgumentsImmutableAndNonMutating(
02507|   state: InferenceState,
02508|   args: Array<Place | SpreadPattern | Hole>,
02509| ): boolean {
02510|   for (const arg of args) {
02511|     if (arg.kind === 'Hole') {
02512|       continue;
02513|     }
02514|     if (arg.kind === 'Identifier' && arg.identifier.type.kind === 'Function') {
02515|       const fnShape = state.env.getFunctionSignature(arg.identifier.type);
02516|       if (fnShape != null) {
02517|         return (
02518|           !fnShape.positionalParams.some(isKnownMutableEffect) &&
02519|           (fnShape.restParam == null ||
02520|             !isKnownMutableEffect(fnShape.restParam))
02521|         );
02522|       }
02523|     }
02524|     const place = arg.kind === 'Identifier' ? arg : arg.place;
02525| 
02526|     const kind = state.kind(place).kind;
02527|     switch (kind) {
02528|       case ValueKind.Primitive:
02529|       case ValueKind.Frozen: {
02530|         /*
02531|          * Only immutable values, or frozen lambdas are allowed.
02532|          * A lambda may appear frozen even if it may mutate its inputs,
02533|          * so we have a second check even for frozen value types
02534|          */
02535|         break;
02536|       }
02537|       default: {
02538|         /**
02539|          * Globals, module locals, and other locally defined functions may
02540|          * mutate their arguments.
02541|          */
02542|         return false;
02543|       }
02544|     }
02545|     const values = state.values(place);
02546|     for (const value of values) {
02547|       if (
02548|         value.kind === 'Functio...
7,599in / 107out / 38reasoning
assistantFeb 10, 04:57:12 AM
glm-4.7 · zai-coding-plan
Let me get the rest of the file for completeness.

Let me read the remaining part of the file:

readcompleted
react/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts
Output
<file>
02801|   } else if (abstractValue.reason.has(ValueReason.Effect)) {
02802|     return 'Modifying a value used previously in an effect function or as an effect dependency is not allowed. Consider moving the modification before calling useEffect()';
02803|   } else if (abstractValue.reason.has(ValueReason.HookCaptured)) {
02804|     return 'Modifying a value previously passed as an argument to a hook is not allowed. Consider moving the modification before calling the hook';
02805|   } else if (abstractValue.reason.has(ValueReason.HookReturn)) {
02806|     return 'Modifying a value returned from a hook is not allowed. Consider moving the modification into the hook where the value is constructed';
02807|   } else {
02808|     return 'This modifies a variable that React considers immutable';
02809|   }
02810| }
02811| 
02812| function getArgumentEffect(
02813|   signatureEffect: Effect | null,
02814|   arg: Place | SpreadPattern,
02815| ): Effect {
02816|   if (signatureEffect != null) {
02817|     if (arg.kind === 'Identifier') {
02818|       return signatureEffect;
02819|     } else if (
02820|       signatureEffect === Effect.Mutate ||
02821|       signatureEffect === Effect.ConditionallyMutate
02822|     ) {
02823|       return signatureEffect;
02824|     } else {
02825|       // see call-spread-argument-mutable-iterator test fixture
02826|       if (signatureEffect === Effect.Freeze) {
02827|         CompilerError.throwTodo({
02828|           reason: 'Support spread syntax for hook arguments',
02829|           loc: arg.place.loc,
02830|         });
02831|       }
02832|       // effects[i] is Effect.Capture | Effect.Read | Effect.Store
02833|       return Effect.ConditionallyMutateIterator;
02834|     }
02835|   } else {
02836|     return Effect.ConditionallyMutate;
02837|   }
02838| }
02839| 
02840| export function getFunctionCallSignature(
02841|   env: Environment,
02842|   type: Type,
02843| ): FunctionSignature | null {
02844|   if (type.kind !== 'Functi...
19,919in / 76out / 11reasoning
assistantFeb 10, 04:57:27 AM
glm-4.7 · zai-coding-plan
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

  1. Hoisted functions: Functions declared after use create special freeze semantics
  2. Destructuring spreads: {...rest} from frozen props can be treated as frozen
  3. Iterators: Known types (Array/Set/Map) have fresh iterators; unknown types may alias
  4. Ref objects: Special handling for ref.current assignment hints
  5. 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:
    1. Local function with pre-computed aliasingEffects
    2. Built-in with aliasing property
    3. 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:

  1. Built-in hooks (useState, useMemo, etc.)
  2. Local functions (constructed from their body)
  3. 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

  1. Hole operands: Spread/rest arguments with no value
  2. SpreadPattern: Represents ...spread syntax (needs iterator mutation)
  3. Function signatures: Null signature = unknown function (use conservative fallback)
  4. 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:

  1. Candidate effects: Compute potential effects per instruction based on semantics
  2. 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

  1. Hoisted functions: Variables used before declaration need special handling
  2. Non-mutating spreads: Props spreading {...rest} can be frozen if source is frozen
  3. Iterator types: GetIterator on Array/Set/Map knows fresh iterator; on unknown types may alias
  4. Ref detection: ref.current assignment generates hint for user feedback
  5. Catch handlers: Values thrown into catch parameters have special aliasing semantics
  6. Multiple predecessors: State merging uses union of value sets + lattice join of kinds
  7. 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

  1. Phi node ranges: Mutated after creation need non-zero start
  2. Hoisted functions: StoreContext extends mutable range to hoisting point
  3. Instruction effects: Overwrite default operand effects
  4. Return values: Frozen for components, Read for function expressions
  5. Self-referencing: Loop backedges handled correctly
  6. 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

  1. No mutable state: Closures with empty captures_mutable become standalone groups
  2. Single function groups: May still be good Durable Object candidates (score threshold)
  3. No service deps: Stateless functions may not need Durable Objects
  4. 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:

  1. 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 *)
}
  1. 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}
  1. 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
  1. 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 ()
  1. 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

  1. Precise abstract interpretation: Tracks value kinds through control flow, merges at phi nodes
  2. Mutation transitivity: Distinguishes direct vs transitive mutations (critical for DO vs KV)
  3. Effect composition: Functions can have precise signatures describing their effects
  4. Conditional mutation: MaybeAlias + MutateConditionally allows safe conservative analysis
  5. 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.

6,004in / 18,280out / 147reasoning
userFeb 11, 04:53:40 AM

You are analyzing the React Compiler's function analysis and effect inference passes. This is research only -- do NOT edit any files.

The goal is to produce a structured analysis document that will help us adapt these concepts for CloudLang, an OCaml compiler that decomposes TypeScript closures into Cloudflare Workers.

Files to read and analyze (all under /home/jm/data/ai/cloudlang-trees/react/react/compiler/packages/babel-plugin-react-compiler/src/):

  1. Inference/AnalyseFunctions.ts - Recursive nested function analysis (THE key pass for us)
  2. Inference/InferReactivePlaces.ts - Reactivity inference
  3. Inference/InferEffectDependencies.ts - Effect dependency inference
  4. Inference/DropManualMemoization.ts - Drops useMemo/useCallback
  5. Inference/InlineImmediatelyInvokedFunctionExpressions.ts - IIFE inlining
  6. Inference/ControlDominators.ts - Control flow dominator analysis for inference

Also read CloudLang's current scope analysis for comparison:

  • /home/jm/data/ai/cloudlang-trees/react/lib/scope.ml - Current 3-pass closure analysis
  • /home/jm/data/ai/cloudlang-trees/react/lib/scope.mli

Produce documentation for each pass using the template format:

# Pass Name

## File
## Purpose
## Input Invariants
## Output Guarantees
## Algorithm (detailed, step-by-step)
## Key Data Structures
## Edge Cases
## Mapping to CloudLang
## Example (before/after)

Specific questions to answer:

  1. AnalyseFunctions (THE most important for us):

    • Walk through the complete algorithm step by step
    • How does it handle the recursive descent into nested functions?
    • What mini-pipeline does it run on each inner function? (analyseFunctions -> inferMutationAliasingEffects -> deadCodeElimination -> inferMutationAliasingRanges -> rewriteInstructionKinds -> inferReactiveScopeVariables)
    • How does it classify context variables as Capture vs Read?
    • What effects does it look for to determine capture? (Capture, Alias, Assign, MaybeAlias, CreateFrom -> captured; Mutate, MutateTransitive, etc. -> captured; ImmutableCapture, Freeze, Create, Impure, Render -> not captured)
    • How does it reset mutable ranges on context variables for the outer function?
    • What's the assertion about Apply effects?
    • How does this relate to CloudLang's analyze_closure which collects free_vars, captured_mutable, called_functions?
  2. InferReactivePlaces:

    • How does it determine which values are "reactive" (change between renders)?
    • What's the stability tracking mechanism?
    • While React-specific, the pattern of "propagating a property through data flow" is potentially useful. What's the general technique?
  3. InferEffectDependencies:

    • How does it infer useEffect dependencies?
    • The general pattern of "what does this function depend on" is relevant to us.
  4. IIFE Inlining:

    • How does it detect and inline IIFEs?
    • This is relevant because TypeScript patterns like const x = (() => { ... })() appear in real code.
  5. ControlDominators:

    • What dominator analysis is used?
    • How does it support the inference passes?
  6. For CloudLang specifically:

    • CloudLang's scope.ml does a simpler version of AnalyseFunctions: it finds free variables and checks if they're mutable state. How would we upgrade to the React Compiler's approach?
    • The React Compiler classifies context variables as Capture vs Read. CloudLang classifies them as captured_mutable vs plain free_vars. What's the delta? What new distinctions would we gain?
    • Sketch an OCaml module signature for an upgraded function analysis pass that uses aliasing effects.
    • Consider: in CloudLang, the "inner functions" are the closures that become separate Workers. The effect signature of each closure determines what bindings (KV, service, D1) it needs. How does AnalyseFunctions' output map to binding inference?

Return your complete analysis as a single structured markdown document./share/share/share

assistantFeb 11, 04:53:40 AM
moonshotai/kimi-k2-thinking · openrouter
The user wants a comprehensive analysis of several React Compiler passes, with special focus on `AnalyseFunctions.ts` since it's most relevant to CloudLang. I need to read all the specified files and produce detailed documentation for each pass. Let me start by reading all the required files in parallel: 1. `Inference/AnalyseFunctions.ts` - most important 2. `Inference/InferReactivePlaces.ts` 3. `Inference/InferEffectDependencies.ts` 4. `Inference/DropManualMemoization.ts` 5. `Inference/InlineImmediatelyInvokedFunctionExpressions.ts` 6. `Inference