[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
+};