-
Notifications
You must be signed in to change notification settings - Fork 47.3k
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?
Conversation
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]
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
Early sketch of a new representation for the middle-phase of the compiler. This is a sea-of-nodes approach, representing instructions and terminals as nodes with direct and control dependencies. Something like this: ```js let array = []; if (cond) { array.push(value); } return array; ``` Would correspond roughly to a graph as follows. Note that the actual representation uses `Instruction` as-is, and maps dependencies into local `Place`s, but for ease of reading i've mapped operands as the node they come from (n0 etc): ``` n0 = ArrayExpression [] n1 = StoreLocal 'array' = n0 // nodes from the consequent n2 = LoadLocal 'array' n3 = PropertyLoad n3 . 'push' n4 = LoadLocal 'value' n5 = MethodCall n2 . n3 ( n4 ) // if terminal n6 = LoadLocal 'cond' n7 = If test=n6 consequent=n5 alternate=(not pictured) // return terminal n8 = LoadLocal 'array'; n9 = Return value=n8 exit=n9 ``` Note that even if there are multiple returns, there will always be a single "exit" node, which is just the node corresponding to the last statement of the outer block in the original program. What's implemented so far is very basic translation of HIR to ReactiveGraph. The only dependencies created are direct data dependencies and basic control flow, which means that e.g. code inside an if consequent will stay "inside" that consequent, but it's possible for mutations or reassignments to get reordered. The reordering happens because we reverse-postorder the graph after construction, which automatically orders strictly based on dependencies and moves things around otherwise. There is lots left to do here, including cleaning up the node dependencies and how they map to local Places, establishing other forms of dependencies, etc. ghstack-source-id: fc633994d81aa93389a2dbbbe83358eea7eeccb0 Pull Request resolved: #31974
function buildBlockScope( | ||
fn: HIRFunction, | ||
builder: Builder, | ||
entry: BlockId, | ||
): ReactiveId { | ||
let block = fn.body.blocks.get(entry)!; | ||
let lastNode: ReactiveNode = { | ||
kind: 'Empty', | ||
id: builder.nextReactiveId, | ||
loc: block.terminal.loc, | ||
outputs: [], | ||
}; | ||
builder.nodes.set(lastNode.id, lastNode); | ||
while (true) { |
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.
graph construction basically hops along the nodes at a given block scope level by following terminal fallthroughs. Inner blocks are handled by recursively calling this function in order to create its nodes. We return the last node of the block scope as its value. The value node of the outermost block becomes the graph's exit node, the value node of inner blocks become dependencies of terminal nodes like IfNode's consequent/alternate.
export type NodeDependencies = Map<ReactiveId, NodeDependency>; | ||
export type NodeDependency = {from: Place; as: Place}; |
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.
from
and as
are absolutely horrible names and i will change them.
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 as
) but am not actually doing this mapping, so from/as will always be the same Place (different objects, but the same identifier).
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; | ||
}; |
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.
scopes are interesting to represent - any subsequent reference to a value declared by the scope should depend on the scope, but i haven't established that link yet during construction.
* (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 comment
The reason will be displayed to describe this comment to others. Learn more.
not populated yet, but this data structure lets us hoist arbitrary dependencies
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]
Early sketch of a new representation for the middle-phase of the compiler. This is a sea-of-nodes approach, representing instructions and terminals as nodes with direct and control dependencies. Something like this: ```js let array = []; if (cond) { array.push(value); } return array; ``` Would correspond roughly to a graph as follows. Note that the actual representation uses `Instruction` as-is, and maps dependencies into local `Place`s, but for ease of reading i've mapped operands as the node they come from (n0 etc): ``` n0 = ArrayExpression [] n1 = StoreLocal 'array' = n0 // nodes from the consequent n2 = LoadLocal 'array' n3 = PropertyLoad n3 . 'push' n4 = LoadLocal 'value' n5 = MethodCall n2 . n3 ( n4 ) // if terminal n6 = LoadLocal 'cond' n7 = If test=n6 consequent=n5 alternate=(not pictured) // return terminal n8 = LoadLocal 'array'; n9 = Return value=n8 exit=n9 ``` Note that even if there are multiple returns, there will always be a single "exit" node, which is just the node corresponding to the last statement of the outer block in the original program. What's implemented so far is very basic translation of HIR to ReactiveGraph. The only dependencies created are direct data dependencies and basic control flow, which means that e.g. code inside an if consequent will stay "inside" that consequent, but it's possible for mutations or reassignments to get reordered. The reordering happens because we reverse-postorder the graph after construction, which automatically orders strictly based on dependencies and moves things around otherwise. There is lots left to do here, including cleaning up the node dependencies and how they map to local Places, establishing other forms of dependencies, etc. ghstack-source-id: 5a92904434b6286ec7c4c7cbdb73b10706d7368d Pull Request resolved: #31974
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.
the graph for this is currently:
BuildReactiveGraph:
Component Component(<unknown> props$14:TObject<BuiltInProps>{reactive})
£0 Entry
£5 Branch deps=[] control=£0
£1 LoadArgument <unknown> props$14:TObject<BuiltInProps>{reactive} control=£0
£4 Intermediate deps=[£1.mutate? $18:TObject<BuiltInProps>{reactive} => read $18:TObject<BuiltInProps>{reactive}] control=£0
[4] mutate? $19{reactive} = PropertyLoad read $18:TObject<BuiltInProps>{reactive}.value
£6 Control control=£5
£2 Intermediate deps=[] control=£0
[1] store $15_@0[1:12]:TObject<BuiltInArray> = Array []
£3 Const store elements$16_@0[1:12]:TObject<BuiltInArray>{reactive} = £2.store $15_@0[1:12]:TObject<BuiltInArray> => capture $15_@0[1:12]:TObject<BuiltInArray>{reactive} control=£0
£7 Intermediate deps=[£3.mutate? $20_@0[1:12]:TObject<BuiltInArray>{reactive} => read $20_@0[1:12]:TObject<BuiltInArray>{reactive}] control=£6
[7] mutate? $21[1:12]:TFunction<<generated_5>>{reactive} = PropertyLoad read $20_@0[1:12]:TObject<BuiltInArray>{reactive}.push
£8 Intermediate deps=[£1.mutate? $22:TObject<BuiltInProps>{reactive} => read $22:TObject<BuiltInProps>{reactive}] control=£6
[9] mutate? $23{reactive} = PropertyLoad read $22:TObject<BuiltInProps>{reactive}.value
£9 Intermediate deps=[£8.mutate? $23{reactive} => read $23{reactive}] control=£6
[10] mutate? $24_@1:TObject<BuiltInJsx>{reactive} = JSX <div>{read $23{reactive}}</div>
£10 Intermediate deps=[£3.mutate? $20_@0[1:12]:TObject<BuiltInArray>{reactive} => store $20_@0[1:12]:TObject<BuiltInArray>{reactive} £7.mutate? $21[1:12]:TFunction<<generated_5>>{reactive} => read $21[1:12]:TFunction<<generated_5>>{reactive} £9.mutate? $24_@1:TObject<BuiltInJsx>{reactive} => read $24_@1:TObject<BuiltInJsx>{reactive}] control=£7
[11] mutate? $25:TPrimitive{reactive} = MethodCall store $20_@0[1:12]:TObject<BuiltInArray>{reactive}.read $21[1:12]:TFunction<<generated_5>>{reactive}(read $24_@1:TObject<BuiltInJsx>{reactive})
£11 Control control=£5
£12 If test=£4.mutate? $19{reactive} => read $19{reactive} consequent=£10 alternate=£11 control=£5
£13 Return £3.mutate? $27:TObject<BuiltInArray>{reactive} => freeze $27:TObject<BuiltInArray>{reactive} control=£0
Exit £13
which is missing some pieces but has a lot right:
- Branch and Join nodes
- Control nodes that connect the strictly necessary control flow bits together, w/o unnecessarily tying together the order of things like unrelated if statements in the same block scope
- Control dependencies on instructions related to the same scope (except that this is missing for StoreLocal/LoadLocal as they get translated into nodes)
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]
Early sketch of a new representation for the middle-phase of the compiler. This is a sea-of-nodes approach, representing instructions and terminals as nodes with direct and control dependencies. Something like this: ```js let array = []; if (cond) { array.push(value); } return array; ``` Would correspond roughly to a graph as follows. Note that the actual representation uses `Instruction` as-is, and maps dependencies into local `Place`s, but for ease of reading i've mapped operands as the node they come from (n0 etc): ``` n0 = ArrayExpression [] n1 = StoreLocal 'array' = n0 // nodes from the consequent n2 = LoadLocal 'array' n3 = PropertyLoad n3 . 'push' n4 = LoadLocal 'value' n5 = MethodCall n2 . n3 ( n4 ) // if terminal n6 = LoadLocal 'cond' n7 = If test=n6 consequent=n5 alternate=(not pictured) // return terminal n8 = LoadLocal 'array'; n9 = Return value=n8 exit=n9 ``` Note that even if there are multiple returns, there will always be a single "exit" node, which is just the node corresponding to the last statement of the outer block in the original program. What's implemented so far is very basic translation of HIR to ReactiveGraph. The only dependencies created are direct data dependencies and basic control flow, which means that e.g. code inside an if consequent will stay "inside" that consequent, but it's possible for mutations or reassignments to get reordered. The reordering happens because we reverse-postorder the graph after construction, which automatically orders strictly based on dependencies and moves things around otherwise. There is lots left to do here, including cleaning up the node dependencies and how they map to local Places, establishing other forms of dependencies, etc. ghstack-source-id: 49414d19361d9bbd58690828bfa6317ba682257a Pull Request resolved: #31974
if (env.config.enableReactiveGraph) { | ||
const reactiveGraph = buildReactiveGraph(hir); | ||
log({ | ||
kind: 'debug', | ||
name: 'BuildReactiveGraph', | ||
value: printReactiveGraph(reactiveGraph), | ||
}); | ||
} |
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
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]
Early sketch of a new representation for the middle-phase of the compiler. This is a sea-of-nodes approach, representing instructions and terminals as nodes with direct and control dependencies. Something like this: ```js let array = []; if (cond) { array.push(value); } return array; ``` Would correspond roughly to a graph as follows. Note that the actual representation uses `Instruction` as-is, and maps dependencies into local `Place`s, but for ease of reading i've mapped operands as the node they come from (n0 etc): ``` n0 = ArrayExpression [] n1 = StoreLocal 'array' = n0 // nodes from the consequent n2 = LoadLocal 'array' n3 = PropertyLoad n3 . 'push' n4 = LoadLocal 'value' n5 = MethodCall n2 . n3 ( n4 ) // if terminal n6 = LoadLocal 'cond' n7 = If test=n6 consequent=n5 alternate=(not pictured) // return terminal n8 = LoadLocal 'array'; n9 = Return value=n8 exit=n9 ``` Note that even if there are multiple returns, there will always be a single "exit" node, which is just the node corresponding to the last statement of the outer block in the original program. What's implemented so far is very basic translation of HIR to ReactiveGraph. The only dependencies created are direct data dependencies and basic control flow, which means that e.g. code inside an if consequent will stay "inside" that consequent, but it's possible for mutations or reassignments to get reordered. The reordering happens because we reverse-postorder the graph after construction, which automatically orders strictly based on dependencies and moves things around otherwise. There is lots left to do here, including cleaning up the node dependencies and how they map to local Places, establishing other forms of dependencies, etc. ghstack-source-id: f182ca652bc01e4ba21753f933f9aa5e7faecacd Pull Request resolved: #31974
Stack from ghstack (oldest at bottom):
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:
Would correspond roughly to a graph as follows. Note that the actual representation uses
Instruction
as-is, and maps dependencies into localPlace
s, but for ease of reading i've mapped operands as the node they come from (n0 etc):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.