6
*/
7
8
import { assertExhaustive } from "../Common/utils";
9
-import { Place, ReactiveBasicBlock, ReactiveFunction } from "./HIR";
9
+import {
10
+ Identifier,
11
+ Instruction,
12
+ InstructionId,
13
+ InstructionKind,
14
+ InstructionValue,
15
+ makeInstructionId,
16
+ Place,
17
+ ReactiveBasicBlock,
18
+ ReactiveFunction,
19
+ ReactiveValueBlock,
20
+} from "./HIR";
21
+import { eachInstructionValueOperand } from "./visitors";
22
23
/**
12
- * Propagates the dependencies of each scope to its parent scope(s).
24
+ * Infers the dependencies of each scope to include variables whose values
25
+ * are non-stable and created prior to the start of the scope. Also propagates
26
+ * dependencies upwards, so that parent scope dependencies are the union of
27
+ * their direct dependencies and those of their child scopes.
28
*/
29
export function propagateScopeDependencies(fn: ReactiveFunction): void {
30
const dependencies: Set<Place> = new Set();
16
- visit(fn.body, dependencies);
31
+ const declarations: DeclMap = new Map();
32
+ if (fn.id !== null) {
33
+ declarations.set(fn.id, { kind: DeclKind.Const, id: makeInstructionId(0) });
34
+ }
35
+ for (const param of fn.params) {
36
+ declarations.set(param.identifier, {
37
+ kind: DeclKind.Dynamic,
38
+ id: makeInstructionId(0),
39
+ });
40
+ }
41
+ visit(fn.body, dependencies, declarations, null);
42
+}
43
+
44
+enum DeclKind {
45
+ Const = "Const",
46
+ Dynamic = "Dynamic",
47
}
48
19
-function visit(block: ReactiveBasicBlock, dependencies: Set<Place>): void {
49
+type DeclMap = Map<Identifier, { kind: DeclKind; id: InstructionId }>;
50
+
51
+function visit(
52
+ block: ReactiveBasicBlock,
53
+ dependencies: Set<Place>,
54
+ declarations: DeclMap,
55
+ scopeStart: InstructionId | null
56
+): void {
57
for (const item of block) {
58
switch (item.kind) {
59
case "block": {
23
- visit(item.instructions, item.dependencies);
24
- for (const dep of item.dependencies) {
25
- dependencies.add(dep);
60
+ const scopeDependencies: Set<Place> = new Set();
61
+ // TODO: it would be sufficient to use a single mapping of declarations
62
+ const scopeDeclarations: DeclMap = new Map(declarations);
63
+ visit(
64
+ item.instructions,
65
+ scopeDependencies,
66
+ scopeDeclarations,
67
+ item.range.start
68
+ );
69
+ item.dependencies = scopeDependencies;
70
+ for (const dep of scopeDependencies) {
71
+ // propagate dependencies upward using the same rules as
72
+ // normal dependency collection. child scopes may have dependencies
73
+ // on values created within the outer scope, which necessarily cannot
74
+ // be dependencies of the outer scope
75
+ visitOperand(dep, dependencies, declarations, scopeStart);
76
+ }
77
+ for (const [ident, kind] of scopeDeclarations) {
78
+ declarations.set(ident, kind);
79
}
80
break;
81
}
82
case "instruction": {
83
+ visitInstruction(
84
+ item.instruction,
85
+ dependencies,
86
+ declarations,
87
+ scopeStart
88
+ );
89
break;
90
}
91
case "terminal": {
92
const terminal = item.terminal;
93
switch (terminal.kind) {
94
case "break":
36
- case "continue":
37
- case "return":
95
+ case "continue": {
96
+ break;
97
+ }
98
+ case "return": {
99
+ if (terminal.value !== null) {
100
+ visitOperand(
101
+ terminal.value,
102
+ dependencies,
103
+ declarations,
104
+ scopeStart
105
+ );
106
+ }
107
+ break;
108
+ }
109
case "throw": {
110
+ visitOperand(
111
+ terminal.value,
112
+ dependencies,
113
+ declarations,
114
+ scopeStart
115
+ );
116
break;
117
}
118
case "for": {
42
- visit(terminal.loop, dependencies);
119
+ visitValueBlock(
120
+ terminal.init,
121
+ dependencies,
122
+ declarations,
123
+ scopeStart
124
+ );
125
+ visitValueBlock(
126
+ terminal.test,
127
+ dependencies,
128
+ declarations,
129
+ scopeStart
130
+ );
131
+ visitValueBlock(
132
+ terminal.update,
133
+ dependencies,
134
+ declarations,
135
+ scopeStart
136
+ );
137
+ visit(terminal.loop, dependencies, declarations, scopeStart);
138
break;
139
}
140
case "while": {
46
- visit(terminal.loop, dependencies);
141
+ visitValueBlock(
142
+ terminal.test,
143
+ dependencies,
144
+ declarations,
145
+ scopeStart
146
+ );
147
+ visit(terminal.loop, dependencies, declarations, scopeStart);
148
break;
149
}
150
case "if": {
50
- visit(terminal.consequent, dependencies);
151
+ visitOperand(terminal.test, dependencies, declarations, scopeStart);
152
+ visit(terminal.consequent, dependencies, declarations, scopeStart);
153
if (terminal.alternate !== null) {
52
- visit(terminal.alternate, dependencies);
154
+ visit(terminal.alternate, dependencies, declarations, scopeStart);
155
}
156
break;
157
}
158
case "switch": {
159
+ visitOperand(terminal.test, dependencies, declarations, scopeStart);
160
for (const case_ of terminal.cases) {
161
if (case_.block !== undefined) {
59
- visit(case_.block, dependencies);
162
+ visit(case_.block, dependencies, declarations, scopeStart);
163
}
164
}
165
break;
179
}
180
}
181
}
182
+
183
+function visitValueBlock(
184
+ block: ReactiveValueBlock,
185
+ dependencies: Set<Place>,
186
+ declarations: DeclMap,
187
+ scopeStart: InstructionId | null
188
+): void {
189
+ for (const initItem of block.instructions) {
190
+ if (initItem.kind === "instruction") {
191
+ visitInstruction(
192
+ initItem.instruction,
193
+ dependencies,
194
+ declarations,
195
+ scopeStart
196
+ );
197
+ }
198
+ }
199
+ if (block.value !== null) {
200
+ visitInstructionValue(block.value, dependencies, declarations, scopeStart);
201
+ }
202
+}
203
+
204
+function visitOperand(
205
+ operand: Place,
206
+ dependencies: Set<Place>,
207
+ declarations: DeclMap,
208
+ scopeStart: InstructionId | null
209
+): void {
210
+ const decl = declarations.get(operand.identifier);
211
+ if (decl === undefined) {
212
+ // Probably a global, ignore for now
213
+ // TODO @josephsavona: improve handling of globals
214
+ return;
215
+ } else if (decl.kind === DeclKind.Const) {
216
+ // constant, dont need to add a dep
217
+ return;
218
+ } else if (scopeStart !== null && decl.id < scopeStart) {
219
+ // Check if there is an existing dependency that describes this operand
220
+ for (const dep of dependencies) {
221
+ // not the same identifier
222
+ if (dep.identifier !== operand.identifier) {
223
+ continue;
224
+ }
225
+ const depPath = dep.memberPath;
226
+ // existing dep covers all paths
227
+ if (depPath === null) {
228
+ return;
229
+ }
230
+ const operandPath = operand.memberPath;
231
+ // existing dep is for a path, this operand covers all paths so swap them
232
+ if (operandPath === null) {
233
+ dependencies.delete(dep);
234
+ dependencies.add(operand);
235
+ return;
236
+ }
237
+ // both the operand and dep have paths, determine if the existing path
238
+ // is a subset of the new path
239
+ let commonPathIndex = 0;
240
+ while (
241
+ commonPathIndex < operandPath.length &&
242
+ commonPathIndex < depPath.length &&
243
+ operandPath[commonPathIndex] === depPath[commonPathIndex]
244
+ ) {
245
+ commonPathIndex++;
246
+ }
247
+ if (commonPathIndex === depPath.length) {
248
+ return;
249
+ }
250
+ }
251
+ dependencies.add(operand);
252
+ }
253
+}
254
+
255
+function visitInstructionValue(
256
+ value: InstructionValue,
257
+ dependencies: Set<Place>,
258
+ declarations: DeclMap,
259
+ scopeStart: InstructionId | null
260
+): void {
261
+ for (const operand of eachInstructionValueOperand(value)) {
262
+ // check for method invocation, we want to depend on the callee, not the method
263
+ if (
264
+ value.kind === "CallExpression" &&
265
+ operand === value.callee &&
266
+ operand.memberPath !== null
267
+ ) {
268
+ const callee = {
269
+ ...operand,
270
+ memberPath: operand.memberPath.slice(0, -1),
271
+ };
272
+ visitOperand(callee, dependencies, declarations, scopeStart);
273
+ } else {
274
+ visitOperand(operand, dependencies, declarations, scopeStart);
275
+ }
276
+ }
277
+}
278
+
279
+function visitInstruction(
280
+ instr: Instruction,
281
+ dependencies: Set<Place>,
282
+ declarations: DeclMap,
283
+ scopeStart: InstructionId | null
284
+): void {
285
+ visitInstructionValue(instr.value, dependencies, declarations, scopeStart);
286
+ const { lvalue } = instr;
287
+ if (
288
+ lvalue !== null &&
289
+ lvalue.kind !== InstructionKind.Reassign &&
290
+ lvalue.place.memberPath === null
291
+ ) {
292
+ const kind = valueKind(instr.value);
293
+ declarations.set(lvalue.place.identifier, { kind, id: instr.id });
294
+ }
295
+}
296
+
297
+function valueKind(value: InstructionValue): DeclKind {
298
+ switch (value.kind) {
299
+ case "BinaryExpression":
300
+ case "JSXText":
301
+ case "Primitive": {
302
+ return DeclKind.Const;
303
+ }
304
+ case "Identifier":
305
+ case "ArrayExpression":
306
+ case "CallExpression":
307
+ case "JsxExpression":
308
+ case "JsxFragment":
309
+ case "NewExpression":
310
+ case "ObjectExpression":
311
+ case "OtherStatement":
312
+ case "UnaryExpression": {
313
+ return DeclKind.Dynamic;
314
+ }
315
+ default: {
316
+ assertExhaustive(value, `Unexpected value kind '${(value as any).kind}'`);
317
+ }
318
+ }
319
+}