If a function is known to freeze its inputs, and captures refs, then we can safely assume those refs are not mutated during render. An example is React Native's PanResponder, which is designed for use in interaction handling. Calling `PanResponder.create()` creates an object that shouldn't be interacted with at render time, so we can treat it as freezing its arguments, returning a frozen value, and not accessing any refs in the callbacks passed to it. ValidateNoRefAccessInRender is updated accordingly - if we see a Freeze <place> and ImmutableCapture <place> for the same place in the same instruction, we know that it's not being mutated. Note that this is a pretty targeted fix. One weakness is that we may not always emit a Freeze effect if a value is already frozen, which could cause this optimization not to kick in. The worst case there is that you'd just get a ref access in render error though, not miscompilation. And we could always choose to always emit Freeze effects, even for frozen values, just to retain the information for validations like this.
9.6 KiB
React Compiler Knowledge Base
This document contains knowledge about the React Compiler gathered during development sessions. It serves as a reference for understanding the codebase architecture and key concepts.
Project Structure
When modifying the compiler, you MUST read the documentation about that pass in compiler/packages/babel-plugin-react-compiler/docs/passes/ to learn more about the role of that pass within the compiler.
packages/babel-plugin-react-compiler/- Main compiler packagesrc/HIR/- High-level Intermediate Representation types and utilitiessrc/Inference/- Effect inference passes (aliasing, mutation, etc.)src/Validation/- Validation passes that check for errorssrc/Entrypoint/Pipeline.ts- Main compilation pipeline with pass orderingsrc/__tests__/fixtures/compiler/- Test fixtureserror.todo-*.js- Unsupported feature, correctly throws Todo error (graceful bailout)error.bug-*.js- Known bug, throws wrong error type or incorrect behavior*.expect.md- Expected output for each fixture
Running Tests
# Run all tests
yarn snap
# Run tests matching a pattern
# Example: yarn snap -p 'error.*'
yarn snap -p <pattern>
# Run a single fixture in debug mode. Use the path relative to the __tests__/fixtures/compiler directory
# For each step of compilation, outputs the step name and state of the compiled program
# Example: yarn snap -p simple.js -d
yarn snap -p <file-basename> -d
# Update fixture outputs (also works with -p)
yarn snap -u
Linting
# Run lint on the compiler source
yarn workspace babel-plugin-react-compiler lint
Formatting
# Run prettier on all files (from the react root directory, not compiler/)
yarn prettier-all
Compiling Arbitrary Files
Use yarn snap compile to compile any file (not just fixtures) with the React Compiler:
# Compile a file and see the output
yarn snap compile <path>
# Compile with debug logging to see the state after each compiler pass
# This is an alternative to `yarn snap -d -p <pattern>` when you don't have a fixture file yet
yarn snap compile --debug <path>
Minimizing Test Cases
Use yarn snap minimize to automatically reduce a failing test case to its minimal reproduction:
# Minimize a file that causes a compiler error
yarn snap minimize <path>
# Minimize and update the file in-place with the minimized version
yarn snap minimize --update <path>
Version Control
This repository uses Sapling (sl) for version control. Sapling is similar to Mercurial: there is not staging area, but new/deleted files must be explicitlyu added/removed.
# Check status
sl status
# Add new files, remove deleted files
sl addremove
# Commit all changes
sl commit -m "Your commit message"
# Commit with multi-line message using heredoc
sl commit -m "$(cat <<'EOF'
Summary line
Detailed description here
EOF
)"
Key Concepts
HIR (High-level Intermediate Representation)
The compiler converts source code to HIR for analysis. Key types in src/HIR/HIR.ts:
-
HIRFunction - A function being compiled
body.blocks- Map of BasicBlockscontext- Captured variables from outer scopeparams- Function parametersreturns- The function's return placealiasingEffects- Effects that describe the function's behavior when called
-
Instruction - A single operation
lvalue- The place being assigned tovalue- The instruction kind (CallExpression, FunctionExpression, LoadLocal, etc.)effects- Array of AliasingEffects for this instruction
-
Terminal - Block terminators (return, branch, etc.)
effects- Array of AliasingEffects
-
Place - A reference to a value
identifier.id- Unique IdentifierId
-
Phi nodes - Join points for values from different control flow paths
- Located at
block.phis phi.place- The result placephi.operands- Map of predecessor block to source place
- Located at
AliasingEffects System
Effects describe data flow and operations. Defined in src/Inference/AliasingEffects.ts:
Data Flow Effects:
Impure- Marks a place as containing an impure value (e.g., Date.now() result, ref.current)Capture a -> b- Value fromais captured intob(mutable capture)Alias a -> b-baliasesaImmutableCapture a -> b- Immutable capture (like Capture but read-only)Assign a -> b- Direct assignmentMaybeAlias a -> b- Possible aliasingCreateFrom a -> b- Created from source
Mutation Effects:
Mutate value- Value is mutatedMutateTransitive value- Value and transitive captures are mutatedMutateConditionally value- May mutateMutateTransitiveConditionally value- May mutate transitively
Other Effects:
Render place- Place is used in render context (JSX props, component return)Freeze place- Place is frozen (made immutable)Create place- New value createdCreateFunction- Function expression created, includescapturesarrayApply- Function application with receiver, function, args, and result
Hook Aliasing Signatures
Located in src/HIR/Globals.ts, hooks can define custom aliasing signatures to control how data flows through them.
Structure:
aliasing: {
receiver: '@receiver', // The hook function itself
params: ['@param0'], // Named positional parameters
rest: '@rest', // Rest parameters (or null)
returns: '@returns', // Return value
temporaries: [], // Temporary values during execution
effects: [ // Array of effects to apply when hook is called
{kind: 'Freeze', value: '@param0', reason: ValueReason.HookCaptured},
{kind: 'Assign', from: '@param0', into: '@returns'},
],
}
Common patterns:
-
RenderHookAliasing (useState, useContext, useMemo, useCallback):
- Freezes arguments (
Freeze @rest) - Marks arguments as render-time (
Render @rest) - Creates frozen return value
- Aliases arguments to return
- Freezes arguments (
-
EffectHookAliasing (useEffect, useLayoutEffect, useInsertionEffect):
- Freezes function and deps
- Creates internal effect object
- Captures function and deps into effect
- Returns undefined
-
Event handler hooks (useEffectEvent):
- Freezes callback (
Freeze @fn) - Aliases input to return (
Assign @fn -> @returns) - NO Render effect (callback not called during render)
- Freezes callback (
Example: useEffectEvent
const UseEffectEventHook = addHook(
DEFAULT_SHAPES,
{
positionalParams: [Effect.Freeze], // Takes one positional param
restParam: null,
returnType: {kind: 'Function', ...},
calleeEffect: Effect.Read,
hookKind: 'useEffectEvent',
returnValueKind: ValueKind.Frozen,
aliasing: {
receiver: '@receiver',
params: ['@fn'], // Name for the callback parameter
rest: null,
returns: '@returns',
temporaries: [],
effects: [
{kind: 'Freeze', value: '@fn', reason: ValueReason.HookCaptured},
{kind: 'Assign', from: '@fn', into: '@returns'},
// Note: NO Render effect - callback is not called during render
],
},
},
BuiltInUseEffectEventId,
);
// Add as both names for compatibility
['useEffectEvent', UseEffectEventHook],
['experimental_useEffectEvent', UseEffectEventHook],
Key insight: If a hook is missing an aliasing config, it falls back to DefaultNonmutatingHook which includes a Render effect on all arguments. This can cause false positives for hooks like useEffectEvent whose callbacks are not called during render.
Feature Flags
Feature flags are configured in src/HIR/Environment.ts, for example enableJsxOutlining. Test fixtures can override the active feature flags used for that fixture via a comment pragma on the first line of the fixture input, for example:
// enableJsxOutlining @enableNameAnonymousFunctions:false
...code...
Would enable the enableJsxOutlining feature and disable the enableNameAnonymousFunctions feature.
Debugging Tips
- Run
yarn snap -p <fixture>to see full HIR output with effects - Look for
@aliasingEffects=on FunctionExpressions - Look for
Impure,Render,Captureeffects on instructions - Check the pass ordering in Pipeline.ts to understand when effects are populated vs validated
Error Handling and Fault Tolerance
The compiler is fault-tolerant: it runs all passes and accumulates errors on the Environment rather than throwing on the first error. This lets users see all compilation errors at once.
Recording errors — Passes record errors via env.recordError(diagnostic). Errors are accumulated on Environment.#errors and checked at the end of the pipeline via env.hasErrors() / env.aggregateErrors().
tryRecord() wrapper — In Pipeline.ts, validation passes are wrapped in env.tryRecord(() => pass(hir)) which catches thrown CompilerErrors (non-invariant) and records them. Infrastructure/transformation passes are NOT wrapped in tryRecord() because later passes depend on their output being structurally valid.
Error categories:
CompilerError.throwTodo()— Unsupported but known pattern. Graceful bailout. Can be caught bytryRecord().CompilerError.invariant()— Truly unexpected/invalid state. Always throws immediately, never caught bytryRecord().- Non-
CompilerErrorexceptions — Always re-thrown.
Key files: Environment.ts (recordError, tryRecord, hasErrors, aggregateErrors), Pipeline.ts (pass orchestration), Program.ts (tryCompileFunction handles the Result).
Test fixtures: __tests__/fixtures/compiler/fault-tolerance/ contains multi-error fixtures verifying all errors are reported.