@samitouri / QOS-React-1 / commits / e9db3cc2d4

[compiler] PruneNonEscapingScopes understands terminal operands

We weren't treating terminal operands as eligible for memoization in PruneNonEscapingScopes, which meant that they could end up un-memoized. Terminal operands can also be compound ReactiveValues like SequenceExpressions, so part of the fix is to make sure we don't just recurse into compound values but record the full aliasing information we would for top-level instructions. Still WIP, this needs to handle terminals other than for..of. ghstack-source-id: 09a29230514e3bc95d1833cd4392de238fabbeda Pull Request resolved: https://github.com/facebook/react/pull/33062

Joe Savona committed May 1, 2025 at 12:41 UTC e9db3cc2d4175849578418a37f33a6fde5b3c6d8
6 files changed +681 -468
compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PrintReactiveFunction.ts
+6
@@ -255,6 +255,12 @@ function writeReactiveValue(writer: Writer, value: ReactiveValue): void {
255 }
256 }
257
258 +export function printReactiveTerminal(terminal: ReactiveTerminal): string {
259 + const writer = new Writer();
260 + writeTerminal(writer, terminal);
261 + return writer.complete();
262 +}
263 +
264 function writeTerminal(writer: Writer, terminal: ReactiveTerminal): void {
265 switch (terminal.kind) {
266 case 'break': {
compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneNonEscapingScopes.ts
+519 -468
@@ -26,7 +26,7 @@ import {
26 } from '../HIR';
27 import {getFunctionCallSignature} from '../Inference/InferReferenceEffects';
28 import {assertExhaustive, getOrInsertDefault} from '../Utils/utils';
29 -import {getPlaceScope} from '../HIR/HIR';
29 +import {getPlaceScope, ReactiveScope} from '../HIR/HIR';
30 import {
31 ReactiveFunctionTransform,
32 ReactiveFunctionVisitor,
@@ -34,6 +34,7 @@ import {
34 eachReactiveValueOperand,
35 visitReactiveFunction,
36 } from './visitors';
37 +import {printPlace} from '../HIR/PrintHIR';
38
39 /*
40 * This pass prunes reactive scopes that are not necessary to bound downstream computation.
@@ -121,9 +122,7 @@ export function pruneNonEscapingScopes(fn: ReactiveFunction): void {
122 state.declare(param.place.identifier.declarationId);
123 }
124 }
124 - visitReactiveFunction(fn, new CollectDependenciesVisitor(fn.env), state);
125 -
126 - // log(() => prettyFormat(state));
125 + visitReactiveFunction(fn, new CollectDependenciesVisitor(fn.env, state), []);
126
127 /*
128 * Then walk outward from the returned values and find all captured operands.
@@ -131,10 +130,6 @@ export function pruneNonEscapingScopes(fn: ReactiveFunction): void {
130 */
131 const memoized = computeMemoizedIdentifiers(state);
132
134 - // log(() => prettyFormat(memoized));
135 -
136 - // log(() => printReactiveFunction(fn));
137 -
133 // Prune scopes that do not declare/reassign any escaping values
134 visitReactiveFunction(fn, new PruneScopesTransform(), memoized);
135 }
@@ -268,7 +263,7 @@ class State {
263 const identifierNode = this.identifiers.get(identifier);
264 CompilerError.invariant(identifierNode !== undefined, {
265 reason: 'Expected identifier to be initialized',
271 - description: null,
266 + description: `[${id}] operand=${printPlace(place)} for identifier declaration ${identifier}`,
267 loc: place.loc,
268 suggestions: null,
269 });
@@ -360,435 +355,6 @@ type LValueMemoization = {
355 level: MemoizationLevel;
356 };
357
363 -/*
364 - * Given a value, returns a description of how it should be memoized:
365 - * - lvalues: optional extra places that are lvalue-like in the sense of
366 - * aliasing the rvalues
367 - * - rvalues: places that are aliased by the instruction's lvalues.
368 - * - level: the level of memoization to apply to this value
369 - */
370 -function computeMemoizationInputs(
371 - env: Environment,
372 - value: ReactiveValue,
373 - lvalue: Place | null,
374 - options: MemoizationOptions,
375 -): {
376 - // can optionally return a custom set of lvalues per instruction
377 - lvalues: Array<LValueMemoization>;
378 - rvalues: Array<Place>;
379 -} {
380 - switch (value.kind) {
381 - case 'ConditionalExpression': {
382 - return {
383 - // Only need to memoize if the rvalues are memoized
384 - lvalues:
385 - lvalue !== null
386 - ? [{place: lvalue, level: MemoizationLevel.Conditional}]
387 - : [],
388 - rvalues: [
389 - // Conditionals do not alias their test value.
390 - ...computeMemoizationInputs(env, value.consequent, null, options)
391 - .rvalues,
392 - ...computeMemoizationInputs(env, value.alternate, null, options)
393 - .rvalues,
394 - ],
395 - };
396 - }
397 - case 'LogicalExpression': {
398 - return {
399 - // Only need to memoize if the rvalues are memoized
400 - lvalues:
401 - lvalue !== null
402 - ? [{place: lvalue, level: MemoizationLevel.Conditional}]
403 - : [],
404 - rvalues: [
405 - ...computeMemoizationInputs(env, value.left, null, options).rvalues,
406 - ...computeMemoizationInputs(env, value.right, null, options).rvalues,
407 - ],
408 - };
409 - }
410 - case 'SequenceExpression': {
411 - return {
412 - // Only need to memoize if the rvalues are memoized
413 - lvalues:
414 - lvalue !== null
415 - ? [{place: lvalue, level: MemoizationLevel.Conditional}]
416 - : [],
417 - /*
418 - * Only the final value of the sequence is a true rvalue:
419 - * values from the sequence's instructions are evaluated
420 - * as separate nodes
421 - */
422 - rvalues: computeMemoizationInputs(env, value.value, null, options)
423 - .rvalues,
424 - };
425 - }
426 - case 'JsxExpression': {
427 - const operands: Array<Place> = [];
428 - if (value.tag.kind === 'Identifier') {
429 - operands.push(value.tag);
430 - }
431 - for (const prop of value.props) {
432 - if (prop.kind === 'JsxAttribute') {
433 - operands.push(prop.place);
434 - } else {
435 - operands.push(prop.argument);
436 - }
437 - }
438 - if (value.children !== null) {
439 - for (const child of value.children) {
440 - operands.push(child);
441 - }
442 - }
443 - const level = options.memoizeJsxElements
444 - ? MemoizationLevel.Memoized
445 - : MemoizationLevel.Unmemoized;
446 - return {
447 - /*
448 - * JSX elements themselves are not memoized unless forced to
449 - * avoid breaking downstream memoization
450 - */
451 - lvalues: lvalue !== null ? [{place: lvalue, level}] : [],
452 - rvalues: operands,
453 - };
454 - }
455 - case 'JsxFragment': {
456 - const level = options.memoizeJsxElements
457 - ? MemoizationLevel.Memoized
458 - : MemoizationLevel.Unmemoized;
459 - return {
460 - /*
461 - * JSX elements themselves are not memoized unless forced to
462 - * avoid breaking downstream memoization
463 - */
464 - lvalues: lvalue !== null ? [{place: lvalue, level}] : [],
465 - rvalues: value.children,
466 - };
467 - }
468 - case 'NextPropertyOf':
469 - case 'StartMemoize':
470 - case 'FinishMemoize':
471 - case 'Debugger':
472 - case 'ComputedDelete':
473 - case 'PropertyDelete':
474 - case 'LoadGlobal':
475 - case 'MetaProperty':
476 - case 'TemplateLiteral':
477 - case 'Primitive':
478 - case 'JSXText':
479 - case 'BinaryExpression':
480 - case 'UnaryExpression': {
481 - const level = options.forceMemoizePrimitives
482 - ? MemoizationLevel.Memoized
483 - : MemoizationLevel.Never;
484 - return {
485 - // All of these instructions return a primitive value and never need to be memoized
486 - lvalues: lvalue !== null ? [{place: lvalue, level}] : [],
487 - rvalues: [],
488 - };
489 - }
490 - case 'Await':
491 - case 'TypeCastExpression': {
492 - return {
493 - // Indirection for the inner value, memoized if the value is
494 - lvalues:
495 - lvalue !== null
496 - ? [{place: lvalue, level: MemoizationLevel.Conditional}]
497 - : [],
498 - rvalues: [value.value],
499 - };
500 - }
501 - case 'IteratorNext': {
502 - return {
503 - // Indirection for the inner value, memoized if the value is
504 - lvalues:
505 - lvalue !== null
506 - ? [{place: lvalue, level: MemoizationLevel.Conditional}]
507 - : [],
508 - rvalues: [value.iterator, value.collection],
509 - };
510 - }
511 - case 'GetIterator': {
512 - return {
513 - // Indirection for the inner value, memoized if the value is
514 - lvalues:
515 - lvalue !== null
516 - ? [{place: lvalue, level: MemoizationLevel.Conditional}]
517 - : [],
518 - rvalues: [value.collection],
519 - };
520 - }
521 - case 'LoadLocal': {
522 - return {
523 - // Indirection for the inner value, memoized if the value is
524 - lvalues:
525 - lvalue !== null
526 - ? [{place: lvalue, level: MemoizationLevel.Conditional}]
527 - : [],
528 - rvalues: [value.place],
529 - };
530 - }
531 - case 'LoadContext': {
532 - return {
533 - // Should never be pruned
534 - lvalues:
535 - lvalue !== null
536 - ? [{place: lvalue, level: MemoizationLevel.Conditional}]
537 - : [],
538 - rvalues: [value.place],
539 - };
540 - }
541 - case 'DeclareContext': {
542 - const lvalues = [
543 - {place: value.lvalue.place, level: MemoizationLevel.Memoized},
544 - ];
545 - if (lvalue !== null) {
546 - lvalues.push({place: lvalue, level: MemoizationLevel.Unmemoized});
547 - }
548 - return {
549 - lvalues,
550 - rvalues: [],
551 - };
552 - }
553 -
554 - case 'DeclareLocal': {
555 - const lvalues = [
556 - {place: value.lvalue.place, level: MemoizationLevel.Unmemoized},
557 - ];
558 - if (lvalue !== null) {
559 - lvalues.push({place: lvalue, level: MemoizationLevel.Unmemoized});
560 - }
561 - return {
562 - lvalues,
563 - rvalues: [],
564 - };
565 - }
566 - case 'PrefixUpdate':
567 - case 'PostfixUpdate': {
568 - const lvalues = [
569 - {place: value.lvalue, level: MemoizationLevel.Conditional},
570 - ];
571 - if (lvalue !== null) {
572 - lvalues.push({place: lvalue, level: MemoizationLevel.Conditional});
573 - }
574 - return {
575 - // Indirection for the inner value, memoized if the value is
576 - lvalues,
577 - rvalues: [value.value],
578 - };
579 - }
580 - case 'StoreLocal': {
581 - const lvalues = [
582 - {place: value.lvalue.place, level: MemoizationLevel.Conditional},
583 - ];
584 - if (lvalue !== null) {
585 - lvalues.push({place: lvalue, level: MemoizationLevel.Conditional});
586 - }
587 - return {
588 - // Indirection for the inner value, memoized if the value is
589 - lvalues,
590 - rvalues: [value.value],
591 - };
592 - }
593 - case 'StoreContext': {
594 - // Should never be pruned
595 - const lvalues = [
596 - {place: value.lvalue.place, level: MemoizationLevel.Memoized},
597 - ];
598 - if (lvalue !== null) {
599 - lvalues.push({place: lvalue, level: MemoizationLevel.Conditional});
600 - }
601 -
602 - return {
603 - lvalues,
604 - rvalues: [value.value],
605 - };
606 - }
607 - case 'StoreGlobal': {
608 - const lvalues = [];
609 - if (lvalue !== null) {
610 - lvalues.push({place: lvalue, level: MemoizationLevel.Unmemoized});
611 - }
612 -
613 - return {
614 - lvalues,
615 - rvalues: [value.value],
616 - };
617 - }
618 - case 'Destructure': {
619 - // Indirection for the inner value, memoized if the value is
620 - const lvalues = [];
621 - if (lvalue !== null) {
622 - lvalues.push({place: lvalue, level: MemoizationLevel.Conditional});
623 - }
624 - lvalues.push(...computePatternLValues(value.lvalue.pattern));
625 - return {
626 - lvalues: lvalues,
627 - rvalues: [value.value],
628 - };
629 - }
630 - case 'ComputedLoad':
631 - case 'PropertyLoad': {
632 - const level = options.forceMemoizePrimitives
633 - ? MemoizationLevel.Memoized
634 - : MemoizationLevel.Conditional;
635 - return {
636 - // Indirection for the inner value, memoized if the value is
637 - lvalues: lvalue !== null ? [{place: lvalue, level}] : [],
638 - /*
639 - * Only the object is aliased to the result, and the result only needs to be
640 - * memoized if the object is
641 - */
642 - rvalues: [value.object],
643 - };
644 - }
645 - case 'ComputedStore': {
646 - /*
647 - * The object being stored to acts as an lvalue (it aliases the value), but
648 - * the computed key is not aliased
649 - */
650 - const lvalues = [
651 - {place: value.object, level: MemoizationLevel.Conditional},
652 - ];
653 - if (lvalue !== null) {
654 - lvalues.push({place: lvalue, level: MemoizationLevel.Conditional});
655 - }
656 - return {
657 - lvalues,
658 - rvalues: [value.value],
659 - };
660 - }
661 - case 'OptionalExpression': {
662 - // Indirection for the inner value, memoized if the value is
663 - const lvalues = [];
664 - if (lvalue !== null) {
665 - lvalues.push({place: lvalue, level: MemoizationLevel.Conditional});
666 - }
667 - return {
668 - lvalues: lvalues,
669 - rvalues: [
670 - ...computeMemoizationInputs(env, value.value, null, options).rvalues,
671 - ],
672 - };
673 - }
674 - case 'TaggedTemplateExpression': {
675 - const signature = getFunctionCallSignature(
676 - env,
677 - value.tag.identifier.type,
678 - );
679 - let lvalues = [];
680 - if (lvalue !== null) {
681 - lvalues.push({place: lvalue, level: MemoizationLevel.Memoized});
682 - }
683 - if (signature?.noAlias === true) {
684 - return {
685 - lvalues,
686 - rvalues: [],
687 - };
688 - }
689 - const operands = [...eachReactiveValueOperand(value)];
690 - lvalues.push(
691 - ...operands
692 - .filter(operand => isMutableEffect(operand.effect, operand.loc))
693 - .map(place => ({place, level: MemoizationLevel.Memoized})),
694 - );
695 - return {
696 - lvalues,
697 - rvalues: operands,
698 - };
699 - }
700 - case 'CallExpression': {
701 - const signature = getFunctionCallSignature(
702 - env,
703 - value.callee.identifier.type,
704 - );
705 - let lvalues = [];
706 - if (lvalue !== null) {
707 - lvalues.push({place: lvalue, level: MemoizationLevel.Memoized});
708 - }
709 - if (signature?.noAlias === true) {
710 - return {
711 - lvalues,
712 - rvalues: [],
713 - };
714 - }
715 - const operands = [...eachReactiveValueOperand(value)];
716 - lvalues.push(
717 - ...operands
718 - .filter(operand => isMutableEffect(operand.effect, operand.loc))
719 - .map(place => ({place, level: MemoizationLevel.Memoized})),
720 - );
721 - return {
722 - lvalues,
723 - rvalues: operands,
724 - };
725 - }
726 - case 'MethodCall': {
727 - const signature = getFunctionCallSignature(
728 - env,
729 - value.property.identifier.type,
730 - );
731 - let lvalues = [];
732 - if (lvalue !== null) {
733 - lvalues.push({place: lvalue, level: MemoizationLevel.Memoized});
734 - }
735 - if (signature?.noAlias === true) {
736 - return {
737 - lvalues,
738 - rvalues: [],
739 - };
740 - }
741 - const operands = [...eachReactiveValueOperand(value)];
742 - lvalues.push(
743 - ...operands
744 - .filter(operand => isMutableEffect(operand.effect, operand.loc))
745 - .map(place => ({place, level: MemoizationLevel.Memoized})),
746 - );
747 - return {
748 - lvalues,
749 - rvalues: operands,
750 - };
751 - }
752 - case 'RegExpLiteral':
753 - case 'ObjectMethod':
754 - case 'FunctionExpression':
755 - case 'ArrayExpression':
756 - case 'NewExpression':
757 - case 'ObjectExpression':
758 - case 'PropertyStore': {
759 - /*
760 - * All of these instructions may produce new values which must be memoized if
761 - * reachable from a return value. Any mutable rvalue may alias any other rvalue
762 - */
763 - const operands = [...eachReactiveValueOperand(value)];
764 - const lvalues = operands
765 - .filter(operand => isMutableEffect(operand.effect, operand.loc))
766 - .map(place => ({place, level: MemoizationLevel.Memoized}));
767 - if (lvalue !== null) {
768 - lvalues.push({place: lvalue, level: MemoizationLevel.Memoized});
769 - }
770 - return {
771 - lvalues,
772 - rvalues: operands,
773 - };
774 - }
775 - case 'UnsupportedNode': {
776 - CompilerError.invariant(false, {
777 - reason: `Unexpected unsupported node`,
778 - description: null,
779 - loc: value.loc,
780 - suggestions: null,
781 - });
782 - }
783 - default: {
784 - assertExhaustive(
785 - value,
786 - `Unexpected value kind \`${(value as any).kind}\``,
787 - );
788 - }
789 - }
790 -}
791 -
358 function computePatternLValues(pattern: Pattern): Array<LValueMemoization> {
359 const lvalues: Array<LValueMemoization> = [];
360 switch (pattern.kind) {
@@ -832,39 +398,468 @@ function computePatternLValues(pattern: Pattern): Array<LValueMemoization> {
398 * Populates the input state with the set of returned identifiers and information about each
399 * identifier's and scope's dependencies.
400 */
835 -class CollectDependenciesVisitor extends ReactiveFunctionVisitor<State> {
401 +class CollectDependenciesVisitor extends ReactiveFunctionVisitor<
402 + Array<ReactiveScope>
403 +> {
404 env: Environment;
405 + state: State;
406 options: MemoizationOptions;
407
839 - constructor(env: Environment) {
408 + constructor(env: Environment, state: State) {
409 super();
410 this.env = env;
411 + this.state = state;
412 this.options = {
413 memoizeJsxElements: !this.env.config.enableForest,
414 forceMemoizePrimitives: this.env.config.enableForest,
415 };
416 }
417
848 - override visitInstruction(
849 - instruction: ReactiveInstruction,
850 - state: State,
851 - ): void {
852 - this.traverseInstruction(instruction, state);
418 + /*
419 + * Given a value, returns a description of how it should be memoized:
420 + * - lvalues: optional extra places that are lvalue-like in the sense of
421 + * aliasing the rvalues
422 + * - rvalues: places that are aliased by the instruction's lvalues.
423 + * - level: the level of memoization to apply to this value
424 + */
425 + computeMemoizationInputs(
426 + value: ReactiveValue,
427 + lvalue: Place | null,
428 + ): {
429 + // can optionally return a custom set of lvalues per instruction
430 + lvalues: Array<LValueMemoization>;
431 + rvalues: Array<Place>;
432 + } {
433 + const env = this.env;
434 + const options = this.options;
435
436 + switch (value.kind) {
437 + case 'ConditionalExpression': {
438 + return {
439 + // Only need to memoize if the rvalues are memoized
440 + lvalues:
441 + lvalue !== null
442 + ? [{place: lvalue, level: MemoizationLevel.Conditional}]
443 + : [],
444 + rvalues: [
445 + // Conditionals do not alias their test value.
446 + ...this.computeMemoizationInputs(value.consequent, null).rvalues,
447 + ...this.computeMemoizationInputs(value.alternate, null).rvalues,
448 + ],
449 + };
450 + }
451 + case 'LogicalExpression': {
452 + return {
453 + // Only need to memoize if the rvalues are memoized
454 + lvalues:
455 + lvalue !== null
456 + ? [{place: lvalue, level: MemoizationLevel.Conditional}]
457 + : [],
458 + rvalues: [
459 + ...this.computeMemoizationInputs(value.left, null).rvalues,
460 + ...this.computeMemoizationInputs(value.right, null).rvalues,
461 + ],
462 + };
463 + }
464 + case 'SequenceExpression': {
465 + for (const instr of value.instructions) {
466 + this.visitValueForMemoization(instr.id, instr.value, instr.lvalue);
467 + }
468 + return {
469 + // Only need to memoize if the rvalues are memoized
470 + lvalues:
471 + lvalue !== null
472 + ? [{place: lvalue, level: MemoizationLevel.Conditional}]
473 + : [],
474 + /*
475 + * Only the final value of the sequence is a true rvalue:
476 + * values from the sequence's instructions are evaluated
477 + * as separate nodes
478 + */
479 + rvalues: this.computeMemoizationInputs(value.value, null).rvalues,
480 + };
481 + }
482 + case 'JsxExpression': {
483 + const operands: Array<Place> = [];
484 + if (value.tag.kind === 'Identifier') {
485 + operands.push(value.tag);
486 + }
487 + for (const prop of value.props) {
488 + if (prop.kind === 'JsxAttribute') {
489 + operands.push(prop.place);
490 + } else {
491 + operands.push(prop.argument);
492 + }
493 + }
494 + if (value.children !== null) {
495 + for (const child of value.children) {
496 + operands.push(child);
497 + }
498 + }
499 + const level = options.memoizeJsxElements
500 + ? MemoizationLevel.Memoized
501 + : MemoizationLevel.Unmemoized;
502 + return {
503 + /*
504 + * JSX elements themselves are not memoized unless forced to
505 + * avoid breaking downstream memoization
506 + */
507 + lvalues: lvalue !== null ? [{place: lvalue, level}] : [],
508 + rvalues: operands,
509 + };
510 + }
511 + case 'JsxFragment': {
512 + const level = options.memoizeJsxElements
513 + ? MemoizationLevel.Memoized
514 + : MemoizationLevel.Unmemoized;
515 + return {
516 + /*
517 + * JSX elements themselves are not memoized unless forced to
518 + * avoid breaking downstream memoization
519 + */
520 + lvalues: lvalue !== null ? [{place: lvalue, level}] : [],
521 + rvalues: value.children,
522 + };
523 + }
524 + case 'NextPropertyOf':
525 + case 'StartMemoize':
526 + case 'FinishMemoize':
527 + case 'Debugger':
528 + case 'ComputedDelete':
529 + case 'PropertyDelete':
530 + case 'LoadGlobal':
531 + case 'MetaProperty':
532 + case 'TemplateLiteral':
533 + case 'Primitive':
534 + case 'JSXText':
535 + case 'BinaryExpression':
536 + case 'UnaryExpression': {
537 + const level = options.forceMemoizePrimitives
538 + ? MemoizationLevel.Memoized
539 + : MemoizationLevel.Never;
540 + return {
541 + // All of these instructions return a primitive value and never need to be memoized
542 + lvalues: lvalue !== null ? [{place: lvalue, level}] : [],
543 + rvalues: [],
544 + };
545 + }
546 + case 'Await':
547 + case 'TypeCastExpression': {
548 + return {
549 + // Indirection for the inner value, memoized if the value is
550 + lvalues:
551 + lvalue !== null
552 + ? [{place: lvalue, level: MemoizationLevel.Conditional}]
553 + : [],
554 + rvalues: [value.value],
555 + };
556 + }
557 + case 'IteratorNext': {
558 + return {
559 + // Indirection for the inner value, memoized if the value is
560 + lvalues:
561 + lvalue !== null
562 + ? [{place: lvalue, level: MemoizationLevel.Conditional}]
563 + : [],
564 + rvalues: [value.iterator, value.collection],
565 + };
566 + }
567 + case 'GetIterator': {
568 + return {
569 + // Indirection for the inner value, memoized if the value is
570 + lvalues:
571 + lvalue !== null
572 + ? [{place: lvalue, level: MemoizationLevel.Conditional}]
573 + : [],
574 + rvalues: [value.collection],
575 + };
576 + }
577 + case 'LoadLocal': {
578 + return {
579 + // Indirection for the inner value, memoized if the value is
580 + lvalues:
581 + lvalue !== null
582 + ? [{place: lvalue, level: MemoizationLevel.Conditional}]
583 + : [],
584 + rvalues: [value.place],
585 + };
586 + }
587 + case 'LoadContext': {
588 + return {
589 + // Should never be pruned
590 + lvalues:
591 + lvalue !== null
592 + ? [{place: lvalue, level: MemoizationLevel.Conditional}]
593 + : [],
594 + rvalues: [value.place],
595 + };
596 + }
597 + case 'DeclareContext': {
598 + const lvalues = [
599 + {place: value.lvalue.place, level: MemoizationLevel.Memoized},
600 + ];
601 + if (lvalue !== null) {
602 + lvalues.push({place: lvalue, level: MemoizationLevel.Unmemoized});
603 + }
604 + return {
605 + lvalues,
606 + rvalues: [],
607 + };
608 + }
609 +
610 + case 'DeclareLocal': {
611 + const lvalues = [
612 + {place: value.lvalue.place, level: MemoizationLevel.Unmemoized},
613 + ];
614 + if (lvalue !== null) {
615 + lvalues.push({place: lvalue, level: MemoizationLevel.Unmemoized});
616 + }
617 + return {
618 + lvalues,
619 + rvalues: [],
620 + };
621 + }
622 + case 'PrefixUpdate':
623 + case 'PostfixUpdate': {
624 + const lvalues = [
625 + {place: value.lvalue, level: MemoizationLevel.Conditional},
626 + ];
627 + if (lvalue !== null) {
628 + lvalues.push({place: lvalue, level: MemoizationLevel.Conditional});
629 + }
630 + return {
631 + // Indirection for the inner value, memoized if the value is
632 + lvalues,
633 + rvalues: [value.value],
634 + };
635 + }
636 + case 'StoreLocal': {
637 + const lvalues = [
638 + {place: value.lvalue.place, level: MemoizationLevel.Conditional},
639 + ];
640 + if (lvalue !== null) {
641 + lvalues.push({place: lvalue, level: MemoizationLevel.Conditional});
642 + }
643 + return {
644 + // Indirection for the inner value, memoized if the value is
645 + lvalues,
646 + rvalues: [value.value],
647 + };
648 + }
649 + case 'StoreContext': {
650 + // Should never be pruned
651 + const lvalues = [
652 + {place: value.lvalue.place, level: MemoizationLevel.Memoized},
653 + ];
654 + if (lvalue !== null) {
655 + lvalues.push({place: lvalue, level: MemoizationLevel.Conditional});
656 + }
657 +
658 + return {
659 + lvalues,
660 + rvalues: [value.value],
661 + };
662 + }
663 + case 'StoreGlobal': {
664 + const lvalues = [];
665 + if (lvalue !== null) {
666 + lvalues.push({place: lvalue, level: MemoizationLevel.Unmemoized});
667 + }
668 +
669 + return {
670 + lvalues,
671 + rvalues: [value.value],
672 + };
673 + }
674 + case 'Destructure': {
675 + // Indirection for the inner value, memoized if the value is
676 + const lvalues = [];
677 + if (lvalue !== null) {
678 + lvalues.push({place: lvalue, level: MemoizationLevel.Conditional});
679 + }
680 + lvalues.push(...computePatternLValues(value.lvalue.pattern));
681 + return {
682 + lvalues: lvalues,
683 + rvalues: [value.value],
684 + };
685 + }
686 + case 'ComputedLoad':
687 + case 'PropertyLoad': {
688 + const level = options.forceMemoizePrimitives
689 + ? MemoizationLevel.Memoized
690 + : MemoizationLevel.Conditional;
691 + return {
692 + // Indirection for the inner value, memoized if the value is
693 + lvalues: lvalue !== null ? [{place: lvalue, level}] : [],
694 + /*
695 + * Only the object is aliased to the result, and the result only needs to be
696 + * memoized if the object is
697 + */
698 + rvalues: [value.object],
699 + };
700 + }
701 + case 'ComputedStore': {
702 + /*
703 + * The object being stored to acts as an lvalue (it aliases the value), but
704 + * the computed key is not aliased
705 + */
706 + const lvalues = [
707 + {place: value.object, level: MemoizationLevel.Conditional},
708 + ];
709 + if (lvalue !== null) {
710 + lvalues.push({place: lvalue, level: MemoizationLevel.Conditional});
711 + }
712 + return {
713 + lvalues,
714 + rvalues: [value.value],
715 + };
716 + }
717 + case 'OptionalExpression': {
718 + // Indirection for the inner value, memoized if the value is
719 + const lvalues = [];
720 + if (lvalue !== null) {
721 + lvalues.push({place: lvalue, level: MemoizationLevel.Conditional});
722 + }
723 + return {
724 + lvalues: lvalues,
725 + rvalues: [
726 + ...this.computeMemoizationInputs(value.value, null).rvalues,
727 + ],
728 + };
729 + }
730 + case 'TaggedTemplateExpression': {
731 + const signature = getFunctionCallSignature(
732 + env,
733 + value.tag.identifier.type,
734 + );
735 + let lvalues = [];
736 + if (lvalue !== null) {
737 + lvalues.push({place: lvalue, level: MemoizationLevel.Memoized});
738 + }
739 + if (signature?.noAlias === true) {
740 + return {
741 + lvalues,
742 + rvalues: [],
743 + };
744 + }
745 + const operands = [...eachReactiveValueOperand(value)];
746 + lvalues.push(
747 + ...operands
748 + .filter(operand => isMutableEffect(operand.effect, operand.loc))
749 + .map(place => ({place, level: MemoizationLevel.Memoized})),
750 + );
751 + return {
752 + lvalues,
753 + rvalues: operands,
754 + };
755 + }
756 + case 'CallExpression': {
757 + const signature = getFunctionCallSignature(
758 + env,
759 + value.callee.identifier.type,
760 + );
761 + let lvalues = [];
762 + if (lvalue !== null) {
763 + lvalues.push({place: lvalue, level: MemoizationLevel.Memoized});
764 + }
765 + if (signature?.noAlias === true) {
766 + return {
767 + lvalues,
768 + rvalues: [],
769 + };
770 + }
771 + const operands = [...eachReactiveValueOperand(value)];
772 + lvalues.push(
773 + ...operands
774 + .filter(operand => isMutableEffect(operand.effect, operand.loc))
775 + .map(place => ({place, level: MemoizationLevel.Memoized})),
776 + );
777 + return {
778 + lvalues,
779 + rvalues: operands,
780 + };
781 + }
782 + case 'MethodCall': {
783 + const signature = getFunctionCallSignature(
784 + env,
785 + value.property.identifier.type,
786 + );
787 + let lvalues = [];
788 + if (lvalue !== null) {
789 + lvalues.push({place: lvalue, level: MemoizationLevel.Memoized});
790 + }
791 + if (signature?.noAlias === true) {
792 + return {
793 + lvalues,
794 + rvalues: [],
795 + };
796 + }
797 + const operands = [...eachReactiveValueOperand(value)];
798 + lvalues.push(
799 + ...operands
800 + .filter(operand => isMutableEffect(operand.effect, operand.loc))
801 + .map(place => ({place, level: MemoizationLevel.Memoized})),
802 + );
803 + return {
804 + lvalues,
805 + rvalues: operands,
806 + };
807 + }
808 + case 'RegExpLiteral':
809 + case 'ObjectMethod':
810 + case 'FunctionExpression':
811 + case 'ArrayExpression':
812 + case 'NewExpression':
813 + case 'ObjectExpression':
814 + case 'PropertyStore': {
815 + /*
816 + * All of these instructions may produce new values which must be memoized if
817 + * reachable from a return value. Any mutable rvalue may alias any other rvalue
818 + */
819 + const operands = [...eachReactiveValueOperand(value)];
820 + const lvalues = operands
821 + .filter(operand => isMutableEffect(operand.effect, operand.loc))
822 + .map(place => ({place, level: MemoizationLevel.Memoized}));
823 + if (lvalue !== null) {
824 + lvalues.push({place: lvalue, level: MemoizationLevel.Memoized});
825 + }
826 + return {
827 + lvalues,
828 + rvalues: operands,
829 + };
830 + }
831 + case 'UnsupportedNode': {
832 + CompilerError.invariant(false, {
833 + reason: `Unexpected unsupported node`,
834 + description: null,
835 + loc: value.loc,
836 + suggestions: null,
837 + });
838 + }
839 + default: {
840 + assertExhaustive(
841 + value,
842 + `Unexpected value kind \`${(value as any).kind}\``,
843 + );
844 + }
845 + }
846 + }
847 +
848 + visitValueForMemoization(
849 + id: InstructionId,
850 + value: ReactiveValue,
851 + lvalue: Place | null,
852 + ): void {
853 + const state = this.state;
854 // Determe the level of memoization for this value and the lvalues/rvalues
855 - const aliasing = computeMemoizationInputs(
856 - this.env,
857 - instruction.value,
858 - instruction.lvalue,
859 - this.options,
860 - );
855 + const aliasing = this.computeMemoizationInputs(value, lvalue);
856
857 // Associate all the rvalues with the instruction's scope if it has one
858 for (const operand of aliasing.rvalues) {
859 const operandId =
860 state.definitions.get(operand.identifier.declarationId) ??
861 operand.identifier.declarationId;
867 - state.visitOperand(instruction.id, operand, operandId);
862 + state.visitOperand(id, operand, operandId);
863 }
864
865 // Add the operands as dependencies of all lvalues.
@@ -898,22 +893,17 @@ class CollectDependenciesVisitor extends ReactiveFunctionVisitor<State> {
893 node.dependencies.add(operandId);
894 }
895
901 - state.visitOperand(instruction.id, lvalue, lvalueId);
896 + state.visitOperand(id, lvalue, lvalueId);
897 }
898
904 - if (instruction.value.kind === 'LoadLocal' && instruction.lvalue !== null) {
899 + if (value.kind === 'LoadLocal' && lvalue !== null) {
900 state.definitions.set(
906 - instruction.lvalue.identifier.declarationId,
907 - instruction.value.place.identifier.declarationId,
901 + lvalue.identifier.declarationId,
902 + value.place.identifier.declarationId,
903 );
909 - } else if (
910 - instruction.value.kind === 'CallExpression' ||
911 - instruction.value.kind === 'MethodCall'
912 - ) {
904 + } else if (value.kind === 'CallExpression' || value.kind === 'MethodCall') {
905 let callee =
914 - instruction.value.kind === 'CallExpression'
915 - ? instruction.value.callee
916 - : instruction.value.property;
906 + value.kind === 'CallExpression' ? value.callee : value.property;
907 if (getHookKind(state.env, callee.identifier) != null) {
908 const signature = getFunctionCallSignature(
909 this.env,
@@ -928,7 +918,7 @@ class CollectDependenciesVisitor extends ReactiveFunctionVisitor<State> {
918 if (signature && signature.noAlias === true) {
919 return;
920 }
931 - for (const operand of instruction.value.args) {
921 + for (const operand of value.args) {
922 const place = operand.kind === 'Spread' ? operand.place : operand;
923 state.escapingValues.add(place.identifier.declarationId);
924 }
@@ -936,16 +926,77 @@ class CollectDependenciesVisitor extends ReactiveFunctionVisitor<State> {
926 }
927 }
928
929 + override visitInstruction(
930 + instruction: ReactiveInstruction,
931 + _scopes: Array<ReactiveScope>,
932 + ): void {
933 + this.visitValueForMemoization(
934 + instruction.id,
935 + instruction.value,
936 + instruction.lvalue,
937 + );
938 + }
939 +
940 override visitTerminal(
941 stmt: ReactiveTerminalStatement<ReactiveTerminal>,
941 - state: State,
942 + scopes: Array<ReactiveScope>,
943 ): void {
943 - this.traverseTerminal(stmt, state);
944 -
944 + this.traverseTerminal(stmt, scopes);
945 if (stmt.terminal.kind === 'return') {
946 - state.escapingValues.add(stmt.terminal.value.identifier.declarationId);
946 + this.state.escapingValues.add(
947 + stmt.terminal.value.identifier.declarationId,
948 + );
949 +
950 + /*
951 + * If the return is within a scope, then those scopes must be evaluated
952 + * with the return and should be considered dependencies of the returned
953 + * value.
954 + *
955 + * This ensures that if those scopes have dependencies that those deps
956 + * are also memoized.
957 + */
958 + const identifierNode = this.state.identifiers.get(
959 + stmt.terminal.value.identifier.declarationId,
960 + );
961 + CompilerError.invariant(identifierNode !== undefined, {
962 + reason: 'Expected identifier to be initialized',
963 + description: null,
964 + loc: stmt.terminal.loc,
965 + suggestions: null,
966 + });
967 + for (const scope of scopes) {
968 + identifierNode.scopes.add(scope.id);
969 + }
970 }
971 }
972 +
973 + override visitScope(
974 + scope: ReactiveScopeBlock,
975 + scopes: Array<ReactiveScope>,
976 + ): void {
977 + /*
978 + * If a scope reassigns any variables, set the chain of active scopes as a dependency
979 + * of those variables. This ensures that if the variable escapes that we treat the
980 + * reassignment scopes — and importantly their dependencies — as needing memoization.
981 + */
982 + for (const reassignment of scope.scope.reassignments) {
983 + const identifierNode = this.state.identifiers.get(
984 + reassignment.declarationId,
985 + );
986 + CompilerError.invariant(identifierNode !== undefined, {
987 + reason: 'Expected identifier to be initialized',
988 + description: null,
989 + loc: reassignment.loc,
990 + suggestions: null,
991 + });
992 + for (const scope of scopes) {
993 + identifierNode.scopes.add(scope.id);
994 + }
995 + identifierNode.scopes.add(scope.scope.id);
996 + }
997 +
998 + this.traverseScope(scope, [...scopes, scope.scope]);
999 + }
1000 }
1001
1002 // Prune reactive scopes that do not have any memoized outputs
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-memoize-for-of-collection-when-loop-body-returns.expect.md new
+67
@@ -0,0 +1,67 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function useHook(nodeID, condition) {
6 + const graph = useContext(GraphContext);
7 + const node = nodeID != null ? graph[nodeID] : null;
8 +
9 + for (const key of Object.keys(node?.fields ?? {})) {
10 + if (condition) {
11 + return new Class(node.fields?.[field]);
12 + }
13 + }
14 + return new Class();
15 +}
16 +
17 +```
18 +
19 +## Code
20 +
21 +```javascript
22 +import { c as _c } from "react/compiler-runtime";
23 +function useHook(nodeID, condition) {
24 + const $ = _c(7);
25 + const graph = useContext(GraphContext);
26 + const node = nodeID != null ? graph[nodeID] : null;
27 + let t0;
28 + if ($[0] !== node?.fields) {
29 + t0 = Object.keys(node?.fields ?? {});
30 + $[0] = node?.fields;
31 + $[1] = t0;
32 + } else {
33 + t0 = $[1];
34 + }
35 + let t1;
36 + if ($[2] !== condition || $[3] !== node || $[4] !== t0) {
37 + t1 = Symbol.for("react.early_return_sentinel");
38 + bb0: for (const key of t0) {
39 + if (condition) {
40 + t1 = new Class(node.fields?.[field]);
41 + break bb0;
42 + }
43 + }
44 + $[2] = condition;
45 + $[3] = node;
46 + $[4] = t0;
47 + $[5] = t1;
48 + } else {
49 + t1 = $[5];
50 + }
51 + if (t1 !== Symbol.for("react.early_return_sentinel")) {
52 + return t1;
53 + }
54 + let t2;
55 + if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
56 + t2 = new Class();
57 + $[6] = t2;
58 + } else {
59 + t2 = $[6];
60 + }
61 + return t2;
62 +}
63 +
64 +```
65 +
66 +### Eval output
67 +(kind: exception) Fixture not implemented
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-memoize-for-of-collection-when-loop-body-returns.js new
+11
@@ -0,0 +1,11 @@
1 +function useHook(nodeID, condition) {
2 + const graph = useContext(GraphContext);
3 + const node = nodeID != null ? graph[nodeID] : null;
4 +
5 + for (const key of Object.keys(node?.fields ?? {})) {
6 + if (condition) {
7 + return new Class(node.fields?.[field]);
8 + }
9 + }
10 + return new Class();
11 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.memoize-loops-that-produce-memoizeable-values.expect.md new
+62
@@ -0,0 +1,62 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function useHook(nodeID, condition) {
6 + const graph = useContext(GraphContext);
7 + const node = nodeID != null ? graph[nodeID] : null;
8 +
9 + // (2) Instead we can create a scope around the loop since the loop produces an escaping value
10 + let value;
11 + for (const key of Object.keys(node?.fields ?? {})) {
12 + if (condition) {
13 + // (1) We currently create a scope just for this instruction, then later prune the scope because
14 + // it's inside a loop
15 + value = new Class(node.fields?.[field]);
16 + break;
17 + }
18 + }
19 + return value;
20 +}
21 +
22 +```
23 +
24 +## Code
25 +
26 +```javascript
27 +import { c as _c } from "react/compiler-runtime";
28 +function useHook(nodeID, condition) {
29 + const $ = _c(6);
30 + const graph = useContext(GraphContext);
31 + const node = nodeID != null ? graph[nodeID] : null;
32 +
33 + let value;
34 + let t0;
35 + if ($[0] !== node?.fields) {
36 + t0 = Object.keys(node?.fields ?? {});
37 + $[0] = node?.fields;
38 + $[1] = t0;
39 + } else {
40 + t0 = $[1];
41 + }
42 + if ($[2] !== condition || $[3] !== node || $[4] !== t0) {
43 + for (const key of t0) {
44 + if (condition) {
45 + value = new Class(node.fields?.[field]);
46 + break;
47 + }
48 + }
49 + $[2] = condition;
50 + $[3] = node;
51 + $[4] = t0;
52 + $[5] = value;
53 + } else {
54 + value = $[5];
55 + }
56 + return value;
57 +}
58 +
59 +```
60 +
61 +### Eval output
62 +(kind: exception) Fixture not implemented
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.memoize-loops-that-produce-memoizeable-values.js new
+16
@@ -0,0 +1,16 @@
1 +function useHook(nodeID, condition) {
2 + const graph = useContext(GraphContext);
3 + const node = nodeID != null ? graph[nodeID] : null;
4 +
5 + // (2) Instead we can create a scope around the loop since the loop produces an escaping value
6 + let value;
7 + for (const key of Object.keys(node?.fields ?? {})) {
8 + if (condition) {
9 + // (1) We currently create a scope just for this instruction, then later prune the scope because
10 + // it's inside a loop
11 + value = new Class(node.fields?.[field]);
12 + break;
13 + }
14 + }
15 + return value;
16 +}