Compare commits

..

3 Commits

Author SHA1 Message Date
Joe Savona
966a5195ab [compiler] Phase 2+7: Wrap pipeline passes in tryRecord for fault tolerance
- Change runWithEnvironment/run/compileFn to return Result<CodegenFunction, CompilerError>
- Wrap all pipeline passes in env.tryRecord() to catch and record CompilerErrors
- Record inference pass errors via env.recordErrors() instead of throwing
- Handle codegen Result explicitly, returning Err on failure
- Add final error check: return Err(env.aggregateErrors()) if any errors accumulated
- Update tryCompileFunction and retryCompileFunction in Program.ts to handle Result
- Keep lint-only passes using env.logErrors() (non-blocking)
- Update 52 test fixture expectations that now report additional errors

This is the core integration that enables fault tolerance: errors are caught,
recorded, and the pipeline continues to discover more errors.
2026-02-20 18:01:15 -08:00
Joe Savona
05243074f3 [compiler] Phase 1: Add error accumulation infrastructure to Environment
Add error accumulation methods to the Environment class:
- #errors field to accumulate CompilerErrors across passes
- recordError() to record a single diagnostic (throws if Invariant)
- recordErrors() to record all diagnostics from a CompilerError
- hasErrors() to check if any errors have been recorded
- aggregateErrors() to retrieve the accumulated CompilerError
- tryRecord() to wrap callbacks and catch CompilerErrors
2026-02-20 17:57:42 -08:00
Joe Savona
056a8e127f [compiler] Add fault tolerance plan document
Add detailed plan for making the React Compiler fault-tolerant by
accumulating errors across all passes instead of stopping at the first
error. This enables reporting multiple compilation errors at once.
2026-02-20 17:57:41 -08:00
16 changed files with 152 additions and 89 deletions

View File

@@ -127,49 +127,49 @@ All validation passes need to record errors on the environment instead of return
These passes already accumulate errors internally and return `Result<void, CompilerError>`. The change is: instead of returning the Result, record errors on `env` and return void. Remove the `.unwrap()` call in Pipeline.ts.
- [x] **4.1 `validateHooksUsage`** (`src/Validation/ValidateHooksUsage.ts`)
- [ ] **4.1 `validateHooksUsage`** (`src/Validation/ValidateHooksUsage.ts`)
- Change signature from `(fn: HIRFunction): Result<void, CompilerError>` to `(fn: HIRFunction): void`
- Record errors on `fn.env` instead of returning `errors.asResult()`
- Update Pipeline.ts call site (line 211): remove `.unwrap()`
- [x] **4.2 `validateNoCapitalizedCalls`** (`src/Validation/ValidateNoCapitalizedCalls.ts`)
- [ ] **4.2 `validateNoCapitalizedCalls`** (`src/Validation/ValidateNoCapitalizedCalls.ts`)
- Change signature to return void
- Fix the hybrid pattern: the direct `CallExpression` path currently throws via `CompilerError.throwInvalidReact()` — change to record on env
- The `MethodCall` path already accumulates — change to record on env
- Update Pipeline.ts call site (line 214): remove `.unwrap()`
- [x] **4.3 `validateUseMemo`** (`src/Validation/ValidateUseMemo.ts`)
- [ ] **4.3 `validateUseMemo`** (`src/Validation/ValidateUseMemo.ts`)
- Change signature to return void
- Record hard errors on env instead of returning `errors.asResult()`
- The soft `voidMemoErrors` path already uses `env.logErrors()` — keep as-is or also record
- Update Pipeline.ts call site (line 170): remove `.unwrap()`
- [x] **4.4 `dropManualMemoization`** (`src/Inference/DropManualMemoization.ts`)
- [ ] **4.4 `dropManualMemoization`** (`src/Inference/DropManualMemoization.ts`)
- Change signature to return void
- Record errors on env instead of returning `errors.asResult()`
- Update Pipeline.ts call site (line 178): remove `.unwrap()`
- [x] **4.5 `validateNoRefAccessInRender`** (`src/Validation/ValidateNoRefAccessInRender.ts`)
- [ ] **4.5 `validateNoRefAccessInRender`** (`src/Validation/ValidateNoRefAccessInRender.ts`)
- Change signature to return void
- Record errors on env instead of returning Result
- Update Pipeline.ts call site (line 275): remove `.unwrap()`
- [x] **4.6 `validateNoSetStateInRender`** (`src/Validation/ValidateNoSetStateInRender.ts`)
- [ ] **4.6 `validateNoSetStateInRender`** (`src/Validation/ValidateNoSetStateInRender.ts`)
- Change signature to return void
- Record errors on env
- Update Pipeline.ts call site (line 279): remove `.unwrap()`
- [x] **4.7 `validateNoImpureFunctionsInRender`** (`src/Validation/ValidateNoImpureFunctionsInRender.ts`)
- [ ] **4.7 `validateNoImpureFunctionsInRender`** (`src/Validation/ValidateNoImpureFunctionsInRender.ts`)
- Change signature to return void
- Record errors on env
- Update Pipeline.ts call site (line 300): remove `.unwrap()`
- [x] **4.8 `validateNoFreezingKnownMutableFunctions`** (`src/Validation/ValidateNoFreezingKnownMutableFunctions.ts`)
- [ ] **4.8 `validateNoFreezingKnownMutableFunctions`** (`src/Validation/ValidateNoFreezingKnownMutableFunctions.ts`)
- Change signature to return void
- Record errors on env
- Update Pipeline.ts call site (line 303): remove `.unwrap()`
- [x] **4.9 `validateExhaustiveDependencies`** (`src/Validation/ValidateExhaustiveDependencies.ts`)
- [ ] **4.9 `validateExhaustiveDependencies`** (`src/Validation/ValidateExhaustiveDependencies.ts`)
- Change signature to return void
- Record errors on env
- Update Pipeline.ts call site (line 315): remove `.unwrap()`

View File

@@ -167,10 +167,14 @@ function runWithEnvironment(
env.tryRecord(() => {
validateContextVariableLValues(hir);
});
validateUseMemo(hir);
env.tryRecord(() => {
validateUseMemo(hir).unwrap();
});
if (env.enableDropManualMemoization) {
dropManualMemoization(hir);
env.tryRecord(() => {
dropManualMemoization(hir).unwrap();
});
log({kind: 'hir', name: 'DropManualMemoization', value: hir});
}
@@ -215,10 +219,14 @@ function runWithEnvironment(
if (env.enableValidations) {
if (env.config.validateHooksUsage) {
validateHooksUsage(hir);
env.tryRecord(() => {
validateHooksUsage(hir).unwrap();
});
}
if (env.config.validateNoCapitalizedCalls) {
validateNoCapitalizedCalls(hir);
env.tryRecord(() => {
validateNoCapitalizedCalls(hir).unwrap();
});
}
}
@@ -276,11 +284,15 @@ function runWithEnvironment(
}
if (env.config.validateRefAccessDuringRender) {
validateNoRefAccessInRender(hir);
env.tryRecord(() => {
validateNoRefAccessInRender(hir).unwrap();
});
}
if (env.config.validateNoSetStateInRender) {
validateNoSetStateInRender(hir);
env.tryRecord(() => {
validateNoSetStateInRender(hir).unwrap();
});
}
if (
@@ -303,10 +315,14 @@ function runWithEnvironment(
}
if (env.config.validateNoImpureFunctionsInRender) {
validateNoImpureFunctionsInRender(hir);
env.tryRecord(() => {
validateNoImpureFunctionsInRender(hir).unwrap();
});
}
validateNoFreezingKnownMutableFunctions(hir);
env.tryRecord(() => {
validateNoFreezingKnownMutableFunctions(hir).unwrap();
});
}
env.tryRecord(() => {
@@ -320,7 +336,9 @@ function runWithEnvironment(
env.config.validateExhaustiveEffectDependencies
) {
// NOTE: this relies on reactivity inference running first
validateExhaustiveDependencies(hir);
env.tryRecord(() => {
validateExhaustiveDependencies(hir).unwrap();
});
}
}

View File

@@ -724,7 +724,6 @@ function tryCompileFunction(
}
}
/**
* Applies React Compiler generated functions to the babel AST by replacing
* existing functions in place or inserting new declarations.

View File

@@ -31,6 +31,7 @@ import {
makeInstructionId,
} from '../HIR';
import {createTemporaryPlace, markInstructionIds} from '../HIR/HIRBuilder';
import {Result} from '../Utils/Result';
type ManualMemoCallee = {
kind: 'useMemo' | 'useCallback';
@@ -388,7 +389,9 @@ 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): void {
export function dropManualMemoization(
func: HIRFunction,
): Result<void, CompilerError> {
const errors = new CompilerError();
const isValidationEnabled =
func.env.config.validatePreserveExistingMemoizationGuarantees ||
@@ -550,9 +553,7 @@ export function dropManualMemoization(func: HIRFunction): void {
}
}
if (errors.hasAnyErrors()) {
func.env.recordErrors(errors);
}
return errors.asResult();
}
function findOptionalPlaces(fn: HIRFunction): Set<IdentifierId> {

View File

@@ -44,6 +44,7 @@ import {
eachInstructionValueOperand,
eachTerminalOperand,
} from '../HIR/visitors';
import {Result} from '../Utils/Result';
import {retainWhere} from '../Utils/utils';
const DEBUG = false;
@@ -87,7 +88,9 @@ const DEBUG = false;
* When we go to compute the dependencies, we then think that the user's manual dep
* logic is part of what the memo computation logic.
*/
export function validateExhaustiveDependencies(fn: HIRFunction): void {
export function validateExhaustiveDependencies(
fn: HIRFunction,
): Result<void, CompilerError> {
const env = fn.env;
const reactive = collectReactiveIdentifiersHIR(fn);
@@ -214,9 +217,7 @@ export function validateExhaustiveDependencies(fn: HIRFunction): void {
},
false, // isFunctionExpression
);
if (error.hasAnyErrors()) {
fn.env.recordErrors(error);
}
return error.asResult();
}
function validateDependencies(

View File

@@ -26,6 +26,7 @@ import {
eachTerminalOperand,
} from '../HIR/visitors';
import {assertExhaustive} from '../Utils/utils';
import {Result} from '../Utils/Result';
/**
* Represents the possible kinds of value which may be stored at a given Place during
@@ -87,7 +88,9 @@ function joinKinds(a: Kind, b: Kind): Kind {
* may not appear as the callee of a conditional call.
* See the note for Kind.PotentialHook for sources of potential hooks
*/
export function validateHooksUsage(fn: HIRFunction): void {
export function validateHooksUsage(
fn: HIRFunction,
): Result<void, CompilerError> {
const unconditionalBlocks = computeUnconditionalBlocks(fn);
const errors = new CompilerError();
@@ -423,9 +426,7 @@ export function validateHooksUsage(fn: HIRFunction): void {
for (const [, error] of errorsByPlace) {
errors.pushErrorDetail(error);
}
if (errors.hasAnyErrors()) {
fn.env.recordErrors(errors);
}
return errors.asResult();
}
function visitFunctionExpression(errors: CompilerError, fn: HIRFunction): void {

View File

@@ -5,12 +5,15 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError, CompilerErrorDetail, EnvironmentConfig} from '..';
import {CompilerError, EnvironmentConfig} from '..';
import {ErrorCategory} from '../CompilerError';
import {HIRFunction, IdentifierId} from '../HIR';
import {DEFAULT_GLOBALS} from '../HIR/Globals';
import {Result} from '../Utils/Result';
export function validateNoCapitalizedCalls(fn: HIRFunction): void {
export function validateNoCapitalizedCalls(
fn: HIRFunction,
): Result<void, CompilerError> {
const envConfig: EnvironmentConfig = fn.env.config;
const ALLOW_LIST = new Set([
...DEFAULT_GLOBALS.keys(),
@@ -45,16 +48,13 @@ export function validateNoCapitalizedCalls(fn: HIRFunction): void {
const calleeIdentifier = value.callee.identifier.id;
const calleeName = capitalLoadGlobals.get(calleeIdentifier);
if (calleeName != null) {
fn.env.recordError(
new CompilerErrorDetail({
category: ErrorCategory.CapitalizedCalls,
reason,
description: `${calleeName} may be a component`,
loc: value.loc,
suggestions: null,
}),
);
continue;
CompilerError.throwInvalidReact({
category: ErrorCategory.CapitalizedCalls,
reason,
description: `${calleeName} may be a component`,
loc: value.loc,
suggestions: null,
});
}
break;
}
@@ -85,7 +85,5 @@ export function validateNoCapitalizedCalls(fn: HIRFunction): void {
}
}
}
if (errors.hasAnyErrors()) {
fn.env.recordErrors(errors);
}
return errors.asResult();
}

View File

@@ -18,6 +18,7 @@ import {
eachTerminalOperand,
} from '../HIR/visitors';
import {AliasingEffect} from '../Inference/AliasingEffects';
import {Result} from '../Utils/Result';
/**
* Validates that functions with known mutations (ie due to types) cannot be passed
@@ -42,7 +43,9 @@ import {AliasingEffect} from '../Inference/AliasingEffects';
* This pass detects functions with *known* mutations (Store or Mutate, not ConditionallyMutate)
* that are passed where a frozen value is expected and rejects them.
*/
export function validateNoFreezingKnownMutableFunctions(fn: HIRFunction): void {
export function validateNoFreezingKnownMutableFunctions(
fn: HIRFunction,
): Result<void, CompilerError> {
const errors = new CompilerError();
const contextMutationEffects: Map<
IdentifierId,
@@ -159,7 +162,5 @@ export function validateNoFreezingKnownMutableFunctions(fn: HIRFunction): void {
visitOperand(operand);
}
}
if (errors.hasAnyErrors()) {
fn.env.recordErrors(errors);
}
return errors.asResult();
}

View File

@@ -9,6 +9,7 @@ import {CompilerDiagnostic, CompilerError} from '..';
import {ErrorCategory} from '../CompilerError';
import {HIRFunction} from '../HIR';
import {getFunctionCallSignature} from '../Inference/InferMutationAliasingEffects';
import {Result} from '../Utils/Result';
/**
* Checks that known-impure functions are not called during render. Examples of invalid functions to
@@ -19,7 +20,9 @@ import {getFunctionCallSignature} from '../Inference/InferMutationAliasingEffect
* this in several of our validation passes and should unify those analyses into a reusable helper
* and use it here.
*/
export function validateNoImpureFunctionsInRender(fn: HIRFunction): void {
export function validateNoImpureFunctionsInRender(
fn: HIRFunction,
): Result<void, CompilerError> {
const errors = new CompilerError();
for (const [, block] of fn.body.blocks) {
for (const instr of block.instructions) {
@@ -52,7 +55,5 @@ export function validateNoImpureFunctionsInRender(fn: HIRFunction): void {
}
}
}
if (errors.hasAnyErrors()) {
fn.env.recordErrors(errors);
}
return errors.asResult();
}

View File

@@ -27,6 +27,7 @@ import {
eachPatternOperand,
eachTerminalOperand,
} from '../HIR/visitors';
import {Err, Ok, Result} from '../Utils/Result';
import {retainWhere} from '../Utils/utils';
/**
@@ -119,14 +120,12 @@ class Env {
}
}
export function validateNoRefAccessInRender(fn: HIRFunction): void {
export function validateNoRefAccessInRender(
fn: HIRFunction,
): Result<void, CompilerError> {
const env = new Env();
collectTemporariesSidemap(fn, env);
const errors = new CompilerError();
validateNoRefAccessInRenderImpl(fn, env, errors);
if (errors.hasAnyErrors()) {
fn.env.recordErrors(errors);
}
return validateNoRefAccessInRenderImpl(fn, env).map(_ => undefined);
}
function collectTemporariesSidemap(fn: HIRFunction, env: Env): void {
@@ -306,8 +305,7 @@ function joinRefAccessTypes(...types: Array<RefAccessType>): RefAccessType {
function validateNoRefAccessInRenderImpl(
fn: HIRFunction,
env: Env,
errors: CompilerError,
): RefAccessType {
): Result<RefAccessType, CompilerError> {
let returnValues: Array<undefined | RefAccessType> = [];
let place;
for (const param of fn.params) {
@@ -338,6 +336,7 @@ function validateNoRefAccessInRenderImpl(
env.resetChanged();
returnValues = [];
const safeBlocks: Array<{block: BlockId; ref: RefId}> = [];
const errors = new CompilerError();
for (const [, block] of fn.body.blocks) {
retainWhere(safeBlocks, entry => entry.block !== block.id);
for (const phi of block.phis) {
@@ -433,15 +432,13 @@ function validateNoRefAccessInRenderImpl(
case 'FunctionExpression': {
let returnType: RefAccessType = {kind: 'None'};
let readRefEffect = false;
const innerErrors = new CompilerError();
const result = validateNoRefAccessInRenderImpl(
instr.value.loweredFunc.func,
env,
innerErrors,
);
if (!innerErrors.hasAnyErrors()) {
returnType = result;
} else {
if (result.isOk()) {
returnType = result.unwrap();
} else if (result.isErr()) {
readRefEffect = true;
}
env.set(instr.lvalue.identifier.id, {
@@ -732,7 +729,7 @@ function validateNoRefAccessInRenderImpl(
}
if (errors.hasAnyErrors()) {
return {kind: 'None'};
return Err(errors);
}
}
@@ -741,8 +738,10 @@ function validateNoRefAccessInRenderImpl(
loc: GeneratedSource,
});
return joinRefAccessTypes(
...returnValues.filter((env): env is RefAccessType => env !== undefined),
return Ok(
joinRefAccessTypes(
...returnValues.filter((env): env is RefAccessType => env !== undefined),
),
);
}

View File

@@ -13,6 +13,7 @@ import {
import {HIRFunction, IdentifierId, isSetStateType} from '../HIR';
import {computeUnconditionalBlocks} from '../HIR/ComputeUnconditionalBlocks';
import {eachInstructionValueOperand} from '../HIR/visitors';
import {Result} from '../Utils/Result';
/**
* Validates that the given function does not have an infinite update loop
@@ -42,21 +43,17 @@ import {eachInstructionValueOperand} from '../HIR/visitors';
* y();
* ```
*/
export function validateNoSetStateInRender(fn: HIRFunction): void {
export function validateNoSetStateInRender(
fn: HIRFunction,
): Result<void, CompilerError> {
const unconditionalSetStateFunctions: Set<IdentifierId> = new Set();
const errors = validateNoSetStateInRenderImpl(
fn,
unconditionalSetStateFunctions,
);
if (errors.hasAnyErrors()) {
fn.env.recordErrors(errors);
}
return validateNoSetStateInRenderImpl(fn, unconditionalSetStateFunctions);
}
function validateNoSetStateInRenderImpl(
fn: HIRFunction,
unconditionalSetStateFunctions: Set<IdentifierId>,
): CompilerError {
): Result<void, CompilerError> {
const unconditionalBlocks = computeUnconditionalBlocks(fn);
let activeManualMemoId: number | null = null;
const errors = new CompilerError();
@@ -95,7 +92,7 @@ function validateNoSetStateInRenderImpl(
validateNoSetStateInRenderImpl(
instr.value.loweredFunc.func,
unconditionalSetStateFunctions,
).hasAnyErrors()
).isErr()
) {
// This function expression unconditionally calls a setState
unconditionalSetStateFunctions.add(instr.lvalue.identifier.id);
@@ -186,5 +183,5 @@ function validateNoSetStateInRenderImpl(
}
}
return errors;
return errors.asResult();
}

View File

@@ -20,8 +20,9 @@ import {
eachInstructionValueOperand,
eachTerminalOperand,
} from '../HIR/visitors';
import {Result} from '../Utils/Result';
export function validateUseMemo(fn: HIRFunction): void {
export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
const errors = new CompilerError();
const voidMemoErrors = new CompilerError();
const useMemos = new Set<IdentifierId>();
@@ -176,9 +177,7 @@ export function validateUseMemo(fn: HIRFunction): void {
}
}
fn.env.logErrors(voidMemoErrors.asResult());
if (errors.hasAnyErrors()) {
fn.env.recordErrors(errors);
}
return errors.asResult();
}
function validateNoContextVariableAssignment(

View File

@@ -14,7 +14,19 @@ function Component() {
## Error
```
Found 1 error:
Found 2 errors:
Error: Invalid type configuration for module
Expected type for object property 'useHookNotTypedAsHook' from module 'ReactCompilerTest' to be a hook based on the property name.
error.invalid-type-provider-hook-name-not-typed-as-hook-namespace.ts:4:9
2 |
3 | function Component() {
> 4 | return ReactCompilerTest.useHookNotTypedAsHook();
| ^^^^^^^^^^^^^^^^^ Invalid type configuration for module
5 | }
6 |
Error: Invalid type configuration for module

View File

@@ -14,7 +14,19 @@ function Component() {
## Error
```
Found 1 error:
Found 2 errors:
Error: Invalid type configuration for module
Expected type for object property 'useHookNotTypedAsHook' from module 'ReactCompilerTest' to be a hook based on the property name.
error.invalid-type-provider-hook-name-not-typed-as-hook.ts:4:9
2 |
3 | function Component() {
> 4 | return useHookNotTypedAsHook();
| ^^^^^^^^^^^^^^^^^^^^^ Invalid type configuration for module
5 | }
6 |
Error: Invalid type configuration for module

View File

@@ -14,7 +14,19 @@ function Component() {
## Error
```
Found 1 error:
Found 2 errors:
Error: Invalid type configuration for module
Expected type for `import ... from 'useDefaultExportNotTypedAsHook'` to be a hook based on the module name.
error.invalid-type-provider-hooklike-module-default-not-hook.ts:4:15
2 |
3 | function Component() {
> 4 | return <div>{foo()}</div>;
| ^^^ Invalid type configuration for module
5 | }
6 |
Error: Invalid type configuration for module

View File

@@ -14,7 +14,19 @@ function Component() {
## Error
```
Found 1 error:
Found 2 errors:
Error: Invalid type configuration for module
Expected type for object property 'useHookNotTypedAsHook' from module 'ReactCompilerTest' to be a hook based on the property name.
error.invalid-type-provider-nonhook-name-typed-as-hook.ts:4:15
2 |
3 | function Component() {
> 4 | return <div>{notAhookTypedAsHook()}</div>;
| ^^^^^^^^^^^^^^^^^^^ Invalid type configuration for module
5 | }
6 |
Error: Invalid type configuration for module