Compare commits

..

44 Commits

Author SHA1 Message Date
Mofei Zhang
489fee593d [compiler][be] repro edge cases for noEmit and module opt-outs
see test fixtures
2025-05-07 17:09:04 -04:00
mofeiZ
c129c2424b [compiler][repro] Nested fbt test fixture (#32779)
Ideally we should detect and bail out on this case to avoid babel build
failures.
2025-05-05 11:52:45 -04:00
mofeiZ
0c1575cee8 [compiler][bugfix] Bail out when a memo block declares hoisted fns (#32765)
Note that bailing out adds false positives for hoisted functions whose
only references are within other functions. For example, this rewrite
would be safe.
```js
// source program
  function foo() {
    return bar();
  }
  function bar() {
    return 42;
  }

// compiler output
let bar;
if (/* deps changed */) {
  function foo() {
    return bar();
  }
  bar = function bar() {
    return 42;
  }
}
```
These false positives are difficult to detect because any maybe-call of
foo before the definition of bar would be invalid.

Instead of bailing out, we should rewrite hoisted function declarations
to the following form.
```js
let bar$0;
if (/* deps changed */) {
  // All references within the declaring memo block
  // or before the function declaration should use
  // the original identifier `bar`
  function foo() {
    return bar();
  }
  function bar() {
    return 42;
  }
  bar$0 = bar;
}
// All references after the declaring memo block
// or after the function declaration should use
// the rewritten declaration `bar$0`
```
2025-05-05 11:45:58 -04:00
Sebastian Markbåge
52ea641449 [Flight] Don't increase serializedSize for every recursive pass (#33123)
I noticed that we increase this in the recursive part of the algorithm.
This would mean that we'd count a key more than once if it has Server
Components inside it recursively resolving. This moves it out to where
we enter from toJSON. Which is called once per JSON entry (and therefore
once per key).
2025-05-05 11:37:39 -04:00
Stephen Zhou
3ec88e797f [eslint-plugin-react-hooks] update doc url for rules of hooks (#33118) 2025-05-05 17:37:06 +02:00
Sebastian "Sebbie" Silbermann
0ca8420f9d [Flight] Use valid CSS selectors in useId format (#33099) 2025-05-04 13:47:32 +02:00
Joe Savona
0db8db178c [compiler] Validate against mutable functions being frozen
This revisits a validation I built a while ago, trying to make it more strict this time to ensure that it's high-signal.

We detect function expressions which are *known* mutable — they definitely can modify a variable defined outside of the function expression itself (modulo control flow). This uses types to look for known Store and Mutate effects only, and disregards mutations of effects. Any such function passed to a location with a Freeze effect is reported as a validation error.

This is behind a flag and disabled by default. If folks agree this makes sense to revisit, i'll test out internally and we can consider enabling by default.

ghstack-source-id: 075a731444ce95e52dbd5ea3be85c16d428927f5
Pull Request resolved: https://github.com/facebook/react/pull/33079
2025-05-03 09:15:32 +09:00
Joe Savona
8570116bd1 [compiler] Fix for uncalled functions that are known-mutable
If a function captures a mutable value but never gets called, we don't infer a mutable range for that function. This means that we also don't alias the function with its mutable captures.

This case is tricky, because we don't generally know for sure what is a mutation and what may just be a normal function call. For example:

```js
hook useFoo() {
  const x = makeObject();
  return () => {
    return readObject(x); // could be a mutation!
  }
}
```

If we pessimistically assume that all such cases are mutations, we'd have to group lots of memo scopes together unnecessarily. However, if there is definitely a mutation:

```js
hook useFoo(createEntryForKey) {
  const cache = new WeakMap();
  return (key) => {
    let entry = cache.get(key);
    if (entry == null) {
      entry = createEntryForKey(key);
      cache.set(key, entry); // known mutation!
    }
    return entry;
  }
}
```

Then we have to ensure that the function and its mutable captures alias together and end up in the same scope. However, aliasing together isn't enough if the function and operands all have empty mutable ranges (end = start + 1).

This pass finds function expressions and object methods that have an empty mutable range and known-mutable operands which also don't have a mutable range, and ensures that the function and those operands are aliased together *and* that their ranges are updated to end after the function expression. This is sufficient to ensure that a reactive scope is created for the alias set.

NOTE: The alternative is to reject these cases. If we do that we'd also want to similarly disallow cases like passing a mutable function to a hook.

ghstack-source-id: 5d8158246a320e80d8da3f0e395ac1953d8920a2
Pull Request resolved: https://github.com/facebook/react/pull/33078
2025-05-03 09:15:32 +09:00
Joe Savona
4f1d2ddf95 [compiler] Add types for WeakMap, WeakSet, and reanimated shared values
Building on mofeiz's recent work to type constructors. Also, types for reanimated values which are useful in the next PR.

ghstack-source-id: 1c81e213a11337ac7e9c85a429ecf3f1d1adef66
Pull Request resolved: https://github.com/facebook/react/pull/33077
2025-05-03 09:15:32 +09:00
Joe Savona
73d7e816b7 [compiler] ValidatePreservedManualMemoization reports detailed errors
This pass didn't previously report the precise difference btw inferred/manual dependencies unless a debug flag was set. But the error message is really good (nice job mofeiz): the only catch is that in theory the inferred dep could be a temporary that can't trivially be reported to the user.

But the messages are really useful for quickly verifying why the compiler couldn't preserve memoization. So here we switch to outputting a detailed message about the discrepancy btw inferred/manual deps so long as the inferred dep root is a named variable. I also slightly adjusted the message to handle the case where there is no diagnostic, which can occur if there were no manual deps but the compiler inferred a dependency.

ghstack-source-id: 534f6f1fec0855e05e85077eba050eb2ba254ef8
Pull Request resolved: https://github.com/facebook/react/pull/33095
2025-05-03 09:09:34 +09:00
Joe Savona
ac2cae5245 [compiler] Fix for string attribute values with emoji
If a JSX attribute value is a string that contains unicode or other characters that need special escaping, we wrap the attribute value in an expression container. However, our unicode to detect this only handled the basic unicode character plane, not the "astral" plane which includes emojis.

This PR updates the regex to detect such extended characters and also use an expression container.

ghstack-source-id: 6d9c8e4dd22285077108e2fa53d66154d1b781fb
Pull Request resolved: https://github.com/facebook/react/pull/33096
2025-05-03 09:07:50 +09:00
Jan Kassens
66de8e5a99 [eslint-plugin-react-hooks] move eslint plugin once again (#33110) 2025-05-02 18:14:56 -04:00
lauren
26ecc98a00 [mcp] Prompt improvements for smarter tool composition (#33108)
Our runtime results are still bogus, but so far this tweak let's the llm
use the tools a little more intelligently.

example:
---

```js
import React from 'react';

function App() {
  const [count, setCount] = React.useState(0);
  const [data, setData] = React.useState([]);

  React.useEffect(() => {
    const processData = () => {
      console.log("Processing data...");
      const newData = [];
      for (let i = 0; i < 1000000000; i++) {
        newData.push({ id: i, value: Math.random() * count });
      }
      return newData;
    };

    const processedData = processData();
    setData(processedData);
  }, [count]);

  return (
    <div>
      <p>Count: {count}</p>
      <p>Data Length: {data.length}</p>
      <ul>
        {data.slice(0, 5).map((item) => (
          <li key={item.id}>Item {item.id}: {item.value.toFixed(2)}</li>
        ))}
      </ul>
    </div>
  );
};
```
why is this component slow? use the react tools to figure this out.

> I'll analyze why this component is slow using the React tools. Let's
first evaluate the performance of the original code.
>
[review-react-runtime ...]
>
> The performance measurement tool timed out, which is a strong
indication of just how severe the performance issue is in this
component. Let's now try to compile the code to see if React Compiler
can identify any issues:
>
[compile...]
>
> Now I understand the issue. Let's analyze why this component > is
extremely slow:
> Main Performance Issues:
>
> Massive Data Processing: The component is trying to create an array
with a billion (1,000,000,000) objects in the processData function. This
is an enormous amount of data that will:
>
> Consume massive amounts of memory
> Take an extremely long time to process
> Likely crash the browser
>
>
> Unnecessary Recalculation: The effect depends on [count], meaning
every time the count changes, it will reprocess this enormous dataset.
> Inefficient Data Handling: Even though only 5 items are displayed, the
component processes a billion items.
>
> Let's fix these issues:
>
[compile...]
>
> Now let's check if our modified code performs better:
>
[review-react-runtime...]
>
> Excellent! The performance has dramatically improved. Now let's check
the React documentation to make sure we're following the best practices:
>
[query-react-dev-docs...]

(hit the maximum length conversation limit because i'm on a free plan)
2025-05-02 17:50:39 -04:00
Jan Kassens
f0ca53d133 [eslint-plugin-react-hooks] another CI update... (#33107)
We currently created a nested directory, this should remove that.

See:

https://github.com/facebook/react/tree/builds/facebook-fbsource/compiled-rn/facebook-fbsource/xplat/js/RKJSModules/vendor/react/eslint-plugin-react-hooks/eslint-plugin-react-hooks
2025-05-02 17:05:56 -04:00
lauren
e39b380a21 [mcp] Fix unresolved imports (#33105)
We need to explicitly import the modules so they'll be inlined correctly
into the bundle.
2025-05-02 16:54:17 -04:00
Jan Kassens
9de0304ad7 Add missing copyright header (#33106)
This made the build fail since there was no file header comment.
2025-05-02 16:52:17 -04:00
Jan Kassens
0d695bea10 [eslint-plugin-react-hooks] update fbsource build (#33104)
In order to sync the lint rules directly to internal, include the eslint
plugin in the build output for fbsource.
2025-05-02 16:03:06 -04:00
Jan Kassens
4c4a57c4f9 [eslint-plugin-react-hooks] updates for component syntax (#33089)
Adds support for Flow's component and hook syntax.
[docs](https://flow.org/en/docs/react/component-syntax/)
2025-05-02 15:04:45 -04:00
lauren
dc2b11817b [mcp] Refactor (#33085)
Just some cleanup. Mainly, we now take the number of iterations as an
argument. Everything else is just code movement and small tweaks.
2025-05-02 14:15:12 -04:00
lauren
b5450b0738 [mcp] Update prompts (#33084)
Some tweaks to the prompt to provide more context on how to use them.
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33084).
* #33085
* __->__ #33084
* #33083
2025-05-02 14:06:20 -04:00
lauren
f150c046ec [mcp] Move to /tools (#33083)
Moves to a tools directory.
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33083).
* #33085
* #33084
* __->__ #33083
2025-05-02 14:06:11 -04:00
lauren
12b094d2f6 [mcp] Update plugins (#33082)
Adds typescript support.
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33082).
* #33085
* #33084
* #33083
* __->__ #33082
* #33101
2025-05-02 13:56:45 -04:00
lauren
e5f0315efa [mcp] Fix package.json (#33101)
Since we use esbuild we need to correctly move dependencies that are
required at runtime into `dependencies` and other packages that are only
used in development in to `devDependencies`. This ensures the correct
packages are included in the build.
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33101).
* #33085
* #33084
* #33083
* #33082
* __->__ #33101
2025-05-02 13:56:01 -04:00
Sebastian Markbåge
f739642745 [Fizz] Always load the external runtime if one is provided (#33091)
Because we now decided whether to outline in the flushing phase, when
we're writing the preamble we don't yet know if we will make that
decision so we don't know if it's safe to omit the external runtime.

However, if you are providing an external runtime it's probably a pretty
safe bet you're streaming something dynamically that's likely to need it
so we can always include it.

The main thing is that this makes it hard to test it because it affects
our tests in ways it wouldn't otherwise so we have to add a bunch of
conditions.
2025-05-01 18:14:42 -04:00
Sebastian Markbåge
0ed6ceb9f6 [Fizz] Add "Queued" Status to SSR:ed Suspense Boundaries (#33087)
Stacked on #33076.

This fixes a bug where we used the "complete" status but the
DOMContentLoaded event. This checks for not "loading" instead.

We also add a new status where the boundary has been marked as complete
by the server but has not yet flushed either due to being throttled,
suspended on CSS or animating.
2025-05-01 16:11:54 -04:00
Sebastian Markbåge
ee7fee8f88 [Fizz] Batch Suspense Boundary Reveal with Throttle (#33076)
Stacked on #33073.

React semantics is that Suspense boundaries reveal with a throttle
(300ms). That helps avoid flashing reveals when a stream reveals many
individual steps back to back. It can also improve overall performance
by batching the layout and paint work that has to happen at each step.

Unfortunately we never implemented this for SSR streaming - only for
client navigations. This is highly noticeable on very dynamic sites with
lots of Suspense boundaries. It can look good with a client nav but feel
glitchy when you reload the page or initial load.

This fixes the Fizz runtime to be throttled and reveals batched into a
single paint at a time. We do this by first tracking the last paint
after the complete (this will be the first paint if `rel="expect"` is
respected). Then in the `completeBoundary` operation we queue the
operation and then flush it all into a throttled batch.

Another motivation is that View Transitions need to operate as a batch
and individual steps get queued in a sequence so it's extra important to
include as much content as possible in each animated step. This will be
done in a follow up for SSR View Transitions.
2025-05-01 16:09:37 -04:00
Sebastian Markbåge
ee077b6ccd [Fizz] Don't handle errors in completeBoundary instruction (#33073)
Stacked on #33066 and #33068.

Currently we're passing `errorDigest` to `completeBoundary` if there is
a client side error (only CSS loading atm). This only exists because of
`completeBoundaryWithStyles`. Normally if there's a server-side error
we'd emit the `clientRenderBoundary` instruction instead. This adds
unnecessary code to the common case where all styles are in the head.
This is about to get worse with batching because client render shouldn't
be throttled but complete should be.

The first commit moves the client render logic inline into
`completeBoundaryWithStyles` so we only pay for it when styles are used.

However, the approach I went with in the second commit is to reuse the
`$RX` instruction instead (`clientRenderBoundary`). That way if you have
both it ends up being amortized. However, it does mean we have to emit
the `$RX` (along with the `$RC` helper if any
`completeBoundaryWithStyles` instruction is needed.
2025-05-01 15:44:17 -04:00
Sebastian Markbåge
bb57fa7351 [Fizz] Share code between inline and external runtime (#33066)
Stacked on #33065.

The runtime is about to be a lot more complicated so we need to start
sharing some more code.

The problem with sharing code is that we want the inline runtime to as
much as possible be isolated in its scope using only a few global
variables to refer across runtimes.

A problem with Closure Compiler is that it refuses to inline functions
if they have closures inside of them. Which makes sense because of how
VMs work it can cause memory leaks. However, in our cases this doesn't
matter and code size matters more. So we can't use many clever tricks.

So this just favors writing the source in the inline form. Then we add
an extra compiler pass to turn those global variables into local
variables in the external runtime.
2025-05-01 14:25:10 -04:00
Joe Savona
e9db3cc2d4 [compiler] PruneNonEscapingScopes understands terminal operands
We weren't treating terminal operands as eligible for memoization in PruneNonEscapingScopes, which meant that they could end up un-memoized. Terminal operands can also be compound ReactiveValues like SequenceExpressions, so part of the fix is to make sure we don't just recurse into compound values but record the full aliasing information we would for top-level instructions.

Still WIP, this needs to handle terminals other than for..of.

ghstack-source-id: 09a29230514e3bc95d1833cd4392de238fabbeda
Pull Request resolved: https://github.com/facebook/react/pull/33062
2025-05-01 12:41:27 +09:00
Jorge Cabiedes
d8074cbc79 [mcp] Make tool more reliable and fix integration issues with babel (#33074)
## Summary

Fix babel presets, and add a bit more context to the tool so that it is
more reliable

## How did you test this change?

Manually tested the mcp integrated with claude desktop
2025-04-30 15:42:00 -07:00
Sebastian Markbåge
71797c871b [Fizz] Ignore error if content node is gone (#33068)
We normally expect the segment to exist whatever the client does while
streaming. However, when hydration errors at the root of the shell for a
whole document render, then we clear nodes from body which can include
our segments. We don't need them anymore because we switched to client
rendering.

It triggers an error accessing parent node which can safely be ignored.
This just helps avoid confusion in this scenario.

This also covers up the error in #33067. Which doesn't actually cause
any visible problems other than error logging. However, ideally we
wouldn't emit completeBoundary instructions if the boundary is inside a
cancelled fallback.
2025-04-30 17:51:39 -04:00
mofeiZ
9d795d3808 [compiler][bugfix] expand StoreContext to const / let / function variants (#32747)
```js
function Component() {
  useEffect(() => {
    let hasCleanedUp = false;
    document.addEventListener(..., () => hasCleanedUp ? foo() : bar());
    // effect return values shouldn't be typed as frozen
    return () => {
      hasCleanedUp = true;
    }
  };
}
```
### Problem
`PruneHoistedContexts` currently strips hoisted declarations and
rewrites the first `StoreContext` reassignment to a declaration. For
example, in the following example, instruction 0 is removed while a
synthetic `DeclareContext let` is inserted before instruction 1.

```js
// source
const cb = () => x; // reference that causes x to be hoisted

let x = 4;
x = 5;

// React Compiler IR
[0] DeclareContext HoistedLet 'x'
...
[1] StoreContext reassign 'x' = 4
[2] StoreContext reassign 'x' = 5
```

Currently, we don't account for `DeclareContext let`. As a result, we're
rewriting to insert duplicate declarations.
```js
// source
const cb = () => x; // reference that causes x to be hoisted

let x;
x = 5;

// React Compiler IR
[0] DeclareContext HoistedLet 'x'
...
[1] DeclareContext Let 'x'
[2] StoreContext reassign 'x' = 5
```

### Solution

Instead of always lowering context variables to a DeclareContext
followed by a StoreContext reassign, we can keep `kind: 'Const' | 'Let'
| 'Reassign' | etc` on StoreContext.
Pros:
- retain more information in HIR, so we can codegen easily `const` and
`let` context variable declarations back
- pruning hoisted `DeclareContext` instructions is simple.

Cons:
- passes are more verbose as we need to check for both `DeclareContext`
and `StoreContext` declarations

~(note: also see alternative implementation in
https://github.com/facebook/react/pull/32745)~

### Testing
Context variables are tricky. I synced and diffed changes in a large
meta codebase and feel pretty confident about landing this. About 0.01%
of compiled files changed. Among these changes, ~25% were [direct
bugfixes](https://www.internalfb.com/phabricator/paste/view/P1800029094).
The [other
changes](https://www.internalfb.com/phabricator/paste/view/P1800028575)
were primarily due to changed (corrected) mutable ranges from
https://github.com/facebook/react/pull/33047. I tried to represent most
interesting changes in new test fixtures

`
2025-04-30 17:18:58 -04:00
mofeiZ
12f4cb85c5 [compiler][bugfix] Returned functions are not always frozen (#33047)
Fixes an edge case in React Compiler's effects inference model.

Returned values should only be typed as 'frozen' if they are (1) local
and (2) not a function expression which may capture and mutate this
function's outer context. See test fixtures for details
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33047).
* #32765
* #32747
* __->__ #33047
2025-04-30 15:50:54 -04:00
Jorge Cabiedes
90a124a980 [mdn] Initial experiment for adding performance tool (#33045)
## Summary
Add a way for the agent to get some data on the performance of react
code

## How did you test this change?
Tested function independently and directly with claude desktop app

---------

Co-authored-by: Sebastian "Sebbie" Silbermann <sebastian.silbermann@vercel.com>
2025-04-30 12:44:05 -07:00
Sebastian Markbåge
49ea8bf569 [Flight] Defer Elements if the parent chunk is too large (#33030)
Same principle as #33029 but for Flight.

We pretty aggressively create separate rows for things in Flight (every
Server Component that's an async function create a microtask). However,
sync Server Components and just plain Host Components are not. Plus we
should ideally ideally inline more of the async ones in the same way
Fizz does.

This means that we can create rows that end up very large. Especially if
all the data is already available. We can't show the parent content
until the whole thing loads on the client.

We don't really know where Suspense boundaries are for Flight but any
Element is potentially a point that can be split.

This heuristic counts roughly how much we've serialized to block the
current chunk and once a limit is exceeded, we start deferring all
Elements. That way they get outlined into future chunks that are later
in the stream. Since they get replaced by Lazy references the parent can
potentially get unblocked.

This can help if you're trying to stream a very large document with a
client nav for example.
2025-04-30 14:21:28 -04:00
Sebastian Markbåge
9a52ad9fd9 [Fizz] Remove globals from external runtime (#33065)
We never emit any inline functions when we use external runtime so this
global shouldn't be needed.
2025-04-30 14:21:14 -04:00
Sebastian "Sebbie" Silbermann
fa8e3a251e [devtools] Restore all Transitions for Tree updates (#33042) 2025-04-30 19:51:40 +02:00
Jack Pope
408d055a3b Add Fragment Refs to Fabric with intersection observer support (#33056)
Adds Fragment Ref support to RN through the Fabric config, starting with
`observeUsing`/`unobserveUsing`. This is mostly a copy from the
implementation on DOM, and some of it can likely be shared in the future
but keeping it separate for now and we can refactor as we add more
features.

Added a basic test with Fabric, but testing specific methods requires so
much mocking that it doesn't seem valuable here.

I built Fabric and ran on the Catalyst app internally to test with
intersection observers end to end.
2025-04-30 10:47:18 -04:00
Sebastian "Sebbie" Silbermann
fbf29ccaa3 [devtools] Restore "double-click to view owners tree" functionality (#33039) 2025-04-30 11:11:33 +02:00
Sebastian Markbåge
62960c67c8 Run Component Track Logs in the console.createTask() of the Fiber (#32809)
Stacked on #32736.

That way you can find the owner stack of each component that rerendered
for context.

In addition to the JSX callsite tasks that we already track, I also
added tracking of the first `setState` call before rendering.

We then run the "Update" entries in that task. That way you can find the
callsite of the first setState and therefore the "cause" of a render
starting by selecting the "Update" track.

Unfortunately this is blocked on bugs in Chrome that makes it so that
these stacks are not reliable in the Performance tab. It basically just
doesn't work.
2025-04-29 22:17:17 -04:00
Sebastian Markbåge
cd4e4d7599 Use console.timeStamp instead of performance.measure in Component Performance Track (#32736)
This is a new extension that Chrome added to the existing
`console.timeStamp` similar to the extensions added to
`performance.measure`. This one should be significantly faster because
it doesn't have the extra object indirection, it doesn't return a
`PerformanceMeasure` entry and doesn't register itself with the global
system of entries.

I also use `performance.measure` in DEV for errors since we can attach
the error to the `properties` extension which doesn't exist for
`console.timeStamp`.

A downside of using this API is that there's no programmatic API for the
site itself to collect its own logs from React. Which the previous
allowed us to use the standard `performance.getEntries()` for. The
recommendation instead will be for the site to patch `console.timeStamp`
if it wants to collect measurements from React just like you're
recommended to patch `console.error` or `fetch` or whatever to collect
other instrumentation metrics.

This extension works in Chrome canary but it doesn't yet work fully in
Chrome stable. We might want to wait until it has propagated to Chrome
to stable. It should be in Chrome 136.
2025-04-29 21:40:10 -04:00
Sebastian Markbåge
18212ca960 [Fizz] Outline if a boundary would add too many bytes to the next completion (#33029)
Follow up to #33027.

This enhances the heuristic so that we accumulate the size of the
currently written boundaries. Starting from the size of the root (minus
preamble) for the shell.

This ensures that if you have many small boundaries they don't all
continue to get inlined. For example, you can wrap each paragraph in a
document in a Suspense boundary to regain document streaming
capabilities if that's what you want.

However, one consideration is if it's worth producing a fallback at all.
Maybe if it's like `null` it's free but if it's like a whole alternative
page, then it's not. It's possible to have completely useless Suspense
boundaries such as when you nest several directly inside each other. So
this uses a limit of at least 500 bytes of the content itself for it to
be worth outlining at all. It also can't be too small because then for
example a long list of paragraphs can never be outlined.

In the fixture I straddle this limit so some paragraphs are too small to
be considered. An unfortunate effect of that is that you can end up with
some of them not being outlined which means that they appear out of
order. SuspenseList is supposed to address that but it's unfortunate.

The limit is still fairly high though so it's unlikely that by default
you'd start outlining anything within the viewport at all. I had to
reduce the `progressiveChunkSize` by an order of magnitude in my fixture
to try it out properly.
2025-04-29 19:13:28 -04:00
Sebastian Markbåge
88b9767404 Hack to recover from reading the wrong Fiber (#33055)
`requestFormReset` incorrectly tries to get the current dispatch queue
from the Fiber. However, the Fiber might be the workInProgress which is
an inconsistent state.

This hack just tries the other Fiber if it detects one of the known
inconsistent states but there can be more.

Really we should stash the dispatch queue somewhere stateful which is
effectively what `setState` does by binding it to the closure.
2025-04-29 13:36:19 -04:00
Pieter De Baets
0038c501a3 [react-native] Pull up enableFastAddPropertiesInDiffing check (#33043)
## Summary

We don't need the isArray check for this experiment, as
`fastAddProperties` already does the same. Also renaming
slowAddProperties to make it clearer we can fully remove this codepath
once fastAddProperties is fully rolled out.

## How did you test this change?

```
yarn test packages/react-native-renderer -r=xplat --variant=true
```
2025-04-29 11:10:18 +01:00
166 changed files with 11821 additions and 4400 deletions

View File

@@ -132,9 +132,9 @@ jobs:
mkdir ./compiled/facebook-www/__test_utils__
mv build/__test_utils__/ReactAllWarnings.js ./compiled/facebook-www/__test_utils__/ReactAllWarnings.js
# Move eslint-plugin-react-hooks into eslint-plugin-react-hooks
# Copy eslint-plugin-react-hooks
mkdir ./compiled/eslint-plugin-react-hooks
mv build/oss-experimental/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js \
cp build/oss-experimental/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js \
./compiled/eslint-plugin-react-hooks/index.js
# Move unstable_server-external-runtime.js into facebook-www
@@ -167,6 +167,12 @@ jobs:
rm $RENDERER_FOLDER/ReactFabric-{dev,prod,profiling}.js
rm $RENDERER_FOLDER/ReactNativeRenderer-{dev,prod,profiling}.js
# Copy eslint-plugin-react-hooks
# NOTE: This is different from www, here we include the full package
# including package.json to include dependencies in fbsource.
mkdir "$BASE_FOLDER/tools"
cp -r build/oss-experimental/eslint-plugin-react-hooks "$BASE_FOLDER/tools"
# Move React Native version file
mv build/facebook-react-native/VERSION_NATIVE_FB ./compiled-rn/VERSION_NATIVE_FB

View File

@@ -103,6 +103,7 @@ import {transformFire} from '../Transform';
import {validateNoImpureFunctionsInRender} from '../Validation/ValidateNoImpureFunctionsInRender';
import {CompilerError} from '..';
import {validateStaticComponents} from '../Validation/ValidateStaticComponents';
import {validateNoFreezingKnownMutableFunctions} from '../Validation/ValidateNoFreezingKnownMutableFunctions';
export type CompilerPipelineValue =
| {kind: 'ast'; name: string; value: CodegenFunction}
@@ -274,6 +275,10 @@ function runWithEnvironment(
if (env.config.validateNoImpureFunctionsInRender) {
validateNoImpureFunctionsInRender(hir).unwrap();
}
if (env.config.validateNoFreezingKnownMutableFunctions) {
validateNoFreezingKnownMutableFunctions(hir).unwrap();
}
}
inferReactivePlaces(hir);

View File

@@ -3609,31 +3609,40 @@ function lowerAssignment(
let temporary;
if (builder.isContextIdentifier(lvalue)) {
if (kind !== InstructionKind.Reassign && !isHoistedIdentifier) {
if (kind === InstructionKind.Const) {
builder.errors.push({
reason: `Expected \`const\` declaration not to be reassigned`,
severity: ErrorSeverity.InvalidJS,
loc: lvalue.node.loc ?? null,
suggestions: null,
});
}
lowerValueToTemporary(builder, {
kind: 'DeclareContext',
lvalue: {
kind: InstructionKind.Let,
place: {...place},
},
loc: place.loc,
if (kind === InstructionKind.Const && !isHoistedIdentifier) {
builder.errors.push({
reason: `Expected \`const\` declaration not to be reassigned`,
severity: ErrorSeverity.InvalidJS,
loc: lvalue.node.loc ?? null,
suggestions: null,
});
}
temporary = lowerValueToTemporary(builder, {
kind: 'StoreContext',
lvalue: {place: {...place}, kind: InstructionKind.Reassign},
value,
loc,
});
if (
kind !== InstructionKind.Const &&
kind !== InstructionKind.Reassign &&
kind !== InstructionKind.Let &&
kind !== InstructionKind.Function
) {
builder.errors.push({
reason: `Unexpected context variable kind`,
severity: ErrorSeverity.InvalidJS,
loc: lvalue.node.loc ?? null,
suggestions: null,
});
temporary = lowerValueToTemporary(builder, {
kind: 'UnsupportedNode',
node: lvalueNode,
loc: lvalueNode.loc ?? GeneratedSource,
});
} else {
temporary = lowerValueToTemporary(builder, {
kind: 'StoreContext',
lvalue: {place: {...place}, kind},
value,
loc,
});
}
} else {
const typeAnnotation = lvalue.get('typeAnnotation');
let type: t.FlowType | t.TSType | null;

View File

@@ -367,6 +367,11 @@ const EnvironmentConfigSchema = z.object({
*/
validateNoImpureFunctionsInRender: z.boolean().default(false),
/**
* Validate against passing mutable functions to hooks
*/
validateNoFreezingKnownMutableFunctions: z.boolean().default(false),
/*
* When enabled, the compiler assumes that hooks follow the Rules of React:
* - Hooks may memoize computation based on any of their parameters, thus

View File

@@ -25,6 +25,9 @@ import {
BuiltInUseRefId,
BuiltInUseStateId,
BuiltInUseTransitionId,
BuiltInWeakMapId,
BuiltInWeakSetId,
ReanimatedSharedValueId,
ShapeRegistry,
addFunction,
addHook,
@@ -491,6 +494,38 @@ const TYPED_GLOBALS: Array<[string, BuiltInType]> = [
true,
),
],
[
'WeakMap',
addFunction(
DEFAULT_SHAPES,
[],
{
positionalParams: [Effect.ConditionallyMutateIterator],
restParam: null,
returnType: {kind: 'Object', shapeId: BuiltInWeakMapId},
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Mutable,
},
null,
true,
),
],
[
'WeakSet',
addFunction(
DEFAULT_SHAPES,
[],
{
positionalParams: [Effect.ConditionallyMutateIterator],
restParam: null,
returnType: {kind: 'Object', shapeId: BuiltInWeakSetId},
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Mutable,
},
null,
true,
),
],
// TODO: rest of Global objects
];
@@ -908,7 +943,7 @@ export function getReanimatedModuleType(registry: ShapeRegistry): ObjectType {
addHook(registry, {
positionalParams: [],
restParam: Effect.Freeze,
returnType: {kind: 'Poly'},
returnType: {kind: 'Object', shapeId: ReanimatedSharedValueId},
returnValueKind: ValueKind.Mutable,
noAlias: true,
calleeEffect: Effect.Read,

View File

@@ -746,6 +746,27 @@ export enum InstructionKind {
Function = 'Function',
}
export function convertHoistedLValueKind(
kind: InstructionKind,
): InstructionKind | null {
switch (kind) {
case InstructionKind.HoistedLet:
return InstructionKind.Let;
case InstructionKind.HoistedConst:
return InstructionKind.Const;
case InstructionKind.HoistedFunction:
return InstructionKind.Function;
case InstructionKind.Let:
case InstructionKind.Const:
case InstructionKind.Function:
case InstructionKind.Reassign:
case InstructionKind.Catch:
return null;
default:
assertExhaustive(kind, 'Unexpected lvalue kind');
}
}
function _staticInvariantInstructionValueHasLocation(
value: InstructionValue,
): SourceLocation {
@@ -880,8 +901,20 @@ export type InstructionValue =
| StoreLocal
| {
kind: 'StoreContext';
/**
* StoreContext kinds:
* Reassign: context variable reassignment in source
* Const: const declaration + assignment in source
* ('const' context vars are ones whose declarations are hoisted)
* Let: let declaration + assignment in source
* Function: function declaration in source (similar to `const`)
*/
lvalue: {
kind: InstructionKind.Reassign;
kind:
| InstructionKind.Reassign
| InstructionKind.Const
| InstructionKind.Let
| InstructionKind.Function;
place: Place;
};
value: Place;
@@ -1692,6 +1725,18 @@ export function isRefOrRefValue(id: Identifier): boolean {
return isUseRefType(id) || isRefValueType(id);
}
/*
* Returns true if the type is a Ref or a custom user type that acts like a ref when it
* shouldn't. For now the only other case of this is Reanimated's shared values.
*/
export function isRefOrRefLikeMutableType(type: Type): boolean {
return (
type.kind === 'Object' &&
(type.shapeId === 'BuiltInUseRefId' ||
type.shapeId == 'ReanimatedSharedValueId')
);
}
export function isSetStateType(id: Identifier): boolean {
return id.type.kind === 'Function' && id.type.shapeId === 'BuiltInSetState';
}

View File

@@ -203,6 +203,8 @@ export const BuiltInPropsId = 'BuiltInProps';
export const BuiltInArrayId = 'BuiltInArray';
export const BuiltInSetId = 'BuiltInSet';
export const BuiltInMapId = 'BuiltInMap';
export const BuiltInWeakSetId = 'BuiltInWeakSet';
export const BuiltInWeakMapId = 'BuiltInWeakMap';
export const BuiltInFunctionId = 'BuiltInFunction';
export const BuiltInJsxId = 'BuiltInJsx';
export const BuiltInObjectId = 'BuiltInObject';
@@ -225,6 +227,9 @@ export const BuiltInStartTransitionId = 'BuiltInStartTransition';
export const BuiltInFireId = 'BuiltInFire';
export const BuiltInFireFunctionId = 'BuiltInFireFunction';
// See getReanimatedModuleType() in Globals.ts — this is part of supporting Reanimated's ref-like types
export const ReanimatedSharedValueId = 'ReanimatedSharedValueId';
// ShapeRegistry with default definitions for built-ins.
export const BUILTIN_SHAPES: ShapeRegistry = new Map();
@@ -764,6 +769,101 @@ addObject(BUILTIN_SHAPES, BuiltInMapId, [
],
]);
addObject(BUILTIN_SHAPES, BuiltInWeakSetId, [
[
/**
* add(value)
* Parameters
* value: the value of the element to add to the Set object.
* Returns the Set object with added value.
*/
'add',
addFunction(BUILTIN_SHAPES, [], {
positionalParams: [Effect.Capture],
restParam: null,
returnType: {kind: 'Object', shapeId: BuiltInWeakSetId},
calleeEffect: Effect.Store,
// returnValueKind is technically dependent on the ValueKind of the set itself
returnValueKind: ValueKind.Mutable,
}),
],
[
/**
* setInstance.delete(value)
* Returns true if value was already in Set; otherwise false.
*/
'delete',
addFunction(BUILTIN_SHAPES, [], {
positionalParams: [Effect.Read],
restParam: null,
returnType: PRIMITIVE_TYPE,
calleeEffect: Effect.Store,
returnValueKind: ValueKind.Primitive,
}),
],
[
'has',
addFunction(BUILTIN_SHAPES, [], {
positionalParams: [Effect.Read],
restParam: null,
returnType: PRIMITIVE_TYPE,
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Primitive,
}),
],
]);
addObject(BUILTIN_SHAPES, BuiltInWeakMapId, [
[
'delete',
addFunction(BUILTIN_SHAPES, [], {
positionalParams: [Effect.Read],
restParam: null,
returnType: PRIMITIVE_TYPE,
calleeEffect: Effect.Store,
returnValueKind: ValueKind.Primitive,
}),
],
[
'get',
addFunction(BUILTIN_SHAPES, [], {
positionalParams: [Effect.Read],
restParam: null,
returnType: {kind: 'Poly'},
calleeEffect: Effect.Capture,
returnValueKind: ValueKind.Mutable,
}),
],
[
'has',
addFunction(BUILTIN_SHAPES, [], {
positionalParams: [Effect.Read],
restParam: null,
returnType: PRIMITIVE_TYPE,
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Primitive,
}),
],
[
/**
* Params
* key: the key of the element to add to the Map object. The key may be
* any JavaScript type (any primitive value or any type of JavaScript
* object).
* value: the value of the element to add to the Map object.
* Returns the Map object.
*/
'set',
addFunction(BUILTIN_SHAPES, [], {
positionalParams: [Effect.Capture, Effect.Capture],
restParam: null,
returnType: {kind: 'Object', shapeId: BuiltInWeakMapId},
calleeEffect: Effect.Store,
returnValueKind: ValueKind.Mutable,
}),
],
]);
addObject(BUILTIN_SHAPES, BuiltInUseStateId, [
['0', {kind: 'Poly'}],
[

View File

@@ -30,6 +30,7 @@ import {
FunctionExpression,
ObjectMethod,
PropertyLiteral,
convertHoistedLValueKind,
} from './HIR';
import {
collectHoistablePropertyLoads,
@@ -246,12 +247,18 @@ function isLoadContextMutable(
id: InstructionId,
): instrValue is LoadContext {
if (instrValue.kind === 'LoadContext') {
CompilerError.invariant(instrValue.place.identifier.scope != null, {
reason:
'[PropagateScopeDependencies] Expected all context variables to be assigned a scope',
loc: instrValue.loc,
});
return id >= instrValue.place.identifier.scope.range.end;
/**
* Not all context variables currently have scopes due to limitations of
* mutability analysis for function expressions.
*
* Currently, many function expressions references are inferred to be
* 'Read' | 'Freeze' effects which don't replay mutable effects of captured
* context.
*/
return (
instrValue.place.identifier.scope != null &&
id >= instrValue.place.identifier.scope.range.end
);
}
return false;
}
@@ -471,6 +478,9 @@ export class DependencyCollectionContext {
}
this.#reassignments.set(identifier, decl);
}
hasDeclared(identifier: Identifier): boolean {
return this.#declarations.has(identifier.declarationId);
}
// Checks if identifier is a valid dependency in the current scope
#checkValidDependency(maybeDependency: ReactiveScopeDependency): boolean {
@@ -672,21 +682,21 @@ export function handleInstruction(
});
} else if (value.kind === 'DeclareLocal' || value.kind === 'DeclareContext') {
/*
* Some variables may be declared and never initialized. We need
* to retain (and hoist) these declarations if they are included
* in a reactive scope. One approach is to simply add all `DeclareLocal`s
* as scope declarations.
* Some variables may be declared and never initialized. We need to retain
* (and hoist) these declarations if they are included in a reactive scope.
* One approach is to simply add all `DeclareLocal`s as scope declarations.
*
* Context variables with hoisted declarations only become live after their
* first assignment. We only declare real DeclareLocal / DeclareContext
* instructions (not hoisted ones) to avoid generating dependencies on
* hoisted declarations.
*/
/*
* We add context variable declarations here, not at `StoreContext`, since
* context Store / Loads are modeled as reads and mutates to the underlying
* variable reference (instead of through intermediate / inlined temporaries)
*/
context.declare(value.lvalue.place.identifier, {
id,
scope: context.currentScope,
});
if (convertHoistedLValueKind(value.lvalue.kind) === null) {
context.declare(value.lvalue.place.identifier, {
id,
scope: context.currentScope,
});
}
} else if (value.kind === 'Destructure') {
context.visitOperand(value.value);
for (const place of eachPatternOperand(value.lvalue.pattern)) {
@@ -698,6 +708,26 @@ export function handleInstruction(
scope: context.currentScope,
});
}
} else if (value.kind === 'StoreContext') {
/**
* Some StoreContext variables have hoisted declarations. If we're storing
* to a context variable that hasn't yet been declared, the StoreContext is
* the declaration.
* (see corresponding logic in PruneHoistedContext)
*/
if (
!context.hasDeclared(value.lvalue.place.identifier) ||
value.lvalue.kind !== InstructionKind.Reassign
) {
context.declare(value.lvalue.place.identifier, {
id,
scope: context.currentScope,
});
}
for (const operand of eachInstructionValueOperand(value)) {
context.visitOperand(operand);
}
} else {
for (const operand of eachInstructionValueOperand(value)) {
context.visitOperand(operand);

View File

@@ -0,0 +1,134 @@
/**
* 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 {
Effect,
HIRFunction,
Identifier,
isMutableEffect,
isRefOrRefLikeMutableType,
makeInstructionId,
} from '../HIR/HIR';
import {eachInstructionValueOperand} from '../HIR/visitors';
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
import DisjointSet from '../Utils/DisjointSet';
/**
* If a function captures a mutable value but never gets called, we don't infer a
* mutable range for that function. This means that we also don't alias the function
* with its mutable captures.
*
* This case is tricky, because we don't generally know for sure what is a mutation
* and what may just be a normal function call. For example:
*
* ```
* hook useFoo() {
* const x = makeObject();
* return () => {
* return readObject(x); // could be a mutation!
* }
* }
* ```
*
* If we pessimistically assume that all such cases are mutations, we'd have to group
* lots of memo scopes together unnecessarily. However, if there is definitely a mutation:
*
* ```
* hook useFoo(createEntryForKey) {
* const cache = new WeakMap();
* return (key) => {
* let entry = cache.get(key);
* if (entry == null) {
* entry = createEntryForKey(key);
* cache.set(key, entry); // known mutation!
* }
* return entry;
* }
* }
* ```
*
* Then we have to ensure that the function and its mutable captures alias together and
* end up in the same scope. However, aliasing together isn't enough if the function
* and operands all have empty mutable ranges (end = start + 1).
*
* This pass finds function expressions and object methods that have an empty mutable range
* and known-mutable operands which also don't have a mutable range, and ensures that the
* function and those operands are aliased together *and* that their ranges are updated to
* end after the function expression. This is sufficient to ensure that a reactive scope is
* created for the alias set.
*/
export function inferAliasForUncalledFunctions(
fn: HIRFunction,
aliases: DisjointSet<Identifier>,
): void {
for (const block of fn.body.blocks.values()) {
instrs: for (const instr of block.instructions) {
const {lvalue, value} = instr;
if (
value.kind !== 'ObjectMethod' &&
value.kind !== 'FunctionExpression'
) {
continue;
}
/*
* If the function is known to be mutated, we will have
* already aliased any mutable operands with it
*/
const range = lvalue.identifier.mutableRange;
if (range.end > range.start + 1) {
continue;
}
/*
* If the function already has operands with an active mutable range,
* then we don't need to do anything — the function will have already
* been visited and included in some mutable alias set. This case can
* also occur due to visiting the same function in an earlier iteration
* of the outer fixpoint loop.
*/
for (const operand of eachInstructionValueOperand(value)) {
if (isMutable(instr, operand)) {
continue instrs;
}
}
const operands: Set<Identifier> = new Set();
for (const effect of value.loweredFunc.func.effects ?? []) {
if (effect.kind !== 'ContextMutation') {
continue;
}
/*
* We're looking for known-mutations only, so we look at the effects
* rather than function context
*/
if (effect.effect === Effect.Store || effect.effect === Effect.Mutate) {
for (const operand of effect.places) {
/*
* It's possible that function effect analysis thinks there was a context mutation,
* but then InferReferenceEffects figures out some operands are globals and therefore
* creates a non-mutable effect for those operands.
* We should change InferReferenceEffects to swap the ContextMutation for a global
* mutation in that case, but for now we just filter them out here
*/
if (
isMutableEffect(operand.effect, operand.loc) &&
!isRefOrRefLikeMutableType(operand.identifier.type)
) {
operands.add(operand.identifier);
}
}
}
}
if (operands.size !== 0) {
operands.add(lvalue.identifier);
aliases.union([...operands]);
// Update mutable ranges, if the ranges are empty then a reactive scope isn't created
for (const operand of operands) {
operand.mutableRange.end = makeInstructionId(instr.id + 1);
}
}
}
}
}

View File

@@ -176,9 +176,15 @@ export function inferMutableLifetimes(
if (
instr.value.kind === 'DeclareContext' ||
(instr.value.kind === 'StoreContext' &&
instr.value.lvalue.kind !== InstructionKind.Reassign)
instr.value.lvalue.kind !== InstructionKind.Reassign &&
!contextVariableDeclarationInstructions.has(
instr.value.lvalue.place.identifier,
))
) {
// Save declarations of context variables
/**
* Save declarations of context variables if they hasn't already been
* declared (due to hoisted declarations).
*/
contextVariableDeclarationInstructions.set(
instr.value.lvalue.place.identifier,
instr.id,

View File

@@ -6,6 +6,7 @@
*/
import {HIRFunction, Identifier} from '../HIR/HIR';
import {inferAliasForUncalledFunctions} from './InerAliasForUncalledFunctions';
import {inferAliases} from './InferAlias';
import {inferAliasForPhis} from './InferAliasForPhis';
import {inferAliasForStores} from './InferAliasForStores';
@@ -76,6 +77,7 @@ export function inferMutableRanges(ir: HIRFunction): void {
while (true) {
inferMutableRangesForAlias(ir, aliases);
inferAliasForPhis(ir, aliases);
inferAliasForUncalledFunctions(ir, aliases);
const nextAliases = aliases.canonicalize();
if (areEqualMaps(prevAliases, nextAliases)) {
break;

View File

@@ -111,7 +111,10 @@ export default function inferReferenceEffects(
* Initial state contains function params
* TODO: include module declarations here as well
*/
const initialState = InferenceState.empty(fn.env);
const initialState = InferenceState.empty(
fn.env,
options.isFunctionExpression,
);
const value: InstructionValue = {
kind: 'Primitive',
loc: fn.loc,
@@ -255,6 +258,7 @@ type FreezeAction = {values: Set<InstructionValue>; reason: Set<ValueReason>};
// Maintains a mapping of top-level variables to the kind of value they hold
class InferenceState {
env: Environment;
#isFunctionExpression: boolean;
// The kind of each value, based on its allocation site
#values: Map<InstructionValue, AbstractValue>;
@@ -267,16 +271,25 @@ class InferenceState {
constructor(
env: Environment,
isFunctionExpression: boolean,
values: Map<InstructionValue, AbstractValue>,
variables: Map<IdentifierId, Set<InstructionValue>>,
) {
this.env = env;
this.#isFunctionExpression = isFunctionExpression;
this.#values = values;
this.#variables = variables;
}
static empty(env: Environment): InferenceState {
return new InferenceState(env, new Map(), new Map());
static empty(
env: Environment,
isFunctionExpression: boolean,
): InferenceState {
return new InferenceState(env, isFunctionExpression, new Map(), new Map());
}
get isFunctionExpression(): boolean {
return this.#isFunctionExpression;
}
// (Re)initializes a @param value with its default @param kind.
@@ -394,9 +407,14 @@ class InferenceState {
freezeValues(values: Set<InstructionValue>, reason: Set<ValueReason>): void {
for (const value of values) {
if (value.kind === 'DeclareContext') {
if (
value.kind === 'DeclareContext' ||
(value.kind === 'StoreContext' &&
(value.lvalue.kind === InstructionKind.Let ||
value.lvalue.kind === InstructionKind.Const))
) {
/**
* Avoid freezing hoisted context declarations
* Avoid freezing context variable declarations, hoisted or otherwise
* function Component() {
* const cb = useBar(() => foo(2)); // produces a hoisted context declaration
* const foo = useFoo(); // reassigns to the context variable
@@ -613,6 +631,7 @@ class InferenceState {
} else {
return new InferenceState(
this.env,
this.#isFunctionExpression,
nextValues ?? new Map(this.#values),
nextVariables ?? new Map(this.#variables),
);
@@ -627,6 +646,7 @@ class InferenceState {
clone(): InferenceState {
return new InferenceState(
this.env,
this.#isFunctionExpression,
new Map(this.#values),
new Map(this.#variables),
);
@@ -1591,6 +1611,14 @@ function inferBlock(
);
const lvalue = instr.lvalue;
if (instrValue.lvalue.kind !== InstructionKind.Reassign) {
state.initialize(instrValue, {
kind: ValueKind.Mutable,
reason: new Set([ValueReason.Other]),
context: new Set(),
});
state.define(instrValue.lvalue.place, instrValue);
}
state.alias(lvalue, instrValue.value);
lvalue.effect = Effect.Store;
continuation = {kind: 'funeffects'};
@@ -1781,8 +1809,15 @@ function inferBlock(
if (block.terminal.kind === 'return' || block.terminal.kind === 'throw') {
if (
state.isDefined(operand) &&
state.kind(operand).kind === ValueKind.Context
((operand.identifier.type.kind === 'Function' &&
state.isFunctionExpression) ||
state.kind(operand).kind === ValueKind.Context)
) {
/**
* Returned values should only be typed as 'frozen' if they are both (1)
* local and (2) not a function expression which may capture and mutate
* this function's outer context.
*/
effect = Effect.ConditionallyMutate;
} else {
effect = Effect.Freeze;

View File

@@ -48,7 +48,7 @@ import {printIdentifier, printPlace} from '../HIR/PrintHIR';
import {eachPatternOperand} from '../HIR/visitors';
import {Err, Ok, Result} from '../Utils/Result';
import {GuardKind} from '../Utils/RuntimeDiagnosticConstants';
import {assertExhaustive, hasOwnProperty} from '../Utils/utils';
import {assertExhaustive} from '../Utils/utils';
import {buildReactiveFunction} from './BuildReactiveFunction';
import {SINGLE_CHILD_FBT_TAGS} from './MemoizeFbtAndMacroOperandsInSameScope';
import {ReactiveFunctionVisitor, visitReactiveFunction} from './visitors';
@@ -374,8 +374,6 @@ function codegenReactiveFunction(
const countMemoBlockVisitor = new CountMemoBlockVisitor(fn.env);
visitReactiveFunction(fn, countMemoBlockVisitor, undefined);
setMissingLocationsToNull(body);
return Ok({
type: 'CodegenFunction',
loc: fn.loc,
@@ -1002,6 +1000,14 @@ function codegenTerminal(
lval = codegenLValue(cx, iterableItem.value.lvalue.pattern);
break;
}
case 'StoreContext': {
CompilerError.throwTodo({
reason: 'Support non-trivial for..in inits',
description: null,
loc: terminal.init.loc,
suggestions: null,
});
}
default:
CompilerError.invariant(false, {
reason: `Expected a StoreLocal or Destructure to be assigned to the collection`,
@@ -1094,6 +1100,14 @@ function codegenTerminal(
lval = codegenLValue(cx, iterableItem.value.lvalue.pattern);
break;
}
case 'StoreContext': {
CompilerError.throwTodo({
reason: 'Support non-trivial for..of inits',
description: null,
loc: terminal.init.loc,
suggestions: null,
});
}
default:
CompilerError.invariant(false, {
reason: `Expected a StoreLocal or Destructure to be assigned to the collection`,
@@ -2313,9 +2327,12 @@ function codegenInstructionValue(
* u0080 to u009F: C1 control codes
* u00A0 to uFFFF: All non-basic Latin characters
* https://en.wikipedia.org/wiki/List_of_Unicode_characters#Control_codes
*
* u010000 to u10FFFF: Astral plane characters
* https://mathiasbynens.be/notes/javascript-unicode
*/
const STRING_REQUIRES_EXPR_CONTAINER_PATTERN =
/[\u{0000}-\u{001F}\u{007F}\u{0080}-\u{FFFF}]|"|\\/u;
/[\u{0000}-\u{001F}\u{007F}\u{0080}-\u{FFFF}\u{010000}-\u{10FFFF}]|"|\\/u;
function codegenJsxAttribute(
cx: Context,
attribute: JsxAttribute,
@@ -2667,33 +2684,3 @@ function compareScopeDeclaration(
else if (aName > bName) return 1;
else return 0;
}
function setMissingLocationsToNull(ast: any): void {
if (Array.isArray(ast)) {
ast.forEach(item => setMissingLocationsToNull(item));
return;
} else if (
ast == null ||
typeof ast !== 'object' ||
typeof ast['type'] !== 'string'
) {
return;
}
if (ast['loc'] == null) {
ast['loc'] = null;
}
for (const key in ast) {
if (!hasOwnProperty(ast, key)) {
continue;
}
const value = ast[key];
if (typeof value !== 'object') {
/*
* We handle this above too, but avoid extra function calls in the majority of
* cases where we're traversing an AST node's properties
*/
continue;
}
setMissingLocationsToNull(ast[key]);
}
}

View File

@@ -255,6 +255,12 @@ function writeReactiveValue(writer: Writer, value: ReactiveValue): void {
}
}
export function printReactiveTerminal(terminal: ReactiveTerminal): string {
const writer = new Writer();
writeTerminal(writer, terminal);
return writer.complete();
}
function writeTerminal(writer: Writer, terminal: ReactiveTerminal): void {
switch (terminal.kind) {
case 'break': {

View File

@@ -7,12 +7,17 @@
import {CompilerError} from '..';
import {
DeclarationId,
convertHoistedLValueKind,
IdentifierId,
InstructionId,
InstructionKind,
Place,
ReactiveFunction,
ReactiveInstruction,
ReactiveScopeBlock,
ReactiveStatement,
} from '../HIR';
import {empty, Stack} from '../Utils/Stack';
import {
ReactiveFunctionTransform,
Transformed,
@@ -22,138 +27,144 @@ import {
/*
* Prunes DeclareContexts lowered for HoistedConsts, and transforms any references back to its
* original instruction kind.
*
* Also detects and bails out on context variables which are:
* - function declarations, which are hoisted by JS engines to the nearest block scope
* - referenced before they are defined (i.e. having a `DeclareContext HoistedConst`)
* - declared
*
* This is because React Compiler converts a `function foo()` function declaration to
* 1. a `let foo;` declaration before reactive memo blocks
* 2. a `foo = function foo() {}` assignment within the block
*
* This means references before the assignment are invalid (see fixture
* error.todo-functiondecl-hoisting)
*/
export function pruneHoistedContexts(fn: ReactiveFunction): void {
const hoistedIdentifiers: HoistedIdentifiers = new Map();
visitReactiveFunction(fn, new Visitor(), hoistedIdentifiers);
visitReactiveFunction(fn, new Visitor(), {
activeScopes: empty(),
uninitialized: new Map(),
});
}
const REWRITTEN_HOISTED_CONST: unique symbol = Symbol(
'REWRITTEN_HOISTED_CONST',
);
const REWRITTEN_HOISTED_LET: unique symbol = Symbol('REWRITTEN_HOISTED_LET');
type VisitorState = {
activeScopes: Stack<Set<IdentifierId>>;
uninitialized: Map<
IdentifierId,
| {
kind: 'unknown-kind';
}
| {
kind: 'func';
definition: Place | null;
}
>;
};
type HoistedIdentifiers = Map<
DeclarationId,
| InstructionKind
| typeof REWRITTEN_HOISTED_CONST
| typeof REWRITTEN_HOISTED_LET
>;
class Visitor extends ReactiveFunctionTransform<HoistedIdentifiers> {
class Visitor extends ReactiveFunctionTransform<VisitorState> {
override visitScope(scope: ReactiveScopeBlock, state: VisitorState): void {
state.activeScopes = state.activeScopes.push(
new Set(scope.scope.declarations.keys()),
);
/**
* Add declared but not initialized / assigned variables. This may include
* function declarations that escape the memo block.
*/
for (const decl of scope.scope.declarations.values()) {
state.uninitialized.set(decl.identifier.id, {kind: 'unknown-kind'});
}
this.traverseScope(scope, state);
state.activeScopes.pop();
for (const decl of scope.scope.declarations.values()) {
state.uninitialized.delete(decl.identifier.id);
}
}
override visitPlace(
_id: InstructionId,
place: Place,
state: VisitorState,
): void {
const maybeHoistedFn = state.uninitialized.get(place.identifier.id);
if (
maybeHoistedFn?.kind === 'func' &&
maybeHoistedFn.definition !== place
) {
CompilerError.throwTodo({
reason: '[PruneHoistedContexts] Rewrite hoisted function references',
loc: place.loc,
});
}
}
override transformInstruction(
instruction: ReactiveInstruction,
state: HoistedIdentifiers,
state: VisitorState,
): Transformed<ReactiveStatement> {
this.visitInstruction(instruction, state);
/**
* Remove hoisted declarations to preserve TDZ
*/
if (
instruction.value.kind === 'DeclareContext' &&
instruction.value.lvalue.kind === 'HoistedConst'
) {
state.set(
instruction.value.lvalue.place.identifier.declarationId,
InstructionKind.Const,
if (instruction.value.kind === 'DeclareContext') {
const maybeNonHoisted = convertHoistedLValueKind(
instruction.value.lvalue.kind,
);
return {kind: 'remove'};
}
if (
instruction.value.kind === 'DeclareContext' &&
instruction.value.lvalue.kind === 'HoistedLet'
) {
state.set(
instruction.value.lvalue.place.identifier.declarationId,
InstructionKind.Let,
);
return {kind: 'remove'};
}
if (
instruction.value.kind === 'DeclareContext' &&
instruction.value.lvalue.kind === 'HoistedFunction'
) {
state.set(
instruction.value.lvalue.place.identifier.declarationId,
InstructionKind.Function,
);
return {kind: 'remove'};
}
if (instruction.value.kind === 'StoreContext') {
const kind = state.get(
instruction.value.lvalue.place.identifier.declarationId,
);
if (kind != null) {
CompilerError.invariant(kind !== REWRITTEN_HOISTED_CONST, {
reason: 'Expected exactly one store to a hoisted const variable',
loc: instruction.loc,
});
if (maybeNonHoisted != null) {
if (
kind === InstructionKind.Const ||
kind === InstructionKind.Function
maybeNonHoisted === InstructionKind.Function &&
state.uninitialized.has(instruction.value.lvalue.place.identifier.id)
) {
state.set(
instruction.value.lvalue.place.identifier.declarationId,
REWRITTEN_HOISTED_CONST,
);
return {
kind: 'replace',
value: {
kind: 'instruction',
instruction: {
...instruction,
value: {
...instruction.value,
lvalue: {
...instruction.value.lvalue,
kind,
},
type: null,
kind: 'StoreLocal',
},
},
state.uninitialized.set(
instruction.value.lvalue.place.identifier.id,
{
kind: 'func',
definition: null,
},
};
} else if (kind !== REWRITTEN_HOISTED_LET) {
/**
* Context variables declared with let may have reassignments. Only
* insert a `DeclareContext` for the first encountered `StoreContext`
* instruction.
*/
state.set(
instruction.value.lvalue.place.identifier.declarationId,
REWRITTEN_HOISTED_LET,
);
return {
kind: 'replace-many',
value: [
{
kind: 'instruction',
instruction: {
id: instruction.id,
lvalue: null,
value: {
kind: 'DeclareContext',
lvalue: {
kind: InstructionKind.Let,
place: {...instruction.value.lvalue.place},
},
loc: instruction.value.loc,
},
loc: instruction.loc,
},
},
{kind: 'instruction', instruction},
],
};
}
return {kind: 'remove'};
}
}
if (
instruction.value.kind === 'StoreContext' &&
instruction.value.lvalue.kind !== InstructionKind.Reassign
) {
/**
* Rewrite StoreContexts let/const that will be pre-declared in
* codegen to reassignments.
*/
const lvalueId = instruction.value.lvalue.place.identifier.id;
const isDeclaredByScope = state.activeScopes.find(scope =>
scope.has(lvalueId),
);
if (isDeclaredByScope) {
if (
instruction.value.lvalue.kind === InstructionKind.Let ||
instruction.value.lvalue.kind === InstructionKind.Const
) {
instruction.value.lvalue.kind = InstructionKind.Reassign;
} else if (instruction.value.lvalue.kind === InstructionKind.Function) {
const maybeHoistedFn = state.uninitialized.get(lvalueId);
if (maybeHoistedFn != null) {
CompilerError.invariant(maybeHoistedFn.kind === 'func', {
reason: '[PruneHoistedContexts] Unexpected hoisted function',
loc: instruction.loc,
});
maybeHoistedFn.definition = instruction.value.lvalue.place;
/**
* References to hoisted functions are now "safe" as variable assignments
* have finished.
*/
state.uninitialized.delete(lvalueId);
}
} else {
CompilerError.throwTodo({
reason: '[PruneHoistedContexts] Unexpected kind',
description: `(${instruction.value.lvalue.kind})`,
loc: instruction.loc,
});
}
}
}
this.visitInstruction(instruction, state);
return {kind: 'keep'};
}
}

View File

@@ -0,0 +1,130 @@
/**
* 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, Effect, ErrorSeverity} from '..';
import {
FunctionEffect,
HIRFunction,
IdentifierId,
isMutableEffect,
isRefOrRefLikeMutableType,
Place,
} from '../HIR';
import {
eachInstructionValueOperand,
eachTerminalOperand,
} from '../HIR/visitors';
import {Result} from '../Utils/Result';
import {Iterable_some} from '../Utils/utils';
/**
* Validates that functions with known mutations (ie due to types) cannot be passed
* where a frozen value is expected. Example:
*
* ```
* function Component() {
* const cache = new Map();
* const onClick = () => {
* cache.set(...);
* }
* useHook(onClick); // ERROR: cannot pass a mutable value
* return <Foo onClick={onClick} /> // ERROR: cannot pass a mutable value
* }
* ```
*
* Because `onClick` function mutates `cache` when called, `onClick` is equivalent to a mutable
* variables. But unlike other mutables values like an array, the receiver of the function has
* no way to avoid mutation — for example, a function can receive an array and choose not to mutate
* it, but there's no way to know that a function is mutable and avoid calling it.
*
* 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();
const contextMutationEffects: Map<
IdentifierId,
Extract<FunctionEffect, {kind: 'ContextMutation'}>
> = new Map();
function visitOperand(operand: Place): void {
if (operand.effect === Effect.Freeze) {
const effect = contextMutationEffects.get(operand.identifier.id);
if (effect != null) {
errors.push({
reason: `This argument is a function which modifies local variables when called, which can bypass memoization and cause the UI not to update`,
description: `Functions that are returned from hooks, passed as arguments to hooks, or passed as props to components may not mutate local variables`,
loc: operand.loc,
severity: ErrorSeverity.InvalidReact,
});
errors.push({
reason: `The function modifies a local variable here`,
loc: effect.loc,
severity: ErrorSeverity.InvalidReact,
});
}
}
}
for (const block of fn.body.blocks.values()) {
for (const instr of block.instructions) {
const {lvalue, value} = instr;
switch (value.kind) {
case 'LoadLocal': {
const effect = contextMutationEffects.get(value.place.identifier.id);
if (effect != null) {
contextMutationEffects.set(lvalue.identifier.id, effect);
}
break;
}
case 'StoreLocal': {
const effect = contextMutationEffects.get(value.value.identifier.id);
if (effect != null) {
contextMutationEffects.set(lvalue.identifier.id, effect);
contextMutationEffects.set(
value.lvalue.place.identifier.id,
effect,
);
}
break;
}
case 'FunctionExpression': {
const knownMutation = (value.loweredFunc.func.effects ?? []).find(
effect => {
return (
effect.kind === 'ContextMutation' &&
(effect.effect === Effect.Store ||
effect.effect === Effect.Mutate) &&
Iterable_some(effect.places, place => {
return (
isMutableEffect(place.effect, place.loc) &&
!isRefOrRefLikeMutableType(place.identifier.type)
);
})
);
},
);
if (knownMutation && knownMutation.kind === 'ContextMutation') {
contextMutationEffects.set(lvalue.identifier.id, knownMutation);
}
break;
}
default: {
for (const operand of eachInstructionValueOperand(value)) {
visitOperand(operand);
}
}
}
}
for (const operand of eachTerminalOperand(block.terminal)) {
visitOperand(operand);
}
}
return errors.asResult();
}

View File

@@ -141,14 +141,14 @@ function getCompareDependencyResultDescription(
): string {
switch (result) {
case CompareDependencyResult.Ok:
return 'dependencies equal';
return 'Dependencies equal';
case CompareDependencyResult.RootDifference:
case CompareDependencyResult.PathDifference:
return 'inferred different dependency than source';
return 'Inferred different dependency than source';
case CompareDependencyResult.RefAccessDifference:
return 'differences in ref.current access';
return 'Differences in ref.current access';
case CompareDependencyResult.Subpath:
return 'inferred less specific property than source';
return 'Inferred less specific property than source';
}
}
@@ -279,17 +279,20 @@ function validateInferredDep(
severity: ErrorSeverity.CannotPreserveMemoization,
reason:
'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',
description: DEBUG
? `The inferred dependency was \`${prettyPrintScopeDependency(
dep,
)}\`, but the source dependencies were [${validDepsInMemoBlock
.map(dep => printManualMemoDependency(dep, true))
.join(', ')}]. Detail: ${
errorDiagnostic
? getCompareDependencyResultDescription(errorDiagnostic)
: 'none'
}`
: null,
description:
DEBUG ||
// If the dependency is a named variable then we can report it. Otherwise only print in debug mode
(dep.identifier.name != null && dep.identifier.name.kind === 'named')
? `The inferred dependency was \`${prettyPrintScopeDependency(
dep,
)}\`, but the source dependencies were [${validDepsInMemoBlock
.map(dep => printManualMemoDependency(dep, true))
.join(', ')}]. ${
errorDiagnostic
? getCompareDependencyResultDescription(errorDiagnostic)
: 'Inferred dependency not present in source'
}`
: null,
loc: memoLocation,
suggestions: null,
});

View File

@@ -1,79 +0,0 @@
## Input
```javascript
import {Stringify} from 'shared-runtime';
/**
* Fixture currently fails with
* Found differences in evaluator results
* Non-forget (expected):
* (kind: ok) <div>{"result":{"value":2},"fn":{"kind":"Function","result":{"value":2}},"shouldInvokeFns":true}</div>
* Forget:
* (kind: exception) bar is not a function
*/
function Foo({value}) {
const result = bar();
function bar() {
return {value};
}
return <Stringify result={result} fn={bar} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{value: 2}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { Stringify } from "shared-runtime";
/**
* Fixture currently fails with
* Found differences in evaluator results
* Non-forget (expected):
* (kind: ok) <div>{"result":{"value":2},"fn":{"kind":"Function","result":{"value":2}},"shouldInvokeFns":true}</div>
* Forget:
* (kind: exception) bar is not a function
*/
function Foo(t0) {
const $ = _c(6);
const { value } = t0;
let bar;
let result;
if ($[0] !== value) {
result = bar();
bar = function bar() {
return { value };
};
$[0] = value;
$[1] = bar;
$[2] = result;
} else {
bar = $[1];
result = $[2];
}
let t1;
if ($[3] !== bar || $[4] !== result) {
t1 = <Stringify result={result} fn={bar} shouldInvokeFns={true} />;
$[3] = bar;
$[4] = result;
$[5] = t1;
} else {
t1 = $[5];
}
return t1;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{ value: 2 }],
};
```

View File

@@ -34,8 +34,7 @@ function bar(a, b) {
if ($[0] !== a || $[1] !== b) {
const x = [a, b];
y = {};
let t;
t = {};
let t = {};
y = x[0][1];
t = x[1][0];

View File

@@ -35,8 +35,7 @@ function bar(a, b) {
if ($[0] !== a || $[1] !== b) {
const x = [a, b];
y = {};
let t;
t = {};
let t = {};
const f0 = function () {
y = x[0][1];
t = x[1][0];

View File

@@ -33,8 +33,7 @@ function useTest() {
const $ = _c(1);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
let w;
w = {};
let w = {};
const t1 = (w = 42);
const t2 = w;

View File

@@ -30,8 +30,7 @@ function Component(props) {
const $ = _c(1);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
let x;
x = null;
let x = null;
const callback = () => {
console.log(x);
};

View File

@@ -41,7 +41,7 @@ function Component(props) {
> 10 | return x;
| ^^^^^^^^^^^^^^^^^
> 11 | }, [props?.items, props.cond]);
| ^^^^ CannotPreserveMemoization: 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 (4:11)
| ^^^^ CannotPreserveMemoization: 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 `props.items`, but the source dependencies were [props?.items, props.cond]. Inferred different dependency than source (4:11)
12 | return (
13 | <ValidateMemoization inputs={[props?.items, props.cond]} output={data} />
14 | );

View File

@@ -41,7 +41,7 @@ function Component(props) {
> 10 | return x;
| ^^^^^^^^^^^^^^^^^
> 11 | }, [props?.items, props.cond]);
| ^^^^ CannotPreserveMemoization: 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 (4:11)
| ^^^^ CannotPreserveMemoization: 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 `props.items`, but the source dependencies were [props?.items, props.cond]. Inferred different dependency than source (4:11)
12 | return (
13 | <ValidateMemoization inputs={[props?.items, props.cond]} output={data} />
14 | );

View File

@@ -0,0 +1,34 @@
## Input
```javascript
// @validateNoFreezingKnownMutableFunctions
function useFoo() {
const cache = new Map();
useHook(() => {
cache.set('key', 'value');
});
}
```
## Error
```
3 | function useFoo() {
4 | const cache = new Map();
> 5 | useHook(() => {
| ^^^^^^^
> 6 | cache.set('key', 'value');
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 7 | });
| ^^^^ InvalidReact: This argument is a function which modifies local variables when called, which can bypass memoization and cause the UI not to update. Functions that are returned from hooks, passed as arguments to hooks, or passed as props to components may not mutate local variables (5:7)
InvalidReact: The function modifies a local variable here (6:6)
8 | }
9 |
```

View File

@@ -0,0 +1,8 @@
// @validateNoFreezingKnownMutableFunctions
function useFoo() {
const cache = new Map();
useHook(() => {
cache.set('key', 'value');
});
}

View File

@@ -29,7 +29,7 @@ function Component(props) {
> 6 | // deps are optional
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 7 | }, [props.items?.edges?.nodes]);
| ^^^^ CannotPreserveMemoization: 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 (3:7)
| ^^^^ CannotPreserveMemoization: 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 `props.items.edges.nodes`, but the source dependencies were [props.items?.edges?.nodes]. Inferred different dependency than source (3:7)
8 | return <Foo data={data} />;
9 | }
10 |

View File

@@ -0,0 +1,30 @@
## Input
```javascript
// @validateNoFreezingKnownMutableFunctions
function Component() {
const cache = new Map();
const fn = () => {
cache.set('key', 'value');
};
return <Foo fn={fn} />;
}
```
## Error
```
5 | cache.set('key', 'value');
6 | };
> 7 | return <Foo fn={fn} />;
| ^^ InvalidReact: This argument is a function which modifies local variables when called, which can bypass memoization and cause the UI not to update. Functions that are returned from hooks, passed as arguments to hooks, or passed as props to components may not mutate local variables (7:7)
InvalidReact: The function modifies a local variable here (5:5)
8 | }
9 |
```

View File

@@ -0,0 +1,8 @@
// @validateNoFreezingKnownMutableFunctions
function Component() {
const cache = new Map();
const fn = () => {
cache.set('key', 'value');
};
return <Foo fn={fn} />;
}

View File

@@ -0,0 +1,36 @@
## Input
```javascript
// @validateNoFreezingKnownMutableFunctions
import {useHook} from 'shared-runtime';
function useFoo() {
useHook(); // for inference to kick in
const cache = new Map();
return () => {
cache.set('key', 'value');
};
}
```
## Error
```
5 | useHook(); // for inference to kick in
6 | const cache = new Map();
> 7 | return () => {
| ^^^^^^^
> 8 | cache.set('key', 'value');
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 9 | };
| ^^^^ InvalidReact: This argument is a function which modifies local variables when called, which can bypass memoization and cause the UI not to update. Functions that are returned from hooks, passed as arguments to hooks, or passed as props to components may not mutate local variables (7:9)
InvalidReact: The function modifies a local variable here (8:8)
10 | }
11 |
```

View File

@@ -0,0 +1,10 @@
// @validateNoFreezingKnownMutableFunctions
import {useHook} from 'shared-runtime';
function useFoo() {
useHook(); // for inference to kick in
const cache = new Map();
return () => {
cache.set('key', 'value');
};
}

View File

@@ -38,7 +38,7 @@ export const FIXTURE_ENTRYPOINT = {
> 12 | Ref.current?.click();
| ^^^^^^^^^^^^^^^^^^^^^^^^^
> 13 | }, []);
| ^^^^ CannotPreserveMemoization: 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 (11:13)
| ^^^^ CannotPreserveMemoization: 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 `Ref.current`, but the source dependencies were []. Inferred dependency not present in source (11:13)
14 |
15 | return <button onClick={onClick} />;
16 | }

View File

@@ -38,7 +38,7 @@ export const FIXTURE_ENTRYPOINT = {
> 12 | notaref.current?.click();
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 13 | }, []);
| ^^^^ CannotPreserveMemoization: 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 (11:13)
| ^^^^ CannotPreserveMemoization: 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 `notaref.current`, but the source dependencies were []. Inferred dependency not present in source (11:13)
14 |
15 | return <button onClick={onClick} />;
16 | }

View File

@@ -0,0 +1,43 @@
## Input
```javascript
import {Stringify} from 'shared-runtime';
/**
* Fixture currently fails with
* Found differences in evaluator results
* Non-forget (expected):
* (kind: ok) <div>{"result":{"value":2},"fn":{"kind":"Function","result":{"value":2}},"shouldInvokeFns":true}</div>
* Forget:
* (kind: exception) bar is not a function
*/
function Foo({value}) {
const result = bar();
function bar() {
return {value};
}
return <Stringify result={result} fn={bar} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{value: 2}],
};
```
## Error
```
10 | */
11 | function Foo({value}) {
> 12 | const result = bar();
| ^^^ Todo: [PruneHoistedContexts] Rewrite hoisted function references (12:12)
13 | function bar() {
14 | return {value};
15 | }
```

View File

@@ -0,0 +1,46 @@
## Input
```javascript
import {Stringify} from 'shared-runtime';
/**
* Also see error.todo-functiondecl-hoisting.tsx which shows *invalid*
* compilation cases.
*
* This bailout specifically is a false positive for since this function's only
* reference-before-definition are within other functions which are not invoked.
*/
function Foo() {
'use memo';
function foo() {
return bar();
}
function bar() {
return 42;
}
return <Stringify fn1={foo} fn2={bar} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [],
};
```
## Error
```
13 | return bar();
14 | }
> 15 | function bar() {
| ^^^ Todo: [PruneHoistedContexts] Rewrite hoisted function references (15:15)
16 | return 42;
17 | }
18 |
```

View File

@@ -0,0 +1,25 @@
import {Stringify} from 'shared-runtime';
/**
* Also see error.todo-functiondecl-hoisting.tsx which shows *invalid*
* compilation cases.
*
* This bailout specifically is a false positive for since this function's only
* reference-before-definition are within other functions which are not invoked.
*/
function Foo() {
'use memo';
function foo() {
return bar();
}
function bar() {
return 42;
}
return <Stringify fn1={foo} fn2={bar} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [],
};

View File

@@ -0,0 +1,56 @@
## Input
```javascript
import fbt from 'fbt';
import {Stringify} from 'shared-runtime';
/**
* MemoizeFbtAndMacroOperands needs to account for nested fbt calls.
* Expected fixture `fbt-param-call-arguments` to succeed but it failed with error:
* /fbt-param-call-arguments.ts: Line 19 Column 11: fbt: unsupported babel node: Identifier
* ---
* t3
* ---
*/
function Component({firstname, lastname}) {
'use memo';
return (
<Stringify>
{fbt(
[
'Name: ',
fbt.param('firstname', <Stringify key={0} name={firstname} />),
', ',
fbt.param(
'lastname',
<Stringify key={0} name={lastname}>
{fbt('(inner fbt)', 'Inner fbt value')}
</Stringify>
),
],
'Name'
)}
</Stringify>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{firstname: 'first', lastname: 'last'}],
sequentialRenders: [{firstname: 'first', lastname: 'last'}],
};
```
## Error
```
Line 19 Column 11: fbt: unsupported babel node: Identifier
---
t3
---
```

View File

@@ -0,0 +1,38 @@
import fbt from 'fbt';
import {Stringify} from 'shared-runtime';
/**
* MemoizeFbtAndMacroOperands needs to account for nested fbt calls.
* Expected fixture `fbt-param-call-arguments` to succeed but it failed with error:
* /fbt-param-call-arguments.ts: Line 19 Column 11: fbt: unsupported babel node: Identifier
* ---
* t3
* ---
*/
function Component({firstname, lastname}) {
'use memo';
return (
<Stringify>
{fbt(
[
'Name: ',
fbt.param('firstname', <Stringify key={0} name={firstname} />),
', ',
fbt.param(
'lastname',
<Stringify key={0} name={lastname}>
{fbt('(inner fbt)', 'Inner fbt value')}
</Stringify>
),
],
'Name'
)}
</Stringify>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{firstname: 'first', lastname: 'last'}],
sequentialRenders: [{firstname: 'first', lastname: 'last'}],
};

View File

@@ -2,13 +2,22 @@
## Input
```javascript
import {Stringify, useIdentity} from 'shared-runtime';
function Component() {
const data = useData();
const data = useIdentity(
new Map([
[0, 'value0'],
[1, 'value1'],
])
);
const items = [];
// NOTE: `i` is a context variable because it's reassigned and also referenced
// within a closure, the `onClick` handler of each item
for (let i = MIN; i <= MAX; i += INCREMENT) {
items.push(<div key={i} onClick={() => data.set(i)} />);
items.push(
<Stringify key={i} onClick={() => data.get(i)} shouldInvokeFns={true} />
);
}
return <>{items}</>;
}
@@ -17,10 +26,6 @@ const MIN = 0;
const MAX = 3;
const INCREMENT = 1;
function useData() {
return new Map();
}
export const FIXTURE_ENTRYPOINT = {
params: [],
fn: Component,
@@ -32,41 +37,47 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
import { Stringify, useIdentity } from "shared-runtime";
function Component() {
const $ = _c(2);
const data = useData();
const $ = _c(3);
let t0;
if ($[0] !== data) {
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = new Map([
[0, "value0"],
[1, "value1"],
]);
$[0] = t0;
} else {
t0 = $[0];
}
const data = useIdentity(t0);
let t1;
if ($[1] !== data) {
const items = [];
for (let i = MIN; i <= MAX; i = i + INCREMENT, i) {
items.push(<div key={i} onClick={() => data.set(i)} />);
items.push(
<Stringify
key={i}
onClick={() => data.get(i)}
shouldInvokeFns={true}
/>,
);
}
t0 = <>{items}</>;
$[0] = data;
$[1] = t0;
t1 = <>{items}</>;
$[1] = data;
$[2] = t1;
} else {
t0 = $[1];
t1 = $[2];
}
return t0;
return t1;
}
const MIN = 0;
const MAX = 3;
const INCREMENT = 1;
function useData() {
const $ = _c(1);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = new Map();
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}
export const FIXTURE_ENTRYPOINT = {
params: [],
fn: Component,
@@ -75,4 +86,4 @@ export const FIXTURE_ENTRYPOINT = {
```
### Eval output
(kind: ok) <div></div><div></div><div></div><div></div>
(kind: ok) <div>{"onClick":{"kind":"Function","result":"value0"},"shouldInvokeFns":true}</div><div>{"onClick":{"kind":"Function","result":"value1"},"shouldInvokeFns":true}</div><div>{"onClick":{"kind":"Function"},"shouldInvokeFns":true}</div><div>{"onClick":{"kind":"Function"},"shouldInvokeFns":true}</div>

View File

@@ -1,10 +1,19 @@
import {Stringify, useIdentity} from 'shared-runtime';
function Component() {
const data = useData();
const data = useIdentity(
new Map([
[0, 'value0'],
[1, 'value1'],
])
);
const items = [];
// NOTE: `i` is a context variable because it's reassigned and also referenced
// within a closure, the `onClick` handler of each item
for (let i = MIN; i <= MAX; i += INCREMENT) {
items.push(<div key={i} onClick={() => data.set(i)} />);
items.push(
<Stringify key={i} onClick={() => data.get(i)} shouldInvokeFns={true} />
);
}
return <>{items}</>;
}
@@ -13,10 +22,6 @@ const MIN = 0;
const MAX = 3;
const INCREMENT = 1;
function useData() {
return new Map();
}
export const FIXTURE_ENTRYPOINT = {
params: [],
fn: Component,

View File

@@ -0,0 +1,82 @@
## Input
```javascript
import {CONST_TRUE, useIdentity} from 'shared-runtime';
const hidden = CONST_TRUE;
function useFoo() {
const makeCb = useIdentity(() => {
const logIntervalId = () => {
log(intervalId);
};
let intervalId;
if (!hidden) {
intervalId = 2;
}
return () => {
logIntervalId();
};
});
return <Stringify fn={makeCb()} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { CONST_TRUE, useIdentity } from "shared-runtime";
const hidden = CONST_TRUE;
function useFoo() {
const $ = _c(4);
const makeCb = useIdentity(_temp);
let t0;
if ($[0] !== makeCb) {
t0 = makeCb();
$[0] = makeCb;
$[1] = t0;
} else {
t0 = $[1];
}
let t1;
if ($[2] !== t0) {
t1 = <Stringify fn={t0} shouldInvokeFns={true} />;
$[2] = t0;
$[3] = t1;
} else {
t1 = $[3];
}
return t1;
}
function _temp() {
const logIntervalId = () => {
log(intervalId);
};
let intervalId;
if (!hidden) {
intervalId = 2;
}
return () => {
logIntervalId();
};
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [],
};
```
### Eval output
(kind: exception) Stringify is not defined

View File

@@ -0,0 +1,25 @@
import {CONST_TRUE, useIdentity} from 'shared-runtime';
const hidden = CONST_TRUE;
function useFoo() {
const makeCb = useIdentity(() => {
const logIntervalId = () => {
log(intervalId);
};
let intervalId;
if (!hidden) {
intervalId = 2;
}
return () => {
logIntervalId();
};
});
return <Stringify fn={makeCb()} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [],
};

View File

@@ -30,8 +30,7 @@ function Foo() {
getX = () => x;
console.log(getX());
let x;
x = 4;
let x = 4;
x = x + 5;
$[0] = getX;
} else {

View File

@@ -0,0 +1,64 @@
## Input
```javascript
import {CONST_NUMBER1, Stringify} from 'shared-runtime';
function useHook({cond}) {
'use memo';
const getX = () => x;
let x;
if (cond) {
x = CONST_NUMBER1;
}
return <Stringify getX={getX} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: () => {},
params: [{cond: true}],
sequentialRenders: [{cond: true}, {cond: true}, {cond: false}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { CONST_NUMBER1, Stringify } from "shared-runtime";
function useHook(t0) {
"use memo";
const $ = _c(2);
const { cond } = t0;
let t1;
if ($[0] !== cond) {
const getX = () => x;
let x;
if (cond) {
x = CONST_NUMBER1;
}
t1 = <Stringify getX={getX} shouldInvokeFns={true} />;
$[0] = cond;
$[1] = t1;
} else {
t1 = $[1];
}
return t1;
}
export const FIXTURE_ENTRYPOINT = {
fn: () => {},
params: [{ cond: true }],
sequentialRenders: [{ cond: true }, { cond: true }, { cond: false }],
};
```
### Eval output
(kind: ok)

View File

@@ -0,0 +1,18 @@
import {CONST_NUMBER1, Stringify} from 'shared-runtime';
function useHook({cond}) {
'use memo';
const getX = () => x;
let x;
if (cond) {
x = CONST_NUMBER1;
}
return <Stringify getX={getX} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: () => {},
params: [{cond: true}],
sequentialRenders: [{cond: true}, {cond: true}, {cond: false}],
};

View File

@@ -36,8 +36,7 @@ function hoisting(cond) {
items.push(bar());
};
let bar;
bar = _temp;
let bar = _temp;
foo();
}
$[0] = cond;

View File

@@ -41,11 +41,9 @@ function hoisting() {
return result;
};
let foo;
foo = () => bar + baz;
let foo = () => bar + baz;
let bar;
bar = 3;
let bar = 3;
const baz = 2;
t0 = qux();
$[0] = t0;

View File

@@ -37,8 +37,7 @@ function useHook(t0) {
if ($[0] !== cond) {
const getX = () => x;
let x;
x = CONST_NUMBER0;
let x = CONST_NUMBER0;
if (cond) {
x = x + CONST_NUMBER1;
x;

View File

@@ -38,8 +38,7 @@ function useHook(t0) {
if ($[0] !== cond) {
const getX = () => x;
let x;
x = CONST_NUMBER0;
let x = CONST_NUMBER0;
if (cond) {
x = x + CONST_NUMBER1;
x;

View File

@@ -29,10 +29,8 @@ function hoisting() {
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
foo = () => bar + baz;
let bar;
bar = 3;
let baz;
baz = 2;
let bar = 3;
let baz = 2;
$[0] = foo;
} else {
foo = $[0];

View File

@@ -0,0 +1,129 @@
## Input
```javascript
import {Stringify, useIdentity} from 'shared-runtime';
function Component({prop1, prop2}) {
'use memo';
const data = useIdentity(
new Map([
[0, 'value0'],
[1, 'value1'],
])
);
let i = 0;
const items = [];
items.push(
<Stringify
key={i}
onClick={() => data.get(i) + prop1}
shouldInvokeFns={true}
/>
);
i = i + 1;
items.push(
<Stringify
key={i}
onClick={() => data.get(i) + prop2}
shouldInvokeFns={true}
/>
);
return <>{items}</>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{prop1: 'prop1', prop2: 'prop2'}],
sequentialRenders: [
{prop1: 'prop1', prop2: 'prop2'},
{prop1: 'prop1', prop2: 'prop2'},
{prop1: 'changed', prop2: 'prop2'},
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { Stringify, useIdentity } from "shared-runtime";
function Component(t0) {
"use memo";
const $ = _c(12);
const { prop1, prop2 } = t0;
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = new Map([
[0, "value0"],
[1, "value1"],
]);
$[0] = t1;
} else {
t1 = $[0];
}
const data = useIdentity(t1);
let t2;
if ($[1] !== data || $[2] !== prop1 || $[3] !== prop2) {
let i = 0;
const items = [];
items.push(
<Stringify
key={i}
onClick={() => data.get(i) + prop1}
shouldInvokeFns={true}
/>,
);
i = i + 1;
const t3 = i;
let t4;
if ($[5] !== data || $[6] !== i || $[7] !== prop2) {
t4 = () => data.get(i) + prop2;
$[5] = data;
$[6] = i;
$[7] = prop2;
$[8] = t4;
} else {
t4 = $[8];
}
let t5;
if ($[9] !== t3 || $[10] !== t4) {
t5 = <Stringify key={t3} onClick={t4} shouldInvokeFns={true} />;
$[9] = t3;
$[10] = t4;
$[11] = t5;
} else {
t5 = $[11];
}
items.push(t5);
t2 = <>{items}</>;
$[1] = data;
$[2] = prop1;
$[3] = prop2;
$[4] = t2;
} else {
t2 = $[4];
}
return t2;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ prop1: "prop1", prop2: "prop2" }],
sequentialRenders: [
{ prop1: "prop1", prop2: "prop2" },
{ prop1: "prop1", prop2: "prop2" },
{ prop1: "changed", prop2: "prop2" },
],
};
```
### Eval output
(kind: ok) <div>{"onClick":{"kind":"Function","result":"value1prop1"},"shouldInvokeFns":true}</div><div>{"onClick":{"kind":"Function","result":"value1prop2"},"shouldInvokeFns":true}</div>
<div>{"onClick":{"kind":"Function","result":"value1prop1"},"shouldInvokeFns":true}</div><div>{"onClick":{"kind":"Function","result":"value1prop2"},"shouldInvokeFns":true}</div>
<div>{"onClick":{"kind":"Function","result":"value1changed"},"shouldInvokeFns":true}</div><div>{"onClick":{"kind":"Function","result":"value1prop2"},"shouldInvokeFns":true}</div>

View File

@@ -0,0 +1,40 @@
import {Stringify, useIdentity} from 'shared-runtime';
function Component({prop1, prop2}) {
'use memo';
const data = useIdentity(
new Map([
[0, 'value0'],
[1, 'value1'],
])
);
let i = 0;
const items = [];
items.push(
<Stringify
key={i}
onClick={() => data.get(i) + prop1}
shouldInvokeFns={true}
/>
);
i = i + 1;
items.push(
<Stringify
key={i}
onClick={() => data.get(i) + prop2}
shouldInvokeFns={true}
/>
);
return <>{items}</>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{prop1: 'prop1', prop2: 'prop2'}],
sequentialRenders: [
{prop1: 'prop1', prop2: 'prop2'},
{prop1: 'prop1', prop2: 'prop2'},
{prop1: 'changed', prop2: 'prop2'},
],
};

View File

@@ -11,6 +11,7 @@ function Component() {
<Text value={'Lauren'} />
<Text value={'சத்யா'} />
<Text value={'Sathya'} />
<Text value={'welcome 👋'} />
</div>
);
}
@@ -42,6 +43,7 @@ function Component() {
<Text value="Lauren" />
<Text value={"\u0B9A\u0BA4\u0BCD\u0BAF\u0BBE"} />
<Text value="Sathya" />
<Text value={"welcome \uD83D\uDC4B"} />
</div>
);
$[0] = t0;
@@ -74,4 +76,4 @@ export const FIXTURE_ENTRYPOINT = {
### Eval output
(kind: ok) <div><span>
</span><span>A E</span><span>나은</span><span>Lauren</span><span>சத்யா</span><span>Sathya</span></div>
</span><span>A E</span><span>나은</span><span>Lauren</span><span>சத்யா</span><span>Sathya</span><span>welcome 👋</span></div>

View File

@@ -7,6 +7,7 @@ function Component() {
<Text value={'Lauren'} />
<Text value={'சத்யா'} />
<Text value={'Sathya'} />
<Text value={'welcome 👋'} />
</div>
);
}

View File

@@ -37,8 +37,7 @@ function Component() {
}
const x = t0;
let x_0;
x_0 = 56;
let x_0 = 56;
const fn = function () {
x_0 = 42;
};

View File

@@ -33,8 +33,7 @@ function component(a) {
m(x);
};
let x;
x = { a };
let x = { a };
m(x);
$[0] = a;
$[1] = y;

View File

@@ -41,7 +41,7 @@ export const FIXTURE_ENTRYPOINT = {
> 10 | }
| ^^^^^^^^^^^^^^^^
> 11 | }, [propA, propB.x.y]);
| ^^^^ CannotPreserveMemoization: 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 (5:11)
| ^^^^ CannotPreserveMemoization: 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 `propB`, but the source dependencies were [propA, propB.x.y]. Inferred less specific property than source (5:11)
12 | }
13 |
14 | export const FIXTURE_ENTRYPOINT = {

View File

@@ -48,9 +48,9 @@ export const FIXTURE_ENTRYPOINT = {
> 13 | }
| ^^^^^^^^^^^^^^^^^
> 14 | }, [propA.a, propB.x.y]);
| ^^^^ CannotPreserveMemoization: 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 (6:14)
| ^^^^ CannotPreserveMemoization: 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 `propA`, but the source dependencies were [propA.a, propB.x.y]. Inferred less specific property than source (6:14)
CannotPreserveMemoization: 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 (6:14)
CannotPreserveMemoization: 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 `propB`, but the source dependencies were [propA.a, propB.x.y]. Inferred less specific property than source (6:14)
15 | }
16 |
17 | export const FIXTURE_ENTRYPOINT = {

View File

@@ -24,7 +24,7 @@ function useHook(maybeRef) {
> 6 | return [maybeRef.current];
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 7 | }, [maybeRef]);
| ^^^^ CannotPreserveMemoization: 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 (5:7)
| ^^^^ CannotPreserveMemoization: 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 `maybeRef.current`, but the source dependencies were [maybeRef]. Differences in ref.current access (5:7)
8 | }
9 |
```

View File

@@ -24,7 +24,7 @@ function useHook(maybeRef, shouldRead) {
> 6 | return () => [maybeRef.current];
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 7 | }, [shouldRead, maybeRef]);
| ^^^^ CannotPreserveMemoization: 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 (5:7)
| ^^^^ CannotPreserveMemoization: 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 `maybeRef.current`, but the source dependencies were [shouldRead, maybeRef]. Differences in ref.current access (5:7)
8 | }
9 |
```

View File

@@ -39,7 +39,7 @@ export const FIXTURE_ENTRYPOINT = {
> 12 | }
| ^^^^^^^^^^^^^^^^^^^^^^
> 13 | }, []);
| ^^^^ CannotPreserveMemoization: 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 (9:13)
| ^^^^ CannotPreserveMemoization: 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 `ref`, but the source dependencies were []. Inferred dependency not present in source (9:13)
14 | }
15 |
16 | export const FIXTURE_ENTRYPOINT = {

View File

@@ -22,7 +22,7 @@ function useHook(x) {
7 | const aliasedProp = x.y.z;
8 |
> 9 | return useCallback(() => [aliasedX, x.y.z], [x, aliasedProp]);
| ^^^^^^^^^^^^^^^^^^^^^^^ CannotPreserveMemoization: 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 (9:9)
| ^^^^^^^^^^^^^^^^^^^^^^^ CannotPreserveMemoization: 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 `aliasedX`, but the source dependencies were [x, aliasedProp]. Inferred different dependency than source (9:9)
10 | }
11 |
```

View File

@@ -38,7 +38,7 @@ export const FIXTURE_ENTRYPOINT = {
> 9 | };
| ^^^^^^^^^^^^
> 10 | }, [propA, propB.x.y]);
| ^^^^ CannotPreserveMemoization: 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 (5:10)
| ^^^^ CannotPreserveMemoization: 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 `propB?.x.y`, but the source dependencies were [propA, propB.x.y]. Inferred different dependency than source (5:10)
11 | }
12 |
13 | export const FIXTURE_ENTRYPOINT = {

View File

@@ -43,7 +43,7 @@ function Component({propA, propB}) {
> 13 | }
| ^^^^^^^^^^^^^^^^^
> 14 | }, [propA?.a, propB.x.y]);
| ^^^^ CannotPreserveMemoization: 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 (6:14)
| ^^^^ CannotPreserveMemoization: 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 `propB`, but the source dependencies were [propA?.a, propB.x.y]. Inferred less specific property than source (6:14)
15 | }
16 |
```

View File

@@ -24,7 +24,7 @@ function Component({propA}) {
> 6 | return propA.x();
| ^^^^^^^^^^^^^^^^^^^^^
> 7 | }, [propA.x]);
| ^^^^ CannotPreserveMemoization: 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 (5:7)
| ^^^^ CannotPreserveMemoization: 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 `propA`, but the source dependencies were [propA.x]. Inferred less specific property than source (5:7)
8 | }
9 |
```

View File

@@ -22,7 +22,7 @@ function useHook(x) {
7 | const aliasedProp = x.y.z;
8 |
> 9 | return useMemo(() => [x, x.y.z], [aliasedX, aliasedProp]);
| ^^^^^^^^^^^^^^^^ CannotPreserveMemoization: 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 (9:9)
| ^^^^^^^^^^^^^^^^ CannotPreserveMemoization: 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 `x`, but the source dependencies were [aliasedX, aliasedProp]. Inferred different dependency than source (9:9)
10 | }
11 |
```

View File

@@ -43,7 +43,7 @@ function Component({propA, propB}) {
> 13 | }
| ^^^^^^^^^^^^^^^^^
> 14 | }, [propA?.a, propB.x.y]);
| ^^^^ CannotPreserveMemoization: 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 (6:14)
| ^^^^ CannotPreserveMemoization: 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 `propB`, but the source dependencies were [propA?.a, propB.x.y]. Inferred less specific property than source (6:14)
15 | }
16 |
```

View File

@@ -43,9 +43,9 @@ function Component({propA, propB}) {
> 13 | }
| ^^^^^^^^^^^^^^^^^
> 14 | }, [propA.a, propB.x.y]);
| ^^^^ CannotPreserveMemoization: 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 (6:14)
| ^^^^ CannotPreserveMemoization: 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 `propA`, but the source dependencies were [propA.a, propB.x.y]. Inferred less specific property than source (6:14)
CannotPreserveMemoization: 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 (6:14)
CannotPreserveMemoization: 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 `propB`, but the source dependencies were [propA.a, propB.x.y]. Inferred less specific property than source (6:14)
15 | }
16 |
```

View File

@@ -30,7 +30,7 @@ function Component({propA}) {
> 8 | };
| ^^^^^^^^^^^^
> 9 | }, [propA.x]);
| ^^^^ CannotPreserveMemoization: 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 (5:9)
| ^^^^ CannotPreserveMemoization: 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 `propA`, but the source dependencies were [propA.x]. Inferred less specific property than source (5:9)
10 | }
11 |
```

View File

@@ -24,7 +24,7 @@ function Component({propA}) {
> 6 | return propA.x();
| ^^^^^^^^^^^^^^^^^^^^^
> 7 | }, [propA.x]);
| ^^^^ CannotPreserveMemoization: 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 (5:7)
| ^^^^ CannotPreserveMemoization: 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 `propA`, but the source dependencies were [propA.x]. Inferred less specific property than source (5:7)
8 | }
9 |
```

View File

@@ -37,7 +37,7 @@ function useFoo(input1) {
> 17 | return [y];
| ^^^^^^^^^^^^^^^
> 18 | }, [(mutate(x), y)]);
| ^^^^ CannotPreserveMemoization: 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 (16:18)
| ^^^^ CannotPreserveMemoization: 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 `input1`, but the source dependencies were [y]. Inferred different dependency than source (16:18)
19 |
20 | return [x, memoized];
21 | }

View File

@@ -65,8 +65,7 @@ function useBar(t0, cond) {
} else {
t1 = $[0];
}
let x;
x = useIdentity(t1);
let x = useIdentity(t1);
if (cond) {
x = b;
}

View File

@@ -47,8 +47,7 @@ function Foo(t0) {
if ($[0] !== arr1 || $[1] !== arr2 || $[2] !== foo) {
const x = [arr1];
let y;
y = [];
let y = [];
getVal1 = _temp;

View File

@@ -47,8 +47,7 @@ function Foo(t0) {
if ($[0] !== arr1 || $[1] !== arr2 || $[2] !== foo) {
const x = [arr1];
let y;
y = [];
let y = [];
let t2;
let t3;
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {

View File

@@ -41,7 +41,7 @@ function Component(props) {
> 10 | return x;
| ^^^^^^^^^^^^^^^^^
> 11 | }, [props?.items, props.cond]);
| ^^^^ CannotPreserveMemoization: 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 (4:11)
| ^^^^ CannotPreserveMemoization: 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 `props.items`, but the source dependencies were [props?.items, props.cond]. Inferred different dependency than source (4:11)
12 | return (
13 | <ValidateMemoization inputs={[props?.items, props.cond]} output={data} />
14 | );

View File

@@ -41,7 +41,7 @@ function Component(props) {
> 10 | return x;
| ^^^^^^^^^^^^^^^^^
> 11 | }, [props?.items, props.cond]);
| ^^^^ CannotPreserveMemoization: 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 (4:11)
| ^^^^ CannotPreserveMemoization: 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 `props.items`, but the source dependencies were [props?.items, props.cond]. Inferred different dependency than source (4:11)
12 | return (
13 | <ValidateMemoization inputs={[props?.items, props.cond]} output={data} />
14 | );

View File

@@ -0,0 +1,30 @@
## Input
```javascript
// @panicThreshold(none)
'use no memo';
function Foo() {
return <button onClick={() => alert('hello!')}>Click me!</button>;
}
```
## Code
```javascript
// @panicThreshold(none)
"use no memo";
function Foo() {
return <button onClick={() => alert("hello!")}>Click me!</button>;
}
function _temp() {
return alert("hello!");
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -0,0 +1,6 @@
// @panicThreshold(none)
'use no memo';
function Foo() {
return <button onClick={() => alert('hello!')}>Click me!</button>;
}

View File

@@ -0,0 +1,108 @@
## Input
```javascript
import {useState, useEffect} from 'react';
import {invoke, Stringify} from 'shared-runtime';
function Content() {
const [announcement, setAnnouncement] = useState('');
const [users, setUsers] = useState([{name: 'John Doe'}, {name: 'Jane Doe'}]);
// This was originally passed down as an onClick, but React Compiler's test
// evaluator doesn't yet support events outside of React
useEffect(() => {
if (users.length === 2) {
let removedUserName = '';
setUsers(prevUsers => {
const newUsers = [...prevUsers];
removedUserName = newUsers.at(-1).name;
newUsers.pop();
return newUsers;
});
setAnnouncement(`Removed user (${removedUserName})`);
}
}, [users]);
return <Stringify users={users} announcement={announcement} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Content,
params: [{}],
sequentialRenders: [{}, {}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { useState, useEffect } from "react";
import { invoke, Stringify } from "shared-runtime";
function Content() {
const $ = _c(8);
const [announcement, setAnnouncement] = useState("");
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = [{ name: "John Doe" }, { name: "Jane Doe" }];
$[0] = t0;
} else {
t0 = $[0];
}
const [users, setUsers] = useState(t0);
let t1;
if ($[1] !== users.length) {
t1 = () => {
if (users.length === 2) {
let removedUserName = "";
setUsers((prevUsers) => {
const newUsers = [...prevUsers];
removedUserName = newUsers.at(-1).name;
newUsers.pop();
return newUsers;
});
setAnnouncement(`Removed user (${removedUserName})`);
}
};
$[1] = users.length;
$[2] = t1;
} else {
t1 = $[2];
}
let t2;
if ($[3] !== users) {
t2 = [users];
$[3] = users;
$[4] = t2;
} else {
t2 = $[4];
}
useEffect(t1, t2);
let t3;
if ($[5] !== announcement || $[6] !== users) {
t3 = <Stringify users={users} announcement={announcement} />;
$[5] = announcement;
$[6] = users;
$[7] = t3;
} else {
t3 = $[7];
}
return t3;
}
export const FIXTURE_ENTRYPOINT = {
fn: Content,
params: [{}],
sequentialRenders: [{}, {}],
};
```
### Eval output
(kind: ok) <div>{"users":[{"name":"John Doe"}],"announcement":"Removed user (Jane Doe)"}</div>
<div>{"users":[{"name":"John Doe"}],"announcement":"Removed user (Jane Doe)"}</div>

View File

@@ -0,0 +1,31 @@
import {useState, useEffect} from 'react';
import {invoke, Stringify} from 'shared-runtime';
function Content() {
const [announcement, setAnnouncement] = useState('');
const [users, setUsers] = useState([{name: 'John Doe'}, {name: 'Jane Doe'}]);
// This was originally passed down as an onClick, but React Compiler's test
// evaluator doesn't yet support events outside of React
useEffect(() => {
if (users.length === 2) {
let removedUserName = '';
setUsers(prevUsers => {
const newUsers = [...prevUsers];
removedUserName = newUsers.at(-1).name;
newUsers.pop();
return newUsers;
});
setAnnouncement(`Removed user (${removedUserName})`);
}
}, [users]);
return <Stringify users={users} announcement={announcement} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Content,
params: [{}],
sequentialRenders: [{}, {}],
};

View File

@@ -0,0 +1,67 @@
## Input
```javascript
function useHook(nodeID, condition) {
const graph = useContext(GraphContext);
const node = nodeID != null ? graph[nodeID] : null;
for (const key of Object.keys(node?.fields ?? {})) {
if (condition) {
return new Class(node.fields?.[field]);
}
}
return new Class();
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
function useHook(nodeID, condition) {
const $ = _c(7);
const graph = useContext(GraphContext);
const node = nodeID != null ? graph[nodeID] : null;
let t0;
if ($[0] !== node?.fields) {
t0 = Object.keys(node?.fields ?? {});
$[0] = node?.fields;
$[1] = t0;
} else {
t0 = $[1];
}
let t1;
if ($[2] !== condition || $[3] !== node || $[4] !== t0) {
t1 = Symbol.for("react.early_return_sentinel");
bb0: for (const key of t0) {
if (condition) {
t1 = new Class(node.fields?.[field]);
break bb0;
}
}
$[2] = condition;
$[3] = node;
$[4] = t0;
$[5] = t1;
} else {
t1 = $[5];
}
if (t1 !== Symbol.for("react.early_return_sentinel")) {
return t1;
}
let t2;
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
t2 = new Class();
$[6] = t2;
} else {
t2 = $[6];
}
return t2;
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -0,0 +1,11 @@
function useHook(nodeID, condition) {
const graph = useContext(GraphContext);
const node = nodeID != null ? graph[nodeID] : null;
for (const key of Object.keys(node?.fields ?? {})) {
if (condition) {
return new Class(node.fields?.[field]);
}
}
return new Class();
}

View File

@@ -0,0 +1,92 @@
## Input
```javascript
import {Stringify} from 'shared-runtime';
/**
* Example showing that returned inner function expressions should not be
* typed with `freeze` effects.
*/
function Foo({a, b}) {
'use memo';
const obj = {};
const updaterFactory = () => {
/**
* This returned function expression *is* a local value. But it might (1)
* capture and mutate its context environment and (2) be called during
* render.
* Typing it with `freeze` effects would be incorrect as it would mean
* inferring that calls to updaterFactory()() do not mutate its captured
* context.
*/
return newValue => {
obj.value = newValue;
obj.a = a;
};
};
const updater = updaterFactory();
updater(b);
return <Stringify cb={obj} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{a: 1, b: 2}],
sequentialRenders: [
{a: 1, b: 2},
{a: 1, b: 3},
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { Stringify } from "shared-runtime";
/**
* Example showing that returned inner function expressions should not be
* typed with `freeze` effects.
*/
function Foo(t0) {
"use memo";
const $ = _c(3);
const { a, b } = t0;
let t1;
if ($[0] !== a || $[1] !== b) {
const obj = {};
const updaterFactory = () => (newValue) => {
obj.value = newValue;
obj.a = a;
};
const updater = updaterFactory();
updater(b);
t1 = <Stringify cb={obj} shouldInvokeFns={true} />;
$[0] = a;
$[1] = b;
$[2] = t1;
} else {
t1 = $[2];
}
return t1;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{ a: 1, b: 2 }],
sequentialRenders: [
{ a: 1, b: 2 },
{ a: 1, b: 3 },
],
};
```
### Eval output
(kind: ok) <div>{"cb":{"value":2,"a":1},"shouldInvokeFns":true}</div>
<div>{"cb":{"value":3,"a":1},"shouldInvokeFns":true}</div>

View File

@@ -0,0 +1,37 @@
import {Stringify} from 'shared-runtime';
/**
* Example showing that returned inner function expressions should not be
* typed with `freeze` effects.
*/
function Foo({a, b}) {
'use memo';
const obj = {};
const updaterFactory = () => {
/**
* This returned function expression *is* a local value. But it might (1)
* capture and mutate its context environment and (2) be called during
* render.
* Typing it with `freeze` effects would be incorrect as it would mean
* inferring that calls to updaterFactory()() do not mutate its captured
* context.
*/
return newValue => {
obj.value = newValue;
obj.a = a;
};
};
const updater = updaterFactory();
updater(b);
return <Stringify cb={obj} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{a: 1, b: 2}],
sequentialRenders: [
{a: 1, b: 2},
{a: 1, b: 3},
],
};

View File

@@ -0,0 +1,100 @@
## Input
```javascript
import {makeArray, Stringify, useIdentity} from 'shared-runtime';
/**
* Example showing that returned inner function expressions should not be
* typed with `freeze` effects.
* Also see repro-returned-inner-fn-mutates-context
*/
function Foo({b}) {
'use memo';
const fnFactory = () => {
/**
* This returned function expression *is* a local value. But it might (1)
* capture and mutate its context environment and (2) be called during
* render.
* Typing it with `freeze` effects would be incorrect as it would mean
* inferring that calls to updaterFactory()() do not mutate its captured
* context.
*/
return () => {
myVar = () => console.log('a');
};
};
let myVar = () => console.log('b');
useIdentity();
const fn = fnFactory();
const arr = makeArray(b);
fn(arr);
return <Stringify cb={myVar} value={arr} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{b: 1}],
sequentialRenders: [{b: 1}, {b: 2}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { makeArray, Stringify, useIdentity } from "shared-runtime";
/**
* Example showing that returned inner function expressions should not be
* typed with `freeze` effects.
* Also see repro-returned-inner-fn-mutates-context
*/
function Foo(t0) {
"use memo";
const $ = _c(3);
const { b } = t0;
const fnFactory = () => () => {
myVar = _temp;
};
let myVar = _temp2;
useIdentity();
const fn = fnFactory();
const arr = makeArray(b);
fn(arr);
let t1;
if ($[0] !== arr || $[1] !== myVar) {
t1 = <Stringify cb={myVar} value={arr} shouldInvokeFns={true} />;
$[0] = arr;
$[1] = myVar;
$[2] = t1;
} else {
t1 = $[2];
}
return t1;
}
function _temp2() {
return console.log("b");
}
function _temp() {
return console.log("a");
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{ b: 1 }],
sequentialRenders: [{ b: 1 }, { b: 2 }],
};
```
### Eval output
(kind: ok) <div>{"cb":{"kind":"Function"},"value":[1],"shouldInvokeFns":true}</div>
<div>{"cb":{"kind":"Function"},"value":[2],"shouldInvokeFns":true}</div>
logs: ['a','a']

View File

@@ -0,0 +1,37 @@
import {makeArray, Stringify, useIdentity} from 'shared-runtime';
/**
* Example showing that returned inner function expressions should not be
* typed with `freeze` effects.
* Also see repro-returned-inner-fn-mutates-context
*/
function Foo({b}) {
'use memo';
const fnFactory = () => {
/**
* This returned function expression *is* a local value. But it might (1)
* capture and mutate its context environment and (2) be called during
* render.
* Typing it with `freeze` effects would be incorrect as it would mean
* inferring that calls to updaterFactory()() do not mutate its captured
* context.
*/
return () => {
myVar = () => console.log('a');
};
};
let myVar = () => console.log('b');
useIdentity();
const fn = fnFactory();
const arr = makeArray(b);
fn(arr);
return <Stringify cb={myVar} value={arr} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{b: 1}],
sequentialRenders: [{b: 1}, {b: 2}],
};

View File

@@ -0,0 +1,77 @@
## Input
```javascript
// @flow
/**
* This hook returns a function that when called with an input object,
* will return the result of mapping that input with the supplied map
* function. Results are cached, so if the same input is passed again,
* the same output object will be returned.
*
* Note that this technically violates the rules of React and is unsafe:
* hooks must return immutable objects and be pure, and a function which
* captures and mutates a value when called is inherently not pure.
*
* However, in this case it is technically safe _if_ the mapping function
* is pure *and* the resulting objects are never modified. This is because
* the function only caches: the result of `returnedFunction(someInput)`
* strictly depends on `returnedFunction` and `someInput`, and cannot
* otherwise change over time.
*/
hook useMemoMap<TInput: interface {}, TOutput>(
map: TInput => TOutput
): TInput => TOutput {
return useMemo(() => {
// The original issue is that `cache` was not memoized together with the returned
// function. This was because neither appears to ever be mutated — the function
// is known to mutate `cache` but the function isn't called.
//
// The fix is to detect cases like this — functions that are mutable but not called -
// and ensure that their mutable captures are aliased together into the same scope.
const cache = new WeakMap<TInput, TOutput>();
return input => {
let output = cache.get(input);
if (output == null) {
output = map(input);
cache.set(input, output);
}
return output;
};
}, [map]);
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
function useMemoMap(map) {
const $ = _c(2);
let t0;
let t1;
if ($[0] !== map) {
const cache = new WeakMap();
t1 = (input) => {
let output = cache.get(input);
if (output == null) {
output = map(input);
cache.set(input, output);
}
return output;
};
$[0] = map;
$[1] = t1;
} else {
t1 = $[1];
}
t0 = t1;
return t0;
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -0,0 +1,38 @@
// @flow
/**
* This hook returns a function that when called with an input object,
* will return the result of mapping that input with the supplied map
* function. Results are cached, so if the same input is passed again,
* the same output object will be returned.
*
* Note that this technically violates the rules of React and is unsafe:
* hooks must return immutable objects and be pure, and a function which
* captures and mutates a value when called is inherently not pure.
*
* However, in this case it is technically safe _if_ the mapping function
* is pure *and* the resulting objects are never modified. This is because
* the function only caches: the result of `returnedFunction(someInput)`
* strictly depends on `returnedFunction` and `someInput`, and cannot
* otherwise change over time.
*/
hook useMemoMap<TInput: interface {}, TOutput>(
map: TInput => TOutput
): TInput => TOutput {
return useMemo(() => {
// The original issue is that `cache` was not memoized together with the returned
// function. This was because neither appears to ever be mutated — the function
// is known to mutate `cache` but the function isn't called.
//
// The fix is to detect cases like this — functions that are mutable but not called -
// and ensure that their mutable captures are aliased together into the same scope.
const cache = new WeakMap<TInput, TOutput>();
return input => {
let output = cache.get(input);
if (output == null) {
output = map(input);
cache.set(input, output);
}
return output;
};
}, [map]);
}

View File

@@ -0,0 +1,62 @@
## Input
```javascript
function useHook(nodeID, condition) {
const graph = useContext(GraphContext);
const node = nodeID != null ? graph[nodeID] : null;
// (2) Instead we can create a scope around the loop since the loop produces an escaping value
let value;
for (const key of Object.keys(node?.fields ?? {})) {
if (condition) {
// (1) We currently create a scope just for this instruction, then later prune the scope because
// it's inside a loop
value = new Class(node.fields?.[field]);
break;
}
}
return value;
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
function useHook(nodeID, condition) {
const $ = _c(6);
const graph = useContext(GraphContext);
const node = nodeID != null ? graph[nodeID] : null;
let value;
let t0;
if ($[0] !== node?.fields) {
t0 = Object.keys(node?.fields ?? {});
$[0] = node?.fields;
$[1] = t0;
} else {
t0 = $[1];
}
if ($[2] !== condition || $[3] !== node || $[4] !== t0) {
for (const key of t0) {
if (condition) {
value = new Class(node.fields?.[field]);
break;
}
}
$[2] = condition;
$[3] = node;
$[4] = t0;
$[5] = value;
} else {
value = $[5];
}
return value;
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -0,0 +1,16 @@
function useHook(nodeID, condition) {
const graph = useContext(GraphContext);
const node = nodeID != null ? graph[nodeID] : null;
// (2) Instead we can create a scope around the loop since the loop produces an escaping value
let value;
for (const key of Object.keys(node?.fields ?? {})) {
if (condition) {
// (1) We currently create a scope just for this instruction, then later prune the scope because
// it's inside a loop
value = new Class(node.fields?.[field]);
break;
}
}
return value;
}

View File

@@ -0,0 +1,122 @@
## Input
```javascript
import {useEffect, useState} from 'react';
/**
* Example of a function expression whose return value shouldn't have
* a "freeze" effect on all operands.
*
* This is because the function expression is passed to `useEffect` and
* thus is not a render function. `cleanedUp` is also created within
* the effect and is not a render variable.
*/
function Component({prop}) {
const [cleanupCount, setCleanupCount] = useState(0);
useEffect(() => {
let cleanedUp = false;
setTimeout(() => {
if (!cleanedUp) {
cleanedUp = true;
setCleanupCount(c => c + 1);
}
}, 0);
// This return value should not have freeze effects
// on its operands
return () => {
if (!cleanedUp) {
cleanedUp = true;
setCleanupCount(c => c + 1);
}
};
}, [prop]);
return <div>{cleanupCount}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{prop: 5}],
sequentialRenders: [{prop: 5}, {prop: 5}, {prop: 6}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { useEffect, useState } from "react";
/**
* Example of a function expression whose return value shouldn't have
* a "freeze" effect on all operands.
*
* This is because the function expression is passed to `useEffect` and
* thus is not a render function. `cleanedUp` is also created within
* the effect and is not a render variable.
*/
function Component(t0) {
const $ = _c(5);
const { prop } = t0;
const [cleanupCount, setCleanupCount] = useState(0);
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = () => {
let cleanedUp = false;
setTimeout(() => {
if (!cleanedUp) {
cleanedUp = true;
setCleanupCount(_temp);
}
}, 0);
return () => {
if (!cleanedUp) {
cleanedUp = true;
setCleanupCount(_temp2);
}
};
};
$[0] = t1;
} else {
t1 = $[0];
}
let t2;
if ($[1] !== prop) {
t2 = [prop];
$[1] = prop;
$[2] = t2;
} else {
t2 = $[2];
}
useEffect(t1, t2);
let t3;
if ($[3] !== cleanupCount) {
t3 = <div>{cleanupCount}</div>;
$[3] = cleanupCount;
$[4] = t3;
} else {
t3 = $[4];
}
return t3;
}
function _temp2(c_0) {
return c_0 + 1;
}
function _temp(c) {
return c + 1;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ prop: 5 }],
sequentialRenders: [{ prop: 5 }, { prop: 5 }, { prop: 6 }],
};
```
### Eval output
(kind: ok) <div>0</div>
<div>0</div>
<div>1</div>

View File

@@ -0,0 +1,38 @@
import {useEffect, useState} from 'react';
/**
* Example of a function expression whose return value shouldn't have
* a "freeze" effect on all operands.
*
* This is because the function expression is passed to `useEffect` and
* thus is not a render function. `cleanedUp` is also created within
* the effect and is not a render variable.
*/
function Component({prop}) {
const [cleanupCount, setCleanupCount] = useState(0);
useEffect(() => {
let cleanedUp = false;
setTimeout(() => {
if (!cleanedUp) {
cleanedUp = true;
setCleanupCount(c => c + 1);
}
}, 0);
// This return value should not have freeze effects
// on its operands
return () => {
if (!cleanedUp) {
cleanedUp = true;
setCleanupCount(c => c + 1);
}
};
}, [prop]);
return <div>{cleanupCount}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{prop: 5}],
sequentialRenders: [{prop: 5}, {prop: 5}, {prop: 6}],
};

View File

@@ -0,0 +1,48 @@
## Input
```javascript
// @noEmit
function Foo() {
'use memo';
return <button onClick={() => alert('hello!')}>Click me!</button>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @noEmit
function Foo() {
"use memo";
const $ = _c(1);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = <button onClick={_temp}>Click me!</button>;
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}
function _temp() {
return alert("hello!");
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [],
};
```
### Eval output
(kind: ok) <button>Click me!</button>

View File

@@ -0,0 +1,11 @@
// @noEmit
function Foo() {
'use memo';
return <button onClick={() => alert('hello!')}>Click me!</button>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [],
};

View File

@@ -79,8 +79,7 @@ function Component(props) {
function Inner(props) {
const $ = _c(7);
let input;
input = null;
let input = null;
if (props.cond) {
input = use(FooContext);
}

View File

@@ -0,0 +1,176 @@
## Input
```javascript
import {ValidateMemoization} from 'shared-runtime';
function Component({a, b, c}) {
const map = new WeakMap();
const mapAlias = map.set(a, 0);
mapAlias.set(c, 0);
const hasB = map.has(b);
return (
<>
<ValidateMemoization inputs={[a, c]} output={map} />
<ValidateMemoization inputs={[a, c]} output={mapAlias} />
<ValidateMemoization inputs={[b]} output={[hasB]} />
</>
);
}
const v1 = {value: 1};
const v2 = {value: 2};
const v3 = {value: 3};
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: v1, b: v1, c: v1}],
sequentialRenders: [
{a: v1, b: v1, c: v1},
{a: v2, b: v1, c: v1},
{a: v1, b: v1, c: v1},
{a: v1, b: v2, c: v1},
{a: v1, b: v1, c: v1},
{a: v3, b: v3, c: v1},
{a: v3, b: v3, c: v1},
{a: v1, b: v1, c: v1},
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { ValidateMemoization } from "shared-runtime";
function Component(t0) {
const $ = _c(27);
const { a, b, c } = t0;
let map;
let mapAlias;
if ($[0] !== a || $[1] !== c) {
map = new WeakMap();
mapAlias = map.set(a, 0);
mapAlias.set(c, 0);
$[0] = a;
$[1] = c;
$[2] = map;
$[3] = mapAlias;
} else {
map = $[2];
mapAlias = $[3];
}
const hasB = map.has(b);
let t1;
if ($[4] !== a || $[5] !== c) {
t1 = [a, c];
$[4] = a;
$[5] = c;
$[6] = t1;
} else {
t1 = $[6];
}
let t2;
if ($[7] !== map || $[8] !== t1) {
t2 = <ValidateMemoization inputs={t1} output={map} />;
$[7] = map;
$[8] = t1;
$[9] = t2;
} else {
t2 = $[9];
}
let t3;
if ($[10] !== a || $[11] !== c) {
t3 = [a, c];
$[10] = a;
$[11] = c;
$[12] = t3;
} else {
t3 = $[12];
}
let t4;
if ($[13] !== mapAlias || $[14] !== t3) {
t4 = <ValidateMemoization inputs={t3} output={mapAlias} />;
$[13] = mapAlias;
$[14] = t3;
$[15] = t4;
} else {
t4 = $[15];
}
let t5;
if ($[16] !== b) {
t5 = [b];
$[16] = b;
$[17] = t5;
} else {
t5 = $[17];
}
let t6;
if ($[18] !== hasB) {
t6 = [hasB];
$[18] = hasB;
$[19] = t6;
} else {
t6 = $[19];
}
let t7;
if ($[20] !== t5 || $[21] !== t6) {
t7 = <ValidateMemoization inputs={t5} output={t6} />;
$[20] = t5;
$[21] = t6;
$[22] = t7;
} else {
t7 = $[22];
}
let t8;
if ($[23] !== t2 || $[24] !== t4 || $[25] !== t7) {
t8 = (
<>
{t2}
{t4}
{t7}
</>
);
$[23] = t2;
$[24] = t4;
$[25] = t7;
$[26] = t8;
} else {
t8 = $[26];
}
return t8;
}
const v1 = { value: 1 };
const v2 = { value: 2 };
const v3 = { value: 3 };
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ a: v1, b: v1, c: v1 }],
sequentialRenders: [
{ a: v1, b: v1, c: v1 },
{ a: v2, b: v1, c: v1 },
{ a: v1, b: v1, c: v1 },
{ a: v1, b: v2, c: v1 },
{ a: v1, b: v1, c: v1 },
{ a: v3, b: v3, c: v1 },
{ a: v3, b: v3, c: v1 },
{ a: v1, b: v1, c: v1 },
],
};
```
### Eval output
(kind: ok) <div>{"inputs":[{"value":1},"[[ cyclic ref *2 ]]"],"output":{}}</div><div>{"inputs":[{"value":1},"[[ cyclic ref *2 ]]"],"output":{}}</div><div>{"inputs":[{"value":1}],"output":[true]}</div>
<div>{"inputs":[{"value":2},{"value":1}],"output":{}}</div><div>{"inputs":[{"value":2},{"value":1}],"output":{}}</div><div>{"inputs":[{"value":1}],"output":[true]}</div>
<div>{"inputs":[{"value":1},"[[ cyclic ref *2 ]]"],"output":{}}</div><div>{"inputs":[{"value":1},"[[ cyclic ref *2 ]]"],"output":{}}</div><div>{"inputs":[{"value":1}],"output":[true]}</div>
<div>{"inputs":[{"value":1},"[[ cyclic ref *2 ]]"],"output":{}}</div><div>{"inputs":[{"value":1},"[[ cyclic ref *2 ]]"],"output":{}}</div><div>{"inputs":[{"value":2}],"output":[false]}</div>
<div>{"inputs":[{"value":1},"[[ cyclic ref *2 ]]"],"output":{}}</div><div>{"inputs":[{"value":1},"[[ cyclic ref *2 ]]"],"output":{}}</div><div>{"inputs":[{"value":1}],"output":[true]}</div>
<div>{"inputs":[{"value":3},{"value":1}],"output":{}}</div><div>{"inputs":[{"value":3},{"value":1}],"output":{}}</div><div>{"inputs":[{"value":3}],"output":[true]}</div>
<div>{"inputs":[{"value":3},{"value":1}],"output":{}}</div><div>{"inputs":[{"value":3},{"value":1}],"output":{}}</div><div>{"inputs":[{"value":3}],"output":[true]}</div>
<div>{"inputs":[{"value":1},"[[ cyclic ref *2 ]]"],"output":{}}</div><div>{"inputs":[{"value":1},"[[ cyclic ref *2 ]]"],"output":{}}</div><div>{"inputs":[{"value":1}],"output":[true]}</div>

View File

@@ -0,0 +1,35 @@
import {ValidateMemoization} from 'shared-runtime';
function Component({a, b, c}) {
const map = new WeakMap();
const mapAlias = map.set(a, 0);
mapAlias.set(c, 0);
const hasB = map.has(b);
return (
<>
<ValidateMemoization inputs={[a, c]} output={map} />
<ValidateMemoization inputs={[a, c]} output={mapAlias} />
<ValidateMemoization inputs={[b]} output={[hasB]} />
</>
);
}
const v1 = {value: 1};
const v2 = {value: 2};
const v3 = {value: 3};
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: v1, b: v1, c: v1}],
sequentialRenders: [
{a: v1, b: v1, c: v1},
{a: v2, b: v1, c: v1},
{a: v1, b: v1, c: v1},
{a: v1, b: v2, c: v1},
{a: v1, b: v1, c: v1},
{a: v3, b: v3, c: v1},
{a: v3, b: v3, c: v1},
{a: v1, b: v1, c: v1},
],
};

View File

@@ -0,0 +1,176 @@
## Input
```javascript
import {ValidateMemoization} from 'shared-runtime';
function Component({a, b, c}) {
const set = new WeakSet();
const setAlias = set.add(a);
setAlias.add(c);
const hasB = set.has(b);
return (
<>
<ValidateMemoization inputs={[a, c]} output={set} />
<ValidateMemoization inputs={[a, c]} output={setAlias} />
<ValidateMemoization inputs={[b]} output={[hasB]} />
</>
);
}
const v1 = {value: 1};
const v2 = {value: 2};
const v3 = {value: 3};
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: v1, b: v1, c: v1}],
sequentialRenders: [
{a: v1, b: v1, c: v1},
{a: v2, b: v1, c: v1},
{a: v1, b: v1, c: v1},
{a: v1, b: v2, c: v1},
{a: v1, b: v1, c: v1},
{a: v3, b: v3, c: v1},
{a: v3, b: v3, c: v1},
{a: v1, b: v1, c: v1},
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { ValidateMemoization } from "shared-runtime";
function Component(t0) {
const $ = _c(27);
const { a, b, c } = t0;
let set;
let setAlias;
if ($[0] !== a || $[1] !== c) {
set = new WeakSet();
setAlias = set.add(a);
setAlias.add(c);
$[0] = a;
$[1] = c;
$[2] = set;
$[3] = setAlias;
} else {
set = $[2];
setAlias = $[3];
}
const hasB = set.has(b);
let t1;
if ($[4] !== a || $[5] !== c) {
t1 = [a, c];
$[4] = a;
$[5] = c;
$[6] = t1;
} else {
t1 = $[6];
}
let t2;
if ($[7] !== set || $[8] !== t1) {
t2 = <ValidateMemoization inputs={t1} output={set} />;
$[7] = set;
$[8] = t1;
$[9] = t2;
} else {
t2 = $[9];
}
let t3;
if ($[10] !== a || $[11] !== c) {
t3 = [a, c];
$[10] = a;
$[11] = c;
$[12] = t3;
} else {
t3 = $[12];
}
let t4;
if ($[13] !== setAlias || $[14] !== t3) {
t4 = <ValidateMemoization inputs={t3} output={setAlias} />;
$[13] = setAlias;
$[14] = t3;
$[15] = t4;
} else {
t4 = $[15];
}
let t5;
if ($[16] !== b) {
t5 = [b];
$[16] = b;
$[17] = t5;
} else {
t5 = $[17];
}
let t6;
if ($[18] !== hasB) {
t6 = [hasB];
$[18] = hasB;
$[19] = t6;
} else {
t6 = $[19];
}
let t7;
if ($[20] !== t5 || $[21] !== t6) {
t7 = <ValidateMemoization inputs={t5} output={t6} />;
$[20] = t5;
$[21] = t6;
$[22] = t7;
} else {
t7 = $[22];
}
let t8;
if ($[23] !== t2 || $[24] !== t4 || $[25] !== t7) {
t8 = (
<>
{t2}
{t4}
{t7}
</>
);
$[23] = t2;
$[24] = t4;
$[25] = t7;
$[26] = t8;
} else {
t8 = $[26];
}
return t8;
}
const v1 = { value: 1 };
const v2 = { value: 2 };
const v3 = { value: 3 };
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ a: v1, b: v1, c: v1 }],
sequentialRenders: [
{ a: v1, b: v1, c: v1 },
{ a: v2, b: v1, c: v1 },
{ a: v1, b: v1, c: v1 },
{ a: v1, b: v2, c: v1 },
{ a: v1, b: v1, c: v1 },
{ a: v3, b: v3, c: v1 },
{ a: v3, b: v3, c: v1 },
{ a: v1, b: v1, c: v1 },
],
};
```
### Eval output
(kind: ok) <div>{"inputs":[{"value":1},"[[ cyclic ref *2 ]]"],"output":{}}</div><div>{"inputs":[{"value":1},"[[ cyclic ref *2 ]]"],"output":{}}</div><div>{"inputs":[{"value":1}],"output":[true]}</div>
<div>{"inputs":[{"value":2},{"value":1}],"output":{}}</div><div>{"inputs":[{"value":2},{"value":1}],"output":{}}</div><div>{"inputs":[{"value":1}],"output":[true]}</div>
<div>{"inputs":[{"value":1},"[[ cyclic ref *2 ]]"],"output":{}}</div><div>{"inputs":[{"value":1},"[[ cyclic ref *2 ]]"],"output":{}}</div><div>{"inputs":[{"value":1}],"output":[true]}</div>
<div>{"inputs":[{"value":1},"[[ cyclic ref *2 ]]"],"output":{}}</div><div>{"inputs":[{"value":1},"[[ cyclic ref *2 ]]"],"output":{}}</div><div>{"inputs":[{"value":2}],"output":[false]}</div>
<div>{"inputs":[{"value":1},"[[ cyclic ref *2 ]]"],"output":{}}</div><div>{"inputs":[{"value":1},"[[ cyclic ref *2 ]]"],"output":{}}</div><div>{"inputs":[{"value":1}],"output":[true]}</div>
<div>{"inputs":[{"value":3},{"value":1}],"output":{}}</div><div>{"inputs":[{"value":3},{"value":1}],"output":{}}</div><div>{"inputs":[{"value":3}],"output":[true]}</div>
<div>{"inputs":[{"value":3},{"value":1}],"output":{}}</div><div>{"inputs":[{"value":3},{"value":1}],"output":{}}</div><div>{"inputs":[{"value":3}],"output":[true]}</div>
<div>{"inputs":[{"value":1},"[[ cyclic ref *2 ]]"],"output":{}}</div><div>{"inputs":[{"value":1},"[[ cyclic ref *2 ]]"],"output":{}}</div><div>{"inputs":[{"value":1}],"output":[true]}</div>

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