@samitouri / QOS-React / commits / 6584a6eec4

[compiler] Hoist dependencies from functions more conservatively (#32616)

Alternative to facebook/react#31584 which sets enableTreatFunctionDepsAsConditional:true` by default. This PR changes dependency hoisting to be more conservative while trying to preserve an optimal "happy path". We assume that a function "is likely called" if we observe the following in the react function body. - a direct callsite - passed directly as a jsx attribute or child - passed directly to a hook - a direct return A function is also "likely called" if it is directly called, passed to jsx / hooks, or returned from another function that "is likely called". Note that this approach marks the function definition site with its hoistable properties (not its use site). I tried implementing use-site hoisting semantics, but it felt both unpredictable (i.e. as a developer, I can't trust that callbacks are well memoized) and not helpful (type + null checks of a value are usually colocated with their use site) In this fixture (copied here for easy reference), it should be safe to use `a.value` and `b.value` as dependencies, even though these functions are conditionally called. ```js // inner-function/nullable-objects/assume-invoked/conditional-call-chain.tsx function Component({a, b}) { const logA = () => { console.log(a.value); }; const logB = () => { console.log(b.value); }; const hasLogged = useRef(false); const log = () => { if (!hasLogged.current) { logA(); logB(); hasLogged.current = true; } }; return <Stringify log={log} shouldInvokeFns={true} />; } ``` On the other hand, this means that we produce invalid output for code like manually implementing `Array.map` ```js // inner-function/nullable-objects/bug-invalid-array-map-manual.js function useFoo({arr1, arr2}) { const cb = e => arr2[0].value + e.value; const y = []; for (let i = 0; i < arr1.length; i++) { y.push(cb(arr1[i])); } return y; } ```

mofeiZ committed Mar 18, 2025 at 18:00 UTC 6584a6eec488a7a155fe2231874aecf178b07a9a
41 files changed +2107 -53
compiler/packages/babel-plugin-react-compiler/src/HIR/CollectHoistablePropertyLoads.ts
+167 -26
@@ -13,11 +13,13 @@ import {
13 BlockId,
14 DependencyPathEntry,
15 GeneratedSource,
16 + getHookKind,
17 HIRFunction,
18 Identifier,
19 IdentifierId,
20 InstructionId,
21 InstructionValue,
22 + LoweredFunction,
23 PropertyLiteral,
24 ReactiveScopeDependency,
25 ScopeId,
@@ -112,6 +114,9 @@ export function collectHoistablePropertyLoads(
114 hoistableFromOptionals,
115 registry,
116 nestedFnImmutableContext: null,
117 + assumedInvokedFns: fn.env.config.enableTreatFunctionDepsAsConditional
118 + ? new Set()
119 + : getAssumedInvokedFunctions(fn),
120 });
121 }
122
@@ -127,6 +132,11 @@ type CollectHoistablePropertyLoadsContext = {
132 * but are currently kept separate for readability.
133 */
134 nestedFnImmutableContext: ReadonlySet<IdentifierId> | null;
135 + /**
136 + * Functions which are assumed to be eventually called (as opposed to ones which might
137 + * not be called, e.g. the 0th argument of Array.map)
138 + */
139 + assumedInvokedFns: ReadonlySet<LoweredFunction>;
140 };
141 function collectHoistablePropertyLoadsImpl(
142 fn: HIRFunction,
@@ -338,7 +348,13 @@ function collectNonNullsInBlocks(
348 context.registry.getOrCreateIdentifier(identifier),
349 );
350 }
341 - const nodes = new Map<BlockId, BlockInfo>();
351 + const nodes = new Map<
352 + BlockId,
353 + {
354 + block: BasicBlock;
355 + assumedNonNullObjects: Set<PropertyPathNode>;
356 + }
357 + >();
358 for (const [_, block] of fn.body.blocks) {
359 const assumedNonNullObjects = new Set<PropertyPathNode>(
360 knownNonNullIdentifiers,
@@ -358,32 +374,30 @@ function collectNonNullsInBlocks(
374 ) {
375 assumedNonNullObjects.add(maybeNonNull);
376 }
361 - if (
362 - (instr.value.kind === 'FunctionExpression' ||
363 - instr.value.kind === 'ObjectMethod') &&
364 - !fn.env.config.enableTreatFunctionDepsAsConditional
365 - ) {
377 + if (instr.value.kind === 'FunctionExpression') {
378 const innerFn = instr.value.loweredFunc;
367 - const innerHoistableMap = collectHoistablePropertyLoadsImpl(
368 - innerFn.func,
369 - {
370 - ...context,
371 - nestedFnImmutableContext:
372 - context.nestedFnImmutableContext ??
373 - new Set(
374 - innerFn.func.context
375 - .filter(place =>
376 - isImmutableAtInstr(place.identifier, instr.id, context),
377 - )
378 - .map(place => place.identifier.id),
379 - ),
380 - },
381 - );
382 - const innerHoistables = assertNonNull(
383 - innerHoistableMap.get(innerFn.func.body.entry),
384 - );
385 - for (const entry of innerHoistables.assumedNonNullObjects) {
386 - assumedNonNullObjects.add(entry);
379 + if (context.assumedInvokedFns.has(innerFn)) {
380 + const innerHoistableMap = collectHoistablePropertyLoadsImpl(
381 + innerFn.func,
382 + {
383 + ...context,
384 + nestedFnImmutableContext:
385 + context.nestedFnImmutableContext ??
386 + new Set(
387 + innerFn.func.context
388 + .filter(place =>
389 + isImmutableAtInstr(place.identifier, instr.id, context),
390 + )
391 + .map(place => place.identifier.id),
392 + ),
393 + },
394 + );
395 + const innerHoistables = assertNonNull(
396 + innerHoistableMap.get(innerFn.func.body.entry),
397 + );
398 + for (const entry of innerHoistables.assumedNonNullObjects) {
399 + assumedNonNullObjects.add(entry);
400 + }
401 }
402 }
403 }
@@ -591,3 +605,130 @@ function reduceMaybeOptionalChains(
605 }
606 } while (changed);
607 }
608 +
609 +function getAssumedInvokedFunctions(
610 + fn: HIRFunction,
611 + temporaries: Map<
612 + IdentifierId,
613 + {fn: LoweredFunction; mayInvoke: Set<LoweredFunction>}
614 + > = new Map(),
615 +): ReadonlySet<LoweredFunction> {
616 + const hoistableFunctions = new Set<LoweredFunction>();
617 + /**
618 + * Step 1: Conservatively collect identifier to function expression mappings
619 + */
620 + for (const block of fn.body.blocks.values()) {
621 + for (const {lvalue, value} of block.instructions) {
622 + /**
623 + * Conservatively only match function expressions which can have guaranteed ssa.
624 + * ObjectMethods and ObjectProperties do not.
625 + */
626 + if (value.kind === 'FunctionExpression') {
627 + temporaries.set(lvalue.identifier.id, {
628 + fn: value.loweredFunc,
629 + mayInvoke: new Set(),
630 + });
631 + } else if (value.kind === 'StoreLocal') {
632 + const lvalue = value.lvalue.place.identifier;
633 + const maybeLoweredFunc = temporaries.get(value.value.identifier.id);
634 + if (maybeLoweredFunc != null) {
635 + temporaries.set(lvalue.id, maybeLoweredFunc);
636 + }
637 + } else if (value.kind === 'LoadLocal') {
638 + const maybeLoweredFunc = temporaries.get(value.place.identifier.id);
639 + if (maybeLoweredFunc != null) {
640 + temporaries.set(lvalue.identifier.id, maybeLoweredFunc);
641 + }
642 + }
643 + }
644 + }
645 + /**
646 + * Step 2: Forward pass to do analysis of assumed function calls. Note that
647 + * this is conservative and does not count indirect references through
648 + * containers (e.g. `return {cb: () => {...}})`).
649 + */
650 + for (const block of fn.body.blocks.values()) {
651 + for (const {lvalue, value} of block.instructions) {
652 + if (value.kind === 'CallExpression') {
653 + const callee = value.callee;
654 + const maybeHook = getHookKind(fn.env, callee.identifier);
655 + const maybeLoweredFunc = temporaries.get(callee.identifier.id);
656 + if (maybeLoweredFunc != null) {
657 + // Direct calls
658 + hoistableFunctions.add(maybeLoweredFunc.fn);
659 + } else if (maybeHook != null) {
660 + /**
661 + * Assume arguments to all hooks are safe to invoke
662 + */
663 + for (const arg of value.args) {
664 + if (arg.kind === 'Identifier') {
665 + const maybeLoweredFunc = temporaries.get(arg.identifier.id);
666 + if (maybeLoweredFunc != null) {
667 + hoistableFunctions.add(maybeLoweredFunc.fn);
668 + }
669 + }
670 + }
671 + }
672 + } else if (value.kind === 'JsxExpression') {
673 + /**
674 + * Assume JSX attributes and children are safe to invoke
675 + */
676 + for (const attr of value.props) {
677 + if (attr.kind === 'JsxSpreadAttribute') {
678 + continue;
679 + }
680 + const maybeLoweredFunc = temporaries.get(attr.place.identifier.id);
681 + if (maybeLoweredFunc != null) {
682 + hoistableFunctions.add(maybeLoweredFunc.fn);
683 + }
684 + }
685 + for (const child of value.children ?? []) {
686 + const maybeLoweredFunc = temporaries.get(child.identifier.id);
687 + if (maybeLoweredFunc != null) {
688 + hoistableFunctions.add(maybeLoweredFunc.fn);
689 + }
690 + }
691 + } else if (value.kind === 'FunctionExpression') {
692 + /**
693 + * Recursively traverse into other function expressions which may invoke
694 + * or pass already declared functions to react (e.g. as JSXAttributes).
695 + *
696 + * If lambda A calls lambda B, we assume lambda B is safe to invoke if
697 + * lambda A is -- even if lambda B is conditionally called. (see
698 + * `conditional-call-chain` fixture for example).
699 + */
700 + const loweredFunc = value.loweredFunc.func;
701 + const lambdasCalled = getAssumedInvokedFunctions(
702 + loweredFunc,
703 + temporaries,
704 + );
705 + const maybeLoweredFunc = temporaries.get(lvalue.identifier.id);
706 + if (maybeLoweredFunc != null) {
707 + for (const called of lambdasCalled) {
708 + maybeLoweredFunc.mayInvoke.add(called);
709 + }
710 + }
711 + }
712 + }
713 + if (block.terminal.kind === 'return') {
714 + /**
715 + * Assume directly returned functions are safe to call
716 + */
717 + const maybeLoweredFunc = temporaries.get(
718 + block.terminal.value.identifier.id,
719 + );
720 + if (maybeLoweredFunc != null) {
721 + hoistableFunctions.add(maybeLoweredFunc.fn);
722 + }
723 + }
724 + }
725 +
726 + for (const [_, {fn, mayInvoke}] of temporaries) {
727 + if (hoistableFunctions.has(fn)) {
728 + for (const called of mayInvoke) {
729 + hoistableFunctions.add(called);
730 + }
731 + }
732 + }
733 + return hoistableFunctions;
734 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/function-expression-prototype-call.expect.md
+2 -2
@@ -23,11 +23,11 @@ import { c as _c } from "react/compiler-runtime";
23 function Component(props) {
24 const $ = _c(4);
25 let t0;
26 - if ($[0] !== props.name) {
26 + if ($[0] !== props) {
27 t0 = function () {
28 return <div>{props.name}</div>;
29 };
30 - $[0] = props.name;
30 + $[0] = props;
31 $[1] = t0;
32 } else {
33 t0 = $[1];
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/array-map-named-callback-cross-context.expect.md new
+133
@@ -0,0 +1,133 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import {Stringify} from 'shared-runtime';
6 +
7 +/**
8 + * Forked from array-map-simple.js
9 + *
10 + * Named lambdas (e.g. cb1) may be defined in the top scope of a function and
11 + * used in a different lambda (getArrMap1).
12 + *
13 + * Here, we should try to determine if cb1 is actually called. In this case:
14 + * - getArrMap1 is assumed to be called as it's passed to JSX
15 + * - cb1 is not assumed to be called since it's only used as a call operand
16 + */
17 +function useFoo({arr1, arr2}) {
18 + const cb1 = e => arr1[0].value + e.value;
19 + const getArrMap1 = () => arr1.map(cb1);
20 + const cb2 = e => arr2[0].value + e.value;
21 + const getArrMap2 = () => arr1.map(cb2);
22 + return (
23 + <Stringify
24 + getArrMap1={getArrMap1}
25 + getArrMap2={getArrMap2}
26 + shouldInvokeFns={true}
27 + />
28 + );
29 +}
30 +
31 +export const FIXTURE_ENTRYPOINT = {
32 + fn: useFoo,
33 + params: [{arr1: [], arr2: []}],
34 + sequentialRenders: [
35 + {arr1: [], arr2: []},
36 + {arr1: [], arr2: null},
37 + {arr1: [{value: 1}, {value: 2}], arr2: [{value: -1}]},
38 + ],
39 +};
40 +
41 +```
42 +
43 +## Code
44 +
45 +```javascript
46 +import { c as _c } from "react/compiler-runtime";
47 +import { Stringify } from "shared-runtime";
48 +
49 +/**
50 + * Forked from array-map-simple.js
51 + *
52 + * Named lambdas (e.g. cb1) may be defined in the top scope of a function and
53 + * used in a different lambda (getArrMap1).
54 + *
55 + * Here, we should try to determine if cb1 is actually called. In this case:
56 + * - getArrMap1 is assumed to be called as it's passed to JSX
57 + * - cb1 is not assumed to be called since it's only used as a call operand
58 + */
59 +function useFoo(t0) {
60 + const $ = _c(13);
61 + const { arr1, arr2 } = t0;
62 + let t1;
63 + if ($[0] !== arr1[0]) {
64 + t1 = (e) => arr1[0].value + e.value;
65 + $[0] = arr1[0];
66 + $[1] = t1;
67 + } else {
68 + t1 = $[1];
69 + }
70 + const cb1 = t1;
71 + let t2;
72 + if ($[2] !== arr1 || $[3] !== cb1) {
73 + t2 = () => arr1.map(cb1);
74 + $[2] = arr1;
75 + $[3] = cb1;
76 + $[4] = t2;
77 + } else {
78 + t2 = $[4];
79 + }
80 + const getArrMap1 = t2;
81 + let t3;
82 + if ($[5] !== arr2) {
83 + t3 = (e_0) => arr2[0].value + e_0.value;
84 + $[5] = arr2;
85 + $[6] = t3;
86 + } else {
87 + t3 = $[6];
88 + }
89 + const cb2 = t3;
90 + let t4;
91 + if ($[7] !== arr1 || $[8] !== cb2) {
92 + t4 = () => arr1.map(cb2);
93 + $[7] = arr1;
94 + $[8] = cb2;
95 + $[9] = t4;
96 + } else {
97 + t4 = $[9];
98 + }
99 + const getArrMap2 = t4;
100 + let t5;
101 + if ($[10] !== getArrMap1 || $[11] !== getArrMap2) {
102 + t5 = (
103 + <Stringify
104 + getArrMap1={getArrMap1}
105 + getArrMap2={getArrMap2}
106 + shouldInvokeFns={true}
107 + />
108 + );
109 + $[10] = getArrMap1;
110 + $[11] = getArrMap2;
111 + $[12] = t5;
112 + } else {
113 + t5 = $[12];
114 + }
115 + return t5;
116 +}
117 +
118 +export const FIXTURE_ENTRYPOINT = {
119 + fn: useFoo,
120 + params: [{ arr1: [], arr2: [] }],
121 + sequentialRenders: [
122 + { arr1: [], arr2: [] },
123 + { arr1: [], arr2: null },
124 + { arr1: [{ value: 1 }, { value: 2 }], arr2: [{ value: -1 }] },
125 + ],
126 +};
127 +
128 +```
129 +
130 +### Eval output
131 +(kind: ok) <div>{"getArrMap1":{"kind":"Function","result":[]},"getArrMap2":{"kind":"Function","result":[]},"shouldInvokeFns":true}</div>
132 +<div>{"getArrMap1":{"kind":"Function","result":[]},"getArrMap2":{"kind":"Function","result":[]},"shouldInvokeFns":true}</div>
133 +<div>{"getArrMap1":{"kind":"Function","result":[2,3]},"getArrMap2":{"kind":"Function","result":[0,1]},"shouldInvokeFns":true}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/array-map-named-callback-cross-context.js new
+35
@@ -0,0 +1,35 @@
1 +import {Stringify} from 'shared-runtime';
2 +
3 +/**
4 + * Forked from array-map-simple.js
5 + *
6 + * Named lambdas (e.g. cb1) may be defined in the top scope of a function and
7 + * used in a different lambda (getArrMap1).
8 + *
9 + * Here, we should try to determine if cb1 is actually called. In this case:
10 + * - getArrMap1 is assumed to be called as it's passed to JSX
11 + * - cb1 is not assumed to be called since it's only used as a call operand
12 + */
13 +function useFoo({arr1, arr2}) {
14 + const cb1 = e => arr1[0].value + e.value;
15 + const getArrMap1 = () => arr1.map(cb1);
16 + const cb2 = e => arr2[0].value + e.value;
17 + const getArrMap2 = () => arr1.map(cb2);
18 + return (
19 + <Stringify
20 + getArrMap1={getArrMap1}
21 + getArrMap2={getArrMap2}
22 + shouldInvokeFns={true}
23 + />
24 + );
25 +}
26 +
27 +export const FIXTURE_ENTRYPOINT = {
28 + fn: useFoo,
29 + params: [{arr1: [], arr2: []}],
30 + sequentialRenders: [
31 + {arr1: [], arr2: []},
32 + {arr1: [], arr2: null},
33 + {arr1: [{value: 1}, {value: 2}], arr2: [{value: -1}]},
34 + ],
35 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/array-map-named-callback.expect.md new
+108
@@ -0,0 +1,108 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +/**
6 + * Forked from array-map-simple.js
7 + *
8 + * Whether lambdas are named or passed inline shouldn't affect whether we expect
9 + * it to be called.
10 + */
11 +function useFoo({arr1, arr2}) {
12 + const cb1 = e => arr1[0].value + e.value;
13 + const x = arr1.map(cb1);
14 + const cb2 = e => arr2[0].value + e.value;
15 + const y = arr1.map(cb2);
16 + return [x, y];
17 +}
18 +
19 +export const FIXTURE_ENTRYPOINT = {
20 + fn: useFoo,
21 + params: [{arr1: [], arr2: []}],
22 + sequentialRenders: [
23 + {arr1: [], arr2: []},
24 + {arr1: [], arr2: null},
25 + {arr1: [{value: 1}, {value: 2}], arr2: [{value: -1}]},
26 + ],
27 +};
28 +
29 +```
30 +
31 +## Code
32 +
33 +```javascript
34 +import { c as _c } from "react/compiler-runtime"; /**
35 + * Forked from array-map-simple.js
36 + *
37 + * Whether lambdas are named or passed inline shouldn't affect whether we expect
38 + * it to be called.
39 + */
40 +function useFoo(t0) {
41 + const $ = _c(13);
42 + const { arr1, arr2 } = t0;
43 + let t1;
44 + if ($[0] !== arr1[0]) {
45 + t1 = (e) => arr1[0].value + e.value;
46 + $[0] = arr1[0];
47 + $[1] = t1;
48 + } else {
49 + t1 = $[1];
50 + }
51 + const cb1 = t1;
52 + let t2;
53 + if ($[2] !== arr1 || $[3] !== cb1) {
54 + t2 = arr1.map(cb1);
55 + $[2] = arr1;
56 + $[3] = cb1;
57 + $[4] = t2;
58 + } else {
59 + t2 = $[4];
60 + }
61 + const x = t2;
62 + let t3;
63 + if ($[5] !== arr2) {
64 + t3 = (e_0) => arr2[0].value + e_0.value;
65 + $[5] = arr2;
66 + $[6] = t3;
67 + } else {
68 + t3 = $[6];
69 + }
70 + const cb2 = t3;
71 + let t4;
72 + if ($[7] !== arr1 || $[8] !== cb2) {
73 + t4 = arr1.map(cb2);
74 + $[7] = arr1;
75 + $[8] = cb2;
76 + $[9] = t4;
77 + } else {
78 + t4 = $[9];
79 + }
80 + const y = t4;
81 + let t5;
82 + if ($[10] !== x || $[11] !== y) {
83 + t5 = [x, y];
84 + $[10] = x;
85 + $[11] = y;
86 + $[12] = t5;
87 + } else {
88 + t5 = $[12];
89 + }
90 + return t5;
91 +}
92 +
93 +export const FIXTURE_ENTRYPOINT = {
94 + fn: useFoo,
95 + params: [{ arr1: [], arr2: [] }],
96 + sequentialRenders: [
97 + { arr1: [], arr2: [] },
98 + { arr1: [], arr2: null },
99 + { arr1: [{ value: 1 }, { value: 2 }], arr2: [{ value: -1 }] },
100 + ],
101 +};
102 +
103 +```
104 +
105 +### Eval output
106 +(kind: ok) [[],[]]
107 +[[],[]]
108 +[[2,3],[0,1]]
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/array-map-named-callback.js new
+23
@@ -0,0 +1,23 @@
1 +/**
2 + * Forked from array-map-simple.js
3 + *
4 + * Whether lambdas are named or passed inline shouldn't affect whether we expect
5 + * it to be called.
6 + */
7 +function useFoo({arr1, arr2}) {
8 + const cb1 = e => arr1[0].value + e.value;
9 + const x = arr1.map(cb1);
10 + const cb2 = e => arr2[0].value + e.value;
11 + const y = arr1.map(cb2);
12 + return [x, y];
13 +}
14 +
15 +export const FIXTURE_ENTRYPOINT = {
16 + fn: useFoo,
17 + params: [{arr1: [], arr2: []}],
18 + sequentialRenders: [
19 + {arr1: [], arr2: []},
20 + {arr1: [], arr2: null},
21 + {arr1: [{value: 1}, {value: 2}], arr2: [{value: -1}]},
22 + ],
23 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/array-map-named-chained-callbacks.expect.md new
+130
@@ -0,0 +1,130 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +/**
6 + * Forked from array-map-simple.js
7 + *
8 + * Here, getVal1 has a known callsite in `cb1`, but `cb1` isn't known to be
9 + * called (it's only passed to array.map). In this case, we should be
10 + * conservative and assume that all named lambdas are conditionally called.
11 + */
12 +function useFoo({arr1, arr2}) {
13 + const getVal1 = () => arr1[0].value;
14 + const cb1 = e => getVal1() + e.value;
15 + const x = arr1.map(cb1);
16 + const getVal2 = () => arr2[0].value;
17 + const cb2 = e => getVal2() + e.value;
18 + const y = arr1.map(cb2);
19 + return [x, y];
20 +}
21 +
22 +export const FIXTURE_ENTRYPOINT = {
23 + fn: useFoo,
24 + params: [{arr1: [], arr2: []}],
25 + sequentialRenders: [
26 + {arr1: [], arr2: []},
27 + {arr1: [], arr2: null},
28 + {arr1: [{value: 1}, {value: 2}], arr2: [{value: -1}]},
29 + ],
30 +};
31 +
32 +```
33 +
34 +## Code
35 +
36 +```javascript
37 +import { c as _c } from "react/compiler-runtime"; /**
38 + * Forked from array-map-simple.js
39 + *
40 + * Here, getVal1 has a known callsite in `cb1`, but `cb1` isn't known to be
41 + * called (it's only passed to array.map). In this case, we should be
42 + * conservative and assume that all named lambdas are conditionally called.
43 + */
44 +function useFoo(t0) {
45 + const $ = _c(17);
46 + const { arr1, arr2 } = t0;
47 + let t1;
48 + if ($[0] !== arr1[0]) {
49 + t1 = () => arr1[0].value;
50 + $[0] = arr1[0];
51 + $[1] = t1;
52 + } else {
53 + t1 = $[1];
54 + }
55 + const getVal1 = t1;
56 + let t2;
57 + if ($[2] !== getVal1) {
58 + t2 = (e) => getVal1() + e.value;
59 + $[2] = getVal1;
60 + $[3] = t2;
61 + } else {
62 + t2 = $[3];
63 + }
64 + const cb1 = t2;
65 + let t3;
66 + if ($[4] !== arr1 || $[5] !== cb1) {
67 + t3 = arr1.map(cb1);
68 + $[4] = arr1;
69 + $[5] = cb1;
70 + $[6] = t3;
71 + } else {
72 + t3 = $[6];
73 + }
74 + const x = t3;
75 + let t4;
76 + if ($[7] !== arr2) {
77 + t4 = () => arr2[0].value;
78 + $[7] = arr2;
79 + $[8] = t4;
80 + } else {
81 + t4 = $[8];
82 + }
83 + const getVal2 = t4;
84 + let t5;
85 + if ($[9] !== getVal2) {
86 + t5 = (e_0) => getVal2() + e_0.value;
87 + $[9] = getVal2;
88 + $[10] = t5;
89 + } else {
90 + t5 = $[10];
91 + }
92 + const cb2 = t5;
93 + let t6;
94 + if ($[11] !== arr1 || $[12] !== cb2) {
95 + t6 = arr1.map(cb2);
96 + $[11] = arr1;
97 + $[12] = cb2;
98 + $[13] = t6;
99 + } else {
100 + t6 = $[13];
101 + }
102 + const y = t6;
103 + let t7;
104 + if ($[14] !== x || $[15] !== y) {
105 + t7 = [x, y];
106 + $[14] = x;
107 + $[15] = y;
108 + $[16] = t7;
109 + } else {
110 + t7 = $[16];
111 + }
112 + return t7;
113 +}
114 +
115 +export const FIXTURE_ENTRYPOINT = {
116 + fn: useFoo,
117 + params: [{ arr1: [], arr2: [] }],
118 + sequentialRenders: [
119 + { arr1: [], arr2: [] },
120 + { arr1: [], arr2: null },
121 + { arr1: [{ value: 1 }, { value: 2 }], arr2: [{ value: -1 }] },
122 + ],
123 +};
124 +
125 +```
126 +
127 +### Eval output
128 +(kind: ok) [[],[]]
129 +[[],[]]
130 +[[2,3],[0,1]]
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/array-map-named-chained-callbacks.js new
+26
@@ -0,0 +1,26 @@
1 +/**
2 + * Forked from array-map-simple.js
3 + *
4 + * Here, getVal1 has a known callsite in `cb1`, but `cb1` isn't known to be
5 + * called (it's only passed to array.map). In this case, we should be
6 + * conservative and assume that all named lambdas are conditionally called.
7 + */
8 +function useFoo({arr1, arr2}) {
9 + const getVal1 = () => arr1[0].value;
10 + const cb1 = e => getVal1() + e.value;
11 + const x = arr1.map(cb1);
12 + const getVal2 = () => arr2[0].value;
13 + const cb2 = e => getVal2() + e.value;
14 + const y = arr1.map(cb2);
15 + return [x, y];
16 +}
17 +
18 +export const FIXTURE_ENTRYPOINT = {
19 + fn: useFoo,
20 + params: [{arr1: [], arr2: []}],
21 + sequentialRenders: [
22 + {arr1: [], arr2: []},
23 + {arr1: [], arr2: null},
24 + {arr1: [{value: 1}, {value: 2}], arr2: [{value: -1}]},
25 + ],
26 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/array-map-simple.expect.md new
+111
@@ -0,0 +1,111 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +/**
6 + * Test that we're not hoisting property reads from lambdas that are created to
7 + * pass to opaque functions, which often have maybe-invoke semantics.
8 + *
9 + * In this example, we shouldn't hoist `arr[0].value` out of the lambda.
10 + * ```js
11 + * e => arr[0].value + e.value <-- created to pass to map
12 + * arr.map(<cb>) <-- argument only invoked if array is non-empty
13 + * ```
14 + */
15 +function useFoo({arr1, arr2}) {
16 + const x = arr1.map(e => arr1[0].value + e.value);
17 + const y = arr1.map(e => arr2[0].value + e.value);
18 + return [x, y];
19 +}
20 +
21 +export const FIXTURE_ENTRYPOINT = {
22 + fn: useFoo,
23 + params: [{arr1: [], arr2: []}],
24 + sequentialRenders: [
25 + {arr1: [], arr2: []},
26 + {arr1: [], arr2: null},
27 + {arr1: [{value: 1}, {value: 2}], arr2: [{value: -1}]},
28 + ],
29 +};
30 +
31 +```
32 +
33 +## Code
34 +
35 +```javascript
36 +import { c as _c } from "react/compiler-runtime"; /**
37 + * Test that we're not hoisting property reads from lambdas that are created to
38 + * pass to opaque functions, which often have maybe-invoke semantics.
39 + *
40 + * In this example, we shouldn't hoist `arr[0].value` out of the lambda.
41 + * ```js
42 + * e => arr[0].value + e.value <-- created to pass to map
43 + * arr.map(<cb>) <-- argument only invoked if array is non-empty
44 + * ```
45 + */
46 +function useFoo(t0) {
47 + const $ = _c(12);
48 + const { arr1, arr2 } = t0;
49 + let t1;
50 + if ($[0] !== arr1) {
51 + let t2;
52 + if ($[2] !== arr1[0]) {
53 + t2 = (e) => arr1[0].value + e.value;
54 + $[2] = arr1[0];
55 + $[3] = t2;
56 + } else {
57 + t2 = $[3];
58 + }
59 + t1 = arr1.map(t2);
60 + $[0] = arr1;
61 + $[1] = t1;
62 + } else {
63 + t1 = $[1];
64 + }
65 + const x = t1;
66 + let t2;
67 + if ($[4] !== arr1 || $[5] !== arr2) {
68 + let t3;
69 + if ($[7] !== arr2) {
70 + t3 = (e_0) => arr2[0].value + e_0.value;
71 + $[7] = arr2;
72 + $[8] = t3;
73 + } else {
74 + t3 = $[8];
75 + }
76 + t2 = arr1.map(t3);
77 + $[4] = arr1;
78 + $[5] = arr2;
79 + $[6] = t2;
80 + } else {
81 + t2 = $[6];
82 + }
83 + const y = t2;
84 + let t3;
85 + if ($[9] !== x || $[10] !== y) {
86 + t3 = [x, y];
87 + $[9] = x;
88 + $[10] = y;
89 + $[11] = t3;
90 + } else {
91 + t3 = $[11];
92 + }
93 + return t3;
94 +}
95 +
96 +export const FIXTURE_ENTRYPOINT = {
97 + fn: useFoo,
98 + params: [{ arr1: [], arr2: [] }],
99 + sequentialRenders: [
100 + { arr1: [], arr2: [] },
101 + { arr1: [], arr2: null },
102 + { arr1: [{ value: 1 }, { value: 2 }], arr2: [{ value: -1 }] },
103 + ],
104 +};
105 +
106 +```
107 +
108 +### Eval output
109 +(kind: ok) [[],[]]
110 +[[],[]]
111 +[[2,3],[0,1]]
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/array-map-simple.js new
+25
@@ -0,0 +1,25 @@
1 +/**
2 + * Test that we're not hoisting property reads from lambdas that are created to
3 + * pass to opaque functions, which often have maybe-invoke semantics.
4 + *
5 + * In this example, we shouldn't hoist `arr[0].value` out of the lambda.
6 + * ```js
7 + * e => arr[0].value + e.value <-- created to pass to map
8 + * arr.map(<cb>) <-- argument only invoked if array is non-empty
9 + * ```
10 + */
11 +function useFoo({arr1, arr2}) {
12 + const x = arr1.map(e => arr1[0].value + e.value);
13 + const y = arr1.map(e => arr2[0].value + e.value);
14 + return [x, y];
15 +}
16 +
17 +export const FIXTURE_ENTRYPOINT = {
18 + fn: useFoo,
19 + params: [{arr1: [], arr2: []}],
20 + sequentialRenders: [
21 + {arr1: [], arr2: []},
22 + {arr1: [], arr2: null},
23 + {arr1: [{value: 1}, {value: 2}], arr2: [{value: -1}]},
24 + ],
25 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/conditional-call-chain.expect.md new
+112
@@ -0,0 +1,112 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import {useRef} from 'react';
6 +import {Stringify} from 'shared-runtime';
7 +
8 +function Component({a, b}) {
9 + const logA = () => {
10 + console.log(a.value);
11 + };
12 + const logB = () => {
13 + console.log(b.value);
14 + };
15 + const hasLogged = useRef(false);
16 + const log = () => {
17 + if (!hasLogged.current) {
18 + logA();
19 + logB();
20 + hasLogged.current = true;
21 + }
22 + };
23 + return <Stringify log={log} shouldInvokeFns={true} />;
24 +}
25 +
26 +export const FIXTURE_ENTRYPOINT = {
27 + fn: Component,
28 + params: [{a: {value: 1}, b: {value: 2}}],
29 + sequentialRenders: [
30 + {a: {value: 1}, b: {value: 2}},
31 + {a: {value: 3}, b: {value: 4}},
32 + ],
33 +};
34 +
35 +```
36 +
37 +## Code
38 +
39 +```javascript
40 +import { c as _c } from "react/compiler-runtime";
41 +import { useRef } from "react";
42 +import { Stringify } from "shared-runtime";
43 +
44 +function Component(t0) {
45 + const $ = _c(9);
46 + const { a, b } = t0;
47 + let t1;
48 + if ($[0] !== a.value) {
49 + t1 = () => {
50 + console.log(a.value);
51 + };
52 + $[0] = a.value;
53 + $[1] = t1;
54 + } else {
55 + t1 = $[1];
56 + }
57 + const logA = t1;
58 + let t2;
59 + if ($[2] !== b.value) {
60 + t2 = () => {
61 + console.log(b.value);
62 + };
63 + $[2] = b.value;
64 + $[3] = t2;
65 + } else {
66 + t2 = $[3];
67 + }
68 + const logB = t2;
69 +
70 + const hasLogged = useRef(false);
71 + let t3;
72 + if ($[4] !== logA || $[5] !== logB) {
73 + t3 = () => {
74 + if (!hasLogged.current) {
75 + logA();
76 + logB();
77 + hasLogged.current = true;
78 + }
79 + };
80 + $[4] = logA;
81 + $[5] = logB;
82 + $[6] = t3;
83 + } else {
84 + t3 = $[6];
85 + }
86 + const log = t3;
87 + let t4;
88 + if ($[7] !== log) {
89 + t4 = <Stringify log={log} shouldInvokeFns={true} />;
90 + $[7] = log;
91 + $[8] = t4;
92 + } else {
93 + t4 = $[8];
94 + }
95 + return t4;
96 +}
97 +
98 +export const FIXTURE_ENTRYPOINT = {
99 + fn: Component,
100 + params: [{ a: { value: 1 }, b: { value: 2 } }],
101 + sequentialRenders: [
102 + { a: { value: 1 }, b: { value: 2 } },
103 + { a: { value: 3 }, b: { value: 4 } },
104 + ],
105 +};
106 +
107 +```
108 +
109 +### Eval output
110 +(kind: ok) <div>{"log":{"kind":"Function"},"shouldInvokeFns":true}</div>
111 +<div>{"log":{"kind":"Function"},"shouldInvokeFns":true}</div>
112 +logs: [1,2]
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/conditional-call-chain.tsx new
+29
@@ -0,0 +1,29 @@
1 +import {useRef} from 'react';
2 +import {Stringify} from 'shared-runtime';
3 +
4 +function Component({a, b}) {
5 + const logA = () => {
6 + console.log(a.value);
7 + };
8 + const logB = () => {
9 + console.log(b.value);
10 + };
11 + const hasLogged = useRef(false);
12 + const log = () => {
13 + if (!hasLogged.current) {
14 + logA();
15 + logB();
16 + hasLogged.current = true;
17 + }
18 + };
19 + return <Stringify log={log} shouldInvokeFns={true} />;
20 +}
21 +
22 +export const FIXTURE_ENTRYPOINT = {
23 + fn: Component,
24 + params: [{a: {value: 1}, b: {value: 2}}],
25 + sequentialRenders: [
26 + {a: {value: 1}, b: {value: 2}},
27 + {a: {value: 3}, b: {value: 4}},
28 + ],
29 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/conditional-call.expect.md new
+85
@@ -0,0 +1,85 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import {useState} from 'react';
6 +import {useIdentity} from 'shared-runtime';
7 +
8 +/**
9 + * Assume that conditionally called functions can be invoked and that their
10 + * property loads are hoistable to the function declaration site.
11 + */
12 +function useMakeCallback({obj}: {obj: {value: number}}) {
13 + const [state, setState] = useState(0);
14 + const cb = () => {
15 + if (obj.value !== 0) setState(obj.value);
16 + };
17 + useIdentity(null);
18 + if (state === 0) {
19 + cb();
20 + }
21 + return {cb};
22 +}
23 +export const FIXTURE_ENTRYPOINT = {
24 + fn: useMakeCallback,
25 + params: [{obj: {value: 1}}],
26 + sequentialRenders: [{obj: {value: 1}}, {obj: {value: 2}}],
27 +};
28 +
29 +```
30 +
31 +## Code
32 +
33 +```javascript
34 +import { c as _c } from "react/compiler-runtime";
35 +import { useState } from "react";
36 +import { useIdentity } from "shared-runtime";
37 +
38 +/**
39 + * Assume that conditionally called functions can be invoked and that their
40 + * property loads are hoistable to the function declaration site.
41 + */
42 +function useMakeCallback(t0) {
43 + const $ = _c(4);
44 + const { obj } = t0;
45 + const [state, setState] = useState(0);
46 + let t1;
47 + if ($[0] !== obj.value) {
48 + t1 = () => {
49 + if (obj.value !== 0) {
50 + setState(obj.value);
51 + }
52 + };
53 + $[0] = obj.value;
54 + $[1] = t1;
55 + } else {
56 + t1 = $[1];
57 + }
58 + const cb = t1;
59 +
60 + useIdentity(null);
61 + if (state === 0) {
62 + cb();
63 + }
64 + let t2;
65 + if ($[2] !== cb) {
66 + t2 = { cb };
67 + $[2] = cb;
68 + $[3] = t2;
69 + } else {
70 + t2 = $[3];
71 + }
72 + return t2;
73 +}
74 +
75 +export const FIXTURE_ENTRYPOINT = {
76 + fn: useMakeCallback,
77 + params: [{ obj: { value: 1 } }],
78 + sequentialRenders: [{ obj: { value: 1 } }, { obj: { value: 2 } }],
79 +};
80 +
81 +```
82 +
83 +### Eval output
84 +(kind: ok) {"cb":"[[ function params=0 ]]"}
85 +{"cb":"[[ function params=0 ]]"}
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/conditional-call.ts new
+23
@@ -0,0 +1,23 @@
1 +import {useState} from 'react';
2 +import {useIdentity} from 'shared-runtime';
3 +
4 +/**
5 + * Assume that conditionally called functions can be invoked and that their
6 + * property loads are hoistable to the function declaration site.
7 + */
8 +function useMakeCallback({obj}: {obj: {value: number}}) {
9 + const [state, setState] = useState(0);
10 + const cb = () => {
11 + if (obj.value !== 0) setState(obj.value);
12 + };
13 + useIdentity(null);
14 + if (state === 0) {
15 + cb();
16 + }
17 + return {cb};
18 +}
19 +export const FIXTURE_ENTRYPOINT = {
20 + fn: useMakeCallback,
21 + params: [{obj: {value: 1}}],
22 + sequentialRenders: [{obj: {value: 1}}, {obj: {value: 2}}],
23 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/conditionally-return-fn.expect.md new
+87
@@ -0,0 +1,87 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import {createHookWrapper} from 'shared-runtime';
6 +
7 +/**
8 + * Assume that conditionally returned functions can be invoked and that their
9 + * property loads are hoistable to the function declaration site.
10 + */
11 +function useMakeCallback({
12 + obj,
13 + shouldMakeCb,
14 + setState,
15 +}: {
16 + obj: {value: number};
17 + shouldMakeCb: boolean;
18 + setState: (newState: number) => void;
19 +}) {
20 + const cb = () => setState(obj.value);
21 + if (shouldMakeCb) return cb;
22 + else return null;
23 +}
24 +
25 +const setState = (arg: number) => {
26 + 'use no memo';
27 + return arg;
28 +};
29 +export const FIXTURE_ENTRYPOINT = {
30 + fn: createHookWrapper(useMakeCallback),
31 + params: [{obj: {value: 1}, shouldMakeCb: true, setState}],
32 + sequentialRenders: [
33 + {obj: {value: 1}, shouldMakeCb: true, setState},
34 + {obj: {value: 2}, shouldMakeCb: true, setState},
35 + ],
36 +};
37 +
38 +```
39 +
40 +## Code
41 +
42 +```javascript
43 +import { c as _c } from "react/compiler-runtime";
44 +import { createHookWrapper } from "shared-runtime";
45 +
46 +/**
47 + * Assume that conditionally returned functions can be invoked and that their
48 + * property loads are hoistable to the function declaration site.
49 + */
50 +function useMakeCallback(t0) {
51 + const $ = _c(3);
52 + const { obj, shouldMakeCb, setState } = t0;
53 + let t1;
54 + if ($[0] !== obj.value || $[1] !== setState) {
55 + t1 = () => setState(obj.value);
56 + $[0] = obj.value;
57 + $[1] = setState;
58 + $[2] = t1;
59 + } else {
60 + t1 = $[2];
61 + }
62 + const cb = t1;
63 + if (shouldMakeCb) {
64 + return cb;
65 + } else {
66 + return null;
67 + }
68 +}
69 +
70 +const setState = (arg: number) => {
71 + "use no memo";
72 + return arg;
73 +};
74 +export const FIXTURE_ENTRYPOINT = {
75 + fn: createHookWrapper(useMakeCallback),
76 + params: [{ obj: { value: 1 }, shouldMakeCb: true, setState }],
77 + sequentialRenders: [
78 + { obj: { value: 1 }, shouldMakeCb: true, setState },
79 + { obj: { value: 2 }, shouldMakeCb: true, setState },
80 + ],
81 +};
82 +
83 +```
84 +
85 +### Eval output
86 +(kind: ok) <div>{"result":{"kind":"Function","result":1},"shouldInvokeFns":true}</div>
87 +<div>{"result":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/conditionally-return-fn.ts new
+32
@@ -0,0 +1,32 @@
1 +import {createHookWrapper} from 'shared-runtime';
2 +
3 +/**
4 + * Assume that conditionally returned functions can be invoked and that their
5 + * property loads are hoistable to the function declaration site.
6 + */
7 +function useMakeCallback({
8 + obj,
9 + shouldMakeCb,
10 + setState,
11 +}: {
12 + obj: {value: number};
13 + shouldMakeCb: boolean;
14 + setState: (newState: number) => void;
15 +}) {
16 + const cb = () => setState(obj.value);
17 + if (shouldMakeCb) return cb;
18 + else return null;
19 +}
20 +
21 +const setState = (arg: number) => {
22 + 'use no memo';
23 + return arg;
24 +};
25 +export const FIXTURE_ENTRYPOINT = {
26 + fn: createHookWrapper(useMakeCallback),
27 + params: [{obj: {value: 1}, shouldMakeCb: true, setState}],
28 + sequentialRenders: [
29 + {obj: {value: 1}, shouldMakeCb: true, setState},
30 + {obj: {value: 2}, shouldMakeCb: true, setState},
31 + ],
32 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/direct-call.expect.md new
+74
@@ -0,0 +1,74 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import {useState} from 'react';
6 +import {useIdentity} from 'shared-runtime';
7 +
8 +function useMakeCallback({obj}: {obj: {value: number}}) {
9 + const [state, setState] = useState(0);
10 + const cb = () => {
11 + if (obj.value !== state) setState(obj.value);
12 + };
13 + useIdentity();
14 + cb();
15 + return [cb];
16 +}
17 +export const FIXTURE_ENTRYPOINT = {
18 + fn: useMakeCallback,
19 + params: [{obj: {value: 1}}],
20 + sequentialRenders: [{obj: {value: 1}}, {obj: {value: 2}}],
21 +};
22 +
23 +```
24 +
25 +## Code
26 +
27 +```javascript
28 +import { c as _c } from "react/compiler-runtime";
29 +import { useState } from "react";
30 +import { useIdentity } from "shared-runtime";
31 +
32 +function useMakeCallback(t0) {
33 + const $ = _c(5);
34 + const { obj } = t0;
35 + const [state, setState] = useState(0);
36 + let t1;
37 + if ($[0] !== obj.value || $[1] !== state) {
38 + t1 = () => {
39 + if (obj.value !== state) {
40 + setState(obj.value);
41 + }
42 + };
43 + $[0] = obj.value;
44 + $[1] = state;
45 + $[2] = t1;
46 + } else {
47 + t1 = $[2];
48 + }
49 + const cb = t1;
50 +
51 + useIdentity();
52 + cb();
53 + let t2;
54 + if ($[3] !== cb) {
55 + t2 = [cb];
56 + $[3] = cb;
57 + $[4] = t2;
58 + } else {
59 + t2 = $[4];
60 + }
61 + return t2;
62 +}
63 +
64 +export const FIXTURE_ENTRYPOINT = {
65 + fn: useMakeCallback,
66 + params: [{ obj: { value: 1 } }],
67 + sequentialRenders: [{ obj: { value: 1 } }, { obj: { value: 2 } }],
68 +};
69 +
70 +```
71 +
72 +### Eval output
73 +(kind: ok) ["[[ function params=0 ]]"]
74 +["[[ function params=0 ]]"]
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/direct-call.ts new
+17
@@ -0,0 +1,17 @@
1 +import {useState} from 'react';
2 +import {useIdentity} from 'shared-runtime';
3 +
4 +function useMakeCallback({obj}: {obj: {value: number}}) {
5 + const [state, setState] = useState(0);
6 + const cb = () => {
7 + if (obj.value !== state) setState(obj.value);
8 + };
9 + useIdentity();
10 + cb();
11 + return [cb];
12 +}
13 +export const FIXTURE_ENTRYPOINT = {
14 + fn: useMakeCallback,
15 + params: [{obj: {value: 1}}],
16 + sequentialRenders: [{obj: {value: 1}}, {obj: {value: 2}}],
17 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/function-with-conditional-callsite-in-another-function.expect.md new
+130
@@ -0,0 +1,130 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import {createHookWrapper} from 'shared-runtime';
6 +
7 +/**
8 + * (Given that the returned lambda is assumed to be invoked, see
9 + * return-function)
10 + *
11 + * If lambda A conditionally calls lambda B, optimistically assume that property
12 + * loads from lambda B has the same hoistability of ones from lambda A. This
13 + * helps optimize components / hooks that create and chain many helper
14 + * functions.
15 + *
16 + * Type systems and code readability encourage developers to colocate length and
17 + * null checks values in the same function as where values are used. i.e.
18 + * developers are unlikely to write the following code.
19 + * ```js
20 + * function useFoo(obj, objNotNullAndHasElements) {
21 + * // ...
22 + * const get0th = () => obj.arr[0].value;
23 + * return () => objNotNullAndHasElements ? get0th : undefined;
24 + * }
25 + * ```
26 + *
27 + * In Meta code, this assumption helps reduce the number of memo dependency
28 + * deopts.
29 + */
30 +function useMakeCallback({
31 + obj,
32 + cond,
33 + setState,
34 +}: {
35 + obj: {value: number};
36 + cond: boolean;
37 + setState: (newState: number) => void;
38 +}) {
39 + const cb = () => setState(obj.value);
40 + // cb's property loads are assumed to be hoistable to the start of this lambda
41 + return () => (cond ? cb() : undefined);
42 +}
43 +
44 +const setState = (arg: number) => {
45 + 'use no memo';
46 + return arg;
47 +};
48 +export const FIXTURE_ENTRYPOINT = {
49 + fn: createHookWrapper(useMakeCallback),
50 + params: [{obj: {value: 1}, cond: true, setState}],
51 + sequentialRenders: [
52 + {obj: {value: 1}, cond: true, setState},
53 + {obj: {value: 2}, cond: true, setState},
54 + ],
55 +};
56 +
57 +```
58 +
59 +## Code
60 +
61 +```javascript
62 +import { c as _c } from "react/compiler-runtime";
63 +import { createHookWrapper } from "shared-runtime";
64 +
65 +/**
66 + * (Given that the returned lambda is assumed to be invoked, see
67 + * return-function)
68 + *
69 + * If lambda A conditionally calls lambda B, optimistically assume that property
70 + * loads from lambda B has the same hoistability of ones from lambda A. This
71 + * helps optimize components / hooks that create and chain many helper
72 + * functions.
73 + *
74 + * Type systems and code readability encourage developers to colocate length and
75 + * null checks values in the same function as where values are used. i.e.
76 + * developers are unlikely to write the following code.
77 + * ```js
78 + * function useFoo(obj, objNotNullAndHasElements) {
79 + * // ...
80 + * const get0th = () => obj.arr[0].value;
81 + * return () => objNotNullAndHasElements ? get0th : undefined;
82 + * }
83 + * ```
84 + *
85 + * In Meta code, this assumption helps reduce the number of memo dependency
86 + * deopts.
87 + */
88 +function useMakeCallback(t0) {
89 + const $ = _c(6);
90 + const { obj, cond, setState } = t0;
91 + let t1;
92 + if ($[0] !== obj.value || $[1] !== setState) {
93 + t1 = () => setState(obj.value);
94 + $[0] = obj.value;
95 + $[1] = setState;
96 + $[2] = t1;
97 + } else {
98 + t1 = $[2];
99 + }
100 + const cb = t1;
101 + let t2;
102 + if ($[3] !== cb || $[4] !== cond) {
103 + t2 = () => (cond ? cb() : undefined);
104 + $[3] = cb;
105 + $[4] = cond;
106 + $[5] = t2;
107 + } else {
108 + t2 = $[5];
109 + }
110 + return t2;
111 +}
112 +
113 +const setState = (arg: number) => {
114 + "use no memo";
115 + return arg;
116 +};
117 +export const FIXTURE_ENTRYPOINT = {
118 + fn: createHookWrapper(useMakeCallback),
119 + params: [{ obj: { value: 1 }, cond: true, setState }],
120 + sequentialRenders: [
121 + { obj: { value: 1 }, cond: true, setState },
122 + { obj: { value: 2 }, cond: true, setState },
123 + ],
124 +};
125 +
126 +```
127 +
128 +### Eval output
129 +(kind: ok) <div>{"result":{"kind":"Function","result":1},"shouldInvokeFns":true}</div>
130 +<div>{"result":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/function-with-conditional-callsite-in-another-function.ts new
+51
@@ -0,0 +1,51 @@
1 +import {createHookWrapper} from 'shared-runtime';
2 +
3 +/**
4 + * (Given that the returned lambda is assumed to be invoked, see
5 + * return-function)
6 + *
7 + * If lambda A conditionally calls lambda B, optimistically assume that property
8 + * loads from lambda B has the same hoistability of ones from lambda A. This
9 + * helps optimize components / hooks that create and chain many helper
10 + * functions.
11 + *
12 + * Type systems and code readability encourage developers to colocate length and
13 + * null checks values in the same function as where values are used. i.e.
14 + * developers are unlikely to write the following code.
15 + * ```js
16 + * function useFoo(obj, objNotNullAndHasElements) {
17 + * // ...
18 + * const get0th = () => obj.arr[0].value;
19 + * return () => objNotNullAndHasElements ? get0th : undefined;
20 + * }
21 + * ```
22 + *
23 + * In Meta code, this assumption helps reduce the number of memo dependency
24 + * deopts.
25 + */
26 +function useMakeCallback({
27 + obj,
28 + cond,
29 + setState,
30 +}: {
31 + obj: {value: number};
32 + cond: boolean;
33 + setState: (newState: number) => void;
34 +}) {
35 + const cb = () => setState(obj.value);
36 + // cb's property loads are assumed to be hoistable to the start of this lambda
37 + return () => (cond ? cb() : undefined);
38 +}
39 +
40 +const setState = (arg: number) => {
41 + 'use no memo';
42 + return arg;
43 +};
44 +export const FIXTURE_ENTRYPOINT = {
45 + fn: createHookWrapper(useMakeCallback),
46 + params: [{obj: {value: 1}, cond: true, setState}],
47 + sequentialRenders: [
48 + {obj: {value: 1}, cond: true, setState},
49 + {obj: {value: 2}, cond: true, setState},
50 + ],
51 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/hook-call.expect.md new
+80
@@ -0,0 +1,80 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import {createHookWrapper, useIdentity} from 'shared-runtime';
6 +
7 +/**
8 + * Assume that functions passed hook arguments are invoked and that their
9 + * property loads are hoistable.
10 + */
11 +function useMakeCallback({
12 + obj,
13 + setState,
14 +}: {
15 + obj: {value: number};
16 + setState: (newState: number) => void;
17 +}) {
18 + const cb = useIdentity(() => setState(obj.value));
19 + return cb;
20 +}
21 +
22 +const setState = (arg: number) => {
23 + 'use no memo';
24 + return arg;
25 +};
26 +export const FIXTURE_ENTRYPOINT = {
27 + fn: createHookWrapper(useMakeCallback),
28 + params: [{obj: {value: 1}, setState}],
29 + sequentialRenders: [
30 + {obj: {value: 1}, setState},
31 + {obj: {value: 2}, setState},
32 + ],
33 +};
34 +
35 +```
36 +
37 +## Code
38 +
39 +```javascript
40 +import { c as _c } from "react/compiler-runtime";
41 +import { createHookWrapper, useIdentity } from "shared-runtime";
42 +
43 +/**
44 + * Assume that functions passed hook arguments are invoked and that their
45 + * property loads are hoistable.
46 + */
47 +function useMakeCallback(t0) {
48 + const $ = _c(3);
49 + const { obj, setState } = t0;
50 + let t1;
51 + if ($[0] !== obj.value || $[1] !== setState) {
52 + t1 = () => setState(obj.value);
53 + $[0] = obj.value;
54 + $[1] = setState;
55 + $[2] = t1;
56 + } else {
57 + t1 = $[2];
58 + }
59 + const cb = useIdentity(t1);
60 + return cb;
61 +}
62 +
63 +const setState = (arg: number) => {
64 + "use no memo";
65 + return arg;
66 +};
67 +export const FIXTURE_ENTRYPOINT = {
68 + fn: createHookWrapper(useMakeCallback),
69 + params: [{ obj: { value: 1 }, setState }],
70 + sequentialRenders: [
71 + { obj: { value: 1 }, setState },
72 + { obj: { value: 2 }, setState },
73 + ],
74 +};
75 +
76 +```
77 +
78 +### Eval output
79 +(kind: ok) <div>{"result":{"kind":"Function","result":1},"shouldInvokeFns":true}</div>
80 +<div>{"result":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/hook-call.ts new
+29
@@ -0,0 +1,29 @@
1 +import {createHookWrapper, useIdentity} from 'shared-runtime';
2 +
3 +/**
4 + * Assume that functions passed hook arguments are invoked and that their
5 + * property loads are hoistable.
6 + */
7 +function useMakeCallback({
8 + obj,
9 + setState,
10 +}: {
11 + obj: {value: number};
12 + setState: (newState: number) => void;
13 +}) {
14 + const cb = useIdentity(() => setState(obj.value));
15 + return cb;
16 +}
17 +
18 +const setState = (arg: number) => {
19 + 'use no memo';
20 + return arg;
21 +};
22 +export const FIXTURE_ENTRYPOINT = {
23 + fn: createHookWrapper(useMakeCallback),
24 + params: [{obj: {value: 1}, setState}],
25 + sequentialRenders: [
26 + {obj: {value: 1}, setState},
27 + {obj: {value: 2}, setState},
28 + ],
29 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/jsx-and-passed.expect.md new
+80
@@ -0,0 +1,80 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import {createHookWrapper} from 'shared-runtime';
6 +
7 +function useFoo({arr1}) {
8 + const cb1 = e => arr1[0].value + e.value;
9 + const x = arr1.map(cb1);
10 + return [x, cb1];
11 +}
12 +
13 +export const FIXTURE_ENTRYPOINT = {
14 + fn: createHookWrapper(useFoo),
15 + params: [{arr1: [], arr2: []}],
16 + sequentialRenders: [
17 + {arr1: [], arr2: []},
18 + {arr1: [], arr2: null},
19 + {arr1: [{value: 1}, {value: 2}], arr2: [{value: -1}]},
20 + ],
21 +};
22 +
23 +```
24 +
25 +## Code
26 +
27 +```javascript
28 +import { c as _c } from "react/compiler-runtime";
29 +import { createHookWrapper } from "shared-runtime";
30 +
31 +function useFoo(t0) {
32 + const $ = _c(8);
33 + const { arr1 } = t0;
34 + let t1;
35 + if ($[0] !== arr1[0]) {
36 + t1 = (e) => arr1[0].value + e.value;
37 + $[0] = arr1[0];
38 + $[1] = t1;
39 + } else {
40 + t1 = $[1];
41 + }
42 + const cb1 = t1;
43 + let t2;
44 + if ($[2] !== arr1 || $[3] !== cb1) {
45 + t2 = arr1.map(cb1);
46 + $[2] = arr1;
47 + $[3] = cb1;
48 + $[4] = t2;
49 + } else {
50 + t2 = $[4];
51 + }
52 + const x = t2;
53 + let t3;
54 + if ($[5] !== cb1 || $[6] !== x) {
55 + t3 = [x, cb1];
56 + $[5] = cb1;
57 + $[6] = x;
58 + $[7] = t3;
59 + } else {
60 + t3 = $[7];
61 + }
62 + return t3;
63 +}
64 +
65 +export const FIXTURE_ENTRYPOINT = {
66 + fn: createHookWrapper(useFoo),
67 + params: [{ arr1: [], arr2: [] }],
68 + sequentialRenders: [
69 + { arr1: [], arr2: [] },
70 + { arr1: [], arr2: null },
71 + { arr1: [{ value: 1 }, { value: 2 }], arr2: [{ value: -1 }] },
72 + ],
73 +};
74 +
75 +```
76 +
77 +### Eval output
78 +(kind: ok) <div>{"result":[[],"[[ function params=1 ]]"],"shouldInvokeFns":true}</div>
79 +<div>{"result":[[],"[[ function params=1 ]]"],"shouldInvokeFns":true}</div>
80 +<div>{"result":[[2,3],"[[ function params=1 ]]"],"shouldInvokeFns":true}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/jsx-and-passed.ts new
+17
@@ -0,0 +1,17 @@
1 +import {createHookWrapper} from 'shared-runtime';
2 +
3 +function useFoo({arr1}) {
4 + const cb1 = e => arr1[0].value + e.value;
5 + const x = arr1.map(cb1);
6 + return [x, cb1];
7 +}
8 +
9 +export const FIXTURE_ENTRYPOINT = {
10 + fn: createHookWrapper(useFoo),
11 + params: [{arr1: [], arr2: []}],
12 + sequentialRenders: [
13 + {arr1: [], arr2: []},
14 + {arr1: [], arr2: null},
15 + {arr1: [{value: 1}, {value: 2}], arr2: [{value: -1}]},
16 + ],
17 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/jsx-function.expect.md new
+75
@@ -0,0 +1,75 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @flow
6 +import {Stringify} from 'shared-runtime';
7 +
8 +/**
9 + * Assume that functions captured directly as jsx attributes are invoked and
10 + * that their property loads are hoistable.
11 + */
12 +function useMakeCallback({
13 + obj,
14 + setState,
15 +}: {
16 + obj: {value: number};
17 + setState: (newState: number) => void;
18 +}) {
19 + return <Stringify cb={() => setState(obj.value)} shouldInvokeFns={true} />;
20 +}
21 +
22 +const setState = (arg: number) => {
23 + 'use no memo';
24 + return arg;
25 +};
26 +export const FIXTURE_ENTRYPOINT = {
27 + fn: useMakeCallback,
28 + params: [{obj: {value: 1}, setState}],
29 + sequentialRenders: [
30 + {obj: {value: 1}, setState},
31 + {obj: {value: 2}, setState},
32 + ],
33 +};
34 +
35 +```
36 +
37 +## Code
38 +
39 +```javascript
40 +import { c as _c } from "react/compiler-runtime";
41 +import { Stringify } from "shared-runtime";
42 +
43 +function useMakeCallback(t0) {
44 + const $ = _c(3);
45 + const { obj, setState } = t0;
46 + let t1;
47 + if ($[0] !== obj.value || $[1] !== setState) {
48 + t1 = <Stringify cb={() => setState(obj.value)} shouldInvokeFns={true} />;
49 + $[0] = obj.value;
50 + $[1] = setState;
51 + $[2] = t1;
52 + } else {
53 + t1 = $[2];
54 + }
55 + return t1;
56 +}
57 +
58 +const setState = (arg: number) => {
59 + "use no memo";
60 + return arg;
61 +};
62 +export const FIXTURE_ENTRYPOINT = {
63 + fn: useMakeCallback,
64 + params: [{ obj: { value: 1 }, setState }],
65 + sequentialRenders: [
66 + { obj: { value: 1 }, setState },
67 + { obj: { value: 2 }, setState },
68 + ],
69 +};
70 +
71 +```
72 +
73 +### Eval output
74 +(kind: ok) <div>{"cb":{"kind":"Function","result":1},"shouldInvokeFns":true}</div>
75 +<div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/jsx-function.tsx new
+29
@@ -0,0 +1,29 @@
1 +// @flow
2 +import {Stringify} from 'shared-runtime';
3 +
4 +/**
5 + * Assume that functions captured directly as jsx attributes are invoked and
6 + * that their property loads are hoistable.
7 + */
8 +function useMakeCallback({
9 + obj,
10 + setState,
11 +}: {
12 + obj: {value: number};
13 + setState: (newState: number) => void;
14 +}) {
15 + return <Stringify cb={() => setState(obj.value)} shouldInvokeFns={true} />;
16 +}
17 +
18 +const setState = (arg: number) => {
19 + 'use no memo';
20 + return arg;
21 +};
22 +export const FIXTURE_ENTRYPOINT = {
23 + fn: useMakeCallback,
24 + params: [{obj: {value: 1}, setState}],
25 + sequentialRenders: [
26 + {obj: {value: 1}, setState},
27 + {obj: {value: 2}, setState},
28 + ],
29 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/return-function.expect.md new
+78
@@ -0,0 +1,78 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import {createHookWrapper} from 'shared-runtime';
6 +
7 +/**
8 + * Assume that directly returned functions are invoked and that their property
9 + * loads are hoistable.
10 + */
11 +function useMakeCallback({
12 + obj,
13 + setState,
14 +}: {
15 + obj: {value: number};
16 + setState: (newState: number) => void;
17 +}) {
18 + return () => setState(obj.value);
19 +}
20 +
21 +const setState = (arg: number) => {
22 + 'use no memo';
23 + return arg;
24 +};
25 +export const FIXTURE_ENTRYPOINT = {
26 + fn: createHookWrapper(useMakeCallback),
27 + params: [{obj: {value: 1}, setState}],
28 + sequentialRenders: [
29 + {obj: {value: 1}, setState},
30 + {obj: {value: 2}, setState},
31 + ],
32 +};
33 +
34 +```
35 +
36 +## Code
37 +
38 +```javascript
39 +import { c as _c } from "react/compiler-runtime";
40 +import { createHookWrapper } from "shared-runtime";
41 +
42 +/**
43 + * Assume that directly returned functions are invoked and that their property
44 + * loads are hoistable.
45 + */
46 +function useMakeCallback(t0) {
47 + const $ = _c(3);
48 + const { obj, setState } = t0;
49 + let t1;
50 + if ($[0] !== obj.value || $[1] !== setState) {
51 + t1 = () => setState(obj.value);
52 + $[0] = obj.value;
53 + $[1] = setState;
54 + $[2] = t1;
55 + } else {
56 + t1 = $[2];
57 + }
58 + return t1;
59 +}
60 +
61 +const setState = (arg: number) => {
62 + "use no memo";
63 + return arg;
64 +};
65 +export const FIXTURE_ENTRYPOINT = {
66 + fn: createHookWrapper(useMakeCallback),
67 + params: [{ obj: { value: 1 }, setState }],
68 + sequentialRenders: [
69 + { obj: { value: 1 }, setState },
70 + { obj: { value: 2 }, setState },
71 + ],
72 +};
73 +
74 +```
75 +
76 +### Eval output
77 +(kind: ok) <div>{"result":{"kind":"Function","result":1},"shouldInvokeFns":true}</div>
78 +<div>{"result":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/return-function.ts new
+28
@@ -0,0 +1,28 @@
1 +import {createHookWrapper} from 'shared-runtime';
2 +
3 +/**
4 + * Assume that directly returned functions are invoked and that their property
5 + * loads are hoistable.
6 + */
7 +function useMakeCallback({
8 + obj,
9 + setState,
10 +}: {
11 + obj: {value: number};
12 + setState: (newState: number) => void;
13 +}) {
14 + return () => setState(obj.value);
15 +}
16 +
17 +const setState = (arg: number) => {
18 + 'use no memo';
19 + return arg;
20 +};
21 +export const FIXTURE_ENTRYPOINT = {
22 + fn: createHookWrapper(useMakeCallback),
23 + params: [{obj: {value: 1}, setState}],
24 + sequentialRenders: [
25 + {obj: {value: 1}, setState},
26 + {obj: {value: 2}, setState},
27 + ],
28 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/use-memo-returned.expect.md new
+82
@@ -0,0 +1,82 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import {useState, useMemo} from 'react';
6 +import {useIdentity} from 'shared-runtime';
7 +
8 +/**
9 + * Assume that conditionally called functions can be invoked and that their
10 + * property loads are hoistable to the function declaration site.
11 + */
12 +function useMakeCallback({
13 + obj,
14 + shouldSynchronizeState,
15 +}: {
16 + obj: {value: number};
17 + shouldSynchronizeState: boolean;
18 +}) {
19 + const [state, setState] = useState(0);
20 + const cb = useMemo(() => {
21 + return () => {
22 + if (obj.value !== 0) setState(obj.value);
23 + };
24 + }, [obj.value, shouldSynchronizeState]);
25 + useIdentity(null);
26 + return cb;
27 +}
28 +export const FIXTURE_ENTRYPOINT = {
29 + fn: useMakeCallback,
30 + params: [{obj: {value: 1}}],
31 + sequentialRenders: [{obj: {value: 1}}, {obj: {value: 2}}],
32 +};
33 +
34 +```
35 +
36 +## Code
37 +
38 +```javascript
39 +import { c as _c } from "react/compiler-runtime";
40 +import { useState, useMemo } from "react";
41 +import { useIdentity } from "shared-runtime";
42 +
43 +/**
44 + * Assume that conditionally called functions can be invoked and that their
45 + * property loads are hoistable to the function declaration site.
46 + */
47 +function useMakeCallback(t0) {
48 + const $ = _c(2);
49 + const { obj, shouldSynchronizeState } = t0;
50 +
51 + const [, setState] = useState(0);
52 + let t1;
53 + let t2;
54 + if ($[0] !== obj.value) {
55 + t2 = () => {
56 + if (obj.value !== 0) {
57 + setState(obj.value);
58 + }
59 + };
60 + $[0] = obj.value;
61 + $[1] = t2;
62 + } else {
63 + t2 = $[1];
64 + }
65 + t1 = t2;
66 + const cb = t1;
67 +
68 + useIdentity(null);
69 + return cb;
70 +}
71 +
72 +export const FIXTURE_ENTRYPOINT = {
73 + fn: useMakeCallback,
74 + params: [{ obj: { value: 1 } }],
75 + sequentialRenders: [{ obj: { value: 1 } }, { obj: { value: 2 } }],
76 +};
77 +
78 +```
79 +
80 +### Eval output
81 +(kind: ok) "[[ function params=0 ]]"
82 +"[[ function params=0 ]]"
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/use-memo-returned.ts new
+28
@@ -0,0 +1,28 @@
1 +import {useState, useMemo} from 'react';
2 +import {useIdentity} from 'shared-runtime';
3 +
4 +/**
5 + * Assume that conditionally called functions can be invoked and that their
6 + * property loads are hoistable to the function declaration site.
7 + */
8 +function useMakeCallback({
9 + obj,
10 + shouldSynchronizeState,
11 +}: {
12 + obj: {value: number};
13 + shouldSynchronizeState: boolean;
14 +}) {
15 + const [state, setState] = useState(0);
16 + const cb = useMemo(() => {
17 + return () => {
18 + if (obj.value !== 0) setState(obj.value);
19 + };
20 + }, [obj.value, shouldSynchronizeState]);
21 + useIdentity(null);
22 + return cb;
23 +}
24 +export const FIXTURE_ENTRYPOINT = {
25 + fn: useMakeCallback,
26 + params: [{obj: {value: 1}}],
27 + sequentialRenders: [{obj: {value: 1}}, {obj: {value: 2}}],
28 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/bug-invalid-array-map-manual.expect.md new
+68
@@ -0,0 +1,68 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function useFoo({arr1, arr2}) {
6 + const cb = e => arr2[0].value + e.value;
7 + const y = [];
8 + for (let i = 0; i < arr1.length; i++) {
9 + y.push(cb(arr1[i]));
10 + }
11 + return y;
12 +}
13 +
14 +export const FIXTURE_ENTRYPOINT = {
15 + fn: useFoo,
16 + params: [{arr1: [], arr2: []}],
17 + sequentialRenders: [
18 + {arr1: [], arr2: []},
19 + {arr1: [], arr2: null},
20 + {arr1: [{value: 1}, {value: 2}], arr2: [{value: -1}]},
21 + ],
22 +};
23 +
24 +```
25 +
26 +## Code
27 +
28 +```javascript
29 +import { c as _c } from "react/compiler-runtime";
30 +function useFoo(t0) {
31 + const $ = _c(5);
32 + const { arr1, arr2 } = t0;
33 + let t1;
34 + if ($[0] !== arr2[0].value) {
35 + t1 = (e) => arr2[0].value + e.value;
36 + $[0] = arr2[0].value;
37 + $[1] = t1;
38 + } else {
39 + t1 = $[1];
40 + }
41 + const cb = t1;
42 + let y;
43 + if ($[2] !== arr1 || $[3] !== cb) {
44 + y = [];
45 + for (let i = 0; i < arr1.length; i++) {
46 + y.push(cb(arr1[i]));
47 + }
48 + $[2] = arr1;
49 + $[3] = cb;
50 + $[4] = y;
51 + } else {
52 + y = $[4];
53 + }
54 + return y;
55 +}
56 +
57 +export const FIXTURE_ENTRYPOINT = {
58 + fn: useFoo,
59 + params: [{ arr1: [], arr2: [] }],
60 + sequentialRenders: [
61 + { arr1: [], arr2: [] },
62 + { arr1: [], arr2: null },
63 + { arr1: [{ value: 1 }, { value: 2 }], arr2: [{ value: -1 }] },
64 + ],
65 +};
66 +
67 +```
68 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/bug-invalid-array-map-manual.js new
+18
@@ -0,0 +1,18 @@
1 +function useFoo({arr1, arr2}) {
2 + const cb = e => arr2[0].value + e.value;
3 + const y = [];
4 + for (let i = 0; i < arr1.length; i++) {
5 + y.push(cb(arr1[i]));
6 + }
7 + return y;
8 +}
9 +
10 +export const FIXTURE_ENTRYPOINT = {
11 + fn: useFoo,
12 + params: [{arr1: [], arr2: []}],
13 + sequentialRenders: [
14 + {arr1: [], arr2: []},
15 + {arr1: [], arr2: null},
16 + {arr1: [{value: 1}, {value: 2}], arr2: [{value: -1}]},
17 + ],
18 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/return-object-of-functions.expect.md new
+57
@@ -0,0 +1,57 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +/**
6 + * Assume that only directly returned functions or JSX attributes are invoked.
7 + * Conservatively estimate that functions wrapped in objects or other containers
8 + * might never be called (and therefore their property loads are not hoistable).
9 + */
10 +function useMakeCallback({arr}) {
11 + return {
12 + getElement0: () => arr[0].value,
13 + getElement1: () => arr[1].value,
14 + };
15 +}
16 +
17 +export const FIXTURE_ENTRYPOINT = {
18 + fn: useMakeCallback,
19 + params: [{arr: [1, 2]}],
20 + sequentialRenders: [{arr: [1, 2]}, {arr: []}],
21 +};
22 +
23 +```
24 +
25 +## Code
26 +
27 +```javascript
28 +import { c as _c } from "react/compiler-runtime"; /**
29 + * Assume that only directly returned functions or JSX attributes are invoked.
30 + * Conservatively estimate that functions wrapped in objects or other containers
31 + * might never be called (and therefore their property loads are not hoistable).
32 + */
33 +function useMakeCallback(t0) {
34 + const $ = _c(2);
35 + const { arr } = t0;
36 + let t1;
37 + if ($[0] !== arr) {
38 + t1 = { getElement0: () => arr[0].value, getElement1: () => arr[1].value };
39 + $[0] = arr;
40 + $[1] = t1;
41 + } else {
42 + t1 = $[1];
43 + }
44 + return t1;
45 +}
46 +
47 +export const FIXTURE_ENTRYPOINT = {
48 + fn: useMakeCallback,
49 + params: [{ arr: [1, 2] }],
50 + sequentialRenders: [{ arr: [1, 2] }, { arr: [] }],
51 +};
52 +
53 +```
54 +
55 +### Eval output
56 +(kind: ok) {"getElement0":"[[ function params=0 ]]","getElement1":"[[ function params=0 ]]"}
57 +{"getElement0":"[[ function params=0 ]]","getElement1":"[[ function params=0 ]]"}
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/return-object-of-functions.js new
+17
@@ -0,0 +1,17 @@
1 +/**
2 + * Assume that only directly returned functions or JSX attributes are invoked.
3 + * Conservatively estimate that functions wrapped in objects or other containers
4 + * might never be called (and therefore their property loads are not hoistable).
5 + */
6 +function useMakeCallback({arr}) {
7 + return {
8 + getElement0: () => arr[0].value,
9 + getElement1: () => arr[1].value,
10 + };
11 +}
12 +
13 +export const FIXTURE_ENTRYPOINT = {
14 + fn: useMakeCallback,
15 + params: [{arr: [1, 2]}],
16 + sequentialRenders: [{arr: [1, 2]}, {arr: []}],
17 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-nested-function-uncond-access-local-var.expect.md
+2 -2
@@ -41,9 +41,9 @@ function useFoo(t0) {
41 local = $[1];
42 }
43 let t1;
44 - if ($[2] !== local.b.c) {
44 + if ($[2] !== local) {
45 t1 = () => [() => local.b.c];
46 - $[2] = local.b.c;
46 + $[2] = local;
47 $[3] = t1;
48 } else {
49 t1 = $[3];
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-object-method-uncond-access.expect.md
+2 -2
@@ -34,13 +34,13 @@ function useFoo(t0) {
34 const $ = _c(4);
35 const { a } = t0;
36 let t1;
37 - if ($[0] !== a.b.c) {
37 + if ($[0] !== a) {
38 t1 = {
39 fn() {
40 return identity(a.b.c);
41 },
42 };
43 - $[0] = a.b.c;
43 + $[0] = a;
44 $[1] = t1;
45 } else {
46 t1 = $[1];
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reactive-control-dependency-on-context-variable.expect.md
+2 -2
@@ -51,7 +51,7 @@ import { identity } from "shared-runtime";
51 function Component(props) {
52 const $ = _c(4);
53 let x;
54 - if ($[0] !== props.cond) {
54 + if ($[0] !== props) {
55 const f = () => {
56 if (props.cond) {
57 x = 1;
@@ -62,7 +62,7 @@ function Component(props) {
62
63 const f2 = identity(f);
64 f2();
65 - $[0] = props.cond;
65 + $[0] = props;
66 $[1] = x;
67 } else {
68 x = $[1];
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/context-var-granular-dep.expect.md
+2 -2
@@ -82,9 +82,9 @@ function Component(t0) {
82 contextVar = $[2];
83 }
84 let t1;
85 - if ($[3] !== contextVar.val) {
85 + if ($[3] !== contextVar) {
86 t1 = { cb: () => contextVar.val * 4 };
87 - $[3] = contextVar.val;
87 + $[3] = contextVar;
88 $[4] = t1;
89 } else {
90 t1 = $[4];
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/rename-source-variables-nested-object-method.expect.md
+2 -2
@@ -43,7 +43,7 @@ const t0 = "module_t0";
43 const c_0 = "module_c_0";
44 function useFoo(props) {
45 const $0 = _c(2);
46 - const c_00 = $0[0] !== props.value;
46 + const c_00 = $0[0] !== props;
47 let t1;
48 if (c_00) {
49 const a = {
@@ -61,7 +61,7 @@ function useFoo(props) {
61 };
62
63 t1 = a.foo().bar();
64 - $0[0] = props.value;
64 + $0[0] = props;
65 $0[1] = t1;
66 } else {
67 t1 = $0[1];
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect-nested-lambdas.expect.md
+10 -15
@@ -35,7 +35,7 @@ function Component(props) {
35 import { c as _c } from "react/compiler-runtime"; // @enableTransitivelyFreezeFunctionExpressions:false
36
37 function Component(props) {
38 - const $ = _c(9);
38 + const $ = _c(7);
39 const item = useMutable(props.itemId);
40 const dispatch = useDispatch();
41 useFreeze(dispatch);
@@ -51,7 +51,8 @@ function Component(props) {
51 }
52 const exit = t0;
53 let t1;
54 - if ($[2] !== exit || $[3] !== item.value) {
54 + let t2;
55 + if ($[2] !== exit || $[3] !== item) {
56 t1 = () => {
57 const cleanup = GlobalEventEmitter.addListener("onInput", () => {
58 if (item.value) {
@@ -60,30 +61,24 @@ function Component(props) {
61 });
62 return () => cleanup.remove();
63 };
64 + t2 = [exit, item];
65 $[2] = exit;
64 - $[3] = item.value;
66 + $[3] = item;
67 $[4] = t1;
68 + $[5] = t2;
69 } else {
70 t1 = $[4];
68 - }
69 - let t2;
70 - if ($[5] !== exit || $[6] !== item) {
71 - t2 = [exit, item];
72 - $[5] = exit;
73 - $[6] = item;
74 - $[7] = t2;
75 - } else {
76 - t2 = $[7];
71 + t2 = $[5];
72 }
73 useEffect(t1, t2);
74
75 maybeMutate(item);
76 let t3;
82 - if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
77 + if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
78 t3 = <div />;
84 - $[8] = t3;
79 + $[6] = t3;
80 } else {
86 - t3 = $[8];
81 + t3 = $[6];
82 }
83 return t3;
84 }
compiler/packages/snap/src/SproutTodoFilter.ts
+1
@@ -450,6 +450,7 @@ const skipFilter = new Set([
450 'invalid-jsx-lowercase-localvar',
451
452 // bugs
453 + 'inner-function/nullable-objects/bug-invalid-array-map-manual',
454 'bug-object-expression-computed-key-modified-during-after-construction-hoisted-sequence-expr',
455 `bug-capturing-func-maybealias-captured-mutate`,
456 'bug-aliased-capture-aliased-mutate',