Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions compiler/.claude/agents/investigate-error.md
Original file line number Diff line number Diff line change
@@ -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/<fixture-name>.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 <fixture-name>` 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 <path-to-fixture>` 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 <fixture-name>` 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/<name>.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/<path>:<line>:<column>` - [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
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,9 @@ export function printTerminal(terminal: Terminal): Array<string> | 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 ')}`;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -1083,7 +1083,9 @@ export function* eachTerminalSuccessor(terminal: Terminal): Iterable<BlockId> {
}
case 'maybe-throw': {
yield terminal.continuation;
yield terminal.handler;
if (terminal.handler !== null) {
yield terminal.handler;
}
break;
}
case 'try': {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {CompilerError} from '..';
import {
BlockId,
GeneratedSource,
GotoVariant,
HIRFunction,
Instruction,
assertConsistentIdentifiers,
Expand All @@ -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 {
Expand Down Expand Up @@ -82,13 +87,7 @@ function pruneMaybeThrowsImpl(fn: HIRFunction): Map<BlockId, BlockId> | 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ReactiveInstruction> = [];
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);
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
Loading
Loading