Identifier.scope includes scope id and range
Refactors `Identifier.scope` to be a `ReactiveScope` object with an id and range. This gives us a place to later add a list of dependencies for the scope.
Joe Savona committed
Nov 22, 2022 at 08:03 UTC
796e14f38e1ff45ee91f8f9448b9e5fb4189ad2e
4 files changed
+32
-227
compiler/forget/src/HIR/HIR.ts
+6
-12
@@ -53,17 +53,6 @@ export type ReactFunction = {
53
scopes: Map<ScopeId, ReactiveScope>;
54
};
55
56
-/**
57
- * Each scope has a set of inputs (which is loosely defined as stages of analysis
58
- * may refine the set of inputs), a set of outputs (values it will produce), and
59
- * a set of instructions to produce the outputs from the inputs.
60
- */
61
-export type ReactiveScope = {
62
- inputs: Set<Place>;
63
- outputs: Set<Place>;
64
- instructions: HIR;
65
-};
66
-
56
/**
57
* A function declaration including its path
58
*/
@@ -297,7 +286,7 @@ export type Identifier = {
286
mutableRange: MutableRange;
287
// The ID of the reactive scope which will compute this value. Multiple variables may have
288
// the same scope id.
300
- scope: ScopeId | null;
289
+ scope: ReactiveScope | null;
290
};
291
292
/**
@@ -325,6 +314,11 @@ export enum Effect {
314
Mutate = "mutate",
315
}
316
317
+export type ReactiveScope = {
318
+ id: ScopeId;
319
+ range: MutableRange;
320
+};
321
+
322
/**
323
* Simulated opaque type for BlockIds to prevent using normal numbers as block ids
324
* accidentally.
compiler/forget/src/HIR/InferReactiveScopeVariables.ts
+25
-28
@@ -12,8 +12,8 @@ import {
12
Instruction,
13
makeInstructionId,
14
makeScopeId,
15
- MutableRange,
15
Place,
16
+ ReactiveScope,
17
ScopeId,
18
} from "./HIR";
19
import { eachInstructionOperand } from "./visitors";
@@ -66,11 +66,11 @@ import { eachInstructionOperand } from "./visitors";
66
export function inferReactiveScopeVariables(fn: HIRFunction) {
67
// Represents the set of reactive scopes as disjoint sets of identifiers
68
// that mutate together.
69
- const scopes = new DisjointSet<Identifier>();
69
+ const scopeIdentifiers = new DisjointSet<Identifier>();
70
for (const [_, block] of fn.body.blocks) {
71
for (const phi of block.phis) {
72
const operands: Array<Identifier> = [phi.id, ...phi.operands.values()];
73
- scopes.union(operands);
73
+ scopeIdentifiers.union(operands);
74
}
75
76
for (const instr of block.instructions) {
@@ -89,18 +89,14 @@ export function inferReactiveScopeVariables(fn: HIRFunction) {
89
}
90
}
91
if (operands.length !== 0) {
92
- scopes.union(operands);
92
+ scopeIdentifiers.union(operands);
93
}
94
}
95
}
96
97
// Maps each scope (by its identifying member) to a ScopeId value
98
- const scopeIds: Map<Identifier, ScopeId> = new Map();
99
- // Store the mutable range and set of identifiers for each scope
100
- const scopeVariables: Map<
101
- ScopeId,
102
- { range: MutableRange; variables: Set<Identifier> }
103
- > = new Map();
98
+ const scopes: Map<Identifier, ReactiveScope> = new Map();
99
+ const scopeVariables: Map<ReactiveScope, Set<Identifier>> = new Map();
100
101
/**
102
* Iterate over all the identifiers and assign a unique ScopeId
@@ -110,21 +106,14 @@ export function inferReactiveScopeVariables(fn: HIRFunction) {
106
* build a MutableRange that describes the span of mutations
107
* across all identifiers in each scope.
108
*/
113
- scopes.forEach((identifier, groupIdentifier) => {
114
- let scopeId = scopeIds.get(groupIdentifier);
115
- if (scopeId == null) {
116
- scopeId = makeScopeId(scopeIds.size);
117
- scopeIds.set(groupIdentifier, scopeId);
118
- }
119
- identifier.scope = scopeId;
120
-
121
- let scope = scopeVariables.get(scopeId);
109
+ scopeIdentifiers.forEach((identifier, groupIdentifier) => {
110
+ let scope = scopes.get(groupIdentifier);
111
if (scope === undefined) {
112
scope = {
124
- range: { ...identifier.mutableRange },
125
- variables: new Set(),
113
+ id: makeScopeId(scopes.size),
114
+ range: identifier.mutableRange,
115
};
127
- scopeVariables.set(scopeId, scope);
116
+ scopes.set(groupIdentifier, scope);
117
} else {
118
scope.range.start = makeInstructionId(
119
Math.min(scope.range.start, identifier.mutableRange.start)
@@ -133,14 +122,22 @@ export function inferReactiveScopeVariables(fn: HIRFunction) {
122
Math.max(scope.range.end, identifier.mutableRange.end)
123
);
124
}
136
- scope.variables.add(identifier);
125
+ identifier.scope = scope;
126
+
127
+ let vars = scopeVariables.get(scope);
128
+ if (vars === undefined) {
129
+ vars = new Set();
130
+ scopeVariables.set(scope, vars);
131
+ }
132
+ vars.add(identifier);
133
});
134
139
- // Update all the identifiers for each scope now that we know
140
- // the scope's full range.
141
- for (const [_, scope] of scopeVariables) {
142
- for (const identifier of scope.variables) {
143
- identifier.mutableRange = scope.range;
135
+ // Copy scope ranges to identifier ranges: not strictly required but this is useful
136
+ // for visualization
137
+ for (const [scope, vars] of scopeVariables) {
138
+ for (const identifier of vars) {
139
+ identifier.mutableRange.start = scope.range.start;
140
+ identifier.mutableRange.end = scope.range.end;
141
}
142
}
143
}
compiler/forget/src/HIR/PrintHIR.ts
+1
-1
@@ -297,7 +297,7 @@ export function printPlace(place: Place): string {
297
298
export function printIdentifier(id: Identifier): string {
299
return `${id.name ?? ""}\$${id.id}${
300
- id.scope !== null ? `_@${id.scope}` : ""
300
+ id.scope !== null ? `_@${id.scope.id}` : ""
301
}`;
302
}
303
compiler/forget/src/HIR/ScopeAnalysis.ts
deleted
-186
@@ -1,186 +0,0 @@
1
-/**
2
- * Copyright (c) Facebook, Inc. and its affiliates.
3
- *
4
- * This source code is licensed under the MIT license found in the
5
- * LICENSE file in the root directory of this source tree.
6
- */
7
-
8
-import {
9
- HIRFunction,
10
- makeScopeId,
11
- ReactFunction,
12
- ReactiveScope,
13
- ScopeId,
14
-} from "./HIR";
15
-
16
-// import * as t from "@babel/types";
17
-
18
-/**
19
- * Converts a function in standard HIR form into a reactive function, breaking down
20
- * the function's body to determine a set of minimal scopes which define computation
21
- * of sub-expressions in the input. A separate phase can then reconstruct an HIR
22
- * function given the reactive function using various heuristics for codegen.
23
- *
24
- * ## algorithm
25
- * We want to find minimal sets of instructions which build up values. This
26
- * involves determining which values "construct together". Values that construct
27
- * together derive from:
28
- * * mutable accesses that may capture one value into another:
29
- * `x = y`, `foo(x, y)`
30
- * The intuition is that because a mutable reference can be captured, we have
31
- * to assume that further modifications of one value may affect the other and
32
- * vice versa, so their construction must be grouped together.
33
- *
34
- * * constructing independent values across the same control flow path:
35
- * `let x = ..., y = ...; if (cond) {x.a = ...; y.b = ...; }`
36
- * The intuition here is that we don't want to repeat control-flow constructs,
37
- * (esp loops) so values that are constructed across multiple basic blocks
38
- * get grouped together.
39
- *
40
- * Once we've grouped values that "construct together", we can then create one
41
- * scope per group. we iterate back over the CFG, and for each instruction/terminal
42
- * we figure out which group that belongs to, based on the set of values its creating,
43
- * and push it onto the appropriate group's CFG.
44
- *
45
- * as an extra optimization to avoid creating scopes for primitives, we can try to
46
- * infer that instructions that produce primitives don't get their own scope.
47
- * so in `tmp1 = a * b; tmp2 = tmp1 * 3; tmp3 = tmp2 > 10`, we can say that tmp1/2/3
48
- * are all primitives, so they should be grouped together even though they don't
49
- * mutate together, and even though there's no control flow.
50
- *
51
- * we'll also need use-def analysis (or similar) to avoid reassignment of variables
52
- * causing overly large grouping of scopes. we really care about the *values* that
53
- * are being constructed together, not the variables. so `foo(x, y)` conjoins x and y,
54
- * but if we later `x = {}; x.a = 5`, that later assignment and modification should *not*
55
- * conjoin with `y`. for now, we accept that these cases will be treated as conjoined and
56
- * grouped together.
57
- */
58
-export default function analyzeScopes(fn: HIRFunction): ReactFunction {
59
- // naive but trivially correct version
60
- const returnScopeId = makeScopeId(0);
61
- const scopes: Map<ScopeId, ReactiveScope> = new Map();
62
- scopes.set(returnScopeId, {
63
- inputs: new Set([...fn.params]),
64
- outputs: new Set(),
65
- instructions: fn.body,
66
- });
67
- return {
68
- loc: fn.loc,
69
- id: fn.id,
70
- params: fn.params,
71
- returnScope: returnScopeId,
72
- scopes,
73
- };
74
-}
75
-
76
-/**
77
- * ```javascript
78
- * function Component({items}) {
79
- * const renderedItems = [];
80
- * const seen = new Set();
81
- * for (const item of items) {
82
- * renderedItems.push(<div>{item}</div>);
83
- * seen.add(item);
84
- * }
85
- * return <Child items={renderedItems} seen={seen} />;
86
- * }
87
- * ```
88
- * Iterate over the IR in tree order (similar to codegen) - single pass with recursion.
89
- * Build a stack of control points and mutable values, associate mutable values with
90
- * control points that occur between each other. control points also naturally group
91
- * together, ie for a continue within a loop.
92
- *
93
- *
94
- * Ideas toward an algorithm:
95
- * - Track the lifetime for which each variable (value, really) is mutable.
96
- * - Values with overlapping mutable lifetimes are conjoined ("memoize together").
97
- * - Keep a stack of mutated values which we can walk. But *also* store block terminals in this stack.
98
- * When you walk back up the stack to find previous mutations of a value, add all the terminals
99
- * along the way until finding it as dependencies. eg set.union(mutValue, terminal).
100
- * Two values that mutate across the same control points will union with the same terminal,
101
- * and get conjoined.
102
- *
103
- * Eg in the following, `renderedItems` and `seen` have overlapping mutable lifetimes:
104
- *
105
- * ```javascript
106
- * function Component({items, maxItems}) {
107
- * const renderedItems = []; // new-mutable renderedItems
108
- * const seen = new Set(); // new-mutable seen
109
- * const max = Math.max(0, maxItems); // new-mutable max; read-frozen maxItems
110
- * for (const item of items) { // read-frozen items; control point
111
- * if (item == null || seen.has(item)) { // mutable seen; read-frozen item; control point
112
- * continue; // control point
113
- * }
114
- *
115
- * seen.add(item); // mutable seen; read-frozen item
116
- * // finding prev `mut seen` hops the above control points
117
- * renderedItems.push(<div>{item}</div>); // mutable renderedItems; read-frozen item
118
- * // finding prev `mut renderedItems` hops the above control points
119
- * if (renderedItems.length >= max) { // read-frozen max; read-mutable renderedItems
120
- * break;
121
- * }
122
- * }
123
- * const count = renderedItems.length; // read-frozen renderedItems
124
- * return <div><h1>{count} Items</h1>{renderedItems}</div>; // read-frozen renderedItems
125
- * }
126
- *
127
- * function Component({items, maxItems}) {
128
- * // scope 0 (inputs: maxItems, outputs: max)
129
- * const c_maxItems = ...;
130
- * let max = c_max_items ? Math.max(0, maxItems) : ...;
131
- * const c_max = ...;
132
- *
133
- * // scope 1 (inputs: items, max, ouputs: renderedItems, seen, count)
134
- * const c_items = ...;
135
- * let renderedItems;
136
- * let seen;
137
- * let count;
138
- * if (c_max || c_items) {
139
- * renderedItems = ....;
140
- * seen = ...;
141
- * for (const item of items) {
142
- * if (item == null || seen.has(item)) { // read-mutable seen; read-frozen item; control point
143
- * continue; // control point
144
- * }
145
- * seen.add(item); // mut-mutable seen; read-frozen item
146
- * renderedItems.push(<div>{item}</div>); // mut-mutable renderedItems; read-frozen item
147
- * if (renderedItems.length >= max) { // read-frozen max; read-mutable renderedItems
148
- * break;
149
- * }
150
- * }
151
- * count = renderedItems.length;
152
- * } else {
153
- * // populate from cache
154
- * }
155
- *
156
- * // scope 2 (inputs: count, ouputs: div)
157
- * const c_count = ...;
158
- * let h1;
159
- * if (c_count) {
160
- * h1 = <h1>{count}</h1>
161
- * } // else from cache
162
- *
163
- * // scope 3 (inputs: h1, renderedItems, outputs: outer div)
164
- * const c_h1 = ...;
165
- * const c_renderedItems = ...;
166
- * let ret;
167
- * if (c_h1 || c_renderedItems) {
168
- * ret = <div>{h1}{renderedItems}</div>; // read-frozen renderedItems
169
- * } // else from cache
170
- * return ret;
171
- * }
172
- * ```
173
- */
174
-
175
-function analyze(fn: HIRFunction): ReactFunction {
176
- const returnScopeId = makeScopeId(0);
177
- const scopes: Map<ScopeId, ReactiveScope> = new Map();
178
-
179
- return {
180
- loc: fn.loc,
181
- id: fn.id,
182
- params: fn.params,
183
- returnScope: returnScopeId,
184
- scopes,
185
- };
186
-}