11
InstructionId,
12
InstructionKind,
13
InstructionValue,
14
+ LValue,
15
makeInstructionId,
16
Place,
17
ReactiveBlock,
20
ReactiveValueBlock,
21
} from "../HIR/HIR";
22
import { eachInstructionValueOperand } from "../HIR/visitors";
23
+import { invariant } from "../Utils/CompilerError";
24
import { assertExhaustive } from "../Utils/utils";
25
26
/**
30
* their direct dependencies and those of their child scopes.
31
*/
32
export function propagateScopeDependencies(fn: ReactiveFunction): void {
31
- const dependencies: Set<Place> = new Set();
32
- const declarations: DeclMap = new Map();
33
+ const context = new Context();
34
if (fn.id !== null) {
34
- declarations.set(fn.id, { kind: DeclKind.Const, id: makeInstructionId(0) });
35
+ context.declare(fn.id, { kind: DeclKind.Const, id: makeInstructionId(0) });
36
}
37
for (const param of fn.params) {
37
- declarations.set(param.identifier, {
38
+ context.declare(param.identifier, {
39
kind: DeclKind.Dynamic,
40
id: makeInstructionId(0),
41
});
42
}
42
- visit(fn.body, dependencies, declarations, []);
43
+ visit(context, fn.body);
44
}
45
46
enum DeclKind {
48
Dynamic = "Dynamic",
49
}
50
50
-type DeclMap = Map<Identifier, { kind: DeclKind; id: InstructionId }>;
51
+type DeclMap = Map<Identifier, Decl>;
52
+type Decl = { kind: DeclKind; id: InstructionId };
53
54
type Scopes = Array<ReactiveScope>;
55
54
-function visit(
55
- block: ReactiveBlock,
56
- dependencies: Set<Place>,
57
- declarations: DeclMap,
58
- scopes: Scopes
59
-): void {
56
+class Context {
57
+ #declarations: DeclMap = new Map();
58
+ #dependencies: Set<Place> = new Set();
59
+ #properties: Map<Identifier, Place> = new Map();
60
+ #scopes: Scopes = [];
61
+
62
+ enter(scope: ReactiveScope, fn: () => void): Set<Place> {
63
+ const previousDependencies = this.#dependencies;
64
+ const scopedDependencies = new Set<Place>();
65
+ this.#dependencies = scopedDependencies;
66
+ this.#scopes.push(scope);
67
+ fn();
68
+ this.#scopes.pop();
69
+ this.#dependencies = previousDependencies;
70
+ return scopedDependencies;
71
+ }
72
+
73
+ declare(identifier: Identifier, decl: Decl): void {
74
+ this.#declarations.set(identifier, decl);
75
+ }
76
+
77
+ declareProperty(lvalue: Place, object: Place, property: string): void {
78
+ invariant(
79
+ lvalue.memberPath === null,
80
+ "Expected property loads to be stored to a temporary (no member path)"
81
+ );
82
+ invariant(
83
+ object.memberPath === null,
84
+ "Expected operands to have null memberPath"
85
+ );
86
+ const objectPlace = this.#properties.get(object.identifier);
87
+ let place: Place;
88
+ if (objectPlace === undefined) {
89
+ place = { ...object, memberPath: [property] };
90
+ } else {
91
+ place = {
92
+ ...objectPlace,
93
+ memberPath: [...(objectPlace.memberPath ?? []), property],
94
+ };
95
+ }
96
+ this.#properties.set(lvalue.identifier, place);
97
+ }
98
+
99
+ #isScopeActive(scope: ReactiveScope): boolean {
100
+ return this.#scopes.indexOf(scope) !== -1;
101
+ }
102
+
103
+ get #currentScope(): ReactiveScope {
104
+ return this.#scopes.at(-1)!;
105
+ }
106
+
107
+ visitOperand(operand: Place): void {
108
+ let maybeDependency: Place;
109
+ if (operand.memberPath !== null) {
110
+ // Operands may have memberPaths when propagating depenencies of an inner scope upward
111
+ // In this case we use the dependency as-is
112
+ maybeDependency = operand;
113
+ } else {
114
+ // Otherwise if this operand is a temporary created for a property load, resolve it to
115
+ // the expanded Place. Fall back to using the operand as-is.
116
+ maybeDependency = this.#properties.get(operand.identifier) ?? operand;
117
+ }
118
+
119
+ const decl = this.#declarations.get(maybeDependency.identifier);
120
+
121
+ // Any value used after its defining scope has concluded must be added as an
122
+ // output of its defining scope. Regardless of whether its a const or not,
123
+ // some later code needs access to the value.
124
+ if (decl !== undefined) {
125
+ const operandScope = maybeDependency.identifier.scope;
126
+ if (operandScope !== null && !this.#isScopeActive(operandScope)) {
127
+ operandScope.outputs.add(maybeDependency.identifier);
128
+ }
129
+ }
130
+
131
+ // If this operand is used in a scope, has a dynamic value, and was defined
132
+ // before this scope, then its a dependency of the scope.
133
+ const currentScope = this.#currentScope;
134
+ if (
135
+ decl !== undefined &&
136
+ decl.kind !== DeclKind.Const &&
137
+ currentScope !== undefined &&
138
+ decl.id < currentScope.range.start
139
+ ) {
140
+ // Check if there is an existing dependency that describes this operand
141
+ for (const dep of this.#dependencies) {
142
+ // not the same identifier
143
+ if (dep.identifier !== maybeDependency.identifier) {
144
+ continue;
145
+ }
146
+ const depPath = dep.memberPath;
147
+ // existing dep covers all paths
148
+ if (depPath === null) {
149
+ return;
150
+ }
151
+ const operandPath = maybeDependency.memberPath;
152
+ // existing dep is for a path, this operand covers all paths so swap them
153
+ if (operandPath === null) {
154
+ this.#dependencies.delete(dep);
155
+ this.#dependencies.add(maybeDependency);
156
+ return;
157
+ }
158
+ // both the operand and dep have paths, determine if the existing path
159
+ // is a subset of the new path
160
+ let commonPathIndex = 0;
161
+ while (
162
+ commonPathIndex < operandPath.length &&
163
+ commonPathIndex < depPath.length &&
164
+ operandPath[commonPathIndex] === depPath[commonPathIndex]
165
+ ) {
166
+ commonPathIndex++;
167
+ }
168
+ if (commonPathIndex === depPath.length) {
169
+ return;
170
+ }
171
+ }
172
+ this.#dependencies.add(maybeDependency);
173
+ }
174
+ }
175
+}
176
+
177
+function visit(context: Context, block: ReactiveBlock): void {
178
for (const item of block) {
179
switch (item.kind) {
180
case "scope": {
63
- const scopeDependencies: Set<Place> = new Set();
64
- // TODO: it would be sufficient to use a single mapping of declarations
65
- const scopeDeclarations: DeclMap = new Map(declarations);
66
- scopes.push(item.scope);
67
- visit(item.instructions, scopeDependencies, scopeDeclarations, scopes);
68
- scopes.pop();
181
+ const scopeDependencies = context.enter(item.scope, () => {
182
+ visit(context, item.instructions);
183
+ });
184
item.scope.dependencies = scopeDependencies;
185
for (const dep of scopeDependencies) {
186
// propagate dependencies upward using the same rules as
187
// normal dependency collection. child scopes may have dependencies
188
// on values created within the outer scope, which necessarily cannot
189
// be dependencies of the outer scope
75
- visitOperand(dep, dependencies, declarations, scopes);
76
- }
77
- for (const [ident, kind] of scopeDeclarations) {
78
- declarations.set(ident, kind);
190
+ context.visitOperand(dep);
191
}
192
break;
193
}
194
case "instruction": {
83
- visitInstruction(item.instruction, dependencies, declarations, scopes);
195
+ visitInstruction(context, item.instruction);
196
break;
197
}
198
case "terminal": {
204
}
205
case "return": {
206
if (terminal.value !== null) {
95
- visitOperand(terminal.value, dependencies, declarations, scopes);
207
+ context.visitOperand(terminal.value);
208
}
209
break;
210
}
211
case "throw": {
100
- visitOperand(terminal.value, dependencies, declarations, scopes);
212
+ context.visitOperand(terminal.value);
213
break;
214
}
215
case "for": {
104
- visitValueBlock(terminal.init, dependencies, declarations, scopes);
105
- visitValueBlock(terminal.test, dependencies, declarations, scopes);
106
- visitValueBlock(
107
- terminal.update,
108
- dependencies,
109
- declarations,
110
- scopes
111
- );
112
- visit(terminal.loop, dependencies, declarations, scopes);
216
+ visitValueBlock(context, terminal.init);
217
+ visitValueBlock(context, terminal.test);
218
+ visitValueBlock(context, terminal.update);
219
+ visit(context, terminal.loop);
220
break;
221
}
222
case "while": {
116
- visitValueBlock(terminal.test, dependencies, declarations, scopes);
117
- visit(terminal.loop, dependencies, declarations, scopes);
223
+ visitValueBlock(context, terminal.test);
224
+ visit(context, terminal.loop);
225
break;
226
}
227
case "if": {
121
- visitOperand(terminal.test, dependencies, declarations, scopes);
122
- visit(terminal.consequent, dependencies, declarations, scopes);
228
+ context.visitOperand(terminal.test);
229
+ visit(context, terminal.consequent);
230
if (terminal.alternate !== null) {
124
- visit(terminal.alternate, dependencies, declarations, scopes);
231
+ visit(context, terminal.alternate);
232
}
233
break;
234
}
235
case "switch": {
129
- visitOperand(terminal.test, dependencies, declarations, scopes);
236
+ context.visitOperand(terminal.test);
237
for (const case_ of terminal.cases) {
238
if (case_.block !== undefined) {
132
- visit(case_.block, dependencies, declarations, scopes);
239
+ visit(context, case_.block);
240
}
241
}
242
break;
257
}
258
}
259
153
-function visitValueBlock(
154
- block: ReactiveValueBlock,
155
- dependencies: Set<Place>,
156
- declarations: DeclMap,
157
- scopes: Scopes
158
-): void {
260
+function visitValueBlock(context: Context, block: ReactiveValueBlock): void {
261
for (const initItem of block.instructions) {
262
if (initItem.kind === "instruction") {
161
- visitInstruction(
162
- initItem.instruction,
163
- dependencies,
164
- declarations,
165
- scopes
166
- );
263
+ visitInstruction(context, initItem.instruction);
264
}
265
}
266
if (block.value !== null) {
170
- visitInstructionValue(block.value, dependencies, declarations, scopes);
171
- }
172
-}
173
-
174
-function visitOperand(
175
- maybeDependency: Place,
176
- dependencies: Set<Place>,
177
- declarations: DeclMap,
178
- scopes: Scopes
179
-): void {
180
- const decl = declarations.get(maybeDependency.identifier);
181
-
182
- // Any value used after its defining scope has concluded must be added as an
183
- // output of its defining scope. Regardless of whether its a const or not,
184
- // some later code needs access to the value.
185
- if (decl !== undefined) {
186
- const operandScope = maybeDependency.identifier.scope;
187
- if (operandScope !== null && scopes.indexOf(operandScope) === -1) {
188
- operandScope.outputs.add(maybeDependency.identifier);
189
- }
190
- }
191
-
192
- // If this operand is used in a scope, has a dynamic value, and was defined
193
- // before this scope, then its a dependency of the scope.
194
- const currentScope = scopes.at(-1);
195
- if (
196
- decl !== undefined &&
197
- decl.kind !== DeclKind.Const &&
198
- currentScope !== undefined &&
199
- decl.id < currentScope.range.start
200
- ) {
201
- // Check if there is an existing dependency that describes this operand
202
- for (const dep of dependencies) {
203
- // not the same identifier
204
- if (dep.identifier !== maybeDependency.identifier) {
205
- continue;
206
- }
207
- const depPath = dep.memberPath;
208
- // existing dep covers all paths
209
- if (depPath === null) {
210
- return;
211
- }
212
- const operandPath = maybeDependency.memberPath;
213
- // existing dep is for a path, this operand covers all paths so swap them
214
- if (operandPath === null) {
215
- dependencies.delete(dep);
216
- dependencies.add(maybeDependency);
217
- return;
218
- }
219
- // both the operand and dep have paths, determine if the existing path
220
- // is a subset of the new path
221
- let commonPathIndex = 0;
222
- while (
223
- commonPathIndex < operandPath.length &&
224
- commonPathIndex < depPath.length &&
225
- operandPath[commonPathIndex] === depPath[commonPathIndex]
226
- ) {
227
- commonPathIndex++;
228
- }
229
- if (commonPathIndex === depPath.length) {
230
- return;
231
- }
232
- }
233
- dependencies.add(maybeDependency);
267
+ visitInstructionValue(context, block.value, null);
268
}
269
}
270
271
function visitInstructionValue(
272
+ context: Context,
273
value: InstructionValue,
239
- dependencies: Set<Place>,
240
- declarations: DeclMap,
241
- scopes: Scopes
274
+ lvalue: LValue | null
275
): void {
276
for (const operand of eachInstructionValueOperand(value)) {
277
// check for method invocation, we want to depend on the callee, not the method
284
...operand,
285
memberPath: operand.memberPath.slice(0, -1),
286
};
254
- visitOperand(callee, dependencies, declarations, scopes);
287
+ context.visitOperand(callee);
288
+ } else if (value.kind === "PropertyLoad" && lvalue !== null) {
289
+ context.declareProperty(lvalue.place, value.object, value.property);
290
} else {
256
- visitOperand(operand, dependencies, declarations, scopes);
291
+ context.visitOperand(operand);
292
}
293
}
294
}
295
261
-function visitInstruction(
262
- instr: Instruction,
263
- dependencies: Set<Place>,
264
- declarations: DeclMap,
265
- scopes: Scopes
266
-): void {
267
- visitInstructionValue(instr.value, dependencies, declarations, scopes);
296
+function visitInstruction(context: Context, instr: Instruction): void {
297
const { lvalue } = instr;
298
+ visitInstructionValue(context, instr.value, lvalue);
299
if (
300
lvalue !== null &&
301
lvalue.kind !== InstructionKind.Reassign &&
305
// TODO: only assign Const if the value is never reassigned
306
const kind =
307
range.end === range.start + 1 ? valueKind(instr.value) : DeclKind.Dynamic;
278
- declarations.set(lvalue.place.identifier, { kind, id: instr.id });
308
+ context.declare(lvalue.place.identifier, {
309
+ kind,
310
+ id: lvalue.place.identifier.mutableRange.start,
311
+ });
312
}
313
}
314
319
case "Primitive": {
320
return DeclKind.Const;
321
}
322
+ case "PropertyLoad":
323
case "Identifier":
324
case "ArrayExpression":
325
case "CallExpression":