Compare commits

..

40 Commits

Author SHA1 Message Date
Joe Savona
5eda822159 [compiler] Alternate take on ref validation
Some of the false positives we've seen have to do with the need to align our ref validation with our understanding of which functions may be called during render. The new mutability/aliasing model makes this much more explicit, with the ability to create Impure effects which we then throw as errors if they are reachable during render. This means we can now revisit ref validation by just emitting impure effects.

That's what this new pass does. It's a bit simpler: it implements the check for `ref.current == null` guarded if blocks. Otherwise it disallows access to `ref.current` specifically. Unlike before, we intentionally allow passing ref objects to functions — we just see a lot of many false positives on disallowing things like `children({ref})` or similar.

Open to feedback! This is also still WIP.
2025-06-25 09:49:17 -07:00
Joe Savona
2de9ddac73 [compiler] Consolidate HIRFunction return information
We now have `HIRFunction.returns: Place` as well as `returnType: Type`. I want to add additional return information, so as a first step i'm consolidating everything under an object at `HIRFunction.returns: {place: Place}`. We use the type of this place as the return type. Next step is to add more properties to this object to represent things like the return kind.
2025-06-25 09:49:17 -07:00
Joe Savona
6ef9e5bd49 [compiler] Avoid empty switch cases
Small cosmetic win, found this when i was looking at some code internally with lots of cases that all share the same logic. Previously, all the but last one would have an empty block.
2025-06-25 09:49:17 -07:00
Joe Savona
440d8c2876 [compiler] Fix bug with reassigning function param in destructuring
Closes #33577, a bug with ExtractScopeDeclarationsFromDestructuring and codegen when a function param is reassigned.
2025-06-25 09:49:17 -07:00
Sebastian Markbåge
cee7939b00 [Fizz] Push a stalled await from debug info to the ownerStack/debugTask (#33634)
If an aborted task is not rendering, then this is an async abort.
Conceptually it's as if the abort happened inside the async gap. The
abort reason's stack frame won't have that on the stack so instead we
use the owner stack and debug task of any halted async debug info.

One thing that's a bit awkward is that if you do have a sync abort and
you use that error as the "reason" then that thing still has a sync
stack in a different component. In another approach I was exploring
having different error objects for each component but I don't think
that's worth it.
2025-06-25 11:14:49 -04:00
Sebastian Markbåge
b42341ddc7 [Flight] Use cacheController instead of abortListeners for Streams (#33633)
Now that we have `cacheSignal()` we can just use that instead of the
`abortListeners` concept which was really just the same thing for
cancelling the streams (ReadableStream, Blob, AsyncIterable).
2025-06-25 09:41:21 -04:00
Pieter De Baets
7a3ffef703 [react-native] Consume ReactNativeAttributePayloadFabric from ReactNativePrivateInterface (#33616)
## Summary

ReactNativeAttributePayloadFabric was synced to react-native in
0e42d33cbc.
We should now consume these methods from the
ReactNativePrivateInterface.

Moving these methods to the React Native repo gives us more flexibility
to experiment with new techniques for bridging and diffing props
payloads.

I did have to leave some stub implementations for existing unit tests,
but moved all detailed tests to the React Native repo.

## How did you test this change?

* `yarn prettier`
* `yarn test ReactFabric-test`
2025-06-25 10:23:36 +01:00
Sebastian Markbåge
e67b4fe22e [Flight] Emit Partial Debug Info if we have any at the point of aborting a render (#33632)
When we abort a render we don't really have much information about the
task that was aborted. Because before a Promise resolves there's no
indication about would have resolved it. In particular we don't know
which I/O would've ultimately called resolve().

However, we can at least emit any information we do have at the point
where we emit it. At the least the stack of the top most Promise.

Currently we synchronously flush at the end of an `abort()` but we
should ideally schedule the flush in a macrotask and emit this debug
information right before that. That way we would give an opportunity for
any `cacheSignal()` abort to trigger rejections all the way up and those
rejections informs the awaited stack.

---------

Co-authored-by: Hendrik Liebau <mail@hendrik-liebau.de>
2025-06-24 16:36:21 -04:00
Sebastian Markbåge
4a523489b7 Get Server Component Function Location for Parent Stacks using Child's Owner Stack (#33629)
This is using the same trick as #30798 but for runtime code too. It's
essential zero cost.

This lets us include a source location for parent stacks of Server
Components when it has an owned child's location. Either from JSX or
I/O.

Ironically, a Component that throws an error will likely itself not get
the stack because it won't have any JSX rendered yet.
2025-06-24 16:35:28 -04:00
Joseph Savona
94cf60bede [compiler] New inference repros/fixes (#33584)
Substantially improves the last major known issue with the new inference
model's implementation: inferring effects of function expressions. I
knowingly used a really simple (dumb) approach in
InferFunctionExpressionAliasingEffects but it worked surprisingly well
on a ton of code. However, investigating during the sync I saw that we
the algorithm was literally running out of memory, or crashing from
arrays that exceeded the maximum capacity. We were accumluating data
flow in a way that could lead to lists of data flow captures compounding
on themselves and growing very large very quickly. Plus, we were
incorrectly recording some data flow, leading to cases where we reported
false positive "can't mutate frozen value" for example.

So I went back to the drawing board. InferMutationAliasingRanges already
builds up a data flow graph which it uses to figure out what values
would be affected by mutations of other values, and update mutable
ranges. Well, the key question that we really want to answer for
inferring a function expression's aliasing effects is which values
alias/capture where. Per the docs I wrote up, we only have to record
such aliasing _if they are observable via mutations_. So, lightbulb:
simulate mutations of the params, free variables, and return of the
function expression and see which params/free-vars would be affected!
That's what we do now, giving us precise information about which such
values alias/capture where. When the "into" is a param/context-var we
use Capture, iwhen the destination is the return we use Alias to be
conservative.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33584).
* #33626
* #33625
* #33624
* __->__ #33584
2025-06-24 10:01:58 -07:00
Sebastian Markbåge
bbc13fa17b [Flight] Add Debug Channel option for stateful connection to the backend in DEV (#33627)
This adds plumbing for opening a stream from the Flight Client to the
Flight Server so it can ask for more data on-demand. In this mode, the
Flight Server keeps the connection open as long as the client is still
alive and there's more objects to load. It retains any depth limited
objects so that they can be asked for later. In this first PR it just
releases the object when it's discovered on the server and doesn't
actually lazy load it yet. That's coming in a follow up.

This strategy is built on the model that each request has its own
channel for this. Instead of some global registry. That ensures that
referential identity is preserved within a Request and the Request can
refer to previously written objects by reference.

The fixture implements a WebSocket per request but it doesn't have to be
done that way. It can be multiplexed through an existing WebSocket for
example. The current protocol is just a Readable(Stream) on the server
and WritableStream on the client. It could even be sent through a HTTP
request body if browsers implemented full duplex (which they don't).

This PR only implements the direction of messages from Client to Server.
However, I also plan on adding Debug Channel in the other direction to
allow debug info (optionally) be sent from Server to Client through this
channel instead of through the main RSC request. So the `debugChannel`
option will be able to take writable or readable or both.

---------

Co-authored-by: Hendrik Liebau <mail@hendrik-liebau.de>
2025-06-24 11:16:09 -04:00
Ricky
12eaef7ef5 [refactor] remove unused fiberstack functions (#33623) 2025-06-23 20:07:04 -04:00
Sebastian Markbåge
c80c69fa96 [Flight] Remove back pointers to the Response from the Chunks (#33620)
This frees some memory that will be even more important in a follow up.

Currently, all `ReactPromise` instances hold onto their original
`Response`. The `Response` holds onto all objects that were in that
response since they're needed in case the parsed content ends up
referring to an existing object. If everything you retain are plain
objects then that's fine and the `Response` gets GC:ed, but if you're
retaining a `Promise` itself then it holds onto the whole `Response`.

The only thing that needs this reference at all is a
`ResolvedModelChunk` since it will lazily initialize e.g. by calling
`.then` on itself and so we need to know where to find any sibling
chunks it may refer to. However, we can just store the `Response` on the
`reason` field for this particular state.

That way when all lazy values are touched and initialized the `Response`
is freed. We also free up some memory by getting rid of the extra field.
2025-06-23 18:37:52 -04:00
Jan Kassens
aab72cb1cb rename ReactFiberContext to ReactFiberLegacyContext (#33622)
It wasn't immediately obvious to me, that all the exports here are
related to legacy context, so renaming for clarity.

Modern context lives in `ReactFiberNewContext` which we could probably
also raname in a separate step to just Context.
2025-06-23 17:21:18 -04:00
Sebastian "Sebbie" Silbermann
fa3feba672 Fix prelease workflows for dry: false (#33582)
## Summary

Follow-up to https://github.com/facebook/react/pull/33525

Fixes `Unsupported tag: "false"`
(https://github.com/facebook/react/actions/runs/15773778995/job/44463562733#step:13:12)
which also affects nightly releases.

## How did you test this change?

- [x] Run successful, manual prerelease from this branch:
https://github.com/facebook/react/actions/runs/15774083406
2025-06-23 11:47:07 -04:00
Sebastian Markbåge
2a911f27dd [Flight] Send the awaited Promise to the client as additional debug information (#33592)
Stacked on #33588, #33589 and #33590.

This lets us automatically show the resolved value in the UI.

<img width="863" alt="Screenshot 2025-06-22 at 12 54 41 AM"
src="https://github.com/user-attachments/assets/a66d1d5e-0513-4767-910c-5c7169fc2df4"
/>

We can also show rejected I/O that may or may not have been handled with
the error message.

<img width="838" alt="Screenshot 2025-06-22 at 12 55 06 AM"
src="https://github.com/user-attachments/assets/e0a8b6ae-08ba-46d8-8cc5-efb60956a1d1"
/>

To get this working we need to keep the Promise around for longer so
that we can access it once we want to emit an async sequence. I do this
by storing the WeakRefs but to ensure that the Promise doesn't get
garbage collected, I keep a WeakMap of Promise to the Promise that it
depended on. This lets the VM still clean up any Promise chains that
have leaves that are cleaned up. So this makes Promises live until the
last Promise downstream is done. At that point we can go back up the
chain to read the values out of them.

Additionally, to get the best possible value we don't want to get a
Promise that's used by internals of a third-party function. We want the
value that the first party gets to observe. To do this I had to change
the logic for which "await" to use, to be the one that is the first
await that happened in user space. It's not enough that the await has
any first party at all on the stack - it has to be the very first frame.
This is a little sketchy because it relies on the `.then()` call or
`await` call not having any third party wrappers. But it gives the best
object since it hides all the internals. For example when you call
`fetch()` we now log that actual `Response` object.
2025-06-23 10:12:45 -04:00
Sebastian Markbåge
18ee505e77 [Flight] Support classes in renderDebugModel (#33590)
This adds better support for serializing class instances as Debug
values.

It adds a new marker on the object `{ "": "$P...", ... }` which
indicates which constructor's prototype to use for this object's
prototype. It doesn't encode arbitrary prototypes and it doesn't encode
any of the properties on the prototype. It might get some of the
properties from the prototype by virtue of `toString` on a `class`
constructor will include the whole class's body.

This will ensure that the instance gets the right name in logs.

Additionally, this now also invokes getters if they're enumerable on the
prototype. This lets us reify values that can only be read from native
classes.

---------

Co-authored-by: Hendrik Liebau <mail@hendrik-liebau.de>
2025-06-22 18:00:08 -04:00
Sebastian Markbåge
1d1b26c701 [Flight] Serialize already resolved Promises as debug models (#33588)
We already support serializing the values of instrumented Promises as
debug values such as in console logs. However, we don't support plain
native promises.

This waits a microtask to see if we can read the value within a
microtask and if so emit it. This is so that we can still close the
connection.

Otherwise, we emit a "halted" row into its row id which replaces the old
"Infinite Promise" reference.

We could potentially wait until the end of the render before cancelling
so that if it resolves before we exit we can still include its value but
that would require a bit more work. Ideally we'd have a way to get these
lazily later anyway.
2025-06-22 17:51:31 -04:00
Sebastian Markbåge
fe3f0ec037 [Flight] Don't use object property initializer for async iterable (#33591)
It turns out this was being compiled to a `_defineProperty` helper by
Babel or Closure. We're supposed to have it error the build when we use
features like this that might get compiled.

We should stick to simple ES5 features.
2025-06-22 10:40:56 -04:00
Sebastian Markbåge
d70ee32b88 [Flight] Eagerly parse stack traces in DebugNode (#33589)
There's a memory leak in DebugNode where the `Error` objects that we
instantiate retains their callstacks which can have Promises on them. In
fact, it's very likely since the current callsite has the "resource" on
it which is the Promise itself. If those Promises are retained then
their `destroy` async hook is never fired which doesn't clean up our map
which can contains the `Error` object. Creating a cycle that can't be
cleaned up.

This fix is just eagerly reifying and parsing the stacks.

I totally expect this to be crazy slow since there's so many Promises
that we end up not needing to visit otherwise. We'll need to optimize it
somehow. Perhaps by being smarter about which ones we might need stacks
for. However, at least it doesn't leak indefinitely.
2025-06-22 10:40:33 -04:00
Sebastian Markbåge
6c7b1a1d98 Rename serializeConsoleMap/Set to serializeDebugMap/Set (#33587)
Follow up to #33583. I forgot to rename these too.
2025-06-21 10:36:07 -04:00
Sebastian Markbåge
ed077194b5 [Flight] Dedupe objects serialized as Debug Models in a separate set (#33583)
Stacked on #33539.

Stores dedupes of `renderConsoleValue` in a separate set. This allows us
to dedupe objects safely since we can't write objects using this
algorithm if they might also be referenced by the "real" serialization.

Also renamed it to `renderDebugModel` since it's not just for console
anymore.
2025-06-20 13:36:39 -04:00
Devon Govett
643257ca52 [Flight] Serialize functions by reference (#33539)
On pages that have a high number of server components (e.g. common when
doing syntax highlighting), the debug outlining can produce extremely
large RSC payloads. For example a documentation page I was working on
had a 13.8 MB payload. I noticed that a majority of this was the source
code for the same function components repeated over and over again (over
4000 times) within `$E()` eval commands.

This PR deduplicates the same functions by serializing by reference,
similar to what is already done for objects. Doing this reduced the
payload size of my page from 13.8 MB to 4.6 MB, and resulted in only 31
evals instead of over 4000. As a result it reduced development page load
and hydration time from 4 seconds to 1.5 seconds. It also means the
deserialized functions will have reference equality just as they did on
the server.
2025-06-20 13:36:07 -04:00
Sebastian "Sebbie" Silbermann
06e89951be [Fizz] Ignore error if content node is gone before reveal (#33531) 2025-06-20 14:21:57 +02:00
Sebastian Markbåge
79d9aed7ed [Fizz] Clean up the replay nodes if we're already rendered past an element (#33581) 2025-06-20 09:26:26 +02:00
Sebastian "Sebbie" Silbermann
c8822e926b Make it clearer what runtime release failed (#33579) 2025-06-20 09:11:27 +02:00
Sebastian "Sebbie" Silbermann
a947eba4f2 Fix CI (#33578) 2025-06-19 23:40:59 +02:00
Ruslan Lesiutin
374dfe8edf build: make enableComponentPerformanceTrack dynamic for native-fb (#33560)
## Summary

Make this flag dynamic, so it can be controlled internally.

## How did you test this change?

Build, observe that `console.timeStamp` is only present in FB artifacts
and `enableComponentPerformanceTrack` is referenced.
2025-06-19 09:47:23 +01:00
Joseph Savona
2bee34867d [compiler] Cleanup debugging code (#33571)
Removes unnecessary debugging code in the new inference passes now that
they've stabilized more.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33571).
* __->__ #33571
* #33558
* #33547
2025-06-18 16:00:55 -07:00
Joseph Savona
d37faa041b [compiler] Preserve Create effects, guarantee effects initialize once (#33558)
Ensures that effects are well-formed with respect to the rules:
* For a given instruction, each place is only initialized once (w one of
Create, CreateFrom, Assign)
* Ensures that Alias targets are already initialized within the same
instruction (should have a Create before them)
* Preserves Create and similar instructions
* Avoids duplicate instructions when inferring effects of function
expressions

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33558).
* #33571
* __->__ #33558
* #33547
2025-06-18 16:00:45 -07:00
Joseph Savona
3a2ff8b51b [compiler] Fix <ValidateMemoization> (#33547)
By accident we were only ever checking the compiled output, but the
intention was in general to be able to compare memoization with/without
forget.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33547).
* #33571
* #33558
* __->__ #33547
2025-06-18 16:00:36 -07:00
Joseph Savona
cc3806377a [compiler] Tests for different orders of createfrom/capture w/wo function expressions (#33543)
Adds some typed helpers to represent aliasing, assign, capture,
createfrom, and mutate effects along with representative runtime
behavior, and then adds tests to demonstrate that we model
capture->createfrom and createfrom->capture correctly.

There is one case (createfrom->capture in a lambda) where we infer a
less precise effect, but in the more conservative direction (we include
more code/deps than necesssary rather than fewer).

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33543).
* #33571
* #33558
* #33547
* __->__ #33543
2025-06-18 15:56:27 -07:00
Joseph Savona
4f543f326c [compiler] Docs describing new inference model (#33533)
Start of docs describing the effects and the inference rules.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33533).
* #33571
* #33558
* #33547
* #33543
* __->__ #33533
* #33532
* #33530
2025-06-18 15:48:01 -07:00
Joseph Savona
7ceb10035f [compiler] Rename InferFunctionExprAliasingEffectsSignature (#33532)
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33532).
* #33571
* #33558
* #33547
* #33543
* #33533
* __->__ #33532
* #33530
2025-06-18 15:47:52 -07:00
Joseph Savona
4335f69987 [compiler] More readable alias signature declarations (#33530)
Now that we have support for defining aliasing signatures in
moduleTypeProvider, which uses string names for
receiver/args/returns/etc, we can reuse that same form for builtin
declarations. The declarations are written in the unparsed form and than
parsed/validated when registered (in the addFunction/addHook call).

This also required flushing out configs/schemas for more effect types.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33530).
* #33571
* #33558
* #33547
* #33543
* #33533
* #33532
* __->__ #33530
2025-06-18 15:47:43 -07:00
Joseph Savona
34179fe344 [compiler] moduleTypeProvider support for aliasing signatures (#33526)
This allows us to type things like `nullthrows()` or `identity()`
functions where the return type is polymorphic on the input.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33526).
* #33571
* #33558
* #33547
* #33543
* #33533
* #33532
* #33530
* __->__ #33526
* #33522
* #33518
2025-06-18 15:43:48 -07:00
Joseph Savona
0e7cdebb32 [compiler] Repro for case of lost precision in new inference (#33522)
In comparing compilation output of the old/new inference models I found
this case (heavily distilled into a fixture). Roughly speaking the
scenario is:

* Create a mutable object `x`
* Extract part of that object and pass it to a hook/jsx so that _part_
becomes frozen
* Mutate `x`, even indirectly.

In the old model we can still independently memoize the value from the
middle step, since we assume that part of the larger value is not
changing. In the new model, the mutation from the later step effectively
overrides the freeze effect in step 2, and considers the value to have
changed later anyway.

We've already rolled out and vetted the previous behavior, confirming
that the heuristic of "that part of the mutable object is fozen now" is
generally safe. I'll fix in a follow-up.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33522).
* #33571
* #33558
* #33547
* #33543
* #33533
* #33532
* #33530
* #33526
* __->__ #33522
* #33518
2025-06-18 15:43:33 -07:00
Joseph Savona
81d8115116 [compiler] Fix infinite loop due to uncached applied signatures (#33518)
When we apply new aliasing signatures we can generate new temporaries,
which causes the abstract memory model to not converge. The fix is to
make sure we cache the applications of these signatures.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33518).
* #33571
* #33558
* #33547
* #33543
* #33533
* #33532
* #33530
* #33526
* #33522
* __->__ #33518
2025-06-18 15:43:23 -07:00
Joseph Savona
8f4ce72f0b [commit] Improve error for hoisting violations (#33514)
The previous error for hoisting violations pointed only to the variable
declaration, but didn't show where the value was accessed before that
declaration. We now track where each hoisted variable is first accessed
and report two errors, one for the reference and one for the
declaration. When we improve our diagnostic infra to support reporting
errors at multiple locations we can merge these into a single conceptual
error.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33514).
* #33571
* #33558
* #33547
* #33543
* #33533
* #33532
* #33530
* #33526
* #33522
* #33518
* __->__ #33514
* #33573
2025-06-18 15:24:41 -07:00
Joseph Savona
7ce2a63acc [compiler] update fixtures (#33573)
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33573).
* #33571
* #33558
* #33547
* #33543
* #33533
* #33532
* #33530
* #33526
* #33522
* #33518
* #33514
* __->__ #33573
2025-06-18 15:24:30 -07:00
146 changed files with 7259 additions and 2446 deletions

View File

@@ -301,10 +301,12 @@ jobs:
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
restore-keys: |
runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-and-compiler-node_modules-v6-
- run: yarn install --frozen-lockfile
- name: Install runtime dependencies
run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- name: Install compiler dependencies
run: yarn install --frozen-lockfile
working-directory: compiler
if: steps.node_modules.outputs.cache-hit != 'true'
- run: ./scripts/react-compiler/build-compiler.sh && ./scripts/react-compiler/link-compiler.sh
- run: yarn workspace eslint-plugin-react-hooks test

View File

@@ -85,7 +85,7 @@ jobs:
--skipTests \
--tags=${{ inputs.dist_tag }} \
--onlyPackages=${{ inputs.only_packages }} ${{ (inputs.dry && '') || '\'}}
${{ inputs.dry && '--dry'}}
${{ inputs.dry && '--dry' || '' }}
- if: '${{ inputs.skip_packages }}'
name: 'Publish all packages EXCEPT ${{ inputs.skip_packages }}'
run: |
@@ -94,19 +94,19 @@ jobs:
--skipTests \
--tags=${{ inputs.dist_tag }} \
--skipPackages=${{ inputs.skip_packages }} ${{ (inputs.dry && '') || '\'}}
${{ inputs.dry && '--dry'}}
${{ inputs.dry && '--dry' || '' }}
- if: '${{ !(inputs.skip_packages && inputs.only_packages) }}'
name: 'Publish all packages'
run: |
scripts/release/publish.js \
--ci \
--tags=${{ inputs.dist_tag }} ${{ (inputs.dry && '') || '\'}}
${{ inputs.dry && '--dry'}}
${{ inputs.dry && '--dry' || '' }}
- name: Notify Discord on failure
if: failure() && inputs.enableFailureNotification == true
uses: tsickert/discord-webhook@86dc739f3f165f16dadc5666051c367efa1692f4
with:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
embed-author-name: "GitHub Actions"
embed-title: 'Publish of $${{ inputs.release_channel }} release failed'
embed-title: '[Runtime] Publish of ${{ inputs.release_channel }}@${{ inputs.dist_tag}} release failed'
embed-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}/attempts/${{ github.run_attempt }}

View File

@@ -110,7 +110,7 @@ jobs:
--tags=${{ inputs.tags }} \
--publishVersion=${{ inputs.version_to_publish }} \
--onlyPackages=${{ inputs.only_packages }} ${{ (inputs.dry && '') || '\'}}
${{ inputs.dry && '--dry'}}
${{ inputs.dry && '--dry' || '' }}
- if: '${{ inputs.skip_packages }}'
name: 'Publish all packages EXCEPT ${{ inputs.skip_packages }}'
run: |
@@ -119,7 +119,7 @@ jobs:
--tags=${{ inputs.tags }} \
--publishVersion=${{ inputs.version_to_publish }} \
--skipPackages=${{ inputs.skip_packages }} ${{ (inputs.dry && '') || '\'}}
${{ inputs.dry && '--dry'}}
${{ inputs.dry && '--dry' || '' }}
- name: Archive released package for debugging
uses: actions/upload-artifact@v4
with:

View File

@@ -285,7 +285,7 @@ function runWithEnvironment(
}
if (env.config.validateRefAccessDuringRender) {
validateNoRefAccessInRender(hir).unwrap();
// validateNoRefAccessInRender(hir).unwrap();
}
if (env.config.validateNoSetStateInRender) {

View File

@@ -221,7 +221,6 @@ export function lower(
params,
fnType: bindings == null ? env.fnType : 'Other',
returnTypeAnnotation: null, // TODO: extract the actual return type node if present
returnType: makeType(),
returns: createTemporaryPlace(env, func.node.loc ?? GeneratedSource),
body: builder.build(),
context,

View File

@@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {Effect, makeIdentifierId, ValueKind, ValueReason} from './HIR';
import {Effect, ValueKind, ValueReason} from './HIR';
import {
BUILTIN_SHAPES,
BuiltInArrayId,
@@ -34,7 +34,6 @@ import {
addFunction,
addHook,
addObject,
signatureArgument,
} from './ObjectShape';
import {BuiltInType, ObjectType, PolyType} from './Types';
import {TypeConfig} from './TypeSchema';
@@ -646,35 +645,35 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
hookKind: 'useEffect',
returnValueKind: ValueKind.Frozen,
aliasing: {
receiver: makeIdentifierId(0),
receiver: '@receiver',
params: [],
rest: makeIdentifierId(1),
returns: makeIdentifierId(2),
temporaries: [signatureArgument(3)],
rest: '@rest',
returns: '@returns',
temporaries: ['@effect'],
effects: [
// Freezes the function and deps
{
kind: 'Freeze',
value: signatureArgument(1),
value: '@rest',
reason: ValueReason.Effect,
},
// Internally creates an effect object that captures the function and deps
{
kind: 'Create',
into: signatureArgument(3),
into: '@effect',
value: ValueKind.Frozen,
reason: ValueReason.KnownReturnSignature,
},
// The effect stores the function and dependencies
{
kind: 'Capture',
from: signatureArgument(1),
into: signatureArgument(3),
from: '@rest',
into: '@effect',
},
// Returns undefined
{
kind: 'Create',
into: signatureArgument(2),
into: '@returns',
value: ValueKind.Primitive,
reason: ValueReason.KnownReturnSignature,
},
@@ -906,6 +905,7 @@ export function installTypeConfig(
noAlias: typeConfig.noAlias === true,
mutableOnlyIfOperandsAreMutable:
typeConfig.mutableOnlyIfOperandsAreMutable === true,
aliasing: typeConfig.aliasing,
});
}
case 'hook': {
@@ -923,6 +923,7 @@ export function installTypeConfig(
),
returnValueKind: typeConfig.returnValueKind ?? ValueKind.Frozen,
noAlias: typeConfig.noAlias === true,
aliasing: typeConfig.aliasing,
});
}
case 'object': {

View File

@@ -279,7 +279,6 @@ export type HIRFunction = {
env: Environment;
params: Array<Place | SpreadPattern>;
returnTypeAnnotation: t.FlowType | t.TSType | null;
returnType: Type;
returns: Place;
context: Array<Place>;
effects: Array<FunctionEffect> | null;
@@ -1453,6 +1452,20 @@ export const ValueKindSchema = z.enum([
ValueKind.Context,
]);
export const ValueReasonSchema = z.enum([
ValueReason.Context,
ValueReason.Effect,
ValueReason.Global,
ValueReason.HookCaptured,
ValueReason.HookReturn,
ValueReason.JsxCaptured,
ValueReason.KnownReturnSignature,
ValueReason.Other,
ValueReason.ReactiveFunctionArgument,
ValueReason.ReducerState,
ValueReason.State,
]);
// The effect with which a value is modified.
export enum Effect {
// Default value: not allowed after lifetime inference
@@ -1756,6 +1769,10 @@ export function isUseStateType(id: Identifier): boolean {
return id.type.kind === 'Object' && id.type.shapeId === 'BuiltInUseState';
}
export function isJsxType(type: Type): boolean {
return type.kind === 'Object' && type.shapeId === 'BuiltInJsx';
}
export function isRefOrRefValue(id: Identifier): boolean {
return isUseRefType(id) || isRefValueType(id);
}

View File

@@ -6,14 +6,18 @@
*/
import {CompilerError} from '../CompilerError';
import {AliasingSignature} from '../Inference/AliasingEffects';
import {AliasingEffect, AliasingSignature} from '../Inference/AliasingEffects';
import {assertExhaustive} from '../Utils/utils';
import {
Effect,
GeneratedSource,
Hole,
makeDeclarationId,
makeIdentifierId,
makeInstructionId,
Place,
SourceLocation,
SpreadPattern,
ValueKind,
ValueReason,
} from './HIR';
@@ -25,6 +29,7 @@ import {
PolyType,
PrimitiveType,
} from './Types';
import {AliasingEffectConfig, AliasingSignatureConfig} from './TypeSchema';
/*
* This file exports types and defaults for JavaScript object shapes. These are
@@ -53,13 +58,20 @@ function createAnonId(): string {
export function addFunction(
registry: ShapeRegistry,
properties: Iterable<[string, BuiltInType | PolyType]>,
fn: Omit<FunctionSignature, 'hookKind'>,
fn: Omit<FunctionSignature, 'hookKind' | 'aliasing'> & {
aliasing?: AliasingSignatureConfig | null | undefined;
},
id: string | null = null,
isConstructor: boolean = false,
): FunctionType {
const shapeId = id ?? createAnonId();
const aliasing =
fn.aliasing != null
? parseAliasingSignatureConfig(fn.aliasing, '<builtin>', GeneratedSource)
: null;
addShape(registry, shapeId, properties, {
...fn,
aliasing,
hookKind: null,
});
return {
@@ -77,11 +89,18 @@ export function addFunction(
*/
export function addHook(
registry: ShapeRegistry,
fn: FunctionSignature & {hookKind: HookKind},
fn: Omit<FunctionSignature, 'aliasing'> & {
hookKind: HookKind;
aliasing?: AliasingSignatureConfig | null | undefined;
},
id: string | null = null,
): FunctionType {
const shapeId = id ?? createAnonId();
addShape(registry, shapeId, [], fn);
const aliasing =
fn.aliasing != null
? parseAliasingSignatureConfig(fn.aliasing, '<builtin>', GeneratedSource)
: null;
addShape(registry, shapeId, [], {...fn, aliasing});
return {
kind: 'Function',
return: fn.returnType,
@@ -90,6 +109,129 @@ export function addHook(
};
}
function parseAliasingSignatureConfig(
typeConfig: AliasingSignatureConfig,
moduleName: string,
loc: SourceLocation,
): AliasingSignature {
const lifetimes = new Map<string, Place>();
function define(temp: string): Place {
CompilerError.invariant(!lifetimes.has(temp), {
reason: `Invalid type configuration for module`,
description: `Expected aliasing signature to have unique names for receiver, params, rest, returns, and temporaries in module '${moduleName}'`,
loc,
});
const place = signatureArgument(lifetimes.size);
lifetimes.set(temp, place);
return place;
}
function lookup(temp: string): Place {
const place = lifetimes.get(temp);
CompilerError.invariant(place != null, {
reason: `Invalid type configuration for module`,
description: `Expected aliasing signature effects to reference known names from receiver/params/rest/returns/temporaries, but '${temp}' is not a known name in '${moduleName}'`,
loc,
});
return place;
}
const receiver = define(typeConfig.receiver);
const params = typeConfig.params.map(define);
const rest = typeConfig.rest != null ? define(typeConfig.rest) : null;
const returns = define(typeConfig.returns);
const temporaries = typeConfig.temporaries.map(define);
const effects = typeConfig.effects.map(
(effect: AliasingEffectConfig): AliasingEffect => {
switch (effect.kind) {
case 'CreateFrom':
case 'Capture':
case 'Alias':
case 'Assign': {
const from = lookup(effect.from);
const into = lookup(effect.into);
return {
kind: effect.kind,
from,
into,
};
}
case 'Mutate':
case 'MutateTransitiveConditionally': {
const value = lookup(effect.value);
return {kind: effect.kind, value};
}
case 'Create': {
const into = lookup(effect.into);
return {
kind: 'Create',
into,
reason: effect.reason,
value: effect.value,
};
}
case 'Freeze': {
const value = lookup(effect.value);
return {
kind: 'Freeze',
value,
reason: effect.reason,
};
}
case 'Impure': {
const place = lookup(effect.place);
return {
kind: 'Impure',
place,
error: CompilerError.throwTodo({
reason: 'Support impure effect declarations',
loc: GeneratedSource,
}),
};
}
case 'Apply': {
const receiver = lookup(effect.receiver);
const fn = lookup(effect.function);
const args: Array<Place | SpreadPattern | Hole> = effect.args.map(
arg => {
if (typeof arg === 'string') {
return lookup(arg);
} else if (arg.kind === 'Spread') {
return {kind: 'Spread', place: lookup(arg.place)};
} else {
return arg;
}
},
);
const into = lookup(effect.into);
return {
kind: 'Apply',
receiver,
function: fn,
mutatesFunction: effect.mutatesFunction,
args,
into,
loc,
signature: null,
};
}
default: {
assertExhaustive(
effect,
`Unexpected effect kind '${(effect as any).kind}'`,
);
}
}
},
);
return {
receiver: receiver.identifier.id,
params: params.map(p => p.identifier.id),
rest: rest != null ? rest.identifier.id : null,
returns: returns.identifier.id,
temporaries,
effects,
};
}
/*
* Add an object to an existing ShapeRegistry.
*
@@ -192,8 +334,7 @@ export type FunctionSignature = {
canonicalName?: string;
aliasing?: AliasingSignature | null;
todo_aliasing?: AliasingSignature | null;
aliasing?: AliasingSignature | null | undefined;
};
/*
@@ -320,24 +461,24 @@ addObject(BUILTIN_SHAPES, BuiltInArrayId, [
calleeEffect: Effect.Store,
returnValueKind: ValueKind.Primitive,
aliasing: {
receiver: makeIdentifierId(0),
receiver: '@receiver',
params: [],
rest: makeIdentifierId(1),
returns: makeIdentifierId(2),
rest: '@rest',
returns: '@returns',
temporaries: [],
effects: [
// Push directly mutates the array itself
{kind: 'Mutate', value: signatureArgument(0)},
{kind: 'Mutate', value: '@receiver'},
// The arguments are captured into the array
{
kind: 'Capture',
from: signatureArgument(1),
into: signatureArgument(0),
from: '@rest',
into: '@receiver',
},
// Returns the new length, a primitive
{
kind: 'Create',
into: signatureArgument(2),
into: '@returns',
value: ValueKind.Primitive,
reason: ValueReason.KnownReturnSignature,
},
@@ -374,58 +515,56 @@ addObject(BUILTIN_SHAPES, BuiltInArrayId, [
noAlias: true,
mutableOnlyIfOperandsAreMutable: true,
aliasing: {
receiver: makeIdentifierId(0),
params: [makeIdentifierId(1)],
receiver: '@receiver',
params: ['@callback'],
rest: null,
returns: makeIdentifierId(2),
returns: '@returns',
temporaries: [
// Temporary representing captured items of the receiver
signatureArgument(3),
'@item',
// Temporary representing the result of the callback
signatureArgument(4),
'@callbackReturn',
/*
* Undefined `this` arg to the callback. Note the signature does not
* support passing an explicit thisArg second param
*/
signatureArgument(5),
'@thisArg',
],
effects: [
// Map creates a new mutable array
{
kind: 'Create',
into: signatureArgument(2),
into: '@returns',
value: ValueKind.Mutable,
reason: ValueReason.KnownReturnSignature,
},
// The first arg to the callback is an item extracted from the receiver array
{
kind: 'CreateFrom',
from: signatureArgument(0),
into: signatureArgument(3),
from: '@receiver',
into: '@item',
},
// The undefined this for the callback
{
kind: 'Create',
into: signatureArgument(5),
into: '@thisArg',
value: ValueKind.Primitive,
reason: ValueReason.KnownReturnSignature,
},
// calls the callback, returning the result into a temporary
{
kind: 'Apply',
receiver: signatureArgument(5),
args: [signatureArgument(3), {kind: 'Hole'}, signatureArgument(0)],
function: signatureArgument(1),
into: signatureArgument(4),
signature: null,
receiver: '@thisArg',
args: ['@item', {kind: 'Hole'}, '@receiver'],
function: '@callback',
into: '@callbackReturn',
mutatesFunction: false,
loc: GeneratedSource,
},
// captures the result of the callback into the return array
{
kind: 'Capture',
from: signatureArgument(4),
into: signatureArgument(2),
from: '@callbackReturn',
into: '@returns',
},
],
},
@@ -577,28 +716,28 @@ addObject(BUILTIN_SHAPES, BuiltInSetId, [
// returnValueKind is technically dependent on the ValueKind of the set itself
returnValueKind: ValueKind.Mutable,
aliasing: {
receiver: makeIdentifierId(0),
receiver: '@receiver',
params: [],
rest: makeIdentifierId(1),
returns: makeIdentifierId(2),
rest: '@rest',
returns: '@returns',
temporaries: [],
effects: [
// Set.add returns the receiver Set
{
kind: 'Assign',
from: signatureArgument(0),
into: signatureArgument(2),
from: '@receiver',
into: '@returns',
},
// Set.add mutates the set itself
{
kind: 'Mutate',
value: signatureArgument(0),
value: '@receiver',
},
// Captures the rest params into the set
{
kind: 'Capture',
from: signatureArgument(1),
into: signatureArgument(0),
from: '@rest',
into: '@receiver',
},
],
},
@@ -1303,30 +1442,30 @@ export const DefaultNonmutatingHook = addHook(
hookKind: 'Custom',
returnValueKind: ValueKind.Frozen,
aliasing: {
receiver: makeIdentifierId(0),
receiver: '@receiver',
params: [],
rest: makeIdentifierId(1),
returns: makeIdentifierId(2),
rest: '@rest',
returns: '@returns',
temporaries: [],
effects: [
// Freeze the arguments
{
kind: 'Freeze',
value: signatureArgument(1),
value: '@rest',
reason: ValueReason.HookCaptured,
},
// Returns a frozen value
{
kind: 'Create',
into: signatureArgument(2),
into: '@returns',
value: ValueKind.Frozen,
reason: ValueReason.HookReturn,
},
// May alias any arguments into the return
{
kind: 'Alias',
from: signatureArgument(1),
into: signatureArgument(2),
from: '@rest',
into: '@returns',
},
],
},

View File

@@ -54,6 +54,8 @@ export function printFunction(fn: HIRFunction): string {
let definition = '';
if (fn.id !== null) {
definition += fn.id;
} else {
definition += '<<anonymous>>';
}
if (fn.params.length !== 0) {
definition +=
@@ -71,10 +73,8 @@ export function printFunction(fn: HIRFunction): string {
} else {
definition += '()';
}
if (definition.length !== 0) {
output.push(definition);
}
output.push(`: ${printType(fn.returnType)} @ ${printPlace(fn.returns)}`);
definition += `: ${printPlace(fn.returns)}`;
output.push(definition);
output.push(...fn.directives);
output.push(printHIR(fn.body));
return output.join('\n');

View File

@@ -8,7 +8,12 @@
import {isValidIdentifier} from '@babel/types';
import {z} from 'zod';
import {Effect, ValueKind} from '..';
import {EffectSchema, ValueKindSchema} from './HIR';
import {
EffectSchema,
ValueKindSchema,
ValueReason,
ValueReasonSchema,
} from './HIR';
export type ObjectPropertiesConfig = {[key: string]: TypeConfig};
export const ObjectPropertiesSchema: z.ZodType<ObjectPropertiesConfig> = z
@@ -31,6 +36,194 @@ export const ObjectTypeSchema: z.ZodType<ObjectTypeConfig> = z.object({
properties: ObjectPropertiesSchema.nullable(),
});
export const LifetimeIdSchema = z.string().refine(id => id.startsWith('@'), {
message: "Placeholder names must start with '@'",
});
export type FreezeEffectConfig = {
kind: 'Freeze';
value: string;
reason: ValueReason;
};
export const FreezeEffectSchema: z.ZodType<FreezeEffectConfig> = z.object({
kind: z.literal('Freeze'),
value: LifetimeIdSchema,
reason: ValueReasonSchema,
});
export type MutateEffectConfig = {
kind: 'Mutate';
value: string;
};
export const MutateEffectSchema: z.ZodType<MutateEffectConfig> = z.object({
kind: z.literal('Mutate'),
value: LifetimeIdSchema,
});
export type MutateTransitiveConditionallyConfig = {
kind: 'MutateTransitiveConditionally';
value: string;
};
export const MutateTransitiveConditionallySchema: z.ZodType<MutateTransitiveConditionallyConfig> =
z.object({
kind: z.literal('MutateTransitiveConditionally'),
value: LifetimeIdSchema,
});
export type CreateEffectConfig = {
kind: 'Create';
into: string;
value: ValueKind;
reason: ValueReason;
};
export const CreateEffectSchema: z.ZodType<CreateEffectConfig> = z.object({
kind: z.literal('Create'),
into: LifetimeIdSchema,
value: ValueKindSchema,
reason: ValueReasonSchema,
});
export type AssignEffectConfig = {
kind: 'Assign';
from: string;
into: string;
};
export const AssignEffectSchema: z.ZodType<AssignEffectConfig> = z.object({
kind: z.literal('Assign'),
from: LifetimeIdSchema,
into: LifetimeIdSchema,
});
export type AliasEffectConfig = {
kind: 'Alias';
from: string;
into: string;
};
export const AliasEffectSchema: z.ZodType<AliasEffectConfig> = z.object({
kind: z.literal('Alias'),
from: LifetimeIdSchema,
into: LifetimeIdSchema,
});
export type CaptureEffectConfig = {
kind: 'Capture';
from: string;
into: string;
};
export const CaptureEffectSchema: z.ZodType<CaptureEffectConfig> = z.object({
kind: z.literal('Capture'),
from: LifetimeIdSchema,
into: LifetimeIdSchema,
});
export type CreateFromEffectConfig = {
kind: 'CreateFrom';
from: string;
into: string;
};
export const CreateFromEffectSchema: z.ZodType<CreateFromEffectConfig> =
z.object({
kind: z.literal('CreateFrom'),
from: LifetimeIdSchema,
into: LifetimeIdSchema,
});
export type ApplyArgConfig =
| string
| {kind: 'Spread'; place: string}
| {kind: 'Hole'};
export const ApplyArgSchema: z.ZodType<ApplyArgConfig> = z.union([
LifetimeIdSchema,
z.object({
kind: z.literal('Spread'),
place: LifetimeIdSchema,
}),
z.object({
kind: z.literal('Hole'),
}),
]);
export type ApplyEffectConfig = {
kind: 'Apply';
receiver: string;
function: string;
mutatesFunction: boolean;
args: Array<ApplyArgConfig>;
into: string;
};
export const ApplyEffectSchema: z.ZodType<ApplyEffectConfig> = z.object({
kind: z.literal('Apply'),
receiver: LifetimeIdSchema,
function: LifetimeIdSchema,
mutatesFunction: z.boolean(),
args: z.array(ApplyArgSchema),
into: LifetimeIdSchema,
});
export type ImpureEffectConfig = {
kind: 'Impure';
place: string;
};
export const ImpureEffectSchema: z.ZodType<ImpureEffectConfig> = z.object({
kind: z.literal('Impure'),
place: LifetimeIdSchema,
});
export type AliasingEffectConfig =
| FreezeEffectConfig
| CreateEffectConfig
| CreateFromEffectConfig
| AssignEffectConfig
| AliasEffectConfig
| CaptureEffectConfig
| ImpureEffectConfig
| MutateEffectConfig
| MutateTransitiveConditionallyConfig
| ApplyEffectConfig;
export const AliasingEffectSchema: z.ZodType<AliasingEffectConfig> = z.union([
FreezeEffectSchema,
CreateEffectSchema,
CreateFromEffectSchema,
AssignEffectSchema,
AliasEffectSchema,
CaptureEffectSchema,
ImpureEffectSchema,
MutateEffectSchema,
MutateTransitiveConditionallySchema,
ApplyEffectSchema,
]);
export type AliasingSignatureConfig = {
receiver: string;
params: Array<string>;
rest: string | null;
returns: string;
effects: Array<AliasingEffectConfig>;
temporaries: Array<string>;
};
export const AliasingSignatureSchema: z.ZodType<AliasingSignatureConfig> =
z.object({
receiver: LifetimeIdSchema,
params: z.array(LifetimeIdSchema),
rest: LifetimeIdSchema.nullable(),
returns: LifetimeIdSchema,
effects: z.array(AliasingEffectSchema),
temporaries: z.array(LifetimeIdSchema),
});
export type FunctionTypeConfig = {
kind: 'function';
positionalParams: Array<Effect>;
@@ -42,6 +235,7 @@ export type FunctionTypeConfig = {
mutableOnlyIfOperandsAreMutable?: boolean | null | undefined;
impure?: boolean | null | undefined;
canonicalName?: string | null | undefined;
aliasing?: AliasingSignatureConfig | null | undefined;
};
export const FunctionTypeSchema: z.ZodType<FunctionTypeConfig> = z.object({
kind: z.literal('function'),
@@ -54,6 +248,7 @@ export const FunctionTypeSchema: z.ZodType<FunctionTypeConfig> = z.object({
mutableOnlyIfOperandsAreMutable: z.boolean().nullable().optional(),
impure: z.boolean().nullable().optional(),
canonicalName: z.string().nullable().optional(),
aliasing: AliasingSignatureSchema.nullable().optional(),
});
export type HookTypeConfig = {
@@ -63,6 +258,7 @@ export type HookTypeConfig = {
returnType: TypeConfig;
returnValueKind?: ValueKind | null | undefined;
noAlias?: boolean | null | undefined;
aliasing?: AliasingSignatureConfig | null | undefined;
};
export const HookTypeSchema: z.ZodType<HookTypeConfig> = z.object({
kind: z.literal('hook'),
@@ -71,6 +267,7 @@ export const HookTypeSchema: z.ZodType<HookTypeConfig> = z.object({
returnType: z.lazy(() => TypeSchema),
returnValueKind: ValueKindSchema.nullable().optional(),
noAlias: z.boolean().nullable().optional(),
aliasing: AliasingSignatureSchema.nullable().optional(),
});
export type BuiltInTypeConfig =

View File

@@ -8,6 +8,7 @@
import {CompilerErrorDetailOptions} from '../CompilerError';
import {
FunctionExpression,
GeneratedSource,
Hole,
IdentifierId,
ObjectMethod,
@@ -18,6 +19,7 @@ import {
ValueReason,
} from '../HIR';
import {FunctionSignature} from '../HIR/ObjectShape';
import {printSourceLocation} from '../HIR/PrintHIR';
/**
* `AliasingEffect` describes a set of "effects" that an instruction/terminal has on one or
@@ -200,10 +202,19 @@ export function hashEffect(effect: AliasingEffect): string {
return [effect.kind, effect.value.identifier.id, effect.reason].join(':');
}
case 'Impure':
case 'Render':
case 'Render': {
return [effect.kind, effect.place.identifier.id].join(':');
}
case 'MutateFrozen':
case 'MutateGlobal': {
return [effect.kind, effect.place.identifier.id].join(':');
return [
effect.kind,
effect.place.identifier.id,
effect.error.severity,
effect.error.reason,
effect.error.description,
printSourceLocation(effect.error.loc ?? GeneratedSource),
].join(':');
}
case 'Mutate':
case 'MutateConditionally':

View File

@@ -22,7 +22,6 @@ import {inferMutableRanges} from './InferMutableRanges';
import inferReferenceEffects from './InferReferenceEffects';
import {assertExhaustive} from '../Utils/utils';
import {inferMutationAliasingEffects} from './InferMutationAliasingEffects';
import {inferMutationAliasingFunctionEffects} from './InferMutationAliasingFunctionEffects';
import {inferMutationAliasingRanges} from './InferMutationAliasingRanges';
export default function analyseFunctions(func: HIRFunction): void {
@@ -68,19 +67,12 @@ function lowerWithMutationAliasing(fn: HIRFunction): void {
analyseFunctions(fn);
inferMutationAliasingEffects(fn, {isFunctionExpression: true});
deadCodeElimination(fn);
inferMutationAliasingRanges(fn, {isFunctionExpression: true});
const functionEffects = inferMutationAliasingRanges(fn, {
isFunctionExpression: true,
}).unwrap();
rewriteInstructionKindsBasedOnReassignment(fn);
inferReactiveScopeVariables(fn);
const effects = inferMutationAliasingFunctionEffects(fn);
fn.env.logger?.debugLogIRs?.({
kind: 'hir',
name: 'AnalyseFunction (inner)',
value: fn,
});
if (effects != null) {
fn.aliasingEffects ??= [];
fn.aliasingEffects?.push(...effects);
}
fn.aliasingEffects = functionEffects;
/**
* Phase 2: populate the Effect of each context variable to use in inferring
@@ -88,7 +80,7 @@ function lowerWithMutationAliasing(fn: HIRFunction): void {
* effects to decide if the function may be mutable or not.
*/
const capturedOrMutated = new Set<IdentifierId>();
for (const effect of effects ?? []) {
for (const effect of functionEffects) {
switch (effect.kind) {
case 'Assign':
case 'Alias':
@@ -140,6 +132,12 @@ function lowerWithMutationAliasing(fn: HIRFunction): void {
operand.effect = Effect.Read;
}
}
fn.env.logger?.debugLogIRs?.({
kind: 'hir',
name: 'AnalyseFunction (inner)',
value: fn,
});
}
function lower(func: HIRFunction): void {

View File

@@ -28,7 +28,9 @@ import {
isMapType,
isPrimitiveType,
isRefOrRefValue,
isRefValueType,
isSetType,
isUseRefType,
makeIdentifierId,
Phi,
Place,
@@ -38,6 +40,7 @@ import {
import {
eachInstructionValueLValue,
eachInstructionValueOperand,
eachTerminalOperand,
eachTerminalSuccessor,
} from '../HIR/visitors';
import {Ok, Result} from '../Utils/Result';
@@ -49,6 +52,7 @@ import {
} from './InferReferenceEffects';
import {
assertExhaustive,
getOrInsertDefault,
getOrInsertWith,
Set_isSuperset,
} from '../Utils/utils';
@@ -191,16 +195,15 @@ export function inferMutationAliasingEffects(
hoistedContextDeclarations,
);
let count = 0;
let iterationCount = 0;
while (queuedStates.size !== 0) {
count++;
if (count > 1000) {
console.log(
'oops infinite loop',
fn.id,
typeof fn.loc !== 'symbol' ? fn.loc?.filename : null,
);
throw new Error('infinite loop');
iterationCount++;
if (iterationCount > 100) {
CompilerError.invariant(false, {
reason: `[InferMutationAliasingEffects] Potential infinite loop`,
description: `A value, temporary place, or effect was not cached properly`,
loc: fn.loc,
});
}
for (const [blockId, block] of fn.body.blocks) {
const incomingState = queuedStates.get(blockId);
@@ -218,11 +221,25 @@ export function inferMutationAliasingEffects(
}
}
}
if (fn.env.config.validateRefAccessDuringRender) {
inferRefAccessEffects(fn, isFunctionExpression);
}
return Ok(undefined);
}
function findHoistedContextDeclarations(fn: HIRFunction): Set<DeclarationId> {
const hoisted = new Set<DeclarationId>();
function findHoistedContextDeclarations(
fn: HIRFunction,
): Map<DeclarationId, Place | null> {
const hoisted = new Map<DeclarationId, Place | null>();
function visit(place: Place): void {
if (
hoisted.has(place.identifier.declarationId) &&
hoisted.get(place.identifier.declarationId) == null
) {
// If this is the first load of the value, store the location
hoisted.set(place.identifier.declarationId, place);
}
}
for (const block of fn.body.blocks.values()) {
for (const instr of block.instructions) {
if (instr.value.kind === 'DeclareContext') {
@@ -232,10 +249,17 @@ function findHoistedContextDeclarations(fn: HIRFunction): Set<DeclarationId> {
kind == InstructionKind.HoistedFunction ||
kind == InstructionKind.HoistedLet
) {
hoisted.add(instr.value.lvalue.place.identifier.declarationId);
hoisted.set(instr.value.lvalue.place.identifier.declarationId, null);
}
} else {
for (const operand of eachInstructionValueOperand(instr.value)) {
visit(operand);
}
}
}
for (const operand of eachTerminalOperand(block.terminal)) {
visit(operand);
}
}
return hoisted;
}
@@ -245,21 +269,40 @@ class Context {
instructionSignatureCache: Map<Instruction, InstructionSignature> = new Map();
effectInstructionValueCache: Map<AliasingEffect, InstructionValue> =
new Map();
applySignatureCache: Map<
AliasingSignature,
Map<AliasingEffect, Array<AliasingEffect> | null>
> = new Map();
catchHandlers: Map<BlockId, Place> = new Map();
functionSignatureCache: Map<FunctionExpression, AliasingSignature> =
new Map();
isFuctionExpression: boolean;
fn: HIRFunction;
hoistedContextDeclarations: Set<DeclarationId>;
hoistedContextDeclarations: Map<DeclarationId, Place | null>;
constructor(
isFunctionExpression: boolean,
fn: HIRFunction,
hoistedContextDeclarations: Set<DeclarationId>,
hoistedContextDeclarations: Map<DeclarationId, Place | null>,
) {
this.isFuctionExpression = isFunctionExpression;
this.fn = fn;
this.hoistedContextDeclarations = hoistedContextDeclarations;
}
cacheApplySignature(
signature: AliasingSignature,
effect: Extract<AliasingEffect, {kind: 'Apply'}>,
f: () => Array<AliasingEffect> | null,
): Array<AliasingEffect> | null {
const inner = getOrInsertDefault(
this.applySignatureCache,
signature,
new Map(),
);
return getOrInsertWith(inner, effect, f);
}
internEffect(effect: AliasingEffect): AliasingEffect {
const hash = hashEffect(effect);
let interned = this.internedEffects.get(hash);
@@ -314,6 +357,11 @@ function inferBlock(
} else if (terminal.kind === 'maybe-throw') {
const handlerParam = context.catchHandlers.get(terminal.handler);
if (handlerParam != null) {
CompilerError.invariant(state.kind(handlerParam) != null, {
reason:
'Expected catch binding to be intialized with a DeclareLocal Catch instruction',
loc: terminal.loc,
});
const effects: Array<AliasingEffect> = [];
for (const instr of block.instructions) {
if (
@@ -333,11 +381,13 @@ function inferBlock(
state.appendAlias(handlerParam, instr.lvalue);
const kind = state.kind(instr.lvalue).kind;
if (kind === ValueKind.Mutable || kind == ValueKind.Context) {
effects.push({
kind: 'Alias',
from: instr.lvalue,
into: handlerParam,
});
effects.push(
context.internEffect({
kind: 'Alias',
from: instr.lvalue,
into: handlerParam,
}),
);
}
}
}
@@ -346,11 +396,11 @@ function inferBlock(
} else if (terminal.kind === 'return') {
if (!context.isFuctionExpression) {
terminal.effects = [
{
context.internEffect({
kind: 'Freeze',
value: terminal.value,
reason: ValueReason.JsxCaptured,
},
}),
];
}
}
@@ -426,14 +476,14 @@ function applySignature(
* Track which values we've already aliased once, so that we can switch to
* appendAlias() for subsequent aliases into the same value
*/
const aliased = new Set<IdentifierId>();
const initialized = new Set<IdentifierId>();
if (DEBUG) {
console.log(printInstruction(instruction));
}
for (const effect of signature.effects) {
applyEffect(context, state, effect, aliased, effects);
applyEffect(context, state, effect, initialized, effects);
}
if (DEBUG) {
console.log(
@@ -458,7 +508,7 @@ function applyEffect(
context: Context,
state: InferenceState,
_effect: AliasingEffect,
aliased: Set<IdentifierId>,
initialized: Set<IdentifierId>,
effects: Array<AliasingEffect>,
): void {
const effect = context.internEffect(_effect);
@@ -474,6 +524,13 @@ function applyEffect(
break;
}
case 'Create': {
CompilerError.invariant(!initialized.has(effect.into.identifier.id), {
reason: `Cannot re-initialize variable within an instruction`,
description: `Re-initialized ${printPlace(effect.into)} in ${printAliasingEffect(effect)}`,
loc: effect.into.loc,
});
initialized.add(effect.into.identifier.id);
let value = context.effectInstructionValueCache.get(effect);
if (value == null) {
value = {
@@ -488,6 +545,7 @@ function applyEffect(
reason: new Set([effect.reason]),
});
state.define(effect.into, value);
effects.push(effect);
break;
}
case 'ImmutableCapture': {
@@ -505,6 +563,13 @@ function applyEffect(
break;
}
case 'CreateFrom': {
CompilerError.invariant(!initialized.has(effect.into.identifier.id), {
reason: `Cannot re-initialize variable within an instruction`,
description: `Re-initialized ${printPlace(effect.into)} in ${printAliasingEffect(effect)}`,
loc: effect.into.loc,
});
initialized.add(effect.into.identifier.id);
const fromValue = state.kind(effect.from);
let value = context.effectInstructionValueCache.get(effect);
if (value == null) {
@@ -523,29 +588,48 @@ function applyEffect(
switch (fromValue.kind) {
case ValueKind.Primitive:
case ValueKind.Global: {
// no need to track this data flow
effects.push({
kind: 'Create',
value: fromValue.kind,
into: effect.into,
reason: [...fromValue.reason][0] ?? ValueReason.Other,
});
break;
}
case ValueKind.Frozen: {
effects.push({
kind: 'ImmutableCapture',
from: effect.from,
kind: 'Create',
value: fromValue.kind,
into: effect.into,
reason: [...fromValue.reason][0] ?? ValueReason.Other,
});
applyEffect(
context,
state,
{
kind: 'ImmutableCapture',
from: effect.from,
into: effect.into,
},
initialized,
effects,
);
break;
}
default: {
effects.push({
// OK: recording information flow
kind: 'CreateFrom', // prev Alias
from: effect.from,
into: effect.into,
});
effects.push(effect);
}
}
break;
}
case 'CreateFunction': {
CompilerError.invariant(!initialized.has(effect.into.identifier.id), {
reason: `Cannot re-initialize variable within an instruction`,
description: `Re-initialized ${printPlace(effect.into)} in ${printAliasingEffect(effect)}`,
loc: effect.into.loc,
});
initialized.add(effect.into.identifier.id);
effects.push(effect);
/**
* We consider the function mutable if it has any mutable context variables or
@@ -602,7 +686,7 @@ function applyEffect(
from: capture,
into: effect.into,
},
aliased,
initialized,
effects,
);
}
@@ -610,6 +694,14 @@ function applyEffect(
}
case 'Alias':
case 'Capture': {
CompilerError.invariant(
effect.kind === 'Capture' || initialized.has(effect.into.identifier.id),
{
reason: `Expected destination value to already be initialized within this instruction for Alias effect`,
description: `Destination ${printPlace(effect.into)} is not initialized in this instruction`,
loc: effect.into.loc,
},
);
/*
* Capture describes potential information flow: storing a pointer to one value
* within another. If the destination is not mutable, or the source value has
@@ -639,11 +731,17 @@ function applyEffect(
}
case ValueKind.Frozen: {
isMutableReferenceType = false;
effects.push({
kind: 'ImmutableCapture',
from: effect.from,
into: effect.into,
});
applyEffect(
context,
state,
{
kind: 'ImmutableCapture',
from: effect.from,
into: effect.into,
},
initialized,
effects,
);
break;
}
default: {
@@ -657,6 +755,13 @@ function applyEffect(
break;
}
case 'Assign': {
CompilerError.invariant(!initialized.has(effect.into.identifier.id), {
reason: `Cannot re-initialize variable within an instruction`,
description: `Re-initialized ${printPlace(effect.into)} in ${printAliasingEffect(effect)}`,
loc: effect.into.loc,
});
initialized.add(effect.into.identifier.id);
/*
* Alias represents potential pointer aliasing. If the type is a global,
* a primitive (copy-on-write semantics) then we can prune the effect
@@ -665,11 +770,17 @@ function applyEffect(
const fromKind = fromValue.kind;
switch (fromKind) {
case ValueKind.Frozen: {
effects.push({
kind: 'ImmutableCapture',
from: effect.from,
into: effect.into,
});
applyEffect(
context,
state,
{
kind: 'ImmutableCapture',
from: effect.from,
into: effect.into,
},
initialized,
effects,
);
let value = context.effectInstructionValueCache.get(effect);
if (value == null) {
value = {
@@ -705,12 +816,7 @@ function applyEffect(
break;
}
default: {
if (aliased.has(effect.into.identifier.id)) {
state.appendAlias(effect.into, effect.from);
} else {
aliased.add(effect.into.identifier.id);
state.alias(effect.into, effect.from);
}
state.assign(effect.into, effect.from);
effects.push(effect);
break;
}
@@ -721,70 +827,78 @@ function applyEffect(
const functionValues = state.values(effect.function);
if (
functionValues.length === 1 &&
functionValues[0].kind === 'FunctionExpression'
functionValues[0].kind === 'FunctionExpression' &&
functionValues[0].loweredFunc.func.aliasingEffects != null
) {
/*
* We're calling a locally declared function, we already know it's effects!
* We just have to substitute in the args for the params
*/
const signature = buildSignatureFromFunctionExpression(
state.env,
functionValues[0],
);
const functionExpr = functionValues[0];
let signature = context.functionSignatureCache.get(functionExpr);
if (signature == null) {
signature = buildSignatureFromFunctionExpression(
state.env,
functionExpr,
);
context.functionSignatureCache.set(functionExpr, signature);
}
if (DEBUG) {
console.log(
`constructed alias signature:\n${printAliasingSignature(signature)}`,
);
}
const signatureEffects = computeEffectsForSignature(
state.env,
const signatureEffects = context.cacheApplySignature(
signature,
effect.into,
effect.receiver,
effect.args,
functionValues[0].loweredFunc.func.context,
effect.loc,
effect,
() =>
computeEffectsForSignature(
state.env,
signature,
effect.into,
effect.receiver,
effect.args,
functionExpr.loweredFunc.func.context,
effect.loc,
),
);
if (signatureEffects != null) {
if (DEBUG) {
console.log('apply function expression effects');
}
applyEffect(
context,
state,
{kind: 'MutateTransitiveConditionally', value: effect.function},
aliased,
initialized,
effects,
);
for (const signatureEffect of signatureEffects) {
applyEffect(context, state, signatureEffect, aliased, effects);
applyEffect(context, state, signatureEffect, initialized, effects);
}
break;
}
}
const signatureEffects =
effect.signature?.aliasing != null
? computeEffectsForSignature(
let signatureEffects = null;
if (effect.signature?.aliasing != null) {
const signature = effect.signature.aliasing;
signatureEffects = context.cacheApplySignature(
effect.signature.aliasing,
effect,
() =>
computeEffectsForSignature(
state.env,
effect.signature.aliasing,
signature,
effect.into,
effect.receiver,
effect.args,
[],
effect.loc,
)
: null;
),
);
}
if (signatureEffects != null) {
if (DEBUG) {
console.log('apply aliasing signature effects');
}
for (const signatureEffect of signatureEffects) {
applyEffect(context, state, signatureEffect, aliased, effects);
applyEffect(context, state, signatureEffect, initialized, effects);
}
} else if (effect.signature != null) {
if (DEBUG) {
console.log('apply legacy signature effects');
}
const legacyEffects = computeEffectsForLegacySignature(
state,
effect.signature,
@@ -794,12 +908,9 @@ function applyEffect(
effect.loc,
);
for (const legacyEffect of legacyEffects) {
applyEffect(context, state, legacyEffect, aliased, effects);
applyEffect(context, state, legacyEffect, initialized, effects);
}
} else {
if (DEBUG) {
console.log('default effects');
}
applyEffect(
context,
state,
@@ -809,7 +920,7 @@ function applyEffect(
value: ValueKind.Mutable,
reason: ValueReason.Other,
},
aliased,
initialized,
effects,
);
/*
@@ -832,21 +943,21 @@ function applyEffect(
kind: 'MutateTransitiveConditionally',
value: operand,
},
aliased,
initialized,
effects,
);
}
const mutateIterator =
arg.kind === 'Spread' ? conditionallyMutateIterator(operand) : null;
if (mutateIterator) {
applyEffect(context, state, mutateIterator, aliased, effects);
applyEffect(context, state, mutateIterator, initialized, effects);
}
applyEffect(
context,
state,
// OK: recording information flow
{kind: 'Alias', from: operand, into: effect.into},
aliased,
initialized,
effects,
);
for (const otherArg of [
@@ -874,7 +985,7 @@ function applyEffect(
from: operand,
into: other,
},
aliased,
initialized,
effects,
);
}
@@ -901,48 +1012,89 @@ function applyEffect(
console.log(prettyFormat(state.debugAbstractValue(value)));
}
let reason: string;
let description: string | null = null;
if (
mutationKind === 'mutate-frozen' &&
context.hoistedContextDeclarations.has(
effect.value.identifier.declarationId,
)
) {
reason = `This variable is accessed before it is declared, which prevents the earlier access from updating when this value changes over time`;
if (
const description =
effect.value.identifier.name !== null &&
effect.value.identifier.name.kind === 'named'
) {
description = `Move the declaration of \`${effect.value.identifier.name.value}\` to before it is first referenced`;
? `Variable \`${effect.value.identifier.name.value}\` is accessed before it is declared`
: null;
const hoistedAccess = context.hoistedContextDeclarations.get(
effect.value.identifier.declarationId,
);
if (hoistedAccess != null && hoistedAccess.loc != effect.value.loc) {
applyEffect(
context,
state,
{
kind: 'MutateFrozen',
place: effect.value,
error: {
severity: ErrorSeverity.InvalidReact,
reason: `This variable is accessed before it is declared, which may prevent it from updating as the assigned value changes over time`,
description,
loc: hoistedAccess.loc,
suggestions: null,
},
},
initialized,
effects,
);
}
applyEffect(
context,
state,
{
kind: 'MutateFrozen',
place: effect.value,
error: {
severity: ErrorSeverity.InvalidReact,
reason: `This variable is accessed before it is declared, which prevents the earlier access from updating when this value changes over time`,
description,
loc: effect.value.loc,
suggestions: null,
},
},
initialized,
effects,
);
} else {
reason = getWriteErrorReason({
const reason = getWriteErrorReason({
kind: value.kind,
reason: value.reason,
context: new Set(),
});
if (
const description =
effect.value.identifier.name !== null &&
effect.value.identifier.name.kind === 'named'
) {
description = `Found mutation of \`${effect.value.identifier.name.value}\``;
}
? `Found mutation of \`${effect.value.identifier.name.value}\``
: null;
applyEffect(
context,
state,
{
kind:
value.kind === ValueKind.Frozen
? 'MutateFrozen'
: 'MutateGlobal',
place: effect.value,
error: {
severity: ErrorSeverity.InvalidReact,
reason,
description,
loc: effect.value.loc,
suggestions: null,
},
},
initialized,
effects,
);
}
effects.push({
kind:
value.kind === ValueKind.Frozen ? 'MutateFrozen' : 'MutateGlobal',
place: effect.value,
error: {
severity: ErrorSeverity.InvalidReact,
reason,
description,
loc: effect.value.loc,
suggestions: null,
},
});
}
break;
}
@@ -1046,7 +1198,7 @@ class InferenceState {
}
// Updates the value at @param place to point to the same value as @param value.
alias(place: Place, value: Place): void {
assign(place: Place, value: Place): void {
const values = this.#variables.get(value.identifier.id);
CompilerError.invariant(values != null, {
reason: `[InferMutationAliasingEffects] Expected value for identifier to be initialized`,
@@ -1124,9 +1276,6 @@ class InferenceState {
kind: ValueKind.Frozen,
reason: new Set([reason]),
});
if (DEBUG) {
console.log(`freeze value: ${printInstructionValue(value)} ${reason}`);
}
if (
value.kind === 'FunctionExpression' &&
(this.env.config.enablePreserveExistingMemoizationGuarantees ||
@@ -1983,8 +2132,6 @@ function computeEffectsForLegacySignature(
const mutateIterator = conditionallyMutateIterator(place);
if (mutateIterator != null) {
effects.push(mutateIterator);
// TODO: should we always push to captures?
captures.push(place);
}
effects.push({
kind: 'Capture',
@@ -2166,17 +2313,6 @@ function computeEffectsForSignature(
// Too many args and there is no rest param to hold them
(args.length > signature.params.length && signature.rest == null)
) {
if (DEBUG) {
if (signature.params.length > args.length) {
console.log(
`not enough args: ${args.length} args for ${signature.params.length} params`,
);
} else {
console.log(
`too many args: ${args.length} args for ${signature.params.length} params, with no rest param`,
);
}
}
return null;
}
// Build substitutions
@@ -2191,9 +2327,6 @@ function computeEffectsForSignature(
continue;
} else if (params == null || i >= params.length || arg.kind === 'Spread') {
if (signature.rest == null) {
if (DEBUG) {
console.log(`no rest value to hold param`);
}
return null;
}
const place = arg.kind === 'Identifier' ? arg : arg.place;
@@ -2301,23 +2434,14 @@ function computeEffectsForSignature(
case 'Apply': {
const applyReceiver = substitutions.get(effect.receiver.identifier.id);
if (applyReceiver == null || applyReceiver.length !== 1) {
if (DEBUG) {
console.log(`too many substitutions for receiver`);
}
return null;
}
const applyFunction = substitutions.get(effect.function.identifier.id);
if (applyFunction == null || applyFunction.length !== 1) {
if (DEBUG) {
console.log(`too many substitutions for function`);
}
return null;
}
const applyInto = substitutions.get(effect.into.identifier.id);
if (applyInto == null || applyInto.length !== 1) {
if (DEBUG) {
console.log(`too many substitutions for into`);
}
return null;
}
const applyArgs: Array<Place | SpreadPattern | Hole> = [];
@@ -2327,18 +2451,12 @@ function computeEffectsForSignature(
} else if (arg.kind === 'Identifier') {
const applyArg = substitutions.get(arg.identifier.id);
if (applyArg == null || applyArg.length !== 1) {
if (DEBUG) {
console.log(`too many substitutions for arg`);
}
return null;
}
applyArgs.push(applyArg[0]);
} else {
const applyArg = substitutions.get(arg.place.identifier.id);
if (applyArg == null || applyArg.length !== 1) {
if (DEBUG) {
console.log(`too many substitutions for arg`);
}
return null;
}
applyArgs.push({kind: 'Spread', place: applyArg[0]});
@@ -2400,3 +2518,127 @@ export type AbstractValue = {
kind: ValueKind;
reason: ReadonlySet<ValueReason>;
};
function inferRefAccessEffects(
fn: HIRFunction,
_isFunctionExpression: boolean,
): void {
const nullish = new Set<IdentifierId>();
const nullishTest = new Map<IdentifierId, Place>();
let guard: {ref: IdentifierId; fallthrough: BlockId} | null = null;
const temporaries: Map<IdentifierId, Place> = new Map();
function visitOperand(operand: Place): AliasingEffect | null {
const nullTestRef = nullishTest.get(operand.identifier.id);
if (isRefValueType(operand.identifier) || nullTestRef != null) {
const refOperand = nullTestRef ?? operand;
return {
kind: 'Impure',
error: {
severity: ErrorSeverity.InvalidReact,
reason:
'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)',
loc: refOperand.loc,
description:
refOperand.identifier.name !== null &&
refOperand.identifier.name.kind === 'named'
? `Cannot access ref value \`${refOperand.identifier.name.value}\``
: null,
suggestions: null,
},
place: refOperand,
};
}
return null;
}
for (const block of fn.body.blocks.values()) {
if (guard !== null && guard.fallthrough === block.id) {
guard = null;
}
for (const instr of block.instructions) {
const {lvalue, value} = instr;
if (value.kind === 'LoadLocal' && isUseRefType(value.place.identifier)) {
temporaries.set(lvalue.identifier.id, value.place);
} else if (
value.kind === 'StoreLocal' &&
isUseRefType(value.value.identifier)
) {
temporaries.set(value.lvalue.place.identifier.id, value.value);
temporaries.set(lvalue.identifier.id, value.value);
} else if (
value.kind === 'BinaryExpression' &&
((isRefValueType(value.left.identifier) &&
nullish.has(value.right.identifier.id)) ||
(nullish.has(value.left.identifier.id) &&
isRefValueType(value.right.identifier)))
) {
const refOperand = isRefValueType(value.left.identifier)
? value.left
: value.right;
const operand = temporaries.get(refOperand.identifier.id) ?? refOperand;
nullishTest.set(lvalue.identifier.id, operand);
} else if (value.kind === 'Primitive' && value.value == null) {
nullish.add(lvalue.identifier.id);
} else if (
value.kind === 'PropertyLoad' &&
isUseRefType(value.object.identifier) &&
value.property === 'current'
) {
const refOperand =
temporaries.get(value.object.identifier.id) ?? value.object;
temporaries.set(lvalue.identifier.id, refOperand);
} else if (
value.kind === 'PropertyStore' &&
value.property === 'current' &&
isUseRefType(value.object.identifier)
) {
const refOperand =
temporaries.get(value.object.identifier.id) ?? value.object;
if (guard != null && refOperand.identifier.id === guard.ref) {
// Allow a single write within the guard
guard = null;
} else {
instr.effects ??= [];
instr.effects.push({
kind: 'Impure',
error: {
severity: ErrorSeverity.InvalidReact,
reason:
'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)',
loc: value.loc,
description:
value.object.identifier.name !== null &&
value.object.identifier.name.kind === 'named'
? `Cannot access ref value \`${value.object.identifier.name.value}\``
: null,
suggestions: null,
},
place: value.object,
});
}
} else {
for (const operand of eachInstructionValueOperand(value)) {
const error = visitOperand(operand);
if (error) {
instr.effects ??= [];
instr.effects.push(error);
}
}
}
}
if (
guard == null &&
block.terminal.kind === 'if' &&
nullishTest.has(block.terminal.test.identifier.id)
) {
const ref = nullishTest.get(block.terminal.test.identifier.id)!;
guard = {ref: ref.identifier.id, fallthrough: block.terminal.fallthrough};
} else {
for (const operand of eachTerminalOperand(block.terminal)) {
const _effect = visitOperand(operand);
// TODO: need a place to store terminal effects generically
}
}
}
}

View File

@@ -1,206 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {HIRFunction, IdentifierId, Place, ValueKind, ValueReason} from '../HIR';
import {getOrInsertDefault} from '../Utils/utils';
import {AliasingEffect} from './AliasingEffects';
/**
* This function tracks data flow within an inner function expression in order to
* compute a set of data-flow aliasing effects describing data flow between the function's
* params, context variables, and return value.
*
* For example, consider the following function expression:
*
* ```
* (x) => { return [x, y] }
* ```
*
* This function captures both param `x` and context variable `y` into the return value.
* Unlike our previous inference which counted this as a mutation of x and y, we want to
* build a signature for the function that describes the data flow. We would infer
* `Capture x -> return, Capture y -> return` effects for this function.
*
* This function *also* propagates more ambient-style effects (MutateFrozen, MutateGlobal, Impure, Render)
* from instructions within the function up to the function itself.
*/
export function inferMutationAliasingFunctionEffects(
fn: HIRFunction,
): Array<AliasingEffect> | null {
const effects: Array<AliasingEffect> = [];
/**
* Map used to identify tracked variables: params, context vars, return value
* This is used to detect mutation/capturing/aliasing of params/context vars
*/
const tracked = new Map<IdentifierId, Place>();
tracked.set(fn.returns.identifier.id, fn.returns);
for (const operand of [...fn.context, ...fn.params]) {
const place = operand.kind === 'Identifier' ? operand : operand.place;
tracked.set(place.identifier.id, place);
}
/**
* Track capturing/aliasing of context vars and params into each other and into the return.
* We don't need to track locals and intermediate values, since we're only concerned with effects
* as they relate to arguments visible outside the function.
*
* For each aliased identifier we track capture/alias/createfrom and then merge this with how
* the value is used. Eg capturing an alias => capture. See joinEffects() helper.
*/
type AliasedIdentifier = {
kind: AliasingKind;
place: Place;
};
const dataFlow = new Map<IdentifierId, Array<AliasedIdentifier>>();
/*
* Check for aliasing of tracked values. Also joins the effects of how the value is
* used (@param kind) with the aliasing type of each value
*/
function lookup(
place: Place,
kind: AliasedIdentifier['kind'],
): Array<AliasedIdentifier> | null {
if (tracked.has(place.identifier.id)) {
return [{kind, place}];
}
return (
dataFlow.get(place.identifier.id)?.map(aliased => ({
kind: joinEffects(aliased.kind, kind),
place: aliased.place,
})) ?? null
);
}
// todo: fixpoint
for (const block of fn.body.blocks.values()) {
for (const phi of block.phis) {
const operands: Array<AliasedIdentifier> = [];
for (const operand of phi.operands.values()) {
const inputs = lookup(operand, 'Alias');
if (inputs != null) {
operands.push(...inputs);
}
}
if (operands.length !== 0) {
dataFlow.set(phi.place.identifier.id, operands);
}
}
for (const instr of block.instructions) {
if (instr.effects == null) continue;
for (const effect of instr.effects) {
if (
effect.kind === 'Assign' ||
effect.kind === 'Capture' ||
effect.kind === 'Alias' ||
effect.kind === 'CreateFrom'
) {
const from = lookup(effect.from, effect.kind);
if (from == null) {
continue;
}
const into = lookup(effect.into, 'Alias');
if (into == null) {
getOrInsertDefault(dataFlow, effect.into.identifier.id, []).push(
...from,
);
} else {
for (const aliased of into) {
getOrInsertDefault(
dataFlow,
aliased.place.identifier.id,
[],
).push(...from);
}
}
} else if (
effect.kind === 'Create' ||
effect.kind === 'CreateFunction'
) {
getOrInsertDefault(dataFlow, effect.into.identifier.id, [
{kind: 'Alias', place: effect.into},
]);
} else if (
effect.kind === 'MutateFrozen' ||
effect.kind === 'MutateGlobal' ||
effect.kind === 'Impure' ||
effect.kind === 'Render'
) {
effects.push(effect);
}
}
}
if (block.terminal.kind === 'return') {
const from = lookup(block.terminal.value, 'Alias');
if (from != null) {
getOrInsertDefault(dataFlow, fn.returns.identifier.id, []).push(
...from,
);
}
}
}
// Create aliasing effects based on observed data flow
let hasReturn = false;
for (const [into, from] of dataFlow) {
const input = tracked.get(into);
if (input == null) {
continue;
}
for (const aliased of from) {
if (
aliased.place.identifier.id === input.identifier.id ||
!tracked.has(aliased.place.identifier.id)
) {
continue;
}
const effect = {kind: aliased.kind, from: aliased.place, into: input};
effects.push(effect);
if (
into === fn.returns.identifier.id &&
(aliased.kind === 'Assign' || aliased.kind === 'CreateFrom')
) {
hasReturn = true;
}
}
}
// TODO: more precise return effect inference
if (!hasReturn) {
effects.unshift({
kind: 'Create',
into: fn.returns,
value:
fn.returnType.kind === 'Primitive'
? ValueKind.Primitive
: ValueKind.Mutable,
reason: ValueReason.KnownReturnSignature,
});
}
return effects;
}
export enum MutationKind {
None = 0,
Conditional = 1,
Definite = 2,
}
type AliasingKind = 'Alias' | 'Capture' | 'CreateFrom' | 'Assign';
function joinEffects(
effect1: AliasingKind,
effect2: AliasingKind,
): AliasingKind {
if (effect1 === 'Capture' || effect2 === 'Capture') {
return 'Capture';
} else if (effect1 === 'Assign' || effect2 === 'Assign') {
return 'Assign';
} else {
return 'Alias';
}
}

View File

@@ -5,7 +5,6 @@
* LICENSE file in the root directory of this source tree.
*/
import prettyFormat from 'pretty-format';
import {CompilerError, SourceLocation} from '..';
import {
BlockId,
@@ -14,8 +13,12 @@ import {
Identifier,
IdentifierId,
InstructionId,
isJsxType,
makeInstructionId,
ValueKind,
ValueReason,
Place,
isPrimitiveType,
} from '../HIR/HIR';
import {
eachInstructionLValue,
@@ -23,43 +26,58 @@ import {
eachTerminalOperand,
} from '../HIR/visitors';
import {assertExhaustive, getOrInsertWith} from '../Utils/utils';
import {printFunction} from '../HIR';
import {printIdentifier, printPlace} from '../HIR/PrintHIR';
import {MutationKind} from './InferMutationAliasingFunctionEffects';
import {Result} from '../Utils/Result';
const DEBUG = false;
const VERBOSE = false;
import {Err, Ok, Result} from '../Utils/Result';
import {AliasingEffect} from './AliasingEffects';
/**
* Infers mutable ranges for all values in the program, using previously inferred
* mutation/aliasing effects. This pass builds a data flow graph using the effects,
* tracking an abstract notion of "when" each effect occurs relative to the others.
* It then walks each mutation effect against the graph, updating the range of each
* node that would be reachable at the "time" that the effect occurred.
* This pass builds an abstract model of the heap and interprets the effects of the
* given function in order to determine the following:
* - The mutable ranges of all identifiers in the function
* - The externally-visible effects of the function, such as mutations of params and
* context-vars, aliasing between params/context-vars/return-value, and impure side
* effects.
* - The legacy `Effect` to store on each Place.
*
* This pass builds a data flow graph using the effects, tracking an abstract notion
* of "when" each effect occurs relative to the others. It then walks each mutation
* effect against the graph, updating the range of each node that would be reachable
* at the "time" that the effect occurred.
*
* This pass also validates against invalid effects: any function that is reachable
* by being called, or via a Render effect, is validated against mutating globals
* or calling impure code.
*
* Note that this function also populates the outer function's aliasing effects with
* any mutations that apply to its params or context variables. For example, a
* function expression such as the following:
* any mutations that apply to its params or context variables.
*
* ## Example
* A function expression such as the following:
*
* ```
* (x) => { x.y = true }
* ```
*
* Would populate a `Mutate x` aliasing effect on the outer function.
*
* ## Returned Function Effects
*
* The function returns (if successful) a list of externally-visible effects.
* This is determined by simulating a conditional, transitive mutation against
* each param, context variable, and return value in turn, and seeing which other
* such values are affected. If they're affected, they must be captured, so we
* record a Capture.
*
* The only tricky bit is the return value, which could _alias_ (or even assign)
* one or more of the params/context-vars rather than just capturing. So we have
* to do a bit more tracking for returns.
*/
export function inferMutationAliasingRanges(
fn: HIRFunction,
{isFunctionExpression}: {isFunctionExpression: boolean},
): Result<void, CompilerError> {
if (VERBOSE) {
console.log();
console.log(printFunction(fn));
}
): Result<Array<AliasingEffect>, CompilerError> {
// The set of externally-visible effects
const functionEffects: Array<AliasingEffect> = [];
/**
* Part 1: Infer mutable ranges for values. We build an abstract model of
* values, the alias/capture edges between them, and the set of mutations.
@@ -115,20 +133,6 @@ export function inferMutationAliasingRanges(
seenBlocks.add(block.id);
for (const instr of block.instructions) {
if (
instr.value.kind === 'FunctionExpression' ||
instr.value.kind === 'ObjectMethod'
) {
state.create(instr.lvalue, {
kind: 'Function',
function: instr.value.loweredFunc.func,
});
} else {
for (const lvalue of eachInstructionLValue(instr)) {
state.create(lvalue, {kind: 'Object'});
}
}
if (instr.effects == null) continue;
for (const effect of instr.effects) {
if (effect.kind === 'Create') {
@@ -141,6 +145,15 @@ export function inferMutationAliasingRanges(
} else if (effect.kind === 'CreateFrom') {
state.createFrom(index++, effect.from, effect.into);
} else if (effect.kind === 'Assign') {
/**
* TODO: Invariant that the node is not initialized yet
*
* InferFunctionExpressionAliasingEffectSignatures currently infers
* Assign effects in some places that should be Alias, leading to
* Assign effects that reinitialize a value. The end result appears to
* be fine, but we should fix that inference pass so that we add the
* invariant here.
*/
if (!state.nodes.has(effect.into.identifier)) {
state.create(effect.into, {kind: 'Object'});
}
@@ -183,8 +196,10 @@ export function inferMutationAliasingRanges(
effect.kind === 'Impure'
) {
errors.push(effect.error);
functionEffects.push(effect);
} else if (effect.kind === 'Render') {
renders.push({index: index++, place: effect.place});
functionEffects.push(effect);
}
}
}
@@ -216,10 +231,6 @@ export function inferMutationAliasingRanges(
}
}
if (VERBOSE) {
console.log(state.debug());
console.log(pretty(mutations));
}
for (const mutation of mutations) {
state.mutate(
mutation.index,
@@ -234,10 +245,6 @@ export function inferMutationAliasingRanges(
for (const render of renders) {
state.render(render.index, render.place.identifier, errors);
}
if (DEBUG) {
console.log(pretty([...state.nodes.keys()]));
}
fn.aliasingEffects ??= [];
for (const param of [...fn.context, ...fn.params]) {
const place = param.kind === 'Identifier' ? param : param.place;
const node = state.nodes.get(place.identifier);
@@ -248,13 +255,13 @@ export function inferMutationAliasingRanges(
if (node.local != null) {
if (node.local.kind === MutationKind.Conditional) {
mutated = true;
fn.aliasingEffects.push({
functionEffects.push({
kind: 'MutateConditionally',
value: {...place, loc: node.local.loc},
});
} else if (node.local.kind === MutationKind.Definite) {
mutated = true;
fn.aliasingEffects.push({
functionEffects.push({
kind: 'Mutate',
value: {...place, loc: node.local.loc},
});
@@ -263,13 +270,13 @@ export function inferMutationAliasingRanges(
if (node.transitive != null) {
if (node.transitive.kind === MutationKind.Conditional) {
mutated = true;
fn.aliasingEffects.push({
functionEffects.push({
kind: 'MutateTransitiveConditionally',
value: {...place, loc: node.transitive.loc},
});
} else if (node.transitive.kind === MutationKind.Definite) {
mutated = true;
fn.aliasingEffects.push({
functionEffects.push({
kind: 'MutateTransitive',
value: {...place, loc: node.transitive.loc},
});
@@ -458,10 +465,82 @@ export function inferMutationAliasingRanges(
}
}
if (VERBOSE) {
console.log(printFunction(fn));
/**
* Part 3
* Finish populating the externally visible effects. Above we bubble-up the side effects
* (MutateFrozen/MutableGlobal/Impure/Render) as well as mutations of context variables.
* Here we populate an effect to create the return value as well as populating alias/capture
* effects for how data flows between the params, context vars, and return.
*/
const returns = fn.returns.identifier;
functionEffects.push({
kind: 'Create',
into: fn.returns,
value: isPrimitiveType(returns)
? ValueKind.Primitive
: isJsxType(returns.type)
? ValueKind.Frozen
: ValueKind.Mutable,
reason: ValueReason.KnownReturnSignature,
});
/**
* Determine precise data-flow effects by simulating transitive mutations of the params/
* captures and seeing what other params/context variables are affected. Anything that
* would be transitively mutated needs a capture relationship.
*/
const tracked: Array<Place> = [];
const ignoredErrors = new CompilerError();
for (const param of [...fn.params, ...fn.context, fn.returns]) {
const place = param.kind === 'Identifier' ? param : param.place;
tracked.push(place);
}
return errors.asResult();
for (const into of tracked) {
const mutationIndex = index++;
state.mutate(
mutationIndex,
into.identifier,
null,
true,
MutationKind.Conditional,
into.loc,
ignoredErrors,
);
for (const from of tracked) {
if (
from.identifier.id === into.identifier.id ||
from.identifier.id === fn.returns.identifier.id
) {
continue;
}
const fromNode = state.nodes.get(from.identifier);
CompilerError.invariant(fromNode != null, {
reason: `Expected a node to exist for all parameters and context variables`,
loc: into.loc,
});
if (fromNode.lastMutated === mutationIndex) {
if (into.identifier.id === fn.returns.identifier.id) {
// The return value could be any of the params/context variables
functionEffects.push({
kind: 'Alias',
from,
into,
});
} else {
// Otherwise params/context-vars can only capture each other
functionEffects.push({
kind: 'Capture',
from,
into,
});
}
}
}
}
if (errors.hasErrors() && !isFunctionExpression) {
return Err(errors);
}
return Ok(functionEffects);
}
function appendFunctionErrors(errors: CompilerError, fn: HIRFunction): void {
@@ -477,6 +556,12 @@ function appendFunctionErrors(errors: CompilerError, fn: HIRFunction): void {
}
}
export enum MutationKind {
None = 0,
Conditional = 1,
Definite = 2,
}
type Node = {
id: Identifier;
createdFrom: Map<Identifier, number>;
@@ -485,6 +570,7 @@ type Node = {
edges: Array<{index: number; node: Identifier; kind: 'capture' | 'alias'}>;
transitive: {kind: MutationKind; loc: SourceLocation} | null;
local: {kind: MutationKind; loc: SourceLocation} | null;
lastMutated: number;
value:
| {kind: 'Object'}
| {kind: 'Phi'}
@@ -502,6 +588,7 @@ class AliasingState {
edges: [],
transitive: null,
local: null,
lastMutated: 0,
value,
});
}
@@ -511,11 +598,6 @@ class AliasingState {
const fromNode = this.nodes.get(from.identifier);
const toNode = this.nodes.get(into.identifier);
if (fromNode == null || toNode == null) {
if (VERBOSE) {
console.log(
`skip: createFrom ${printPlace(from)}${!!fromNode} -> ${printPlace(into)}${!!toNode}`,
);
}
return;
}
fromNode.edges.push({index, node: into.identifier, kind: 'alias'});
@@ -528,11 +610,6 @@ class AliasingState {
const fromNode = this.nodes.get(from.identifier);
const toNode = this.nodes.get(into.identifier);
if (fromNode == null || toNode == null) {
if (VERBOSE) {
console.log(
`skip: capture ${printPlace(from)}${!!fromNode} -> ${printPlace(into)}${!!toNode}`,
);
}
return;
}
fromNode.edges.push({index, node: into.identifier, kind: 'capture'});
@@ -545,11 +622,6 @@ class AliasingState {
const fromNode = this.nodes.get(from.identifier);
const toNode = this.nodes.get(into.identifier);
if (fromNode == null || toNode == null) {
if (VERBOSE) {
console.log(
`skip: assign ${printPlace(from)}${!!fromNode} -> ${printPlace(into)}${!!toNode}`,
);
}
return;
}
fromNode.edges.push({index, node: into.identifier, kind: 'alias'});
@@ -598,17 +670,13 @@ class AliasingState {
mutate(
index: number,
start: Identifier,
end: InstructionId,
// Null is used for simulated mutations
end: InstructionId | null,
transitive: boolean,
kind: MutationKind,
loc: SourceLocation,
errors: CompilerError,
): void {
if (DEBUG) {
console.log(
`mutate ix=${index} start=$${start.id} end=[${end}]${transitive ? ' transitive' : ''} kind=${kind}`,
);
}
const seen = new Set<Identifier>();
const queue: Array<{
place: Identifier;
@@ -623,21 +691,14 @@ class AliasingState {
seen.add(current);
const node = this.nodes.get(current);
if (node == null) {
if (DEBUG) {
console.log(
`no node! ${printIdentifier(start)} for identifier ${printIdentifier(current)}`,
);
}
continue;
}
if (DEBUG) {
console.log(
` mutate $${node.id.id} transitive=${transitive} direction=${direction}`,
node.lastMutated = Math.max(node.lastMutated, index);
if (end != null) {
node.id.mutableRange.end = makeInstructionId(
Math.max(node.id.mutableRange.end, end),
);
}
node.id.mutableRange.end = makeInstructionId(
Math.max(node.id.mutableRange.end, end),
);
if (
node.value.kind === 'Function' &&
node.transitive == null &&
@@ -701,37 +762,5 @@ class AliasingState {
}
}
}
if (DEBUG) {
const nodes = new Map();
for (const id of seen) {
const node = this.nodes.get(id);
nodes.set(id.id, node);
}
console.log(pretty(nodes));
}
}
debug(): string {
return pretty(this.nodes);
}
}
export function pretty(v: any): string {
return prettyFormat(v, {
plugins: [
{
test: v =>
v !== null && typeof v === 'object' && v.kind === 'Identifier',
serialize: v => printPlace(v),
},
{
test: v =>
v !== null &&
typeof v === 'object' &&
typeof v.declarationId === 'number',
serialize: v =>
`${printIdentifier(v)}:${v.mutableRange.start}:${v.mutableRange.end}`,
},
],
});
}

View File

@@ -0,0 +1,544 @@
# The Mutability & Aliasing Model
This document describes the new (as of June 2025) mutability and aliasing model powering React Compiler. The mutability and aliasing system is a conceptual subcomponent whose primary role is to determine minimal sets of values that mutate together, and the range of instructions over which those mutations occur. These minimal sets of values that mutate together, and the corresponding instructions doing those mutations, are ultimately grouped into reactive scopes, which then translate into memoization blocks in the output (after substantial additional processing described in the comments of those passes).
To build an intuition, consider the following example:
```js
function Component() {
// a is created and mutated over the course of these two instructions:
const a = {};
mutate(a);
// b and c are created and mutated together — mutate might modify b via c
const b = {};
const c = {b};
mutate(c);
// does not modify a/b/c
return <Foo a={a} c={c} />
}
```
The goal of mutability and aliasing inference is to understand the set of instructions that create/modify a, b, and c.
In code, the mutability and aliasing model is compromised of the following phases:
* `InferMutationAliasingEffects`. Infers a set of mutation and aliasing effects for each instruction. The approach is to generate a set of candidate effects based purely on the semantics of each instruction and the types of the operands, then use abstract interpretation to determine the actual effects (or errros) that would apply. For example, an instruction that by default has a Capture effect might downgrade to an ImmutableCapture effect if the value is known to be frozen.
* `InferMutationAliasingRanges`. Infers a mutable range (start:end instruction ids) for each value in the program, and annotates each Place with its effect type for usage in later passes. This builds a graph of data flow through the program over time in order to understand which mutations effect which values.
* `InferReactiveScopeVariables`. Given the per-Place effects, determines disjoint sets of values that mutate together and assigns all identifiers in each set to a unique scope, and updates the range to include the ranges of all constituent values.
Finally, `AnalyzeFunctions` needs to understand the mutation and aliasing semantics of nested FunctionExpression and ObjectMethod values. `AnalyzeFunctions` calls `InferFunctionExpressionAliasingEffectsSignature` to determine the publicly observable set of mutation/aliasing effects for nested functions.
## Mutation and Aliasing Effects
The inference model is based on a set of "effects" that describe subtle aspects of mutation, aliasing, and other changes to the state of values over time
### Creation Effects
#### Create
```js
{
kind: 'Create';
into: Place;
value: ValueKind;
reason: ValueReason;
}
```
Describes the creation of a new value with the given kind, and reason for having that kind. For example, `x = 10` might have an effect like `Create x = ValueKind.Primitive [ValueReason.Other]`.
#### CreateFunction
```js
{
kind: 'CreateFunction';
captures: Array<Place>;
function: FunctionExpression | ObjectMethod;
into: Place;
}
```
Describes the creation of new function value, capturing the given set of mutable values. CreateFunction is used to specifically track function types so that we can precisely model calls to those functions with `Apply`.
#### Apply
```js
{
kind: 'Apply';
receiver: Place;
function: Place; // same as receiver for function calls
mutatesFunction: boolean; // indicates if this is a type that we consdier to mutate the function itself by default
args: Array<Place | SpreadPattern | Hole>;
into: Place; // where result is stored
signature: FunctionSignature | null;
}
```
Describes the potential creation of a value by calling a function. This models `new`, function calls, and method calls. The inference algorithm uses the most precise signature it can determine:
* If the function is a locally created function expression, we use a signature inferred from the behavior of that function to interpret the effects of calling it with the given arguments.
* Else if the function has a known aliasing signature (new style precise effects signature), we apply the arguments to that signature to get a precise set of effects.
* Else if the function has a legacy style signature (with per-param effects) we convert the legacy per-Place effects into aliasing effects (described in this doc) and apply those.
* Else fall back to inferring a generic set of effects.
The generic fallback is to assume:
- The return value may alias any of the arguments (Alias param -> return)
- Any arguments *may* be transitively mutated (MutateTransitiveConditionally param)
- Any argument may be captured into any other argument (Capture paramN -> paramM for all N,M where N != M)
### Aliasing Effects
These effects describe data-flow only, separately from mutation or other state-changing semantics.
#### Assign
```js
{
kind: 'Assign';
from: Place;
into: Place;
}
```
Describes an `x = y` assignment, where the receiving (into) value is overwritten with a new (from) value. After this effect, any previous assignments/aliases to the receiving value are dropped. Note that `Alias` initializes the receiving value.
> TODO: InferMutationAliasingRanges may not fully reset aliases on encountering this effect
#### Alias
```js
{
kind: 'Alias';
from: Place;
into: Place;
}
```
Describes that an assignment _may_ occur, but that the possible assignment is non-exclusive. The canonical use-case for `Alias` is a function that may return more than one of its arguments, such as `(x, y, z) => x ? y : z`. Here, the result of this function may be `y` or `z`, but neither one overwrites the other. Note that `Alias` does _not_ initialize the receiving value: it should always be paired with an effect to create the receiving value.
#### Capture
```js
{
kind: 'Capture';
from: Place;
into: Place;
}
```
Describes that a reference to one variable (from) is stored within another value (into). Examples include:
- An array expression captures the items of the array (`array = [capturedValue]`)
- Array.prototype.push captures the pushed values into the array (`array.push(capturedValue)`)
- Property assignment captures the value onto the object (`object.property = capturedValue`)
#### CreateFrom
```js
{
kind: 'CreateFrom';
from: Place;
into: Place;
}
```
This is somewhat the inverse of `Capture`. The `CreateFrom` effect describes that a variable is initialized by extracting _part_ of another value, without taking a direct alias to the full other value. Examples include:
- Indexing into an array (`createdFrom = array[0]`)
- Reading an object property (`createdFrom = object.property`)
- Getting a Map key (`createdFrom = map.get(key)`)
#### ImmutableCapture
Describes immutable data flow from one value to another. This is not currently used for anything, but is intended to eventually power a more sophisticated escape analysis.
### State-Changing Effects
The following effects describe state changes to specific values, not data flow. In many cases, JavaScript semantics will involve a combination of both data-flow effects *and* state-change effects. For example, `object.property = value` has data flow (`Capture object <- value`) and mutation (`Mutate object`).
#### Freeze
```js
{
kind: 'Freeze',
// The reference being frozen
value: Place;
// The reason the value is frozen (passed to a hook, passed to jsx, etc)
reason: ValueReason;
}
```
Once a reference to a value has been passed to React, that value is generally not safe to mutate further. This is not a strictly required property of React, but is a natural consequence of making components and hooks composable without leaking implementation details. Concretely, once a value has been passed as a JSX prop, passed as argument to a hook, or returned from a hook, it must be assumed that the other "side" — receiver of the prop/argument/return value — will use that value as an input to an effect or memoization unit. Mutating that value (instead of creating a new value) will fail to cause the consuming computation to update:
```js
// INVALID DO NOT DO THIS
function Component(props) {
const array = useArray(props.value);
// OOPS! this value is memoized, the array won't get re-created
// when `props.value` changes, so we might just keep pushing new
// values to the same array on every render!
array.push(props.otherValue);
}
function useArray(a) {
return useMemo(() => [a], [a]);
}
```
The **Freeze** effect accepts a variable reference and a reason that the value is being frozen. Note: _freeze only applies to the reference, not the underlying value_. Our inference is conservative, and assumes that there may still be other references to the same underlying value which are mutated later. For example:
```js
const x = {};
const y = [];
x.y = y;
freeze(y); // y _reference_ is frozen
x.y.push(props.value); // but y is still considered mutable bc of this
```
#### Mutate (and MutateConditionally)
```js
{
kind: 'Mutate';
value: Place;
}
```
Mutate indicates that a value is mutated, without modifying any of the values that it may transitively have captured. Canonical examples include:
- Pushing an item onto an array modifies the array, but does not modify any items stored _within_ the array (unless the array has a reference to itself!)
- Assigning a value to an object property modifies the object, but not any values stored in the object's other properties.
This helps explain the distinction between Assign/Alias and Capture: Mutate only affects assign/alias but not captures.
`MutateConditionally` is an alternative in which the mutation _may_ happen depending on the type of the value. The conditional variant is not generally used and included for completeness.
#### MutateTransitiveConditionally (and MutateTransitive)
`MutateTransitiveConditionally` represents an operation that may mutate _any_ aspect of a value, including reaching arbitrarily deep into nested values to mutate them. This is the default semantic for unknown functions — we have no idea what they do, so we assume that they are idempotent but may mutate any aspect of the mutable values that are passed to them.
There is also `MutateTransitive` for completeness, but this is not generally used.
### Side Effects
Finally, there are a few effects that describe error, or potential error, conditions:
- `MutateFrozen` is always an error, because it indicates known mutation of a value that should not be mutated.
- `MutateGlobal` indicates known mutation of a global value, which is not safe during render. This effect is an error if reachable during render, but allowed if only reachable via an event handler or useEffect.
- `Impure` indicates calling some other logic that is impure/side-effecting. This is an error if reachable during render, but allowed if only reachable via an event handler or useEffect.
- TODO: we could probably merge this and MutateGlobal
- `Render` indicates a value that is not mutated, but is known to be called during render. It's used for a few particular places like JSX tags and JSX children, which we assume are accessed during render (while other props may be event handlers etc). This helps to detect more MutateGlobal/Impure effects and reject more invalid programs.
## Rules
### Mutation of Alias Mutates the Source Value
```
Alias a <- b
Mutate a
=>
Mutate b
```
Example:
```js
const a = maybeIdentity(b); // Alias a <- b
a.property = value; // a could be b, so this mutates b
```
### Mutation of Assignment Mutates the Source Value
```
Assign a <- b
Mutate a
=>
Mutate b
```
Example:
```js
const a = b;
a.property = value // a _is_ b, this mutates b
```
### Mutation of CreateFrom Mutates the Source Value
```
CreateFrom a <- b
Mutate a
=>
Mutate b
```
Example:
```js
const a = b[index];
a.property = value // the contents of b are transitively mutated
```
### Mutation of Capture Does *Not* Mutate the Source Value
```
Capture a <- b
Mutate a
!=>
~Mutate b~
```
Example:
```js
const a = {};
a.b = b;
a.property = value; // mutates a, not b
```
### Mutation of Source Affects Alias, Assignment, CreateFrom, and Capture
```
Alias a <- b OR Assign a <- b OR CreateFrom a <- b OR Capture a <- b
Mutate b
=>
Mutate a
```
A derived value changes when it's source value is mutated.
Example:
```js
const x = {};
const y = [x];
x.y = true; // this changes the value within `y` ie mutates y
```
### TransitiveMutation of Alias, Assignment, CreateFrom, or Capture Mutates the Source
```
Alias a <- b OR Assign a <- b OR CreateFrom a <- b OR Capture a <- b
MutateTransitive a
=>
MutateTransitive b
```
Remember, the intuition for a transitive mutation is that it's something that could traverse arbitrarily deep into an object and mutate whatever it finds. Imagine something that recurses into every nested object/array and sets `.field = value`. Given a function `mutate()` that does this, then:
```js
const a = b; // assign
mutate(a); // clearly can transitively mutate b
const a = maybeIdentity(b); // alias
mutate(a); // clearly can transitively mutate b
const a = b[index]; // createfrom
mutate(a); // clearly can transitively mutate b
const a = {};
a.b = b; // capture
mutate(a); // can transitively mutate b
```
### Freeze Does Not Freeze the Value
Freeze does not freeze the value itself:
```
Create x
Assign y <- x OR Alias y <- x OR CreateFrom y <- x OR Capture y <- x
Freeze y
!=>
~Freeze x~
```
This means that subsequent mutations of the original value are valid:
```
Create x
Assign y <- x OR Alias y <- x OR CreateFrom y <- x OR Capture y <- x
Freeze y
Mutate x
=>
Mutate x (mutation is ok)
```
As well as mutations through other assignments/aliases/captures/createfroms of the original value:
```
Create x
Assign y <- x OR Alias y <- x OR CreateFrom y <- x OR Capture y <- x
Freeze y
Alias z <- x OR Capture z <- x OR CreateFrom z <- x OR Assign z <- x
Mutate z
=>
Mutate x (mutation is ok)
```
### Freeze Freezes The Reference
Although freeze doesn't freeze the value, it does affect the reference. The reference cannot be used to mutate.
Conditional mutations of the reference are no-ops:
```
Create x
Assign y <- x OR Alias y <- x OR CreateFrom y <- x OR Capture y <- x
Freeze y
MutateConditional y
=>
(no mutation)
```
And known mutations of the reference are errors:
```
Create x
Assign y <- x OR Alias y <- x OR CreateFrom y <- x OR Capture y <- x
Freeze y
MutateConditional y
=>
MutateFrozen y error=...
```
### Corollary: Transitivity of Assign/Alias/CreateFrom/Capture
A key part of the inference model is inferring a signature for function expressions. The signature is a minimal set of effects that describes the publicly observable behavior of the function. This can include "global" effects like side effects (MutateGlobal/Impure) as well as mutations/aliasing of parameters and free variables.
In order to determine the aliasing of params and free variables into each other and/or the return value, we may encounter chains of assign, alias, createfrom, and capture effects. For example:
```js
const f = (x) => {
const y = [x]; // capture y <- x
const z = y[0]; // createfrom z <- y
return z; // assign return <- z
}
// <Effect> return <- x
```
In this example we can see that there should be some effect on `f` that tracks the flow of data from `x` into the return value. The key constraint is preserving the semantics around how local/transitive mutations of the destination would affect the source.
#### Each of the effects is transitive with itself
```
Assign b <- a
Assign c <- b
=>
Assign c <- a
```
```
Alias b <- a
Alias c <- b
=>
Alias c <- a
```
```
CreateFrom b <- a
CreateFrom c <- b
=>
CreateFrom c <- a
```
```
Capture b <- a
Capture c <- b
=>
Capture c <- a
```
#### Alias > Assign
```
Assign b <- a
Alias c <- b
=>
Alias c <- a
```
```
Alias b <- a
Assign c <- b
=>
Alias c <- a
```
### CreateFrom > Assign/Alias
Intuition:
```
CreateFrom b <- a
Alias c <- b OR Assign c <- b
=>
CreateFrom c <- a
```
```
Alias b <- a OR Assign b <- a
CreateFrom c <- b
=>
CreateFrom c <- a
```
### Capture > Assign/Alias
Intuition: capturing means that a local mutation of the destination will not affect the source, so we preserve the capture.
```
Capture b <- a
Alias c <- b OR Assign c <- b
=>
Capture c <- a
```
```
Alias b <- a OR Assign b <- a
Capture c <- b
=>
Capture c <- a
```
### Capture And CreateFrom
Intuition: these effects are inverses of each other (capturing into an object, extracting from an object). The result is based on the order of operations:
Capture then CreatFrom is equivalent to Alias: we have to assume that the result _is_ the original value and that a local mutation of the result could mutate the original.
```js
const b = [a]; // capture
const c = b[0]; // createfrom
mutate(c); // this clearly can mutate a, so the result must be one of Assign/Alias/CreateFrom
```
We use Alias as the return type because the mutability kind of the result is not derived from the source value (there's a fresh object in between due to the capture), so the full set of effects in practice would be a Create+Alias.
```
Capture b <- a
CreateFrom c <- b
=>
Alias c <- a
```
Meanwhile the opposite direction preserves the capture, because the result is not the same as the source:
```js
const b = a[0]; // createfrom
const c = [b]; // capture
mutate(c); // does not mutate a, so the result must be Capture
```
```
CreateFrom b <- a
Capture c <- b
=>
Capture c <- a
```

View File

@@ -25,7 +25,6 @@ import {
makeBlockId,
makeInstructionId,
makePropertyLiteral,
makeType,
markInstructionIds,
promoteTemporary,
reversePostorderBlocks,
@@ -253,7 +252,6 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
env,
params: [obj],
returnTypeAnnotation: null,
returnType: makeType(),
returns: createTemporaryPlace(env, GeneratedSource),
context: [],
effects: null,

View File

@@ -21,7 +21,6 @@ import {
makeBlockId,
makeIdentifierName,
makeInstructionId,
makeType,
ObjectProperty,
Place,
promoteTemporary,
@@ -368,7 +367,6 @@ function emitOutlinedFn(
env,
params: [propsObj],
returnTypeAnnotation: null,
returnType: makeType(),
returns: createTemporaryPlace(env, GeneratedSource),
context: [],
effects: null,

View File

@@ -349,11 +349,9 @@ function codegenReactiveFunction(
fn: ReactiveFunction,
): Result<CodegenFunction, CompilerError> {
for (const param of fn.params) {
if (param.kind === 'Identifier') {
cx.temp.set(param.identifier.declarationId, null);
} else {
cx.temp.set(param.place.identifier.declarationId, null);
}
const place = param.kind === 'Identifier' ? param : param.place;
cx.temp.set(place.identifier.declarationId, null);
cx.declare(place.identifier);
}
const params = fn.params.map(param => convertParameter(param));
@@ -1183,7 +1181,7 @@ function codegenTerminal(
? codegenPlaceToExpression(cx, case_.test)
: null;
const block = codegenBlock(cx, case_.block!);
return t.switchCase(test, [block]);
return t.switchCase(test, block.body.length === 0 ? [] : [block]);
}),
);
}

View File

@@ -79,6 +79,10 @@ export function extractScopeDeclarationsFromDestructuring(
fn: ReactiveFunction,
): void {
const state = new State(fn.env);
for (const param of fn.params) {
const place = param.kind === 'Identifier' ? param : param.place;
state.declared.add(place.identifier.declarationId);
}
visitReactiveFunction(fn, new Visitor(), state);
}

View File

@@ -90,7 +90,8 @@ function apply(func: HIRFunction, unifier: Unifier): void {
}
}
}
func.returnType = unifier.get(func.returnType);
const returns = func.returns.identifier;
returns.type = unifier.get(returns.type);
}
type TypeEquation = {
@@ -143,12 +144,12 @@ function* generate(
}
}
if (returnTypes.length > 1) {
yield equation(func.returnType, {
yield equation(func.returns.identifier.type, {
kind: 'Phi',
operands: returnTypes,
});
} else if (returnTypes.length === 1) {
yield equation(func.returnType, returnTypes[0]!);
yield equation(func.returns.identifier.type, returnTypes[0]!);
}
}
@@ -407,7 +408,7 @@ function* generateInstructionTypes(
yield equation(left, {
kind: 'Function',
shapeId: BuiltInFunctionId,
return: value.loweredFunc.func.returnType,
return: value.loweredFunc.func.returns.identifier.type,
isConstructor: false,
});
break;

View File

@@ -50,8 +50,7 @@ function Component(props) {
console.log(handlers.value);
break bb0;
}
default: {
}
default:
}
t0 = handlers;

View File

@@ -67,8 +67,7 @@ function Component(props) {
case "b": {
break bb1;
}
case "c": {
}
case "c":
default: {
x = 6;
}

View File

@@ -24,8 +24,6 @@ export const FIXTURE_ENTRYPOINT = {
4 | const ref = useRef();
> 5 | useEffect(() => {}, [ref.current]);
| ^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (5:5)
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (5:5)
6 | }
7 |
8 | export const FIXTURE_ENTRYPOINT = {

View File

@@ -19,6 +19,8 @@ function Component(props) {
3 | const ref = useRef(null);
> 4 | const value = ref.current;
| ^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (4:4)
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef). Cannot access ref value `value` (5:5)
5 | return value;
6 | }
7 |

View File

@@ -19,12 +19,17 @@ function Component(props) {
## Error
```
7 | return <Foo item={item} current={current} />;
8 | };
> 9 | return <Items>{props.items.map(item => renderItem(item))}</Items>;
| ^^^^^^^^^^^^^^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (9:9)
10 | }
11 |
4 | const renderItem = item => {
5 | const aliasedRef = ref;
> 6 | const current = aliasedRef.current;
| ^^^^^^^^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (6:6)
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef). Cannot access ref value `current` (7:7)
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (7:7)
7 | return <Foo item={item} current={current} />;
8 | };
9 | return <Items>{props.items.map(item => renderItem(item))}</Items>;
```

View File

@@ -21,15 +21,13 @@ function Component() {
## Error
```
7 | };
8 | const changeRef = setRef;
> 9 | changeRef();
| ^^^^^^^^^ InvalidReact: This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef) (9:9)
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (9:9)
10 |
11 | return <button ref={ref} />;
12 | }
4 |
5 | const setRef = () => {
> 6 | ref.current = false;
| ^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (6:6)
7 | };
8 | const changeRef = setRef;
9 | changeRef();
```

View File

@@ -38,13 +38,15 @@ export const FIXTURE_ENTRYPOINT = {
## Error
```
19 | useEffect(() => setState(2), []);
17 | * $2 = Function context=setState
18 | */
> 19 | useEffect(() => setState(2), []);
| ^^^^^^^^ InvalidReact: This variable is accessed before it is declared, which may prevent it from updating as the assigned value changes over time. Variable `setState` is accessed before it is declared (19:19)
InvalidReact: This variable is accessed before it is declared, which prevents the earlier access from updating when this value changes over time. Variable `setState` is accessed before it is declared (21:21)
20 |
> 21 | const [state, setState] = useState(0);
| ^^^^^^^^ InvalidReact: This variable is accessed before it is declared, which prevents the earlier access from updating when this value changes over time. Move the declaration of `setState` to before it is first referenced (21:21)
21 | const [state, setState] = useState(0);
22 | return <Stringify state={state} />;
23 | }
24 |
```

View File

@@ -18,6 +18,10 @@ function Component({ref}) {
2 | function Component({ref}) {
> 3 | const value = ref.current;
| ^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (3:3)
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef). Cannot access ref value `value` (4:4)
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (4:4)
4 | return <div>{value}</div>;
5 | }
6 |

View File

@@ -18,6 +18,10 @@ function Component(props) {
2 | function Component(props) {
> 3 | const value = props.ref.current;
| ^^^^^^^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (3:3)
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef). Cannot access ref value `value` (4:4)
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (4:4)
4 | return <div>{value}</div>;
5 | }
6 |

View File

@@ -18,12 +18,17 @@ function Component(props) {
## Error
```
6 | return <Foo item={item} current={current} />;
7 | };
> 8 | return <Items>{props.items.map(item => renderItem(item))}</Items>;
| ^^^^^^^^^^^^^^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (8:8)
9 | }
10 |
3 | const ref = useRef(null);
4 | const renderItem = item => {
> 5 | const current = ref.current;
| ^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (5:5)
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef). Cannot access ref value `current` (6:6)
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (6:6)
6 | return <Foo item={item} current={current} />;
7 | };
8 | return <Items>{props.items.map(item => renderItem(item))}</Items>;
```

View File

@@ -19,8 +19,6 @@ function Component(props) {
3 | const ref = useRef(null);
> 4 | ref.current = props.value;
| ^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (4:4)
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (5:5)
5 | return ref.current;
6 | }
7 |

View File

@@ -27,9 +27,9 @@ export const FIXTURE_ENTRYPOINT = {
4 | component C() {
5 | const r = useRef(null);
> 6 | const guard = r.current == null;
| ^^^^^^^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (6:6)
| ^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef). Cannot access ref value `r` (6:6)
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef). Cannot access ref value `guard` (7:7)
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (8:8)
7 | if (guard) {
8 | r.current = 1;
9 | }

View File

@@ -34,15 +34,13 @@ export const FIXTURE_ENTRYPOINT = {
## Error
```
15 | ref.current.inner = null;
13 | // The ref is modified later, extending its range and preventing memoization of onChange
14 | const reset = () => {
> 15 | ref.current.inner = null;
| ^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (15:15)
16 | };
> 17 | reset();
| ^^^^^ InvalidReact: This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef) (17:17)
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (17:17)
17 | reset();
18 |
19 | return <input onChange={onChange} />;
20 | }
```

View File

@@ -0,0 +1,81 @@
## Input
```javascript
import {Stringify, mutate} from 'shared-runtime';
function Component({foo, bar}) {
let x = {foo};
let y = {bar};
const f0 = function () {
let a = {y};
let b = {x};
a.y.x = b;
};
f0();
mutate(y);
return <Stringify x={y} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{foo: 2, bar: 3}],
sequentialRenders: [
{foo: 2, bar: 3},
{foo: 2, bar: 3},
{foo: 2, bar: 4},
{foo: 3, bar: 4},
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { Stringify, mutate } from "shared-runtime";
function Component(t0) {
const $ = _c(3);
const { foo, bar } = t0;
let t1;
if ($[0] !== bar || $[1] !== foo) {
const x = { foo };
const y = { bar };
const f0 = function () {
const a = { y };
const b = { x };
a.y.x = b;
};
f0();
mutate(y);
t1 = <Stringify x={y} />;
$[0] = bar;
$[1] = foo;
$[2] = t1;
} else {
t1 = $[2];
}
return t1;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ foo: 2, bar: 3 }],
sequentialRenders: [
{ foo: 2, bar: 3 },
{ foo: 2, bar: 3 },
{ foo: 2, bar: 4 },
{ foo: 3, bar: 4 },
],
};
```
### Eval output
(kind: ok) <div>{"x":{"bar":3,"x":{"x":{"foo":2}},"wat0":"joe"}}</div>
<div>{"x":{"bar":3,"x":{"x":{"foo":2}},"wat0":"joe"}}</div>
<div>{"x":{"bar":4,"x":{"x":{"foo":2}},"wat0":"joe"}}</div>
<div>{"x":{"bar":4,"x":{"x":{"foo":3}},"wat0":"joe"}}</div>

View File

@@ -0,0 +1,25 @@
import {Stringify, mutate} from 'shared-runtime';
function Component({foo, bar}) {
let x = {foo};
let y = {bar};
const f0 = function () {
let a = {y};
let b = {x};
a.y.x = b;
};
f0();
mutate(y);
return <Stringify x={y} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{foo: 2, bar: 3}],
sequentialRenders: [
{foo: 2, bar: 3},
{foo: 2, bar: 3},
{foo: 2, bar: 4},
{foo: 3, bar: 4},
],
};

View File

@@ -31,13 +31,15 @@ function Component({content, refetch}) {
## Error
```
17 | // This has to error: onRefetch needs to memoize with `content` as a
18 | // dependency, but the dependency comes later
> 19 | const {data = null} = content;
| ^^^^^^^^^^^ InvalidReact: This variable is accessed before it is declared, which prevents the earlier access from updating when this value changes over time. Move the declaration of `data` to before it is first referenced (19:19)
20 |
21 | return <Foo data={data} onSubmit={onSubmit} />;
22 | }
9 | // TDZ violation!
10 | const onRefetch = useCallback(() => {
> 11 | refetch(data);
| ^^^^ InvalidReact: This variable is accessed before it is declared, which may prevent it from updating as the assigned value changes over time. Variable `data` is accessed before it is declared (11:11)
InvalidReact: This variable is accessed before it is declared, which prevents the earlier access from updating when this value changes over time. Variable `data` is accessed before it is declared (19:19)
12 | }, [refetch]);
13 |
14 | // The context variable gets frozen here since it's passed to a hook
```

View File

@@ -0,0 +1,97 @@
## Input
```javascript
import {useMemo} from 'react';
import {identity, ValidateMemoization} from 'shared-runtime';
function Component({a, b}) {
const x = useMemo(() => ({a}), [a, b]);
const f = () => {
return identity(x);
};
const x2 = f();
x2.b = b;
return <ValidateMemoization inputs={[a, b]} output={x} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: 0, b: 0}],
sequentialRenders: [
{a: 0, b: 0},
{a: 0, b: 1},
{a: 1, b: 1},
{a: 0, b: 0},
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { useMemo } from "react";
import { identity, ValidateMemoization } from "shared-runtime";
function Component(t0) {
const $ = _c(10);
const { a, b } = t0;
let t1;
let x;
if ($[0] !== a || $[1] !== b) {
t1 = { a };
x = t1;
const f = () => identity(x);
const x2 = f();
x2.b = b;
$[0] = a;
$[1] = b;
$[2] = x;
$[3] = t1;
} else {
x = $[2];
t1 = $[3];
}
let t2;
if ($[4] !== a || $[5] !== b) {
t2 = [a, b];
$[4] = a;
$[5] = b;
$[6] = t2;
} else {
t2 = $[6];
}
let t3;
if ($[7] !== t2 || $[8] !== x) {
t3 = <ValidateMemoization inputs={t2} output={x} />;
$[7] = t2;
$[8] = x;
$[9] = t3;
} else {
t3 = $[9];
}
return t3;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ a: 0, b: 0 }],
sequentialRenders: [
{ a: 0, b: 0 },
{ a: 0, b: 1 },
{ a: 1, b: 1 },
{ a: 0, b: 0 },
],
};
```
### Eval output
(kind: ok) <div>{"inputs":[0,0],"output":{"a":0,"b":0}}</div>
<div>{"inputs":[0,1],"output":{"a":0,"b":1}}</div>
<div>{"inputs":[1,1],"output":{"a":1,"b":1}}</div>
<div>{"inputs":[0,0],"output":{"a":0,"b":0}}</div>

View File

@@ -0,0 +1,24 @@
import {useMemo} from 'react';
import {identity, ValidateMemoization} from 'shared-runtime';
function Component({a, b}) {
const x = useMemo(() => ({a}), [a, b]);
const f = () => {
return identity(x);
};
const x2 = f();
x2.b = b;
return <ValidateMemoization inputs={[a, b]} output={x} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: 0, b: 0}],
sequentialRenders: [
{a: 0, b: 0},
{a: 0, b: 1},
{a: 1, b: 1},
{a: 0, b: 0},
],
};

View File

@@ -0,0 +1,92 @@
## Input
```javascript
import {useMemo} from 'react';
import {identity, ValidateMemoization} from 'shared-runtime';
function Component({a, b}) {
const x = useMemo(() => ({a}), [a, b]);
const x2 = identity(x);
x2.b = b;
return <ValidateMemoization inputs={[a, b]} output={x} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: 0, b: 0}],
sequentialRenders: [
{a: 0, b: 0},
{a: 0, b: 1},
{a: 1, b: 1},
{a: 0, b: 0},
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { useMemo } from "react";
import { identity, ValidateMemoization } from "shared-runtime";
function Component(t0) {
const $ = _c(10);
const { a, b } = t0;
let t1;
let x;
if ($[0] !== a || $[1] !== b) {
t1 = { a };
x = t1;
const x2 = identity(x);
x2.b = b;
$[0] = a;
$[1] = b;
$[2] = x;
$[3] = t1;
} else {
x = $[2];
t1 = $[3];
}
let t2;
if ($[4] !== a || $[5] !== b) {
t2 = [a, b];
$[4] = a;
$[5] = b;
$[6] = t2;
} else {
t2 = $[6];
}
let t3;
if ($[7] !== t2 || $[8] !== x) {
t3 = <ValidateMemoization inputs={t2} output={x} />;
$[7] = t2;
$[8] = x;
$[9] = t3;
} else {
t3 = $[9];
}
return t3;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ a: 0, b: 0 }],
sequentialRenders: [
{ a: 0, b: 0 },
{ a: 0, b: 1 },
{ a: 1, b: 1 },
{ a: 0, b: 0 },
],
};
```
### Eval output
(kind: ok) <div>{"inputs":[0,0],"output":{"a":0,"b":0}}</div>
<div>{"inputs":[0,1],"output":{"a":0,"b":1}}</div>
<div>{"inputs":[1,1],"output":{"a":1,"b":1}}</div>
<div>{"inputs":[0,0],"output":{"a":0,"b":0}}</div>

View File

@@ -0,0 +1,21 @@
import {useMemo} from 'react';
import {identity, ValidateMemoization} from 'shared-runtime';
function Component({a, b}) {
const x = useMemo(() => ({a}), [a, b]);
const x2 = identity(x);
x2.b = b;
return <ValidateMemoization inputs={[a, b]} output={x} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: 0, b: 0}],
sequentialRenders: [
{a: 0, b: 0},
{a: 0, b: 1},
{a: 1, b: 1},
{a: 0, b: 0},
],
};

View File

@@ -0,0 +1,54 @@
## Input
```javascript
// @flow @enableNewMutationAliasingModel
import fbt from 'fbt';
component Component() {
const sections = Object.keys(items);
for (let i = 0; i < sections.length; i += 3) {
chunks.push(
sections.slice(i, i + 3).map(section => {
return <Child />;
})
);
}
return <Child />;
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import fbt from "fbt";
function Component() {
const $ = _c(1);
const sections = Object.keys(items);
for (let i = 0; i < sections.length; i = i + 3, i) {
chunks.push(sections.slice(i, i + 3).map(_temp));
}
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = <Child />;
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}
function _temp(section) {
return <Child />;
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -0,0 +1,17 @@
// @flow @enableNewMutationAliasingModel
import fbt from 'fbt';
component Component() {
const sections = Object.keys(items);
for (let i = 0; i < sections.length; i += 3) {
chunks.push(
sections.slice(i, i + 3).map(section => {
return <Child />;
})
);
}
return <Child />;
}

View File

@@ -0,0 +1,60 @@
## Input
```javascript
function Component() {
const x = {};
const fn = () => {
new Object()
.build(x)
.build({})
.build({})
.build({})
.build({})
.build({})
.build({});
};
return <Stringify x={x} fn={fn} />;
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
function Component() {
const $ = _c(2);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = {};
$[0] = t0;
} else {
t0 = $[0];
}
const x = t0;
let t1;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
const fn = () => {
new Object()
.build(x)
.build({})
.build({})
.build({})
.build({})
.build({})
.build({});
};
t1 = <Stringify x={x} fn={fn} />;
$[1] = t1;
} else {
t1 = $[1];
}
return t1;
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -0,0 +1,14 @@
function Component() {
const x = {};
const fn = () => {
new Object()
.build(x)
.build({})
.build({})
.build({})
.build({})
.build({})
.build({});
};
return <Stringify x={x} fn={fn} />;
}

View File

@@ -0,0 +1,60 @@
## Input
```javascript
function Component({a, b}) {
const y = {a};
const x = {b};
const f = () => {
let z = null;
while (z == null) {
z = x;
}
// z is a phi with a backedge, and we don't realize it could be x,
// and therefore fail to record a Capture x <- y effect for this
// function expression
z.y = y;
};
f();
mutate(x);
return <div>{x}</div>;
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
function Component(t0) {
const $ = _c(3);
const { a, b } = t0;
let t1;
if ($[0] !== a || $[1] !== b) {
const y = { a };
const x = { b };
const f = () => {
let z = null;
while (z == null) {
z = x;
}
z.y = y;
};
f();
mutate(x);
t1 = <div>{x}</div>;
$[0] = a;
$[1] = b;
$[2] = t1;
} else {
t1 = $[2];
}
return t1;
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -0,0 +1,17 @@
function Component({a, b}) {
const y = {a};
const x = {b};
const f = () => {
let z = null;
while (z == null) {
z = x;
}
// z is a phi with a backedge, and we don't realize it could be x,
// and therefore fail to record a Capture x <- y effect for this
// function expression
z.y = y;
};
f();
mutate(x);
return <div>{x}</div>;
}

View File

@@ -0,0 +1,53 @@
## Input
```javascript
// @flow @enableNewMutationAliasingModel
import {identity, Stringify, useFragment} from 'shared-runtime';
component Example() {
const data = useFragment();
const {a, b} = identity(data);
const el = <Stringify tooltip={b} />;
identity(a.at(0));
return <Stringify icon={el} />;
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { identity, Stringify, useFragment } from "shared-runtime";
function Example() {
const $ = _c(2);
const data = useFragment();
let t0;
if ($[0] !== data) {
const { a, b } = identity(data);
const el = <Stringify tooltip={b} />;
identity(a.at(0));
t0 = <Stringify icon={el} />;
$[0] = data;
$[1] = t0;
} else {
t0 = $[1];
}
return t0;
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -0,0 +1,15 @@
// @flow @enableNewMutationAliasingModel
import {identity, Stringify, useFragment} from 'shared-runtime';
component Example() {
const data = useFragment();
const {a, b} = identity(data);
const el = <Stringify tooltip={b} />;
identity(a.at(0));
return <Stringify icon={el} />;
}

View File

@@ -0,0 +1,74 @@
## Input
```javascript
// @enableNewMutationAliasingModel:true
export const App = () => {
const [selected, setSelected] = useState(new Set<string>());
const onSelectedChange = (value: string) => {
const newSelected = new Set(selected);
if (newSelected.has(value)) {
// This should not count as a mutation of `selected`
newSelected.delete(value);
} else {
// This should not count as a mutation of `selected`
newSelected.add(value);
}
setSelected(newSelected);
};
return <Stringify selected={selected} onSelectedChange={onSelectedChange} />;
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel:true
export const App = () => {
const $ = _c(6);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = new Set();
$[0] = t0;
} else {
t0 = $[0];
}
const [selected, setSelected] = useState(t0);
let t1;
if ($[1] !== selected) {
t1 = (value) => {
const newSelected = new Set(selected);
if (newSelected.has(value)) {
newSelected.delete(value);
} else {
newSelected.add(value);
}
setSelected(newSelected);
};
$[1] = selected;
$[2] = t1;
} else {
t1 = $[2];
}
const onSelectedChange = t1;
let t2;
if ($[3] !== onSelectedChange || $[4] !== selected) {
t2 = <Stringify selected={selected} onSelectedChange={onSelectedChange} />;
$[3] = onSelectedChange;
$[4] = selected;
$[5] = t2;
} else {
t2 = $[5];
}
return t2;
};
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -0,0 +1,18 @@
// @enableNewMutationAliasingModel:true
export const App = () => {
const [selected, setSelected] = useState(new Set<string>());
const onSelectedChange = (value: string) => {
const newSelected = new Set(selected);
if (newSelected.has(value)) {
// This should not count as a mutation of `selected`
newSelected.delete(value);
} else {
// This should not count as a mutation of `selected`
newSelected.add(value);
}
setSelected(newSelected);
};
return <Stringify selected={selected} onSelectedChange={onSelectedChange} />;
};

View File

@@ -0,0 +1,161 @@
## Input
```javascript
import {useMemo} from 'react';
import {
mutate,
typedCapture,
typedCreateFrom,
typedMutate,
ValidateMemoization,
} from 'shared-runtime';
function Component({a, b, c}: {a: number; b: number; c: number}) {
const x = useMemo(() => [{value: a}], [a, b, c]);
if (b === 0) {
// This object should only depend on c, it cannot be affected by the later mutation
x.push({value: c});
} else {
// This mutation shouldn't affect the object in the consequent
mutate(x);
}
return (
<>
<ValidateMemoization inputs={[a, b, c]} output={x} />;
{/* TODO: should only depend on c */}
<ValidateMemoization inputs={[a, b, c]} output={x[0]} />;
</>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: 0, b: 0, c: 0}],
sequentialRenders: [
{a: 0, b: 0, c: 0},
{a: 0, b: 1, c: 0},
{a: 1, b: 1, c: 0},
{a: 1, b: 1, c: 1},
{a: 1, b: 1, c: 0},
{a: 1, b: 0, c: 0},
{a: 0, b: 0, c: 0},
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { useMemo } from "react";
import {
mutate,
typedCapture,
typedCreateFrom,
typedMutate,
ValidateMemoization,
} from "shared-runtime";
function Component(t0) {
const $ = _c(22);
const { a, b, c } = t0;
let t1;
let x;
if ($[0] !== a || $[1] !== b || $[2] !== c) {
t1 = [{ value: a }];
x = t1;
if (b === 0) {
x.push({ value: c });
} else {
mutate(x);
}
$[0] = a;
$[1] = b;
$[2] = c;
$[3] = x;
$[4] = t1;
} else {
x = $[3];
t1 = $[4];
}
let t2;
if ($[5] !== a || $[6] !== b || $[7] !== c) {
t2 = [a, b, c];
$[5] = a;
$[6] = b;
$[7] = c;
$[8] = t2;
} else {
t2 = $[8];
}
let t3;
if ($[9] !== t2 || $[10] !== x) {
t3 = <ValidateMemoization inputs={t2} output={x} />;
$[9] = t2;
$[10] = x;
$[11] = t3;
} else {
t3 = $[11];
}
let t4;
if ($[12] !== a || $[13] !== b || $[14] !== c) {
t4 = [a, b, c];
$[12] = a;
$[13] = b;
$[14] = c;
$[15] = t4;
} else {
t4 = $[15];
}
let t5;
if ($[16] !== t4 || $[17] !== x[0]) {
t5 = <ValidateMemoization inputs={t4} output={x[0]} />;
$[16] = t4;
$[17] = x[0];
$[18] = t5;
} else {
t5 = $[18];
}
let t6;
if ($[19] !== t3 || $[20] !== t5) {
t6 = (
<>
{t3};{t5};
</>
);
$[19] = t3;
$[20] = t5;
$[21] = t6;
} else {
t6 = $[21];
}
return t6;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ a: 0, b: 0, c: 0 }],
sequentialRenders: [
{ a: 0, b: 0, c: 0 },
{ a: 0, b: 1, c: 0 },
{ a: 1, b: 1, c: 0 },
{ a: 1, b: 1, c: 1 },
{ a: 1, b: 1, c: 0 },
{ a: 1, b: 0, c: 0 },
{ a: 0, b: 0, c: 0 },
],
};
```
### Eval output
(kind: ok) <div>{"inputs":[0,0,0],"output":[{"value":0},{"value":0}]}</div>;<div>{"inputs":[0,0,0],"output":{"value":0}}</div>;
<div>{"inputs":[0,1,0],"output":[{"value":0},"joe"]}</div>;<div>{"inputs":[0,1,0],"output":{"value":0}}</div>;
<div>{"inputs":[1,1,0],"output":[{"value":1},"joe"]}</div>;<div>{"inputs":[1,1,0],"output":{"value":1}}</div>;
<div>{"inputs":[1,1,1],"output":[{"value":1},"joe"]}</div>;<div>{"inputs":[1,1,1],"output":{"value":1}}</div>;
<div>{"inputs":[1,1,0],"output":[{"value":1},"joe"]}</div>;<div>{"inputs":[1,1,0],"output":{"value":1}}</div>;
<div>{"inputs":[1,0,0],"output":[{"value":1},{"value":0}]}</div>;<div>{"inputs":[1,0,0],"output":{"value":1}}</div>;
<div>{"inputs":[0,0,0],"output":[{"value":0},{"value":0}]}</div>;<div>{"inputs":[0,0,0],"output":{"value":0}}</div>;

View File

@@ -0,0 +1,41 @@
import {useMemo} from 'react';
import {
mutate,
typedCapture,
typedCreateFrom,
typedMutate,
ValidateMemoization,
} from 'shared-runtime';
function Component({a, b, c}: {a: number; b: number; c: number}) {
const x = useMemo(() => [{value: a}], [a, b, c]);
if (b === 0) {
// This object should only depend on c, it cannot be affected by the later mutation
x.push({value: c});
} else {
// This mutation shouldn't affect the object in the consequent
mutate(x);
}
return (
<>
<ValidateMemoization inputs={[a, b, c]} output={x} />;
{/* TODO: should only depend on c */}
<ValidateMemoization inputs={[a, b, c]} output={x[0]} />;
</>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: 0, b: 0, c: 0}],
sequentialRenders: [
{a: 0, b: 0, c: 0},
{a: 0, b: 1, c: 0},
{a: 1, b: 1, c: 0},
{a: 1, b: 1, c: 1},
{a: 1, b: 1, c: 0},
{a: 1, b: 0, c: 0},
{a: 0, b: 0, c: 0},
],
};

View File

@@ -0,0 +1,116 @@
## Input
```javascript
import {useMemo} from 'react';
import {
typedCapture,
typedCreateFrom,
typedMutate,
ValidateMemoization,
} from 'shared-runtime';
function Component({a, b}) {
const x = useMemo(() => [{a}], [a]);
const f = () => {
const y = typedCreateFrom(x);
const z = typedCapture(y);
return z;
};
const z = f();
// does not mutate x, so x should not depend on b
typedMutate(z, b);
// TODO: this *should* only depend on `a`
return <ValidateMemoization inputs={[a, b]} output={x} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: 0, b: 0}],
sequentialRenders: [
{a: 0, b: 0},
{a: 0, b: 1},
{a: 1, b: 1},
{a: 0, b: 0},
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { useMemo } from "react";
import {
typedCapture,
typedCreateFrom,
typedMutate,
ValidateMemoization,
} from "shared-runtime";
function Component(t0) {
const $ = _c(10);
const { a, b } = t0;
let t1;
let x;
if ($[0] !== a || $[1] !== b) {
t1 = [{ a }];
x = t1;
const f = () => {
const y = typedCreateFrom(x);
const z = typedCapture(y);
return z;
};
const z_0 = f();
typedMutate(z_0, b);
$[0] = a;
$[1] = b;
$[2] = x;
$[3] = t1;
} else {
x = $[2];
t1 = $[3];
}
let t2;
if ($[4] !== a || $[5] !== b) {
t2 = [a, b];
$[4] = a;
$[5] = b;
$[6] = t2;
} else {
t2 = $[6];
}
let t3;
if ($[7] !== t2 || $[8] !== x) {
t3 = <ValidateMemoization inputs={t2} output={x} />;
$[7] = t2;
$[8] = x;
$[9] = t3;
} else {
t3 = $[9];
}
return t3;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ a: 0, b: 0 }],
sequentialRenders: [
{ a: 0, b: 0 },
{ a: 0, b: 1 },
{ a: 1, b: 1 },
{ a: 0, b: 0 },
],
};
```
### Eval output
(kind: ok) <div>{"inputs":[0,0],"output":[{"a":0}]}</div>
<div>{"inputs":[0,1],"output":[{"a":0}]}</div>
<div>{"inputs":[1,1],"output":[{"a":1}]}</div>
<div>{"inputs":[0,0],"output":[{"a":0}]}</div>

View File

@@ -0,0 +1,33 @@
import {useMemo} from 'react';
import {
typedCapture,
typedCreateFrom,
typedMutate,
ValidateMemoization,
} from 'shared-runtime';
function Component({a, b}) {
const x = useMemo(() => [{a}], [a]);
const f = () => {
const y = typedCreateFrom(x);
const z = typedCapture(y);
return z;
};
const z = f();
// does not mutate x, so x should not depend on b
typedMutate(z, b);
// TODO: this *should* only depend on `a`
return <ValidateMemoization inputs={[a, b]} output={x} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: 0, b: 0}],
sequentialRenders: [
{a: 0, b: 0},
{a: 0, b: 1},
{a: 1, b: 1},
{a: 0, b: 0},
],
};

View File

@@ -0,0 +1,153 @@
## Input
```javascript
import {useMemo} from 'react';
import {
typedCapture,
typedCreateFrom,
typedMutate,
ValidateMemoization,
} from 'shared-runtime';
function Component({a, b}) {
const o: any = useMemo(() => ({a}), [a]);
const x: Array<any> = useMemo(() => [o], [o, b]);
const y = typedCapture(x);
const z = typedCapture(y);
x.push(z);
x.push(b);
return (
<>
<ValidateMemoization inputs={[a]} output={o} />;
<ValidateMemoization inputs={[a, b]} output={x} />;
</>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: 0, b: 0}],
sequentialRenders: [
{a: 0, b: 0},
{a: 0, b: 1},
{a: 1, b: 1},
{a: 0, b: 0},
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { useMemo } from "react";
import {
typedCapture,
typedCreateFrom,
typedMutate,
ValidateMemoization,
} from "shared-runtime";
function Component(t0) {
const $ = _c(20);
const { a, b } = t0;
let t1;
let t2;
if ($[0] !== a) {
t2 = { a };
$[0] = a;
$[1] = t2;
} else {
t2 = $[1];
}
t1 = t2;
const o = t1;
let t3;
let x;
if ($[2] !== b || $[3] !== o) {
t3 = [o];
x = t3;
const y = typedCapture(x);
const z = typedCapture(y);
x.push(z);
x.push(b);
$[2] = b;
$[3] = o;
$[4] = x;
$[5] = t3;
} else {
x = $[4];
t3 = $[5];
}
let t4;
if ($[6] !== a) {
t4 = [a];
$[6] = a;
$[7] = t4;
} else {
t4 = $[7];
}
let t5;
if ($[8] !== o || $[9] !== t4) {
t5 = <ValidateMemoization inputs={t4} output={o} />;
$[8] = o;
$[9] = t4;
$[10] = t5;
} else {
t5 = $[10];
}
let t6;
if ($[11] !== a || $[12] !== b) {
t6 = [a, b];
$[11] = a;
$[12] = b;
$[13] = t6;
} else {
t6 = $[13];
}
let t7;
if ($[14] !== t6 || $[15] !== x) {
t7 = <ValidateMemoization inputs={t6} output={x} />;
$[14] = t6;
$[15] = x;
$[16] = t7;
} else {
t7 = $[16];
}
let t8;
if ($[17] !== t5 || $[18] !== t7) {
t8 = (
<>
{t5};{t7};
</>
);
$[17] = t5;
$[18] = t7;
$[19] = t8;
} else {
t8 = $[19];
}
return t8;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ a: 0, b: 0 }],
sequentialRenders: [
{ a: 0, b: 0 },
{ a: 0, b: 1 },
{ a: 1, b: 1 },
{ a: 0, b: 0 },
],
};
```
### Eval output
(kind: ok) <div>{"inputs":[0],"output":{"a":0}}</div>;<div>{"inputs":[0,0],"output":[{"a":0},[["[[ cyclic ref *2 ]]"]],0]}</div>;
<div>{"inputs":[0],"output":{"a":0}}</div>;<div>{"inputs":[0,1],"output":[{"a":0},[["[[ cyclic ref *2 ]]"]],1]}</div>;
<div>{"inputs":[1],"output":{"a":1}}</div>;<div>{"inputs":[1,1],"output":[{"a":1},[["[[ cyclic ref *2 ]]"]],1]}</div>;
<div>{"inputs":[0],"output":{"a":0}}</div>;<div>{"inputs":[0,0],"output":[{"a":0},[["[[ cyclic ref *2 ]]"]],0]}</div>;

View File

@@ -0,0 +1,34 @@
import {useMemo} from 'react';
import {
typedCapture,
typedCreateFrom,
typedMutate,
ValidateMemoization,
} from 'shared-runtime';
function Component({a, b}) {
const o: any = useMemo(() => ({a}), [a]);
const x: Array<any> = useMemo(() => [o], [o, b]);
const y = typedCapture(x);
const z = typedCapture(y);
x.push(z);
x.push(b);
return (
<>
<ValidateMemoization inputs={[a]} output={o} />;
<ValidateMemoization inputs={[a, b]} output={x} />;
</>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: 0, b: 0}],
sequentialRenders: [
{a: 0, b: 0},
{a: 0, b: 1},
{a: 1, b: 1},
{a: 0, b: 0},
],
};

View File

@@ -0,0 +1,115 @@
## Input
```javascript
import {useMemo} from 'react';
import {
typedCapture,
typedCreateFrom,
typedMutate,
ValidateMemoization,
} from 'shared-runtime';
function Component({a, b}: {a: number; b: number}) {
const x = useMemo(() => ({a}), [a, b]);
const f = () => {
const y = typedCapture(x);
const z = typedCreateFrom(y);
return z;
};
const z = f();
// mutates x
typedMutate(z, b);
return <ValidateMemoization inputs={[a, b]} output={x} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: 0, b: 0}],
sequentialRenders: [
{a: 0, b: 0},
{a: 0, b: 1},
{a: 1, b: 1},
{a: 0, b: 0},
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { useMemo } from "react";
import {
typedCapture,
typedCreateFrom,
typedMutate,
ValidateMemoization,
} from "shared-runtime";
function Component(t0) {
const $ = _c(10);
const { a, b } = t0;
let t1;
let x;
if ($[0] !== a || $[1] !== b) {
t1 = { a };
x = t1;
const f = () => {
const y = typedCapture(x);
const z = typedCreateFrom(y);
return z;
};
const z_0 = f();
typedMutate(z_0, b);
$[0] = a;
$[1] = b;
$[2] = x;
$[3] = t1;
} else {
x = $[2];
t1 = $[3];
}
let t2;
if ($[4] !== a || $[5] !== b) {
t2 = [a, b];
$[4] = a;
$[5] = b;
$[6] = t2;
} else {
t2 = $[6];
}
let t3;
if ($[7] !== t2 || $[8] !== x) {
t3 = <ValidateMemoization inputs={t2} output={x} />;
$[7] = t2;
$[8] = x;
$[9] = t3;
} else {
t3 = $[9];
}
return t3;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ a: 0, b: 0 }],
sequentialRenders: [
{ a: 0, b: 0 },
{ a: 0, b: 1 },
{ a: 1, b: 1 },
{ a: 0, b: 0 },
],
};
```
### Eval output
(kind: ok) <div>{"inputs":[0,0],"output":{"a":0,"property":0}}</div>
<div>{"inputs":[0,1],"output":{"a":0,"property":1}}</div>
<div>{"inputs":[1,1],"output":{"a":1,"property":1}}</div>
<div>{"inputs":[0,0],"output":{"a":0,"property":0}}</div>

View File

@@ -0,0 +1,32 @@
import {useMemo} from 'react';
import {
typedCapture,
typedCreateFrom,
typedMutate,
ValidateMemoization,
} from 'shared-runtime';
function Component({a, b}: {a: number; b: number}) {
const x = useMemo(() => ({a}), [a, b]);
const f = () => {
const y = typedCapture(x);
const z = typedCreateFrom(y);
return z;
};
const z = f();
// mutates x
typedMutate(z, b);
return <ValidateMemoization inputs={[a, b]} output={x} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: 0, b: 0}],
sequentialRenders: [
{a: 0, b: 0},
{a: 0, b: 1},
{a: 1, b: 1},
{a: 0, b: 0},
],
};

View File

@@ -0,0 +1,106 @@
## Input
```javascript
import {useMemo} from 'react';
import {
typedCapture,
typedCreateFrom,
typedMutate,
ValidateMemoization,
} from 'shared-runtime';
function Component({a, b}: {a: number; b: number}) {
const x = useMemo(() => ({a}), [a, b]);
const y = typedCapture(x);
const z = typedCreateFrom(y);
// mutates x
typedMutate(z, b);
return <ValidateMemoization inputs={[a, b]} output={x} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: 0, b: 0}],
sequentialRenders: [
{a: 0, b: 0},
{a: 0, b: 1},
{a: 1, b: 1},
{a: 0, b: 0},
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { useMemo } from "react";
import {
typedCapture,
typedCreateFrom,
typedMutate,
ValidateMemoization,
} from "shared-runtime";
function Component(t0) {
const $ = _c(10);
const { a, b } = t0;
let t1;
let x;
if ($[0] !== a || $[1] !== b) {
t1 = { a };
x = t1;
const y = typedCapture(x);
const z = typedCreateFrom(y);
typedMutate(z, b);
$[0] = a;
$[1] = b;
$[2] = x;
$[3] = t1;
} else {
x = $[2];
t1 = $[3];
}
let t2;
if ($[4] !== a || $[5] !== b) {
t2 = [a, b];
$[4] = a;
$[5] = b;
$[6] = t2;
} else {
t2 = $[6];
}
let t3;
if ($[7] !== t2 || $[8] !== x) {
t3 = <ValidateMemoization inputs={t2} output={x} />;
$[7] = t2;
$[8] = x;
$[9] = t3;
} else {
t3 = $[9];
}
return t3;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ a: 0, b: 0 }],
sequentialRenders: [
{ a: 0, b: 0 },
{ a: 0, b: 1 },
{ a: 1, b: 1 },
{ a: 0, b: 0 },
],
};
```
### Eval output
(kind: ok) <div>{"inputs":[0,0],"output":{"a":0,"property":0}}</div>
<div>{"inputs":[0,1],"output":{"a":0,"property":1}}</div>
<div>{"inputs":[1,1],"output":{"a":1,"property":1}}</div>
<div>{"inputs":[0,0],"output":{"a":0,"property":0}}</div>

View File

@@ -0,0 +1,28 @@
import {useMemo} from 'react';
import {
typedCapture,
typedCreateFrom,
typedMutate,
ValidateMemoization,
} from 'shared-runtime';
function Component({a, b}: {a: number; b: number}) {
const x = useMemo(() => ({a}), [a, b]);
const y = typedCapture(x);
const z = typedCreateFrom(y);
// mutates x
typedMutate(z, b);
return <ValidateMemoization inputs={[a, b]} output={x} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: 0, b: 0}],
sequentialRenders: [
{a: 0, b: 0},
{a: 0, b: 1},
{a: 1, b: 1},
{a: 0, b: 0},
],
};

View File

@@ -0,0 +1,103 @@
## Input
```javascript
import {useMemo} from 'react';
import {
typedCapture,
typedCreateFrom,
typedMutate,
ValidateMemoization,
} from 'shared-runtime';
function Component({a, b}) {
const x = useMemo(() => [{a}], [a]);
const y = typedCreateFrom(x);
const z = typedCapture(y);
// does not mutate x, so x should not depend on b
typedMutate(z, b);
return <ValidateMemoization inputs={[a]} output={x} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: 0, b: 0}],
sequentialRenders: [
{a: 0, b: 0},
{a: 0, b: 1},
{a: 1, b: 1},
{a: 0, b: 0},
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { useMemo } from "react";
import {
typedCapture,
typedCreateFrom,
typedMutate,
ValidateMemoization,
} from "shared-runtime";
function Component(t0) {
const $ = _c(7);
const { a, b } = t0;
let t1;
let t2;
if ($[0] !== a) {
t2 = [{ a }];
$[0] = a;
$[1] = t2;
} else {
t2 = $[1];
}
t1 = t2;
const x = t1;
const y = typedCreateFrom(x);
const z = typedCapture(y);
typedMutate(z, b);
let t3;
if ($[2] !== a) {
t3 = [a];
$[2] = a;
$[3] = t3;
} else {
t3 = $[3];
}
let t4;
if ($[4] !== t3 || $[5] !== x) {
t4 = <ValidateMemoization inputs={t3} output={x} />;
$[4] = t3;
$[5] = x;
$[6] = t4;
} else {
t4 = $[6];
}
return t4;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ a: 0, b: 0 }],
sequentialRenders: [
{ a: 0, b: 0 },
{ a: 0, b: 1 },
{ a: 1, b: 1 },
{ a: 0, b: 0 },
],
};
```
### Eval output
(kind: ok) <div>{"inputs":[0],"output":[{"a":0}]}</div>
<div>{"inputs":[0],"output":[{"a":0}]}</div>
<div>{"inputs":[1],"output":[{"a":1}]}</div>
<div>{"inputs":[0],"output":[{"a":0}]}</div>

View File

@@ -0,0 +1,28 @@
import {useMemo} from 'react';
import {
typedCapture,
typedCreateFrom,
typedMutate,
ValidateMemoization,
} from 'shared-runtime';
function Component({a, b}) {
const x = useMemo(() => [{a}], [a]);
const y = typedCreateFrom(x);
const z = typedCapture(y);
// does not mutate x, so x should not depend on b
typedMutate(z, b);
return <ValidateMemoization inputs={[a]} output={x} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: 0, b: 0}],
sequentialRenders: [
{a: 0, b: 0},
{a: 0, b: 1},
{a: 1, b: 1},
{a: 0, b: 0},
],
};

View File

@@ -0,0 +1,122 @@
## Input
```javascript
import {useMemo} from 'react';
import {
typedCapture,
typedCreateFrom,
typedMutate,
ValidateMemoization,
} from 'shared-runtime';
function Component({a, b}) {
const x = useMemo(() => [{a}], [a, b]);
let z: any;
if (b) {
z = x;
} else {
z = typedCapture(x);
}
// could mutate x
typedMutate(z, b);
return <ValidateMemoization inputs={[a, b]} output={x} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: 0, b: 0}],
sequentialRenders: [
{a: 0, b: 0},
{a: 0, b: 1},
{a: 1, b: 1},
{a: 0, b: 0},
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { useMemo } from "react";
import {
typedCapture,
typedCreateFrom,
typedMutate,
ValidateMemoization,
} from "shared-runtime";
function Component(t0) {
const $ = _c(12);
const { a, b } = t0;
let t1;
let t2;
if ($[0] !== a) {
t2 = { a };
$[0] = a;
$[1] = t2;
} else {
t2 = $[1];
}
let x;
if ($[2] !== b || $[3] !== t2) {
t1 = [t2];
x = t1;
let z;
if (b) {
z = x;
} else {
z = typedCapture(x);
}
typedMutate(z, b);
$[2] = b;
$[3] = t2;
$[4] = x;
$[5] = t1;
} else {
x = $[4];
t1 = $[5];
}
let t3;
if ($[6] !== a || $[7] !== b) {
t3 = [a, b];
$[6] = a;
$[7] = b;
$[8] = t3;
} else {
t3 = $[8];
}
let t4;
if ($[9] !== t3 || $[10] !== x) {
t4 = <ValidateMemoization inputs={t3} output={x} />;
$[9] = t3;
$[10] = x;
$[11] = t4;
} else {
t4 = $[11];
}
return t4;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ a: 0, b: 0 }],
sequentialRenders: [
{ a: 0, b: 0 },
{ a: 0, b: 1 },
{ a: 1, b: 1 },
{ a: 0, b: 0 },
],
};
```
### Eval output
(kind: ok) <div>{"inputs":[0,0],"output":[{"a":0}]}</div>
<div>{"inputs":[0,1],"output":[{"a":0}]}</div>
<div>{"inputs":[1,1],"output":[{"a":1}]}</div>
<div>{"inputs":[0,0],"output":[{"a":0}]}</div>

View File

@@ -0,0 +1,32 @@
import {useMemo} from 'react';
import {
typedCapture,
typedCreateFrom,
typedMutate,
ValidateMemoization,
} from 'shared-runtime';
function Component({a, b}) {
const x = useMemo(() => [{a}], [a, b]);
let z: any;
if (b) {
z = x;
} else {
z = typedCapture(x);
}
// could mutate x
typedMutate(z, b);
return <ValidateMemoization inputs={[a, b]} output={x} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: 0, b: 0}],
sequentialRenders: [
{a: 0, b: 0},
{a: 0, b: 1},
{a: 1, b: 1},
{a: 0, b: 0},
],
};

View File

@@ -0,0 +1,121 @@
## Input
```javascript
// @enableNewMutationAliasingModel
import {useMemo} from 'react';
import {
identity,
makeObject_Primitives,
typedIdentity,
useIdentity,
ValidateMemoization,
} from 'shared-runtime';
function Component({a, b}) {
// create a mutable value with input `a`
const x = useMemo(() => makeObject_Primitives(a), [a]);
// freeze the value
useIdentity(x);
// known to pass-through via aliasing signature
const x2 = typedIdentity(x);
// Unknown function so we assume it conditionally mutates,
// but x2 is frozen so this downgrades to a read.
// x should *not* take b as a dependency
identity(x2, b);
return <ValidateMemoization inputs={[a]} output={x} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: 0, b: 0}],
sequentialRenders: [
{a: 0, b: 0},
{a: 1, b: 0},
{a: 1, b: 1},
{a: 0, b: 1},
{a: 0, b: 0},
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
import { useMemo } from "react";
import {
identity,
makeObject_Primitives,
typedIdentity,
useIdentity,
ValidateMemoization,
} from "shared-runtime";
function Component(t0) {
const $ = _c(7);
const { a, b } = t0;
let t1;
let t2;
if ($[0] !== a) {
t2 = makeObject_Primitives(a);
$[0] = a;
$[1] = t2;
} else {
t2 = $[1];
}
t1 = t2;
const x = t1;
useIdentity(x);
const x2 = typedIdentity(x);
identity(x2, b);
let t3;
if ($[2] !== a) {
t3 = [a];
$[2] = a;
$[3] = t3;
} else {
t3 = $[3];
}
let t4;
if ($[4] !== t3 || $[5] !== x) {
t4 = <ValidateMemoization inputs={t3} output={x} />;
$[4] = t3;
$[5] = x;
$[6] = t4;
} else {
t4 = $[6];
}
return t4;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ a: 0, b: 0 }],
sequentialRenders: [
{ a: 0, b: 0 },
{ a: 1, b: 0 },
{ a: 1, b: 1 },
{ a: 0, b: 1 },
{ a: 0, b: 0 },
],
};
```
### Eval output
(kind: ok) <div>{"inputs":[0],"output":{"a":0,"b":"value1","c":true}}</div>
<div>{"inputs":[1],"output":{"a":0,"b":"value1","c":true}}</div>
<div>{"inputs":[1],"output":{"a":0,"b":"value1","c":true}}</div>
<div>{"inputs":[0],"output":{"a":0,"b":"value1","c":true}}</div>
<div>{"inputs":[0],"output":{"a":0,"b":"value1","c":true}}</div>

View File

@@ -0,0 +1,40 @@
// @enableNewMutationAliasingModel
import {useMemo} from 'react';
import {
identity,
makeObject_Primitives,
typedIdentity,
useIdentity,
ValidateMemoization,
} from 'shared-runtime';
function Component({a, b}) {
// create a mutable value with input `a`
const x = useMemo(() => makeObject_Primitives(a), [a]);
// freeze the value
useIdentity(x);
// known to pass-through via aliasing signature
const x2 = typedIdentity(x);
// Unknown function so we assume it conditionally mutates,
// but x2 is frozen so this downgrades to a read.
// x should *not* take b as a dependency
identity(x2, b);
return <ValidateMemoization inputs={[a]} output={x} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: 0, b: 0}],
sequentialRenders: [
{a: 0, b: 0},
{a: 1, b: 0},
{a: 1, b: 1},
{a: 0, b: 1},
{a: 0, b: 0},
],
};

View File

@@ -0,0 +1,112 @@
## Input
```javascript
// @enableNewMutationAliasingModel
import {
identity,
makeObject_Primitives,
typedIdentity,
useIdentity,
ValidateMemoization,
} from 'shared-runtime';
function Component({a, b}) {
// create a mutable value with input `a`
const x = makeObject_Primitives(a);
// known to pass-through via aliasing signature
const x2 = typedIdentity(x);
// Unknown function so we assume it conditionally mutates,
// and x is still mutable so
identity(x2, b);
return <ValidateMemoization inputs={[a, b]} output={x} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: 0, b: 0}],
sequentialRenders: [
{a: 0, b: 0},
{a: 1, b: 0},
{a: 1, b: 1},
{a: 0, b: 1},
{a: 0, b: 0},
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
import {
identity,
makeObject_Primitives,
typedIdentity,
useIdentity,
ValidateMemoization,
} from "shared-runtime";
function Component(t0) {
const $ = _c(9);
const { a, b } = t0;
let x;
if ($[0] !== a || $[1] !== b) {
x = makeObject_Primitives(a);
const x2 = typedIdentity(x);
identity(x2, b);
$[0] = a;
$[1] = b;
$[2] = x;
} else {
x = $[2];
}
let t1;
if ($[3] !== a || $[4] !== b) {
t1 = [a, b];
$[3] = a;
$[4] = b;
$[5] = t1;
} else {
t1 = $[5];
}
let t2;
if ($[6] !== t1 || $[7] !== x) {
t2 = <ValidateMemoization inputs={t1} output={x} />;
$[6] = t1;
$[7] = x;
$[8] = t2;
} else {
t2 = $[8];
}
return t2;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ a: 0, b: 0 }],
sequentialRenders: [
{ a: 0, b: 0 },
{ a: 1, b: 0 },
{ a: 1, b: 1 },
{ a: 0, b: 1 },
{ a: 0, b: 0 },
],
};
```
### Eval output
(kind: ok) <div>{"inputs":[0,0],"output":{"a":0,"b":"value1","c":true}}</div>
<div>{"inputs":[1,0],"output":{"a":0,"b":"value1","c":true}}</div>
<div>{"inputs":[1,1],"output":{"a":0,"b":"value1","c":true}}</div>
<div>{"inputs":[0,1],"output":{"a":0,"b":"value1","c":true}}</div>
<div>{"inputs":[0,0],"output":{"a":0,"b":"value1","c":true}}</div>

View File

@@ -0,0 +1,35 @@
// @enableNewMutationAliasingModel
import {
identity,
makeObject_Primitives,
typedIdentity,
useIdentity,
ValidateMemoization,
} from 'shared-runtime';
function Component({a, b}) {
// create a mutable value with input `a`
const x = makeObject_Primitives(a);
// known to pass-through via aliasing signature
const x2 = typedIdentity(x);
// Unknown function so we assume it conditionally mutates,
// and x is still mutable so
identity(x2, b);
return <ValidateMemoization inputs={[a, b]} output={x} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: 0, b: 0}],
sequentialRenders: [
{a: 0, b: 0},
{a: 1, b: 0},
{a: 1, b: 1},
{a: 0, b: 1},
{a: 0, b: 0},
],
};

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @validateRefAccessDuringRender false
// @validateRefAccessDuringRender:false
function VideoTab() {
const ref = useRef();
const t = ref.current;
@@ -18,7 +18,7 @@ function VideoTab() {
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateRefAccessDuringRender false
import { c as _c } from "react/compiler-runtime"; // @validateRefAccessDuringRender:false
function VideoTab() {
const $ = _c(1);
const ref = useRef();

View File

@@ -1,4 +1,4 @@
// @validateRefAccessDuringRender false
// @validateRefAccessDuringRender:false
function VideoTab() {
const ref = useRef();
const t = ref.current;

View File

@@ -0,0 +1,65 @@
## Input
```javascript
import {Stringify, useIdentity} from 'shared-runtime';
function Component({other, ...props}, ref) {
[props, ref] = useIdentity([props, ref]);
return <Stringify props={props} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: 0, b: 'hello', children: <div>Hello</div>}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { Stringify, useIdentity } from "shared-runtime";
function Component(t0, ref) {
const $ = _c(7);
let props;
if ($[0] !== t0) {
let { other, ...t1 } = t0;
props = t1;
$[0] = t0;
$[1] = props;
} else {
props = $[1];
}
let t1;
if ($[2] !== props || $[3] !== ref) {
t1 = [props, ref];
$[2] = props;
$[3] = ref;
$[4] = t1;
} else {
t1 = $[4];
}
[props, ref] = useIdentity(t1);
let t2;
if ($[5] !== props) {
t2 = <Stringify props={props} />;
$[5] = props;
$[6] = t2;
} else {
t2 = $[6];
}
return t2;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ a: 0, b: "hello", children: <div>Hello</div> }],
};
```
### Eval output
(kind: ok) <div>{"props":{"a":0,"b":"hello","children":{"type":"div","key":null,"props":{"children":"Hello"},"_owner":"[[ cyclic ref *3 ]]","_store":{}}}}</div>

View File

@@ -0,0 +1,11 @@
import {Stringify, useIdentity} from 'shared-runtime';
function Component({other, ...props}, ref) {
[props, ref] = useIdentity([props, ref]);
return <Stringify props={props} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: 0, b: 'hello', children: <div>Hello</div>}],
};

View File

@@ -50,10 +50,8 @@ function Component(props) {
case 1: {
break bb0;
}
case 2: {
}
default: {
}
case 2:
default:
}
} else {
if (props.cond2) {

View File

@@ -41,8 +41,7 @@ function foo() {
case 2: {
break bb0;
}
default: {
}
default:
}
}

View File

@@ -43,22 +43,17 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
function foo(x) {
bb0: switch (x) {
case 0: {
}
case 1: {
}
case 0:
case 1:
case 2: {
break bb0;
}
case 3: {
break bb0;
}
case 4: {
}
case 5: {
}
default: {
}
case 4:
case 5:
default:
}
}

View File

@@ -2,6 +2,7 @@
## Input
```javascript
import {useMemo} from 'react';
import {ValidateMemoization} from 'shared-runtime';
function Component({a, b, c}) {
@@ -13,9 +14,21 @@ function Component({a, b, c}) {
return (
<>
<ValidateMemoization inputs={[a, c]} output={map} />
<ValidateMemoization inputs={[a, c]} output={mapAlias} />
<ValidateMemoization inputs={[b]} output={[hasB]} />
<ValidateMemoization
inputs={[a, c]}
output={map}
onlyCheckCompiled={true}
/>
<ValidateMemoization
inputs={[a, c]}
output={mapAlias}
onlyCheckCompiled={true}
/>
<ValidateMemoization
inputs={[b]}
output={[hasB]}
onlyCheckCompiled={true}
/>
</>
);
}
@@ -44,6 +57,7 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
import { useMemo } from "react";
import { ValidateMemoization } from "shared-runtime";
function Component(t0) {
@@ -76,7 +90,9 @@ function Component(t0) {
}
let t2;
if ($[7] !== map || $[8] !== t1) {
t2 = <ValidateMemoization inputs={t1} output={map} />;
t2 = (
<ValidateMemoization inputs={t1} output={map} onlyCheckCompiled={true} />
);
$[7] = map;
$[8] = t1;
$[9] = t2;
@@ -94,7 +110,13 @@ function Component(t0) {
}
let t4;
if ($[13] !== mapAlias || $[14] !== t3) {
t4 = <ValidateMemoization inputs={t3} output={mapAlias} />;
t4 = (
<ValidateMemoization
inputs={t3}
output={mapAlias}
onlyCheckCompiled={true}
/>
);
$[13] = mapAlias;
$[14] = t3;
$[15] = t4;
@@ -119,7 +141,9 @@ function Component(t0) {
}
let t7;
if ($[20] !== t5 || $[21] !== t6) {
t7 = <ValidateMemoization inputs={t5} output={t6} />;
t7 = (
<ValidateMemoization inputs={t5} output={t6} onlyCheckCompiled={true} />
);
$[20] = t5;
$[21] = t6;
$[22] = t7;

View File

@@ -1,3 +1,4 @@
import {useMemo} from 'react';
import {ValidateMemoization} from 'shared-runtime';
function Component({a, b, c}) {
@@ -9,9 +10,21 @@ function Component({a, b, c}) {
return (
<>
<ValidateMemoization inputs={[a, c]} output={map} />
<ValidateMemoization inputs={[a, c]} output={mapAlias} />
<ValidateMemoization inputs={[b]} output={[hasB]} />
<ValidateMemoization
inputs={[a, c]}
output={map}
onlyCheckCompiled={true}
/>
<ValidateMemoization
inputs={[a, c]}
output={mapAlias}
onlyCheckCompiled={true}
/>
<ValidateMemoization
inputs={[b]}
output={[hasB]}
onlyCheckCompiled={true}
/>
</>
);
}

View File

@@ -2,6 +2,7 @@
## Input
```javascript
import {useMemo} from 'react';
import {ValidateMemoization} from 'shared-runtime';
function Component({a, b, c}) {
@@ -13,9 +14,21 @@ function Component({a, b, c}) {
return (
<>
<ValidateMemoization inputs={[a, c]} output={set} />
<ValidateMemoization inputs={[a, c]} output={setAlias} />
<ValidateMemoization inputs={[b]} output={[hasB]} />
<ValidateMemoization
inputs={[a, c]}
output={set}
onlyCheckCompiled={true}
/>
<ValidateMemoization
inputs={[a, c]}
output={setAlias}
onlyCheckCompiled={true}
/>
<ValidateMemoization
inputs={[b]}
output={[hasB]}
onlyCheckCompiled={true}
/>
</>
);
}
@@ -44,6 +57,7 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
import { useMemo } from "react";
import { ValidateMemoization } from "shared-runtime";
function Component(t0) {
@@ -76,7 +90,9 @@ function Component(t0) {
}
let t2;
if ($[7] !== set || $[8] !== t1) {
t2 = <ValidateMemoization inputs={t1} output={set} />;
t2 = (
<ValidateMemoization inputs={t1} output={set} onlyCheckCompiled={true} />
);
$[7] = set;
$[8] = t1;
$[9] = t2;
@@ -94,7 +110,13 @@ function Component(t0) {
}
let t4;
if ($[13] !== setAlias || $[14] !== t3) {
t4 = <ValidateMemoization inputs={t3} output={setAlias} />;
t4 = (
<ValidateMemoization
inputs={t3}
output={setAlias}
onlyCheckCompiled={true}
/>
);
$[13] = setAlias;
$[14] = t3;
$[15] = t4;
@@ -119,7 +141,9 @@ function Component(t0) {
}
let t7;
if ($[20] !== t5 || $[21] !== t6) {
t7 = <ValidateMemoization inputs={t5} output={t6} />;
t7 = (
<ValidateMemoization inputs={t5} output={t6} onlyCheckCompiled={true} />
);
$[20] = t5;
$[21] = t6;
$[22] = t7;

View File

@@ -1,3 +1,4 @@
import {useMemo} from 'react';
import {ValidateMemoization} from 'shared-runtime';
function Component({a, b, c}) {
@@ -9,9 +10,21 @@ function Component({a, b, c}) {
return (
<>
<ValidateMemoization inputs={[a, c]} output={set} />
<ValidateMemoization inputs={[a, c]} output={setAlias} />
<ValidateMemoization inputs={[b]} output={[hasB]} />
<ValidateMemoization
inputs={[a, c]}
output={set}
onlyCheckCompiled={true}
/>
<ValidateMemoization
inputs={[a, c]}
output={setAlias}
onlyCheckCompiled={true}
/>
<ValidateMemoization
inputs={[b]}
output={[hasB]}
onlyCheckCompiled={true}
/>
</>
);
}

View File

@@ -30,6 +30,7 @@ export {
export {
Effect,
ValueKind,
ValueReason,
printHIR,
printFunctionWithOutlined,
validateEnvironmentConfig,

View File

@@ -18,7 +18,11 @@ import type {
CompilerReactTarget,
CompilerPipelineValue,
} from 'babel-plugin-react-compiler/src/Entrypoint';
import type {Effect, ValueKind} from 'babel-plugin-react-compiler/src/HIR';
import type {
Effect,
ValueKind,
ValueReason,
} from 'babel-plugin-react-compiler/src/HIR';
import type {parseConfigPragmaForTests as ParseConfigPragma} from 'babel-plugin-react-compiler/src/Utils/TestUtils';
import * as HermesParser from 'hermes-parser';
import invariant from 'invariant';
@@ -42,6 +46,7 @@ function makePluginOptions(
debugIRLogger: (value: CompilerPipelineValue) => void,
EffectEnum: typeof Effect,
ValueKindEnum: typeof ValueKind,
ValueReasonEnum: typeof ValueReason,
): [PluginOptions, Array<{filename: string | null; event: LoggerEvent}>] {
// TODO(@mofeiZ) rewrite snap fixtures to @validatePreserveExistingMemo:false
let validatePreserveExistingMemoizationGuarantees = false;
@@ -77,6 +82,7 @@ function makePluginOptions(
moduleTypeProvider: makeSharedRuntimeTypeProvider({
EffectEnum,
ValueKindEnum,
ValueReasonEnum,
}),
assertValidMutableRanges: true,
validatePreserveExistingMemoizationGuarantees,
@@ -209,6 +215,7 @@ export async function transformFixtureInput(
debugIRLogger: (value: CompilerPipelineValue) => void,
EffectEnum: typeof Effect,
ValueKindEnum: typeof ValueKind,
ValueReasonEnum: typeof ValueReason,
): Promise<{kind: 'ok'; value: TransformResult} | {kind: 'err'; msg: string}> {
// Extract the first line to quickly check for custom test directives
const firstLine = input.substring(0, input.indexOf('\n'));
@@ -237,6 +244,7 @@ export async function transformFixtureInput(
debugIRLogger,
EffectEnum,
ValueKindEnum,
ValueReasonEnum,
);
const forgetResult = transformFromAstSync(inputAst, input, {
filename: virtualFilepath,

View File

@@ -24,6 +24,7 @@ import type {
CompilerPipelineValue,
Effect,
ValueKind,
ValueReason,
} from 'babel-plugin-react-compiler/src';
import chalk from 'chalk';
@@ -78,6 +79,9 @@ async function compile(
const ValueKindEnum = importedCompilerPlugin[
'ValueKind'
] as typeof ValueKind;
const ValueReasonEnum = importedCompilerPlugin[
'ValueReason'
] as typeof ValueReason;
const printFunctionWithOutlined = importedCompilerPlugin[
PRINT_HIR_IMPORT
] as typeof PrintFunctionWithOutlined;
@@ -128,6 +132,7 @@ async function compile(
debugIRLogger,
EffectEnum,
ValueKindEnum,
ValueReasonEnum,
);
if (result.kind === 'err') {

View File

@@ -5,15 +5,21 @@
* LICENSE file in the root directory of this source tree.
*/
import type {Effect, ValueKind} from 'babel-plugin-react-compiler/src';
import type {
Effect,
ValueKind,
ValueReason,
} from 'babel-plugin-react-compiler/src';
import type {TypeConfig} from 'babel-plugin-react-compiler/src/HIR/TypeSchema';
export function makeSharedRuntimeTypeProvider({
EffectEnum,
ValueKindEnum,
ValueReasonEnum,
}: {
EffectEnum: typeof Effect;
ValueKindEnum: typeof ValueKind;
ValueReasonEnum: typeof ValueReason;
}) {
return function sharedRuntimeTypeProvider(
moduleName: string,
@@ -69,6 +75,127 @@ export function makeSharedRuntimeTypeProvider({
returnValueKind: ValueKindEnum.Mutable,
noAlias: true,
},
typedIdentity: {
kind: 'function',
positionalParams: [EffectEnum.Read],
restParam: null,
calleeEffect: EffectEnum.Read,
returnType: {kind: 'type', name: 'Any'},
returnValueKind: ValueKindEnum.Mutable,
aliasing: {
receiver: '@receiver',
params: ['@value'],
rest: null,
returns: '@return',
temporaries: [],
effects: [{kind: 'Assign', from: '@value', into: '@return'}],
},
},
typedAssign: {
kind: 'function',
positionalParams: [EffectEnum.Read],
restParam: null,
calleeEffect: EffectEnum.Read,
returnType: {kind: 'type', name: 'Any'},
returnValueKind: ValueKindEnum.Mutable,
aliasing: {
receiver: '@receiver',
params: ['@value'],
rest: null,
returns: '@return',
temporaries: [],
effects: [{kind: 'Assign', from: '@value', into: '@return'}],
},
},
typedAlias: {
kind: 'function',
positionalParams: [EffectEnum.Read],
restParam: null,
calleeEffect: EffectEnum.Read,
returnType: {kind: 'type', name: 'Any'},
returnValueKind: ValueKindEnum.Mutable,
aliasing: {
receiver: '@receiver',
params: ['@value'],
rest: null,
returns: '@return',
temporaries: [],
effects: [
{
kind: 'Create',
into: '@return',
value: ValueKindEnum.Mutable,
reason: ValueReasonEnum.KnownReturnSignature,
},
{kind: 'Alias', from: '@value', into: '@return'},
],
},
},
typedCapture: {
kind: 'function',
positionalParams: [EffectEnum.Read],
restParam: null,
calleeEffect: EffectEnum.Read,
returnType: {kind: 'type', name: 'Array'},
returnValueKind: ValueKindEnum.Mutable,
aliasing: {
receiver: '@receiver',
params: ['@value'],
rest: null,
returns: '@return',
temporaries: [],
effects: [
{
kind: 'Create',
into: '@return',
value: ValueKindEnum.Mutable,
reason: ValueReasonEnum.KnownReturnSignature,
},
{kind: 'Capture', from: '@value', into: '@return'},
],
},
},
typedCreateFrom: {
kind: 'function',
positionalParams: [EffectEnum.Read],
restParam: null,
calleeEffect: EffectEnum.Read,
returnType: {kind: 'type', name: 'Any'},
returnValueKind: ValueKindEnum.Mutable,
aliasing: {
receiver: '@receiver',
params: ['@value'],
rest: null,
returns: '@return',
temporaries: [],
effects: [{kind: 'CreateFrom', from: '@value', into: '@return'}],
},
},
typedMutate: {
kind: 'function',
positionalParams: [EffectEnum.Read, EffectEnum.Capture],
restParam: null,
calleeEffect: EffectEnum.Store,
returnType: {kind: 'type', name: 'Primitive'},
returnValueKind: ValueKindEnum.Primitive,
aliasing: {
receiver: '@receiver',
params: ['@object', '@value'],
rest: null,
returns: '@return',
temporaries: [],
effects: [
{
kind: 'Create',
into: '@return',
value: ValueKindEnum.Primitive,
reason: ValueReasonEnum.KnownReturnSignature,
},
{kind: 'Mutate', value: '@object'},
{kind: 'Capture', from: '@value', into: '@object'},
],
},
},
},
};
} else if (moduleName === 'ReactCompilerTest') {

View File

@@ -272,7 +272,7 @@ export function ValidateMemoization({
}: {
inputs: Array<any>;
output: any;
onlyCheckCompiled: boolean;
onlyCheckCompiled?: boolean;
}): React.ReactElement {
'use no forget';
// Wrap rawOutput as it might be a function, which useState would invoke.
@@ -280,8 +280,9 @@ export function ValidateMemoization({
const [previousInputs, setPreviousInputs] = React.useState(inputs);
const [previousOutput, setPreviousOutput] = React.useState(output);
if (
onlyCheckCompiled &&
(globalThis as any).__SNAP_EVALUATOR_MODE === 'forget'
!onlyCheckCompiled ||
(onlyCheckCompiled &&
(globalThis as any).__SNAP_EVALUATOR_MODE === 'forget')
) {
if (
inputs.length !== previousInputs.length ||
@@ -396,4 +397,28 @@ export function typedLog(...values: Array<any>): void {
console.log(...values);
}
export function typedIdentity<T>(value: T): T {
return value;
}
export function typedAssign<T>(x: T): T {
return x;
}
export function typedAlias<T>(x: T): T {
return x;
}
export function typedCapture<T>(x: T): Array<T> {
return [x];
}
export function typedCreateFrom<T>(array: Array<T>): T {
return array[0];
}
export function typedMutate(x: any, v: any = null): void {
x.property = v;
}
export default typedLog;

View File

@@ -104,6 +104,9 @@ async function renderApp(req, res, next) {
if (req.headers['cache-control']) {
proxiedHeaders['Cache-Control'] = req.get('cache-control');
}
if (req.get('rsc-request-id')) {
proxiedHeaders['rsc-request-id'] = req.get('rsc-request-id');
}
const requestsPrerender = req.path === '/prerender';

View File

@@ -50,7 +50,27 @@ const {readFile} = require('fs').promises;
const React = require('react');
async function renderApp(res, returnValue, formState, noCache) {
const activeDebugChannels =
process.env.NODE_ENV === 'development' ? new Map() : null;
function getDebugChannel(req) {
if (process.env.NODE_ENV !== 'development') {
return undefined;
}
const requestId = req.get('rsc-request-id');
if (!requestId) {
return undefined;
}
return activeDebugChannels.get(requestId);
}
async function renderApp(
res,
returnValue,
formState,
noCache,
promiseForDebugChannel
) {
const {renderToPipeableStream} = await import(
'react-server-dom-webpack/server'
);
@@ -101,7 +121,9 @@ async function renderApp(res, returnValue, formState, noCache) {
);
// For client-invoked server actions we refresh the tree and return a return value.
const payload = {root, returnValue, formState};
const {pipe} = renderToPipeableStream(payload, moduleMap);
const {pipe} = renderToPipeableStream(payload, moduleMap, {
debugChannel: await promiseForDebugChannel,
});
pipe(res);
}
@@ -166,7 +188,7 @@ app.get('/', async function (req, res) {
if ('prerender' in req.query) {
await prerenderApp(res, null, null, noCache);
} else {
await renderApp(res, null, null, noCache);
await renderApp(res, null, null, noCache, getDebugChannel(req));
}
});
@@ -204,7 +226,7 @@ app.post('/', bodyParser.text(), async function (req, res) {
// We handle the error on the client
}
// Refresh the client and return the value
renderApp(res, result, null, noCache);
renderApp(res, result, null, noCache, getDebugChannel(req));
} else {
// This is the progressive enhancement case
const UndiciRequest = require('undici').Request;
@@ -220,11 +242,11 @@ app.post('/', bodyParser.text(), async function (req, res) {
// Wait for any mutations
const result = await action();
const formState = decodeFormState(result, formData);
renderApp(res, null, formState, noCache);
renderApp(res, null, formState, noCache, undefined);
} catch (x) {
const {setServerState} = await import('../src/ServerState.js');
setServerState('Error: ' + x.message);
renderApp(res, null, null, noCache);
renderApp(res, null, null, noCache, undefined);
}
}
});
@@ -324,7 +346,7 @@ if (process.env.NODE_ENV === 'development') {
});
}
app.listen(3001, () => {
const httpServer = app.listen(3001, () => {
console.log('Regional Flight Server listening on port 3001...');
});
@@ -346,3 +368,27 @@ app.on('error', function (error) {
throw error;
}
});
if (process.env.NODE_ENV === 'development') {
// Open a websocket server for Debug information
const WebSocket = require('ws');
const webSocketServer = new WebSocket.Server({noServer: true});
httpServer.on('upgrade', (request, socket, head) => {
const DEBUG_CHANNEL_PATH = '/debug-channel?';
if (request.url.startsWith(DEBUG_CHANNEL_PATH)) {
const requestId = request.url.slice(DEBUG_CHANNEL_PATH.length);
const promiseForWs = new Promise(resolve => {
webSocketServer.handleUpgrade(request, socket, head, ws => {
ws.on('close', () => {
activeDebugChannels.delete(requestId);
});
resolve(ws);
});
});
activeDebugChannels.set(requestId, promiseForWs);
} else {
socket.destroy();
}
});
}

View File

@@ -33,12 +33,22 @@ function Foo({children}) {
return <div>{children}</div>;
}
async function delayedError(text, ms) {
return new Promise((_, reject) =>
setTimeout(() => reject(new Error(text)), ms)
);
}
async function delay(text, ms) {
return new Promise(resolve => setTimeout(() => resolve(text), ms));
}
async function delayTwice() {
await delay('', 20);
try {
await delayedError('Delayed exception', 20);
} catch (x) {
// Ignored
}
await delay('', 10);
}

View File

@@ -42,17 +42,43 @@ function Shell({data}) {
}
async function hydrateApp() {
const {root, returnValue, formState} = await createFromFetch(
fetch('/', {
headers: {
Accept: 'text/x-component',
},
}),
{
callServer,
findSourceMapURL,
}
);
let response;
if (
process.env.NODE_ENV === 'development' &&
typeof WebSocketStream === 'function'
) {
const requestId = crypto.randomUUID();
const wss = new WebSocketStream(
'ws://localhost:3001/debug-channel?' + requestId
);
const debugChannel = await wss.opened;
response = createFromFetch(
fetch('/', {
headers: {
Accept: 'text/x-component',
'rsc-request-id': requestId,
},
}),
{
callServer,
debugChannel,
findSourceMapURL,
}
);
} else {
response = createFromFetch(
fetch('/', {
headers: {
Accept: 'text/x-component',
},
}),
{
callServer,
findSourceMapURL,
}
);
}
const {root, returnValue, formState} = await response;
ReactDOM.hydrateRoot(
document,

View File

@@ -4,7 +4,6 @@ import React, {
useEffect,
useState,
unstable_addTransitionType as addTransitionType,
use,
} from 'react';
import Chrome from './Chrome';

View File

@@ -79,7 +79,9 @@ import {
logDedupedComponentRender,
logComponentErrored,
logIOInfo,
logIOInfoErrored,
logComponentAwait,
logComponentAwaitErrored,
} from './ReactFlightPerformanceTrack';
import {
@@ -96,6 +98,8 @@ import {getOwnerStackByComponentInfoInDev} from 'shared/ReactComponentInfoStack'
import {injectInternals} from './ReactFlightClientDevToolsHook';
import {OMITTED_PROP_ERROR} from './ReactFlightPropertyAccess';
import ReactVersion from 'shared/ReactVersion';
import isArray from 'shared/isArray';
@@ -155,12 +159,12 @@ const RESOLVED_MODEL = 'resolved_model';
const RESOLVED_MODULE = 'resolved_module';
const INITIALIZED = 'fulfilled';
const ERRORED = 'rejected';
const HALTED = 'halted'; // DEV-only. Means it never resolves even if connection closes.
type PendingChunk<T> = {
status: 'pending',
value: null | Array<(T) => mixed>,
reason: null | Array<(mixed) => mixed>,
_response: Response,
_children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
_debugInfo?: null | ReactDebugInfo, // DEV-only
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
@@ -169,7 +173,6 @@ type BlockedChunk<T> = {
status: 'blocked',
value: null | Array<(T) => mixed>,
reason: null | Array<(mixed) => mixed>,
_response: Response,
_children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
_debugInfo?: null | ReactDebugInfo, // DEV-only
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
@@ -177,8 +180,7 @@ type BlockedChunk<T> = {
type ResolvedModelChunk<T> = {
status: 'resolved_model',
value: UninitializedModel,
reason: null,
_response: Response,
reason: Response,
_children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
_debugInfo?: null | ReactDebugInfo, // DEV-only
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
@@ -187,7 +189,6 @@ type ResolvedModuleChunk<T> = {
status: 'resolved_module',
value: ClientReference<T>,
reason: null,
_response: Response,
_children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
_debugInfo?: null | ReactDebugInfo, // DEV-only
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
@@ -196,7 +197,6 @@ type InitializedChunk<T> = {
status: 'fulfilled',
value: T,
reason: null | FlightStreamController,
_response: Response,
_children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
_debugInfo?: null | ReactDebugInfo, // DEV-only
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
@@ -207,7 +207,6 @@ type InitializedStreamChunk<
status: 'fulfilled',
value: T,
reason: FlightStreamController,
_response: Response,
_children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
_debugInfo?: null | ReactDebugInfo, // DEV-only
then(resolve: (ReadableStream) => mixed, reject?: (mixed) => mixed): void,
@@ -216,7 +215,14 @@ type ErroredChunk<T> = {
status: 'rejected',
value: null,
reason: mixed,
_response: Response,
_children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
_debugInfo?: null | ReactDebugInfo, // DEV-only
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
};
type HaltedChunk<T> = {
status: 'halted',
value: null,
reason: null,
_children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
_debugInfo?: null | ReactDebugInfo, // DEV-only
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
@@ -227,19 +233,14 @@ type SomeChunk<T> =
| ResolvedModelChunk<T>
| ResolvedModuleChunk<T>
| InitializedChunk<T>
| ErroredChunk<T>;
| ErroredChunk<T>
| HaltedChunk<T>;
// $FlowFixMe[missing-this-annot]
function ReactPromise(
status: any,
value: any,
reason: any,
response: Response,
) {
function ReactPromise(status: any, value: any, reason: any) {
this.status = status;
this.value = value;
this.reason = reason;
this._response = response;
if (enableProfilerTimer && enableComponentPerformanceTrack) {
this._children = [];
}
@@ -266,7 +267,11 @@ ReactPromise.prototype.then = function <T>(
initializeModuleChunk(chunk);
break;
}
if (__DEV__ && enableAsyncDebugInfo) {
if (
__DEV__ &&
enableAsyncDebugInfo &&
(typeof resolve !== 'function' || !(resolve: any).isReactInternalListener)
) {
// Because only native Promises get picked up when we're awaiting we need to wrap
// this in a native Promise in DEV. This means that these callbacks are no longer sync
// but the lazy initialization is still sync and the .value can be inspected after,
@@ -307,6 +312,9 @@ ReactPromise.prototype.then = function <T>(
chunk.reason.push(reject);
}
break;
case HALTED: {
break;
}
default:
if (reject) {
reject(chunk.reason);
@@ -320,6 +328,8 @@ export type FindSourceMapURLCallback = (
environmentName: string,
) => null | string;
export type DebugChannelCallback = (message: string) => void;
export type Response = {
_bundlerConfig: ServerConsumerModuleMap,
_serverReferenceConfig: null | ServerManifest,
@@ -343,6 +353,7 @@ export type Response = {
_debugRootStack?: null | Error, // DEV-only
_debugRootTask?: null | ConsoleTask, // DEV-only
_debugFindSourceMapURL?: void | FindSourceMapURLCallback, // DEV-only
_debugChannel?: void | DebugChannelCallback, // DEV-only
_replayConsole: boolean, // DEV-only
_rootEnvironmentName: string, // DEV-only, the requested environment name.
};
@@ -364,6 +375,7 @@ function readChunk<T>(chunk: SomeChunk<T>): T {
return chunk.value;
case PENDING:
case BLOCKED:
case HALTED:
// eslint-disable-next-line no-throw-literal
throw ((chunk: any): Thenable<T>);
default:
@@ -378,12 +390,12 @@ export function getRoot<T>(response: Response): Thenable<T> {
function createPendingChunk<T>(response: Response): PendingChunk<T> {
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
return new ReactPromise(PENDING, null, null, response);
return new ReactPromise(PENDING, null, null);
}
function createBlockedChunk<T>(response: Response): BlockedChunk<T> {
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
return new ReactPromise(BLOCKED, null, null, response);
return new ReactPromise(BLOCKED, null, null);
}
function createErrorChunk<T>(
@@ -391,7 +403,7 @@ function createErrorChunk<T>(
error: mixed,
): ErroredChunk<T> {
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
return new ReactPromise(ERRORED, null, error, response);
return new ReactPromise(ERRORED, null, error);
}
function wakeChunk<T>(listeners: Array<(T) => mixed>, value: T): void {
@@ -463,7 +475,7 @@ function createResolvedModelChunk<T>(
value: UninitializedModel,
): ResolvedModelChunk<T> {
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
return new ReactPromise(RESOLVED_MODEL, value, null, response);
return new ReactPromise(RESOLVED_MODEL, value, response);
}
function createResolvedModuleChunk<T>(
@@ -471,7 +483,7 @@ function createResolvedModuleChunk<T>(
value: ClientReference<T>,
): ResolvedModuleChunk<T> {
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
return new ReactPromise(RESOLVED_MODULE, value, null, response);
return new ReactPromise(RESOLVED_MODULE, value, null);
}
function createInitializedTextChunk(
@@ -479,7 +491,7 @@ function createInitializedTextChunk(
value: string,
): InitializedChunk<string> {
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
return new ReactPromise(INITIALIZED, value, null, response);
return new ReactPromise(INITIALIZED, value, null);
}
function createInitializedBufferChunk(
@@ -487,7 +499,7 @@ function createInitializedBufferChunk(
value: $ArrayBufferView | ArrayBuffer,
): InitializedChunk<Uint8Array> {
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
return new ReactPromise(INITIALIZED, value, null, response);
return new ReactPromise(INITIALIZED, value, null);
}
function createInitializedIteratorResultChunk<T>(
@@ -496,12 +508,7 @@ function createInitializedIteratorResultChunk<T>(
done: boolean,
): InitializedChunk<IteratorResult<T, T>> {
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
return new ReactPromise(
INITIALIZED,
{done: done, value: value},
null,
response,
);
return new ReactPromise(INITIALIZED, {done: done, value: value}, null);
}
function createInitializedStreamChunk<
@@ -514,7 +521,7 @@ function createInitializedStreamChunk<
// We use the reason field to stash the controller since we already have that
// field. It's a bit of a hack but efficient.
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
return new ReactPromise(INITIALIZED, value, controller, response);
return new ReactPromise(INITIALIZED, value, controller);
}
function createResolvedIteratorResultChunk<T>(
@@ -526,10 +533,11 @@ function createResolvedIteratorResultChunk<T>(
const iteratorResultJSON =
(done ? '{"done":true,"value":' : '{"done":false,"value":') + value + '}';
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
return new ReactPromise(RESOLVED_MODEL, iteratorResultJSON, null, response);
return new ReactPromise(RESOLVED_MODEL, iteratorResultJSON, response);
}
function resolveIteratorResultChunk<T>(
response: Response,
chunk: SomeChunk<IteratorResult<T, T>>,
value: UninitializedModel,
done: boolean,
@@ -537,10 +545,11 @@ function resolveIteratorResultChunk<T>(
// To reuse code as much code as possible we add the wrapper element as part of the JSON.
const iteratorResultJSON =
(done ? '{"done":true,"value":' : '{"done":false,"value":') + value + '}';
resolveModelChunk(chunk, iteratorResultJSON);
resolveModelChunk(response, chunk, iteratorResultJSON);
}
function resolveModelChunk<T>(
response: Response,
chunk: SomeChunk<T>,
value: UninitializedModel,
): void {
@@ -557,6 +566,7 @@ function resolveModelChunk<T>(
const resolvedChunk: ResolvedModelChunk<T> = (chunk: any);
resolvedChunk.status = RESOLVED_MODEL;
resolvedChunk.value = value;
resolvedChunk.reason = response;
if (resolveListeners !== null) {
// This is unfortunate that we're reading this eagerly if
// we already have listeners attached since they might no
@@ -602,6 +612,7 @@ function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
initializingHandler = null;
const resolvedModel = chunk.value;
const response = chunk.reason;
// We go to the BLOCKED state until we've fully resolved this.
// We do this before parsing in case we try to initialize the same chunk
@@ -616,7 +627,7 @@ function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
}
try {
const value: T = parseModel(chunk._response, resolvedModel);
const value: T = parseModel(response, resolvedModel);
// Invoke any listeners added while resolving this model. I.e. cyclic
// references. This may or may not fully resolve the model depending on
// if they were blocked.
@@ -679,6 +690,15 @@ export function reportGlobalError(response: Response, error: Error): void {
triggerErrorOnChunk(chunk, error);
}
});
if (__DEV__) {
const debugChannel = response._debugChannel;
if (debugChannel !== undefined) {
// If we don't have any more ways of reading data, we don't have to send any
// more neither. So we close the writable side.
debugChannel('');
response._debugChannel = undefined;
}
}
if (enableProfilerTimer && enableComponentPerformanceTrack) {
markAllTracksInOrder();
flushComponentPerformance(
@@ -1052,6 +1072,10 @@ function waitForReference<T>(
}
}
}
// Use to avoid the microtask resolution in DEV.
if (__DEV__ && enableAsyncDebugInfo) {
(fulfill: any).isReactInternalListener = true;
}
function reject(error: mixed): void {
if (handler.errored) {
@@ -1359,6 +1383,7 @@ function getOutlinedModel<T>(
return chunkValue;
case PENDING:
case BLOCKED:
case HALTED:
return waitForReference(chunk, parentObject, key, response, map, path);
default:
// This is an error. Instead of erroring directly, we're going to encode this on
@@ -1406,6 +1431,17 @@ function createFormData(
return formData;
}
function applyConstructor(
response: Response,
model: Function,
parentObject: Object,
key: string,
): void {
Object.setPrototypeOf(parentObject, model.prototype);
// Delete the property. It was just a placeholder.
return undefined;
}
function extractIterator(response: Response, model: Array<any>): Iterator<any> {
// $FlowFixMe[incompatible-use]: This uses raw Symbols because we're extracting from a native array.
return model[Symbol.iterator]();
@@ -1462,10 +1498,6 @@ function parseModelString(
}
case '@': {
// Promise
if (value.length === 2) {
// Infinite promise that never resolves.
return new Promise(() => {});
}
const id = parseInt(value.slice(2), 16);
const chunk = getChunk(response, id);
if (enableProfilerTimer && enableComponentPerformanceTrack) {
@@ -1586,16 +1618,60 @@ function parseModelString(
// BigInt
return BigInt(value.slice(2));
}
case 'P': {
if (__DEV__) {
// In DEV mode we allow debug objects to specify themselves as instances of
// another constructor.
const ref = value.slice(2);
return getOutlinedModel(
response,
ref,
parentObject,
key,
applyConstructor,
);
}
//Fallthrough
}
case 'E': {
if (__DEV__) {
// In DEV mode we allow indirect eval to produce functions for logging.
// This should not compile to eval() because then it has local scope access.
const code = value.slice(2);
try {
// eslint-disable-next-line no-eval
return (0, eval)(value.slice(2));
return (0, eval)(code);
} catch (x) {
// We currently use this to express functions so we fail parsing it,
// let's just return a blank function as a place holder.
if (code.startsWith('(async function')) {
const idx = code.indexOf('(', 15);
if (idx !== -1) {
const name = code.slice(15, idx).trim();
// eslint-disable-next-line no-eval
return (0, eval)(
'({' + JSON.stringify(name) + ':async function(){}})',
)[name];
}
} else if (code.startsWith('(function')) {
const idx = code.indexOf('(', 9);
if (idx !== -1) {
const name = code.slice(9, idx).trim();
// eslint-disable-next-line no-eval
return (0, eval)(
'({' + JSON.stringify(name) + ':function(){}})',
)[name];
}
} else if (code.startsWith('(class')) {
const idx = code.indexOf('{', 6);
if (idx !== -1) {
const name = code.slice(6, idx).trim();
// eslint-disable-next-line no-eval
return (0, eval)('({' + JSON.stringify(name) + ':class{}})')[
name
];
}
}
return function () {};
}
}
@@ -1603,17 +1679,21 @@ function parseModelString(
}
case 'Y': {
if (__DEV__) {
if (value.length > 2) {
const debugChannel = response._debugChannel;
if (debugChannel) {
const ref = value.slice(2);
debugChannel('R:' + ref); // Release this reference immediately
}
}
// In DEV mode we encode omitted objects in logs as a getter that throws
// so that when you try to access it on the client, you know why that
// happened.
Object.defineProperty(parentObject, key, {
get: function () {
// TODO: We should ideally throw here to indicate a difference.
return (
'This object has been omitted by React in the console log ' +
'to avoid sending too much data from the server. Try logging smaller ' +
'or more specific objects.'
);
return OMITTED_PROP_ERROR;
},
enumerable: true,
configurable: false,
@@ -1670,9 +1750,10 @@ function ResponseInstance(
encodeFormAction: void | EncodeFormActionCallback,
nonce: void | string,
temporaryReferences: void | TemporaryReferenceSet,
findSourceMapURL: void | FindSourceMapURLCallback,
replayConsole: boolean,
environmentName: void | string,
findSourceMapURL: void | FindSourceMapURLCallback, // DEV-only
replayConsole: boolean, // DEV-only
environmentName: void | string, // DEV-only
debugChannel: void | DebugChannelCallback, // DEV-only
) {
const chunks: Map<number, SomeChunk<any>> = new Map();
this._bundlerConfig = bundlerConfig;
@@ -1727,6 +1808,7 @@ function ResponseInstance(
);
}
this._debugFindSourceMapURL = findSourceMapURL;
this._debugChannel = debugChannel;
this._replayConsole = replayConsole;
this._rootEnvironmentName = rootEnv;
}
@@ -1742,9 +1824,10 @@ export function createResponse(
encodeFormAction: void | EncodeFormActionCallback,
nonce: void | string,
temporaryReferences: void | TemporaryReferenceSet,
findSourceMapURL: void | FindSourceMapURLCallback,
replayConsole: boolean,
environmentName: void | string,
findSourceMapURL: void | FindSourceMapURLCallback, // DEV-only
replayConsole: boolean, // DEV-only
environmentName: void | string, // DEV-only
debugChannel: void | DebugChannelCallback, // DEV-only
): Response {
// $FlowFixMe[invalid-constructor]: the shapes are exact here but Flow doesn't like constructors
return new ResponseInstance(
@@ -1758,9 +1841,26 @@ export function createResponse(
findSourceMapURL,
replayConsole,
environmentName,
debugChannel,
);
}
function resolveDebugHalt(response: Response, id: number): void {
const chunks = response._chunks;
let chunk = chunks.get(id);
if (!chunk) {
chunks.set(id, (chunk = createPendingChunk(response)));
} else {
}
if (chunk.status !== PENDING && chunk.status !== BLOCKED) {
return;
}
const haltedChunk: HaltedChunk<any> = (chunk: any);
haltedChunk.status = HALTED;
haltedChunk.value = null;
haltedChunk.reason = null;
}
function resolveModel(
response: Response,
id: number,
@@ -1771,7 +1871,7 @@ function resolveModel(
if (!chunk) {
chunks.set(id, createResolvedModelChunk(response, model));
} else {
resolveModelChunk(chunk, model);
resolveModelChunk(response, chunk, model);
}
}
@@ -1945,7 +2045,7 @@ function startReadableStream<T>(
// to synchronous emitting.
previousBlockedChunk = null;
}
resolveModelChunk(chunk, json);
resolveModelChunk(response, chunk, json);
});
}
},
@@ -2033,7 +2133,12 @@ function startAsyncIterable<T>(
false,
);
} else {
resolveIteratorResultChunk(buffer[nextWriteIndex], value, false);
resolveIteratorResultChunk(
response,
buffer[nextWriteIndex],
value,
false,
);
}
nextWriteIndex++;
},
@@ -2046,12 +2151,18 @@ function startAsyncIterable<T>(
true,
);
} else {
resolveIteratorResultChunk(buffer[nextWriteIndex], value, true);
resolveIteratorResultChunk(
response,
buffer[nextWriteIndex],
value,
true,
);
}
nextWriteIndex++;
while (nextWriteIndex < buffer.length) {
// In generators, any extra reads from the iterator have the value undefined.
resolveIteratorResultChunk(
response,
buffer[nextWriteIndex++],
'"$undefined"',
true,
@@ -2069,32 +2180,33 @@ function startAsyncIterable<T>(
}
},
};
const iterable: $AsyncIterable<T, T, void> = {
[ASYNC_ITERATOR](): $AsyncIterator<T, T, void> {
let nextReadIndex = 0;
return createIterator(arg => {
if (arg !== undefined) {
throw new Error(
'Values cannot be passed to next() of AsyncIterables passed to Client Components.',
const iterable: $AsyncIterable<T, T, void> = ({}: any);
// $FlowFixMe[cannot-write]
iterable[ASYNC_ITERATOR] = (): $AsyncIterator<T, T, void> => {
let nextReadIndex = 0;
return createIterator(arg => {
if (arg !== undefined) {
throw new Error(
'Values cannot be passed to next() of AsyncIterables passed to Client Components.',
);
}
if (nextReadIndex === buffer.length) {
if (closed) {
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
return new ReactPromise(
INITIALIZED,
{done: true, value: undefined},
null,
);
}
if (nextReadIndex === buffer.length) {
if (closed) {
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
return new ReactPromise(
INITIALIZED,
{done: true, value: undefined},
null,
response,
);
}
buffer[nextReadIndex] =
createPendingChunk<IteratorResult<T, T>>(response);
}
return buffer[nextReadIndex++];
});
},
buffer[nextReadIndex] =
createPendingChunk<IteratorResult<T, T>>(response);
}
return buffer[nextReadIndex++];
});
};
// TODO: If it's a single shot iterator we can optimize memory by cleaning up the buffer after
// reading through the end, but currently we favor code size over this optimization.
resolveStream(
@@ -2627,9 +2739,15 @@ function initializeFakeStack(
// $FlowFixMe[cannot-write]
debugInfo.debugStack = createFakeJSXCallStackInDEV(response, stack, env);
}
if (debugInfo.owner != null) {
const owner = debugInfo.owner;
if (owner != null) {
// Initialize any owners not yet initialized.
initializeFakeStack(response, debugInfo.owner);
initializeFakeStack(response, owner);
if (owner.debugLocation === undefined && debugInfo.debugStack != null) {
// If we are the child of this owner, then the owner should be the bottom frame
// our stack. We can use it as the implied location of the owner.
owner.debugLocation = debugInfo.debugStack;
}
}
}
@@ -2816,7 +2934,29 @@ function initializeIOInfo(response: Response, ioInfo: ReactIOInfo): void {
// $FlowFixMe[cannot-write]
ioInfo.end += response._timeOrigin;
logIOInfo(ioInfo, response._rootEnvironmentName);
const env = response._rootEnvironmentName;
const promise = ioInfo.value;
if (promise) {
const thenable: Thenable<mixed> = (promise: any);
switch (thenable.status) {
case INITIALIZED:
logIOInfo(ioInfo, env, thenable.value);
break;
case ERRORED:
logIOInfoErrored(ioInfo, env, thenable.reason);
break;
default:
// If we haven't resolved the Promise yet, wait to log until have so we can include
// its data in the log.
promise.then(
logIOInfo.bind(null, ioInfo, env),
logIOInfoErrored.bind(null, ioInfo, env),
);
break;
}
} else {
logIOInfo(ioInfo, env, undefined);
}
}
function resolveIOInfo(
@@ -2831,7 +2971,7 @@ function resolveIOInfo(
chunks.set(id, chunk);
initializeModelChunk(chunk);
} else {
resolveModelChunk(chunk, model);
resolveModelChunk(response, chunk, model);
if (chunk.status === RESOLVED_MODEL) {
initializeModelChunk(chunk);
}
@@ -3100,13 +3240,55 @@ function flushComponentPerformance(
}
// $FlowFixMe: Refined.
const asyncInfo: ReactAsyncInfo = candidateInfo;
logComponentAwait(
asyncInfo,
trackIdx,
time,
endTime,
response._rootEnvironmentName,
);
const env = response._rootEnvironmentName;
const promise = asyncInfo.awaited.value;
if (promise) {
const thenable: Thenable<mixed> = (promise: any);
switch (thenable.status) {
case INITIALIZED:
logComponentAwait(
asyncInfo,
trackIdx,
time,
endTime,
env,
thenable.value,
);
break;
case ERRORED:
logComponentAwaitErrored(
asyncInfo,
trackIdx,
time,
endTime,
env,
thenable.reason,
);
break;
default:
// We assume that we should have received the data by now since this is logged at the
// end of the response stream. This is more sensitive to ordering so we don't wait
// to log it.
logComponentAwait(
asyncInfo,
trackIdx,
time,
endTime,
env,
undefined,
);
break;
}
} else {
logComponentAwait(
asyncInfo,
trackIdx,
time,
endTime,
env,
undefined,
);
}
}
}
}
@@ -3329,6 +3511,10 @@ function processFullStringRow(
}
// Fallthrough
default: /* """ "{" "[" "t" "f" "n" "0" - "9" */ {
if (__DEV__ && row === '') {
resolveDebugHalt(response, id);
return;
}
// We assume anything else is JSON.
resolveModel(response, id, row);
return;

View File

@@ -17,14 +17,143 @@ import type {
import {enableProfilerTimer} from 'shared/ReactFeatureFlags';
import {OMITTED_PROP_ERROR} from './ReactFlightPropertyAccess';
import hasOwnProperty from 'shared/hasOwnProperty';
import isArray from 'shared/isArray';
const supportsUserTiming =
enableProfilerTimer &&
typeof console !== 'undefined' &&
typeof console.timeStamp === 'function';
typeof console.timeStamp === 'function' &&
typeof performance !== 'undefined' &&
// $FlowFixMe[method-unbinding]
typeof performance.measure === 'function';
const IO_TRACK = 'Server Requests ⚛';
const COMPONENTS_TRACK = 'Server Components ⚛';
const EMPTY_ARRAY = 0;
const COMPLEX_ARRAY = 1;
const PRIMITIVE_ARRAY = 2; // Primitive values only
const ENTRIES_ARRAY = 3; // Tuple arrays of string and value (like Headers, Map, etc)
function getArrayKind(array: Object): 0 | 1 | 2 | 3 {
let kind = EMPTY_ARRAY;
for (let i = 0; i < array.length; i++) {
const value = array[i];
if (typeof value === 'object' && value !== null) {
if (
isArray(value) &&
value.length === 2 &&
typeof value[0] === 'string'
) {
// Key value tuple
if (kind !== EMPTY_ARRAY && kind !== ENTRIES_ARRAY) {
return COMPLEX_ARRAY;
}
kind = ENTRIES_ARRAY;
} else {
return COMPLEX_ARRAY;
}
} else if (typeof value === 'function') {
return COMPLEX_ARRAY;
} else if (typeof value === 'string' && value.length > 50) {
return COMPLEX_ARRAY;
} else if (kind !== EMPTY_ARRAY && kind !== PRIMITIVE_ARRAY) {
return COMPLEX_ARRAY;
} else {
kind = PRIMITIVE_ARRAY;
}
}
return kind;
}
function addObjectToProperties(
object: Object,
properties: Array<[string, string]>,
indent: number,
): void {
for (const key in object) {
if (hasOwnProperty.call(object, key) && key[0] !== '_') {
const value = object[key];
addValueToProperties(key, value, properties, indent);
}
}
}
function addValueToProperties(
propertyName: string,
value: mixed,
properties: Array<[string, string]>,
indent: number,
): void {
let desc;
switch (typeof value) {
case 'object':
if (value === null) {
desc = 'null';
break;
} else {
// $FlowFixMe[method-unbinding]
const objectToString = Object.prototype.toString.call(value);
let objectName = objectToString.slice(8, objectToString.length - 1);
if (objectName === 'Array') {
const array: Array<any> = (value: any);
const kind = getArrayKind(array);
if (kind === PRIMITIVE_ARRAY || kind === EMPTY_ARRAY) {
desc = JSON.stringify(array);
break;
} else if (kind === ENTRIES_ARRAY) {
properties.push(['\xa0\xa0'.repeat(indent) + propertyName, '']);
for (let i = 0; i < array.length; i++) {
const entry = array[i];
addValueToProperties(entry[0], entry[1], properties, indent + 1);
}
return;
}
}
if (objectName === 'Object') {
const proto: any = Object.getPrototypeOf(value);
if (proto && typeof proto.constructor === 'function') {
objectName = proto.constructor.name;
}
}
properties.push([
'\xa0\xa0'.repeat(indent) + propertyName,
objectName === 'Object' ? '' : objectName,
]);
if (indent < 3) {
addObjectToProperties(value, properties, indent + 1);
}
return;
}
case 'function':
if (value.name === '') {
desc = '() => {}';
} else {
desc = value.name + '() {}';
}
break;
case 'string':
if (value === OMITTED_PROP_ERROR) {
desc = '...';
} else {
desc = JSON.stringify(value);
}
break;
case 'undefined':
desc = 'undefined';
break;
case 'boolean':
desc = value ? 'true' : 'false';
break;
default:
// eslint-disable-next-line react-internal/safe-string-coercion
desc = String(value);
}
properties.push(['\xa0\xa0'.repeat(indent) + propertyName, desc]);
}
export function markAllTracksInOrder() {
if (supportsUserTiming) {
// Ensure we create the Server Component track groups earlier than the Client Scheduler
@@ -133,12 +262,7 @@ export function logComponentErrored(
const isPrimaryEnv = env === rootEnv;
const entryName =
isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']';
if (
__DEV__ &&
typeof performance !== 'undefined' &&
// $FlowFixMe[method-unbinding]
typeof performance.measure === 'function'
) {
if (__DEV__) {
const message =
typeof error === 'object' &&
error !== null &&
@@ -228,12 +352,68 @@ function getIOColor(
}
}
export function logComponentAwaitErrored(
asyncInfo: ReactAsyncInfo,
trackIdx: number,
startTime: number,
endTime: number,
rootEnv: string,
error: mixed,
): void {
if (supportsUserTiming && endTime > 0) {
const env = asyncInfo.env;
const name = asyncInfo.awaited.name;
const isPrimaryEnv = env === rootEnv;
const entryName =
'await ' +
(isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']');
const debugTask = asyncInfo.debugTask;
if (__DEV__ && debugTask) {
const message =
typeof error === 'object' &&
error !== null &&
typeof error.message === 'string'
? // eslint-disable-next-line react-internal/safe-string-coercion
String(error.message)
: // eslint-disable-next-line react-internal/safe-string-coercion
String(error);
const properties = [['Rejected', message]];
debugTask.run(
// $FlowFixMe[method-unbinding]
performance.measure.bind(performance, entryName, {
start: startTime < 0 ? 0 : startTime,
end: endTime,
detail: {
devtools: {
color: 'error',
track: trackNames[trackIdx],
trackGroup: COMPONENTS_TRACK,
properties,
tooltipText: entryName + ' Rejected',
},
},
}),
);
} else {
console.timeStamp(
entryName,
startTime < 0 ? 0 : startTime,
endTime,
trackNames[trackIdx],
COMPONENTS_TRACK,
'error',
);
}
}
}
export function logComponentAwait(
asyncInfo: ReactAsyncInfo,
trackIdx: number,
startTime: number,
endTime: number,
rootEnv: string,
value: mixed,
): void {
if (supportsUserTiming && endTime > 0) {
const env = asyncInfo.env;
@@ -245,17 +425,26 @@ export function logComponentAwait(
(isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']');
const debugTask = asyncInfo.debugTask;
if (__DEV__ && debugTask) {
const properties: Array<[string, string]> = [];
if (typeof value === 'object' && value !== null) {
addObjectToProperties(value, properties, 0);
} else if (value !== undefined) {
addValueToProperties('Resolved', value, properties, 0);
}
debugTask.run(
// $FlowFixMe[method-unbinding]
console.timeStamp.bind(
console,
entryName,
startTime < 0 ? 0 : startTime,
endTime,
trackNames[trackIdx],
COMPONENTS_TRACK,
color,
),
performance.measure.bind(performance, entryName, {
start: startTime < 0 ? 0 : startTime,
end: endTime,
detail: {
devtools: {
color: color,
track: trackNames[trackIdx],
trackGroup: COMPONENTS_TRACK,
properties,
},
},
}),
);
} else {
console.timeStamp(
@@ -270,7 +459,63 @@ export function logComponentAwait(
}
}
export function logIOInfo(ioInfo: ReactIOInfo, rootEnv: string): void {
export function logIOInfoErrored(
ioInfo: ReactIOInfo,
rootEnv: string,
error: mixed,
): void {
const startTime = ioInfo.start;
const endTime = ioInfo.end;
if (supportsUserTiming && endTime >= 0) {
const name = ioInfo.name;
const env = ioInfo.env;
const isPrimaryEnv = env === rootEnv;
const entryName =
isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']';
const debugTask = ioInfo.debugTask;
if (__DEV__ && debugTask) {
const message =
typeof error === 'object' &&
error !== null &&
typeof error.message === 'string'
? // eslint-disable-next-line react-internal/safe-string-coercion
String(error.message)
: // eslint-disable-next-line react-internal/safe-string-coercion
String(error);
const properties = [['Rejected', message]];
debugTask.run(
// $FlowFixMe[method-unbinding]
performance.measure.bind(performance, entryName, {
start: startTime < 0 ? 0 : startTime,
end: endTime,
detail: {
devtools: {
color: 'error',
track: IO_TRACK,
properties,
tooltipText: entryName + ' Rejected',
},
},
}),
);
} else {
console.timeStamp(
entryName,
startTime < 0 ? 0 : startTime,
endTime,
IO_TRACK,
undefined,
'error',
);
}
}
}
export function logIOInfo(
ioInfo: ReactIOInfo,
rootEnv: string,
value: mixed,
): void {
const startTime = ioInfo.start;
const endTime = ioInfo.end;
if (supportsUserTiming && endTime >= 0) {
@@ -282,17 +527,25 @@ export function logIOInfo(ioInfo: ReactIOInfo, rootEnv: string): void {
const debugTask = ioInfo.debugTask;
const color = getIOColor(name);
if (__DEV__ && debugTask) {
const properties: Array<[string, string]> = [];
if (typeof value === 'object' && value !== null) {
addObjectToProperties(value, properties, 0);
} else if (value !== undefined) {
addValueToProperties('Resolved', value, properties, 0);
}
debugTask.run(
// $FlowFixMe[method-unbinding]
console.timeStamp.bind(
console,
entryName,
startTime < 0 ? 0 : startTime,
endTime,
IO_TRACK,
undefined,
color,
),
performance.measure.bind(performance, entryName, {
start: startTime < 0 ? 0 : startTime,
end: endTime,
detail: {
devtools: {
color: color,
track: IO_TRACK,
properties,
},
},
}),
);
} else {
console.timeStamp(

View File

@@ -0,0 +1,13 @@
/**
* 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.
*
* @flow
*/
export const OMITTED_PROP_ERROR =
'This object has been omitted by React in the console log ' +
'to avoid sending too much data from the server. Try logging smaller ' +
'or more specific objects.';

View File

@@ -69,7 +69,7 @@ function getErrorForJestMatcher(error) {
function normalizeComponentInfo(debugInfo) {
if (Array.isArray(debugInfo.stack)) {
const {debugTask, debugStack, ...copy} = debugInfo;
const {debugTask, debugStack, debugLocation, ...copy} = debugInfo;
copy.stack = formatV8Stack(debugInfo.stack);
if (debugInfo.owner) {
copy.owner = normalizeComponentInfo(debugInfo.owner);
@@ -3208,12 +3208,29 @@ describe('ReactFlight', () => {
return 'hello';
}
class MyClass {
constructor() {
this.x = 1;
}
method() {}
get y() {
return this.x + 1;
}
get z() {
return this.x + 5;
}
}
Object.defineProperty(MyClass.prototype, 'y', {enumerable: true});
function ServerComponent() {
console.log('hi', {
prop: 123,
fn: foo,
map: new Map([['foo', foo]]),
promise: new Promise(() => {}),
promise: Promise.resolve('yo'),
infinitePromise: new Promise(() => {}),
Class: MyClass,
instance: new MyClass(),
});
throw new Error('err');
}
@@ -3258,9 +3275,14 @@ describe('ReactFlight', () => {
});
ownerStacks = [];
// Let the Promises resolve.
await 0;
await 0;
await 0;
// The error should not actually get logged because we're not awaiting the root
// so it's not thrown but the server log also shouldn't be replayed.
await ReactNoopFlightClient.read(transport);
await ReactNoopFlightClient.read(transport, {close: true});
expect(mockConsoleLog).toHaveBeenCalledTimes(1);
expect(mockConsoleLog.mock.calls[0][0]).toBe('hi');
@@ -3276,9 +3298,40 @@ describe('ReactFlight', () => {
expect(typeof loggedFn2).toBe('function');
expect(loggedFn2).not.toBe(foo);
expect(loggedFn2.toString()).toBe(foo.toString());
expect(loggedFn2).toBe(loggedFn);
const promise = mockConsoleLog.mock.calls[0][1].promise;
expect(promise).toBeInstanceOf(Promise);
expect(await promise).toBe('yo');
const infinitePromise = mockConsoleLog.mock.calls[0][1].infinitePromise;
expect(infinitePromise).toBeInstanceOf(Promise);
let resolved = false;
infinitePromise.then(
() => (resolved = true),
x => {
console.error(x);
resolved = true;
},
);
await 0;
await 0;
await 0;
// This should not reject upon aborting the stream.
expect(resolved).toBe(false);
const Class = mockConsoleLog.mock.calls[0][1].Class;
const instance = mockConsoleLog.mock.calls[0][1].instance;
expect(typeof Class).toBe('function');
expect(Class.prototype.constructor).toBe(Class);
expect(instance instanceof Class).toBe(true);
expect(Object.getPrototypeOf(instance)).toBe(Class.prototype);
expect(instance.x).toBe(1);
expect(instance.hasOwnProperty('y')).toBe(true);
expect(instance.y).toBe(2); // Enumerable getter was reified
expect(instance.hasOwnProperty('z')).toBe(false);
expect(instance.z).toBe(6); // Not enumerable getter was transferred as part of the toString() of the class
expect(typeof instance.method).toBe('function'); // Methods are included only if they're part of the toString()
expect(ownerStacks).toEqual(['\n in App (at **)']);
});
@@ -3327,19 +3380,10 @@ describe('ReactFlight', () => {
await ReactNoopFlightClient.read(transport);
expect(mockConsoleLog).toHaveBeenCalledTimes(1);
// TODO: Support cyclic objects in console encoding.
// expect(mockConsoleLog.mock.calls[0][0]).toBe('hi');
// const cyclic2 = mockConsoleLog.mock.calls[0][1].cyclic;
// expect(cyclic2).not.toBe(cyclic); // Was serialized and therefore cloned
// expect(cyclic2.cycle).toBe(cyclic2);
expect(mockConsoleLog.mock.calls[0][0]).toBe(
'Unknown Value: React could not send it from the server.',
);
expect(mockConsoleLog.mock.calls[0][1].message).toBe(
'Converting circular structure to JSON\n' +
" --> starting at object with constructor 'Object'\n" +
" --- property 'cycle' closes the circle",
);
expect(mockConsoleLog.mock.calls[0][0]).toBe('hi');
const cyclic2 = mockConsoleLog.mock.calls[0][1].cyclic;
expect(cyclic2).not.toBe(cyclic); // Was serialized and therefore cloned
expect(cyclic2.cycle).toBe(cyclic2);
});
// @gate !__DEV__ || enableComponentPerformanceTrack

View File

@@ -0,0 +1,139 @@
/**
* 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.
*
* @emails react-core
* @jest-environment node
*/
'use strict';
if (typeof Blob === 'undefined') {
global.Blob = require('buffer').Blob;
}
if (typeof File === 'undefined' || typeof FormData === 'undefined') {
global.File = require('undici').File;
global.FormData = require('undici').FormData;
}
function formatV8Stack(stack) {
let v8StyleStack = '';
if (stack) {
for (let i = 0; i < stack.length; i++) {
const [name] = stack[i];
if (v8StyleStack !== '') {
v8StyleStack += '\n';
}
v8StyleStack += ' in ' + name + ' (at **)';
}
}
return v8StyleStack;
}
function normalizeComponentInfo(debugInfo) {
if (Array.isArray(debugInfo.stack)) {
const {debugTask, debugStack, ...copy} = debugInfo;
copy.stack = formatV8Stack(debugInfo.stack);
if (debugInfo.owner) {
copy.owner = normalizeComponentInfo(debugInfo.owner);
}
return copy;
} else {
return debugInfo;
}
}
function getDebugInfo(obj) {
const debugInfo = obj._debugInfo;
if (debugInfo) {
const copy = [];
for (let i = 0; i < debugInfo.length; i++) {
copy.push(normalizeComponentInfo(debugInfo[i]));
}
return copy;
}
return debugInfo;
}
let act;
let React;
let ReactNoop;
let ReactNoopFlightServer;
let ReactNoopFlightClient;
describe('ReactFlight', () => {
beforeEach(() => {
// Mock performance.now for timing tests
let time = 10;
const now = jest.fn().mockImplementation(() => {
return time++;
});
Object.defineProperty(performance, 'timeOrigin', {
value: time,
configurable: true,
});
Object.defineProperty(performance, 'now', {
value: now,
configurable: true,
});
jest.resetModules();
jest.mock('react', () => require('react/react.react-server'));
ReactNoopFlightServer = require('react-noop-renderer/flight-server');
// This stores the state so we need to preserve it
const flightModules = require('react-noop-renderer/flight-modules');
jest.resetModules();
__unmockReact();
jest.mock('react-noop-renderer/flight-modules', () => flightModules);
React = require('react');
ReactNoop = require('react-noop-renderer');
ReactNoopFlightClient = require('react-noop-renderer/flight-client');
act = require('internal-test-utils').act;
});
afterEach(() => {
jest.restoreAllMocks();
});
// @gate __DEV__ && enableComponentPerformanceTrack
it('can render deep but cut off JSX in debug info', async () => {
function createDeepJSX(n) {
if (n <= 0) {
return null;
}
return <div>{createDeepJSX(n - 1)}</div>;
}
function ServerComponent(props) {
return <div>not using props</div>;
}
const debugChannel = {onMessage(message) {}};
const transport = ReactNoopFlightServer.render(
{
root: (
<ServerComponent>
{createDeepJSX(100) /* deper than objectLimit */}
</ServerComponent>
),
},
{debugChannel},
);
await act(async () => {
const rootModel = await ReactNoopFlightClient.read(transport, {
debugChannel,
});
const root = rootModel.root;
const children = getDebugInfo(root)[1].props.children;
expect(children.type).toBe('div');
expect(children.props.children.type).toBe('div');
ReactNoop.render(root);
});
expect(ReactNoop).toMatchRenderedOutput(<div>not using props</div>);
});
});

View File

@@ -5831,13 +5831,21 @@ export function attach(
}
function getSourceForInstance(instance: DevToolsInstance): Source | null {
const unresolvedSource = instance.source;
let unresolvedSource = instance.source;
if (unresolvedSource === null) {
// We don't have any source yet. We can try again later in case an owned child mounts later.
// TODO: We won't have any information here if the child is filtered.
return null;
}
if (instance.kind === VIRTUAL_INSTANCE) {
// We might have found one on the virtual instance.
const debugLocation = instance.data.debugLocation;
if (debugLocation != null) {
unresolvedSource = debugLocation;
}
}
// If we have the debug stack (the creation stack of the JSX) for any owned child of this
// component, then at the bottom of that stack will be a stack frame that is somewhere within
// the component's function body. Typically it would be the callsite of the JSX unless there's

View File

@@ -6,7 +6,7 @@ export const markShellTime =
export const clientRenderBoundary =
'$RX=function(b,c,d,e,f){var a=document.getElementById(b);a&&(b=a.previousSibling,b.data="$!",a=a.dataset,c&&(a.dgst=c),d&&(a.msg=d),e&&(a.stck=e),f&&(a.cstck=f),b._reactRetry&&b._reactRetry())};';
export const completeBoundary =
'$RB=[];$RV=function(b){$RT=performance.now();for(var a=0;a<b.length;a+=2){var c=b[a],e=b[a+1];e.parentNode.removeChild(e);var f=c.parentNode;if(f){var g=c.previousSibling,h=0;do{if(c&&8===c.nodeType){var d=c.data;if("/$"===d||"/&"===d)if(0===h)break;else h--;else"$"!==d&&"$?"!==d&&"$~"!==d&&"$!"!==d&&"&"!==d||h++}d=c.nextSibling;f.removeChild(c);c=d}while(c);for(;e.firstChild;)f.insertBefore(e.firstChild,c);g.data="$";g._reactRetry&&g._reactRetry()}}b.length=0};$RC=function(b,a){if(a=document.getElementById(a))(b=document.getElementById(b))?(b.previousSibling.data="$~",$RB.push(b,a),2===$RB.length&&(b="number"!==typeof $RT?0:$RT,a=performance.now(),setTimeout($RV.bind(null,$RB),2300>a&&2E3<a?2300-a:b+300-a))):a.parentNode.removeChild(a)};';
'$RB=[];$RV=function(b){$RT=performance.now();for(var a=0;a<b.length;a+=2){var c=b[a],e=b[a+1];null!==e.parentNode&&e.parentNode.removeChild(e);var f=c.parentNode;if(f){var g=c.previousSibling,h=0;do{if(c&&8===c.nodeType){var d=c.data;if("/$"===d||"/&"===d)if(0===h)break;else h--;else"$"!==d&&"$?"!==d&&"$~"!==d&&"$!"!==d&&"&"!==d||h++}d=c.nextSibling;f.removeChild(c);c=d}while(c);for(;e.firstChild;)f.insertBefore(e.firstChild,c);g.data="$";g._reactRetry&&g._reactRetry()}}b.length=0};\n$RC=function(b,a){if(a=document.getElementById(a))(b=document.getElementById(b))?(b.previousSibling.data="$~",$RB.push(b,a),2===$RB.length&&(b="number"!==typeof $RT?0:$RT,a=performance.now(),setTimeout($RV.bind(null,$RB),2300>a&&2E3<a?2300-a:b+300-a))):a.parentNode.removeChild(a)};';
export const completeBoundaryUpgradeToViewTransitions =
'$RV=function(A,g){function k(a,b){var e=a.getAttribute(b);e&&(b=a.style,l.push(a,b.viewTransitionName,b.viewTransitionClass),"auto"!==e&&(b.viewTransitionClass=e),(a=a.getAttribute("vt-name"))||(a="_T_"+K++ +"_"),b.viewTransitionName=a,B=!0)}var B=!1,K=0,l=[];try{var f=document.__reactViewTransition;if(f){f.finished.finally($RV.bind(null,g));return}var m=new Map;for(f=1;f<g.length;f+=2)for(var h=g[f].querySelectorAll("[vt-share]"),d=0;d<h.length;d++){var c=h[d];m.set(c.getAttribute("vt-name"),c)}var u=[];for(h=0;h<g.length;h+=2){var C=g[h],x=C.parentNode;if(x){var v=x.getBoundingClientRect();if(v.left||v.top||v.width||v.height){c=C;for(f=0;c;){if(8===c.nodeType){var r=c.data;if("/$"===r)if(0===f)break;else f--;else"$"!==r&&"$?"!==r&&"$~"!==r&&"$!"!==r||f++}else if(1===c.nodeType){d=c;var D=d.getAttribute("vt-name"),y=m.get(D);k(d,y?"vt-share":"vt-exit");y&&(k(y,"vt-share"),m.set(D,null));var E=d.querySelectorAll("[vt-share]");for(d=0;d<E.length;d++){var F=E[d],G=F.getAttribute("vt-name"),\nH=m.get(G);H&&(k(F,"vt-share"),k(H,"vt-share"),m.set(G,null))}}c=c.nextSibling}for(var I=g[h+1],t=I.firstElementChild;t;)null!==m.get(t.getAttribute("vt-name"))&&k(t,"vt-enter"),t=t.nextElementSibling;c=x;do for(var n=c.firstElementChild;n;){var J=n.getAttribute("vt-update");J&&"none"!==J&&!l.includes(n)&&k(n,"vt-update");n=n.nextElementSibling}while((c=c.parentNode)&&1===c.nodeType&&"none"!==c.getAttribute("vt-update"));u.push.apply(u,I.querySelectorAll(\'img[src]:not([loading="lazy"])\'))}}}if(B){var z=\ndocument.__reactViewTransition=document.startViewTransition({update:function(){A(g);for(var a=[document.documentElement.clientHeight,document.fonts.ready],b={},e=0;e<u.length;b={g:b.g},e++)if(b.g=u[e],!b.g.complete){var p=b.g.getBoundingClientRect();0<p.bottom&&0<p.right&&p.top<window.innerHeight&&p.left<window.innerWidth&&(p=new Promise(function(w){return function(q){w.g.addEventListener("load",q);w.g.addEventListener("error",q)}}(b)),a.push(p))}return Promise.race([Promise.all(a),new Promise(function(w){var q=\nperformance.now();setTimeout(w,2300>q&&2E3<q?2300-q:500)})])},types:[]});z.ready.finally(function(){for(var a=l.length-3;0<=a;a-=3){var b=l[a],e=b.style;e.viewTransitionName=l[a+1];e.viewTransitionClass=l[a+1];""===b.getAttribute("style")&&b.removeAttribute("style")}});z.finished.finally(function(){document.__reactViewTransition===z&&(document.__reactViewTransition=null)});$RB=[];return}}catch(a){}A(g)}.bind(null,$RV);';
export const completeBoundaryWithStyles =

View File

@@ -34,11 +34,16 @@ export function revealCompletedBoundaries(batch) {
for (let i = 0; i < batch.length; i += 2) {
const suspenseIdNode = batch[i];
const contentNode = batch[i + 1];
// We can detach the content now.
// Completions of boundaries within this contentNode will now find the boundary
// in its designated place.
contentNode.parentNode.removeChild(contentNode);
if (contentNode.parentNode === null) {
// If the client has failed hydration we may have already deleted the streaming
// segments. The server may also have emitted a complete instruction but cancelled
// the segment. Regardless we can ignore this case.
} else {
// We can detach the content now.
// Completions of boundaries within this contentNode will now find the boundary
// in its designated place.
contentNode.parentNode.removeChild(contentNode);
}
// Clear all the existing children. This is complicated because
// there can be embedded Suspense boundaries in the fallback.
// This is similar to clearSuspenseBoundary in ReactFiberConfigDOM.

View File

@@ -171,6 +171,7 @@ export function experimental_renderToHTML(
undefined,
'Markup',
undefined,
false,
);
const flightResponse = createFlightResponse(
null,

View File

@@ -12,7 +12,6 @@ import type {
TouchedViewDataAtPoint,
ViewConfig,
} from './ReactNativeTypes';
import {create, diff} from './ReactNativeAttributePayloadFabric';
import {dispatchEvent} from './ReactFabricEventEmitter';
import {
NoEventPriority,
@@ -35,6 +34,8 @@ import {
deepFreezeAndThrowOnMutationInDev,
createPublicInstance,
createPublicTextInstance,
createAttributePayload,
diffAttributePayloads,
type PublicInstance as ReactNativePublicInstance,
type PublicTextInstance,
type PublicRootInstance,
@@ -190,7 +191,10 @@ export function createInstance(
}
}
const updatePayload = create(props, viewConfig.validAttributes);
const updatePayload = createAttributePayload(
props,
viewConfig.validAttributes,
);
const node = createNode(
tag, // reactTag
@@ -456,7 +460,11 @@ export function cloneInstance(
newChildSet: ?ChildSet,
): Instance {
const viewConfig = instance.canonical.viewConfig;
const updatePayload = diff(oldProps, newProps, viewConfig.validAttributes);
const updatePayload = diffAttributePayloads(
oldProps,
newProps,
viewConfig.validAttributes,
);
// TODO: If the event handlers have changed, we need to update the current props
// in the commit phase but there is no host config hook to do it yet.
// So instead we hack it by updating it in the render phase.
@@ -505,7 +513,7 @@ export function cloneHiddenInstance(
): Instance {
const viewConfig = instance.canonical.viewConfig;
const node = instance.node;
const updatePayload = create(
const updatePayload = createAttributePayload(
{style: {display: 'none'}},
viewConfig.validAttributes,
);

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