11
Place,
12
ReactiveBlock,
13
ReactiveFunction,
14
+ ReactiveInstruction,
15
ReactiveScope,
16
ReactiveScopeBlock,
17
ReactiveScopeDependency,
162
} else {
163
if (
164
currentScope !== null &&
164
- canMergeScopes(currentScope.scope.scope, instr.scope) &&
165
+ canMergeScopes(currentScope.scope, instr) &&
166
// If there are intermediate instructions, we can only merge the scopes
167
// if those intermediate instructions are all used by the second scope.
168
// if not, merging them would make those values unavailable to subsequent
244
return true;
245
}
246
246
-function canMergeScopes(a: ReactiveScope, b: ReactiveScope): boolean {
247
+function canMergeScopes(a: ReactiveScopeBlock, b: ReactiveScopeBlock): boolean {
248
// Don't merge scopes with reassignments
248
- if (a.reassignments.size !== 0 || b.reassignments.size !== 0) {
249
+ if (a.scope.reassignments.size !== 0 || b.scope.reassignments.size !== 0) {
250
return false;
251
}
251
- if (areEqualDependencies(a.dependencies, b.dependencies)) {
252
+ // Merge scopes whose dependencies are identical
253
+ if (areEqualDependencies(a.scope.dependencies, b.scope.dependencies)) {
254
return true;
255
}
256
+ // Merge scopes where the outputs of the previous scope are the inputs
257
+ // of the subsequent scope. Note that the output of a scope is not
258
+ // guaranteed to change when its inputs change, for example `foo(x)`
259
+ // may not change when `x` changes, for example `foo(x) { return x < 10}`
260
+ // will not change as x changes from 0 -> 1.
261
+ // Therefore we check that the outputs of the previous scope are of a type
262
+ // that is guaranteed to invalidate with its inputs, and only merge in this case.
263
if (
264
areEqualDependencies(
265
new Set(
257
- [...a.declarations.values()].map((declaration) => ({
266
+ [...a.scope.declarations.values()].map((declaration) => ({
267
identifier: declaration.identifier,
268
path: [],
269
}))
270
),
262
- b.dependencies
263
- )
271
+ b.scope.dependencies
272
+ ) &&
273
+ scopeAlwaysInvalidatesOnDependencyChanges(a)
274
) {
275
return true;
276
}
305
function areEqualPaths(a: Array<string>, b: Array<string>): boolean {
306
return a.length === b.length && a.every((item, ix) => item === b[ix]);
307
}
308
+
309
+function scopeAlwaysInvalidatesOnDependencyChanges(
310
+ scope: ReactiveScopeBlock
311
+): boolean {
312
+ const visitor = new DeclarationTypeVisitor(scope.scope);
313
+ visitor.visitScope(scope, undefined);
314
+ return visitor.alwaysInvalidatesOnInputChange;
315
+}
316
+
317
+class DeclarationTypeVisitor extends ReactiveFunctionVisitor<void> {
318
+ scope: ReactiveScope;
319
+ alwaysInvalidatesOnInputChange: boolean = false;
320
+
321
+ constructor(scope: ReactiveScope) {
322
+ super();
323
+ this.scope = scope;
324
+ }
325
+
326
+ override visitInstruction(
327
+ instruction: ReactiveInstruction,
328
+ state: void
329
+ ): void {
330
+ this.traverseInstruction(instruction, state);
331
+ if (
332
+ instruction.lvalue === null ||
333
+ !this.scope.declarations.has(instruction.lvalue.identifier.id)
334
+ ) {
335
+ // no lvalue or this instruction isn't directly constructing a
336
+ // scope output value, skip
337
+ return;
338
+ }
339
+ switch (instruction.value.kind) {
340
+ case "FunctionExpression":
341
+ case "ArrayExpression":
342
+ case "JsxExpression":
343
+ case "JsxFragment":
344
+ case "ObjectExpression": {
345
+ // These instruction types *always* allocate. If they execute
346
+ // they will produce a new value, triggering downstream reactive
347
+ // updates
348
+ this.alwaysInvalidatesOnInputChange = true;
349
+ break;
350
+ }
351
+ }
352
+ }
353
+}