@samitouri / QOS-React / commits / 90bee81902

[compiler] Do not inline IIFEs in value blocks (#33548)

As discussed in chat, this is a simple fix to stop introducing labels inside expressions. The useMemo-with-optional test was added in https://github.com/facebook/react/commit/d70b2c2c4e85c2a7061214c15a8ff13167d10422 and crashes for the same reason- an unexpected label as a value block terminal. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33548). * __->__ #33548 * #33546

Jordan Brown committed Jun 16, 2025 at 21:53 UTC 90bee819028bfecb724df298da798607b6a76abf
7 files changed +203 -122
compiler/packages/babel-plugin-react-compiler/src/Inference/InlineImmediatelyInvokedFunctionExpressions.ts
+90 -83
@@ -17,6 +17,7 @@ import {
17 InstructionKind,
18 LabelTerminal,
19 Place,
20 + isStatementBlockKind,
21 makeInstructionId,
22 promoteTemporary,
23 reversePostorderBlocks,
@@ -90,100 +91,106 @@ export function inlineImmediatelyInvokedFunctionExpressions(
91 */
92 const queue = Array.from(fn.body.blocks.values());
93 queue: for (const block of queue) {
93 - for (let ii = 0; ii < block.instructions.length; ii++) {
94 - const instr = block.instructions[ii]!;
95 - switch (instr.value.kind) {
96 - case 'FunctionExpression': {
97 - if (instr.lvalue.identifier.name === null) {
98 - functions.set(instr.lvalue.identifier.id, instr.value);
99 - }
100 - break;
101 - }
102 - case 'CallExpression': {
103 - if (instr.value.args.length !== 0) {
104 - // We don't support inlining when there are arguments
105 - continue;
106 - }
107 - const body = functions.get(instr.value.callee.identifier.id);
108 - if (body === undefined) {
109 - // Not invoking a local function expression, can't inline
110 - continue;
94 + /*
95 + * We can't handle labels inside expressions yet, so we don't inline IIFEs if they are in an
96 + * expression block.
97 + */
98 + if (isStatementBlockKind(block.kind)) {
99 + for (let ii = 0; ii < block.instructions.length; ii++) {
100 + const instr = block.instructions[ii]!;
101 + switch (instr.value.kind) {
102 + case 'FunctionExpression': {
103 + if (instr.lvalue.identifier.name === null) {
104 + functions.set(instr.lvalue.identifier.id, instr.value);
105 + }
106 + break;
107 }
108 + case 'CallExpression': {
109 + if (instr.value.args.length !== 0) {
110 + // We don't support inlining when there are arguments
111 + continue;
112 + }
113 + const body = functions.get(instr.value.callee.identifier.id);
114 + if (body === undefined) {
115 + // Not invoking a local function expression, can't inline
116 + continue;
117 + }
118
113 - if (
114 - body.loweredFunc.func.params.length > 0 ||
115 - body.loweredFunc.func.async ||
116 - body.loweredFunc.func.generator
117 - ) {
118 - // Can't inline functions with params, or async/generator functions
119 - continue;
120 - }
119 + if (
120 + body.loweredFunc.func.params.length > 0 ||
121 + body.loweredFunc.func.async ||
122 + body.loweredFunc.func.generator
123 + ) {
124 + // Can't inline functions with params, or async/generator functions
125 + continue;
126 + }
127
122 - // We know this function is used for an IIFE and can prune it later
123 - inlinedFunctions.add(instr.value.callee.identifier.id);
128 + // We know this function is used for an IIFE and can prune it later
129 + inlinedFunctions.add(instr.value.callee.identifier.id);
130
125 - // Create a new block which will contain code following the IIFE call
126 - const continuationBlockId = fn.env.nextBlockId;
127 - const continuationBlock: BasicBlock = {
128 - id: continuationBlockId,
129 - instructions: block.instructions.slice(ii + 1),
130 - kind: block.kind,
131 - phis: new Set(),
132 - preds: new Set(),
133 - terminal: block.terminal,
134 - };
135 - fn.body.blocks.set(continuationBlockId, continuationBlock);
131 + // Create a new block which will contain code following the IIFE call
132 + const continuationBlockId = fn.env.nextBlockId;
133 + const continuationBlock: BasicBlock = {
134 + id: continuationBlockId,
135 + instructions: block.instructions.slice(ii + 1),
136 + kind: block.kind,
137 + phis: new Set(),
138 + preds: new Set(),
139 + terminal: block.terminal,
140 + };
141 + fn.body.blocks.set(continuationBlockId, continuationBlock);
142
137 - /*
138 - * Trim the original block to contain instructions up to (but not including)
139 - * the IIFE
140 - */
141 - block.instructions.length = ii;
143 + /*
144 + * Trim the original block to contain instructions up to (but not including)
145 + * the IIFE
146 + */
147 + block.instructions.length = ii;
148
143 - /*
144 - * To account for complex control flow within the lambda, we treat the lambda
145 - * as if it were a single labeled statement, and replace all returns with gotos
146 - * to the label fallthrough.
147 - */
148 - const newTerminal: LabelTerminal = {
149 - block: body.loweredFunc.func.body.entry,
150 - id: makeInstructionId(0),
151 - kind: 'label',
152 - fallthrough: continuationBlockId,
153 - loc: block.terminal.loc,
154 - };
155 - block.terminal = newTerminal;
149 + /*
150 + * To account for complex control flow within the lambda, we treat the lambda
151 + * as if it were a single labeled statement, and replace all returns with gotos
152 + * to the label fallthrough.
153 + */
154 + const newTerminal: LabelTerminal = {
155 + block: body.loweredFunc.func.body.entry,
156 + id: makeInstructionId(0),
157 + kind: 'label',
158 + fallthrough: continuationBlockId,
159 + loc: block.terminal.loc,
160 + };
161 + block.terminal = newTerminal;
162
157 - // We store the result in the IIFE temporary
158 - const result = instr.lvalue;
163 + // We store the result in the IIFE temporary
164 + const result = instr.lvalue;
165
160 - // Declare the IIFE temporary
161 - declareTemporary(fn.env, block, result);
166 + // Declare the IIFE temporary
167 + declareTemporary(fn.env, block, result);
168
163 - // Promote the temporary with a name as we require this to persist
164 - promoteTemporary(result.identifier);
169 + // Promote the temporary with a name as we require this to persist
170 + promoteTemporary(result.identifier);
171
166 - /*
167 - * Rewrite blocks from the lambda to replace any `return` with a
168 - * store to the result and `goto` the continuation block
169 - */
170 - for (const [id, block] of body.loweredFunc.func.body.blocks) {
171 - block.preds.clear();
172 - rewriteBlock(fn.env, block, continuationBlockId, result);
173 - fn.body.blocks.set(id, block);
174 - }
172 + /*
173 + * Rewrite blocks from the lambda to replace any `return` with a
174 + * store to the result and `goto` the continuation block
175 + */
176 + for (const [id, block] of body.loweredFunc.func.body.blocks) {
177 + block.preds.clear();
178 + rewriteBlock(fn.env, block, continuationBlockId, result);
179 + fn.body.blocks.set(id, block);
180 + }
181
176 - /*
177 - * Ensure we visit the continuation block, since there may have been
178 - * sequential IIFEs that need to be visited.
179 - */
180 - queue.push(continuationBlock);
181 - continue queue;
182 - }
183 - default: {
184 - for (const place of eachInstructionValueOperand(instr.value)) {
185 - // Any other use of a function expression means it isn't an IIFE
186 - functions.delete(place.identifier.id);
182 + /*
183 + * Ensure we visit the continuation block, since there may have been
184 + * sequential IIFEs that need to be visited.
185 + */
186 + queue.push(continuationBlock);
187 + continue queue;
188 + }
189 + default: {
190 + for (const place of eachInstructionValueOperand(instr.value)) {
191 + // Any other use of a function expression means it isn't an IIFE
192 + functions.delete(place.identifier.id);
193 + }
194 }
195 }
196 }
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-useMemo-with-optional.expect.md deleted
-32
@@ -1,32 +0,0 @@
1 -
2 -## Input
3 -
4 -```javascript
5 -function Component(props) {
6 - return (
7 - useMemo(() => {
8 - return [props.value];
9 - }) || []
10 - );
11 -}
12 -
13 -```
14 -
15 -
16 -## Error
17 -
18 -```
19 - 1 | function Component(props) {
20 - 2 | return (
21 -> 3 | useMemo(() => {
22 - | ^^^^^^^^^^^^^^^
23 -> 4 | return [props.value];
24 - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^
25 -> 5 | }) || []
26 - | ^^^^^^^^^^^^^ Todo: Support labeled statements combined with value blocks (conditional, logical, optional chaining, etc) (3:5)
27 - 6 | );
28 - 7 | }
29 - 8 |
30 -```
31 -
32 -
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-useMemo-with-optional.js deleted
-7
@@ -1,7 +0,0 @@
1 -function Component(props) {
2 - return (
3 - useMemo(() => {
4 - return [props.value];
5 - }) || []
6 - );
7 -}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/iife-inline-ternary.expect.md new
+40
@@ -0,0 +1,40 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function Component(props) {
6 + const x = props.foo
7 + ? 1
8 + : (() => {
9 + throw new Error('Did not receive 1');
10 + })();
11 + return items;
12 +}
13 +
14 +export const FIXTURE_ENTRYPOINT = {
15 + fn: Component,
16 + params: [{foo: true}],
17 +};
18 +
19 +```
20 +
21 +## Code
22 +
23 +```javascript
24 +function Component(props) {
25 + props.foo ? 1 : _temp();
26 + return items;
27 +}
28 +function _temp() {
29 + throw new Error("Did not receive 1");
30 +}
31 +
32 +export const FIXTURE_ENTRYPOINT = {
33 + fn: Component,
34 + params: [{ foo: true }],
35 +};
36 +
37 +```
38 +
39 +### Eval output
40 +(kind: exception) items is not defined
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/iife-inline-ternary.js new
+13
@@ -0,0 +1,13 @@
1 +function Component(props) {
2 + const x = props.foo
3 + ? 1
4 + : (() => {
5 + throw new Error('Did not receive 1');
6 + })();
7 + return items;
8 +}
9 +
10 +export const FIXTURE_ENTRYPOINT = {
11 + fn: Component,
12 + params: [{foo: true}],
13 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-with-optional.expect.md new
+47
@@ -0,0 +1,47 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import {useMemo} from 'react';
6 +function Component(props) {
7 + return (
8 + useMemo(() => {
9 + return [props.value];
10 + }) || []
11 + );
12 +}
13 +
14 +export const FIXTURE_ENTRYPOINT = {
15 + fn: Component,
16 + params: [{value: 1}],
17 +};
18 +
19 +```
20 +
21 +## Code
22 +
23 +```javascript
24 +import { c as _c } from "react/compiler-runtime";
25 +import { useMemo } from "react";
26 +function Component(props) {
27 + const $ = _c(2);
28 + let t0;
29 + if ($[0] !== props.value) {
30 + t0 = (() => [props.value])() || [];
31 + $[0] = props.value;
32 + $[1] = t0;
33 + } else {
34 + t0 = $[1];
35 + }
36 + return t0;
37 +}
38 +
39 +export const FIXTURE_ENTRYPOINT = {
40 + fn: Component,
41 + params: [{ value: 1 }],
42 +};
43 +
44 +```
45 +
46 +### Eval output
47 +(kind: ok) [1]
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-with-optional.js new
+13
@@ -0,0 +1,13 @@
1 +import {useMemo} from 'react';
2 +function Component(props) {
3 + return (
4 + useMemo(() => {
5 + return [props.value];
6 + }) || []
7 + );
8 +}
9 +
10 +export const FIXTURE_ENTRYPOINT = {
11 + fn: Component,
12 + params: [{value: 1}],
13 +};