Compare commits

...

10 Commits

Author SHA1 Message Date
Joe Savona
d09f026bb0 Update on "[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):

```
// instructions before the 'if': note that these are dangling 
// (no nodes depends on them) but they would get ordered as control deps of n7, the if node
n0 = ArrayExpression []
n1 = StoreLocal 'array' = n0

// nodes from the consequent
n2 = LoadLocal 'array'
n3 = PropertyLoad n2 . '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.

Converting back to HIR is a bit tricky, but shouldn’t be too bad. For all the nodes that correspond to a given block scope we can just emit the nodes to HIR in order, since they’re already in reverse post order. The catch is that nodes for inner block scopes are also mixed in and have to be emitted at the right point. I need to experiment with this more. 

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-poisoned]
2025-01-21 15:24:15 -08:00
Joe Savona
3383816b0a Update on "[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):

```
// instructions before the 'if': note that these are dangling 
// (no nodes depends on them) but they would get ordered as control deps of n7, the if node
n0 = ArrayExpression []
n1 = StoreLocal 'array' = n0

// nodes from the consequent
n2 = LoadLocal 'array'
n3 = PropertyLoad n2 . '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.

Converting back to HIR is a bit tricky, but shouldn’t be too bad. For all the nodes that correspond to a given block scope we can just emit the nodes to HIR in order, since they’re already in reverse post order. The catch is that nodes for inner block scopes are also mixed in and have to be emitted at the right point. I need to experiment with this more. 

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-poisoned]
2025-01-08 14:56:22 -08:00
Joe Savona
52c360f96c Update on "[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):

```
// instructions before the 'if': note that these are dangling 
// (no nodes depends on them) but they would get ordered as control deps of n7, the if node
n0 = ArrayExpression []
n1 = StoreLocal 'array' = n0

// nodes from the consequent
n2 = LoadLocal 'array'
n3 = PropertyLoad n2 . '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.

Converting back to HIR is a bit tricky, but shouldn’t be too bad. For all the nodes that correspond to a given block scope we can just emit the nodes to HIR in order, since they’re already in reverse post order. The catch is that nodes for inner block scopes are also mixed in and have to be emitted at the right point. I need to experiment with this more. 

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-poisoned]
2025-01-08 14:11:00 -08:00
Joe Savona
5be650c85f Update on "[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):

```
// instructions before the 'if': note that these are dangling 
// (no nodes depends on them) but they would get ordered as control deps of n7, the if node
n0 = ArrayExpression []
n1 = StoreLocal 'array' = n0

// nodes from the consequent
n2 = LoadLocal 'array'
n3 = PropertyLoad n2 . '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.

Converting back to HIR is a bit tricky, but shouldn’t be too bad. For all the nodes that correspond to a given block scope we can just emit the nodes to HIR in order, since they’re already in reverse post order. The catch is that nodes for inner block scopes are also mixed in and have to be emitted at the right point. I need to experiment with this more. 

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-poisoned]
2025-01-08 09:54:36 -08:00
Joe Savona
a5d0912f71 Update on "[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):

```
// instructions before the 'if': note that these are dangling 
// (no nodes depends on them) but they would get ordered as control deps of n7, the if node
n0 = ArrayExpression []
n1 = StoreLocal 'array' = n0

// nodes from the consequent
n2 = LoadLocal 'array'
n3 = PropertyLoad n2 . '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.

Converting back to HIR is a bit tricky, but shouldn’t be too bad. For all the nodes that correspond to a given block scope we can just emit the nodes to HIR in order, since they’re already in reverse post order. The catch is that nodes for inner block scopes are also mixed in and have to be emitted at the right point. I need to experiment with this more. 

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-poisoned]
2025-01-07 21:27:16 -08:00
Joe Savona
4e104fce8e Update on "[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):

```
// instructions before the 'if': note that these are dangling 
// (no nodes depends on them) but they would get ordered as control deps of n7, the if node
n0 = ArrayExpression []
n1 = StoreLocal 'array' = n0

// nodes from the consequent
n2 = LoadLocal 'array'
n3 = PropertyLoad n2 . '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.

Converting back to HIR is a bit tricky, but shouldn’t be too bad. For all the nodes that correspond to a given block scope we can just emit the nodes to HIR in order, since they’re already in reverse post order. The catch is that nodes for inner block scopes are also mixed in and have to be emitted at the right point. I need to experiment with this more. 

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-poisoned]
2025-01-07 11:38:58 -08:00
Joe Savona
9e1f36ab3e Update on "[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):

```
// instructions before the 'if': note that these are dangling 
// (no nodes depends on them) but they would get ordered as control deps of n7, the if node
n0 = ArrayExpression []
n1 = StoreLocal 'array' = n0

// nodes from the consequent
n2 = LoadLocal 'array'
n3 = PropertyLoad n2 . '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.

Converting back to HIR is a bit tricky, but shouldn’t be too bad. For all the nodes that correspond to a given block scope we can just emit the nodes to HIR in order, since they’re already in reverse post order. The catch is that nodes for inner block scopes are also mixed in and have to be emitted at the right point. I need to experiment with this more. 

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-poisoned]
2025-01-06 12:20:56 -08:00
Joe Savona
3f0031d6e0 Update on "[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):

```
// instructions before the 'if': note that these are dangling 
// (no nodes depends on them) but they would get ordered as control deps of n7, the if node
n0 = ArrayExpression []
n1 = StoreLocal 'array' = n0

// nodes from the consequent
n2 = LoadLocal 'array'
n3 = PropertyLoad n2 . '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.

Converting back to HIR is a bit tricky, but shouldn’t be too bad. For all the nodes that correspond to a given block scope we can just emit the nodes to HIR in order, since they’re already in reverse post order. The catch is that nodes for inner block scopes are also mixed in and have to be emitted at the right point. I need to experiment with this more. 

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-poisoned]
2025-01-06 11:31:12 -08:00
Joe Savona
0522415f9d Update on "[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):

```
// instructions before the 'if': note that these are dangling 
// (no nodes depends on them) but they would get ordered as control deps of n7, the if node
n0 = ArrayExpression []
n1 = StoreLocal 'array' = n0

// nodes from the consequent
n2 = LoadLocal 'array'
n3 = PropertyLoad n2 . '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.

Converting back to HIR is a bit tricky, but shouldn’t be too bad. For all the nodes that correspond to a given block scope we can just emit the nodes to HIR in order, since they’re already in reverse post order. The catch is that nodes for inner block scopes are also mixed in and have to be emitted at the right point. I need to experiment with this more. 

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-poisoned]
2025-01-06 11:27:47 -08:00
Joe Savona
87711389ff [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-poisoned]
2025-01-03 16:58:01 -08:00
8 changed files with 1095 additions and 0 deletions

View File

@@ -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',

View File

@@ -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.

View File

@@ -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;
}

View File

@@ -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}`);
}
}
}
}

View File

@@ -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

View File

@@ -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];
}

View File

@@ -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

View File

@@ -0,0 +1,8 @@
// @enableReactiveGraph
function Component(props) {
const elements = [];
if (props.value) {
elements.push(<div>{props.value}</div>);
}
return elements;
}