Compiler HIR & Passes
The compiler lowers a function body into a high-level intermediate representation (HIR) made from function context, parameters, places, instructions, and control-flow blocks.
It exists so later compiler passes can reason about mutable operands, captured values, scope ranges, and memoizable units instead of working only from nested syntax.
Sources: compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts:72-263, compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts:266-1572, compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/InferReactiveScopeVariables.ts:30-46
Core concepts
HIR
HIR is the compiler’s representation of a function after Babel nodes have been lowered into explicit values, places, instructions, and terminals.
Sources: compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts:72-263, compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts:266-1572
Place
A Place is the HIR value shape used to identify a local, context value, temporary, or member-expression operand. Identifier places carry an identifier, effect, reactivity flag, and source location; member expressions carry places for their object and, when computed, their property.
Sources: compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts:3750-3759, compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts:3329-3398
Temporary
A temporary is a compiler-created Place used to hold an expression result before another HIR instruction consumes it. buildTemporaryPlace creates it with an unknown effect and reactive: false.
Sources: compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts:3750-3759
Reactive scope
A reactive scope groups variables that mutate together into an independently memoizeable unit; the first inference pass assigns mutable operands to scopes but does not infer every instruction needed to compute each scope.
Scope terminal
A scope terminal is an HIR rewrite record describing where a reactive scope starts or ends, including its instruction position and fallthrough block.
Sources: compiler/packages/babel-plugin-react-compiler/src/HIR/BuildReactiveScopeTerminalsHIR.ts:190-202
How a function becomes HIR
lower creates an HIRBuilder, initializes captured context, and resolves function parameters into places. Identifier parameters become identifier places, while destructuring and rest parameters receive compiler-created temporaries and are lowered through lowerAssignment.
lowerStatement converts statements into HIR control-flow operations. A return lowers its argument into a temporary and terminates the current block with a return terminal; an if reserves continuation and branch blocks before lowering each branch.
lowerExpression dispatches on Babel expression types. Identifiers become local or context loads, literals become primitive values, and object properties become explicit HIR properties whose values are lowered into temporaries.
Sub-expressions are lowered into temporaries before being inserted into larger HIR values. For example, object-property values and spread arguments call lowerExpressionToTemporary before being stored in the lowered object representation.
Member access also becomes explicit. Non-computed properties become PropertyLoad values with an object place and property literal, while computed properties are first lowered into a temporary and represented as ComputedLoad.
The lowering call order is:
Evidence
- lower-functioncompiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts:72
- lower-statementcompiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts:266
- lower-expressioncompiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts:1627
- temporary-valuecompiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts:3680
The key difference from a Babel AST is that the compiler does not leave these operations embedded only in syntax nodes: lowerValueToTemporary either reuses an unnamed local load or pushes an instruction containing the value and returns a place for it.
Nested functions are lowered recursively. lowerFunction gathers captured context from the nested function’s parent scopes, then calls lower with the existing bindings and the captured identifiers.
Captured dependencies are collected from the nested function’s parent scope up to the component scope. The traversal considers identifiers and JSX opening elements, reduces JSX member names to their base identifier, and records bindings that belong to the captured pure scopes.
Optional member and call expressions preserve conditional evaluation by creating alternate, consequent, and continuation blocks. The guarded operation is lowered in the consequent path, while the alternate path stores undefined and jumps to the continuation.
Sources: compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts:72-263, compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts:266-1572, compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts:1627-2806, compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts:3769-3775, compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts:3329-3398, compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts:3680-3696, compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts:3641-3670, compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts:4434-4532, compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts:2808-2916
How reactive scopes are inferred
The first reactive-scope pass groups mutable operands that mutate together. More generally, all mutable operands of an instruction, including its lvalue, must belong to the same scope.
The next pass aligns inferred scope ranges with block-scope boundaries. A scope cannot end halfway through a loop or conditional block, so the pass extends its end to the first instruction after the mutable range in the same block scope.
Scope ranges must then be structurally compatible: two scopes must be disjoint or nested, because partially overlapping ranges cannot be emitted as valid generated blocks.
The four stages can be summarized as follows.
| Stage | What it receives or produces | Why it matters |
|---|---|---|
InferReactiveScopeVariables | Mutable HIR operands | Groups variables that mutate together. |
AlignReactiveScopesToBlockScopes | Scope ranges and block scopes | Prevents a scope from ending inside a control-flow block. |
| Overlap-merging pass | Reactive scope ranges | Makes ranges disjoint or nested. |
BuildReactiveBlocks | Statements belonging to each scope | Groups scope statements into a ReactiveScopeBlock. |
Together, these stages move from mutation grouping to legal boundaries, compatible nesting, and grouped scope blocks.
After ranges are valid, buildReactiveScopeTerminalsHIR makes their boundaries explicit in the HIR. It queues start and end rewrites while traversing scopes, applies those rewrites by splitting blocks, repoints phi operands, restores block ordering and predecessor information, and finally fixes ranges after instruction IDs change.
This boundary rewrite proceeds as follows:
Evidence
- scope-terminal-passcompiler/packages/babel-plugin-react-compiler/src/HIR/BuildReactiveScopeTerminalsHIR.ts:78
- scope-traversalcompiler/packages/babel-plugin-react-compiler/src/HIR/BuildReactiveScopeTerminalsHIR.ts:85
- start-rewritecompiler/packages/babel-plugin-react-compiler/src/HIR/BuildReactiveScopeTerminalsHIR.ts:214
- end-rewritecompiler/packages/babel-plugin-react-compiler/src/HIR/BuildReactiveScopeTerminalsHIR.ts:230
- block-rewritercompiler/packages/babel-plugin-react-compiler/src/HIR/BuildReactiveScopeTerminalsHIR.ts:271
handleRewrite slices the current block at the rewrite index, inserts either a scope or goto terminal, updates predecessor state, and selects the next block based on whether the rewrite starts or ends a scope.
Sources: compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/InferReactiveScopeVariables.ts:42-70, compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/AlignReactiveScopesToBlockScopesHIR.ts:41-69, compiler/packages/babel-plugin-react-compiler/src/HIR/MergeOverlappingReactiveScopesHIR.ts:25-65, compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/InferReactiveScopeVariables.ts:30-46, compiler/packages/babel-plugin-react-compiler/src/HIR/BuildReactiveScopeTerminalsHIR.ts:78-188, compiler/packages/babel-plugin-react-compiler/src/HIR/BuildReactiveScopeTerminalsHIR.ts:271-311
What a pass needs to decide what to memoize
A pass needs the mutable operands associated with each scope and the instruction-level relationships that produced them. The scope-inference pass explicitly assigns variables that mutate together to one scope and requires mutable operands of an instruction to share that scope.
It also needs legal control-flow ranges. Scope alignment prevents memoization from stopping inside a block, while overlap handling ensures the resulting ranges can be represented as disjoint or nested blocks.
Captured context supplies dependency information for nested functions. gatherCapturedContext records bindings from the captured parent scopes, including the base identifier of a JSX component or member name.
A pass can also use reorderability information. isReorderableExpression accepts literals and recursively safe forms such as selected unary, logical, conditional, array, and object expressions; lowerReorderableExpression records an error when an expression fails that safety check.
Manual memoization is represented in HIR through StartMemoize and FinishMemoize; these instructions preserve semantic information for analysis even though memoization instructions are pruned during code generation. Memoization levels distinguish values that are memoized, conditionally memoized, unmemoized, or never memoized.
Sources: compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/InferReactiveScopeVariables.ts:42-70, compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/AlignReactiveScopesToBlockScopesHIR.ts:62-69, compiler/packages/babel-plugin-react-compiler/src/HIR/MergeOverlappingReactiveScopesHIR.ts:30-49, compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts:4434-4532, compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts:3104-3288, compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts:3087-3102, compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts:1119-1129, compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneNonEscapingScopes.ts:144-164
How it connects
HIR lowering is the compiler’s syntax-to-representation boundary: Babel NodePath inputs are converted into HIRFunction data and explicit HIR operations.
Reactive-scope inference and boundary construction connect to Reactive Scopes & Codegen. Mutation and alias assumptions connect to Mutability Inference & Aliasing. The ordered compiler flow is described in Compiler Overview & Pipeline, while unsupported or unsafe constructs are covered by Compiler Validation & Diagnostics.
Sources: compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts:72-263, compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts:1627-2806
Key takeaways
- HIR converts Babel syntax into explicit places, temporaries, values, instructions, and control-flow blocks.
- Reactive scopes first group variables that mutate together, then align and merge their ranges.
- Scope terminals make inferred boundaries explicit by rewriting HIR blocks and fallthroughs.
- Memoization analysis depends on mutable operands, captured bindings, reorderability, manual memoization markers, and legal scope ranges.
- Temporaries and explicit branches expose intermediate values and conditional evaluation to compiler passes.