[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
d70b2c2c4e
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
This commit is contained in:
Jordan Brown 2025-06-16 21:53:50 -04:00 committed by GitHub
parent 75e78d243f
commit 90bee81902
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 212 additions and 131 deletions

View File

@ -17,6 +17,7 @@ import {
InstructionKind,
LabelTerminal,
Place,
isStatementBlockKind,
makeInstructionId,
promoteTemporary,
reversePostorderBlocks,
@ -90,100 +91,106 @@ export function inlineImmediatelyInvokedFunctionExpressions(
*/
const queue = Array.from(fn.body.blocks.values());
queue: for (const block of queue) {
for (let ii = 0; ii < block.instructions.length; ii++) {
const instr = block.instructions[ii]!;
switch (instr.value.kind) {
case 'FunctionExpression': {
if (instr.lvalue.identifier.name === null) {
functions.set(instr.lvalue.identifier.id, instr.value);
/*
* We can't handle labels inside expressions yet, so we don't inline IIFEs if they are in an
* expression block.
*/
if (isStatementBlockKind(block.kind)) {
for (let ii = 0; ii < block.instructions.length; ii++) {
const instr = block.instructions[ii]!;
switch (instr.value.kind) {
case 'FunctionExpression': {
if (instr.lvalue.identifier.name === null) {
functions.set(instr.lvalue.identifier.id, instr.value);
}
break;
}
break;
}
case 'CallExpression': {
if (instr.value.args.length !== 0) {
// We don't support inlining when there are arguments
continue;
case 'CallExpression': {
if (instr.value.args.length !== 0) {
// We don't support inlining when there are arguments
continue;
}
const body = functions.get(instr.value.callee.identifier.id);
if (body === undefined) {
// Not invoking a local function expression, can't inline
continue;
}
if (
body.loweredFunc.func.params.length > 0 ||
body.loweredFunc.func.async ||
body.loweredFunc.func.generator
) {
// Can't inline functions with params, or async/generator functions
continue;
}
// We know this function is used for an IIFE and can prune it later
inlinedFunctions.add(instr.value.callee.identifier.id);
// Create a new block which will contain code following the IIFE call
const continuationBlockId = fn.env.nextBlockId;
const continuationBlock: BasicBlock = {
id: continuationBlockId,
instructions: block.instructions.slice(ii + 1),
kind: block.kind,
phis: new Set(),
preds: new Set(),
terminal: block.terminal,
};
fn.body.blocks.set(continuationBlockId, continuationBlock);
/*
* Trim the original block to contain instructions up to (but not including)
* the IIFE
*/
block.instructions.length = ii;
/*
* To account for complex control flow within the lambda, we treat the lambda
* as if it were a single labeled statement, and replace all returns with gotos
* to the label fallthrough.
*/
const newTerminal: LabelTerminal = {
block: body.loweredFunc.func.body.entry,
id: makeInstructionId(0),
kind: 'label',
fallthrough: continuationBlockId,
loc: block.terminal.loc,
};
block.terminal = newTerminal;
// We store the result in the IIFE temporary
const result = instr.lvalue;
// Declare the IIFE temporary
declareTemporary(fn.env, block, result);
// Promote the temporary with a name as we require this to persist
promoteTemporary(result.identifier);
/*
* Rewrite blocks from the lambda to replace any `return` with a
* store to the result and `goto` the continuation block
*/
for (const [id, block] of body.loweredFunc.func.body.blocks) {
block.preds.clear();
rewriteBlock(fn.env, block, continuationBlockId, result);
fn.body.blocks.set(id, block);
}
/*
* Ensure we visit the continuation block, since there may have been
* sequential IIFEs that need to be visited.
*/
queue.push(continuationBlock);
continue queue;
}
const body = functions.get(instr.value.callee.identifier.id);
if (body === undefined) {
// Not invoking a local function expression, can't inline
continue;
}
if (
body.loweredFunc.func.params.length > 0 ||
body.loweredFunc.func.async ||
body.loweredFunc.func.generator
) {
// Can't inline functions with params, or async/generator functions
continue;
}
// We know this function is used for an IIFE and can prune it later
inlinedFunctions.add(instr.value.callee.identifier.id);
// Create a new block which will contain code following the IIFE call
const continuationBlockId = fn.env.nextBlockId;
const continuationBlock: BasicBlock = {
id: continuationBlockId,
instructions: block.instructions.slice(ii + 1),
kind: block.kind,
phis: new Set(),
preds: new Set(),
terminal: block.terminal,
};
fn.body.blocks.set(continuationBlockId, continuationBlock);
/*
* Trim the original block to contain instructions up to (but not including)
* the IIFE
*/
block.instructions.length = ii;
/*
* To account for complex control flow within the lambda, we treat the lambda
* as if it were a single labeled statement, and replace all returns with gotos
* to the label fallthrough.
*/
const newTerminal: LabelTerminal = {
block: body.loweredFunc.func.body.entry,
id: makeInstructionId(0),
kind: 'label',
fallthrough: continuationBlockId,
loc: block.terminal.loc,
};
block.terminal = newTerminal;
// We store the result in the IIFE temporary
const result = instr.lvalue;
// Declare the IIFE temporary
declareTemporary(fn.env, block, result);
// Promote the temporary with a name as we require this to persist
promoteTemporary(result.identifier);
/*
* Rewrite blocks from the lambda to replace any `return` with a
* store to the result and `goto` the continuation block
*/
for (const [id, block] of body.loweredFunc.func.body.blocks) {
block.preds.clear();
rewriteBlock(fn.env, block, continuationBlockId, result);
fn.body.blocks.set(id, block);
}
/*
* Ensure we visit the continuation block, since there may have been
* sequential IIFEs that need to be visited.
*/
queue.push(continuationBlock);
continue queue;
}
default: {
for (const place of eachInstructionValueOperand(instr.value)) {
// Any other use of a function expression means it isn't an IIFE
functions.delete(place.identifier.id);
default: {
for (const place of eachInstructionValueOperand(instr.value)) {
// Any other use of a function expression means it isn't an IIFE
functions.delete(place.identifier.id);
}
}
}
}

View File

@ -1,32 +0,0 @@
## Input
```javascript
function Component(props) {
return (
useMemo(() => {
return [props.value];
}) || []
);
}
```
## Error
```
1 | function Component(props) {
2 | return (
> 3 | useMemo(() => {
| ^^^^^^^^^^^^^^^
> 4 | return [props.value];
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 5 | }) || []
| ^^^^^^^^^^^^^ Todo: Support labeled statements combined with value blocks (conditional, logical, optional chaining, etc) (3:5)
6 | );
7 | }
8 |
```

View File

@ -1,7 +0,0 @@
function Component(props) {
return (
useMemo(() => {
return [props.value];
}) || []
);
}

View File

@ -0,0 +1,40 @@
## Input
```javascript
function Component(props) {
const x = props.foo
? 1
: (() => {
throw new Error('Did not receive 1');
})();
return items;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{foo: true}],
};
```
## Code
```javascript
function Component(props) {
props.foo ? 1 : _temp();
return items;
}
function _temp() {
throw new Error("Did not receive 1");
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ foo: true }],
};
```
### Eval output
(kind: exception) items is not defined

View File

@ -0,0 +1,13 @@
function Component(props) {
const x = props.foo
? 1
: (() => {
throw new Error('Did not receive 1');
})();
return items;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{foo: true}],
};

View File

@ -0,0 +1,47 @@
## Input
```javascript
import {useMemo} from 'react';
function Component(props) {
return (
useMemo(() => {
return [props.value];
}) || []
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 1}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { useMemo } from "react";
function Component(props) {
const $ = _c(2);
let t0;
if ($[0] !== props.value) {
t0 = (() => [props.value])() || [];
$[0] = props.value;
$[1] = t0;
} else {
t0 = $[1];
}
return t0;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ value: 1 }],
};
```
### Eval output
(kind: ok) [1]

View File

@ -0,0 +1,13 @@
import {useMemo} from 'react';
function Component(props) {
return (
useMemo(() => {
return [props.value];
}) || []
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 1}],
};