Remove `tryRecord()` from the compilation pipeline now that all passes record
errors directly via `env.recordError()` / `env.recordErrors()`. A single
catch-all try/catch in Program.ts provides the safety net for any pass that
incorrectly throws instead of recording.
Key changes:
- Remove all ~64 `env.tryRecord()` wrappers in Pipeline.ts
- Delete `tryRecord()` method from Environment.ts
- Add `CompileUnexpectedThrow` logger event so thrown errors are detectable
- Log `CompileUnexpectedThrow` in Program.ts catch-all for non-invariant throws
- Fail snap tests on `CompileUnexpectedThrow` to surface pass bugs in dev
- Convert throwTodo/throwDiagnostic calls in HIRBuilder (fbt, this),
CodegenReactiveFunction (for-in/for-of), and BuildReactiveFunction to
record errors or use invariants as appropriate
- Remove try/catch from BuildHIR's lower() since inner throws are now recorded
- CollectOptionalChainDependencies: return null instead of throwing on
unsupported optional chain patterns (graceful optimization skip)
- 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.
`invariant()` was a pain to use - we always record a single location,
but the API required passing a compiler detail. This PR replaces
`invariant()` (keeping the name) with `simpleInvariant()`s signature,
and updates call sites accordingly. I've noticed that agents
consistently get invariant() wrong, which aligns with it being tedious
to call when you're writing code by hand. The simplified API should help
a bit.
With `ValidateExhaustiveMemoDependencies` we can now check exhaustive
dependencies for useMemo and useCallback within the compiler, without
relying on the separate exhaustive-deps rule. Until now we've bailed out
of any component/hook that suppresses this rule, since the suppression
_might_ affect a memoization value. Compiling code with incorrect memo
deps can change behavior so this wasn't safe. The downside was that a
suppression within a useEffect could prevent memoization, even though
non-exhaustive deps for effects do not cause problems for memoization
specifically.
So here, we change to ignore ESLint suppressions if we have both the
compiler's hooks validation and memo deps validations enabled.
Now we just have to test out the new validation and refine before we can
enable this by default.
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35184).
* #35201
* #35202
* #35192
* #35190
* #35186
* #35185
* __->__ #35184
This deprecates the `noEmit: boolean` flag and adds `outputMode:
'client' | 'client-no-memo' | 'ssr' | 'lint'` as the replacement.
OutputMode defaults to null and takes precedence if specified, otherwise
we use 'client' mode for noEmit=false and 'lint' mode for noEmit=true.
Key points:
* Retrying failed compilation switches from 'client' mode to
'client-no-memo'
* Validations are enabled behind
Environment.proto.shouldEnableValidations, enabled for all modes except
'client-no-memo'. Similar for dropping manual memoization.
* OptimizeSSR is now gated by the outputMode==='ssr', not a feature flag
* Creation of reactive scopes, and related codegen logic, is now gated
by outputMode==='client'
With #34176 we now have granular lint rules created for each compiler
ErrorCategory. However, we had remnants of our old error severities
still in use which makes reporting errors quite clunky. Previously you
would need to specify both a category and severity which often ended up
being the same.
This PR moves severity definition into our rules which are generated
from our categories. For now I decided to defer "upgrading" categories
from a simple string to a sum type since we are only using severities to
map errors to eslint severity.
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34401).
* #34409
* #34404
* #34403
* #34402
* __->__ #34401
NOTE: this is a merged version of @mofeiZ's original PR along with my
edits per offline discussion. The description is updated to reflect the
latest approach.
The key problem we're trying to solve with this PR is to allow
developers more control over the compiler's various validations. The
idea is to have a number of rules targeting a specific category of
issues, such as enforcing immutability of props/state/etc or disallowing
access to refs during render. We don't want to have to run the compiler
again for every single rule, though, so @mofeiZ added an LRU cache that
caches the full compilation output of N most recent files. The first
rule to run on a given file will cause it to get cached, and then
subsequent rules can pull from the cache, with each rule filtering down
to its specific category of errors.
For the categories, I went through and assigned a category roughly 1:1
to existing validations, and then used my judgement on some places that
felt distinct enough to warrant a separate error. Every error in the
compiler now has to supply both a severity (for legacy reasons) and a
category (for ESLint). Each category corresponds 1:1 to a ESLint rule
definition, so that the set of rules is automatically populated based on
the defined categories.
Categories include a flag for whether they should be in the recommended
set or not.
Note that as with the original version of this PR, only
eslint-plugin-react-compiler is changed. We still have to update the
main lint rule.
## Test Plan
* Created a sample project using ESLint v9 and verified that the plugin
can be configured correctly and detects errors
* Edited `fixtures/eslint-v9` and introduced errors, verified that the w
latest config changes in that fixture it correctly detects the errors
* In the sample project, confirmed that the LRU caching is correctly
caching compiler output, ie compiling files just once.
Co-authored-by: Mofei Zhang <feifei0@meta.com>
Work in progress, i'm experimenting with revamping our diagnostic infra.
Starting with a better format for representing errors, with an ability
to point ot multiple locations, along with better printing of errors. Of
course, Babel still controls the printing in the majority case so this
still needs more work.
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33751).
* #33981
* #33777
* #33767
* #33765
* #33760
* #33759
* #33758
* __->__ #33751
* #33752
* #33753
React Compiler's program traversal logic is pretty lengthy and complex
as we've added a lot of features piecemeal. `compileProgram` is 300+
lines long and has confusing control flow (defining helpers inline,
invoking visitors, mutating-asts-while-iterating, mutating global
`ALREADY_COMPILED` state).
- Moved more stuff to `ProgramContext`
- Separated `compileProgram` into a bunch of helpers
Tested by syncing this stack to a Meta codebase and observing no
compilation output changes (D74487851, P1806855669, P1806855379)
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33147).
* #33149
* #33148
* __->__ #33147
Previously the CompileSuccess event would emit first before CompileSkip,
so the lsp's codelens would incorrectly flag skipped components/hooks
(via 'use no memo') as being optimized.
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33012).
* __->__ #33012
* #33011
* #33010
Currently, inferred effect dependencies are considered a
"compiler-required" feature. This means that untransformed callsites
should escalate to a build error.
`ValidateNoUntransformedReferences` iterates 'special effect' callsites
and checks that the compiler was able to successfully transform them.
Prior to this PR, this relied on checking the number of arguments passed
to this special effect.
This obviously doesn't work with `noEmit: true`, which is used for our
eslint plugin (this avoids mutating the babel program as other linters
run with the same ast). This PR adds a set of `babel.SourceLocation`s to
do best effort matching in this mode.
Avoid failing builds when imported function specifiers conflict by using
babel's `generateUid`. Failing a build is very disruptive, as it usually
presents to developers similar to a javascript parse error.
```js
import {logRender as _logRender} from 'instrument-runtime';
const logRender = () => { /* local conflicting implementation */ }
function Component_optimized() {
_logRender(); // inserted by compiler
}
```
Currently, we fail builds (even in `panicThreshold:none` cases) when
import specifiers are detected to conflict with existing local
variables. The reason we destructively throw (instead of bailing out) is
because (1) we first generate identifier references to the conflicting
name in compiled functions, (2) replaced original functions with
compiled functions, and then (3) finally check for conflicts.
When we finally check for conflicts, it's too late to bail out.
```js
// import {logRender} from 'instrument-runtime';
const logRender = () => { /* local conflicting implementation */ }
function Component_optimized() {
logRender(); // inserted by compiler
}
```
Traverse program after running compiler transform to find untransformed
references to compiler features (e.g. `inferEffectDeps`, `fire`).
Hard error to fail the babel pipeline when the compiler fails to
transform these features to give predictable runtime semantics.
Untransformed calls to functions like `fire` will throw at runtime
anyways, so let's fail the build to catch these earlier.
Note that with this fails the build *regardless of panicThreshold*
Removes `EnvironmentConfig.enableMinimalTransformsForRetry` in favor of
`run` parameters. This is a minimal difference but lets us explicitly
opt out certain compiler passes based on mode parameters, instead of
environment configurations
Retry flags don't really make sense to have in `EnvironmentConfig`
anyways as the config is user-facing API, while retrying is a compiler
implementation detail.
(per @josephsavona's feedback
https://github.com/facebook/react/pull/32164#issuecomment-2608616479)
> Re the "hacky" framing of this in the PR title: I think this is fine.
I can see having something like a compilation or output mode that we use
when running the pipeline. Rather than changing environment settings
when we re-run, various passes could take effect based on the
combination of the mode + env flags. The modes might be:
>
> * Full: transform, validate, memoize. This is the default today.
> * Transform: Along the lines of the backup mode in this PR. Only
applies transforms that do not require following the rules of React,
like `fire()`.
> * Validate: This could be used for ESLint.
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/32511).
* #32512
* __->__ #32511
Reduce false positive bailouts by using the same
`isReferencedIdentifier` logic that the compiler also uses for
determining context variables and a function's own hoisted declarations.
Details:
Previously, we counted every babel identifier as a reference. This is
problematic because babel counts most string symbols as an identifier.
```js
print(x); // x is an identifier as expected
obj.x // x is.. also an identifier here
{x: 2} // x is also an identifier here
```
This PR adds a check for `isReferencedIdentifier`. Note that only
non-lval
references pass this check. This should be fine as we don't need to
hoist function declarations before writes to the same lvalue (which
should error in strict mode anyways)
```js
print(x); // isReferencedIdentifier(x) -> true
obj.x // isReferencedIdentifier(x) -> false
{x: 2} // isReferencedIdentifier(x) -> false
x = 2 // isReferencedIdentifier(x) -> false
```
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/32596).
* __->__ #32596
* #32595
* #32594
* #32593
* #32522
* #32521
Hacky retry pipeline for when transforming `fire(...)` calls encounters
validation, todo, or memoization invariant bailouts. Would love feedback
on how we implement this to be extensible to other compiler
non-memoization features (e.g. inlineJSX)
Some observations:
- Compiler "front-end" passes (e.g. lower, type, effect, and mutability
inferences) should be shared for all compiler features -- memo and
otherwise
- Many passes (anything dealing with reactive scope ranges, scope blocks
/ dependencies, and optimizations such as ReactiveIR #31974) can be left
out of the retry pipeline. This PR hackily skips memoization features by
removing reactive scope creation, but we probably should restructure the
pipeline to skip these entirely on a retry
- We should maintain a canonical set of "validation flags"
Note the newly added fixtures are prefixed with `bailout-...` when the
retry fire pipeline is used. These fixture outputs contain correctly
inserted `useFire` calls and no memoization.
Traverse the compiled functions to ensure there are no lingering fires
and that all
fire calls are inside an effect lambda.
Also corrects the import to import from the compiler runtime instead
--
Adds `target: 'donotuse_meta_internal'`, which inserts useMemoCache
imports directly from `react`. Note that this is only valid for Meta
bundles, as others do not [re-export the `c`
function](5b0ef217ef/packages/react/index.fb.js (L68-L70)).
```js
// target=donotuse_meta_internal
import {c as _c} from 'react';
// target=19
import {c as _c} from 'react/compiler-runtime';
// target=17,18
import {c as _c} from 'react-compiler-runtime';
```
Meta is a bit special in that react runtime and compiler are guaranteed
to be up-to-date and compatible. It also has its own bundling and module
resolution logic, which makes importing from `react/compiler-runtime`
tricky.
I'm also fine with implementing the alternative which adds an internal
stub for `react-compiler-runtime` and
[bundles](5b0ef217ef/scripts/rollup/bundles.js (L120))
the runtime for internal builds.
Currently, the react compiler can not compile within callbacks which can
potentially cause over rendering. Consider this example:
```jsx
function Component(countries, onDelete) {
const name = useFoo();
return countries.map(() => {
return (
<Foo>
<Bar name={name}/>
<Baz onclick={onDelete} />
</Foo>
);
});
}
```
In this case, there's no memoization of the nested jsx elements. But
instead if we were to manually refactor the nested jsx into separate
component like this:
```jsx
function Component(countries, onDelete) {
const name = useFoo();
return countries.map(() => {
return <Temp name={name} onDelete={onDelete} />;
});
}
function Temp({ name, onDelete }) {
return (
<Foo>
<Bar name={name} />
<Baz onclick={onDelete} />
</Foo>
);
}
```
The compiler can now optimise both these components:
```jsx
function Component(countries, onDelete) {
const $ = _c(4);
const name = useFoo();
let t0;
if ($[0] !== name || $[1] !== onDelete || $[2] !== countries) {
t0 = countries.map(() => <Temp name={name} onDelete={onDelete} />);
$[0] = name;
$[1] = onDelete;
$[2] = countries;
$[3] = t0;
} else {
t0 = $[3];
}
return t0;
}
function Temp(t0) {
const $ = _c(7);
const { name, onDelete } = t0;
let t1;
if ($[0] !== name) {
t1 = <Bar name={name} />;
$[0] = name;
$[1] = t1;
} else {
t1 = $[1];
}
let t2;
if ($[2] !== onDelete) {
t2 = <Baz onclick={onDelete} />;
$[2] = onDelete;
$[3] = t2;
} else {
t2 = $[3];
}
let t3;
if ($[4] !== t1 || $[5] !== t2) {
t3 = (
<Foo>
{t1}
{t2}
</Foo>
);
$[4] = t1;
$[5] = t2;
$[6] = t3;
} else {
t3 = $[6];
}
return t3;
}
```
Now, when `countries` is updated by adding one single value, only the
newly added value is re-rendered and not the entire list. Rather than
having to do this manually, this PR teaches the react compiler to do
this transformation.
This PR adds a new pass (`OutlineJsx`) to capture nested jsx statements
and outline them in a separate component. This newly outlined component
can then by memoized by the compiler, giving us more fine grained
rendering.
Updates the compiler to always import from `react-compiler-runtime` by
default. The runtime then decides whether to use the official or
userspace implementation of useMemoCache.
Addresses a todo from a while back. We now validate environment options when parsing the plugin options, which means we can stop re-parsing/validating in later phases.
ghstack-source-id: b19806e843e1254716705b33dcf86afb7223f6c7
Pull Request resolved: https://github.com/facebook/react/pull/30726
This PR updates the babel plugin to continue the compilation pipeline as
normal on components/hooks that have been opted out using a directive.
Instead, we no longer emit the compiled function when the directive is
present.
Previously, we would skip over the entire pipeline. By continuing to
enter the pipeline, we'll be able to detect if there are unused
directives.
The end result is:
- (no change) 'use forget' will always opt into compilation
- (new) 'use no forget' will opt out of compilation but continue to log
errors without throwing them. This means that a Program containing
multiple functions (some of which are opted out) will continue to
compile correctly
ghstack-source-id: 5bd85df2f8
Pull Request resolved: https://github.com/facebook/react/pull/30720
Previously the compiler would add an import for the specified context
callee even if the context access was not lowered, leading to unused
imports.
This PR tracks if lowering has happened and adds the import only when
necessary.
ghstack-source-id: 6ad794da41116e1034783b6c4a58fbfe7790343e
Pull Request resolved: https://github.com/facebook/react/pull/30628
If a value is specified for the LowerContextAccess environment config,
we rewrite the callee from 'useContext' to the specificed value.
This will allow us run an experiment internally.
ghstack-source-id: 00e161b988c8f8a1cf96efff8095f050cb534cc1
Pull Request resolved: https://github.com/facebook/react/pull/30612
To handle more cases, always append the synthetic outlined function as a
new child of the module rather than make assumptions about the original
function. This should handle whatever case where the original function
expression may be a child of a variety of parents
ghstack-source-id: 8581edb8be
Pull Request resolved: https://github.com/facebook/react/pull/30466
Addresses follow up feedback from #30446. Since the outlined function is
guaranteed to have a module-scoped unique identifier name, we can
simplify the insertion logic for the outlined function to always emit a
function declaration rather than switch based on the original function
type. This is fine because the outlined function is synthetic anyway.
ghstack-source-id: 0a4d1f7b0a
Pull Request resolved: https://github.com/facebook/react/pull/30464
Previously we would insert new (Arrow)FunctionExpressions as a sibling
of the original function. However this would break in the outlining case
as it would cause the original function expression's parent to become a
SequenceExpression, breaking a bunch of assumptions in the babel plugin.
To get around this, we synthesize a new VariableDeclaration to contain
the newly inserted function expression and therefore insert it as a true
sibling to the original function.
Yeah, it's kinda gross
ghstack-source-id: df13e3b439962b95af4bbd82ef4302624668faf7
Pull Request resolved: https://github.com/facebook/react/pull/30446
Updates the prettier config to format all `.ts` and `.tsx` files in the
repo using the existing defaults and removing overrides.
The first commit in this PR contains the config changes, the second is
just the result of running `yarn prettier-all`.
Implements general-purpose function outlining. Specifically, anonymous function expressions which have no dependencies/context variables are extracted into named top-level functions. The original function expression is replaced with a `LoadGlobal` of the generated name.
Note that the architecture is designed to allow very general purpose forms of outlining, though we currently are very conservative in what we outline. Specifically, the outlining allows annotating functions with an optional ReactiveFunctionType, which if set will cause the outlined function to get compiled as that type. So we could for example outline a helper hook or helper component, set the type, and then have the hook/component get memoized as well. For now though we just outline with no type set, and generate the function as-is without running it through compilation.
ghstack-source-id: 2a7da6c8e85c3f8becb22d3869d9b6200f7db126
Pull Request resolved: https://github.com/facebook/react/pull/30331
Refactors Program.ts to first traverse the `Program` node and build up a queue of functions to visit, then iterate that queue and compile the functions. This doesn't change behavior, but allows the next diff to add additional items to the queue during compilation (for function outlining).
ghstack-source-id: 858527c30ccc26b3aa6fe75a4746fce0820b316f
Pull Request resolved: https://github.com/facebook/react/pull/30330
Summary: We now expect that candidate components that have Flow or TS type annotations on their first parameters have annotations that are potentially objects--this lets us reject compiling functions that explicitly take e.g. `number` as a parameter.
ghstack-source-id: e2c23348265b7ef651232b962ed7be7f6fed1930
Pull Request resolved: https://github.com/facebook/react/pull/29866
Summary: We can tighten our criteria for what is a component by requiring that a component or hook contain JSX or hook calls directly within its body, excluding nested functions . Currently, if we see them within the body anywhere -- including nested functions -- we treat it as a component if the other requirements are met. This change makes this stricter.
We also now expect components (but not necessarily hooks) to have return statements, and those returns must be potential React nodes (we can reject functions that return function or object literals, for example).
ghstack-source-id: 4507cc3955216c564bf257c0b81bfb551ae6ae55
Pull Request resolved: https://github.com/facebook/react/pull/29865