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