Skip to content

Commit 692434b

Browse files
committed
[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.
1 parent deed64e commit 692434b

File tree

26 files changed

+634
-90
lines changed

26 files changed

+634
-90
lines changed

compiler/fault-tolerance-overview.md

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -51,20 +51,20 @@ Add error accumulation to the `Environment` class so that any pass can record er
5151

5252
Change `runWithEnvironment` to run all passes and check for errors at the end instead of letting exceptions propagate.
5353

54-
- [ ] **2.1 Change `runWithEnvironment` return type** (`src/Entrypoint/Pipeline.ts`)
54+
- [x] **2.1 Change `runWithEnvironment` return type** (`src/Entrypoint/Pipeline.ts`)
5555
- Change return type from `CodegenFunction` to `Result<CodegenFunction, CompilerError>`
5656
- At the end of the pipeline, check `env.hasErrors()`:
5757
- If no errors: return `Ok(ast)`
5858
- If errors: return `Err(env.aggregateErrors())`
5959

60-
- [ ] **2.2 Update `compileFn` to propagate the Result** (`src/Entrypoint/Pipeline.ts`)
60+
- [x] **2.2 Update `compileFn` to propagate the Result** (`src/Entrypoint/Pipeline.ts`)
6161
- Change `compileFn` return type from `CodegenFunction` to `Result<CodegenFunction, CompilerError>`
6262
- Propagate the Result from `runWithEnvironment`
6363

64-
- [ ] **2.3 Update `run` to propagate the Result** (`src/Entrypoint/Pipeline.ts`)
64+
- [x] **2.3 Update `run` to propagate the Result** (`src/Entrypoint/Pipeline.ts`)
6565
- Same change for the internal `run` function
6666

67-
- [ ] **2.4 Update callers in Program.ts** (`src/Entrypoint/Program.ts`)
67+
- [x] **2.4 Update callers in Program.ts** (`src/Entrypoint/Program.ts`)
6868
- In `tryCompileFunction`, change from try/catch around `compileFn` to handling the `Result`:
6969
- If `Ok(codegenFn)`: return the compiled function
7070
- If `Err(compilerError)`: return `{kind: 'error', error: compilerError}`
@@ -248,31 +248,31 @@ The inference passes are the most critical to handle correctly because they prod
248248

249249
Walk through `runWithEnvironment` and wrap each pass call site. This is the integration work tying Phases 3-6 together.
250250

251-
- [ ] **7.1 Wrap `lower()` call** (line 163)
251+
- [x] **7.1 Wrap `lower()` call** (line 163)
252252
- Change from `lower(func, env).unwrap()` to `lower(func, env)` (direct return after Phase 3.1)
253253

254-
- [ ] **7.2 Wrap validation calls that use `.unwrap()`** (lines 169-303)
254+
- [x] **7.2 Wrap validation calls that use `.unwrap()`** (lines 169-303)
255255
- Remove `.unwrap()` from all validation calls after they're updated in Phase 4
256256
- For validations guarded by `env.enableValidations`, keep the guard but remove the `.unwrap()`
257257

258-
- [ ] **7.3 Wrap inference calls** (lines 233-267)
258+
- [x] **7.3 Wrap inference calls** (lines 233-267)
259259
- After Phase 5, `inferMutationAliasingEffects` and `inferMutationAliasingRanges` record errors directly
260260
- Remove the `mutabilityAliasingErrors` / `mutabilityAliasingRangeErrors` variables and their conditional throw logic
261261

262-
- [ ] **7.4 Wrap `env.logErrors()` calls** (lines 286-331)
262+
- [x] **7.4 Wrap `env.logErrors()` calls** (lines 286-331)
263263
- After Phase 4.13-4.16, these passes record on env directly
264264
- Remove the `env.logErrors()` wrapper calls
265265

266-
- [ ] **7.5 Wrap codegen** (lines 575-578)
266+
- [x] **7.5 Wrap codegen** (lines 575-578)
267267
- After Phase 6.1, `codegenFunction` returns directly
268268
- Remove the `.unwrap()`
269269

270-
- [ ] **7.6 Add final error check** (end of `runWithEnvironment`)
270+
- [x] **7.6 Add final error check** (end of `runWithEnvironment`)
271271
- After all passes complete, check `env.hasErrors()`
272272
- If no errors: return `Ok(ast)`
273273
- If errors: return `Err(env.aggregateErrors())`
274274

275-
- [ ] **7.7 Consider wrapping each pass in `env.tryRecord()`** as a safety net
275+
- [x] **7.7 Consider wrapping each pass in `env.tryRecord()`** as a safety net
276276
- Even after individual passes are updated, wrapping each pass call in `env.tryRecord()` provides defense-in-depth
277277
- If a pass unexpectedly throws a CompilerError (e.g., from a code path we missed), it gets caught and recorded rather than aborting the pipeline
278278
- Non-CompilerError exceptions and invariants still propagate immediately
@@ -318,3 +318,10 @@ Walk through `runWithEnvironment` and wrap each pass call site. This is the inte
318318
- The `assertConsistentIdentifiers`, `assertTerminalSuccessorsExist`, `assertTerminalPredsExist`, `assertValidBlockNesting`, `assertValidMutableRanges`, `assertWellFormedBreakTargets`, `assertScopeInstructionsWithinScopes` assertion functions should continue to throw — they are invariant checks on internal data structure consistency
319319
- The `panicThreshold` mechanism in Program.ts should continue to work — it now operates on the aggregated error from the Result rather than a caught exception, but the behavior is the same
320320

321+
## Key Learnings
322+
323+
* **Phase 2+7 (Pipeline tryRecord wrapping) was sufficient for basic fault tolerance.** Wrapping all passes in `env.tryRecord()` immediately enabled the compiler to continue past errors that previously threw. This caused 52 test fixtures to produce additional errors that were previously masked by the first error bailing out. For example, `error.todo-reassign-const` previously reported only "Support destructuring of context variables" but now also reports the immutability violation.
324+
* **Lint-only passes (Pattern B: `env.logErrors()`) should not use `tryRecord()`/`recordError()`** because those errors are intentionally non-blocking. They are reported via the logger only and should not cause the pipeline to return `Err`. The `logErrors` pattern was kept for `validateNoDerivedComputationsInEffects_exp`, `validateNoSetStateInEffects`, `validateNoJSXInTryStatement`, and `validateStaticComponents`.
325+
* **Inference passes that return `Result` with validation errors** (`inferMutationAliasingEffects`, `inferMutationAliasingRanges`) were changed to record errors via `env.recordErrors()` instead of throwing, allowing subsequent passes to proceed.
326+
* **Value-producing passes** (`memoizeFbtAndMacroOperandsInSameScope`, `renameVariables`, `buildReactiveFunction`) need safe default values when wrapped in `tryRecord()` since the callback can't return values. We initialize with empty defaults (e.g., `new Set()`) before the `tryRecord()` call.
327+

compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts

Lines changed: 64 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,11 @@ import {NodePath} from '@babel/traverse';
99
import * as t from '@babel/types';
1010
import prettyFormat from 'pretty-format';
1111
import {CompilerOutputMode, Logger, ProgramContext} from '.';
12+
import {CompilerError} from '../CompilerError';
13+
import {Err, Ok, Result} from '../Utils/Result';
1214
import {
1315
HIRFunction,
16+
IdentifierId,
1417
ReactiveFunction,
1518
assertConsistentIdentifiers,
1619
assertTerminalPredsExist,
@@ -89,7 +92,6 @@ import {validateNoJSXInTryStatement} from '../Validation/ValidateNoJSXInTryState
8992
import {propagateScopeDependenciesHIR} from '../HIR/PropagateScopeDependenciesHIR';
9093
import {outlineJSX} from '../Optimization/OutlineJsx';
9194
import {optimizePropsMethodCalls} from '../Optimization/OptimizePropsMethodCalls';
92-
import {validateNoImpureFunctionsInRender} from '../Validation/ValidateNoImpureFunctionsInRender';
9395
import {validateStaticComponents} from '../Validation/ValidateStaticComponents';
9496
import {validateNoFreezingKnownMutableFunctions} from '../Validation/ValidateNoFreezingKnownMutableFunctions';
9597
import {inferMutationAliasingEffects} from '../Inference/InferMutationAliasingEffects';
@@ -118,7 +120,7 @@ function run(
118120
logger: Logger | null,
119121
filename: string | null,
120122
code: string | null,
121-
): CodegenFunction {
123+
): Result<CodegenFunction, CompilerError> {
122124
const contextIdentifiers = findContextIdentifiers(func);
123125
const env = new Environment(
124126
func.scope,
@@ -149,7 +151,7 @@ function runWithEnvironment(
149151
t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
150152
>,
151153
env: Environment,
152-
): CodegenFunction {
154+
): Result<CodegenFunction, CompilerError> {
153155
const log = (value: CompilerPipelineValue): void => {
154156
env.logger?.debugLogIRs?.(value);
155157
};
@@ -159,11 +161,17 @@ function runWithEnvironment(
159161
pruneMaybeThrows(hir);
160162
log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
161163

162-
validateContextVariableLValues(hir);
163-
validateUseMemo(hir).unwrap();
164+
env.tryRecord(() => {
165+
validateContextVariableLValues(hir);
166+
});
167+
env.tryRecord(() => {
168+
validateUseMemo(hir).unwrap();
169+
});
164170

165171
if (env.enableDropManualMemoization) {
166-
dropManualMemoization(hir).unwrap();
172+
env.tryRecord(() => {
173+
dropManualMemoization(hir).unwrap();
174+
});
167175
log({kind: 'hir', name: 'DropManualMemoization', value: hir});
168176
}
169177

@@ -196,10 +204,14 @@ function runWithEnvironment(
196204

197205
if (env.enableValidations) {
198206
if (env.config.validateHooksUsage) {
199-
validateHooksUsage(hir).unwrap();
207+
env.tryRecord(() => {
208+
validateHooksUsage(hir).unwrap();
209+
});
200210
}
201211
if (env.config.validateNoCapitalizedCalls) {
202-
validateNoCapitalizedCalls(hir).unwrap();
212+
env.tryRecord(() => {
213+
validateNoCapitalizedCalls(hir).unwrap();
214+
});
203215
}
204216
}
205217

@@ -213,7 +225,7 @@ function runWithEnvironment(
213225
log({kind: 'hir', name: 'InferMutationAliasingEffects', value: hir});
214226
if (env.enableValidations) {
215227
if (mutabilityAliasingErrors.isErr()) {
216-
throw mutabilityAliasingErrors.unwrapErr();
228+
env.recordErrors(mutabilityAliasingErrors.unwrapErr());
217229
}
218230
}
219231

@@ -234,9 +246,11 @@ function runWithEnvironment(
234246
log({kind: 'hir', name: 'InferMutationAliasingRanges', value: hir});
235247
if (env.enableValidations) {
236248
if (mutabilityAliasingRangeErrors.isErr()) {
237-
throw mutabilityAliasingRangeErrors.unwrapErr();
249+
env.recordErrors(mutabilityAliasingRangeErrors.unwrapErr());
238250
}
239-
validateLocalsNotReassignedAfterRender(hir);
251+
env.tryRecord(() => {
252+
validateLocalsNotReassignedAfterRender(hir);
253+
});
240254
}
241255

242256
if (env.enableValidations) {
@@ -245,11 +259,15 @@ function runWithEnvironment(
245259
}
246260

247261
if (env.config.validateRefAccessDuringRender) {
248-
validateNoRefAccessInRender(hir).unwrap();
262+
env.tryRecord(() => {
263+
validateNoRefAccessInRender(hir).unwrap();
264+
});
249265
}
250266

251267
if (env.config.validateNoSetStateInRender) {
252-
validateNoSetStateInRender(hir).unwrap();
268+
env.tryRecord(() => {
269+
validateNoSetStateInRender(hir).unwrap();
270+
});
253271
}
254272

255273
if (
@@ -258,7 +276,9 @@ function runWithEnvironment(
258276
) {
259277
env.logErrors(validateNoDerivedComputationsInEffects_exp(hir));
260278
} else if (env.config.validateNoDerivedComputationsInEffects) {
261-
validateNoDerivedComputationsInEffects(hir);
279+
env.tryRecord(() => {
280+
validateNoDerivedComputationsInEffects(hir);
281+
});
262282
}
263283

264284
if (env.config.validateNoSetStateInEffects && env.outputMode === 'lint') {
@@ -269,11 +289,9 @@ function runWithEnvironment(
269289
env.logErrors(validateNoJSXInTryStatement(hir));
270290
}
271291

272-
if (env.config.validateNoImpureFunctionsInRender) {
273-
validateNoImpureFunctionsInRender(hir).unwrap();
274-
}
275-
276-
validateNoFreezingKnownMutableFunctions(hir).unwrap();
292+
env.tryRecord(() => {
293+
validateNoFreezingKnownMutableFunctions(hir).unwrap();
294+
});
277295
}
278296

279297
inferReactivePlaces(hir);
@@ -285,7 +303,9 @@ function runWithEnvironment(
285303
env.config.validateExhaustiveEffectDependencies
286304
) {
287305
// NOTE: this relies on reactivity inference running first
288-
validateExhaustiveDependencies(hir).unwrap();
306+
env.tryRecord(() => {
307+
validateExhaustiveDependencies(hir).unwrap();
308+
});
289309
}
290310
}
291311

@@ -314,7 +334,8 @@ function runWithEnvironment(
314334
log({kind: 'hir', name: 'InferReactiveScopeVariables', value: hir});
315335
}
316336

317-
const fbtOperands = memoizeFbtAndMacroOperandsInSameScope(hir);
337+
let fbtOperands: Set<IdentifierId> = new Set();
338+
fbtOperands = memoizeFbtAndMacroOperandsInSameScope(hir);
318339
log({
319340
kind: 'hir',
320341
name: 'MemoizeFbtAndMacroOperandsInSameScope',
@@ -406,7 +427,8 @@ function runWithEnvironment(
406427
value: hir,
407428
});
408429

409-
const reactiveFunction = buildReactiveFunction(hir);
430+
let reactiveFunction!: ReactiveFunction;
431+
reactiveFunction = buildReactiveFunction(hir);
410432
log({
411433
kind: 'reactive',
412434
name: 'BuildReactiveFunction',
@@ -493,7 +515,8 @@ function runWithEnvironment(
493515
value: reactiveFunction,
494516
});
495517

496-
const uniqueIdentifiers = renameVariables(reactiveFunction);
518+
let uniqueIdentifiers: Set<string> = new Set();
519+
uniqueIdentifiers = renameVariables(reactiveFunction);
497520
log({
498521
kind: 'reactive',
499522
name: 'RenameVariables',
@@ -511,20 +534,29 @@ function runWithEnvironment(
511534
env.config.enablePreserveExistingMemoizationGuarantees ||
512535
env.config.validatePreserveExistingMemoizationGuarantees
513536
) {
514-
validatePreservedManualMemoization(reactiveFunction).unwrap();
537+
env.tryRecord(() => {
538+
validatePreservedManualMemoization(reactiveFunction).unwrap();
539+
});
515540
}
516541

517-
const ast = codegenFunction(reactiveFunction, {
542+
const codegenResult = codegenFunction(reactiveFunction, {
518543
uniqueIdentifiers,
519544
fbtOperands,
520-
}).unwrap();
545+
});
546+
if (codegenResult.isErr()) {
547+
env.recordErrors(codegenResult.unwrapErr());
548+
return Err(env.aggregateErrors());
549+
}
550+
const ast = codegenResult.unwrap();
521551
log({kind: 'ast', name: 'Codegen', value: ast});
522552
for (const outlined of ast.outlined) {
523553
log({kind: 'ast', name: 'Codegen (outlined)', value: outlined.fn});
524554
}
525555

526556
if (env.config.validateSourceLocations) {
527-
validateSourceLocations(func, ast).unwrap();
557+
env.tryRecord(() => {
558+
validateSourceLocations(func, ast).unwrap();
559+
});
528560
}
529561

530562
/**
@@ -536,7 +568,10 @@ function runWithEnvironment(
536568
throw new Error('unexpected error');
537569
}
538570

539-
return ast;
571+
if (env.hasErrors()) {
572+
return Err(env.aggregateErrors());
573+
}
574+
return Ok(ast);
540575
}
541576

542577
export function compileFn(
@@ -550,7 +585,7 @@ export function compileFn(
550585
logger: Logger | null,
551586
filename: string | null,
552587
code: string | null,
553-
): CodegenFunction {
588+
): Result<CodegenFunction, CompilerError> {
554589
return run(
555590
func,
556591
config,

compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -697,19 +697,21 @@ function tryCompileFunction(
697697
}
698698

699699
try {
700-
return {
701-
kind: 'compile',
702-
compiledFn: compileFn(
703-
fn,
704-
programContext.opts.environment,
705-
fnType,
706-
outputMode,
707-
programContext,
708-
programContext.opts.logger,
709-
programContext.filename,
710-
programContext.code,
711-
),
712-
};
700+
const result = compileFn(
701+
fn,
702+
programContext.opts.environment,
703+
fnType,
704+
outputMode,
705+
programContext,
706+
programContext.opts.logger,
707+
programContext.filename,
708+
programContext.code,
709+
);
710+
if (result.isOk()) {
711+
return {kind: 'compile', compiledFn: result.unwrap()};
712+
} else {
713+
return {kind: 'error', error: result.unwrapErr()};
714+
}
713715
} catch (err) {
714716
return {kind: 'error', error: err};
715717
}

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-call-freezes-captured-memberexpr.expect.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export const FIXTURE_ENTRYPOINT = {
2929
## Error
3030

3131
```
32-
Found 1 error:
32+
Found 2 errors:
3333
3434
Error: This value cannot be modified
3535
@@ -43,6 +43,32 @@ error.hook-call-freezes-captured-memberexpr.ts:13:2
4343
14 | return <Stringify x={x} cb={cb} />;
4444
15 | }
4545
16 |
46+
47+
Error: Cannot modify local variables after render completes
48+
49+
This argument is a function which may reassign or mutate `x` after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead.
50+
51+
error.hook-call-freezes-captured-memberexpr.ts:9:25
52+
7 | * After this custom hook call, it's no longer valid to mutate x.
53+
8 | */
54+
> 9 | const cb = useIdentity(() => {
55+
| ^^^^^^^
56+
> 10 | x.value++;
57+
| ^^^^^^^^^^^^^^
58+
> 11 | });
59+
| ^^^^ This function may (indirectly) reassign or modify `x` after render
60+
12 |
61+
13 | x.value += count;
62+
14 | return <Stringify x={x} cb={cb} />;
63+
64+
error.hook-call-freezes-captured-memberexpr.ts:10:4
65+
8 | */
66+
9 | const cb = useIdentity(() => {
67+
> 10 | x.value++;
68+
| ^ This modifies `x`
69+
11 | });
70+
12 |
71+
13 | x.value += count;
4672
```
4773
4874

0 commit comments

Comments
 (0)