Compare commits

..

26 Commits

Author SHA1 Message Date
Joe Savona
557022396b [compiler] Improved ref validation for non-mutating functions
If a function is known to freeze its inputs, and captures refs, then we can safely assume those refs are not mutated during render.

An example is React Native's PanResponder, which is designed for use in interaction handling. Calling `PanResponder.create()` creates an object that shouldn't be interacted with at render time, so we can treat it as freezing its arguments, returning a frozen value, and not accessing any refs in the callbacks passed to it. ValidateNoRefAccessInRender is updated accordingly - if we see a Freeze <place> and ImmutableCapture <place> for the same place in the same instruction, we know that it's not being mutated.

Note that this is a pretty targeted fix. One weakness is that we may not always emit a Freeze effect if a value is already frozen, which could cause this optimization not to kick in. The worst case there is that you'd just get a ref access in render error though, not miscompilation. And we could always choose to always emit Freeze effects, even for frozen values, just to retain the information for validations like this.
2026-02-24 09:45:59 -08:00
Joseph Savona
b354bbd2d2 [compiler] Update docs with fault tolerance summary, remove planning doc (#35888)
Add concise fault tolerance documentation to CLAUDE.md and the passes
README covering error accumulation, tryRecord wrapping, and the
distinction between validation vs infrastructure passes. Remove the
detailed planning document now that the work is complete.
2026-02-23 16:18:44 -08:00
Joseph Savona
c92c579715 [compiler] Fix Pipeline.ts early-exit, formatting, and style issues (#35884)
Fix the transformFire early-exit in Pipeline.ts to only trigger on new
errors from transformFire itself, not pre-existing errors from earlier
passes. The previous `env.hasErrors()` check was too broad — it would
early-exit on validation errors that existed before transformFire ran.

Also add missing blank line in CodegenReactiveFunction.ts Context class,
and fix formatting in ValidateMemoizedEffectDependencies.ts.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35884).
* #35888
* __->__ #35884
2026-02-23 16:16:41 -08:00
Joseph Savona
011cede068 [compiler] Rename mismatched variable names after type changes (#35883)
Rename `state: Environment` to `env: Environment` in
ValidateMemoizedEffectDependencies visitor methods, and
`errorState: Environment` to `env: Environment` in
ValidatePreservedManualMemoization's validateInferredDep.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35883).
* #35888
* #35884
* __->__ #35883
2026-02-23 16:13:46 -08:00
Joseph Savona
2e0927dc70 [compiler] Remove local CompilerError accumulators, emit directly to env.recordError() (#35882)
Removes unnecessary indirection in 17 compiler passes that previously
accumulated errors in a local `CompilerError` instance before flushing
them to `env.recordErrors()` at the end of each pass. Errors are now
emitted directly via `env.recordError()` as they're discovered.

For passes with recursive error-detection patterns
(ValidateNoRefAccessInRender,
ValidateNoSetStateInRender), the internal accumulator is kept but
flushed
via individual `recordError()` calls. For InferMutationAliasingRanges,
a `shouldRecordErrors` flag preserves the conditional suppression logic.
For TransformFire, the throw-based error propagation is replaced with
direct recording plus an early-exit check in Pipeline.ts.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35882).
* #35888
* #35884
* #35883
* __->__ #35882
2026-02-23 16:11:50 -08:00
Joseph Savona
9075330979 [compiler] Remove tryRecord, add catch-all error handling, fix remaining throws (#35881)
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)

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35881).
* #35888
* #35884
* #35883
* #35882
* __->__ #35881
2026-02-23 16:10:17 -08:00
Joseph Savona
8a33fb3a1c [compiler] Cleanup: consistent tryRecord() wrapping and error recording (#35880)
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35880).
* #35888
* #35884
* #35883
* #35882
* #35881
* __->__ #35880
2026-02-23 16:08:04 -08:00
Joseph Savona
cebe42e245 [compiler] Add fault tolerance test fixtures (#35879)
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35879).
* #35888
* #35884
* #35883
* #35882
* #35881
* #35880
* __->__ #35879
2026-02-23 16:06:39 -08:00
Joseph Savona
d6558f36e2 [compiler] Phase 3: Make lower() always produce HIRFunction (#35878)
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35878).
* #35888
* #35884
* #35883
* #35882
* #35881
* #35880
* #35879
* __->__ #35878
2026-02-23 16:05:05 -08:00
Joseph Savona
59d7c27087 [compiler] Phase 8: Add multi-error test fixture and update plan (#35877)
Add test fixture demonstrating fault tolerance: the compiler now reports
both a mutation error and a ref access error in the same function, where
previously only one would be reported before bailing out.

Update plan doc to mark all phases as complete.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35877).
* #35888
* #35884
* #35883
* #35882
* #35881
* #35880
* #35879
* #35878
* __->__ #35877
2026-02-23 16:02:32 -08:00
Joseph Savona
9b2d8013ee [compiler] Phase 4 (batch 2), 5, 6: Update remaining passes for fault tolerance (#35876)
Update remaining validation passes to record errors on env:
- validateMemoizedEffectDependencies
- validatePreservedManualMemoization
- validateSourceLocations (added env parameter)
- validateContextVariableLValues (changed throwTodo to recordError)
- validateLocalsNotReassignedAfterRender (changed throw to recordError)
- validateNoDerivedComputationsInEffects (changed throw to recordError)

Update inference passes:
- inferMutationAliasingEffects: return void, errors on env
- inferMutationAliasingRanges: return Array<AliasingEffect> directly,
errors on env

Update codegen:
- codegenFunction: return CodegenFunction directly, errors on env
- codegenReactiveFunction: same pattern

Update Pipeline.ts to call all passes directly without tryRecord/unwrap.
Also update AnalyseFunctions.ts which called
inferMutationAliasingRanges.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35876).
* #35888
* #35884
* #35883
* #35882
* #35881
* #35880
* #35879
* #35878
* #35877
* __->__ #35876
2026-02-23 16:01:02 -08:00
Joseph Savona
e3e5d95cc4 [compiler] Phase 4 (batch 1): Update validation passes to record errors on env (#35875)
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().

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35875).
* #35888
* #35884
* #35883
* #35882
* #35881
* #35880
* #35879
* #35878
* #35877
* #35876
* __->__ #35875
2026-02-23 15:35:52 -08:00
Joseph Savona
426a394845 [compiler] Phase 2+7: Wrap pipeline passes in tryRecord for fault tolerance (#35874)
- 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.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35874).
* #35888
* #35884
* #35883
* #35882
* #35881
* #35880
* #35879
* #35878
* #35877
* #35876
* #35875
* __->__ #35874
2026-02-23 15:26:28 -08:00
Joseph Savona
eca778cf8b [compiler] Phase 1: Add error accumulation infrastructure to Environment (#35873)
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

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35873).
* #35888
* #35884
* #35883
* #35882
* #35881
* #35880
* #35879
* #35878
* #35877
* #35876
* #35875
* #35874
* __->__ #35873
2026-02-23 15:18:23 -08:00
Joseph Savona
0dbb43bc57 [compiler] Add fault tolerance plan document (#35872)
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.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35872).
* #35888
* #35884
* #35883
* #35882
* #35881
* #35880
* #35879
* #35878
* #35877
* #35876
* #35875
* #35874
* #35873
* __->__ #35872
2026-02-23 15:15:29 -08:00
Joseph Savona
8b6b11f703 [compiler] Remove fallback compilation pipeline dead code (#35827)
Remove dead code left behind after the removal of retryCompileFunction,
enableFire, and inferEffectDependencies:
- Delete ValidateNoUntransformedReferences.ts (always a no-op)
- Remove CompileProgramMetadata type and retryErrors from ProgramContext
- Remove 'client-no-memo' output mode
- Change compileProgram return type from CompileProgramMetadata | null
to void
2026-02-23 08:54:49 -08:00
Andrew Clark
ab18f33d46 Fix context propagation through suspended Suspense boundaries (#35839)
When a Suspense boundary suspends during initial mount, the primary
children's fibers are discarded because there is no current tree to
preserve them. If the suspended promise never resolves, the only way to
retry is something external like a context change. However, lazy context
propagation could not find the consumer fibers — they no longer exist in
the tree — so the Suspense boundary was never marked for retry and
remained stuck in fallback state indefinitely.

The fix teaches context propagation to conservatively mark suspended
Suspense boundaries for retry when a parent context changes, even when
the consumer fibers can't be found. This matches the existing
conservative approach used for dehydrated (SSR) Suspense boundaries.
2026-02-20 22:03:11 -05:00
Joseph Savona
b16b768fbd [compiler] Feature flag cleanup (#35825)
Cleans up feature flags that do not have an active experiment and which
we don't currently plan to ship, one commit per flag. Notable removals:
* Automatic (inferred) effect dependencies / Fire: abandoned due to
early feedback. Shipped useEffectEvent which addresses some of the
use-cases.
* Inline JSX transform (experimented, not a consistent win)
* Context selectors (experimented, not a sufficient/consistent win given
the benefit the compiler already provides)
* Instruction Reordering (will try a different approach)

To decide which features to remove, I looked at Meta's internal repos as
well as eslint-pugin-react-hooks to see which flags were never
overridden anywhere. That gave a longer list of flags, from which I then
removed some features that I know are used in OSS.
2026-02-20 12:29:12 -08:00
Sebastian "Sebbie" Silbermann
2ba3065527 [Flight] Add support for transporting Error.cause (#35810) 2026-02-19 15:50:34 -08:00
Josh Story
38cd020c1f Don't outline Suspense boundaries with suspensey CSS during shell flush (#35824)
When flushing the shell, stylesheets with precedence are emitted in the
`<head>` which blocks paint regardless. Outlining a boundary solely
because it has suspensey CSS provides no benefit during the shell flush
and causes a higher-level fallback to be shown unnecessarily (e.g.
"Middle Fallback" instead of "Inner Fallback").

This change passes a flushingInShell flag to hasSuspenseyContent so the
host config can skip stylesheet-only suspensey content when flushing the
shell. Suspensey images (used for ViewTransition animation reveals)
still trigger outlining during the shell since their motivation is
different.

When flushing streamed completions the behavior is unchanged — suspensey
CSS still causes outlining so the parent content can display sooner
while the stylesheet loads.
2026-02-19 12:29:21 -08:00
Tim Neutkens
f247ebaf44 [Flight] Walk parsed JSON instead of using reviver for parsing RSC payload (#35776)
## Summary

Follow-up to https://github.com/vercel/next.js/pull/89823 with the
actual changes to React.

Replaces the `JSON.parse` reviver callback in `initializeModelChunk`
with a two-step approach: plain `JSON.parse()` followed by a recursive
`reviveModel()` post-process (same as in Flight Reply Server). This
yields a **~75% speedup** in RSC chunk deserialization.

| Payload | Original (ms) | Walk (ms) | Speedup |
|---------|---------------|-----------|---------|
| Small (2 elements, 142B) | 0.0024 | 0.0007 | **+72%** |
| Medium (~12 elements, 914B) | 0.0116 | 0.0031 | **+73%** |
| Large (~90 elements, 16.7KB) | 0.1836 | 0.0451 | **+75%** |
| XL (~200 elements, 25.7KB) | 0.3742 | 0.0913 | **+76%** |
| Table (1000 rows, 110KB) | 3.0862 | 0.6887 | **+78%** |

## Problem

`createFromJSONCallback` returns a reviver function passed as the second
argument to `JSON.parse()`. This reviver is called for **every key-value
pair** in the parsed JSON. While the logic inside the reviver is
lightweight, the dominant cost is the **C++ → JavaScript boundary
crossing** — V8's `JSON.parse` is implemented in C++, and calling back
into JavaScript for every node incurs significant overhead.

Even a trivial no-op reviver `(k, v) => v` makes `JSON.parse` **~4x
slower** than bare `JSON.parse` without a reviver:

```
108 KB payload:
  Bare JSON.parse:    0.60 ms
  Trivial reviver:    2.95 ms  (+391%)
```

## Change

Replace the reviver with a two-step process:

1. `JSON.parse(resolvedModel)` — parse the entire payload in C++ with no
callbacks
2. `reviveModel` — recursively walk the resulting object in pure
JavaScript to apply RSC transformations

The `reviveModel` function includes additional optimizations over the
original reviver:
- **Short-circuits plain strings**: only calls `parseModelString` when
the string starts with `$`, skipping the vast majority of strings (class
names, text content, etc.)
- **Stays entirely in JavaScript** — no C++ boundary crossings during
the walk

## Results

You can find the related applications in the [Next.js PR
](https://github.com/vercel/next.js/pull/89823)as I've been testing this
on Next.js applications.

### Table as Server Component with 1000 items

Before:

```
    "min": 13.782875000000786,
    "max": 22.23400000000038,
    "avg": 17.116868530000083,
    "p50": 17.10766700000022,
    "p75": 18.50787499999933,
    "p95": 20.426249999998618,
    "p99": 21.814125000000786
```

After:

```
    "min": 10.963916999999128,
    "max": 18.096083000000363,
    "avg": 13.543286884999988,
    "p50": 13.58350000000064,
    "p75": 14.871791999999914,
    "p95": 16.08429099999921,
    "p99": 17.591458000000785
```

### Table as Client Component with 1000 items

Before:

```
    "min": 3.888875000000553,
    "max": 9.044959000000745,
    "avg": 4.651271475000067,
    "p50": 4.555749999999534,
    "p75": 4.966624999999112,
    "p95": 5.47754200000054,
    "p99": 6.109499999998661
````

After:

```
    "min": 3.5986250000005384,
    "max": 5.374291000000085,
    "avg": 4.142990245000046,
    "p50": 4.10570799999914,
    "p75": 4.392041999999492,
    "p95": 4.740084000000934,
    "p99": 5.1652500000000146
```

### Nested Suspense

Before:

```
  Requests:  200
  Min:       73ms
  Max:       106ms
  Avg:       78ms
  P50:       77ms
  P75:       80ms
  P95:       85ms
  P99:       94ms
```

After:

```
  Requests:  200
  Min:       56ms
  Max:       67ms
  Avg:       59ms
  P50:       58ms
  P75:       60ms
  P95:       65ms
  P99:       66ms
```

### Even more nested Suspense (double-level Suspense)

Before:

```
  Requests:  200
  Min:       159ms
  Max:       208ms
  Avg:       169ms
  P50:       167ms
  P75:       173ms
  P95:       183ms
  P99:       188ms
```

After:

```
  Requests:  200
  Min:       125ms
  Max:       170ms
  Avg:       134ms
  P50:       132ms
  P75:       138ms
  P95:       148ms
  P99:       160ms
```

## How did you test this change?

Ran it across many Next.js benchmark applications.

The entire Next.js test suite passes with this change.

---------

Co-authored-by: Hendrik Liebau <mail@hendrik-liebau.de>
2026-02-19 08:37:41 -08:00
Sebastian "Sebbie" Silbermann
3a2bee26d2 [DevTools] Fix alignment of breadcrumbs separator (#35817) 2026-02-18 10:53:49 -08:00
chirokas
4842fbea02 [react-dom] Add support for onFullscreenChange and onFullscreenError events (#34621) 2026-02-17 17:34:16 -08:00
Nick Gerleman
61db53c179 [Native] Add RCTSelectableText as a recognized Text component (#35780)
## Summary

Add "RCTSelectableText" to the list of component names recognized as
being inside a text element, alongside "RCTText".

React Native's new text stack, tries to optimize and allows
differentiating between a custom TextView, with lower level control,
that can reuse the work performed during Fabric/Yoga layout, and a
native TextView, used for fidelity. On Android at least, the only place
we've needed native TextView for fidelity/native UX has been support for
`selectable` text, which has many unique UI interactions.

## How did you test this change?

When I patch this in, alongside
https://github.com/facebook/react-native/pull/55552, we no longer see
warnings when we render text inside of RCTSelectableText component.

---------

Co-authored-by: Eli White <github@eli-white.com>
2026-02-17 16:16:06 -08:00
Joseph Savona
4ac47537dd [compiler] Track locations for dependencies (#35794)
Tracks locations for reactive scope dependencies, both on the deps and
portions of the path. The immediate need for this is a non-public
experiment where we're exploring type-directed compilation, and
sometimes look up the types of expressions by location. We need to
preserve locations accurately for that to work, including the locations
of the deps.

## Test Plan

Locations for dependencies are not easy to test: i manually spot-checked
the new fixture to ensure that the deps look right. This is fine as
best-effort since it doesn't impact any of our core compilation logic, i
may fix forward if there are issues and will think about how to test.
2026-02-17 14:06:21 -08:00
Hendrik Liebau
47d1ad1454 [Flight] Skip transferReferencedDebugInfo during debug info resolution (#35795)
When the Flight Client resolves chunk references during model parsing,
it calls `transferReferencedDebugInfo` to propagate debug info entries
from referenced chunks to the parent chunk. Debug info on chunks is
later moved to their resolved values, where it is used by React DevTools
to show performance tracks and what a component was suspended by.

Debug chunks themselves (specifically `ReactComponentInfo`,
`ReactAsyncInfo`, `ReactIOInfo`, and their outlined references) are
metadata that is never rendered. They don't need debug info attached to
them. Without this fix, debug info entries accumulate on outlined debug
chunks via their references to other debug chunks (e.g. owner chains and
props deduplication paths). Since each outlined chunk's accumulated
entries are copied to every chunk that references it, this creates
exponential growth in deep component trees, which can cause the dev
server to hang and run out of memory.

This generalizes the existing skip of `transferReferencedDebugInfo` for
Element owner/stack references (which already recognizes that references
to debug chunks don't need debug info transferred) to all references
resolved during debug info resolution. It adds an
`isInitializingDebugInfo` flag set in `initializeDebugChunk` and
`resolveIOInfo`, which propagates through all nested
`initializeModelChunk` calls within the same synchronous stack. For the
async path, `waitForReference` captures the flag at call time into
`InitializationReference.isDebug`, so deferred fulfillments also skip
the transfer.
2026-02-16 09:22:32 -08:00
408 changed files with 3479 additions and 16681 deletions

View File

@@ -35,6 +35,20 @@ yarn snap -p <file-basename> -d
yarn snap -u
```
## Linting
```bash
# Run lint on the compiler source
yarn workspace babel-plugin-react-compiler lint
```
## Formatting
```bash
# Run prettier on all files (from the react root directory, not compiler/)
yarn prettier-all
```
## Compiling Arbitrary Files
Use `yarn snap compile` to compile any file (not just fixtures) with the React Compiler:
@@ -215,12 +229,12 @@ const UseEffectEventHook = addHook(
Feature flags are configured in `src/HIR/Environment.ts`, for example `enableJsxOutlining`. Test fixtures can override the active feature flags used for that fixture via a comment pragma on the first line of the fixture input, for example:
```javascript
// enableJsxOutlining @enableChangeVariableCodegen:false
// enableJsxOutlining @enableNameAnonymousFunctions:false
...code...
```
Would enable the `enableJsxOutlining` feature and disable the `enableChangeVariableCodegen` feature.
Would enable the `enableJsxOutlining` feature and disable the `enableNameAnonymousFunctions` feature.
## Debugging Tips
@@ -229,20 +243,19 @@ Would enable the `enableJsxOutlining` feature and disable the `enableChangeVaria
3. Look for `Impure`, `Render`, `Capture` effects on instructions
4. Check the pass ordering in Pipeline.ts to understand when effects are populated vs validated
## Error Handling for Unsupported Features
## Error Handling and Fault Tolerance
When the compiler encounters an unsupported but known pattern, use `CompilerError.throwTodo()` instead of `CompilerError.invariant()`. Todo errors cause graceful bailouts in production; Invariant errors are hard failures indicating unexpected/invalid states.
The compiler is fault-tolerant: it runs all passes and accumulates errors on the `Environment` rather than throwing on the first error. This lets users see all compilation errors at once.
```typescript
// Unsupported but expected pattern - graceful bailout
CompilerError.throwTodo({
reason: `Support [description of unsupported feature]`,
loc: terminal.loc,
});
**Recording errors** — Passes record errors via `env.recordError(diagnostic)`. Errors are accumulated on `Environment.#errors` and checked at the end of the pipeline via `env.hasErrors()` / `env.aggregateErrors()`.
// Invariant is for truly unexpected/invalid states - hard failure
CompilerError.invariant(false, {
reason: `Unexpected [thing]`,
loc: terminal.loc,
});
```
**`tryRecord()` wrapper** — In Pipeline.ts, validation passes are wrapped in `env.tryRecord(() => pass(hir))` which catches thrown `CompilerError`s (non-invariant) and records them. Infrastructure/transformation passes are NOT wrapped in `tryRecord()` because later passes depend on their output being structurally valid.
**Error categories:**
- `CompilerError.throwTodo()` — Unsupported but known pattern. Graceful bailout. Can be caught by `tryRecord()`.
- `CompilerError.invariant()` — Truly unexpected/invalid state. Always throws immediately, never caught by `tryRecord()`.
- Non-`CompilerError` exceptions — Always re-thrown.
**Key files:** `Environment.ts` (`recordError`, `tryRecord`, `hasErrors`, `aggregateErrors`), `Pipeline.ts` (pass orchestration), `Program.ts` (`tryCompileFunction` handles the `Result`).
**Test fixtures:** `__tests__/fixtures/compiler/fault-tolerance/` contains multi-error fixtures verifying all errors are reported.

View File

@@ -1,14 +0,0 @@
import { c as _c } from "react/compiler-runtime";
export default function TestComponent(t0) {
const $ = _c(2);
const { x } = t0;
let t1;
if ($[0] !== x || true) {
t1 = <Button>{x}</Button>;
$[0] = x;
$[1] = t1;
} else {
t1 = $[1];
}
return t1;
}

View File

@@ -283,37 +283,6 @@ test('error is displayed when config has validation error', async ({page}) => {
expect(output.replace(/\s+/g, ' ')).toContain('Unexpected compilationMode');
});
test('disableMemoizationForDebugging flag works as expected', async ({
page,
}) => {
const store: Store = {
source: TEST_SOURCE,
config: `import type { PluginOptions } from 'babel-plugin-react-compiler/dist';
({
environment: {
disableMemoizationForDebugging: true
}
} satisfies PluginOptions);`,
showInternals: false,
};
const hash = encodeStore(store);
await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
await page.waitForFunction(isMonacoLoaded);
await expandConfigs(page);
await page.screenshot({
fullPage: true,
path: 'test-results/07-config-disableMemoizationForDebugging-flag.png',
});
const text =
(await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
const output = await formatPrint(text);
expect(output).not.toEqual('');
expect(output).toMatchSnapshot('disableMemoizationForDebugging-output.txt');
});
test('error is displayed when source has syntax error', async ({page}) => {
const syntaxErrorSource = `function TestComponent(props) {
const oops = props.

View File

@@ -70,9 +70,6 @@ The `occursCheck` method prevents infinite types by detecting when a type variab
- `DeclareContext` and `LoadContext` generate no type equations (intentionally untyped)
- `StoreContext` with `Const` kind does propagate the rvalue type to enable ref inference through context variables
### Event Handler Inference
When `enableInferEventHandlers` is enabled, JSX props starting with "on" (e.g., `onClick`) on built-in DOM elements (excluding web components with hyphens) are inferred as `Function<BuiltInEventHandlerId>`.
## TODOs
1. **Hook vs Function type ambiguity**:
> "TODO: callee could be a hook or a function, so this type equation isn't correct. We should change Hook to a subtype of Function or change unifier logic."

View File

@@ -205,8 +205,6 @@ if ($[0] !== "source_hash_abc123") {
}
```
### Change Detection for Debugging
When `enableChangeDetectionForDebugging` is configured, additional code is generated to detect when cached values unexpectedly change.
### Labeled Breaks
Control flow with labeled breaks (for early returns or loop exits) uses `codegenLabel` to generate consistent label names:
@@ -231,7 +229,6 @@ type CodegenFunction = {
prunedMemoBlocks: number; // Scopes that were pruned
prunedMemoValues: number; // Values in pruned scopes
hasInferredEffect: boolean;
hasFireRewrite: boolean;
};
```

View File

@@ -1,203 +0,0 @@
# transformFire
## File
`src/Transform/TransformFire.ts`
## Purpose
This pass transforms `fire(fn())` calls inside `useEffect` lambdas into calls to a `useFire` hook that provides stable function references. The `fire()` function is a React API that allows effect callbacks to call functions with their current values while maintaining stable effect dependencies.
Without this transform, if an effect depends on a function that changes every render, the effect would re-run on every render. The `useFire` hook provides a stable wrapper that always calls the latest version of the function.
## Input Invariants
- The `enableFire` feature flag must be enabled
- `fire()` calls must only appear inside `useEffect` lambdas
- Each `fire()` call must have exactly one argument (a function call expression)
- The function being fired must be consistent across all `fire()` calls in the same effect
## Output Guarantees
- All `fire(fn(...args))` calls are replaced with direct calls `fired_fn(...args)`
- A `useFire(fn)` hook call is inserted before the `useEffect`
- The fired function is stored in a temporary and captured by the effect
- The original function `fn` is removed from the effect's captured context
## Algorithm
### Phase 1: Find Fire Calls
```typescript
function replaceFireFunctions(fn: HIRFunction, context: Context): void {
// For each useEffect call instruction:
// 1. Find all fire() calls in the effect lambda
// 2. Validate they have proper arguments
// 3. Track which functions are being fired
for (const [, block] of fn.body.blocks) {
for (const instr of block.instructions) {
if (isUseEffectCall(instr)) {
const lambda = getEffectLambda(instr);
findAndReplaceFireCalls(lambda, fireFunctions);
}
}
}
}
```
### Phase 2: Insert useFire Hooks
For each function being fired, insert a `useFire` call:
```typescript
// Before:
useEffect(() => {
fire(foo(props));
}, [foo, props]);
// After:
const t0 = useFire(foo);
useEffect(() => {
t0(props);
}, [t0, props]);
```
### Phase 3: Replace Fire Calls
Transform `fire(fn(...args))` to `firedFn(...args)`:
```typescript
// The fire() wrapper is removed
// The inner function call uses the useFire'd version
fire(foo(x, y)) t0(x, y) // where t0 = useFire(foo)
```
### Phase 4: Validate No Remaining Fire Uses
```typescript
function ensureNoMoreFireUses(fn: HIRFunction, context: Context): void {
// Ensure all fire() uses have been transformed
// Report errors for any remaining fire() calls
}
```
## Edge Cases
### Fire Outside Effect
`fire()` calls outside `useEffect` lambdas cause a validation error:
```javascript
// ERROR: fire() can only be used inside useEffect
function Component() {
fire(callback());
}
```
### Mixed Fire and Non-Fire Calls
All calls to the same function must either all use `fire()` or none:
```javascript
// ERROR: Cannot mix fire() and non-fire calls
useEffect(() => {
fire(foo(x));
foo(y); // Error: foo is used with and without fire()
});
```
### Multiple Arguments to Fire
`fire()` accepts exactly one argument (the function call):
```javascript
// ERROR: fire() takes exactly one argument
fire(foo, bar) // Invalid
fire() // Invalid
```
### Nested Effects
Fire calls in nested effects are validated separately:
```javascript
useEffect(() => {
useEffect(() => { // Error: nested effects not allowed
fire(foo());
});
});
```
### Deep Scope Handling
The pass handles fire calls within deeply nested scopes inside effects:
```javascript
useEffect(() => {
if (cond) {
while (x) {
fire(foo(x)); // Still transformed correctly
}
}
});
```
## TODOs
None in the source file.
## Example
### Fixture: `transform-fire/basic.js`
**Input:**
```javascript
// @enableFire
function Component(props) {
const foo = (props_0) => {
console.log(props_0);
};
useEffect(() => {
fire(foo(props));
});
return null;
}
```
**After TransformFire:**
```
bb0 (block):
[1] $25 = Function @context[] ... // foo definition
[2] StoreLocal Const foo$32 = $25
[3] $45 = LoadGlobal import { useFire } from 'react/compiler-runtime'
[4] $46 = LoadLocal foo$32
[5] $47 = Call $45($46) // useFire(foo)
[6] StoreLocal Const #t44$44 = $47
[7] $34 = LoadGlobal(global) useEffect
[8] $35 = Function @context[#t44$44, props$24] ...
<<anonymous>>():
[1] $37 = LoadLocal #t44$44 // Load the fired function
[2] $38 = LoadLocal props$24
[3] $39 = Call $37($38) // Call it directly (no fire wrapper)
[4] Return Void
[9] Call $34($35) // useEffect(lambda)
[10] Return null
```
**Generated Code:**
```javascript
import { useFire as _useFire } from "react/compiler-runtime";
function Component(props) {
const $ = _c(4);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = (props_0) => {
console.log(props_0);
};
$[0] = t0;
} else {
t0 = $[0];
}
const foo = t0;
const t1 = _useFire(foo);
let t2;
if ($[1] !== props || $[2] !== t1) {
t2 = () => {
t1(props);
};
$[1] = props;
$[2] = t1;
$[3] = t2;
} else {
t2 = $[3];
}
useEffect(t2);
return null;
}
```
Key observations:
- `useFire` is imported from `react/compiler-runtime`
- `fire(foo(props))` becomes `t1(props)` where `t1 = _useFire(foo)`
- The effect now depends on `t1` (stable) and `props` (reactive)
- The original `foo` function is memoized and passed to `useFire`

View File

@@ -1,174 +0,0 @@
# lowerContextAccess
## File
`src/Optimization/LowerContextAccess.ts`
## Purpose
This pass optimizes `useContext` calls by generating selector functions that extract only the needed properties from the context. Instead of subscribing to the entire context object, components can subscribe to specific slices, enabling more granular re-rendering.
When a component destructures specific properties from a context, this pass transforms the `useContext` call to use a selector-based API that only triggers re-renders when the selected properties change.
## Input Invariants
- The `lowerContextAccess` configuration must be set with:
- `source`: The module to import the lowered context hook from
- `importSpecifierName`: The name of the hook function
- The function must use `useContext` with destructuring patterns
- Only object destructuring patterns with identifier values are supported
## Output Guarantees
- `useContext(Ctx)` calls with destructuring are replaced with selector calls
- A selector function is generated that extracts the needed properties
- The return type is changed from object to array for positional access
- Unused original `useContext` calls are removed by dead code elimination
## Algorithm
### Phase 1: Collect Context Access Patterns
```typescript
function lowerContextAccess(fn: HIRFunction, config: ExternalFunction): void {
const contextAccess: Map<IdentifierId, CallExpression> = new Map();
const contextKeys: Map<IdentifierId, Array<string>> = new Map();
for (const [, block] of fn.body.blocks) {
for (const instr of block.instructions) {
// Find useContext calls
if (isUseContextCall(instr)) {
contextAccess.set(instr.lvalue.identifier.id, instr.value);
}
// Find destructuring patterns that access context results
if (isDestructure(instr) && contextAccess.has(instr.value.value.id)) {
const keys = extractPropertyKeys(instr.value.pattern);
contextKeys.set(instr.value.value.id, keys);
}
}
}
}
```
### Phase 2: Generate Selector Functions
For each context access with known keys:
```typescript
// Original:
const {foo, bar} = useContext(MyContext);
// Selector function generated:
(ctx) => [ctx.foo, ctx.bar]
```
### Phase 3: Transform Context Calls
```typescript
// Before:
$0 = useContext(MyContext)
{foo, bar} = $0
// After:
$0 = useContext_withSelector(MyContext, (ctx) => [ctx.foo, ctx.bar])
[foo, bar] = $0
```
### Phase 4: Update Destructuring
Change object destructuring to array destructuring to match selector return:
```typescript
// Before: { foo: foo$15, bar: bar$16 } = $14
// After: [ foo$15, bar$16 ] = $14
```
## Edge Cases
### Dynamic Property Access
If context properties are accessed dynamically (not through destructuring), the optimization is skipped:
```javascript
const ctx = useContext(MyContext);
const x = ctx[dynamicKey]; // Cannot optimize
```
### Spread in Destructuring
Spread patterns prevent optimization:
```javascript
const {foo, ...rest} = useContext(MyContext); // Cannot optimize
```
### Non-Identifier Values
Only simple identifier destructuring is supported:
```javascript
const {foo: bar} = useContext(MyContext); // Supported (rename)
const {foo = defaultVal} = useContext(MyContext); // Not supported
```
### Multiple Context Accesses
Each `useContext` call is transformed independently:
```javascript
const {a} = useContext(CtxA); // Transformed
const {b} = useContext(CtxB); // Transformed separately
```
### Hook Guards
When `enableEmitHookGuards` is enabled, the selector function includes proper hook guard annotations.
## TODOs
None in the source file.
## Example
### Fixture: `lower-context-selector-simple.js`
**Input:**
```javascript
// @lowerContextAccess
function App() {
const {foo, bar} = useContext(MyContext);
return <Bar foo={foo} bar={bar} />;
}
```
**After OptimizePropsMethodCalls (where lowering happens):**
```
bb0 (block):
[1] $12 = LoadGlobal(global) useContext // Original (now unused)
[2] $13 = LoadGlobal(global) MyContext
[3] $22 = LoadGlobal import { useContext_withSelector } from 'react-compiler-runtime'
[4] $36 = Function @context[]
<<anonymous>>(#t23$30):
[1] $31 = LoadLocal #t23$30
[2] $32 = PropertyLoad $31.foo
[3] $33 = LoadLocal #t23$30
[4] $34 = PropertyLoad $33.bar
[5] $35 = Array [$32, $34] // Return [foo, bar]
[6] Return $35
[5] $14 = Call $22($13, $36) // useContext_withSelector(MyContext, selector)
[6] $17 = Destructure Const { foo: foo$15, bar: bar$16 } = $14
...
```
**Generated Code:**
```javascript
import { c as _c } from "react/compiler-runtime";
import { useContext_withSelector } from "react-compiler-runtime";
function App() {
const $ = _c(2);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = (ctx) => [ctx.foo, ctx.bar];
$[0] = t0;
} else {
t0 = $[0];
}
const { foo, bar } = useContext_withSelector(MyContext, t0);
let t1;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
t1 = <Bar foo={foo} bar={bar} />;
$[1] = t1;
} else {
t1 = $[1];
}
return t1;
}
```
Key observations:
- `useContext` is replaced with `useContext_withSelector`
- A selector function `(ctx) => [ctx.foo, ctx.bar]` is generated
- The selector function is memoized (first cache slot)
- Only `foo` and `bar` properties are extracted, enabling granular subscriptions
- The selector return type changes from object to array

View File

@@ -49,13 +49,8 @@ const ALLOW_LIST = new Set([
...(envConfig.validateNoCapitalizedCalls ?? []), // User-configured allowlist
]);
const hookPattern = envConfig.hookPattern != null
? new RegExp(envConfig.hookPattern)
: null;
const isAllowed = (name: string): boolean => {
return ALLOW_LIST.has(name) ||
(hookPattern != null && hookPattern.test(name));
return ALLOW_LIST.has(name);
};
```
@@ -137,13 +132,6 @@ Users can allowlist specific functions via configuration:
validateNoCapitalizedCalls: ['MyUtility', 'SomeFactory']
```
### Hook Patterns
Functions matching the configured hook pattern are allowed even if capitalized:
```typescript
// With hookPattern: 'React\\$use.*'
const x = React$useState(); // Allowed if it matches the hook pattern
```
### Method Calls vs Function Calls
Both direct function calls and method calls on objects are checked:
```javascript

View File

@@ -1,93 +0,0 @@
# validateMemoizedEffectDependencies
## File
`src/Validation/ValidateMemoizedEffectDependencies.ts`
## Purpose
Validates that all known effect dependencies (for `useEffect`, `useLayoutEffect`, and `useInsertionEffect`) are properly memoized. This prevents a common bug where unmemoized effect dependencies can cause infinite re-render loops or other unexpected behavior.
## Input Invariants
- Operates on ReactiveFunction (post-reactive scope inference)
- Reactive scopes have been assigned to values that need memoization
- Must run after scope inference but before codegen
## Validation Rules
This pass checks two conditions:
1. **Unmemoized dependencies with assigned scopes**: Disallows effect dependencies that should be memoized (have a reactive scope assigned) but where that reactive scope does not exist in the output. This catches cases where a reactive scope was pruned, such as when it spans a hook call.
2. **Mutable dependencies at effect call site**: Disallows effect dependencies whose mutable range encompasses the effect call. This catches values that the compiler knows may be mutated after the effect is set up.
When either condition is violated, the pass produces:
```
Compilation Skipped: React Compiler has skipped optimizing this component because
the effect dependencies could not be memoized. Unmemoized effect dependencies can
trigger an infinite loop or other unexpected behavior
```
## Algorithm
1. Traverse the reactive function using a visitor pattern
2. Track all scopes that exist in the AST by adding them to a `Set<ScopeId>` during `visitScope`
3. Only record a scope if its dependencies are also memoized (transitive memoization check)
4. When visiting an instruction that is an effect hook call (`useEffect`, `useLayoutEffect`, `useInsertionEffect`) with at least 2 arguments (function + deps array):
- Check if the dependency array is mutable at the call site using `isMutable()`
- Check if the dependency array's scope exists using `isUnmemoized()`
- If either check fails, push an error
### Key Helper Functions
**isEffectHook(identifier)**: Returns true if the identifier is `useEffect`, `useLayoutEffect`, or `useInsertionEffect`.
**isUnmemoized(operand, scopes)**: Returns true if the operand has a scope assigned (`operand.scope != null`) but that scope doesn't exist in the set of valid scopes.
## Edge Cases
- Only validates effects with 2+ arguments (ignores effects without dependency arrays)
- Transitive memoization: A scope is only considered valid if all its dependencies are also memoized
- Merged scopes are tracked together with their primary scope
## TODOs
From the source code:
```typescript
// TODO: isMutable is not safe to call here as it relies on identifier mutableRange
// which is no longer valid at this point in the pipeline
```
## Example
### Fixture: `error.invalid-useEffect-dep-not-memoized.js`
**Input:**
```javascript
// @validateMemoizedEffectDependencies
import {useEffect} from 'react';
function Component(props) {
const data = {};
useEffect(() => {
console.log(props.value);
}, [data]);
mutate(data);
return data;
}
```
**Error:**
```
Found 1 error:
Compilation Skipped: React Compiler has skipped optimizing this component because
the effect dependencies could not be memoized. Unmemoized effect dependencies can
trigger an infinite loop or other unexpected behavior
error.invalid-useEffect-dep-not-memoized.ts:6:2
4 | function Component(props) {
5 | const data = {};
> 6 | useEffect(() => {
| ^^^^^^^^^^^^^^^^^
> 7 | console.log(props.value);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 8 | }, [data]);
| ^^^^^^^^^^^^^
```
**Why it fails:** The `data` object is mutated after the `useEffect` call, which extends its mutable range past the effect. This means `data` cannot be safely memoized as an effect dependency because it might change after the effect is set up.

View File

@@ -25,7 +25,7 @@ This directory contains detailed documentation for each pass in the React Compil
┌─────────────────────────────────────────────────────────────────────────────────────┐
│ PHASE 2: OPTIMIZATION │
│ │
│ constantPropagation ──▶ deadCodeElimination ──▶ instructionReordering
│ constantPropagation ──▶ deadCodeElimination
│ │
└─────────────────────────────────────────────────────────────────────────────────────┘
@@ -195,8 +195,6 @@ This directory contains detailed documentation for each pass in the React Compil
| # | Pass | File | Description |
|---|------|------|-------------|
| 32 | [transformFire](32-transformFire.md) | `Transform/TransformFire.ts` | Transform `fire()` calls in effects |
| 33 | [lowerContextAccess](33-lowerContextAccess.md) | `Optimization/LowerContextAccess.ts` | Optimize context access with selectors |
| 34 | [optimizePropsMethodCalls](34-optimizePropsMethodCalls.md) | `Optimization/OptimizePropsMethodCalls.ts` | Normalize props method calls |
| 35 | [optimizeForSSR](35-optimizeForSSR.md) | `Optimization/OptimizeForSSR.ts` | SSR-specific optimizations |
| 36 | [outlineJSX](36-outlineJSX.md) | `Optimization/OutlineJsx.ts` | Outline JSX to components |
@@ -220,7 +218,6 @@ This directory contains detailed documentation for each pass in the React Compil
| 49 | [validateNoRefAccessInRender](49-validateNoRefAccessInRender.md) | `Validation/ValidateNoRefAccessInRender.ts` | Ref access constraints |
| 50 | [validateNoFreezingKnownMutableFunctions](50-validateNoFreezingKnownMutableFunctions.md) | `Validation/ValidateNoFreezingKnownMutableFunctions.ts` | Mutable function isolation |
| 51 | [validateExhaustiveDependencies](51-validateExhaustiveDependencies.md) | `Validation/ValidateExhaustiveDependencies.ts` | Dependency array completeness |
| 52 | [validateMemoizedEffectDependencies](52-validateMemoizedEffectDependencies.md) | `Validation/ValidateMemoizedEffectDependencies.ts` | Effect scope memoization |
| 53 | [validatePreservedManualMemoization](53-validatePreservedManualMemoization.md) | `Validation/ValidatePreservedManualMemoization.ts` | Manual memo preservation |
| 54 | [validateStaticComponents](54-validateStaticComponents.md) | `Validation/ValidateStaticComponents.ts` | Component identity stability |
| 55 | [validateSourceLocations](55-validateSourceLocations.md) | `Validation/ValidateSourceLocations.ts` | Source location preservation |
@@ -275,8 +272,6 @@ Many passes are controlled by feature flags in `Environment.ts`:
| Flag | Enables Pass |
|------|--------------|
| `enableFire` | transformFire |
| `lowerContextAccess` | lowerContextAccess |
| `enableJsxOutlining` | outlineJSX |
| `enableFunctionOutlining` | outlineFunctions |
| `validateNoSetStateInRender` | validateNoSetStateInRender |
@@ -307,6 +302,15 @@ yarn snap minimize <path>
yarn snap -u
```
## Fault Tolerance
The pipeline is fault-tolerant: all passes run to completion, accumulating errors on `Environment` rather than aborting on the first error.
- **Validation passes** are wrapped in `env.tryRecord()` in Pipeline.ts, which catches non-invariant `CompilerError`s and records them. If a validation pass throws, compilation continues.
- **Infrastructure/transformation passes** (enterSSA, eliminateRedundantPhi, inferMutationAliasingEffects, codegen, etc.) are NOT wrapped in `tryRecord()` because subsequent passes depend on their output being structurally valid. If they fail, compilation aborts.
- **`lower()` (BuildHIR)** always produces an `HIRFunction`, recording errors on `env` instead of returning `Err`. Unsupported constructs (e.g., `var`) are lowered best-effort.
- At the end of the pipeline, `env.hasErrors()` determines whether to return `Ok(codegen)` or `Err(aggregatedErrors)`.
## Further Reading
- [MUTABILITY_ALIASING_MODEL.md](../../src/Inference/MUTABILITY_ALIASING_MODEL.md): Detailed aliasing model docs

View File

@@ -11,7 +11,6 @@ import {
injectReanimatedFlag,
pipelineUsesReanimatedPlugin,
} from '../Entrypoint/Reanimated';
import validateNoUntransformedReferences from '../Entrypoint/ValidateNoUntransformedReferences';
import {CompilerError} from '..';
const ENABLE_REACT_COMPILER_TIMINGS =
@@ -64,19 +63,12 @@ export default function BabelPluginReactCompiler(
},
};
}
const result = compileProgram(prog, {
compileProgram(prog, {
opts,
filename: pass.filename ?? null,
comments: pass.file.ast.comments ?? [],
code: pass.file.code,
});
validateNoUntransformedReferences(
prog,
pass.filename ?? null,
opts.logger,
opts.environment,
result,
);
if (ENABLE_REACT_COMPILER_TIMINGS === true) {
performance.mark(`${filename}:end`, {
detail: 'BabelPlugin:Program:end',

View File

@@ -565,15 +565,12 @@ function printCodeFrame(
function printErrorSummary(category: ErrorCategory, message: string): string {
let heading: string;
switch (category) {
case ErrorCategory.AutomaticEffectDependencies:
case ErrorCategory.CapitalizedCalls:
case ErrorCategory.Config:
case ErrorCategory.EffectDerivationsOfState:
case ErrorCategory.EffectSetState:
case ErrorCategory.ErrorBoundaries:
case ErrorCategory.Factories:
case ErrorCategory.FBT:
case ErrorCategory.Fire:
case ErrorCategory.Gating:
case ErrorCategory.Globals:
case ErrorCategory.Hooks:
@@ -637,10 +634,6 @@ export enum ErrorCategory {
* Checking that useMemos always return a value
*/
VoidUseMemo = 'VoidUseMemo',
/**
* Checking for higher order functions acting as factories for components/hooks
*/
Factories = 'Factories',
/**
* Checks that manual memoization is preserved
*/
@@ -718,14 +711,6 @@ export enum ErrorCategory {
* Suppressions
*/
Suppression = 'Suppression',
/**
* Issues with auto deps
*/
AutomaticEffectDependencies = 'AutomaticEffectDependencies',
/**
* Issues with `fire`
*/
Fire = 'Fire',
/**
* fbt-specific issues
*/
@@ -790,16 +775,6 @@ export function getRuleForCategory(category: ErrorCategory): LintRule {
function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
switch (category) {
case ErrorCategory.AutomaticEffectDependencies: {
return {
category,
severity: ErrorSeverity.Error,
name: 'automatic-effect-dependencies',
description:
'Verifies that automatic effect dependencies are compiled if opted-in',
preset: LintRulePreset.Off,
};
}
case ErrorCategory.CapitalizedCalls: {
return {
category,
@@ -870,17 +845,6 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
preset: LintRulePreset.Recommended,
};
}
case ErrorCategory.Factories: {
return {
category,
severity: ErrorSeverity.Error,
name: 'component-hook-factories',
description:
'Validates against higher order functions defining nested components or hooks. ' +
'Components and hooks should be defined at the module level',
preset: LintRulePreset.Recommended,
};
}
case ErrorCategory.FBT: {
return {
category,
@@ -890,15 +854,6 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
preset: LintRulePreset.Off,
};
}
case ErrorCategory.Fire: {
return {
category,
severity: ErrorSeverity.Error,
name: 'fire',
description: 'Validates usage of `fire`',
preset: LintRulePreset.Off,
};
}
case ErrorCategory.Gating: {
return {
category,

View File

@@ -19,7 +19,7 @@ import {getOrInsertWith} from '../Utils/utils';
import {ExternalFunction, isHookName} from '../HIR/Environment';
import {Err, Ok, Result} from '../Utils/Result';
import {LoggerEvent, ParsedPluginOptions} from './Options';
import {BabelFn, getReactCompilerRuntimeModule} from './Program';
import {getReactCompilerRuntimeModule} from './Program';
import {SuppressionRange} from './Suppression';
export function validateRestrictedImports(
@@ -84,12 +84,6 @@ export class ProgramContext {
// generated imports
imports: Map<string, Map<string, NonLocalImportSpecifier>> = new Map();
/**
* Metadata from compilation
*/
retryErrors: Array<{fn: BabelFn; error: CompilerError}> = [];
inferredEffectLocations: Set<t.SourceLocation> = new Set();
constructor({
program,
suppressions,
@@ -108,14 +102,7 @@ export class ProgramContext {
}
isHookName(name: string): boolean {
if (this.opts.environment.hookPattern == null) {
return isHookName(name);
} else {
const match = new RegExp(this.opts.environment.hookPattern).exec(name);
return (
match != null && typeof match[1] === 'string' && isHookName(match[1])
);
}
return isHookName(name);
}
hasReference(name: string): boolean {

View File

@@ -228,8 +228,6 @@ const CompilerOutputModeSchema = z.enum([
'ssr',
// Build optimized for the client, with auto memoization
'client',
// Build optimized for the client without auto memo
'client-no-memo',
// Lint mode, the output is unused but validations should run
'lint',
]);
@@ -254,10 +252,9 @@ export type LoggerEvent =
| CompileErrorEvent
| CompileDiagnosticEvent
| CompileSkipEvent
| CompileUnexpectedThrowEvent
| PipelineErrorEvent
| TimingEvent
| AutoDepsDecorationsEvent
| AutoDepsEligibleEvent;
| TimingEvent;
export type CompileErrorEvent = {
kind: 'CompileError';
@@ -290,21 +287,15 @@ export type PipelineErrorEvent = {
fnLoc: t.SourceLocation | null;
data: string;
};
export type CompileUnexpectedThrowEvent = {
kind: 'CompileUnexpectedThrow';
fnLoc: t.SourceLocation | null;
data: string;
};
export type TimingEvent = {
kind: 'Timing';
measurement: PerformanceMeasure;
};
export type AutoDepsDecorationsEvent = {
kind: 'AutoDepsDecorations';
fnLoc: t.SourceLocation;
decorations: Array<t.SourceLocation>;
};
export type AutoDepsEligibleEvent = {
kind: 'AutoDepsEligible';
fnLoc: t.SourceLocation;
depArrayLoc: t.SourceLocation;
};
export type Logger = {
logEvent: (filename: string | null, event: LoggerEvent) => void;
debugLogIRs?: (value: CompilerPipelineValue) => void;

View File

@@ -9,6 +9,8 @@ import {NodePath} from '@babel/traverse';
import * as t from '@babel/types';
import prettyFormat from 'pretty-format';
import {CompilerOutputMode, Logger, ProgramContext} from '.';
import {CompilerError} from '../CompilerError';
import {Err, Ok, Result} from '../Utils/Result';
import {
HIRFunction,
ReactiveFunction,
@@ -34,15 +36,12 @@ import {
dropManualMemoization,
inferReactivePlaces,
inlineImmediatelyInvokedFunctionExpressions,
inferEffectDependencies,
} from '../Inference';
import {
constantPropagation,
deadCodeElimination,
pruneMaybeThrows,
inlineJsxTransform,
} from '../Optimization';
import {instructionReordering} from '../Optimization/InstructionReordering';
import {
CodegenFunction,
alignObjectMethodScopes,
@@ -69,7 +68,6 @@ import {alignReactiveScopesToBlockScopesHIR} from '../ReactiveScopes/AlignReacti
import {flattenReactiveLoopsHIR} from '../ReactiveScopes/FlattenReactiveLoopsHIR';
import {flattenScopesWithHooksOrUseHIR} from '../ReactiveScopes/FlattenScopesWithHooksOrUseHIR';
import {pruneAlwaysInvalidatingScopes} from '../ReactiveScopes/PruneAlwaysInvalidatingScopes';
import pruneInitializationDependencies from '../ReactiveScopes/PruneInitializationDependencies';
import {stabilizeBlockIds} from '../ReactiveScopes/StabilizeBlockIds';
import {
eliminateRedundantPhi,
@@ -80,7 +78,6 @@ import {inferTypes} from '../TypeInference';
import {
validateContextVariableLValues,
validateHooksUsage,
validateMemoizedEffectDependencies,
validateNoCapitalizedCalls,
validateNoRefAccessInRender,
validateNoSetStateInRender,
@@ -89,14 +86,11 @@ import {
} from '../Validation';
import {validateLocalsNotReassignedAfterRender} from '../Validation/ValidateLocalsNotReassignedAfterRender';
import {outlineFunctions} from '../Optimization/OutlineFunctions';
import {lowerContextAccess} from '../Optimization/LowerContextAccess';
import {validateNoSetStateInEffects} from '../Validation/ValidateNoSetStateInEffects';
import {validateNoJSXInTryStatement} from '../Validation/ValidateNoJSXInTryStatement';
import {propagateScopeDependenciesHIR} from '../HIR/PropagateScopeDependenciesHIR';
import {outlineJSX} from '../Optimization/OutlineJsx';
import {optimizePropsMethodCalls} from '../Optimization/OptimizePropsMethodCalls';
import {transformFire} from '../Transform';
import {validateNoImpureFunctionsInRender} from '../Validation/ValidateNoImpureFunctionsInRender';
import {validateStaticComponents} from '../Validation/ValidateStaticComponents';
import {validateNoFreezingKnownMutableFunctions} from '../Validation/ValidateNoFreezingKnownMutableFunctions';
import {inferMutationAliasingEffects} from '../Inference/InferMutationAliasingEffects';
@@ -125,7 +119,7 @@ function run(
logger: Logger | null,
filename: string | null,
code: string | null,
): CodegenFunction {
): Result<CodegenFunction, CompilerError> {
const contextIdentifiers = findContextIdentifiers(func);
const env = new Environment(
func.scope,
@@ -156,26 +150,21 @@ function runWithEnvironment(
t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
>,
env: Environment,
): CodegenFunction {
): Result<CodegenFunction, CompilerError> {
const log = (value: CompilerPipelineValue): void => {
env.logger?.debugLogIRs?.(value);
};
const hir = lower(func, env).unwrap();
const hir = lower(func, env);
log({kind: 'hir', name: 'HIR', value: hir});
pruneMaybeThrows(hir);
log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
validateContextVariableLValues(hir);
validateUseMemo(hir).unwrap();
validateUseMemo(hir);
if (
env.enableDropManualMemoization &&
!env.config.enablePreserveExistingManualUseMemo &&
!env.config.disableMemoizationForDebugging &&
!env.config.enableChangeDetectionForDebugging
) {
dropManualMemoization(hir).unwrap();
if (env.enableDropManualMemoization) {
dropManualMemoization(hir);
log({kind: 'hir', name: 'DropManualMemoization', value: hir});
}
@@ -208,35 +197,21 @@ function runWithEnvironment(
if (env.enableValidations) {
if (env.config.validateHooksUsage) {
validateHooksUsage(hir).unwrap();
validateHooksUsage(hir);
}
if (env.config.validateNoCapitalizedCalls) {
validateNoCapitalizedCalls(hir).unwrap();
validateNoCapitalizedCalls(hir);
}
}
if (env.config.enableFire) {
transformFire(hir);
log({kind: 'hir', name: 'TransformFire', value: hir});
}
if (env.config.lowerContextAccess) {
lowerContextAccess(hir, env.config.lowerContextAccess);
}
optimizePropsMethodCalls(hir);
log({kind: 'hir', name: 'OptimizePropsMethodCalls', value: hir});
analyseFunctions(hir);
log({kind: 'hir', name: 'AnalyseFunctions', value: hir});
const mutabilityAliasingErrors = inferMutationAliasingEffects(hir);
inferMutationAliasingEffects(hir);
log({kind: 'hir', name: 'InferMutationAliasingEffects', value: hir});
if (env.enableValidations) {
if (mutabilityAliasingErrors.isErr()) {
throw mutabilityAliasingErrors.unwrapErr();
}
}
if (env.outputMode === 'ssr') {
optimizeForSSR(hir);
@@ -246,37 +221,26 @@ function runWithEnvironment(
// Note: Has to come after infer reference effects because "dead" code may still affect inference
deadCodeElimination(hir);
log({kind: 'hir', name: 'DeadCodeElimination', value: hir});
if (env.config.enableInstructionReordering) {
instructionReordering(hir);
log({kind: 'hir', name: 'InstructionReordering', value: hir});
}
pruneMaybeThrows(hir);
log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
const mutabilityAliasingRangeErrors = inferMutationAliasingRanges(hir, {
inferMutationAliasingRanges(hir, {
isFunctionExpression: false,
});
log({kind: 'hir', name: 'InferMutationAliasingRanges', value: hir});
if (env.enableValidations) {
if (mutabilityAliasingRangeErrors.isErr()) {
throw mutabilityAliasingRangeErrors.unwrapErr();
}
validateLocalsNotReassignedAfterRender(hir);
}
if (env.enableValidations) {
if (env.config.assertValidMutableRanges) {
assertValidMutableRanges(hir);
}
if (env.config.validateRefAccessDuringRender) {
validateNoRefAccessInRender(hir).unwrap();
validateNoRefAccessInRender(hir);
}
if (env.config.validateNoSetStateInRender) {
validateNoSetStateInRender(hir).unwrap();
validateNoSetStateInRender(hir);
}
if (
@@ -296,11 +260,7 @@ function runWithEnvironment(
env.logErrors(validateNoJSXInTryStatement(hir));
}
if (env.config.validateNoImpureFunctionsInRender) {
validateNoImpureFunctionsInRender(hir).unwrap();
}
validateNoFreezingKnownMutableFunctions(hir).unwrap();
validateNoFreezingKnownMutableFunctions(hir);
}
inferReactivePlaces(hir);
@@ -312,7 +272,7 @@ function runWithEnvironment(
env.config.validateExhaustiveEffectDependencies
) {
// NOTE: this relies on reactivity inference running first
validateExhaustiveDependencies(hir).unwrap();
validateExhaustiveDependencies(hir);
}
}
@@ -426,6 +386,7 @@ function runWithEnvironment(
});
assertTerminalSuccessorsExist(hir);
assertTerminalPredsExist(hir);
propagateScopeDependenciesHIR(hir);
log({
kind: 'hir',
@@ -433,24 +394,6 @@ function runWithEnvironment(
value: hir,
});
if (env.config.inferEffectDependencies) {
inferEffectDependencies(hir);
log({
kind: 'hir',
name: 'InferEffectDependencies',
value: hir,
});
}
if (env.config.inlineJsxTransform) {
inlineJsxTransform(hir, env.config.inlineJsxTransform);
log({
kind: 'hir',
name: 'inlineJsxTransform',
value: hir,
});
}
const reactiveFunction = buildReactiveFunction(hir);
log({
kind: 'reactive',
@@ -503,15 +446,6 @@ function runWithEnvironment(
value: reactiveFunction,
});
if (env.config.enableChangeDetectionForDebugging != null) {
pruneInitializationDependencies(reactiveFunction);
log({
kind: 'reactive',
name: 'PruneInitializationDependencies',
value: reactiveFunction,
});
}
propagateEarlyReturns(reactiveFunction);
log({
kind: 'reactive',
@@ -561,28 +495,24 @@ function runWithEnvironment(
value: reactiveFunction,
});
if (env.config.validateMemoizedEffectDependencies) {
validateMemoizedEffectDependencies(reactiveFunction).unwrap();
}
if (
env.config.enablePreserveExistingMemoizationGuarantees ||
env.config.validatePreserveExistingMemoizationGuarantees
) {
validatePreservedManualMemoization(reactiveFunction).unwrap();
validatePreservedManualMemoization(reactiveFunction);
}
const ast = codegenFunction(reactiveFunction, {
uniqueIdentifiers,
fbtOperands,
}).unwrap();
});
log({kind: 'ast', name: 'Codegen', value: ast});
for (const outlined of ast.outlined) {
log({kind: 'ast', name: 'Codegen (outlined)', value: outlined.fn});
}
if (env.config.validateSourceLocations) {
validateSourceLocations(func, ast).unwrap();
validateSourceLocations(func, ast, env);
}
/**
@@ -594,7 +524,10 @@ function runWithEnvironment(
throw new Error('unexpected error');
}
return ast;
if (env.hasErrors()) {
return Err(env.aggregateErrors());
}
return Ok(ast);
}
export function compileFn(
@@ -608,7 +541,7 @@ export function compileFn(
logger: Logger | null,
filename: string | null,
code: string | null,
): CodegenFunction {
): Result<CodegenFunction, CompilerError> {
return run(
func,
config,

View File

@@ -350,10 +350,6 @@ function isFilePartOfSources(
return false;
}
export type CompileProgramMetadata = {
retryErrors: Array<{fn: BabelFn; error: CompilerError}>;
inferredEffectLocations: Set<t.SourceLocation>;
};
/**
* Main entrypoint for React Compiler.
*
@@ -364,7 +360,7 @@ export type CompileProgramMetadata = {
export function compileProgram(
program: NodePath<t.Program>,
pass: CompilerPass,
): CompileProgramMetadata | null {
): void {
/**
* This is directly invoked by the react-compiler babel plugin, so exceptions
* thrown by this function will fail the babel build.
@@ -377,7 +373,7 @@ export function compileProgram(
* the outlined functions.
*/
if (shouldSkipCompilation(program, pass)) {
return null;
return;
}
const restrictedImportsErr = validateRestrictedImports(
program,
@@ -385,7 +381,7 @@ export function compileProgram(
);
if (restrictedImportsErr) {
handleError(restrictedImportsErr, pass, null);
return null;
return;
}
/*
* Record lint errors and critical errors as depending on Forget's config,
@@ -479,16 +475,11 @@ export function compileProgram(
);
handleError(error, programContext, null);
}
return null;
return;
}
// Insert React Compiler generated functions into the Babel AST
applyCompiledFunctions(program, compiledFns, pass, programContext);
return {
retryErrors: programContext.retryErrors,
inferredEffectLocations: programContext.inferredEffectLocations,
};
}
type CompileSource = {
@@ -518,10 +509,6 @@ function findFunctionsToCompile(
const fnType = getReactFunctionType(fn, pass);
if (pass.opts.environment.validateNoDynamicallyCreatedComponentsOrHooks) {
validateNoDynamicallyCreatedComponentsOrHooks(fn, pass, programContext);
}
if (fnType === null || programContext.alreadyCompiled.has(fn.node)) {
return;
}
@@ -633,15 +620,7 @@ function processFn(
} else {
handleError(compileResult.error, programContext, fn.node.loc ?? null);
}
if (outputMode === 'client') {
const retryResult = retryCompileFunction(fn, fnType, programContext);
if (retryResult == null) {
return null;
}
compiledFn = retryResult;
} else {
return null;
}
return null;
} else {
compiledFn = compileResult.compiledFn;
}
@@ -678,16 +657,6 @@ function processFn(
if (programContext.hasModuleScopeOptOut) {
return null;
} else if (programContext.opts.outputMode === 'lint') {
/**
* inferEffectDependencies + noEmit is currently only used for linting. In
* this mode, add source locations for where the compiler *can* infer effect
* dependencies.
*/
for (const loc of compiledFn.inferredEffectLocations) {
if (loc !== GeneratedSource) {
programContext.inferredEffectLocations.add(loc);
}
}
return null;
} else if (
programContext.opts.compilationMode === 'annotation' &&
@@ -728,67 +697,37 @@ function tryCompileFunction(
}
try {
return {
kind: 'compile',
compiledFn: compileFn(
fn,
programContext.opts.environment,
fnType,
outputMode,
programContext,
programContext.opts.logger,
programContext.filename,
programContext.code,
),
};
} catch (err) {
return {kind: 'error', error: err};
}
}
/**
* If non-memo feature flags are enabled, retry compilation with a more minimal
* feature set.
*
* @returns a CodegenFunction if retry was successful
*/
function retryCompileFunction(
fn: BabelFn,
fnType: ReactFunctionType,
programContext: ProgramContext,
): CodegenFunction | null {
const environment = programContext.opts.environment;
if (
!(environment.enableFire || environment.inferEffectDependencies != null)
) {
return null;
}
/**
* Note that function suppressions are not checked in the retry pipeline, as
* they only affect auto-memoization features.
*/
try {
const retryResult = compileFn(
const result = compileFn(
fn,
environment,
programContext.opts.environment,
fnType,
'client-no-memo',
outputMode,
programContext,
programContext.opts.logger,
programContext.filename,
programContext.code,
);
if (!retryResult.hasFireRewrite && !retryResult.hasInferredEffect) {
return null;
if (result.isOk()) {
return {kind: 'compile', compiledFn: result.unwrap()};
} else {
return {kind: 'error', error: result.unwrapErr()};
}
return retryResult;
} catch (err) {
// TODO: we might want to log error here, but this will also result in duplicate logging
if (err instanceof CompilerError) {
programContext.retryErrors.push({fn, error: err});
/**
* A pass incorrectly threw instead of recording the error.
* Log for detection in development.
*/
if (
err instanceof CompilerError &&
err.details.every(detail => detail.category !== ErrorCategory.Invariant)
) {
programContext.logEvent({
kind: 'CompileUnexpectedThrow',
fnLoc: fn.node.loc ?? null,
data: err.toString(),
});
}
return null;
return {kind: 'error', error: err};
}
}
@@ -876,84 +815,17 @@ function shouldSkipCompilation(
return false;
}
/**
* Validates that Components/Hooks are always defined at module level. This prevents scope reference
* errors that occur when the compiler attempts to optimize the nested component/hook while its
* parent function remains uncompiled.
*/
function validateNoDynamicallyCreatedComponentsOrHooks(
fn: BabelFn,
pass: CompilerPass,
programContext: ProgramContext,
): void {
const parentNameExpr = getFunctionName(fn);
const parentName =
parentNameExpr !== null && parentNameExpr.isIdentifier()
? parentNameExpr.node.name
: '<anonymous>';
const validateNestedFunction = (
nestedFn: NodePath<
t.FunctionDeclaration | t.FunctionExpression | t.ArrowFunctionExpression
>,
): void => {
if (
nestedFn.node === fn.node ||
programContext.alreadyCompiled.has(nestedFn.node)
) {
return;
}
if (nestedFn.scope.getProgramParent() !== nestedFn.scope.parent) {
const nestedFnType = getReactFunctionType(nestedFn as BabelFn, pass);
const nestedFnNameExpr = getFunctionName(nestedFn as BabelFn);
const nestedName =
nestedFnNameExpr !== null && nestedFnNameExpr.isIdentifier()
? nestedFnNameExpr.node.name
: '<anonymous>';
if (nestedFnType === 'Component' || nestedFnType === 'Hook') {
CompilerError.throwDiagnostic({
category: ErrorCategory.Factories,
reason: `Components and hooks cannot be created dynamically`,
description: `The function \`${nestedName}\` appears to be a React ${nestedFnType.toLowerCase()}, but it's defined inside \`${parentName}\`. Components and Hooks should always be declared at module scope`,
details: [
{
kind: 'error',
message: 'this function dynamically created a component/hook',
loc: parentNameExpr?.node.loc ?? fn.node.loc ?? null,
},
{
kind: 'error',
message: 'the component is created here',
loc: nestedFnNameExpr?.node.loc ?? nestedFn.node.loc ?? null,
},
],
});
}
}
nestedFn.skip();
};
fn.traverse({
FunctionDeclaration: validateNestedFunction,
FunctionExpression: validateNestedFunction,
ArrowFunctionExpression: validateNestedFunction,
});
}
function getReactFunctionType(
fn: BabelFn,
pass: CompilerPass,
): ReactFunctionType | null {
const hookPattern = pass.opts.environment.hookPattern;
if (fn.node.body.type === 'BlockStatement') {
const optInDirectives = tryFindDirectiveEnablingMemoization(
fn.node.body.directives,
pass.opts,
);
if (optInDirectives.unwrapOr(null) != null) {
return getComponentOrHookLike(fn, hookPattern) ?? 'Other';
return getComponentOrHookLike(fn) ?? 'Other';
}
}
@@ -974,13 +846,13 @@ function getReactFunctionType(
}
case 'infer': {
// Check if this is a component or hook-like function
return componentSyntaxType ?? getComponentOrHookLike(fn, hookPattern);
return componentSyntaxType ?? getComponentOrHookLike(fn);
}
case 'syntax': {
return componentSyntaxType;
}
case 'all': {
return getComponentOrHookLike(fn, hookPattern) ?? 'Other';
return getComponentOrHookLike(fn) ?? 'Other';
}
default: {
assertExhaustive(
@@ -1022,10 +894,7 @@ function hasMemoCacheFunctionImport(
return hasUseMemoCache;
}
function isHookName(s: string, hookPattern: string | null): boolean {
if (hookPattern !== null) {
return new RegExp(hookPattern).test(s);
}
function isHookName(s: string): boolean {
return /^use[A-Z0-9]/.test(s);
}
@@ -1034,16 +903,13 @@ function isHookName(s: string, hookPattern: string | null): boolean {
* containing a hook name.
*/
function isHook(
path: NodePath<t.Expression | t.PrivateName>,
hookPattern: string | null,
): boolean {
function isHook(path: NodePath<t.Expression | t.PrivateName>): boolean {
if (path.isIdentifier()) {
return isHookName(path.node.name, hookPattern);
return isHookName(path.node.name);
} else if (
path.isMemberExpression() &&
!path.node.computed &&
isHook(path.get('property'), hookPattern)
isHook(path.get('property'))
) {
const obj = path.get('object').node;
const isPascalCaseNameSpace = /^[A-Z].*/;
@@ -1184,19 +1050,18 @@ function getComponentOrHookLike(
node: NodePath<
t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
>,
hookPattern: string | null,
): ReactFunctionType | null {
const functionName = getFunctionName(node);
// Check if the name is component or hook like:
if (functionName !== null && isComponentName(functionName)) {
let isComponent =
callsHooksOrCreatesJsx(node, hookPattern) &&
callsHooksOrCreatesJsx(node) &&
isValidComponentParams(node.get('params')) &&
!returnsNonNode(node);
return isComponent ? 'Component' : null;
} else if (functionName !== null && isHook(functionName, hookPattern)) {
} else if (functionName !== null && isHook(functionName)) {
// Hooks have hook invocations or JSX, but can take any # of arguments
return callsHooksOrCreatesJsx(node, hookPattern) ? 'Hook' : null;
return callsHooksOrCreatesJsx(node) ? 'Hook' : null;
}
/*
@@ -1206,7 +1071,7 @@ function getComponentOrHookLike(
if (node.isFunctionExpression() || node.isArrowFunctionExpression()) {
if (isForwardRefCallback(node) || isMemoCallback(node)) {
// As an added check we also look for hook invocations or JSX
return callsHooksOrCreatesJsx(node, hookPattern) ? 'Component' : null;
return callsHooksOrCreatesJsx(node) ? 'Component' : null;
}
}
return null;
@@ -1232,7 +1097,6 @@ function callsHooksOrCreatesJsx(
node: NodePath<
t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
>,
hookPattern: string | null,
): boolean {
let invokesHooks = false;
let createsJsx = false;
@@ -1243,7 +1107,7 @@ function callsHooksOrCreatesJsx(
},
CallExpression(call) {
const callee = call.get('callee');
if (callee.isExpression() && isHook(callee, hookPattern)) {
if (callee.isExpression() && isHook(callee)) {
invokesHooks = true;
}
},

View File

@@ -1,327 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {NodePath} from '@babel/core';
import * as t from '@babel/types';
import {CompilerError, EnvironmentConfig, Logger} from '..';
import {getOrInsertWith} from '../Utils/utils';
import {Environment, GeneratedSource} from '../HIR';
import {DEFAULT_EXPORT} from '../HIR/Environment';
import {CompileProgramMetadata} from './Program';
import {
CompilerDiagnostic,
CompilerDiagnosticOptions,
ErrorCategory,
} from '../CompilerError';
function throwInvalidReact(
options: CompilerDiagnosticOptions,
{logger, filename}: TraversalState,
): never {
logger?.logEvent(filename, {
kind: 'CompileError',
fnLoc: null,
detail: new CompilerDiagnostic(options),
});
CompilerError.throwDiagnostic(options);
}
function isAutodepsSigil(
arg: NodePath<t.ArgumentPlaceholder | t.SpreadElement | t.Expression>,
): boolean {
// Check for AUTODEPS identifier imported from React
if (arg.isIdentifier() && arg.node.name === 'AUTODEPS') {
const binding = arg.scope.getBinding(arg.node.name);
if (binding && binding.path.isImportSpecifier()) {
const importSpecifier = binding.path.node as t.ImportSpecifier;
if (importSpecifier.imported.type === 'Identifier') {
return (importSpecifier.imported as t.Identifier).name === 'AUTODEPS';
}
}
return false;
}
// Check for React.AUTODEPS member expression
if (arg.isMemberExpression() && !arg.node.computed) {
const object = arg.get('object');
const property = arg.get('property');
if (
object.isIdentifier() &&
object.node.name === 'React' &&
property.isIdentifier() &&
property.node.name === 'AUTODEPS'
) {
return true;
}
}
return false;
}
function assertValidEffectImportReference(
autodepsIndex: number,
paths: Array<NodePath<t.Node>>,
context: TraversalState,
): void {
for (const path of paths) {
const parent = path.parentPath;
if (parent != null && parent.isCallExpression()) {
const args = parent.get('arguments');
const maybeCalleeLoc = path.node.loc;
const hasInferredEffect =
maybeCalleeLoc != null &&
context.inferredEffectLocations.has(maybeCalleeLoc);
/**
* Error on effect calls that still have AUTODEPS in their args
*/
const hasAutodepsArg = args.some(isAutodepsSigil);
if (hasAutodepsArg && !hasInferredEffect) {
const maybeErrorDiagnostic = matchCompilerDiagnostic(
path,
context.transformErrors,
);
/**
* Note that we cannot easily check the type of the first argument here,
* as it may have already been transformed by the compiler (and not
* memoized).
*/
throwInvalidReact(
{
category: ErrorCategory.AutomaticEffectDependencies,
reason:
'Cannot infer dependencies of this effect. This will break your build!',
description:
'To resolve, either pass a dependency array or fix reported compiler bailout diagnostics' +
(maybeErrorDiagnostic ? ` ${maybeErrorDiagnostic}` : ''),
details: [
{
kind: 'error',
message: 'Cannot infer dependencies',
loc: parent.node.loc ?? GeneratedSource,
},
],
},
context,
);
}
}
}
}
function assertValidFireImportReference(
paths: Array<NodePath<t.Node>>,
context: TraversalState,
): void {
if (paths.length > 0) {
const maybeErrorDiagnostic = matchCompilerDiagnostic(
paths[0],
context.transformErrors,
);
throwInvalidReact(
{
category: ErrorCategory.Fire,
reason: '[Fire] Untransformed reference to compiler-required feature.',
description:
'Either remove this `fire` call or ensure it is successfully transformed by the compiler' +
(maybeErrorDiagnostic != null ? ` ${maybeErrorDiagnostic}` : ''),
details: [
{
kind: 'error',
message: 'Untransformed `fire` call',
loc: paths[0].node.loc ?? GeneratedSource,
},
],
},
context,
);
}
}
export default function validateNoUntransformedReferences(
path: NodePath<t.Program>,
filename: string | null,
logger: Logger | null,
env: EnvironmentConfig,
compileResult: CompileProgramMetadata | null,
): void {
const moduleLoadChecks = new Map<
string,
Map<string, CheckInvalidReferenceFn>
>();
if (env.enableFire) {
/**
* Error on any untransformed references to `fire` (e.g. including non-call
* expressions)
*/
for (const module of Environment.knownReactModules) {
const react = getOrInsertWith(moduleLoadChecks, module, () => new Map());
react.set('fire', assertValidFireImportReference);
}
}
if (env.inferEffectDependencies) {
for (const {
function: {source, importSpecifierName},
autodepsIndex,
} of env.inferEffectDependencies) {
const module = getOrInsertWith(moduleLoadChecks, source, () => new Map());
module.set(
importSpecifierName,
assertValidEffectImportReference.bind(null, autodepsIndex),
);
}
}
if (moduleLoadChecks.size > 0) {
transformProgram(path, moduleLoadChecks, filename, logger, compileResult);
}
}
type TraversalState = {
shouldInvalidateScopes: boolean;
program: NodePath<t.Program>;
logger: Logger | null;
filename: string | null;
transformErrors: Array<{fn: NodePath<t.Node>; error: CompilerError}>;
inferredEffectLocations: Set<t.SourceLocation>;
};
type CheckInvalidReferenceFn = (
paths: Array<NodePath<t.Node>>,
context: TraversalState,
) => void;
function validateImportSpecifier(
specifier: NodePath<t.ImportSpecifier>,
importSpecifierChecks: Map<string, CheckInvalidReferenceFn>,
state: TraversalState,
): void {
const imported = specifier.get('imported');
const specifierName: string =
imported.node.type === 'Identifier'
? imported.node.name
: imported.node.value;
const checkFn = importSpecifierChecks.get(specifierName);
if (checkFn == null) {
return;
}
if (state.shouldInvalidateScopes) {
state.shouldInvalidateScopes = false;
state.program.scope.crawl();
}
const local = specifier.get('local');
const binding = local.scope.getBinding(local.node.name);
CompilerError.invariant(binding != null, {
reason: 'Expected binding to be found for import specifier',
loc: local.node.loc ?? GeneratedSource,
});
checkFn(binding.referencePaths, state);
}
function validateNamespacedImport(
specifier: NodePath<t.ImportNamespaceSpecifier | t.ImportDefaultSpecifier>,
importSpecifierChecks: Map<string, CheckInvalidReferenceFn>,
state: TraversalState,
): void {
if (state.shouldInvalidateScopes) {
state.shouldInvalidateScopes = false;
state.program.scope.crawl();
}
const local = specifier.get('local');
const binding = local.scope.getBinding(local.node.name);
const defaultCheckFn = importSpecifierChecks.get(DEFAULT_EXPORT);
CompilerError.invariant(binding != null, {
reason: 'Expected binding to be found for import specifier',
loc: local.node.loc ?? GeneratedSource,
});
const filteredReferences = new Map<
CheckInvalidReferenceFn,
Array<NodePath<t.Node>>
>();
for (const reference of binding.referencePaths) {
if (defaultCheckFn != null) {
getOrInsertWith(filteredReferences, defaultCheckFn, () => []).push(
reference,
);
}
const parent = reference.parentPath;
if (
parent != null &&
parent.isMemberExpression() &&
parent.get('object') === reference
) {
if (parent.node.computed || parent.node.property.type !== 'Identifier') {
continue;
}
const checkFn = importSpecifierChecks.get(parent.node.property.name);
if (checkFn != null) {
getOrInsertWith(filteredReferences, checkFn, () => []).push(parent);
}
}
}
for (const [checkFn, references] of filteredReferences) {
checkFn(references, state);
}
}
function transformProgram(
path: NodePath<t.Program>,
moduleLoadChecks: Map<string, Map<string, CheckInvalidReferenceFn>>,
filename: string | null,
logger: Logger | null,
compileResult: CompileProgramMetadata | null,
): void {
const traversalState: TraversalState = {
shouldInvalidateScopes: true,
program: path,
filename,
logger,
transformErrors: compileResult?.retryErrors ?? [],
inferredEffectLocations:
compileResult?.inferredEffectLocations ?? new Set(),
};
path.traverse({
ImportDeclaration(path: NodePath<t.ImportDeclaration>) {
const importSpecifierChecks = moduleLoadChecks.get(
path.node.source.value,
);
if (importSpecifierChecks == null) {
return;
}
const specifiers = path.get('specifiers');
for (const specifier of specifiers) {
if (specifier.isImportSpecifier()) {
validateImportSpecifier(
specifier,
importSpecifierChecks,
traversalState,
);
} else {
validateNamespacedImport(
specifier as NodePath<
t.ImportNamespaceSpecifier | t.ImportDefaultSpecifier
>,
importSpecifierChecks,
traversalState,
);
}
}
},
});
}
function matchCompilerDiagnostic(
badReference: NodePath<t.Node>,
transformErrors: Array<{fn: NodePath<t.Node>; error: CompilerError}>,
): string | null {
for (const {fn, error} of transformErrors) {
if (fn.isAncestor(badReference)) {
return error.toString();
}
}
return null;
}

View File

@@ -124,9 +124,7 @@ export function collectHoistablePropertyLoads(
hoistableFromOptionals,
registry,
nestedFnImmutableContext: null,
assumedInvokedFns: fn.env.config.enableTreatFunctionDepsAsConditional
? new Set()
: getAssumedInvokedFunctions(fn),
assumedInvokedFns: getAssumedInvokedFunctions(fn),
});
}
@@ -142,9 +140,7 @@ export function collectHoistablePropertyLoadsInInnerFn(
hoistableFromOptionals,
registry: new PropertyPathRegistry(),
nestedFnImmutableContext: null,
assumedInvokedFns: fn.env.config.enableTreatFunctionDepsAsConditional
? new Set()
: getAssumedInvokedFunctions(fn),
assumedInvokedFns: getAssumedInvokedFunctions(fn),
};
const nestedFnImmutableContext = new Set(
fn.context

View File

@@ -310,16 +310,13 @@ function traverseOptionalBlock(
* - a optional base block with a separate nested optional-chain (e.g. a(c?.d)?.d)
*/
const testBlock = context.blocks.get(maybeTest.terminal.fallthrough)!;
if (testBlock!.terminal.kind !== 'branch') {
/**
* Fallthrough of the inner optional should be a block with no
* instructions, terminating with Test($<temporary written to from
* StoreLocal>)
*/
CompilerError.throwTodo({
reason: `Unexpected terminal kind \`${testBlock.terminal.kind}\` for optional fallthrough block`,
loc: maybeTest.terminal.loc,
});
/**
* Fallthrough of the inner optional should be a block with no
* instructions, terminating with Test($<temporary written to from
* StoreLocal>)
*/
if (testBlock.terminal.kind !== 'branch') {
return null;
}
/**
* Recurse into inner optional blocks to collect inner optional-chain

View File

@@ -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 {
@@ -54,14 +59,6 @@ import {FlowTypeEnv} from '../Flood/Types';
import {defaultModuleTypeProvider} from './DefaultModuleTypeProvider';
import {assertExhaustive} from '../Utils/utils';
export const ReactElementSymbolSchema = z.object({
elementSymbol: z.union([
z.literal('react.element'),
z.literal('react.transitional.element'),
]),
globalDevVar: z.string(),
});
export const ExternalFunctionSchema = z.object({
// Source for the imported module that exports the `importSpecifierName` functions
source: z.string(),
@@ -82,8 +79,6 @@ export const InstrumentationSchema = z
);
export type ExternalFunction = z.infer<typeof ExternalFunctionSchema>;
export const USE_FIRE_FUNCTION_NAME = 'useFire';
export const EMIT_FREEZE_GLOBAL_GATING = '__DEV__';
export const MacroSchema = z.string();
@@ -236,24 +231,9 @@ export const EnvironmentConfigSchema = z.object({
.enum(['off', 'all', 'missing-only', 'extra-only'])
.default('off'),
/**
* When this is true, rather than pruning existing manual memoization but ensuring or validating
* that the memoized values remain memoized, the compiler will simply not prune existing calls to
* useMemo/useCallback.
*/
enablePreserveExistingManualUseMemo: z.boolean().default(false),
// 🌲
enableForest: z.boolean().default(false),
/**
* Enable use of type annotations in the source to drive type inference. By default
* Forget attemps to infer types using only information that is guaranteed correct
* given the source, and does not trust user-supplied type annotations. This mode
* enables trusting user type annotations.
*/
enableUseTypeAnnotations: z.boolean().default(false),
/**
* Allows specifying a function that can populate HIR with type information from
* Flow
@@ -268,53 +248,8 @@ export const EnvironmentConfigSchema = z.object({
*/
enableOptionalDependencies: z.boolean().default(true),
enableFire: z.boolean().default(false),
enableNameAnonymousFunctions: z.boolean().default(false),
/**
* Enables inference and auto-insertion of effect dependencies. Takes in an array of
* configurable module and import pairs to allow for user-land experimentation. For example,
* [
* {
* module: 'react',
* imported: 'useEffect',
* autodepsIndex: 1,
* },{
* module: 'MyExperimentalEffectHooks',
* imported: 'useExperimentalEffect',
* autodepsIndex: 2,
* },
* ]
* would insert dependencies for calls of `useEffect` imported from `react` and calls of
* useExperimentalEffect` from `MyExperimentalEffectHooks`.
*
* `autodepsIndex` tells the compiler which index we expect the AUTODEPS to appear in.
* With the configuration above, we'd insert dependencies for `useEffect` if it has two
* arguments, and the second is AUTODEPS.
*
* Still experimental.
*/
inferEffectDependencies: z
.nullable(
z.array(
z.object({
function: ExternalFunctionSchema,
autodepsIndex: z.number().min(1, 'autodepsIndex must be > 0'),
}),
),
)
.default(null),
/**
* Enables inlining ReactElement object literals in place of JSX
* An alternative to the standard JSX transform which replaces JSX with React's jsxProd() runtime
* Currently a prod-only optimization, requiring Fast JSX dependencies
*
* The symbol configuration is set for backwards compatability with pre-React 19 transforms
*/
inlineJsxTransform: ReactElementSymbolSchema.nullable().default(null),
/*
* Enable validation of hooks to partially check that the component honors the rules of hooks.
* When disabled, the component is assumed to follow the rules (though the Babel plugin looks
@@ -366,16 +301,6 @@ export const EnvironmentConfigSchema = z.object({
*/
validateStaticComponents: z.boolean().default(false),
/**
* Validates that the dependencies of all effect hooks are memoized. This helps ensure
* that Forget does not introduce infinite renders caused by a dependency changing,
* triggering an effect, which triggers re-rendering, which causes a dependency to change,
* triggering the effect, etc.
*
* Covers useEffect, useLayoutEffect, useInsertionEffect.
*/
validateMemoizedEffectDependencies: z.boolean().default(false),
/**
* Validates that there are no capitalized calls other than those allowed by the allowlist.
* Calls to capitalized functions are often functions that used to be components and may
@@ -422,38 +347,8 @@ export const EnvironmentConfigSchema = z.object({
* then this flag will assume that `x` is not subusequently modified.
*/
enableTransitivelyFreezeFunctionExpressions: z.boolean().default(true),
/*
* Enables codegen mutability debugging. This emits a dev-mode only to log mutations
* to values that Forget assumes are immutable (for Forget compiled code).
* For example:
* emitFreeze: {
* source: 'ReactForgetRuntime',
* importSpecifierName: 'makeReadOnly',
* }
*
* produces:
* import {makeReadOnly} from 'ReactForgetRuntime';
*
* function Component(props) {
* if (c_0) {
* // ...
* $[0] = __DEV__ ? makeReadOnly(x) : x;
* } else {
* x = $[0];
* }
* }
*/
enableEmitFreeze: ExternalFunctionSchema.nullable().default(null),
enableEmitHookGuards: ExternalFunctionSchema.nullable().default(null),
/**
* Enable instruction reordering. See InstructionReordering.ts for the details
* of the approach.
*/
enableInstructionReordering: z.boolean().default(false),
/**
* Enables function outlinining, where anonymous functions that do not close over
* local variables can be extracted into top-level helper functions.
@@ -535,80 +430,12 @@ export const EnvironmentConfigSchema = z.object({
// Enable validation of mutable ranges
assertValidMutableRanges: z.boolean().default(false),
/*
* Enable emitting "change variables" which store the result of whether a particular
* reactive scope dependency has changed since the scope was last executed.
*
* Ex:
* ```
* const c_0 = $[0] !== input; // change variable
* let output;
* if (c_0) ...
* ```
*
* Defaults to false, where the comparison is inlined:
*
* ```
* let output;
* if ($[0] !== input) ...
* ```
*/
enableChangeVariableCodegen: z.boolean().default(false),
/**
* Enable emitting comments that explain Forget's output, and which
* values are being checked and which values produced by each memo block.
*
* Intended for use in demo purposes (incl playground)
*/
enableMemoizationComments: z.boolean().default(false),
/**
* [TESTING ONLY] Throw an unknown exception during compilation to
* simulate unexpected exceptions e.g. errors from babel functions.
*/
throwUnknownException__testonly: z.boolean().default(false),
/**
* Enables deps of a function epxression to be treated as conditional. This
* makes sure we don't load a dep when it's a property (to check if it has
* changed) and instead check the receiver.
*
* This makes sure we don't end up throwing when the reciver is null. Consider
* this code:
*
* ```
* function getLength() {
* return props.bar.length;
* }
* ```
*
* It's only safe to memoize `getLength` against props, not props.bar, as
* props.bar could be null when this `getLength` function is created.
*
* This does cause the memoization to now be coarse grained, which is
* non-ideal.
*/
enableTreatFunctionDepsAsConditional: z.boolean().default(false),
/**
* When true, always act as though the dependencies of a memoized value
* have changed. This makes the compiler not actually perform any optimizations,
* but is useful for debugging. Implicitly also sets
* @enablePreserveExistingManualUseMemo, because otherwise memoization in the
* original source will be disabled as well.
*/
disableMemoizationForDebugging: z.boolean().default(false),
/**
* When true, rather using memoized values, the compiler will always re-compute
* values, and then use a heuristic to compare the memoized value to the newly
* computed one. This detects cases where rules of react violations may cause the
* compiled code to behave differently than the original.
*/
enableChangeDetectionForDebugging:
ExternalFunctionSchema.nullable().default(null),
/**
* The react native re-animated library uses custom Babel transforms that
* requires the calls to library API remain unmodified.
@@ -619,19 +446,6 @@ export const EnvironmentConfigSchema = z.object({
*/
enableCustomTypeDefinitionForReanimated: z.boolean().default(false),
/**
* If specified, this value is used as a pattern for determing which global values should be
* treated as hooks. The pattern should have a single capture group, which will be used as
* the hook name for the purposes of resolving hook definitions (for builtin hooks)_.
*
* For example, by default `React$useState` would not be treated as a hook. By specifying
* `hookPattern: 'React$(\w+)'`, the compiler will treat this value equivalently to `useState()`.
*
* This setting is intended for cases where Forget is compiling code that has been prebundled
* and identifiers have been changed.
*/
hookPattern: z.string().nullable().default(null),
/**
* If enabled, this will treat objects named as `ref` or if their names end with the substring `Ref`,
* and contain a property named `current`, as React refs.
@@ -656,28 +470,6 @@ export const EnvironmentConfigSchema = z.object({
*/
enableTreatSetIdentifiersAsStateSetters: z.boolean().default(false),
/*
* If specified a value, the compiler lowers any calls to `useContext` to use
* this value as the callee.
*
* A selector function is compiled and passed as an argument along with the
* context to this function call.
*
* The compiler automatically figures out the keys by looking for the immediate
* destructuring of the return value from the useContext call. In the future,
* this can be extended to different kinds of context access like property
* loads and accesses over multiple statements as well.
*
* ```
* // input
* const {foo, bar} = useContext(MyContext);
*
* // output
* const {foo, bar} = useCompiledContext(MyContext, (c) => [c.foo, c.bar]);
* ```
*/
lowerContextAccess: ExternalFunctionSchema.nullable().default(null),
/**
* If enabled, will validate useMemos that don't return any values:
*
@@ -689,13 +481,6 @@ export const EnvironmentConfigSchema = z.object({
*/
validateNoVoidUseMemo: z.boolean().default(true),
/**
* Validates that Components/Hooks are always defined at module level. This prevents scope
* reference errors that occur when the compiler attempts to optimize the nested component/hook
* while its parent function remains uncompiled.
*/
validateNoDynamicallyCreatedComponentsOrHooks: z.boolean().default(false),
/**
* When enabled, allows setState calls in effects based on valid patterns involving refs:
* - Allow setState where the value being set is derived from a ref. This is useful where
@@ -717,15 +502,6 @@ export const EnvironmentConfigSchema = z.object({
* 3. Force update / external sync - should use useSyncExternalStore
*/
enableVerboseNoSetStateInEffect: z.boolean().default(false),
/**
* Enables inference of event handler types for JSX props on built-in DOM elements.
* When enabled, functions passed to event handler props (props starting with "on")
* on primitive JSX tags are inferred to have the BuiltinEventHandlerId type, which
* allows ref access within those functions since DOM event handlers are guaranteed
* by React to only execute in response to events, not during render.
*/
enableInferEventHandlers: z.boolean().default(false),
});
export type EnvironmentConfig = z.infer<typeof EnvironmentConfigSchema>;
@@ -767,9 +543,6 @@ export class Environment {
fnType: ReactFunctionType;
outputMode: CompilerOutputMode;
programContext: ProgramContext;
hasFireRewrite: boolean;
hasInferredEffect: boolean;
inferredEffectLocations: Set<SourceLocation> = new Set();
#contextIdentifiers: Set<t.Identifier>;
#hoistedIdentifiers: Set<t.Identifier>;
@@ -777,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,
@@ -799,20 +578,6 @@ export class Environment {
this.programContext = programContext;
this.#shapes = new Map(DEFAULT_SHAPES);
this.#globals = new Map(DEFAULT_GLOBALS);
this.hasFireRewrite = false;
this.hasInferredEffect = false;
if (
config.disableMemoizationForDebugging &&
config.enableChangeDetectionForDebugging != null
) {
CompilerError.throwInvalidConfig({
reason: `Invalid environment config: the 'disableMemoizationForDebugging' and 'enableChangeDetectionForDebugging' options cannot be used together`,
description: null,
loc: null,
suggestions: null,
});
}
for (const [hookName, hook] of this.config.customHooks) {
CompilerError.invariant(!this.#globals.has(hookName), {
@@ -875,9 +640,6 @@ export class Environment {
case 'ssr': {
return true;
}
case 'client-no-memo': {
return false;
}
default: {
assertExhaustive(
this.outputMode,
@@ -894,8 +656,7 @@ export class Environment {
// linting also enables memoization so that we can check if manual memoization is preserved
return true;
}
case 'ssr':
case 'client-no-memo': {
case 'ssr': {
return false;
}
default: {
@@ -914,9 +675,6 @@ export class Environment {
case 'ssr': {
return true;
}
case 'client-no-memo': {
return false;
}
default: {
assertExhaustive(
this.outputMode,
@@ -955,6 +713,52 @@ 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);
}
}
/**
* 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;
}
isContextIdentifier(node: t.Identifier): boolean {
return this.#contextIdentifiers.has(node);
}
@@ -1029,18 +833,6 @@ export class Environment {
binding: NonLocalBinding,
loc: SourceLocation,
): Global | null {
if (this.config.hookPattern != null) {
const match = new RegExp(this.config.hookPattern).exec(binding.name);
if (
match != null &&
typeof match[1] === 'string' &&
isHookName(match[1])
) {
const resolvedName = match[1];
return this.#globals.get(resolvedName) ?? this.#getCustomHookType();
}
}
switch (binding.kind) {
case 'ModuleLocal': {
// don't resolve module locals

View File

@@ -9,9 +9,6 @@ import {Effect, ValueKind, ValueReason} from './HIR';
import {
BUILTIN_SHAPES,
BuiltInArrayId,
BuiltInAutodepsId,
BuiltInFireFunctionId,
BuiltInFireId,
BuiltInMapId,
BuiltInMixedReadonlyId,
BuiltInObjectId,
@@ -846,26 +843,6 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
BuiltInUseOperatorId,
),
],
[
'fire',
addFunction(
DEFAULT_SHAPES,
[],
{
positionalParams: [],
restParam: null,
returnType: {
kind: 'Function',
return: {kind: 'Poly'},
shapeId: BuiltInFireFunctionId,
isConstructor: false,
},
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Frozen,
},
BuiltInFireId,
),
],
[
'useEffectEvent',
addHook(
@@ -887,7 +864,6 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
BuiltInUseEffectEventId,
),
],
['AUTODEPS', addObject(DEFAULT_SHAPES, BuiltInAutodepsId, [])],
];
TYPED_GLOBALS.push(

View File

@@ -1888,12 +1888,6 @@ export function isDispatcherType(id: Identifier): boolean {
return id.type.kind === 'Function' && id.type.shapeId === 'BuiltInDispatch';
}
export function isFireFunctionType(id: Identifier): boolean {
return (
id.type.kind === 'Function' && id.type.shapeId === 'BuiltInFireFunction'
);
}
export function isEffectEventFunctionType(id: Identifier): boolean {
return (
id.type.kind === 'Function' &&

View File

@@ -7,7 +7,12 @@
import {Binding, NodePath} from '@babel/traverse';
import * as t from '@babel/types';
import {CompilerError, ErrorCategory} from '../CompilerError';
import {
CompilerError,
CompilerDiagnostic,
CompilerErrorDetail,
ErrorCategory,
} from '../CompilerError';
import {Environment} from './Environment';
import {
BasicBlock,
@@ -110,7 +115,6 @@ export default class HIRBuilder {
#bindings: Bindings;
#env: Environment;
#exceptionHandlerStack: Array<BlockId> = [];
errors: CompilerError = new CompilerError();
/**
* Traversal context: counts the number of `fbt` tag parents
* of the current babel node.
@@ -148,6 +152,10 @@ export default class HIRBuilder {
this.#current = newBlock(this.#entry, options?.entryBlockKind ?? 'block');
}
recordError(error: CompilerDiagnostic | CompilerErrorDetail): void {
this.#env.recordError(error);
}
currentBlockKind(): BlockKind {
return this.#current.kind;
}
@@ -308,34 +316,28 @@ export default class HIRBuilder {
resolveBinding(node: t.Identifier): Identifier {
if (node.name === 'fbt') {
CompilerError.throwDiagnostic({
category: ErrorCategory.Todo,
reason: 'Support local variables named `fbt`',
description:
'Local variables named `fbt` may conflict with the fbt plugin and are not yet supported',
details: [
{
kind: 'error',
message: 'Rename to avoid conflict with fbt plugin',
loc: node.loc ?? GeneratedSource,
},
],
});
this.recordError(
new CompilerErrorDetail({
category: ErrorCategory.Todo,
reason: 'Support local variables named `fbt`',
description:
'Local variables named `fbt` may conflict with the fbt plugin and are not yet supported',
loc: node.loc ?? GeneratedSource,
suggestions: null,
}),
);
}
if (node.name === 'this') {
CompilerError.throwDiagnostic({
category: ErrorCategory.UnsupportedSyntax,
reason: '`this` is not supported syntax',
description:
'React Compiler does not support compiling functions that use `this`',
details: [
{
kind: 'error',
message: '`this` was used here',
loc: node.loc ?? GeneratedSource,
},
],
});
this.recordError(
new CompilerErrorDetail({
category: ErrorCategory.UnsupportedSyntax,
reason: '`this` is not supported syntax',
description:
'React Compiler does not support compiling functions that use `this`',
loc: node.loc ?? GeneratedSource,
suggestions: null,
}),
);
}
const originalName = node.name;
let name = originalName;
@@ -381,12 +383,15 @@ export default class HIRBuilder {
instr => instr.value.kind === 'FunctionExpression',
)
) {
CompilerError.throwTodo({
reason: `Support functions with unreachable code that may contain hoisted declarations`,
loc: block.instructions[0]?.loc ?? block.terminal.loc,
description: null,
suggestions: null,
});
this.recordError(
new CompilerErrorDetail({
reason: `Support functions with unreachable code that may contain hoisted declarations`,
loc: block.instructions[0]?.loc ?? block.terminal.loc,
description: null,
suggestions: null,
category: ErrorCategory.Todo,
}),
);
}
}
ir.blocks = rpoBlocks;

View File

@@ -383,12 +383,8 @@ export const BuiltInUseTransitionId = 'BuiltInUseTransition';
export const BuiltInUseOptimisticId = 'BuiltInUseOptimistic';
export const BuiltInSetOptimisticId = 'BuiltInSetOptimistic';
export const BuiltInStartTransitionId = 'BuiltInStartTransition';
export const BuiltInFireId = 'BuiltInFire';
export const BuiltInFireFunctionId = 'BuiltInFireFunction';
export const BuiltInUseEffectEventId = 'BuiltInUseEffectEvent';
export const BuiltInEffectEventId = 'BuiltInEffectEventFunction';
export const BuiltInAutodepsId = 'BuiltInAutoDepsId';
export const BuiltInEventHandlerId = 'BuiltInEventHandlerId';
// See getReanimatedModuleType() in Globals.ts — this is part of supporting Reanimated's ref-like types
export const ReanimatedSharedValueId = 'ReanimatedSharedValueId';
@@ -1249,19 +1245,6 @@ addFunction(
BuiltInEffectEventId,
);
addFunction(
BUILTIN_SHAPES,
[],
{
positionalParams: [],
restParam: Effect.ConditionallyMutate,
returnType: {kind: 'Poly'},
calleeEffect: Effect.ConditionallyMutate,
returnValueKind: ValueKind.Mutable,
},
BuiltInEventHandlerId,
);
/**
* MixedReadOnly =
* | primitive

View File

@@ -54,7 +54,7 @@ function lowerWithMutationAliasing(fn: HIRFunction): void {
deadCodeElimination(fn);
const functionEffects = inferMutationAliasingRanges(fn, {
isFunctionExpression: true,
}).unwrap();
});
rewriteInstructionKindsBasedOnReassignment(fn);
inferReactiveScopeVariables(fn);
fn.aliasingEffects = functionEffects;

View File

@@ -31,7 +31,6 @@ import {
makeInstructionId,
} from '../HIR';
import {createTemporaryPlace, markInstructionIds} from '../HIR/HIRBuilder';
import {Result} from '../Utils/Result';
type ManualMemoCallee = {
kind: 'useMemo' | 'useCallback';
@@ -294,7 +293,7 @@ function extractManualMemoizationArgs(
instr: TInstruction<CallExpression> | TInstruction<MethodCall>,
kind: 'useCallback' | 'useMemo',
sidemap: IdentifierSidemap,
errors: CompilerError,
env: Environment,
): {
fnPlace: Place;
depsList: Array<ManualMemoDependency> | null;
@@ -304,7 +303,7 @@ function extractManualMemoizationArgs(
Place | SpreadPattern | undefined
>;
if (fnPlace == null || fnPlace.kind !== 'Identifier') {
errors.pushDiagnostic(
env.recordError(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
reason: `Expected a callback function to be passed to ${kind}`,
@@ -336,7 +335,7 @@ function extractManualMemoizationArgs(
? sidemap.maybeDepsLists.get(depsListPlace.identifier.id)
: null;
if (maybeDepsList == null) {
errors.pushDiagnostic(
env.recordError(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
reason: `Expected the dependency list for ${kind} to be an array literal`,
@@ -355,7 +354,7 @@ function extractManualMemoizationArgs(
for (const dep of maybeDepsList.deps) {
const maybeDep = sidemap.maybeDeps.get(dep.identifier.id);
if (maybeDep == null) {
errors.pushDiagnostic(
env.recordError(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
reason: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
@@ -389,10 +388,7 @@ 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,
): Result<void, CompilerError> {
const errors = new CompilerError();
export function dropManualMemoization(func: HIRFunction): void {
const isValidationEnabled =
func.env.config.validatePreserveExistingMemoizationGuarantees ||
func.env.config.validateNoSetStateInRender ||
@@ -439,7 +435,7 @@ export function dropManualMemoization(
instr as TInstruction<CallExpression> | TInstruction<MethodCall>,
manualMemo.kind,
sidemap,
errors,
func.env,
);
if (memoDetails == null) {
@@ -467,7 +463,7 @@ export function dropManualMemoization(
* is rare and likely sketchy.
*/
if (!sidemap.functions.has(fnPlace.identifier.id)) {
errors.pushDiagnostic(
func.env.recordError(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
reason: `Expected the first argument to be an inline function expression`,
@@ -552,8 +548,6 @@ export function dropManualMemoization(
markInstructionIds(func.body);
}
}
return errors.asResult();
}
function findOptionalPlaces(fn: HIRFunction): Set<IdentifierId> {

View File

@@ -1,675 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import * as t from '@babel/types';
import {CompilerError, SourceLocation} from '..';
import {
ArrayExpression,
Effect,
FunctionExpression,
GeneratedSource,
HIRFunction,
IdentifierId,
Instruction,
makeInstructionId,
TInstruction,
InstructionId,
ScopeId,
ReactiveScopeDependency,
Place,
ReactiveScope,
ReactiveScopeDependencies,
Terminal,
isUseRefType,
isSetStateType,
isFireFunctionType,
makeScopeId,
HIR,
BasicBlock,
BlockId,
isEffectEventFunctionType,
} from '../HIR';
import {collectHoistablePropertyLoadsInInnerFn} from '../HIR/CollectHoistablePropertyLoads';
import {collectOptionalChainSidemap} from '../HIR/CollectOptionalChainDependencies';
import {ReactiveScopeDependencyTreeHIR} from '../HIR/DeriveMinimalDependenciesHIR';
import {DEFAULT_EXPORT} from '../HIR/Environment';
import {
createTemporaryPlace,
fixScopeAndIdentifierRanges,
markInstructionIds,
markPredecessors,
reversePostorderBlocks,
} from '../HIR/HIRBuilder';
import {
collectTemporariesSidemap,
DependencyCollectionContext,
handleInstruction,
} from '../HIR/PropagateScopeDependenciesHIR';
import {buildDependencyInstructions} from '../HIR/ScopeDependencyUtils';
import {
eachInstructionOperand,
eachTerminalOperand,
terminalFallthrough,
} from '../HIR/visitors';
import {empty} from '../Utils/Stack';
import {getOrInsertWith} from '../Utils/utils';
import {deadCodeElimination} from '../Optimization';
import {BuiltInAutodepsId} from '../HIR/ObjectShape';
/**
* Infers reactive dependencies captured by useEffect lambdas and adds them as
* a second argument to the useEffect call if no dependency array is provided.
*/
export function inferEffectDependencies(fn: HIRFunction): void {
const fnExpressions = new Map<
IdentifierId,
TInstruction<FunctionExpression>
>();
const autodepFnConfigs = new Map<string, Map<string, number>>();
for (const effectTarget of fn.env.config.inferEffectDependencies!) {
const moduleTargets = getOrInsertWith(
autodepFnConfigs,
effectTarget.function.source,
() => new Map<string, number>(),
);
moduleTargets.set(
effectTarget.function.importSpecifierName,
effectTarget.autodepsIndex,
);
}
const autodepFnLoads = new Map<IdentifierId, number>();
const autodepModuleLoads = new Map<IdentifierId, Map<string, number>>();
const scopeInfos = new Map<ScopeId, ReactiveScopeDependencies>();
const loadGlobals = new Set<IdentifierId>();
/**
* When inserting LoadLocals, we need to retain the reactivity of the base
* identifier, as later passes e.g. PruneNonReactiveDeps take the reactivity of
* a base identifier as the "maximal" reactivity of all its references.
* Concretely,
* reactive(Identifier i) = Union_{reference of i}(reactive(reference))
*/
const reactiveIds = inferReactiveIdentifiers(fn);
const rewriteBlocks: Array<BasicBlock> = [];
for (const [, block] of fn.body.blocks) {
if (block.terminal.kind === 'scope') {
const scopeBlock = fn.body.blocks.get(block.terminal.block)!;
if (
scopeBlock.instructions.length === 1 &&
scopeBlock.terminal.kind === 'goto' &&
scopeBlock.terminal.block === block.terminal.fallthrough
) {
scopeInfos.set(
block.terminal.scope.id,
block.terminal.scope.dependencies,
);
}
}
const rewriteInstrs: Array<SpliceInfo> = [];
for (const instr of block.instructions) {
const {value, lvalue} = instr;
if (value.kind === 'FunctionExpression') {
fnExpressions.set(
lvalue.identifier.id,
instr as TInstruction<FunctionExpression>,
);
} else if (value.kind === 'PropertyLoad') {
if (
typeof value.property === 'string' &&
autodepModuleLoads.has(value.object.identifier.id)
) {
const moduleTargets = autodepModuleLoads.get(
value.object.identifier.id,
)!;
const propertyName = value.property;
const numRequiredArgs = moduleTargets.get(propertyName);
if (numRequiredArgs != null) {
autodepFnLoads.set(lvalue.identifier.id, numRequiredArgs);
}
}
} else if (value.kind === 'LoadGlobal') {
loadGlobals.add(lvalue.identifier.id);
/*
* TODO: Handle properties on default exports, like
* import React from 'react';
* React.useEffect(...);
*/
if (value.binding.kind === 'ImportNamespace') {
const moduleTargets = autodepFnConfigs.get(value.binding.module);
if (moduleTargets != null) {
autodepModuleLoads.set(lvalue.identifier.id, moduleTargets);
}
}
if (
value.binding.kind === 'ImportSpecifier' ||
value.binding.kind === 'ImportDefault'
) {
const moduleTargets = autodepFnConfigs.get(value.binding.module);
if (moduleTargets != null) {
const importSpecifierName =
value.binding.kind === 'ImportSpecifier'
? value.binding.imported
: DEFAULT_EXPORT;
const numRequiredArgs = moduleTargets.get(importSpecifierName);
if (numRequiredArgs != null) {
autodepFnLoads.set(lvalue.identifier.id, numRequiredArgs);
}
}
}
} else if (
value.kind === 'CallExpression' ||
value.kind === 'MethodCall'
) {
const callee =
value.kind === 'CallExpression' ? value.callee : value.property;
const autodepsArgIndex = value.args.findIndex(
arg =>
arg.kind === 'Identifier' &&
arg.identifier.type.kind === 'Object' &&
arg.identifier.type.shapeId === BuiltInAutodepsId,
);
const autodepsArgExpectedIndex = autodepFnLoads.get(
callee.identifier.id,
);
if (
value.args.length > 0 &&
autodepsArgExpectedIndex != null &&
autodepsArgIndex === autodepsArgExpectedIndex &&
autodepFnLoads.has(callee.identifier.id) &&
value.args[0].kind === 'Identifier'
) {
// We have a useEffect call with no deps array, so we need to infer the deps
const effectDeps: Array<Place> = [];
const deps: ArrayExpression = {
kind: 'ArrayExpression',
elements: effectDeps,
loc: GeneratedSource,
};
const depsPlace = createTemporaryPlace(fn.env, GeneratedSource);
depsPlace.effect = Effect.Read;
const fnExpr = fnExpressions.get(value.args[0].identifier.id);
if (fnExpr != null) {
// We have a function expression, so we can infer its dependencies
const scopeInfo =
fnExpr.lvalue.identifier.scope != null
? scopeInfos.get(fnExpr.lvalue.identifier.scope.id)
: null;
let minimalDeps: Set<ReactiveScopeDependency>;
if (scopeInfo != null) {
minimalDeps = new Set(scopeInfo);
} else {
minimalDeps = inferMinimalDependencies(fnExpr);
}
/**
* Step 1: push dependencies to the effect deps array
*
* Note that it's invalid to prune all non-reactive deps in this pass, see
* the `infer-effect-deps/pruned-nonreactive-obj` fixture for an
* explanation.
*/
const usedDeps = [];
for (const maybeDep of minimalDeps) {
if (
((isUseRefType(maybeDep.identifier) ||
isSetStateType(maybeDep.identifier)) &&
!reactiveIds.has(maybeDep.identifier.id)) ||
isFireFunctionType(maybeDep.identifier) ||
isEffectEventFunctionType(maybeDep.identifier)
) {
// exclude non-reactive hook results, which will never be in a memo block
continue;
}
const dep = truncateDepAtCurrent(maybeDep);
const {place, value, exitBlockId} = buildDependencyInstructions(
dep,
fn.env,
);
rewriteInstrs.push({
kind: 'block',
location: instr.id,
value,
exitBlockId: exitBlockId,
});
effectDeps.push(place);
usedDeps.push(dep);
}
// For LSP autodeps feature.
const decorations: Array<t.SourceLocation> = [];
for (const loc of collectDepUsages(usedDeps, fnExpr.value)) {
if (typeof loc === 'symbol') {
continue;
}
decorations.push(loc);
}
if (typeof value.loc !== 'symbol') {
fn.env.logger?.logEvent(fn.env.filename, {
kind: 'AutoDepsDecorations',
fnLoc: value.loc,
decorations,
});
}
// Step 2: push the inferred deps array as an argument of the useEffect
rewriteInstrs.push({
kind: 'instr',
location: instr.id,
value: {
id: makeInstructionId(0),
loc: GeneratedSource,
lvalue: {...depsPlace, effect: Effect.Mutate},
value: deps,
effects: null,
},
});
value.args[autodepsArgIndex] = {
...depsPlace,
effect: Effect.Freeze,
};
fn.env.inferredEffectLocations.add(callee.loc);
} else if (loadGlobals.has(value.args[0].identifier.id)) {
// Global functions have no reactive dependencies, so we can insert an empty array
rewriteInstrs.push({
kind: 'instr',
location: instr.id,
value: {
id: makeInstructionId(0),
loc: GeneratedSource,
lvalue: {...depsPlace, effect: Effect.Mutate},
value: deps,
effects: null,
},
});
value.args[autodepsArgIndex] = {
...depsPlace,
effect: Effect.Freeze,
};
fn.env.inferredEffectLocations.add(callee.loc);
}
} else if (
value.args.length >= 2 &&
value.args.length - 1 === autodepFnLoads.get(callee.identifier.id) &&
value.args[0] != null &&
value.args[0].kind === 'Identifier'
) {
const penultimateArg = value.args[value.args.length - 2];
const depArrayArg = value.args[value.args.length - 1];
if (
depArrayArg.kind !== 'Spread' &&
penultimateArg.kind !== 'Spread' &&
typeof depArrayArg.loc !== 'symbol' &&
typeof penultimateArg.loc !== 'symbol' &&
typeof value.loc !== 'symbol'
) {
fn.env.logger?.logEvent(fn.env.filename, {
kind: 'AutoDepsEligible',
fnLoc: value.loc,
depArrayLoc: {
...depArrayArg.loc,
start: penultimateArg.loc.end,
end: depArrayArg.loc.end,
},
});
}
}
}
}
rewriteSplices(block, rewriteInstrs, rewriteBlocks);
}
if (rewriteBlocks.length > 0) {
for (const block of rewriteBlocks) {
fn.body.blocks.set(block.id, block);
}
/**
* Fixup the HIR to restore RPO, ensure correct predecessors, and renumber
* instructions.
*/
reversePostorderBlocks(fn.body);
markPredecessors(fn.body);
// Renumber instructions and fix scope ranges
markInstructionIds(fn.body);
fixScopeAndIdentifierRanges(fn.body);
deadCodeElimination(fn);
fn.env.hasInferredEffect = true;
}
}
function truncateDepAtCurrent(
dep: ReactiveScopeDependency,
): ReactiveScopeDependency {
const idx = dep.path.findIndex(path => path.property === 'current');
if (idx === -1) {
return dep;
} else {
return {...dep, path: dep.path.slice(0, idx)};
}
}
type SpliceInfo =
| {kind: 'instr'; location: InstructionId; value: Instruction}
| {
kind: 'block';
location: InstructionId;
value: HIR;
exitBlockId: BlockId;
};
function rewriteSplices(
originalBlock: BasicBlock,
splices: Array<SpliceInfo>,
rewriteBlocks: Array<BasicBlock>,
): void {
if (splices.length === 0) {
return;
}
/**
* Splice instructions or value blocks into the original block.
* --- original block ---
* bb_original
* instr1
* ...
* instr2 <-- splice location
* instr3
* ...
* <original terminal>
*
* If there is more than one block in the splice, this means that we're
* splicing in a set of value-blocks of the following structure:
* --- blocks we're splicing in ---
* bb_entry:
* instrEntry
* ...
* <splice terminal> fallthrough=bb_exit
*
* bb1(value):
* ...
*
* bb_exit:
* instrExit
* ...
* <synthetic terminal>
*
*
* --- rewritten blocks ---
* bb_original
* instr1
* ... (original instructions)
* instr2
* instrEntry
* ... (spliced instructions)
* <splice terminal> fallthrough=bb_exit
*
* bb1(value):
* ...
*
* bb_exit:
* instrExit
* ... (spliced instructions)
* instr3
* ... (original instructions)
* <original terminal>
*/
const originalInstrs = originalBlock.instructions;
let currBlock: BasicBlock = {...originalBlock, instructions: []};
rewriteBlocks.push(currBlock);
let cursor = 0;
for (const rewrite of splices) {
while (originalInstrs[cursor].id < rewrite.location) {
CompilerError.invariant(
originalInstrs[cursor].id < originalInstrs[cursor + 1].id,
{
reason:
'[InferEffectDependencies] Internal invariant broken: expected block instructions to be sorted',
loc: originalInstrs[cursor].loc,
},
);
currBlock.instructions.push(originalInstrs[cursor]);
cursor++;
}
CompilerError.invariant(originalInstrs[cursor].id === rewrite.location, {
reason:
'[InferEffectDependencies] Internal invariant broken: splice location not found',
loc: originalInstrs[cursor].loc,
});
if (rewrite.kind === 'instr') {
currBlock.instructions.push(rewrite.value);
} else if (rewrite.kind === 'block') {
const {entry, blocks} = rewrite.value;
const entryBlock = blocks.get(entry)!;
// splice in all instructions from the entry block
currBlock.instructions.push(...entryBlock.instructions);
if (blocks.size > 1) {
/**
* We're splicing in a set of value-blocks, which means we need
* to push new blocks and update terminals.
*/
CompilerError.invariant(
terminalFallthrough(entryBlock.terminal) === rewrite.exitBlockId,
{
reason:
'[InferEffectDependencies] Internal invariant broken: expected entry block to have a fallthrough',
loc: entryBlock.terminal.loc,
},
);
const originalTerminal = currBlock.terminal;
currBlock.terminal = entryBlock.terminal;
for (const [id, block] of blocks) {
if (id === entry) {
continue;
}
if (id === rewrite.exitBlockId) {
block.terminal = originalTerminal;
currBlock = block;
}
rewriteBlocks.push(block);
}
}
}
}
currBlock.instructions.push(...originalInstrs.slice(cursor));
}
function inferReactiveIdentifiers(fn: HIRFunction): Set<IdentifierId> {
const reactiveIds: Set<IdentifierId> = new Set();
for (const [, block] of fn.body.blocks) {
for (const instr of block.instructions) {
/**
* No need to traverse into nested functions as
* 1. their effects are recorded in `LoweredFunction.dependencies`
* 2. we don't mark `reactive` in these anyways
*/
for (const place of eachInstructionOperand(instr)) {
if (place.reactive) {
reactiveIds.add(place.identifier.id);
}
}
}
for (const place of eachTerminalOperand(block.terminal)) {
if (place.reactive) {
reactiveIds.add(place.identifier.id);
}
}
}
return reactiveIds;
}
function collectDepUsages(
deps: Array<ReactiveScopeDependency>,
fnExpr: FunctionExpression,
): Array<SourceLocation> {
const identifiers: Map<IdentifierId, ReactiveScopeDependency> = new Map();
const loadedDeps: Set<IdentifierId> = new Set();
const sourceLocations = [];
for (const dep of deps) {
identifiers.set(dep.identifier.id, dep);
}
for (const [, block] of fnExpr.loweredFunc.func.body.blocks) {
for (const instr of block.instructions) {
if (
instr.value.kind === 'LoadLocal' &&
identifiers.has(instr.value.place.identifier.id)
) {
loadedDeps.add(instr.lvalue.identifier.id);
}
for (const place of eachInstructionOperand(instr)) {
if (loadedDeps.has(place.identifier.id)) {
// TODO(@jbrown215): handle member exprs!!
sourceLocations.push(place.identifier.loc);
}
}
}
}
return sourceLocations;
}
function inferMinimalDependencies(
fnInstr: TInstruction<FunctionExpression>,
): Set<ReactiveScopeDependency> {
const fn = fnInstr.value.loweredFunc.func;
const temporaries = collectTemporariesSidemap(fn, new Set());
const {
hoistableObjects,
processedInstrsInOptional,
temporariesReadInOptional,
} = collectOptionalChainSidemap(fn);
const hoistablePropertyLoads = collectHoistablePropertyLoadsInInnerFn(
fnInstr,
temporaries,
hoistableObjects,
);
const hoistableToFnEntry = hoistablePropertyLoads.get(fn.body.entry);
CompilerError.invariant(hoistableToFnEntry != null, {
reason:
'[InferEffectDependencies] Internal invariant broken: missing entry block',
loc: fnInstr.loc,
});
const dependencies = inferDependencies(
fnInstr,
new Map([...temporaries, ...temporariesReadInOptional]),
processedInstrsInOptional,
);
const tree = new ReactiveScopeDependencyTreeHIR(
[...hoistableToFnEntry.assumedNonNullObjects].map(o => o.fullPath),
);
for (const dep of dependencies) {
tree.addDependency({...dep});
}
return tree.deriveMinimalDependencies();
}
function inferDependencies(
fnInstr: TInstruction<FunctionExpression>,
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
processedInstrsInOptional: ReadonlySet<Instruction | Terminal>,
): Set<ReactiveScopeDependency> {
const fn = fnInstr.value.loweredFunc.func;
const context = new DependencyCollectionContext(
new Set(),
temporaries,
processedInstrsInOptional,
);
for (const dep of fn.context) {
context.declare(dep.identifier, {
id: makeInstructionId(0),
scope: empty(),
});
}
const placeholderScope: ReactiveScope = {
id: makeScopeId(0),
range: {
start: fnInstr.id,
end: makeInstructionId(fnInstr.id + 1),
},
dependencies: new Set(),
reassignments: new Set(),
declarations: new Map(),
earlyReturnValue: null,
merged: new Set(),
loc: GeneratedSource,
};
context.enterScope(placeholderScope);
inferDependenciesInFn(fn, context, temporaries);
context.exitScope(placeholderScope, false);
const resultUnfiltered = context.deps.get(placeholderScope);
CompilerError.invariant(resultUnfiltered != null, {
reason:
'[InferEffectDependencies] Internal invariant broken: missing scope dependencies',
loc: fn.loc,
});
const fnContext = new Set(fn.context.map(dep => dep.identifier.id));
const result = new Set<ReactiveScopeDependency>();
for (const dep of resultUnfiltered) {
if (fnContext.has(dep.identifier.id)) {
result.add(dep);
}
}
return result;
}
function inferDependenciesInFn(
fn: HIRFunction,
context: DependencyCollectionContext,
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
): void {
for (const [, block] of fn.body.blocks) {
// Record referenced optional chains in phis
for (const phi of block.phis) {
for (const operand of phi.operands) {
const maybeOptionalChain = temporaries.get(operand[1].identifier.id);
if (maybeOptionalChain) {
context.visitDependency(maybeOptionalChain);
}
}
}
for (const instr of block.instructions) {
if (
instr.value.kind === 'FunctionExpression' ||
instr.value.kind === 'ObjectMethod'
) {
context.declare(instr.lvalue.identifier, {
id: instr.id,
scope: context.currentScope,
});
/**
* Recursively visit the inner function to extract dependencies
*/
const innerFn = instr.value.loweredFunc.func;
context.enterInnerFn(instr as TInstruction<FunctionExpression>, () => {
inferDependenciesInFn(innerFn, context, temporaries);
});
} else {
handleInstruction(instr, context);
}
}
}
}

View File

@@ -45,7 +45,7 @@ import {
eachTerminalOperand,
eachTerminalSuccessor,
} from '../HIR/visitors';
import {Ok, Result} from '../Utils/Result';
import {
assertExhaustive,
getOrInsertDefault,
@@ -100,7 +100,7 @@ export function inferMutationAliasingEffects(
{isFunctionExpression}: {isFunctionExpression: boolean} = {
isFunctionExpression: false,
},
): Result<void, CompilerError> {
): void {
const initialState = InferenceState.empty(fn.env, isFunctionExpression);
// Map of blocks to the last (merged) incoming state that was processed
@@ -220,7 +220,7 @@ export function inferMutationAliasingEffects(
}
}
}
return Ok(undefined);
return;
}
function findHoistedContextDeclarations(

View File

@@ -20,13 +20,14 @@ import {
Place,
isPrimitiveType,
} from '../HIR/HIR';
import {Environment} from '../HIR/Environment';
import {
eachInstructionLValue,
eachInstructionValueOperand,
eachTerminalOperand,
} from '../HIR/visitors';
import {assertExhaustive, getOrInsertWith} from '../Utils/utils';
import {Err, Ok, Result} from '../Utils/Result';
import {AliasingEffect, MutationReason} from './AliasingEffects';
/**
@@ -74,7 +75,7 @@ import {AliasingEffect, MutationReason} from './AliasingEffects';
export function inferMutationAliasingRanges(
fn: HIRFunction,
{isFunctionExpression}: {isFunctionExpression: boolean},
): Result<Array<AliasingEffect>, CompilerError> {
): Array<AliasingEffect> {
// The set of externally-visible effects
const functionEffects: Array<AliasingEffect> = [];
@@ -107,7 +108,7 @@ export function inferMutationAliasingRanges(
let index = 0;
const errors = new CompilerError();
const shouldRecordErrors = !isFunctionExpression && fn.env.enableValidations;
for (const param of [...fn.params, ...fn.context, fn.returns]) {
const place = param.kind === 'Identifier' ? param : param.place;
@@ -200,7 +201,9 @@ export function inferMutationAliasingRanges(
effect.kind === 'MutateGlobal' ||
effect.kind === 'Impure'
) {
errors.pushDiagnostic(effect.error);
if (shouldRecordErrors) {
fn.env.recordError(effect.error);
}
functionEffects.push(effect);
} else if (effect.kind === 'Render') {
renders.push({index: index++, place: effect.place});
@@ -245,11 +248,15 @@ export function inferMutationAliasingRanges(
mutation.kind,
mutation.place.loc,
mutation.reason,
errors,
shouldRecordErrors ? fn.env : null,
);
}
for (const render of renders) {
state.render(render.index, render.place.identifier, errors);
state.render(
render.index,
render.place.identifier,
shouldRecordErrors ? fn.env : null,
);
}
for (const param of [...fn.context, ...fn.params]) {
const place = param.kind === 'Identifier' ? param : param.place;
@@ -498,7 +505,6 @@ export function inferMutationAliasingRanges(
* would be transitively mutated needs a capture relationship.
*/
const tracked: Array<Place> = [];
const ignoredErrors = new CompilerError();
for (const param of [...fn.params, ...fn.context, fn.returns]) {
const place = param.kind === 'Identifier' ? param : param.place;
tracked.push(place);
@@ -513,7 +519,7 @@ export function inferMutationAliasingRanges(
MutationKind.Conditional,
into.loc,
null,
ignoredErrors,
null,
);
for (const from of tracked) {
if (
@@ -547,19 +553,17 @@ export function inferMutationAliasingRanges(
}
}
if (errors.hasAnyErrors() && !isFunctionExpression) {
return Err(errors);
}
return Ok(functionEffects);
return functionEffects;
}
function appendFunctionErrors(errors: CompilerError, fn: HIRFunction): void {
function appendFunctionErrors(env: Environment | null, fn: HIRFunction): void {
if (env == null) return;
for (const effect of fn.aliasingEffects ?? []) {
switch (effect.kind) {
case 'Impure':
case 'MutateFrozen':
case 'MutateGlobal': {
errors.pushDiagnostic(effect.error);
env.recordError(effect.error);
break;
}
}
@@ -660,7 +664,7 @@ class AliasingState {
}
}
render(index: number, start: Identifier, errors: CompilerError): void {
render(index: number, start: Identifier, env: Environment | null): void {
const seen = new Set<Identifier>();
const queue: Array<Identifier> = [start];
while (queue.length !== 0) {
@@ -674,7 +678,7 @@ class AliasingState {
continue;
}
if (node.value.kind === 'Function') {
appendFunctionErrors(errors, node.value.function);
appendFunctionErrors(env, node.value.function);
}
for (const [alias, when] of node.createdFrom) {
if (when >= index) {
@@ -706,7 +710,7 @@ class AliasingState {
startKind: MutationKind,
loc: SourceLocation,
reason: MutationReason | null,
errors: CompilerError,
env: Environment | null,
): void {
const seen = new Map<Identifier, MutationKind>();
const queue: Array<{
@@ -738,7 +742,7 @@ class AliasingState {
node.transitive == null &&
node.local == null
) {
appendFunctionErrors(errors, node.value.function);
appendFunctionErrors(env, node.value.function);
}
if (transitive) {
if (node.transitive == null || node.transitive.kind < kind) {

View File

@@ -9,4 +9,3 @@ export {default as analyseFunctions} from './AnalyseFunctions';
export {dropManualMemoization} from './DropManualMemoization';
export {inferReactivePlaces} from './InferReactivePlaces';
export {inlineImmediatelyInvokedFunctionExpressions} from './InlineImmediatelyInvokedFunctionExpressions';
export {inferEffectDependencies} from './InferEffectDependencies';

View File

@@ -1,790 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {
BasicBlock,
BlockId,
BuiltinTag,
DeclarationId,
Effect,
forkTemporaryIdentifier,
GotoTerminal,
GotoVariant,
HIRFunction,
Identifier,
IfTerminal,
Instruction,
InstructionKind,
JsxAttribute,
makeInstructionId,
makePropertyLiteral,
ObjectProperty,
Phi,
Place,
promoteTemporary,
SpreadPattern,
} from '../HIR';
import {
createTemporaryPlace,
fixScopeAndIdentifierRanges,
markInstructionIds,
markPredecessors,
reversePostorderBlocks,
} from '../HIR/HIRBuilder';
import {CompilerError, EnvironmentConfig} from '..';
import {
mapInstructionLValues,
mapInstructionOperands,
mapInstructionValueOperands,
mapTerminalOperands,
} from '../HIR/visitors';
import {ErrorCategory} from '../CompilerError';
type InlinedJsxDeclarationMap = Map<
DeclarationId,
{identifier: Identifier; blockIdsToIgnore: Set<BlockId>}
>;
/**
* A prod-only, RN optimization to replace JSX with inlined ReactElement object literals
*
* Example:
* <>foo</>
* _______________
* let t1;
* if (__DEV__) {
* t1 = <>foo</>
* } else {
* t1 = {...}
* }
*
*/
export function inlineJsxTransform(
fn: HIRFunction,
inlineJsxTransformConfig: NonNullable<
EnvironmentConfig['inlineJsxTransform']
>,
): void {
const inlinedJsxDeclarations: InlinedJsxDeclarationMap = new Map();
/**
* Step 1: Codegen the conditional and ReactElement object literal
*/
for (const [_, currentBlock] of [...fn.body.blocks]) {
let fallthroughBlockInstructions: Array<Instruction> | null = null;
const instructionCount = currentBlock.instructions.length;
for (let i = 0; i < instructionCount; i++) {
const instr = currentBlock.instructions[i]!;
// TODO: Support value blocks
if (currentBlock.kind === 'value') {
fn.env.logger?.logEvent(fn.env.filename, {
kind: 'CompileDiagnostic',
fnLoc: null,
detail: {
category: ErrorCategory.Todo,
reason: 'JSX Inlining is not supported on value blocks',
loc: instr.loc,
},
});
continue;
}
switch (instr.value.kind) {
case 'JsxExpression':
case 'JsxFragment': {
/**
* Split into blocks for new IfTerminal:
* current, then, else, fallthrough
*/
const currentBlockInstructions = currentBlock.instructions.slice(
0,
i,
);
const thenBlockInstructions = currentBlock.instructions.slice(
i,
i + 1,
);
const elseBlockInstructions: Array<Instruction> = [];
fallthroughBlockInstructions ??= currentBlock.instructions.slice(
i + 1,
);
const fallthroughBlockId = fn.env.nextBlockId;
const fallthroughBlock: BasicBlock = {
kind: currentBlock.kind,
id: fallthroughBlockId,
instructions: fallthroughBlockInstructions,
terminal: currentBlock.terminal,
preds: new Set(),
phis: new Set(),
};
/**
* Complete current block
* - Add instruction for variable declaration
* - Add instruction for LoadGlobal used by conditional
* - End block with a new IfTerminal
*/
const varPlace = createTemporaryPlace(fn.env, instr.value.loc);
promoteTemporary(varPlace.identifier);
const varLValuePlace = createTemporaryPlace(fn.env, instr.value.loc);
const thenVarPlace = {
...varPlace,
identifier: forkTemporaryIdentifier(
fn.env.nextIdentifierId,
varPlace.identifier,
),
};
const elseVarPlace = {
...varPlace,
identifier: forkTemporaryIdentifier(
fn.env.nextIdentifierId,
varPlace.identifier,
),
};
const varInstruction: Instruction = {
id: makeInstructionId(0),
lvalue: {...varLValuePlace},
value: {
kind: 'DeclareLocal',
lvalue: {place: {...varPlace}, kind: InstructionKind.Let},
type: null,
loc: instr.value.loc,
},
effects: null,
loc: instr.loc,
};
currentBlockInstructions.push(varInstruction);
const devGlobalPlace = createTemporaryPlace(fn.env, instr.value.loc);
const devGlobalInstruction: Instruction = {
id: makeInstructionId(0),
lvalue: {...devGlobalPlace, effect: Effect.Mutate},
value: {
kind: 'LoadGlobal',
binding: {
kind: 'Global',
name: inlineJsxTransformConfig.globalDevVar,
},
loc: instr.value.loc,
},
effects: null,
loc: instr.loc,
};
currentBlockInstructions.push(devGlobalInstruction);
const thenBlockId = fn.env.nextBlockId;
const elseBlockId = fn.env.nextBlockId;
const ifTerminal: IfTerminal = {
kind: 'if',
test: {...devGlobalPlace, effect: Effect.Read},
consequent: thenBlockId,
alternate: elseBlockId,
fallthrough: fallthroughBlockId,
loc: instr.loc,
id: makeInstructionId(0),
};
currentBlock.instructions = currentBlockInstructions;
currentBlock.terminal = ifTerminal;
/**
* Set up then block where we put the original JSX return
*/
const thenBlock: BasicBlock = {
id: thenBlockId,
instructions: thenBlockInstructions,
kind: 'block',
phis: new Set(),
preds: new Set(),
terminal: {
kind: 'goto',
block: fallthroughBlockId,
variant: GotoVariant.Break,
id: makeInstructionId(0),
loc: instr.loc,
},
};
fn.body.blocks.set(thenBlockId, thenBlock);
const resassignElsePlace = createTemporaryPlace(
fn.env,
instr.value.loc,
);
const reassignElseInstruction: Instruction = {
id: makeInstructionId(0),
lvalue: {...resassignElsePlace},
value: {
kind: 'StoreLocal',
lvalue: {
place: elseVarPlace,
kind: InstructionKind.Reassign,
},
value: {...instr.lvalue},
type: null,
loc: instr.value.loc,
},
effects: null,
loc: instr.loc,
};
thenBlockInstructions.push(reassignElseInstruction);
/**
* Set up else block where we add new codegen
*/
const elseBlockTerminal: GotoTerminal = {
kind: 'goto',
block: fallthroughBlockId,
variant: GotoVariant.Break,
id: makeInstructionId(0),
loc: instr.loc,
};
const elseBlock: BasicBlock = {
id: elseBlockId,
instructions: elseBlockInstructions,
kind: 'block',
phis: new Set(),
preds: new Set(),
terminal: elseBlockTerminal,
};
fn.body.blocks.set(elseBlockId, elseBlock);
/**
* ReactElement object literal codegen
*/
const {refProperty, keyProperty, propsProperty} =
createPropsProperties(
fn,
instr,
elseBlockInstructions,
instr.value.kind === 'JsxExpression' ? instr.value.props : [],
instr.value.children,
);
const reactElementInstructionPlace = createTemporaryPlace(
fn.env,
instr.value.loc,
);
const reactElementInstruction: Instruction = {
id: makeInstructionId(0),
lvalue: {...reactElementInstructionPlace, effect: Effect.Store},
value: {
kind: 'ObjectExpression',
properties: [
createSymbolProperty(
fn,
instr,
elseBlockInstructions,
'$$typeof',
inlineJsxTransformConfig.elementSymbol,
),
instr.value.kind === 'JsxExpression'
? createTagProperty(
fn,
instr,
elseBlockInstructions,
instr.value.tag,
)
: createSymbolProperty(
fn,
instr,
elseBlockInstructions,
'type',
'react.fragment',
),
refProperty,
keyProperty,
propsProperty,
],
loc: instr.value.loc,
},
effects: null,
loc: instr.loc,
};
elseBlockInstructions.push(reactElementInstruction);
const reassignConditionalInstruction: Instruction = {
id: makeInstructionId(0),
lvalue: {...createTemporaryPlace(fn.env, instr.value.loc)},
value: {
kind: 'StoreLocal',
lvalue: {
place: {...elseVarPlace},
kind: InstructionKind.Reassign,
},
value: {...reactElementInstruction.lvalue},
type: null,
loc: instr.value.loc,
},
effects: null,
loc: instr.loc,
};
elseBlockInstructions.push(reassignConditionalInstruction);
/**
* Create phis to reassign the var
*/
const operands: Map<BlockId, Place> = new Map();
operands.set(thenBlockId, {
...elseVarPlace,
});
operands.set(elseBlockId, {
...thenVarPlace,
});
const phiIdentifier = forkTemporaryIdentifier(
fn.env.nextIdentifierId,
varPlace.identifier,
);
const phiPlace = {
...createTemporaryPlace(fn.env, instr.value.loc),
identifier: phiIdentifier,
};
const phis: Set<Phi> = new Set([
{
kind: 'Phi',
operands,
place: phiPlace,
},
]);
fallthroughBlock.phis = phis;
fn.body.blocks.set(fallthroughBlockId, fallthroughBlock);
/**
* Track this JSX instruction so we can replace references in step 2
*/
inlinedJsxDeclarations.set(instr.lvalue.identifier.declarationId, {
identifier: phiIdentifier,
blockIdsToIgnore: new Set([thenBlockId, elseBlockId]),
});
break;
}
case 'FunctionExpression':
case 'ObjectMethod': {
inlineJsxTransform(
instr.value.loweredFunc.func,
inlineJsxTransformConfig,
);
break;
}
}
}
}
/**
* Step 2: Replace declarations with new phi values
*/
for (const [blockId, block] of fn.body.blocks) {
for (const instr of block.instructions) {
mapInstructionOperands(instr, place =>
handlePlace(place, blockId, inlinedJsxDeclarations),
);
mapInstructionLValues(instr, lvalue =>
handlelValue(lvalue, blockId, inlinedJsxDeclarations),
);
mapInstructionValueOperands(instr.value, place =>
handlePlace(place, blockId, inlinedJsxDeclarations),
);
}
mapTerminalOperands(block.terminal, place =>
handlePlace(place, blockId, inlinedJsxDeclarations),
);
if (block.terminal.kind === 'scope') {
const scope = block.terminal.scope;
for (const dep of scope.dependencies) {
dep.identifier = handleIdentifier(
dep.identifier,
inlinedJsxDeclarations,
);
}
for (const [origId, decl] of [...scope.declarations]) {
const newDecl = handleIdentifier(
decl.identifier,
inlinedJsxDeclarations,
);
if (newDecl.id !== origId) {
scope.declarations.delete(origId);
scope.declarations.set(decl.identifier.id, {
identifier: newDecl,
scope: decl.scope,
});
}
}
}
}
/**
* Step 3: Fixup the HIR
* Restore RPO, ensure correct predecessors, renumber instructions, fix scope and ranges.
*/
reversePostorderBlocks(fn.body);
markPredecessors(fn.body);
markInstructionIds(fn.body);
fixScopeAndIdentifierRanges(fn.body);
}
function createSymbolProperty(
fn: HIRFunction,
instr: Instruction,
nextInstructions: Array<Instruction>,
propertyName: string,
symbolName: string,
): ObjectProperty {
const symbolPlace = createTemporaryPlace(fn.env, instr.value.loc);
const symbolInstruction: Instruction = {
id: makeInstructionId(0),
lvalue: {...symbolPlace, effect: Effect.Mutate},
value: {
kind: 'LoadGlobal',
binding: {kind: 'Global', name: 'Symbol'},
loc: instr.value.loc,
},
effects: null,
loc: instr.loc,
};
nextInstructions.push(symbolInstruction);
const symbolForPlace = createTemporaryPlace(fn.env, instr.value.loc);
const symbolForInstruction: Instruction = {
id: makeInstructionId(0),
lvalue: {...symbolForPlace, effect: Effect.Read},
value: {
kind: 'PropertyLoad',
object: {...symbolInstruction.lvalue},
property: makePropertyLiteral('for'),
loc: instr.value.loc,
},
effects: null,
loc: instr.loc,
};
nextInstructions.push(symbolForInstruction);
const symbolValuePlace = createTemporaryPlace(fn.env, instr.value.loc);
const symbolValueInstruction: Instruction = {
id: makeInstructionId(0),
lvalue: {...symbolValuePlace, effect: Effect.Mutate},
value: {
kind: 'Primitive',
value: symbolName,
loc: instr.value.loc,
},
effects: null,
loc: instr.loc,
};
nextInstructions.push(symbolValueInstruction);
const $$typeofPlace = createTemporaryPlace(fn.env, instr.value.loc);
const $$typeofInstruction: Instruction = {
id: makeInstructionId(0),
lvalue: {...$$typeofPlace, effect: Effect.Mutate},
value: {
kind: 'MethodCall',
receiver: symbolInstruction.lvalue,
property: symbolForInstruction.lvalue,
args: [symbolValueInstruction.lvalue],
loc: instr.value.loc,
},
effects: null,
loc: instr.loc,
};
const $$typeofProperty: ObjectProperty = {
kind: 'ObjectProperty',
key: {name: propertyName, kind: 'string'},
type: 'property',
place: {...$$typeofPlace, effect: Effect.Capture},
};
nextInstructions.push($$typeofInstruction);
return $$typeofProperty;
}
function createTagProperty(
fn: HIRFunction,
instr: Instruction,
nextInstructions: Array<Instruction>,
componentTag: BuiltinTag | Place,
): ObjectProperty {
let tagProperty: ObjectProperty;
switch (componentTag.kind) {
case 'BuiltinTag': {
const tagPropertyPlace = createTemporaryPlace(fn.env, instr.value.loc);
const tagInstruction: Instruction = {
id: makeInstructionId(0),
lvalue: {...tagPropertyPlace, effect: Effect.Mutate},
value: {
kind: 'Primitive',
value: componentTag.name,
loc: instr.value.loc,
},
effects: null,
loc: instr.loc,
};
tagProperty = {
kind: 'ObjectProperty',
key: {name: 'type', kind: 'string'},
type: 'property',
place: {...tagPropertyPlace, effect: Effect.Capture},
};
nextInstructions.push(tagInstruction);
break;
}
case 'Identifier': {
tagProperty = {
kind: 'ObjectProperty',
key: {name: 'type', kind: 'string'},
type: 'property',
place: {...componentTag, effect: Effect.Capture},
};
break;
}
}
return tagProperty;
}
function createPropsProperties(
fn: HIRFunction,
instr: Instruction,
nextInstructions: Array<Instruction>,
propAttributes: Array<JsxAttribute>,
children: Array<Place> | null,
): {
refProperty: ObjectProperty;
keyProperty: ObjectProperty;
propsProperty: ObjectProperty;
} {
let refProperty: ObjectProperty | undefined;
let keyProperty: ObjectProperty | undefined;
const props: Array<ObjectProperty | SpreadPattern> = [];
const jsxAttributesWithoutKey = propAttributes.filter(
p => p.kind === 'JsxAttribute' && p.name !== 'key',
);
const jsxSpreadAttributes = propAttributes.filter(
p => p.kind === 'JsxSpreadAttribute',
);
const spreadPropsOnly =
jsxAttributesWithoutKey.length === 0 && jsxSpreadAttributes.length === 1;
propAttributes.forEach(prop => {
switch (prop.kind) {
case 'JsxAttribute': {
switch (prop.name) {
case 'key': {
keyProperty = {
kind: 'ObjectProperty',
key: {name: 'key', kind: 'string'},
type: 'property',
place: {...prop.place},
};
break;
}
case 'ref': {
/**
* In the current JSX implementation, ref is both
* a property on the element and a property on props.
*/
refProperty = {
kind: 'ObjectProperty',
key: {name: 'ref', kind: 'string'},
type: 'property',
place: {...prop.place},
};
const refPropProperty: ObjectProperty = {
kind: 'ObjectProperty',
key: {name: 'ref', kind: 'string'},
type: 'property',
place: {...prop.place},
};
props.push(refPropProperty);
break;
}
default: {
const attributeProperty: ObjectProperty = {
kind: 'ObjectProperty',
key: {name: prop.name, kind: 'string'},
type: 'property',
place: {...prop.place},
};
props.push(attributeProperty);
}
}
break;
}
case 'JsxSpreadAttribute': {
props.push({
kind: 'Spread',
place: {...prop.argument},
});
break;
}
}
});
const propsPropertyPlace = createTemporaryPlace(fn.env, instr.value.loc);
if (children) {
let childrenPropProperty: ObjectProperty;
if (children.length === 1) {
childrenPropProperty = {
kind: 'ObjectProperty',
key: {name: 'children', kind: 'string'},
type: 'property',
place: {...children[0], effect: Effect.Capture},
};
} else {
const childrenPropPropertyPlace = createTemporaryPlace(
fn.env,
instr.value.loc,
);
const childrenPropInstruction: Instruction = {
id: makeInstructionId(0),
lvalue: {...childrenPropPropertyPlace, effect: Effect.Mutate},
value: {
kind: 'ArrayExpression',
elements: [...children],
loc: instr.value.loc,
},
effects: null,
loc: instr.loc,
};
nextInstructions.push(childrenPropInstruction);
childrenPropProperty = {
kind: 'ObjectProperty',
key: {name: 'children', kind: 'string'},
type: 'property',
place: {...childrenPropPropertyPlace, effect: Effect.Capture},
};
}
props.push(childrenPropProperty);
}
if (refProperty == null) {
const refPropertyPlace = createTemporaryPlace(fn.env, instr.value.loc);
const refInstruction: Instruction = {
id: makeInstructionId(0),
lvalue: {...refPropertyPlace, effect: Effect.Mutate},
value: {
kind: 'Primitive',
value: null,
loc: instr.value.loc,
},
effects: null,
loc: instr.loc,
};
refProperty = {
kind: 'ObjectProperty',
key: {name: 'ref', kind: 'string'},
type: 'property',
place: {...refPropertyPlace, effect: Effect.Capture},
};
nextInstructions.push(refInstruction);
}
if (keyProperty == null) {
const keyPropertyPlace = createTemporaryPlace(fn.env, instr.value.loc);
const keyInstruction: Instruction = {
id: makeInstructionId(0),
lvalue: {...keyPropertyPlace, effect: Effect.Mutate},
value: {
kind: 'Primitive',
value: null,
loc: instr.value.loc,
},
effects: null,
loc: instr.loc,
};
keyProperty = {
kind: 'ObjectProperty',
key: {name: 'key', kind: 'string'},
type: 'property',
place: {...keyPropertyPlace, effect: Effect.Capture},
};
nextInstructions.push(keyInstruction);
}
let propsProperty: ObjectProperty;
if (spreadPropsOnly) {
const spreadProp = jsxSpreadAttributes[0];
CompilerError.invariant(spreadProp.kind === 'JsxSpreadAttribute', {
reason: 'Spread prop attribute must be of kind JSXSpreadAttribute',
loc: instr.loc,
});
propsProperty = {
kind: 'ObjectProperty',
key: {name: 'props', kind: 'string'},
type: 'property',
place: {...spreadProp.argument, effect: Effect.Mutate},
};
} else {
const propsInstruction: Instruction = {
id: makeInstructionId(0),
lvalue: {...propsPropertyPlace, effect: Effect.Mutate},
value: {
kind: 'ObjectExpression',
properties: props,
loc: instr.value.loc,
},
effects: null,
loc: instr.loc,
};
propsProperty = {
kind: 'ObjectProperty',
key: {name: 'props', kind: 'string'},
type: 'property',
place: {...propsPropertyPlace, effect: Effect.Capture},
};
nextInstructions.push(propsInstruction);
}
return {refProperty, keyProperty, propsProperty};
}
function handlePlace(
place: Place,
blockId: BlockId,
inlinedJsxDeclarations: InlinedJsxDeclarationMap,
): Place {
const inlinedJsxDeclaration = inlinedJsxDeclarations.get(
place.identifier.declarationId,
);
if (
inlinedJsxDeclaration == null ||
inlinedJsxDeclaration.blockIdsToIgnore.has(blockId)
) {
return place;
}
return {...place, identifier: inlinedJsxDeclaration.identifier};
}
function handlelValue(
lvalue: Place,
blockId: BlockId,
inlinedJsxDeclarations: InlinedJsxDeclarationMap,
): Place {
const inlinedJsxDeclaration = inlinedJsxDeclarations.get(
lvalue.identifier.declarationId,
);
if (
inlinedJsxDeclaration == null ||
inlinedJsxDeclaration.blockIdsToIgnore.has(blockId)
) {
return lvalue;
}
return {...lvalue, identifier: inlinedJsxDeclaration.identifier};
}
function handleIdentifier(
identifier: Identifier,
inlinedJsxDeclarations: InlinedJsxDeclarationMap,
): Identifier {
const inlinedJsxDeclaration = inlinedJsxDeclarations.get(
identifier.declarationId,
);
return inlinedJsxDeclaration == null
? identifier
: inlinedJsxDeclaration.identifier;
}

View File

@@ -1,503 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError} from '..';
import {
BasicBlock,
Environment,
GeneratedSource,
HIRFunction,
IdentifierId,
Instruction,
InstructionId,
Place,
isExpressionBlockKind,
makeInstructionId,
markInstructionIds,
} from '../HIR';
import {printInstruction} from '../HIR/PrintHIR';
import {
eachInstructionLValue,
eachInstructionValueLValue,
eachInstructionValueOperand,
eachTerminalOperand,
} from '../HIR/visitors';
import {getOrInsertWith} from '../Utils/utils';
/**
* This pass implements conservative instruction reordering to move instructions closer to
* to where their produced values are consumed. The goal is to group instructions in a way that
* is more optimal for future optimizations. Notably, MergeReactiveScopesThatAlwaysInvalidateTogether
* can only merge two candidate scopes if there are no intervenining instructions that are used by
* some later code: instruction reordering can move those intervening instructions later in many cases,
* thereby allowing more scopes to merge together.
*
* The high-level approach is to build a dependency graph where nodes correspond either to
* instructions OR to a particular lvalue assignment of another instruction. So
* `Destructure [x, y] = z` creates 3 nodes: one for the instruction, and one each for x and y.
* The lvalue nodes depend on the instruction node that assigns them.
*
* Dependency edges are added for all the lvalues and rvalues of each instruction, so for example
* the node for `t$2 = CallExpression t$0 ( t$1 )` will take dependencies on the nodes for t$0 and t$1.
*
* Individual instructions are grouped into two categories:
* - "Reorderable" instructions include a safe set of instructions that we know are fine to reorder.
* This includes JSX elements/fragments/text, primitives, template literals, and globals.
* These instructions are never emitted until they are referenced, and can even be moved across
* basic blocks until they are used.
* - All other instructions are non-reorderable, and take an explicit dependency on the last such
* non-reorderable instruction in their block. This largely ensures that mutations are serialized,
* since all potentially mutating instructions are in this category.
*
* The only remaining mutation not handled by the above is variable reassignment. To ensure that all
* reads/writes of a variable access the correct version, all references (lvalues and rvalues) to
* each named variable are serialized. Thus `x = 1; y = x; x = 2; z = x` will establish a chain
* of dependencies and retain the correct ordering.
*
* The algorithm proceeds one basic block at a time, first building up the dependnecy graph and then
* reordering.
*
* The reordering weights nodes according to their transitive dependencies, and whether a particular node
* needs memoization or not. Larger dependencies go first, followed by smaller dependencies, which in
* testing seems to allow scopes to merge more effectively. Over time we can likely continue to improve
* the reordering heuristic.
*
* An obvious area for improvement is to allow reordering of LoadLocals that occur after the last write
* of the named variable. We can add this in a follow-up.
*/
export function instructionReordering(fn: HIRFunction): void {
// Shared nodes are emitted when they are first used
const shared: Nodes = new Map();
const references = findReferencedRangeOfTemporaries(fn);
for (const [, block] of fn.body.blocks) {
reorderBlock(fn.env, block, shared, references);
}
CompilerError.invariant(shared.size === 0, {
reason: `InstructionReordering: expected all reorderable nodes to have been emitted`,
loc:
[...shared.values()]
.map(node => node.instruction?.loc)
.filter(loc => loc != null)[0] ?? GeneratedSource,
});
markInstructionIds(fn.body);
}
const DEBUG = false;
type Nodes = Map<IdentifierId, Node>;
type Node = {
instruction: Instruction | null;
dependencies: Set<IdentifierId>;
reorderability: Reorderability;
depth: number | null;
};
// Inclusive start and end
type References = {
singleUseIdentifiers: SingleUseIdentifiers;
lastAssignments: LastAssignments;
};
type LastAssignments = Map<string, InstructionId>;
type SingleUseIdentifiers = Set<IdentifierId>;
enum ReferenceKind {
Read,
Write,
}
function findReferencedRangeOfTemporaries(fn: HIRFunction): References {
const singleUseIdentifiers = new Map<IdentifierId, number>();
const lastAssignments: LastAssignments = new Map();
function reference(
instr: InstructionId,
place: Place,
kind: ReferenceKind,
): void {
if (
place.identifier.name !== null &&
place.identifier.name.kind === 'named'
) {
if (kind === ReferenceKind.Write) {
const name = place.identifier.name.value;
const previous = lastAssignments.get(name);
if (previous === undefined) {
lastAssignments.set(name, instr);
} else {
lastAssignments.set(
name,
makeInstructionId(Math.max(previous, instr)),
);
}
}
return;
} else if (kind === ReferenceKind.Read) {
const previousCount = singleUseIdentifiers.get(place.identifier.id) ?? 0;
singleUseIdentifiers.set(place.identifier.id, previousCount + 1);
}
}
for (const [, block] of fn.body.blocks) {
for (const instr of block.instructions) {
for (const operand of eachInstructionValueLValue(instr.value)) {
reference(instr.id, operand, ReferenceKind.Read);
}
for (const lvalue of eachInstructionLValue(instr)) {
reference(instr.id, lvalue, ReferenceKind.Write);
}
}
for (const operand of eachTerminalOperand(block.terminal)) {
reference(block.terminal.id, operand, ReferenceKind.Read);
}
}
return {
singleUseIdentifiers: new Set(
[...singleUseIdentifiers]
.filter(([, count]) => count === 1)
.map(([id]) => id),
),
lastAssignments,
};
}
function reorderBlock(
env: Environment,
block: BasicBlock,
shared: Nodes,
references: References,
): void {
const locals: Nodes = new Map();
const named: Map<string, IdentifierId> = new Map();
let previous: IdentifierId | null = null;
for (const instr of block.instructions) {
const {lvalue, value} = instr;
// Get or create a node for this lvalue
const reorderability = getReorderability(instr, references);
const node = getOrInsertWith(
locals,
lvalue.identifier.id,
() =>
({
instruction: instr,
dependencies: new Set(),
reorderability,
depth: null,
}) as Node,
);
/**
* Ensure non-reoderable instructions have their order retained by
* adding explicit dependencies to the previous such instruction.
*/
if (reorderability === Reorderability.Nonreorderable) {
if (previous !== null) {
node.dependencies.add(previous);
}
previous = lvalue.identifier.id;
}
/**
* Establish dependencies on operands
*/
for (const operand of eachInstructionValueOperand(value)) {
const {name, id} = operand.identifier;
if (name !== null && name.kind === 'named') {
// Serialize all accesses to named variables
const previous = named.get(name.value);
if (previous !== undefined) {
node.dependencies.add(previous);
}
named.set(name.value, lvalue.identifier.id);
} else if (locals.has(id) || shared.has(id)) {
node.dependencies.add(id);
}
}
/**
* Establish nodes for lvalues, with dependencies on the node
* for the instruction itself. This ensures that any consumers
* of the lvalue will take a dependency through to the original
* instruction.
*/
for (const lvalueOperand of eachInstructionValueLValue(value)) {
const lvalueNode = getOrInsertWith(
locals,
lvalueOperand.identifier.id,
() =>
({
instruction: null,
dependencies: new Set(),
depth: null,
}) as Node,
);
lvalueNode.dependencies.add(lvalue.identifier.id);
const name = lvalueOperand.identifier.name;
if (name !== null && name.kind === 'named') {
const previous = named.get(name.value);
if (previous !== undefined) {
node.dependencies.add(previous);
}
named.set(name.value, lvalue.identifier.id);
}
}
}
const nextInstructions: Array<Instruction> = [];
const seen = new Set<IdentifierId>();
DEBUG && console.log(`bb${block.id}`);
/**
* The ideal order for emitting instructions may change the final instruction,
* but value blocks have special semantics for the final instruction of a block -
* that's the expression's value!. So we choose between a less optimal strategy
* for value blocks which preserves the final instruction order OR a more optimal
* ordering for statement-y blocks.
*/
if (isExpressionBlockKind(block.kind)) {
// First emit everything that can't be reordered
if (previous !== null) {
DEBUG && console.log(`(last non-reorderable instruction)`);
DEBUG && print(env, locals, shared, seen, previous);
emit(env, locals, shared, nextInstructions, previous);
}
/*
* For "value" blocks the final instruction represents its value, so we have to be
* careful to not change the ordering. Emit the last instruction explicitly.
* Any non-reorderable instructions will get emitted first, and any unused
* reorderable instructions can be deferred to the shared node list.
*/
if (block.instructions.length !== 0) {
DEBUG && console.log(`(block value)`);
DEBUG &&
print(
env,
locals,
shared,
seen,
block.instructions.at(-1)!.lvalue.identifier.id,
);
emit(
env,
locals,
shared,
nextInstructions,
block.instructions.at(-1)!.lvalue.identifier.id,
);
}
/*
* Then emit the dependencies of the terminal operand. In many cases they will have
* already been emitted in the previous step and this is a no-op.
* TODO: sort the dependencies based on weight, like we do for other nodes. Not a big
* deal though since most terminals have a single operand
*/
for (const operand of eachTerminalOperand(block.terminal)) {
DEBUG && console.log(`(terminal operand)`);
DEBUG && print(env, locals, shared, seen, operand.identifier.id);
emit(env, locals, shared, nextInstructions, operand.identifier.id);
}
// Anything not emitted yet is globally reorderable
for (const [id, node] of locals) {
if (node.instruction == null) {
continue;
}
CompilerError.invariant(
node.reorderability === Reorderability.Reorderable,
{
reason: `Expected all remaining instructions to be reorderable`,
description:
node.instruction != null
? `Instruction [${node.instruction.id}] was not emitted yet but is not reorderable`
: `Lvalue $${id} was not emitted yet but is not reorderable`,
loc: node.instruction?.loc ?? block.terminal.loc,
},
);
DEBUG && console.log(`save shared: $${id}`);
shared.set(id, node);
}
} else {
/**
* If this is not a value block, then the order within the block doesn't matter
* and we can optimize more. The observation is that blocks often have instructions
* such as:
*
* ```
* t$0 = nonreorderable
* t$1 = nonreorderable <-- this gets in the way of merging t$0 and t$2
* t$2 = reorderable deps[ t$0 ]
* return t$2
* ```
*
* Ie where there is some pair of nonreorderable+reorderable values, with some intervening
* also non-reorderable instruction. If we emit all non-reorderable instructions first,
* then we'll keep the original order. But reordering instructions doesn't just mean moving
* them later: we can also move them _earlier_. By starting from terminal operands we
* end up emitting:
*
* ```
* t$0 = nonreorderable // dep of t$2
* t$2 = reorderable deps[ t$0 ]
* t$1 = nonreorderable <-- not in the way of merging anymore!
* return t$2
* ```
*
* Ie all nonreorderable transitive deps of the terminal operands will get emitted first,
* but we'll be able to intersperse the depending reorderable instructions in between
* them in a way that works better with scope merging.
*/
for (const operand of eachTerminalOperand(block.terminal)) {
DEBUG && console.log(`(terminal operand)`);
DEBUG && print(env, locals, shared, seen, operand.identifier.id);
emit(env, locals, shared, nextInstructions, operand.identifier.id);
}
// Anything not emitted yet is globally reorderable
for (const id of Array.from(locals.keys()).reverse()) {
const node = locals.get(id);
if (node === undefined) {
continue;
}
if (node.reorderability === Reorderability.Reorderable) {
DEBUG && console.log(`save shared: $${id}`);
shared.set(id, node);
} else {
DEBUG && console.log('leftover');
DEBUG && print(env, locals, shared, seen, id);
emit(env, locals, shared, nextInstructions, id);
}
}
}
block.instructions = nextInstructions;
DEBUG && console.log();
}
function getDepth(env: Environment, nodes: Nodes, id: IdentifierId): number {
const node = nodes.get(id)!;
if (node == null) {
return 0;
}
if (node.depth != null) {
return node.depth;
}
node.depth = 0; // in case of cycles
let depth = node.reorderability === Reorderability.Reorderable ? 1 : 10;
for (const dep of node.dependencies) {
depth += getDepth(env, nodes, dep);
}
node.depth = depth;
return depth;
}
function print(
env: Environment,
locals: Nodes,
shared: Nodes,
seen: Set<IdentifierId>,
id: IdentifierId,
depth: number = 0,
): void {
if (seen.has(id)) {
DEBUG && console.log(`${'| '.repeat(depth)}$${id} <skipped>`);
return;
}
seen.add(id);
const node = locals.get(id) ?? shared.get(id);
if (node == null) {
return;
}
const deps = [...node.dependencies];
deps.sort((a, b) => {
const aDepth = getDepth(env, locals, a);
const bDepth = getDepth(env, locals, b);
return bDepth - aDepth;
});
for (const dep of deps) {
print(env, locals, shared, seen, dep, depth + 1);
}
DEBUG &&
console.log(
`${'| '.repeat(depth)}$${id} ${printNode(node)} deps=[${deps
.map(x => `$${x}`)
.join(', ')}] depth=${node.depth}`,
);
}
function printNode(node: Node): string {
const {instruction} = node;
if (instruction === null) {
return '<lvalue-only>';
}
switch (instruction.value.kind) {
case 'FunctionExpression':
case 'ObjectMethod': {
return `[${instruction.id}] ${instruction.value.kind}`;
}
default: {
return printInstruction(instruction);
}
}
}
function emit(
env: Environment,
locals: Nodes,
shared: Nodes,
instructions: Array<Instruction>,
id: IdentifierId,
): void {
const node = locals.get(id) ?? shared.get(id);
if (node == null) {
return;
}
locals.delete(id);
shared.delete(id);
const deps = [...node.dependencies];
deps.sort((a, b) => {
const aDepth = getDepth(env, locals, a);
const bDepth = getDepth(env, locals, b);
return bDepth - aDepth;
});
for (const dep of deps) {
emit(env, locals, shared, instructions, dep);
}
if (node.instruction !== null) {
instructions.push(node.instruction);
}
}
enum Reorderability {
Reorderable,
Nonreorderable,
}
function getReorderability(
instr: Instruction,
references: References,
): Reorderability {
switch (instr.value.kind) {
case 'JsxExpression':
case 'JsxFragment':
case 'JSXText':
case 'LoadGlobal':
case 'Primitive':
case 'TemplateLiteral':
case 'BinaryExpression':
case 'UnaryExpression': {
return Reorderability.Reorderable;
}
case 'LoadLocal': {
const name = instr.value.place.identifier.name;
if (name !== null && name.kind === 'named') {
const lastAssignment = references.lastAssignments.get(name.value);
if (
lastAssignment !== undefined &&
lastAssignment < instr.id &&
references.singleUseIdentifiers.has(instr.lvalue.identifier.id)
) {
return Reorderability.Reorderable;
}
}
return Reorderability.Nonreorderable;
}
default: {
return Reorderability.Nonreorderable;
}
}
}

View File

@@ -1,308 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {
ArrayExpression,
BasicBlock,
CallExpression,
Destructure,
Environment,
ExternalFunction,
GeneratedSource,
HIRFunction,
IdentifierId,
Instruction,
LoadGlobal,
LoadLocal,
NonLocalImportSpecifier,
Place,
PropertyLoad,
isUseContextHookType,
makeBlockId,
makeInstructionId,
makePropertyLiteral,
markInstructionIds,
promoteTemporary,
reversePostorderBlocks,
} from '../HIR';
import {createTemporaryPlace} from '../HIR/HIRBuilder';
import {enterSSA} from '../SSA';
import {inferTypes} from '../TypeInference';
export function lowerContextAccess(
fn: HIRFunction,
loweredContextCalleeConfig: ExternalFunction,
): void {
const contextAccess: Map<IdentifierId, CallExpression> = new Map();
const contextKeys: Map<IdentifierId, Array<string>> = new Map();
// collect context access and keys
for (const [, block] of fn.body.blocks) {
for (const instr of block.instructions) {
const {value, lvalue} = instr;
if (
value.kind === 'CallExpression' &&
isUseContextHookType(value.callee.identifier)
) {
contextAccess.set(lvalue.identifier.id, value);
continue;
}
if (value.kind !== 'Destructure') {
continue;
}
const destructureId = value.value.identifier.id;
if (!contextAccess.has(destructureId)) {
continue;
}
const keys = getContextKeys(value);
if (keys === null) {
return;
}
if (contextKeys.has(destructureId)) {
/*
* TODO(gsn): Add support for accessing context over multiple
* statements.
*/
return;
} else {
contextKeys.set(destructureId, keys);
}
}
}
let importLoweredContextCallee: NonLocalImportSpecifier | null = null;
if (contextAccess.size > 0 && contextKeys.size > 0) {
for (const [, block] of fn.body.blocks) {
let nextInstructions: Array<Instruction> | null = null;
for (let i = 0; i < block.instructions.length; i++) {
const instr = block.instructions[i];
const {lvalue, value} = instr;
if (
value.kind === 'CallExpression' &&
isUseContextHookType(value.callee.identifier) &&
contextKeys.has(lvalue.identifier.id)
) {
importLoweredContextCallee ??=
fn.env.programContext.addImportSpecifier(
loweredContextCalleeConfig,
);
const loweredContextCalleeInstr = emitLoadLoweredContextCallee(
fn.env,
importLoweredContextCallee,
);
if (nextInstructions === null) {
nextInstructions = block.instructions.slice(0, i);
}
nextInstructions.push(loweredContextCalleeInstr);
const keys = contextKeys.get(lvalue.identifier.id)!;
const selectorFnInstr = emitSelectorFn(fn.env, keys);
nextInstructions.push(selectorFnInstr);
const lowerContextCallId = loweredContextCalleeInstr.lvalue;
value.callee = lowerContextCallId;
const selectorFn = selectorFnInstr.lvalue;
value.args.push(selectorFn);
}
if (nextInstructions) {
nextInstructions.push(instr);
}
}
if (nextInstructions) {
block.instructions = nextInstructions;
}
}
markInstructionIds(fn.body);
inferTypes(fn);
}
}
function emitLoadLoweredContextCallee(
env: Environment,
importedLowerContextCallee: NonLocalImportSpecifier,
): Instruction {
const loadGlobal: LoadGlobal = {
kind: 'LoadGlobal',
binding: {...importedLowerContextCallee},
loc: GeneratedSource,
};
return {
id: makeInstructionId(0),
loc: GeneratedSource,
lvalue: createTemporaryPlace(env, GeneratedSource),
effects: null,
value: loadGlobal,
};
}
function getContextKeys(value: Destructure): Array<string> | null {
const keys = [];
const pattern = value.lvalue.pattern;
switch (pattern.kind) {
case 'ArrayPattern': {
return null;
}
case 'ObjectPattern': {
for (const place of pattern.properties) {
if (
place.kind !== 'ObjectProperty' ||
place.type !== 'property' ||
place.key.kind !== 'identifier' ||
place.place.identifier.name === null ||
place.place.identifier.name.kind !== 'named'
) {
return null;
}
keys.push(place.key.name);
}
return keys;
}
}
}
function emitPropertyLoad(
env: Environment,
obj: Place,
property: string,
): {instructions: Array<Instruction>; element: Place} {
const loadObj: LoadLocal = {
kind: 'LoadLocal',
place: obj,
loc: GeneratedSource,
};
const object: Place = createTemporaryPlace(env, GeneratedSource);
const loadLocalInstr: Instruction = {
lvalue: object,
value: loadObj,
id: makeInstructionId(0),
effects: null,
loc: GeneratedSource,
};
const loadProp: PropertyLoad = {
kind: 'PropertyLoad',
object,
property: makePropertyLiteral(property),
loc: GeneratedSource,
};
const element: Place = createTemporaryPlace(env, GeneratedSource);
const loadPropInstr: Instruction = {
lvalue: element,
value: loadProp,
id: makeInstructionId(0),
effects: null,
loc: GeneratedSource,
};
return {
instructions: [loadLocalInstr, loadPropInstr],
element: element,
};
}
function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
const obj: Place = createTemporaryPlace(env, GeneratedSource);
promoteTemporary(obj.identifier);
const instr: Array<Instruction> = [];
const elements = [];
for (const key of keys) {
const {instructions, element: prop} = emitPropertyLoad(env, obj, key);
instr.push(...instructions);
elements.push(prop);
}
const arrayInstr = emitArrayInstr(elements, env);
instr.push(arrayInstr);
const block: BasicBlock = {
kind: 'block',
id: makeBlockId(0),
instructions: instr,
terminal: {
id: makeInstructionId(0),
kind: 'return',
returnVariant: 'Explicit',
loc: GeneratedSource,
value: arrayInstr.lvalue,
effects: null,
},
preds: new Set(),
phis: new Set(),
};
const fn: HIRFunction = {
loc: GeneratedSource,
id: null,
nameHint: null,
fnType: 'Other',
env,
params: [obj],
returnTypeAnnotation: null,
returns: createTemporaryPlace(env, GeneratedSource),
context: [],
body: {
entry: block.id,
blocks: new Map([[block.id, block]]),
},
generator: false,
async: false,
directives: [],
aliasingEffects: [],
};
reversePostorderBlocks(fn.body);
markInstructionIds(fn.body);
enterSSA(fn);
inferTypes(fn);
const fnInstr: Instruction = {
id: makeInstructionId(0),
value: {
kind: 'FunctionExpression',
name: null,
nameHint: null,
loweredFunc: {
func: fn,
},
type: 'ArrowFunctionExpression',
loc: GeneratedSource,
},
lvalue: createTemporaryPlace(env, GeneratedSource),
effects: null,
loc: GeneratedSource,
};
return fnInstr;
}
function emitArrayInstr(elements: Array<Place>, env: Environment): Instruction {
const array: ArrayExpression = {
kind: 'ArrayExpression',
elements,
loc: GeneratedSource,
};
const arrayLvalue: Place = createTemporaryPlace(env, GeneratedSource);
const arrayInstr: Instruction = {
id: makeInstructionId(0),
value: array,
lvalue: arrayLvalue,
effects: null,
loc: GeneratedSource,
};
return arrayInstr;
}

View File

@@ -8,4 +8,3 @@
export {constantPropagation} from './ConstantPropagation';
export {deadCodeElimination} from './DeadCodeElimination';
export {pruneMaybeThrows} from './PruneMaybeThrows';
export {inlineJsxTransform} from './InlineJsxTransform';

View File

@@ -1007,11 +1007,10 @@ class Driver {
const test = this.visitValueBlock(testBlockId, loc);
const testBlock = this.cx.ir.blocks.get(test.block)!;
if (testBlock.terminal.kind !== 'branch') {
CompilerError.throwTodo({
reason: `Unexpected terminal kind \`${testBlock.terminal.kind}\` for ${terminalKind} test block`,
description: null,
CompilerError.invariant(false, {
reason: `Expected a branch terminal for ${terminalKind} test block`,
description: `Got \`${testBlock.terminal.kind}\``,
loc: testBlock.terminal.loc,
suggestions: null,
});
}
return {

View File

@@ -13,7 +13,11 @@ import {
pruneUnusedLabels,
renameVariables,
} from '.';
import {CompilerError, ErrorCategory} from '../CompilerError';
import {
CompilerError,
CompilerErrorDetail,
ErrorCategory,
} from '../CompilerError';
import {Environment, ExternalFunction} from '../HIR';
import {
ArrayPattern,
@@ -46,13 +50,13 @@ import {
} from '../HIR/HIR';
import {printIdentifier, printInstruction, printPlace} from '../HIR/PrintHIR';
import {eachPatternOperand} from '../HIR/visitors';
import {Err, Ok, Result} from '../Utils/Result';
import {GuardKind} from '../Utils/RuntimeDiagnosticConstants';
import {assertExhaustive} from '../Utils/utils';
import {buildReactiveFunction} from './BuildReactiveFunction';
import {SINGLE_CHILD_FBT_TAGS} from './MemoizeFbtAndMacroOperandsInSameScope';
import {ReactiveFunctionVisitor, visitReactiveFunction} from './visitors';
import {EMIT_FREEZE_GLOBAL_GATING, ReactFunctionType} from '../HIR/Environment';
import {ReactFunctionType} from '../HIR/Environment';
import {ProgramContext} from '../Entrypoint';
export const MEMO_CACHE_SENTINEL = 'react.memo_cache_sentinel';
@@ -100,17 +104,6 @@ export type CodegenFunction = {
fn: CodegenFunction;
type: ReactFunctionType | null;
}>;
/**
* This is true if the compiler has compiled inferred effect dependencies
*/
hasInferredEffect: boolean;
inferredEffectLocations: Set<SourceLocation>;
/**
* This is true if the compiler has compiled a fire to a useFire call
*/
hasFireRewrite: boolean;
};
export function codegenFunction(
@@ -122,7 +115,7 @@ export function codegenFunction(
uniqueIdentifiers: Set<string>;
fbtOperands: Set<IdentifierId>;
},
): Result<CodegenFunction, CompilerError> {
): CodegenFunction {
const cx = new Context(
fn.env,
fn.id ?? '[[ anonymous ]]',
@@ -152,11 +145,7 @@ export function codegenFunction(
};
}
const compileResult = codegenReactiveFunction(cx, fn);
if (compileResult.isErr()) {
return compileResult;
}
const compiled = compileResult.unwrap();
const compiled = codegenReactiveFunction(cx, fn);
const hookGuard = fn.env.config.enableEmitHookGuards;
if (hookGuard != null && fn.env.outputMode === 'client') {
@@ -284,7 +273,7 @@ export function codegenFunction(
emitInstrumentForget.globalGating,
);
if (assertResult.isErr()) {
return assertResult;
fn.env.recordErrors(assertResult.unwrapErr());
}
}
@@ -334,20 +323,17 @@ export function codegenFunction(
),
reactiveFunction,
);
if (codegen.isErr()) {
return codegen;
}
outlined.push({fn: codegen.unwrap(), type});
outlined.push({fn: codegen, type});
}
compiled.outlined = outlined;
return compileResult;
return compiled;
}
function codegenReactiveFunction(
cx: Context,
fn: ReactiveFunction,
): Result<CodegenFunction, CompilerError> {
): CodegenFunction {
for (const param of fn.params) {
const place = param.kind === 'Identifier' ? param : param.place;
cx.temp.set(place.identifier.declarationId, null);
@@ -365,14 +351,10 @@ function codegenReactiveFunction(
}
}
if (cx.errors.hasAnyErrors()) {
return Err(cx.errors);
}
const countMemoBlockVisitor = new CountMemoBlockVisitor(fn.env);
visitReactiveFunction(fn, countMemoBlockVisitor, undefined);
return Ok({
return {
type: 'CodegenFunction',
loc: fn.loc,
id: fn.id !== null ? t.identifier(fn.id) : null,
@@ -387,10 +369,7 @@ function codegenReactiveFunction(
prunedMemoBlocks: countMemoBlockVisitor.prunedMemoBlocks,
prunedMemoValues: countMemoBlockVisitor.prunedMemoValues,
outlined: [],
hasFireRewrite: fn.env.hasFireRewrite,
hasInferredEffect: fn.env.hasInferredEffect,
inferredEffectLocations: fn.env.inferredEffectLocations,
});
};
}
class CountMemoBlockVisitor extends ReactiveFunctionVisitor<void> {
@@ -441,7 +420,6 @@ class Context {
*/
#declarations: Set<DeclarationId> = new Set();
temp: Temporaries;
errors: CompilerError = new CompilerError();
objectMethods: Map<IdentifierId, ObjectMethod> = new Map();
uniqueIdentifiers: Set<string>;
fbtOperands: Set<IdentifierId>;
@@ -460,6 +438,11 @@ class Context {
this.fbtOperands = fbtOperands;
this.temp = temporaries !== null ? new Map(temporaries) : new Map();
}
recordError(error: CompilerErrorDetail): void {
this.env.recordError(error);
}
get nextCacheIndex(): number {
return this.#nextCacheIndex++;
}
@@ -574,30 +557,6 @@ function codegenBlockNoReset(
return t.blockStatement(statements);
}
function wrapCacheDep(cx: Context, value: t.Expression): t.Expression {
if (
cx.env.config.enableEmitFreeze != null &&
cx.env.outputMode === 'client'
) {
const emitFreezeIdentifier = cx.env.programContext.addImportSpecifier(
cx.env.config.enableEmitFreeze,
).name;
cx.env.programContext
.assertGlobalBinding(EMIT_FREEZE_GLOBAL_GATING, cx.env.scope)
.unwrap();
return t.conditionalExpression(
t.identifier(EMIT_FREEZE_GLOBAL_GATING),
t.callExpression(t.identifier(emitFreezeIdentifier), [
value,
t.stringLiteral(cx.fnName),
]),
value,
);
} else {
return value;
}
}
function codegenReactiveScope(
cx: Context,
statements: Array<t.Statement>,
@@ -612,12 +571,9 @@ function codegenReactiveScope(
value: t.Expression;
}> = [];
const changeExpressions: Array<t.Expression> = [];
const changeExpressionComments: Array<string> = [];
const outputComments: Array<string> = [];
for (const dep of [...scope.dependencies].sort(compareScopeDependency)) {
const index = cx.nextCacheIndex;
changeExpressionComments.push(printDependencyComment(dep));
const comparison = t.binaryExpression(
'!==',
t.memberExpression(
@@ -627,18 +583,7 @@ function codegenReactiveScope(
),
codegenDependency(cx, dep),
);
if (cx.env.config.enableChangeVariableCodegen) {
const changeIdentifier = t.identifier(cx.synthesizeName(`c_${index}`));
statements.push(
t.variableDeclaration('const', [
t.variableDeclarator(changeIdentifier, comparison),
]),
);
changeExpressions.push(changeIdentifier);
} else {
changeExpressions.push(comparison);
}
changeExpressions.push(comparison);
/*
* Adding directly to cacheStoreStatements rather than cacheLoads, because there
* is no corresponding cacheLoadStatement for dependencies
@@ -676,13 +621,12 @@ function codegenReactiveScope(
});
const name = convertIdentifier(identifier);
outputComments.push(name.name);
if (!cx.hasDeclared(identifier)) {
statements.push(
t.variableDeclaration('let', [createVariableDeclarator(name, null)]),
);
}
cacheLoads.push({name, index, value: wrapCacheDep(cx, name)});
cacheLoads.push({name, index, value: name});
cx.declare(identifier);
}
for (const reassignment of scope.reassignments) {
@@ -691,8 +635,7 @@ function codegenReactiveScope(
firstOutputIndex = index;
}
const name = convertIdentifier(reassignment);
outputComments.push(name.name);
cacheLoads.push({name, index, value: wrapCacheDep(cx, name)});
cacheLoads.push({name, index, value: name});
}
let testCondition = (changeExpressions as Array<t.Expression>).reduce(
@@ -724,187 +667,44 @@ function codegenReactiveScope(
);
}
if (cx.env.config.disableMemoizationForDebugging) {
CompilerError.invariant(
cx.env.config.enableChangeDetectionForDebugging == null,
{
reason: `Expected to not have both change detection enabled and memoization disabled`,
description: `Incompatible config options`,
loc: GeneratedSource,
},
);
testCondition = t.logicalExpression(
'||',
testCondition,
t.booleanLiteral(true),
);
}
let computationBlock = codegenBlock(cx, block);
let memoStatement;
const detectionFunction = cx.env.config.enableChangeDetectionForDebugging;
if (detectionFunction != null && changeExpressions.length > 0) {
const loc =
typeof scope.loc === 'symbol'
? 'unknown location'
: `(${scope.loc.start.line}:${scope.loc.end.line})`;
const importedDetectionFunctionIdentifier =
cx.env.programContext.addImportSpecifier(detectionFunction).name;
const cacheLoadOldValueStatements: Array<t.Statement> = [];
const changeDetectionStatements: Array<t.Statement> = [];
const idempotenceDetectionStatements: Array<t.Statement> = [];
for (const {name, index, value} of cacheLoads) {
const loadName = cx.synthesizeName(`old$${name.name}`);
const slot = t.memberExpression(
t.identifier(cx.synthesizeName('$')),
t.numericLiteral(index),
true,
);
cacheStoreStatements.push(
t.expressionStatement(t.assignmentExpression('=', slot, value)),
);
cacheLoadOldValueStatements.push(
t.variableDeclaration('let', [
t.variableDeclarator(t.identifier(loadName), slot),
]),
);
changeDetectionStatements.push(
t.expressionStatement(
t.callExpression(t.identifier(importedDetectionFunctionIdentifier), [
t.identifier(loadName),
t.cloneNode(name, true),
t.stringLiteral(name.name),
t.stringLiteral(cx.fnName),
t.stringLiteral('cached'),
t.stringLiteral(loc),
]),
for (const {name, index, value} of cacheLoads) {
cacheStoreStatements.push(
t.expressionStatement(
t.assignmentExpression(
'=',
t.memberExpression(
t.identifier(cx.synthesizeName('$')),
t.numericLiteral(index),
true,
),
value,
),
);
idempotenceDetectionStatements.push(
t.expressionStatement(
t.callExpression(t.identifier(importedDetectionFunctionIdentifier), [
t.cloneNode(slot, true),
t.cloneNode(name, true),
t.stringLiteral(name.name),
t.stringLiteral(cx.fnName),
t.stringLiteral('recomputed'),
t.stringLiteral(loc),
]),
),
);
idempotenceDetectionStatements.push(
t.expressionStatement(t.assignmentExpression('=', name, slot)),
);
}
const condition = cx.synthesizeName('condition');
const recomputationBlock = t.cloneNode(computationBlock, true);
memoStatement = t.blockStatement([
...computationBlock.body,
t.variableDeclaration('let', [
t.variableDeclarator(t.identifier(condition), testCondition),
]),
t.ifStatement(
t.unaryExpression('!', t.identifier(condition)),
t.blockStatement([
...cacheLoadOldValueStatements,
...changeDetectionStatements,
]),
),
...cacheStoreStatements,
t.ifStatement(
t.identifier(condition),
t.blockStatement([
...recomputationBlock.body,
...idempotenceDetectionStatements,
]),
),
]);
} else {
for (const {name, index, value} of cacheLoads) {
cacheStoreStatements.push(
t.expressionStatement(
t.assignmentExpression(
'=',
t.memberExpression(
t.identifier(cx.synthesizeName('$')),
t.numericLiteral(index),
true,
),
value,
);
cacheLoadStatements.push(
t.expressionStatement(
t.assignmentExpression(
'=',
name,
t.memberExpression(
t.identifier(cx.synthesizeName('$')),
t.numericLiteral(index),
true,
),
),
);
cacheLoadStatements.push(
t.expressionStatement(
t.assignmentExpression(
'=',
name,
t.memberExpression(
t.identifier(cx.synthesizeName('$')),
t.numericLiteral(index),
true,
),
),
),
);
}
computationBlock.body.push(...cacheStoreStatements);
memoStatement = t.ifStatement(
testCondition,
computationBlock,
t.blockStatement(cacheLoadStatements),
),
);
}
computationBlock.body.push(...cacheStoreStatements);
memoStatement = t.ifStatement(
testCondition,
computationBlock,
t.blockStatement(cacheLoadStatements),
);
if (cx.env.config.enableMemoizationComments) {
if (changeExpressionComments.length) {
t.addComment(
memoStatement,
'leading',
` check if ${printDelimitedCommentList(
changeExpressionComments,
'or',
)} changed`,
true,
);
t.addComment(
memoStatement,
'leading',
` "useMemo" for ${printDelimitedCommentList(outputComments, 'and')}:`,
true,
);
} else {
t.addComment(
memoStatement,
'leading',
' cache value with no dependencies',
true,
);
t.addComment(
memoStatement,
'leading',
` "useMemo" for ${printDelimitedCommentList(outputComments, 'and')}:`,
true,
);
}
if (computationBlock.body.length > 0) {
t.addComment(
computationBlock.body[0]!,
'leading',
` Inputs changed, recompute`,
true,
);
}
if (cacheLoadStatements.length > 0) {
t.addComment(
cacheLoadStatements[0]!,
'leading',
` Inputs did not change, use cached value`,
true,
);
}
}
statements.push(memoStatement);
const earlyReturnValue = scope.earlyReturnValue;
@@ -979,12 +779,15 @@ function codegenTerminal(
loc: terminal.init.loc,
});
if (terminal.init.instructions.length !== 2) {
CompilerError.throwTodo({
reason: 'Support non-trivial for..in inits',
description: null,
loc: terminal.init.loc,
suggestions: null,
});
cx.recordError(
new CompilerErrorDetail({
reason: 'Support non-trivial for..in inits',
category: ErrorCategory.Todo,
loc: terminal.init.loc,
suggestions: null,
}),
);
return t.emptyStatement();
}
const iterableCollection = terminal.init.instructions[0];
const iterableItem = terminal.init.instructions[1];
@@ -999,12 +802,15 @@ function codegenTerminal(
break;
}
case 'StoreContext': {
CompilerError.throwTodo({
reason: 'Support non-trivial for..in inits',
description: null,
loc: terminal.init.loc,
suggestions: null,
});
cx.recordError(
new CompilerErrorDetail({
reason: 'Support non-trivial for..in inits',
category: ErrorCategory.Todo,
loc: terminal.init.loc,
suggestions: null,
}),
);
return t.emptyStatement();
}
default:
CompilerError.invariant(false, {
@@ -1074,12 +880,15 @@ function codegenTerminal(
loc: terminal.test.loc,
});
if (terminal.test.instructions.length !== 2) {
CompilerError.throwTodo({
reason: 'Support non-trivial for..of inits',
description: null,
loc: terminal.init.loc,
suggestions: null,
});
cx.recordError(
new CompilerErrorDetail({
reason: 'Support non-trivial for..of inits',
category: ErrorCategory.Todo,
loc: terminal.init.loc,
suggestions: null,
}),
);
return t.emptyStatement();
}
const iterableItem = terminal.test.instructions[1];
let lval: t.LVal;
@@ -1093,12 +902,15 @@ function codegenTerminal(
break;
}
case 'StoreContext': {
CompilerError.throwTodo({
reason: 'Support non-trivial for..of inits',
description: null,
loc: terminal.init.loc,
suggestions: null,
});
cx.recordError(
new CompilerErrorDetail({
reason: 'Support non-trivial for..of inits',
category: ErrorCategory.Todo,
loc: terminal.init.loc,
suggestions: null,
}),
);
return t.emptyStatement();
}
default:
CompilerError.invariant(false, {
@@ -1431,41 +1243,6 @@ function codegenForInit(
}
}
function printDependencyComment(dependency: ReactiveScopeDependency): string {
const identifier = convertIdentifier(dependency.identifier);
let name = identifier.name;
if (dependency.path !== null) {
for (const path of dependency.path) {
name += `.${path.property}`;
}
}
return name;
}
function printDelimitedCommentList(
items: Array<string>,
finalCompletion: string,
): string {
if (items.length === 2) {
return items.join(` ${finalCompletion} `);
} else if (items.length <= 1) {
return items.join('');
}
let output = [];
for (let i = 0; i < items.length; i++) {
const item = items[i]!;
if (i < items.length - 2) {
output.push(`${item}, `);
} else if (i === items.length - 2) {
output.push(`${item}, ${finalCompletion} `);
} else {
output.push(item);
}
}
return output.join('');
}
function codegenDependency(
cx: Context,
dependency: ReactiveScopeDependency,
@@ -1897,7 +1674,7 @@ function codegenInstructionValue(
cx.temp,
),
reactiveFunction,
).unwrap();
);
/*
* ObjectMethod builder must be backwards compatible with older versions of babel.
@@ -2096,7 +1873,7 @@ function codegenInstructionValue(
cx.temp,
),
reactiveFunction,
).unwrap();
);
if (instrValue.type === 'ArrowFunctionExpression') {
let body: t.BlockStatement | t.Expression = fn.body;
@@ -2192,22 +1969,26 @@ function codegenInstructionValue(
} else {
if (t.isVariableDeclaration(stmt)) {
const declarator = stmt.declarations[0];
cx.errors.push({
reason: `(CodegenReactiveFunction::codegenInstructionValue) Cannot declare variables in a value block, tried to declare '${
(declarator.id as t.Identifier).name
}'`,
category: ErrorCategory.Todo,
loc: declarator.loc ?? null,
suggestions: null,
});
cx.recordError(
new CompilerErrorDetail({
reason: `(CodegenReactiveFunction::codegenInstructionValue) Cannot declare variables in a value block, tried to declare '${
(declarator.id as t.Identifier).name
}'`,
category: ErrorCategory.Todo,
loc: declarator.loc ?? null,
suggestions: null,
}),
);
return t.stringLiteral(`TODO handle ${declarator.id}`);
} else {
cx.errors.push({
reason: `(CodegenReactiveFunction::codegenInstructionValue) Handle conversion of ${stmt.type} to expression`,
category: ErrorCategory.Todo,
loc: stmt.loc ?? null,
suggestions: null,
});
cx.recordError(
new CompilerErrorDetail({
reason: `(CodegenReactiveFunction::codegenInstructionValue) Handle conversion of ${stmt.type} to expression`,
category: ErrorCategory.Todo,
loc: stmt.loc ?? null,
suggestions: null,
}),
);
return t.stringLiteral(`TODO handle ${stmt.type}`);
}
}

View File

@@ -1,294 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError} from '../CompilerError';
import {
Environment,
Identifier,
IdentifierId,
InstructionId,
Place,
PropertyLiteral,
ReactiveBlock,
ReactiveFunction,
ReactiveInstruction,
ReactiveScopeBlock,
ReactiveTerminalStatement,
getHookKind,
isUseRefType,
isUseStateType,
} from '../HIR';
import {eachCallArgument, eachInstructionLValue} from '../HIR/visitors';
import DisjointSet from '../Utils/DisjointSet';
import {assertExhaustive} from '../Utils/utils';
import {ReactiveFunctionVisitor, visitReactiveFunction} from './visitors';
/**
* This pass is built based on the observation by @jbrown215 that arguments
* to useState and useRef are only used the first time a component is rendered.
* Any subsequent times, the arguments will be evaluated but ignored. In this pass,
* we use this fact to improve the output of the compiler by not recomputing values that
* are only used as arguments (or inputs to arguments to) useState and useRef.
*
* This pass isn't yet stress-tested so it's not enabled by default. It's only enabled
* to support certain debug modes that detect non-idempotent code, since non-idempotent
* code can "safely" be used if its only passed to useState and useRef. We plan to rewrite
* this pass in HIR and enable it as an optimization in the future.
*
* Algorithm:
* We take two passes over the reactive function AST. In the first pass, we gather
* aliases and build relationships between property accesses--the key thing we need
* to do here is to find that, e.g., $0.x and $1 refer to the same value if
* $1 = PropertyLoad $0.x.
*
* In the second pass, we traverse the AST in reverse order and track how each place
* is used. If a place is read from in any Terminal, we mark the place as "Update", meaning
* it is used whenever the component is updated/re-rendered. If a place is read from in
* a useState or useRef hook call, we mark it as "Create", since it is only used when the
* component is created. In other instructions, we propagate the inferred place for the
* instructions lvalues onto any other instructions that are read.
*
* Whenever we finish this reverse pass over a reactive block, we can look at the blocks
* dependencies and see whether the dependencies are used in an "Update" context or only
* in a "Create" context. If a dependency is create-only, then we can remove that dependency
* from the block.
*/
type CreateUpdate = 'Create' | 'Update' | 'Unknown';
type KindMap = Map<IdentifierId, CreateUpdate>;
class Visitor extends ReactiveFunctionVisitor<CreateUpdate> {
map: KindMap = new Map();
aliases: DisjointSet<IdentifierId>;
paths: Map<IdentifierId, Map<PropertyLiteral, IdentifierId>>;
env: Environment;
constructor(
env: Environment,
aliases: DisjointSet<IdentifierId>,
paths: Map<IdentifierId, Map<PropertyLiteral, IdentifierId>>,
) {
super();
this.aliases = aliases;
this.paths = paths;
this.env = env;
}
join(values: Array<CreateUpdate>): CreateUpdate {
function join2(l: CreateUpdate, r: CreateUpdate): CreateUpdate {
if (l === 'Update' || r === 'Update') {
return 'Update';
} else if (l === 'Create' || r === 'Create') {
return 'Create';
} else if (l === 'Unknown' || r === 'Unknown') {
return 'Unknown';
}
assertExhaustive(r, `Unhandled variable kind ${r}`);
}
return values.reduce(join2, 'Unknown');
}
isCreateOnlyHook(id: Identifier): boolean {
return isUseStateType(id) || isUseRefType(id);
}
override visitPlace(
_: InstructionId,
place: Place,
state: CreateUpdate,
): void {
this.map.set(
place.identifier.id,
this.join([state, this.map.get(place.identifier.id) ?? 'Unknown']),
);
}
override visitBlock(block: ReactiveBlock, state: CreateUpdate): void {
super.visitBlock([...block].reverse(), state);
}
override visitInstruction(instruction: ReactiveInstruction): void {
const state = this.join(
[...eachInstructionLValue(instruction)].map(
operand => this.map.get(operand.identifier.id) ?? 'Unknown',
),
);
const visitCallOrMethodNonArgs = (): void => {
switch (instruction.value.kind) {
case 'CallExpression': {
this.visitPlace(instruction.id, instruction.value.callee, state);
break;
}
case 'MethodCall': {
this.visitPlace(instruction.id, instruction.value.property, state);
this.visitPlace(instruction.id, instruction.value.receiver, state);
break;
}
}
};
const isHook = (): boolean => {
let callee = null;
switch (instruction.value.kind) {
case 'CallExpression': {
callee = instruction.value.callee.identifier;
break;
}
case 'MethodCall': {
callee = instruction.value.property.identifier;
break;
}
}
return callee != null && getHookKind(this.env, callee) != null;
};
switch (instruction.value.kind) {
case 'CallExpression':
case 'MethodCall': {
if (
instruction.lvalue &&
this.isCreateOnlyHook(instruction.lvalue.identifier)
) {
[...eachCallArgument(instruction.value.args)].forEach(operand =>
this.visitPlace(instruction.id, operand, 'Create'),
);
visitCallOrMethodNonArgs();
} else {
this.traverseInstruction(instruction, isHook() ? 'Update' : state);
}
break;
}
default: {
this.traverseInstruction(instruction, state);
}
}
}
override visitScope(scope: ReactiveScopeBlock): void {
const state = this.join(
[
...scope.scope.declarations.keys(),
...[...scope.scope.reassignments.values()].map(ident => ident.id),
].map(id => this.map.get(id) ?? 'Unknown'),
);
super.visitScope(scope, state);
[...scope.scope.dependencies].forEach(ident => {
let target: undefined | IdentifierId =
this.aliases.find(ident.identifier.id) ?? ident.identifier.id;
ident.path.forEach(token => {
target &&= this.paths.get(target)?.get(token.property);
});
if (target && this.map.get(target) === 'Create') {
scope.scope.dependencies.delete(ident);
}
});
}
override visitTerminal(
stmt: ReactiveTerminalStatement,
state: CreateUpdate,
): void {
CompilerError.invariant(state !== 'Create', {
reason: "Visiting a terminal statement with state 'Create'",
loc: stmt.terminal.loc,
});
super.visitTerminal(stmt, state);
}
override visitReactiveFunctionValue(
_id: InstructionId,
_dependencies: Array<Place>,
fn: ReactiveFunction,
state: CreateUpdate,
): void {
visitReactiveFunction(fn, this, state);
}
}
export default function pruneInitializationDependencies(
fn: ReactiveFunction,
): void {
const [aliases, paths] = getAliases(fn);
visitReactiveFunction(fn, new Visitor(fn.env, aliases, paths), 'Update');
}
function update(
map: Map<IdentifierId, Map<PropertyLiteral, IdentifierId>>,
key: IdentifierId,
path: PropertyLiteral,
value: IdentifierId,
): void {
const inner = map.get(key) ?? new Map();
inner.set(path, value);
map.set(key, inner);
}
class AliasVisitor extends ReactiveFunctionVisitor {
scopeIdentifiers: DisjointSet<IdentifierId> = new DisjointSet<IdentifierId>();
scopePaths: Map<IdentifierId, Map<PropertyLiteral, IdentifierId>> = new Map();
override visitInstruction(instr: ReactiveInstruction): void {
if (
instr.value.kind === 'StoreLocal' ||
instr.value.kind === 'StoreContext'
) {
this.scopeIdentifiers.union([
instr.value.lvalue.place.identifier.id,
instr.value.value.identifier.id,
]);
} else if (
instr.value.kind === 'LoadLocal' ||
instr.value.kind === 'LoadContext'
) {
instr.lvalue &&
this.scopeIdentifiers.union([
instr.lvalue.identifier.id,
instr.value.place.identifier.id,
]);
} else if (instr.value.kind === 'PropertyLoad') {
instr.lvalue &&
update(
this.scopePaths,
instr.value.object.identifier.id,
instr.value.property,
instr.lvalue.identifier.id,
);
} else if (instr.value.kind === 'PropertyStore') {
update(
this.scopePaths,
instr.value.object.identifier.id,
instr.value.property,
instr.value.value.identifier.id,
);
}
}
}
function getAliases(
fn: ReactiveFunction,
): [
DisjointSet<IdentifierId>,
Map<IdentifierId, Map<PropertyLiteral, IdentifierId>>,
] {
const visitor = new AliasVisitor();
visitReactiveFunction(fn, visitor, null);
let disjoint = visitor.scopeIdentifiers;
let scopePaths = new Map<IdentifierId, Map<PropertyLiteral, IdentifierId>>();
for (const [key, value] of visitor.scopePaths) {
for (const [path, id] of value) {
update(
scopePaths,
disjoint.find(key) ?? key,
path,
disjoint.find(id) ?? id,
);
}
}
return [disjoint, scopePaths];
}

View File

@@ -1,739 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError, CompilerErrorDetailOptions, SourceLocation} from '..';
import {
ArrayExpression,
CallExpression,
Effect,
Environment,
FunctionExpression,
GeneratedSource,
HIRFunction,
Identifier,
IdentifierId,
Instruction,
InstructionId,
InstructionKind,
InstructionValue,
isUseEffectHookType,
LoadLocal,
makeInstructionId,
NonLocalImportSpecifier,
Place,
promoteTemporary,
} from '../HIR';
import {createTemporaryPlace, markInstructionIds} from '../HIR/HIRBuilder';
import {getOrInsertWith} from '../Utils/utils';
import {
BuiltInFireFunctionId,
BuiltInFireId,
DefaultNonmutatingHook,
} from '../HIR/ObjectShape';
import {eachInstructionOperand} from '../HIR/visitors';
import {printSourceLocationLine} from '../HIR/PrintHIR';
import {USE_FIRE_FUNCTION_NAME} from '../HIR/Environment';
import {ErrorCategory} from '../CompilerError';
/*
* TODO(jmbrown):
* - traverse object methods
* - method calls
* - React.useEffect calls
*/
const CANNOT_COMPILE_FIRE = 'Cannot compile `fire`';
export function transformFire(fn: HIRFunction): void {
const context = new Context(fn.env);
replaceFireFunctions(fn, context);
if (!context.hasErrors()) {
ensureNoMoreFireUses(fn, context);
}
context.throwIfErrorsFound();
}
function replaceFireFunctions(fn: HIRFunction, context: Context): void {
let importedUseFire: NonLocalImportSpecifier | null = null;
let hasRewrite = false;
for (const [, block] of fn.body.blocks) {
const rewriteInstrs = new Map<InstructionId, Array<Instruction>>();
const deleteInstrs = new Set<InstructionId>();
for (const instr of block.instructions) {
const {value, lvalue} = instr;
if (
value.kind === 'CallExpression' &&
isUseEffectHookType(value.callee.identifier) &&
value.args.length > 0 &&
value.args[0].kind === 'Identifier'
) {
const lambda = context.getFunctionExpression(
value.args[0].identifier.id,
);
if (lambda != null) {
const capturedCallees =
visitFunctionExpressionAndPropagateFireDependencies(
lambda,
context,
true,
);
// Add useFire calls for all fire calls in found in the lambda
const newInstrs = [];
for (const [
fireCalleePlace,
fireCalleeInfo,
] of capturedCallees.entries()) {
if (!context.hasCalleeWithInsertedFire(fireCalleePlace)) {
context.addCalleeWithInsertedFire(fireCalleePlace);
importedUseFire ??= fn.env.programContext.addImportSpecifier({
source: fn.env.programContext.reactRuntimeModule,
importSpecifierName: USE_FIRE_FUNCTION_NAME,
});
const loadUseFireInstr = makeLoadUseFireInstruction(
fn.env,
importedUseFire,
);
const loadFireCalleeInstr = makeLoadFireCalleeInstruction(
fn.env,
fireCalleeInfo.capturedCalleeIdentifier,
);
const callUseFireInstr = makeCallUseFireInstruction(
fn.env,
loadUseFireInstr.lvalue,
loadFireCalleeInstr.lvalue,
);
const storeUseFireInstr = makeStoreUseFireInstruction(
fn.env,
callUseFireInstr.lvalue,
fireCalleeInfo.fireFunctionBinding,
);
newInstrs.push(
loadUseFireInstr,
loadFireCalleeInstr,
callUseFireInstr,
storeUseFireInstr,
);
// We insert all of these instructions before the useEffect is loaded
const loadUseEffectInstrId = context.getLoadGlobalInstrId(
value.callee.identifier.id,
);
if (loadUseEffectInstrId == null) {
context.pushError({
loc: value.loc,
description: null,
category: ErrorCategory.Invariant,
reason: '[InsertFire] No LoadGlobal found for useEffect call',
suggestions: null,
});
continue;
}
rewriteInstrs.set(loadUseEffectInstrId, newInstrs);
}
}
ensureNoRemainingCalleeCaptures(
lambda.loweredFunc.func,
context,
capturedCallees,
);
if (
value.args.length > 1 &&
value.args[1] != null &&
value.args[1].kind === 'Identifier'
) {
const depArray = value.args[1];
const depArrayExpression = context.getArrayExpression(
depArray.identifier.id,
);
if (depArrayExpression != null) {
for (const dependency of depArrayExpression.elements) {
if (dependency.kind === 'Identifier') {
const loadOfDependency = context.getLoadLocalInstr(
dependency.identifier.id,
);
if (loadOfDependency != null) {
const replacedDepArrayItem = capturedCallees.get(
loadOfDependency.place.identifier.id,
);
if (replacedDepArrayItem != null) {
loadOfDependency.place =
replacedDepArrayItem.fireFunctionBinding;
}
}
}
}
} else {
context.pushError({
loc: value.args[1].loc,
description:
'You must use an array literal for an effect dependency array when that effect uses `fire()`',
category: ErrorCategory.Fire,
reason: CANNOT_COMPILE_FIRE,
suggestions: null,
});
}
} else if (value.args.length > 1 && value.args[1].kind === 'Spread') {
context.pushError({
loc: value.args[1].place.loc,
description:
'You must use an array literal for an effect dependency array when that effect uses `fire()`',
category: ErrorCategory.Fire,
reason: CANNOT_COMPILE_FIRE,
suggestions: null,
});
}
}
} else if (
value.kind === 'CallExpression' &&
value.callee.identifier.type.kind === 'Function' &&
value.callee.identifier.type.shapeId === BuiltInFireId &&
context.inUseEffectLambda()
) {
/*
* We found a fire(callExpr()) call. We remove the `fire()` call and replace the callExpr()
* with a freshly generated fire function binding. We'll insert the useFire call before the
* useEffect call, which happens in the CallExpression (useEffect) case above.
*/
/*
* We only allow fire to be called with a CallExpression: `fire(f())`
* TODO: add support for method calls: `fire(this.method())`
*/
if (value.args.length === 1 && value.args[0].kind === 'Identifier') {
const callExpr = context.getCallExpression(
value.args[0].identifier.id,
);
if (callExpr != null) {
const calleeId = callExpr.callee.identifier.id;
const loadLocal = context.getLoadLocalInstr(calleeId);
if (loadLocal == null) {
context.pushError({
loc: value.loc,
description: null,
category: ErrorCategory.Invariant,
reason:
'[InsertFire] No loadLocal found for fire call argument',
suggestions: null,
});
continue;
}
const fireFunctionBinding =
context.getOrGenerateFireFunctionBinding(
loadLocal.place,
value.loc,
);
loadLocal.place = {...fireFunctionBinding};
// Delete the fire call expression
deleteInstrs.add(instr.id);
} else {
context.pushError({
loc: value.loc,
description:
'`fire()` can only receive a function call such as `fire(fn(a,b)). Method calls and other expressions are not allowed',
category: ErrorCategory.Fire,
reason: CANNOT_COMPILE_FIRE,
suggestions: null,
});
}
} else {
let description: string =
'fire() can only take in a single call expression as an argument';
if (value.args.length === 0) {
description += ' but received none';
} else if (value.args.length > 1) {
description += ' but received multiple arguments';
} else if (value.args[0].kind === 'Spread') {
description += ' but received a spread argument';
}
context.pushError({
loc: value.loc,
description,
category: ErrorCategory.Fire,
reason: CANNOT_COMPILE_FIRE,
suggestions: null,
});
}
} else if (value.kind === 'CallExpression') {
context.addCallExpression(lvalue.identifier.id, value);
} else if (
value.kind === 'FunctionExpression' &&
context.inUseEffectLambda()
) {
visitFunctionExpressionAndPropagateFireDependencies(
value,
context,
false,
);
} else if (value.kind === 'FunctionExpression') {
context.addFunctionExpression(lvalue.identifier.id, value);
} else if (value.kind === 'LoadLocal') {
context.addLoadLocalInstr(lvalue.identifier.id, value);
} else if (
value.kind === 'LoadGlobal' &&
value.binding.kind === 'ImportSpecifier' &&
value.binding.module === 'react' &&
value.binding.imported === 'fire' &&
context.inUseEffectLambda()
) {
deleteInstrs.add(instr.id);
} else if (value.kind === 'LoadGlobal') {
context.addLoadGlobalInstrId(lvalue.identifier.id, instr.id);
} else if (value.kind === 'ArrayExpression') {
context.addArrayExpression(lvalue.identifier.id, value);
}
}
block.instructions = rewriteInstructions(rewriteInstrs, block.instructions);
block.instructions = deleteInstructions(deleteInstrs, block.instructions);
if (rewriteInstrs.size > 0 || deleteInstrs.size > 0) {
hasRewrite = true;
fn.env.hasFireRewrite = true;
}
}
if (hasRewrite) {
markInstructionIds(fn.body);
}
}
/**
* Traverses a function expression to find fire calls fire(foo()) and replaces them with
* fireFoo().
*
* When a function captures a fire call we need to update its context to reflect the newly created
* fire function bindings and update the LoadLocals referenced by the function's dependencies.
*
* @param isUseEffect is necessary so we can keep track of when we should additionally insert
* useFire hooks calls.
*/
function visitFunctionExpressionAndPropagateFireDependencies(
fnExpr: FunctionExpression,
context: Context,
enteringUseEffect: boolean,
): FireCalleesToFireFunctionBinding {
let withScope = enteringUseEffect
? context.withUseEffectLambdaScope.bind(context)
: context.withFunctionScope.bind(context);
const calleesCapturedByFnExpression = withScope(() =>
replaceFireFunctions(fnExpr.loweredFunc.func, context),
);
// For each replaced callee, update the context of the function expression to track it
for (
let contextIdx = 0;
contextIdx < fnExpr.loweredFunc.func.context.length;
contextIdx++
) {
const contextItem = fnExpr.loweredFunc.func.context[contextIdx];
const replacedCallee = calleesCapturedByFnExpression.get(
contextItem.identifier.id,
);
if (replacedCallee != null) {
fnExpr.loweredFunc.func.context[contextIdx] = {
...replacedCallee.fireFunctionBinding,
};
}
}
context.mergeCalleesFromInnerScope(calleesCapturedByFnExpression);
return calleesCapturedByFnExpression;
}
/*
* eachInstructionOperand is not sufficient for our cases because:
* 1. fire is a global, which will not appear
* 2. The HIR may be malformed, so can't rely on function deps and must
* traverse the whole function.
*/
function* eachReachablePlace(fn: HIRFunction): Iterable<Place> {
for (const [, block] of fn.body.blocks) {
for (const instr of block.instructions) {
if (
instr.value.kind === 'FunctionExpression' ||
instr.value.kind === 'ObjectMethod'
) {
yield* eachReachablePlace(instr.value.loweredFunc.func);
} else {
yield* eachInstructionOperand(instr);
}
}
}
}
function ensureNoRemainingCalleeCaptures(
fn: HIRFunction,
context: Context,
capturedCallees: FireCalleesToFireFunctionBinding,
): void {
for (const place of eachReachablePlace(fn)) {
const calleeInfo = capturedCallees.get(place.identifier.id);
if (calleeInfo != null) {
const calleeName =
calleeInfo.capturedCalleeIdentifier.name?.kind === 'named'
? calleeInfo.capturedCalleeIdentifier.name.value
: '<unknown>';
context.pushError({
loc: place.loc,
description: `All uses of ${calleeName} must be either used with a fire() call in \
this effect or not used with a fire() call at all. ${calleeName} was used with fire() on line \
${printSourceLocationLine(calleeInfo.fireLoc)} in this effect`,
category: ErrorCategory.Fire,
reason: CANNOT_COMPILE_FIRE,
suggestions: null,
});
}
}
}
function ensureNoMoreFireUses(fn: HIRFunction, context: Context): void {
for (const place of eachReachablePlace(fn)) {
if (
place.identifier.type.kind === 'Function' &&
place.identifier.type.shapeId === BuiltInFireId
) {
context.pushError({
loc: place.identifier.loc,
description: 'Cannot use `fire` outside of a useEffect function',
category: ErrorCategory.Fire,
reason: CANNOT_COMPILE_FIRE,
suggestions: null,
});
}
}
}
function makeLoadUseFireInstruction(
env: Environment,
importedLoadUseFire: NonLocalImportSpecifier,
): Instruction {
const useFirePlace = createTemporaryPlace(env, GeneratedSource);
useFirePlace.effect = Effect.Read;
useFirePlace.identifier.type = DefaultNonmutatingHook;
const instrValue: InstructionValue = {
kind: 'LoadGlobal',
binding: {...importedLoadUseFire},
loc: GeneratedSource,
};
return {
id: makeInstructionId(0),
value: instrValue,
lvalue: {...useFirePlace},
loc: GeneratedSource,
effects: null,
};
}
function makeLoadFireCalleeInstruction(
env: Environment,
fireCalleeIdentifier: Identifier,
): Instruction {
const loadedFireCallee = createTemporaryPlace(env, GeneratedSource);
const fireCallee: Place = {
kind: 'Identifier',
identifier: fireCalleeIdentifier,
reactive: false,
effect: Effect.Unknown,
loc: fireCalleeIdentifier.loc,
};
return {
id: makeInstructionId(0),
value: {
kind: 'LoadLocal',
loc: GeneratedSource,
place: {...fireCallee},
},
lvalue: {...loadedFireCallee},
loc: GeneratedSource,
effects: null,
};
}
function makeCallUseFireInstruction(
env: Environment,
useFirePlace: Place,
argPlace: Place,
): Instruction {
const useFireCallResultPlace = createTemporaryPlace(env, GeneratedSource);
useFireCallResultPlace.effect = Effect.Read;
const useFireCall: CallExpression = {
kind: 'CallExpression',
callee: {...useFirePlace},
args: [argPlace],
loc: GeneratedSource,
};
return {
id: makeInstructionId(0),
value: useFireCall,
lvalue: {...useFireCallResultPlace},
loc: GeneratedSource,
effects: null,
};
}
function makeStoreUseFireInstruction(
env: Environment,
useFireCallResultPlace: Place,
fireFunctionBindingPlace: Place,
): Instruction {
promoteTemporary(fireFunctionBindingPlace.identifier);
const fireFunctionBindingLValuePlace = createTemporaryPlace(
env,
GeneratedSource,
);
return {
id: makeInstructionId(0),
value: {
kind: 'StoreLocal',
lvalue: {
kind: InstructionKind.Const,
place: {...fireFunctionBindingPlace},
},
value: {...useFireCallResultPlace},
type: null,
loc: GeneratedSource,
},
lvalue: fireFunctionBindingLValuePlace,
loc: GeneratedSource,
effects: null,
};
}
type FireCalleesToFireFunctionBinding = Map<
IdentifierId,
{
fireFunctionBinding: Place;
capturedCalleeIdentifier: Identifier;
fireLoc: SourceLocation;
}
>;
class Context {
#env: Environment;
#errors: CompilerError = new CompilerError();
/*
* Used to look up the call expression passed to a `fire(callExpr())`. Gives back
* the `callExpr()`.
*/
#callExpressions = new Map<IdentifierId, CallExpression>();
/*
* We keep track of function expressions so that we can traverse them when
* we encounter a lambda passed to a useEffect call
*/
#functionExpressions = new Map<IdentifierId, FunctionExpression>();
/*
* Mapping from lvalue ids to the LoadLocal for it. Allows us to replace dependency LoadLocals.
*/
#loadLocals = new Map<IdentifierId, LoadLocal>();
/*
* Maps all of the fire callees found in a component/hook to the generated fire function places
* we create for them. Allows us to reuse already-inserted useFire results
*/
#fireCalleesToFireFunctions: Map<IdentifierId, Place> = new Map();
/*
* The callees for which we have already created fire bindings. Used to skip inserting a new
* useFire call for a fire callee if one has already been created.
*/
#calleesWithInsertedFire = new Set<IdentifierId>();
/*
* A mapping from fire callees to the created fire function bindings that are reachable from this
* scope.
*
* We additionally keep track of the captured callee identifier so that we can properly reference
* it in the place where we LoadLocal the callee as an argument to useFire.
*/
#capturedCalleeIdentifierIds: FireCalleesToFireFunctionBinding = new Map();
/*
* We only transform fire calls if we're syntactically within a useEffect lambda (for now)
*/
#inUseEffectLambda = false;
/*
* Mapping from useEffect callee identifier ids to the instruction id of the
* load global instruction for the useEffect call. We use this to insert the
* useFire calls before the useEffect call
*/
#loadGlobalInstructionIds = new Map<IdentifierId, InstructionId>();
constructor(env: Environment) {
this.#env = env;
}
/*
* We keep track of array expressions so we can rewrite dependency arrays passed to useEffect
* to use the fire functions
*/
#arrayExpressions = new Map<IdentifierId, ArrayExpression>();
pushError(error: CompilerErrorDetailOptions): void {
this.#errors.push(error);
}
withFunctionScope(fn: () => void): FireCalleesToFireFunctionBinding {
fn();
return this.#capturedCalleeIdentifierIds;
}
withUseEffectLambdaScope(fn: () => void): FireCalleesToFireFunctionBinding {
const capturedCalleeIdentifierIds = this.#capturedCalleeIdentifierIds;
const inUseEffectLambda = this.#inUseEffectLambda;
this.#capturedCalleeIdentifierIds = new Map();
this.#inUseEffectLambda = true;
const resultCapturedCalleeIdentifierIds = this.withFunctionScope(fn);
this.#capturedCalleeIdentifierIds = capturedCalleeIdentifierIds;
this.#inUseEffectLambda = inUseEffectLambda;
return resultCapturedCalleeIdentifierIds;
}
addCallExpression(id: IdentifierId, callExpr: CallExpression): void {
this.#callExpressions.set(id, callExpr);
}
getCallExpression(id: IdentifierId): CallExpression | undefined {
return this.#callExpressions.get(id);
}
addLoadLocalInstr(id: IdentifierId, loadLocal: LoadLocal): void {
this.#loadLocals.set(id, loadLocal);
}
getLoadLocalInstr(id: IdentifierId): LoadLocal | undefined {
return this.#loadLocals.get(id);
}
getOrGenerateFireFunctionBinding(
callee: Place,
fireLoc: SourceLocation,
): Place {
const fireFunctionBinding = getOrInsertWith(
this.#fireCalleesToFireFunctions,
callee.identifier.id,
() => createTemporaryPlace(this.#env, GeneratedSource),
);
fireFunctionBinding.identifier.type = {
kind: 'Function',
shapeId: BuiltInFireFunctionId,
return: {kind: 'Poly'},
isConstructor: false,
};
this.#capturedCalleeIdentifierIds.set(callee.identifier.id, {
fireFunctionBinding,
capturedCalleeIdentifier: callee.identifier,
fireLoc,
});
return fireFunctionBinding;
}
mergeCalleesFromInnerScope(
innerCallees: FireCalleesToFireFunctionBinding,
): void {
for (const [id, calleeInfo] of innerCallees.entries()) {
this.#capturedCalleeIdentifierIds.set(id, calleeInfo);
}
}
addCalleeWithInsertedFire(id: IdentifierId): void {
this.#calleesWithInsertedFire.add(id);
}
hasCalleeWithInsertedFire(id: IdentifierId): boolean {
return this.#calleesWithInsertedFire.has(id);
}
inUseEffectLambda(): boolean {
return this.#inUseEffectLambda;
}
addFunctionExpression(id: IdentifierId, fn: FunctionExpression): void {
this.#functionExpressions.set(id, fn);
}
getFunctionExpression(id: IdentifierId): FunctionExpression | undefined {
return this.#functionExpressions.get(id);
}
addLoadGlobalInstrId(id: IdentifierId, instrId: InstructionId): void {
this.#loadGlobalInstructionIds.set(id, instrId);
}
getLoadGlobalInstrId(id: IdentifierId): InstructionId | undefined {
return this.#loadGlobalInstructionIds.get(id);
}
addArrayExpression(id: IdentifierId, array: ArrayExpression): void {
this.#arrayExpressions.set(id, array);
}
getArrayExpression(id: IdentifierId): ArrayExpression | undefined {
return this.#arrayExpressions.get(id);
}
hasErrors(): boolean {
return this.#errors.hasAnyErrors();
}
throwIfErrorsFound(): void {
if (this.hasErrors()) throw this.#errors;
}
}
function deleteInstructions(
deleteInstrs: Set<InstructionId>,
instructions: Array<Instruction>,
): Array<Instruction> {
if (deleteInstrs.size > 0) {
const newInstrs = instructions.filter(instr => !deleteInstrs.has(instr.id));
return newInstrs;
}
return instructions;
}
function rewriteInstructions(
rewriteInstrs: Map<InstructionId, Array<Instruction>>,
instructions: Array<Instruction>,
): Array<Instruction> {
if (rewriteInstrs.size > 0) {
const newInstrs = [];
for (const instr of instructions) {
const newInstrsAtId = rewriteInstrs.get(instr.id);
if (newInstrsAtId != null) {
newInstrs.push(...newInstrsAtId, instr);
} else {
newInstrs.push(instr);
}
}
return newInstrs;
}
return instructions;
}

View File

@@ -4,5 +4,3 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
export {transformFire} from './TransformFire';

View File

@@ -8,7 +8,6 @@
import * as t from '@babel/types';
import {CompilerError} from '../CompilerError';
import {Environment} from '../HIR';
import {lowerType} from '../HIR/BuildHIR';
import {
GeneratedSource,
HIRFunction,
@@ -26,7 +25,6 @@ import {
} from '../HIR/HIR';
import {
BuiltInArrayId,
BuiltInEventHandlerId,
BuiltInFunctionId,
BuiltInJsxId,
BuiltInMixedReadonlyId,
@@ -223,22 +221,11 @@ function* generateInstructionTypes(
}
case 'StoreLocal': {
if (env.config.enableUseTypeAnnotations) {
yield equation(
value.lvalue.place.identifier.type,
value.value.identifier.type,
);
const valueType =
value.type === null ? makeType() : lowerType(value.type);
yield equation(valueType, value.lvalue.place.identifier.type);
yield equation(left, valueType);
} else {
yield equation(left, value.value.identifier.type);
yield equation(
value.lvalue.place.identifier.type,
value.value.identifier.type,
);
}
yield equation(left, value.value.identifier.type);
yield equation(
value.lvalue.place.identifier.type,
value.value.identifier.type,
);
break;
}
@@ -422,12 +409,7 @@ function* generateInstructionTypes(
}
case 'TypeCastExpression': {
if (env.config.enableUseTypeAnnotations) {
yield equation(value.type, value.value.identifier.type);
yield equation(left, value.type);
} else {
yield equation(left, value.value.identifier.type);
}
yield equation(left, value.value.identifier.type);
break;
}
@@ -473,41 +455,6 @@ function* generateInstructionTypes(
}
}
}
if (env.config.enableInferEventHandlers) {
if (
value.kind === 'JsxExpression' &&
value.tag.kind === 'BuiltinTag' &&
!value.tag.name.includes('-')
) {
/*
* Infer event handler types for built-in DOM elements.
* Props starting with "on" (e.g., onClick, onSubmit) on primitive tags
* are inferred as event handlers. This allows functions with ref access
* to be passed to these props, since DOM event handlers are guaranteed
* by React to only execute in response to events, never during render.
*
* We exclude tags with hyphens to avoid web components (custom elements),
* which are required by the HTML spec to contain a hyphen. Web components
* may call event handler props during their lifecycle methods (e.g.,
* connectedCallback), which would be unsafe for ref access.
*/
for (const prop of value.props) {
if (
prop.kind === 'JsxAttribute' &&
prop.name.startsWith('on') &&
prop.name.length > 2 &&
prop.name[2] === prop.name[2].toUpperCase()
) {
yield equation(prop.place.identifier.type, {
kind: 'Function',
shapeId: BuiltInEventHandlerId,
return: makeType(),
isConstructor: false,
});
}
}
}
}
yield equation(left, {kind: 'Object', shapeId: BuiltInJsxId});
break;
}

View File

@@ -39,14 +39,6 @@ function tryParseTestPragmaValue(val: string): Result<unknown, unknown> {
const testComplexConfigDefaults: PartialEnvironmentConfig = {
validateNoCapitalizedCalls: [],
enableChangeDetectionForDebugging: {
source: 'react-compiler-runtime',
importSpecifierName: '$structuralCheck',
},
enableEmitFreeze: {
source: 'react-compiler-runtime',
importSpecifierName: 'makeReadOnly',
},
enableEmitInstrumentForget: {
fn: {
source: 'react-compiler-runtime',
@@ -62,37 +54,6 @@ const testComplexConfigDefaults: PartialEnvironmentConfig = {
source: 'react-compiler-runtime',
importSpecifierName: '$dispatcherGuard',
},
inlineJsxTransform: {
elementSymbol: 'react.transitional.element',
globalDevVar: 'DEV',
},
lowerContextAccess: {
source: 'react-compiler-runtime',
importSpecifierName: 'useContext_withSelector',
},
inferEffectDependencies: [
{
function: {
source: 'react',
importSpecifierName: 'useEffect',
},
autodepsIndex: 1,
},
{
function: {
source: 'shared-runtime',
importSpecifierName: 'useSpecialEffect',
},
autodepsIndex: 2,
},
{
function: {
source: 'useEffectWrapper',
importSpecifierName: 'default',
},
autodepsIndex: 1,
},
],
};
function* splitPragma(

View File

@@ -5,7 +5,9 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError} from '..';
import {CompilerDiagnostic, CompilerError} from '..';
import {ErrorCategory} from '../CompilerError';
import {Environment} from '../HIR/Environment';
import {HIRFunction, IdentifierId, Place} from '../HIR';
import {printPlace} from '../HIR/PrintHIR';
import {eachInstructionValueLValue, eachPatternOperand} from '../HIR/visitors';
@@ -17,12 +19,13 @@ import {eachInstructionValueLValue, eachPatternOperand} from '../HIR/visitors';
*/
export function validateContextVariableLValues(fn: HIRFunction): void {
const identifierKinds: IdentifierKinds = new Map();
validateContextVariableLValuesImpl(fn, identifierKinds);
validateContextVariableLValuesImpl(fn, identifierKinds, fn.env);
}
function validateContextVariableLValuesImpl(
fn: HIRFunction,
identifierKinds: IdentifierKinds,
env: Environment,
): void {
for (const [, block] of fn.body.blocks) {
for (const instr of block.instructions) {
@@ -30,30 +33,30 @@ function validateContextVariableLValuesImpl(
switch (value.kind) {
case 'DeclareContext':
case 'StoreContext': {
visit(identifierKinds, value.lvalue.place, 'context');
visit(identifierKinds, value.lvalue.place, 'context', env);
break;
}
case 'LoadContext': {
visit(identifierKinds, value.place, 'context');
visit(identifierKinds, value.place, 'context', env);
break;
}
case 'StoreLocal':
case 'DeclareLocal': {
visit(identifierKinds, value.lvalue.place, 'local');
visit(identifierKinds, value.lvalue.place, 'local', env);
break;
}
case 'LoadLocal': {
visit(identifierKinds, value.place, 'local');
visit(identifierKinds, value.place, 'local', env);
break;
}
case 'PostfixUpdate':
case 'PrefixUpdate': {
visit(identifierKinds, value.lvalue, 'local');
visit(identifierKinds, value.lvalue, 'local', env);
break;
}
case 'Destructure': {
for (const lvalue of eachPatternOperand(value.lvalue.pattern)) {
visit(identifierKinds, lvalue, 'destructure');
visit(identifierKinds, lvalue, 'destructure', env);
}
break;
}
@@ -62,18 +65,24 @@ function validateContextVariableLValuesImpl(
validateContextVariableLValuesImpl(
value.loweredFunc.func,
identifierKinds,
env,
);
break;
}
default: {
for (const _ of eachInstructionValueLValue(value)) {
CompilerError.throwTodo({
reason:
'ValidateContextVariableLValues: unhandled instruction variant',
loc: value.loc,
description: `Handle '${value.kind} lvalues`,
suggestions: null,
});
fn.env.recordError(
CompilerDiagnostic.create({
category: ErrorCategory.Todo,
reason:
'ValidateContextVariableLValues: unhandled instruction variant',
description: `Handle '${value.kind} lvalues`,
}).withDetails({
kind: 'error',
loc: value.loc,
message: null,
}),
);
}
}
}
@@ -90,6 +99,7 @@ function visit(
identifiers: IdentifierKinds,
place: Place,
kind: 'local' | 'context' | 'destructure',
env: Environment,
): void {
const prev = identifiers.get(place.identifier.id);
if (prev !== undefined) {
@@ -97,12 +107,18 @@ function visit(
const isContext = kind === 'context';
if (wasContext !== isContext) {
if (prev.kind === 'destructure' || kind === 'destructure') {
CompilerError.throwTodo({
reason: `Support destructuring of context variables`,
loc: kind === 'destructure' ? place.loc : prev.place.loc,
description: null,
suggestions: null,
});
env.recordError(
CompilerDiagnostic.create({
category: ErrorCategory.Todo,
reason: `Support destructuring of context variables`,
description: null,
}).withDetails({
kind: 'error',
loc: kind === 'destructure' ? place.loc : prev.place.loc,
message: null,
}),
);
return;
}
CompilerError.invariant(false, {

View File

@@ -29,6 +29,9 @@ import {
isStableType,
isSubPath,
isSubPathIgnoringOptionals,
isUseEffectHookType,
isUseInsertionEffectHookType,
isUseLayoutEffectHookType,
isUseRefType,
LoadGlobal,
ManualMemoDependency,
@@ -41,9 +44,7 @@ import {
eachInstructionValueOperand,
eachTerminalOperand,
} from '../HIR/visitors';
import {Result} from '../Utils/Result';
import {retainWhere} from '../Utils/utils';
import {isEffectHook} from './ValidateMemoizedEffectDependencies';
const DEBUG = false;
@@ -86,9 +87,7 @@ 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,
): Result<void, CompilerError> {
export function validateExhaustiveDependencies(fn: HIRFunction): void {
const env = fn.env;
const reactive = collectReactiveIdentifiersHIR(fn);
@@ -103,7 +102,6 @@ export function validateExhaustiveDependencies(
loc: place.loc,
});
}
const error = new CompilerError();
let startMemo: StartMemoize | null = null;
function onStartMemoize(
@@ -144,7 +142,7 @@ export function validateExhaustiveDependencies(
'all',
);
if (diagnostic != null) {
error.pushDiagnostic(diagnostic);
fn.env.recordError(diagnostic);
}
}
@@ -209,13 +207,12 @@ export function validateExhaustiveDependencies(
effectReportMode,
);
if (diagnostic != null) {
error.pushDiagnostic(diagnostic);
fn.env.recordError(diagnostic);
}
},
},
false, // isFunctionExpression
);
return error.asResult();
}
function validateDependencies(
@@ -1111,3 +1108,11 @@ function createDiagnostic(
suggestions: suggestion != null ? [suggestion] : null,
});
}
export function isEffectHook(identifier: Identifier): boolean {
return (
isUseEffectHookType(identifier) ||
isUseLayoutEffectHookType(identifier) ||
isUseInsertionEffectHookType(identifier)
);
}

View File

@@ -6,13 +6,9 @@
*/
import * as t from '@babel/types';
import {
CompilerError,
CompilerErrorDetail,
ErrorCategory,
} from '../CompilerError';
import {CompilerErrorDetail, ErrorCategory} from '../CompilerError';
import {computeUnconditionalBlocks} from '../HIR/ComputeUnconditionalBlocks';
import {isHookName} from '../HIR/Environment';
import {Environment, isHookName} from '../HIR/Environment';
import {
HIRFunction,
IdentifierId,
@@ -26,7 +22,6 @@ 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
@@ -88,20 +83,17 @@ 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,
): Result<void, CompilerError> {
export function validateHooksUsage(fn: HIRFunction): void {
const unconditionalBlocks = computeUnconditionalBlocks(fn);
const errors = new CompilerError();
const errorsByPlace = new Map<t.SourceLocation, CompilerErrorDetail>();
function recordError(
function trackError(
loc: SourceLocation,
errorDetail: CompilerErrorDetail,
): void {
if (typeof loc === 'symbol') {
errors.pushErrorDetail(errorDetail);
fn.env.recordError(errorDetail);
} else {
errorsByPlace.set(loc, errorDetail);
}
@@ -121,7 +113,7 @@ export function validateHooksUsage(
* If that same place is also used as a conditional call, upgrade the error to a conditonal hook error
*/
if (previousError === undefined || previousError.reason !== reason) {
recordError(
trackError(
place.loc,
new CompilerErrorDetail({
category: ErrorCategory.Hooks,
@@ -137,7 +129,7 @@ export function validateHooksUsage(
const previousError =
typeof place.loc !== 'symbol' ? errorsByPlace.get(place.loc) : undefined;
if (previousError === undefined) {
recordError(
trackError(
place.loc,
new CompilerErrorDetail({
category: ErrorCategory.Hooks,
@@ -154,7 +146,7 @@ export function validateHooksUsage(
const previousError =
typeof place.loc !== 'symbol' ? errorsByPlace.get(place.loc) : undefined;
if (previousError === undefined) {
recordError(
trackError(
place.loc,
new CompilerErrorDetail({
category: ErrorCategory.Hooks,
@@ -399,7 +391,7 @@ export function validateHooksUsage(
}
case 'ObjectMethod':
case 'FunctionExpression': {
visitFunctionExpression(errors, instr.value.loweredFunc.func);
visitFunctionExpression(fn.env, instr.value.loweredFunc.func);
break;
}
default: {
@@ -424,18 +416,17 @@ export function validateHooksUsage(
}
for (const [, error] of errorsByPlace) {
errors.pushErrorDetail(error);
fn.env.recordError(error);
}
return errors.asResult();
}
function visitFunctionExpression(errors: CompilerError, fn: HIRFunction): void {
function visitFunctionExpression(env: Environment, fn: HIRFunction): void {
for (const [, block] of fn.body.blocks) {
for (const instr of block.instructions) {
switch (instr.value.kind) {
case 'ObjectMethod':
case 'FunctionExpression': {
visitFunctionExpression(errors, instr.value.loweredFunc.func);
visitFunctionExpression(env, instr.value.loweredFunc.func);
break;
}
case 'MethodCall':
@@ -446,7 +437,7 @@ function visitFunctionExpression(errors: CompilerError, fn: HIRFunction): void {
: instr.value.property;
const hookKind = getHookKind(fn.env, callee.identifier);
if (hookKind != null) {
errors.pushErrorDetail(
env.recordError(
new CompilerErrorDetail({
category: ErrorCategory.Hooks,
reason:

View File

@@ -7,6 +7,7 @@
import {CompilerDiagnostic, CompilerError, Effect} from '..';
import {ErrorCategory} from '../CompilerError';
import {Environment} from '../HIR/Environment';
import {HIRFunction, IdentifierId, Place} from '../HIR';
import {
eachInstructionLValue,
@@ -27,15 +28,15 @@ export function validateLocalsNotReassignedAfterRender(fn: HIRFunction): void {
contextVariables,
false,
false,
fn.env,
);
if (reassignment !== null) {
const errors = new CompilerError();
const variable =
reassignment.identifier.name != null &&
reassignment.identifier.name.kind === 'named'
? `\`${reassignment.identifier.name.value}\``
: 'variable';
errors.pushDiagnostic(
fn.env.recordError(
CompilerDiagnostic.create({
category: ErrorCategory.Immutability,
reason: 'Cannot reassign variable after render completes',
@@ -46,7 +47,6 @@ export function validateLocalsNotReassignedAfterRender(fn: HIRFunction): void {
message: `Cannot reassign ${variable} after render completes`,
}),
);
throw errors;
}
}
@@ -55,6 +55,7 @@ function getContextReassignment(
contextVariables: Set<IdentifierId>,
isFunctionExpression: boolean,
isAsync: boolean,
env: Environment,
): Place | null {
const reassigningFunctions = new Map<IdentifierId, Place>();
for (const [, block] of fn.body.blocks) {
@@ -68,6 +69,7 @@ function getContextReassignment(
contextVariables,
true,
isAsync || value.loweredFunc.func.async,
env,
);
if (reassignment === null) {
// If the function itself doesn't reassign, does one of its dependencies?
@@ -84,13 +86,12 @@ function getContextReassignment(
// if the function or its depends reassign, propagate that fact on the lvalue
if (reassignment !== null) {
if (isAsync || value.loweredFunc.func.async) {
const errors = new CompilerError();
const variable =
reassignment.identifier.name !== null &&
reassignment.identifier.name.kind === 'named'
? `\`${reassignment.identifier.name.value}\``
: 'variable';
errors.pushDiagnostic(
env.recordError(
CompilerDiagnostic.create({
category: ErrorCategory.Immutability,
reason: 'Cannot reassign variable in async function',
@@ -102,7 +103,7 @@ function getContextReassignment(
message: `Cannot reassign ${variable}`,
}),
);
throw errors;
return null;
}
reassigningFunctions.set(lvalue.identifier.id, reassignment);
}

View File

@@ -1,134 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError} from '..';
import {ErrorCategory} from '../CompilerError';
import {
Identifier,
Instruction,
ReactiveFunction,
ReactiveInstruction,
ReactiveScopeBlock,
ScopeId,
isUseEffectHookType,
isUseInsertionEffectHookType,
isUseLayoutEffectHookType,
} from '../HIR';
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
import {
ReactiveFunctionVisitor,
visitReactiveFunction,
} from '../ReactiveScopes/visitors';
import {Result} from '../Utils/Result';
/**
* Validates that all known effect dependencies are memoized. The algorithm checks two things:
* - Disallow effect dependencies that should be memoized (have a reactive scope assigned) but
* where that reactive scope does not exist. This checks for cases where a reactive scope was
* pruned for some reason, such as spanning a hook.
* - Disallow effect dependencies whose a mutable range that encompasses the effect call.
*
* This latter check corresponds to any values which Forget knows may be mutable and may be mutated
* after the effect. Note that it's possible Forget may miss not memoize a value for some other reason,
* but in general this is a bug. The only reason Forget would _choose_ to skip memoization of an
* effect dependency is because it's mutated later.
*
* Example:
*
* ```javascript
* const object = {}; // mutable range starts here...
*
* useEffect(() => {
* console.log('hello');
* }, [object]); // the dependency array picks up the mutable range of its mutable contents
*
* mutate(object); // ... mutable range ends here after this mutation
* ```
*/
export function validateMemoizedEffectDependencies(
fn: ReactiveFunction,
): Result<void, CompilerError> {
const errors = new CompilerError();
visitReactiveFunction(fn, new Visitor(), errors);
return errors.asResult();
}
class Visitor extends ReactiveFunctionVisitor<CompilerError> {
scopes: Set<ScopeId> = new Set();
override visitScope(
scopeBlock: ReactiveScopeBlock,
state: CompilerError,
): void {
this.traverseScope(scopeBlock, state);
/*
* Record scopes that exist in the AST so we can later check to see if
* effect dependencies which should be memoized (have a scope assigned)
* actually are memoized (that scope exists).
* However, we only record scopes if *their* dependencies are also
* memoized, allowing a transitive memoization check.
*/
let areDependenciesMemoized = true;
for (const dep of scopeBlock.scope.dependencies) {
if (isUnmemoized(dep.identifier, this.scopes)) {
areDependenciesMemoized = false;
break;
}
}
if (areDependenciesMemoized) {
this.scopes.add(scopeBlock.scope.id);
for (const id of scopeBlock.scope.merged) {
this.scopes.add(id);
}
}
}
override visitInstruction(
instruction: ReactiveInstruction,
state: CompilerError,
): void {
this.traverseInstruction(instruction, state);
if (
instruction.value.kind === 'CallExpression' &&
isEffectHook(instruction.value.callee.identifier) &&
instruction.value.args.length >= 2
) {
const deps = instruction.value.args[1]!;
if (
deps.kind === 'Identifier' &&
/*
* TODO: isMutable is not safe to call here as it relies on identifier mutableRange which is no longer valid at this point
* in the pipeline
*/
(isMutable(instruction as Instruction, deps) ||
isUnmemoized(deps.identifier, this.scopes))
) {
state.push({
category: ErrorCategory.EffectDependencies,
reason:
'React Compiler has skipped optimizing this component because the effect dependencies could not be memoized. Unmemoized effect dependencies can trigger an infinite loop or other unexpected behavior',
description: null,
loc: typeof instruction.loc !== 'symbol' ? instruction.loc : null,
suggestions: null,
});
}
}
}
}
function isUnmemoized(operand: Identifier, scopes: Set<ScopeId>): boolean {
return operand.scope != null && !scopes.has(operand.scope.id);
}
export function isEffectHook(identifier: Identifier): boolean {
return (
isUseEffectHookType(identifier) ||
isUseLayoutEffectHookType(identifier) ||
isUseInsertionEffectHookType(identifier)
);
}

View File

@@ -5,33 +5,21 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError, EnvironmentConfig} from '..';
import {CompilerErrorDetail, 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,
): Result<void, CompilerError> {
export function validateNoCapitalizedCalls(fn: HIRFunction): void {
const envConfig: EnvironmentConfig = fn.env.config;
const ALLOW_LIST = new Set([
...DEFAULT_GLOBALS.keys(),
...(envConfig.validateNoCapitalizedCalls ?? []),
]);
/*
* The hook pattern may allow uppercase names, like React$useState, so we need to be sure that we
* do not error in those cases
*/
const hookPattern =
envConfig.hookPattern != null ? new RegExp(envConfig.hookPattern) : null;
const isAllowed = (name: string): boolean => {
return (
ALLOW_LIST.has(name) || (hookPattern != null && hookPattern.test(name))
);
return ALLOW_LIST.has(name);
};
const errors = new CompilerError();
const capitalLoadGlobals = new Map<IdentifierId, string>();
const capitalizedProperties = new Map<IdentifierId, string>();
const reason =
@@ -56,13 +44,16 @@ export function validateNoCapitalizedCalls(
const calleeIdentifier = value.callee.identifier.id;
const calleeName = capitalLoadGlobals.get(calleeIdentifier);
if (calleeName != null) {
CompilerError.throwInvalidReact({
category: ErrorCategory.CapitalizedCalls,
reason,
description: `${calleeName} may be a component`,
loc: value.loc,
suggestions: null,
});
fn.env.recordError(
new CompilerErrorDetail({
category: ErrorCategory.CapitalizedCalls,
reason,
description: `${calleeName} may be a component`,
loc: value.loc,
suggestions: null,
}),
);
continue;
}
break;
}
@@ -80,18 +71,19 @@ export function validateNoCapitalizedCalls(
const propertyIdentifier = value.property.identifier.id;
const propertyName = capitalizedProperties.get(propertyIdentifier);
if (propertyName != null) {
errors.push({
category: ErrorCategory.CapitalizedCalls,
reason,
description: `${propertyName} may be a component`,
loc: value.loc,
suggestions: null,
});
fn.env.recordError(
new CompilerErrorDetail({
category: ErrorCategory.CapitalizedCalls,
reason,
description: `${propertyName} may be a component`,
loc: value.loc,
suggestions: null,
}),
);
}
break;
}
}
}
}
return errors.asResult();
}

View File

@@ -6,7 +6,7 @@
*/
import {CompilerError, SourceLocation} from '..';
import {ErrorCategory} from '../CompilerError';
import {CompilerErrorDetail, ErrorCategory} from '../CompilerError';
import {
ArrayExpression,
BlockId,
@@ -20,6 +20,7 @@ import {
eachInstructionValueOperand,
eachTerminalOperand,
} from '../HIR/visitors';
import {Environment} from '../HIR/Environment';
/**
* Validates that useEffect is not used for derived computations which could/should
@@ -49,8 +50,6 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
const functions: Map<IdentifierId, FunctionExpression> = new Map();
const locals: Map<IdentifierId, IdentifierId> = new Map();
const errors = new CompilerError();
for (const block of fn.body.blocks.values()) {
for (const instr of block.instructions) {
const {lvalue, value} = instr;
@@ -90,22 +89,19 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
validateEffect(
effectFunction.loweredFunc.func,
dependencies,
errors,
fn.env,
);
}
}
}
}
}
if (errors.hasAnyErrors()) {
throw errors;
}
}
function validateEffect(
effectFunction: HIRFunction,
effectDeps: Array<IdentifierId>,
errors: CompilerError,
env: Environment,
): void {
for (const operand of effectFunction.context) {
if (isSetStateType(operand.identifier)) {
@@ -219,13 +215,15 @@ function validateEffect(
}
for (const loc of setStateLocations) {
errors.push({
category: ErrorCategory.EffectDerivationsOfState,
reason:
'Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)',
description: null,
loc,
suggestions: null,
});
env.recordError(
new CompilerErrorDetail({
category: ErrorCategory.EffectDerivationsOfState,
reason:
'Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)',
description: null,
loc,
suggestions: null,
}),
);
}
}

View File

@@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerDiagnostic, CompilerError, Effect} from '..';
import {CompilerDiagnostic, Effect} from '..';
import {ErrorCategory} from '../CompilerError';
import {
HIRFunction,
@@ -18,7 +18,6 @@ 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
@@ -43,10 +42,7 @@ import {Result} from '../Utils/Result';
* 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,
): Result<void, CompilerError> {
const errors = new CompilerError();
export function validateNoFreezingKnownMutableFunctions(fn: HIRFunction): void {
const contextMutationEffects: Map<
IdentifierId,
Extract<AliasingEffect, {kind: 'Mutate'} | {kind: 'MutateTransitive'}>
@@ -63,7 +59,7 @@ export function validateNoFreezingKnownMutableFunctions(
place.identifier.name.kind === 'named'
? `\`${place.identifier.name.value}\``
: 'a local variable';
errors.pushDiagnostic(
fn.env.recordError(
CompilerDiagnostic.create({
category: ErrorCategory.Immutability,
reason: 'Cannot modify local variables after render completes',
@@ -162,5 +158,4 @@ export function validateNoFreezingKnownMutableFunctions(
visitOperand(operand);
}
}
return errors.asResult();
}

View File

@@ -5,11 +5,10 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerDiagnostic, CompilerError} from '..';
import {CompilerDiagnostic} 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
@@ -20,10 +19,7 @@ import {Result} from '../Utils/Result';
* 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,
): Result<void, CompilerError> {
const errors = new CompilerError();
export function validateNoImpureFunctionsInRender(fn: HIRFunction): void {
for (const [, block] of fn.body.blocks) {
for (const instr of block.instructions) {
const value = instr.value;
@@ -35,7 +31,7 @@ export function validateNoImpureFunctionsInRender(
callee.identifier.type,
);
if (signature != null && signature.impure === true) {
errors.pushDiagnostic(
fn.env.recordError(
CompilerDiagnostic.create({
category: ErrorCategory.Purity,
reason: 'Cannot call impure function during render',
@@ -55,5 +51,4 @@ export function validateNoImpureFunctionsInRender(
}
}
}
return errors.asResult();
}

View File

@@ -15,21 +15,18 @@ import {
GeneratedSource,
HIRFunction,
IdentifierId,
Identifier,
Place,
SourceLocation,
getHookKindForType,
isRefValueType,
isUseRefType,
} from '../HIR';
import {BuiltInEventHandlerId} from '../HIR/ObjectShape';
import {
eachInstructionOperand,
eachInstructionValueOperand,
eachPatternOperand,
eachTerminalOperand,
} from '../HIR/visitors';
import {Err, Ok, Result} from '../Utils/Result';
import {retainWhere} from '../Utils/utils';
/**
@@ -122,12 +119,14 @@ class Env {
}
}
export function validateNoRefAccessInRender(
fn: HIRFunction,
): Result<void, CompilerError> {
export function validateNoRefAccessInRender(fn: HIRFunction): void {
const env = new Env();
collectTemporariesSidemap(fn, env);
return validateNoRefAccessInRenderImpl(fn, env).map(_ => undefined);
const errors = new CompilerError();
validateNoRefAccessInRenderImpl(fn, env, errors);
for (const detail of errors.details) {
fn.env.recordError(detail);
}
}
function collectTemporariesSidemap(fn: HIRFunction, env: Env): void {
@@ -178,11 +177,6 @@ function refTypeOfType(place: Place): RefAccessType {
}
}
function isEventHandlerType(identifier: Identifier): boolean {
const type = identifier.type;
return type.kind === 'Function' && type.shapeId === BuiltInEventHandlerId;
}
function tyEqual(a: RefAccessType, b: RefAccessType): boolean {
if (a.kind !== b.kind) {
return false;
@@ -312,7 +306,8 @@ function joinRefAccessTypes(...types: Array<RefAccessType>): RefAccessType {
function validateNoRefAccessInRenderImpl(
fn: HIRFunction,
env: Env,
): Result<RefAccessType, CompilerError> {
errors: CompilerError,
): RefAccessType {
let returnValues: Array<undefined | RefAccessType> = [];
let place;
for (const param of fn.params) {
@@ -343,7 +338,6 @@ 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) {
@@ -439,13 +433,15 @@ 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 (result.isOk()) {
returnType = result.unwrap();
} else if (result.isErr()) {
if (!innerErrors.hasAnyErrors()) {
returnType = result;
} else {
readRefEffect = true;
}
env.set(instr.lvalue.identifier.id, {
@@ -491,29 +487,26 @@ function validateNoRefAccessInRenderImpl(
*/
if (!didError) {
const isRefLValue = isUseRefType(instr.lvalue.identifier);
const isEventHandlerLValue = isEventHandlerType(
instr.lvalue.identifier,
);
for (const operand of eachInstructionValueOperand(instr.value)) {
/**
* By default we check that function call operands are not refs,
* ref values, or functions that can access refs.
*/
if (
isRefLValue ||
isEventHandlerLValue ||
(hookKind != null &&
hookKind !== 'useState' &&
hookKind !== 'useReducer')
) {
if (
isRefLValue ||
(hookKind != null &&
hookKind !== 'useState' &&
hookKind !== 'useReducer')
) {
for (const operand of eachInstructionValueOperand(
instr.value,
)) {
/**
* Allow passing refs or ref-accessing functions when:
* 1. lvalue is a ref (mergeRefs pattern: `mergeRefs(ref1, ref2)`)
* 2. lvalue is an event handler (DOM events execute outside render)
* 3. calling hooks (independently validated for ref safety)
* 2. calling hooks (independently validated for ref safety)
*/
validateNoDirectRefValueAccess(errors, operand, env);
} else if (interpolatedAsJsx.has(instr.lvalue.identifier.id)) {
}
} else if (interpolatedAsJsx.has(instr.lvalue.identifier.id)) {
for (const operand of eachInstructionValueOperand(
instr.value,
)) {
/**
* Special case: the lvalue is passed as a jsx child
*
@@ -522,7 +515,98 @@ function validateNoRefAccessInRenderImpl(
* render function which attempts to obey the rules.
*/
validateNoRefValueAccess(errors, env, operand);
} else {
}
} else if (hookKind == null && instr.effects != null) {
/**
* For non-hook functions with known aliasing effects, use the
* effects to determine what validation to apply for each place.
* Track visited id:kind pairs to avoid duplicate errors.
*/
const visitedEffects: Set<string> = new Set();
for (const effect of instr.effects) {
let place: Place | null = null;
let validation: 'ref-passed' | 'direct-ref' | 'none' = 'none';
switch (effect.kind) {
case 'Freeze': {
place = effect.value;
validation = 'direct-ref';
break;
}
case 'Mutate':
case 'MutateTransitive':
case 'MutateConditionally':
case 'MutateTransitiveConditionally': {
place = effect.value;
validation = 'ref-passed';
break;
}
case 'Render': {
place = effect.place;
validation = 'ref-passed';
break;
}
case 'Capture':
case 'Alias':
case 'MaybeAlias':
case 'Assign':
case 'CreateFrom': {
place = effect.from;
validation = 'ref-passed';
break;
}
case 'ImmutableCapture': {
/**
* ImmutableCapture can come from two sources:
* 1. A known signature that explicitly freezes the operand
* (e.g. PanResponder.create) — safe, the function doesn't
* call callbacks during render.
* 2. Downgraded defaults when the operand is already frozen
* (e.g. foo(propRef)) — the function is unknown and may
* access the ref.
*
* We distinguish these by checking whether the same operand
* also has a Freeze effect on this instruction, which only
* comes from known signatures.
*/
place = effect.from;
const isFrozen = instr.effects.some(
e =>
e.kind === 'Freeze' &&
e.value.identifier.id === effect.from.identifier.id,
);
validation = isFrozen ? 'direct-ref' : 'ref-passed';
break;
}
case 'Create':
case 'CreateFunction':
case 'Apply':
case 'Impure':
case 'MutateFrozen':
case 'MutateGlobal': {
break;
}
}
if (place !== null && validation !== 'none') {
const key = `${place.identifier.id}:${validation}`;
if (!visitedEffects.has(key)) {
visitedEffects.add(key);
if (validation === 'direct-ref') {
validateNoDirectRefValueAccess(errors, place, env);
} else {
validateNoRefPassedToFunction(
errors,
env,
place,
place.loc,
);
}
}
}
}
} else {
for (const operand of eachInstructionValueOperand(
instr.value,
)) {
validateNoRefPassedToFunction(
errors,
env,
@@ -741,7 +825,7 @@ function validateNoRefAccessInRenderImpl(
}
if (errors.hasAnyErrors()) {
return Err(errors);
return {kind: 'None'};
}
}
@@ -750,10 +834,8 @@ function validateNoRefAccessInRenderImpl(
loc: GeneratedSource,
});
return Ok(
joinRefAccessTypes(
...returnValues.filter((env): env is RefAccessType => env !== undefined),
),
return joinRefAccessTypes(
...returnValues.filter((env): env is RefAccessType => env !== undefined),
);
}

View File

@@ -13,7 +13,6 @@ 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
@@ -43,17 +42,21 @@ import {Result} from '../Utils/Result';
* y();
* ```
*/
export function validateNoSetStateInRender(
fn: HIRFunction,
): Result<void, CompilerError> {
export function validateNoSetStateInRender(fn: HIRFunction): void {
const unconditionalSetStateFunctions: Set<IdentifierId> = new Set();
return validateNoSetStateInRenderImpl(fn, unconditionalSetStateFunctions);
const errors = validateNoSetStateInRenderImpl(
fn,
unconditionalSetStateFunctions,
);
for (const detail of errors.details) {
fn.env.recordError(detail);
}
}
function validateNoSetStateInRenderImpl(
fn: HIRFunction,
unconditionalSetStateFunctions: Set<IdentifierId>,
): Result<void, CompilerError> {
): CompilerError {
const unconditionalBlocks = computeUnconditionalBlocks(fn);
let activeManualMemoId: number | null = null;
const errors = new CompilerError();
@@ -92,7 +95,7 @@ function validateNoSetStateInRenderImpl(
validateNoSetStateInRenderImpl(
instr.value.loweredFunc.func,
unconditionalSetStateFunctions,
).isErr()
).hasAnyErrors()
) {
// This function expression unconditionally calls a setState
unconditionalSetStateFunctions.add(instr.lvalue.identifier.id);
@@ -183,5 +186,5 @@ function validateNoSetStateInRenderImpl(
}
}
return errors.asResult();
return errors;
}

View File

@@ -27,6 +27,7 @@ import {
ScopeId,
SourceLocation,
} from '../HIR';
import {Environment} from '../HIR/Environment';
import {printIdentifier, printManualMemoDependency} from '../HIR/PrintHIR';
import {
eachInstructionValueLValue,
@@ -37,7 +38,6 @@ import {
ReactiveFunctionVisitor,
visitReactiveFunction,
} from '../ReactiveScopes/visitors';
import {Result} from '../Utils/Result';
import {getOrInsertDefault} from '../Utils/utils';
/**
@@ -47,15 +47,12 @@ import {getOrInsertDefault} from '../Utils/utils';
* This can occur if a value's mutable range somehow extended to include a hook and
* was pruned.
*/
export function validatePreservedManualMemoization(
fn: ReactiveFunction,
): Result<void, CompilerError> {
export function validatePreservedManualMemoization(fn: ReactiveFunction): void {
const state = {
errors: new CompilerError(),
env: fn.env,
manualMemoState: null,
};
visitReactiveFunction(fn, new Visitor(), state);
return state.errors.asResult();
}
const DEBUG = false;
@@ -113,7 +110,7 @@ type ManualMemoBlockState = {
};
type VisitorState = {
errors: CompilerError;
env: Environment;
manualMemoState: ManualMemoBlockState | null;
};
@@ -233,7 +230,7 @@ function validateInferredDep(
temporaries: Map<IdentifierId, ManualMemoDependency>,
declsWithinMemoBlock: Set<DeclarationId>,
validDepsInMemoBlock: Array<ManualMemoDependency>,
errorState: CompilerError,
env: Environment,
memoLocation: SourceLocation,
): void {
let normalizedDep: ManualMemoDependency;
@@ -283,7 +280,7 @@ function validateInferredDep(
errorDiagnostic = merge(errorDiagnostic ?? compareResult, compareResult);
}
}
errorState.pushDiagnostic(
env.recordError(
CompilerDiagnostic.create({
category: ErrorCategory.PreserveManualMemo,
reason: 'Existing memoization could not be preserved',
@@ -429,7 +426,7 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
this.temporaries,
state.manualMemoState.decls,
state.manualMemoState.depsFromSource,
state.errors,
state.env,
state.manualMemoState.loc,
);
}
@@ -532,7 +529,7 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
!this.scopes.has(identifier.scope.id) &&
!this.prunedScopes.has(identifier.scope.id)
) {
state.errors.pushDiagnostic(
state.env.recordError(
CompilerDiagnostic.create({
category: ErrorCategory.PreserveManualMemo,
reason: 'Existing memoization could not be preserved',
@@ -578,7 +575,7 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
for (const identifier of decls) {
if (isUnmemoized(identifier, this.scopes)) {
state.errors.pushDiagnostic(
state.env.recordError(
CompilerDiagnostic.create({
category: ErrorCategory.PreserveManualMemo,
reason: 'Existing memoization could not be preserved',

View File

@@ -7,9 +7,9 @@
import {NodePath} from '@babel/traverse';
import * as t from '@babel/types';
import {CompilerDiagnostic, CompilerError, ErrorCategory} from '..';
import {CompilerDiagnostic, ErrorCategory} from '..';
import {CodegenFunction} from '../ReactiveScopes';
import {Result} from '../Utils/Result';
import {Environment} from '../HIR/Environment';
/**
* IMPORTANT: This validation is only intended for use in unit tests.
@@ -123,9 +123,8 @@ export function validateSourceLocations(
t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
>,
generatedAst: CodegenFunction,
): Result<void, CompilerError> {
const errors = new CompilerError();
env: Environment,
): void {
/*
* Step 1: Collect important locations from the original source
* Note: Multiple node types can share the same location (e.g. VariableDeclarator and Identifier)
@@ -240,7 +239,7 @@ export function validateSourceLocations(
loc: t.SourceLocation,
nodeType: string,
): void => {
errors.pushDiagnostic(
env.recordError(
CompilerDiagnostic.create({
category: ErrorCategory.Todo,
reason: 'Important source location missing in generated code',
@@ -260,7 +259,7 @@ export function validateSourceLocations(
expectedType: string,
actualTypes: Set<string>,
): void => {
errors.pushDiagnostic(
env.recordError(
CompilerDiagnostic.create({
category: ErrorCategory.Todo,
reason:
@@ -308,6 +307,4 @@ export function validateSourceLocations(
}
}
}
return errors.asResult();
}

View File

@@ -16,14 +16,13 @@ import {
IdentifierId,
SourceLocation,
} from '../HIR';
import {Environment} from '../HIR/Environment';
import {
eachInstructionValueOperand,
eachTerminalOperand,
} from '../HIR/visitors';
import {Result} from '../Utils/Result';
export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
const errors = new CompilerError();
export function validateUseMemo(fn: HIRFunction): void {
const voidMemoErrors = new CompilerError();
const useMemos = new Set<IdentifierId>();
const react = new Set<IdentifierId>();
@@ -91,7 +90,7 @@ export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
firstParam.kind === 'Identifier'
? firstParam.loc
: firstParam.place.loc;
errors.pushDiagnostic(
fn.env.recordError(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
reason: 'useMemo() callbacks may not accept parameters',
@@ -107,7 +106,7 @@ export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
}
if (body.loweredFunc.func.async || body.loweredFunc.func.generator) {
errors.pushDiagnostic(
fn.env.recordError(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
reason:
@@ -123,7 +122,7 @@ export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
);
}
validateNoContextVariableAssignment(body.loweredFunc.func, errors);
validateNoContextVariableAssignment(body.loweredFunc.func, fn.env);
if (fn.env.config.validateNoVoidUseMemo) {
if (!hasNonVoidReturn(body.loweredFunc.func)) {
@@ -177,12 +176,11 @@ export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
}
}
fn.env.logErrors(voidMemoErrors.asResult());
return errors.asResult();
}
function validateNoContextVariableAssignment(
fn: HIRFunction,
errors: CompilerError,
env: Environment,
): void {
const context = new Set(fn.context.map(place => place.identifier.id));
for (const block of fn.body.blocks.values()) {
@@ -191,7 +189,7 @@ function validateNoContextVariableAssignment(
switch (value.kind) {
case 'StoreContext': {
if (context.has(value.lvalue.place.identifier.id)) {
errors.pushDiagnostic(
env.recordError(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
reason:

View File

@@ -7,7 +7,6 @@
export {validateContextVariableLValues} from './ValidateContextVariableLValues';
export {validateHooksUsage} from './ValidateHooksUsage';
export {validateMemoizedEffectDependencies} from './ValidateMemoizedEffectDependencies';
export {validateNoCapitalizedCalls} from './ValidateNoCapitalizedCalls';
export {validateNoRefAccessInRender} from './ValidateNoRefAccessInRender';
export {validateNoSetStateInRender} from './ValidateNoSetStateInRender';

View File

@@ -24,24 +24,6 @@ describe('parseConfigPragma()', () => {
);
});
it('effect autodeps config must have at least 1 required argument', () => {
expect(() => {
validateEnvironmentConfig({
inferEffectDependencies: [
{
function: {
source: 'react',
importSpecifierName: 'useEffect',
},
autodepsIndex: 0,
},
],
} as any);
}).toThrowErrorMatchingInlineSnapshot(
`"Error: Could not validate environment config. Update React Compiler config to fix the error. Validation error: AutodepsIndex must be > 0 at "inferEffectDependencies[0].autodepsIndex"."`,
);
});
it('can parse stringy enums', () => {
const stringyHook = {
effectKind: 'freeze',

View File

@@ -1,148 +0,0 @@
## Input
```javascript
// @enableInferEventHandlers
import {useRef} from 'react';
// Simulates react-hook-form's handleSubmit
function handleSubmit<T>(callback: (data: T) => void | Promise<void>) {
return (event: any) => {
event.preventDefault();
callback({} as T);
};
}
// Simulates an upload function
async function upload(file: any): Promise<{blob: {url: string}}> {
return {blob: {url: 'https://example.com/file.jpg'}};
}
interface SignatureRef {
toFile(): any;
}
function Component() {
const ref = useRef<SignatureRef>(null);
const onSubmit = async (value: any) => {
// This should be allowed: accessing ref.current in an async event handler
// that's wrapped and passed to onSubmit prop
let sigUrl: string;
if (value.hasSignature) {
const {blob} = await upload(ref.current?.toFile());
sigUrl = blob?.url || '';
} else {
sigUrl = value.signature;
}
console.log('Signature URL:', sigUrl);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input type="text" name="signature" />
<button type="submit">Submit</button>
</form>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableInferEventHandlers
import { useRef } from "react";
// Simulates react-hook-form's handleSubmit
function handleSubmit(callback) {
const $ = _c(2);
let t0;
if ($[0] !== callback) {
t0 = (event) => {
event.preventDefault();
callback({} as T);
};
$[0] = callback;
$[1] = t0;
} else {
t0 = $[1];
}
return t0;
}
// Simulates an upload function
async function upload(file) {
const $ = _c(1);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = { blob: { url: "https://example.com/file.jpg" } };
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}
interface SignatureRef {
toFile(): any;
}
function Component() {
const $ = _c(4);
const ref = useRef(null);
const onSubmit = async (value) => {
let sigUrl;
if (value.hasSignature) {
const { blob } = await upload(ref.current?.toFile());
sigUrl = blob?.url || "";
} else {
sigUrl = value.signature;
}
console.log("Signature URL:", sigUrl);
};
const t0 = handleSubmit(onSubmit);
let t1;
let t2;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = <input type="text" name="signature" />;
t2 = <button type="submit">Submit</button>;
$[0] = t1;
$[1] = t2;
} else {
t1 = $[0];
t2 = $[1];
}
let t3;
if ($[2] !== t0) {
t3 = (
<form onSubmit={t0}>
{t1}
{t2}
</form>
);
$[2] = t0;
$[3] = t3;
} else {
t3 = $[3];
}
return t3;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};
```
### Eval output
(kind: ok) <form><input type="text" name="signature"><button type="submit">Submit</button></form>

View File

@@ -1,48 +0,0 @@
// @enableInferEventHandlers
import {useRef} from 'react';
// Simulates react-hook-form's handleSubmit
function handleSubmit<T>(callback: (data: T) => void | Promise<void>) {
return (event: any) => {
event.preventDefault();
callback({} as T);
};
}
// Simulates an upload function
async function upload(file: any): Promise<{blob: {url: string}}> {
return {blob: {url: 'https://example.com/file.jpg'}};
}
interface SignatureRef {
toFile(): any;
}
function Component() {
const ref = useRef<SignatureRef>(null);
const onSubmit = async (value: any) => {
// This should be allowed: accessing ref.current in an async event handler
// that's wrapped and passed to onSubmit prop
let sigUrl: string;
if (value.hasSignature) {
const {blob} = await upload(ref.current?.toFile());
sigUrl = blob?.url || '';
} else {
sigUrl = value.signature;
}
console.log('Signature URL:', sigUrl);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input type="text" name="signature" />
<button type="submit">Submit</button>
</form>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};

View File

@@ -1,100 +0,0 @@
## Input
```javascript
// @enableInferEventHandlers
import {useRef} from 'react';
// Simulates react-hook-form's handleSubmit or similar event handler wrappers
function handleSubmit<T>(callback: (data: T) => void) {
return (event: any) => {
event.preventDefault();
callback({} as T);
};
}
function Component() {
const ref = useRef<HTMLInputElement>(null);
const onSubmit = (data: any) => {
// This should be allowed: accessing ref.current in an event handler
// that's wrapped by handleSubmit and passed to onSubmit prop
if (ref.current !== null) {
console.log(ref.current.value);
}
};
return (
<>
<input ref={ref} />
<form onSubmit={handleSubmit(onSubmit)}>
<button type="submit">Submit</button>
</form>
</>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableInferEventHandlers
import { useRef } from "react";
// Simulates react-hook-form's handleSubmit or similar event handler wrappers
function handleSubmit(callback) {
const $ = _c(2);
let t0;
if ($[0] !== callback) {
t0 = (event) => {
event.preventDefault();
callback({} as T);
};
$[0] = callback;
$[1] = t0;
} else {
t0 = $[1];
}
return t0;
}
function Component() {
const $ = _c(1);
const ref = useRef(null);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
const onSubmit = (data) => {
if (ref.current !== null) {
console.log(ref.current.value);
}
};
t0 = (
<>
<input ref={ref} />
<form onSubmit={handleSubmit(onSubmit)}>
<button type="submit">Submit</button>
</form>
</>
);
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};
```
### Eval output
(kind: ok) <input><form><button type="submit">Submit</button></form>

View File

@@ -1,36 +0,0 @@
// @enableInferEventHandlers
import {useRef} from 'react';
// Simulates react-hook-form's handleSubmit or similar event handler wrappers
function handleSubmit<T>(callback: (data: T) => void) {
return (event: any) => {
event.preventDefault();
callback({} as T);
};
}
function Component() {
const ref = useRef<HTMLInputElement>(null);
const onSubmit = (data: any) => {
// This should be allowed: accessing ref.current in an event handler
// that's wrapped by handleSubmit and passed to onSubmit prop
if (ref.current !== null) {
console.log(ref.current.value);
}
};
return (
<>
<input ref={ref} />
<form onSubmit={handleSubmit(onSubmit)}>
<button type="submit">Submit</button>
</form>
</>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};

View File

@@ -1,53 +0,0 @@
## Input
```javascript
// @validateNoCapitalizedCalls @hookPattern:".*\b(use[^$]+)$"
import * as React from 'react';
const React$useState = React.useState;
const THIS_IS_A_CONSTANT = () => {};
function Component() {
const b = Boolean(true); // OK
const n = Number(3); // OK
const s = String('foo'); // OK
const [state, setState] = React$useState(0); // OK
const [state2, setState2] = React.useState(1); // OK
const constant = THIS_IS_A_CONSTANT(); // OK
return 3;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [],
isComponent: true,
};
```
## Code
```javascript
// @validateNoCapitalizedCalls @hookPattern:".*\b(use[^$]+)$"
import * as React from "react";
const React$useState = React.useState;
const THIS_IS_A_CONSTANT = () => {};
function Component() {
Boolean(true);
Number(3);
String("foo");
React$useState(0);
React.useState(1);
THIS_IS_A_CONSTANT();
return 3;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [],
isComponent: true,
};
```
### Eval output
(kind: ok) 3

View File

@@ -1,19 +0,0 @@
// @validateNoCapitalizedCalls @hookPattern:".*\b(use[^$]+)$"
import * as React from 'react';
const React$useState = React.useState;
const THIS_IS_A_CONSTANT = () => {};
function Component() {
const b = Boolean(true); // OK
const n = Number(3); // OK
const s = String('foo'); // OK
const [state, setState] = React$useState(0); // OK
const [state2, setState2] = React.useState(1); // OK
const constant = THIS_IS_A_CONSTANT(); // OK
return 3;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [],
isComponent: true,
};

View File

@@ -1,49 +0,0 @@
## Input
```javascript
// @enableChangeDetectionForDebugging
function Component(props) {
let x = null;
if (props.cond) {
x = [];
x.push(props.value);
}
return x;
}
```
## Code
```javascript
import { $structuralCheck } from "react-compiler-runtime";
import { c as _c } from "react/compiler-runtime"; // @enableChangeDetectionForDebugging
function Component(props) {
const $ = _c(2);
let x = null;
if (props.cond) {
{
x = [];
x.push(props.value);
let condition = $[0] !== props.value;
if (!condition) {
let old$x = $[1];
$structuralCheck(old$x, x, "x", "Component", "cached", "(3:6)");
}
$[0] = props.value;
$[1] = x;
if (condition) {
x = [];
x.push(props.value);
$structuralCheck($[1], x, "x", "Component", "recomputed", "(3:6)");
x = $[1];
}
}
}
return x;
}
```

View File

@@ -1,9 +0,0 @@
// @enableChangeDetectionForDebugging
function Component(props) {
let x = null;
if (props.cond) {
x = [];
x.push(props.value);
}
return x;
}

View File

@@ -1,39 +0,0 @@
## Input
```javascript
// @enableEmitFreeze @enableEmitInstrumentForget
function useFoo(props) {
return foo(props.x);
}
```
## Code
```javascript
import {
makeReadOnly,
shouldInstrument,
useRenderCounter,
} from "react-compiler-runtime";
import { c as _c } from "react/compiler-runtime"; // @enableEmitFreeze @enableEmitInstrumentForget
function useFoo(props) {
if (DEV && shouldInstrument)
useRenderCounter("useFoo", "/codegen-emit-imports-same-source.ts");
const $ = _c(2);
let t0;
if ($[0] !== props.x) {
t0 = foo(props.x);
$[0] = props.x;
$[1] = __DEV__ ? makeReadOnly(t0, "useFoo") : t0;
} else {
t0 = $[1];
}
return t0;
}
```

View File

@@ -1,5 +0,0 @@
// @enableEmitFreeze @enableEmitInstrumentForget
function useFoo(props) {
return foo(props.x);
}

View File

@@ -1,44 +0,0 @@
## Input
```javascript
// @enableEmitFreeze true
function MyComponentName(props) {
let x = {};
foo(x, props.a);
foo(x, props.b);
let y = [];
y.push(x);
return y;
}
```
## Code
```javascript
import { makeReadOnly } from "react-compiler-runtime";
import { c as _c } from "react/compiler-runtime"; // @enableEmitFreeze true
function MyComponentName(props) {
const $ = _c(3);
let y;
if ($[0] !== props.a || $[1] !== props.b) {
const x = {};
foo(x, props.a);
foo(x, props.b);
y = [];
y.push(x);
$[0] = props.a;
$[1] = props.b;
$[2] = __DEV__ ? makeReadOnly(y, "MyComponentName") : y;
} else {
y = $[2];
}
return y;
}
```

View File

@@ -1,11 +0,0 @@
// @enableEmitFreeze true
function MyComponentName(props) {
let x = {};
foo(x, props.a);
foo(x, props.b);
let y = [];
y.push(x);
return y;
}

View File

@@ -24,18 +24,9 @@ function useThing(fn) {
```
Found 1 error:
Compilation Skipped: `this` is not supported syntax
Error: Expected a non-reserved identifier name
React Compiler does not support compiling functions that use `this`.
error.reserved-words.ts:8:28
6 |
7 | if (ref.current === null) {
> 8 | ref.current = function (this: unknown, ...args) {
| ^^^^^^^^^^^^^ `this` was used here
9 | return fnRef.current.call(this, ...args);
10 | };
11 | }
`this` is a reserved word in JavaScript and cannot be used as an identifier name.
```

View File

@@ -1,37 +0,0 @@
## Input
```javascript
// @enableEmitFreeze @instrumentForget
let makeReadOnly = 'conflicting identifier';
function useFoo(props) {
return foo(props.x);
}
```
## Code
```javascript
import { makeReadOnly as _makeReadOnly } from "react-compiler-runtime";
import { c as _c } from "react/compiler-runtime"; // @enableEmitFreeze @instrumentForget
let makeReadOnly = "conflicting identifier";
function useFoo(props) {
const $ = _c(2);
let t0;
if ($[0] !== props.x) {
t0 = foo(props.x);
$[0] = props.x;
$[1] = __DEV__ ? _makeReadOnly(t0, "useFoo") : t0;
} else {
t0 = $[1];
}
return t0;
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -1,6 +0,0 @@
// @enableEmitFreeze @instrumentForget
let makeReadOnly = 'conflicting identifier';
function useFoo(props) {
return foo(props.x);
}

View File

@@ -1,33 +0,0 @@
## Input
```javascript
// @enableEmitFreeze @instrumentForget
function useFoo(props) {
return foo(props.x, __DEV__);
}
```
## Code
```javascript
import { makeReadOnly } from "react-compiler-runtime";
import { c as _c } from "react/compiler-runtime"; // @enableEmitFreeze @instrumentForget
function useFoo(props) {
const $ = _c(2);
let t0;
if ($[0] !== props.x) {
t0 = foo(props.x, __DEV__);
$[0] = props.x;
$[1] = __DEV__ ? makeReadOnly(t0, "useFoo") : t0;
} else {
t0 = $[1];
}
return t0;
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -1,4 +0,0 @@
// @enableEmitFreeze @instrumentForget
function useFoo(props) {
return foo(props.x, __DEV__);
}

View File

@@ -17,16 +17,17 @@ function Component(props) {
```
Found 1 error:
Todo: (BuildHIR::lowerAssignment) Handle computed properties in ObjectPattern
Invariant: [InferMutationAliasingEffects] Expected value kind to be initialized
error._todo.computed-lval-in-destructure.ts:3:9
1 | function Component(props) {
2 | const computedKey = props.key;
> 3 | const {[computedKey]: x} = props.val;
| ^^^^^^^^^^^^^^^^ (BuildHIR::lowerAssignment) Handle computed properties in ObjectPattern
<unknown> x$8.
error._todo.computed-lval-in-destructure.ts:5:9
3 | const {[computedKey]: x} = props.val;
4 |
5 | return x;
> 5 | return x;
| ^ this is uninitialized
6 | }
7 |
```

View File

@@ -1,34 +0,0 @@
## Input
```javascript
// @enableEmitFreeze @instrumentForget
function useFoo(props) {
const __DEV__ = 'conflicting global';
console.log(__DEV__);
return foo(props.x);
}
```
## Error
```
Found 1 error:
Todo: Encountered conflicting global in generated program
Conflict from local binding __DEV__.
error.emit-freeze-conflicting-global.ts:3:8
1 | // @enableEmitFreeze @instrumentForget
2 | function useFoo(props) {
> 3 | const __DEV__ = 'conflicting global';
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Encountered conflicting global in generated program
4 | console.log(__DEV__);
5 | return foo(props.x);
6 | }
```

View File

@@ -1,6 +0,0 @@
// @enableEmitFreeze @instrumentForget
function useFoo(props) {
const __DEV__ = 'conflicting global';
console.log(__DEV__);
return foo(props.x);
}

View File

@@ -0,0 +1,60 @@
## Input
```javascript
// @validateRefAccessDuringRender
/**
* This fixture tests fault tolerance: the compiler should report
* multiple independent errors rather than stopping at the first one.
*
* Error 1: Ref access during render (ref.current)
* Error 2: Mutation of frozen value (props)
*/
function Component(props) {
const ref = useRef(null);
// Error: reading ref during render
const value = ref.current;
// Error: mutating frozen value (props, which is frozen after hook call)
props.items = [];
return <div>{value}</div>;
}
```
## Error
```
Found 2 errors:
Error: This value cannot be modified
Modifying component props or hook arguments is not allowed. Consider using a local variable instead.
error.fault-tolerance-reports-multiple-errors.ts:16:2
14 |
15 | // Error: mutating frozen value (props, which is frozen after hook call)
> 16 | props.items = [];
| ^^^^^ value cannot be modified
17 |
18 | return <div>{value}</div>;
19 | }
Error: Cannot access refs during render
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
error.fault-tolerance-reports-multiple-errors.ts:13:16
11 |
12 | // Error: reading ref during render
> 13 | const value = ref.current;
| ^^^^^^^^^^^ Cannot access ref value during render
14 |
15 | // Error: mutating frozen value (props, which is frozen after hook call)
16 | props.items = [];
```

View File

@@ -0,0 +1,19 @@
// @validateRefAccessDuringRender
/**
* This fixture tests fault tolerance: the compiler should report
* multiple independent errors rather than stopping at the first one.
*
* Error 1: Ref access during render (ref.current)
* Error 2: Mutation of frozen value (props)
*/
function Component(props) {
const ref = useRef(null);
// Error: reading ref during render
const value = ref.current;
// Error: mutating frozen value (props, which is frozen after hook call)
props.items = [];
return <div>{value}</div>;
}

View File

@@ -29,7 +29,7 @@ export const FIXTURE_ENTRYPOINT = {
## Error
```
Found 1 error:
Found 2 errors:
Error: This value cannot be modified
@@ -43,6 +43,32 @@ error.hook-call-freezes-captured-memberexpr.ts:13:2
14 | return <Stringify x={x} cb={cb} />;
15 | }
16 |
Error: Cannot modify local variables after render completes
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.
error.hook-call-freezes-captured-memberexpr.ts:9:25
7 | * After this custom hook call, it's no longer valid to mutate x.
8 | */
> 9 | const cb = useIdentity(() => {
| ^^^^^^^
> 10 | x.value++;
| ^^^^^^^^^^^^^^
> 11 | });
| ^^^^ This function may (indirectly) reassign or modify `x` after render
12 |
13 | x.value += count;
14 | return <Stringify x={x} cb={cb} />;
error.hook-call-freezes-captured-memberexpr.ts:10:4
8 | */
9 | const cb = useIdentity(() => {
> 10 | x.value++;
| ^ This modifies `x`
11 | });
12 |
13 | x.value += count;
```

View File

@@ -15,7 +15,7 @@ function component(a, b) {
## Error
```
Found 1 error:
Found 3 errors:
Error: useMemo() callbacks may not be async or generator functions
@@ -32,6 +32,37 @@ error.invalid-ReactUseMemo-async-callback.ts:2:24
5 | return x;
6 | }
7 |
Error: Found missing memoization dependencies
Missing dependencies can cause a value to update less often than it should, resulting in stale UI.
error.invalid-ReactUseMemo-async-callback.ts:3:10
1 | function component(a, b) {
2 | let x = React.useMemo(async () => {
> 3 | await a;
| ^ Missing dependency `a`
4 | }, []);
5 | return x;
6 | }
Inferred dependencies: `[a]`
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `a`, but the source dependencies were []. Inferred dependency not present in source.
error.invalid-ReactUseMemo-async-callback.ts:2:24
1 | function component(a, b) {
> 2 | let x = React.useMemo(async () => {
| ^^^^^^^^^^^^^
> 3 | await a;
| ^^^^^^^^^^^^
> 4 | }, []);
| ^^^^ Could not preserve existing manual memoization
5 | return x;
6 | }
7 |
```

View File

@@ -22,7 +22,7 @@ function Component({item, cond}) {
## Error
```
Found 2 errors:
Found 3 errors:
Error: Calling setState from useMemo may trigger an infinite loop
@@ -49,6 +49,39 @@ error.invalid-conditional-setState-in-useMemo.ts:8:6
9 | }
10 | }, [cond, key, init]);
11 |
Error: Found missing/extra memoization dependencies
Missing dependencies can cause a value to update less often than it should, resulting in stale UI. Extra dependencies can cause a value to update more often than it should, resulting in performance problems such as excessive renders or effects firing too often.
error.invalid-conditional-setState-in-useMemo.ts:7:18
5 | useMemo(() => {
6 | if (cond) {
> 7 | setPrevItem(item);
| ^^^^ Missing dependency `item`
8 | setState(0);
9 | }
10 | }, [cond, key, init]);
error.invalid-conditional-setState-in-useMemo.ts:10:12
8 | setState(0);
9 | }
> 10 | }, [cond, key, init]);
| ^^^ Unnecessary dependency `key`. Values declared outside of a component/hook should not be listed as dependencies as the component will not re-render if they change
11 |
12 | return state;
13 | }
error.invalid-conditional-setState-in-useMemo.ts:10:17
8 | setState(0);
9 | }
> 10 | }, [cond, key, init]);
| ^^^^ Unnecessary dependency `init`. Values declared outside of a component/hook should not be listed as dependencies as the component will not re-render if they change
11 |
12 | return state;
13 | }
Inferred dependencies: `[cond, item]`
```

View File

@@ -16,7 +16,7 @@ function useInvalidMutation(options) {
## Error
```
Found 1 error:
Found 2 errors:
Error: This value cannot be modified
@@ -30,6 +30,27 @@ error.invalid-mutation-in-closure.ts:4:4
5 | }
6 | return test;
7 | }
Error: Cannot modify local variables after render completes
This argument is a function which may reassign or mutate `options` after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead.
error.invalid-mutation-in-closure.ts:6:9
4 | options.foo = 'bar';
5 | }
> 6 | return test;
| ^^^^ This function may (indirectly) reassign or modify `options` after render
7 | }
8 |
error.invalid-mutation-in-closure.ts:4:4
2 | function test() {
3 | foo(options.foo); // error should not point on this line
> 4 | options.foo = 'bar';
| ^^^^^^^ This modifies `options`
5 | }
6 | return test;
7 | }
```

View File

@@ -15,7 +15,7 @@ function useFoo() {
## Error
```
Found 1 error:
Found 2 errors:
Error: Cannot reassign variable after render completes
@@ -29,6 +29,31 @@ error.invalid-reassign-local-in-hook-return-value.ts:4:4
5 | };
6 | }
7 |
Error: Cannot modify local variables after render completes
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.
error.invalid-reassign-local-in-hook-return-value.ts:3:9
1 | function useFoo() {
2 | let x = 0;
> 3 | return value => {
| ^^^^^^^^^^
> 4 | x = value;
| ^^^^^^^^^^^^^^
> 5 | };
| ^^^^ This function may (indirectly) reassign or modify `x` after render
6 | }
7 |
error.invalid-reassign-local-in-hook-return-value.ts:4:4
2 | let x = 0;
3 | return value => {
> 4 | x = value;
| ^ This modifies `x`
5 | };
6 | }
7 |
```

View File

@@ -47,7 +47,7 @@ function Component() {
## Error
```
Found 1 error:
Found 2 errors:
Error: Cannot reassign variable after render completes
@@ -61,6 +61,32 @@ error.invalid-reassign-local-variable-in-effect.ts:7:4
8 | };
9 |
10 | const onMount = newValue => {
Error: Cannot modify local variables after render completes
This argument is a function which may reassign or mutate `local` after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead.
error.invalid-reassign-local-variable-in-effect.ts:33:12
31 | };
32 |
> 33 | useEffect(() => {
| ^^^^^^^
> 34 | onMount();
| ^^^^^^^^^^^^^^
> 35 | }, [onMount]);
| ^^^^ This function may (indirectly) reassign or modify `local` after render
36 |
37 | return 'ok';
38 | }
error.invalid-reassign-local-variable-in-effect.ts:7:4
5 |
6 | const reassignLocal = newValue => {
> 7 | local = newValue;
| ^^^^^ This modifies `local`
8 | };
9 |
10 | const onMount = newValue => {
```

View File

@@ -48,7 +48,7 @@ function Component() {
## Error
```
Found 1 error:
Found 2 errors:
Error: Cannot reassign variable after render completes
@@ -62,6 +62,32 @@ error.invalid-reassign-local-variable-in-hook-argument.ts:8:4
9 | };
10 |
11 | const callback = newValue => {
Error: Cannot modify local variables after render completes
This argument is a function which may reassign or mutate `local` after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead.
error.invalid-reassign-local-variable-in-hook-argument.ts:34:14
32 | };
33 |
> 34 | useIdentity(() => {
| ^^^^^^^
> 35 | callback();
| ^^^^^^^^^^^^^^^
> 36 | });
| ^^^^ This function may (indirectly) reassign or modify `local` after render
37 |
38 | return 'ok';
39 | }
error.invalid-reassign-local-variable-in-hook-argument.ts:8:4
6 |
7 | const reassignLocal = newValue => {
> 8 | local = newValue;
| ^^^^^ This modifies `local`
9 | };
10 |
11 | const callback = newValue => {
```

View File

@@ -41,7 +41,7 @@ function Component() {
## Error
```
Found 1 error:
Found 2 errors:
Error: Cannot reassign variable after render completes
@@ -55,6 +55,27 @@ error.invalid-reassign-local-variable-in-jsx-callback.ts:5:4
6 | };
7 |
8 | const onClick = newValue => {
Error: Cannot modify local variables after render completes
This argument is a function which may reassign or mutate `local` after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead.
error.invalid-reassign-local-variable-in-jsx-callback.ts:31:26
29 | };
30 |
> 31 | return <button onClick={onClick}>Submit</button>;
| ^^^^^^^ This function may (indirectly) reassign or modify `local` after render
32 | }
33 |
error.invalid-reassign-local-variable-in-jsx-callback.ts:5:4
3 |
4 | const reassignLocal = newValue => {
> 5 | local = newValue;
| ^^^^^ This modifies `local`
6 | };
7 |
8 | const onClick = newValue => {
```

View File

@@ -26,7 +26,7 @@ function useKeyedState({key, init}) {
## Error
```
Found 1 error:
Found 3 errors:
Error: Calling setState from useMemo may trigger an infinite loop
@@ -40,6 +40,61 @@ error.invalid-setState-in-useMemo-indirect-useCallback.ts:13:4
14 | }, [key, init]);
15 |
16 | return state;
Error: Found missing memoization dependencies
Missing dependencies can cause a value to update less often than it should, resulting in stale UI.
error.invalid-setState-in-useMemo-indirect-useCallback.ts:9:13
7 | const fn = useCallback(() => {
8 | setPrevKey(key);
> 9 | setState(init);
| ^^^^ Missing dependency `init`
10 | });
11 |
12 | useMemo(() => {
error.invalid-setState-in-useMemo-indirect-useCallback.ts:8:15
6 |
7 | const fn = useCallback(() => {
> 8 | setPrevKey(key);
| ^^^ Missing dependency `key`
9 | setState(init);
10 | });
11 |
Error: Found missing/extra memoization dependencies
Missing dependencies can cause a value to update less often than it should, resulting in stale UI. Extra dependencies can cause a value to update more often than it should, resulting in performance problems such as excessive renders or effects firing too often.
error.invalid-setState-in-useMemo-indirect-useCallback.ts:13:4
11 |
12 | useMemo(() => {
> 13 | fn();
| ^^ Missing dependency `fn`
14 | }, [key, init]);
15 |
16 | return state;
error.invalid-setState-in-useMemo-indirect-useCallback.ts:14:6
12 | useMemo(() => {
13 | fn();
> 14 | }, [key, init]);
| ^^^ Unnecessary dependency `key`
15 |
16 | return state;
17 | }
error.invalid-setState-in-useMemo-indirect-useCallback.ts:14:11
12 | useMemo(() => {
13 | fn();
> 14 | }, [key, init]);
| ^^^^ Unnecessary dependency `init`
15 |
16 | return state;
17 | }
Inferred dependencies: `[fn]`
```

View File

@@ -1,44 +0,0 @@
## Input
```javascript
// @validateMemoizedEffectDependencies
function Component(props) {
// Items cannot be memoized bc its mutation spans a hook call
const items = [props.value];
const [state, _setState] = useState(null);
mutate(items);
// Items is no longer mutable here, but it hasn't been memoized
useEffect(() => {
console.log(items);
}, [items]);
return [items, state];
}
```
## Error
```
Found 1 error:
Compilation Skipped: React Compiler has skipped optimizing this component because the effect dependencies could not be memoized. Unmemoized effect dependencies can trigger an infinite loop or other unexpected behavior
error.invalid-useEffect-dep-not-memoized-bc-range-overlaps-hook.ts:9:2
7 |
8 | // Items is no longer mutable here, but it hasn't been memoized
> 9 | useEffect(() => {
| ^^^^^^^^^^^^^^^^^
> 10 | console.log(items);
| ^^^^^^^^^^^^^^^^^^^^^^^
> 11 | }, [items]);
| ^^^^^^^^^^^^^^ React Compiler has skipped optimizing this component because the effect dependencies could not be memoized. Unmemoized effect dependencies can trigger an infinite loop or other unexpected behavior
12 |
13 | return [items, state];
14 | }
```

View File

@@ -1,14 +0,0 @@
// @validateMemoizedEffectDependencies
function Component(props) {
// Items cannot be memoized bc its mutation spans a hook call
const items = [props.value];
const [state, _setState] = useState(null);
mutate(items);
// Items is no longer mutable here, but it hasn't been memoized
useEffect(() => {
console.log(items);
}, [items]);
return [items, state];
}

View File

@@ -1,41 +0,0 @@
## Input
```javascript
// @validateMemoizedEffectDependencies
import {useEffect} from 'react';
function Component(props) {
const data = {};
useEffect(() => {
console.log(props.value);
}, [data]);
mutate(data);
return data;
}
```
## Error
```
Found 1 error:
Compilation Skipped: React Compiler has skipped optimizing this component because the effect dependencies could not be memoized. Unmemoized effect dependencies can trigger an infinite loop or other unexpected behavior
error.invalid-useEffect-dep-not-memoized.ts:6:2
4 | function Component(props) {
5 | const data = {};
> 6 | useEffect(() => {
| ^^^^^^^^^^^^^^^^^
> 7 | console.log(props.value);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 8 | }, [data]);
| ^^^^^^^^^^^^^ React Compiler has skipped optimizing this component because the effect dependencies could not be memoized. Unmemoized effect dependencies can trigger an infinite loop or other unexpected behavior
9 | mutate(data);
10 | return data;
11 | }
```

View File

@@ -1,11 +0,0 @@
// @validateMemoizedEffectDependencies
import {useEffect} from 'react';
function Component(props) {
const data = {};
useEffect(() => {
console.log(props.value);
}, [data]);
mutate(data);
return data;
}

View File

@@ -1,41 +0,0 @@
## Input
```javascript
// @validateMemoizedEffectDependencies
import {useInsertionEffect} from 'react';
function Component(props) {
const data = {};
useInsertionEffect(() => {
console.log(props.value);
}, [data]);
mutate(data);
return data;
}
```
## Error
```
Found 1 error:
Compilation Skipped: React Compiler has skipped optimizing this component because the effect dependencies could not be memoized. Unmemoized effect dependencies can trigger an infinite loop or other unexpected behavior
error.invalid-useInsertionEffect-dep-not-memoized.ts:6:2
4 | function Component(props) {
5 | const data = {};
> 6 | useInsertionEffect(() => {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
> 7 | console.log(props.value);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 8 | }, [data]);
| ^^^^^^^^^^^^^ React Compiler has skipped optimizing this component because the effect dependencies could not be memoized. Unmemoized effect dependencies can trigger an infinite loop or other unexpected behavior
9 | mutate(data);
10 | return data;
11 | }
```

View File

@@ -1,11 +0,0 @@
// @validateMemoizedEffectDependencies
import {useInsertionEffect} from 'react';
function Component(props) {
const data = {};
useInsertionEffect(() => {
console.log(props.value);
}, [data]);
mutate(data);
return data;
}

View File

@@ -1,41 +0,0 @@
## Input
```javascript
// @validateMemoizedEffectDependencies
import {useLayoutEffect} from 'react';
function Component(props) {
const data = {};
useLayoutEffect(() => {
console.log(props.value);
}, [data]);
mutate(data);
return data;
}
```
## Error
```
Found 1 error:
Compilation Skipped: React Compiler has skipped optimizing this component because the effect dependencies could not be memoized. Unmemoized effect dependencies can trigger an infinite loop or other unexpected behavior
error.invalid-useLayoutEffect-dep-not-memoized.ts:6:2
4 | function Component(props) {
5 | const data = {};
> 6 | useLayoutEffect(() => {
| ^^^^^^^^^^^^^^^^^^^^^^^
> 7 | console.log(props.value);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 8 | }, [data]);
| ^^^^^^^^^^^^^ React Compiler has skipped optimizing this component because the effect dependencies could not be memoized. Unmemoized effect dependencies can trigger an infinite loop or other unexpected behavior
9 | mutate(data);
10 | return data;
11 | }
```

View File

@@ -1,11 +0,0 @@
// @validateMemoizedEffectDependencies
import {useLayoutEffect} from 'react';
function Component(props) {
const data = {};
useLayoutEffect(() => {
console.log(props.value);
}, [data]);
mutate(data);
return data;
}

View File

@@ -15,7 +15,7 @@ function component(a, b) {
## Error
```
Found 1 error:
Found 3 errors:
Error: useMemo() callbacks may not be async or generator functions
@@ -32,6 +32,37 @@ error.invalid-useMemo-async-callback.ts:2:18
5 | return x;
6 | }
7 |
Error: Found missing memoization dependencies
Missing dependencies can cause a value to update less often than it should, resulting in stale UI.
error.invalid-useMemo-async-callback.ts:3:10
1 | function component(a, b) {
2 | let x = useMemo(async () => {
> 3 | await a;
| ^ Missing dependency `a`
4 | }, []);
5 | return x;
6 | }
Inferred dependencies: `[a]`
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `a`, but the source dependencies were []. Inferred dependency not present in source.
error.invalid-useMemo-async-callback.ts:2:18
1 | function component(a, b) {
> 2 | let x = useMemo(async () => {
| ^^^^^^^^^^^^^
> 3 | await a;
| ^^^^^^^^^^^^
> 4 | }, []);
| ^^^^ Could not preserve existing manual memoization
5 | return x;
6 | }
7 |
```

View File

@@ -13,7 +13,7 @@ function component(a, b) {
## Error
```
Found 1 error:
Found 3 errors:
Error: useMemo() callbacks may not accept parameters
@@ -26,6 +26,32 @@ error.invalid-useMemo-callback-args.ts:2:18
3 | return x;
4 | }
5 |
Error: Found missing memoization dependencies
Missing dependencies can cause a value to update less often than it should, resulting in stale UI.
error.invalid-useMemo-callback-args.ts:2:23
1 | function component(a, b) {
> 2 | let x = useMemo(c => a, []);
| ^ Missing dependency `a`
3 | return x;
4 | }
5 |
Inferred dependencies: `[a]`
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `a`, but the source dependencies were []. Inferred dependency not present in source.
error.invalid-useMemo-callback-args.ts:2:18
1 | function component(a, b) {
> 2 | let x = useMemo(c => a, []);
| ^^^^^^ Could not preserve existing manual memoization
3 | return x;
4 | }
5 |
```

View File

@@ -32,7 +32,7 @@ export const FIXTURE_ENTRYPOINT = {
## Error
```
Found 1 error:
Found 2 errors:
Error: Cannot reassign variable after render completes
@@ -46,6 +46,28 @@ error.mutable-range-shared-inner-outer-function.ts:8:6
9 | b = [];
10 | } else {
11 | a = {};
Error: Cannot modify local variables after render completes
This argument is a function which may reassign or mutate `a` after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead.
error.mutable-range-shared-inner-outer-function.ts:17:23
15 | b.push(false);
16 | };
> 17 | return <div onClick={f} />;
| ^ This function may (indirectly) reassign or modify `a` after render
18 | }
19 |
20 | export const FIXTURE_ENTRYPOINT = {
error.mutable-range-shared-inner-outer-function.ts:8:6
6 | const f = () => {
7 | if (cond) {
> 8 | a = {};
| ^ This modifies `a`
9 | b = [];
10 | } else {
11 | a = {};
```

Some files were not shown because too many files have changed in this diff Show More