Skip to content

[compiler] Phase 4 (batch 1): Update validation passes to record errors on env#563

Closed
everettbu wants to merge 4 commits into
mainfrom
pr35843
Closed

[compiler] Phase 4 (batch 1): Update validation passes to record errors on env#563
everettbu wants to merge 4 commits into
mainfrom
pr35843

Conversation

@everettbu

Copy link
Copy Markdown

Mirror of facebook/react#35843
Original author: josephsavona


Update 9 validation passes to record errors directly on fn.env instead of
returning Result<void, CompilerError>:

  • validateHooksUsage
  • validateNoCapitalizedCalls (also changed throwInvalidReact to recordError)
  • validateUseMemo
  • dropManualMemoization
  • validateNoRefAccessInRender
  • validateNoSetStateInRender
  • validateNoImpureFunctionsInRender
  • validateNoFreezingKnownMutableFunctions
  • validateExhaustiveDependencies

Each pass now calls fn.env.recordErrors() instead of returning errors.asResult().
Pipeline.ts call sites updated to remove tryRecord() wrappers and .unwrap().


Stack created with Sapling. Best reviewed with ReviewStack.

  • #35853
  • #35852
  • #35851
  • #35850
  • #35849
  • #35848
  • #35847
  • #35845
  • #35844
  • -> #35843
  • #35842
  • #35841
  • #35840

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.
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
…erance

- 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.
…rs on env

Update 9 validation passes to record errors directly on fn.env instead of
returning Result<void, CompilerError>:
- validateHooksUsage
- validateNoCapitalizedCalls (also changed throwInvalidReact to recordError)
- validateUseMemo
- dropManualMemoization
- validateNoRefAccessInRender
- validateNoSetStateInRender
- validateNoImpureFunctionsInRender
- validateNoFreezingKnownMutableFunctions
- validateExhaustiveDependencies

Each pass now calls fn.env.recordErrors() instead of returning errors.asResult().
Pipeline.ts call sites updated to remove tryRecord() wrappers and .unwrap().
@everettbu everettbu added CLA Signed React Core Team Opened by a member of the React Core Team labels Feb 21, 2026
@greptile-apps

greptile-apps Bot commented Feb 21, 2026

Copy link
Copy Markdown

Greptile Summary

This PR implements Phase 4 (batch 1) of the React Compiler fault tolerance plan, updating 9 validation passes to record errors directly on fn.env instead of returning Result<void, CompilerError>. The pipeline (runWithEnvironment) now returns Result<CodegenFunction, CompilerError> and accumulates errors across all passes, allowing multiple errors to be reported at once instead of bailing out on the first one. Non-validation passes are wrapped in env.tryRecord() as a safety net.

  • 9 validation passes updated: validateHooksUsage, validateNoCapitalizedCalls, validateUseMemo, dropManualMemoization, validateNoRefAccessInRender, validateNoSetStateInRender, validateNoImpureFunctionsInRender, validateNoFreezingKnownMutableFunctions, validateExhaustiveDependencies — all changed from returning Result to recording errors on fn.env and returning void
  • Pipeline return type changed: run, runWithEnvironment, and compileFn now return Result<CodegenFunction, CompilerError>, with Program.ts updated to handle the Result
  • Environment error infrastructure: New recordError, recordErrors, hasErrors, aggregateErrors, and tryRecord methods added to Environment
  • Test fixture updates: ~25 test fixtures updated to reflect additional errors now surfaced by the fault-tolerant pipeline; most show more errors being reported (as intended)
  • Two test regressions in error quality: error.invalid-hook-for and error.dont-hoist-inline-reference now show internal invariant errors instead of their original user-friendly validation errors, because the pipeline continues past recorded errors into passes that hit invariants on corrupted state
  • Double-logging concern: recordError logs errors with fnLoc: null during pipeline execution, and Program.ts re-logs the same errors with the proper fnLoc — visible as duplicate entries in the dynamic-gating-bailout-nopanic test

Confidence Score: 4/5

  • This PR is safe to merge as part of the incremental fault tolerance rollout, with two known error quality regressions that should be tracked for follow-up.
  • The core changes are mechanical and well-structured: 9 validation passes consistently updated from Result-returning to env-recording, pipeline return type properly propagated, and Program.ts correctly handles the new Result. The design is well-documented in the fault tolerance overview. Two test cases show degraded error quality (user-friendly errors replaced by internal invariants), and there's a minor double-logging issue — but these appear to be known trade-offs of the phased approach rather than bugs.
  • Pay attention to error.invalid-hook-for.expect.md and error.dont-hoist-inline-reference.expect.md where user-facing errors regressed to internal invariants, and Environment.ts for the double-logging concern.

Important Files Changed

Filename Overview
compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts Adds error accumulation infrastructure: recordError, recordErrors, hasErrors, aggregateErrors, and tryRecord methods. Logging in recordError uses fnLoc: null which causes double-logging when Program.ts also logs the aggregated errors.
compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts Core pipeline changes: return type changed to Result, passes wrapped in tryRecord(), validation passes no longer use .unwrap(). Value-producing passes use definite assignment assertions or pre-initialized defaults. Codegen errors cause early return.
compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts Updated to handle Result from compileFn instead of relying solely on try/catch. Clean pattern: Ok -> compile result, Err -> error result. Try/catch retained for truly unexpected exceptions.
compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts Most complex refactoring: changed from Result-based to errors-parameter pattern. Inner function validation uses separate innerErrors for signal detection only (consistent with original behavior). Return type changed from Result to RefAccessType.
compiler/packages/babel-plugin-react-compiler/src/tests/fixtures/compiler/rules-of-hooks/error.invalid-hook-for.expect.md Error changed from 2 user-friendly hook validation errors to 1 internal invariant. Previously validateHooksUsage caught the issue; now the pipeline continues past it and hits an invariant in BuildReactiveFunction.
compiler/packages/babel-plugin-react-compiler/src/tests/fixtures/compiler/error.dont-hoist-inline-reference.expect.md Error changed from user-facing Todo to an Invariant. The Todo from enterSSA is now recorded via tryRecord, but the corrupted state causes an invariant in InferMutationAliasingEffects.
compiler/packages/babel-plugin-react-compiler/src/tests/fixtures/compiler/gating/dynamic-gating-bailout-nopanic.expect.md New duplicate log line added with fnLoc: null from recordError, in addition to existing log with proper fnLoc from Program.ts error handling.

Last reviewed commit: 821d6a8

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

39 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +744 to +750
if (this.logger != null) {
this.logger.logEvent(this.filename, {
kind: 'CompileError',
detail: error,
fnLoc: null,
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Double-logging of compilation errors
recordError logs each error with fnLoc: null here, and then when the pipeline returns Err(aggregateErrors()), Program.ts's logError/handleError iterates through the same error details and logs them again (with the proper fnLoc). This is visible in the dynamic-gating-bailout-nopanic test fixture, which now has a duplicate log entry — the first with "fnLoc":null and the second with the correct fnLoc.

This may be intentional during the transition period, but it could confuse consumers of the logger output. Consider either:

  • Deferring the logging here and letting Program.ts handle all logging at the end, or
  • Skipping the re-logging in Program.ts for errors that were already recorded on env
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:
**Double-logging of compilation errors**
`recordError` logs each error with `fnLoc: null` here, and then when the pipeline returns `Err(aggregateErrors())`, `Program.ts`'s `logError`/`handleError` iterates through the same error details and logs them again (with the proper `fnLoc`). This is visible in the `dynamic-gating-bailout-nopanic` test fixture, which now has a duplicate log entry — the first with `"fnLoc":null` and the second with the correct `fnLoc`.

This may be intentional during the transition period, but it could confuse consumers of the logger output. Consider either:
- Deferring the logging here and letting `Program.ts` handle all logging at the end, or
- Skipping the re-logging in `Program.ts` for errors that were already recorded on env

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

Comment on lines +19 to 37
Found 1 error:

Error: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)
Invariant: Unexpected empty block with `goto` terminal

error.invalid-hook-for.ts:4:9
2 | let i = 0;
3 | for (let x = 0; useHook(x) < 10; useHook(i), x++) {
> 4 | i += useHook(x);
| ^^^^^^^ Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)
5 | }
6 | return i;
7 | }
Block bb5 is empty.

Error: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)

error.invalid-hook-for.ts:3:35
error.invalid-hook-for.ts:3:2
1 | function Component(props) {
2 | let i = 0;
> 3 | for (let x = 0; useHook(x) < 10; useHook(i), x++) {
| ^^^^^^^ Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)
4 | i += useHook(x);
5 | }
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 4 | i += useHook(x);
| ^^^^^^^^^^^^^^^^^^^^
> 5 | }
| ^^^^ Unexpected empty block with `goto` terminal
6 | return i;
7 | }
8 |
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

User-facing hook errors replaced by internal invariant
This test previously reported 2 clear "Hooks must always be called in a consistent order" errors but now reports an internal invariant ("Unexpected empty block with goto terminal") from BuildReactiveFunction. This happens because validateHooksUsage now records its errors on env instead of throwing, allowing the pipeline to continue with malformed HIR (hooks in a for-loop), which triggers the invariant in a later pass.

While this is a known trade-off of the phased fault tolerance approach, this particular case is a regression in error quality for end users — they lose the actionable hook validation message and instead see an opaque internal error. Is this expected to be resolved in a later phase (e.g., when BuildReactiveFunction is made more resilient)?

Prompt To Fix With AI
This is a comment left during a code review.
Path: compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/rules-of-hooks/error.invalid-hook-for.expect.md
Line: 19-37

Comment:
**User-facing hook errors replaced by internal invariant**
This test previously reported 2 clear "Hooks must always be called in a consistent order" errors but now reports an internal invariant ("Unexpected empty block with `goto` terminal") from `BuildReactiveFunction`. This happens because `validateHooksUsage` now records its errors on env instead of throwing, allowing the pipeline to continue with malformed HIR (hooks in a for-loop), which triggers the invariant in a later pass.

While this is a known trade-off of the phased fault tolerance approach, this particular case is a regression in error quality for end users — they lose the actionable hook validation message and instead see an opaque internal error. Is this expected to be resolved in a later phase (e.g., when `BuildReactiveFunction` is made more resilient)?

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

@greptile-apps

greptile-apps Bot commented Feb 21, 2026

Copy link
Copy Markdown
Additional Comments (1)

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.dont-hoist-inline-reference.expect.md
User-facing Todo error replaced by internal invariant
Similar to the error.invalid-hook-for case: the original Todo error from enterSSA ("[hoisting] EnterSSA: Expected identifier to be defined before being used") is now recorded via tryRecord, but the pipeline continues with corrupted state, causing an invariant in InferMutationAliasingEffects. The user now sees "Invariant: [InferMutationAliasingEffects] Expected value kind to be initialized" instead of the more descriptive original error. Is this expected to be addressed in a later phase?

Prompt To Fix With AI
This is a comment left during a code review.
Path: compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.dont-hoist-inline-reference.expect.md
Line: 22-36

Comment:
**User-facing Todo error replaced by internal invariant**
Similar to the `error.invalid-hook-for` case: the original Todo error from `enterSSA` ("[hoisting] EnterSSA: Expected identifier to be defined before being used") is now recorded via `tryRecord`, but the pipeline continues with corrupted state, causing an invariant in `InferMutationAliasingEffects`. The user now sees "Invariant: [InferMutationAliasingEffects] Expected value kind to be initialized" instead of the more descriptive original error. Is this expected to be addressed in a later phase?

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

@everettbu

Copy link
Copy Markdown
Author

Upstream PR was closed or merged. Code is synced via branch mirror.

@everettbu everettbu closed this Feb 23, 2026
@everettbu
everettbu deleted the pr35843 branch February 23, 2026 17:34
@everettbu
everettbu restored the pr35843 branch February 23, 2026 18:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed React Core Team Opened by a member of the React Core Team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants