Skip to content
Closed
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
327 changes: 327 additions & 0 deletions compiler/fault-tolerance-overview.md

Large diffs are not rendered by default.

222 changes: 154 additions & 68 deletions compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -704,19 +704,21 @@ function tryCompileFunction(
}

try {
return {
kind: 'compile',
compiledFn: compileFn(
fn,
programContext.opts.environment,
fnType,
outputMode,
programContext,
programContext.opts.logger,
programContext.filename,
programContext.code,
),
};
const result = compileFn(
fn,
programContext.opts.environment,
fnType,
outputMode,
programContext,
programContext.opts.logger,
programContext.filename,
programContext.code,
);
if (result.isOk()) {
return {kind: 'compile', compiledFn: result.unwrap()};
} else {
return {kind: 'error', error: result.unwrapErr()};
}
} catch (err) {
return {kind: 'error', error: err};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@
import * as t from '@babel/types';
import {ZodError, z} from 'zod/v4';
import {fromZodError} from 'zod-validation-error/v4';
import {CompilerError} from '../CompilerError';
import {
CompilerDiagnostic,
CompilerError,
CompilerErrorDetail,
ErrorCategory,
} from '../CompilerError';
import {CompilerOutputMode, Logger, ProgramContext} from '../Entrypoint';
import {Err, Ok, Result} from '../Utils/Result';
import {
Expand Down Expand Up @@ -545,6 +550,12 @@ export class Environment {

#flowTypeEnvironment: FlowTypeEnv | null;

/**
* Accumulated compilation errors. Passes record errors here instead of
* throwing, so the pipeline can continue and report all errors at once.
*/
#errors: CompilerError = new CompilerError();

constructor(
scope: BabelScope,
fnType: ReactFunctionType,
Expand Down Expand Up @@ -709,6 +720,82 @@ export class Environment {
}
}

/**
* Record a single diagnostic or error detail on this environment.
* If the error is an Invariant, it is immediately thrown since invariants
* represent internal bugs that cannot be recovered from.
* Otherwise, the error is accumulated and optionally logged.
*/
recordError(error: CompilerDiagnostic | CompilerErrorDetail): void {
if (error.category === ErrorCategory.Invariant) {
const compilerError = new CompilerError();
if (error instanceof CompilerDiagnostic) {
compilerError.pushDiagnostic(error);
} else {
compilerError.pushErrorDetail(error);
}
throw compilerError;
}
if (error instanceof CompilerDiagnostic) {
this.#errors.pushDiagnostic(error);
} else {
this.#errors.pushErrorDetail(error);
}
if (this.logger != null) {
this.logger.logEvent(this.filename, {
kind: 'CompileError',
detail: error,
fnLoc: null,
});
}
Comment on lines +744 to +750

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate error logging with fnLoc: null

recordError logs each error to the logger with fnLoc: null. However, after compilation completes, Program.ts calls handleError/logError on the aggregated CompilerError, which logs each error again with the actual fnLoc. This causes duplicate log entries, as visible in the dynamic-gating-bailout-nopanic test fixture where the same PreserveManualMemo error now appears twice in the Logs section — once with "fnLoc":null and once with the real fnLoc.

This may be intentional (the early log provides immediate feedback during compilation, while the later one has the function location), but it's worth confirming this is the desired behavior, especially since consumers of the logger may not expect duplicates.

Prompt To Fix With AI
This is a comment left during a code review.
Path: compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts
Line: 744-750

Comment:
**Duplicate error logging with `fnLoc: null`**

`recordError` logs each error to the logger with `fnLoc: null`. However, after compilation completes, `Program.ts` calls `handleError`/`logError` on the aggregated `CompilerError`, which logs each error *again* with the actual `fnLoc`. This causes duplicate log entries, as visible in the `dynamic-gating-bailout-nopanic` test fixture where the same `PreserveManualMemo` error now appears twice in the Logs section — once with `"fnLoc":null` and once with the real `fnLoc`.

This may be intentional (the early log provides immediate feedback during compilation, while the later one has the function location), but it's worth confirming this is the desired behavior, especially since consumers of the logger may not expect duplicates.

How can I resolve this? If you propose a fix, please make it concise.

}

/**
* Record all diagnostics from a CompilerError onto this environment.
*/
recordErrors(error: CompilerError): void {
for (const detail of error.details) {
this.recordError(detail);
}
}

/**
* Returns true if any errors have been recorded during compilation.
*/
hasErrors(): boolean {
return this.#errors.hasAnyErrors();
}

/**
* Returns the accumulated CompilerError containing all recorded diagnostics.
*/
aggregateErrors(): CompilerError {
return this.#errors;
}

/**
* Wraps a callback in try/catch: if the callback throws a CompilerError
* that is NOT an invariant, the error is recorded and execution continues.
* Non-CompilerError exceptions and invariants are re-thrown.
*/
tryRecord(fn: () => void): void {
try {
fn();
} catch (err) {
if (err instanceof CompilerError) {
// Check if any detail is an invariant — if so, re-throw
for (const detail of err.details) {
if (detail.category === ErrorCategory.Invariant) {
throw err;
}
}
this.recordErrors(err);
} else {
throw err;
}
}
}

isContextIdentifier(node: t.Identifier): boolean {
return this.#contextIdentifiers.has(node);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ function lowerWithMutationAliasing(fn: HIRFunction): void {
deadCodeElimination(fn);
const functionEffects = inferMutationAliasingRanges(fn, {
isFunctionExpression: true,
}).unwrap();
});
rewriteInstructionKindsBasedOnReassignment(fn);
inferReactiveScopeVariables(fn);
fn.aliasingEffects = functionEffects;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import {
makeInstructionId,
} from '../HIR';
import {createTemporaryPlace, markInstructionIds} from '../HIR/HIRBuilder';
import {Result} from '../Utils/Result';

type ManualMemoCallee = {
kind: 'useMemo' | 'useCallback';
Expand Down Expand Up @@ -389,9 +388,7 @@ function extractManualMemoizationArgs(
* This pass also validates that useMemo callbacks return a value (not void), ensuring that useMemo
* is only used for memoizing values and not for running arbitrary side effects.
*/
export function dropManualMemoization(
func: HIRFunction,
): Result<void, CompilerError> {
export function dropManualMemoization(func: HIRFunction): void {
const errors = new CompilerError();
const isValidationEnabled =
func.env.config.validatePreserveExistingMemoizationGuarantees ||
Expand Down Expand Up @@ -553,7 +550,9 @@ export function dropManualMemoization(
}
}

return errors.asResult();
if (errors.hasAnyErrors()) {
func.env.recordErrors(errors);
}
}

function findOptionalPlaces(fn: HIRFunction): Set<IdentifierId> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ import {
eachTerminalOperand,
eachTerminalSuccessor,
} from '../HIR/visitors';
import {Ok, Result} from '../Utils/Result';

import {
assertExhaustive,
getOrInsertDefault,
Expand Down Expand Up @@ -100,7 +100,7 @@ export function inferMutationAliasingEffects(
{isFunctionExpression}: {isFunctionExpression: boolean} = {
isFunctionExpression: false,
},
): Result<void, CompilerError> {
): void {
const initialState = InferenceState.empty(fn.env, isFunctionExpression);

// Map of blocks to the last (merged) incoming state that was processed
Expand Down Expand Up @@ -220,7 +220,7 @@ export function inferMutationAliasingEffects(
}
}
}
return Ok(undefined);
return;
}

function findHoistedContextDeclarations(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
eachTerminalOperand,
} from '../HIR/visitors';
import {assertExhaustive, getOrInsertWith} from '../Utils/utils';
import {Err, Ok, Result} from '../Utils/Result';

import {AliasingEffect, MutationReason} from './AliasingEffects';

/**
Expand Down Expand Up @@ -74,7 +74,7 @@ import {AliasingEffect, MutationReason} from './AliasingEffects';
export function inferMutationAliasingRanges(
fn: HIRFunction,
{isFunctionExpression}: {isFunctionExpression: boolean},
): Result<Array<AliasingEffect>, CompilerError> {
): Array<AliasingEffect> {
// The set of externally-visible effects
const functionEffects: Array<AliasingEffect> = [];

Expand Down Expand Up @@ -547,10 +547,14 @@ export function inferMutationAliasingRanges(
}
}

if (errors.hasAnyErrors() && !isFunctionExpression) {
return Err(errors);
if (
errors.hasAnyErrors() &&
!isFunctionExpression &&
fn.env.enableValidations
) {
fn.env.recordErrors(errors);
}
return Ok(functionEffects);
return functionEffects;
}

function appendFunctionErrors(errors: CompilerError, fn: HIRFunction): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ import {
} from '../HIR/HIR';
import {printIdentifier, printInstruction, printPlace} from '../HIR/PrintHIR';
import {eachPatternOperand} from '../HIR/visitors';
import {Err, Ok, Result} from '../Utils/Result';

import {GuardKind} from '../Utils/RuntimeDiagnosticConstants';
import {assertExhaustive} from '../Utils/utils';
import {buildReactiveFunction} from './BuildReactiveFunction';
Expand Down Expand Up @@ -111,7 +111,7 @@ export function codegenFunction(
uniqueIdentifiers: Set<string>;
fbtOperands: Set<IdentifierId>;
},
): Result<CodegenFunction, CompilerError> {
): CodegenFunction {
const cx = new Context(
fn.env,
fn.id ?? '[[ anonymous ]]',
Expand Down Expand Up @@ -141,11 +141,7 @@ export function codegenFunction(
};
}

const compileResult = codegenReactiveFunction(cx, fn);
if (compileResult.isErr()) {
return compileResult;
}
const compiled = compileResult.unwrap();
const compiled = codegenReactiveFunction(cx, fn);

const hookGuard = fn.env.config.enableEmitHookGuards;
if (hookGuard != null && fn.env.outputMode === 'client') {
Expand Down Expand Up @@ -273,7 +269,7 @@ export function codegenFunction(
emitInstrumentForget.globalGating,
);
if (assertResult.isErr()) {
return assertResult;
fn.env.recordErrors(assertResult.unwrapErr());
}
}

Expand Down Expand Up @@ -323,20 +319,17 @@ export function codegenFunction(
),
reactiveFunction,
);
if (codegen.isErr()) {
return codegen;
}
outlined.push({fn: codegen.unwrap(), type});
outlined.push({fn: codegen, type});
}
compiled.outlined = outlined;

return compileResult;
return compiled;
}

function codegenReactiveFunction(
cx: Context,
fn: ReactiveFunction,
): Result<CodegenFunction, CompilerError> {
): CodegenFunction {
for (const param of fn.params) {
const place = param.kind === 'Identifier' ? param : param.place;
cx.temp.set(place.identifier.declarationId, null);
Expand All @@ -355,13 +348,13 @@ function codegenReactiveFunction(
}

if (cx.errors.hasAnyErrors()) {
return Err(cx.errors);
fn.env.recordErrors(cx.errors);
}

const countMemoBlockVisitor = new CountMemoBlockVisitor(fn.env);
visitReactiveFunction(fn, countMemoBlockVisitor, undefined);

return Ok({
return {
type: 'CodegenFunction',
loc: fn.loc,
id: fn.id !== null ? t.identifier(fn.id) : null,
Expand All @@ -376,7 +369,7 @@ function codegenReactiveFunction(
prunedMemoBlocks: countMemoBlockVisitor.prunedMemoBlocks,
prunedMemoValues: countMemoBlockVisitor.prunedMemoValues,
outlined: [],
});
};
}

class CountMemoBlockVisitor extends ReactiveFunctionVisitor<void> {
Expand Down Expand Up @@ -1665,7 +1658,7 @@ function codegenInstructionValue(
cx.temp,
),
reactiveFunction,
).unwrap();
);

/*
* ObjectMethod builder must be backwards compatible with older versions of babel.
Expand Down Expand Up @@ -1864,7 +1857,7 @@ function codegenInstructionValue(
cx.temp,
),
reactiveFunction,
).unwrap();
);

if (instrValue.type === 'ArrowFunctionExpression') {
let body: t.BlockStatement | t.Expression = fn.body;
Expand Down
Loading
Loading