-
Notifications
You must be signed in to change notification settings - Fork 47.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[compiler] Early sketch of ReactiveIR
Early sketch of a new representation for the middle-phase of the compiler. This is a sea-of-nodes approach, representing instructions and terminals as nodes with direct and control dependencies. Something like this: ```js let array = []; if (cond) { array.push(value); } return array; ``` Would correspond roughly to a graph as follows. Note that the actual representation uses `Instruction` as-is, and maps dependencies into local `Place`s, but for ease of reading i've mapped operands as the node they come from (n0 etc): ``` n0 = ArrayExpression [] n1 = StoreLocal 'array' = n0 // nodes from the consequent n2 = LoadLocal 'array' n3 = PropertyLoad n3 . 'push' n4 = LoadLocal 'value' n5 = MethodCall n2 . n3 ( n4 ) // if terminal n6 = LoadLocal 'cond' n7 = If test=n6 consequent=n5 alternate=(not pictured) // return terminal n8 = LoadLocal 'array'; n9 = Return value=n8 exit=n9 ``` Note that even if there are multiple returns, there will always be a single "exit" node, which is just the node corresponding to the last statement of the outer block in the original program. What's implemented so far is very basic translation of HIR to ReactiveGraph. The only dependencies created are direct data dependencies and basic control flow, which means that e.g. code inside an if consequent will stay "inside" that consequent, but it's possible for mutations or reassignments to get reordered. The reordering happens because we reverse-postorder the graph after construction, which automatically orders strictly based on dependencies and moves things around otherwise. There is lots left to do here, including cleaning up the node dependencies and how they map to local Places, establishing other forms of dependencies, etc. ghstack-source-id: fc633994d81aa93389a2dbbbe83358eea7eeccb0 Pull Request resolved: #31974
- Loading branch information
1 parent
bf883be
commit de873ab
Showing
5 changed files
with
605 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
228 changes: 228 additions & 0 deletions
228
compiler/packages/babel-plugin-react-compiler/src/ReactiveIR/BuildReactiveGraph.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,228 @@ | ||
/** | ||
* 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, HIRFunction, Identifier, IdentifierId, Place} from '../HIR'; | ||
import {printIdentifier, printPlace} from '../HIR/PrintHIR'; | ||
import { | ||
eachInstructionLValue, | ||
eachInstructionValueOperand, | ||
terminalFallthrough, | ||
} from '../HIR/visitors'; | ||
import { | ||
IfNode, | ||
InstructionNode, | ||
LoadArgumentNode, | ||
makeReactiveId, | ||
NodeDependencies, | ||
NodeReference, | ||
populateReactiveGraphNodeOutputs, | ||
printReactiveNodes, | ||
ReactiveGraph, | ||
ReactiveId, | ||
ReactiveNode, | ||
ReturnNode, | ||
reversePostorderReactiveGraph, | ||
ScopeNode, | ||
} from './ReactiveIR'; | ||
|
||
export function buildReactiveGraph(fn: HIRFunction): ReactiveGraph { | ||
const builder = new Builder(); | ||
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}, | ||
}; | ||
builder.nodes.set(node.id, node); | ||
builder.declare(node.id, place); | ||
} | ||
|
||
const exitNode = buildBlockScope(fn, builder, fn.body.entry); | ||
|
||
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<IdentifierId, {node: ReactiveId; from: Place}> = new Map(); | ||
nodes: Map<ReactiveId, ReactiveNode> = new Map(); | ||
args: Set<IdentifierId> = new Set(); | ||
|
||
get nextReactiveId(): ReactiveId { | ||
return makeReactiveId(this._nextNodeId++); | ||
} | ||
|
||
declare(node: ReactiveId, place: Place): void { | ||
this.#environment.set(place.identifier.id, {node, from: place}); | ||
} | ||
|
||
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; | ||
} | ||
} | ||
|
||
function buildBlockScope( | ||
fn: HIRFunction, | ||
builder: Builder, | ||
entry: BlockId, | ||
): ReactiveId { | ||
let block = fn.body.blocks.get(entry)!; | ||
let lastNode: ReactiveNode = { | ||
kind: 'Empty', | ||
id: builder.nextReactiveId, | ||
loc: block.terminal.loc, | ||
outputs: [], | ||
}; | ||
builder.nodes.set(lastNode.id, lastNode); | ||
while (true) { | ||
// iterate instructions of the block | ||
for (const instr of block.instructions) { | ||
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}, | ||
}); | ||
} | ||
const node: InstructionNode = { | ||
kind: 'Value', | ||
controlDependency: null, | ||
dependencies, | ||
id: builder.nextReactiveId, | ||
loc: instr.loc, | ||
outputs: [], | ||
value: instr, | ||
}; | ||
builder.nodes.set(node.id, node); | ||
lastNode = node; | ||
for (const lvalue of eachInstructionLValue(instr)) { | ||
builder.declare(node.id, lvalue); | ||
} | ||
} | ||
|
||
// handle the terminal | ||
const terminal = block.terminal; | ||
switch (terminal.kind) { | ||
case 'if': { | ||
const testDep = builder.lookup( | ||
terminal.test.identifier, | ||
terminal.test.loc, | ||
); | ||
const test: NodeReference = { | ||
node: testDep.node, | ||
from: testDep.from, | ||
as: {...terminal.test}, | ||
}; | ||
const consequent = buildBlockScope(fn, builder, terminal.consequent); | ||
const alternate = buildBlockScope(fn, builder, terminal.alternate); | ||
const ifNode: IfNode = { | ||
kind: 'If', | ||
alternate, | ||
consequent, | ||
id: builder.nextReactiveId, | ||
loc: terminal.loc, | ||
outputs: [], | ||
test, | ||
}; | ||
builder.nodes.set(ifNode.id, ifNode); | ||
lastNode = ifNode; | ||
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, | ||
}; | ||
builder.nodes.set(returnNode.id, returnNode); | ||
lastNode = returnNode; | ||
break; | ||
} | ||
case 'scope': { | ||
const body = buildBlockScope(fn, builder, terminal.block); | ||
const scopeNode: ScopeNode = { | ||
kind: 'Scope', | ||
body, | ||
dependencies: new Map(), | ||
id: builder.nextReactiveId, | ||
loc: terminal.scope.loc, | ||
outputs: [], | ||
scope: terminal.scope, | ||
}; | ||
builder.nodes.set(scopeNode.id, scopeNode); | ||
lastNode = scopeNode; | ||
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.id; | ||
} |
Oops, something went wrong.