lower (BuildHIR)
File
src/HIR/BuildHIR.ts
Purpose
Converts a Babel AST function node into a High-level Intermediate Representation (HIR), which represents code as a control-flow graph (CFG) with basic blocks, instructions, and terminals. This is the first major transformation pass in the React Compiler pipeline, enabling precise expression-level memoization analysis.
Input Invariants
- Input must be a valid Babel
NodePath<t.Function>(FunctionDeclaration, FunctionExpression, or ArrowFunctionExpression) - The function must be a component or hook (determined by the environment)
- Babel scope analysis must be available for binding resolution
- An
Environmentinstance must be provided with compiler configuration - Optional
bindingsmap for nested function lowering (recursive calls) - Optional
capturedRefsmap for context variables captured from outer scope
Output Guarantees
- Returns
Result<HIRFunction, CompilerError>- either a successfully lowered function or compilation errors - The HIR function contains:
- A complete CFG with basic blocks (
body.blocks: Map<BlockId, BasicBlock>) - Each block has an array of instructions and exactly one terminal
- All control flow is explicit (if/else, loops, switch, logical operators, ternary)
- Parameters are converted to
PlaceorSpreadPattern - Context captures are tracked in
contextarray - Function metadata (id, async, generator, directives)
- A complete CFG with basic blocks (
- All identifiers get unique
IdentifierIdvalues - Instructions have placeholder instruction IDs (set to 0, assigned later)
- Effects are null (populated by later inference passes)
Algorithm
The lowering algorithm uses a recursive descent pattern with a HIRBuilder helper class:
-
Initialization: Create an
HIRBuilderwith environment and optional bindings. Process captured context variables. -
Parameter Processing: For each function parameter:
- Simple identifiers: resolve binding and create Place
- Patterns (object/array): create temporary Place, then emit destructuring assignments
- Rest elements: wrap in SpreadPattern
- Unsupported: emit Todo error
-
Body Processing:
- Arrow function expressions: lower body expression to temporary, emit implicit return
- Block statements: recursively lower each statement
-
Statement Lowering (
lowerStatement): Handle each statement type:- Control flow: Create separate basic blocks for branches, loops connect back to conditional blocks
- Variable declarations: Create
DeclareLocal/DeclareContextorStoreLocal/StoreContextinstructions - Expressions: Lower to temporary and discard result
- Hoisting: Detect forward references and emit
DeclareContextfor hoisted identifiers
-
Expression Lowering (
lowerExpression): Convert expressions toInstructionValue:- Identifiers: Create
LoadLocal,LoadContext, orLoadGlobalbased on binding - Literals: Create
Primitivevalues - Operators: Create
BinaryExpression,UnaryExpressionetc. - Calls: Distinguish
CallExpressionvsMethodCall(member expression callee) - Control flow expressions: Create separate value blocks for branches (ternary, logical, optional chaining)
- JSX: Lower to
JsxExpressionwith lowered tag, props, and children
- Identifiers: Create
-
Block Management: The builder maintains:
- A current work-in-progress block accumulating instructions
- Completed blocks map
- Scope stack for break/continue resolution
- Exception handler stack for try/catch
-
Termination: Add implicit void return at end if no explicit return
Key Data Structures
HIRBuilder (from HIRBuilder.ts)
#current: WipBlock- Work-in-progress block being populated#completed: Map<BlockId, BasicBlock>- Finished blocks#scopes: Array<Scope>- Stack for break/continue target resolution (LoopScope, LabelScope, SwitchScope)#exceptionHandlerStack: Array<BlockId>- Stack of catch handlers for try/catch#bindings: Bindings- Map of variable names to their identifiers#context: Map<t.Identifier, SourceLocation>- Captured context variables- Methods:
push(),reserve(),enter(),terminate(),terminateWithContinuation()
Core HIR Types
- BasicBlock: Contains
instructions: Array<Instruction>,terminal: Terminal,preds: Set<BlockId>,phis: Set<Phi>,kind: BlockKind - Instruction: Contains
id,lvalue(Place),value(InstructionValue),effects(null initially),loc - Terminal: Block terminator -
if,branch,goto,return,throw,for,while,switch,ternary,logical, etc. - Place: Reference to a value -
{kind: 'Identifier', identifier, effect, reactive, loc} - InstructionValue: The operation -
LoadLocal,StoreLocal,CallExpression,BinaryExpression,FunctionExpression, etc.
Block Kinds
block- Regular sequential blockloop- Loop header/test blockvalue- Block that produces a value (ternary/logical branches)sequence- Sequence expression blockcatch- Exception handler block
Edge Cases
-
Hoisting: Forward references to
let/const/functiondeclarations emitDeclareContextbefore the reference, enabling correct temporal dead zone handling -
Context Variables: Variables captured by nested functions use
LoadContext/StoreContextinstead ofLoadLocal/StoreLocal -
For-of/For-in Loops: Synthesize iterator instructions (
GetIterator,IteratorNext,NextPropertyOf) -
Optional Chaining: Creates nested
OptionalTerminalstructures with short-circuit branches -
Logical Expressions: Create branching structures where left side stores to temporary, right side only evaluated if needed
-
Try/Catch: Adds
MaybeThrowTerminalafter each instruction in try block, modeling potential control flow to handler -
JSX in fbt: Tracks
fbtDepthcounter to handle whitespace differently in fbt/fbs tags -
Unsupported Syntax:
vardeclarations,withstatements, inlineclassdeclarations,eval- emit appropriate errors
TODOs
returnTypeAnnotation: null, // TODO: extract the actual return type node if presentTODO(gsn): In the future, we could only pass in the context identifiers that are actually used by this function and its nested functions- Multiple
// TODO remove type castin destructuring pattern handling // TODO: should JSX namespaced names be handled here as well?
Example
Input JavaScript:
export default function foo(x, y) {
if (x) {
return foo(false, y);
}
return [y * 10];
}
Output HIR (simplified):
foo(<unknown> x$0, <unknown> y$1): <unknown> $12
bb0 (block):
[1] <unknown> $6 = LoadLocal <unknown> x$0
[2] If (<unknown> $6) then:bb2 else:bb1 fallthrough=bb1
bb2 (block):
predecessor blocks: bb0
[3] <unknown> $2 = LoadGlobal(module) foo
[4] <unknown> $3 = false
[5] <unknown> $4 = LoadLocal <unknown> y$1
[6] <unknown> $5 = Call <unknown> $2(<unknown> $3, <unknown> $4)
[7] Return Explicit <unknown> $5
bb1 (block):
predecessor blocks: bb0
[8] <unknown> $7 = LoadLocal <unknown> y$1
[9] <unknown> $8 = 10
[10] <unknown> $9 = Binary <unknown> $7 * <unknown> $8
[11] <unknown> $10 = Array [<unknown> $9]
[12] Return Explicit <unknown> $10
Key observations:
- The function has 3 basic blocks: entry (bb0), consequent (bb2), alternate/fallthrough (bb1)
- The if statement creates an
IfTerminalat the end of bb0 - Each branch ends with its own
ReturnTerminal - All values are stored in temporaries (
$N) or named identifiers (x$0,y$1) - Instructions have sequential IDs within blocks
- Types and effects are
<unknown>at this stage (populated by later passes)