Compare commits
10 Commits
pr35832
...
gh/josephs
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d09f026bb0 | ||
|
|
3383816b0a | ||
|
|
52c360f96c | ||
|
|
5be650c85f | ||
|
|
a5d0912f71 | ||
|
|
4e104fce8e | ||
|
|
9e1f36ab3e | ||
|
|
3f0031d6e0 | ||
|
|
0522415f9d | ||
|
|
87711389ff |
@@ -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',
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -0,0 +1,500 @@
|
||||
/**
|
||||
* 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 entryNode: EntryNode = {
|
||||
kind: 'Entry',
|
||||
id: builder.nextReactiveId,
|
||||
loc: fn.loc,
|
||||
outputs: [],
|
||||
};
|
||||
builder.nodes.set(entryNode.id, entryNode);
|
||||
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: entryNode.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,
|
||||
entryNode.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,
|
||||
dependencies: [],
|
||||
};
|
||||
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(
|
||||
public declarations: Map<DeclarationId, ReactiveId> = new Map(),
|
||||
public scopes: Map<ScopeId, ReactiveId> = new Map(),
|
||||
) {}
|
||||
|
||||
fork(): ControlContext {
|
||||
return new ControlContext(
|
||||
new Map(this.declarations),
|
||||
/*
|
||||
* we fork with empty scope context, because within the fork the first
|
||||
* occurence of each scope must depend on the fork's control
|
||||
*/
|
||||
new Map(),
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
const instructionNodeId = builder.nextReactiveId;
|
||||
|
||||
// Generic handling of scope and variable control dependencies
|
||||
const instructionControls: Array<ReactiveId> = [];
|
||||
if (value.kind !== 'LoadLocal') {
|
||||
const instructionScope = getScopeForInstruction(instr);
|
||||
if (instructionScope != null) {
|
||||
const previousScopeNode = context.getScope(instructionScope.id);
|
||||
if (previousScopeNode != null) {
|
||||
instructionControls.push(previousScopeNode);
|
||||
}
|
||||
context.recordScope(instructionScope.id, instructionNodeId);
|
||||
}
|
||||
}
|
||||
for (const lvalue of eachInstructionValueLValue(value)) {
|
||||
const previousDeclarationNode = context.getDeclaration(
|
||||
lvalue.identifier,
|
||||
);
|
||||
if (previousDeclarationNode != null) {
|
||||
instructionControls.push(previousDeclarationNode);
|
||||
}
|
||||
context.recordDeclaration(lvalue.identifier, instructionNodeId);
|
||||
}
|
||||
let instructionControl: ReactiveId;
|
||||
if (instructionControls.length === 0) {
|
||||
instructionControl = control;
|
||||
} else if (instructionControls.length === 1) {
|
||||
instructionControl = instructionControls[0]!;
|
||||
} else {
|
||||
const node: ControlNode = {
|
||||
kind: 'Control',
|
||||
control,
|
||||
id: builder.nextReactiveId,
|
||||
loc: instr.loc,
|
||||
outputs: [],
|
||||
dependencies: instructionControls,
|
||||
};
|
||||
builder.nodes.set(node.id, node);
|
||||
instructionControl = node.id;
|
||||
}
|
||||
|
||||
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: instructionNodeId,
|
||||
loc: value.loc,
|
||||
lvalue: value.lvalue.place,
|
||||
outputs: [],
|
||||
value: {
|
||||
node: dep.node,
|
||||
from: dep.from,
|
||||
as: value.value,
|
||||
},
|
||||
control: instructionControl,
|
||||
};
|
||||
builder.nodes.set(node.id, node);
|
||||
builder.declare(lvalue, node.id);
|
||||
builder.declare(value.lvalue.place, 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},
|
||||
});
|
||||
}
|
||||
const node: InstructionNode = {
|
||||
kind: 'Value',
|
||||
control: instructionControl,
|
||||
dependencies,
|
||||
id: instructionNodeId,
|
||||
loc: instr.loc,
|
||||
outputs: [],
|
||||
value: instr,
|
||||
};
|
||||
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.fork();
|
||||
const consequentControl = builder.controlNode(branch.id, terminal.loc);
|
||||
const consequent = buildBlockScope(
|
||||
fn,
|
||||
builder,
|
||||
consequentContext,
|
||||
terminal.consequent,
|
||||
consequentControl,
|
||||
);
|
||||
const alternateContext = context.fork();
|
||||
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,
|
||||
},
|
||||
};
|
||||
for (const scope of consequentContext.scopes.keys()) {
|
||||
context.recordScope(scope, ifNode.id);
|
||||
}
|
||||
for (const scope of alternateContext.scopes.keys()) {
|
||||
context.recordScope(scope, ifNode.id);
|
||||
}
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,474 @@
|
||||
/**
|
||||
* 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};
|
||||
|
||||
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>;
|
||||
dependencies: 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;
|
||||
/**
|
||||
* 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> {
|
||||
switch (node.kind) {
|
||||
case 'Entry':
|
||||
case 'LoadArgument': {
|
||||
break;
|
||||
}
|
||||
case 'Control':
|
||||
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}'`);
|
||||
}
|
||||
}
|
||||
if (node.kind !== 'Entry' && node.control != null) {
|
||||
yield node.control;
|
||||
}
|
||||
}
|
||||
|
||||
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} Join${control}`);
|
||||
switch (node.terminal.kind) {
|
||||
case 'If': {
|
||||
buffer.push(
|
||||
` If test=${printNodeReference(node.terminal.test)} consequent=£${node.terminal.consequent} alternate=£${node.terminal.alternate}${control}`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
// assertExhaustive(
|
||||
// node.terminal,
|
||||
// `Unsupported terminal kind ${(node.terminal as any).kind}`,
|
||||
// );
|
||||
}
|
||||
}
|
||||
// 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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableReactiveGraph
|
||||
function Component(props) {
|
||||
const x = [];
|
||||
const y = [];
|
||||
if (props.condition) {
|
||||
x.push(props.x);
|
||||
}
|
||||
y.push(props.y);
|
||||
return [x, y];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableReactiveGraph
|
||||
function Component(props) {
|
||||
const $ = _c(4);
|
||||
let t0;
|
||||
if ($[0] !== props.condition || $[1] !== props.x || $[2] !== props.y) {
|
||||
const x = [];
|
||||
const y = [];
|
||||
if (props.condition) {
|
||||
x.push(props.x);
|
||||
}
|
||||
|
||||
y.push(props.y);
|
||||
t0 = [x, y];
|
||||
$[0] = props.condition;
|
||||
$[1] = props.x;
|
||||
$[2] = props.y;
|
||||
$[3] = t0;
|
||||
} else {
|
||||
t0 = $[3];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -0,0 +1,10 @@
|
||||
// @enableReactiveGraph
|
||||
function Component(props) {
|
||||
const x = [];
|
||||
const y = [];
|
||||
if (props.condition) {
|
||||
x.push(props.x);
|
||||
}
|
||||
y.push(props.y);
|
||||
return [x, y];
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableReactiveGraph
|
||||
function Component(props) {
|
||||
const elements = [];
|
||||
if (props.value) {
|
||||
elements.push(<div>{props.value}</div>);
|
||||
}
|
||||
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(<div>{props.value}</div>);
|
||||
}
|
||||
$[0] = props.value;
|
||||
$[1] = elements;
|
||||
} else {
|
||||
elements = $[1];
|
||||
}
|
||||
return elements;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -0,0 +1,8 @@
|
||||
// @enableReactiveGraph
|
||||
function Component(props) {
|
||||
const elements = [];
|
||||
if (props.value) {
|
||||
elements.push(<div>{props.value}</div>);
|
||||
}
|
||||
return elements;
|
||||
}
|
||||
Reference in New Issue
Block a user