@samitouri / QOS-React-2 / commits / 46024acb82

MergeOverlappingReactiveScopes (rewritten against ReactiveFunction)

See the background in #982. This PR reimplements part of InferReactiveScopes, merging overlapping reactive scopes, but against ReactiveFunction instead of the HIR.

Joe Savona committed Jan 11, 2023 at 14:04 UTC 46024acb8203aacbaafe20404205fa16e259804e
2 files changed +211 -2
compiler/forget/src/ReactiveScopes/AlignReactiveScopesToBlockScopes.ts
+2
@@ -81,6 +81,7 @@ function visitBlock(context: Context, block: ReactiveBlock): void {
81 if (id !== null) {
82 context.visitId(id);
83 }
84 + // TODO: visit terminal operands!
85 eachTerminalBlock(
86 stmt.terminal,
87 (block) => {
@@ -120,6 +121,7 @@ function visitValueBlock(
121 }
122 }
123 }
124 + // TODO: visit the block value!!
125 }
126
127 type PendingReactiveScope = { active: boolean; scope: ReactiveScope };
compiler/forget/src/ReactiveScopes/MergeOverlappingReactiveScopes.ts
+209 -2
@@ -5,7 +5,22 @@
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 -import { ReactiveFunction } from "../HIR";
8 +import {
9 + InstructionId,
10 + InstructionValue,
11 + makeInstructionId,
12 + ReactiveBlock,
13 + ReactiveFunction,
14 + ReactiveInstruction,
15 + ReactiveScope,
16 + ReactiveValueBlock,
17 + ScopeId,
18 +} from "../HIR";
19 +import { eachInstructionValueOperand } from "../HIR/visitors";
20 +import { invariant } from "../Utils/CompilerError";
21 +import DisjointSet from "../Utils/DisjointSet";
22 +import { retainWhere } from "../Utils/utils";
23 +import { eachTerminalBlock, eachTerminalOperand } from "./visitors";
24
25 /**
26 * Note: this is the 3rd of 3 passes that determine how to break a function into discrete
@@ -82,4 +97,196 @@ import { ReactiveFunction } from "../HIR";
97 * Then we would see that 'x' is active, but that it is shadowed. The two scopes would have
98 * to be merged.
99 */
85 -export function mergeOverlappingReactiveScopes(fn: ReactiveFunction): void {}
100 +export function mergeOverlappingReactiveScopes(fn: ReactiveFunction): void {
101 + const context = new Context();
102 + context.enter(() => {
103 + visitBlock(context, fn.body);
104 + });
105 +}
106 +
107 +function visitBlock(context: Context, block: ReactiveBlock): void {
108 + for (const stmt of block) {
109 + switch (stmt.kind) {
110 + case "instruction": {
111 + visitValue(context, stmt.instruction.id, stmt.instruction.value);
112 + visitInstruction(context, stmt.instruction);
113 + break;
114 + }
115 + case "terminal": {
116 + const id = stmt.terminal.id;
117 + if (id !== null) {
118 + context.visitId(id);
119 + eachTerminalOperand(stmt.terminal, (operand) => {
120 + visitValue(context, id, operand);
121 + });
122 + }
123 + eachTerminalBlock(
124 + stmt.terminal,
125 + (block) => {
126 + context.enter(() => {
127 + visitBlock(context, block);
128 + });
129 + },
130 + (valueBlock) => visitValueBlock(context, valueBlock)
131 + );
132 + break;
133 + }
134 + case "scope": {
135 + invariant(false, "Expected scopes to be constructed later");
136 + }
137 + }
138 + }
139 +}
140 +
141 +function visitValueBlock(context: Context, block: ReactiveValueBlock): void {
142 + visitBlock(context, block.instructions);
143 + // TODO: visit the value block value!
144 +}
145 +
146 +function visitInstruction(
147 + context: Context,
148 + instruction: ReactiveInstruction
149 +): void {
150 + context.visitId(instruction.id);
151 + invariant(
152 + instruction.lvalue !== null,
153 + "Expected lvalues to not be null when assigning scopes. " +
154 + "Pruning lvalues too early can result in missing scope information."
155 + );
156 + if (
157 + instruction.lvalue.place.identifier.scope !== null &&
158 + instruction.id >= instruction.lvalue.place.identifier.scope.range.start &&
159 + instruction.id < instruction.lvalue.place.identifier.scope.range.end
160 + ) {
161 + context.visitScope(instruction.lvalue.place.identifier.scope);
162 + }
163 +}
164 +
165 +function visitValue(
166 + context: Context,
167 + id: InstructionId,
168 + value: InstructionValue
169 +): void {
170 + context.visitId(id);
171 + for (const operand of eachInstructionValueOperand(value)) {
172 + if (
173 + operand.identifier.scope !== null &&
174 + id >= operand.identifier.scope.range.start &&
175 + id < operand.identifier.scope.range.end
176 + ) {
177 + context.visitScope(operand.identifier.scope);
178 + }
179 + }
180 +}
181 +
182 +class BlockScope {
183 + seen: Set<ScopeId> = new Set();
184 + scopes: Array<ShadowableReactiveScope> = [];
185 +}
186 +
187 +type ShadowableReactiveScope = {
188 + scope: ReactiveScope;
189 + shadowedBy: ReactiveScope | null;
190 +};
191 +
192 +class Context {
193 + scopes: Array<BlockScope> = [];
194 + seenScopes: Set<ScopeId> = new Set();
195 + joinedScopes: DisjointSet<ReactiveScope> = new DisjointSet();
196 +
197 + visitId(id: InstructionId): void {
198 + const currentBlock = this.scopes[this.scopes.length - 1]!;
199 + retainWhere(currentBlock.scopes, (pending) => {
200 + if (pending.scope.range.end > id) {
201 + return true;
202 + } else {
203 + currentBlock.seen.delete(pending.scope.id);
204 + return false;
205 + }
206 + });
207 + }
208 +
209 + visitScope(scope: ReactiveScope): void {
210 + const currentBlock = this.scopes[this.scopes.length - 1]!;
211 + // Fast-path for the first time we see a new scope
212 + if (!this.seenScopes.has(scope.id)) {
213 + this.seenScopes.add(scope.id);
214 + currentBlock.seen.add(scope.id);
215 + currentBlock.scopes.push({ shadowedBy: null, scope });
216 + return;
217 + }
218 + // Scope has already been seen, find it in the current block or a parent
219 + let index = this.scopes.length - 1;
220 + let nextBlock = currentBlock;
221 + while (!nextBlock.seen.has(scope.id)) {
222 + // scopes that cross control-flow boundaries are merged with overlapping
223 + // scopes
224 + this.joinedScopes.union([scope, ...nextBlock.scopes.map((s) => s.scope)]);
225 + index--;
226 + if (index < 0) {
227 + // TODO: handle reassignments in multiple branches. these create new identifiers that
228 + // add an entry to this.seenScopes but which are then removed when their blocks exit.
229 + // this is also wrong for codegen, different versions of an identifier could be cached
230 + // differently and so a reassigned version of a variable needs a separate declaration.
231 + // console.log(`scope ${scope.id} not found`);
232 +
233 + // for (let i = this.scopes.length - 1; i > index; i--) {
234 + // const s = this.scopes[i];
235 + // console.log(
236 + // JSON.stringify(
237 + // {
238 + // seen: Array.from(s.seen),
239 + // scopes: s.scopes,
240 + // },
241 + // null,
242 + // 2
243 + // )
244 + // );
245 + // }
246 + currentBlock.seen.add(scope.id);
247 + currentBlock.scopes.push({ shadowedBy: null, scope });
248 + return;
249 + }
250 + nextBlock = this.scopes[index]!;
251 + }
252 +
253 + // Handle interleaving within a given block scope
254 + let found = false;
255 + for (let i = 0; i < nextBlock.scopes.length; i++) {
256 + const current = nextBlock.scopes[i]!;
257 + if (current.scope.id === scope.id) {
258 + found = true;
259 + if (current.shadowedBy !== null) {
260 + this.joinedScopes.union([current.shadowedBy, current.scope]);
261 + }
262 + } else if (found && current.shadowedBy === null) {
263 + // `scope` is shadowing `current`, but we don't know they are interleaved yet
264 + current.shadowedBy = scope;
265 + }
266 + }
267 + if (!currentBlock.seen.has(scope.id)) {
268 + currentBlock.seen.add(scope.id);
269 + currentBlock.scopes.push({ shadowedBy: null, scope });
270 + }
271 + }
272 +
273 + enter(fn: () => void): void {
274 + this.scopes.push(new BlockScope());
275 + fn();
276 + this.scopes.pop();
277 + if (this.scopes.length === 0) {
278 + this.joinedScopes.forEach((scope, groupScope) => {
279 + if (scope !== groupScope) {
280 + groupScope.range.start = makeInstructionId(
281 + Math.min(groupScope.range.start, scope.range.start)
282 + );
283 + groupScope.range.end = makeInstructionId(
284 + Math.max(groupScope.range.end, scope.range.end)
285 + );
286 + scope.range = groupScope.range;
287 + scope.id = groupScope.id;
288 + }
289 + });
290 + }
291 + }
292 +}