-
Notifications
You must be signed in to change notification settings - Fork 47.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[compiler] Early sketch of ReactiveIR #31974
base: gh/josephsavona/64/base
Are you sure you want to change the base?
Changes from 3 commits
8771138
0522415
3f0031d
9e1f36a
4e104fc
a5d0912
5be650c
52c360f
3383816
d09f026
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<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(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<DeclarationId, ReactiveId> = new Map(), | ||
private scopes: Map<ScopeId, ReactiveId> = 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; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<ReactiveId, ReactiveNode>; | ||
nextNodeId: number; | ||
exit: ReactiveId; | ||
loc: SourceLocation; | ||
id: string | null; | ||
params: Array<Place | SpreadPattern>; | ||
generator: boolean; | ||
async: boolean; | ||
env: Environment; | ||
directives: Array<string>; | ||
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<ReactiveId, NodeDependency>; | ||
export type NodeDependency = {from: Place; as: Place}; | ||
Comment on lines
+70
to
+71
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
also right now i've established the data structures for mapping the value of one node into a local Place in its consuming node (the |
||
|
||
export type EntryNode = { | ||
kind: 'Entry'; | ||
id: ReactiveId; | ||
loc: SourceLocation; | ||
outputs: Array<ReactiveId>; | ||
}; | ||
|
||
export type LoadArgumentNode = { | ||
kind: 'LoadArgument'; | ||
id: ReactiveId; | ||
loc: SourceLocation; | ||
outputs: Array<ReactiveId>; | ||
place: Place; | ||
control: ReactiveId; | ||
}; | ||
|
||
export type ConstNode = { | ||
kind: 'Const'; | ||
id: ReactiveId; | ||
loc: SourceLocation; | ||
outputs: Array<ReactiveId>; | ||
lvalue: Place; | ||
value: NodeReference; | ||
control: ReactiveId; | ||
}; | ||
|
||
// An individual instruction | ||
export type InstructionNode = { | ||
kind: 'Value'; | ||
id: ReactiveId; | ||
loc: SourceLocation; | ||
outputs: Array<ReactiveId>; | ||
dependencies: NodeDependencies; | ||
control: ReactiveId; | ||
value: Instruction; | ||
}; | ||
|
||
export type ReturnNode = { | ||
kind: 'Return'; | ||
id: ReactiveId; | ||
loc: SourceLocation; | ||
value: NodeReference; | ||
outputs: Array<ReactiveId>; | ||
control: ReactiveId; | ||
}; | ||
|
||
export type BranchNode = { | ||
kind: 'Branch'; | ||
id: ReactiveId; | ||
loc: SourceLocation; | ||
outputs: Array<ReactiveId>; | ||
dependencies: Array<ReactiveId>; // 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<ReactiveId>; | ||
phis: Map<DeclarationId, PhiNode>; | ||
terminal: NodeTerminal; | ||
control: ReactiveId; // join node always has a control, which is the corresponding Branch node | ||
}; | ||
|
||
export type PhiNode = { | ||
place: Place; | ||
operands: Map<ReactiveId, Place>; | ||
}; | ||
|
||
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<ReactiveId>; | ||
control: ReactiveId; | ||
}; | ||
|
||
export type ScopeNode = { | ||
kind: 'Scope'; | ||
id: ReactiveId; | ||
loc: SourceLocation; | ||
outputs: Array<ReactiveId>; | ||
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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not populated yet, but this data structure lets us hoist arbitrary dependencies |
||
/** | ||
* The nodes that produce the values declared by the scope | ||
*/ | ||
// declarations: NodeDependencies; | ||
body: ReactiveId; | ||
control: ReactiveId; | ||
}; | ||
|
||
function _staticInvariantReactiveNodeHasIdLocationAndOutputs( | ||
node: ReactiveNode, | ||
): [ReactiveId, SourceLocation, Array<ReactiveId>, 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<ReactiveId, ReactiveNode> = 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<ReactiveId> { | ||
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<NodeReference> { | ||
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<string> = []; | ||
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<string> = []; | ||
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<ReactiveId, ReactiveNode>, | ||
): string { | ||
const buffer: Array<string> = []; | ||
writeReactiveNodes(buffer, nodes); | ||
return buffer.join('\n'); | ||
} | ||
|
||
function writeReactiveNodes( | ||
buffer: Array<string>, | ||
nodes: Map<ReactiveId, ReactiveNode>, | ||
): 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}`); | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
|
||
## Input | ||
|
||
```javascript | ||
// @enableReactiveGraph | ||
function Component(props) { | ||
const elements = []; | ||
if (props.value) { | ||
elements.push(<div>{props.value}</div>); | ||
} | ||
return elements; | ||
} | ||
Comment on lines
+7
to
+12
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the graph for this is currently:
which is missing some pieces but has a lot right:
|
||
|
||
``` | ||
|
||
## 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(<div>{props.value}</div>); | ||
} | ||
$[0] = props.value; | ||
$[1] = elements; | ||
} else { | ||
elements = $[1]; | ||
} | ||
return elements; | ||
} | ||
|
||
``` | ||
### Eval output | ||
(kind: exception) Fixture not implemented |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
// @enableReactiveGraph | ||
function Component(props) { | ||
const elements = []; | ||
if (props.value) { | ||
elements.push(<div>{props.value}</div>); | ||
} | ||
return elements; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
note that i'm experimenting with running this before constructing scope terminals, since that's where it would have to go in order to reorder to avoid unnecessary scope interleaving