main
ts 1,123 lines 36 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 {CompilerError} from '../CompilerError';
9 import {
10 DeclarationId,
11 Environment,
12 GeneratedSource,
13 Identifier,
14 InstructionId,
15 Pattern,
16 Place,
17 ReactiveFunction,
18 ReactiveInstruction,
19 ReactiveScopeBlock,
20 ReactiveStatement,
21 ReactiveTerminal,
22 ReactiveTerminalStatement,
23 ReactiveValue,
24 ScopeId,
25 getHookKind,
26 isMutableEffect,
27 } from '../HIR';
28 import {assertExhaustive, getOrInsertDefault} from '../Utils/utils';
29 import {getPlaceScope, ReactiveScope} from '../HIR/HIR';
30 import {
31 ReactiveFunctionTransform,
32 ReactiveFunctionVisitor,
33 Transformed,
34 eachReactiveValueOperand,
35 visitReactiveFunction,
36 } from './visitors';
37 import {printPlace} from '../HIR/PrintHIR';
38 import {getFunctionCallSignature} from '../Inference/InferMutationAliasingEffects';
39
40 /*
41 * This pass prunes reactive scopes that are not necessary to bound downstream computation.
42 * Specifically, the pass identifies the set of identifiers which may "escape". Values can
43 * escape in one of two ways:
44 * * They are directly returned by the function and/or transitively aliased by a return
45 * value.
46 * * They are passed as input to a hook. This is because any value passed to a hook may
47 * have its referenced ultimately stored by React (ie, be aliased by an external value).
48 * For example, the closure passed to useEffect escapes.
49 *
50 * Example to build intuition:
51 *
52 * ```javascript
53 * function Component(props) {
54 * const a = {}; // not aliased or returned: *not* memoized
55 * const b = {}; // aliased by c, which is returned: memoized
56 * const c = [b]; // directly returned: memoized
57 * return c;
58 * }
59 * ```
60 *
61 * However, this logic alone is insufficient for two reasons:
62 * - Statically memoizing JSX elements *may* be inefficient compared to using dynamic
63 * memoization with `React.memo()`. Static memoization may be JIT'd and can look at
64 * the precise props w/o dynamic iteration, but incurs potentially large code-size
65 * overhead. Dynamic memoization with `React.memo()` incurs potentially increased
66 * runtime overhead for smaller code size. We plan to experiment with both variants
67 * for JSX.
68 * - Because we merge values whose mutations _interleave_ into a single scope, there
69 * can be cases where a non-escaping value needs to be memoized anyway to avoid breaking
70 * a memoization input. As a rule, for any scope that has a memoized output, all of that
71 * scope's transitive dependencies must also be memoized _even if they don't escape_.
72 * Failing to memoize them would cause the scope to invalidate more often than necessary
73 * and break downstream memoization.
74 *
75 * Example of this second case:
76 *
77 * ```javascript
78 * function Component(props) {
79 * // a can be independently memoized but it doesn't escape, so naively we may think its
80 * // safe to not memoize. but not memoizing would break caching of b, which does
81 * // escape.
82 * const a = [props.a];
83 *
84 * // b and c are interleaved and grouped into a single scope,
85 * // but they are independent values. c does not escape, but
86 * // we need to ensure that a is memoized or else b will invalidate
87 * // on every render since a is a dependency.
88 * const b = [];
89 * const c = {};
90 * c.a = a;
91 * b.push(props.b);
92 *
93 * return b;
94 * }
95 * ```
96 *
97 * ## Algorithm
98 *
99 * 1. First we build up a graph, a mapping of IdentifierId to a node describing all the
100 * scopes and inputs involved in creating that identifier. Individual nodes are marked
101 * as definitely aliased, conditionally aliased, or unaliased:
102 * a. Arrays, objects, function calls all produce a new value and are always marked as aliased
103 * b. Conditional and logical expressions (and a few others) are conditinally aliased,
104 * depending on whether their result value is aliased.
105 * c. JSX is always unaliased (though its props children may be)
106 * 2. The same pass which builds the graph also stores the set of returned identifiers and set of
107 * identifiers passed as arguments to hooks.
108 * 3. We traverse the graph starting from the returned identifiers and mark reachable dependencies
109 * as escaping, based on the combination of the parent node's type and its children (eg a
110 * conditional node with an aliased dep promotes to aliased).
111 * 4. Finally we prune scopes whose outputs weren't marked.
112 */
113 export function pruneNonEscapingScopes(fn: ReactiveFunction): void {
114 /*
115 * First build up a map of which instructions are involved in creating which values,
116 * and which values are returned.
117 */
118 const state = new State(fn.env);
119 for (const param of fn.params) {
120 if (param.kind === 'Identifier') {
121 state.declare(param.identifier.declarationId);
122 } else {
123 state.declare(param.place.identifier.declarationId);
124 }
125 }
126 visitReactiveFunction(fn, new CollectDependenciesVisitor(fn.env, state), []);
127
128 /*
129 * Then walk outward from the returned values and find all captured operands.
130 * This forms the set of identifiers which should be memoized.
131 */
132 const memoized = computeMemoizedIdentifiers(state);
133
134 // Prune scopes that do not declare/reassign any escaping values
135 visitReactiveFunction(fn, new PruneScopesTransform(), memoized);
136 }
137
138 export type MemoizationOptions = {
139 memoizeJsxElements: boolean;
140 forceMemoizePrimitives: boolean;
141 };
142
143 // Describes how to determine whether a value should be memoized, relative to dependees and dependencies
144 enum MemoizationLevel {
145 // The value should be memoized if it escapes
146 Memoized = 'Memoized',
147 /*
148 * Values that are memoized if their dependencies are memoized (used for logical/ternary and
149 * other expressions that propagate dependencies wo changing them)
150 */
151 Conditional = 'Conditional',
152 /*
153 * Values that cannot be compared with Object.is, but which by default don't need to be memoized
154 * unless forced
155 */
156 Unmemoized = 'Unmemoized',
157 // The value will never be memoized: used for values that can be cheaply compared w Object.is
158 Never = 'Never',
159 }
160
161 /*
162 * Given an identifier that appears as an lvalue multiple times with different memoization levels,
163 * determines the final memoization level.
164 */
165 function joinAliases(
166 kind1: MemoizationLevel,
167 kind2: MemoizationLevel,
168 ): MemoizationLevel {
169 if (
170 kind1 === MemoizationLevel.Memoized ||
171 kind2 === MemoizationLevel.Memoized
172 ) {
173 return MemoizationLevel.Memoized;
174 } else if (
175 kind1 === MemoizationLevel.Conditional ||
176 kind2 === MemoizationLevel.Conditional
177 ) {
178 return MemoizationLevel.Conditional;
179 } else if (
180 kind1 === MemoizationLevel.Unmemoized ||
181 kind2 === MemoizationLevel.Unmemoized
182 ) {
183 return MemoizationLevel.Unmemoized;
184 } else {
185 return MemoizationLevel.Never;
186 }
187 }
188
189 // A node in the graph describing the memoization level of a given identifier as well as its dependencies and scopes.
190 type IdentifierNode = {
191 level: MemoizationLevel;
192 memoized: boolean;
193 dependencies: Set<DeclarationId>;
194 scopes: Set<ScopeId>;
195 seen: boolean;
196 };
197
198 // A scope node describing its dependencies
199 type ScopeNode = {
200 dependencies: Array<DeclarationId>;
201 seen: boolean;
202 };
203
204 // Stores the identifier and scope graphs, set of returned identifiers, etc
205 class State {
206 env: Environment;
207 /*
208 * Maps lvalues for LoadLocal to the identifier being loaded, to resolve indirections
209 * in subsequent lvalues/rvalues.
210 *
211 * NOTE: this pass uses DeclarationId rather than IdentifierId because the pass is not
212 * aware of control-flow, only data flow via mutation. Instead of precisely modeling
213 * control flow, we analyze all values that may flow into a particular program variable,
214 * and then whether that program variable may escape (if so, the values flowing in may
215 * escape too). Thus we use DeclarationId to captures all values that may flow into
216 * a particular program variable, regardless of control flow paths.
217 *
218 * In the future when we convert to HIR everywhere this pass can account for control
219 * flow and use SSA ids.
220 */
221 definitions: Map<DeclarationId, DeclarationId> = new Map();
222
223 identifiers: Map<DeclarationId, IdentifierNode> = new Map();
224 scopes: Map<ScopeId, ScopeNode> = new Map();
225 escapingValues: Set<DeclarationId> = new Set();
226
227 constructor(env: Environment) {
228 this.env = env;
229 }
230
231 // Declare a new identifier, used for function id and params
232 declare(id: DeclarationId): void {
233 this.identifiers.set(id, {
234 level: MemoizationLevel.Never,
235 memoized: false,
236 dependencies: new Set(),
237 scopes: new Set(),
238 seen: false,
239 });
240 }
241
242 /*
243 * Associates the identifier with its scope, if there is one and it is active for the given instruction id:
244 * - Records the scope and its dependencies
245 * - Associates the identifier with this scope
246 */
247 visitOperand(
248 id: InstructionId,
249 place: Place,
250 identifier: DeclarationId,
251 ): void {
252 const scope = getPlaceScope(id, place);
253 if (scope !== null) {
254 let node = this.scopes.get(scope.id);
255 if (node === undefined) {
256 node = {
257 dependencies: [...scope.dependencies].map(
258 dep => dep.identifier.declarationId,
259 ),
260 seen: false,
261 };
262 this.scopes.set(scope.id, node);
263 }
264 const identifierNode = this.identifiers.get(identifier);
265 CompilerError.invariant(identifierNode !== undefined, {
266 reason: 'Expected identifier to be initialized',
267 description: `[${id}] operand=${printPlace(place)} for identifier declaration ${identifier}`,
268 loc: place.loc,
269 });
270 identifierNode.scopes.add(scope.id);
271 }
272 }
273 }
274
275 /*
276 * Given a state derived from visiting the function, walks the graph from the returned nodes
277 * to determine which other values should be memoized. Returns a set of all identifiers
278 * that should be memoized.
279 */
280 function computeMemoizedIdentifiers(state: State): Set<DeclarationId> {
281 const memoized = new Set<DeclarationId>();
282
283 // Visit an identifier, optionally forcing it to be memoized
284 function visit(id: DeclarationId, forceMemoize: boolean = false): boolean {
285 const node = state.identifiers.get(id);
286 CompilerError.invariant(node !== undefined, {
287 reason: `Expected a node for all identifiers, none found for \`${id}\``,
288 loc: GeneratedSource,
289 });
290 if (node.seen) {
291 return node.memoized;
292 }
293 node.seen = true;
294
295 /*
296 * Note: in case of cycles we temporarily mark the identifier as non-memoized,
297 * this is reset later after processing dependencies
298 */
299 node.memoized = false;
300
301 // Visit dependencies, determine if any of them are memoized
302 let hasMemoizedDependency = false;
303 for (const dep of node.dependencies) {
304 const isDepMemoized = visit(dep);
305 hasMemoizedDependency ||= isDepMemoized;
306 }
307
308 if (
309 node.level === MemoizationLevel.Memoized ||
310 (node.level === MemoizationLevel.Conditional &&
311 (hasMemoizedDependency || forceMemoize)) ||
312 (node.level === MemoizationLevel.Unmemoized && forceMemoize)
313 ) {
314 node.memoized = true;
315 memoized.add(id);
316 for (const scope of node.scopes) {
317 forceMemoizeScopeDependencies(scope);
318 }
319 }
320 return node.memoized;
321 }
322
323 // Force all the scope's optionally-memoizeable dependencies (not "Never") to be memoized
324 function forceMemoizeScopeDependencies(id: ScopeId): void {
325 const node = state.scopes.get(id);
326 CompilerError.invariant(node !== undefined, {
327 reason: 'Expected a node for all scopes',
328 loc: GeneratedSource,
329 });
330 if (node.seen) {
331 return;
332 }
333 node.seen = true;
334
335 for (const dep of node.dependencies) {
336 visit(dep, true);
337 }
338 return;
339 }
340
341 // Walk from the "roots" aka returned identifiers.
342 for (const value of state.escapingValues) {
343 visit(value);
344 }
345
346 return memoized;
347 }
348
349 type LValueMemoization = {
350 place: Place;
351 level: MemoizationLevel;
352 };
353
354 function computePatternLValues(pattern: Pattern): Array<LValueMemoization> {
355 const lvalues: Array<LValueMemoization> = [];
356 switch (pattern.kind) {
357 case 'ArrayPattern': {
358 for (const item of pattern.items) {
359 if (item.kind === 'Identifier') {
360 lvalues.push({place: item, level: MemoizationLevel.Conditional});
361 } else if (item.kind === 'Spread') {
362 lvalues.push({place: item.place, level: MemoizationLevel.Memoized});
363 }
364 }
365 break;
366 }
367 case 'ObjectPattern': {
368 for (const property of pattern.properties) {
369 if (property.kind === 'ObjectProperty') {
370 lvalues.push({
371 place: property.place,
372 level: MemoizationLevel.Conditional,
373 });
374 } else {
375 lvalues.push({
376 place: property.place,
377 level: MemoizationLevel.Memoized,
378 });
379 }
380 }
381 break;
382 }
383 default: {
384 assertExhaustive(
385 pattern,
386 `Unexpected pattern kind \`${(pattern as any).kind}\``,
387 );
388 }
389 }
390 return lvalues;
391 }
392
393 /*
394 * Populates the input state with the set of returned identifiers and information about each
395 * identifier's and scope's dependencies.
396 */
397 class CollectDependenciesVisitor extends ReactiveFunctionVisitor<
398 Array<ReactiveScope>
399 > {
400 env: Environment;
401 state: State;
402 options: MemoizationOptions;
403
404 constructor(env: Environment, state: State) {
405 super();
406 this.env = env;
407 this.state = state;
408 this.options = {
409 memoizeJsxElements: !this.env.config.enableForest,
410 forceMemoizePrimitives:
411 this.env.config.enableForest ||
412 this.env.config.enablePreserveExistingMemoizationGuarantees,
413 };
414 }
415
416 /*
417 * Given a value, returns a description of how it should be memoized:
418 * - lvalues: optional extra places that are lvalue-like in the sense of
419 * aliasing the rvalues
420 * - rvalues: places that are aliased by the instruction's lvalues.
421 * - level: the level of memoization to apply to this value
422 */
423 computeMemoizationInputs(
424 value: ReactiveValue,
425 lvalue: Place | null,
426 ): {
427 // can optionally return a custom set of lvalues per instruction
428 lvalues: Array<LValueMemoization>;
429 rvalues: Array<Place>;
430 } {
431 const env = this.env;
432 const options = this.options;
433
434 switch (value.kind) {
435 case 'ConditionalExpression': {
436 return {
437 // Only need to memoize if the rvalues are memoized
438 lvalues:
439 lvalue !== null
440 ? [{place: lvalue, level: MemoizationLevel.Conditional}]
441 : [],
442 rvalues: [
443 // Conditionals do not alias their test value.
444 ...this.computeMemoizationInputs(value.consequent, null).rvalues,
445 ...this.computeMemoizationInputs(value.alternate, null).rvalues,
446 ],
447 };
448 }
449 case 'LogicalExpression': {
450 return {
451 // Only need to memoize if the rvalues are memoized
452 lvalues:
453 lvalue !== null
454 ? [{place: lvalue, level: MemoizationLevel.Conditional}]
455 : [],
456 rvalues: [
457 ...this.computeMemoizationInputs(value.left, null).rvalues,
458 ...this.computeMemoizationInputs(value.right, null).rvalues,
459 ],
460 };
461 }
462 case 'SequenceExpression': {
463 for (const instr of value.instructions) {
464 this.visitValueForMemoization(instr.id, instr.value, instr.lvalue);
465 }
466 return {
467 // Only need to memoize if the rvalues are memoized
468 lvalues:
469 lvalue !== null
470 ? [{place: lvalue, level: MemoizationLevel.Conditional}]
471 : [],
472 /*
473 * Only the final value of the sequence is a true rvalue:
474 * values from the sequence's instructions are evaluated
475 * as separate nodes
476 */
477 rvalues: this.computeMemoizationInputs(value.value, null).rvalues,
478 };
479 }
480 case 'JsxExpression': {
481 const operands: Array<Place> = [];
482 if (value.tag.kind === 'Identifier') {
483 operands.push(value.tag);
484 }
485 for (const prop of value.props) {
486 if (prop.kind === 'JsxAttribute') {
487 operands.push(prop.place);
488 } else {
489 operands.push(prop.argument);
490 }
491 }
492 if (value.children !== null) {
493 for (const child of value.children) {
494 operands.push(child);
495 }
496 }
497 const level = options.memoizeJsxElements
498 ? MemoizationLevel.Memoized
499 : MemoizationLevel.Unmemoized;
500 return {
501 /*
502 * JSX elements themselves are not memoized unless forced to
503 * avoid breaking downstream memoization
504 */
505 lvalues: lvalue !== null ? [{place: lvalue, level}] : [],
506 rvalues: operands,
507 };
508 }
509 case 'JsxFragment': {
510 const level = options.memoizeJsxElements
511 ? MemoizationLevel.Memoized
512 : MemoizationLevel.Unmemoized;
513 return {
514 /*
515 * JSX elements themselves are not memoized unless forced to
516 * avoid breaking downstream memoization
517 */
518 lvalues: lvalue !== null ? [{place: lvalue, level}] : [],
519 rvalues: value.children,
520 };
521 }
522 case 'NextPropertyOf':
523 case 'StartMemoize':
524 case 'FinishMemoize':
525 case 'Debugger':
526 case 'ComputedDelete':
527 case 'PropertyDelete':
528 case 'LoadGlobal':
529 case 'MetaProperty':
530 case 'TemplateLiteral':
531 case 'Primitive':
532 case 'JSXText':
533 case 'BinaryExpression':
534 case 'UnaryExpression': {
535 if (options.forceMemoizePrimitives) {
536 /**
537 * Because these instructions produce primitives we usually don't consider
538 * them as escape points: they are known to copy, not return references.
539 * However if we're forcing memoization of primitives then we mark these
540 * instructions as needing memoization and walk their rvalues to ensure
541 * any scopes transitively reachable from the rvalues are considered for
542 * memoization. Note: we may still prune primitive-producing scopes if
543 * they don't ultimately escape at all.
544 */
545 const level = MemoizationLevel.Conditional;
546 return {
547 lvalues: lvalue !== null ? [{place: lvalue, level}] : [],
548 rvalues: [...eachReactiveValueOperand(value)],
549 };
550 }
551 const level = MemoizationLevel.Never;
552 return {
553 // All of these instructions return a primitive value and never need to be memoized
554 lvalues: lvalue !== null ? [{place: lvalue, level}] : [],
555 rvalues: [],
556 };
557 }
558 case 'Await':
559 case 'TypeCastExpression': {
560 return {
561 // Indirection for the inner value, memoized if the value is
562 lvalues:
563 lvalue !== null
564 ? [{place: lvalue, level: MemoizationLevel.Conditional}]
565 : [],
566 rvalues: [value.value],
567 };
568 }
569 case 'IteratorNext': {
570 return {
571 // Indirection for the inner value, memoized if the value is
572 lvalues:
573 lvalue !== null
574 ? [{place: lvalue, level: MemoizationLevel.Conditional}]
575 : [],
576 rvalues: [value.iterator, value.collection],
577 };
578 }
579 case 'GetIterator': {
580 return {
581 // Indirection for the inner value, memoized if the value is
582 lvalues:
583 lvalue !== null
584 ? [{place: lvalue, level: MemoizationLevel.Conditional}]
585 : [],
586 rvalues: [value.collection],
587 };
588 }
589 case 'LoadLocal': {
590 return {
591 // Indirection for the inner value, memoized if the value is
592 lvalues:
593 lvalue !== null
594 ? [{place: lvalue, level: MemoizationLevel.Conditional}]
595 : [],
596 rvalues: [value.place],
597 };
598 }
599 case 'LoadContext': {
600 return {
601 // Should never be pruned
602 lvalues:
603 lvalue !== null
604 ? [{place: lvalue, level: MemoizationLevel.Conditional}]
605 : [],
606 rvalues: [value.place],
607 };
608 }
609 case 'DeclareContext': {
610 const lvalues = [
611 {place: value.lvalue.place, level: MemoizationLevel.Memoized},
612 ];
613 if (lvalue !== null) {
614 lvalues.push({place: lvalue, level: MemoizationLevel.Unmemoized});
615 }
616 return {
617 lvalues,
618 rvalues: [],
619 };
620 }
621
622 case 'DeclareLocal': {
623 const lvalues = [
624 {place: value.lvalue.place, level: MemoizationLevel.Unmemoized},
625 ];
626 if (lvalue !== null) {
627 lvalues.push({place: lvalue, level: MemoizationLevel.Unmemoized});
628 }
629 return {
630 lvalues,
631 rvalues: [],
632 };
633 }
634 case 'PrefixUpdate':
635 case 'PostfixUpdate': {
636 const lvalues = [
637 {place: value.lvalue, level: MemoizationLevel.Conditional},
638 ];
639 if (lvalue !== null) {
640 lvalues.push({place: lvalue, level: MemoizationLevel.Conditional});
641 }
642 return {
643 // Indirection for the inner value, memoized if the value is
644 lvalues,
645 rvalues: [value.value],
646 };
647 }
648 case 'StoreLocal': {
649 const lvalues = [
650 {place: value.lvalue.place, level: MemoizationLevel.Conditional},
651 ];
652 if (lvalue !== null) {
653 lvalues.push({place: lvalue, level: MemoizationLevel.Conditional});
654 }
655 return {
656 // Indirection for the inner value, memoized if the value is
657 lvalues,
658 rvalues: [value.value],
659 };
660 }
661 case 'StoreContext': {
662 // Should never be pruned
663 const lvalues = [
664 {place: value.lvalue.place, level: MemoizationLevel.Memoized},
665 ];
666 if (lvalue !== null) {
667 lvalues.push({place: lvalue, level: MemoizationLevel.Conditional});
668 }
669
670 return {
671 lvalues,
672 rvalues: [value.value],
673 };
674 }
675 case 'StoreGlobal': {
676 const lvalues = [];
677 if (lvalue !== null) {
678 lvalues.push({place: lvalue, level: MemoizationLevel.Unmemoized});
679 }
680
681 return {
682 lvalues,
683 rvalues: [value.value],
684 };
685 }
686 case 'Destructure': {
687 // Indirection for the inner value, memoized if the value is
688 const lvalues = [];
689 if (lvalue !== null) {
690 lvalues.push({place: lvalue, level: MemoizationLevel.Conditional});
691 }
692 lvalues.push(...computePatternLValues(value.lvalue.pattern));
693 return {
694 lvalues: lvalues,
695 rvalues: [value.value],
696 };
697 }
698 case 'ComputedLoad':
699 case 'PropertyLoad': {
700 const level = MemoizationLevel.Conditional;
701 return {
702 // Indirection for the inner value, memoized if the value is
703 lvalues: lvalue !== null ? [{place: lvalue, level}] : [],
704 /*
705 * Only the object is aliased to the result, and the result only needs to be
706 * memoized if the object is
707 */
708 rvalues: [value.object],
709 };
710 }
711 case 'ComputedStore': {
712 /*
713 * The object being stored to acts as an lvalue (it aliases the value), but
714 * the computed key is not aliased
715 */
716 const lvalues = [
717 {place: value.object, level: MemoizationLevel.Conditional},
718 ];
719 if (lvalue !== null) {
720 lvalues.push({place: lvalue, level: MemoizationLevel.Conditional});
721 }
722 return {
723 lvalues,
724 rvalues: [value.value],
725 };
726 }
727 case 'OptionalExpression': {
728 // Indirection for the inner value, memoized if the value is
729 const lvalues = [];
730 if (lvalue !== null) {
731 lvalues.push({place: lvalue, level: MemoizationLevel.Conditional});
732 }
733 return {
734 lvalues: lvalues,
735 rvalues: [
736 ...this.computeMemoizationInputs(value.value, null).rvalues,
737 ],
738 };
739 }
740 case 'TaggedTemplateExpression': {
741 const signature = getFunctionCallSignature(
742 env,
743 value.tag.identifier.type,
744 );
745 let lvalues = [];
746 if (lvalue !== null) {
747 lvalues.push({place: lvalue, level: MemoizationLevel.Memoized});
748 }
749 if (signature?.noAlias === true) {
750 return {
751 lvalues,
752 rvalues: [],
753 };
754 }
755 const operands = [...eachReactiveValueOperand(value)];
756 lvalues.push(
757 ...operands
758 .filter(operand => isMutableEffect(operand.effect, operand.loc))
759 .map(place => ({place, level: MemoizationLevel.Memoized})),
760 );
761 return {
762 lvalues,
763 rvalues: operands,
764 };
765 }
766 case 'CallExpression': {
767 const signature = getFunctionCallSignature(
768 env,
769 value.callee.identifier.type,
770 );
771 let lvalues = [];
772 if (lvalue !== null) {
773 lvalues.push({place: lvalue, level: MemoizationLevel.Memoized});
774 }
775 if (signature?.noAlias === true) {
776 return {
777 lvalues,
778 rvalues: [],
779 };
780 }
781 const operands = [...eachReactiveValueOperand(value)];
782 lvalues.push(
783 ...operands
784 .filter(operand => isMutableEffect(operand.effect, operand.loc))
785 .map(place => ({place, level: MemoizationLevel.Memoized})),
786 );
787 return {
788 lvalues,
789 rvalues: operands,
790 };
791 }
792 case 'MethodCall': {
793 const signature = getFunctionCallSignature(
794 env,
795 value.property.identifier.type,
796 );
797 let lvalues = [];
798 if (lvalue !== null) {
799 lvalues.push({place: lvalue, level: MemoizationLevel.Memoized});
800 }
801 if (signature?.noAlias === true) {
802 return {
803 lvalues,
804 rvalues: [],
805 };
806 }
807 const operands = [...eachReactiveValueOperand(value)];
808 lvalues.push(
809 ...operands
810 .filter(operand => isMutableEffect(operand.effect, operand.loc))
811 .map(place => ({place, level: MemoizationLevel.Memoized})),
812 );
813 return {
814 lvalues,
815 rvalues: operands,
816 };
817 }
818 case 'RegExpLiteral':
819 case 'ObjectMethod':
820 case 'FunctionExpression':
821 case 'ArrayExpression':
822 case 'NewExpression':
823 case 'ObjectExpression':
824 case 'PropertyStore': {
825 /*
826 * All of these instructions may produce new values which must be memoized if
827 * reachable from a return value. Any mutable rvalue may alias any other rvalue
828 */
829 const operands = [...eachReactiveValueOperand(value)];
830 const lvalues = operands
831 .filter(operand => isMutableEffect(operand.effect, operand.loc))
832 .map(place => ({place, level: MemoizationLevel.Memoized}));
833 if (lvalue !== null) {
834 lvalues.push({place: lvalue, level: MemoizationLevel.Memoized});
835 }
836 return {
837 lvalues,
838 rvalues: operands,
839 };
840 }
841 case 'UnsupportedNode': {
842 const lvalues = [];
843 if (lvalue !== null) {
844 lvalues.push({place: lvalue, level: MemoizationLevel.Never});
845 }
846 return {
847 lvalues,
848 rvalues: [],
849 };
850 }
851 default: {
852 assertExhaustive(
853 value,
854 `Unexpected value kind \`${(value as any).kind}\``,
855 );
856 }
857 }
858 }
859
860 visitValueForMemoization(
861 id: InstructionId,
862 value: ReactiveValue,
863 lvalue: Place | null,
864 ): void {
865 const state = this.state;
866 // Determe the level of memoization for this value and the lvalues/rvalues
867 const aliasing = this.computeMemoizationInputs(value, lvalue);
868
869 // Associate all the rvalues with the instruction's scope if it has one
870 for (const operand of aliasing.rvalues) {
871 const operandId =
872 state.definitions.get(operand.identifier.declarationId) ??
873 operand.identifier.declarationId;
874 state.visitOperand(id, operand, operandId);
875 }
876
877 // Add the operands as dependencies of all lvalues.
878 for (const {place: lvalue, level} of aliasing.lvalues) {
879 const lvalueId =
880 state.definitions.get(lvalue.identifier.declarationId) ??
881 lvalue.identifier.declarationId;
882 let node = state.identifiers.get(lvalueId);
883 if (node === undefined) {
884 node = {
885 level: MemoizationLevel.Never,
886 memoized: false,
887 dependencies: new Set(),
888 scopes: new Set(),
889 seen: false,
890 };
891 state.identifiers.set(lvalueId, node);
892 }
893 node.level = joinAliases(node.level, level);
894 /*
895 * This looks like NxM iterations but in practice all instructions with multiple
896 * lvalues have only a single rvalue
897 */
898 for (const operand of aliasing.rvalues) {
899 const operandId =
900 state.definitions.get(operand.identifier.declarationId) ??
901 operand.identifier.declarationId;
902 if (operandId === lvalueId) {
903 continue;
904 }
905 node.dependencies.add(operandId);
906 }
907
908 state.visitOperand(id, lvalue, lvalueId);
909 }
910
911 if (value.kind === 'LoadLocal' && lvalue !== null) {
912 state.definitions.set(
913 lvalue.identifier.declarationId,
914 value.place.identifier.declarationId,
915 );
916 } else if (value.kind === 'CallExpression' || value.kind === 'MethodCall') {
917 let callee =
918 value.kind === 'CallExpression' ? value.callee : value.property;
919 if (getHookKind(state.env, callee.identifier) != null) {
920 const signature = getFunctionCallSignature(
921 this.env,
922 callee.identifier.type,
923 );
924 /*
925 * Hook values are assumed to escape by default since they can be inputs
926 * to reactive scopes in the hook. However if the hook is annotated as
927 * noAlias we know that the arguments cannot escape and don't need to
928 * be memoized.
929 */
930 if (signature && signature.noAlias === true) {
931 return;
932 }
933 for (const operand of value.args) {
934 const place = operand.kind === 'Spread' ? operand.place : operand;
935 state.escapingValues.add(place.identifier.declarationId);
936 }
937 }
938 }
939 }
940
941 override visitInstruction(
942 instruction: ReactiveInstruction,
943 _scopes: Array<ReactiveScope>,
944 ): void {
945 this.visitValueForMemoization(
946 instruction.id,
947 instruction.value,
948 instruction.lvalue,
949 );
950 }
951
952 override visitTerminal(
953 stmt: ReactiveTerminalStatement<ReactiveTerminal>,
954 scopes: Array<ReactiveScope>,
955 ): void {
956 this.traverseTerminal(stmt, scopes);
957 if (stmt.terminal.kind === 'return') {
958 this.state.escapingValues.add(
959 stmt.terminal.value.identifier.declarationId,
960 );
961
962 /*
963 * If the return is within a scope, then those scopes must be evaluated
964 * with the return and should be considered dependencies of the returned
965 * value.
966 *
967 * This ensures that if those scopes have dependencies that those deps
968 * are also memoized.
969 */
970 const identifierNode = this.state.identifiers.get(
971 stmt.terminal.value.identifier.declarationId,
972 );
973 CompilerError.invariant(identifierNode !== undefined, {
974 reason: 'Expected identifier to be initialized',
975 loc: stmt.terminal.loc,
976 });
977 for (const scope of scopes) {
978 identifierNode.scopes.add(scope.id);
979 }
980 }
981 }
982
983 override visitScope(
984 scope: ReactiveScopeBlock,
985 scopes: Array<ReactiveScope>,
986 ): void {
987 /*
988 * If a scope reassigns any variables, set the chain of active scopes as a dependency
989 * of those variables. This ensures that if the variable escapes that we treat the
990 * reassignment scopes — and importantly their dependencies — as needing memoization.
991 */
992 for (const reassignment of scope.scope.reassignments) {
993 const identifierNode = this.state.identifiers.get(
994 reassignment.declarationId,
995 );
996 CompilerError.invariant(identifierNode !== undefined, {
997 reason: 'Expected identifier to be initialized',
998 loc: reassignment.loc,
999 });
1000 for (const scope of scopes) {
1001 identifierNode.scopes.add(scope.id);
1002 }
1003 identifierNode.scopes.add(scope.scope.id);
1004 }
1005
1006 this.traverseScope(scope, [...scopes, scope.scope]);
1007 }
1008 }
1009
1010 // Prune reactive scopes that do not have any memoized outputs
1011 class PruneScopesTransform extends ReactiveFunctionTransform<
1012 Set<DeclarationId>
1013 > {
1014 prunedScopes: Set<ScopeId> = new Set();
1015 /**
1016 * Track reassignments so we can correctly set `pruned` flags for
1017 * inlined useMemos.
1018 */
1019 reassignments: Map<DeclarationId, Set<Identifier>> = new Map();
1020
1021 override transformScope(
1022 scopeBlock: ReactiveScopeBlock,
1023 state: Set<DeclarationId>,
1024 ): Transformed<ReactiveStatement> {
1025 this.visitScope(scopeBlock, state);
1026
1027 /**
1028 * Scopes may initially appear "empty" because the value being memoized
1029 * is early-returned from within the scope. For now we intentionaly keep
1030 * these scopes, and let them get pruned later by PruneUnusedScopes
1031 * _after_ handling the early-return case in PropagateEarlyReturns.
1032 *
1033 * Also keep the scope if an early return was created by some earlier pass,
1034 * which may happen in alternate compiler configurations.
1035 */
1036 if (
1037 (scopeBlock.scope.declarations.size === 0 &&
1038 scopeBlock.scope.reassignments.size === 0) ||
1039 scopeBlock.scope.earlyReturnValue !== null
1040 ) {
1041 return {kind: 'keep'};
1042 }
1043
1044 const hasMemoizedOutput =
1045 Array.from(scopeBlock.scope.declarations.values()).some(decl =>
1046 state.has(decl.identifier.declarationId),
1047 ) ||
1048 Array.from(scopeBlock.scope.reassignments).some(identifier =>
1049 state.has(identifier.declarationId),
1050 );
1051 if (hasMemoizedOutput) {
1052 return {kind: 'keep'};
1053 } else {
1054 this.prunedScopes.add(scopeBlock.scope.id);
1055 return {
1056 kind: 'replace-many',
1057 value: scopeBlock.instructions,
1058 };
1059 }
1060 }
1061
1062 /**
1063 * If we pruned the scope for a non-escaping value, we know it doesn't
1064 * need to be memoized. Remove associated `Memoize` instructions so that
1065 * we don't report false positives on "missing" memoization of these values.
1066 */
1067 override transformInstruction(
1068 instruction: ReactiveInstruction,
1069 state: Set<DeclarationId>,
1070 ): Transformed<ReactiveStatement> {
1071 this.traverseInstruction(instruction, state);
1072
1073 const value = instruction.value;
1074 if (value.kind === 'StoreLocal' && value.lvalue.kind === 'Reassign') {
1075 // Complex cases of useMemo inlining result in a temporary that is reassigned
1076 const ids = getOrInsertDefault(
1077 this.reassignments,
1078 value.lvalue.place.identifier.declarationId,
1079 new Set(),
1080 );
1081 ids.add(value.value.identifier);
1082 } else if (
1083 value.kind === 'LoadLocal' &&
1084 value.place.identifier.scope != null &&
1085 instruction.lvalue != null &&
1086 instruction.lvalue.identifier.scope == null
1087 ) {
1088 /*
1089 * Simpler cases result in a direct assignment to the original lvalue, with a
1090 * LoadLocal
1091 */
1092 const ids = getOrInsertDefault(
1093 this.reassignments,
1094 instruction.lvalue.identifier.declarationId,
1095 new Set(),
1096 );
1097 ids.add(value.place.identifier);
1098 } else if (value.kind === 'FinishMemoize') {
1099 let decls;
1100 if (value.decl.identifier.scope == null) {
1101 /**
1102 * If the manual memo was a useMemo that got inlined, iterate through
1103 * all reassignments to the iife temporary to ensure they're memoized.
1104 */
1105 decls = this.reassignments.get(value.decl.identifier.declarationId) ?? [
1106 value.decl.identifier,
1107 ];
1108 } else {
1109 decls = [value.decl.identifier];
1110 }
1111
1112 if (
1113 [...decls].every(
1114 decl => decl.scope == null || this.prunedScopes.has(decl.scope.id),
1115 )
1116 ) {
1117 value.pruned = true;
1118 }
1119 }
1120
1121 return {kind: 'keep'};
1122 }
1123 }