From 05243074f3b63b5b4268526084688df3e49d0f08 Mon Sep 17 00:00:00 2001 From: Joe Savona Date: Fri, 20 Feb 2026 10:50:35 -0800 Subject: [PATCH] [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 --- compiler/fault-tolerance-overview.md | 4 +- .../src/HIR/Environment.ts | 89 ++++++++++++++++++- 2 files changed, 90 insertions(+), 3 deletions(-) diff --git a/compiler/fault-tolerance-overview.md b/compiler/fault-tolerance-overview.md index b3b9207382..57d68d1a78 100644 --- a/compiler/fault-tolerance-overview.md +++ b/compiler/fault-tolerance-overview.md @@ -31,7 +31,7 @@ Note that some errors may continue to cause an eager bailout: Add error accumulation to the `Environment` class so that any pass can record errors during compilation without halting. -- [ ] **1.1 Add error accumulator to Environment** (`src/HIR/Environment.ts`) +- [x] **1.1 Add error accumulator to Environment** (`src/HIR/Environment.ts`) - Add a `#errors: CompilerError` field, initialized in the constructor - Add a `recordError(error: CompilerDiagnostic | CompilerErrorDetail)` method that: - If an Invariant-category detail, immediately throw it @@ -41,7 +41,7 @@ Add error accumulation to the `Environment` class so that any pass can record er - Add a `aggregateErrors(): CompilerError` method that returns the accumulated error object - Consider whether `recordError` should accept the same options as `CompilerError.push()` for convenience (reason, description, severity, loc, etc.) -- [ ] **1.2 Add a `tryRecord` helper on Environment** (`src/HIR/Environment.ts`) +- [x] **1.2 Add a `tryRecord` helper on Environment** (`src/HIR/Environment.ts`) - Add a `tryRecord(fn: () => void): void` method that wraps a callback in try/catch: - If `fn` throws a `CompilerError` that is NOT an invariant, record it via `recordError` - If `fn` throws a non-CompilerError or a CompilerError invariant, re-throw diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts index 80caee2caf..51b1fd6928 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts @@ -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 { @@ -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, @@ -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, + }); + } + } + + /** + * 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); }