diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts index eb8fe5ad4e8..3f589bba954 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts @@ -583,6 +583,10 @@ function findOptionalPlaces(fn: HIRFunction): Set { testBlock = fn.body.blocks.get(terminal.fallthrough)!; break; } + case 'maybe-throw': { + testBlock = fn.body.blocks.get(terminal.continuation)!; + break; + } default: { CompilerError.invariant(false, { reason: `Unexpected terminal in optional`, diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/BuildReactiveFunction.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/BuildReactiveFunction.ts index c65ef1193fb..d43d3ebbb53 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/BuildReactiveFunction.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/BuildReactiveFunction.ts @@ -22,6 +22,7 @@ import { ReactiveBreakTerminal, ReactiveContinueTerminal, ReactiveFunction, + ReactiveInstruction, ReactiveLogicalValue, ReactiveSequenceValue, ReactiveTerminalStatement, @@ -62,6 +63,84 @@ class Driver { this.cx = cx; } + /* + * Wraps a continuation result with preceding instructions. If there are no + * instructions, returns the continuation as-is. Otherwise, wraps the continuation's + * value in a SequenceExpression with the instructions prepended. + */ + wrapWithSequence( + instructions: Array, + continuation: { + block: BlockId; + value: ReactiveValue; + place: Place; + id: InstructionId; + }, + loc: SourceLocation, + ): {block: BlockId; value: ReactiveValue; place: Place; id: InstructionId} { + if (instructions.length === 0) { + return continuation; + } + const sequence: ReactiveSequenceValue = { + kind: 'SequenceExpression', + instructions, + id: continuation.id, + value: continuation.value, + loc, + }; + return { + block: continuation.block, + value: sequence, + place: continuation.place, + id: continuation.id, + }; + } + + /* + * Extracts the result value from instructions at the end of a value block. + * Value blocks generally end in a StoreLocal to assign the value of the + * expression. These StoreLocal instructions can be pruned since we represent + * value blocks as compound values in ReactiveFunction (no phis). However, + * it's also possible to have a value block that ends in an AssignmentExpression, + * which we need to keep. So we only prune StoreLocal for temporaries. + */ + extractValueBlockResult( + instructions: BasicBlock['instructions'], + blockId: BlockId, + loc: SourceLocation, + ): {block: BlockId; place: Place; value: ReactiveValue; id: InstructionId} { + CompilerError.invariant(instructions.length !== 0, { + reason: `Expected non-empty instructions in extractValueBlockResult`, + description: null, + loc, + }); + const instr = instructions.at(-1)!; + let place: Place = instr.lvalue; + let value: ReactiveValue = instr.value; + if ( + value.kind === 'StoreLocal' && + value.lvalue.place.identifier.name === null + ) { + place = value.lvalue.place; + value = { + kind: 'LoadLocal', + place: value.value, + loc: value.value.loc, + }; + } + if (instructions.length === 1) { + return {block: blockId, place, value, id: instr.id}; + } + const sequence: ReactiveSequenceValue = { + kind: 'SequenceExpression', + instructions: instructions.slice(0, -1), + id: instr.id, + value, + loc, + }; + return {block: blockId, place, value: sequence, id: instr.id}; + } + traverseBlock(block: BasicBlock): ReactiveBlock { const blockValue: ReactiveBlock = []; this.visitBlock(block, blockValue); @@ -846,164 +925,138 @@ class Driver { } visitValueBlock( - id: BlockId, + blockId: BlockId, loc: SourceLocation, + fallthrough: BlockId | null = null, ): {block: BlockId; value: ReactiveValue; place: Place; id: InstructionId} { - const defaultBlock = this.cx.ir.blocks.get(id)!; - if (defaultBlock.terminal.kind === 'branch') { - const instructions = defaultBlock.instructions; - if (instructions.length === 0) { + const block = this.cx.ir.blocks.get(blockId)!; + // If we've reached the fallthrough block, stop recursing + if (fallthrough !== null && blockId === fallthrough) { + CompilerError.invariant(false, { + reason: 'Did not expect to reach the fallthrough of a value block', + description: `Reached bb${blockId}, which is the fallthrough for this value block`, + loc, + }); + } + if (block.terminal.kind === 'branch') { + if (block.instructions.length === 0) { return { - block: defaultBlock.id, - place: defaultBlock.terminal.test, + block: block.id, + place: block.terminal.test, value: { kind: 'LoadLocal', - place: defaultBlock.terminal.test, - loc: defaultBlock.terminal.test.loc, + place: block.terminal.test, + loc: block.terminal.test.loc, }, - id: defaultBlock.terminal.id, - }; - } else if (defaultBlock.instructions.length === 1) { - const instr = defaultBlock.instructions[0]!; - CompilerError.invariant( - instr.lvalue.identifier.id === - defaultBlock.terminal.test.identifier.id, - { - reason: - 'Expected branch block to end in an instruction that sets the test value', - loc: instr.lvalue.loc, - }, - ); - return { - block: defaultBlock.id, - place: instr.lvalue!, - value: instr.value, - id: instr.id, - }; - } else { - const instr = defaultBlock.instructions.at(-1)!; - const sequence: ReactiveSequenceValue = { - kind: 'SequenceExpression', - instructions: defaultBlock.instructions.slice(0, -1), - id: instr.id, - value: instr.value, - loc: loc, - }; - return { - block: defaultBlock.id, - place: defaultBlock.terminal.test, - value: sequence, - id: defaultBlock.terminal.id, + id: block.terminal.id, }; } - } else if (defaultBlock.terminal.kind === 'goto') { - const instructions = defaultBlock.instructions; - if (instructions.length === 0) { + return this.extractValueBlockResult(block.instructions, block.id, loc); + } else if (block.terminal.kind === 'goto') { + if (block.instructions.length === 0) { CompilerError.invariant(false, { - reason: 'Expected goto value block to have at least one instruction', - loc: GeneratedSource, + reason: 'Unexpected empty block with `goto` terminal', + description: `Block bb${block.id} is empty`, + loc, }); - } else if (defaultBlock.instructions.length === 1) { - const instr = defaultBlock.instructions[0]!; - let place: Place = instr.lvalue; - let value: ReactiveValue = instr.value; - if ( - /* - * Value blocks generally end in a StoreLocal to assign the value of the - * expression for this branch. These StoreLocal instructions can be pruned, - * since we represent the value blocks as a compund value in ReactiveFunction - * (no phis). However, it's also possible to have a value block that ends in - * an AssignmentExpression, which we need to keep. So we only prune - * StoreLocal for temporaries — any named/promoted values must be used - * elsewhere and aren't safe to prune. - */ - value.kind === 'StoreLocal' && - value.lvalue.place.identifier.name === null - ) { - place = value.lvalue.place; - value = { - kind: 'LoadLocal', - place: value.value, - loc: value.value.loc, - }; - } - return { - block: defaultBlock.id, - place, - value, - id: instr.id, - }; - } else { - const instr = defaultBlock.instructions.at(-1)!; - let place: Place = instr.lvalue; - let value: ReactiveValue = instr.value; - if ( - /* - * Value blocks generally end in a StoreLocal to assign the value of the - * expression for this branch. These StoreLocal instructions can be pruned, - * since we represent the value blocks as a compund value in ReactiveFunction - * (no phis). However, it's also possible to have a value block that ends in - * an AssignmentExpression, which we need to keep. So we only prune - * StoreLocal for temporaries — any named/promoted values must be used - * elsewhere and aren't safe to prune. - */ - value.kind === 'StoreLocal' && - value.lvalue.place.identifier.name === null - ) { - place = value.lvalue.place; - value = { - kind: 'LoadLocal', - place: value.value, - loc: value.value.loc, - }; - } - const sequence: ReactiveSequenceValue = { - kind: 'SequenceExpression', - instructions: defaultBlock.instructions.slice(0, -1), - id: instr.id, - value, - loc: loc, - }; - return { - block: defaultBlock.id, - place, - value: sequence, - id: instr.id, - }; } + return this.extractValueBlockResult(block.instructions, block.id, loc); + } else if (block.terminal.kind === 'maybe-throw') { + /* + * ReactiveFunction does not explicitly model maybe-throw semantics, + * so maybe-throw terminals in value blocks flatten away. In general + * we recurse to the continuation block. + * + * However, if the last portion + * of the value block is a potentially throwing expression, then the + * value block could be of the form + * ``` + * bb1: + * ...StoreLocal for the value block... + * maybe-throw continuation=bb2 + * bb2: + * goto (exit the value block) + * ``` + * + * Ie what would have been a StoreLocal+goto is split up because of + * the maybe-throw. We detect this case and return the value of the + * current block as the result of the value block + */ + const continuationId = block.terminal.continuation; + const continuationBlock = this.cx.ir.blocks.get(continuationId)!; + if ( + continuationBlock.instructions.length === 0 && + continuationBlock.terminal.kind === 'goto' + ) { + return this.extractValueBlockResult( + block.instructions, + continuationBlock.id, + loc, + ); + } + + const continuation = this.visitValueBlock( + continuationId, + loc, + fallthrough, + ); + return this.wrapWithSequence(block.instructions, continuation, loc); } else { /* * The value block ended in a value terminal, recurse to get the value - * of that terminal + * of that terminal and stitch them together in a sequence. */ - const init = this.visitValueBlockTerminal(defaultBlock.terminal); - // Code following the logical terminal + const init = this.visitValueBlockTerminal(block.terminal); const final = this.visitValueBlock(init.fallthrough, loc); - // Stitch the two together... - const sequence: ReactiveSequenceValue = { - kind: 'SequenceExpression', - instructions: [ - ...defaultBlock.instructions, - { - id: init.id, - loc, - lvalue: init.place, - value: init.value, - }, + return this.wrapWithSequence( + [ + ...block.instructions, + {id: init.id, loc, lvalue: init.place, value: init.value}, ], - id: final.id, - value: final.value, + final, loc, - }; - return { - block: init.fallthrough, - value: sequence, - place: final.place, - id: final.id, - }; + ); } } + /* + * Visits the test block of a value terminal (optional, logical, ternary) and + * returns the result along with the branch terminal. Throws a todo error if + * the test block does not end in a branch terminal. + */ + visitTestBlock( + testBlockId: BlockId, + loc: SourceLocation, + terminalKind: string, + ): { + test: { + block: BlockId; + value: ReactiveValue; + place: Place; + id: InstructionId; + }; + branch: {consequent: BlockId; alternate: BlockId; loc: SourceLocation}; + } { + const test = this.visitValueBlock(testBlockId, loc); + const testBlock = this.cx.ir.blocks.get(test.block)!; + if (testBlock.terminal.kind !== 'branch') { + CompilerError.throwTodo({ + reason: `Unexpected terminal kind \`${testBlock.terminal.kind}\` for ${terminalKind} test block`, + description: null, + loc: testBlock.terminal.loc, + suggestions: null, + }); + } + return { + test, + branch: { + consequent: testBlock.terminal.consequent, + alternate: testBlock.terminal.alternate, + loc: testBlock.terminal.loc, + }, + }; + } + visitValueBlockTerminal(terminal: Terminal): { value: ReactiveValue; place: Place; @@ -1012,7 +1065,11 @@ class Driver { } { switch (terminal.kind) { case 'sequence': { - const block = this.visitValueBlock(terminal.block, terminal.loc); + const block = this.visitValueBlock( + terminal.block, + terminal.loc, + terminal.fallthrough, + ); return { value: block.value, place: block.place, @@ -1021,26 +1078,22 @@ class Driver { }; } case 'optional': { - const test = this.visitValueBlock(terminal.test, terminal.loc); - const testBlock = this.cx.ir.blocks.get(test.block)!; - if (testBlock.terminal.kind !== 'branch') { - CompilerError.throwTodo({ - reason: `Unexpected terminal kind \`${testBlock.terminal.kind}\` for optional test block`, - description: null, - loc: testBlock.terminal.loc, - suggestions: null, - }); - } + const {test, branch} = this.visitTestBlock( + terminal.test, + terminal.loc, + 'optional', + ); const consequent = this.visitValueBlock( - testBlock.terminal.consequent, + branch.consequent, terminal.loc, + terminal.fallthrough, ); const call: ReactiveSequenceValue = { kind: 'SequenceExpression', instructions: [ { id: test.id, - loc: testBlock.terminal.loc, + loc: branch.loc, lvalue: test.place, value: test.value, }, @@ -1063,20 +1116,15 @@ class Driver { }; } case 'logical': { - const test = this.visitValueBlock(terminal.test, terminal.loc); - const testBlock = this.cx.ir.blocks.get(test.block)!; - if (testBlock.terminal.kind !== 'branch') { - CompilerError.throwTodo({ - reason: `Unexpected terminal kind \`${testBlock.terminal.kind}\` for logical test block`, - description: null, - loc: testBlock.terminal.loc, - suggestions: null, - }); - } - + const {test, branch} = this.visitTestBlock( + terminal.test, + terminal.loc, + 'logical', + ); const leftFinal = this.visitValueBlock( - testBlock.terminal.consequent, + branch.consequent, terminal.loc, + terminal.fallthrough, ); const left: ReactiveSequenceValue = { kind: 'SequenceExpression', @@ -1093,8 +1141,9 @@ class Driver { loc: terminal.loc, }; const right = this.visitValueBlock( - testBlock.terminal.alternate, + branch.alternate, terminal.loc, + terminal.fallthrough, ); const value: ReactiveLogicalValue = { kind: 'LogicalExpression', @@ -1111,23 +1160,20 @@ class Driver { }; } case 'ternary': { - const test = this.visitValueBlock(terminal.test, terminal.loc); - const testBlock = this.cx.ir.blocks.get(test.block)!; - if (testBlock.terminal.kind !== 'branch') { - CompilerError.throwTodo({ - reason: `Unexpected terminal kind \`${testBlock.terminal.kind}\` for ternary test block`, - description: null, - loc: testBlock.terminal.loc, - suggestions: null, - }); - } + const {test, branch} = this.visitTestBlock( + terminal.test, + terminal.loc, + 'ternary', + ); const consequent = this.visitValueBlock( - testBlock.terminal.consequent, + branch.consequent, terminal.loc, + terminal.fallthrough, ); const alternate = this.visitValueBlock( - testBlock.terminal.alternate, + branch.alternate, terminal.loc, + terminal.fallthrough, ); const value: ReactiveTernaryValue = { kind: 'ConditionalExpression', @@ -1145,11 +1191,10 @@ class Driver { }; } case 'maybe-throw': { - CompilerError.throwTodo({ - reason: `Support value blocks (conditional, logical, optional chaining, etc) within a try/catch statement`, + CompilerError.invariant(false, { + reason: `Unexpected maybe-throw in visitValueBlockTerminal - should be handled in visitValueBlock`, description: null, loc: terminal.loc, - suggestions: null, }); } case 'label': { diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateExhaustiveDependencies.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateExhaustiveDependencies.ts index e8a0c7dec31..b8647ec7c9b 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateExhaustiveDependencies.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateExhaustiveDependencies.ts @@ -1016,6 +1016,10 @@ export function findOptionalPlaces( testBlock = fn.body.blocks.get(terminal.block)!; break; } + case 'maybe-throw': { + testBlock = fn.body.blocks.get(terminal.continuation)!; + break; + } default: { CompilerError.invariant(false, { reason: `Unexpected terminal in optional`, diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-unexpected-terminal-in-optional.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-unexpected-terminal-in-optional.expect.md deleted file mode 100644 index 30b95149263..00000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-unexpected-terminal-in-optional.expect.md +++ /dev/null @@ -1,34 +0,0 @@ - -## Input - -```javascript -const Foo = ({json}) => { - try { - const foo = JSON.parse(json)?.foo; - return {foo}; - } catch { - return null; - } -}; - -``` - - -## Error - -``` -Found 1 error: - -Invariant: Unexpected terminal in optional - -error.bug-invariant-unexpected-terminal-in-optional.ts:3:16 - 1 | const Foo = ({json}) => { - 2 | try { -> 3 | const foo = JSON.parse(json)?.foo; - | ^^^^ Unexpected maybe-throw in optional - 4 | return {foo}; - 5 | } catch { - 6 | return null; -``` - - \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-unexpected-terminal-in-optional.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-unexpected-terminal-in-optional.js deleted file mode 100644 index 961640bfbd3..00000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-unexpected-terminal-in-optional.js +++ /dev/null @@ -1,8 +0,0 @@ -const Foo = ({json}) => { - try { - const foo = JSON.parse(json)?.foo; - return {foo}; - } catch { - return null; - } -}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-logical-expression-within-try-catch.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-logical-expression-within-try-catch.expect.md deleted file mode 100644 index 1aa5c4b5a76..00000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-logical-expression-within-try-catch.expect.md +++ /dev/null @@ -1,35 +0,0 @@ - -## Input - -```javascript -function Component(props) { - let result; - try { - result = props.cond && props.foo; - } catch (e) { - console.log(e); - } - return result; -} - -``` - - -## Error - -``` -Found 1 error: - -Todo: Support value blocks (conditional, logical, optional chaining, etc) within a try/catch statement - -error.todo-logical-expression-within-try-catch.ts:4:13 - 2 | let result; - 3 | try { -> 4 | result = props.cond && props.foo; - | ^^^^^ Support value blocks (conditional, logical, optional chaining, etc) within a try/catch statement - 5 | } catch (e) { - 6 | console.log(e); - 7 | } -``` - - \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-logical-expression-within-try-catch.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-logical-expression-within-try-catch.js deleted file mode 100644 index 4ccfb99d0c6..00000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-logical-expression-within-try-catch.js +++ /dev/null @@ -1,9 +0,0 @@ -function Component(props) { - let result; - try { - result = props.cond && props.foo; - } catch (e) { - console.log(e); - } - return result; -} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-optional-call-chain-in-logical-expr.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-optional-call-chain-in-logical-expr.expect.md deleted file mode 100644 index 38bb5800a82..00000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-optional-call-chain-in-logical-expr.expect.md +++ /dev/null @@ -1,37 +0,0 @@ - -## Input - -```javascript -import {useNoAlias} from 'shared-runtime'; - -function useFoo(props: {value: {x: string; y: string} | null}) { - const value = props.value; - return useNoAlias(value?.x, value?.y) ?? {}; -} - -export const FIXTURE_ENTRYPONT = { - fn: useFoo, - props: [{value: null}], -}; - -``` - - -## Error - -``` -Found 1 error: - -Todo: Unexpected terminal kind `optional` for logical test block - -error.todo-optional-call-chain-in-logical-expr.ts:5:30 - 3 | function useFoo(props: {value: {x: string; y: string} | null}) { - 4 | const value = props.value; -> 5 | return useNoAlias(value?.x, value?.y) ?? {}; - | ^^^^^^^^ Unexpected terminal kind `optional` for logical test block - 6 | } - 7 | - 8 | export const FIXTURE_ENTRYPONT = { -``` - - \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-optional-call-chain-in-ternary.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-optional-call-chain-in-ternary.expect.md deleted file mode 100644 index 897b29ce6a5..00000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-optional-call-chain-in-ternary.expect.md +++ /dev/null @@ -1,37 +0,0 @@ - -## Input - -```javascript -import {useNoAlias} from 'shared-runtime'; - -function useFoo(props: {value: {x: string; y: string} | null}) { - const value = props.value; - return useNoAlias(value?.x, value?.y) ? {} : null; -} - -export const FIXTURE_ENTRYPONT = { - fn: useFoo, - props: [{value: null}], -}; - -``` - - -## Error - -``` -Found 1 error: - -Todo: Unexpected terminal kind `optional` for ternary test block - -error.todo-optional-call-chain-in-ternary.ts:5:30 - 3 | function useFoo(props: {value: {x: string; y: string} | null}) { - 4 | const value = props.value; -> 5 | return useNoAlias(value?.x, value?.y) ? {} : null; - | ^^^^^^^^ Unexpected terminal kind `optional` for ternary test block - 6 | } - 7 | - 8 | export const FIXTURE_ENTRYPONT = { -``` - - \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-repro-declaration-for-all-identifiers.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-repro-declaration-for-all-identifiers.expect.md index 6998caf4049..87bd7cfa0f6 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-repro-declaration-for-all-identifiers.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-repro-declaration-for-all-identifiers.expect.md @@ -18,13 +18,15 @@ function Foo() { ``` Found 1 error: -Todo: Support value blocks (conditional, logical, optional chaining, etc) within a try/catch statement +Invariant: Expected a variable declaration -error.todo-repro-declaration-for-all-identifiers.ts:5:20 +Got ExpressionStatement. + +error.todo-repro-declaration-for-all-identifiers.ts:5:4 3 | // NOTE: this fixture previously failed during LeaveSSA; 4 | // double-check this code when supporting value blocks in try/catch > 5 | for (let i = 0; i < 2; i++) {} - | ^ Support value blocks (conditional, logical, optional chaining, etc) within a try/catch statement + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Expected a variable declaration 6 | } catch {} 7 | } 8 | diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-call-chain-in-logical-expr.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-call-chain-in-logical-expr.expect.md new file mode 100644 index 00000000000..60c1a427cd5 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-call-chain-in-logical-expr.expect.md @@ -0,0 +1,37 @@ + +## Input + +```javascript +import {useNoAlias} from 'shared-runtime'; + +function useFoo(props: {value: {x: string; y: string} | null}) { + const value = props.value; + return useNoAlias(value?.x, value?.y) ?? {}; +} + +export const FIXTURE_ENTRYPONT = { + fn: useFoo, + props: [{value: null}], +}; + +``` + +## Code + +```javascript +import { useNoAlias } from "shared-runtime"; + +function useFoo(props) { + const value = props.value; + return useNoAlias(value?.x, value?.y) ?? {}; +} + +export const FIXTURE_ENTRYPONT = { + fn: useFoo, + props: [{ value: null }], +}; + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-optional-call-chain-in-logical-expr.ts b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-call-chain-in-logical-expr.ts similarity index 100% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-optional-call-chain-in-logical-expr.ts rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-call-chain-in-logical-expr.ts diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-call-chain-in-ternary.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-call-chain-in-ternary.expect.md new file mode 100644 index 00000000000..c8c2a77fff6 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-call-chain-in-ternary.expect.md @@ -0,0 +1,37 @@ + +## Input + +```javascript +import {useNoAlias} from 'shared-runtime'; + +function useFoo(props: {value: {x: string; y: string} | null}) { + const value = props.value; + return useNoAlias(value?.x, value?.y) ? {} : null; +} + +export const FIXTURE_ENTRYPONT = { + fn: useFoo, + props: [{value: null}], +}; + +``` + +## Code + +```javascript +import { useNoAlias } from "shared-runtime"; + +function useFoo(props) { + const value = props.value; + return useNoAlias(value?.x, value?.y) ? {} : null; +} + +export const FIXTURE_ENTRYPONT = { + fn: useFoo, + props: [{ value: null }], +}; + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-optional-call-chain-in-ternary.ts b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-call-chain-in-ternary.ts similarity index 100% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-optional-call-chain-in-ternary.ts rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-call-chain-in-ternary.ts diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-logical-and-optional.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-logical-and-optional.expect.md new file mode 100644 index 00000000000..c6ef6ba668d --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-logical-and-optional.expect.md @@ -0,0 +1,85 @@ + +## Input + +```javascript +function Component({cond, obj, items}) { + try { + // items.length is accessed WITHIN the && expression + const result = cond && obj?.value && items.length; + return
{String(result)}
; + } catch { + return
error
; + } +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{cond: true, obj: {value: 'hello'}, items: [1, 2]}], + sequentialRenders: [ + {cond: true, obj: {value: 'hello'}, items: [1, 2]}, + {cond: true, obj: {value: 'hello'}, items: [1, 2]}, + {cond: true, obj: {value: 'world'}, items: [1, 2, 3]}, + {cond: false, obj: {value: 'hello'}, items: [1]}, + {cond: true, obj: null, items: [1]}, + {cond: true, obj: {value: 'test'}, items: null}, // errors because items.length throws WITHIN the && chain + {cond: null, obj: {value: 'test'}, items: [1]}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +function Component(t0) { + const $ = _c(3); + const { cond, obj, items } = t0; + try { + const result = cond && obj?.value && items.length; + const t1 = String(result); + let t2; + if ($[0] !== t1) { + t2 =
{t1}
; + $[0] = t1; + $[1] = t2; + } else { + t2 = $[1]; + } + return t2; + } catch { + let t1; + if ($[2] === Symbol.for("react.memo_cache_sentinel")) { + t1 =
error
; + $[2] = t1; + } else { + t1 = $[2]; + } + return t1; + } +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ cond: true, obj: { value: "hello" }, items: [1, 2] }], + sequentialRenders: [ + { cond: true, obj: { value: "hello" }, items: [1, 2] }, + { cond: true, obj: { value: "hello" }, items: [1, 2] }, + { cond: true, obj: { value: "world" }, items: [1, 2, 3] }, + { cond: false, obj: { value: "hello" }, items: [1] }, + { cond: true, obj: null, items: [1] }, + { cond: true, obj: { value: "test" }, items: null }, // errors because items.length throws WITHIN the && chain + { cond: null, obj: { value: "test" }, items: [1] }, + ], +}; + +``` + +### Eval output +(kind: ok)
2
+
2
+
3
+
false
+
undefined
+
error
+
null
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-logical-and-optional.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-logical-and-optional.js new file mode 100644 index 00000000000..1ad8011b86f --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-logical-and-optional.js @@ -0,0 +1,23 @@ +function Component({cond, obj, items}) { + try { + // items.length is accessed WITHIN the && expression + const result = cond && obj?.value && items.length; + return
{String(result)}
; + } catch { + return
error
; + } +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{cond: true, obj: {value: 'hello'}, items: [1, 2]}], + sequentialRenders: [ + {cond: true, obj: {value: 'hello'}, items: [1, 2]}, + {cond: true, obj: {value: 'hello'}, items: [1, 2]}, + {cond: true, obj: {value: 'world'}, items: [1, 2, 3]}, + {cond: false, obj: {value: 'hello'}, items: [1]}, + {cond: true, obj: null, items: [1]}, + {cond: true, obj: {value: 'test'}, items: null}, // errors because items.length throws WITHIN the && chain + {cond: null, obj: {value: 'test'}, items: [1]}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-logical-expression.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-logical-expression.expect.md new file mode 100644 index 00000000000..a28358220ab --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-logical-expression.expect.md @@ -0,0 +1,69 @@ + +## Input + +```javascript +function Component(props) { + let result; + try { + // items.length is accessed WITHIN the && expression + result = props.cond && props.foo && props.items.length; + } catch (e) { + result = 'error'; + } + return result; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{cond: true, foo: true, items: [1, 2, 3]}], + sequentialRenders: [ + {cond: true, foo: true, items: [1, 2, 3]}, + {cond: true, foo: true, items: [1, 2, 3]}, + {cond: true, foo: true, items: [1, 2, 3, 4]}, + {cond: false, foo: true, items: [1, 2, 3]}, + {cond: true, foo: false, items: [1, 2, 3]}, + {cond: true, foo: true, items: null}, // errors because props.items.length throws + {cond: null, foo: true, items: [1]}, + ], +}; + +``` + +## Code + +```javascript +function Component(props) { + let result; + try { + result = props.cond && props.foo && props.items.length; + } catch (t0) { + result = "error"; + } + + return result; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ cond: true, foo: true, items: [1, 2, 3] }], + sequentialRenders: [ + { cond: true, foo: true, items: [1, 2, 3] }, + { cond: true, foo: true, items: [1, 2, 3] }, + { cond: true, foo: true, items: [1, 2, 3, 4] }, + { cond: false, foo: true, items: [1, 2, 3] }, + { cond: true, foo: false, items: [1, 2, 3] }, + { cond: true, foo: true, items: null }, // errors because props.items.length throws + { cond: null, foo: true, items: [1] }, + ], +}; + +``` + +### Eval output +(kind: ok) 3 +3 +4 +false +false +"error" +null \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-logical-expression.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-logical-expression.js new file mode 100644 index 00000000000..7744d1e6d7a --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-logical-expression.js @@ -0,0 +1,24 @@ +function Component(props) { + let result; + try { + // items.length is accessed WITHIN the && expression + result = props.cond && props.foo && props.items.length; + } catch (e) { + result = 'error'; + } + return result; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{cond: true, foo: true, items: [1, 2, 3]}], + sequentialRenders: [ + {cond: true, foo: true, items: [1, 2, 3]}, + {cond: true, foo: true, items: [1, 2, 3]}, + {cond: true, foo: true, items: [1, 2, 3, 4]}, + {cond: false, foo: true, items: [1, 2, 3]}, + {cond: true, foo: false, items: [1, 2, 3]}, + {cond: true, foo: true, items: null}, // errors because props.items.length throws + {cond: null, foo: true, items: [1]}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-multiple-value-blocks.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-multiple-value-blocks.expect.md new file mode 100644 index 00000000000..3e59f530af6 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-multiple-value-blocks.expect.md @@ -0,0 +1,163 @@ + +## Input + +```javascript +function Component({a, b, cond, items}) { + try { + const x = a?.value; + // items.length is accessed WITHIN the ternary expression - throws if items is null + const y = cond ? b?.first : items.length; + const z = x && y; + return ( +
+ {String(x)}-{String(y)}-{String(z)} +
+ ); + } catch { + return
error
; + } +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [ + { + a: {value: 'A'}, + b: {first: 'B1', second: 'B2'}, + cond: true, + items: [1, 2, 3], + }, + ], + sequentialRenders: [ + { + a: {value: 'A'}, + b: {first: 'B1', second: 'B2'}, + cond: true, + items: [1, 2, 3], + }, + { + a: {value: 'A'}, + b: {first: 'B1', second: 'B2'}, + cond: true, + items: [1, 2, 3], + }, + { + a: {value: 'A'}, + b: {first: 'B1', second: 'B2'}, + cond: false, + items: [1, 2], + }, + {a: null, b: {first: 'B1', second: 'B2'}, cond: true, items: [1, 2, 3]}, + {a: {value: 'A'}, b: null, cond: true, items: [1, 2, 3]}, // b?.first is safe (returns undefined) + {a: {value: 'A'}, b: {first: 'B1', second: 'B2'}, cond: false, items: null}, // errors because items.length throws when cond=false + { + a: {value: ''}, + b: {first: 'B1', second: 'B2'}, + cond: true, + items: [1, 2, 3, 4], + }, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +function Component(t0) { + const $ = _c(5); + const { a, b, cond, items } = t0; + try { + const x = a?.value; + + const y = cond ? b?.first : items.length; + const z = x && y; + + const t1 = String(x); + const t2 = String(y); + const t3 = String(z); + let t4; + if ($[0] !== t1 || $[1] !== t2 || $[2] !== t3) { + t4 = ( +
+ {t1}-{t2}-{t3} +
+ ); + $[0] = t1; + $[1] = t2; + $[2] = t3; + $[3] = t4; + } else { + t4 = $[3]; + } + return t4; + } catch { + let t1; + if ($[4] === Symbol.for("react.memo_cache_sentinel")) { + t1 =
error
; + $[4] = t1; + } else { + t1 = $[4]; + } + return t1; + } +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [ + { + a: { value: "A" }, + b: { first: "B1", second: "B2" }, + cond: true, + items: [1, 2, 3], + }, + ], + + sequentialRenders: [ + { + a: { value: "A" }, + b: { first: "B1", second: "B2" }, + cond: true, + items: [1, 2, 3], + }, + { + a: { value: "A" }, + b: { first: "B1", second: "B2" }, + cond: true, + items: [1, 2, 3], + }, + { + a: { value: "A" }, + b: { first: "B1", second: "B2" }, + cond: false, + items: [1, 2], + }, + { a: null, b: { first: "B1", second: "B2" }, cond: true, items: [1, 2, 3] }, + { a: { value: "A" }, b: null, cond: true, items: [1, 2, 3] }, // b?.first is safe (returns undefined) + { + a: { value: "A" }, + b: { first: "B1", second: "B2" }, + cond: false, + items: null, + }, // errors because items.length throws when cond=false + { + a: { value: "" }, + b: { first: "B1", second: "B2" }, + cond: true, + items: [1, 2, 3, 4], + }, + ], +}; + +``` + +### Eval output +(kind: ok)
A-B1-B1
+
A-B1-B1
+
A-2-2
+
undefined-B1-undefined
+
A-undefined-undefined
+
error
+
-B1-
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-multiple-value-blocks.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-multiple-value-blocks.js new file mode 100644 index 00000000000..288f3e4cca3 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-multiple-value-blocks.js @@ -0,0 +1,56 @@ +function Component({a, b, cond, items}) { + try { + const x = a?.value; + // items.length is accessed WITHIN the ternary expression - throws if items is null + const y = cond ? b?.first : items.length; + const z = x && y; + return ( +
+ {String(x)}-{String(y)}-{String(z)} +
+ ); + } catch { + return
error
; + } +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [ + { + a: {value: 'A'}, + b: {first: 'B1', second: 'B2'}, + cond: true, + items: [1, 2, 3], + }, + ], + sequentialRenders: [ + { + a: {value: 'A'}, + b: {first: 'B1', second: 'B2'}, + cond: true, + items: [1, 2, 3], + }, + { + a: {value: 'A'}, + b: {first: 'B1', second: 'B2'}, + cond: true, + items: [1, 2, 3], + }, + { + a: {value: 'A'}, + b: {first: 'B1', second: 'B2'}, + cond: false, + items: [1, 2], + }, + {a: null, b: {first: 'B1', second: 'B2'}, cond: true, items: [1, 2, 3]}, + {a: {value: 'A'}, b: null, cond: true, items: [1, 2, 3]}, // b?.first is safe (returns undefined) + {a: {value: 'A'}, b: {first: 'B1', second: 'B2'}, cond: false, items: null}, // errors because items.length throws when cond=false + { + a: {value: ''}, + b: {first: 'B1', second: 'B2'}, + cond: true, + items: [1, 2, 3, 4], + }, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-nested-optional-chaining.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-nested-optional-chaining.expect.md new file mode 100644 index 00000000000..cd92103cc55 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-nested-optional-chaining.expect.md @@ -0,0 +1,104 @@ + +## Input + +```javascript +function Component({data, fallback}) { + try { + // fallback.default is accessed WITHIN the optional chain via nullish coalescing + const value = data?.nested?.deeply?.value ?? fallback.default; + return
{value}
; + } catch { + return
error
; + } +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [ + {data: {nested: {deeply: {value: 'found'}}}, fallback: {default: 'none'}}, + ], + sequentialRenders: [ + {data: {nested: {deeply: {value: 'found'}}}, fallback: {default: 'none'}}, + {data: {nested: {deeply: {value: 'found'}}}, fallback: {default: 'none'}}, + {data: {nested: {deeply: {value: 'changed'}}}, fallback: {default: 'none'}}, + {data: {nested: {deeply: null}}, fallback: {default: 'none'}}, // uses fallback.default + {data: {nested: null}, fallback: {default: 'none'}}, // uses fallback.default + {data: null, fallback: null}, // errors because fallback.default throws + {data: {nested: {deeply: {value: 42}}}, fallback: {default: 'none'}}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +function Component(t0) { + const $ = _c(3); + const { data, fallback } = t0; + try { + const value = data?.nested?.deeply?.value ?? fallback.default; + let t1; + if ($[0] !== value) { + t1 =
{value}
; + $[0] = value; + $[1] = t1; + } else { + t1 = $[1]; + } + return t1; + } catch { + let t1; + if ($[2] === Symbol.for("react.memo_cache_sentinel")) { + t1 =
error
; + $[2] = t1; + } else { + t1 = $[2]; + } + return t1; + } +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [ + { + data: { nested: { deeply: { value: "found" } } }, + fallback: { default: "none" }, + }, + ], + + sequentialRenders: [ + { + data: { nested: { deeply: { value: "found" } } }, + fallback: { default: "none" }, + }, + { + data: { nested: { deeply: { value: "found" } } }, + fallback: { default: "none" }, + }, + { + data: { nested: { deeply: { value: "changed" } } }, + fallback: { default: "none" }, + }, + { data: { nested: { deeply: null } }, fallback: { default: "none" } }, // uses fallback.default + { data: { nested: null }, fallback: { default: "none" } }, // uses fallback.default + { data: null, fallback: null }, // errors because fallback.default throws + { + data: { nested: { deeply: { value: 42 } } }, + fallback: { default: "none" }, + }, + ], +}; + +``` + +### Eval output +(kind: ok)
found
+
found
+
changed
+
none
+
none
+
error
+
42
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-nested-optional-chaining.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-nested-optional-chaining.js new file mode 100644 index 00000000000..b079525a971 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-nested-optional-chaining.js @@ -0,0 +1,25 @@ +function Component({data, fallback}) { + try { + // fallback.default is accessed WITHIN the optional chain via nullish coalescing + const value = data?.nested?.deeply?.value ?? fallback.default; + return
{value}
; + } catch { + return
error
; + } +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [ + {data: {nested: {deeply: {value: 'found'}}}, fallback: {default: 'none'}}, + ], + sequentialRenders: [ + {data: {nested: {deeply: {value: 'found'}}}, fallback: {default: 'none'}}, + {data: {nested: {deeply: {value: 'found'}}}, fallback: {default: 'none'}}, + {data: {nested: {deeply: {value: 'changed'}}}, fallback: {default: 'none'}}, + {data: {nested: {deeply: null}}, fallback: {default: 'none'}}, // uses fallback.default + {data: {nested: null}, fallback: {default: 'none'}}, // uses fallback.default + {data: null, fallback: null}, // errors because fallback.default throws + {data: {nested: {deeply: {value: 42}}}, fallback: {default: 'none'}}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-nullish-coalescing.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-nullish-coalescing.expect.md new file mode 100644 index 00000000000..614a379b51e --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-nullish-coalescing.expect.md @@ -0,0 +1,84 @@ + +## Input + +```javascript +function Component({a, b, fallback}) { + try { + // fallback.value is accessed WITHIN the ?? chain + const result = a ?? b ?? fallback.value; + return {result}; + } catch { + return error; + } +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 'first', b: 'second', fallback: {value: 'default'}}], + sequentialRenders: [ + {a: 'first', b: 'second', fallback: {value: 'default'}}, + {a: 'first', b: 'second', fallback: {value: 'default'}}, + {a: null, b: 'second', fallback: {value: 'default'}}, + {a: null, b: null, fallback: {value: 'fallback'}}, + {a: undefined, b: undefined, fallback: {value: 'fallback'}}, + {a: 0, b: 'not zero', fallback: {value: 'default'}}, + {a: null, b: null, fallback: null}, // errors because fallback.value throws WITHIN the ?? chain + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +function Component(t0) { + const $ = _c(3); + const { a, b, fallback } = t0; + try { + const result = a ?? b ?? fallback.value; + let t1; + if ($[0] !== result) { + t1 = {result}; + $[0] = result; + $[1] = t1; + } else { + t1 = $[1]; + } + return t1; + } catch { + let t1; + if ($[2] === Symbol.for("react.memo_cache_sentinel")) { + t1 = error; + $[2] = t1; + } else { + t1 = $[2]; + } + return t1; + } +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: "first", b: "second", fallback: { value: "default" } }], + sequentialRenders: [ + { a: "first", b: "second", fallback: { value: "default" } }, + { a: "first", b: "second", fallback: { value: "default" } }, + { a: null, b: "second", fallback: { value: "default" } }, + { a: null, b: null, fallback: { value: "fallback" } }, + { a: undefined, b: undefined, fallback: { value: "fallback" } }, + { a: 0, b: "not zero", fallback: { value: "default" } }, + { a: null, b: null, fallback: null }, // errors because fallback.value throws WITHIN the ?? chain + ], +}; + +``` + +### Eval output +(kind: ok) first +first +second +fallback +fallback +0 +error \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-nullish-coalescing.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-nullish-coalescing.js new file mode 100644 index 00000000000..07cf9e18c08 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-nullish-coalescing.js @@ -0,0 +1,23 @@ +function Component({a, b, fallback}) { + try { + // fallback.value is accessed WITHIN the ?? chain + const result = a ?? b ?? fallback.value; + return {result}; + } catch { + return error; + } +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 'first', b: 'second', fallback: {value: 'default'}}], + sequentialRenders: [ + {a: 'first', b: 'second', fallback: {value: 'default'}}, + {a: 'first', b: 'second', fallback: {value: 'default'}}, + {a: null, b: 'second', fallback: {value: 'default'}}, + {a: null, b: null, fallback: {value: 'fallback'}}, + {a: undefined, b: undefined, fallback: {value: 'fallback'}}, + {a: 0, b: 'not zero', fallback: {value: 'default'}}, + {a: null, b: null, fallback: null}, // errors because fallback.value throws WITHIN the ?? chain + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-optional-call.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-optional-call.expect.md new file mode 100644 index 00000000000..48926d1ff72 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-optional-call.expect.md @@ -0,0 +1,132 @@ + +## Input + +```javascript +function Component({obj, arg}) { + try { + // arg.value is accessed WITHIN the optional call expression as an argument + // When obj is non-null but arg is null, arg.value throws inside the optional chain + const result = obj?.method?.(arg.value); + return {result ?? 'no result'}; + } catch { + return error; + } +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{obj: {method: x => 'called:' + x}, arg: {value: 1}}], + sequentialRenders: [ + {obj: {method: x => 'called:' + x}, arg: {value: 1}}, + {obj: {method: x => 'called:' + x}, arg: {value: 1}}, + {obj: {method: x => 'different:' + x}, arg: {value: 2}}, + {obj: {method: null}, arg: {value: 3}}, + {obj: {notMethod: true}, arg: {value: 4}}, + {obj: null, arg: {value: 5}}, // obj is null, short-circuits so arg.value is NOT evaluated + {obj: {method: x => 'test:' + x}, arg: null}, // errors because arg.value throws WITHIN the optional call + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +function Component(t0) { + const $ = _c(6); + const { obj, arg } = t0; + try { + let t1; + if ($[0] !== arg || $[1] !== obj) { + t1 = obj?.method?.(arg.value); + $[0] = arg; + $[1] = obj; + $[2] = t1; + } else { + t1 = $[2]; + } + const result = t1; + const t2 = result ?? "no result"; + let t3; + if ($[3] !== t2) { + t3 = {t2}; + $[3] = t2; + $[4] = t3; + } else { + t3 = $[4]; + } + return t3; + } catch { + let t1; + if ($[5] === Symbol.for("react.memo_cache_sentinel")) { + t1 = error; + $[5] = t1; + } else { + t1 = $[5]; + } + return t1; + } +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [ + { + obj: { + method: (x) => { + return "called:" + x; + }, + }, + arg: { value: 1 }, + }, + ], + sequentialRenders: [ + { + obj: { + method: (x) => { + return "called:" + x; + }, + }, + arg: { value: 1 }, + }, + { + obj: { + method: (x) => { + return "called:" + x; + }, + }, + arg: { value: 1 }, + }, + { + obj: { + method: (x) => { + return "different:" + x; + }, + }, + arg: { value: 2 }, + }, + { obj: { method: null }, arg: { value: 3 } }, + { obj: { notMethod: true }, arg: { value: 4 } }, + { obj: null, arg: { value: 5 } }, // obj is null, short-circuits so arg.value is NOT evaluated + { + obj: { + method: (x) => { + return "test:" + x; + }, + }, + arg: null, + }, // errors because arg.value throws WITHIN the optional call + ], +}; + +``` + +### Eval output +(kind: ok) called:1 +called:1 +different:2 +no result +no result +no result +error \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-optional-call.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-optional-call.js new file mode 100644 index 00000000000..794bf3d46fa --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-optional-call.js @@ -0,0 +1,24 @@ +function Component({obj, arg}) { + try { + // arg.value is accessed WITHIN the optional call expression as an argument + // When obj is non-null but arg is null, arg.value throws inside the optional chain + const result = obj?.method?.(arg.value); + return {result ?? 'no result'}; + } catch { + return error; + } +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{obj: {method: x => 'called:' + x}, arg: {value: 1}}], + sequentialRenders: [ + {obj: {method: x => 'called:' + x}, arg: {value: 1}}, + {obj: {method: x => 'called:' + x}, arg: {value: 1}}, + {obj: {method: x => 'different:' + x}, arg: {value: 2}}, + {obj: {method: null}, arg: {value: 3}}, + {obj: {notMethod: true}, arg: {value: 4}}, + {obj: null, arg: {value: 5}}, // obj is null, short-circuits so arg.value is NOT evaluated + {obj: {method: x => 'test:' + x}, arg: null}, // errors because arg.value throws WITHIN the optional call + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-optional-chaining.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-optional-chaining.expect.md new file mode 100644 index 00000000000..0eee83b73c0 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-optional-chaining.expect.md @@ -0,0 +1,84 @@ + +## Input + +```javascript +function Foo({json}) { + try { + const foo = JSON.parse(json)?.foo; + return {foo}; + } catch { + return null; + } +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{json: '{"foo": "hello"}'}], + sequentialRenders: [ + {json: '{"foo": "hello"}'}, + {json: '{"foo": "hello"}'}, + {json: '{"foo": "world"}'}, + {json: '{"bar": "no foo"}'}, + {json: '{}'}, + {json: 'invalid json'}, + {json: '{"foo": 42}'}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +function Foo(t0) { + const $ = _c(4); + const { json } = t0; + try { + let t1; + if ($[0] !== json) { + t1 = JSON.parse(json)?.foo; + $[0] = json; + $[1] = t1; + } else { + t1 = $[1]; + } + const foo = t1; + let t2; + if ($[2] !== foo) { + t2 = {foo}; + $[2] = foo; + $[3] = t2; + } else { + t2 = $[3]; + } + return t2; + } catch { + return null; + } +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{ json: '{"foo": "hello"}' }], + sequentialRenders: [ + { json: '{"foo": "hello"}' }, + { json: '{"foo": "hello"}' }, + { json: '{"foo": "world"}' }, + { json: '{"bar": "no foo"}' }, + { json: "{}" }, + { json: "invalid json" }, + { json: '{"foo": 42}' }, + ], +}; + +``` + +### Eval output +(kind: ok) hello +hello +world + + +null +42 \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-optional-chaining.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-optional-chaining.js new file mode 100644 index 00000000000..da7c5bbab23 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-optional-chaining.js @@ -0,0 +1,22 @@ +function Foo({json}) { + try { + const foo = JSON.parse(json)?.foo; + return {foo}; + } catch { + return null; + } +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{json: '{"foo": "hello"}'}], + sequentialRenders: [ + {json: '{"foo": "hello"}'}, + {json: '{"foo": "hello"}'}, + {json: '{"foo": "world"}'}, + {json: '{"bar": "no foo"}'}, + {json: '{}'}, + {json: 'invalid json'}, + {json: '{"foo": 42}'}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-ternary-expression.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-ternary-expression.expect.md new file mode 100644 index 00000000000..9c8eff9825d --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-ternary-expression.expect.md @@ -0,0 +1,63 @@ + +## Input + +```javascript +function Component(props) { + let result; + try { + // fallback.value is accessed WITHIN the ternary's false branch + result = props.cond ? props.a : props.fallback.value; + } catch (e) { + result = 'error'; + } + return result; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{cond: true, a: 'hello', fallback: {value: 'world'}}], + sequentialRenders: [ + {cond: true, a: 'hello', fallback: {value: 'world'}}, + {cond: true, a: 'hello', fallback: {value: 'world'}}, + {cond: false, a: 'hello', fallback: {value: 'world'}}, + {cond: true, a: 'foo', fallback: {value: 'bar'}}, + {cond: false, a: 'foo', fallback: null}, // errors because fallback.value throws WITHIN the ternary + ], +}; + +``` + +## Code + +```javascript +function Component(props) { + let result; + try { + result = props.cond ? props.a : props.fallback.value; + } catch (t0) { + result = "error"; + } + + return result; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ cond: true, a: "hello", fallback: { value: "world" } }], + sequentialRenders: [ + { cond: true, a: "hello", fallback: { value: "world" } }, + { cond: true, a: "hello", fallback: { value: "world" } }, + { cond: false, a: "hello", fallback: { value: "world" } }, + { cond: true, a: "foo", fallback: { value: "bar" } }, + { cond: false, a: "foo", fallback: null }, // errors because fallback.value throws WITHIN the ternary + ], +}; + +``` + +### Eval output +(kind: ok) "hello" +"hello" +"world" +"foo" +"error" \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-ternary-expression.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-ternary-expression.js new file mode 100644 index 00000000000..0536f6eb93a --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-ternary-expression.js @@ -0,0 +1,22 @@ +function Component(props) { + let result; + try { + // fallback.value is accessed WITHIN the ternary's false branch + result = props.cond ? props.a : props.fallback.value; + } catch (e) { + result = 'error'; + } + return result; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{cond: true, a: 'hello', fallback: {value: 'world'}}], + sequentialRenders: [ + {cond: true, a: 'hello', fallback: {value: 'world'}}, + {cond: true, a: 'hello', fallback: {value: 'world'}}, + {cond: false, a: 'hello', fallback: {value: 'world'}}, + {cond: true, a: 'foo', fallback: {value: 'bar'}}, + {cond: false, a: 'foo', fallback: null}, // errors because fallback.value throws WITHIN the ternary + ], +};