@samitouri / QOS-React-2 / commits / 796e14f38e

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