From cd0c4879a2959db91f9bd51a09dafefedd95fb17 Mon Sep 17 00:00:00 2001 From: Joseph Savona <6425824+josephsavona@users.noreply.github.com> Date: Tue, 3 Feb 2026 18:04:34 -0500 Subject: [PATCH] [compiler] Fix `for` loops in try/catch (#35686) This is a combination of a) a subagent for investigating compiler errors and b) testing that agent by fixing bugs with for loops within try/catch. My recent diffs to support maybe-throw within value blocks was incomplete and handled many cases, like optionals/logicals/etc within try/catch. However, the handling for for loops was making more assumptions and needed additional fixes. Key changes: * `maybe-throw` terminal `handler` is now nullable. PruneMaybeThrows nulls the handler for blocks that cannot throw, rather than changing to a `goto`. This preserves more information, and makes it easier for BuildReactiveFunction's visitValueBlock() to reconstruct the value blocks * Updates BuildReactiveFunction's handling of `for` init/test/update (and similar for `for..of` and `for..in`) to correctly extract value blocks. The previous logic made assumptions about the shape of the SequenceExpression which were incorrect in some cases within try/catch. The new helper extracts a flattened SequenceExpression. Supporting changes: * The agent itself (tested via this diff) * Updated the script for invoking snap to keep `compiler/` as the working directory, allowing relative paths to work more easily * Add an `--update` (`-u`) flag to `yarn snap minimize`, which updates the fixture in place w the minimized version --- compiler/.claude/agents/investigate-error.md | 113 +++++++ .../src/HIR/HIR.ts | 2 +- .../src/HIR/PrintHIR.ts | 4 +- .../src/HIR/visitors.ts | 6 +- .../Inference/InferMutationAliasingEffects.ts | 2 +- .../src/Optimization/PruneMaybeThrows.ts | 21 +- .../ReactiveScopes/BuildReactiveFunction.ts | 151 ++++------ ...-invariant-expected-break-target.expect.md | 32 -- ...ror.bug-invariant-expected-break-target.js | 15 - ...-declaration-for-all-identifiers.expect.md | 35 --- ...o-repro-declaration-for-all-identifiers.js | 7 - ...-declaration-for-all-identifiers.expect.md | 50 ++++ .../repro-declaration-for-all-identifiers.js | 12 + .../compiler/repro-for-in-in-try.expect.md | 102 +++++++ .../fixtures/compiler/repro-for-in-in-try.js | 23 ++ .../compiler/repro-for-loop-in-try.expect.md | 95 ++++++ .../compiler/repro-for-loop-in-try.js | 23 ++ .../compiler/repro-for-of-in-try.expect.md | 102 +++++++ .../fixtures/compiler/repro-for-of-in-try.js | 23 ++ ...epro-nested-try-catch-in-usememo.expect.md | 114 +++++++ .../repro-nested-try-catch-in-usememo.js | 38 +++ compiler/packages/snap/src/minimize.ts | 60 +--- compiler/packages/snap/src/reporter.ts | 19 +- compiler/packages/snap/src/runner.ts | 281 +++++++++++------- 24 files changed, 960 insertions(+), 370 deletions(-) create mode 100644 compiler/.claude/agents/investigate-error.md delete mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-expected-break-target.expect.md delete mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-expected-break-target.js delete mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-repro-declaration-for-all-identifiers.expect.md delete mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-repro-declaration-for-all-identifiers.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-declaration-for-all-identifiers.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-declaration-for-all-identifiers.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-for-in-in-try.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-for-in-in-try.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-for-loop-in-try.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-for-loop-in-try.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-for-of-in-try.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-for-of-in-try.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-nested-try-catch-in-usememo.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-nested-try-catch-in-usememo.js diff --git a/compiler/.claude/agents/investigate-error.md b/compiler/.claude/agents/investigate-error.md new file mode 100644 index 000000000000..b3673c596f74 --- /dev/null +++ b/compiler/.claude/agents/investigate-error.md @@ -0,0 +1,113 @@ +--- +name: investigate-error +description: Investigates React compiler errors to determine the root cause and identify potential mitigation(s). Use this agent when the user asks to 'investigate a bug', 'debug why this fixture errors', 'understand why the compiler is failing', 'find the root cause of a compiler issue', or when they provide a snippet of code and ask to debug. Use automatically when encountering a failing test case, in order to understand the root cause. +model: opus +color: pink +--- + +You are an expert React Compiler debugging specialist with deep knowledge of compiler internals, intermediate representations, and optimization passes. Your mission is to systematically investigate compiler bugs to identify root causes and provide actionable information for fixes. + +## Your Investigation Process + +### Step 1: Create Test Fixture +Create a new fixture file at `packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/.js` containing the problematic code. Use a descriptive name that reflects the issue (e.g., `bug-optional-chain-in-effect.js`). + +### Step 2: Run Debug Compilation +Execute `yarn snap -d -p ` to compile the fixture with full debug output. This shows the state of the program after each compilation pass. + +### Step 3: Analyze Compilation Results + +### Step 3a: If the fixture compiles successfully +- Compare the output against the user's expected behavior +- Review each compilation pass output from the `-d` flag +- Identify the first pass where the output diverges from expected behavior +- Proceed to binary search simplification + +### Step 3b: If the fixture errors +Execute `yarn snap minimize --update ` to remove non-critical aspects of the failing test case. This **updates the fixture in place**. + +Re-read the fixture file to see the latest, minimal reproduction of the error. + +### Step 4: Iteratively adjust the fixture until it stops erroring +After the previous step the fixture will have all extraneous aspects removed. Try to make further edits to determine the specific feature that is causing the error. + +Ideas: +* Replace immediately-invoked function expressions with labeled blocks +* Remove statements +* Simplify calls (remove arguments, replace the call with its lone argument) +* Simplify control flow statements by picking a single branch. Try using a labeled block with just the selected block +* Replace optional member/call expressions with non-optional versions +* Remove items in array/object expressions +* Remove properties from member expressions + +Try to make the minimal possible edit to get the fixture stop erroring. + +### Step 5: Compare Debug Outputs +With both minimal versions (failing and non-failing): +- Run `yarn snap -d -p ` on both +- Compare the debug output pass-by-pass +- Identify the exact pass where behavior diverges +- Note specific differences in HIR, effects, or generated code + +### Step 6: Investigate Compiler Logic +- Read the documentation for the problematic pass in `packages/babel-plugin-react-compiler/docs/passes/` +- Examine the pass implementation in `packages/babel-plugin-react-compiler/src/` +- Key directories to investigate: + - `src/HIR/` - IR definitions and utilities + - `src/Inference/` - Effect inference (aliasing, mutation) + - `src/Validation/` - Validation passes + - `src/Optimization/` - Optimization passes + - `src/ReactiveScopes/` - Reactive scope analysis +- Identify specific code locations that may be handling the pattern incorrectly + +## Output Format + +Provide a structured investigation report: + +``` +## Investigation Summary + +### Bug Description +[Brief description of the issue] + +### Minimal Failing Fixture +```javascript +// packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/.js +[minimal code that reproduces the error] +``` + +### Minimal Non-Failing Fixture +```javascript +// The simplest change that makes it work +[code that compiles correctly] +``` + +### Problematic Compiler Pass +[Name of the pass where the issue occurs] + +### Root Cause Analysis +[Explanation of what the compiler is doing wrong] + +### Suspect Code Locations +- `packages/babel-plugin-react-compiler/src/::` - [description of what may be incorrect] +- [additional locations if applicable] + +### Suggested Fix Direction +[Brief suggestion of how the bug might be fixed] +``` + +## Key Debugging Tips + +1. The debug output (`-d` flag) shows the program state after each pass - use this to pinpoint where things go wrong +2. Look for `@aliasingEffects=` on FunctionExpressions to understand data flow +3. Check for `Impure`, `Render`, `Capture` effects on instructions +4. The pass ordering in `Pipeline.ts` shows when effects are populated vs validated +5. Todo errors indicate unsupported but known patterns; Invariant errors indicate unexpected states + +## Important Reminders + +- Always create the fixture file before running tests +- Use descriptive fixture names that indicate the bug being investigated +- Keep both failing and non-failing minimal versions for your report +- Provide specific file:line:column references when identifying suspect code +- Read the relevant pass documentation before making conclusions about the cause diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts index ca9cd6945a9b..dca41eac92fe 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts @@ -612,7 +612,7 @@ export type TryTerminal = { export type MaybeThrowTerminal = { kind: 'maybe-throw'; continuation: BlockId; - handler: BlockId; + handler: BlockId | null; id: InstructionId; loc: SourceLocation; fallthrough?: never; diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts index b1e3746b0e35..56764c5ad07e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts @@ -291,7 +291,9 @@ export function printTerminal(terminal: Terminal): Array | string { break; } case 'maybe-throw': { - value = `[${terminal.id}] MaybeThrow continuation=bb${terminal.continuation} handler=bb${terminal.handler}`; + const handlerStr = + terminal.handler !== null ? `bb${terminal.handler}` : '(none)'; + value = `[${terminal.id}] MaybeThrow continuation=bb${terminal.continuation} handler=${handlerStr}`; if (terminal.effects != null) { value += `\n ${terminal.effects.map(printAliasingEffect).join('\n ')}`; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/visitors.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/visitors.ts index d27bfe0b0aed..abad1bbdf654 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/visitors.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/visitors.ts @@ -909,7 +909,7 @@ export function mapTerminalSuccessors( } case 'maybe-throw': { const continuation = fn(terminal.continuation); - const handler = fn(terminal.handler); + const handler = terminal.handler !== null ? fn(terminal.handler) : null; return { kind: 'maybe-throw', continuation, @@ -1083,7 +1083,9 @@ export function* eachTerminalSuccessor(terminal: Terminal): Iterable { } case 'maybe-throw': { yield terminal.continuation; - yield terminal.handler; + if (terminal.handler !== null) { + yield terminal.handler; + } break; } case 'try': { diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts index ca9166263111..0fb2cf9823c7 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts @@ -508,7 +508,7 @@ function inferBlock( const terminal = block.terminal; if (terminal.kind === 'try' && terminal.handlerBinding != null) { context.catchHandlers.set(terminal.handler, terminal.handlerBinding); - } else if (terminal.kind === 'maybe-throw') { + } else if (terminal.kind === 'maybe-throw' && terminal.handler !== null) { const handlerParam = context.catchHandlers.get(terminal.handler); if (handlerParam != null) { CompilerError.invariant(state.kind(handlerParam) != null, { diff --git a/compiler/packages/babel-plugin-react-compiler/src/Optimization/PruneMaybeThrows.ts b/compiler/packages/babel-plugin-react-compiler/src/Optimization/PruneMaybeThrows.ts index 5f934c145ff2..6d3164692556 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Optimization/PruneMaybeThrows.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Optimization/PruneMaybeThrows.ts @@ -9,7 +9,6 @@ import {CompilerError} from '..'; import { BlockId, GeneratedSource, - GotoVariant, HIRFunction, Instruction, assertConsistentIdentifiers, @@ -25,9 +24,15 @@ import { } from '../HIR/HIRBuilder'; import {printPlace} from '../HIR/PrintHIR'; -/* - * This pass prunes `maybe-throw` terminals for blocks that can provably *never* throw. - * For now this is very conservative, and only affects blocks with primitives or +/** + * This pass updates `maybe-throw` terminals for blocks that can provably *never* throw, + * nulling out the handler to indicate that control will always continue. Note that + * rewriting to a `goto` disrupts the structure of the HIR, making it more difficult to + * reconstruct an ast during BuildReactiveFunction. Preserving the maybe-throw makes the + * continuations clear, while nulling out the handler tells us that control cannot flow + * to the handler. + * + * For now the analysis is very conservative, and only affects blocks with primitives or * array/object literals. Even a variable reference could throw bc of the TDZ. */ export function pruneMaybeThrows(fn: HIRFunction): void { @@ -82,13 +87,7 @@ function pruneMaybeThrowsImpl(fn: HIRFunction): Map | null { if (!canThrow) { const source = terminalMapping.get(block.id) ?? block.id; terminalMapping.set(terminal.continuation, source); - block.terminal = { - kind: 'goto', - block: terminal.continuation, - variant: GotoVariant.Break, - id: terminal.id, - loc: terminal.loc, - }; + terminal.handler = null; } } return terminalMapping.size > 0 ? terminalMapping : null; 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 d43d3ebbb53b..f5b2a654ec4f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/BuildReactiveFunction.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/BuildReactiveFunction.ts @@ -141,6 +141,61 @@ class Driver { return {block: blockId, place, value: sequence, id: instr.id}; } + /* + * Converts the result of visitValueBlock into a SequenceExpression that includes + * the instruction with its lvalue. This is needed for for/for-of/for-in init/test + * blocks where the instruction's lvalue assignment must be preserved. + * + * This also flattens nested SequenceExpressions that can occur from MaybeThrow + * handling in try-catch blocks. + */ + valueBlockResultToSequence( + result: { + block: BlockId; + value: ReactiveValue; + place: Place; + id: InstructionId; + }, + loc: SourceLocation, + ): ReactiveSequenceValue { + // Collect all instructions from potentially nested SequenceExpressions + const instructions: Array = []; + let innerValue: ReactiveValue = result.value; + + // Flatten nested SequenceExpressions + while (innerValue.kind === 'SequenceExpression') { + instructions.push(...innerValue.instructions); + innerValue = innerValue.value; + } + + /* + * Only add the final instruction if the innermost value is not just a LoadLocal + * of the same place we're storing to (which would be a no-op). + * This happens when MaybeThrow blocks cause the sequence to already contain + * all the necessary instructions. + */ + const isLoadOfSamePlace = + innerValue.kind === 'LoadLocal' && + innerValue.place.identifier.id === result.place.identifier.id; + + if (!isLoadOfSamePlace) { + instructions.push({ + id: result.id, + lvalue: result.place, + value: innerValue, + loc, + }); + } + + return { + kind: 'SequenceExpression', + instructions, + id: result.id, + value: {kind: 'Primitive', value: undefined, loc}, + loc, + }; + } + traverseBlock(block: BasicBlock): ReactiveBlock { const blockValue: ReactiveBlock = []; this.visitBlock(block, blockValue); @@ -441,29 +496,7 @@ class Driver { scheduleIds.push(scheduleId); const init = this.visitValueBlock(terminal.init, terminal.loc); - const initBlock = this.cx.ir.blocks.get(init.block)!; - let initValue = init.value; - if (initValue.kind === 'SequenceExpression') { - const last = initBlock.instructions.at(-1)!; - initValue.instructions.push(last); - initValue.value = { - kind: 'Primitive', - value: undefined, - loc: terminal.loc, - }; - } else { - initValue = { - kind: 'SequenceExpression', - instructions: [initBlock.instructions.at(-1)!], - id: terminal.id, - loc: terminal.loc, - value: { - kind: 'Primitive', - value: undefined, - loc: terminal.loc, - }, - }; - } + const initValue = this.valueBlockResultToSequence(init, terminal.loc); const testValue = this.visitValueBlock( terminal.test, @@ -524,54 +557,10 @@ class Driver { scheduleIds.push(scheduleId); const init = this.visitValueBlock(terminal.init, terminal.loc); - const initBlock = this.cx.ir.blocks.get(init.block)!; - let initValue = init.value; - if (initValue.kind === 'SequenceExpression') { - const last = initBlock.instructions.at(-1)!; - initValue.instructions.push(last); - initValue.value = { - kind: 'Primitive', - value: undefined, - loc: terminal.loc, - }; - } else { - initValue = { - kind: 'SequenceExpression', - instructions: [initBlock.instructions.at(-1)!], - id: terminal.id, - loc: terminal.loc, - value: { - kind: 'Primitive', - value: undefined, - loc: terminal.loc, - }, - }; - } + const initValue = this.valueBlockResultToSequence(init, terminal.loc); const test = this.visitValueBlock(terminal.test, terminal.loc); - const testBlock = this.cx.ir.blocks.get(test.block)!; - let testValue = test.value; - if (testValue.kind === 'SequenceExpression') { - const last = testBlock.instructions.at(-1)!; - testValue.instructions.push(last); - testValue.value = { - kind: 'Primitive', - value: undefined, - loc: terminal.loc, - }; - } else { - testValue = { - kind: 'SequenceExpression', - instructions: [testBlock.instructions.at(-1)!], - id: terminal.id, - loc: terminal.loc, - value: { - kind: 'Primitive', - value: undefined, - loc: terminal.loc, - }, - }; - } + const testValue = this.valueBlockResultToSequence(test, terminal.loc); let loopBody: ReactiveBlock; if (loopId) { @@ -621,29 +610,7 @@ class Driver { scheduleIds.push(scheduleId); const init = this.visitValueBlock(terminal.init, terminal.loc); - const initBlock = this.cx.ir.blocks.get(init.block)!; - let initValue = init.value; - if (initValue.kind === 'SequenceExpression') { - const last = initBlock.instructions.at(-1)!; - initValue.instructions.push(last); - initValue.value = { - kind: 'Primitive', - value: undefined, - loc: terminal.loc, - }; - } else { - initValue = { - kind: 'SequenceExpression', - instructions: [initBlock.instructions.at(-1)!], - id: terminal.id, - loc: terminal.loc, - value: { - kind: 'Primitive', - value: undefined, - loc: terminal.loc, - }, - }; - } + const initValue = this.valueBlockResultToSequence(init, terminal.loc); let loopBody: ReactiveBlock; if (loopId) { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-expected-break-target.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-expected-break-target.expect.md deleted file mode 100644 index 226ab20ac269..000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-expected-break-target.expect.md +++ /dev/null @@ -1,32 +0,0 @@ - -## Input - -```javascript -import {useMemo} from 'react'; - -export default function useFoo(text) { - return useMemo(() => { - try { - let formattedText = ''; - try { - formattedText = format(text); - } catch { - console.log('error'); - } - return formattedText || ''; - } catch (e) {} - }, [text]); -} - -``` - - -## Error - -``` -Found 1 error: - -Invariant: Expected a break target -``` - - \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-expected-break-target.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-expected-break-target.js deleted file mode 100644 index 4616e0232aaf..000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-expected-break-target.js +++ /dev/null @@ -1,15 +0,0 @@ -import {useMemo} from 'react'; - -export default function useFoo(text) { - return useMemo(() => { - try { - let formattedText = ''; - try { - formattedText = format(text); - } catch { - console.log('error'); - } - return formattedText || ''; - } catch (e) {} - }, [text]); -} 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 deleted file mode 100644 index 87bd7cfa0f6d..000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-repro-declaration-for-all-identifiers.expect.md +++ /dev/null @@ -1,35 +0,0 @@ - -## Input - -```javascript -function Foo() { - try { - // NOTE: this fixture previously failed during LeaveSSA; - // double-check this code when supporting value blocks in try/catch - for (let i = 0; i < 2; i++) {} - } catch {} -} - -``` - - -## Error - -``` -Found 1 error: - -Invariant: Expected a variable declaration - -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++) {} - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Expected a variable declaration - 6 | } catch {} - 7 | } - 8 | -``` - - \ 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.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-repro-declaration-for-all-identifiers.js deleted file mode 100644 index a633a175374d..000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-repro-declaration-for-all-identifiers.js +++ /dev/null @@ -1,7 +0,0 @@ -function Foo() { - try { - // NOTE: this fixture previously failed during LeaveSSA; - // double-check this code when supporting value blocks in try/catch - for (let i = 0; i < 2; i++) {} - } catch {} -} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-declaration-for-all-identifiers.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-declaration-for-all-identifiers.expect.md new file mode 100644 index 000000000000..65046bb9e23d --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-declaration-for-all-identifiers.expect.md @@ -0,0 +1,50 @@ + +## Input + +```javascript +function Foo() { + try { + for (let i = 0; i < 2; i++) {} + } catch {} + return ok; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [], + sequentialRenders: [{}, {}, {}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +function Foo() { + const $ = _c(1); + try { + for (let i = 0; i < 2; i++) {} + } catch {} + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = ok; + $[0] = t0; + } else { + t0 = $[0]; + } + return t0; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [], + sequentialRenders: [{}, {}, {}], +}; + +``` + +### Eval output +(kind: ok) ok +ok +ok \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-declaration-for-all-identifiers.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-declaration-for-all-identifiers.js new file mode 100644 index 000000000000..f8f36eaa1142 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-declaration-for-all-identifiers.js @@ -0,0 +1,12 @@ +function Foo() { + try { + for (let i = 0; i < 2; i++) {} + } catch {} + return ok; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [], + sequentialRenders: [{}, {}, {}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-for-in-in-try.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-for-in-in-try.expect.md new file mode 100644 index 000000000000..7526832e6afa --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-for-in-in-try.expect.md @@ -0,0 +1,102 @@ + +## Input + +```javascript +function Foo({obj}) { + const keys = []; + try { + for (const key in obj) { + keys.push(key); + } + } catch (e) { + return Error; + } + return {keys.join(', ')}; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{obj: {a: 1, b: 2}}], + sequentialRenders: [ + {obj: {a: 1, b: 2}}, + {obj: {a: 1, b: 2}}, + {obj: {x: 'hello', y: 'world'}}, + {obj: {}}, + {obj: {single: 'value'}}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +function Foo(t0) { + const $ = _c(6); + const { obj } = t0; + let keys; + let t1; + if ($[0] !== obj) { + t1 = Symbol.for("react.early_return_sentinel"); + bb0: { + keys = []; + try { + for (const key in obj) { + keys.push(key); + } + } catch (t2) { + let t3; + if ($[3] === Symbol.for("react.memo_cache_sentinel")) { + t3 = Error; + $[3] = t3; + } else { + t3 = $[3]; + } + t1 = t3; + break bb0; + } + } + $[0] = obj; + $[1] = keys; + $[2] = t1; + } else { + keys = $[1]; + t1 = $[2]; + } + if (t1 !== Symbol.for("react.early_return_sentinel")) { + return t1; + } + + const t2 = keys.join(", "); + let t3; + if ($[4] !== t2) { + t3 = {t2}; + $[4] = t2; + $[5] = t3; + } else { + t3 = $[5]; + } + return t3; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{ obj: { a: 1, b: 2 } }], + sequentialRenders: [ + { obj: { a: 1, b: 2 } }, + { obj: { a: 1, b: 2 } }, + { obj: { x: "hello", y: "world" } }, + { obj: {} }, + { obj: { single: "value" } }, + ], +}; + +``` + +### Eval output +(kind: ok) a, b +a, b +x, y + +single \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-for-in-in-try.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-for-in-in-try.js new file mode 100644 index 000000000000..73fb38474496 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-for-in-in-try.js @@ -0,0 +1,23 @@ +function Foo({obj}) { + const keys = []; + try { + for (const key in obj) { + keys.push(key); + } + } catch (e) { + return Error; + } + return {keys.join(', ')}; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{obj: {a: 1, b: 2}}], + sequentialRenders: [ + {obj: {a: 1, b: 2}}, + {obj: {a: 1, b: 2}}, + {obj: {x: 'hello', y: 'world'}}, + {obj: {}}, + {obj: {single: 'value'}}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-for-loop-in-try.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-for-loop-in-try.expect.md new file mode 100644 index 000000000000..932872e53884 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-for-loop-in-try.expect.md @@ -0,0 +1,95 @@ + +## Input + +```javascript +function Foo({items}) { + const results = []; + try { + for (let i = 0; i < items.length; i++) { + results.push(items[i]); + } + } catch (e) { + return Error; + } + return {results.join(', ')}; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{items: ['a', 'b', 'c']}], + sequentialRenders: [ + {items: ['a', 'b', 'c']}, + {items: ['a', 'b', 'c']}, + {items: ['x', 'y']}, + {items: []}, + {items: ['single']}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +function Foo(t0) { + const $ = _c(5); + const { items } = t0; + let results; + let t1; + if ($[0] !== items) { + t1 = Symbol.for("react.early_return_sentinel"); + bb0: { + results = []; + try { + for (let i = 0; i < items.length; i++) { + results.push(items[i]); + } + } catch (t2) { + t1 = Error; + break bb0; + } + } + $[0] = items; + $[1] = results; + $[2] = t1; + } else { + results = $[1]; + t1 = $[2]; + } + if (t1 !== Symbol.for("react.early_return_sentinel")) { + return t1; + } + + const t2 = results.join(", "); + let t3; + if ($[3] !== t2) { + t3 = {t2}; + $[3] = t2; + $[4] = t3; + } else { + t3 = $[4]; + } + return t3; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{ items: ["a", "b", "c"] }], + sequentialRenders: [ + { items: ["a", "b", "c"] }, + { items: ["a", "b", "c"] }, + { items: ["x", "y"] }, + { items: [] }, + { items: ["single"] }, + ], +}; + +``` + +### Eval output +(kind: ok) a, b, c +a, b, c +x, y + +single \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-for-loop-in-try.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-for-loop-in-try.js new file mode 100644 index 000000000000..8038d61a21d9 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-for-loop-in-try.js @@ -0,0 +1,23 @@ +function Foo({items}) { + const results = []; + try { + for (let i = 0; i < items.length; i++) { + results.push(items[i]); + } + } catch (e) { + return Error; + } + return {results.join(', ')}; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{items: ['a', 'b', 'c']}], + sequentialRenders: [ + {items: ['a', 'b', 'c']}, + {items: ['a', 'b', 'c']}, + {items: ['x', 'y']}, + {items: []}, + {items: ['single']}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-for-of-in-try.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-for-of-in-try.expect.md new file mode 100644 index 000000000000..20a46449dc9e --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-for-of-in-try.expect.md @@ -0,0 +1,102 @@ + +## Input + +```javascript +function Foo({obj}) { + const items = []; + try { + for (const [key, value] of Object.entries(obj)) { + items.push(`${key}: ${value}`); + } + } catch (e) { + return Error; + } + return {items.join(', ')}; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{obj: {a: 1, b: 2}}], + sequentialRenders: [ + {obj: {a: 1, b: 2}}, + {obj: {a: 1, b: 2}}, + {obj: {x: 'hello', y: 'world'}}, + {obj: {}}, + {obj: {single: 'value'}}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +function Foo(t0) { + const $ = _c(6); + const { obj } = t0; + let items; + let t1; + if ($[0] !== obj) { + t1 = Symbol.for("react.early_return_sentinel"); + bb0: { + items = []; + try { + for (const [key, value] of Object.entries(obj)) { + items.push(`${key}: ${value}`); + } + } catch (t2) { + let t3; + if ($[3] === Symbol.for("react.memo_cache_sentinel")) { + t3 = Error; + $[3] = t3; + } else { + t3 = $[3]; + } + t1 = t3; + break bb0; + } + } + $[0] = obj; + $[1] = items; + $[2] = t1; + } else { + items = $[1]; + t1 = $[2]; + } + if (t1 !== Symbol.for("react.early_return_sentinel")) { + return t1; + } + + const t2 = items.join(", "); + let t3; + if ($[4] !== t2) { + t3 = {t2}; + $[4] = t2; + $[5] = t3; + } else { + t3 = $[5]; + } + return t3; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{ obj: { a: 1, b: 2 } }], + sequentialRenders: [ + { obj: { a: 1, b: 2 } }, + { obj: { a: 1, b: 2 } }, + { obj: { x: "hello", y: "world" } }, + { obj: {} }, + { obj: { single: "value" } }, + ], +}; + +``` + +### Eval output +(kind: ok) a: 1, b: 2 +a: 1, b: 2 +x: hello, y: world + +single: value \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-for-of-in-try.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-for-of-in-try.js new file mode 100644 index 000000000000..5f5bae2e9ba6 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-for-of-in-try.js @@ -0,0 +1,23 @@ +function Foo({obj}) { + const items = []; + try { + for (const [key, value] of Object.entries(obj)) { + items.push(`${key}: ${value}`); + } + } catch (e) { + return Error; + } + return {items.join(', ')}; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{obj: {a: 1, b: 2}}], + sequentialRenders: [ + {obj: {a: 1, b: 2}}, + {obj: {a: 1, b: 2}}, + {obj: {x: 'hello', y: 'world'}}, + {obj: {}}, + {obj: {single: 'value'}}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-nested-try-catch-in-usememo.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-nested-try-catch-in-usememo.expect.md new file mode 100644 index 000000000000..bb3d068db6da --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-nested-try-catch-in-usememo.expect.md @@ -0,0 +1,114 @@ + +## Input + +```javascript +// @compilationMode:"infer" +import {useMemo} from 'react'; + +function useFoo(text) { + return useMemo(() => { + try { + let formattedText = ''; + try { + formattedText = format(text); + } catch { + formattedText = text; + } + return formattedText || ''; + } catch (e) { + return ''; + } + }, [text]); +} + +function format(text) { + return text.toUpperCase(); +} + +function Foo({text}) { + const result = useFoo(text); + return {result}; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{text: 'hello'}], + sequentialRenders: [ + {text: 'hello'}, + {text: 'hello'}, + {text: 'world'}, + {text: ''}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @compilationMode:"infer" +import { useMemo } from "react"; + +function useFoo(text) { + const $ = _c(2); + let t0; + try { + let formattedText; + try { + let t2; + if ($[0] !== text) { + t2 = format(text); + $[0] = text; + $[1] = t2; + } else { + t2 = $[1]; + } + formattedText = t2; + } catch { + formattedText = text; + } + + t0 = formattedText || ""; + } catch (t1) { + t0 = ""; + } + return t0; +} + +function format(text) { + return text.toUpperCase(); +} + +function Foo(t0) { + const $ = _c(2); + const { text } = t0; + const result = useFoo(text); + let t1; + if ($[0] !== result) { + t1 = {result}; + $[0] = result; + $[1] = t1; + } else { + t1 = $[1]; + } + return t1; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{ text: "hello" }], + sequentialRenders: [ + { text: "hello" }, + { text: "hello" }, + { text: "world" }, + { text: "" }, + ], +}; + +``` + +### Eval output +(kind: ok) HELLO +HELLO +WORLD + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-nested-try-catch-in-usememo.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-nested-try-catch-in-usememo.js new file mode 100644 index 000000000000..64bdc404004a --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-nested-try-catch-in-usememo.js @@ -0,0 +1,38 @@ +// @compilationMode:"infer" +import {useMemo} from 'react'; + +function useFoo(text) { + return useMemo(() => { + try { + let formattedText = ''; + try { + formattedText = format(text); + } catch { + formattedText = text; + } + return formattedText || ''; + } catch (e) { + return ''; + } + }, [text]); +} + +function format(text) { + return text.toUpperCase(); +} + +function Foo({text}) { + const result = useFoo(text); + return {result}; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{text: 'hello'}], + sequentialRenders: [ + {text: 'hello'}, + {text: 'hello'}, + {text: 'world'}, + {text: ''}, + ], +}; diff --git a/compiler/packages/snap/src/minimize.ts b/compiler/packages/snap/src/minimize.ts index 30848241f7e0..1560cf0d2a13 100644 --- a/compiler/packages/snap/src/minimize.ts +++ b/compiler/packages/snap/src/minimize.ts @@ -11,15 +11,9 @@ import generate from '@babel/generator'; import traverse from '@babel/traverse'; import * as t from '@babel/types'; import type {parseConfigPragmaForTests as ParseConfigPragma} from 'babel-plugin-react-compiler/src/Utils/TestUtils'; -import fs from 'fs'; -import path from 'path'; -import {parseInput, parseLanguage, parseSourceType} from './compiler.js'; +import {parseInput} from './compiler.js'; import {PARSE_CONFIG_PRAGMA_IMPORT, PROJECT_SRC} from './constants.js'; -type MinimizeOptions = { - path: string; -}; - type CompileSuccess = {kind: 'success'}; type CompileParseError = {kind: 'parse_error'; message: string}; type CompileErrors = { @@ -2016,55 +2010,3 @@ export function minimize( return {kind: 'minimized', source: currentCode}; } - -/** - * Main minimize function that reads the input file, runs minimization, - * and reports results. - */ -export async function runMinimize(options: MinimizeOptions): Promise { - // Resolve the input path - const inputPath = path.isAbsolute(options.path) - ? options.path - : path.resolve(process.cwd(), options.path); - - // Check if file exists - if (!fs.existsSync(inputPath)) { - console.error(`Error: File not found: ${inputPath}`); - process.exit(1); - } - - // Read the input file - const input = fs.readFileSync(inputPath, 'utf-8'); - const filename = path.basename(inputPath); - const firstLine = input.substring(0, input.indexOf('\n')); - const language = parseLanguage(firstLine); - const sourceType = parseSourceType(firstLine); - - console.log(`Minimizing: ${inputPath}`); - - const originalLines = input.split('\n').length; - - // Run the minimization - const result = minimize(input, filename, language, sourceType); - - if (result.kind === 'success') { - console.log('Could not minimize: the input compiles successfully.'); - process.exit(0); - } - - if (result.kind === 'minimal') { - console.log( - 'Could not minimize: the input fails but is already minimal and cannot be reduced further.', - ); - process.exit(0); - } - - // Output the minimized code - console.log('--- Minimized Code ---'); - console.log(result.source); - - const minimizedLines = result.source.split('\n').length; - console.log( - `\nReduced from ${originalLines} lines to ${minimizedLines} lines`, - ); -} diff --git a/compiler/packages/snap/src/reporter.ts b/compiler/packages/snap/src/reporter.ts index 9f7d1d9a5072..29ca81e34597 100644 --- a/compiler/packages/snap/src/reporter.ts +++ b/compiler/packages/snap/src/reporter.ts @@ -139,15 +139,24 @@ export async function update(results: TestResults): Promise { * Report test results to the user * @returns boolean indicatig whether all tests passed */ -export function report(results: TestResults): boolean { +export function report( + results: TestResults, + verbose: boolean = false, +): boolean { const failures: Array<[string, TestResult]> = []; for (const [basename, result] of results) { if (result.actual === result.expected && result.unexpectedError == null) { - console.log( - chalk.green.inverse.bold(' PASS ') + ' ' + chalk.dim(basename), - ); + if (verbose) { + console.log( + chalk.green.inverse.bold(' PASS ') + ' ' + chalk.dim(basename), + ); + } } else { - console.log(chalk.red.inverse.bold(' FAIL ') + ' ' + chalk.dim(basename)); + if (verbose) { + console.log( + chalk.red.inverse.bold(' FAIL ') + ' ' + chalk.dim(basename), + ); + } failures.push([basename, result]); } } diff --git a/compiler/packages/snap/src/runner.ts b/compiler/packages/snap/src/runner.ts index f127bd5a35e5..c5443eaddecd 100644 --- a/compiler/packages/snap/src/runner.ts +++ b/compiler/packages/snap/src/runner.ts @@ -23,28 +23,177 @@ import { } from './runner-watch'; import * as runnerWorker from './runner-worker'; import {execSync} from 'child_process'; -import {runMinimize} from './minimize'; +import fs from 'fs'; +import path from 'path'; +import {minimize} from './minimize'; +import {parseLanguage, parseSourceType} from './compiler'; const WORKER_PATH = require.resolve('./runner-worker.js'); const NUM_WORKERS = cpus().length - 1; readline.emitKeypressEvents(process.stdin); -type RunnerOptions = { +type TestOptions = { sync: boolean; workerThreads: boolean; watch: boolean; update: boolean; pattern?: string; debug: boolean; + verbose: boolean; }; -async function runTestCommand(opts: RunnerOptions): Promise { - await main(opts); +type MinimizeOptions = { + path: string; + update: boolean; +}; + +async function runTestCommand(opts: TestOptions): Promise { + const worker: Worker & typeof runnerWorker = new Worker(WORKER_PATH, { + enableWorkerThreads: opts.workerThreads, + numWorkers: NUM_WORKERS, + }) as any; + worker.getStderr().pipe(process.stderr); + worker.getStdout().pipe(process.stdout); + + // Check if watch mode should be enabled + const shouldWatch = opts.watch; + + if (shouldWatch) { + makeWatchRunner( + state => onChange(worker, state, opts.sync, opts.verbose), + opts.debug, + opts.pattern, + ); + if (opts.pattern) { + /** + * Warm up wormers when in watch mode. Loading the Forget babel plugin + * and all of its transitive dependencies takes 1-3s (per worker) on a M1. + * As jest-worker dispatches tasks using a round-robin strategy, we can + * avoid an additional 1-3s wait on the first num_workers runs by warming + * up workers eagerly. + */ + for (let i = 0; i < NUM_WORKERS - 1; i++) { + worker.transformFixture( + { + fixturePath: 'tmp', + snapshotPath: './tmp.expect.md', + inputPath: './tmp.js', + input: ` + function Foo(props) { + return identity(props); + } + `, + snapshot: null, + }, + 0, + false, + false, + ); + } + } + } else { + // Non-watch mode. For simplicity we re-use the same watchSrc() function. + // After the first build completes run tests and exit + const tsWatch: ts.WatchOfConfigFile = + watchSrc( + () => {}, + async (isTypecheckSuccess: boolean) => { + let isSuccess = false; + if (!isTypecheckSuccess) { + console.error( + 'Found typescript errors in Forget source code, skipping test fixtures.', + ); + } else { + try { + execSync('yarn build', {cwd: PROJECT_ROOT}); + console.log('Built compiler successfully with tsup'); + + // Determine which filter to use + let testFilter: TestFilter | null = null; + if (opts.pattern) { + testFilter = { + paths: [opts.pattern], + }; + } + + const results = await runFixtures( + worker, + testFilter, + 0, + opts.debug, + false, // no requireSingleFixture in non-watch mode + opts.sync, + ); + if (opts.update) { + update(results); + isSuccess = true; + } else { + isSuccess = report(results, opts.verbose); + } + } catch (e) { + console.warn('Failed to build compiler with tsup:', e); + } + } + tsWatch?.close(); + await worker.end(); + process.exit(isSuccess ? 0 : 1); + }, + ); + } } -async function runMinimizeCommand(path: string): Promise { - await runMinimize({path}); +async function runMinimizeCommand(opts: MinimizeOptions): Promise { + // Resolve the input path + const inputPath = path.isAbsolute(opts.path) + ? opts.path + : path.resolve(process.cwd(), opts.path); + + // Check if file exists + if (!fs.existsSync(inputPath)) { + console.error(`Error: File not found: ${inputPath}`); + process.exit(1); + } + + // Read the input file + const input = fs.readFileSync(inputPath, 'utf-8'); + const filename = path.basename(inputPath); + const firstLine = input.substring(0, input.indexOf('\n')); + const language = parseLanguage(firstLine); + const sourceType = parseSourceType(firstLine); + + console.log(`Minimizing: ${inputPath}`); + + const originalLines = input.split('\n').length; + + // Run the minimization + const result = minimize(input, filename, language, sourceType); + + if (result.kind === 'success') { + console.log('Could not minimize: the input compiles successfully.'); + process.exit(0); + } + + if (result.kind === 'minimal') { + console.log( + 'Could not minimize: the input fails but is already minimal and cannot be reduced further.', + ); + process.exit(0); + } + + // Output the minimized code + console.log('--- Minimized Code ---'); + console.log(result.source); + + const minimizedLines = result.source.split('\n').length; + console.log( + `\nReduced from ${originalLines} lines to ${minimizedLines} lines`, + ); + + if (opts.update) { + fs.writeFileSync(inputPath, result.source, 'utf-8'); + console.log(`\nUpdated ${inputPath} with minimized code.`); + } } yargs(hideBin(process.argv)) @@ -85,10 +234,14 @@ yargs(hideBin(process.argv)) .boolean('debug') .alias('d', 'debug') .describe('debug', 'Enable debug logging to print HIR for each pass') - .default('debug', false); + .default('debug', false) + .boolean('verbose') + .alias('v', 'verbose') + .describe('verbose', 'Print individual test results') + .default('verbose', false); }, async argv => { - await runTestCommand(argv as RunnerOptions); + await runTestCommand(argv as TestOptions); }, ) .command( @@ -99,10 +252,17 @@ yargs(hideBin(process.argv)) .string('path') .alias('p', 'path') .describe('path', 'Path to the file to minimize') - .demandOption('path'); + .demandOption('path') + .boolean('update') + .alias('u', 'update') + .describe( + 'update', + 'Update the input file in-place with the minimized version', + ) + .default('update', false); }, async argv => { - await runMinimizeCommand(argv.path as string); + await runMinimizeCommand(argv as unknown as MinimizeOptions); }, ) .help('help') @@ -162,6 +322,7 @@ async function onChange( worker: Worker & typeof runnerWorker, state: RunnerState, sync: boolean, + verbose: boolean, ) { const {compilerVersion, isCompilerBuildValid, mode, filter, debug} = state; if (isCompilerBuildValid) { @@ -194,7 +355,7 @@ async function onChange( update(results); state.lastUpdate = end; } else { - report(results); + report(results, verbose); } console.log(`Completed in ${Math.floor(end - start)} ms`); } else { @@ -216,101 +377,3 @@ async function onChange( '[any] - rerun tests\n', ); } - -/** - * Runs the compiler in watch or single-execution mode - */ -export async function main(opts: RunnerOptions): Promise { - const worker: Worker & typeof runnerWorker = new Worker(WORKER_PATH, { - enableWorkerThreads: opts.workerThreads, - numWorkers: NUM_WORKERS, - }) as any; - worker.getStderr().pipe(process.stderr); - worker.getStdout().pipe(process.stdout); - - // Check if watch mode should be enabled - const shouldWatch = opts.watch; - - if (shouldWatch) { - makeWatchRunner( - state => onChange(worker, state, opts.sync), - opts.debug, - opts.pattern, - ); - if (opts.pattern) { - /** - * Warm up wormers when in watch mode. Loading the Forget babel plugin - * and all of its transitive dependencies takes 1-3s (per worker) on a M1. - * As jest-worker dispatches tasks using a round-robin strategy, we can - * avoid an additional 1-3s wait on the first num_workers runs by warming - * up workers eagerly. - */ - for (let i = 0; i < NUM_WORKERS - 1; i++) { - worker.transformFixture( - { - fixturePath: 'tmp', - snapshotPath: './tmp.expect.md', - inputPath: './tmp.js', - input: ` - function Foo(props) { - return identity(props); - } - `, - snapshot: null, - }, - 0, - false, - false, - ); - } - } - } else { - // Non-watch mode. For simplicity we re-use the same watchSrc() function. - // After the first build completes run tests and exit - const tsWatch: ts.WatchOfConfigFile = - watchSrc( - () => {}, - async (isTypecheckSuccess: boolean) => { - let isSuccess = false; - if (!isTypecheckSuccess) { - console.error( - 'Found typescript errors in Forget source code, skipping test fixtures.', - ); - } else { - try { - execSync('yarn build', {cwd: PROJECT_ROOT}); - console.log('Built compiler successfully with tsup'); - - // Determine which filter to use - let testFilter: TestFilter | null = null; - if (opts.pattern) { - testFilter = { - paths: [opts.pattern], - }; - } - - const results = await runFixtures( - worker, - testFilter, - 0, - opts.debug, - false, // no requireSingleFixture in non-watch mode - opts.sync, - ); - if (opts.update) { - update(results); - isSuccess = true; - } else { - isSuccess = report(results); - } - } catch (e) { - console.warn('Failed to build compiler with tsup:', e); - } - } - tsWatch?.close(); - await worker.end(); - process.exit(isSuccess ? 0 : 1); - }, - ); - } -}