main
ts 118 lines 3.68 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 import {ReactiveFunctionTransform, Transformed, visitReactiveFunction} from '.';
9 import {
10 Identifier,
11 ReactiveFunction,
12 ReactiveInstruction,
13 ReactiveScopeBlock,
14 ReactiveStatement,
15 } from '../HIR';
16
17 /**
18 * Some instructions will *always* produce a new value, and unless memoized will *always*
19 * invalidate downstream reactive scopes. This pass finds such values and prunes downstream
20 * memoization.
21 *
22 * NOTE: function calls are an edge-case: function calls *may* return primitives, so this
23 * pass optimistically assumes they do. Therefore, unmemoized function calls will *not*
24 * prune downstream memoization. Only guaranteed new allocations, such as object and array
25 * literals, will cause pruning.
26 */
27 export function pruneAlwaysInvalidatingScopes(fn: ReactiveFunction): void {
28 visitReactiveFunction(fn, new Transform(), false);
29 }
30
31 class Transform extends ReactiveFunctionTransform<boolean> {
32 alwaysInvalidatingValues: Set<Identifier> = new Set();
33 unmemoizedValues: Set<Identifier> = new Set();
34
35 override transformInstruction(
36 instruction: ReactiveInstruction,
37 withinScope: boolean,
38 ): Transformed<ReactiveStatement> {
39 this.visitInstruction(instruction, withinScope);
40
41 const {lvalue, value} = instruction;
42 switch (value.kind) {
43 case 'ArrayExpression':
44 case 'ObjectExpression':
45 case 'JsxExpression':
46 case 'JsxFragment':
47 case 'NewExpression': {
48 if (lvalue !== null) {
49 this.alwaysInvalidatingValues.add(lvalue.identifier);
50 if (!withinScope) {
51 this.unmemoizedValues.add(lvalue.identifier);
52 }
53 }
54 break;
55 }
56 case 'StoreLocal': {
57 if (this.alwaysInvalidatingValues.has(value.value.identifier)) {
58 this.alwaysInvalidatingValues.add(value.lvalue.place.identifier);
59 }
60 if (this.unmemoizedValues.has(value.value.identifier)) {
61 this.unmemoizedValues.add(value.lvalue.place.identifier);
62 }
63 break;
64 }
65 case 'LoadLocal': {
66 if (
67 lvalue !== null &&
68 this.alwaysInvalidatingValues.has(value.place.identifier)
69 ) {
70 this.alwaysInvalidatingValues.add(lvalue.identifier);
71 }
72 if (
73 lvalue !== null &&
74 this.unmemoizedValues.has(value.place.identifier)
75 ) {
76 this.unmemoizedValues.add(lvalue.identifier);
77 }
78 break;
79 }
80 }
81 return {kind: 'keep'};
82 }
83
84 override transformScope(
85 scopeBlock: ReactiveScopeBlock,
86 _withinScope: boolean,
87 ): Transformed<ReactiveStatement> {
88 this.visitScope(scopeBlock, true);
89
90 for (const dep of scopeBlock.scope.dependencies) {
91 if (this.unmemoizedValues.has(dep.identifier)) {
92 /*
93 * This scope depends on an always-invalidating value so the scope will always invalidate:
94 * prune it to avoid wasted comparisons
95 */
96 for (const [_, decl] of scopeBlock.scope.declarations) {
97 if (this.alwaysInvalidatingValues.has(decl.identifier)) {
98 this.unmemoizedValues.add(decl.identifier);
99 }
100 }
101 for (const identifier of scopeBlock.scope.reassignments) {
102 if (this.alwaysInvalidatingValues.has(identifier)) {
103 this.unmemoizedValues.add(identifier);
104 }
105 }
106 return {
107 kind: 'replace',
108 value: {
109 kind: 'pruned-scope',
110 scope: scopeBlock.scope,
111 instructions: scopeBlock.instructions,
112 },
113 };
114 }
115 }
116 return {kind: 'keep'};
117 }
118 }