diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts index ca6abc0748eb9..aee101e78a503 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts @@ -99,6 +99,8 @@ import {propagateScopeDependenciesHIR} from '../HIR/PropagateScopeDependenciesHI import {outlineJSX} from '../Optimization/OutlineJsx'; import {optimizePropsMethodCalls} from '../Optimization/OptimizePropsMethodCalls'; import {transformFire} from '../Transform'; +import {buildReactiveGraph} from '../ReactiveIR/BuildReactiveGraph'; +import {printReactiveGraph} from '../ReactiveIR/ReactiveIR'; export type CompilerPipelineValue = | {kind: 'ast'; name: string; value: CodegenFunction} @@ -314,6 +316,15 @@ function runWithEnvironment( value: hir, }); + if (env.config.enableReactiveGraph) { + const reactiveGraph = buildReactiveGraph(hir); + log({ + kind: 'debug', + name: 'BuildReactiveGraph', + value: printReactiveGraph(reactiveGraph), + }); + } + alignReactiveScopesToBlockScopesHIR(hir); log({ kind: 'hir', diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts index f3f426df56e44..f6543e9337371 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts @@ -395,6 +395,12 @@ const EnvironmentConfigSchema = z.object({ */ enableInstructionReordering: z.boolean().default(false), + /** + * Enables ReactiveGraph-based optimizations including reordering across terminal + * boundaries + */ + enableReactiveGraph: z.boolean().default(false), + /** * Enables function outlinining, where anonymous functions that do not close over * local variables can be extracted into top-level helper functions. diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveIR/BuildReactiveGraph.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveIR/BuildReactiveGraph.ts new file mode 100644 index 0000000000000..663ade0ab1922 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveIR/BuildReactiveGraph.ts @@ -0,0 +1,454 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import {CompilerError, SourceLocation} from '..'; +import { + BlockId, + DeclarationId, + HIRFunction, + Identifier, + IdentifierId, + Instruction, + InstructionKind, + Place, + ReactiveScope, + ScopeId, +} from '../HIR'; +import {printIdentifier, printInstruction, printPlace} from '../HIR/PrintHIR'; +import { + eachInstructionLValue, + eachInstructionValueLValue, + eachInstructionValueOperand, + terminalFallthrough, +} from '../HIR/visitors'; +import { + BranchNode, + ConstNode, + ControlNode, + EntryNode, + InstructionNode, + JoinNode, + LoadArgumentNode, + makeReactiveId, + NodeDependencies, + NodeReference, + populateReactiveGraphNodeOutputs, + printReactiveNodes, + ReactiveGraph, + ReactiveId, + ReactiveNode, + ReturnNode, + reversePostorderReactiveGraph, + ScopeNode, +} from './ReactiveIR'; + +export function buildReactiveGraph(fn: HIRFunction): ReactiveGraph { + const builder = new Builder(); + const context = new ControlContext(); + const control: EntryNode = { + kind: 'Entry', + id: builder.nextReactiveId, + loc: fn.loc, + outputs: [], + }; + builder.nodes.set(control.id, control); + for (const param of fn.params) { + const place = param.kind === 'Identifier' ? param : param.place; + const node: LoadArgumentNode = { + kind: 'LoadArgument', + id: builder.nextReactiveId, + loc: place.loc, + outputs: [], + place: {...place}, + control: control.id, + }; + builder.nodes.set(node.id, node); + builder.declare(place, node.id); + context.recordDeclaration(place.identifier, node.id); + } + + const exitNode = buildBlockScope( + fn, + builder, + context, + fn.body.entry, + control.id, + ); + + const graph: ReactiveGraph = { + async: fn.async, + directives: fn.directives, + env: fn.env, + exit: exitNode, + fnType: fn.fnType, + generator: fn.generator, + id: fn.id, + loc: fn.loc, + nextNodeId: builder._nextNodeId, + nodes: builder.nodes, + params: fn.params, + }; + populateReactiveGraphNodeOutputs(graph); + reversePostorderReactiveGraph(graph); + return graph; +} + +class Builder { + _nextNodeId: number = 0; + #environment: Map = new Map(); + nodes: Map = new Map(); + args: Set = new Set(); + + get nextReactiveId(): ReactiveId { + return makeReactiveId(this._nextNodeId++); + } + + declare(place: Place, node: ReactiveId): void { + this.#environment.set(place.identifier.id, {node, from: place}); + } + + controlNode(control: ReactiveId, loc: SourceLocation): ReactiveId { + const node: ControlNode = { + kind: 'Control', + id: this.nextReactiveId, + loc, + outputs: [], + control, + }; + this.nodes.set(node.id, node); + return node.id; + } + + lookup( + identifier: Identifier, + loc: SourceLocation, + ): {node: ReactiveId; from: Place} { + const dep = this.#environment.get(identifier.id); + if (dep == null) { + console.log(printReactiveNodes(this.nodes)); + for (const [id, dep] of this.#environment) { + console.log(`t#${id} => £${dep.node} . ${printPlace(dep.from)}`); + } + + console.log(); + console.log(`could not find ${printIdentifier(identifier)}`); + } + CompilerError.invariant(dep != null, { + reason: `No source node for identifier ${printIdentifier(identifier)}`, + loc, + }); + return dep; + } +} + +class ControlContext { + constructor( + private declarations: Map = new Map(), + private scopes: Map = new Map(), + ) {} + + clone(): ControlContext { + return new ControlContext(new Map(this.declarations), new Map(this.scopes)); + } + + recordScope(scope: ScopeId, node: ReactiveId): void { + this.scopes.set(scope, node); + } + + getScope(scope: ScopeId): ReactiveId | undefined { + return this.scopes.get(scope); + } + + recordDeclaration(identifier: Identifier, node: ReactiveId): void { + this.declarations.set(identifier.declarationId, node); + } + + getDeclaration(identifier: Identifier): ReactiveId | undefined { + return this.declarations.get(identifier.declarationId); + } + + assertDeclaration(identifier: Identifier, loc: SourceLocation): ReactiveId { + const id = this.declarations.get(identifier.declarationId); + CompilerError.invariant(id != null, { + reason: `Could not find declaration for ${printIdentifier(identifier)}`, + loc, + }); + return id; + } +} + +function buildBlockScope( + fn: HIRFunction, + builder: Builder, + context: ControlContext, + entry: BlockId, + control: ReactiveId, +): ReactiveId { + let block = fn.body.blocks.get(entry)!; + let lastNode = control; + while (true) { + // iterate instructions of the block + for (const instr of block.instructions) { + const {lvalue, value} = instr; + if (value.kind === 'LoadLocal') { + const declaration = context.assertDeclaration( + value.place.identifier, + value.place.loc, + ); + builder.declare(lvalue, declaration); + } else if ( + value.kind === 'StoreLocal' && + value.lvalue.kind === InstructionKind.Const + ) { + const dep = builder.lookup(value.value.identifier, value.value.loc); + const node: ConstNode = { + kind: 'Const', + id: builder.nextReactiveId, + loc: value.loc, + lvalue: value.lvalue.place, + outputs: [], + value: { + node: dep.node, + from: dep.from, + as: value.value, + }, + control, + }; + builder.nodes.set(node.id, node); + builder.declare(lvalue, node.id); + builder.declare(value.lvalue.place, node.id); + context.recordDeclaration(value.lvalue.place.identifier, node.id); + } else if ( + value.kind === 'StoreLocal' && + value.lvalue.kind === InstructionKind.Let + ) { + CompilerError.throwTodo({ + reason: `Handle StoreLocal kind ${value.lvalue.kind}`, + loc: value.loc, + }); + } else if ( + value.kind === 'StoreLocal' && + value.lvalue.kind === InstructionKind.Reassign + ) { + CompilerError.throwTodo({ + reason: `Handle StoreLocal kind ${value.lvalue.kind}`, + loc: value.loc, + }); + } else if (value.kind === 'StoreLocal') { + CompilerError.throwTodo({ + reason: `Handle StoreLocal kind ${value.lvalue.kind}`, + loc: value.loc, + }); + } else if ( + value.kind === 'Destructure' || + value.kind === 'PrefixUpdate' || + value.kind === 'PostfixUpdate' + ) { + CompilerError.throwTodo({ + reason: `Handle ${value.kind}`, + loc: value.loc, + }); + } else { + for (const _ of eachInstructionValueLValue(value)) { + CompilerError.invariant(false, { + reason: `Expected all lvalue-producing instructions to be special-cased (got ${value.kind})`, + loc: value.loc, + }); + } + const dependencies: NodeDependencies = new Map(); + for (const operand of eachInstructionValueOperand(instr.value)) { + const dep = builder.lookup(operand.identifier, operand.loc); + dependencies.set(dep.node, { + from: {...dep.from}, + as: {...operand}, + }); + } + let scopeControl = control; + const affectedScope = getScopeForInstruction(instr); + if (affectedScope != null) { + const previousScopeNode = context.getScope(affectedScope.id); + scopeControl = previousScopeNode ?? scopeControl; + } + const node: InstructionNode = { + kind: 'Value', + control: scopeControl, + dependencies, + id: builder.nextReactiveId, + loc: instr.loc, + outputs: [], + value: instr, + }; + if (affectedScope != null) { + context.recordScope(affectedScope.id, node.id); + } + builder.nodes.set(node.id, node); + lastNode = node.id; + for (const lvalue of eachInstructionLValue(instr)) { + builder.declare(lvalue, node.id); + } + } + } + + // handle the terminal + const terminal = block.terminal; + switch (terminal.kind) { + case 'if': { + /* + * TODO: we need to see what things the consequent/alternate depended on + * as mutation/reassignment deps, and then add those as control deps of + * the if. this ensures that anything depended on in the body will come + * first. + * + * Can likely have a cloneable mapping of the last node for each + * DeclarationId/ScopeId, and also record which DeclId/ScopeId was accessed + * during a call to buildBlockScope, and then look at that after processing + * consequent/alternate + */ + const testDep = builder.lookup( + terminal.test.identifier, + terminal.test.loc, + ); + const test: NodeReference = { + node: testDep.node, + from: testDep.from, + as: {...terminal.test}, + }; + const branch: BranchNode = { + kind: 'Branch', + control, + dependencies: [], + id: builder.nextReactiveId, + loc: terminal.loc, + outputs: [], + }; + builder.nodes.set(branch.id, branch); + const consequentContext = context.clone(); + const consequentControl = builder.controlNode(branch.id, terminal.loc); + const consequent = buildBlockScope( + fn, + builder, + consequentContext, + terminal.consequent, + consequentControl, + ); + const alternateContext = context.clone(); + const alternateControl = builder.controlNode(branch.id, terminal.loc); + const alternate = + terminal.alternate !== terminal.fallthrough + ? buildBlockScope( + fn, + builder, + alternateContext, + terminal.alternate, + alternateControl, + ) + : alternateControl; + const ifNode: JoinNode = { + kind: 'Join', + control: branch.id, + id: builder.nextReactiveId, + loc: terminal.loc, + outputs: [], + phis: new Map(), + terminal: { + kind: 'If', + test, + consequent, + alternate, + }, + }; + builder.nodes.set(ifNode.id, ifNode); + lastNode = ifNode.id; + break; + } + case 'return': { + const valueDep = builder.lookup( + terminal.value.identifier, + terminal.value.loc, + ); + const value: NodeReference = { + node: valueDep.node, + from: valueDep.from, + as: {...terminal.value}, + }; + const returnNode: ReturnNode = { + kind: 'Return', + id: builder.nextReactiveId, + loc: terminal.loc, + outputs: [], + value, + control, + }; + builder.nodes.set(returnNode.id, returnNode); + lastNode = returnNode.id; + break; + } + case 'scope': { + const body = buildBlockScope( + fn, + builder, + context, + terminal.block, + control, + ); + const scopeNode: ScopeNode = { + kind: 'Scope', + body, + dependencies: new Map(), + id: builder.nextReactiveId, + loc: terminal.scope.loc, + outputs: [], + scope: terminal.scope, + control, + }; + builder.nodes.set(scopeNode.id, scopeNode); + lastNode = scopeNode.id; + break; + } + case 'goto': { + break; + } + default: { + CompilerError.throwTodo({ + reason: `Support ${terminal.kind} nodes`, + loc: terminal.loc, + }); + } + } + + // Continue iteration in the fallthrough + const fallthrough = terminalFallthrough(terminal); + if (fallthrough != null) { + block = fn.body.blocks.get(fallthrough)!; + } else { + break; + } + } + return lastNode; +} + +function getScopeForInstruction(instr: Instruction): ReactiveScope | null { + let scope: ReactiveScope | null = null; + for (const operand of eachInstructionValueOperand(instr.value)) { + if ( + operand.identifier.scope == null || + instr.id < operand.identifier.scope.range.start || + instr.id >= operand.identifier.scope.range.end + ) { + continue; + } + CompilerError.invariant( + scope == null || operand.identifier.scope.id === scope.id, + { + reason: `Multiple scopes for instruction ${printInstruction(instr)}`, + loc: instr.loc, + }, + ); + scope = operand.identifier.scope; + } + return scope; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveIR/ReactiveIR.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveIR/ReactiveIR.ts new file mode 100644 index 0000000000000..a0053c59ec15f --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveIR/ReactiveIR.ts @@ -0,0 +1,461 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import {CompilerError} from '..'; +import { + DeclarationId, + Environment, + Instruction, + Place, + ReactiveScope, + SourceLocation, + SpreadPattern, +} from '../HIR'; +import {ReactFunctionType} from '../HIR/Environment'; +import {printInstruction, printPlace} from '../HIR/PrintHIR'; +import {assertExhaustive} from '../Utils/utils'; + +export type ReactiveGraph = { + nodes: Map; + nextNodeId: number; + exit: ReactiveId; + loc: SourceLocation; + id: string | null; + params: Array; + generator: boolean; + async: boolean; + env: Environment; + directives: Array; + fnType: ReactFunctionType; +}; + +/* + * Simulated opaque type for Reactive IDs to prevent using normal numbers as ids + * accidentally. + */ +const opaqueReactiveId = Symbol(); +export type ReactiveId = number & {[opaqueReactiveId]: 'ReactiveId'}; + +export function makeReactiveId(id: number): ReactiveId { + CompilerError.invariant(id >= 0 && Number.isInteger(id), { + reason: 'Expected reactive node id to be a non-negative integer', + description: null, + loc: null, + suggestions: null, + }); + return id as ReactiveId; +} + +export type ReactiveNode = + | EntryNode + | LoadArgumentNode + | ConstNode + | InstructionNode + | BranchNode + | JoinNode + | ControlNode + | ReturnNode + | ScopeNode; + +export type NodeReference = { + node: ReactiveId; + from: Place; + as: Place; +}; + +export type NodeDependencies = Map; +export type NodeDependency = {from: Place; as: Place}; + +export type EntryNode = { + kind: 'Entry'; + id: ReactiveId; + loc: SourceLocation; + outputs: Array; +}; + +export type LoadArgumentNode = { + kind: 'LoadArgument'; + id: ReactiveId; + loc: SourceLocation; + outputs: Array; + place: Place; + control: ReactiveId; +}; + +export type ConstNode = { + kind: 'Const'; + id: ReactiveId; + loc: SourceLocation; + outputs: Array; + lvalue: Place; + value: NodeReference; + control: ReactiveId; +}; + +// An individual instruction +export type InstructionNode = { + kind: 'Value'; + id: ReactiveId; + loc: SourceLocation; + outputs: Array; + dependencies: NodeDependencies; + control: ReactiveId; + value: Instruction; +}; + +export type ReturnNode = { + kind: 'Return'; + id: ReactiveId; + loc: SourceLocation; + value: NodeReference; + outputs: Array; + control: ReactiveId; +}; + +export type BranchNode = { + kind: 'Branch'; + id: ReactiveId; + loc: SourceLocation; + outputs: Array; + dependencies: Array; // values/scopes depended on by more than one branch, or by the terminal + control: ReactiveId; +}; + +export type JoinNode = { + kind: 'Join'; + id: ReactiveId; + loc: SourceLocation; + outputs: Array; + phis: Map; + terminal: NodeTerminal; + control: ReactiveId; // join node always has a control, which is the corresponding Branch node +}; + +export type PhiNode = { + place: Place; + operands: Map; +}; + +export type NodeTerminal = IfBranch; + +export type IfBranch = { + kind: 'If'; + test: NodeReference; + consequent: ReactiveId; + alternate: ReactiveId; +}; + +export type ControlNode = { + kind: 'Control'; + id: ReactiveId; + loc: SourceLocation; + outputs: Array; + control: ReactiveId; +}; + +export type ScopeNode = { + kind: 'Scope'; + id: ReactiveId; + loc: SourceLocation; + outputs: Array; + scope: ReactiveScope; + /** + * The hoisted dependencies of the scope. Instructions "within" the scope + * (ie, the declarations or their deps) will also depend on these same values + * but we explicitly describe them here to ensure that all deps come before the scope + */ + dependencies: NodeDependencies; + /** + * The nodes that produce the values declared by the scope + */ + // declarations: NodeDependencies; + body: ReactiveId; + control: ReactiveId; +}; + +function _staticInvariantReactiveNodeHasIdLocationAndOutputs( + node: ReactiveNode, +): [ReactiveId, SourceLocation, Array, ReactiveId | null] { + // If this fails, it is because a variant of ReactiveNode is missing a .id and/or .loc - add it! + let control: ReactiveId | null = null; + if (node.kind !== 'Entry') { + const nonNullControl: ReactiveId = node.control; + control = nonNullControl; + } + return [node.id, node.loc, node.outputs, control]; +} + +/** + * Populates the outputs of each node in the graph + */ +export function populateReactiveGraphNodeOutputs(graph: ReactiveGraph): void { + // Populate node outputs + for (const [, node] of graph.nodes) { + node.outputs.length = 0; + } + for (const [, node] of graph.nodes) { + for (const dep of eachNodeDependency(node)) { + const sourceNode = graph.nodes.get(dep); + CompilerError.invariant(sourceNode != null, { + reason: `Expected source dependency ${dep} to exist`, + loc: node.loc, + }); + sourceNode.outputs.push(node.id); + } + } + const exitNode = graph.nodes.get(graph.exit)!; + exitNode.outputs.push(graph.exit); +} + +/** + * Puts the nodes of the graph into reverse postorder, such that nodes + * appear before any of their "successors" (consumers/dependents). + */ +export function reversePostorderReactiveGraph(graph: ReactiveGraph): void { + const nodes: Map = new Map(); + function visit(id: ReactiveId): void { + if (nodes.has(id)) { + return; + } + const node = graph.nodes.get(id); + CompilerError.invariant(node != null, { + reason: `Missing definition for ID ${id}`, + loc: null, + }); + for (const dep of eachNodeDependency(node)) { + visit(dep); + } + nodes.set(id, node); + } + for (const [_id, node] of graph.nodes) { + if (node.outputs.length === 0 && node.kind !== 'Control') { + visit(node.id); + } + } + visit(graph.exit); + graph.nodes = nodes; +} + +export function* eachNodeDependency(node: ReactiveNode): Iterable { + if (node.kind !== 'Entry' && node.control != null) { + yield node.control; + } + switch (node.kind) { + case 'Entry': + case 'Control': + case 'LoadArgument': { + break; + } + case 'Branch': { + yield* node.dependencies; + break; + } + case 'Join': { + for (const phi of node.phis.values()) { + for (const operand of phi.operands.keys()) { + yield operand; + } + } + yield node.terminal.test.node; + yield node.terminal.consequent; + yield node.terminal.alternate; + break; + } + case 'Const': { + yield node.value.node; + break; + } + case 'Return': { + yield node.value.node; + break; + } + case 'Value': { + yield* [...node.dependencies.keys()]; + break; + } + case 'Scope': { + yield* [...node.dependencies.keys()]; + // yield* [...node.declarations.keys()]; + yield node.body; + break; + } + default: { + assertExhaustive(node, `Unexpected node kind '${(node as any).kind}'`); + } + } +} + +export function* eachNodeReference( + node: ReactiveNode, +): Iterable { + switch (node.kind) { + case 'Entry': + case 'Control': + case 'LoadArgument': { + break; + } + case 'Const': { + yield node.value; + break; + } + case 'Return': { + yield node.value; + break; + } + case 'Branch': { + break; + } + case 'Join': { + for (const phi of node.phis.values()) { + for (const [pred, operand] of phi.operands) { + yield { + node: pred, + from: operand, + as: operand, + }; + } + } + yield node.terminal.test; + break; + } + case 'Value': { + yield* [...node.dependencies].map(([node, dep]) => ({ + node, + from: dep.from, + as: dep.as, + })); + break; + } + case 'Scope': { + yield* [...node.dependencies].map(([node, dep]) => ({ + node, + from: dep.from, + as: dep.as, + })); + // yield* [...node.declarations].map(([node, dep]) => ({ + // node, + // from: dep.from, + // as: dep.as, + // })); + break; + } + default: { + assertExhaustive(node, `Unexpected node kind '${(node as any).kind}'`); + } + } +} + +function printNodeReference({node, from, as}: NodeReference): string { + return `£${node}.${printPlace(from)} => ${printPlace(as)}`; +} + +export function printNodeDependencies(deps: NodeDependencies): string { + const buffer: Array = []; + for (const [id, dep] of deps) { + buffer.push(printNodeReference({node: id, from: dep.from, as: dep.as})); + } + return buffer.join(', '); +} + +export function printReactiveGraph(graph: ReactiveGraph): string { + const buffer: Array = []; + buffer.push( + `${graph.fnType} ${graph.id ?? ''}(` + + graph.params + .map(param => { + if (param.kind === 'Identifier') { + return printPlace(param); + } else { + return `...${printPlace(param.place)}`; + } + }) + .join(', ') + + ')', + ); + writeReactiveNodes(buffer, graph.nodes); + buffer.push(`Exit £${graph.exit}`); + return buffer.join('\n'); +} + +export function printReactiveNodes( + nodes: Map, +): string { + const buffer: Array = []; + writeReactiveNodes(buffer, nodes); + return buffer.join('\n'); +} + +function writeReactiveNodes( + buffer: Array, + nodes: Map, +): void { + for (const [id, node] of nodes) { + const deps = [...eachNodeReference(node)] + .map(id => printNodeReference(id)) + .join(' '); + const control = + node.kind !== 'Entry' && node.control != null + ? ` control=£${node.control}` + : ''; + switch (node.kind) { + case 'Entry': { + buffer.push(`£${id} Entry`); + break; + } + case 'LoadArgument': { + buffer.push(`£${id} LoadArgument ${printPlace(node.place)}${control}`); + break; + } + case 'Control': { + buffer.push(`£${id} Control${control}`); + break; + } + case 'Const': { + buffer.push( + `£${id} Const ${printPlace(node.lvalue)} = ${printNodeReference(node.value)}${control}`, + ); + break; + } + case 'Return': { + buffer.push( + `£${id} Return ${printNodeReference(node.value)}${control}`, + ); + break; + } + case 'Branch': { + buffer.push( + `£${id} Branch deps=[${node.dependencies.map(id => `£${id}`).join(', ')}]${control}`, + ); + break; + } + case 'Join': { + buffer.push( + `£${id} If test=${printNodeReference(node.terminal.test)} consequent=£${node.terminal.consequent} alternate=£${node.terminal.alternate}${control}`, + ); + // for (const phi of node.phis.values()) { + // buffer.push(` ${printPlace(phi.place)}: `) + // } + break; + } + case 'Value': { + buffer.push(`£${id} Value deps=[${deps}]${control}`); + buffer.push(' ' + printInstruction(node.value)); + break; + } + case 'Scope': { + buffer.push( + // `£${id} Scope @${node.scope.id} deps=[${printNodeDependencies(node.dependencies)}] declarations=[${printNodeDependencies(node.declarations)}]`, + `£${id} Scope @${node.scope.id} deps=[${printNodeDependencies(node.dependencies)}] body=£${node.body}${control}`, + ); + break; + } + default: { + assertExhaustive(node, `Unexpected node kind ${(node as any).kind}`); + } + } + } +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reactive-graph.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reactive-graph.expect.md new file mode 100644 index 0000000000000..dc33785608779 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reactive-graph.expect.md @@ -0,0 +1,39 @@ + +## Input + +```javascript +// @enableReactiveGraph +function Component(props) { + const elements = []; + if (props.value) { + elements.push(
{props.value}
); + } + return elements; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableReactiveGraph +function Component(props) { + const $ = _c(2); + let elements; + if ($[0] !== props.value) { + elements = []; + if (props.value) { + elements.push(
{props.value}
); + } + $[0] = props.value; + $[1] = elements; + } else { + elements = $[1]; + } + return elements; +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reactive-graph.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reactive-graph.js new file mode 100644 index 0000000000000..ac76010e16d75 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reactive-graph.js @@ -0,0 +1,8 @@ +// @enableReactiveGraph +function Component(props) { + const elements = []; + if (props.value) { + elements.push(
{props.value}
); + } + return elements; +}