diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InlineImmediatelyInvokedFunctionExpressions.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InlineImmediatelyInvokedFunctionExpressions.ts index c6c6f2f54fb..bbf3b0aeca0 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InlineImmediatelyInvokedFunctionExpressions.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InlineImmediatelyInvokedFunctionExpressions.ts @@ -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); - } - break; - } - 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; + /* + * 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; } + 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; - } + 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); + // 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); + // 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; + /* + * 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; + /* + * 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; + // We store the result in the IIFE temporary + const result = instr.lvalue; - // Declare the IIFE temporary - declareTemporary(fn.env, block, result); + // Declare the IIFE temporary + declareTemporary(fn.env, block, result); - // Promote the temporary with a name as we require this to persist - promoteTemporary(result.identifier); + // 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); - } + /* + * 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); + /* + * 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); + } } } } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-useMemo-with-optional.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-useMemo-with-optional.expect.md deleted file mode 100644 index b24f1f18f0d..00000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-useMemo-with-optional.expect.md +++ /dev/null @@ -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 | -``` - - \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-useMemo-with-optional.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-useMemo-with-optional.js deleted file mode 100644 index 533e2c370fe..00000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-useMemo-with-optional.js +++ /dev/null @@ -1,7 +0,0 @@ -function Component(props) { - return ( - useMemo(() => { - return [props.value]; - }) || [] - ); -} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/iife-inline-ternary.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/iife-inline-ternary.expect.md new file mode 100644 index 00000000000..bf4dddc6fc3 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/iife-inline-ternary.expect.md @@ -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 \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/iife-inline-ternary.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/iife-inline-ternary.js new file mode 100644 index 00000000000..56d20f7f9ff --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/iife-inline-ternary.js @@ -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}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-with-optional.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-with-optional.expect.md new file mode 100644 index 00000000000..260d695e09d --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-with-optional.expect.md @@ -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] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-with-optional.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-with-optional.js new file mode 100644 index 00000000000..a96c044a3b8 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-with-optional.js @@ -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}], +};