Compare commits

..

374 Commits

Author SHA1 Message Date
Mofei Zhang
3e5400f4ea [compiler] Aggregate error reporting, separate eslint rules
NOTE: this is a merged version of @mofeiZ's original PR along with my edits per offline discussion. The description is updated to reflect the latest approach.

The key problem we're trying to solve with this PR is to allow developers more control over the compiler's various validations. The idea is to have a number of rules targeting a specific category of issues, such as enforcing immutability of props/state/etc or disallowing access to refs during render. We don't want to have to run the compiler again for every single rule, though, so @mofeiZ added an LRU cache that caches the full compilation output of N most recent files. The first rule to run on a given file will cause it to get cached, and then subsequent rules can pull from the cache, with each rule filtering down to its specific category of errors.

For the categories, I went through and assigned a category roughly 1:1 to existing validations, and then used my judgement on some places that felt distinct enough to warrant a separate error. Every error in the compiler now has to supply both a severity (for legacy reasons) and a category (for ESLint). Each category corresponds 1:1 to a ESLint rule definition, so that the set of rules is automatically populated based on the defined categories.

Categories include a flag for whether they should be in the recommended set or not.

Note that as with the original version of this PR, only eslint-plugin-react-compiler is changed. We still have to update the main lint rule.
2025-08-19 15:53:36 -07:00
Sebastian Markbåge
0bdb9206b7 [Fizz] If we haven't painted yet, wait to reveal everything until next paint (#34230)
Before the first rAF, we don't know if there has been other paints
before this and if so when. (We could get from performance observer.) We
can assume that it's not earlier than 0 so we used delay up until the
throttle time starting from zero but if the first paint is about to
happen that can be very soon after.

Instead, this reveals it during the next paint which should let us be
able to get into the first paint. If we can trust `rel="expect"` to have
done its thing we should schedule our raf before first paint but ofc
browsers can cheat and paint earlier if they want to.

If we're wrong, this is at least more batched than doing it
synchronously. However it will mean that things might get more flashy
than it should be if it would've been throttled. An alternative would be
to always throttle first reveal.
2025-08-18 20:22:40 -04:00
lauren
f508edc83f [compiler] Stop publishing eslint-plugin-react-compiler to npm (#34228)
While we still use this package internally, we now ask users to install
eslint-plugin-react-hooks instead, so this package can now be deprecated
on npm.
2025-08-18 11:34:55 -04:00
Sebastian Markbåge
0c89b160f6 [Flight] Add DebugInfo for Bundler Chunks (#34226)
This adds a "suspended by" row for each chunk that is referenced from a
client reference. So when you select a client component, you can see
what bundles will block that client component when loading on the
client.

This is only done in the browser build since if we added it on the
server, it would show up as a blocking resource and while it's possible
we expect that a typical server request won't block on loading JS.

<img width="664" height="486" alt="Screenshot 2025-08-17 at 3 45 14 PM"
src="https://github.com/user-attachments/assets/b1f83445-2a4e-4470-9a20-7cd215ab0482"
/>

<img width="745" height="678" alt="Screenshot 2025-08-17 at 3 46 58 PM"
src="https://github.com/user-attachments/assets/3558eae1-cf34-4e11-9d0e-02ec076356a4"
/>

Currently this is only included if it ends up wrapped in a lazy like in
the typical type position of a Client Component, but there's a general
issue that maybe hard references need to transfer their debug info to
the parent which can transfer it to the Fiber.
2025-08-18 11:34:00 -04:00
Benjamin
87a45ae37f [eslint-plugin-react-hooks][RulesOfHooks] handle React.useEffect in addition to useEffect (#34076)
## Summary

This is a fix for https://github.com/facebook/react/issues/34074

## How did you test this change?

I added tests in the eslint package, and ran `yarn jest`. After adding
the new tests, I have this:

On main | On this branch
-|-
<img width="356" height="88" alt="image"
src="https://github.com/user-attachments/assets/4ae099a1-0156-4032-b2ca-635ebadcaa3f"
/> | <img width="435" height="120" alt="image"
src="https://github.com/user-attachments/assets/b06c04b8-6cec-43de-befa-a8b4dd20500e"
/>

## Changes

- Add tests to check that we are checking both `CallExpression`
(`useEffect(`), and `MemberExpression` (`React.useEffect(`). To do that,
I copied the `getNodeWithoutReactNamespace(` fn from `ExhaustiveDeps.ts`
to `RulesOfHooks.ts`
2025-08-18 09:12:49 -04:00
Sebastian "Sebbie" Silbermann
01ed0e9642 [DevTools] Avoid uncached Promise when symbolicating sources in environments without file fetching (#34224) 2025-08-18 12:46:19 +02:00
Sebastian "Sebbie" Silbermann
b58a8e3c40 [DevTools] Handle mount of disconnected Suspense boundaries (#34208) 2025-08-18 10:15:56 +02:00
Sebastian Markbåge
42b1b33a24 [DevTools] Add byteSize field to ReactIOInfo and show this in the tooltip (#34221)
This is intended to be used by various client side resources where the
transfer size is interesting to know how it'll perform in various
network conditions. Not intended to be added by the server.

For now it's only added internally by DevTools itself on img/css but
I'll add it from Flight Client too in a follow up.

This now shows this as the "transfer size" which is the encoded body
size + headers/overhead. Where as the "fileSize" that I add to images is
the decoded body size, like what you'd see on disk. This is what Chrome
shows so it's less confusing if you compare Network tab and this view.
2025-08-17 16:17:11 -04:00
Sebastian Markbåge
7a36dfedc7 [Fizz] Delay retrying hydration until after an animation frame (#34220)
The theory here is that when we reveal a boundary coming from the server
we want to paint that before hydrating it. Hydration gets scheduled in a
macrotask with the scheduler but it's in theory possible that it runs
before the paint. If that's the case, then the JS that runs before
yielding during hydration might slightly delay the paint and we might
miss a window to skip the previous paint.
2025-08-16 12:16:58 -04:00
Sebastian "Sebbie" Silbermann
546bac7281 [DevTools] Always attempt to mount dehydrated roots (#34209) 2025-08-16 10:45:39 +02:00
Sebastian "Sebbie" Silbermann
2cb8edbb05 [DevTools] Handle dehydrated Suspense boundaries (#34196) 2025-08-16 10:34:19 +02:00
Sebastian Markbåge
431bb0bddb [DevTools] Mark Unknown Reasons for Suspending with a Note (#34200)
We currently only track the reason something might suspend in
development mode through debug info but this excludes some cases. As a
result we can end up with boundary that suspends but has no cause. This
tries to detect that and show a notice for why that might be. I'm also
trying to make it work with old React versions to cover everything.

In production we don't track any of this meta data like `_debugInfo`,
`_debugThenable` etc. so after resolution there's no information to take
from. Except suspensey images / css which we can track in prod too. We
could track lazy component types already. We'd have to add something
that tracks after the fact if something used a lazy child, child as a
promise, hooks, etc. which doesn't exist today. So that's not backwards
compatible and might add some perf/memory cost. However, another
strategy is also to try to replay the components after the fact which
could be backwards compatible. That's tricky for child position since
there's so many rules for how to do that which would have to be
replicated.

If you're in development you get a different error. Given that we've
added instrumentation very recently. If you're on an older development
version of React, then you get a different error. Unfortunately I think
my feature test is not quite perfect because it's tricky to test for the
instrumentation I just added.
https://github.com/facebook/react/pull/34146 So I think for some
prereleases that has `_debugOwner` but doesn't have that you'll get a
misleading error.

Finally, if you're in a modern development environment, the only reason
we should have any gaps is because of throw-a-Promise. This will
highlight it as missing. We can detect that something threw if a
Suspense boundary commits with a RetryCache but since it's a WeakSet we
can't look into it to see anything about what it might have been. I
don't plan on doing anything to improve this since it would only apply
to new versions of React anyway and it's just inherently flawed. So just
deprecate it #34032.

Note that nothing in here can detect that we suspended Transition. So
throwing at the root or in an update won't show that anywhere.
2025-08-15 18:32:27 -04:00
Joseph Savona
5063b3283f [compiler] Remove now-unused FunctionEffect type (#34029)
The new mutation/aliasing model significantly expands on the idea of
FunctionEffect. The type (and its usage in HIRFunction.effects) was only
necessary for the now-deleted old inference model so we can clean up
this code now.
2025-08-15 15:27:30 -07:00
Joseph Savona
eaf6adb127 [compiler][wip] Remove old mutation/aliasing implementation (#34028)
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34028).
* #34029
* __->__ #34028
2025-08-15 15:21:28 -07:00
Joseph Savona
6ffcac8558 [compiler] Add support for diagnostic hints (#34126)
Hints are meant as additional information to present to the developer
about an error. The first use-case here is for the suggestion to name
refs with "-Ref" if we encounter a mutation that looks like it might be
a ref. The original error printing used a second error detail which
printed the source code twice, a hint with just extra text is less
noisy.
2025-08-15 15:09:27 -07:00
Joseph Savona
724b324b96 [compiler] Add hint to name variables with "Ref" suffix (#34125)
If you have a ref that the compiler doesn't know is a ref (say, a value
returned from a custom hook) and try to assign its `.current = ...`, we
currently fail with a generic error that hook return values are not
mutable. However, an assignment to `.current` specifically is a very
strong hint that the value is likely to be a ref. So in this PR, we
track the reason for the mutation and if it ends up being an error, we
use it to show an additional hint to the user. See the fixture for an
example of the message.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34125).
* #34126
* __->__ #34125
* #34124
2025-08-15 15:05:29 -07:00
Jack Pope
45a6532a08 Add compareDocumentPosition to Fabric FragmentInstance (#34103)
Stacked on https://github.com/facebook/react/pull/34069

Same basic semantics as the react-dom for determining document position
of a Fragment compared to a given node. It's simpler here because we
don't have to deal with inserted nodes or portals. So we can skip a
bunch of the validation logic.

The logic for handling empty fragments is the same so I've split out
`compareDocumentPositionForEmptyFragment` into a shared module. There
doesn't seem to be a great place to put shared DOM logic between Fabric
and DOM configs at the moment. There may be more of this coming as we
add more and more DOM APIs to RN.

For testing I've written Fantom tests internally which pass the basic
cases on this build. The renderer we have configured for Fabric tests in
the repo doesn't support the Element APIs we need like
`compareDocumentPosition`.
2025-08-15 15:07:42 -04:00
Sebastian "Sebbie" Silbermann
8dba9311e5 [DevTools] Handle fallback unmount in Suspense update path (#34199) 2025-08-15 19:40:35 +02:00
Sebastian "Sebbie" Silbermann
2d98b45d92 [DevTools] Fix Suspense boundaries always being marked as not suspended (#34206) 2025-08-15 19:39:59 +02:00
Sebastian Markbåge
2ba7b07ce1 [DevTools] Compute a min and max range for the currently selected suspense boundary (#34201)
This computes a min and max range for the whole suspense boundary even
when selecting a single component so that each component in a boundary
has a consistent range.

The start of this range is the earliest start of I/O in that boundary or
the end of the previous suspense boundary, whatever is earlier. If the
end of the previous boundary would make the range large, then we cap it
since it's likely that the other boundary was just an independent
render.

The end of the range is the latest end of I/O in that boundary. If this
is smaller than the end of the previous boundary plus the 300ms
throttle, then we extend the end. This visualizes what throttling could
potentially do if the previous boundary committed right at its end. Ofc,
it might not have committed exactly at that time in this render. So this
is just showing a potential throttle that could happen. To see actual
throttle, you look in the Performance Track.

<img width="661" height="353" alt="Screenshot 2025-08-14 at 12 41 43 AM"
src="https://github.com/user-attachments/assets/b0155e5e-a83f-400c-a6b9-5c38a9d8a34f"
/>

We could come up with some annotation to highlight that this is eligible
to be throttled in this case. If the lines don't extend to the edge,
then it's likely it was throttled.
2025-08-15 13:34:07 -04:00
Jack Pope
a96a0f3903 Fix fragmentInstance#compareDocumentPosition nesting and portal cases (#34069)
Found a couple of issues while integrating
FragmentInstance#compareDocumentPosition into Fabric.

1. Basic checks of nested host instances were inaccurate. For example,
checking the first child of the first child of the Fragment would not
return CONTAINED_BY.
2. Then fixing that logic exposed issues with Portals. The DOM
positioning relied on the assumption that the first and last top-level
children were in the same order as the Fiber tree. I added additional
checks against the parent's position in the DOM, and special cased a
portaled Fragment by getting its DOM parent from the child instance,
rather than taking the instance from the Fiber return. This should be
accurate in more cases. Though its still a guess and I'm not sure yet
I've covered every variation of this. Portals are hard to deal with and
we may end up having to push more results towards
IMPLEMENTATION_SPECIFIC if accuracy is an issue.
2025-08-15 12:14:23 -04:00
Sebastian "Sebbie" Silbermann
02a8811864 [SuspenseTab] Scuffed version of Suspense rects (#34188) 2025-08-14 18:24:41 +02:00
Sebastian "Sebbie" Silbermann
379a083b9a Include stack of cause in React instrumentation errors (#34198) 2025-08-13 19:18:02 +02:00
Sebastian "Sebbie" Silbermann
534bed5fa7 Use yarn run in Flight fixture (#34197) 2025-08-13 15:49:44 +02:00
Sebastian Markbåge
db06f6b751 [DevTools] Track virtual debug info from suspensey images (#34181)
Same as #34166 but for Suspensey images.

The trick here is to check the `SuspenseyImagesMode` since not all
versions of React and not all subtrees will have Suspensey images
enabled yet.

The other trick is to read back from `currentSrc` to get the image url
we actually resolved to in this case. Similar to how for Suspensey CSS
we check if the media query would've matched.

<img width="591" height="205" alt="Screenshot 2025-08-11 at 9 32 56 PM"
src="https://github.com/user-attachments/assets/ac98785c-d3e0-407c-84e0-c27f86c0ecac"
/>
2025-08-13 09:26:21 -04:00
Sebastian "Sebbie" Silbermann
9433fe357a Fail tests if unasserted console calls contain undefined (#34191) 2025-08-13 08:48:04 +02:00
Sebastian "Sebbie" Silbermann
0032b2a3ee [Flight] Log error if prod elements are rendered (#34189) 2025-08-13 08:47:09 +02:00
Sebastian Markbåge
14c50e344c [DevTools] Use Visually Lighter Skeletons (#34185)
The skeletons right now are too jarring because they're visually heavier
than the content that comes in later. This makes them draw attention to
themselves as flashing things.

A good skeleton and loading indicator should ideally start as invisible
as possible and then gradually become more visible the longer time
passes so that if it loads quickly then it was never much visible at
all.

Even at its max it should never be heavier weight than the final content
so that it visually reverts into lesser. Another rule of thumb is that
it should be as close as possible to the final content in size but if
it's unknown it should always be smaller than the final content so that
the content grows into its slot rather than the slot contracting.

This makes the skeleton fade from invisible into the dimmest color just
as a subtle hint that something is still loading.

I also added a missing skeleton since the stack traces in rendered by
can now suspend while source mapping.

The other tweak I did is use disabled buttons in all the cases where we
load the ability to enable a button. This is more subtle and if you
hover over you can see why it's still disabled. Rather than flashing the
button each time you change element.
2025-08-12 23:10:31 -04:00
Sebastian Markbåge
f1222f7652 [Fiber] Don't bind retry listener if it's in the cache (#34183)
This did an unnecessary bind allocation even if there's cache hit.
2025-08-12 21:42:24 -04:00
Josh Story
9baecbf02b [Fizz] Avoid hanging when suspending after aborting while rendering (#34192)
This fixes an edge case where you abort the render while rendering a
component that ends up Suspending. It technically only applied if you
were deep enough to be inside `renderNode` and was not susceptible to
hanging if the abort + suspending component was being tried inside
retryRenderTask/retryReplaytask.

The fix is to preempt the thenable checks in renderNode and check if the
request is aborting and if so just bubble up to the task handler.

The reason this hung before is a new task would get scheduled after we
had aborted every other task (minus the currently rendering one). This
led to a situation where the task count would not hit zero.
2025-08-12 16:46:56 -07:00
Sebastian "Sebbie" Silbermann
0422a00e3e [DevTools] Fix missing key warning (#34186) 2025-08-12 19:58:19 +02:00
Sebastian Markbåge
47fd2f5e14 [DevTools] Fix index (#34187)
I used the wrong indexer and tested with one entry.
2025-08-12 13:57:35 -04:00
Jan Kassens
1dc3bdead1 Remove unused arguments from ReactElement (#34174)
After various feature flag removals recently, these arguments became
unused and can be deleted.
2025-08-12 11:09:35 -04:00
Sebastian "Sebbie" Silbermann
de06211dbe [DevTools] Send Suspense rects to frontend (#34170) 2025-08-12 16:48:35 +02:00
Sebastian "Sebbie" Silbermann
ac7820a99e Create fresh Offscreen instance when replaying (#34127) 2025-08-11 20:55:48 +02:00
Sebastian Markbåge
3c67bbe5f9 [DevTools] Track suspensey CSS on "suspended by" (#34166)
We need to track that Suspensey CSS (Host Resources) can contribute to
the loading state. We can pick up the start/end time from the
Performance Observer API since we know which resource was loaded.

If DOM nodes are not filtered there's a link to the `<link>` instance.
The `"awaited by"` stack is the callsite of the JSX creating the
`<link>`.

<img width="591" height="447" alt="Screenshot 2025-08-11 at 1 35 21 AM"
src="https://github.com/user-attachments/assets/63af0ca9-de8d-4c74-a797-af0a009b5d73"
/>

Inspecting the link itself:

<img width="592" height="344" alt="Screenshot 2025-08-11 at 1 31 43 AM"
src="https://github.com/user-attachments/assets/89603dbc-6721-4bbf-8b58-6010719b29e3"
/>

In this approach I only include it if the page currently matches the
media query. It might contribute in some other scenario but we're not
showing every possible state but every possible scenario that might
suspend if timing changes in the current state.
2025-08-11 12:28:32 -04:00
Sebastian Markbåge
2c9a42dfd7 [DevTools] If the await doesn't have a stack use the stack from use() if any (#34162)
Stacked on #34148.

This picks up the stack for the await from the `use()` Hook if one was
used to get this async info.

When you select a component that used hooks, we already collect this
information.

If you select a Suspense boundary, this lazily invokes the first
component that awaited this data to inspects its hooks and produce a
stack trace for the use().

When all we have for the name is "Promise" I also use the name of the
first callsite in the stack trace if there's more than one. Which in
practice will be the name of the custom Hook that called it. Ideally
we'd use source mapping and ignore listing for this but that would
require suspending the display. We could maybe make the SuspendedByRow
wrapped in a Suspense boundary for this case.

<img width="438" height="401" alt="Screenshot 2025-08-10 at 10 07 55 PM"
src="https://github.com/user-attachments/assets/2a68917d-c27b-4c00-84aa-0ceb51c4e541"
/>
2025-08-11 12:28:10 -04:00
Jan Kassens
f1e70b5e0a [easy] remove leftover reference to disableDefaultPropsExceptForClasses (#34169)
Noticed that I missed this in some earlier cleanup diff.

Test Plan:
grep for disableDefaultPropsExceptForClasses
2025-08-11 12:13:33 -04:00
Sebastian Markbåge
d587434c35 [DevTools] Pick up suspended by info from use() (#34148)
Similar to #34144 but for `use()`.

`use()` dependencies don't get added to the `fiber._debugInfo` set
because that just models the things blocking the children, and not the
Fiber component itself. This picks up any debug info from the thenable
state that we stashed onto `_debugThenableState` so that we know it used
`use()`.

<img width="593" height="425" alt="Screenshot 2025-08-09 at 4 03 40 PM"
src="https://github.com/user-attachments/assets/c7e06884-4efd-47fa-a76b-132935db6ddc"
/>

Without #34146 this doesn't pick up uninstrumented promises but after
it, it'll pick those up as well. An instrumented promise that doesn't
have anything in its debug info is not picked up. For example, if it
didn't depend on any I/O on the server.

This doesn't yet pick up the stack trace of the `use()` call. That
information is in the Hooks information but needs a follow up to extract
it.
2025-08-11 12:10:05 -04:00
Sebastian Markbåge
ca292f7a57 [DevTools] Don't show "awaited by" if there's nothing to show (#34163)
E.g. if the owner is null or the same as current component and no stack.
This happens for example when you return a plain Promise in the child
position and inspect the component it was returned in since there's no
hook stack and the owner is the same as the instance itself so there's
nothing new to link to.

Before:

<img width="267" height="99" alt="Screenshot 2025-08-10 at 10 28 32 PM"
src="https://github.com/user-attachments/assets/23341ab2-2888-457d-a1d1-128f3e0bd5ec"
/>

After:

<img width="253" height="91" alt="Screenshot 2025-08-10 at 10 29 04 PM"
src="https://github.com/user-attachments/assets/b33bb38b-891a-4f46-bc16-15604b033cdb"
/>
2025-08-11 11:48:09 -04:00
Sebastian Markbåge
62a634b972 [DebugTools] Use thenables from the _debugThenableState if available (#34161)
In the case where a Promise is not cached, then the thenable state might
contain an older version. This version is the one that was actually
observed by the committed render, so that's the version we'll want to
inspect.

We used to not store the thenable state but now we have it on
`_debugThenableState` in DEV.

<img width="593" height="359" alt="Screenshot 2025-08-10 at 8 26 04 PM"
src="https://github.com/user-attachments/assets/51ee53f3-a31a-4e3f-a4cf-bb20b6efe0cb"
/>
2025-08-11 11:46:27 -04:00
Sebastian Markbåge
53d07944df [Fiber] Assign implicit debug info to used thenables (#34146)
Similar to #34137 but for Promises.

This lets us pick up the debug info from a raw Promise as a child which
is not covered by `_debugThenables`. Currently ChildFiber doesn't stash
its thenables so we can't pick them up from devtools after the fact
without some debug info added to the parent.

It also lets us track some approximate start/end time of use():ed
promises based on the first time we saw this particular Promise.
2025-08-11 11:44:05 -04:00
Sebastian Markbåge
34ce3acafd [DevTools] Pick up suspended by info from React.lazy in type position (#34144)
Normally, we pick up debug info from instrumented Promise or React.Lazy
while we're reconciling in ReactChildFiber when they appear in the child
position. We add those to the `_debugInfo` of the Fiber.

However, we don't do that for for Lazy in the Component type position.
Instead, we have to pick up the debug info from it explicitly in
DevTools. Likely this is the info added by #34137. Older versions
wouldn't be covered by this particular mechanism but more generally from
throwing a Promise.


<img width="592" height="449" alt="Screenshot 2025-08-08 at 11 32 33 PM"
src="https://github.com/user-attachments/assets/87211c64-a7df-47b7-a784-5cdc7c5fae16"
/>
2025-08-11 11:42:59 -04:00
Sebastian Markbåge
6445b3154e [Fiber] Add additional debugInfo to React.lazy constructors in DEV (#34137)
This creates a debug info object for the React.lazy call when it's
called on the client. We have some additional information we can track
for these since they're created by React earlier.

We can track the stack trace where `React.lazy` was called to associate
it back to something useful. We can track the start time when we
initialized it for the first time and the end time when it resolves. The
name from the promise if available.

This data is currently only picked up in child position and not
component position. The component position is in a follow up.

<img width="592" height="451" alt="Screenshot 2025-08-08 at 2 49 33 PM"
src="https://github.com/user-attachments/assets/913d2629-6df5-40f6-b036-ae13631379b9"
/>

This begs for ignore listing in the front end since these stacks aren't
filtered on the server.
2025-08-11 11:42:23 -04:00
Sebastian Markbåge
ab5238d5a4 [DevTools] Show name prop of Suspense / Activity in the Components Tree view (#34135)
The name prop will be used in the Suspense tab to help identity a
boundary. Activity will also allow names. A custom component can be
identified by the name of the component but built-ins doesn't have that.

This PR adds it to the Components Tree View as well since otherwise you
only have the key to go on. Normally we don't add all the props to avoid
making this view too noisy but this is an exception along with key to
help identify a boundary quickly in the tree.

Unlike the SuspenseNode store, this wouldn't ever have a name inferred
by owner since that kind of context already exists in this view.

<img width="600" height="161" alt="Screenshot 2025-08-08 at 1 20 36 PM"
src="https://github.com/user-attachments/assets/fe50d624-887a-4b9d-9186-75f131f83195"
/>

I also made both the key and name prop searchable.

<img width="608" height="206" alt="Screenshot 2025-08-08 at 1 32 27 PM"
src="https://github.com/user-attachments/assets/d3502d9c-7614-45fc-b973-57f06dd9cddc"
/>
2025-08-11 11:41:46 -04:00
Sebastian Markbåge
7a934a16b8 [DevTools] Show Owner Stacks in "rendered by" View (#34130)
This shows the stack trace of the JSX at each level so now you can also
jump to the code location for the JSX callsite. The visual is similar to
the owner stacks with `createTask` except when you click the `<...>` you
jump to the Instance in the Components panel.

<img width="593" height="450" alt="Screenshot 2025-08-08 at 12 19 21 AM"
src="https://github.com/user-attachments/assets/dac35faf-9d99-46ce-8b41-7c6fe24625d2"
/>

I'm not sure it's really necessary to have all the JSX stacks of every
owner. We could just have it for the current component and then the rest
of the owners you could get to if you just click that owner instance.

As a bonus, I also use the JSX callsite as the fallback for the "View
Source" button. This is primarily useful for built-ins like `<div>` and
`<Suspense>` that don't have any implementation to jump to anyway. It's
useful to be able to jump to where a boundary was defined.
2025-08-11 11:41:30 -04:00
Sebastian Markbåge
59ef3c4baf [DevTools] Allow Introspection of React Elements and React.lazy (#34129)
With RSC it's common to get React.lazy objects in the children position.
This first formats them nicely.

Then it adds introspection support for both lazy and elements.

Unfortunately because of quirks with the hydration mechanism we have to
expose it under the name `_payload` instead of something direct. Also
because the name "type" is taken we can't expose the type field on an
element neither. That whole algorithm could use a rewrite.

<img width="422" height="137" alt="Screenshot 2025-08-07 at 11 37 03 PM"
src="https://github.com/user-attachments/assets/a6f65f58-dbc4-4b8f-928b-d7f629fc51b2"
/>

<img width="516" height="275" alt="Screenshot 2025-08-07 at 11 36 36 PM"
src="https://github.com/user-attachments/assets/650bafdb-a633-4d78-9487-a750a18074ce"
/>

For JSX an alternative or additional feature might be instead to jump to
the first Instance that was rendered using that JSX. We know that based
on the equality of the memoizedProps on the Fiber. It's just a matter of
whether we do that eagerly or more lazily when you click but you may not
have a match so would be nice to indicate that before you click.
2025-08-11 11:41:14 -04:00
Sebastian "Sebbie" Silbermann
72965f3615 [DevTools] Restore reconciling Suspense stack after fallback was reconciled (#34168) 2025-08-11 17:12:39 +02:00
Sebastian Markbåge
594fb5e9ab [DevTools] Always skip 1 frame (#34167)
Follow up to #34093.

There's an issue where the skipFrames argument isn't part of the cache
key so the other parsers that expect skipping one frame might skip zero
and show the internal `fakeJSXDEV` callsite. Ideally we should include
the skipFrames as part of the cache key but we can also always just skip
one.
2025-08-11 01:50:26 -04:00
Sebastian "Sebbie" Silbermann
98286cf8e3 [DevTools] Send suspense nodes to frontend store (#34070) 2025-08-10 10:12:20 +02:00
Sophie Alpert
cf6e502ed2 Hot reloading: Avoid stack overflow on wide trees (#34145)
Every sibling added to the stack here. Not sure this needs to be
recursive at all but certainly for siblings this can just be a loop.
2025-08-09 08:02:22 -07:00
Sebastian Markbåge
3958d5d84b [Flight] Copy the name field of a serialized function debug value (#34085)
This ensures that if the name is set manually after the declaration,
then we get that name when we log the value. For example Node.js
`Response` is declared as `_Response` and then later assigned a new
name.

We should probably really serialize all static enumerable properties but
"name" is non-enumerable so it's still a special case.
2025-08-07 10:55:01 -04:00
Sebastian Markbåge
738aebdbac [DevTools] Add Badge to Owners and sometimes stack traces (#34106)
Stacked on #34101.

This adds a badge to owners if they are different from the currently
selected component's environment.

<img width="590" height="566" alt="Screenshot 2025-08-04 at 5 15 02 PM"
src="https://github.com/user-attachments/assets/e898254f-1b4c-498e-8713-978d90545340"
/>

We also add one to the end of stack traces if the stack trace has a
different environment than the owner which can happen when you call a
function (without rendering a component) into a third party environment
but the owner component was in the first party.

One awkward thing is that Suspense boundaries are always in the client
environment so their Server Components are always badged.
2025-08-07 10:39:08 -04:00
Sebastian Markbåge
4c9c109cea [Fiber] Try to give a stack trace to every entry in the Scheduler Performance Track (#34123)
For "render" and "commit" phases we don't give any specific stack atm.
This tries to always provide something useful to say the cause of the
render.

For normal renders this will now show the same thing as the "Event" and
"Update" entries already showed. We stash the task that was used for
those and use them throughout the render and commit phases.

For Suspense (Retry lane) and Idle (Offscreen lane), we don't have any
updates. Instead for those there's a component that left work behind in
previous passes. For those I use the debugTask of the `<Suspense>` or
`<Activity>` boundary to indicate that this was the root of the render.

Similarly when an Action is invoked on a `<form action={...}>` component
using the built-in submit handler, there's no actionable stack in user
space that called it. So we use the stack of the JSX for the form
instead.
2025-08-07 10:26:30 -04:00
Ruslan Lesiutin
552a5dadcf [DevTools] fix: handle store mutations synchronously in TreeContext (#34119)
If there is a commit that removes the currently inspected (selected)
elements in the Components tree, we are going to kick off the transition
to re-render the Tree. The elements will be re-rendered with the
previous inspectedElementID, which was just removed and all consecutive
calls to store object with this id would produce errors, since this
element was just removed.

We should handle store mutations synchronously. Doesn't make sense to
start a transition in this case, because Elements depend on the
TreeState and could make calls to store in render function.

Before:
<img width="2286" height="1734" alt="Screenshot 2025-08-06 at 17 41 14"
src="https://github.com/user-attachments/assets/97d92220-3488-47b2-aa6b-70fa39345f6b"
/>


After:


https://github.com/user-attachments/assets/3da36aff-6987-4b76-b741-ca59f829f8e6
2025-08-07 14:05:56 +01:00
Joseph Savona
f468d37739 [compiler] remove use of inspect module (#34124) 2025-08-06 23:59:55 -07:00
Joseph Savona
c403a7c548 [compiler] Upstream experimental flow integration (#34121)
all credit on the Flood/ code goes to @mvitousek and @jbrown215, i'm
just the one upstreaming it
2025-08-06 15:58:07 -07:00
Sebastian Markbåge
fa212fc2b1 [DevTools] Measure the Rectangle of Suspense boundaries as we reconcile (#34090)
Stacked on #34089.

This measures the client rects of the direct children of Suspense
boundaries as we reconcile. This will be used by the Suspense tab to
visualize the boundaries given their outlines.

We could ask for this more lazily just in case we're currently looking
at the Suspense tab. We could also do something like monitor the sizes
using a ResizeObserver to cover when they change.

However, it should be pretty cheap to this in the reconciliation phase
since we're already mostly visiting these nodes on the way down. We have
also already done all the layouts at this point since it was part of the
commit phase and paint already. So we're just reading cached values in
this phase. We can also infer that things are expected to change when
parents or sibling changes. Similar technique as ViewTransitions.
2025-08-06 14:56:52 -04:00
Sebastian Markbåge
b080063331 [DevTools] Source Map Stack Traces such in await locations (#34094)
Stacked on #34093.

Instead of using the original `ReactStackTrace` that has the call sites
on the server, this parses the `Error` object which has the virtual call
sites on the client. We'll need this technique for things stack traces
suspending on the client anyway like `use()`.

We can then use these callsites to source map in the front end.

We currently don't source map function names but might be useful for
this use case as well as getting original component names from prod.

One thing this doesn't do yet is that it doesn't ignore list the stack
traces on the client using the source map's ignore list setting. It's
not super important since we expect to have already ignore listed on the
server but this will become important for client stack traces like
`use()`.
2025-08-06 13:45:06 -04:00
Sebastian Markbåge
66f09bd054 [DevTools] Sort "Suspended By" view by the start time (#34105)
or end time if they have the same start time.

<img width="517" height="411" alt="Screenshot 2025-08-04 at 4 00 23 PM"
src="https://github.com/user-attachments/assets/b99be67b-5727-4e24-98c0-ee064fb21e2f"
/>

They would typically appear in this order naturally but not always.
Especially in Suspense boundaries where the order can also be depended
on when the components are discovered.
2025-08-06 11:23:00 -04:00
Sebastian Markbåge
0825d019be [DevTools] Prefer I/O stack and show await stack after only if it's a different owner (#34101)
Stacked on #34094.

This shows the I/O stack if available. If it's not available or if it
has a different owner (like if it was passed in) then we show the
`"awaited at:"` stack below it so you can see where it started and where
it was awaited. If it's the same owner this tends to be unnecessary
noise. We could maybe be smarter if the stacks are very different then
you might want to show both even with the same owner.

<img width="517" height="478" alt="Screenshot 2025-08-04 at 11 57 28 AM"
src="https://github.com/user-attachments/assets/2dbfbed4-4671-4a5f-8e6e-ebec6fe8a1b7"
/>

Additionally, this adds an inferred await if there's no owner and no
stack for the await. The inferred await of a function/class component is
just the owner. No stack. Because the stack trace would be the return
value. This will also be the case if you use throw-a-Promise. The
inferred await in the child position of a built-in is the JSX location
of that await like if you pass a promise to a child. This inference
already happens when you pass a Promise from RSC so in this case it
already has an await - so this is mainly for client promises.
2025-08-06 11:21:01 -04:00
Sebastian Markbåge
c97ec75324 [DevTools] Disconnect and Reconnect children of Suspense boundaries instead of Unmounting and Mounting (#34089)
Stacked on #34082.

This keeps the DevToolsInstance children alive inside Offscreen trees
while they're hidden. However, they're sent as unmounted to the front
end store.

This allows DevTools state to be preserved between these two states.

Such as it keeps the "suspended by" set on the SuspenseNode alive since
the children are still mounted. So now you when you resuspend, you can
see what in the children was suspended. This is useful when you're
simulating a suspense but can also be a bit misleading when something
suspended for real since it'll only show the previous suspended set and
not what is currently suspending it since that hasn't committed yet.

SuspenseNodes inside resuspended trees are now kept alive too. That way
they can contribute to the timeline even when resuspended. We can choose
whether to keep them visible in the rects while hidden or not.

In the future we'll also need to add more special cases around Activity.
Because right now if SuspenseNodes are kept alive in the Suspense tab UI
while hidden, then they're also alive inside Activity that are hidden
which maybe we don't want. Maybe simplest would be that they both
disappear from the Suspense tab UI but can be considered for the
timeline.

Another case is that when Activity goes hidden, Fiber will no longer
cause its content to suspend the parent but that's not modeled here. So
hidden Activity will show up as "suspended by" in a parent Suspense.
When they disconnect, they should really be removed from the "suspended
by" set of the parent (and perhaps be shown only on the Activity
boundary itself).
2025-08-06 11:05:19 -04:00
Sebastian Markbåge
99fd4f2ac1 [DevTools] Reorder moved filtered Fibers with backing DevToolsInstance (#34104)
Instead, we just continue to collect the unfiltered children.

---------

Co-authored-by: Sebastian Sebbie Silbermann <sebastian.silbermann@vercel.com>
2025-08-05 12:39:45 -04:00
Joseph Savona
7deda941f7 [compiler] Delete PropagatePhiTypes (#34107)
We moved this logic into InferTypes a long time ago and the PRs to clean
it up keep getting lost in the shuffle.
2025-08-04 15:15:51 -07:00
Joseph Savona
d3b26b2953 [compiler] rebase #32285 (#34102)
Redo of #32285 which was created with ghstack and is tedious to rebase
with sapling.
2025-08-04 12:04:44 -07:00
lauren
b211d7023c [compiler] Add repros for various invariants (#34099)
We received some bug reports about invariants reported by the compiler
in their codebase. Adding them as repros.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34099).
* #34100
* __->__ #34099
2025-08-04 14:36:12 -04:00
Sebastian Markbåge
ba4bdb2ab5 [DevTools] Consume SuspenseNodes that were skipped when we're bailing out of a subtree (#34082)
This searches through the remaining children to see if any of them were
children of the bailed out FiberInstance and if so we should reuse them
in the new set. It's faster to do this than search through children of
the FiberInstance for Suspense boundaries.
2025-08-04 13:04:47 -04:00
Sebastian Markbåge
be11cb5c4b [DevTools] Tweak the presentation of the Promise value (#34097)
Show the value as "fulfilled: Type" or "rejected: Type" immediately
instead of having to expand it twice. We could show all the properties
of the object immediately like we do in the Performance Track but it's
not always particularly interesting data in the value that isn't already
in the header.

I also moved it to the end after the stack traces since I think the
stack is more interesting but I'm also visually trying to connect the
stack trace with the "name" since typically the "name" will come from
part of the stack trace.

Before:

<img width="517" height="433" alt="Screenshot 2025-08-03 at 11 39 49 PM"
src="https://github.com/user-attachments/assets/ad28d8a2-c149-4957-a393-20ff3932a819"
/>

After:

<img width="520" height="476" alt="Screenshot 2025-08-03 at 11 58 35 PM"
src="https://github.com/user-attachments/assets/53a755b0-bb68-4305-9d16-d6fac7ca4910"
/>
2025-08-04 09:42:48 -04:00
Sebastian Markbåge
557745eb0b [DevTools] Add structure full stack parsing to DevTools (#34093)
We'll need complete parsing of stack traces for both owner stacks and
async debug info so we need to expand the stack parsing capabilities a
bit. This refactors the source location extraction to use some helpers
we can use for other things too.

This is a fork of `ReactFlightStackConfigV8` which also supports
DevTools requirements like checking both `react_stack_bottom_frame` and
`react-stack-bottom-frame` as well as supporting Firefox stacks.

It also supports extracting the first frame of a component stack or the
last frame of an owner stack for the source location.
2025-08-04 09:37:46 -04:00
Sebastian Markbåge
d3f800d47a [DevTools] Style clickable Owner components with angle brackets and bold (#34096)
We have two type of links that appear next to each other now. One type
of link jumps to a Component instance in the DevTools. The other opens a
source location - e.g. in your editor.

This clarifies that something will jump to the Component instance by
marking it as bold and using angle brackets around the name.

This can be seen in the "rendered by" list of owner as well as in the
async stack traces when the stack was in a different owner than the one
currently selected.

<img width="516" height="387" alt="Screenshot 2025-08-03 at 11 27 38 PM"
src="https://github.com/user-attachments/assets/5da50262-1e74-4e46-a6f8-96b4c1e4db31"
/>

The idea is to connect this styling to the owner stacks using
`createTask` where this same pattern occurs (albeit the task name is not
clickable):

<img width="454" height="188" alt="Screenshot 2025-08-03 at 11 23 45 PM"
src="https://github.com/user-attachments/assets/81a55c8f-963a-4fda-846a-97f49ef0c469"
/>

In fact, I was going to add the stack traces to the "rendered by" list
to give the ability to jump to the JSX location in the owner stack so
that it becomes this same view.
2025-08-04 09:28:31 -04:00
Sebastian Markbåge
8e3db095aa [DevTools] Make a non-editable name of KeyValue clickable (#34095)
This has been bothering me. You can click the arrow and the value to
expand/collapse a KeyValue row but not the name.

When the name is not editable it should be clickable. Such as when
inspecting a Promise value.
2025-08-04 09:27:37 -04:00
Sebastian Markbåge
041754697c [DevTools] Only show state for ClassComponents (#34091)
The only thing that uses `memoizedState` as a public API is
ClassComponents. Everything else uses it as internals. We shouldn't ever
show those internals.

Before those internals showed up for example on a suspended Suspense
boundary:

<img width="436" height="370" alt="Screenshot 2025-08-03 at 8 13 37 PM"
src="https://github.com/user-attachments/assets/7fe275a7-d5da-421d-a000-523825916630"
/>
2025-08-04 09:26:12 -04:00
Ruslan Lesiutin
30fca45c1c fix: apply initial horizontal offset on tree mount (#34088)
When the element is pre-selected and the Tree component is mounted,
right now we are only applying initial vertical offset, but not the
horizontal one.

Because of this, if the DOM element was selected on Elements panel and
then user opens Components panel for the first time of the browser
DevTools session, depending on the element's depth, it could be hidden.

Similarly to vertical offset, apply horizontal one, but via ref setter.

### Before:

https://github.com/user-attachments/assets/0ab3cca9-93c1-4e9e-8d23-88330d438912

### After:

https://github.com/user-attachments/assets/10de153a-1e55-4cf7-b1ff-4cc7cb35ba10
2025-08-04 12:12:53 +01:00
Sebastian Markbåge
c499adf8c8 [Flight] Allow Temporary References to be awaited (#34084)
Fixes #33534.

`.then` method can be tested when you await a value that's not a
Promise. For regular Client References we have a way to mark those as
"async" and yield a reference to the unwrapped value in case it's a
Promise on the Client.

However, the realization is that we never serialize Promises as opaque
when passed from the client to the server. If a Promise is passed, then
it would've been deserialized as a Promise (while still registered as a
temporary reference) and not one of these Proxy objects.

Technically it could be a non-function value on the client which would
be wrong but you're not supposed to dot into it in the first place.

So we can just assume it's `undefined`.
2025-08-02 18:44:20 -04:00
Dennis Kats
1d163962b2 Allow returning a temporary reference inside an async function (#33761)
<!--
  Thanks for submitting a pull request!
We appreciate you spending the time to work on these changes. Please
provide enough information so that others can review your pull request.
The three fields below are mandatory.

Before submitting a pull request, please make sure the following is
done:

1. Fork [the repository](https://github.com/facebook/react) and create
your branch from `main`.
  2. Run `yarn` in the repository root.
3. If you've fixed a bug or added code that should be tested, add tests!
4. Ensure the test suite passes (`yarn test`). Tip: `yarn test --watch
TestName` is helpful in development.
5. Run `yarn test --prod` to test in the production environment. It
supports the same options as `yarn test`.
6. If you need a debugger, run `yarn test --debug --watch TestName`,
open `chrome://inspect`, and press "Inspect".
7. Format your code with
[prettier](https://github.com/prettier/prettier) (`yarn prettier`).
8. Make sure your code lints (`yarn lint`). Tip: `yarn linc` to only
check changed files.
  9. Run the [Flow](https://flowtype.org/) type checks (`yarn flow`).
  10. If you haven't already, complete the CLA.

Learn more about contributing:
https://reactjs.org/docs/how-to-contribute.html
-->

## Summary

Fixes `await`-ing and returning temporary references in `async`
functions. These two operations invoke `.then()` under the hood if it is
available, which currently results in an "Cannot access then on the
server. You cannot dot into a temporary client reference..." error. This
can easily be reproduced by returning a temporary reference from a
server function.

Fixes #33534 

## How did you test this change?
I added a test in a new test file. I wasn't sure where else to put it.
<img width="771" height="138" alt="image"
src="https://github.com/user-attachments/assets/09ffe6eb-271a-4842-a9fe-c68e17b3fb41"
/>


<!--
Demonstrate the code is solid. Example: The exact commands you ran and
their output, screenshots / videos if the pull request changes the user
interface.
How exactly did you verify that your PR solves the issue you wanted to
solve?
  If you leave this empty, your PR will very likely be closed.
-->
2025-08-02 18:11:54 -04:00
Joseph Savona
ddf8bc3fba [compiler] Improve merging of scopes that invalidate together (#34049)
We try to merge consecutive reactive scopes that will always invalidate
together, but there's one common case that isn't handled.

```js
const y = [[x]];
```

Here we'll create two consecutive scopes for the inner and outer array
expressions. Because the input to the second scope is a temporary,
they'll merge into one scope.

But if we name the inner array, the merging stops:

```js
const array = [x];
const y = [array];
```

This is because the merging logic checks if all the dependencies of the
second scope are outputs of the first scope, but doesn't account for
renaming due to LoadLocal/StoreLocal. The fix is to track these
temporaries.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34049).
* __->__ #34049
* #34047
* #34044
2025-08-01 13:00:01 -07:00
Joseph Savona
0860b9cc1f [compiler] Add definitions for Object entries/keys/values (#34047)
Fixes remaining issue in #32261, where passing a previously useMemo()-d
value to `Object.entries()` makes the compiler think the value is
mutated and fail validatePreserveExistingMemo. While I was there I added
Object.keys() and Object.values() too.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34047).
* #34049
* __->__ #34047
* #34044
2025-08-01 12:59:49 -07:00
Sebastian Markbåge
538ac7ae4b [Flight] Fix debug info leaking to outer handler (#34081)
The `waitForReference` call for debug info can trigger inside a
different object's initializingHandler. In that case, we can get
confused by which one is the root object.

We have this special case to detect if the initializing handler's object
is `null` and we have an empty string key, then we should replace the
root object's value with the resolved value.


52612a7cbd/packages/react-client/src/ReactFlightClient.js (L1374)

However, if the initializing handler actually should have the value
`null` then we might get confused by this and replace it with the
resolved value from a debug object. This fixes it by just using a
non-empty string as the key for the waitForReference on debug value
since we're not going to use it anyway.

It used to be impossible to get into this state since a `null` value at
the root couldn't have any reference inside itself but now the debug
info for a `null` value can have outstanding references.

However, a better fix might be using a placeholder marker object instead
of null or better yet ensuring that we know which root we're
initializing in the debug model.
2025-08-01 15:44:48 -04:00
lauren
52612a7cbd [compiler] Emit more specific error when making identifiers with reserved words (#34080)
This currently throws an invariant which may be misleading. I checked
the ecma262 spec and used the same list of reserved words in our check.
To err on the side of being conservative, we also error when strict mode
reserved words are used.
2025-08-01 15:10:34 -04:00
Sebastian "Sebbie" Silbermann
bdb4a96f62 [DevTools] Lazily compute initial Tree state (#34078) 2025-08-01 17:49:25 +02:00
Sebastian Markbåge
c260b38d0a [DevTools] Clean up Virtual Instances from id map (#34063)
This was a pretty glaring memory leak. 🙈

I forgot to clean up the VirtualInstances from the id map so the Server
Component instances always leaked in DEV.
2025-07-31 10:30:31 -04:00
Sebastian Markbåge
5bbf9be246 [DevTools] Model Hidden Offscreen Boundaries as Unmounts (#34062)
This is modeling Offscreen boundaries as the thing that unmounts a tree
in the frontend. This will let us model this as a "hide" that preserves
state instead in a follow up but not yet.

By doing it this way, we don't have to special case suspended Suspense
boundaries, at least not for the modern versions that use Offscreen as
the internal node. It's still special cased for the old React versions.
Instead, this is handled by the Offscreen fiber getting hidden.

By giving this fiber an FilteredFiberInstance, we also have somewhere to
store the children on (separately from the parent children set which can
include other siblings too like the loading state).

One consequence is that Activity boundary content now disappears when
they're hidden which is probably a good thing since otherwise it would
be confusing and noisy when it's used to render multiple pages at once.
2025-07-31 10:30:10 -04:00
Josh Story
8de7aed892 [Fizz] Count Boundary bytes that may contribute to the preamble in the request byteSize (#34059)
Stacked on #34058

When tracking how large the shell is we currently only track the bytes
of everything above Suspense boundaries. However since Boundaries that
contribute to the preamble will always be inlined when the shell flushes
they should also be considered as part of the request byteSize since
they always flush alongside the shell. This change adds this tracking
2025-07-30 18:18:57 -07:00
Josh Story
98773466ce [Fizz] Don't outline Boundaries that may contribute to the preamble (#34058)
Suspense boundaries that may have contributed to the preamble should not
be outlined due to size because these boundaries are only meant to be in
fallback state if the boundary actually errors. This change excludes any
boundary which has the potential to contribute to the preamble. We could
alternatively track which boundaries actually contributed to the
preamble but in practice there will be very few and I think this is
sufficient.

One problem with this approach is it makes Suspense above body opt out
of the mode where we omit rel="expect" for large shells. In essence
Suspense above body has the semantics of a Shell (it blocks flushing
until resolved) but it doesn't get tracked as request bytes and thus we
will not opt users into the skipped blocking shell for very large
boundaries.

This will be fixed in a followup
2025-07-30 18:06:47 -07:00
Sebastian Markbåge
9784cb379e [DevTools] No suspending above the root (#34055)
Follow up to #34050.

It's not actually possible to suspend *above* the root since even if you
suspend in the first child position, you're still suspending the
HostRoot which always has a corresponding FiberInstance and
SuspenseNode.
2025-07-30 11:31:27 -04:00
Sebastian Markbåge
dcf2a6f665 [DevTools] Keep a Suspense Tree Parellel to the Instance tree in the Backend (#34050)
This keeps a data structure of Suspense boundaries and the root which
can keep track which boundaries might participate in a loading sequence
and everything that suspends them. This will power the Suspense tab.

Now when you select a `<Suspense>` boundary the "suspended by" section
shows the whole boundary instead of just that component.

In the future, we'll likely need to add "Activity" boundaries to this
tree as well, so that we can track what suspended the root of an
Activity when filtering a subtree. Similar to how the root SuspenseNode
now tracks suspending at the root. Maybe it's ok to just traverse to
collect this information on-demand when you select one though since this
doesn't contribute to the deduping.

We'll also need to add implicit Suspense boundaries for the rows of a
SuspenseList with `tail=hidden/collapsed`.
2025-07-30 09:55:09 -04:00
Sebastian "Sebbie" Silbermann
36c63d4f9c [DevTools] Layout for Suspense tab (#34042) 2025-07-30 07:12:18 +02:00
Joseph Savona
88b40f6e41 Enable ref validation in linter (#34044)
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34044).
* #34027
* __->__ #34044
2025-07-29 12:30:29 -07:00
Joseph Savona
04a7a61918 [compiler] Allow assigning ref-accessing functions to objects if not mutated (#34026)
Allows assigning a ref-accessing function to an object so long as that
object is not subsequently transitively mutated. We should likely
rewrite the ref validation to use the new mutation/aliasing effects,
which would provide a more consistent behavior across instruction types
and require fewer special cases like this.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34026).
* #34027
* __->__ #34026
2025-07-29 10:57:26 -07:00
Joseph Savona
c2326b1336 [compiler] disallow ref access in state initializer, reducer/initializer (#34025)
Per title, disallow ref access in `useState()` initializer function,
`useReducer()` reducer, and `useReducer()` init function.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34025).
* #34027
* #34026
* __->__ #34025
2025-07-29 10:56:04 -07:00
Joseph Savona
4395689980 [compiler] ref guards apply up to fallthrough of the test (#34024)
Fixes #30782

When developers do an `if (ref.current == null)` guard for lazy ref
initialization, the "safe" blocks should extend up to the if's
fallthrough. Previously we only allowed writing to the ref in the if
consequent, but this meant that you couldn't use a ternary, logical, etc
in the if body.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34024).
* #34027
* #34026
* #34025
* __->__ #34024
2025-07-29 10:53:13 -07:00
Joseph Savona
6891dcb87d [compiler] treat ref-like identifiers as refs by default (#34005)
`@enableTreatRefLikeIdentifiersAsRefs` is now on by default. I made one
small fix to the render helper logic as part of this, uncovered by
including more tests.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34005).
* #34027
* #34026
* #34025
* #34024
* __->__ #34005
2025-07-29 10:51:10 -07:00
Joseph Savona
3f40eb73a8 [compiler] Allow passing refs to render helpers (#34006)
We infer render helpers as functions whose result is immediately
interpolated into jsx. This is a very conservative approximation, to
help with common cases like `<Foo>{props.renderItem(ref)}</Foo>`. The
idea is similar to hooks that it's ultimately on the developer to catch
ref-in-render validations (and the runtime detects them too), so we can
be a bit more relaxed since there are valid reasons to use this pattern.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34006).
* #34027
* #34026
* #34025
* #34024
* #34005
* __->__ #34006
* #34004
2025-07-29 10:06:23 -07:00
Joseph Savona
1d7e942da7 [compiler] Allow mergeRefs pattern (and detect refs passed as ref prop) (#34004)
Two related changes:
* ValidateNoRefAccessInRender now allows the mergeRefs pattern, ie a
function that aggregates multiple refs into a new ref. This is the main
case where we have seen false positive no-ref-in-render errors.
* Behind `@enableTreatRefLikeIdentifiersAsRefs`, we infer values passed
as the `ref` prop to some JSX as refs.

The second change is potentially helpful for situations such as

```js
function Component({ref: parentRef}) {
  const childRef = useRef(null);
  const mergedRef = mergeRefs(parentRef, childRef);
  useEffect(() => {
    // generally accesses childRef, not mergedRef
  }, []);
  return <Foo ref={mergedRef} />;
}
```

Ie where you create a merged ref but don't access its `.current`
property. Without inferring `ref` props as refs, we'd fail to allow this
merge refs case.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34004).
* #34027
* #34026
* #34025
* #34024
* #34005
* #34006
* __->__ #34004
2025-07-29 10:06:11 -07:00
Joseph Savona
79dc706498 [compiler] Improve ref validation error message (#34003)
Improves the error message for ValidateNoRefAccessInRender, using the
new diagnostic type as well as providing a longer but succinct summary
of what refs are for and why they're unsafe to access in render.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34003).
* #34027
* #34026
* #34025
* #34024
* #34005
* #34006
* #34004
* __->__ #34003
2025-07-29 10:03:28 -07:00
Joseph Savona
85bbe39ef8 [compiler] Fixes to enableTreatRefLikeIdentifiersAsRefs (#34000)
We added the `@enableTreatRefLikeIdentifiersAsRefs` feature a while back
but never enabled it. Since then we've continued to see examples that
motivate this mode, so here we're fixing it up to prepare to enable by
default. It now works as follows:

* If we find a property load or property store where both a) the
object's name is ref-like (`ref` or `-Ref`) and b) the property is
`current`, we infer the object itself as a ref and the value of the
property as a ref value. Originally the feature only detected property
loads, not stores.
* Inferred refs are not considered stable (this is a change from the
original implementation). The only way to get a stable ref is by calling
`useRef()`. We've seen issues with assuming refs are stable.

With this change, cases like the following now correctly error:

```js
function Foo(props) {
  const fooRef = props.fooRef;
  fooRef.current = true;
  ^^^^^^^^^^^^^^ cannot modify ref in render
}
```

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34000).
* #34027
* #34026
* #34025
* #34024
* #34005
* #34006
* #34004
* #34003
* __->__ #34000
2025-07-29 09:57:48 -07:00
lauren
820af20971 [eslint] Disallow use within try/catch blocks (#34040)
Follow up to #34032. The linter now ensures that `use` cannot be used
within try/catch.
2025-07-29 12:33:42 -04:00
Sebastian Markbåge
9be531cd37 [Fiber] Treat unwrapping React.lazy more like a use() (#34031)
While we want to get rid of React.lazy's special wrapper type and just
use a Promise for the type, we still have the wrapper.

However, this is still conceptually the same as a Usable in that it
should be have the same if you `use(promise)` or render a Promise as a
child or type position.

This PR makes it behave like a `use()` when we unwrap them. We could
move to a model where it actually reaches the internal of the Lazy's
Promise when it unwraps but for now I leave the lazy API signature
intact by just catching the Promise and then "use()" that.

This lets us align on the semantics with `use()` such as the suspense
yield optimization. It also lets us warn or fork based on legacy
throw-a-Promise behavior where as `React.lazy` is not deprecated.
2025-07-29 11:50:12 -04:00
Sebastian "Sebbie" Silbermann
b1cbb482d5 [DevTools] More robust resize handling (#34036) 2025-07-29 17:45:00 +02:00
Sebastian "Sebbie" Silbermann
9c9136b441 [DevTools] Swap Components tab layout based on container size (#34035) 2025-07-29 17:23:35 +02:00
Sebastian "Sebbie" Silbermann
33a2bf78c4 [DevTools] Silence unactionable bundle warnings in shell (#34034) 2025-07-29 11:18:47 +02:00
Sebastian Markbåge
5d7e8b90e2 [DevTools] Use use() instead of throwing a Promise in Caches (#34033) 2025-07-29 03:45:56 -04:00
Sebastian Markbåge
71236c9409 [DevTools] Include the description derived from the promise (#34017)
Stacked on #34016.

This is using the same thing we already do for the performance track to
provide a description of the I/O based on the content of the resolved
Promise. E.g. a Response's URL.

<img width="375" height="388" alt="Screenshot 2025-07-28 at 1 09 49 AM"
src="https://github.com/user-attachments/assets/f3fdc40f-4e21-4e83-b49e-21c7ec975137"
/>
2025-07-28 15:11:04 -04:00
lauren
7ee7571212 [compiler] Enable validateNoVoidUseMemo in eslint & playground (#34022)
Enables `validateNoVoidUseMemo` by default only in eslint (it defaults
to false otherwise) as well as the playground.
2025-07-28 13:42:14 -04:00
lauren
6b22f31f1a [compiler] Aggregate all errors reported from DropManualMemoization (#34002)
Noticed this from my previous PR that this pass was throwing on the
first error. This PR is a small refactor to aggregate every violation
and report them all at once.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34002).
* #34022
* __->__ #34002
2025-07-28 13:25:25 -04:00
lauren
b5c1637109 [compiler] Reuse DropManualMemoization for ValidateNoVoidUseMemo (#34001)
Much of the logic in the new validation pass is already implemented in
DropManualMemoization, so let's combine them. I opted to keep the
environment flag so we can more precisely control the rollout.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34001).
* #34022
* #34002
* __->__ #34001
2025-07-28 12:54:43 -04:00
lauren
c60eebffea [compiler] Add new ValidateNoVoidUseMemo pass (#33990)
Adds a new validation pass to validate against `useMemo`s that don't
return anything. This usually indicates some kind of "useEffect"-like
code that has side effects that need to be memoized to prevent
overfiring, and is an anti-pattern.

A follow up validation could also look at the return value of `useMemo`s
to see if they are being used.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33990).
* #34022
* #34002
* #34001
* __->__ #33990
* #33989
2025-07-28 12:46:42 -04:00
lauren
5dd622eabe [compiler] Disambiguate between void, implicit, and explicit returns (#33989)
Adds a new property to ReturnTerminals to disambiguate whether it was
explicit, implicit (arrow function expressions), or void (where it was
omitted). I will use this property in the next PR adding a new
validation pass.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33989).
* #34022
* #34002
* #34001
* #33990
* __->__ #33989
2025-07-28 12:46:30 -04:00
dan
904989f044 Clean up 19.1.1 changelog (#34023)
See
https://github.com/facebook/react/pull/34021#issuecomment-3128006800.

The purpose of the changelog is to communicate to React users what
changed in the release.

Therefore, it is important that the changelog is written oriented
towards React end users. Historically this means that we omit
internal-only changes, i.e. changes that have no effect on the end user
behavior. If internal changes are mentioned in the changelog (e.g. if
they affect end user behavior), they should be phrased in a way that is
understandable to the end user — in particular, they should not refer to
internal API names or concepts.

We also try to group changes according to the publicly known packages.

In this PR:

- Make #33680 an actual link (otherwise it isn't linkified in
CHANGELOG.md on GitHub).
- Remove two changelog entries listed under "React" that don't affect
anyone who upgrades the "React" package, that are phrased using
terminology and internal function names unfamiliar to React users, and
that seem to be RN-specific changes (so should probably go into the RN
changelog that goes out with the next renderer sync that includes these
changes).
2025-07-28 17:32:23 +01:00
Sebastian "Sebbie" Silbermann
ab2681af03 [DevTools] Skeleton for Suspense tab (#34020) 2025-07-28 18:26:55 +02:00
Sebastian Markbåge
101b20b663 [DevTools] Add a little bar indicating time span of an async entry relative to others (#34016)
Stacked on #34012.

This shows a time track for when some I/O started and when it finished
relative to other I/O in the same component (or later in the same
suspense boundary).

This is not meant to be a precise visualization since the data might be
misleading if you're running this in dev which has other perf
characteristics anyway. It's just meant to be a general way to orient
yourself in the data.

We can also highlight rejected promises here.

The color scheme is the same as Chrome's current Performance Track
colors to add continuity but those could change.

<img width="478" height="480" alt="Screenshot 2025-07-27 at 11 48 03 PM"
src="https://github.com/user-attachments/assets/545dd591-a91f-4c47-be96-41d80f09a94a"
/>
2025-07-28 12:22:33 -04:00
Jack Pope
eaee5308cc Add changelog entry for 19.1.1 (#34021)
Add changelog details matching release notes:
https://github.com/facebook/react/releases/tag/v19.1.1
2025-07-28 12:09:56 -04:00
Sebastian Markbåge
4a58b63865 [DevTools] Add "suspended by" Section to Component Inspector Sidebar (#34012)
This collects the ReactAsyncInfo between instances. It associates it
with the parent. Typically this would be a Server Component's Promise
return value but it can also be Promises in a fragment. It can also be
associated with a client component when you pass a Promise into the
child position e.g. `<div>{promise}</div>` then it's associated with the
div. If an instance is filtered, then it gets associated with the parent
of that's unfiltered.

The stack trace currently isn't source mapped. I'll do that in a follow
up.

We also need to add a "short name" from the Promise for the description
(e.g. url). I'll also add a little marker showing the relative time span
of each entry.

<img width="447" height="591" alt="Screenshot 2025-07-26 at 7 56 00 PM"
src="https://github.com/user-attachments/assets/7c966540-7b1b-4568-8cb9-f25cefd5a918"
/>
<img width="446" height="570" alt="Screenshot 2025-07-26 at 7 55 23 PM"
src="https://github.com/user-attachments/assets/4eac235b-e735-41e8-9c6e-a7633af64e4b"
/>
2025-07-28 12:05:56 -04:00
Hiroshi Ogawa
cc015840ef fix: React.use inside React.lazy-ed component on SSR (#33941) 2025-07-28 10:36:08 +02:00
Sebastian "Sebbie" Silbermann
19baee813c [Runtime] Fix CI (#33999) 2025-07-25 21:04:35 +02:00
Joseph Savona
2aa5f9d4e3 [compiler] fix false positive "mutate frozen" validation with refs (#33993)
The test case here previously reported a "Cannot modify local variables
after render completes" error (from
ValidateNoFreezingKnownMutableFunctions). This happens because one of
the functions passed to a hook clearly mutates a ref — except that we
try to ignore mutations of refs! The problem in this case is that the
`const ref = ...` was getting converted to a context variable since the
ref is accessed in a function before its declaration. We don't infer
types for context variables at all, and our ref handling is based on
types, so we failed to ignore this ref mutation.

The fix is to recognize that `StoreLocal const ...` is a special case:
the variable may be referenced in code before the declaration, but at
runtime it's either a TDZ error or the variable will have the type from
the declaration. So we can safely infer a type.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33993).
* __->__ #33993
* #33991
* #33984
2025-07-25 10:08:09 -07:00
Joseph Savona
8c587a2a41 [compiler] clarify text for setState-in-effect error (#33991)
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33991).
* #33993
* __->__ #33991
* #33984
2025-07-25 10:07:55 -07:00
Joseph Savona
12483a119b [compiler] Fix for edge cases of mutation of potentially frozen values (#33984)
Fixes two related cases of mutation of potentially frozen values.

The first is method calls on frozen values. Previously, we modeled
unknown function calls as potentially aliasing their receiver+args into
the return value. If the receiver or argument were known to be frozen,
then we would downgrade the `Alias` effect into an `ImmutableCapture`.
However, within a function expression it's possible to call a function
using a frozen value as an argument (that gets `Alias`-ed into the
return) but where we don't have the context locally to know that the
value is frozen.

This results in cases like this:

```js
const frozen = useContext(...);
useEffect(() => {
  frozen.method().property = true;
  ^^^^^^^^^^^^^^^^^^^^^^^^ cannot mutate frozen value
}, [...]);
```

Within the function we would infer:

```
t0 = MethodCall ...
  Create t0 = mutable
  Alias t0 <- frozen
t1 = PropertyStore ...
  Mutate t0
```

And then transitively infer the function expression as having a `Mutate
'frozen'` effect, which when evaluated against the outer context
(`frozen` is frozen) is an error.

The fix is to model unknown function calls as _maybe_ aliasing their
receiver/args in the return, and then considering mutations of a
maybe-aliased value to only be a conditional mutation of the source:


```
t0 = MethodCall ...
  Create t0 = mutable
  MaybeAlias t0 <- frozen // maybe alias now
t1 = PropertyStore ...
  Mutate t0
```

Then, the `Mutate t0` turns into a `MutateConditional 'frozen'`, which
just gets ignored when we process the outer context.

The second, related fix is for known mutation of phis that may be a
frozen value. The previous inference model correctly recorded these as
errors, the new model does not. We now correctly report a validation
error for this case in the new model.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33984).
* #33993
* #33991
* __->__ #33984
2025-07-25 10:07:24 -07:00
Sebastian Markbåge
b2c30493ce [DevTools] Use the hard coded url instead of the local storage url for presets (and make VSCode default) (#33995)
Stacked on #33983.

Previously, the source of truth is the url stored in local storage but
that means if we change the presets then they don't take effect (e.g.
#33994). This PR uses the hardcoded value instead when a preset is
selected.

This also has the benefit that if you switch between custom and vs code
in the selector, then the custom url is preserved instead of getting
reset when you checkout other options.

Currently the default is custom with empty string, which means that
there's no code editor configured at all by default. It doesn't make a
lot of sense that we have it not working by default when so many people
use VS Code. So this also makes VS Code the default if there's no
EDITOR_URL env specified.
2025-07-25 10:27:27 -04:00
Sebastian Markbåge
36c2bf5c3e [DevTools] Allow all file links in Chrome DevTools to open in external editor (#33985)
Stacked on #33983.

Allow React to be configured as the default handler of all links in
Chrome DevTools. To do this you need to configure the Chrome DevTools
setting for "Link Handling:" to be set to "React Developer Tools". By
default this doesn't do anything but if you then check the box added in
#33983 it starts open local files directly in the external editor.

This needs docs to show how to enable that option.

(As far as I can tell this broke in Chrome Canary 🙄 but hopefully fixed
before stable.)
2025-07-25 10:27:09 -04:00
Sebastian Markbåge
190758e623 [DevTools] Add column to vscode editor preset (#33994)
We should jump to the right column.

Unfortunately, the way presets are set up now you have to switch off and
switch to the preset for this to take effect.
2025-07-25 10:21:00 -04:00
Sebastian Markbåge
b1a6f03f8a [DevTools] Rerender when the browser theme changes (#33992)
When the browser theme changes, we don't immediately rerender the UI so
we don't pick up the new theme if the React devtools are set to auto.

This picks up the change immediately.
2025-07-25 10:19:09 -04:00
Sebastian Markbåge
142fd27bf6 [DevTools] Add Option to Open Local Files directly in External Editor (#33983)
The `useOpenResource` hook is now used to open links. Currently, the
`<>` icon for the component stacks and the link in the bottom of the
components stack. But it'll also be used for many new links like stacks.
If this new option is configured, and this is a local file then this is
opened directly in the external editor. Otherwise it fallbacks to open
in the Sources tab or whatever the standalone or inline is configured to
use.

<img width="453" height="252" alt="Screenshot 2025-07-24 at 4 09 09 PM"
src="https://github.com/user-attachments/assets/04cae170-dd30-4485-a9ee-e8fe1612978e"
/>

I prominently surface this option in the Source pane to make it
discoverable.

<img width="588" height="144" alt="Screenshot 2025-07-24 at 4 03 48 PM"
src="https://github.com/user-attachments/assets/0f3a7da9-2fae-4b5b-90ec-769c5a9c5361"
/>

When this is configured, the "Open in Editor" is hidden since that's
just the default. I plan on deprecating this button to avoid having the
two buttons going forward.

Notably there's one exception where this doesn't work. When you click an
Action or Event listener it takes you to the Sources tab and you have to
open in editor from there. That's because we use the `inspect()`
mechanism instead of extracting the source location. That's because we
can't do the "throw trick" since these can have side-effects. The Chrome
debugger protocol would solve this but it pops up an annoying dialog. We
could maybe only attach the debugger only for that case. Especially if
the dialog disappears before you focus on the browser again.
2025-07-25 10:16:43 -04:00
Sebastian "Sebbie" Silbermann
7ca2d4cd2e Work around Chrome DevTools crash on performance.measure (#33997) 2025-07-25 12:32:30 +02:00
Sebastian Markbåge
99be14c883 [Flight] Promote enableAsyncDebugInfo to stable without enableComponentPerformanceTrack (#33996)
There's a lot of overlap between `enableComponentPerformanceTrack` and
`enableAsyncDebugInfo` because they both rely on timing information. The
former is mainly emit timestamps for how long server components and
awaits took. The latter how long I/O took.

`enableAsyncDebugInfo` is currently primarily for the component
performance track but its meta data is useful for other debug tools too.
This promotes that flag to stable.

However, `enableComponentPerformanceTrack` needs more work due to
performance concerns with Chrome DevTools so I need to separate them.
This keeps doing most of the timing tracking on the server but doesn't
emit the per-server component time stamps when
`enableComponentPerformanceTrack` is false.
2025-07-25 04:59:46 -04:00
Josh Story
5a04619f60 [Flight] Properly close stream when no chunks need to be written after prerender (#33982)
There is an edge case when prerendering where if you have nothing to
write you can end up in a state where the prerender is in status closed
before you can provide a destination. In this case the destination is
never closed becuase it assumes it already would have been.

This condition can happen now because of the introduction of the deubg
stream. Before this a request would never entere closed status if there
was no active destination. When a destination was added it would perform
a flush and possibly close the stream. Now, it is possible to flush
without a destination because you might have debug chunks to stream and
you can end up closing the stream independent of an active destination.

There are a number of ways we can solve this but the one that seems to
adhere best to the original design is to only set the status to CLOSED
when a destination is active. This means that if you don't have an
active destination when the pendingChunks count hits zero it will not
enter CLOSED status until you startFlowing.
2025-07-24 19:38:31 -07:00
Joseph Savona
129aa85e16 [compiler] Use diagnostic for "found suppression" error (#33981) 2025-07-24 15:54:24 -07:00
Joseph Savona
bcea86945c [compiler][rfc] Enable more validations in playground. (#33777)
This is mostly to kick off conversation, i think we should go with a
modified version of the implemented approach that i'll describe here.

The playground currently serves two roles. The primary one we think
about is for verifying compiler output. We use it for this sometimes,
and developers frequently use it for this, including to send us repros
if they have a potential bug. The second mode is to help developers
learn about React. Part of that includes learning how to use React
correctly — where it's helpful to see feedback about problematic code —
and also to understand what kind of tools we provide compared to other
frameworks, to make an informed choice about what tools they want to
use.

Currently we primarily think about the first role, but I think we should
emphasize the second more. In this PR i'm doing the worst of both:
enabling all the validations used by both the compiler and the linter by
default. This means that code that would actually compile can fail with
validations, which isn't great.

What I think we should actually do is compile twice, one in
"compilation" mode and once in "linter" mode, and combine the results as
follows:
* If "compilation" mode succeeds, show the compiled output _and_ any
linter errors.
* If "compilation" mode fails, show only the compilation mode failures.

We should also distinguish which case it is when we show errors:
"Compilation succeeded", "Compilation succeeded with linter errors",
"Compilation failed".

This lets developers continue to verify compiler output, while also
turning the playground into a much more useful tool for learning React.
Thoughts?

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33777).
* #33981
* __->__ #33777
2025-07-24 15:52:45 -07:00
Joseph Savona
2ae8b3dacf [compiler] Use new diagnostic printing in playground (#33767)
Per title

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33767).
* #33981
* #33777
* __->__ #33767
2025-07-24 15:47:56 -07:00
Joseph Savona
7f510554ad [compiler] Cleanup diagnostic messages (#33765)
Minor sytlistic cleanup

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33765).
* #33981
* #33777
* #33767
* __->__ #33765
2025-07-24 15:45:17 -07:00
Joseph Savona
a39da6c61f [compiler] Use new diagnostics for core inference errors (#33760)
Uses the new diagnostic type for errors created during mutation/aliasing
inference, such as errors for mutating immutable values like props or
state, reassigning globals, etc.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33760).
* #33981
* #33777
* #33767
* #33765
* __->__ #33760
2025-07-24 15:43:08 -07:00
Joseph Savona
48bc166428 [compiler] Update diagnostics for ValidatePreservedManualMemoization (#33759)
Uses the new diagnostic infrastructure for this validation, which lets
us provide a more targeted message on the text that we highlight (eg
"This dependency may be mutated later") separately from the overall
error message.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33759).
* #33981
* #33777
* #33767
* #33765
* #33760
* __->__ #33759
* #33758
2025-07-24 15:39:53 -07:00
Joseph Savona
72848027a5 [compiler] Improve more error messages (#33758)
This PR uses the new diagnostic type for most of the error messages
produced in our explicit validation passes (`Validation/` directory).
One of the validations produced multiple errors as a hack to showing
multiple related locations, which we can now consolidate into a single
diagnostic.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33758).
* #33981
* #33777
* #33767
* #33765
* #33760
* #33759
* __->__ #33758
2025-07-24 15:39:42 -07:00
Joseph Savona
707e321f8f [compiler][wip] Improve diagnostic infra (#33751)
Work in progress, i'm experimenting with revamping our diagnostic infra.
Starting with a better format for representing errors, with an ability
to point ot multiple locations, along with better printing of errors. Of
course, Babel still controls the printing in the majority case so this
still needs more work.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33751).
* #33981
* #33777
* #33767
* #33765
* #33760
* #33759
* #33758
* __->__ #33751
* #33752
* #33753
2025-07-24 15:37:06 -07:00
Joseph Savona
0d39496eab [compiler] Enable additional lints by default (#33752)
Enable more validations to help catch bad patterns, but only in the
linter. These rules are already enabled by default in the compiler _if_
violations could produce unsafe output.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33752).
* #33981
* #33777
* #33767
* #33765
* #33760
* #33759
* #33758
* #33751
* __->__ #33752
* #33753
2025-07-24 15:36:54 -07:00
Joseph Savona
6f4294af9b [compiler] Validate against setState in all effect types (#33753)
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33753).
* #33981
* #33777
* #33767
* #33765
* #33760
* #33759
* #33758
* #33751
* #33752
* __->__ #33753
2025-07-24 15:36:40 -07:00
Joseph Savona
448f781a52 [compiler] Fix for false positive mutation of destructured spread object (#33786)
When destructuring, spread creates a new mutable object that _captures_
part of the original rvalue. This new value is safe to modify.

When making this change I realized that we weren't inferring array
pattern spread as creating an array (in type inference) so I also added
that here.
2025-07-24 15:16:28 -07:00
Sebastian Markbåge
5020d48d28 [DevTools] Feature detect createSidebarPane (#33988)
Same as #33987 but for the sidebar pane creation.
2025-07-24 17:42:06 -04:00
Sebastian Markbåge
3082604bdc [DevTools] Feature detect sources panel (#33987)
I broke Firefox DevTools extension in #33968.

It turns out the Firefox has a placeholder object for the sources panel
which is empty. We need to detect the actual event handler.
2025-07-24 17:38:12 -04:00
Sebastian Markbåge
4f34cc4a2e [Fiber] Don't throw away the Error object retaining the owner stack (#33976)
We currently throw away the Error once we've used to the owner stack of
a Fiber once. This maybe helps a bit with memory and redoing it but we
really don't expect most Fibers to hit this at all. It's not very hot.

If we throw away the Error, then we can't use native debugger protocols
to inspect the native stack. Instead, we'd have to maintain a url to
resource map indefinitely like what Chrome DevTools does to map a url to
a resource. Technically it's not even technically correct since the file
path might not be reversible and could in theory conflict.
2025-07-24 13:33:03 -04:00
Sebastian Markbåge
3d14fcf03f [Flight] Use about: protocol instead of rsc: protocol for fake evals (#33977)
Chrome DevTools Extensions has a silly problem where they block access
to load Resources from all protocols except [an allow
list](eb970fbc64/front_end/models/extensions/ExtensionServer.ts (L60)).

https://issues.chromium.org/issues/416196401

Even though these are `eval()` and not actually loaded from the network
they're blocked. They can really be any string. We just have to pick one
of:

```js
'http:', 'https:', 'file:', 'data:', 'chrome-extension:', 'about:'
```

That way React DevTools extensions can load this content to source map
them.

Webpack has the same issue with its `webpack://` and
`webpack-internal://` urls.
2025-07-24 11:07:11 -04:00
Sebastian Markbåge
edac0dded9 [DevTools] Add a Code Editor Sidebar Pane in the Chrome Sources Tab (#33968)
This adds a "Code Editor" pane for the Chrome extension in the bottom
right corner of the "Sources" panel. If you end up getting linked to the
"Sources" panel from stack traces in console, performance tab, stacks in
React Component tab like the one added in #33954 basically everywhere
there's a link to source code. Then going from there to open in a code
editor should be more convenient. This adds a button to open the current
file.

<img width="1387" height="389" alt="Screenshot 2025-07-22 at 10 22
19 PM"
src="https://github.com/user-attachments/assets/fe01f84c-83c2-4639-9b64-4af1a90c3f7d"
/>

This only makes sense in the extensions since in standalone it needs to
always open by default in an editor. Unfortunately Firefox doesn't
support extending the Sources panel.

Chrome is also a bit buggy where it doesn't send a selection update
event when you switch tabs in the Sources panel. Only when the actual
cursor position changes. This means that the link can be lagging behind
sometimes. We also have some general bugs where if React DevTools loses
connection it can break the UI which includes this pane too.

This has a small inline configuration too so that it's discoverable:

<img width="559" height="143" alt="Screenshot 2025-07-22 at 10 22 42 PM"
src="https://github.com/user-attachments/assets/1270bda8-ce10-4f9d-9fcb-080c0198366a"
/>

<img width="527" height="123" alt="Screenshot 2025-07-22 at 10 22 30 PM"
src="https://github.com/user-attachments/assets/45848c95-afd8-495f-a7cf-eb2f46e698f2"
/>

Since we can't add a separate link to open-in-editor or open-in-sources
everywhere I plan on adding an option to open in editor by default in a
follow up. That option needs to be even more discoverable.

I moved the configuration from the Components settings to the General
settings since this is now a much more general features for opening
links to resources in all types of panes.

<img width="673" height="311" alt="Screenshot 2025-07-22 at 10 22 57 PM"
src="https://github.com/user-attachments/assets/ea2c0871-942c-4b55-a362-025835d2c2bd"
/>
2025-07-23 10:28:11 -04:00
Sebastian Markbåge
3586a7f9e8 [DevTools] Allow file:/// urls to be opened in editor (#33965)
If a `file:///` path is specified as the url of a file, like after
source mapping into an ESM file, then we should be able to open it in a
code editor.
2025-07-23 10:21:50 -04:00
Sebastian "Sebbie" Silbermann
f6fb1a07a5 [Flight] Remove superfluous whitespace when console method is called with non-strings (#33953) 2025-07-23 10:07:37 +02:00
Sebastian Markbåge
7513996f20 [DevTools] Unify by using ReactFunctionLocation type instead of Source (#33955)
In RSC and other stacks now we use a lot of `ReactFunctionLocation` type
to represent the location of a function. I.e. the location of the
beginning of the function (the enclosing line/col) that is represented
by the "Source" of the function. This is also what the parent Component
Stacks represents.

As opposed to `ReactCallSite` which is what normal stack traces and
owner stacks represent. I.e. the line/column number of the callsite into
the next function.

We can start sharing more code by using the `ReactFunctionLocation` type
to represent the component source location and it also helps clarify
which ones are function locations and which ones are callsites as we
start adding more stack traces (e.g. for async debug info and owner
stack traces).
2025-07-22 10:53:08 -04:00
Sebastian Markbåge
bb4418d647 [DevTools] Linkify Source View (#33954)
This makes it so you can click the source location itself to view the
source. This is similar styling as the link to jump to function props
like events and actions. We're going to need a lot more linkifying to
jump to various source locations. Also, I always was trying to click
this file anyway.

Hover state:

<img width="485" height="382" alt="Screenshot 2025-07-21 at 4 36 10 PM"
src="https://github.com/user-attachments/assets/1f0f8f8c-6866-4e62-ab84-1fb5ba012986"
/>
2025-07-21 17:36:37 -04:00
Jordan Brown
074e92777c Change autodeps configuration (#33800) 2025-07-21 13:04:02 -07:00
Sebastian "Sebbie" Silbermann
ac7da9d46d [Flight] Make it more obvious what the short name in the I/O description represents (#33944) 2025-07-21 19:53:58 +02:00
Sebastian Markbåge
0dca9c2471 [Flight] Use the Promise of the first await even if that is cut off (#33948)
We need a "value" to represent the I/O that was loaded. We don't
normally actually use the Promise at the callsite that started the I/O
because that's usually deep inside internals. Instead we override the
value of the I/O entry with the Promise that was first awaited in user
space. This means that you could potentially have different values
depending on if multiple things await the same I/O. We just take one of
them. (Maybe we should actually just write the first user space awaited
Promise as the I/O entry? This might instead have other implications
like less deduping.)

When you pass a Promise forward, we may skip the awaits that happened in
earlier components because they're not part of the currently rendering
component. That's mainly for the stack and time stamps though. The value
is still probably conceptually the best value because it represents the
I/O value as far user space is concerned.

This writes the I/O early with the first await we find in user space
even if we're not going to use that particular await for the stack.
2025-07-21 13:22:10 -04:00
Sebastian Markbåge
b9af1404ea [Flight] Use the JSX as the await stack if an await is not available (#33947)
If you pass a promise to a client component to be rendered `<Client
promise={promise} />` then there's an internal await inside Flight.
There might also be user space awaits but those awaits may already have
happened before we render this component. Conceptually they were part of
the parent component and not this component. It's tricky to attribute
which await should be used for the stack in this case.

If we can't find an await we can use the JSX callsite as the stack
frame.

However, we don't want to do this for simple cases like if you return a
non-native Promise from a Server Component. Since that would now use the
stack of the thing that rendered the Server Component which is worse
than the stack of the I/O. To fix this, I update the
`debugOwner`/`debugTask`/`debugStack` when we start rendering inside the
Server Component. Conceptually these represent the "parent" component
and is used for errors referring to the parent like when we serialize
client component props the parent is the JSX of the client component.
However, when we're directly inside the Server Component we don't have a
callsite of the parent really. Conceptually it would be the return call
of the Server Component. This might negatively affect other types of
errors but I think this is ok since this feature mainly exists for the
case when you enter the child JSX.
2025-07-21 13:21:17 -04:00
Rubén Norte
e9638c33d7 Clean up feature flag to use lazy public instances in Fabric (#33943)
## Summary

We have thoroughly tested this flag in production and proved stability
and performance, so we can clean it up and "ship it".
2025-07-21 10:27:46 +01:00
Sebastian Markbåge
28d4bc496b [Flight] Make debug info and console log resolve in predictable order (#33665)
This resolves an outstanding issue where it was possible for debug info
and console logs to become out of order if they up blocked. E.g. by a
future reference or a client reference that hasn't loaded yet. Such as
if you console.log a client reference followed by one that doesn't. This
encodes the order similar to how the stream chunks work.

This also blocks the main chunk from resolving until the last debug info
has fully loaded, including future references and client references.
This also ensures that we could send some of that data in a different
stream, since then it can come out of order.
2025-07-19 20:13:26 -04:00
Jordan Brown
dffacc7b80 InferEffectDeps takes a React.AUTODEPS sigil (#33799)
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33799).
* #33800
* __->__ #33799
2025-07-17 05:31:52 -07:00
Sebastian Markbåge
da7487b681 [Flight] Skip the stack frame of built-in wrappers that create or await Promises (#33798)
We already do this with `"new Promise"` and `"Promise.then"`. There are
also many helpers that both create promises and awaits other promises
inside of it like `Promise.all`.

The way this is filtered is different from just filtering out all
anonymous stacks since they're used to determine where the boundary is
between ignore listed and user space.

Ideally we'd cover more wrappers that are internal to Promise libraries.
2025-07-16 15:57:22 -04:00
Ruslan Lesiutin
9fec565a9b fix: log renders from passive effects for only newly finished work (#33797)
This fixes displaying incorrect component render entries on a timeline,
when we are reconnecting passive effects.

### Before
<img width="2318" height="1127" alt="1"
src="https://github.com/user-attachments/assets/9b6b2824-d2de-43a3-8615-2c45d67c3668"
/>

The cloned nodes will persist original `actualStartTime`, when these
were first mounted. When we "replay", the end time will be "now" or
whatever the actual start time of the sibling. Depending on when this is
being recorded, the diff between end and start could be tens of seconds
and doesn't represent what React was doing.

We shouldn't log these entries at all.

### After
We are only logging newly finished renders, but could potentially loose
renders that never commit.
2025-07-16 18:09:35 +01:00
Jack Pope
996d0eb055 Allow runtime_build_and_test action to trigger manually (#33796) 2025-07-16 12:41:35 -04:00
Sebastian "Sebbie" Silbermann
d85ec5f5bd [Flight] Assume __turbopack_load_by_url__ returns a cached Promise (#33792) 2025-07-16 13:20:10 +02:00
Henry Q. Dineen
fe813143e2 [compiler] Check TSAsExpression and TSNonNullExpression reorderability (#33788)
## Summary

The `TSAsExpression` and `TSNonNullExpression` nodes are supported by
`lowerExpression()` but `isReorderableExpression()` does not check if
they can be reordered. This PR updates `isReorderableExpression()` to
handle these two node types by adding cases that fall through to the
existing `TypeCastExpression` case.

We ran `react-compiler-healthcheck` at scale on several of our repos and
found dozens of `` (BuildHIR::node.lowerReorderableExpression)
Expression type `TSAsExpression` cannot be safely reordered`` errors and
a handful for `TSNonNullExpression`.


## How did you test this change?

In this case I added two fixture tests
2025-07-15 11:50:20 -07:00
Sebastian Markbåge
2f0e7e570d [Flight] Don't block on debug channel if it's not wired up (#33757)
React Elements reference debug data (their stack and owner) in the debug
channel. If the debug channel isn't wired up this can block the client
from resolving.

We can infer that if there's no debug channel wired up and the reference
wasn't emitted before the element, then it's probably because it's in
the debug channel. So we can skip it.

This should also apply to debug chunks but they're not yet blocking
until #33665 lands.
2025-07-15 11:45:34 -04:00
Sebastian "Sebbie" Silbermann
56d0ddae18 [Flight] Switch to __turbopack_load_by_url__ (#33791) 2025-07-15 16:55:31 +02:00
Sebastian "Sebbie" Silbermann
345ca24f13 [Flight] Remove unused fork configs (#33785) 2025-07-15 07:23:00 +02:00
Jordan Brown
97cdd5d3c3 [eslint] Do not allow useEffectEvent fns to be called in arbitrary closures (#33544)
Summary:

useEffectEvent is meant to be used specifically in combination with
useEffect, and using
the feature in arbitrary closures can lead to surprising reactivity
semantics. In order to
minimize risk in the experimental rollout, we are going to restrict its
usage to being
called directly inside an effect or another useEffectEvent, effectively
enforcing the function
coloring statically. Without an effect system this is the best we can
do.
2025-07-10 16:51:12 -04:00
Sebastian Markbåge
eb7f8b42c9 [Flight] Add Separate Outgoing Debug Channel (#33754)
This lets us pass a writable on the server side and readable on the
client side to send debug info through a separate channel so that it
doesn't interfere with the main payload as much. The main payload refers
to chunks defined in the debug info which means it's still blocked on it
though. This ensures that the debug data has loaded by the time the
value is rendered so that the next step can forward the data.

This will be a bit fragile to race conditions until #33665 lands.
Another follow up needed is the ability to skip the debug channel on the
receiving side. Right now it'll block forever if you don't provide one
since we're blocking on the debug data.
2025-07-10 16:22:44 -04:00
Sebastian Markbåge
eed2560762 [Flight] Treat empty message as a close signal (#33756)
We typically treat an empty message as closing the debug channel stream
but for the Noop renderer we don't use an intermediate stream but just
pass the message through.


bbc13fa17b/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientBrowser.js (L59-L60)

For that simple case we should just treat it as a close without an
intermediate stream.
2025-07-10 16:16:57 -04:00
Josh Story
463b808176 [Fizz] Reset the segent id assignment when postponing the root (#33755)
When postponing the root we encode the segment Id into the postponed
state but we should really be reseting it to zero so we can restart the
counter from the beginning when the resume is actually just a re-render.

This also no longer assigns the root segment id based on the postponed
state when resuming the root for the same reason. In the future we may
use the embedded replay segment id if we implement resuming the root
without re-rendering everything but that is not yet implemented or
planned.
2025-07-10 12:12:09 -07:00
Joseph Savona
96c61b7f1f [compiler] Add CompilerError.UnsupportedJS variant (#33750)
We use this variant for syntax we intentionally don't support: with
statements, eval, and inline class declarations.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33750).
* #33753
* #33752
* #33751
* __->__ #33750
* #33748
2025-07-09 22:24:20 -07:00
Joseph Savona
0bfa404bac [compiler] More precise errors for invalid import/export/namespace statements (#33748)
import, export, and TS namespace statements can only be used at the
top-level of a module, which is enforced by parsers already. Here we add
a backup validation of that. As of this PR, we now have only major
statement type (class declarations) listed as a todo.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33748).
* #33753
* #33752
* #33751
* #33750
* __->__ #33748
2025-07-09 22:24:07 -07:00
Joseph Savona
81e1ee7476 [compiler] Support inline enums (flow/ts), type declarations (#33747)
Supports inline enum declarations in both Flow and TS by treating the
node as pass-through (enums can't capture values mutably). Related, this
PR extends the set of type-related declarations that we ignore.
Previously we threw a todo for things like DeclareClass or
DeclareVariable, but these are type related and can simply be dropped
just like we dropped TypeAlias.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33747).
* #33753
* #33752
* #33751
* #33750
* #33748
* __->__ #33747
2025-07-09 22:21:02 -07:00
Joseph Savona
4a3ff8eed6 [compiler] Errors for eval(), with statments, class declarations (#33746)
* Error for `eval()`
* More specific error message for `with (expr) { ... }` syntax
* More specific error message for class declarations

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33746).
* #33752
* #33751
* #33750
* #33748
* #33747
* __->__ #33746
2025-07-09 22:18:30 -07:00
Joseph Savona
ec4374c387 [compiler] Show logged errors in playground (#33740)
In playground it's helpful to show all errors, even those that don't
completely abort compilation. For example, to help demonstrate that the
compiler catches things like setState in effects. This detects these
errors and ensures we show them.
2025-07-09 09:22:49 -07:00
Sebastian Markbåge
60b5271a9a [Flight] Call finishHaltedTask on sync aborted tasks in stream abort listeners (#33743)
This is the same as we do for currently rendering tasks. They get
effectively sync aborted when the listener is invoked.

We potentially miss out on some debug info in that case but that would
only apply to any entries inside the stream which doesn't really have
their own debug info anyway.
2025-07-09 10:43:56 -04:00
Sebastian Markbåge
033edca721 [Flight] Yolo Retention of Promises (#33737)
Follow up to #33736.

If we need to save on CPU/memory pressure, we can instead just pray and
hope that a Promise doesn't get garbage collected before we need to read
it.

This can cause fragile access to the Promise value in devtools
especially if it's a slow and pressured render.

Basically, you'd have to hope that GC doesn't run after the inner await
finishes its microtask callback and before the resolution of the
component being rendered is invoked.
2025-07-09 10:39:08 -04:00
Sebastian Markbåge
e6dc25daea [Flight] Always defer Promise values if they're not already resolved (#33742)
If we have the ability to lazy load Promise values, i.e. if we have a
debug channel, then we should always use it for Promises that aren't
already resolved and instrumented.

There's little downside to this since they're async anyway.

This also lets us avoid adding `.then()` listeners too early. E.g. if
adding the listener would have side-effect. This avoids covering up
"unhandled rejection" errors. Since if we listen to a promise eagerly,
including reject listeners, we'd have marked that Promise's rejection as
handled where as maybe it wouldn't have been otherwise.

In this mode we can also indefinitely wait for the Promise to resolve
instead of just waiting a microtask for it to resolve.
2025-07-09 09:08:27 -04:00
Sebastian Markbåge
150f022444 [Flight] Ignore async stack frames when determining if a Promise was created from user space (#33739)
We use the stack of a Promise as the start of the I/O instead of the
actual I/O since that can symbolize the start of the operation even if
the actual I/O is batched, deduped or pooled. It can also group multiple
I/O operations into one.

We want the deepest possible Promise since otherwise it would just be
the Component's Promise.

However, we don't really need deeper than the boundary between first
party and third party. We can't just take the outer most that has third
party things on the stack though because third party can have callbacks
into first party and then we want the inner one. So we take the inner
most Promise that depends on I/O that has a first party stack on it.

The realization is that for the purposes of determining whether we have
a first party stack we need to ignore async stack frames. They can
appear on the stack when we resume third party code inside a resumption
frame of a first party stack.

<img width="832" alt="Screenshot 2025-07-08 at 6 34 25 PM"
src="https://github.com/user-attachments/assets/1636f980-be4c-4340-ad49-8d2b31953436"
/>

---------

Co-authored-by: Sebastian Sebbie Silbermann <sebastian.silbermann@vercel.com>
2025-07-09 09:08:09 -04:00
Sebastian Markbåge
49ded1d12a [Flight] Optimize Retention of Weak Promises Abit (#33736)
We don't really need to retain a reference to whatever Promise another
Promise was created in. Only awaits need to retain both their trigger
and their previous context.
2025-07-09 09:07:06 -04:00
Sebastian Markbåge
3a43e72d66 [Flight] Create a fast path parseStackTrace which skips generating a string stack (#33735)
When we know that the object that we pass in is immediately parsed, then
we know it couldn't have been reified into a unstructured stack yet. In
this path we assume that we'll trigger `Error.prepareStackTrace`.

Since we know that nobody else will read the stack after us, we can skip
generating a string stack and just return empty. We can also skip
caching.
2025-07-09 09:06:55 -04:00
Sebastian Markbåge
8ba3501cd9 [Flight] Don't dedupe references to deferred objects (#33741)
If we're about to defer an object, then we shouldn't store a reference
to it because then we can end up deduping by referring to the deferred
string. If in a different context, we should still be able to emit the
object.
2025-07-08 21:47:33 -04:00
Joseph Savona
956d770adf [compiler] Improve IIFE inlining (#33726)
We currently inline IIFEs by creating a temporary and a labeled block w
the original code. The original return statements turn into an
assignment to the temporary and break out of the label. However, many
cases of IIFEs are due to inlining of manual `useMemo()`, and these
cases often have only a single return statement. Here, the output is
cleaner if we avoid the temporary and label - so that's what we do in
this PR.

Note that the most complex part of the change is actually around
ValidatePreserveExistingMemo - we have some logic to track the IIFE
temporary reassignmetns which needs to be updated to handle the simpler
version of inlining.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33726).
* __->__ #33726
* #33725
2025-07-08 16:36:57 -07:00
Joseph Savona
d35fef9e21 [compiler] Fix for consecutive DCE'd branches with phis (#33725)
This is an optimized version of @asmjmp0's fix in
https://github.com/facebook/react/pull/31940. When we merge consecutive
blocks we need to take care to rewrite later phis whose operands will
now be different blocks due to merging. Rather than iterate all the
blocks on each merge as in #31940, we can do a single iteration over all
the phis at the end to fix them up.

Note: this is a redo of #31959

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33725).
* #33726
* __->__ #33725
2025-07-08 16:36:47 -07:00
Sebastian Markbåge
a7a116577d [Flight] Don't track Promise stack if there's no owner (#33734)
This is a compromise because there can be a lot of Promise instances
created. They're useful because they generally provide a better stack
when batching/pooled connections are used.

This restores stack collection for I/O nodes so we have something to
fallback on if there's no owner.

That way we can at least get a name or something out of I/O that was
spawned outside a render but mostly avoids collecting starting I/O
outside of render.
2025-07-08 13:02:29 -04:00
Sebastian Markbåge
777264b4ef [Flight] Fix stack getting object limited (#33733)
Because the object limit is unfortunately depth first due to limitations
of JSON stringify, we need to ensure that things we really don't want
outlined are first in the enumeration order.

We add the stack length to the object limit to ensure that the stack
frames aren't outlined. In console all the user space arguments are at
the end of the args. In server component props, the props are at the end
of the properties of the element.

For the `value` of I/O we had it before the stack so it could steal the
limit from the stack. The fix is to put it at the end.
2025-07-08 12:54:29 -04:00
Josh Story
befc1246b0 [Fizz] Render preamble eagerly (#33730)
We unnecessarily render the preamble in a task. This updates the
implementation to perform this render inline.

Testing this is tricky because one of the only ways you could assert
this was even happening is based on how things error if you abort while
rendering the root.

While adding a test for this I discovered that not all abortable tasks
report errors when aborted during a normal render. I've asserted the
current behavior and will address the other issue at another time and
updated the assertion later as necessary
2025-07-08 08:20:12 -07:00
Sebastian Markbåge
bbea677b77 [Flight] Lazy load objects from the debug channel (#33728)
When a debug channel is available, we now allow objects to be lazily
requested though the debug channel and only then will the server send
it.

The client will actually eagerly ask for the next level of objects once
it parses its payload. That way those objects have likely loaded by the
time you actually expand that deep e.g. in the console repl. This is
needed since the console repl is synchronous when you ask it to invoke
getters.

Each level is lazily parsed which means that we don't parse the next
level even though we eagerly loaded it. We parse it once the getter is
invoked (in Chrome DevTools you have to click a little `(...)` to invoke
the getter). When the getter is invoked, the chunk is initialized and
parsed. This then causes the next level to be asked for through the
debug channel. Ensuring that if you expand one more level you can do so
synchronously.

Currently debug chunks are eagerly parsed, which means that if you have
things like server component props that are lazy they can end up being
immediately asked for, but I'm trying to move to make the debug chunks
lazy.
2025-07-08 10:49:25 -04:00
Sebastian Markbåge
f1ecf82bfb [Flight] Optimize Async Stack Collection (#33727)
We need to optimize the collection of debug info for dev mode. This is
an incredibly hot path since it instruments all I/O and Promises in the
app.

These optimizations focus primarily on the collection of stack traces.
They are expensive to collect because we need to eagerly collect the
stacks since they can otherwise cause memory leaks. We also need to do
some of the processing of them up front. We also end up only using a few
of them in the end but we don't know which ones we'll use.

The first compromise here is that I now only collect the stacks of
"awaits" if they were in a specific request's render. In some cases it's
useful to collect them even outside of this if they're part of a
sequence that started early. I still collect stacks for the created
Promises outside of this though which can still provide some context.

The other optimization to awaits, is that since we'll only use the inner
most one that had an await directly in userspace, we can stop collecting
stacks on a chain of awaits after we find one. This requires a quick
filter on a single callsite to determine. Since we now only collect
stacks from awaits that belongs to a specific Request we can use that
request's specific filter option. Technically this might not be quite
correct if that same thing ends up deduped across Requests but that's an
edge case.

Additionally, I now stop collecting stack for I/O nodes. They're almost
always superseded by the Promise that wraps them anyway. Even if you
write mostly Promise free code, you'll likely end up with a Promise at
the root of the component eventually anyway and then you end up using
its stack anyway. You have to really contort the code to end up with
zero Promises at which point it's not very useful anyway. At best it's
maybe mostly useful for giving a name to the I/O when the rest is just
stuff like `new Promise`.

However, a possible alternative optimization could be to *only* collect
the stack of spawned I/O and not the stack of Promises. The issue with
Promises (not awaits) is that we never know what will end up resolving
them in the end when they're created so we have to always eagerly
collect stacks. This could be an issue when you have a lot of
abstractions that end up not actually be related to I/O at all. The
issue with collecting stacks only for I/O is that the actual I/O can be
pooled or batched so you end up not having the stack when the conceptual
start of each operation within the batch started. Which is why I decided
to keep the Promise stack.
2025-07-08 10:49:08 -04:00
Sebastian Markbåge
b44a99bf58 [Fiber] Name content inside "Suspense fallback" (#33724)
Same as #33723 but for Fiber.
2025-07-08 00:00:00 -04:00
Ricky
e4314a0a0f [tests] Assert on component stack for Maximum Update error (#33686)
Good to assert these include the component stack
2025-07-07 13:58:03 -04:00
Ricky
e43986f1f3 Finally remove favorSafetyOverHydrationPerf (#33619)
This is rolled out to 100%.

Let me merge it though.
2025-07-07 13:57:51 -04:00
Sebastian Markbåge
c932e45780 [Fizz] Name content inside "Suspense fallback" (#33723)
Content in Suspense fallbacks are really not considered part of the
Suspense but since it does have some behavior it should be marked
somehow separately from the Suspense content.

A follow up would be to do the same in Fiber.
2025-07-07 13:48:33 -04:00
Sebastian Markbåge
223f81d877 [Flight] Flush performance track once we have no more pending chunks (#33719)
Stacked on #33718. Alternative to #33716.

The issue with flushing the Server Components track in its current form
is that we need to decide how long to wait before flushing whatever we
have. That's because the root's end time will be determined by the end
time of that last child.

However, if a child isn't actually used then we don't necessarily need
to include it in the Server Components track since it wasn't blocking
the initial render.

This waits for 100ms after the last pending chunk is resolved and if
nothing is invoking any more lazy initializers after that then we log
the Server Components track with the information we have at that point.
We also don't eagerly initialize any chunks that wasn't already
initialized so if nothing was rendered, then nothing will be logged.

This is somewhat an artifact of the current visualization. If we did
another transposed form we wouldn't necessarily need to wait until the
end and can log things as they're discovered.
2025-07-07 11:42:30 -04:00
Sebastian Markbåge
8a6c589be7 [Flight] Keep a separate ref count for debug chunks (#33717)
Same as #33716 but without the separate close signal.

We'll need the ref count for separate debug channel anyway but I'm not
sure we'll need the separate close signal.
2025-07-07 11:42:20 -04:00
Sebastian Markbåge
7cafeff340 [Flight] Close Debug Channel when All Lazy References Have Been GC:ed (#33718)
When we have a debug channel open that can ask for more objects. That
doesn't close until all lazy objects have been explicitly asked for. If
you GC an object before the lazy references inside of it before asking
for or releasing the objects, then it'll never close.

This ensures that if there are no more PendingChunk and no more
ResolvedModelChunk then we can close the connection.

There's two sources of retaining the Response object. On one side we
have a handle to it from the stream coming from the server. On the other
side we have a handle to it from ResolvedModelChunk to ask for more data
when we lazily parse a model.

This PR makes a weak handle from the stream to the Response. However, it
keeps a strong reference alive whenever we're waiting on a pending chunk
because then the stream might be the root if the only listeners are the
callbacks passed to the promise and no references to the promise itself.

The pending chunks count can end up being zero even if we might get more
data because the references might be inside lazy chunks. In this case
the lazy chunks keeps the Response alive. When the lazy chunk gets
parsed it can find more chunks that then end up pending to keep the
response strongly alive until they resolve.
2025-07-07 11:28:15 -04:00
Sebastian Markbåge
0378b46e7e [Flight] Include I/O not awaited in user space (#33715)
If I/O is not awaited in user space in a "previous" path we used to just
drop it on the floor. There's a few strategies we could apply here. My
first commit just emits it without an await but that would mean we don't
have an await stack when there's no I/O in a follow up.

I went with a strategy where the "previous" I/O is used only if the
"next" didn't have I/O. This may still drop I/O on the floor if there's
two back to back within internals for example. It would only log the
first one even though the outer await may have started earlier.

It may also log deeper in the "next" path if that had user space stacks
and then the outer await will appear as if it awaited after.

So it's not perfect.
2025-07-07 10:33:27 -04:00
Sebastian "Sebbie" Silbermann
bb402876f7 [Flight] Pass line/column to filterStackFrame (#33707) 2025-07-07 13:51:53 +02:00
Sebastian Markbåge
9a645e1d10 [Flight] Ignore "new Promise" and async_hooks even if they're not ignore listed (#33714)
These are part of the internals of Promises and async functions even if
anonymous functions are otherwise not ignore listed.
2025-07-06 17:05:15 -04:00
Sebastian Markbåge
2d7f0c4259 [Flight] Insert an extra await node for awaiting on the promise returned by then callback (#33713)
When a `.then()` callback returns another Promise, there's effectively
another "await" on that Promise that happens in the internals but that
was not modeled. In effect the Promise returned by `.then()` is blocked
on both the original Promise AND the promise returned by the callback.

This models that by cloning the original node and treat that as the
await on the original Promise. Then we use the existing Node to await
the new Promise but its "previous" points to the clone. That way we have
a forked node that awaits both.

---------

Co-authored-by: Sebastian Sebbie Silbermann <sebastian.silbermann@vercel.com>
2025-07-06 15:34:36 -04:00
Sebastian "Sebbie" Silbermann
4aad5e45ba [Flight] Consistent format of virtual rsc: sources (#33706) 2025-07-06 09:45:43 +02:00
Sebastian Markbåge
453a19a107 [Flight] Collect Debug Info from Rejections in Aborted Render (#33708)
This delays the abort by splitting the abort into a first step that just
flags a task as abort and tracks the time that we aborted. This first
step also invokes the `cacheSignal()` abort handler.

Then in a macrotask do we finish flushing the abort (or halt). This
ensures that any microtasks after the abort signal can finish flushing
which may emit rejections or fulfill (e.g. if you try/catch the abort or
if it was allSettled). These rejections are themselves signals for which
promise was blocked on what promise which forms a graph that we can use
for debug info. Notably this doesn't include any additional data in the
output since we don't include any data produced after the abort. It just
uses the additional execution to collect more debug info.

The abort itself might not have been spawned from I/O but it's still
interesting to mark Promises that aborted as interesting since they may
have been blocked on I/O. So we take the inner most Promise that
resolved after the end time (presumably due to the abort signal but also
could've just finished after but that's still after the abort).

Since the microtasks can spawn new Promises after the ones that reject
we ignore any of those that started after the abort.
2025-07-05 17:01:41 -04:00
Ruslan Lesiutin
5d87cd2244 React DevTools 6.1.4 -> 6.1.5 (#33702)
Same as 6.1.4, but with 2 hotfixes:
* fix: check if profiling for all profiling hooks
([hoxyq](https://github.com/hoxyq) in
[#33701](https://github.com/facebook/react/pull/33701))
* fix: fallback to reading string stack trace when failed
([hoxyq](https://github.com/hoxyq) in
[#33700](https://github.com/facebook/react/pull/33700))
2025-07-04 16:31:00 +01:00
Ruslan Lesiutin
5f71eed2eb [devtools] fix: check if profiling for all profiling hooks (#33701)
Follow-up to https://github.com/facebook/react/pull/33652.

Don't know how the other were missed. Double-checked that Profiler works
in dev mode.

Now all hooks start with `!isProfiling` check and return, if true.
2025-07-04 16:21:51 +01:00
Ruslan Lesiutin
455424dbf3 [devtools] fix: fallback to reading string stack trace when failed (#33700)
Discovered while testing with Hermes.
2025-07-04 15:36:52 +01:00
Ruslan Lesiutin
9fd4c09d68 React DevTools 6.1.3 -> 6.1.4 (#33699)
Changes from 6.1.3:
* feat: static Components panel layout
([hoxyq](https://github.com/hoxyq) in
[#33696](https://github.com/facebook/react/pull/33696))
* fix: support optionality of structured stack trace function name
([hoxyq](https://github.com/hoxyq) in
[#33697](https://github.com/facebook/react/pull/33697))
* fix: rename bottom stack frame ([hoxyq](https://github.com/hoxyq) in
[#33680](https://github.com/facebook/react/pull/33680))
2025-07-04 12:55:53 +01:00
Ruslan Lesiutin
d45db667d4 feat: static Components panel layout (#33696)
## Summary

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

With https://github.com/facebook/react/pull/33517, we now preserve at
least some minimal indent. This actually doesn't work with the current
setup, because we don't allow the container to overflow, so basically
deeply nested elements will go off the screen.

With these changes, we completely change the approach:
- The layout will be static and it will have a constant indentation that
will always be preserved.
- The container will allow overflows, so users will be able to scroll
horizontally and vertically.
- We will implement automatic horizontal and vertical scrolls, if
selected element is not in a viewport.
- New: added vertical delimiter that can be used for simpler visual
navigation.

## Demo
### Current public release

https://github.com/user-attachments/assets/58645d42-c6b8-408b-b76f-95fb272f2e1e

### With https://github.com/facebook/react/pull/33517 

https://github.com/user-attachments/assets/845285c8-5a01-4739-bcd7-ffc089e771bf

### This PR

https://github.com/user-attachments/assets/72086b84-8d84-4626-94b3-e22e114e028e
2025-07-04 12:29:19 +01:00
Ruslan Lesiutin
3fc1bc6f28 [devtools] fix: support optionality of structured stack trace function name (#33697)
Follow-up to https://github.com/facebook/react/pull/33680.

Turns out `.getFunctionName` not always returns string.
2025-07-04 10:32:09 +01:00
Sebastian Markbåge
ef8b6fa257 [Flight] Don't double badge consoles that are replayed from a third party (#33685)
If a FlightClient runs inside a FlightServer like fetching from a third
party and that logs, then we currently double badge them since we just
add on another badge. The issue is that this might be unnecessarily
noisy but we also transfer the original format of the current server
into the second badge.

This extracts our own badge and then adds the environment name as
structured data which lets the client decide how to format it.

Before:

<img width="599" alt="Screenshot 2025-07-02 at 2 30 07 PM"
src="https://github.com/user-attachments/assets/4bf26a29-b3a8-4024-8eb9-a3f90dbff97a"
/>

After:

<img width="590" alt="Screenshot 2025-07-02 at 2 32 56 PM"
src="https://github.com/user-attachments/assets/f06bbb6d-fbb1-4ae6-b0e3-775849fe3c53"
/>
2025-07-02 18:22:14 -04:00
Sebastian Markbåge
0b78161d7d [Fiber] Highlight a Component with Deeply Equal Props in the Performance Track (#33660)
Stacked on #33658 and #33659.

If we detect that a component is receiving only deeply equal objects,
then we highlight it as potentially problematic and worth looking into.

<img width="1055" alt="Screenshot 2025-06-27 at 4 15 28 PM"
src="https://github.com/user-attachments/assets/e96c6a05-7fff-4fd7-b59a-36ed79f8e609"
/>

It's fairly conservative and can bail out for a number of reasons:

- We only log it on the first parent that triggered this case since
other children could be indirect causes.
- If children has changed then we bail out since this component will
rerender anyway. This means that it won't warn for a lot of cases that
receive plain DOM children since the DOM children won't themselves get
logged.
- If the component's total render time including children is 100ms or
less then we skip warning because rerendering might not be a big deal.
- We don't warn if you have shallow equality but could memoize the JSX
element itself since we don't typically recommend that and React
Compiler doesn't do that. It only warns if you have nested objects too.
- If the depth of the objects is deeper than like the 3 levels that we
print diffs for then we wouldn't warn since we don't know if they were
equal (although we might still warn on a child).
- If the component had any updates scheduled on itself (e.g. setState)
then we don't warn since it would rerender anyway. This should really
consider Context updates too but we don't do that atm. Technically you
should still memoize the incoming props even if you also had unrelated
updates since it could apply to deeper bailouts.
2025-07-02 17:33:07 -04:00
Sebastian Markbåge
dcf83f7c2d Disable ScrollTimeline in Safari (#33499)
Stacked on #33501.

This disables the use of ScrollTimeline when detected in Safari in the
recommended SwipeRecognizer approach. I'm instead using a polyfill using
touch events on iOS.

Safari seems set to [release ScrollTimeline
soon](https://webkit.org/blog/16993/news-from-wwdc25-web-technology-coming-this-fall-in-safari-26-beta/).
Unfortunately it's not really what you'd expect.

First of all, [it's not running in sync with the
scroll](https://bugs.webkit.org/show_bug.cgi?id=288402) which is kind of
its main point. Instead, it is running at 60fps and out of sync with the
scroll just like JS. In fact, it is worse than JS because with JS you
can at least spawn CSS animations that run at 120fps. So our polyfill
can respond to touches at 60fps while gesturing and then run at 120fps
upon release. That's better than with ScrollTimeline.

Second, [there's a bug which interrupts scrolling if you start a
ViewTransition](https://bugs.webkit.org/show_bug.cgi?id=288795) when the
element is being removed as part of that. The element can still respond
to touches so in a polyfill this isn't an issue. But it essentially
makes it useless to use ScrollTimeline with swipe-away gestures.

So we're better off in every scenario by not using it.

The UA detection is a bit unfortunate. Not sure if there's something
more specific but we also had to do a UA detection for Chrome for View
Transitions. Those are the only two we have in all of React.


![safarimeme](https://github.com/user-attachments/assets/d4ca9eba-489e-4ade-b462-2ffeee3a470c)
2025-07-02 17:01:49 -04:00
Sebastian Markbåge
94fce500bc [Flight] Use a heuristic to extract a useful description of I/O from the Promise value (#33662)
It's useful to be able to distinguish between different invocations of
common helper libraries (like fetch) without having to click through
each one.

This adds a heuristic to extract a useful description of I/O from the
Promise value. We try to find things like getUser(id) -> User where
User.id is the id or fetch(url) -> Response where Response.url is the
url.

For urls we use the filename (or hostname if there is none) as the short
name if it can fit. The full url is in the tooltip.

<img width="845" alt="Screenshot 2025-06-27 at 7 58 20 PM"
src="https://github.com/user-attachments/assets/95f10c08-13a8-449e-97e8-52f0083a65dc"
/>
2025-07-02 16:12:37 -04:00
Sebastian Markbåge
508f7aa78f [Fiber] Switch back to using performance.measure for trigger logs (#33659)
Stacked on #33658.

Unfortunately `console.timeStamp` has the same bug that
`performance.measure` used to have where equal start/end times stack in
call order instead of reverse call-order. We rely on that in general so
we should really switch back all.

But there is one case in particular where we always add the same
start/time and that's for the "triggers" -
Mount/Unmount/Reconnect/Disconnect. Switching to `console.timeStamp`
broke this because they now showed below the thing that mounted.

After:

<img width="726" alt="Screenshot 2025-06-27 at 3 31 16 PM"
src="https://github.com/user-attachments/assets/422341c8-bef6-4909-9403-933d76b71508"
/>

Also fixed a bug where clamped update times could end up logging zero
width entries that stacked up on top of each other causing a two row
scheduler lane which should always be one row.
2025-07-02 16:10:52 -04:00
Sebastian Markbåge
e104795f63 [Fiber] Show Diff Render Props in Performance Track in DEV (#33658)
<img width="634" alt="Screenshot 2025-06-27 at 1 13 20 PM"
src="https://github.com/user-attachments/assets/dc8c488b-4a23-453f-918f-36b245364934"
/>

We have to be careful with performance in DEV. It can slow down DX since
these are ran whether you're currently running a performance trace or
not. It can also show up as misleading since these add time to the
"Remaining Effects" entry.

I'm not adding all props to the entries. Instead, I'm only adding the
changed props after diffing and none for initial mount. I'm trying to as
much as possible pick a fast path when possible. I'm also only logging
this for the "render" entries and not the effects. If we did something
for effects, it would be more like checking with dep changed.

This could still have a negative effect on dev performance since we're
now also using the slower `performance.measure` API when there's a diff.
2025-07-02 16:10:07 -04:00
Sebastian Markbåge
c0d151ce7e Clear width/height from Keyframes to Optimize View Transitions (#33576)
View Transitions has this annoying quirk where it adds `width` and
`height` to keyframes automatically when generating keyframes even when
it's not needed. This causes them to deopt from running on the
compositor thread in both Chrome and Safari. @bramus has a [good article
on
it](https://www.bram.us/2025/02/07/view-transitions-applied-more-performant-view-transition-group-animations/).

In React we can automatically rewrite the keyframes when we're starting
a View Transition to drop the `width` and `height` from the keyframes
when they have the same value and the same value as the pseudo element.

To compare it against the pseudo element we first apply the new
keyframes without the width/height and then read it back to see if it
has changed. For gestures, we have already cancelled the previous
animation so we can just read out from that.
2025-07-02 16:09:26 -04:00
Sebastian Markbåge
fc41c24aa6 Add ScrollTimeline Polyfill for Swipe Recognizer using a new CustomTimeline protocol (#33501)
The React API is just that we now accept this protocol as an alternative
to a native `AnimationTimeline` to be passed to
`startGestureTransition`. This is specifically the DOM version.

```js
interface CustomTimeline {
  currentTime: number;
  animate(animation: Animation): void | (() => void);
}
```

Instead, of passing this to the `Animation` that we start to control the
View Transition keyframes, we instead inverse the control and pass the
`Animation` to this one. It lets any custom implementation drive the
updates. It can do so by updating the time every frame or letting it run
a time based animation (such as momentum scroll).

In this case I added a basic polyfill for `ScrollTimeline` in the
example but we'll need a better one.
2025-07-02 16:07:46 -04:00
Jan Kassens
73aa744b70 Remove now dead argument from resolveClassComponentProps (#33682)
No longer used after https://github.com/facebook/react/pull/33648
2025-07-02 10:45:37 -04:00
Jan Kassens
602917c8cb Cleanup disableDefaultPropsExceptForClasses flag (#33648) 2025-07-01 15:52:56 -04:00
Ruslan Lesiutin
91d097b2c5 fix: rename bottom stack frame (#33680)
`react-stack-bottom-frame` -> `react_stack_bottom_frame`.

This survives `@babel/plugin-transform-function-name`, but now frames
will be displayed as `at Object.react_stack_bottom_frame (...)` in V8.
Checks that were relying on exact function name match were updated to
use either `.indexOf()` or `.includes()`

For backwards compatibility, both React DevTools and Flight Client will
look for both options. I am not so sure about the latter and if React
version is locked.
2025-07-01 18:06:26 +01:00
Sebastian Markbåge
7216c0f002 [Flight] Don't assume _debugStack and _owner is defined for prod elements (#33675)
We generally treat these types of fields as optional on ReactDebugInfo
and should on ReactElement too.

That way we can consume prod payloads from third parties.
2025-06-30 16:15:19 -04:00
Jan Kassens
6a3d16ca74 Back out "Remove Dead Code in WWW JS" (#33673)
Original commit changeset: 65c4decb56

This was removed by dead code removal. Adding back the TODO with
commented out code.
2025-06-30 15:26:45 -04:00
Facebook Community Bot
65c4decb56 Remove Dead Code in WWW JS
Differential Revision: D77531947

Pull Request resolved: https://github.com/facebook/react/pull/33672
2025-06-30 08:24:29 -07:00
Dawid Małecki
1e0d12b6f2 Align AttributeConfiguration type in ReactNativeTypes (#33671) 2025-06-30 15:36:49 +01:00
Sebastian Markbåge
e9cab42ece Special case printing Promises in Performance Track Properties (#33670)
Before:
<img width="266" alt="Screenshot 2025-06-30 at 8 32 23 AM"
src="https://github.com/user-attachments/assets/98aae5e1-4b2c-49bd-9b71-040b788c36ba"
/>

After:
<img width="342" alt="Screenshot 2025-06-30 at 8 39 17 AM"
src="https://github.com/user-attachments/assets/cd91c4a6-f6ae-4bec-9cd9-f42f4af468fe"
/>
2025-06-30 09:21:04 -04:00
Sebastian Markbåge
3cfcdfb307 [Flight] Resolve Deep Cycles (#33664)
Stacked on #33666.

If we ever get a future reference to a cycle and that reference gets
eagerly parsed before the target has loaded then we can end up with a
cycle that never gets resolved. That's because our cycle resolution only
works if the cyclic future reference is created synchronously within the
parsing path of the child.

I haven't been able to construct a normal scenario where this would
break. So this doesn't fail any tests. However, I can construct it with
debug info since those are eagerly evaluated. It's also a prerequisite
if the debug data can come out of order, like if it's on a different
stream.

The fix here is to make all the internal dependencies in the "listener"
list into introspectable objects instead of closures. That way we can
traverse the list of dependencies of a blocked reference to see if it
ends up in a cycle and therefore skip the reference.

It would be nice to address this once and for all to be more resilient
to server changes, but I'm not sure if it's worth this complexity and
the extra CPU cost of tracing the dependencies. Especially if it's just
for debug data.

closes #32316
fixes vercel/next.js#72104

---------

Co-authored-by: Hendrik Liebau <mail@hendrik-liebau.de>
2025-06-29 10:56:16 -04:00
Sebastian Markbåge
9c2a8dd5f8 [Flight] Ensure we dedupe references if we later discover that it's the model root (#33666)
I noticed we weren't deduping these cases.
2025-06-29 10:47:33 -04:00
Sebastian Markbåge
811e203ed4 [Flight] Don't replay performance logs when replayConsoleLogs is false (#33656)
This is the same principle. They're both side-effects and go to the
`console.*` namespace.
2025-06-27 16:27:45 -04:00
Ruslan Lesiutin
d92056efb3 React DevTools 6.1.2 -> 6.1.3 (#33657)
Full list of changes:

* devtools: emit performance entries only when profiling
([hoxyq](https://github.com/hoxyq) in
[#33652](https://github.com/facebook/react/pull/33652))
* Get Server Component Function Location for Parent Stacks using Child's
Owner Stack ([sebmarkbage](https://github.com/sebmarkbage) in
[#33629](https://github.com/facebook/react/pull/33629))
* Added minimum indent size to Component Tree
([jsdf](https://github.com/jsdf) in
[#33517](https://github.com/facebook/react/pull/33517))
* [devtools-shell] layout options for testing
([jsdf](https://github.com/jsdf) in
[#33516](https://github.com/facebook/react/pull/33516))
* Remove feature flag enableRenderableContext
([kassens](https://github.com/kassens) in
[#33505](https://github.com/facebook/react/pull/33505))
* refactor[devtools]: update css for settings and support css variables
in shadow dom scnenario ([hoxyq](https://github.com/hoxyq) in
[#33487](https://github.com/facebook/react/pull/33487))
* [mcp] Add MCP tool to print out the component tree of the currently
open React App ([jorge-cab](https://github.com/jorge-cab) in
[#33305](https://github.com/facebook/react/pull/33305))
* [scripts] Switch back to flow parser for prettier
([rickhanlonii](https://github.com/rickhanlonii) in
[#33414](https://github.com/facebook/react/pull/33414))
* upgrade json5 ([rickhanlonii](https://github.com/rickhanlonii) in
[#33358](https://github.com/facebook/react/pull/33358))
* Get source location from structured callsites in prepareStackTrace
([sebmarkbage](https://github.com/sebmarkbage) in
[#33143](https://github.com/facebook/react/pull/33143))
* Clean up enableSiblingPrerendering flag
([jackpope](https://github.com/jackpope) in
[#32319](https://github.com/facebook/react/pull/32319))
2025-06-27 16:17:08 +01:00
Ruslan Lesiutin
58ac15cdc9 devtools: emit performance entries only when profiling (#33652)
## Summary

This floods Timings track in dev mode and also hurts performance in dev.

Making sure we are buffering Performance entries (all of them are marks)
only when profiling in RDT. This should be removed once we roll out Perf
tracks.
2025-06-27 15:32:08 +01:00
Sebastian Markbåge
bfc8801e0f [Flight] Write Debug Info to Separate Priority Queue (#33654)
This writes all debug info to a separate priority queue. In the future
I'll put this on a different channel.

Ideally I think we'd put it in the bottom of the stream but because it
actually blocks the elements from resolving anyway it ends up being
better to put them ahead. At least for now.

When we have two separate channels it's not possible to rely on the
order for consistency Even then we might write to that queue first for
this reason. We can't rely on it though. Which will show up like things
turning into Lazy instead of Element similar to how outlining can.
2025-06-27 09:45:11 -04:00
Sebastian Markbåge
d2a288febf Include Component Props in Performance Track (#33655)
Similar to how we can include a Promise resolved value we can include
Component Props.

For now I left out props for Client Components for perf unless they
error. I'll try it for Client Components in general in a separate PR.

<img width="730" alt="Screenshot 2025-06-26 at 5 54 29 PM"
src="https://github.com/user-attachments/assets/f0c86911-2899-4b5f-b45f-5326bdbc630f"
/>
<img width="762" alt="Screenshot 2025-06-26 at 5 54 12 PM"
src="https://github.com/user-attachments/assets/97540d19-5950-4346-99e6-066af086040e"
/>
2025-06-27 08:45:56 -04:00
Dhruv
4db4b21c63 Fix typo "Complier" to "Compiler" and remove duplicate issue reference (#33653)
<!--
  Thanks for submitting a pull request!
We appreciate you spending the time to work on these changes. Please
provide enough information so that others can review your pull request.
The three fields below are mandatory.

Before submitting a pull request, please make sure the following is
done:

1. Fork [the repository](https://github.com/facebook/react) and create
your branch from `main`.
  2. Run `yarn` in the repository root.
3. If you've fixed a bug or added code that should be tested, add tests!
4. Ensure the test suite passes (`yarn test`). Tip: `yarn test --watch
TestName` is helpful in development.
5. Run `yarn test --prod` to test in the production environment. It
supports the same options as `yarn test`.
6. If you need a debugger, run `yarn test --debug --watch TestName`,
open `chrome://inspect`, and press "Inspect".
7. Format your code with
[prettier](https://github.com/prettier/prettier) (`yarn prettier`).
8. Make sure your code lints (`yarn lint`). Tip: `yarn linc` to only
check changed files.
  9. Run the [Flow](https://flowtype.org/) type checks (`yarn flow`).
  10. If you haven't already, complete the CLA.

Learn more about contributing:
https://reactjs.org/docs/how-to-contribute.html
-->

## Summary
Fixed a typo in the changelog.md file: corrected "Complier" to
"Compiler" and removed a duplicate issue reference for improved clarity.
<!--
Explain the **motivation** for making this change. What existing problem
does the pull request solve?
-->

## How did you test this change?
Manually reviewed the changelog text to ensure correctness. No code
changes were made.
<!--
Demonstrate the code is solid. Example: The exact commands you ran and
their output, screenshots / videos if the pull request changes the user
interface.
How exactly did you verify that your PR solves the issue you wanted to
solve?
  If you leave this empty, your PR will very likely be closed.
-->
2025-06-26 08:34:45 -07:00
Sebastian Markbåge
31d91651e0 [Fizz] Rename ReactFizzContext to ReactFizzLegacyContext (#33649)
#33622 forgot these.
2025-06-25 21:18:25 -04:00
Sebastian Markbåge
9406162bc9 [Flight] Emit start time before an await if one wasn't emitted already (#33646)
There's a special case where if we create a new task, e.g. to serialize
a promise like `<div>{promise}</div>` then that row doesn't have any
start time emitted but it has a `task.time` inherited. We mostly don't
need this because every other operation emits its own start time. E.g.
when we started rendering a Server Component or the real start time of a
real `await`.

For these implied awaits we don't have a start time. Ideally it would
probably be when we started the serialization, like when we called
`.then()` but we can't just emit that eagerly and we can't just advance
the `task.time` because that time represents the last render or previous
await and we use that to cut off awaits. However for this case we don't
want to cut off any inner awaits inside the node we're serializing if
they happened before the `.then()`.

Therefore, I just use the time of the previous operation - which is
likely either the resolution of a previous promise that blocked the
`<div>` like the promise of the Server Component that rendered it, or
just the start of the Server Component if it was sync.
2025-06-25 17:28:59 -04:00
Hendrik Liebau
9b2a545b32 [Flight] Add tests for component and owner stacks of halted components (#33644)
This PR adds tests for the Node.js and Edge builds to verify that
component stacks and owner stacks of halted components appear as
expected, now that recent enhancements for those have been implemented
(the latest one being #33634).

---------

Co-authored-by: Sebastian "Sebbie" Silbermann <silbermann.sebastian@gmail.com>
2025-06-25 22:34:35 +02:00
Sebastian Markbåge
bb6c9d521e [Flight] Log aborted await and component renders (#33641)
<img width="926" alt="Screenshot 2025-06-25 at 1 02 14 PM"
src="https://github.com/user-attachments/assets/1877d13d-5259-4cc4-8f48-12981e3073fe"
/>

The I/O entry doesn't show as aborted in the Server Request track
because technically it wasn't. The end time is just made up. It's still
going. It's not aborted until the abort signal propagates and if we do
get that signal wired up before it emits, it instead would show up as
rejected.

---------

Co-authored-by: Hendrik Liebau <mail@hendrik-liebau.de>
2025-06-25 16:28:54 -04:00
Joseph Savona
123ff13b19 [compiler] Consolidate HIRFunction return information (#33640)
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.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33640).
* #33643
* #33642
* __->__ #33640
* #33625
* #33624
2025-06-25 11:10:38 -07:00
Joseph Savona
e130c08b06 [compiler] Avoid empty switch cases (#33625)
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.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33625).
* #33643
* #33642
* #33640
* __->__ #33625
* #33624
2025-06-25 11:10:26 -07:00
Joseph Savona
9894c488e0 [compiler] Fix bug with reassigning function param in destructuring (#33624)
Closes #33577, a bug with ExtractScopeDeclarationsFromDestructuring and
codegen when a function param is reassigned.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33624).
* #33643
* #33642
* #33640
* #33625
* __->__ #33624
2025-06-25 11:10:09 -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
Joseph Savona
b067c6fe79 [compiler] Improve error message for mutating hook args/return (#33513)
The previous error message was generic, because the old style function
signature didn't support a way to specify a reason alongside a freeze
effect. This meant we could only say why a value was frozen for
instructions, but not hooks which use function signatures. By defining a
new aliasing signature for custom hooks we can specify a reason and
provide a better error message.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33513).
* #33571
* #33558
* #33547
* #33543
* #33533
* #33532
* #33530
* #33526
* #33522
* #33518
* #33514
* __->__ #33513
2025-06-18 13:04:53 -07:00
Joseph Savona
e081cb3446 [compiler] FunctionExpression context locations point to first reference (#33512)
This has always been awkward: `FunctionExpression.context` places have
locations set to the declaration of the identifier, whereas other
references have locations pointing to the reference itself. Here, we
update context operands to have their location point to the first
reference of that variable within the function.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33512).
* #33571
* #33558
* #33547
* #33543
* #33533
* #33532
* #33530
* #33526
* #33522
* #33518
* #33514
* #33513
* __->__ #33512
* #33504
* #33500
* #33497
* #33496
2025-06-18 13:02:43 -07:00
Joseph Savona
7b67dc92b0 [commit] Better error message for invalid hoisting (#33504)
We're already tracking which variables are hoisted context variables, so
if we see a mutation of a frozen value we can emit a custom error
message to help users identify the problem.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33504).
* #33571
* #33558
* #33547
* #33543
* #33533
* #33532
* #33530
* #33526
* #33522
* #33518
* #33514
* #33513
* #33512
* __->__ #33504
* #33500
* #33497
* #33496
2025-06-18 13:02:32 -07:00
Joseph Savona
7c28c15465 [compiler] Fix AnalyzeFunctions to fully reset context identifiers (#33500)
AnalyzeFunctions had logic to reset the mutable ranges of context
variables after visiting inner function expressions. However, there was
a bug in that logic: InferReactiveScopeVariables makes all the
identifiers in a scope point to the same mutable range instance. That
meant that it was possible for a later function expression to indirectly
cause an earlier function expressions' context variables to get a
non-zero mutable range.

The fix is to not just reset start/end of context var ranges, but assign
a new range instance. Thanks for the help on debugging, @mofeiz!

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33500).
* #33571
* #33558
* #33547
* #33543
* #33533
* #33532
* #33530
* #33526
* #33522
* #33518
* #33514
* #33513
* #33512
* #33504
* __->__ #33500
* #33497
* #33496
2025-06-18 13:02:23 -07:00
Joseph Savona
90ccbd71c1 [compiler] Enable new inference by default (#33497)
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33497).
* #33571
* #33558
* #33547
* #33543
* #33533
* #33532
* #33530
* #33526
* #33522
* #33518
* #33514
* #33513
* #33512
* #33504
* #33500
* __->__ #33497
* #33496
2025-06-18 13:02:12 -07:00
Joseph Savona
0cf6d0c929 [compiler] Update fixtures for new inference (#33496)
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33496).
* #33571
* #33558
* #33547
* #33543
* #33533
* #33532
* #33530
* #33526
* #33522
* #33518
* #33514
* #33513
* #33512
* #33504
* #33500
* #33497
* __->__ #33496
2025-06-18 13:01:56 -07:00
Joseph Savona
df080d228b [compiler] Copy fixtures affected by new inference (#33495)
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33495).
* #33571
* #33558
* #33547
* #33543
* #33533
* #33532
* #33530
* #33526
* #33522
* #33518
* #33514
* #33513
* #33512
* #33504
* #33500
* #33497
* #33496
* __->__ #33495
* #33494
* #33572
2025-06-18 12:58:16 -07:00
Joseph Savona
66cfe048d3 [compiler] New mutability/aliasing model (#33494)
Squashed, review-friendly version of the stack from
https://github.com/facebook/react/pull/33488.

This is new version of our mutability and inference model, designed to
replace the core algorithm for determining the sets of instructions
involved in constructing a given value or set of values. The new model
replaces InferReferenceEffects, InferMutableRanges (and all of its
subcomponents), and parts of AnalyzeFunctions. The new model does not
use per-Place effect values, but in order to make this drop-in the end
_result_ of the inference adds these per-Place effects.

I'll write up a larger document on the model, first i'm doing some
housekeeping to rebase the PR.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33494).
* #33571
* #33558
* #33547
* #33543
* #33533
* #33532
* #33530
* #33526
* #33522
* #33518
* #33514
* #33513
* #33512
* #33504
* #33500
* #33497
* #33496
* #33495
* __->__ #33494
* #33572
2025-06-18 12:58:06 -07:00
Joseph Savona
ae962653d6 [compiler] Remove unnecessary fixture (#33572)
This is covered by iife-inline-ternary

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33572).
* #33571
* #33558
* #33547
* #33543
* #33533
* #33532
* #33530
* #33526
* #33522
* #33518
* #33514
* #33513
* #33512
* #33504
* #33500
* #33497
* #33496
* #33495
* #33494
* __->__ #33572
2025-06-18 12:57:54 -07:00
Sebastian Markbåge
e1dc03492e Expose cacheSignal() alongside cache() (#33557)
This was really meant to be there from the beginning. A `cache()`:ed
entry has a life time. On the server this ends when the render finishes.
On the client this ends when the cache of that scope gets refreshed.

When a cache is no longer needed, it should be possible to abort any
outstanding network requests or other resources. That's what
`cacheSignal()` gives you. It returns an `AbortSignal` which aborts when
the cache lifetime is done based on the same execution scope as a
`cache()`ed function - i.e. `AsyncLocalStorage` on the server or the
render scope on the client.

```js
import {cacheSignal} from 'react';
async function Component() {
  await fetch(url, { signal: cacheSignal() });
}
```

For `fetch` in particular, a patch should really just do this
automatically for you. But it's useful for other resources like database
connections.

Another reason it's useful to have a `cacheSignal()` is to ignore any
errors that might have triggered from the act of being aborted. This is
just a general useful JavaScript pattern if you have access to a signal:

```js
async function getData(id, signal) {
  try {
     await queryDatabase(id, { signal });
  } catch (x) {
     if (!signal.aborted) {
       logError(x); // only log if it's a real error and not due to cancellation
     }
     return null;
  }
}
```

This just gets you a convenient way to get to it without drilling
through so a more idiomatic code in React might look something like.

```js
import {cacheSignal} from "react";

async function getData(id) {
  try {
     await queryDatabase(id);
  } catch (x) {
     if (!cacheSignal()?.aborted) {
       logError(x);
     }
     return null;
  }
}
```

If it's called outside of a React render, we normally treat any cached
functions as uncached. They're not an error call. They can still load
data. It's just not cached. This is not like an aborted signal because
then you couldn't issue any requests. It's also not like an infinite
abort signal because it's not actually cached forever. Therefore,
`cacheSignal()` returns `null` when called outside of a React render
scope.

Notably the `signal` option passed to `renderToReadableStream` in both
SSR (Fizz) and RSC (Flight Server) is not the same instance that comes
out of `cacheSignal()`. If you abort the `signal` passed in, then the
`cacheSignal()` is also aborted with the same reason. However, the
`cacheSignal()` can also get aborted if the render completes
successfully or fatally errors during render - allowing any outstanding
work that wasn't used to clean up. In the future we might also expand on
this to give different
[`TaskSignal`](https://developer.mozilla.org/en-US/docs/Web/API/TaskSignal)
to different scopes to pass different render or network priorities.

On the client version of `"react"` this exposes a noop (both for
Fiber/Fizz) due to `disableClientCache` flag but it's exposed so that
you can write shared code.
2025-06-17 17:04:40 -04:00
Jordan Brown
90bee81902 [compiler] Do not inline IIFEs in value blocks (#33548)
As discussed in chat, this is a simple fix to stop introducing labels
inside expressions.

The useMemo-with-optional test was added in
d70b2c2c4e
and crashes for the same reason- an unexpected label as a value block
terminal.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33548).
* __->__ #33548
* #33546
2025-06-16 21:53:50 -04:00
Jordan Brown
75e78d243f [compiler] Add repro for IIFE in ternary causing a bailout (#33546)
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33546).
* #33548
* __->__ #33546
2025-06-16 21:53:27 -04:00
Jan Kassens
5d24c64cc9 Remove feature flag enableDO_NOT_USE_disableStrictPassiveEffect (#33524) 2025-06-16 12:22:47 -04:00
lauren
6b7e207cab [ci] Don't skip experimental prerelease incorrectly (#33527)
Previously the experimental workflow relied on the canary one running
first to avoid race conditions. However, I didn't account for the fact
that the canary one can now be skipped.
2025-06-13 15:29:59 -04:00
lauren
d60f77a533 [ci] Update prerelease workflows to allow publishing specific packages (#33525)
It may be useful at times to publish only specific packages as an
experimental tag. For example, if we need to cherry pick some fixes for
an old release, we can first do so by creating that as an experimental
release just for that package to allow for quick testing by downstream
projects.

Similar to .github/workflows/runtime_releases_from_npm_manual.yml I
added three options (`dry`, `only_packages`, `skip_packages`) to
`runtime_prereleases.yml` which both the manual and nightly workflows
reuse. I also added a discord notification when the manual workflow is
run.
2025-06-13 14:22:55 -04:00
James Friend
12bc60f509 [devtools] Added minimum indent size to Component Tree (#33517)
## Summary

The devtools Components tab's component tree view currently has a
behavior where the indentation of each level of the tree scales based on
the available width of the view. If the view is narrow or component
names are long, all indentation showing the hierarchy of the tree scales
down with the view width until there is no indentation at all. This
makes it impossible to see the nesting of the tree, making the tree view
much less useful. With long component names and deep hierarchies this
issue is particularly egregious. For comparison, the Chrome Dev Tools
Elements panel uses a fixed indentation size, so it doesn't suffer from
this issue.

This PR adds a minimum pixel value for the indentation width, so that
even when the window is narrow some indentation will still be visible,
maintaining the visual representation of the component tree hierarchy.

Alternatively, we could match the behavior of the Chrome Dev Tools and
just use a constant indentation width.

## How did you test this change?

- tests (yarn test-build-devtools)
- tested in browser:
- added an alternate left/right split pane layout to
react-devtools-shell to test with
(https://github.com/facebook/react/pull/33516)
- tested resizing the tree view in different layout modes

### before this change:



https://github.com/user-attachments/assets/470991f1-dc05-473f-a2cb-4f7333f6bae4

with a long component name:



https://github.com/user-attachments/assets/1568fc64-c7d7-4659-bfb1-9bfc9592fb9d





### after this change:




https://github.com/user-attachments/assets/f60bd7fc-97f6-4680-9656-f0db3d155411

with a long component name:


https://github.com/user-attachments/assets/6ac3f58c-42ea-4c5a-9a52-c3b397f37b45
2025-06-13 15:28:31 +01:00
James Friend
ed023cfc73 [devtools-shell] layout options for testing (#33516)
## Summary

This PR adds a 'Layout' selector to the devtools shell main example, as
well as a resizable split pane, allowing more realistic testing of how
the devtools behaves when used in a vertical or horizontal layout and at
different sizes (e.g. when resizing the Chrome Dev Tools pane).

## How did you test this change?



https://github.com/user-attachments/assets/81179413-7b46-47a9-bc52-4f7ec414e8be
2025-06-13 15:25:04 +01:00
Sebastian "Sebbie" Silbermann
a00ca6f6b5 [Fizz] Delay detachment of completed boundaries until reveal (#33511) 2025-06-11 21:24:24 +02:00
lauren
888ea60d8e [compiler][repro] Postfix operator is incorrectly compiled (#33508)
This bug was reported via our wg and appears to only affect values
created as a ref.

Currently, postfix operators used in a callback gets compiled to:

```js
modalId.current = modalId.current + 1; // 1
const id = modalId.current; // 1
return id;
```

which is semantically incorrect. The postfix increment operator should
return the value before incrementing. In other words something like this
should have been compiled instead:

```js
const id = modalId.current; // 0
modalId.current = modalId.current + 1; // 1
return id;
```

This bug does not trigger when the incremented value is a plain
primitive, instead there is a TODO bailout.
2025-06-11 14:40:42 -04:00
Jan Kassens
b7e2de632b Stringify context as SomeContext instead of SomeContext.Provider (#33507)
This matches the change in React 19 to use `<SomeContext>` as the
preferred way to provide a context.
2025-06-11 12:08:04 -04:00
Sebastian Markbåge
ff93c4448c [Flight] Track Debug Info from Synchronously Unwrapped Promises (#33485)
Stacked on #33482.

There's a flaw with getting information from the execution context of
the ping. For the soft-deprecated "throw a promise" technique, this is a
bit unreliable because you could in theory throw the same one multiple
times. Similarly, a more fundamental flaw with that API is that it
doesn't allow for tracking the information of Promises that are already
synchronously able to resolve.

This stops tracking the async debug info in the case of throwing a
Promise and only when you render a Promise. That means some loss of data
but we should just warn for throwing a Promise anyway.

Instead, this also adds support for tracking `use()`d thenables and
forwarding `_debugInfo` from then. This is done by extracting the info
from the Promise after the fact instead of in the resolve so that it
only happens once at the end after the pings are done.

This also supports passing the same Promise in multiple places and
tracking the debug info at each location, even if it was already
instrumented with a synchronous value by the time of the second use.
2025-06-11 12:07:10 -04:00
Jan Kassens
6c86e56a0f Remove feature flag enableRenderableContext (#33505)
The flag is fully rolled out.
2025-06-11 11:53:04 -04:00
Sebastian Markbåge
56408a5b12 [Flight] Emit timestamps only in forwards advancing time in debug info (#33482)
Previously you weren't guaranteed to have only advancing time entries,
you could jump back in time, but now it omits unnecessary duplicates and
clamps automatically if you emit a previous time entry to enforce
forwards order only.

The reason I didn't do this originally is because `await` can jump in
the order because we're trying to encode a graph into a flat timeline
for simplicity of the protocol and consumers.

```js
async function a() {
  await fetch1();
  await fetch2();
}

async function b() {
  await fetch3();
}

async function foo() {
  const p = a();
  await b();
  return p;
}
```

This can effectively create two parallel sequences:

```
--1.................----2.......--
------3......---------------------
```

This can now be flattened to either:

```
--1.................3---2.......--
```

Or:

```
------3......1......----2.......--
```

Depending on which one we visit first. Regardless, information is lost.

I'd say that the second one is worse encoding of this scenario because
it pretends that we weren't waiting for part of the timespan that we
were. To solve this I think we should probably make `emitAsyncSequence`
create a temporary flat list and then sort it by start time before
emitting.

Although we weren't actually blocked since there was some CPU time that
was able to proceed to get to 3. So maybe the second one is actually
better. If we wanted that consistently we'd have to figure out what the
intersection was.

---------

Co-authored-by: Hendrik Liebau <mail@hendrik-liebau.de>
2025-06-10 11:03:20 -04:00
Sebastian Markbåge
c38e268978 [Fiber] Fix hydration of useId in SuspenseList (#33491)
Includes #31412.

The issue is that `pushTreeFork` stores some global state when reconcile
children. This gets popped by `popTreeContext` in `completeWork`.
Normally `completeWork` returns its own `Fiber` again if it wants to do
a second pass which will call `pushTreeFork` again in the next pass.
However, `SuspenseList` doesn't return itself, it returns the next child
to work on.

The fix is to keep track of the count and push it again it when we
return the next child to attempt.

There are still some outstanding issues with hydration. Like the
backwards test still has the wrong behavior in it because it hydrates
backwards and so it picks up the DOM nodes in reverse order.
`tail="hidden"` also doesn't work correctly.

There's also another issue with `useId` and `AsyncIterable` in
SuspenseList when there's an unknown number of children. We don't
support those showing one at a time yet though so it's not an issue yet.
To fix it we need to add variable total count to the `useId` algorithm.
E.g. by falling back to varint encoding.

---------

Co-authored-by: Rick Hanlon <rickhanlonii@fb.com>
Co-authored-by: Ricky <rickhanlonii@gmail.com>
2025-06-09 19:37:49 -04:00
Ruslan Lesiutin
80c03eb7e0 refactor[devtools]: update css for settings and support css variables in shadow dom scnenario (#33487)
## Summary

Minor changes around css and styling of Settings dialog.

1. `:root` selector was updated to `:is(:root, :host)` to make css
variables available on Shadow Root
2. CSS tweaks around Settings dialog: removed references to deleted
styles, removed unused styles, ironed out styling for cases when input
styles are enhanced by user agent stylesheet

<!--
Explain the **motivation** for making this change. What existing problem
does the pull request solve?
-->

## How did you test this change?

| Before | After |
|--------|--------|
| ![Screenshot 2025-06-09 at 15 35
55](https://github.com/user-attachments/assets/1ac5d002-744b-4b10-9501-d4f2a7c827d2)
| ![Screenshot 2025-06-09 at 15 26
12](https://github.com/user-attachments/assets/8cc07cda-99a5-4930-973b-b139b193e349)
|
| ![Screenshot 2025-06-09 at 15 36
02](https://github.com/user-attachments/assets/1af4257c-928d-4ec6-a614-801cc1936f4b)
| ![Screenshot 2025-06-09 at 15 26
25](https://github.com/user-attachments/assets/7a3a0f7c-5f3d-4567-a782-dd37368a15ae)
|
| ![Screenshot 2025-06-09 at 15 36
05](https://github.com/user-attachments/assets/a1e00381-2901-4e22-b1c6-4a3f66ba78c9)
| ![Screenshot 2025-06-09 at 15 26
30](https://github.com/user-attachments/assets/bdefce68-cbb5-4b88-b44c-a74f28533f7d)
|
| ![Screenshot 2025-06-09 at 15 36
12](https://github.com/user-attachments/assets/4eda6234-0ef0-40ca-ad9d-5990a2b1e8b4)
| ![Screenshot 2025-06-09 at 15 26
37](https://github.com/user-attachments/assets/5cac305e-fd29-460c-b0b8-30e477b8c26e)
|
2025-06-09 18:25:19 +01:00
Wesley LeMahieu
b6c0aa8814 [compiler]: fix link compiler & 4 broken tests from path containing spaces (#33409)
## Summary

Problem #1: Running the `link-compiler.sh` bash script via `"prebuild"`
script fails if a developer has cloned the `react` repo into a folder
that contains _any_ spaces. 3 tests fail because of this.

<img width="1003" alt="fail-1"
src="https://github.com/user-attachments/assets/1fbfa9ce-4f84-48d7-b49c-b6e967b8c7ca"
/>
<img width="1011" alt="fail-2"
src="https://github.com/user-attachments/assets/0a8c6371-a2df-4276-af98-38f4784cf0da"
/>
<img width="1027" alt="fail-3"
src="https://github.com/user-attachments/assets/1c4f4429-800c-4b44-b3da-a59ac85a16b9"
/>

For example, my current folder is:
`/Users/wes/Development/Open Source Contributions/react`

The link compiler error returns:
`./scripts/react-compiler/link-compiler.sh: line 15: cd:
/Users/wes/Development/Open: No such file or directory`

Problem #2: 1 test in `ReactChildren-test.js` fails due the existing
stack trace regex which should be lightly revised.

`([^(\[\n]+)[^\n]*/g` is more robust for stack traces: it captures the
function/class name (with dots) and does not break on spaces in file
paths.
`([\S]+)[^\n]*/g` is simpler but breaks if there are spaces and doesn't
handle dotted names well.

Additionally, we trim the whitespace off the name to resolve extra
spaces breaking this test as well:

```
-     in div (at **)
+     in div  (at **)
```

<img width="987" alt="fail-4"
src="https://github.com/user-attachments/assets/56a673bc-513f-4458-95b2-224129c77144"
/>

All of the above tests pass if I hyphenate my local folder:
`/Users/wes/Development/Open-Source-Contributions/react`

I selfishly want to keep spaces in my folder names. 🫣

## How did you test this change?

**npx yarn prebuild**

Before:
<img width="896" alt="Screenshot at Jun 01 11-42-56"
src="https://github.com/user-attachments/assets/4692775c-1e5c-4851-9bd7-e12ed5455e47"
/>

After:
<img width="420" alt="Screenshot at Jun 01 11-43-42"
src="https://github.com/user-attachments/assets/4e303c00-02b7-4540-ba19-927b2d7034fb"
/>

**npx yarn test**
**npx yarn test
./packages/react/src/\_\_tests\_\_/ReactChildren-test.js**
**npx yarn test -r=xplat --env=development --variant=true --ci
--shard=3/5**

Before:
<img width="438" alt="before"
src="https://github.com/user-attachments/assets/f5eedb22-18c3-4124-a04b-daa95c0f7652"
/>

After:
<img width="439" alt="after"
src="https://github.com/user-attachments/assets/a94218ba-7c6a-4f08-85d3-57540e9d0029"
/>

<img width="650" alt="Screenshot at Jun 02 18-03-39"
src="https://github.com/user-attachments/assets/3eae993c-a56b-46c8-ae02-d249cb053fe7"
/>

<img width="685" alt="Screenshot at Jun 03 12-53-47"
src="https://github.com/user-attachments/assets/5b2caa33-d3dc-4804-981d-52cb10b6226f"
/>
2025-06-09 08:40:27 -07:00
Sebastian Markbåge
428ab82001 [Flight] Simulate fetch to third party in fixture (#33484)
This adds some I/O to go get the third party thing to test how it
overlaps.

With #33482, this is what it looks like. The await gets cut off when the
third party component starts rendering. I.e. after the latency to start.

<img width="735" alt="Screenshot 2025-06-08 at 5 42 46 PM"
src="https://github.com/user-attachments/assets/f68d9a84-05a1-4125-b3f0-8f3e4eaaa5c1"
/>

This doesn't fully simulate everything because it should actually also
simulate each chunk of the stream coming back too. We could wrap the
ReadableStream to simulate that. In that scenario, it would probably get
some awaits on the chunks at the end too.
2025-06-09 10:04:40 -04:00
Jordan Brown
4df098c4c2 [compiler] Don't include useEffectEvent values in autodeps (#33450)
Summary: useEffectEvent values are not meant to be added to the dep
array
2025-06-09 09:26:45 -04:00
Hendrik Liebau
95bcf87e6b Format ReactNativeAttributePayloadFabric.js with Prettier (#33486)
The prettier check for this file is currently failing on `main`, after
#32119 was merged.
2025-06-09 12:42:10 +01:00
Hanno J. Gödecke
911dbd9e34 feat(ReactNative): prioritize attribute config process function to allow processing function props (#32119)
## Summary

In react-native props that are passed as function get converted to a
boolean (`true`). This is the default pattern for event handlers in
react-native.
However, there are reasons for why you might want to opt-out of this
behavior, and instead, pass along the actual function as the prop.
Right now, there is no way to do this, and props that are functions
always get set to `true`.
The `ViewConfig` attributes already have the API for a `process`
function. I simply moved the check for the process function up, so if a
ViewConfig's prop attribute configured a process function this is always
called first.
This provides an API to opt out of the default behavior. 

This is the accompanied PR for react-native:

- https://github.com/facebook/react-native/pull/48777

## How did you test this change?

<!--
Demonstrate the code is solid. Example: The exact commands you ran and
their output, screenshots / videos if the pull request changes the user
interface.
How exactly did you verify that your PR solves the issue you wanted to
solve?
  If you leave this empty, your PR will very likely be closed.
-->

I modified the code manually in a template react-native app and
confirmed its working. This is a code path you only need in very special
cases, thus it's a bit hard to provide a test for this. I recorded a
video where you can see that the changes are active and the prop is
being passed as native value.

For this I created a custom native component with a view config that
looked like this:

```js
const viewConfig = {
  uiViewClassName: 'CustomView',
  bubblingEventTypes: {},
  directEventTypes: {},
  validAttributes: {
    nativeProp: {
      process: (nativeProp) => {
		// Identity function that simply returns the prop function callback
        // to opt out of this prop being set to `true` as its a function
        return nativeProp
      },
    },
  },
}
```



https://github.com/user-attachments/assets/493534b2-a508-4142-a760-0b1b24419e19

Additionally I made sure that this doesn't conflict with any existing
view configs in react native. In general, this shouldn't be a breaking
change, as for existing view configs it didn't made a difference if you
simply set `myProp: true` or `myProp: { process: () => {...} }` because
as soon as it was detected that the prop is a function the config
wouldn't be used (which is what this PR fixes).
Probably everyone, including the react-native core components use
`myProp: true` for callback props, so this change should be fine.
2025-06-09 10:55:28 +01:00
Hendrik Liebau
c0b5a0cad3 [Flight] Use Web Streams APIs for 3rd-party component in Flight fixture (#33481) 2025-06-08 06:33:25 +02:00
Hendrik Liebau
e4b88ae4c6 [Flight] Add Web Streams APIs to unbundled Node entries for Webpack (#33480) 2025-06-07 23:39:25 +02:00
Sebastian Markbåge
6c8bcdaf1b [Flight] Clarify Semantics for Awaiting Cached Data (#33438)
Technically the async call graph spans basically all the way back to the
start of the app potentially, but we don't want to include everything.
Similarly we don't want to include everything from previous components
in every child component. So we need some heuristics for filtering out
data.

We roughly want to be able to inspect is what might contribute to a
Suspense loading sequence even if it didn't this time e.g. due to a race
condition.

One flaw with the previous approach was that awaiting a cached promise
in a sibling that happened to finish after another sibling would be
excluded. However, in a different race condition that might end up being
used so I wanted to include an empty "await" in that scenario to have
some association from that component.

However, for data that resolved fully before the request even started,
it's a little different. This can be things that are part of the start
up sequence of the app or externally cached data. We decided that this
should be excluded because it doesn't contribute to the loading sequence
in the expected scenario. I.e. if it's cached. Things that end up being
cache misses would still be included. If you want to test externally
cached data misses, then it's up to you or the framework to simulate
those. E.g. by dropping the cache. This also helps free up some noise
since static / cached data can be excluded in visualizations.

I also apply this principle to forwarding debug info. If you reuse a
cached RSC payload, then the Server Component render time and its awaits
gets clamped to the caller as if it has zero render/await time. The I/O
entry is still back dated but if it was fully resolved before we started
then it's completely excluded.
2025-06-07 17:26:36 -04:00
Sebastian Markbåge
b367b60927 [Flight] Add "use ..." boundary after the change instead of before it (#33478)
I noticed that the ThirdPartyComponent in the fixture was showing the
wrong stack and the `"use third-party"` is in the wrong location.

<img width="628" alt="Screenshot 2025-06-06 at 11 22 11 PM"
src="https://github.com/user-attachments/assets/f0013380-d79e-4765-b371-87fd61b3056b"
/>

When creating the initial JSX inside the third party server, we should
make sure that it has no owner. In a real cross-server environment you
get this by default by just executing in different context. But since
the fixture example is inside the same AsyncLocalStorage as the parent
it already has an owner which gets transferred. So we should make sure
that were we create the JSX has no owner to simulate this.

When we then parse a null owner on the receiving side, we replace its
owner/stack with the owner/stack of the call to `createFrom...` to
connect them. This worked fine with only two environments. The bug was
that when we did this and then transferred the result to a third
environment we took the original parsed stack trace. We should instead
parse a new one from the replaced stack in the current environment.

The second bug was that the `"use third-party"` badge ends up in the
wrong place when we do this kind of thing. Because the stack of the
thing entering the new environment is the call to `createFrom...` which
is in the old environment even though the component itself executes in
the new environment. So to see if there's a change we should be
comparing the current environment of the task to the owner's environment
instead of the next environment after the task.

After:

<img width="494" alt="Screenshot 2025-06-07 at 1 13 28 AM"
src="https://github.com/user-attachments/assets/e2e870ba-f125-4526-a853-bd29f164cf09"
/>
2025-06-07 11:28:57 -04:00
Sebastian Markbåge
9666605abf [Flight] Add Web Stream support to the Flight Server in Node (#33474)
This needs some tweaks to the implementation and a conversion but simple
enough.

---------

Co-authored-by: Hendrik Liebau <mail@hendrik-liebau.de>
2025-06-07 10:40:09 -04:00
Sebastian Markbåge
65ec57df37 [Fizz] Add Web Streams to Fizz Node entry point (#33475)
New take on #33441.

This uses a wrapper instead of a separate bundle.
2025-06-06 20:16:43 -04:00
Sebastian "Sebbie" Silbermann
b3d5e90786 [Fizz] Include unit of threshold in rel=expect deopt error (#33476) 2025-06-07 02:11:33 +02:00
Sebastian Markbåge
280ff6fed2 [Flight] Add Web Stream support to the Flight Client in Node (#33473)
This effectively lets us consume Web Streams in a Node build. In fact
the Node entry point is now just adding Node stream APIs.

For the client, this is simple because the configs are not actually
stream type specific. The server is a little trickier.
2025-06-06 17:14:15 -04:00
Sebastian Markbåge
82f3684c63 Revert Node Web Streams (#33472)
Reverts #33457, #33456 and #33442.

There are too many issues with wrappers, lazy init, stateful modules,
duplicate instantiation of async_hooks and duplication of code.

Instead, we'll just do a wrapper polyfill that uses Node Streams
internally.

I kept the client indirection files that I added for consistency with
the server though.
2025-06-06 16:26:36 -04:00
Josh Story
142aa0744d [Fizz] Support deeply nested Suspense inside fallback (#33467)
When deeply nested Suspense boundaries inside a fallback of another
boundary resolve it is possible to encounter situations where you either
attempt to flush an aborted Segment or you have a boundary without any
root segment. We intended for both of these conditions to be impossible
to arrive at legitimately however it turns out in this situation you
can. The fix is two-fold

1. allow flushing aborted segments by simply skipping them. This does
remove some protection against future misconfiguraiton of React because
it is no longer an invariant that you hsould never attempt to flush an
aborted segment but there are legitimate cases where this can come up
and simply omitting the segment is fine b/c we know that the user will
never observe this. A semantically better solution would be to avoid
flushing boudaries inside an unneeded fallback but to do this we would
need to track all boundaries inside a fallback or create back pointers
which add to memory overhead and possibly make GC harder to do
efficiently. By flushing extra we're maintaining status quo and only
suffer in performance not with broken semantics.

2. when queuing completed segments allow for queueing aborted segments
and if we are eliding the enqueued segment allow for child segments that
are errored to be enqueued too. This will mean that we can maintain the
invariant that a boundary must have a root segment the first time we
flush it, it just might be aborted (see point 1 above).

This change has two seemingly similar test cases to exercise this fix.
The reason we need both is that when you have empty segments you hit
different code paths within Fizz and so each one (without this fix)
triggers a different error pathway.

This change also includes a fix to our tests where we were not
appropriately setting CSPnonce back to null at the start of each test so
in some contexts scripts would not run for some tests
2025-06-06 11:59:15 -07:00
Sebastian Markbåge
6ccf328499 [Fizz] Shorten throttle to hit a specific target metric (#33463)
Adding throttling or delaying on images, can obviously impact metrics.
However, it's all in the name of better actual user experience overall.
(Note that it's not strictly worse even for metric. Often it's actually
strictly better due to less work being done overall thanks to batching.)

Metrics can impact things like search ranking but I believe this is on a
curve. If you're already pretty good, then a slight delay won't suddenly
make you rank in a completely different category. Similarly, if you're
already pretty bad then a slight delay won't make it suddenly way worse.
It's still in the same realm. It's just one weight of many. I don't
think this will make a meaningful practical impact and if it does,
that's probably a bug in the weights that will get fixed.

However, because there's a race to try to "make everything green" in
terms of web vitals, if you go from green to yellow only because of some
throttling or suspensey images, it can feel bad. Therefore this
implements a heuristic where if the only reason we'd miss a specific
target is because of throttling or suspensey images, then we shorten the
timeout to hit the metric. This is a worse user experience because it
can lead to extra flashing but feeling good about "green" matters too.

If you then have another reveal that happens to be the largest
contentful paint after that, then that's throttled again so that it
doesn't become flashy after that. If you've already missed the deadline
then you're not going to hit your metric target anyway. It can affect
average but not median.

This is mainly about LCP. It doesn't affect FCP since that doesn't have
a throttle. If your LCP is the same as your FCP then it also doesn't
matter.

We assume that `performance.now()`'s zero point starts at the "start of
the navigation" which makes this simple. Even if we used the
`PerformanceNavigationTiming` API it would just tell us the same thing.

This only implements for Fizz since these metrics tend to currently only
by tracked for initial loads, but with soft navs tracking we could
consider implementing the same for Fiber throttles.
2025-06-06 14:01:15 -04:00
lauren
a374e0ec87 [ci] Fix missing permissions for stale job (#33466)
Missed these the last time.
2025-06-06 13:32:51 -04:00
Sebastian Markbåge
ab859e31be [Flight] Build Node.js Web Streams builds for Turbopack and Parcel (#33457)
Same as #33456 and #33442 but for Turbopack and Parcel.
2025-06-06 11:07:40 -04:00
Sebastian Markbåge
e8d15fa19e [Flight] Build node-webstreams version of bundled webpack server (#33456)
Follow up to #33442. This is the bundled version.

To keep type check passes from exploding and the maintainance of the
annoying `paths: []` list small, this doesn't add this to flow type
checks. We might miss some config but every combination should already
be covered by other one passes.

I also don't add any jest tests because to test these double export
entry points we need conditional importing to cover builds and
non-builds which turns out to be difficult for the Flight builds so
these aren't covered by any basic build tests.

This approach is what I'm going for, for the other bundlers too.
2025-06-06 11:07:15 -04:00
Sebastian Markbåge
d177272802 [Fizz] Error and deopt from rel=expect for large documents without boundaries (#33454)
We want to make sure that we can block the reveal of a well designed
complete shell reliably. In the Suspense model, client transitions don't
have any way to implicitly resolve. This means you need to use Suspense
or SuspenseList to explicitly split the document. Relying on implicit
would mean you can't add a Suspense boundary later where needed. So we
highly encourage the use of them around large content.

However, if you have constructed a too large shell (e.g. by not adding
any Suspense boundaries at all) then that might take too long to render
on the client. We shouldn't punish users (or overzealous metrics
tracking tools like search engines) in that scenario.

This opts out of render blocking if the shell ends up too large to be
intentional and too slow to load. Instead it deopts to showing the
content split up in arbitrary ways (browser default). It only does this
for SSR, and not client navs so it's not reliable.

In fact, we issue an error to `onError`. This error is recoverable in
that the document is still produced. It's up to your framework to decide
if this errors the build or just surface it for action later.

What should be the limit though? There's a trade off here. If this limit
is too low then you can't fit a reasonably well built UI within it
without getting errors. If it's too high then things that accidentally
fall below it might take too long to load.

I came up with 512kB of uncompressed shell HTML. See the comment in code
for the rationale for this number. TL;DR: Data and theory indicates that
having this much content inside `rel="expect"` doesn't meaningfully
change metrics. Research of above-the-fold content on various websites
indicate that this can comfortable fit all of them which should be
enough for any intentional initial paint.
2025-06-06 10:29:48 -04:00
Sebastian Markbåge
22b929156c [Fizz] Suspensey Images for View Transition Reveals (#33433)
Block the view transition on suspensey images Up to 500ms just like the
client.

We can't use `decode()` because a bug in Chrome where those are blocked
on `startViewTransition` finishing we instead rely on sync decoding but
also that the image is live when it's animating in and we assume it
doesn't start visible.

However, we can block the View Transition from starting on the `"load"`
or `"error"` events.

The nice thing about blocking inside `startViewTransition` is that we
have already done the layout so we can only wait on images that are
within the viewport at this point. We might want to do that in Fiber
too. If many image doesn't have fixed size but need to load first, they
can all end up in the viewport. We might consider only doing this for
images that have a fixed size or only a max number that doesn't have a
fixed size.
2025-06-06 10:14:13 -04:00
Ricky
a3be6829c6 [tests] remove pretest compiler script (#33452)
This shouldn't be needed now that the lint rule was move
2025-06-06 09:16:58 -04:00
Hendrik Liebau
b1759882c0 [Flight] Bypass caches in Flight fixture if requested (#33445) 2025-06-06 06:42:58 +02:00
Timothy Yung
dddcae7a11 Enable the enableEagerAlternateStateNodeCleanup Feature Flag (#33447)
## Summary

Enables the `enableEagerAlternateStateNodeCleanup` feature flag for all
variants, while maintaining the `__VARIANT__` for the internal React
Native flavor for backtesting reasons.

## How did you test this change?

```
$ yarn test
```
2025-06-05 14:22:35 -07:00
Hendrik Liebau
43714eb4e9 Do not notify Discord for draft pull requests (#33446)
When I added the `ready_for_review` event in #32344, no notifications
for opened draft PRs were sent due to some other condition. This is not
the case anymore, so we need to exclude draft PRs from triggering a
notification when the workflow is run because of an `opened` event. This
event is still needed because the `ready_for_review` event only fires
when an existing draft PR is converted to a non-draft state. It does not
trigger for pull requests that are opened directly as ready-for-review.
2025-06-05 15:08:57 -04:00
Sebastian Markbåge
a5110b22f0 [Flight] Add a Node.js Web Streams bundle for unbundled client/server for Webpack (#33442)
Like #33441 but for Flight.

This is just one of the many combinations needed. I'm just starting with
one.
2025-06-05 14:29:02 -04:00
Hendrik Liebau
b4477d3800 [Flight] Add a cached 3rd-party component to the Flight fixture (#33443)
This should allow us to visualize what
https://github.com/facebook/react/pull/33438 is trying to convey.

An uncached 3rd-party component is displayed like this in the dev tools:

<img width="1072" alt="Screenshot 2025-06-05 at 12 57 32"
src="https://github.com/user-attachments/assets/d418ae23-d113-4dc9-98b8-ab426710454a"
/>

However, when the component is restored from a cache, it looks like
this:

<img width="1072" alt="Screenshot 2025-06-05 at 12 56 56"
src="https://github.com/user-attachments/assets/a0e34379-d8c0-4b14-8b54-b5c06211232b"
/>

The `Server Components ⚛` track is missing completely here, and the
`Loading profile...` phase also took way longer than without caching the
3rd-party component.

On `main`, the `Server Components ⚛` track is not missing:

<img width="1072" alt="Screenshot 2025-06-05 at 14 31 20"
src="https://github.com/user-attachments/assets/c35e405d-27ca-4b04-a34c-03bd959a7687"
/>

The cached 3rd-party component starts before the current render, and is
also not excluded here, which is of course expected without #33438.
2025-06-05 17:19:54 +02:00
Sebastian Markbåge
93f1668045 [Fizz] Add Node Web Streams bundle for SSR (#33441)
We highly recommend using Node Streams in Node.js because it's much
faster and it is less likely to cause issues when chained in things like
compression algorithms that need explicit flushing which the Web Streams
ecosystem doesn't have a good solution for. However, that said, people
want to be able to use the worse option for various reasons.

The `.edge` builds aren't technically intended for Node.js. A Node.js
environments needs to be patched in various ways to support it. It's
also less optimal since it can't use [Node.js exclusive
features](https://github.com/facebook/react/pull/33388) and have to use
[the lowest common
denominator](https://github.com/facebook/react/pull/27399) such as JS
implementations instead of native.

This adds a Web Streams build of Fizz but exclusively for Node.js so
that in it we can rely on Node.js modules. The main difference compared
to Edge is that SSR now uses `createHash` from the `"crypto"` module and
imports `TextEncoder` from `"util"`. We use `setImmediate` instead of
`setTimeout`.

The public API is just `react-dom/server` which in Node.js automatically
imports `react-dom/server.node` which re-exports the legacy bundle, Node
Streams bundle and Node Web Streams bundle. The main downside is if your
bundler isn't smart to DCE this barrel file.

With Flight the difference is larger but that's a bigger lift.
2025-06-05 10:50:41 -04:00
Sebastian Markbåge
37054867c1 [Flight] Forward debugInfo from awaited instrumented Promises (#33415)
Stacked on #33403.

When a Promise is coming from React such as when it's passed from
another environment, we should forward the debug information from that
environment. We already do that when rendered as a child.

This makes it possible to also `await promise` and have the information
from that instrumented promise carry through to the next render.

This is a bit tricky because the current protocol is that we have to
read it from the Promise after it resolves so it has time to be assigned
to the promise. `async_hooks` doesn't pass us the instance (even though
it has it) when it gets resolved so we need to keep it around. However,
we have to be very careful because if we get this wrong it'll cause a
memory leak since we retain things by `asyncId` and then manually listen
for `destroy()` which can only be called once a Promise is GC:ed, which
it can't be if we retain it. We have to therefore use a `WeakRef` in
case it never resolves, and then read the `_debugInfo` when it resolves.
We could maybe install a setter or something instead but that's also
heavy.

The other issues is that we don't use native Promises in
ReactFlightClient so our instrumented promises aren't picked up by the
`async_hooks` implementation and so we never get a handle to our
thenable instance. To solve this we can create a native wrapper only in
DEV.
2025-06-04 00:49:03 -04:00
Sebastian Markbåge
d742611ce4 Replace Implicit Options on SuspenseList with Explicit Options (#33424)
We want to change the defaults for `revealOrder` and `tail` on
SuspenseList. This is an intermediate step to allow experimental users
to upgrade.

To explicitly specify these options I added `revealOrder="independent"`
and `tail="visible"`.

I then added warnings if `undefined` or `null` is passed. You must now
always explicitly specify them. However, semantics are still preserved
for now until the next step.

We also want to change the rendering order of the `children` prop for
`revealOrder="backwards"`. As an intermediate step I first added
`revealOrder="unstable_legacy-backwards"` option. This will only be
temporary until all users can switch to the new `"backwards"` semantics
once we flip it in the next step.

I also clarified the types that the directional props requires iterable
children but not iterable inside of those. Rows with multiple items can
be modeled as explicit fragments.
2025-06-03 17:40:30 -04:00
Sebastian Markbåge
1540081725 [Flight] Encode Async I/O Tasks using the Enclosing Line/Column (#33403)
Stacked on #33402.

There's a bug in Chrome Performance tracking which uses the enclosing
line/column instead of the callsite in stacks.

For our fake eval:ed functions that represents functions on the server,
we can position the enclosing function body at the position of the
callsite to simulate getting the right line.

Unfortunately, that doesn't give us exactly the right callsite when it's
used for other purposes that uses the callsite like console logs and
error reporting and stacks inside breakpoints. So I don't think we want
to always do this.

For ReactAsyncInfo/ReactIOInfo, the only thing we're going to use the
fake task for is the Performance tracking, so it doesn't have any
downsides until Chrome fixes the bug and we'd have to revert it.
Therefore this PR uses that techniques only for those entries.

We could do this for Server Components too but we're going to use those
for other things too like console logs. I don't think it's worth
duplicating the Task objects. That would also make it inconsistent with
Client Components.

For Client Components, we could in theory also generate fake evals but
that would be way slower since there's so many of them and currently we
rely on the native implementation for those. So doesn't seem worth
fixing.

But since we can at least fix it for RSC I/O/awaits we can do this hack.
2025-06-03 17:30:31 -04:00
Sebastian Markbåge
9cc74fec74 [Flight] Emit the time we awaited something inside a Server Component (#33402)
Stacked on #33400. 

<img width="1261" alt="Screenshot 2025-06-01 at 10 27 47 PM"
src="https://github.com/user-attachments/assets/a5a73ee2-49e0-4851-84ac-e0df6032efb5"
/>

This is emitted with the start/end time and stack of the "await". Which
may be different than the thing that started the I/O.

These awaits aren't quite as simple as just every await since you can
start a sequence in parallel there can actually be multiple overlapping
awaits and there can be CPU work interleaved with the await on the same
component.

```js
function getData() {
  await fetch(...);
  await fetch(...);
}
const promise = getData();
doWork();
await promise;
```

This has two "I/O" awaits but those are actually happening in parallel
with `doWork()`.

Since these also could have started before we started rendering this
sequence (e.g. a component) we have to clamp it so that we don't
consider awaits that start before the component.

What we're conceptually trying to convey is the time this component was
blocked due to that I/O resource. Whether it's blocked from completing
the last result or if it's blocked from issuing a waterfall request.
2025-06-03 17:29:41 -04:00
Sebastian Markbåge
157ac578de [Flight] Include env in ReactAsyncInfo and ReactIOInfo (#33400)
Stacked on #33395.

This lets us keep track of which environment this was fetched and
awaited.

Currently the IO and await is in the same environment. It's just kept
when forwarded. Once we support forwarding information from a Promise
fetched from another environment and awaited in this environment then
the await can end up being in a different environment.

There's a question of when the await is inside Flight itself such as
when you return a promise fetched from another environment whether that
should mean that the await is in the current environment. I don't think
so since the original stack trace is the best stack trace. It's only if
you `await` it in user space in this environment first that this might
happen and even then it should only be considered if there wasn't a
better await earlier or if reading from the other environment was itself
I/O.

The timing of *when* we read `environmentName()` is a little interesting
here too.
2025-06-03 17:28:46 -04:00
Sebastian Markbåge
45da4e055d [Flight] Track Owner on AsyncInfo and IOInfo (#33395)
Stacked on #33394.

This lets us create async stack traces to the owner that was in context
when the I/O was started or awaited.

<img width="615" alt="Screenshot 2025-06-01 at 12 31 52 AM"
src="https://github.com/user-attachments/assets/6ff5a146-33d6-4a4b-84af-1b57e73047d4"
/>

This owner might not be the immediate closest parent where the I/O was
awaited.
2025-06-03 16:12:26 -04:00
Sebastian Markbåge
d8919a0a68 [Flight] Log "Server Requests" Track (#33394)
Stacked on #33392.

This adds another track to the Performance Track called `"Server
Requests"`.

<img width="1015" alt="Screenshot 2025-06-01 at 12 02 14 AM"
src="https://github.com/user-attachments/assets/c4d164c4-cfdf-4e14-9a87-3f011f65fd20"
/>

This logs the flat list of I/O awaited on by Server Components. There
will be other views that are more focused on what data blocks a specific
Component or Suspense boundary but this is just the list of all the I/O
basically so you can get an overview of those waterfalls without the
noise of all the Component trees and rendering. It's similar to what the
"Network" track is on the client.

I've been going back and forth on what to call this track but I went
with `"Server Requests"` for now. The idea is that the name should
communicate that this is something that happens on the server and is a
pairing with the `"Server Components"` track. Although we don't use that
feature, since it's missing granularity, it's also similar to "Server
Timings".
2025-06-03 15:31:12 -04:00
Sebastian "Sebbie" Silbermann
2e9f8cd3e0 Clear bundler cache before bundling fixtures (#33426) 2025-06-03 21:10:13 +02:00
Sebastian Markbåge
65a46c7eeb [Flight] Track the function name that was called for I/O entries (#33392)
Stacked on #33390.

The stack trace doesn't include the thing you called when calling into
ignore listed content. We consider the ignore listed content
conceptually the abstraction that you called that's interesting.

This extracts the name of the first ignore listed function that was
called from user space. For example `"fetch"`. So we can know what kind
of request this is.

This could be enhanced and tweaked with heuristics in the future. For
example, when you create a Promise yourself and call I/O inside of it
like my `delay` examples, then we use that Promise as the I/O node but
its stack doesn't have the actual I/O performed. It might be better to
use the inner I/O node in that case. E.g. `setTimeout`. Currently I pick
the name from the first party code instead - in my example `delay`.

Another case that could be improved is the case where your whole
component is third-party. In that case we still log the I/O but it has
no context about what kind of I/O since the whole stack is ignored it
just gets the component name for example. We could for example look at
the first name that is in a different package than the package name of
the ignored listed component. So if
`node_modules/my-component-library/index.js` calls into
`node_modules/mysql/connection.js` then we could use the name from the
inner.
2025-06-03 15:04:28 -04:00
Sebastian Markbåge
3fb17d16a4 [Flight] Encode ReactIOInfo as its own row type (#33390)
Stacked on #33388.

This encodes the I/O entries as their own row type (`"J"`). This makes
it possible to parse them directly without first parsing the debug info
for each component. E.g. if you're just interested in logging the I/O
without all the places it was awaited.

This is not strictly necessary since the debug info is also readily
available without parsing the actual trees. (That's how the Server
Components Performance Track works.) However, we might want to exclude
this information in profiling builds while retaining some limited form
of I/O tracking.

It also allows for logging side-effects that are not awaited if we
wanted to.
2025-06-03 14:16:34 -04:00
Sebastian Markbåge
acee65d6d0 [Flight] Track Awaits on I/O as Debug Info (#33388)
This lets us track what data each Server Component depended on. This
will be used by Performance Track and React DevTools.

We use Node.js `async_hooks`. This has a number of downside. It is
Node.js specific so this feature is not available in other runtimes
until something equivalent becomes available. It's [discouraged by
Node.js docs](https://nodejs.org/api/async_hooks.html#async-hooks). It's
also slow which makes this approach only really viable in development
mode. At least with stack traces. However, it's really the only solution
that gives us the data that we need.

The [Diagnostic
Channel](https://nodejs.org/api/diagnostics_channel.html) API is not
sufficient. Not only is many Node.js built-in APIs missing but all
libraries like databases are also missing. Were as `async_hooks` covers
pretty much anything async in the Node.js ecosystem.

However, even if coverage was wider it's not actually showing the
information we want. It's not enough to show the low level I/O that is
happening because that doesn't provide the context. We need the stack
trace in user space code where it was initiated and where it was
awaited. It's also not each low level socket operation that we want to
surface but some higher level concept which can span a sequence of I/O
operations but as far as user space is concerned.

Therefore this solution is anchored on stack traces and ignore listing
to determine what the interesting span is. It is somewhat
Promise-centric (and in particular async/await) because it allows us to
model an abstract span instead of just random I/O. Async/await points
are also especially useful because this allows Async Stacks to show the
full sequence which is not supported by random callbacks. However, if no
Promises are involved we still to our best to show the stack causing
plain I/O callbacks.

Additionally, we don't want to track all possible I/O. For example,
side-effects like logging that doesn't affect the rendering performance
doesn't need to be included. We only want to include things that
actually block the rendering output. We also need to track which data
blocks each component so that we can track which data caused a
particular subtree to suspend.

We can do this using `async_hooks` because we can track the graph of
what resolved what and then spawned what.

To track what suspended what, something has to resolve. Therefore it
needs to run to completion before we can show what it was suspended on.
So something that never resolves, won't be tracked for example.

We use the `async_hooks` in `ReactFlightServerConfigDebugNode` to build
up an `ReactFlightAsyncSequence` graph that collects the stack traces
for basically all I/O and Promises allocated in the whole app. This is
pretty heavy, especially the stack traces, but it's because we don't
know which ones we'll need until they resolve. We don't materialize the
stacks until we need them though.

Once they end up pinging the Flight runtime, we collect which current
executing task that pinged the runtime and then log the sequence that
led up until that runtime into the RSC protocol. Currently we only
include things that weren't already resolved before we started rendering
this task/component, so that we don't log the entire history each time.

Each operation is split into two parts. First a `ReactIOInfo` which
represents an I/O operation and its start/end time. Basically the start
point where it was start. This is basically represents where you called
`new Promise()` or when entering an `async function` which has an
implied Promise. It can be started in a different component than where
it's awaited and it can be awaited in multiple places. Therefore this is
global information and not associated with a specific Component.

The second part is `ReactAsyncInfo`. This represents where this I/O was
`await`:ed or `.then()` called. This is associated with a point in the
tree (usually the Promise that's a direct child of a Component). Since
you can have multiple different I/O awaited in a sequence technically it
forms a dependency graph but to simplify the model these awaits as
flattened into the `ReactDebugInfo` list. Basically it contains each
await in a sequence that affected this part from unblocking.

This means that the same `ReactAsyncInfo` can appear in mutliple
components if they all await the same `ReactIOInfo` but the same Promise
only appears once.

Promises that are only resolved by other Promises or immediately are not
considered here. Only if they're resolved by an I/O operation. We pick
the Promise basically on the border between user space code and ignored
listed code (`node_modules`) to pick the most specific span but abstract
enough to not give too much detail irrelevant to the current audience.
Similarly, the deepest `await` in user space is marked as the relevant
`await` point.

This feature is only available in the `node` builds of React. Not if you
use the `edge` builds inside of Node.js.

---------

Co-authored-by: Sebastian "Sebbie" Silbermann <silbermann.sebastian@gmail.com>
2025-06-03 14:14:40 -04:00
Sebastian Markbåge
1ae0a845bd Use underscore instead of « » for useId algorithm (#33422)
Alternative to #33421. The difference is that this also adds an
underscore between the "R" and the ID.

The reason we wanted to use special characters is because we use the
full spectrum of A-Z 0-9 in our ID generation so we can basically
collide with any common word (or anyone using a similar algorithm,
base64 or even base16). It's a little less likely that someone would put
`_R_` specifically unless you generate like two IDs separated by
underscore.


![9w2ogt](https://github.com/user-attachments/assets/21b2d2ac-1a3a-4657-ba0b-1616e49dfdee)
2025-06-03 11:30:17 -04:00
Jorge Cabiedes
2b4064eb9b [mcp] Add MCP tool to print out the component tree of the currently open React App (#33305)
## Summary

This tool leverages DevTools to get the component tree from the
currently open React App. This gives realtime information to agents
about the state of the app.

## How did you test this change?

Tested integration with Claude Desktop
2025-06-02 21:42:34 -07:00
Ricky
3531b26729 [scripts] Switch back to flow parser for prettier (#33414)
Prettier 3.3 (which we're on) should support modern flow features
according to https://prettier.io/blog/2024/06/01/3.3.0
2025-06-03 00:00:28 -04:00
Sebastian "Sebbie" Silbermann
4a1f29079c [Fizz] Add Owner Stacks when render is aborted (#32735) 2025-06-02 19:27:49 +02:00
mofeiZ
526dd340b3 [compiler][patch] Emit unary expressions instead of negative numbers (#33383)
This is a babel bug + edge case.

Babel compact mode produces invalid JavaScript (i.e. parse error) when
given a `NumericLiteral` with a negative value.

See https://codesandbox.io/p/devbox/5d47fr for repro.
2025-06-02 11:43:45 -04:00
Wesley LeMahieu
ee76351917 fix typo in compiler validation filename (#33345)
## Summary

While investigating the root cause of #33208, I noticed a clear typo for
one of the validation files.

## How did you test this change?

Inside `/react/compiler/packages/babel-plugin-react-compiler` I ran the
test script successfully:

<img width="415" alt="Screenshot at May 22 16-43-06"
src="https://github.com/user-attachments/assets/3fe8c5e1-37ce-4a31-b35e-7e323e57cd9d"
/>
2025-05-30 16:31:16 -07:00
Pieter De Baets
8b55eb4e72 Cleanup props diffing experiments (#33381)
## Summary

We completed testing on these internally, so can cleanup the separate
fast and slow paths and remove the `enableShallowPropDiffing` flag which
we're not pursuing.

## How did you test this change?

```
yarn test ReactNativeAttributePayloadFabric
```
2025-05-30 17:17:59 +01:00
Mateusz Burzyński
14094f80cb Allow nonce to be used on hoistable styles (#32461)
fixes https://github.com/facebook/react/issues/32449

This is my first time touching this code. There are multiple systems in
place here and I wouldn't be surprised to learn that this has to be
handled in some other areas too. I have found some other style-related
code areas but I had no time yet to double-check them.

cc @gnoff
2025-05-29 08:17:10 -07:00
Sebastian "Sebbie" Silbermann
5717f1933f [react-dom] Enforce small gap between completed navigation and default Transition indicator (#33354) 2025-05-28 19:46:12 +02:00
Ricky
b07717d857 [devtools] upgrade json5 (#33358) 2025-05-28 10:31:09 -04:00
Jan Kassens
283f87f083 Revert "enableViewTransition in www" (#33362)
We need to do some more testing here.

Reverts facebook/react#33357
2025-05-27 17:17:45 -04:00
mofeiZ
f9ae0a4c2e [compiler][gating] Custom opt out directives (experimental option) (#33328)
Adding an experimental / unstable compiler config to enable custom
opt-out directives
2025-05-27 12:02:29 -04:00
Jan Kassens
f702620cea [fb-www] ship enableViewTransition (#33357) 2025-05-27 11:23:27 -04:00
Sebastian Markbåge
c0464aedb1 [Fizz] Block on Suspensey Fonts during reveal (#33342)
This is the same technique we do for the client except we don't check
whether this is newly created font loading to keep code small.

Unfortunately, we can't use this technique for Suspensey images. They'll
need to block before we call `startViewTransition` in a separate
refactor. This is due to a bug in Chrome where `img.decode()` doesn't
resolve until `startViewTransition` does.
2025-05-23 13:26:02 -04:00
Sebastian Markbåge
6a1dfe3777 Disable moveBefore experiment (#33348)
There seems to be some bugs still to work out in Chrome. See #33187.

Additionally, since you can't really rely on this function existing
across browsers, it's hard to depend on its behavior anyway. In fact,
you now have a source of inconsistent behaviors across browsers to deal
with.

Ideally it would also be more widely spread in fake DOM implementations
like JSDOM so that we can use it unconditionally. #33177.

We still want to enable this since it's a great feature but maybe not
until it's more widely available cross-browsers with fewer bugs.
2025-05-23 13:25:13 -04:00
Jordan Brown
99efc627a5 [eslint] Add an option to require dependencies on effect hooks (#33344)
Summary:

To prepare for automatic effect dependencies, some codebases may want to
codemod
existing useEffect calls with no deps to include an explicit undefined
second argument
in order to preserve the "run on every render" behavior. In sufficiently
large codebases,
this may require a temporary enforcement period where all effects
provide an explicit
dependencies argument.

Outside of migration, relying on a component to render can lead to real
bugs,
especially when working with memoization.
2025-05-23 10:09:41 -04:00
0xFango
bfaeb4a461 Fix incorrect use of NoLanes in executionContext check (#33170)
## Summary

This PR fixes a likely incorrect condition in the
`scheduleUpdateOnFiber` function inside `ReactFiberWorkLoop.js`.

Previously, the code checked:

```js
(executionContext & RenderContext) !== NoLanes
````

However, `NoLanes` is part of the lane priority system, not the
execution context flags. The intent here seems to be to detect whether
the current execution context includes `RenderContext`, which should be
compared against `NoContext`, not `NoLanes`.

This fix replaces `NoLanes` with `NoContext` for semantic correctness
and consistency with other checks throughout the codebase.

**Fixes
[[#33169](https://github.com/facebook/react/issues/33169)](https://github.com/facebook/react/issues/33169)**

---

## How did you test this change?

I ran the following commands to validate correctness and ensure nothing
was broken:

* `yarn lint`
* `yarn linc`
* `yarn test`
* `yarn test --prod`
* `yarn flow`
* `yarn prettier`

All checks passed. Since this is a minor internal logic fix and doesn't
change public behavior or APIs, no additional tests are necessary at
this time.
2025-05-22 22:02:39 -04:00
Christoph Nakazawa
3e9db65fc3 Fix typo in error message. (#33313)
## Summary

I am writing code that isn't so good, so I saw this error message many
times. It appears to have a typo. This PR fixes the typo.

## How did you test this change?

Ran the tests
2025-05-22 16:18:23 -04:00
mofeiZ
0d072884f9 [compiler] Inferred effect dependencies now include optional chains (#33326)
Inferred effect dependencies now include optional chains.

This is a temporary solution while
https://github.com/facebook/react/pull/32099 and its followups are
worked on. Ideally, we should model reactive scope dependencies in the
IR similarly to `ComputeIR` -- dependencies should be hoisted and all
references rewritten to use the hoisted dependencies.

`
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33326).
* __->__ #33326
* #33325
* #32286
2025-05-22 16:14:49 -04:00
mofeiZ
abf9fd559d [compiler] Add reactive flag on scope dependencies (#33325)
When collecting scope dependencies, mark each dependency with `reactive:
true | false`. This prepares for later PRs
https://github.com/facebook/react/pull/33326 and
https://github.com/facebook/react/pull/32099 which rewrite scope
dependencies into instructions.

Note that some reactive objects may have non-reactive properties, but we
do not currently track this.

Technically, state[0] is reactive and state[1] is not. Currently, both
would be marked as reactive.
```js
const state = useState();
```
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33325).
* #33326
* __->__ #33325
* #32286
2025-05-22 16:14:05 -04:00
mofeiZ
13f20044f3 [compiler] Prepare HIRBuilder to be used by later passes (#32286)
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/32286).
* #33326
* #33325
* __->__ #32286
2025-05-22 16:13:50 -04:00
Sebastian Markbåge
8ce15b0f56 [Fizz] Apply View Transition Name and Class to SSR:ed View Transitions (#33332)
Stacked on #33330.

This walks the element tree to activate the various classes under
different scenarios. There are some edge case things that are a little
different since we can't express every scenario without virtual nodes.

The main thing that's still missing though is avoiding animating updates
if it can be contained to a layout or enter/exit/share if they're out of
the viewport. I.e. layout stuff.
2025-05-22 10:40:28 -04:00
Sebastian Markbåge
91ac1fea1a [Fizz] Pass batch as argument to revealCompletedBoundaries (#33330)
Follow up to #33293.

This solves a race condition when boundaries are added to the batch
after the `startViewTransition` call.

This doesn't matter yet but it will once we start assigning names before
the `startViewTransition` call.

A possible alternative solution might be to ensure the names are added
synchronously in the event that adds to the batch. It's possible to keep
adding to a batch until the snapshot has happened.
2025-05-22 10:25:13 -04:00
Sebastian Markbåge
08064ea671 [Fizz] Make ViewTransition enter/exit/share null the same as none (#33331)
I believe that these mean the same thing. We don't have to emit the
attribute if it's `none` for these cases because if there is no matching
scenario we won't apply the animation in this case.

The only case where we have to emit `none` in the attribute is for
`vt-update` because those can block updates from propagating upwards.
2025-05-22 10:21:28 -04:00
Sebastian Markbåge
99781d605b [Fizz] Track boundaries in future rows as postponed (#33329)
Follow up to #33321.

We can mark boundaries that were blocked in the prerender as postponed
but without anything to replayed inside them. That way they're not
emitted in the prerender but is unblocked when replayed.

Technically this does some unnecessary replaying of the path to the
otherwise already completed boundary but it simplifies our model by just
marking the boundary as needing replaying.
2025-05-22 10:20:13 -04:00
mofeiZ
459a2c4298 [compiler][gating] Experimental directive based gating (#33149)
Adds `dynamicGating` as an experimental option for testing rollout DX at
Meta. If specified, this enables dynamic gating which matches `use memo
if(...)` directives.

#### Example usage
Input file
```js
// @dynamicGating:{"source":"myModule"}
export function MyComponent() {
  'use memo if(isEnabled)';
   return <div>...</div>;
}
```
Compiler output
```js
import {isEnabled} from 'myModule';
export const MyComponent = isEnabled()
  ? <optimized version>
  : <original version>;
```
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33149).
* __->__ #33149
* #33148
2025-05-21 17:23:29 -04:00
Sebastian Markbåge
1c43d0aed7 Unify serverAct helpers (#33327)
This uses the richer `serverAct` helper that we already use in other
tests.

This avoids using the `Scheduler`. We don't use that package on the
server so it doesn't make sense to simulate going through it.
Additionally, we really should be getting rid of it on the client too to
favor `postTask` polyfills.
2025-05-21 16:13:54 -04:00
Jack Pope
1835b3f7d9 New children notify fragment instances in Fabric (#33093)
When a new child of a fragment instance is inserted, we need to notify
the instance to keep any relevant tracking up to date. For example, we
automatically observe the new child with any active
IntersectionObserver.

For mutable renderers (DOM), we reuse the existing traversal in
`commitPlacement` that does the insertions for HostComponents. Immutable
renderers (Fabric) exit this path before the traversal though, so
currently we can't notify the fragment instances.

Here I've created a separate traversal in `commitPlacement`,
specifically for immutable renders when `enableFragmentRefs` is on.
2025-05-21 15:47:47 -04:00
Sebastian Markbåge
f4041aa388 [Fizz] Unblock SuspenseList when prerendering (#33321)
There's an interesting case when a SuspenseList is partially prerendered
but some of the completed boundaries are blocked by rows to be resumed.

This handles it but just unblocking the future rows to avoid stalling.

However, the correct semantics will need special handling in the
postponed state.
2025-05-21 15:31:22 -04:00
Jack Pope
3710c4d4f9 Prevent errors from comment node roots with enableViewTransition (#33205)
We have many cases internally where the `containerInstance` resolves to
a comment node. `restoreRootViewTransitionName` is called when
`enableViewTransition` is on, even without introducing a
`<ViewTransition />`. So that means it can crash pages because
`containerInstance.style` is `undefined` just by turning on the flag.

This skips cancel/restore of root view transition name if a comment node is the root.
2025-05-21 13:57:35 -04:00
Sebastian Markbåge
2388481283 [Fizz] Set keyPath for SuspenseList (#33320)
I missed setting the `keyPath` because the `renderChildrenArray` that
this is forked from doesn't need to set a path but since this is
rendered from the `SuspenseList` element it needs it.
2025-05-20 21:08:47 -04:00
Sebastian Markbåge
9c7b10e22e [Fizz] Clean up row that was blocked by an aborted boundary (#33318)
Fixes a bug that we caused us to hang after an abort because we didn't
manage the ref count correctly.
2025-05-20 20:31:16 -04:00
Sebastian Markbåge
50389e1792 [Fizz] Hoist hoistables to each row and transfer the dependencies to future rows (#33312)
Stacked on #33311.

When a row contains Suspense boundaries that themselves depend on CSS,
they will not resolve until the CSS has loaded on the client. We need
future rows in a list to be blocked until this happens. We could do
something in the runtime but a simpler approach is to just add those CSS
dependencies to all those boundaries as well.

To do this, we first hoist the HoistableState from a completed boundary
onto its parent row. Then when the row finishes do we hoist it onto the
next row and onto any boundaries within that row.
2025-05-20 14:48:51 -04:00
Sebastian Markbåge
99aa685cef [Fizz] Support SuspenseList revealOrder="together" (#33311)
Stacked on #33308.

For "together" mode, we can be a self-blocking row that adds all its
boundaries to the blocked set, but there's no parent row that unblocks
it.

A particular quirk of this mode is that it's not enough to just unblock
them all on the server together. Because if one boundary downloads all
its html and then issues a complete instruction it'll appear before the
others while streaming in. What we actually want is to reveal them all
in a single batch.

This implementation takes a short cut by unblocking the rows in
`flushPartialBoundary`. That ensures that all the segments of every
boundary has a chance to flush before we start emitting any of the
complete boundary instructions. Once the last one unblocks, all the
complete boundary instructions are queued. Ideally this would be a
single `<script>` tag so that they can't be split up even if we get a
chunk containing some of them.

~A downside of this approach is that we always outline these boundaries.
We could inline them if they all complete before the parent flushes.
E.g. by checking if the row is blocked only by its own boundaries and if
all the boundaries would fit without getting outlined, then we can
inline them all at once.~ I went ahead and did this because it solves an
issue with `renderToString` where it doesn't support the script runtime
so it can only handle this if inlined.
2025-05-20 14:42:05 -04:00
Jan Kassens
d38c7e10d3 Remove leftover Rust script (#33314)
For now we removed Rust from the codebase, remove this leftover script.

Also remove some dupes and Rust related files from `.gitignore`.
2025-05-20 12:20:51 -04:00
Sebastian Markbåge
c4676e72a6 [Fizz] Handle nested SuspenseList (#33308)
Follow up to #33306.

If we're nested inside a SuspenseList and we have a row, then we can
point our last row to block the parent row and unblock the parent when
the last child unblocks.
2025-05-20 09:39:46 -04:00
Sebastian Markbåge
4c6967be29 [Fiber] Support AsyncIterable children in SuspenseList (#33299)
We support AsyncIterable (more so when it's a cached form like in coming
from Flight) as children.

This fixes some warnings and bugs when passed to SuspenseList.

Ideally SuspenseList with `tail="hidden"` should support unblocking
before the full result has resolved but that's an optimization on top.
We also might want to change semantics for this for
`revealOrder="backwards"` so it becomes possible to stream items in
reverse order.
2025-05-20 09:39:25 -04:00
Joseph Savona
c6c2a52ad8 [compiler] Fix error message for custom hooks (#33310)
We were printing "Custom" instead of "hook".
2025-05-19 15:29:58 -07:00
Sebastian Markbåge
5dc1b212c3 [Fizz] Support basic SuspenseList forwards/backwards revealOrder (#33306)
Basically we track a `SuspenseListRow` on the task. These keep track of
"pending tasks" that block the row. A row is blocked by:

- First itself completing rendering.
- A previous row completing.
- Any tasks inside the row and before the Suspense boundary inside the
row. This is mainly because we don't yet know if we'll discover more
SuspenseBoundaries.
- Previous row's SuspenseBoundaries completing.

If a boundary might get outlined, then we can't consider it completed
until we have written it because it determined whether other future
boundaries in the row can finish.

This is just handling basic semantics. Features not supported yet that
need follow ups later:

- CSS dependencies of previous rows should be added as dependencies of
future row's suspense boundary. Because otherwise if the client is
blocked on CSS then a previous row could be blocked but the server
doesn't know it.
- I need a second pass on nested SuspenseList semantics.
- `revealOrder="together"`
- `tail="hidden"`/`tail="collapsed"`. This needs some new runtime
semantics to the Fizz runtime and to allow the hydration to handle
missing rows in the HTML. This should also be future compatible with
AsyncIterable where we don't know how many rows upfront.
- Need to double check resuming semantics.

---------

Co-authored-by: Sebastian "Sebbie" Silbermann <silbermann.sebastian@gmail.com>
2025-05-19 15:16:42 -04:00
Jan Kassens
a3abf5f2f8 [eslint-plugin-react-hooks] add experimental_autoDependenciesHooks option (#33294) 2025-05-19 15:08:30 -04:00
1390 changed files with 61254 additions and 17934 deletions

View File

@@ -468,13 +468,14 @@ module.exports = {
files: ['packages/react-server-dom-webpack/**/*.js'],
globals: {
__webpack_chunk_load__: 'readonly',
__webpack_get_script_filename__: 'readonly',
__webpack_require__: 'readonly',
},
},
{
files: ['packages/react-server-dom-turbopack/**/*.js'],
globals: {
__turbopack_load__: 'readonly',
__turbopack_load_by_url__: 'readonly',
__turbopack_require__: 'readonly',
},
},
@@ -561,6 +562,7 @@ module.exports = {
ConsoleTask: 'readonly', // TOOD: Figure out what the official name of this will be.
ReturnType: 'readonly',
AnimationFrameID: 'readonly',
WeakRef: 'readonly',
// For Flow type annotation. Only `BigInt` is valid at runtime.
bigint: 'readonly',
BigInt: 'readonly',
@@ -607,10 +609,12 @@ module.exports = {
symbol: 'readonly',
SyntheticEvent: 'readonly',
SyntheticMouseEvent: 'readonly',
SyntheticPointerEvent: 'readonly',
Thenable: 'readonly',
TimeoutID: 'readonly',
WheelEventHandler: 'readonly',
FinalizationRegistry: 'readonly',
Exclude: 'readonly',
Omit: 'readonly',
Keyframe: 'readonly',
PropertyIndexedKeyframes: 'readonly',
@@ -620,6 +624,7 @@ module.exports = {
ScrollTimeline: 'readonly',
EventListenerOptionsOrUseCapture: 'readonly',
FocusOptions: 'readonly',
OptionalEffectTiming: 'readonly',
spyOnDev: 'readonly',
spyOnDevAndProd: 'readonly',

View File

@@ -11,6 +11,7 @@ permissions: {}
jobs:
check_access:
if: ${{ github.event.pull_request.draft == false }}
runs-on: ubuntu-latest
outputs:
is_member_or_collaborator: ${{ steps.check_is_member_or_collaborator.outputs.is_member_or_collaborator }}

View File

@@ -6,6 +6,12 @@ on:
pull_request:
paths-ignore:
- compiler/**
workflow_dispatch:
inputs:
commit_sha:
required: false
type: string
default: ''
permissions: {}
@@ -28,7 +34,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
- name: Check cache hit
uses: actions/cache/restore@v4
id: node_modules
@@ -69,7 +75,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
- name: Check cache hit
uses: actions/cache/restore@v4
id: node_modules
@@ -117,7 +123,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
- uses: actions/github-script@v7
id: set-matrix
with:
@@ -136,7 +142,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -166,7 +172,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -198,7 +204,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -254,7 +260,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -280,6 +286,37 @@ jobs:
if: steps.node_modules.outputs.cache-hit != 'true'
- run: yarn test ${{ matrix.params }} --ci --shard=${{ matrix.shard }}
# Hardcoded to improve parallelism
test-linter:
name: Test eslint-plugin-react-hooks
needs: [runtime_compiler_node_modules_cache]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: |
yarn.lock
compiler/yarn.lock
- name: Restore cached node_modules
uses: actions/cache@v4
id: node_modules
with:
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
- 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
# ----- BUILD -----
build_and_lint:
name: yarn build and lint
@@ -294,7 +331,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -389,7 +426,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -434,7 +471,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -462,7 +499,7 @@ jobs:
merge-multiple: true
- name: Display structure of build
run: ls -R build
- run: echo ${{ github.event.pull_request.head.sha || github.sha }} >> build/COMMIT_SHA
- run: echo ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }} >> build/COMMIT_SHA
- name: Scrape warning messages
run: |
mkdir -p ./build/__test_utils__
@@ -499,7 +536,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -539,7 +576,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -576,7 +613,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -617,7 +654,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -691,7 +728,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -748,7 +785,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -793,7 +830,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -842,7 +879,7 @@ jobs:
node ./scripts/print-warnings/print-warnings.js > build/__test_utils__/ReactAllWarnings.js
- name: Display structure of build for PR
run: ls -R build
- run: echo ${{ github.event.pull_request.head.sha || github.sha }} >> build/COMMIT_SHA
- run: echo ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }} >> build/COMMIT_SHA
- run: node ./scripts/tasks/danger
- name: Archive sizebot results
uses: actions/upload-artifact@v4

View File

@@ -11,6 +11,7 @@ permissions: {}
jobs:
check_access:
if: ${{ github.event.pull_request.draft == false }}
runs-on: ubuntu-latest
outputs:
is_member_or_collaborator: ${{ steps.check_is_member_or_collaborator.outputs.is_member_or_collaborator }}

View File

@@ -17,6 +17,17 @@ on:
description: 'Whether to notify the team on Discord when the release fails. Useful if this workflow is called from an automation.'
required: false
type: boolean
only_packages:
description: Packages to publish (space separated)
type: string
skip_packages:
description: Packages to NOT publish (space separated)
type: string
dry:
required: true
description: Dry run instead of publish?
type: boolean
default: true
secrets:
DISCORD_WEBHOOK_URL:
description: 'Discord webhook URL to notify on failure. Only required if enableFailureNotification is true.'
@@ -61,15 +72,41 @@ jobs:
if: steps.node_modules.outputs.cache-hit != 'true'
- run: yarn --cwd scripts/release install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- run: cp ./scripts/release/ci-npmrc ~/.npmrc
- run: |
GH_TOKEN=${{ secrets.GH_TOKEN }} scripts/release/prepare-release-from-ci.js --skipTests -r ${{ inputs.release_channel }} --commit=${{ inputs.commit_sha }}
cp ./scripts/release/ci-npmrc ~/.npmrc
scripts/release/publish.js --ci --tags ${{ inputs.dist_tag }}
- name: Check prepared files
run: ls -R build/node_modules
- if: '${{ inputs.only_packages }}'
name: 'Publish ${{ inputs.only_packages }}'
run: |
scripts/release/publish.js \
--ci \
--skipTests \
--tags=${{ inputs.dist_tag }} \
--onlyPackages=${{ inputs.only_packages }} ${{ (inputs.dry && '') || '\'}}
${{ inputs.dry && '--dry' || '' }}
- if: '${{ inputs.skip_packages }}'
name: 'Publish all packages EXCEPT ${{ inputs.skip_packages }}'
run: |
scripts/release/publish.js \
--ci \
--skipTests \
--tags=${{ inputs.dist_tag }} \
--skipPackages=${{ inputs.skip_packages }} ${{ (inputs.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' || '' }}
- 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

@@ -5,6 +5,25 @@ on:
inputs:
prerelease_commit_sha:
required: true
only_packages:
description: Packages to publish (space separated)
type: string
skip_packages:
description: Packages to NOT publish (space separated)
type: string
dry:
required: true
description: Dry run instead of publish?
type: boolean
default: true
experimental_only:
type: boolean
description: Only publish to the experimental tag
default: false
force_notify:
description: Force a Discord notification?
type: boolean
default: false
permissions: {}
@@ -12,8 +31,26 @@ env:
TZ: /usr/share/zoneinfo/America/Los_Angeles
jobs:
notify:
if: ${{ inputs.force_notify || inputs.dry == false || inputs.dry == 'false' }}
runs-on: ubuntu-latest
steps:
- name: Discord Webhook Action
uses: tsickert/discord-webhook@86dc739f3f165f16dadc5666051c367efa1692f4
with:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
embed-author-name: ${{ github.event.sender.login }}
embed-author-url: ${{ github.event.sender.html_url }}
embed-author-icon-url: ${{ github.event.sender.avatar_url }}
embed-title: "⚠️ Publishing ${{ inputs.experimental_only && 'EXPERIMENTAL' || 'CANARY & EXPERIMENTAL' }} release ${{ (inputs.dry && ' (dry run)') || '' }}"
embed-description: |
```json
${{ toJson(inputs) }}
```
embed-url: https://github.com/facebook/react/actions/runs/${{ github.run_id }}
publish_prerelease_canary:
if: ${{ !inputs.experimental_only }}
name: Publish to Canary channel
uses: facebook/react/.github/workflows/runtime_prereleases.yml@main
permissions:
@@ -33,6 +70,9 @@ jobs:
# downstream consumers might still expect that tag. We can remove this
# after some time has elapsed and the change has been communicated.
dist_tag: canary,next
only_packages: ${{ inputs.only_packages }}
skip_packages: ${{ inputs.skip_packages }}
dry: ${{ inputs.dry }}
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -48,10 +88,15 @@ jobs:
# different versions of the same package, even if they use different
# dist tags.
needs: publish_prerelease_canary
# Ensures the job runs even if canary is skipped
if: always()
with:
commit_sha: ${{ inputs.prerelease_commit_sha }}
release_channel: experimental
dist_tag: experimental
only_packages: ${{ inputs.only_packages }}
skip_packages: ${{ inputs.skip_packages }}
dry: ${{ inputs.dry }}
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -22,6 +22,7 @@ jobs:
release_channel: stable
dist_tag: canary,next
enableFailureNotification: true
dry: false
secrets:
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
@@ -43,6 +44,7 @@ jobs:
release_channel: experimental
dist_tag: experimental
enableFailureNotification: true
dry: false
secrets:
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

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

@@ -6,7 +6,10 @@ on:
- cron: '0 * * * *'
workflow_dispatch:
permissions: {}
permissions:
# https://github.com/actions/stale/tree/v9/?tab=readme-ov-file#recommended-permissions
issues: write
pull-requests: write
env:
TZ: /usr/share/zoneinfo/America/Los_Angeles

View File

@@ -3,13 +3,12 @@
const {esNextPaths} = require('./scripts/shared/pathsByLanguageVersion');
module.exports = {
plugins: ['prettier-plugin-hermes-parser'],
bracketSpacing: false,
singleQuote: true,
bracketSameLine: true,
trailingComma: 'es5',
printWidth: 80,
parser: 'hermes',
parser: 'flow',
arrowParens: 'avoid',
overrides: [
{

View File

@@ -1,3 +1,8 @@
## 19.1.1 (July 28, 2025)
### React
* Fixed Owner Stacks to work with ES2015 function.name semantics ([#33680](https://github.com/facebook/react/pull/33680) by @hoxyq)
## 19.1.0 (March 28, 2025)
### Owner Stack
@@ -19,11 +24,11 @@ An Owner Stack is a string representing the components that are directly respons
* Updated `useId` to use valid CSS selectors, changing format from `:r123:` to `«r123»`. [#32001](https://github.com/facebook/react/pull/32001)
* Added a dev-only warning for null/undefined created in useEffect, useInsertionEffect, and useLayoutEffect. [#32355](https://github.com/facebook/react/pull/32355)
* Fixed a bug where dev-only methods were exported in production builds. React.act is no longer available in production builds. [#32200](https://github.com/facebook/react/pull/32200)
* Improved consistency across prod and dev to improve compatibility with Google Closure Complier and bindings [#31808](https://github.com/facebook/react/pull/31808)
* Improved consistency across prod and dev to improve compatibility with Google Closure Compiler and bindings [#31808](https://github.com/facebook/react/pull/31808)
* Improve passive effect scheduling for consistent task yielding. [#31785](https://github.com/facebook/react/pull/31785)
* Fixed asserts in React Native when passChildrenWhenCloningPersistedNodes is enabled for OffscreenComponent rendering. [#32528](https://github.com/facebook/react/pull/32528)
* Fixed component name resolution for Portal [#32640](https://github.com/facebook/react/pull/32640)
* Added support for beforetoggle and toggle events on the dialog element. #32479 [#32479](https://github.com/facebook/react/pull/32479)
* Added support for beforetoggle and toggle events on the dialog element. [#32479](https://github.com/facebook/react/pull/32479)
### React DOM
* Fixed double warning when the `href` attribute is an empty string [#31783](https://github.com/facebook/react/pull/31783)

View File

@@ -8,6 +8,7 @@ module.exports = {
'@babel/plugin-syntax-jsx',
'@babel/plugin-transform-flow-strip-types',
['@babel/plugin-transform-class-properties', {loose: true}],
['@babel/plugin-transform-private-methods', {loose: true}],
'@babel/plugin-transform-classes',
],
presets: [

14
compiler/.gitignore vendored
View File

@@ -1,28 +1,14 @@
.DS_Store
.spr.yml
# Generated by Cargo
# will have compiled files and executables
debug/
target/
# These are backup files generated by rustfmt
**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
node_modules
.watchmanconfig
.watchman-cookie-*
dist
.vscode
!packages/playground/.vscode
.spr.yml
testfilter.txt
bundle-oss.sh
# forgive
*.vsix
.vscode-test

View File

@@ -1,5 +1,5 @@
import { c as _c } from "react/compiler-runtime"; // 
        @compilationMode:"all"
@compilationMode:"all"
function nonReactFn() {
  const $ = _c(1);
  let t0;

View File

@@ -11,6 +11,7 @@ import * as t from '@babel/types';
import BabelPluginReactCompiler, {
CompilerError,
CompilerErrorDetail,
CompilerDiagnostic,
Effect,
ErrorSeverity,
parseConfigPragmaForTests,
@@ -44,6 +45,7 @@ import {
PrintedCompilerPipelineValue,
} from './Output';
import {transformFromAstSync} from '@babel/core';
import {LoggerEvent} from 'babel-plugin-react-compiler/dist/Entrypoint';
function parseInput(
input: string,
@@ -140,9 +142,13 @@ const COMMON_HOOKS: Array<[string, Hook]> = [
],
];
function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
function compile(
source: string,
mode: 'compiler' | 'linter',
): [CompilerOutput, 'flow' | 'typescript'] {
const results = new Map<string, Array<PrintedCompilerPipelineValue>>();
const error = new CompilerError();
const otherErrors: Array<CompilerErrorDetail | CompilerDiagnostic> = [];
const upsert: (result: PrintedCompilerPipelineValue) => void = result => {
const entry = results.get(result.name);
if (Array.isArray(entry)) {
@@ -201,6 +207,23 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
};
const parsedOptions = parseConfigPragmaForTests(pragma, {
compilationMode: 'infer',
environment:
mode === 'linter'
? {
// enabled in compiler
validateRefAccessDuringRender: false,
// enabled in linter
validateNoSetStateInRender: true,
validateNoSetStateInEffects: true,
validateNoJSXInTryStatements: true,
validateNoImpureFunctionsInRender: true,
validateStaticComponents: true,
validateNoFreezingKnownMutableFunctions: true,
validateNoVoidUseMemo: true,
}
: {
/* use defaults for compiler mode */
},
});
const opts: PluginOptions = parsePluginOptions({
...parsedOptions,
@@ -210,7 +233,11 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
},
logger: {
debugLogIRs: logIR,
logEvent: () => {},
logEvent: (_filename: string | null, event: LoggerEvent) => {
if (event.kind === 'CompileError') {
otherErrors.push(event.detail);
}
},
},
});
transformOutput = invokeCompiler(source, language, opts);
@@ -220,7 +247,7 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
* (i.e. object shape that is not CompilerError)
*/
if (err instanceof CompilerError && err.details.length > 0) {
error.details.push(...err.details);
error.merge(err);
} else {
/**
* Handle unexpected failures by logging (to get a stack trace)
@@ -237,10 +264,17 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
);
}
}
if (error.hasErrors()) {
return [{kind: 'err', results, error: error}, language];
// Only include logger errors if there weren't other errors
if (!error.hasErrors() && otherErrors.length !== 0) {
otherErrors.forEach(e => error.details.push(e));
}
return [{kind: 'ok', results, transformOutput}, language];
if (error.hasErrors()) {
return [{kind: 'err', results, error}, language];
}
return [
{kind: 'ok', results, transformOutput, errors: error.details},
language,
];
}
export default function Editor(): JSX.Element {
@@ -249,7 +283,11 @@ export default function Editor(): JSX.Element {
const dispatchStore = useStoreDispatch();
const {enqueueSnackbar} = useSnackbar();
const [compilerOutput, language] = useMemo(
() => compile(deferredStore.source),
() => compile(deferredStore.source, 'compiler'),
[deferredStore.source],
);
const [linterOutput] = useMemo(
() => compile(deferredStore.source, 'linter'),
[deferredStore.source],
);
@@ -275,19 +313,26 @@ export default function Editor(): JSX.Element {
});
});
let mergedOutput: CompilerOutput;
let errors: Array<CompilerErrorDetail | CompilerDiagnostic>;
if (compilerOutput.kind === 'ok') {
errors = linterOutput.kind === 'ok' ? [] : linterOutput.error.details;
mergedOutput = {
...compilerOutput,
errors,
};
} else {
mergedOutput = compilerOutput;
errors = compilerOutput.error.details;
}
return (
<>
<div className="relative flex basis top-14">
<div className={clsx('relative sm:basis-1/4')}>
<Input
language={language}
errors={
compilerOutput.kind === 'err' ? compilerOutput.error.details : []
}
/>
<Input language={language} errors={errors} />
</div>
<div className={clsx('flex sm:flex flex-wrap')}>
<Output store={deferredStore} compilerOutput={compilerOutput} />
<Output store={deferredStore} compilerOutput={mergedOutput} />
</div>
</div>
</>

View File

@@ -36,13 +36,18 @@ export default function Input({errors, language}: Props): JSX.Element {
const uri = monaco.Uri.parse(`file:///index.js`);
const model = monaco.editor.getModel(uri);
invariant(model, 'Model must exist for the selected input file.');
renderReactCompilerMarkers({monaco, model, details: errors});
renderReactCompilerMarkers({
monaco,
model,
details: errors,
source: store.source,
});
/**
* N.B. that `tabSize` is a model property, not an editor property.
* So, the tab size has to be set per model.
*/
model.updateOptions({tabSize: 2});
}, [monaco, errors]);
}, [monaco, errors, store.source]);
useEffect(() => {
/**

View File

@@ -11,7 +11,11 @@ import {
InformationCircleIcon,
} from '@heroicons/react/outline';
import MonacoEditor, {DiffEditor} from '@monaco-editor/react';
import {type CompilerError} from 'babel-plugin-react-compiler';
import {
CompilerErrorDetail,
CompilerDiagnostic,
type CompilerError,
} from 'babel-plugin-react-compiler';
import parserBabel from 'prettier/plugins/babel';
import * as prettierPluginEstree from 'prettier/plugins/estree';
import * as prettier from 'prettier/standalone';
@@ -44,6 +48,7 @@ export type CompilerOutput =
kind: 'ok';
transformOutput: CompilerTransformOutput;
results: Map<string, Array<PrintedCompilerPipelineValue>>;
errors: Array<CompilerErrorDetail | CompilerDiagnostic>;
}
| {
kind: 'err';
@@ -123,10 +128,36 @@ async function tabify(
parser: transformOutput.language === 'flow' ? 'babel-flow' : 'babel-ts',
plugins: [parserBabel, prettierPluginEstree],
});
let output: string;
let language: string;
if (compilerOutput.errors.length === 0) {
output = code;
language = 'javascript';
} else {
language = 'markdown';
output = `
# Summary
React Compiler compiled this function successfully, but there are lint errors that indicate potential issues with the original code.
## ${compilerOutput.errors.length} Lint Errors
${compilerOutput.errors.map(e => e.printErrorMessage(source, {eslint: false})).join('\n\n')}
## Output
\`\`\`js
${code}
\`\`\`
`.trim();
}
reorderedTabs.set(
'JS',
'Output',
<TextTabContent
output={code}
output={output}
language={language}
diff={null}
showInfoPanel={false}></TextTabContent>,
);
@@ -142,6 +173,18 @@ async function tabify(
</>,
);
}
} else if (compilerOutput.kind === 'err') {
const errors = compilerOutput.error.printErrorMessage(source, {
eslint: false,
});
reorderedTabs.set(
'Output',
<TextTabContent
output={errors}
language="markdown"
diff={null}
showInfoPanel={false}></TextTabContent>,
);
}
tabs.forEach((tab, name) => {
reorderedTabs.set(name, tab);
@@ -162,17 +205,32 @@ function getSourceMapUrl(code: string, map: string): string | null {
}
function Output({store, compilerOutput}: Props): JSX.Element {
const [tabsOpen, setTabsOpen] = useState<Set<string>>(() => new Set(['JS']));
const [tabsOpen, setTabsOpen] = useState<Set<string>>(
() => new Set(['Output']),
);
const [tabs, setTabs] = useState<Map<string, React.ReactNode>>(
() => new Map(),
);
/*
* Update the active tab back to the output or errors tab when the compilation state
* changes between success/failure.
*/
const [previousOutputKind, setPreviousOutputKind] = useState(
compilerOutput.kind,
);
if (compilerOutput.kind !== previousOutputKind) {
setPreviousOutputKind(compilerOutput.kind);
setTabsOpen(new Set(['Output']));
}
useEffect(() => {
tabify(store.source, compilerOutput).then(tabs => {
setTabs(tabs);
});
}, [store.source, compilerOutput]);
const changedPasses: Set<string> = new Set(['JS', 'HIR']); // Initial and final passes should always be bold
const changedPasses: Set<string> = new Set(['Output', 'HIR']); // Initial and final passes should always be bold
let lastResult: string = '';
for (const [passName, results] of compilerOutput.results) {
for (const result of results) {
@@ -196,20 +254,6 @@ function Output({store, compilerOutput}: Props): JSX.Element {
tabs={tabs}
changedPasses={changedPasses}
/>
{compilerOutput.kind === 'err' ? (
<div
className="flex flex-wrap absolute bottom-0 bg-white grow border-y border-grey-200 transition-all ease-in"
style={{width: 'calc(100vw - 650px)'}}>
<div className="w-full p-4 basis-full border-b">
<h2>COMPILER ERRORS</h2>
</div>
<pre
className="p-4 basis-full text-red-600 overflow-y-scroll whitespace-pre-wrap"
style={{width: 'calc(100vw - 650px)', height: '150px'}}>
<code>{compilerOutput.error.toString()}</code>
</pre>
</div>
) : null}
</>
);
}
@@ -218,10 +262,12 @@ function TextTabContent({
output,
diff,
showInfoPanel,
language,
}: {
output: string;
diff: string | null;
showInfoPanel: boolean;
language: string;
}): JSX.Element {
const [diffMode, setDiffMode] = useState(false);
return (
@@ -272,7 +318,7 @@ function TextTabContent({
/>
) : (
<MonacoEditor
defaultLanguage="javascript"
language={language ?? 'javascript'}
value={output}
options={{
...monacoOptions,

View File

@@ -28,5 +28,5 @@ export const monacoOptions: Partial<EditorProps['options']> = {
automaticLayout: true,
wordWrap: 'on',
wrappingIndent: 'deepIndent',
wrappingIndent: 'same',
};

View File

@@ -6,7 +6,11 @@
*/
import {Monaco} from '@monaco-editor/react';
import {CompilerErrorDetail, ErrorSeverity} from 'babel-plugin-react-compiler';
import {
CompilerDiagnostic,
CompilerErrorDetail,
ErrorSeverity,
} from 'babel-plugin-react-compiler';
import {MarkerSeverity, type editor} from 'monaco-editor';
function mapReactCompilerSeverityToMonaco(
@@ -22,38 +26,46 @@ function mapReactCompilerSeverityToMonaco(
}
function mapReactCompilerDiagnosticToMonacoMarker(
detail: CompilerErrorDetail,
detail: CompilerErrorDetail | CompilerDiagnostic,
monaco: Monaco,
source: string,
): editor.IMarkerData | null {
if (detail.loc == null || typeof detail.loc === 'symbol') {
const loc = detail.primaryLocation();
if (loc == null || typeof loc === 'symbol') {
return null;
}
const severity = mapReactCompilerSeverityToMonaco(detail.severity, monaco);
let message = detail.printErrorMessage();
let message = detail.printErrorMessage(source, {eslint: true});
return {
severity,
message,
startLineNumber: detail.loc.start.line,
startColumn: detail.loc.start.column + 1,
endLineNumber: detail.loc.end.line,
endColumn: detail.loc.end.column + 1,
startLineNumber: loc.start.line,
startColumn: loc.start.column + 1,
endLineNumber: loc.end.line,
endColumn: loc.end.column + 1,
};
}
type ReactCompilerMarkerConfig = {
monaco: Monaco;
model: editor.ITextModel;
details: Array<CompilerErrorDetail>;
details: Array<CompilerErrorDetail | CompilerDiagnostic>;
source: string;
};
let decorations: Array<string> = [];
export function renderReactCompilerMarkers({
monaco,
model,
details,
source,
}: ReactCompilerMarkerConfig): void {
const markers: Array<editor.IMarkerData> = [];
for (const detail of details) {
const marker = mapReactCompilerDiagnosticToMonacoMarker(detail, monaco);
const marker = mapReactCompilerDiagnosticToMonacoMarker(
detail,
monaco,
source,
);
if (marker == null) {
continue;
}

View File

@@ -8,8 +8,8 @@ set -eo pipefail
HERE=$(pwd)
cd ../../packages/react-compiler-runtime && yarn --silent link && cd $HERE
cd ../../packages/babel-plugin-react-compiler && yarn --silent link && cd $HERE
cd ../../packages/react-compiler-runtime && yarn --silent link && cd "$HERE"
cd ../../packages/babel-plugin-react-compiler && yarn --silent link && cd "$HERE"
yarn --silent link babel-plugin-react-compiler
yarn --silent link react-compiler-runtime

View File

@@ -12,6 +12,7 @@ import {
pipelineUsesReanimatedPlugin,
} from '../Entrypoint/Reanimated';
import validateNoUntransformedReferences from '../Entrypoint/ValidateNoUntransformedReferences';
import {CompilerError} from '..';
const ENABLE_REACT_COMPILER_TIMINGS =
process.env['ENABLE_REACT_COMPILER_TIMINGS'] === '1';
@@ -34,51 +35,58 @@ export default function BabelPluginReactCompiler(
*/
Program: {
enter(prog, pass): void {
const filename = pass.filename ?? 'unknown';
if (ENABLE_REACT_COMPILER_TIMINGS === true) {
performance.mark(`${filename}:start`, {
detail: 'BabelPlugin:Program:start',
});
}
let opts = parsePluginOptions(pass.opts);
const isDev =
(typeof __DEV__ !== 'undefined' && __DEV__ === true) ||
process.env['NODE_ENV'] === 'development';
if (
opts.enableReanimatedCheck === true &&
pipelineUsesReanimatedPlugin(pass.file.opts.plugins)
) {
opts = injectReanimatedFlag(opts);
}
if (
opts.environment.enableResetCacheOnSourceFileChanges !== false &&
isDev
) {
opts = {
...opts,
environment: {
...opts.environment,
enableResetCacheOnSourceFileChanges: true,
},
};
}
const result = compileProgram(prog, {
opts,
filename: pass.filename ?? null,
comments: pass.file.ast.comments ?? [],
code: pass.file.code,
});
validateNoUntransformedReferences(
prog,
pass.filename ?? null,
opts.logger,
opts.environment,
result,
);
if (ENABLE_REACT_COMPILER_TIMINGS === true) {
performance.mark(`${filename}:end`, {
detail: 'BabelPlugin:Program:end',
try {
const filename = pass.filename ?? 'unknown';
if (ENABLE_REACT_COMPILER_TIMINGS === true) {
performance.mark(`${filename}:start`, {
detail: 'BabelPlugin:Program:start',
});
}
let opts = parsePluginOptions(pass.opts);
const isDev =
(typeof __DEV__ !== 'undefined' && __DEV__ === true) ||
process.env['NODE_ENV'] === 'development';
if (
opts.enableReanimatedCheck === true &&
pipelineUsesReanimatedPlugin(pass.file.opts.plugins)
) {
opts = injectReanimatedFlag(opts);
}
if (
opts.environment.enableResetCacheOnSourceFileChanges !== false &&
isDev
) {
opts = {
...opts,
environment: {
...opts.environment,
enableResetCacheOnSourceFileChanges: true,
},
};
}
const result = compileProgram(prog, {
opts,
filename: pass.filename ?? null,
comments: pass.file.ast.comments ?? [],
code: pass.file.code,
});
validateNoUntransformedReferences(
prog,
pass.filename ?? null,
opts.logger,
opts.environment,
result,
);
if (ENABLE_REACT_COMPILER_TIMINGS === true) {
performance.mark(`${filename}:end`, {
detail: 'BabelPlugin:Program:end',
});
}
} catch (e) {
if (e instanceof CompilerError) {
throw e.withPrintedMessage(pass.file.code, {eslint: false});
}
throw e;
}
},
exit(_, pass): void {

View File

@@ -5,6 +5,8 @@
* LICENSE file in the root directory of this source tree.
*/
import * as t from '@babel/types';
import {codeFrameColumns} from '@babel/code-frame';
import type {SourceLocation} from './HIR';
import {Err, Ok, Result} from './Utils/Result';
import {assertExhaustive} from './Utils/utils';
@@ -15,6 +17,11 @@ export enum ErrorSeverity {
* misunderstanding on the users part.
*/
InvalidJS = 'InvalidJS',
/**
* JS syntax that is not supported and which we do not plan to support. Developers should
* rewrite to use supported forms.
*/
UnsupportedJS = 'UnsupportedJS',
/**
* Code that breaks the rules of React.
*/
@@ -39,6 +46,29 @@ export enum ErrorSeverity {
Invariant = 'Invariant',
}
export type CompilerDiagnosticOptions = {
category: ErrorCategory;
severity: ErrorSeverity;
reason: string;
description: string;
details: Array<CompilerDiagnosticDetail>;
suggestions?: Array<CompilerSuggestion> | null | undefined;
};
export type CompilerDiagnosticDetail =
/**
* A/the source of the error
*/
| {
kind: 'error';
loc: SourceLocation | null;
message: string;
}
| {
kind: 'hint';
message: string;
};
export enum CompilerSuggestionOperation {
InsertBefore,
InsertAfter,
@@ -62,13 +92,124 @@ export type CompilerSuggestion =
};
export type CompilerErrorDetailOptions = {
category: ErrorCategory;
severity: ErrorSeverity;
reason: string;
description?: string | null | undefined;
severity: ErrorSeverity;
loc: SourceLocation | null;
suggestions?: Array<CompilerSuggestion> | null | undefined;
};
export type PrintErrorMessageOptions = {
/**
* ESLint uses 1-indexed columns and prints one error at a time
* So it doesn't require the "Found # error(s)" text
*/
eslint: boolean;
};
export class CompilerDiagnostic {
options: CompilerDiagnosticOptions;
constructor(options: CompilerDiagnosticOptions) {
this.options = options;
}
static create(
options: Omit<CompilerDiagnosticOptions, 'details'>,
): CompilerDiagnostic {
return new CompilerDiagnostic({...options, details: []});
}
get reason(): CompilerDiagnosticOptions['reason'] {
return this.options.reason;
}
get description(): CompilerDiagnosticOptions['description'] {
return this.options.description;
}
get severity(): CompilerDiagnosticOptions['severity'] {
return this.options.severity;
}
get suggestions(): CompilerDiagnosticOptions['suggestions'] {
return this.options.suggestions;
}
get category(): ErrorCategory {
return this.options.category;
}
withDetail(detail: CompilerDiagnosticDetail): CompilerDiagnostic {
this.options.details.push(detail);
return this;
}
primaryLocation(): SourceLocation | null {
const firstErrorDetail = this.options.details.filter(
d => d.kind === 'error',
)[0];
return firstErrorDetail != null && firstErrorDetail.kind === 'error'
? firstErrorDetail.loc
: null;
}
printErrorMessage(source: string, options: PrintErrorMessageOptions): string {
const buffer = [
printErrorSummary(this.severity, this.reason),
'\n\n',
this.description,
];
for (const detail of this.options.details) {
switch (detail.kind) {
case 'error': {
const loc = detail.loc;
if (loc == null || typeof loc === 'symbol') {
continue;
}
let codeFrame: string;
try {
codeFrame = printCodeFrame(source, loc, detail.message);
} catch (e) {
codeFrame = detail.message;
}
buffer.push('\n\n');
if (loc.filename != null) {
const line = loc.start.line;
const column = options.eslint
? loc.start.column + 1
: loc.start.column;
buffer.push(`${loc.filename}:${line}:${column}\n`);
}
buffer.push(codeFrame);
break;
}
case 'hint': {
buffer.push('\n\n');
buffer.push(detail.message);
break;
}
default: {
assertExhaustive(
detail,
`Unexpected detail kind ${(detail as any).kind}`,
);
}
}
}
return buffer.join('');
}
toString(): string {
const buffer = [printErrorSummary(this.severity, this.reason)];
if (this.description != null) {
buffer.push(`. ${this.description}.`);
}
const loc = this.primaryLocation();
if (loc != null && typeof loc !== 'symbol') {
buffer.push(` (${loc.start.line}:${loc.start.column})`);
}
return buffer.join('');
}
}
/*
* Each bailout or invariant in HIR lowering creates an {@link CompilerErrorDetail}, which is then
* aggregated into a single {@link CompilerError} later.
@@ -95,35 +236,66 @@ export class CompilerErrorDetail {
get suggestions(): CompilerErrorDetailOptions['suggestions'] {
return this.options.suggestions;
}
get category(): ErrorCategory {
return this.options.category;
}
printErrorMessage(): string {
const buffer = [`${this.severity}: ${this.reason}`];
primaryLocation(): SourceLocation | null {
return this.loc;
}
printErrorMessage(source: string, options: PrintErrorMessageOptions): string {
const buffer = [printErrorSummary(this.severity, this.reason)];
if (this.description != null) {
buffer.push(`. ${this.description}`);
buffer.push(`\n\n${this.description}.`);
}
if (this.loc != null && typeof this.loc !== 'symbol') {
buffer.push(` (${this.loc.start.line}:${this.loc.end.line})`);
const loc = this.loc;
if (loc != null && typeof loc !== 'symbol') {
let codeFrame: string;
try {
codeFrame = printCodeFrame(source, loc, this.reason);
} catch (e) {
codeFrame = '';
}
buffer.push(`\n\n`);
if (loc.filename != null) {
const line = loc.start.line;
const column = options.eslint ? loc.start.column + 1 : loc.start.column;
buffer.push(`${loc.filename}:${line}:${column}\n`);
}
buffer.push(codeFrame);
buffer.push('\n\n');
}
return buffer.join('');
}
toString(): string {
return this.printErrorMessage();
const buffer = [printErrorSummary(this.severity, this.reason)];
if (this.description != null) {
buffer.push(`. ${this.description}.`);
}
const loc = this.loc;
if (loc != null && typeof loc !== 'symbol') {
buffer.push(` (${loc.start.line}:${loc.start.column})`);
}
return buffer.join('');
}
}
export class CompilerError extends Error {
details: Array<CompilerErrorDetail> = [];
details: Array<CompilerErrorDetail | CompilerDiagnostic> = [];
printedMessage: string | null = null;
static invariant(
condition: unknown,
options: Omit<CompilerErrorDetailOptions, 'severity'>,
options: Omit<CompilerErrorDetailOptions, 'severity' | 'category'>,
): asserts condition {
if (!condition) {
const errors = new CompilerError();
errors.pushErrorDetail(
new CompilerErrorDetail({
...options,
category: ErrorCategory.Invariant,
severity: ErrorSeverity.Invariant,
}),
);
@@ -131,24 +303,35 @@ export class CompilerError extends Error {
}
}
static throwDiagnostic(options: CompilerDiagnosticOptions): never {
const errors = new CompilerError();
errors.pushDiagnostic(new CompilerDiagnostic(options));
throw errors;
}
static throwTodo(
options: Omit<CompilerErrorDetailOptions, 'severity'>,
options: Omit<CompilerErrorDetailOptions, 'severity' | 'category'>,
): never {
const errors = new CompilerError();
errors.pushErrorDetail(
new CompilerErrorDetail({...options, severity: ErrorSeverity.Todo}),
new CompilerErrorDetail({
...options,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
}),
);
throw errors;
}
static throwInvalidJS(
options: Omit<CompilerErrorDetailOptions, 'severity'>,
options: Omit<CompilerErrorDetailOptions, 'severity' | 'category'>,
): never {
const errors = new CompilerError();
errors.pushErrorDetail(
new CompilerErrorDetail({
...options,
severity: ErrorSeverity.InvalidJS,
category: ErrorCategory.Syntax,
}),
);
throw errors;
@@ -168,13 +351,14 @@ export class CompilerError extends Error {
}
static throwInvalidConfig(
options: Omit<CompilerErrorDetailOptions, 'severity'>,
options: Omit<CompilerErrorDetailOptions, 'severity' | 'category'>,
): never {
const errors = new CompilerError();
errors.pushErrorDetail(
new CompilerErrorDetail({
...options,
severity: ErrorSeverity.InvalidConfig,
category: ErrorCategory.Config,
}),
);
throw errors;
@@ -193,20 +377,52 @@ export class CompilerError extends Error {
}
override get message(): string {
return this.toString();
return this.printedMessage ?? this.toString();
}
override set message(_message: string) {}
override toString(): string {
if (this.printedMessage) {
return this.printedMessage;
}
if (Array.isArray(this.details)) {
return this.details.map(detail => detail.toString()).join('\n\n');
}
return this.name;
}
withPrintedMessage(
source: string,
options: PrintErrorMessageOptions,
): CompilerError {
this.printedMessage = this.printErrorMessage(source, options);
return this;
}
printErrorMessage(source: string, options: PrintErrorMessageOptions): string {
if (options.eslint && this.details.length === 1) {
return this.details[0].printErrorMessage(source, options);
}
return (
`Found ${this.details.length} error${this.details.length === 1 ? '' : 's'}:\n\n` +
this.details
.map(detail => detail.printErrorMessage(source, options).trim())
.join('\n\n')
);
}
merge(other: CompilerError): void {
this.details.push(...other.details);
}
pushDiagnostic(diagnostic: CompilerDiagnostic): void {
this.details.push(diagnostic);
}
push(options: CompilerErrorDetailOptions): CompilerErrorDetail {
const detail = new CompilerErrorDetail({
category: options.category,
reason: options.reason,
description: options.description ?? null,
severity: options.severity,
@@ -241,13 +457,388 @@ export class CompilerError extends Error {
case ErrorSeverity.InvalidJS:
case ErrorSeverity.InvalidReact:
case ErrorSeverity.InvalidConfig:
case ErrorSeverity.UnsupportedJS: {
return true;
}
case ErrorSeverity.CannotPreserveMemoization:
case ErrorSeverity.Todo:
case ErrorSeverity.Todo: {
return false;
default:
}
default: {
assertExhaustive(detail.severity, 'Unhandled error severity');
}
}
});
}
}
function printCodeFrame(
source: string,
loc: t.SourceLocation,
message: string,
): string {
return codeFrameColumns(
source,
{
start: {
line: loc.start.line,
column: loc.start.column + 1,
},
end: {
line: loc.end.line,
column: loc.end.column + 1,
},
},
{
message,
},
);
}
function printErrorSummary(severity: ErrorSeverity, message: string): string {
let severityCategory: string;
switch (severity) {
case ErrorSeverity.InvalidConfig:
case ErrorSeverity.InvalidJS:
case ErrorSeverity.InvalidReact:
case ErrorSeverity.UnsupportedJS: {
severityCategory = 'Error';
break;
}
case ErrorSeverity.CannotPreserveMemoization: {
severityCategory = 'Memoization';
break;
}
case ErrorSeverity.Invariant: {
severityCategory = 'Invariant';
break;
}
case ErrorSeverity.Todo: {
severityCategory = 'Todo';
break;
}
default: {
assertExhaustive(severity, `Unexpected severity '${severity}'`);
}
}
return `${severityCategory}: ${message}`;
}
/**
* See getRuleForCategory() for how these map to ESLint rules
*/
export enum ErrorCategory {
// Checking for valid hooks usage (non conditional, non-first class, non reactive, etc)
Hooks = 'Hooks',
// Checking for no capitalized calls (not definitively an error, hence separating)
CapitalizedCalls = 'CapitalizedCalls',
// Checking for static components
StaticComponents = 'StaticComponents',
// Checking for valid usage of manual memoization
UseMemo = 'UseMemo',
// Checks that manual memoization is preserved
PreserveManualMemo = 'PreserveManualMemo',
// Checking for no mutations of props, hook arguments, hook return values
Immutability = 'Immutability',
// Checking for assignments to globals
Globals = 'Globals',
// Checking for valid usage of refs, ie no access during render
Refs = 'Refs',
// Checks for memoized effect deps
EffectDependencies = 'EffectDependencies',
// Checks for no setState in effect bodies
EffectSetState = 'EffectSetState',
EffectDerivationsOfState = 'EffectDerivationsOfState',
// Validates against try/catch in place of error boundaries
ErrorBoundaries = 'ErrorBoundaries',
// Checking for pure functions
Purity = 'Purity',
// Validates against setState in render
RenderSetState = 'RenderSetState',
// Internal invariants
Invariant = 'Invariant',
// Todos
Todo = 'Todo',
// Syntax errors
Syntax = 'Syntax',
// Checks for use of unsupported syntax
UnsupportedSyntax = 'UnsupportedSyntax',
// Config errors
Config = 'Config',
// Gating error
Gating = 'Gating',
// Suppressions
Suppression = 'Suppression',
// Issues with auto deps
AutomaticEffectDependencies = 'AutomaticEffectDependencies',
// Issues with `fire`
Fire = 'Fire',
// fbt-specific issues
FBT = 'FBT',
}
export type LintRule = {
// Stores the category the rule corresponds to, used to filter errors when reporting
category: ErrorCategory;
/**
* The "name" of the rule as it will be used by developers to enable/disable, eg
* "eslint-disable-nest line <name>"
*/
name: string;
/**
* A description of the rule that appears somewhere in ESLint. This does not affect
* how error messages are formatted
*/
description: string;
/**
* If true, this rule will automatically appear in the default, "recommended" ESLint
* rule set. Otherwise it will be part of an `allRules` export that developers can
* use to opt-in to showing output of all possible rules.
*
* NOTE: not all validations are enabled by default! Setting this flag only affects
* whether a given rule is part of the recommended set. The corresponding validation
* also should be enabled by default if you want the error to actually show up!
*/
recommended: boolean;
};
export function getRuleForCategory(category: ErrorCategory): LintRule {
switch (category) {
case ErrorCategory.AutomaticEffectDependencies: {
return {
category,
name: 'automatic-effect-dependencies',
description:
'Verifies that automatic effect dependencies are compiled if opted-in',
recommended: true,
};
}
case ErrorCategory.CapitalizedCalls: {
return {
category,
name: 'capitalized-calls',
description:
'Validates against calling capitalized functions/methods instead of using JSX',
recommended: false,
};
}
case ErrorCategory.Config: {
return {
category,
name: 'config',
description: 'Validates the configuration',
recommended: true,
};
}
case ErrorCategory.EffectDependencies: {
return {
category,
name: 'memoized-effect-dependencies',
description: 'Validates that effect dependencies are memoized',
recommended: false,
};
}
case ErrorCategory.EffectDerivationsOfState: {
return {
category,
name: 'no-deriving-state-in-effects',
description:
'Validates against deriving values from state in an effect',
recommended: false,
};
}
case ErrorCategory.EffectSetState: {
return {
category,
name: 'set-state-in-effect',
description:
'Validates against calling setState synchronously in an effect',
recommended: true,
};
}
case ErrorCategory.ErrorBoundaries: {
return {
category,
name: 'error-boundaries',
description:
'Validates usage of error boundaries instead of try/catch for errors in JSX',
recommended: true,
};
}
case ErrorCategory.FBT: {
return {
category,
name: 'fbt',
description: 'Validates usage of fbt',
recommended: false,
};
}
case ErrorCategory.Fire: {
return {
category,
name: 'fire',
description: 'Validates usage of `fire`',
recommended: false,
};
}
case ErrorCategory.Gating: {
return {
category,
name: 'gating',
description: 'Validates configuration of gating mode',
recommended: true,
};
}
case ErrorCategory.Globals: {
return {
category,
name: 'globals',
description:
'Validates against assignment/mutation of globals during render',
recommended: true,
};
}
case ErrorCategory.Hooks: {
return {
category,
name: 'hooks',
description: 'Validates the rules of hooks',
recommended: true,
};
}
case ErrorCategory.Immutability: {
return {
category,
name: 'immutability',
description:
'Validates that immutable values (props, state, etc) are not mutated',
recommended: true,
};
}
case ErrorCategory.Invariant: {
return {
category,
name: 'invariant',
description: 'Internal invariants',
recommended: false,
};
}
case ErrorCategory.PreserveManualMemo: {
return {
category,
name: 'preserve-manual-memoization',
description:
'Validates that existing manual memoized is preserved by the compiler',
recommended: true,
};
}
case ErrorCategory.Purity: {
return {
category,
name: 'purity',
description:
'Validates that the component/hook is pure, and does not call known-impure functions',
recommended: true,
};
}
case ErrorCategory.Refs: {
return {
category,
name: 'refs',
description:
'Validates correct usage of refs, not reading/writing during render',
recommended: true,
};
}
case ErrorCategory.RenderSetState: {
return {
category,
name: 'set-state-in-render',
description: 'Validates against setting state during render',
recommended: true,
};
}
case ErrorCategory.StaticComponents: {
return {
category,
name: 'static-components',
description:
'Validates that components are static, not recreated every render',
recommended: true,
};
}
case ErrorCategory.Suppression: {
return {
category,
name: 'rule-suppression',
description: 'Validates against suppression of other rules',
recommended: false,
};
}
case ErrorCategory.Syntax: {
return {
category,
name: 'syntax',
description: 'Validates against invalid syntax',
recommended: false,
};
}
case ErrorCategory.Todo: {
return {
category,
name: 'todo',
description: 'Unimplemented features',
recommended: false,
};
}
case ErrorCategory.UnsupportedSyntax: {
return {
category,
name: 'unsupported-syntax',
description: 'Validates against syntax that we do not plan to support',
recommended: true,
};
}
case ErrorCategory.UseMemo: {
return {
category,
name: 'use-memo',
description: 'Validates usage of the useMemo() hook',
recommended: true,
};
}
default: {
assertExhaustive(category, `Unsupported category ${category}`);
}
}
}
export const LintRules: Array<LintRule> = Object.keys(ErrorCategory).map(
category => getRuleForCategory(category as any),
);

View File

@@ -9,7 +9,7 @@ import {NodePath} from '@babel/core';
import * as t from '@babel/types';
import {Scope as BabelScope} from '@babel/traverse';
import {CompilerError, ErrorSeverity} from '../CompilerError';
import {CompilerError, ErrorCategory, ErrorSeverity} from '../CompilerError';
import {
EnvironmentConfig,
GeneratedSource,
@@ -38,6 +38,7 @@ export function validateRestrictedImports(
ImportDeclaration(importDeclPath) {
if (restrictedImports.has(importDeclPath.node.source.value)) {
error.push({
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
reason: 'Bailing out due to blocklisted import',
description: `Import from module ${importDeclPath.node.source.value}`,
@@ -205,6 +206,7 @@ export class ProgramContext {
}
const error = new CompilerError();
error.push({
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
reason: 'Encountered conflicting global in generated program',
description: `Conflict from local binding ${name}`,

View File

@@ -7,7 +7,12 @@
import * as t from '@babel/types';
import {z} from 'zod';
import {CompilerError, CompilerErrorDetailOptions} from '../CompilerError';
import {
CompilerDiagnostic,
CompilerError,
CompilerErrorDetail,
CompilerErrorDetailOptions,
} from '../CompilerError';
import {
EnvironmentConfig,
ExternalFunction,
@@ -37,6 +42,14 @@ const PanicThresholdOptionsSchema = z.enum([
]);
export type PanicThresholdOptions = z.infer<typeof PanicThresholdOptionsSchema>;
const DynamicGatingOptionsSchema = z.object({
source: z.string(),
});
export type DynamicGatingOptions = z.infer<typeof DynamicGatingOptionsSchema>;
const CustomOptOutDirectiveSchema = z
.nullable(z.array(z.string()))
.default(null);
type CustomOptOutDirective = z.infer<typeof CustomOptOutDirectiveSchema>;
export type PluginOptions = {
environment: EnvironmentConfig;
@@ -65,6 +78,28 @@ export type PluginOptions = {
*/
gating: ExternalFunction | null;
/**
* If specified, this enables dynamic gating which matches `use memo if(...)`
* directives.
*
* Example usage:
* ```js
* // @dynamicGating:{"source":"myModule"}
* export function MyComponent() {
* 'use memo if(isEnabled)';
* return <div>...</div>;
* }
* ```
* This will emit:
* ```js
* import {isEnabled} from 'myModule';
* export const MyComponent = isEnabled()
* ? <optimized version>
* : <original version>;
* ```
*/
dynamicGating: DynamicGatingOptions | null;
panicThreshold: PanicThresholdOptions;
/*
@@ -106,6 +141,11 @@ export type PluginOptions = {
*/
ignoreUseNoForget: boolean;
/**
* Unstable / do not use
*/
customOptOutDirectives: CustomOptOutDirective;
sources: Array<string> | ((filename: string) => boolean) | null;
/**
@@ -189,7 +229,7 @@ export type LoggerEvent =
export type CompileErrorEvent = {
kind: 'CompileError';
fnLoc: t.SourceLocation | null;
detail: CompilerErrorDetailOptions;
detail: CompilerErrorDetail | CompilerDiagnostic;
};
export type CompileDiagnosticEvent = {
kind: 'CompileDiagnostic';
@@ -244,6 +284,7 @@ export const defaultOptions: PluginOptions = {
logger: null,
gating: null,
noEmit: false,
dynamicGating: null,
eslintSuppressionRules: null,
flowSuppressions: true,
ignoreUseNoForget: false,
@@ -251,6 +292,7 @@ export const defaultOptions: PluginOptions = {
return filename.indexOf('node_modules') === -1;
},
enableReanimatedCheck: true,
customOptOutDirectives: null,
target: '19',
} as const;
@@ -292,6 +334,40 @@ export function parsePluginOptions(obj: unknown): PluginOptions {
}
break;
}
case 'dynamicGating': {
if (value == null) {
parsedOptions[key] = null;
} else {
const result = DynamicGatingOptionsSchema.safeParse(value);
if (result.success) {
parsedOptions[key] = result.data;
} else {
CompilerError.throwInvalidConfig({
reason:
'Could not parse dynamic gating. Update React Compiler config to fix the error',
description: `${fromZodError(result.error)}`,
loc: null,
suggestions: null,
});
}
}
break;
}
case 'customOptOutDirectives': {
const result = CustomOptOutDirectiveSchema.safeParse(value);
if (result.success) {
parsedOptions[key] = result.data;
} else {
CompilerError.throwInvalidConfig({
reason:
'Could not parse custom opt out directives. Update React Compiler config to fix the error',
description: `${fromZodError(result.error)}`,
loc: null,
suggestions: null,
});
}
break;
}
default: {
parsedOptions[key] = value;
}

View File

@@ -33,9 +33,7 @@ import {findContextIdentifiers} from '../HIR/FindContextIdentifiers';
import {
analyseFunctions,
dropManualMemoization,
inferMutableRanges,
inferReactivePlaces,
inferReferenceEffects,
inlineImmediatelyInvokedFunctionExpressions,
inferEffectDependencies,
} from '../Inference';
@@ -92,18 +90,19 @@ import {
} from '../Validation';
import {validateLocalsNotReassignedAfterRender} from '../Validation/ValidateLocalsNotReassignedAfterRender';
import {outlineFunctions} from '../Optimization/OutlineFunctions';
import {propagatePhiTypes} from '../TypeInference/PropagatePhiTypes';
import {lowerContextAccess} from '../Optimization/LowerContextAccess';
import {validateNoSetStateInPassiveEffects} from '../Validation/ValidateNoSetStateInPassiveEffects';
import {validateNoSetStateInEffects} from '../Validation/ValidateNoSetStateInEffects';
import {validateNoJSXInTryStatement} from '../Validation/ValidateNoJSXInTryStatement';
import {propagateScopeDependenciesHIR} from '../HIR/PropagateScopeDependenciesHIR';
import {outlineJSX} from '../Optimization/OutlineJsx';
import {optimizePropsMethodCalls} from '../Optimization/OptimizePropsMethodCalls';
import {transformFire} from '../Transform';
import {validateNoImpureFunctionsInRender} from '../Validation/ValidateNoImpureFunctionsInRender';
import {CompilerError} from '..';
import {validateStaticComponents} from '../Validation/ValidateStaticComponents';
import {validateNoFreezingKnownMutableFunctions} from '../Validation/ValidateNoFreezingKnownMutableFunctions';
import {inferMutationAliasingEffects} from '../Inference/InferMutationAliasingEffects';
import {inferMutationAliasingRanges} from '../Inference/InferMutationAliasingRanges';
import {validateNoDerivedComputationsInEffects} from '../Validation/ValidateNoDerivedComputationsInEffects';
export type CompilerPipelineValue =
| {kind: 'ast'; name: string; value: CodegenFunction}
@@ -130,6 +129,7 @@ function run(
mode,
config,
contextIdentifiers,
func,
logger,
filename,
code,
@@ -171,7 +171,7 @@ function runWithEnvironment(
!env.config.disableMemoizationForDebugging &&
!env.config.enableChangeDetectionForDebugging
) {
dropManualMemoization(hir);
dropManualMemoization(hir).unwrap();
log({kind: 'hir', name: 'DropManualMemoization', value: hir});
}
@@ -226,15 +226,13 @@ function runWithEnvironment(
analyseFunctions(hir);
log({kind: 'hir', name: 'AnalyseFunctions', value: hir});
const fnEffectErrors = inferReferenceEffects(hir);
const mutabilityAliasingErrors = inferMutationAliasingEffects(hir);
log({kind: 'hir', name: 'InferMutationAliasingEffects', value: hir});
if (env.isInferredMemoEnabled) {
if (fnEffectErrors.length > 0) {
CompilerError.throw(fnEffectErrors[0]);
if (mutabilityAliasingErrors.isErr()) {
throw mutabilityAliasingErrors.unwrapErr();
}
}
log({kind: 'hir', name: 'InferReferenceEffects', value: hir});
validateLocalsNotReassignedAfterRender(hir);
// Note: Has to come after infer reference effects because "dead" code may still affect inference
deadCodeElimination(hir);
@@ -248,8 +246,16 @@ function runWithEnvironment(
pruneMaybeThrows(hir);
log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
inferMutableRanges(hir);
log({kind: 'hir', name: 'InferMutableRanges', value: hir});
const mutabilityAliasingRangeErrors = inferMutationAliasingRanges(hir, {
isFunctionExpression: false,
});
log({kind: 'hir', name: 'InferMutationAliasingRanges', value: hir});
if (env.isInferredMemoEnabled) {
if (mutabilityAliasingRangeErrors.isErr()) {
throw mutabilityAliasingRangeErrors.unwrapErr();
}
validateLocalsNotReassignedAfterRender(hir);
}
if (env.isInferredMemoEnabled) {
if (env.config.assertValidMutableRanges) {
@@ -264,8 +270,12 @@ function runWithEnvironment(
validateNoSetStateInRender(hir).unwrap();
}
if (env.config.validateNoSetStateInPassiveEffects) {
env.logErrors(validateNoSetStateInPassiveEffects(hir));
if (env.config.validateNoDerivedComputationsInEffects) {
validateNoDerivedComputationsInEffects(hir);
}
if (env.config.validateNoSetStateInEffects) {
env.logErrors(validateNoSetStateInEffects(hir));
}
if (env.config.validateNoJSXInTryStatements) {
@@ -276,9 +286,7 @@ function runWithEnvironment(
validateNoImpureFunctionsInRender(hir).unwrap();
}
if (env.config.validateNoFreezingKnownMutableFunctions) {
validateNoFreezingKnownMutableFunctions(hir).unwrap();
}
validateNoFreezingKnownMutableFunctions(hir).unwrap();
}
inferReactivePlaces(hir);
@@ -291,13 +299,6 @@ function runWithEnvironment(
value: hir,
});
propagatePhiTypes(hir);
log({
kind: 'hir',
name: 'PropagatePhiTypes',
value: hir,
});
if (env.isInferredMemoEnabled) {
if (env.config.validateStaticComponents) {
env.logErrors(validateStaticComponents(hir));

View File

@@ -10,9 +10,10 @@ import * as t from '@babel/types';
import {
CompilerError,
CompilerErrorDetail,
ErrorCategory,
ErrorSeverity,
} from '../CompilerError';
import {ReactFunctionType} from '../HIR/Environment';
import {ExternalFunction, ReactFunctionType} from '../HIR/Environment';
import {CodegenFunction} from '../ReactiveScopes';
import {isComponentDeclaration} from '../Utils/ComponentDeclaration';
import {isHookDeclaration} from '../Utils/HookDeclaration';
@@ -31,6 +32,7 @@ import {
suppressionsToCompilerError,
} from './Suppression';
import {GeneratedSource} from '../HIR';
import {Err, Ok, Result} from '../Utils/Result';
export type CompilerPass = {
opts: PluginOptions;
@@ -40,26 +42,104 @@ export type CompilerPass = {
};
export const OPT_IN_DIRECTIVES = new Set(['use forget', 'use memo']);
export const OPT_OUT_DIRECTIVES = new Set(['use no forget', 'use no memo']);
const DYNAMIC_GATING_DIRECTIVE = new RegExp('^use memo if\\(([^\\)]*)\\)$');
export function findDirectiveEnablingMemoization(
export function tryFindDirectiveEnablingMemoization(
directives: Array<t.Directive>,
): t.Directive | null {
return (
directives.find(directive =>
OPT_IN_DIRECTIVES.has(directive.value.value),
) ?? null
opts: PluginOptions,
): Result<t.Directive | null, CompilerError> {
const optIn = directives.find(directive =>
OPT_IN_DIRECTIVES.has(directive.value.value),
);
if (optIn != null) {
return Ok(optIn);
}
const dynamicGating = findDirectivesDynamicGating(directives, opts);
if (dynamicGating.isOk()) {
return Ok(dynamicGating.unwrap()?.directive ?? null);
} else {
return Err(dynamicGating.unwrapErr());
}
}
export function findDirectiveDisablingMemoization(
directives: Array<t.Directive>,
{customOptOutDirectives}: PluginOptions,
): t.Directive | null {
if (customOptOutDirectives != null) {
return (
directives.find(
directive =>
customOptOutDirectives.indexOf(directive.value.value) !== -1,
) ?? null
);
}
return (
directives.find(directive =>
OPT_OUT_DIRECTIVES.has(directive.value.value),
) ?? null
);
}
function findDirectivesDynamicGating(
directives: Array<t.Directive>,
opts: PluginOptions,
): Result<
{
gating: ExternalFunction;
directive: t.Directive;
} | null,
CompilerError
> {
if (opts.dynamicGating === null) {
return Ok(null);
}
const errors = new CompilerError();
const result: Array<{directive: t.Directive; match: string}> = [];
for (const directive of directives) {
const maybeMatch = DYNAMIC_GATING_DIRECTIVE.exec(directive.value.value);
if (maybeMatch != null && maybeMatch[1] != null) {
if (t.isValidIdentifier(maybeMatch[1])) {
result.push({directive, match: maybeMatch[1]});
} else {
errors.push({
reason: `Dynamic gating directive is not a valid JavaScript identifier`,
description: `Found '${directive.value.value}'`,
severity: ErrorSeverity.InvalidReact,
category: ErrorCategory.Gating,
loc: directive.loc ?? null,
suggestions: null,
});
}
}
}
if (errors.hasErrors()) {
return Err(errors);
} else if (result.length > 1) {
const error = new CompilerError();
error.push({
reason: `Multiple dynamic gating directives found`,
description: `Expected a single directive but found [${result
.map(r => r.directive.value.value)
.join(', ')}]`,
severity: ErrorSeverity.InvalidReact,
category: ErrorCategory.Gating,
loc: result[0].directive.loc ?? null,
suggestions: null,
});
return Err(error);
} else if (result.length === 1) {
return Ok({
gating: {
source: opts.dynamicGating.source,
importSpecifierName: result[0].match,
},
directive: result[0].directive,
});
} else {
return Ok(null);
}
}
function isCriticalError(err: unknown): boolean {
return !(err instanceof CompilerError) || err.isCritical();
@@ -104,7 +184,7 @@ function logError(
context.opts.logger.logEvent(context.filename, {
kind: 'CompileError',
fnLoc,
detail: detail.options,
detail,
});
}
} else {
@@ -326,7 +406,8 @@ export function compileProgram(
code: pass.code,
suppressions,
hasModuleScopeOptOut:
findDirectiveDisablingMemoization(program.node.directives) != null,
findDirectiveDisablingMemoization(program.node.directives, pass.opts) !=
null,
});
const queue: Array<CompileSource> = findFunctionsToCompile(
@@ -378,6 +459,7 @@ export function compileProgram(
reason:
'Unexpected compiled functions when module scope opt-out is present',
severity: ErrorSeverity.Invariant,
category: ErrorCategory.Invariant,
loc: null,
}),
);
@@ -477,13 +559,36 @@ function processFn(
fnType: ReactFunctionType,
programContext: ProgramContext,
): null | CodegenFunction {
let directives;
let directives: {
optIn: t.Directive | null;
optOut: t.Directive | null;
};
if (fn.node.body.type !== 'BlockStatement') {
directives = {optIn: null, optOut: null};
} else {
directives = {
optIn: findDirectiveEnablingMemoization(fn.node.body.directives),
optOut: findDirectiveDisablingMemoization(fn.node.body.directives),
optIn: null,
optOut: null,
};
} else {
const optIn = tryFindDirectiveEnablingMemoization(
fn.node.body.directives,
programContext.opts,
);
if (optIn.isErr()) {
/**
* If parsing opt-in directive fails, it's most likely that React Compiler
* was not tested or rolled out on this function. In that case, we handle
* the error and fall back to the safest option which is to not optimize
* the function.
*/
handleError(optIn.unwrapErr(), programContext, fn.node.loc ?? null);
return null;
}
directives = {
optIn: optIn.unwrapOr(null),
optOut: findDirectiveDisablingMemoization(
fn.node.body.directives,
programContext.opts,
),
};
}
@@ -659,25 +764,31 @@ function applyCompiledFunctions(
pass: CompilerPass,
programContext: ProgramContext,
): void {
const referencedBeforeDeclared =
pass.opts.gating != null
? getFunctionReferencedBeforeDeclarationAtTopLevel(program, compiledFns)
: null;
let referencedBeforeDeclared = null;
for (const result of compiledFns) {
const {kind, originalFn, compiledFn} = result;
const transformedFn = createNewFunctionNode(originalFn, compiledFn);
programContext.alreadyCompiled.add(transformedFn);
if (referencedBeforeDeclared != null && kind === 'original') {
CompilerError.invariant(pass.opts.gating != null, {
reason: "Expected 'gating' import to be present",
loc: null,
});
let dynamicGating: ExternalFunction | null = null;
if (originalFn.node.body.type === 'BlockStatement') {
const result = findDirectivesDynamicGating(
originalFn.node.body.directives,
pass.opts,
);
if (result.isOk()) {
dynamicGating = result.unwrap()?.gating ?? null;
}
}
const functionGating = dynamicGating ?? pass.opts.gating;
if (kind === 'original' && functionGating != null) {
referencedBeforeDeclared ??=
getFunctionReferencedBeforeDeclarationAtTopLevel(program, compiledFns);
insertGatedFunctionDeclaration(
originalFn,
transformedFn,
programContext,
pass.opts.gating,
functionGating,
referencedBeforeDeclared.has(result),
);
} else {
@@ -704,6 +815,7 @@ function shouldSkipCompilation(
description:
"When the 'sources' config options is specified, the React compiler will only compile files with a name",
severity: ErrorSeverity.InvalidConfig,
category: ErrorCategory.Config,
loc: null,
}),
);
@@ -733,8 +845,13 @@ function getReactFunctionType(
): ReactFunctionType | null {
const hookPattern = pass.opts.environment.hookPattern;
if (fn.node.body.type === 'BlockStatement') {
if (findDirectiveEnablingMemoization(fn.node.body.directives) != null)
const optInDirectives = tryFindDirectiveEnablingMemoization(
fn.node.body.directives,
pass.opts,
);
if (optInDirectives.unwrapOr(null) != null) {
return getComponentOrHookLike(fn, hookPattern) ?? 'Other';
}
}
// Component and hook declarations are known components/hooks

View File

@@ -8,9 +8,10 @@
import {NodePath} from '@babel/core';
import * as t from '@babel/types';
import {
CompilerDiagnostic,
CompilerError,
CompilerErrorDetail,
CompilerSuggestionOperation,
ErrorCategory,
ErrorSeverity,
} from '../CompilerError';
import {assertExhaustive} from '../Utils/utils';
@@ -181,12 +182,12 @@ export function suppressionsToCompilerError(
'Unhandled suppression source',
);
}
error.pushErrorDetail(
new CompilerErrorDetail({
reason: `${reason}. React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior`,
description: suppressionRange.disableComment.value.trim(),
error.pushDiagnostic(
CompilerDiagnostic.create({
reason: reason,
description: `React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. Found suppression \`${suppressionRange.disableComment.value.trim()}\``,
severity: ErrorSeverity.InvalidReact,
loc: suppressionRange.disableComment.loc ?? null,
category: ErrorCategory.Suppression,
suggestions: [
{
description: suggestion,
@@ -197,6 +198,10 @@ export function suppressionsToCompilerError(
op: CompilerSuggestionOperation.Remove,
},
],
}).withDetail({
kind: 'error',
loc: suppressionRange.disableComment.loc ?? null,
message: 'Found React rule suppression',
}),
);
}

View File

@@ -8,35 +8,67 @@
import {NodePath} from '@babel/core';
import * as t from '@babel/types';
import {
CompilerError,
CompilerErrorDetailOptions,
EnvironmentConfig,
ErrorSeverity,
Logger,
} from '..';
import {CompilerError, EnvironmentConfig, ErrorSeverity, Logger} from '..';
import {getOrInsertWith} from '../Utils/utils';
import {Environment} from '../HIR';
import {Environment, GeneratedSource} from '../HIR';
import {DEFAULT_EXPORT} from '../HIR/Environment';
import {CompileProgramMetadata} from './Program';
import {
CompilerDiagnostic,
CompilerDiagnosticOptions,
ErrorCategory,
} from '../CompilerError';
function throwInvalidReact(
options: Omit<CompilerErrorDetailOptions, 'severity'>,
options: Omit<CompilerDiagnosticOptions, 'severity'>,
{logger, filename}: TraversalState,
): never {
const detail: CompilerErrorDetailOptions = {
...options,
const detail: CompilerDiagnosticOptions = {
severity: ErrorSeverity.InvalidReact,
...options,
};
logger?.logEvent(filename, {
kind: 'CompileError',
fnLoc: null,
detail,
detail: new CompilerDiagnostic(detail),
});
CompilerError.throw(detail);
CompilerError.throwDiagnostic(detail);
}
function isAutodepsSigil(
arg: NodePath<t.ArgumentPlaceholder | t.SpreadElement | t.Expression>,
): boolean {
// Check for AUTODEPS identifier imported from React
if (arg.isIdentifier() && arg.node.name === 'AUTODEPS') {
const binding = arg.scope.getBinding(arg.node.name);
if (binding && binding.path.isImportSpecifier()) {
const importSpecifier = binding.path.node as t.ImportSpecifier;
if (importSpecifier.imported.type === 'Identifier') {
return (importSpecifier.imported as t.Identifier).name === 'AUTODEPS';
}
}
return false;
}
// Check for React.AUTODEPS member expression
if (arg.isMemberExpression() && !arg.node.computed) {
const object = arg.get('object');
const property = arg.get('property');
if (
object.isIdentifier() &&
object.node.name === 'React' &&
property.isIdentifier() &&
property.node.name === 'AUTODEPS'
) {
return true;
}
}
return false;
}
function assertValidEffectImportReference(
numArgs: number,
autodepsIndex: number,
paths: Array<NodePath<t.Node>>,
context: TraversalState,
): void {
@@ -49,11 +81,10 @@ function assertValidEffectImportReference(
maybeCalleeLoc != null &&
context.inferredEffectLocations.has(maybeCalleeLoc);
/**
* Only error on untransformed references of the form `useMyEffect(...)`
* or `moduleNamespace.useMyEffect(...)`, with matching argument counts.
* TODO: do we also want a mode to also hard error on non-call references?
* Error on effect calls that still have AUTODEPS in their args
*/
if (args.length === numArgs && !hasInferredEffect) {
const hasAutodepsArg = args.some(isAutodepsSigil);
if (hasAutodepsArg && !hasInferredEffect) {
const maybeErrorDiagnostic = matchCompilerDiagnostic(
path,
context.transformErrors,
@@ -65,14 +96,19 @@ function assertValidEffectImportReference(
*/
throwInvalidReact(
{
category: ErrorCategory.AutomaticEffectDependencies,
reason:
'[InferEffectDependencies] React Compiler is unable to infer dependencies of this effect. ' +
'This will break your build! ' +
'To resolve, either pass your own dependency array or fix reported compiler bailout diagnostics.',
description: maybeErrorDiagnostic
? `(Bailout reason: ${maybeErrorDiagnostic})`
: null,
loc: parent.node.loc ?? null,
'Cannot infer dependencies of this effect. This will break your build!',
description:
'To resolve, either pass a dependency array or fix reported compiler bailout diagnostics.' +
(maybeErrorDiagnostic ? ` ${maybeErrorDiagnostic}` : ''),
details: [
{
kind: 'error',
message: 'Cannot infer dependencies',
loc: parent.node.loc ?? GeneratedSource,
},
],
},
context,
);
@@ -92,13 +128,20 @@ function assertValidFireImportReference(
);
throwInvalidReact(
{
reason:
'[Fire] Untransformed reference to compiler-required feature. ' +
'Either remove this `fire` call or ensure it is successfully transformed by the compiler',
description: maybeErrorDiagnostic
? `(Bailout reason: ${maybeErrorDiagnostic})`
: null,
loc: paths[0].node.loc ?? null,
category: ErrorCategory.Fire,
reason: '[Fire] Untransformed reference to compiler-required feature.',
description:
'Either remove this `fire` call or ensure it is successfully transformed by the compiler' +
maybeErrorDiagnostic
? ` ${maybeErrorDiagnostic}`
: '',
details: [
{
kind: 'error',
message: 'Untransformed `fire` call',
loc: paths[0].node.loc ?? GeneratedSource,
},
],
},
context,
);
@@ -128,12 +171,12 @@ export default function validateNoUntransformedReferences(
if (env.inferEffectDependencies) {
for (const {
function: {source, importSpecifierName},
numRequiredArgs,
autodepsIndex,
} of env.inferEffectDependencies) {
const module = getOrInsertWith(moduleLoadChecks, source, () => new Map());
module.set(
importSpecifierName,
assertValidEffectImportReference.bind(null, numRequiredArgs),
assertValidEffectImportReference.bind(null, autodepsIndex),
);
}
}

View File

@@ -0,0 +1,752 @@
/**
* TypeScript definitions for Flow type JSON representations
* Based on the output of /data/sandcastle/boxes/fbsource/fbcode/flow/src/typing/convertTypes.ml
*/
// Base type for all Flow types with a kind field
export interface BaseFlowType {
kind: string;
}
// Type for representing polarity
export type Polarity = 'positive' | 'negative' | 'neutral';
// Type for representing a name that might be null
export type OptionalName = string | null;
// Open type
export interface OpenType extends BaseFlowType {
kind: 'Open';
}
// Def type
export interface DefType extends BaseFlowType {
kind: 'Def';
def: DefT;
}
// Eval type
export interface EvalType extends BaseFlowType {
kind: 'Eval';
type: FlowType;
destructor: Destructor;
}
// Generic type
export interface GenericType extends BaseFlowType {
kind: 'Generic';
name: string;
bound: FlowType;
no_infer: boolean;
}
// ThisInstance type
export interface ThisInstanceType extends BaseFlowType {
kind: 'ThisInstance';
instance: InstanceT;
is_this: boolean;
name: string;
}
// ThisTypeApp type
export interface ThisTypeAppType extends BaseFlowType {
kind: 'ThisTypeApp';
t1: FlowType;
t2: FlowType;
t_list?: Array<FlowType>;
}
// TypeApp type
export interface TypeAppType extends BaseFlowType {
kind: 'TypeApp';
type: FlowType;
targs: Array<FlowType>;
from_value: boolean;
use_desc: boolean;
}
// FunProto type
export interface FunProtoType extends BaseFlowType {
kind: 'FunProto';
}
// ObjProto type
export interface ObjProtoType extends BaseFlowType {
kind: 'ObjProto';
}
// NullProto type
export interface NullProtoType extends BaseFlowType {
kind: 'NullProto';
}
// FunProtoBind type
export interface FunProtoBindType extends BaseFlowType {
kind: 'FunProtoBind';
}
// Intersection type
export interface IntersectionType extends BaseFlowType {
kind: 'Intersection';
members: Array<FlowType>;
}
// Union type
export interface UnionType extends BaseFlowType {
kind: 'Union';
members: Array<FlowType>;
}
// Maybe type
export interface MaybeType extends BaseFlowType {
kind: 'Maybe';
type: FlowType;
}
// Optional type
export interface OptionalType extends BaseFlowType {
kind: 'Optional';
type: FlowType;
use_desc: boolean;
}
// Keys type
export interface KeysType extends BaseFlowType {
kind: 'Keys';
type: FlowType;
}
// Annot type
export interface AnnotType extends BaseFlowType {
kind: 'Annot';
type: FlowType;
use_desc: boolean;
}
// Opaque type
export interface OpaqueType extends BaseFlowType {
kind: 'Opaque';
opaquetype: {
opaque_id: string;
underlying_t: FlowType | null;
super_t: FlowType | null;
opaque_type_args: Array<{
name: string;
type: FlowType;
polarity: Polarity;
}>;
opaque_name: string;
};
}
// Namespace type
export interface NamespaceType extends BaseFlowType {
kind: 'Namespace';
namespace_symbol: {
symbol: string;
};
values_type: FlowType;
types_tmap: PropertyMap;
}
// Any type
export interface AnyType extends BaseFlowType {
kind: 'Any';
}
// StrUtil type
export interface StrUtilType extends BaseFlowType {
kind: 'StrUtil';
op: 'StrPrefix' | 'StrSuffix';
prefix?: string;
suffix?: string;
remainder?: FlowType;
}
// TypeParam definition
export interface TypeParam {
name: string;
bound: FlowType;
polarity: Polarity;
default: FlowType | null;
}
// EnumInfo types
export type EnumInfo = ConcreteEnum | AbstractEnum;
export interface ConcreteEnum {
kind: 'ConcreteEnum';
enum_name: string;
enum_id: string;
members: Array<string>;
representation_t: FlowType;
has_unknown_members: boolean;
}
export interface AbstractEnum {
kind: 'AbstractEnum';
representation_t: FlowType;
}
// CanonicalRendersForm types
export type CanonicalRendersForm =
| InstrinsicRenders
| NominalRenders
| StructuralRenders
| DefaultRenders;
export interface InstrinsicRenders {
kind: 'InstrinsicRenders';
name: string;
}
export interface NominalRenders {
kind: 'NominalRenders';
renders_id: string;
renders_name: string;
renders_super: FlowType;
}
export interface StructuralRenders {
kind: 'StructuralRenders';
renders_variant: 'RendersNormal' | 'RendersMaybe' | 'RendersStar';
renders_structural_type: FlowType;
}
export interface DefaultRenders {
kind: 'DefaultRenders';
}
// InstanceT definition
export interface InstanceT {
inst: InstType;
static: FlowType;
super: FlowType;
implements: Array<FlowType>;
}
// InstType definition
export interface InstType {
class_name: string | null;
class_id: string;
type_args: Array<{
name: string;
type: FlowType;
polarity: Polarity;
}>;
own_props: PropertyMap;
proto_props: PropertyMap;
call_t: null | {
id: number;
call: FlowType;
};
}
// DefT types
export type DefT =
| NumGeneralType
| StrGeneralType
| BoolGeneralType
| BigIntGeneralType
| EmptyType
| MixedType
| NullType
| VoidType
| SymbolType
| FunType
| ObjType
| ArrType
| ClassType
| InstanceType
| SingletonStrType
| NumericStrKeyType
| SingletonNumType
| SingletonBoolType
| SingletonBigIntType
| TypeType
| PolyType
| ReactAbstractComponentType
| RendersType
| EnumValueType
| EnumObjectType;
export interface NumGeneralType extends BaseFlowType {
kind: 'NumGeneral';
}
export interface StrGeneralType extends BaseFlowType {
kind: 'StrGeneral';
}
export interface BoolGeneralType extends BaseFlowType {
kind: 'BoolGeneral';
}
export interface BigIntGeneralType extends BaseFlowType {
kind: 'BigIntGeneral';
}
export interface EmptyType extends BaseFlowType {
kind: 'Empty';
}
export interface MixedType extends BaseFlowType {
kind: 'Mixed';
}
export interface NullType extends BaseFlowType {
kind: 'Null';
}
export interface VoidType extends BaseFlowType {
kind: 'Void';
}
export interface SymbolType extends BaseFlowType {
kind: 'Symbol';
}
export interface FunType extends BaseFlowType {
kind: 'Fun';
static: FlowType;
funtype: FunTypeObj;
}
export interface ObjType extends BaseFlowType {
kind: 'Obj';
objtype: ObjTypeObj;
}
export interface ArrType extends BaseFlowType {
kind: 'Arr';
arrtype: ArrTypeObj;
}
export interface ClassType extends BaseFlowType {
kind: 'Class';
type: FlowType;
}
export interface InstanceType extends BaseFlowType {
kind: 'Instance';
instance: InstanceT;
}
export interface SingletonStrType extends BaseFlowType {
kind: 'SingletonStr';
from_annot: boolean;
value: string;
}
export interface NumericStrKeyType extends BaseFlowType {
kind: 'NumericStrKey';
number: string;
string: string;
}
export interface SingletonNumType extends BaseFlowType {
kind: 'SingletonNum';
from_annot: boolean;
number: string;
string: string;
}
export interface SingletonBoolType extends BaseFlowType {
kind: 'SingletonBool';
from_annot: boolean;
value: boolean;
}
export interface SingletonBigIntType extends BaseFlowType {
kind: 'SingletonBigInt';
from_annot: boolean;
value: string;
}
export interface TypeType extends BaseFlowType {
kind: 'Type';
type_kind: TypeTKind;
type: FlowType;
}
export type TypeTKind =
| 'TypeAliasKind'
| 'TypeParamKind'
| 'OpaqueKind'
| 'ImportTypeofKind'
| 'ImportClassKind'
| 'ImportEnumKind'
| 'InstanceKind'
| 'RenderTypeKind';
export interface PolyType extends BaseFlowType {
kind: 'Poly';
tparams: Array<TypeParam>;
t_out: FlowType;
id: string;
}
export interface ReactAbstractComponentType extends BaseFlowType {
kind: 'ReactAbstractComponent';
config: FlowType;
renders: FlowType;
instance: ComponentInstance;
component_kind: ComponentKind;
}
export type ComponentInstance =
| {kind: 'RefSetterProp'; type: FlowType}
| {kind: 'Omitted'};
export type ComponentKind =
| {kind: 'Structural'}
| {kind: 'Nominal'; id: string; name: string; types: Array<FlowType> | null};
export interface RendersType extends BaseFlowType {
kind: 'Renders';
form: CanonicalRendersForm;
}
export interface EnumValueType extends BaseFlowType {
kind: 'EnumValue';
enum_info: EnumInfo;
}
export interface EnumObjectType extends BaseFlowType {
kind: 'EnumObject';
enum_value_t: FlowType;
enum_info: EnumInfo;
}
// ObjKind types
export type ObjKind =
| {kind: 'Exact'}
| {kind: 'Inexact'}
| {kind: 'Indexed'; dicttype: DictType};
// DictType definition
export interface DictType {
dict_name: string | null;
key: FlowType;
value: FlowType;
dict_polarity: Polarity;
}
// ArrType types
export type ArrTypeObj = ArrayAT | TupleAT | ROArrayAT;
export interface ArrayAT {
kind: 'ArrayAT';
elem_t: FlowType;
}
export interface TupleAT {
kind: 'TupleAT';
elem_t: FlowType;
elements: Array<TupleElement>;
min_arity: number;
max_arity: number;
inexact: boolean;
}
export interface ROArrayAT {
kind: 'ROArrayAT';
elem_t: FlowType;
}
// TupleElement definition
export interface TupleElement {
name: string | null;
t: FlowType;
polarity: Polarity;
optional: boolean;
}
// Flags definition
export interface Flags {
obj_kind: ObjKind;
}
// Property types
export type Property =
| FieldProperty
| GetProperty
| SetProperty
| GetSetProperty
| MethodProperty;
export interface FieldProperty {
kind: 'Field';
type: FlowType;
polarity: Polarity;
}
export interface GetProperty {
kind: 'Get';
type: FlowType;
}
export interface SetProperty {
kind: 'Set';
type: FlowType;
}
export interface GetSetProperty {
kind: 'GetSet';
get_type: FlowType;
set_type: FlowType;
}
export interface MethodProperty {
kind: 'Method';
type: FlowType;
}
// PropertyMap definition
export interface PropertyMap {
[key: string]: Property; // For other properties in the map
}
// ObjType definition
export interface ObjTypeObj {
flags: Flags;
props: PropertyMap;
proto_t: FlowType;
call_t: number | null;
}
// FunType definition
export interface FunTypeObj {
this_t: {
type: FlowType;
status: ThisStatus;
};
params: Array<{
name: string | null;
type: FlowType;
}>;
rest_param: null | {
name: string | null;
type: FlowType;
};
return_t: FlowType;
type_guard: null | {
inferred: boolean;
param_name: string;
type_guard: FlowType;
one_sided: boolean;
};
effect: Effect;
}
// ThisStatus types
export type ThisStatus =
| {kind: 'This_Method'; unbound: boolean}
| {kind: 'This_Function'};
// Effect types
export type Effect =
| {kind: 'HookDecl'; id: string}
| {kind: 'HookAnnot'}
| {kind: 'ArbitraryEffect'}
| {kind: 'AnyEffect'};
// Destructor types
export type Destructor =
| NonMaybeTypeDestructor
| PropertyTypeDestructor
| ElementTypeDestructor
| OptionalIndexedAccessNonMaybeTypeDestructor
| OptionalIndexedAccessResultTypeDestructor
| ExactTypeDestructor
| ReadOnlyTypeDestructor
| PartialTypeDestructor
| RequiredTypeDestructor
| SpreadTypeDestructor
| SpreadTupleTypeDestructor
| RestTypeDestructor
| ValuesTypeDestructor
| ConditionalTypeDestructor
| TypeMapDestructor
| ReactElementPropsTypeDestructor
| ReactElementConfigTypeDestructor
| ReactCheckComponentConfigDestructor
| ReactDRODestructor
| MakeHooklikeDestructor
| MappedTypeDestructor
| EnumTypeDestructor;
export interface NonMaybeTypeDestructor {
kind: 'NonMaybeType';
}
export interface PropertyTypeDestructor {
kind: 'PropertyType';
name: string;
}
export interface ElementTypeDestructor {
kind: 'ElementType';
index_type: FlowType;
}
export interface OptionalIndexedAccessNonMaybeTypeDestructor {
kind: 'OptionalIndexedAccessNonMaybeType';
index: OptionalIndexedAccessIndex;
}
export type OptionalIndexedAccessIndex =
| {kind: 'StrLitIndex'; name: string}
| {kind: 'TypeIndex'; type: FlowType};
export interface OptionalIndexedAccessResultTypeDestructor {
kind: 'OptionalIndexedAccessResultType';
}
export interface ExactTypeDestructor {
kind: 'ExactType';
}
export interface ReadOnlyTypeDestructor {
kind: 'ReadOnlyType';
}
export interface PartialTypeDestructor {
kind: 'PartialType';
}
export interface RequiredTypeDestructor {
kind: 'RequiredType';
}
export interface SpreadTypeDestructor {
kind: 'SpreadType';
target: SpreadTarget;
operands: Array<SpreadOperand>;
operand_slice: Slice | null;
}
export type SpreadTarget =
| {kind: 'Value'; make_seal: 'Sealed' | 'Frozen' | 'As_Const'}
| {kind: 'Annot'; make_exact: boolean};
export type SpreadOperand = {kind: 'Type'; type: FlowType} | Slice;
export interface Slice {
kind: 'Slice';
prop_map: PropertyMap;
generics: Array<string>;
dict: DictType | null;
reachable_targs: Array<{
type: FlowType;
polarity: Polarity;
}>;
}
export interface SpreadTupleTypeDestructor {
kind: 'SpreadTupleType';
inexact: boolean;
resolved_rev: string;
unresolved: string;
}
export interface RestTypeDestructor {
kind: 'RestType';
merge_mode: RestMergeMode;
type: FlowType;
}
export type RestMergeMode =
| {kind: 'SpreadReversal'}
| {kind: 'ReactConfigMerge'; polarity: Polarity}
| {kind: 'Omit'};
export interface ValuesTypeDestructor {
kind: 'ValuesType';
}
export interface ConditionalTypeDestructor {
kind: 'ConditionalType';
distributive_tparam_name: string | null;
infer_tparams: string;
extends_t: FlowType;
true_t: FlowType;
false_t: FlowType;
}
export interface TypeMapDestructor {
kind: 'ObjectKeyMirror';
}
export interface ReactElementPropsTypeDestructor {
kind: 'ReactElementPropsType';
}
export interface ReactElementConfigTypeDestructor {
kind: 'ReactElementConfigType';
}
export interface ReactCheckComponentConfigDestructor {
kind: 'ReactCheckComponentConfig';
props: {
[key: string]: Property;
};
}
export interface ReactDRODestructor {
kind: 'ReactDRO';
dro_type:
| 'HookReturn'
| 'HookArg'
| 'Props'
| 'ImmutableAnnot'
| 'DebugAnnot';
}
export interface MakeHooklikeDestructor {
kind: 'MakeHooklike';
}
export interface MappedTypeDestructor {
kind: 'MappedType';
homomorphic: Homomorphic;
distributive_tparam_name: string | null;
property_type: FlowType;
mapped_type_flags: {
variance: Polarity;
optional: 'MakeOptional' | 'RemoveOptional' | 'KeepOptionality';
};
}
export type Homomorphic =
| {kind: 'Homomorphic'}
| {kind: 'Unspecialized'}
| {kind: 'SemiHomomorphic'; type: FlowType};
export interface EnumTypeDestructor {
kind: 'EnumType';
}
// Union of all possible Flow types
export type FlowType =
| OpenType
| DefType
| EvalType
| GenericType
| ThisInstanceType
| ThisTypeAppType
| TypeAppType
| FunProtoType
| ObjProtoType
| NullProtoType
| FunProtoBindType
| IntersectionType
| UnionType
| MaybeType
| OptionalType
| KeysType
| AnnotType
| OpaqueType
| NamespaceType
| AnyType
| StrUtilType;

View File

@@ -0,0 +1,131 @@
import {CompilerError, SourceLocation} from '..';
import {
ConcreteType,
printConcrete,
printType,
StructuralValue,
Type,
VariableId,
} from './Types';
export function unsupportedLanguageFeature(
desc: string,
loc: SourceLocation,
): never {
CompilerError.throwInvalidJS({
reason: `Typedchecker does not currently support language feature: ${desc}`,
loc,
});
}
export type UnificationError =
| {
kind: 'TypeUnification';
left: ConcreteType<Type>;
right: ConcreteType<Type>;
}
| {
kind: 'StructuralUnification';
left: StructuralValue;
right: ConcreteType<Type>;
};
function printUnificationError(err: UnificationError): string {
if (err.kind === 'TypeUnification') {
return `${printConcrete(err.left, printType)} is incompatible with ${printConcrete(err.right, printType)}`;
} else {
return `structural ${err.left.kind} is incompatible with ${printConcrete(err.right, printType)}`;
}
}
export function raiseUnificationErrors(
errs: null | Array<UnificationError>,
loc: SourceLocation,
): void {
if (errs != null) {
if (errs.length === 0) {
CompilerError.invariant(false, {
reason: 'Should not have array of zero errors',
loc,
});
} else if (errs.length === 1) {
CompilerError.throwInvalidJS({
reason: `Unable to unify types because ${printUnificationError(errs[0])}`,
loc,
});
} else {
const messages = errs
.map(err => `\t* ${printUnificationError(err)}`)
.join('\n');
CompilerError.throwInvalidJS({
reason: `Unable to unify types because:\n${messages}`,
loc,
});
}
}
}
export function unresolvableTypeVariable(
id: VariableId,
loc: SourceLocation,
): never {
CompilerError.throwInvalidJS({
reason: `Unable to resolve free variable ${id} to a concrete type`,
loc,
});
}
export function cannotAddVoid(explicit: boolean, loc: SourceLocation): never {
if (explicit) {
CompilerError.throwInvalidJS({
reason: `Undefined is not a valid operand of \`+\``,
loc,
});
} else {
CompilerError.throwInvalidJS({
reason: `Value may be undefined, which is not a valid operand of \`+\``,
loc,
});
}
}
export function unsupportedTypeAnnotation(
desc: string,
loc: SourceLocation,
): never {
CompilerError.throwInvalidJS({
reason: `Typedchecker does not currently support type annotation: ${desc}`,
loc,
});
}
export function checkTypeArgumentArity(
desc: string,
expected: number,
actual: number,
loc: SourceLocation,
): void {
if (expected !== actual) {
CompilerError.throwInvalidJS({
reason: `Expected ${desc} to have ${expected} type parameters, got ${actual}`,
loc,
});
}
}
export function notAFunction(desc: string, loc: SourceLocation): void {
CompilerError.throwInvalidJS({
reason: `Cannot call ${desc} because it is not a function`,
loc,
});
}
export function notAPolymorphicFunction(
desc: string,
loc: SourceLocation,
): void {
CompilerError.throwInvalidJS({
reason: `Cannot call ${desc} with type arguments because it is not a polymorphic function`,
loc,
});
}

View File

@@ -0,0 +1,312 @@
import {GeneratedSource} from '../HIR';
import {assertExhaustive} from '../Utils/utils';
import {unsupportedLanguageFeature} from './TypeErrors';
import {
ConcreteType,
ResolvedType,
TypeParameter,
TypeParameterId,
DEBUG,
printConcrete,
printType,
} from './Types';
export function substitute(
type: ConcreteType<ResolvedType>,
typeParameters: Array<TypeParameter<ResolvedType>>,
typeArguments: Array<ResolvedType>,
): ResolvedType {
const substMap = new Map<TypeParameterId, ResolvedType>();
for (let i = 0; i < typeParameters.length; i++) {
// TODO: Length checks to make sure type params match up with args
const typeParameter = typeParameters[i];
const typeArgument = typeArguments[i];
substMap.set(typeParameter.id, typeArgument);
}
const substitutionFunction = (t: ResolvedType): ResolvedType => {
// TODO: We really want a stateful mapper or visitor here so that we can model nested polymorphic types
if (t.type.kind === 'Generic' && substMap.has(t.type.id)) {
const substitutedType = substMap.get(t.type.id)!;
return substitutedType;
}
return {
kind: 'Concrete',
type: mapType(substitutionFunction, t.type),
platform: t.platform,
};
};
const substituted = mapType(substitutionFunction, type);
if (DEBUG) {
let substs = '';
for (let i = 0; i < typeParameters.length; i++) {
const typeParameter = typeParameters[i];
const typeArgument = typeArguments[i];
substs += `[${typeParameter.name}${typeParameter.id} := ${printType(typeArgument)}]`;
}
console.log(
`${printConcrete(type, printType)}${substs} = ${printConcrete(substituted, printType)}`,
);
}
return {kind: 'Concrete', type: substituted, platform: /* TODO */ 'shared'};
}
export function mapType<T, U>(
f: (t: T) => U,
type: ConcreteType<T>,
): ConcreteType<U> {
switch (type.kind) {
case 'Mixed':
case 'Number':
case 'String':
case 'Boolean':
case 'Void':
return type;
case 'Nullable':
return {
kind: 'Nullable',
type: f(type.type),
};
case 'Array':
return {
kind: 'Array',
element: f(type.element),
};
case 'Set':
return {
kind: 'Set',
element: f(type.element),
};
case 'Map':
return {
kind: 'Map',
key: f(type.key),
value: f(type.value),
};
case 'Function':
return {
kind: 'Function',
typeParameters:
type.typeParameters?.map(param => ({
id: param.id,
name: param.name,
bound: f(param.bound),
})) ?? null,
params: type.params.map(f),
returnType: f(type.returnType),
};
case 'Component': {
return {
kind: 'Component',
children: type.children != null ? f(type.children) : null,
props: new Map([...type.props.entries()].map(([k, v]) => [k, f(v)])),
};
}
case 'Generic':
return {
kind: 'Generic',
id: type.id,
bound: f(type.bound),
};
case 'Object':
return type;
case 'Tuple':
return {
kind: 'Tuple',
id: type.id,
members: type.members.map(f),
};
case 'Structural':
return type;
case 'Enum':
case 'Union':
case 'Instance':
unsupportedLanguageFeature(type.kind, GeneratedSource);
default:
assertExhaustive(type, 'Unknown type kind');
}
}
export function diff<R, T>(
a: ConcreteType<T>,
b: ConcreteType<T>,
onChild: (a: T, b: T) => R,
onChildMismatch: (child: R, cur: R) => R,
onMismatch: (a: ConcreteType<T>, b: ConcreteType<T>, cur: R) => R,
init: R,
): R {
let errors = init;
// Check if kinds match
if (a.kind !== b.kind) {
errors = onMismatch(a, b, errors);
return errors;
}
// Based on kind, check other properties
switch (a.kind) {
case 'Mixed':
case 'Number':
case 'String':
case 'Boolean':
case 'Void':
// Simple types, no further checks needed
break;
case 'Nullable':
// Check the nested type
errors = onChildMismatch(onChild(a.type, (b as typeof a).type), errors);
break;
case 'Array':
case 'Set':
// Check the element type
errors = onChildMismatch(
onChild(a.element, (b as typeof a).element),
errors,
);
break;
case 'Map':
// Check both key and value types
errors = onChildMismatch(onChild(a.key, (b as typeof a).key), errors);
errors = onChildMismatch(onChild(a.value, (b as typeof a).value), errors);
break;
case 'Function': {
const bFunc = b as typeof a;
// Check type parameters
if ((a.typeParameters == null) !== (bFunc.typeParameters == null)) {
errors = onMismatch(a, b, errors);
}
if (a.typeParameters != null && bFunc.typeParameters != null) {
if (a.typeParameters.length !== bFunc.typeParameters.length) {
errors = onMismatch(a, b, errors);
}
// Type parameters are just numbers, so we can compare them directly
for (let i = 0; i < a.typeParameters.length; i++) {
if (a.typeParameters[i] !== bFunc.typeParameters[i]) {
errors = onMismatch(a, b, errors);
}
}
}
// Check parameters
if (a.params.length !== bFunc.params.length) {
errors = onMismatch(a, b, errors);
}
for (let i = 0; i < a.params.length; i++) {
errors = onChildMismatch(onChild(a.params[i], bFunc.params[i]), errors);
}
// Check return type
errors = onChildMismatch(onChild(a.returnType, bFunc.returnType), errors);
break;
}
case 'Component': {
const bComp = b as typeof a;
// Check children
if (a.children !== bComp.children) {
errors = onMismatch(a, b, errors);
}
// Check props
if (a.props.size !== bComp.props.size) {
errors = onMismatch(a, b, errors);
}
for (const [k, v] of a.props) {
const bProp = bComp.props.get(k);
if (bProp == null) {
errors = onMismatch(a, b, errors);
} else {
errors = onChildMismatch(onChild(v, bProp), errors);
}
}
break;
}
case 'Generic': {
// Check that the type parameter IDs match
if (a.id !== (b as typeof a).id) {
errors = onMismatch(a, b, errors);
}
break;
}
case 'Structural': {
const bStruct = b as typeof a;
// Check that the structural IDs match
if (a.id !== bStruct.id) {
errors = onMismatch(a, b, errors);
}
break;
}
case 'Object': {
const bNom = b as typeof a;
// Check that the nominal IDs match
if (a.id !== bNom.id) {
errors = onMismatch(a, b, errors);
}
break;
}
case 'Tuple': {
const bTuple = b as typeof a;
// Check that the tuple IDs match
if (a.id !== bTuple.id) {
errors = onMismatch(a, b, errors);
}
for (let i = 0; i < a.members.length; i++) {
errors = onChildMismatch(
onChild(a.members[i], bTuple.members[i]),
errors,
);
}
break;
}
case 'Enum':
case 'Instance':
case 'Union': {
unsupportedLanguageFeature(a.kind, GeneratedSource);
}
default:
assertExhaustive(a, 'Unknown type kind');
}
return errors;
}
export function filterOptional(t: ResolvedType): ResolvedType {
if (t.kind === 'Concrete' && t.type.kind === 'Nullable') {
return t.type.type;
}
return t;
}

File diff suppressed because it is too large Load Diff

View File

@@ -5,13 +5,14 @@
* LICENSE file in the root directory of this source tree.
*/
import invariant from 'invariant';
import {HIRFunction, Identifier, MutableRange} from './HIR';
import {HIRFunction, MutableRange, Place} from './HIR';
import {
eachInstructionLValue,
eachInstructionOperand,
eachTerminalOperand,
} from './visitors';
import {CompilerError} from '..';
import {printPlace} from './PrintHIR';
/*
* Checks that all mutable ranges in the function are well-formed, with
@@ -20,38 +21,43 @@ import {
export function assertValidMutableRanges(fn: HIRFunction): void {
for (const [, block] of fn.body.blocks) {
for (const phi of block.phis) {
visitIdentifier(phi.place.identifier);
for (const [, operand] of phi.operands) {
visitIdentifier(operand.identifier);
visit(phi.place, `phi for block bb${block.id}`);
for (const [pred, operand] of phi.operands) {
visit(operand, `phi predecessor bb${pred} for block bb${block.id}`);
}
}
for (const instr of block.instructions) {
for (const operand of eachInstructionLValue(instr)) {
visitIdentifier(operand.identifier);
visit(operand, `instruction [${instr.id}]`);
}
for (const operand of eachInstructionOperand(instr)) {
visitIdentifier(operand.identifier);
visit(operand, `instruction [${instr.id}]`);
}
}
for (const operand of eachTerminalOperand(block.terminal)) {
visitIdentifier(operand.identifier);
visit(operand, `terminal [${block.terminal.id}]`);
}
}
}
function visitIdentifier(identifier: Identifier): void {
validateMutableRange(identifier.mutableRange);
if (identifier.scope !== null) {
validateMutableRange(identifier.scope.range);
function visit(place: Place, description: string): void {
validateMutableRange(place, place.identifier.mutableRange, description);
if (place.identifier.scope !== null) {
validateMutableRange(place, place.identifier.scope.range, description);
}
}
function validateMutableRange(mutableRange: MutableRange): void {
invariant(
(mutableRange.start === 0 && mutableRange.end === 0) ||
mutableRange.end > mutableRange.start,
'Identifier scope mutableRange was invalid: [%s:%s]',
mutableRange.start,
mutableRange.end,
function validateMutableRange(
place: Place,
range: MutableRange,
description: string,
): void {
CompilerError.invariant(
(range.start === 0 && range.end === 0) || range.end > range.start,
{
reason: `Invalid mutable range: [${range.start}:${range.end}]`,
description: `${printPlace(place)} in ${description}`,
loc: place.loc,
},
);
}

View File

@@ -9,8 +9,10 @@ import {NodePath, Scope} from '@babel/traverse';
import * as t from '@babel/types';
import invariant from 'invariant';
import {
CompilerDiagnostic,
CompilerError,
CompilerSuggestionOperation,
ErrorCategory,
ErrorSeverity,
} from '../CompilerError';
import {Err, Ok, Result} from '../Utils/Result';
@@ -47,7 +49,7 @@ import {
makeType,
promoteTemporary,
} from './HIR';
import HIRBuilder, {Bindings} from './HIRBuilder';
import HIRBuilder, {Bindings, createTemporaryPlace} from './HIRBuilder';
import {BuiltInArrayId} from './ObjectShape';
/*
@@ -70,21 +72,23 @@ import {BuiltInArrayId} from './ObjectShape';
export function lower(
func: NodePath<t.Function>,
env: Environment,
// Bindings captured from the outer function, in case lower() is called recursively (for lambdas)
bindings: Bindings | null = null,
capturedRefs: Array<t.Identifier> = [],
// the outermost function being compiled, in case lower() is called recursively (for lambdas)
parent: NodePath<t.Function> | null = null,
capturedRefs: Map<t.Identifier, SourceLocation> = new Map(),
): Result<HIRFunction, CompilerError> {
const builder = new HIRBuilder(env, parent ?? func, bindings, capturedRefs);
const builder = new HIRBuilder(env, {
bindings,
context: capturedRefs,
});
const context: HIRFunction['context'] = [];
for (const ref of capturedRefs ?? []) {
for (const [ref, loc] of capturedRefs ?? []) {
context.push({
kind: 'Identifier',
identifier: builder.resolveBinding(ref),
effect: Effect.Unknown,
reactive: false,
loc: ref.loc ?? GeneratedSource,
loc,
});
}
@@ -102,12 +106,18 @@ export function lower(
if (param.isIdentifier()) {
const binding = builder.resolveIdentifier(param);
if (binding.kind !== 'Identifier') {
builder.errors.push({
reason: `(BuildHIR::lower) Could not find binding for param \`${param.node.name}\``,
severity: ErrorSeverity.Invariant,
loc: param.node.loc ?? null,
suggestions: null,
});
builder.errors.pushDiagnostic(
CompilerDiagnostic.create({
severity: ErrorSeverity.Invariant,
category: ErrorCategory.Invariant,
reason: 'Could not find binding',
description: `[BuildHIR] Could not find binding for param \`${param.node.name}\`.`,
}).withDetail({
kind: 'error',
loc: param.node.loc ?? null,
message: 'Could not find binding',
}),
);
return;
}
const place: Place = {
@@ -161,12 +171,18 @@ export function lower(
'Assignment',
);
} else {
builder.errors.push({
reason: `(BuildHIR::lower) Handle ${param.node.type} params`,
severity: ErrorSeverity.Todo,
loc: param.node.loc ?? null,
suggestions: null,
});
builder.errors.pushDiagnostic(
CompilerDiagnostic.create({
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
reason: `Handle ${param.node.type} parameters`,
description: `[BuildHIR] Add support for ${param.node.type} parameters.`,
}).withDetail({
kind: 'error',
loc: param.node.loc ?? null,
message: 'Unsupported parameter type',
}),
);
}
});
@@ -176,22 +192,29 @@ export function lower(
const fallthrough = builder.reserve('block');
const terminal: ReturnTerminal = {
kind: 'return',
returnVariant: 'Implicit',
loc: GeneratedSource,
value: lowerExpressionToTemporary(builder, body),
id: makeInstructionId(0),
effects: null,
};
builder.terminateWithContinuation(terminal, fallthrough);
} else if (body.isBlockStatement()) {
lowerStatement(builder, body);
directives = body.get('directives').map(d => d.node.value.value);
} else {
builder.errors.push({
severity: ErrorSeverity.InvalidJS,
reason: `Unexpected function body kind`,
description: `Expected function body to be an expression or a block statement, got \`${body.type}\``,
loc: body.node.loc ?? null,
suggestions: null,
});
builder.errors.pushDiagnostic(
CompilerDiagnostic.create({
severity: ErrorSeverity.InvalidJS,
category: ErrorCategory.Syntax,
reason: `Unexpected function body kind`,
description: `Expected function body to be an expression or a block statement, got \`${body.type}\`.`,
}).withDetail({
kind: 'error',
loc: body.node.loc ?? null,
message: 'Expected a block statement or expression',
}),
);
}
if (builder.errors.hasErrors()) {
@@ -201,6 +224,7 @@ export function lower(
builder.terminate(
{
kind: 'return',
returnVariant: 'Void',
loc: GeneratedSource,
value: lowerValueToTemporary(builder, {
kind: 'Primitive',
@@ -208,6 +232,7 @@ export function lower(
loc: GeneratedSource,
}),
id: makeInstructionId(0),
effects: null,
},
null,
);
@@ -215,9 +240,9 @@ export function lower(
return Ok({
id,
params,
fnType: parent == null ? env.fnType : 'Other',
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,
generator: func.node.generator === true,
@@ -225,6 +250,7 @@ export function lower(
loc: func.node.loc ?? GeneratedSource,
env,
effects: null,
aliasingEffects: null,
directives,
});
}
@@ -251,6 +277,7 @@ function lowerStatement(
reason:
'(BuildHIR::lowerStatement) Support ThrowStatement inside of try/catch',
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: stmt.node.loc ?? null,
suggestions: null,
});
@@ -282,9 +309,11 @@ function lowerStatement(
}
const terminal: ReturnTerminal = {
kind: 'return',
returnVariant: 'Explicit',
loc: stmt.node.loc ?? GeneratedSource,
value,
id: makeInstructionId(0),
effects: null,
};
builder.terminate(terminal, 'block');
return;
@@ -436,6 +465,7 @@ function lowerStatement(
} else if (!binding.path.isVariableDeclarator()) {
builder.errors.push({
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
reason: 'Unsupported declaration type for hoisting',
description: `variable "${binding.identifier.name}" declared with ${binding.path.type}`,
suggestions: null,
@@ -445,6 +475,7 @@ function lowerStatement(
} else {
builder.errors.push({
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
reason: 'Handle non-const declarations for hoisting',
description: `variable "${binding.identifier.name}" declared with ${binding.kind}`,
suggestions: null,
@@ -525,6 +556,7 @@ function lowerStatement(
reason:
'(BuildHIR::lowerStatement) Handle non-variable initialization in ForStatement',
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: stmt.node.loc ?? null,
suggestions: null,
});
@@ -597,6 +629,7 @@ function lowerStatement(
builder.errors.push({
reason: `(BuildHIR::lowerStatement) Handle empty test in ForStatement`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: stmt.node.loc ?? null,
suggestions: null,
});
@@ -748,6 +781,7 @@ function lowerStatement(
builder.errors.push({
reason: `Expected at most one \`default\` branch in a switch statement, this code should have failed to parse`,
severity: ErrorSeverity.InvalidJS,
category: ErrorCategory.Syntax,
loc: case_.node.loc ?? null,
suggestions: null,
});
@@ -820,6 +854,7 @@ function lowerStatement(
builder.errors.push({
reason: `(BuildHIR::lowerStatement) Handle ${nodeKind} kinds in VariableDeclaration`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: stmt.node.loc ?? null,
suggestions: null,
});
@@ -848,6 +883,7 @@ function lowerStatement(
builder.errors.push({
reason: `(BuildHIR::lowerAssignment) Could not find binding for declaration.`,
severity: ErrorSeverity.Invariant,
category: ErrorCategory.Invariant,
loc: id.node.loc ?? null,
suggestions: null,
});
@@ -865,6 +901,7 @@ function lowerStatement(
builder.errors.push({
reason: `Expect \`const\` declaration not to be reassigned`,
severity: ErrorSeverity.InvalidJS,
category: ErrorCategory.Syntax,
loc: id.node.loc ?? null,
suggestions: [
{
@@ -912,6 +949,7 @@ function lowerStatement(
reason: `Expected variable declaration to be an identifier if no initializer was provided`,
description: `Got a \`${id.type}\``,
severity: ErrorSeverity.InvalidJS,
category: ErrorCategory.Syntax,
loc: stmt.node.loc ?? null,
suggestions: null,
});
@@ -1020,6 +1058,7 @@ function lowerStatement(
builder.errors.push({
reason: `(BuildHIR::lowerStatement) Handle for-await loops`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: stmt.node.loc ?? null,
suggestions: null,
});
@@ -1235,6 +1274,7 @@ function lowerStatement(
kind: 'Debugger',
loc,
},
effects: null,
loc,
});
return;
@@ -1251,6 +1291,7 @@ function lowerStatement(
builder.errors.push({
reason: `(BuildHIR::lowerStatement) Handle TryStatement without a catch clause`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: stmt.node.loc ?? null,
suggestions: null,
});
@@ -1260,6 +1301,7 @@ function lowerStatement(
builder.errors.push({
reason: `(BuildHIR::lowerStatement) Handle TryStatement with a finalizer ('finally') clause`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: stmt.node.loc ?? null,
suggestions: null,
});
@@ -1348,13 +1390,89 @@ function lowerStatement(
return;
}
case 'TypeAlias':
case 'TSInterfaceDeclaration':
case 'TSTypeAliasDeclaration': {
// We do not preserve type annotations/syntax through transformation
case 'WithStatement': {
builder.errors.push({
reason: `JavaScript 'with' syntax is not supported`,
description: `'with' syntax is considered deprecated and removed from JavaScript standards, consider alternatives`,
severity: ErrorSeverity.UnsupportedJS,
category: ErrorCategory.UnsupportedSyntax,
loc: stmtPath.node.loc ?? null,
suggestions: null,
});
lowerValueToTemporary(builder, {
kind: 'UnsupportedNode',
loc: stmtPath.node.loc ?? GeneratedSource,
node: stmtPath.node,
});
return;
}
case 'ClassDeclaration': {
/**
* In theory we could support inline class declarations, but this is rare enough in practice
* and complex enough to support that we don't anticipate supporting anytime soon. Developers
* are encouraged to lift classes out of component/hook declarations.
*/
builder.errors.push({
reason: 'Inline `class` declarations are not supported',
description: `Move class declarations outside of components/hooks`,
severity: ErrorSeverity.UnsupportedJS,
category: ErrorCategory.UnsupportedSyntax,
loc: stmtPath.node.loc ?? null,
suggestions: null,
});
lowerValueToTemporary(builder, {
kind: 'UnsupportedNode',
loc: stmtPath.node.loc ?? GeneratedSource,
node: stmtPath.node,
});
return;
}
case 'EnumDeclaration':
case 'TSEnumDeclaration': {
lowerValueToTemporary(builder, {
kind: 'UnsupportedNode',
loc: stmtPath.node.loc ?? GeneratedSource,
node: stmtPath.node,
});
return;
}
case 'ExportAllDeclaration':
case 'ExportDefaultDeclaration':
case 'ExportNamedDeclaration':
case 'ImportDeclaration':
case 'TSExportAssignment':
case 'TSImportEqualsDeclaration': {
builder.errors.push({
reason:
'JavaScript `import` and `export` statements may only appear at the top level of a module',
severity: ErrorSeverity.InvalidJS,
category: ErrorCategory.Syntax,
loc: stmtPath.node.loc ?? null,
suggestions: null,
});
lowerValueToTemporary(builder, {
kind: 'UnsupportedNode',
loc: stmtPath.node.loc ?? GeneratedSource,
node: stmtPath.node,
});
return;
}
case 'TSNamespaceExportDeclaration': {
builder.errors.push({
reason:
'TypeScript `namespace` statements may only appear at the top level of a module',
severity: ErrorSeverity.InvalidJS,
category: ErrorCategory.Syntax,
loc: stmtPath.node.loc ?? null,
suggestions: null,
});
lowerValueToTemporary(builder, {
kind: 'UnsupportedNode',
loc: stmtPath.node.loc ?? GeneratedSource,
node: stmtPath.node,
});
return;
}
case 'ClassDeclaration':
case 'DeclareClass':
case 'DeclareExportAllDeclaration':
case 'DeclareExportDeclaration':
@@ -1365,31 +1483,14 @@ function lowerStatement(
case 'DeclareOpaqueType':
case 'DeclareTypeAlias':
case 'DeclareVariable':
case 'EnumDeclaration':
case 'ExportAllDeclaration':
case 'ExportDefaultDeclaration':
case 'ExportNamedDeclaration':
case 'ImportDeclaration':
case 'InterfaceDeclaration':
case 'OpaqueType':
case 'TSDeclareFunction':
case 'TSEnumDeclaration':
case 'TSExportAssignment':
case 'TSImportEqualsDeclaration':
case 'TSInterfaceDeclaration':
case 'TSModuleDeclaration':
case 'TSNamespaceExportDeclaration':
case 'WithStatement': {
builder.errors.push({
reason: `(BuildHIR::lowerStatement) Handle ${stmtPath.type} statements`,
severity: ErrorSeverity.Todo,
loc: stmtPath.node.loc ?? null,
suggestions: null,
});
lowerValueToTemporary(builder, {
kind: 'UnsupportedNode',
loc: stmtPath.node.loc ?? GeneratedSource,
node: stmtPath.node,
});
case 'TSTypeAliasDeclaration':
case 'TypeAlias': {
// We do not preserve type annotations/syntax through transformation
return;
}
default: {
@@ -1440,6 +1541,7 @@ function lowerObjectPropertyKey(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Expected Identifier, got ${key.type} key in ObjectExpression`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: key.node.loc ?? null,
suggestions: null,
});
@@ -1465,6 +1567,7 @@ function lowerObjectPropertyKey(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Expected Identifier, got ${key.type} key in ObjectExpression`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: key.node.loc ?? null,
suggestions: null,
});
@@ -1522,6 +1625,7 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${valuePath.type} values in ObjectExpression`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: valuePath.node.loc ?? null,
suggestions: null,
});
@@ -1548,6 +1652,7 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${propertyPath.node.kind} functions in ObjectExpression`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: propertyPath.node.loc ?? null,
suggestions: null,
});
@@ -1569,6 +1674,7 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${propertyPath.type} properties in ObjectExpression`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: propertyPath.node.loc ?? null,
suggestions: null,
});
@@ -1602,6 +1708,7 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${element.type} elements in ArrayExpression`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: element.node.loc ?? null,
suggestions: null,
});
@@ -1622,6 +1729,7 @@ function lowerExpression(
reason: `Expected an expression as the \`new\` expression receiver (v8 intrinsics are not supported)`,
description: `Got a \`${calleePath.node.type}\``,
severity: ErrorSeverity.InvalidJS,
category: ErrorCategory.Syntax,
loc: calleePath.node.loc ?? null,
suggestions: null,
});
@@ -1648,6 +1756,7 @@ function lowerExpression(
builder.errors.push({
reason: `Expected Expression, got ${calleePath.type} in CallExpression (v8 intrinsics not supported). This error is likely caused by a bug in React Compiler. Please file an issue`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: calleePath.node.loc ?? null,
suggestions: null,
});
@@ -1682,6 +1791,7 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Expected Expression, got ${leftPath.type} lval in BinaryExpression`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: leftPath.node.loc ?? null,
suggestions: null,
});
@@ -1694,6 +1804,7 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Pipe operator not supported`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: leftPath.node.loc ?? null,
suggestions: null,
});
@@ -1723,6 +1834,7 @@ function lowerExpression(
builder.errors.push({
reason: `Expected sequence expression to have at least one expression`,
severity: ErrorSeverity.InvalidJS,
category: ErrorCategory.Syntax,
loc: expr.node.loc ?? null,
suggestions: null,
});
@@ -1892,6 +2004,7 @@ function lowerExpression(
place: leftValue,
loc: exprLoc,
},
effects: null,
loc: exprLoc,
});
builder.terminateWithContinuation(
@@ -1934,6 +2047,7 @@ function lowerExpression(
reason: `(BuildHIR::lowerExpression) Unsupported syntax on the left side of an AssignmentExpression`,
description: `Expected an LVal, got: ${left.type}`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: left.node.loc ?? null,
suggestions: null,
});
@@ -1962,6 +2076,7 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${operator} operators in AssignmentExpression`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: expr.node.loc ?? null,
suggestions: null,
});
@@ -2061,6 +2176,7 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Expected Identifier or MemberExpression, got ${expr.type} lval in AssignmentExpression`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: expr.node.loc ?? null,
suggestions: null,
});
@@ -2100,6 +2216,7 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${attribute.type} attributes in JSXElement`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: attribute.node.loc ?? null,
suggestions: null,
});
@@ -2113,6 +2230,7 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Unexpected colon in attribute name \`${propName}\``,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: namePath.node.loc ?? null,
suggestions: null,
});
@@ -2143,6 +2261,7 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${valueExpr.type} attribute values in JSXElement`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: valueExpr.node?.loc ?? null,
suggestions: null,
});
@@ -2153,6 +2272,7 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${expression.type} expressions in JSXExpressionContainer within JSXElement`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: valueExpr.node.loc ?? null,
suggestions: null,
});
@@ -2208,11 +2328,18 @@ function lowerExpression(
});
for (const [name, locations] of Object.entries(fbtLocations)) {
if (locations.length > 1) {
CompilerError.throwTodo({
reason: `Support <${tagName}> tags with multiple <${tagName}:${name}> values`,
loc: locations.at(-1) ?? GeneratedSource,
description: null,
suggestions: null,
CompilerError.throwDiagnostic({
severity: ErrorSeverity.Todo,
category: ErrorCategory.FBT,
reason: 'Support duplicate fbt tags',
description: `Support \`<${tagName}>\` tags with multiple \`<${tagName}:${name}>\` values`,
details: locations.map(loc => {
return {
kind: 'error',
message: `Multiple \`<${tagName}:${name}>\` tags found`,
loc,
};
}),
});
}
}
@@ -2265,6 +2392,7 @@ function lowerExpression(
reason:
'(BuildHIR::lowerExpression) Handle tagged template with interpolations',
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -2283,6 +2411,7 @@ function lowerExpression(
reason:
'(BuildHIR::lowerExpression) Handle tagged template where cooked value is different from raw value',
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -2305,6 +2434,7 @@ function lowerExpression(
builder.errors.push({
reason: `Unexpected quasi and subexpression lengths in template literal`,
severity: ErrorSeverity.InvalidJS,
category: ErrorCategory.Syntax,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -2315,6 +2445,7 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerAssignment) Handle TSType in TemplateLiteral.`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -2357,6 +2488,7 @@ function lowerExpression(
builder.errors.push({
reason: `Only object properties can be deleted`,
severity: ErrorSeverity.InvalidJS,
category: ErrorCategory.Syntax,
loc: expr.node.loc ?? null,
suggestions: [
{
@@ -2372,6 +2504,7 @@ function lowerExpression(
builder.errors.push({
reason: `Throw expressions are not supported`,
severity: ErrorSeverity.InvalidJS,
category: ErrorCategory.Syntax,
loc: expr.node.loc ?? null,
suggestions: [
{
@@ -2493,6 +2626,7 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle UpdateExpression with ${argument.type} argument`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -2501,6 +2635,7 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle UpdateExpression to variables captured within lambdas.`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -2521,6 +2656,7 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Found an invalid UpdateExpression without a previously reported error`,
severity: ErrorSeverity.Invariant,
category: ErrorCategory.Invariant,
loc: exprLoc,
suggestions: null,
});
@@ -2530,6 +2666,7 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Support UpdateExpression where argument is a global`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: exprLoc,
suggestions: null,
});
@@ -2585,6 +2722,7 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle MetaProperty expressions other than import.meta`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -2594,6 +2732,7 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${exprPath.type} expressions`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -2827,6 +2966,7 @@ function lowerOptionalCallExpression(
args,
loc,
},
effects: null,
loc,
});
} else {
@@ -2840,6 +2980,7 @@ function lowerOptionalCallExpression(
args,
loc,
},
effects: null,
loc,
});
}
@@ -2889,6 +3030,7 @@ function lowerReorderableExpression(
builder.errors.push({
reason: `(BuildHIR::node.lowerReorderableExpression) Expression type \`${expr.type}\` cannot be safely reordered`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: expr.node.loc ?? null,
suggestions: null,
});
@@ -2936,6 +3078,8 @@ function isReorderableExpression(
}
}
}
case 'TSAsExpression':
case 'TSNonNullExpression':
case 'TypeCastExpression': {
return isReorderableExpression(
builder,
@@ -3083,6 +3227,7 @@ function lowerArguments(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${argPath.type} arguments in CallExpression`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: argPath.node.loc ?? null,
suggestions: null,
});
@@ -3118,6 +3263,7 @@ function lowerMemberExpression(
builder.errors.push({
reason: `(BuildHIR::lowerMemberExpression) Handle ${propertyNode.type} property`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: propertyNode.node.loc ?? null,
suggestions: null,
});
@@ -3139,6 +3285,7 @@ function lowerMemberExpression(
builder.errors.push({
reason: `(BuildHIR::lowerMemberExpression) Expected Expression, got ${propertyNode.type} property`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: propertyNode.node.loc ?? null,
suggestions: null,
});
@@ -3198,6 +3345,7 @@ function lowerJsxElementName(
reason: `Expected JSXNamespacedName to have no colons in the namespace or name`,
description: `Got \`${namespace}\` : \`${name}\``,
severity: ErrorSeverity.InvalidJS,
category: ErrorCategory.Syntax,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -3212,6 +3360,7 @@ function lowerJsxElementName(
builder.errors.push({
reason: `(BuildHIR::lowerJsxElementName) Handle ${exprPath.type} tags`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -3310,6 +3459,7 @@ function lowerJsxElement(
builder.errors.push({
reason: `(BuildHIR::lowerJsxElement) Unhandled JsxElement, got: ${exprPath.type}`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -3417,7 +3567,7 @@ function lowerFunction(
| t.ObjectMethod
>,
): LoweredFunction | null {
const componentScope: Scope = builder.parentFunction.scope;
const componentScope: Scope = builder.environment.parentFunction.scope;
const capturedContext = gatherCapturedContext(expr, componentScope);
/*
@@ -3432,14 +3582,12 @@ function lowerFunction(
expr,
builder.environment,
builder.bindings,
[...builder.context, ...capturedContext],
builder.parentFunction,
new Map([...builder.context, ...capturedContext]),
);
let loweredFunc: HIRFunction;
if (lowering.isErr()) {
lowering
.unwrapErr()
.details.forEach(detail => builder.errors.pushErrorDetail(detail));
const functionErrors = lowering.unwrapErr();
builder.errors.merge(functionErrors);
return null;
}
loweredFunc = lowering.unwrap();
@@ -3456,7 +3604,7 @@ function lowerExpressionToTemporary(
return lowerValueToTemporary(builder, value);
}
function lowerValueToTemporary(
export function lowerValueToTemporary(
builder: HIRBuilder,
value: InstructionValue,
): Place {
@@ -3466,9 +3614,10 @@ function lowerValueToTemporary(
const place: Place = buildTemporaryPlace(builder, value.loc);
builder.push({
id: makeInstructionId(0),
value: value,
loc: value.loc,
lvalue: {...place},
value: value,
effects: null,
loc: value.loc,
});
return place;
}
@@ -3492,6 +3641,17 @@ function lowerIdentifier(
return place;
}
default: {
if (binding.kind === 'Global' && binding.name === 'eval') {
builder.errors.push({
reason: `The 'eval' function is not supported`,
description:
'Eval is an anti-pattern in JavaScript, and the code executed cannot be evaluated by React Compiler',
severity: ErrorSeverity.UnsupportedJS,
category: ErrorCategory.UnsupportedSyntax,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
}
return lowerValueToTemporary(builder, {
kind: 'LoadGlobal',
binding,
@@ -3544,6 +3704,7 @@ function lowerIdentifierForAssignment(
builder.errors.push({
reason: `(BuildHIR::lowerAssignment) Could not find binding for declaration.`,
severity: ErrorSeverity.Invariant,
category: ErrorCategory.Invariant,
loc: path.node.loc ?? null,
suggestions: null,
});
@@ -3556,6 +3717,7 @@ function lowerIdentifierForAssignment(
builder.errors.push({
reason: `Cannot reassign a \`const\` variable`,
severity: ErrorSeverity.InvalidJS,
category: ErrorCategory.Syntax,
loc: path.node.loc ?? null,
description:
binding.identifier.name != null
@@ -3613,6 +3775,7 @@ function lowerAssignment(
builder.errors.push({
reason: `Expected \`const\` declaration not to be reassigned`,
severity: ErrorSeverity.InvalidJS,
category: ErrorCategory.Syntax,
loc: lvalue.node.loc ?? null,
suggestions: null,
});
@@ -3627,6 +3790,7 @@ function lowerAssignment(
builder.errors.push({
reason: `Unexpected context variable kind`,
severity: ErrorSeverity.InvalidJS,
category: ErrorCategory.Syntax,
loc: lvalue.node.loc ?? null,
suggestions: null,
});
@@ -3698,6 +3862,7 @@ function lowerAssignment(
builder.errors.push({
reason: `(BuildHIR::lowerAssignment) Handle ${property.type} properties in MemberExpression`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: property.node.loc ?? null,
suggestions: null,
});
@@ -3710,6 +3875,7 @@ function lowerAssignment(
reason:
'(BuildHIR::lowerAssignment) Expected private name to appear as a non-computed property',
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: property.node.loc ?? null,
suggestions: null,
});
@@ -3775,6 +3941,7 @@ function lowerAssignment(
} else if (identifier.kind === 'Global') {
builder.errors.push({
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
reason:
'Expected reassignment of globals to enable forceTemporaries',
loc: element.node.loc ?? GeneratedSource,
@@ -3814,6 +3981,7 @@ function lowerAssignment(
} else if (identifier.kind === 'Global') {
builder.errors.push({
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
reason:
'Expected reassignment of globals to enable forceTemporaries',
loc: element.node.loc ?? GeneratedSource,
@@ -3887,6 +4055,7 @@ function lowerAssignment(
builder.errors.push({
reason: `(BuildHIR::lowerAssignment) Handle ${argument.node.type} rest element in ObjectPattern`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: argument.node.loc ?? null,
suggestions: null,
});
@@ -3918,6 +4087,7 @@ function lowerAssignment(
} else if (identifier.kind === 'Global') {
builder.errors.push({
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
reason:
'Expected reassignment of globals to enable forceTemporaries',
loc: property.node.loc ?? GeneratedSource,
@@ -3935,6 +4105,7 @@ function lowerAssignment(
builder.errors.push({
reason: `(BuildHIR::lowerAssignment) Handle ${property.type} properties in ObjectPattern`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: property.node.loc ?? null,
suggestions: null,
});
@@ -3944,6 +4115,7 @@ function lowerAssignment(
builder.errors.push({
reason: `(BuildHIR::lowerAssignment) Handle computed properties in ObjectPattern`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: property.node.loc ?? null,
suggestions: null,
});
@@ -3958,6 +4130,7 @@ function lowerAssignment(
builder.errors.push({
reason: `(BuildHIR::lowerAssignment) Expected object property value to be an LVal, got: ${element.type}`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: element.node.loc ?? null,
suggestions: null,
});
@@ -3980,6 +4153,7 @@ function lowerAssignment(
} else if (identifier.kind === 'Global') {
builder.errors.push({
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
reason:
'Expected reassignment of globals to enable forceTemporaries',
loc: element.node.loc ?? GeneratedSource,
@@ -4129,6 +4303,7 @@ function lowerAssignment(
builder.errors.push({
reason: `(BuildHIR::lowerAssignment) Handle ${lvaluePath.type} assignments`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: lvaluePath.node.loc ?? null,
suggestions: null,
});
@@ -4151,6 +4326,11 @@ function captureScopes({from, to}: {from: Scope; to: Scope}): Set<Scope> {
return scopes;
}
/**
* Returns a mapping of "context" identifiers — references to free variables that
* will become part of the function expression's `context` array — along with the
* source location of their first reference within the function.
*/
function gatherCapturedContext(
fn: NodePath<
| t.FunctionExpression
@@ -4159,8 +4339,8 @@ function gatherCapturedContext(
| t.ObjectMethod
>,
componentScope: Scope,
): Array<t.Identifier> {
const capturedIds = new Set<t.Identifier>();
): Map<t.Identifier, SourceLocation> {
const capturedIds = new Map<t.Identifier, SourceLocation>();
/*
* Capture all the scopes from the parent of this function up to and including
@@ -4203,8 +4383,15 @@ function gatherCapturedContext(
// Add the base identifier binding as a dependency.
const binding = baseIdentifier.scope.getBinding(baseIdentifier.node.name);
if (binding !== undefined && pureScopes.has(binding.scope)) {
capturedIds.add(binding.identifier);
if (
binding !== undefined &&
pureScopes.has(binding.scope) &&
!capturedIds.has(binding.identifier)
) {
capturedIds.set(
binding.identifier,
path.node.loc ?? binding.identifier.loc ?? GeneratedSource,
);
}
}
@@ -4241,7 +4428,7 @@ function gatherCapturedContext(
},
});
return [...capturedIds.keys()];
return capturedIds;
}
function notNull<T>(value: T | null): value is T {

View File

@@ -241,7 +241,10 @@ type PropertyPathNode =
class PropertyPathRegistry {
roots: Map<IdentifierId, RootNode> = new Map();
getOrCreateIdentifier(identifier: Identifier): PropertyPathNode {
getOrCreateIdentifier(
identifier: Identifier,
reactive: boolean,
): PropertyPathNode {
/**
* Reads from a statically scoped variable are always safe in JS,
* with the exception of TDZ (not addressed by this pass).
@@ -255,12 +258,19 @@ class PropertyPathRegistry {
optionalProperties: new Map(),
fullPath: {
identifier,
reactive,
path: [],
},
hasOptional: false,
parent: null,
};
this.roots.set(identifier.id, rootNode);
} else {
CompilerError.invariant(reactive === rootNode.fullPath.reactive, {
reason:
'[HoistablePropertyLoads] Found inconsistencies in `reactive` flag when deduping identifier reads within the same scope',
loc: identifier.loc,
});
}
return rootNode;
}
@@ -278,6 +288,7 @@ class PropertyPathRegistry {
parent: parent,
fullPath: {
identifier: parent.fullPath.identifier,
reactive: parent.fullPath.reactive,
path: parent.fullPath.path.concat(entry),
},
hasOptional: parent.hasOptional || entry.optional,
@@ -293,7 +304,7 @@ class PropertyPathRegistry {
* so all subpaths of a PropertyLoad should already exist
* (e.g. a.b is added before a.b.c),
*/
let currNode = this.getOrCreateIdentifier(n.identifier);
let currNode = this.getOrCreateIdentifier(n.identifier, n.reactive);
if (n.path.length === 0) {
return currNode;
}
@@ -315,10 +326,11 @@ function getMaybeNonNullInInstruction(
instr: InstructionValue,
context: CollectHoistablePropertyLoadsContext,
): PropertyPathNode | null {
let path = null;
let path: ReactiveScopeDependency | null = null;
if (instr.kind === 'PropertyLoad') {
path = context.temporaries.get(instr.object.identifier.id) ?? {
identifier: instr.object.identifier,
reactive: instr.object.reactive,
path: [],
};
} else if (instr.kind === 'Destructure') {
@@ -381,7 +393,7 @@ function collectNonNullsInBlocks(
) {
const identifier = fn.params[0].identifier;
knownNonNullIdentifiers.add(
context.registry.getOrCreateIdentifier(identifier),
context.registry.getOrCreateIdentifier(identifier, true),
);
}
const nodes = new Map<
@@ -616,9 +628,11 @@ function reduceMaybeOptionalChains(
changed = false;
for (const original of optionalChainNodes) {
let {identifier, path: origPath} = original.fullPath;
let currNode: PropertyPathNode =
registry.getOrCreateIdentifier(identifier);
let {identifier, path: origPath, reactive} = original.fullPath;
let currNode: PropertyPathNode = registry.getOrCreateIdentifier(
identifier,
reactive,
);
for (let i = 0; i < origPath.length; i++) {
const entry = origPath[i];
// If the base is known to be non-null, replace with a non-optional load

View File

@@ -290,6 +290,7 @@ function traverseOptionalBlock(
);
baseObject = {
identifier: maybeTest.instructions[0].value.place.identifier,
reactive: maybeTest.instructions[0].value.place.reactive,
path,
};
test = maybeTest.terminal;
@@ -391,6 +392,7 @@ function traverseOptionalBlock(
);
const load = {
identifier: baseObject.identifier,
reactive: baseObject.reactive,
path: [
...baseObject.path,
{

View File

@@ -25,8 +25,9 @@ export class ReactiveScopeDependencyTreeHIR {
* `identifier.path`, or `identifier?.path` is in this map, it is safe to
* evaluate (non-optional) PropertyLoads from.
*/
#hoistableObjects: Map<Identifier, HoistableNode> = new Map();
#deps: Map<Identifier, DependencyNode> = new Map();
#hoistableObjects: Map<Identifier, HoistableNode & {reactive: boolean}> =
new Map();
#deps: Map<Identifier, DependencyNode & {reactive: boolean}> = new Map();
/**
* @param hoistableObjects a set of paths from which we can safely evaluate
@@ -35,9 +36,10 @@ export class ReactiveScopeDependencyTreeHIR {
* duplicates when traversing the CFG.
*/
constructor(hoistableObjects: Iterable<ReactiveScopeDependency>) {
for (const {path, identifier} of hoistableObjects) {
for (const {path, identifier, reactive} of hoistableObjects) {
let currNode = ReactiveScopeDependencyTreeHIR.#getOrCreateRoot(
identifier,
reactive,
this.#hoistableObjects,
path.length > 0 && path[0].optional ? 'Optional' : 'NonNull',
);
@@ -70,7 +72,8 @@ export class ReactiveScopeDependencyTreeHIR {
static #getOrCreateRoot<T extends string>(
identifier: Identifier,
roots: Map<Identifier, TreeNode<T>>,
reactive: boolean,
roots: Map<Identifier, TreeNode<T> & {reactive: boolean}>,
defaultAccessType: T,
): TreeNode<T> {
// roots can always be accessed unconditionally in JS
@@ -79,9 +82,16 @@ export class ReactiveScopeDependencyTreeHIR {
if (rootNode === undefined) {
rootNode = {
properties: new Map(),
reactive,
accessType: defaultAccessType,
};
roots.set(identifier, rootNode);
} else {
CompilerError.invariant(reactive === rootNode.reactive, {
reason: '[DeriveMinimalDependenciesHIR] Conflicting reactive root flag',
description: `Identifier ${printIdentifier(identifier)}`,
loc: GeneratedSource,
});
}
return rootNode;
}
@@ -92,9 +102,10 @@ export class ReactiveScopeDependencyTreeHIR {
* safe-to-evaluate subpath
*/
addDependency(dep: ReactiveScopeDependency): void {
const {identifier, path} = dep;
const {identifier, reactive, path} = dep;
let depCursor = ReactiveScopeDependencyTreeHIR.#getOrCreateRoot(
identifier,
reactive,
this.#deps,
PropertyAccessType.UnconditionalAccess,
);
@@ -172,7 +183,13 @@ export class ReactiveScopeDependencyTreeHIR {
deriveMinimalDependencies(): Set<ReactiveScopeDependency> {
const results = new Set<ReactiveScopeDependency>();
for (const [rootId, rootNode] of this.#deps.entries()) {
collectMinimalDependenciesInSubtree(rootNode, rootId, [], results);
collectMinimalDependenciesInSubtree(
rootNode,
rootNode.reactive,
rootId,
[],
results,
);
}
return results;
@@ -294,25 +311,24 @@ type HoistableNode = TreeNode<'Optional' | 'NonNull'>;
type DependencyNode = TreeNode<PropertyAccessType>;
/**
* TODO: this is directly pasted from DeriveMinimalDependencies. Since we no
* longer have conditionally accessed nodes, we can simplify
*
* Recursively calculates minimal dependencies in a subtree.
* @param node DependencyNode representing a dependency subtree.
* @returns a minimal list of dependencies in this subtree.
*/
function collectMinimalDependenciesInSubtree(
node: DependencyNode,
reactive: boolean,
rootIdentifier: Identifier,
path: Array<DependencyPathEntry>,
results: Set<ReactiveScopeDependency>,
): void {
if (isDependency(node.accessType)) {
results.add({identifier: rootIdentifier, path});
results.add({identifier: rootIdentifier, reactive, path});
} else {
for (const [childName, childNode] of node.properties) {
collectMinimalDependenciesInSubtree(
childNode,
reactive,
rootIdentifier,
[
...path,

View File

@@ -47,8 +47,9 @@ import {
ShapeRegistry,
addHook,
} from './ObjectShape';
import {Scope as BabelScope} from '@babel/traverse';
import {Scope as BabelScope, NodePath} from '@babel/traverse';
import {TypeSchema} from './TypeSchema';
import {FlowTypeEnv} from '../Flood/Types';
export const ReactElementSymbolSchema = z.object({
elementSymbol: z.union([
@@ -243,6 +244,12 @@ export const EnvironmentConfigSchema = z.object({
*/
enableUseTypeAnnotations: z.boolean().default(false),
/**
* Allows specifying a function that can populate HIR with type information from
* Flow
*/
flowTypeProvider: z.nullable(z.function().args(z.string())).default(null),
/**
* Enables inference of optional dependency chains. Without this flag
* a property chain such as `props?.items?.foo` will infer as a dep on
@@ -260,21 +267,19 @@ export const EnvironmentConfigSchema = z.object({
* {
* module: 'react',
* imported: 'useEffect',
* numRequiredArgs: 1,
* autodepsIndex: 1,
* },{
* module: 'MyExperimentalEffectHooks',
* imported: 'useExperimentalEffect',
* numRequiredArgs: 2,
* autodepsIndex: 2,
* },
* ]
* would insert dependencies for calls of `useEffect` imported from `react` and calls of
* useExperimentalEffect` from `MyExperimentalEffectHooks`.
*
* `numRequiredArgs` tells the compiler the amount of arguments required to append a dependency
* array to the end of the call. With the configuration above, we'd insert dependencies for
* `useEffect` if it is only given a single argument and it would be appended to the argument list.
*
* numRequiredArgs must always be greater than 0, otherwise there is no function to analyze for dependencies
* `autodepsIndex` tells the compiler which index we expect the AUTODEPS to appear in.
* With the configuration above, we'd insert dependencies for `useEffect` if it has two
* arguments, and the second is AUTODEPS.
*
* Still experimental.
*/
@@ -283,7 +288,7 @@ export const EnvironmentConfigSchema = z.object({
z.array(
z.object({
function: ExternalFunctionSchema,
numRequiredArgs: z.number().min(1, 'numRequiredArgs must be > 0'),
autodepsIndex: z.number().min(1, 'autodepsIndex must be > 0'),
}),
),
)
@@ -315,10 +320,16 @@ export const EnvironmentConfigSchema = z.object({
validateNoSetStateInRender: z.boolean().default(true),
/**
* Validates that setState is not called directly within a passive effect (useEffect).
* Validates that setState is not called synchronously within an effect (useEffect and friends).
* Scheduling a setState (with an event listener, subscription, etc) is valid.
*/
validateNoSetStateInPassiveEffects: z.boolean().default(false),
validateNoSetStateInEffects: z.boolean().default(false),
/**
* Validates that effects are not used to calculate derived data which could instead be computed
* during render.
*/
validateNoDerivedComputationsInEffects: z.boolean().default(false),
/**
* Validates against creating JSX within a try block and recommends using an error boundary
@@ -605,7 +616,7 @@ export const EnvironmentConfigSchema = z.object({
*
* Here the variables `ref` and `myRef` will be typed as Refs.
*/
enableTreatRefLikeIdentifiersAsRefs: z.boolean().default(false),
enableTreatRefLikeIdentifiersAsRefs: z.boolean().default(true),
/*
* If specified a value, the compiler lowers any calls to `useContext` to use
@@ -628,6 +639,17 @@ export const EnvironmentConfigSchema = z.object({
* ```
*/
lowerContextAccess: ExternalFunctionSchema.nullable().default(null),
/**
* If enabled, will validate useMemos that don't return any values:
*
* Valid:
* useMemo(() => foo, [foo]);
* useMemo(() => { return foo }, [foo]);
* Invalid:
* useMemo(() => { ... }, [...]);
*/
validateNoVoidUseMemo: z.boolean().default(false),
});
export type EnvironmentConfig = z.infer<typeof EnvironmentConfigSchema>;
@@ -675,6 +697,9 @@ export class Environment {
#contextIdentifiers: Set<t.Identifier>;
#hoistedIdentifiers: Set<t.Identifier>;
parentFunction: NodePath<t.Function>;
#flowTypeEnvironment: FlowTypeEnv | null;
constructor(
scope: BabelScope,
@@ -682,6 +707,7 @@ export class Environment {
compilerMode: CompilerMode,
config: EnvironmentConfig,
contextIdentifiers: Set<t.Identifier>,
parentFunction: NodePath<t.Function>, // the outermost function being compiled
logger: Logger | null,
filename: string | null,
code: string | null,
@@ -740,8 +766,29 @@ export class Environment {
this.#moduleTypes.set(REANIMATED_MODULE_NAME, reanimatedModuleType);
}
this.parentFunction = parentFunction;
this.#contextIdentifiers = contextIdentifiers;
this.#hoistedIdentifiers = new Set();
if (config.flowTypeProvider != null) {
this.#flowTypeEnvironment = new FlowTypeEnv();
CompilerError.invariant(code != null, {
reason:
'Expected Environment to be initialized with source code when a Flow type provider is specified',
loc: null,
});
this.#flowTypeEnvironment.init(this, code);
} else {
this.#flowTypeEnvironment = null;
}
}
get typeContext(): FlowTypeEnv {
CompilerError.invariant(this.#flowTypeEnvironment != null, {
reason: 'Flow type environment not initialized',
loc: null,
});
return this.#flowTypeEnvironment;
}
get isInferredMemoEnabled(): boolean {

View File

@@ -9,6 +9,7 @@ import {Effect, ValueKind, ValueReason} from './HIR';
import {
BUILTIN_SHAPES,
BuiltInArrayId,
BuiltInAutodepsId,
BuiltInFireFunctionId,
BuiltInFireId,
BuiltInMapId,
@@ -17,6 +18,7 @@ import {
BuiltInSetId,
BuiltInUseActionStateId,
BuiltInUseContextHookId,
BuiltInUseEffectEventId,
BuiltInUseEffectHookId,
BuiltInUseInsertionEffectHookId,
BuiltInUseLayoutEffectHookId,
@@ -27,6 +29,7 @@ import {
BuiltInUseTransitionId,
BuiltInWeakMapId,
BuiltInWeakSetId,
BuiltinEffectEventId,
ReanimatedSharedValueId,
ShapeRegistry,
addFunction,
@@ -111,6 +114,99 @@ const TYPED_GLOBALS: Array<[string, BuiltInType]> = [
returnValueKind: ValueKind.Mutable,
}),
],
[
'entries',
addFunction(DEFAULT_SHAPES, [], {
positionalParams: [Effect.Capture],
restParam: null,
returnType: {kind: 'Object', shapeId: BuiltInArrayId},
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Mutable,
aliasing: {
receiver: '@receiver',
params: ['@object'],
rest: null,
returns: '@returns',
temporaries: [],
effects: [
{
kind: 'Create',
into: '@returns',
reason: ValueReason.KnownReturnSignature,
value: ValueKind.Mutable,
},
// Object values are captured into the return
{
kind: 'Capture',
from: '@object',
into: '@returns',
},
],
},
}),
],
[
'keys',
addFunction(DEFAULT_SHAPES, [], {
positionalParams: [Effect.Read],
restParam: null,
returnType: {kind: 'Object', shapeId: BuiltInArrayId},
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Mutable,
aliasing: {
receiver: '@receiver',
params: ['@object'],
rest: null,
returns: '@returns',
temporaries: [],
effects: [
{
kind: 'Create',
into: '@returns',
reason: ValueReason.KnownReturnSignature,
value: ValueKind.Mutable,
},
// Only keys are captured, and keys are immutable
{
kind: 'ImmutableCapture',
from: '@object',
into: '@returns',
},
],
},
}),
],
[
'values',
addFunction(DEFAULT_SHAPES, [], {
positionalParams: [Effect.Capture],
restParam: null,
returnType: {kind: 'Object', shapeId: BuiltInArrayId},
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Mutable,
aliasing: {
receiver: '@receiver',
params: ['@object'],
rest: null,
returns: '@returns',
temporaries: [],
effects: [
{
kind: 'Create',
into: '@returns',
reason: ValueReason.KnownReturnSignature,
value: ValueKind.Mutable,
},
// Object values are captured into the return
{
kind: 'Capture',
from: '@object',
into: '@returns',
},
],
},
}),
],
]),
],
[
@@ -642,6 +738,41 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
calleeEffect: Effect.Read,
hookKind: 'useEffect',
returnValueKind: ValueKind.Frozen,
aliasing: {
receiver: '@receiver',
params: [],
rest: '@rest',
returns: '@returns',
temporaries: ['@effect'],
effects: [
// Freezes the function and deps
{
kind: 'Freeze',
value: '@rest',
reason: ValueReason.Effect,
},
// Internally creates an effect object that captures the function and deps
{
kind: 'Create',
into: '@effect',
value: ValueKind.Frozen,
reason: ValueReason.KnownReturnSignature,
},
// The effect stores the function and dependencies
{
kind: 'Capture',
from: '@rest',
into: '@effect',
},
// Returns undefined
{
kind: 'Create',
into: '@returns',
value: ValueKind.Primitive,
reason: ValueReason.KnownReturnSignature,
},
],
},
},
BuiltInUseEffectHookId,
),
@@ -722,6 +853,28 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
BuiltInFireId,
),
],
[
'useEffectEvent',
addHook(
DEFAULT_SHAPES,
{
positionalParams: [],
restParam: Effect.Freeze,
returnType: {
kind: 'Function',
return: {kind: 'Poly'},
shapeId: BuiltinEffectEventId,
isConstructor: false,
},
calleeEffect: Effect.Read,
hookKind: 'useEffectEvent',
// Frozen because it should not mutate any locally-bound values
returnValueKind: ValueKind.Frozen,
},
BuiltInUseEffectEventId,
),
],
['AUTODEPS', addObject(DEFAULT_SHAPES, BuiltInAutodepsId, [])],
];
TYPED_GLOBALS.push(
@@ -847,6 +1000,7 @@ export function installTypeConfig(
noAlias: typeConfig.noAlias === true,
mutableOnlyIfOperandsAreMutable:
typeConfig.mutableOnlyIfOperandsAreMutable === true,
aliasing: typeConfig.aliasing,
});
}
case 'hook': {
@@ -864,6 +1018,7 @@ export function installTypeConfig(
),
returnValueKind: typeConfig.returnValueKind ?? ValueKind.Frozen,
noAlias: typeConfig.noAlias === true,
aliasing: typeConfig.aliasing,
});
}
case 'object': {

View File

@@ -7,12 +7,14 @@
import {BindingKind} from '@babel/traverse';
import * as t from '@babel/types';
import {CompilerError, CompilerErrorDetailOptions} from '../CompilerError';
import {CompilerError} from '../CompilerError';
import {assertExhaustive} from '../Utils/utils';
import {Environment, ReactFunctionType} from './Environment';
import type {HookKind} from './ObjectShape';
import {Type, makeType} from './Types';
import {z} from 'zod';
import type {AliasingEffect} from '../Inference/AliasingEffects';
import {isReservedWord} from '../Utils/Keyword';
/*
* *******************************************************************************************
@@ -100,6 +102,7 @@ export type ReactiveInstruction = {
id: InstructionId;
lvalue: Place | null;
value: ReactiveValue;
effects?: Array<AliasingEffect> | null; // TODO make non-optional
loc: SourceLocation;
};
@@ -277,31 +280,15 @@ 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;
body: HIR;
generator: boolean;
async: boolean;
directives: Array<string>;
aliasingEffects: Array<AliasingEffect> | null;
};
export type FunctionEffect =
| {
kind: 'GlobalMutation';
error: CompilerErrorDetailOptions;
}
| {
kind: 'ReactMutation';
error: CompilerErrorDetailOptions;
}
| {
kind: 'ContextMutation';
places: ReadonlySet<Place>;
effect: Effect;
loc: SourceLocation;
};
/*
* Each reactive scope may have its own control-flow, so the instructions form
* a control-flow graph. The graph comprises a set of basic blocks which reference
@@ -443,12 +430,25 @@ export type ThrowTerminal = {
};
export type Case = {test: Place | null; block: BlockId};
export type ReturnVariant = 'Void' | 'Implicit' | 'Explicit';
export type ReturnTerminal = {
kind: 'return';
/**
* Void:
* () => { ... }
* function() { ... }
* Implicit (ArrowFunctionExpression only):
* () => foo
* Explicit:
* () => { return ... }
* function () { return ... }
*/
returnVariant: ReturnVariant;
loc: SourceLocation;
value: Place;
id: InstructionId;
fallthrough?: never;
effects: Array<AliasingEffect> | null;
};
export type GotoTerminal = {
@@ -609,6 +609,7 @@ export type MaybeThrowTerminal = {
id: InstructionId;
loc: SourceLocation;
fallthrough?: never;
effects: Array<AliasingEffect> | null;
};
export type ReactiveScopeTerminal = {
@@ -645,12 +646,14 @@ export type Instruction = {
lvalue: Place;
value: InstructionValue;
loc: SourceLocation;
effects: Array<AliasingEffect> | null;
};
export type TInstruction<T extends InstructionValue> = {
id: InstructionId;
lvalue: Place;
value: T;
effects: Array<AliasingEffect> | null;
loc: SourceLocation;
};
@@ -1301,12 +1304,21 @@ export function forkTemporaryIdentifier(
* original source code.
*/
export function makeIdentifierName(name: string): ValidatedIdentifier {
CompilerError.invariant(t.isValidIdentifier(name), {
reason: `Expected a valid identifier name`,
loc: GeneratedSource,
description: `\`${name}\` is not a valid JavaScript identifier`,
suggestions: null,
});
if (isReservedWord(name)) {
CompilerError.throwInvalidJS({
reason: 'Expected a non-reserved identifier name',
loc: GeneratedSource,
description: `\`${name}\` is a reserved word in JavaScript and cannot be used as an identifier name`,
suggestions: null,
});
} else {
CompilerError.invariant(t.isValidIdentifier(name), {
reason: `Expected a valid identifier name`,
loc: GeneratedSource,
description: `\`${name}\` is not a valid JavaScript identifier`,
suggestions: null,
});
}
return {
kind: 'named',
value: name as ValidIdentifierName,
@@ -1380,6 +1392,21 @@ export enum ValueReason {
*/
JsxCaptured = 'jsx-captured',
/**
* Argument to a hook
*/
HookCaptured = 'hook-captured',
/**
* Return value of a hook
*/
HookReturn = 'hook-return',
/**
* Passed to an effect
*/
Effect = 'effect',
/**
* Return value of a function with known frozen return value, e.g. `useState`.
*/
@@ -1430,6 +1457,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
@@ -1568,6 +1609,18 @@ export type DependencyPathEntry = {
export type DependencyPath = Array<DependencyPathEntry>;
export type ReactiveScopeDependency = {
identifier: Identifier;
/**
* Reflects whether the base identifier is reactive. Note that some reactive
* objects may have non-reactive properties, but we do not currently track
* this.
*
* ```js
* // Technically, result[0] is reactive and result[1] is not.
* // Currently, both dependencies would be marked as reactive.
* const result = useState();
* ```
*/
reactive: boolean;
path: DependencyPath;
};
@@ -1721,6 +1774,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);
}
@@ -1773,6 +1830,13 @@ export function isFireFunctionType(id: Identifier): boolean {
);
}
export function isEffectEventFunctionType(id: Identifier): boolean {
return (
id.type.kind === 'Function' &&
id.type.shapeId === 'BuiltInEffectEventFunction'
);
}
export function isStableType(id: Identifier): boolean {
return (
isSetStateType(id) ||

View File

@@ -7,7 +7,7 @@
import {Binding, NodePath} from '@babel/traverse';
import * as t from '@babel/types';
import {CompilerError} from '../CompilerError';
import {CompilerError, ErrorCategory, ErrorSeverity} from '../CompilerError';
import {Environment} from './Environment';
import {
BasicBlock,
@@ -106,11 +106,10 @@ export default class HIRBuilder {
#current: WipBlock;
#entry: BlockId;
#scopes: Array<Scope> = [];
#context: Array<t.Identifier>;
#context: Map<t.Identifier, SourceLocation>;
#bindings: Bindings;
#env: Environment;
#exceptionHandlerStack: Array<BlockId> = [];
parentFunction: NodePath<t.Function>;
errors: CompilerError = new CompilerError();
/**
* Traversal context: counts the number of `fbt` tag parents
@@ -122,7 +121,7 @@ export default class HIRBuilder {
return this.#env.nextIdentifierId;
}
get context(): Array<t.Identifier> {
get context(): Map<t.Identifier, SourceLocation> {
return this.#context;
}
@@ -136,16 +135,17 @@ export default class HIRBuilder {
constructor(
env: Environment,
parentFunction: NodePath<t.Function>, // the outermost function being compiled
bindings: Bindings | null = null,
context: Array<t.Identifier> | null = null,
options?: {
bindings?: Bindings | null;
context?: Map<t.Identifier, SourceLocation>;
entryBlockKind?: BlockKind;
},
) {
this.#env = env;
this.#bindings = bindings ?? new Map();
this.parentFunction = parentFunction;
this.#context = context ?? [];
this.#bindings = options?.bindings ?? new Map();
this.#context = options?.context ?? new Map();
this.#entry = makeBlockId(env.nextBlockId);
this.#current = newBlock(this.#entry, 'block');
this.#current = newBlock(this.#entry, options?.entryBlockKind ?? 'block');
}
currentBlockKind(): BlockKind {
@@ -165,6 +165,7 @@ export default class HIRBuilder {
handler: exceptionHandler,
id: makeInstructionId(0),
loc: instruction.loc,
effects: null,
},
continuationBlock,
);
@@ -239,7 +240,7 @@ export default class HIRBuilder {
// Check if the binding is from module scope
const outerBinding =
this.parentFunction.scope.parent.getBinding(originalName);
this.#env.parentFunction.scope.parent.getBinding(originalName);
if (babelBinding === outerBinding) {
const path = babelBinding.path;
if (path.isImportDefaultSpecifier()) {
@@ -293,7 +294,7 @@ export default class HIRBuilder {
const binding = this.#resolveBabelBinding(path);
if (binding) {
// Check if the binding is from module scope, if so return null
const outerBinding = this.parentFunction.scope.parent.getBinding(
const outerBinding = this.#env.parentFunction.scope.parent.getBinding(
path.node.name,
);
if (binding === outerBinding) {
@@ -307,9 +308,19 @@ export default class HIRBuilder {
resolveBinding(node: t.Identifier): Identifier {
if (node.name === 'fbt') {
CompilerError.throwTodo({
reason: 'Support local variables named "fbt"',
loc: node.loc ?? null,
CompilerError.throwDiagnostic({
severity: ErrorSeverity.Todo,
category: ErrorCategory.FBT,
reason: 'Support local variables named `fbt`',
description:
'Local variables named `fbt` may conflict with the fbt plugin and are not yet supported',
details: [
{
kind: 'error',
message: 'Rename to avoid conflict with fbt plugin',
loc: node.loc ?? GeneratedSource,
},
],
});
}
const originalName = node.name;
@@ -376,7 +387,7 @@ export default class HIRBuilder {
}
// Terminate the current block w the given terminal, and start a new block
terminate(terminal: Terminal, nextBlockKind: BlockKind | null): void {
terminate(terminal: Terminal, nextBlockKind: BlockKind | null): BlockId {
const {id: blockId, kind, instructions} = this.#current;
this.#completed.set(blockId, {
kind,
@@ -390,6 +401,7 @@ export default class HIRBuilder {
const nextId = this.#env.nextBlockId;
this.#current = newBlock(nextId, nextBlockKind);
}
return blockId;
}
/*
@@ -746,6 +758,11 @@ function getReversePostorderedBlocks(func: HIR): HIR['blocks'] {
* (eg bb2 then bb1), we ensure that they get reversed back to the correct order.
*/
const block = func.blocks.get(blockId)!;
CompilerError.invariant(block != null, {
reason: '[HIRBuilder] Unexpected null block',
description: `expected block ${blockId} to exist`,
loc: GeneratedSource,
});
const successors = [...eachTerminalSuccessor(block.terminal)].reverse();
const fallthrough = terminalFallthrough(block.terminal);

View File

@@ -12,6 +12,7 @@ import {
GeneratedSource,
HIRFunction,
Instruction,
Place,
} from './HIR';
import {markPredecessors} from './HIRBuilder';
import {terminalFallthrough, terminalHasFallthrough} from './visitors';
@@ -80,20 +81,22 @@ export function mergeConsecutiveBlocks(fn: HIRFunction): void {
suggestions: null,
});
const operand = Array.from(phi.operands.values())[0]!;
const lvalue: Place = {
kind: 'Identifier',
identifier: phi.place.identifier,
effect: Effect.ConditionallyMutate,
reactive: false,
loc: GeneratedSource,
};
const instr: Instruction = {
id: predecessor.terminal.id,
lvalue: {
kind: 'Identifier',
identifier: phi.place.identifier,
effect: Effect.ConditionallyMutate,
reactive: false,
loc: GeneratedSource,
},
lvalue: {...lvalue},
value: {
kind: 'LoadLocal',
place: {...operand},
loc: GeneratedSource,
},
effects: [{kind: 'Alias', from: {...operand}, into: {...lvalue}}],
loc: GeneratedSource,
};
predecessor.instructions.push(instr);
@@ -104,6 +107,17 @@ export function mergeConsecutiveBlocks(fn: HIRFunction): void {
merged.merge(block.id, predecessorId);
fn.body.blocks.delete(block.id);
}
for (const [, block] of fn.body.blocks) {
for (const phi of block.phis) {
for (const [predecessorId, operand] of phi.operands) {
const mapped = merged.get(predecessorId);
if (mapped !== predecessorId) {
phi.operands.delete(predecessorId);
phi.operands.set(mapped, operand);
}
}
}
}
markPredecessors(fn.body);
for (const [, {terminal}] of fn.body.blocks) {
if (terminalHasFallthrough(terminal)) {

View File

@@ -6,14 +6,30 @@
*/
import {CompilerError} from '../CompilerError';
import {Effect, ValueKind, ValueReason} from './HIR';
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';
import {
BuiltInType,
FunctionType,
makeType,
ObjectType,
PolyType,
PrimitiveType,
} from './Types';
import {AliasingEffectConfig, AliasingSignatureConfig} from './TypeSchema';
/*
* This file exports types and defaults for JavaScript object shapes. These are
@@ -42,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 {
@@ -66,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,
@@ -79,6 +109,130 @@ 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 'ImmutableCapture':
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.
*
@@ -131,6 +285,7 @@ export type HookKind =
| 'useCallback'
| 'useTransition'
| 'useImperativeHandle'
| 'useEffectEvent'
| 'Custom';
/*
@@ -179,6 +334,8 @@ export type FunctionSignature = {
impure?: boolean;
canonicalName?: string;
aliasing?: AliasingSignature | null | undefined;
};
/*
@@ -226,6 +383,9 @@ export const BuiltInUseTransitionId = 'BuiltInUseTransition';
export const BuiltInStartTransitionId = 'BuiltInStartTransition';
export const BuiltInFireId = 'BuiltInFire';
export const BuiltInFireFunctionId = 'BuiltInFireFunction';
export const BuiltInUseEffectEventId = 'BuiltInUseEffectEvent';
export const BuiltinEffectEventId = 'BuiltInEffectEventFunction';
export const BuiltInAutodepsId = 'BuiltInAutoDepsId';
// See getReanimatedModuleType() in Globals.ts — this is part of supporting Reanimated's ref-like types
export const ReanimatedSharedValueId = 'ReanimatedSharedValueId';
@@ -302,6 +462,30 @@ addObject(BUILTIN_SHAPES, BuiltInArrayId, [
returnType: PRIMITIVE_TYPE,
calleeEffect: Effect.Store,
returnValueKind: ValueKind.Primitive,
aliasing: {
receiver: '@receiver',
params: [],
rest: '@rest',
returns: '@returns',
temporaries: [],
effects: [
// Push directly mutates the array itself
{kind: 'Mutate', value: '@receiver'},
// The arguments are captured into the array
{
kind: 'Capture',
from: '@rest',
into: '@receiver',
},
// Returns the new length, a primitive
{
kind: 'Create',
into: '@returns',
value: ValueKind.Primitive,
reason: ValueReason.KnownReturnSignature,
},
],
},
}),
],
[
@@ -332,6 +516,60 @@ addObject(BUILTIN_SHAPES, BuiltInArrayId, [
returnValueKind: ValueKind.Mutable,
noAlias: true,
mutableOnlyIfOperandsAreMutable: true,
aliasing: {
receiver: '@receiver',
params: ['@callback'],
rest: null,
returns: '@returns',
temporaries: [
// Temporary representing captured items of the receiver
'@item',
// Temporary representing the result of the callback
'@callbackReturn',
/*
* Undefined `this` arg to the callback. Note the signature does not
* support passing an explicit thisArg second param
*/
'@thisArg',
],
effects: [
// Map creates a new mutable array
{
kind: 'Create',
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: '@receiver',
into: '@item',
},
// The undefined this for the callback
{
kind: 'Create',
into: '@thisArg',
value: ValueKind.Primitive,
reason: ValueReason.KnownReturnSignature,
},
// calls the callback, returning the result into a temporary
{
kind: 'Apply',
receiver: '@thisArg',
args: ['@item', {kind: 'Hole'}, '@receiver'],
function: '@callback',
into: '@callbackReturn',
mutatesFunction: false,
},
// captures the result of the callback into the return array
{
kind: 'Capture',
from: '@callbackReturn',
into: '@returns',
},
],
},
}),
],
[
@@ -479,6 +717,32 @@ addObject(BUILTIN_SHAPES, BuiltInSetId, [
calleeEffect: Effect.Store,
// returnValueKind is technically dependent on the ValueKind of the set itself
returnValueKind: ValueKind.Mutable,
aliasing: {
receiver: '@receiver',
params: [],
rest: '@rest',
returns: '@returns',
temporaries: [],
effects: [
// Set.add returns the receiver Set
{
kind: 'Assign',
from: '@receiver',
into: '@returns',
},
// Set.add mutates the set itself
{
kind: 'Mutate',
value: '@receiver',
},
// Captures the rest params into the set
{
kind: 'Capture',
from: '@rest',
into: '@receiver',
},
],
},
}),
],
[
@@ -948,6 +1212,21 @@ addObject(BUILTIN_SHAPES, BuiltInRefValueId, [
['*', {kind: 'Object', shapeId: BuiltInRefValueId}],
]);
addObject(BUILTIN_SHAPES, ReanimatedSharedValueId, []);
addFunction(
BUILTIN_SHAPES,
[],
{
positionalParams: [],
restParam: Effect.ConditionallyMutate,
returnType: {kind: 'Poly'},
calleeEffect: Effect.ConditionallyMutate,
returnValueKind: ValueKind.Mutable,
},
BuiltinEffectEventId,
);
/**
* MixedReadOnly =
* | primitive
@@ -1166,6 +1445,53 @@ export const DefaultNonmutatingHook = addHook(
calleeEffect: Effect.Read,
hookKind: 'Custom',
returnValueKind: ValueKind.Frozen,
aliasing: {
receiver: '@receiver',
params: [],
rest: '@rest',
returns: '@returns',
temporaries: [],
effects: [
// Freeze the arguments
{
kind: 'Freeze',
value: '@rest',
reason: ValueReason.HookCaptured,
},
// Returns a frozen value
{
kind: 'Create',
into: '@returns',
value: ValueKind.Frozen,
reason: ValueReason.HookReturn,
},
// May alias any arguments into the return
{
kind: 'Alias',
from: '@rest',
into: '@returns',
},
],
},
},
'DefaultNonmutatingHook',
);
export function signatureArgument(id: number): Place {
const place: Place = {
kind: 'Identifier',
effect: Effect.Unknown,
loc: GeneratedSource,
reactive: false,
identifier: {
declarationId: makeDeclarationId(id),
id: makeIdentifierId(id),
loc: GeneratedSource,
mutableRange: {start: makeInstructionId(0), end: makeInstructionId(0)},
name: null,
scope: null,
type: makeType(),
},
};
return place;
}

View File

@@ -5,7 +5,6 @@
* LICENSE file in the root directory of this source tree.
*/
import generate from '@babel/generator';
import {CompilerError} from '../CompilerError';
import {printReactiveScopeSummary} from '../ReactiveScopes/PrintReactiveFunction';
import DisjointSet from '../Utils/DisjointSet';
@@ -35,6 +34,7 @@ import type {
Type,
} from './HIR';
import {GotoVariant, InstructionKind} from './HIR';
import {AliasingEffect, AliasingSignature} from '../Inference/AliasingEffects';
export type Options = {
indent: number;
@@ -53,6 +53,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 +=
@@ -67,13 +69,13 @@ export function printFunction(fn: HIRFunction): string {
})
.join(', ') +
')';
} else {
definition += '()';
}
if (definition.length !== 0) {
output.push(definition);
}
output.push(printType(fn.returnType));
output.push(printHIR(fn.body));
definition += `: ${printPlace(fn.returns)}`;
output.push(definition);
output.push(...fn.directives);
output.push(printHIR(fn.body));
return output.join('\n');
}
@@ -151,7 +153,10 @@ export function printMixedHIR(
export function printInstruction(instr: ReactiveInstruction): string {
const id = `[${instr.id}]`;
const value = printInstructionValue(instr.value);
let value = printInstructionValue(instr.value);
if (instr.effects != null) {
value += `\n ${instr.effects.map(printAliasingEffect).join('\n ')}`;
}
if (instr.lvalue !== null) {
return `${id} ${printPlace(instr.lvalue)} = ${value}`;
@@ -210,9 +215,12 @@ export function printTerminal(terminal: Terminal): Array<string> | string {
break;
}
case 'return': {
value = `[${terminal.id}] Return${
value = `[${terminal.id}] Return ${terminal.returnVariant}${
terminal.value != null ? ' ' + printPlace(terminal.value) : ''
}`;
if (terminal.effects != null) {
value += `\n ${terminal.effects.map(printAliasingEffect).join('\n ')}`;
}
break;
}
case 'goto': {
@@ -281,6 +289,9 @@ export function printTerminal(terminal: Terminal): Array<string> | string {
}
case 'maybe-throw': {
value = `[${terminal.id}] MaybeThrow continuation=bb${terminal.continuation} handler=bb${terminal.handler}`;
if (terminal.effects != null) {
value += `\n ${terminal.effects.map(printAliasingEffect).join('\n ')}`;
}
break;
}
case 'scope': {
@@ -454,7 +465,7 @@ export function printInstructionValue(instrValue: ReactiveValue): string {
break;
}
case 'UnsupportedNode': {
value = `UnsupportedNode(${generate(instrValue.node).code})`;
value = `UnsupportedNode ${instrValue.node.type}`;
break;
}
case 'LoadLocal': {
@@ -543,20 +554,11 @@ export function printInstructionValue(instrValue: ReactiveValue): string {
const context = instrValue.loweredFunc.func.context
.map(dep => printPlace(dep))
.join(',');
const effects =
instrValue.loweredFunc.func.effects
?.map(effect => {
if (effect.kind === 'ContextMutation') {
return `ContextMutation places=[${[...effect.places]
.map(place => printPlace(place))
.join(', ')}] effect=${effect.effect}`;
} else {
return `GlobalMutation`;
}
})
.join(', ') ?? '';
const type = printType(instrValue.loweredFunc.func.returnType).trim();
value = `${kind} ${name} @context[${context}] @effects[${effects}]${type !== '' ? ` return${type}` : ''}:\n${fn}`;
const aliasingEffects =
instrValue.loweredFunc.func.aliasingEffects
?.map(printAliasingEffect)
?.join(', ') ?? '';
value = `${kind} ${name} @context[${context}] @aliasingEffects=[${aliasingEffects}]\n${fn}`;
break;
}
case 'TaggedTemplateExpression': {
@@ -700,7 +702,7 @@ export function printInstructionValue(instrValue: ReactiveValue): string {
break;
}
case 'FinishMemoize': {
value = `FinishMemoize decl=${printPlace(instrValue.decl)}`;
value = `FinishMemoize decl=${printPlace(instrValue.decl)}${instrValue.pruned ? ' pruned' : ''}`;
break;
}
default: {
@@ -922,3 +924,110 @@ function getFunctionName(
return defaultValue;
}
}
export function printAliasingEffect(effect: AliasingEffect): string {
switch (effect.kind) {
case 'Assign': {
return `Assign ${printPlaceForAliasEffect(effect.into)} = ${printPlaceForAliasEffect(effect.from)}`;
}
case 'Alias': {
return `Alias ${printPlaceForAliasEffect(effect.into)} <- ${printPlaceForAliasEffect(effect.from)}`;
}
case 'MaybeAlias': {
return `MaybeAlias ${printPlaceForAliasEffect(effect.into)} <- ${printPlaceForAliasEffect(effect.from)}`;
}
case 'Capture': {
return `Capture ${printPlaceForAliasEffect(effect.into)} <- ${printPlaceForAliasEffect(effect.from)}`;
}
case 'ImmutableCapture': {
return `ImmutableCapture ${printPlaceForAliasEffect(effect.into)} <- ${printPlaceForAliasEffect(effect.from)}`;
}
case 'Create': {
return `Create ${printPlaceForAliasEffect(effect.into)} = ${effect.value}`;
}
case 'CreateFrom': {
return `Create ${printPlaceForAliasEffect(effect.into)} = kindOf(${printPlaceForAliasEffect(effect.from)})`;
}
case 'CreateFunction': {
return `Function ${printPlaceForAliasEffect(effect.into)} = Function captures=[${effect.captures.map(printPlaceForAliasEffect).join(', ')}]`;
}
case 'Apply': {
const receiverCallee =
effect.receiver.identifier.id === effect.function.identifier.id
? printPlaceForAliasEffect(effect.receiver)
: `${printPlaceForAliasEffect(effect.receiver)}.${printPlaceForAliasEffect(effect.function)}`;
const args = effect.args
.map(arg => {
if (arg.kind === 'Identifier') {
return printPlaceForAliasEffect(arg);
} else if (arg.kind === 'Hole') {
return ' ';
}
return `...${printPlaceForAliasEffect(arg.place)}`;
})
.join(', ');
let signature = '';
if (effect.signature != null) {
if (effect.signature.aliasing != null) {
signature = printAliasingSignature(effect.signature.aliasing);
} else {
signature = JSON.stringify(effect.signature, null, 2);
}
}
return `Apply ${printPlaceForAliasEffect(effect.into)} = ${receiverCallee}(${args})${signature != '' ? '\n ' : ''}${signature}`;
}
case 'Freeze': {
return `Freeze ${printPlaceForAliasEffect(effect.value)} ${effect.reason}`;
}
case 'Mutate':
case 'MutateConditionally':
case 'MutateTransitive':
case 'MutateTransitiveConditionally': {
return `${effect.kind} ${printPlaceForAliasEffect(effect.value)}`;
}
case 'MutateFrozen': {
return `MutateFrozen ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`;
}
case 'MutateGlobal': {
return `MutateGlobal ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`;
}
case 'Impure': {
return `Impure ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`;
}
case 'Render': {
return `Render ${printPlaceForAliasEffect(effect.place)}`;
}
default: {
assertExhaustive(effect, `Unexpected kind '${(effect as any).kind}'`);
}
}
}
function printPlaceForAliasEffect(place: Place): string {
return printIdentifier(place.identifier);
}
export function printAliasingSignature(signature: AliasingSignature): string {
const tokens: Array<string> = ['function '];
if (signature.temporaries.length !== 0) {
tokens.push('<');
tokens.push(
signature.temporaries.map(temp => `$${temp.identifier.id}`).join(', '),
);
tokens.push('>');
}
tokens.push('(');
tokens.push('this=$' + String(signature.receiver));
for (const param of signature.params) {
tokens.push(', $' + String(param));
}
if (signature.rest != null) {
tokens.push(`, ...$${String(signature.rest)}`);
}
tokens.push('): ');
tokens.push('$' + String(signature.returns) + ':');
for (const effect of signature.effects) {
tokens.push('\n ' + printAliasingEffect(effect));
}
return tokens.join('');
}

View File

@@ -316,6 +316,7 @@ function collectTemporariesSidemapImpl(
) {
temporaries.set(lvalue.identifier.id, {
identifier: value.place.identifier,
reactive: value.place.reactive,
path: [],
});
}
@@ -369,11 +370,13 @@ function getProperty(
if (resolvedDependency == null) {
property = {
identifier: object.identifier,
reactive: object.reactive,
path: [{property: propertyName, optional}],
};
} else {
property = {
identifier: resolvedDependency.identifier,
reactive: resolvedDependency.reactive,
path: [...resolvedDependency.path, {property: propertyName, optional}],
};
}
@@ -532,6 +535,7 @@ export class DependencyCollectionContext {
this.visitDependency(
this.#temporaries.get(place.identifier.id) ?? {
identifier: place.identifier,
reactive: place.reactive,
path: [],
},
);
@@ -596,6 +600,7 @@ export class DependencyCollectionContext {
) {
maybeDependency = {
identifier: maybeDependency.identifier,
reactive: maybeDependency.reactive,
path: [],
};
}
@@ -617,7 +622,11 @@ export class DependencyCollectionContext {
identifier =>
identifier.declarationId === place.identifier.declarationId,
) &&
this.#checkValidDependency({identifier: place.identifier, path: []})
this.#checkValidDependency({
identifier: place.identifier,
reactive: place.reactive,
path: [],
})
) {
currentScope.reassignments.add(place.identifier);
}

View File

@@ -0,0 +1,285 @@
import {
Place,
ReactiveScopeDependency,
Identifier,
makeInstructionId,
InstructionKind,
GeneratedSource,
BlockId,
makeTemporaryIdentifier,
Effect,
GotoVariant,
HIR,
} from './HIR';
import {CompilerError} from '../CompilerError';
import {Environment} from './Environment';
import HIRBuilder from './HIRBuilder';
import {lowerValueToTemporary} from './BuildHIR';
type DependencyInstructions = {
place: Place;
value: HIR;
exitBlockId: BlockId;
};
export function buildDependencyInstructions(
dep: ReactiveScopeDependency,
env: Environment,
): DependencyInstructions {
const builder = new HIRBuilder(env, {
entryBlockKind: 'value',
});
let dependencyValue: Identifier;
if (dep.path.every(path => !path.optional)) {
dependencyValue = writeNonOptionalDependency(dep, env, builder);
} else {
dependencyValue = writeOptionalDependency(dep, builder, null);
}
const exitBlockId = builder.terminate(
{
kind: 'unsupported',
loc: GeneratedSource,
id: makeInstructionId(0),
},
null,
);
return {
place: {
kind: 'Identifier',
identifier: dependencyValue,
effect: Effect.Freeze,
reactive: dep.reactive,
loc: GeneratedSource,
},
value: builder.build(),
exitBlockId,
};
}
/**
* Write instructions for a simple dependency (without optional chains)
*/
function writeNonOptionalDependency(
dep: ReactiveScopeDependency,
env: Environment,
builder: HIRBuilder,
): Identifier {
const loc = dep.identifier.loc;
let curr: Identifier = makeTemporaryIdentifier(env.nextIdentifierId, loc);
builder.push({
lvalue: {
identifier: curr,
kind: 'Identifier',
effect: Effect.Mutate,
reactive: dep.reactive,
loc,
},
value: {
kind: 'LoadLocal',
place: {
identifier: dep.identifier,
kind: 'Identifier',
effect: Effect.Freeze,
reactive: dep.reactive,
loc,
},
loc,
},
id: makeInstructionId(1),
loc: loc,
effects: null,
});
/**
* Iteratively build up dependency instructions by reading from the last written
* instruction.
*/
for (const path of dep.path) {
const next = makeTemporaryIdentifier(env.nextIdentifierId, loc);
builder.push({
lvalue: {
identifier: next,
kind: 'Identifier',
effect: Effect.Mutate,
reactive: dep.reactive,
loc,
},
value: {
kind: 'PropertyLoad',
object: {
identifier: curr,
kind: 'Identifier',
effect: Effect.Freeze,
reactive: dep.reactive,
loc,
},
property: path.property,
loc,
},
id: makeInstructionId(1),
loc: loc,
effects: null,
});
curr = next;
}
return curr;
}
/**
* Write a dependency into optional blocks.
*
* e.g. `a.b?.c.d` is written to an optional block that tests `a.b` and
* conditionally evaluates `c.d`.
*/
function writeOptionalDependency(
dep: ReactiveScopeDependency,
builder: HIRBuilder,
parentAlternate: BlockId | null,
): Identifier {
const env = builder.environment;
/**
* Reserve an identifier which will be used to store the result of this
* dependency.
*/
const dependencyValue: Place = {
kind: 'Identifier',
identifier: makeTemporaryIdentifier(env.nextIdentifierId, GeneratedSource),
effect: Effect.Mutate,
reactive: dep.reactive,
loc: GeneratedSource,
};
/**
* Reserve a block which is the fallthrough (and transitive successor) of this
* optional chain.
*/
const continuationBlock = builder.reserve(builder.currentBlockKind());
let alternate;
if (parentAlternate != null) {
alternate = parentAlternate;
} else {
/**
* If an outermost alternate block has not been reserved, write one
*
* $N = Primitive undefined
* $M = StoreLocal $OptionalResult = $N
* goto fallthrough
*/
alternate = builder.enter('value', () => {
const temp = lowerValueToTemporary(builder, {
kind: 'Primitive',
value: undefined,
loc: GeneratedSource,
});
lowerValueToTemporary(builder, {
kind: 'StoreLocal',
lvalue: {kind: InstructionKind.Const, place: {...dependencyValue}},
value: {...temp},
type: null,
loc: GeneratedSource,
});
return {
kind: 'goto',
variant: GotoVariant.Break,
block: continuationBlock.id,
id: makeInstructionId(0),
loc: GeneratedSource,
};
});
}
// Reserve the consequent block, which is the successor of the test block
const consequent = builder.reserve('value');
let testIdentifier: Identifier | null = null;
const testBlock = builder.enter('value', () => {
const testDependency = {
...dep,
path: dep.path.slice(0, dep.path.length - 1),
};
const firstOptional = dep.path.findIndex(path => path.optional);
CompilerError.invariant(firstOptional !== -1, {
reason:
'[ScopeDependencyUtils] Internal invariant broken: expected optional path',
loc: dep.identifier.loc,
description: null,
suggestions: null,
});
if (firstOptional === dep.path.length - 1) {
// Base case: the test block is simple
testIdentifier = writeNonOptionalDependency(testDependency, env, builder);
} else {
// Otherwise, the test block is a nested optional chain
testIdentifier = writeOptionalDependency(
testDependency,
builder,
alternate,
);
}
return {
kind: 'branch',
test: {
identifier: testIdentifier,
effect: Effect.Freeze,
kind: 'Identifier',
loc: GeneratedSource,
reactive: dep.reactive,
},
consequent: consequent.id,
alternate,
id: makeInstructionId(0),
loc: GeneratedSource,
fallthrough: continuationBlock.id,
};
});
builder.enterReserved(consequent, () => {
CompilerError.invariant(testIdentifier !== null, {
reason: 'Satisfy type checker',
description: null,
loc: null,
suggestions: null,
});
lowerValueToTemporary(builder, {
kind: 'StoreLocal',
lvalue: {kind: InstructionKind.Const, place: {...dependencyValue}},
value: lowerValueToTemporary(builder, {
kind: 'PropertyLoad',
object: {
identifier: testIdentifier,
kind: 'Identifier',
effect: Effect.Freeze,
reactive: dep.reactive,
loc: GeneratedSource,
},
property: dep.path.at(-1)!.property,
loc: GeneratedSource,
}),
type: null,
loc: GeneratedSource,
});
return {
kind: 'goto',
variant: GotoVariant.Break,
block: continuationBlock.id,
id: makeInstructionId(0),
loc: GeneratedSource,
};
});
builder.terminateWithContinuation(
{
kind: 'optional',
optional: dep.path.at(-1)!.optional,
test: testBlock,
fallthrough: continuationBlock.id,
id: makeInstructionId(0),
loc: GeneratedSource,
},
continuationBlock,
);
return dependencyValue.identifier;
}

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,209 @@ 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 ImmutableCaptureEffectConfig = {
kind: 'ImmutableCapture';
from: string;
into: string;
};
export const ImmutableCaptureEffectSchema: z.ZodType<ImmutableCaptureEffectConfig> =
z.object({
kind: z.literal('ImmutableCapture'),
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
| ImmutableCaptureEffectConfig
| ImpureEffectConfig
| MutateEffectConfig
| MutateTransitiveConditionallyConfig
| ApplyEffectConfig;
export const AliasingEffectSchema: z.ZodType<AliasingEffectConfig> = z.union([
FreezeEffectSchema,
CreateEffectSchema,
CreateFromEffectSchema,
AssignEffectSchema,
AliasEffectSchema,
CaptureEffectSchema,
ImmutableCaptureEffectSchema,
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 +250,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 +263,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 +273,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 +282,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

@@ -345,6 +345,51 @@ export function* eachPatternOperand(pattern: Pattern): Iterable<Place> {
}
}
export function* eachPatternItem(
pattern: Pattern,
): Iterable<Place | SpreadPattern> {
switch (pattern.kind) {
case 'ArrayPattern': {
for (const item of pattern.items) {
if (item.kind === 'Identifier') {
yield item;
} else if (item.kind === 'Spread') {
yield item;
} else if (item.kind === 'Hole') {
continue;
} else {
assertExhaustive(
item,
`Unexpected item kind \`${(item as any).kind}\``,
);
}
}
break;
}
case 'ObjectPattern': {
for (const property of pattern.properties) {
if (property.kind === 'ObjectProperty') {
yield property.place;
} else if (property.kind === 'Spread') {
yield property;
} else {
assertExhaustive(
property,
`Unexpected item kind \`${(property as any).kind}\``,
);
}
}
break;
}
default: {
assertExhaustive(
pattern,
`Unexpected pattern kind \`${(pattern as any).kind}\``,
);
}
}
}
export function mapInstructionLValues(
instr: Instruction,
fn: (place: Place) => Place,
@@ -732,9 +777,11 @@ export function mapTerminalSuccessors(
case 'return': {
return {
kind: 'return',
returnVariant: terminal.returnVariant,
loc: terminal.loc,
value: terminal.value,
id: makeInstructionId(0),
effects: terminal.effects,
};
}
case 'throw': {
@@ -842,6 +889,7 @@ export function mapTerminalSuccessors(
handler,
id: makeInstructionId(0),
loc: terminal.loc,
effects: terminal.effects,
};
}
case 'try': {

View File

@@ -0,0 +1,264 @@
/**
* 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 {CompilerDiagnostic} from '../CompilerError';
import {
FunctionExpression,
GeneratedSource,
Hole,
IdentifierId,
ObjectMethod,
Place,
SourceLocation,
SpreadPattern,
ValueKind,
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
* more values in a program. These effects include mutation of values, freezing values,
* tracking data flow between values, and other specialized cases.
*/
export type AliasingEffect =
/**
* Marks the given value and its direct aliases as frozen.
*
* Captured values are *not* considered frozen, because we cannot be sure that a previously
* captured value will still be captured at the point of the freeze.
*
* For example:
* const x = {};
* const y = [x];
* y.pop(); // y dosn't contain x anymore!
* freeze(y);
* mutate(x); // safe to mutate!
*
* The exception to this is FunctionExpressions - since it is impossible to change which
* value a function closes over[1] we can transitively freeze functions and their captures.
*
* [1] Except for `let` values that are reassigned and closed over by a function, but we
* handle this explicitly with StoreContext/LoadContext.
*/
| {kind: 'Freeze'; value: Place; reason: ValueReason}
/**
* Mutate the value and any direct aliases (not captures). Errors if the value is not mutable.
*/
| {kind: 'Mutate'; value: Place; reason?: MutationReason | null}
/**
* Mutate the value and any direct aliases (not captures), but only if the value is known mutable.
* This should be rare.
*
* TODO: this is only used for IteratorNext, but even then MutateTransitiveConditionally is more
* correct for iterators of unknown types.
*/
| {kind: 'MutateConditionally'; value: Place}
/**
* Mutate the value, any direct aliases, and any transitive captures. Errors if the value is not mutable.
*/
| {kind: 'MutateTransitive'; value: Place}
/**
* Mutates any of the value, its direct aliases, and its transitive captures that are mutable.
*/
| {kind: 'MutateTransitiveConditionally'; value: Place}
/**
* Records information flow from `from` to `into` in cases where local mutation of the destination
* will *not* mutate the source:
*
* - Capture a -> b and Mutate(b) X=> (does not imply) Mutate(a)
* - Capture a -> b and MutateTransitive(b) => (does imply) Mutate(a)
*
* Example: `array.push(item)`. Information from item is captured into array, but there is not a
* direct aliasing, and local mutations of array will not modify item.
*/
| {kind: 'Capture'; from: Place; into: Place}
/**
* Records information flow from `from` to `into` in cases where local mutation of the destination
* *will* mutate the source:
*
* - Alias a -> b and Mutate(b) => (does imply) Mutate(a)
* - Alias a -> b and MutateTransitive(b) => (does imply) Mutate(a)
*
* Example: `c = identity(a)`. We don't know what `identity()` returns so we can't use Assign.
* But we have to assume that it _could_ be returning its input, such that a local mutation of
* c could be mutating a.
*/
| {kind: 'Alias'; from: Place; into: Place}
/**
* Indicates the potential for information flow from `from` to `into`. This is used for a specific
* case: functions with unknown signatures. If the compiler sees a call such as `foo(x)`, it has to
* consider several possibilities (which may depend on the arguments):
* - foo(x) returns a new mutable value that does not capture any information from x.
* - foo(x) returns a new mutable value that *does* capture information from x.
* - foo(x) returns x itself, ie foo is the identity function
*
* The same is true of functions that take multiple arguments: `cond(a, b, c)` could conditionally
* return b or c depending on the value of a.
*
* To represent this case, MaybeAlias represents the fact that an aliasing relationship could exist.
* Any mutations that flow through this relationship automatically become conditional.
*/
| {kind: 'MaybeAlias'; from: Place; into: Place}
/**
* Records direct assignment: `into = from`.
*/
| {kind: 'Assign'; from: Place; into: Place}
/**
* Creates a value of the given type at the given place
*/
| {kind: 'Create'; into: Place; value: ValueKind; reason: ValueReason}
/**
* Creates a new value with the same kind as the starting value.
*/
| {kind: 'CreateFrom'; from: Place; into: Place}
/**
* Immutable data flow, used for escape analysis. Does not influence mutable range analysis:
*/
| {kind: 'ImmutableCapture'; from: Place; into: Place}
/**
* Calls the function at the given place with the given arguments either captured or aliased,
* and captures/aliases the result into the given place.
*/
| {
kind: 'Apply';
receiver: Place;
function: Place;
mutatesFunction: boolean;
args: Array<Place | SpreadPattern | Hole>;
into: Place;
signature: FunctionSignature | null;
loc: SourceLocation;
}
/**
* Constructs a function value with the given captures. The mutability of the function
* will be determined by the mutability of the capture values when evaluated.
*/
| {
kind: 'CreateFunction';
captures: Array<Place>;
function: FunctionExpression | ObjectMethod;
into: Place;
}
/**
* Mutation of a value known to be immutable
*/
| {kind: 'MutateFrozen'; place: Place; error: CompilerDiagnostic}
/**
* Mutation of a global
*/
| {
kind: 'MutateGlobal';
place: Place;
error: CompilerDiagnostic;
}
/**
* Indicates a side-effect that is not safe during render
*/
| {kind: 'Impure'; place: Place; error: CompilerDiagnostic}
/**
* Indicates that a given place is accessed during render. Used to distingush
* hook arguments that are known to be called immediately vs those used for
* event handlers/effects, and for JSX values known to be called during render
* (tags, children) vs those that may be events/effect (other props).
*/
| {
kind: 'Render';
place: Place;
};
export type MutationReason = {kind: 'AssignCurrentProperty'};
export function hashEffect(effect: AliasingEffect): string {
switch (effect.kind) {
case 'Apply': {
return [
effect.kind,
effect.receiver.identifier.id,
effect.function.identifier.id,
effect.mutatesFunction,
effect.args
.map(a => {
if (a.kind === 'Hole') {
return '';
} else if (a.kind === 'Identifier') {
return a.identifier.id;
} else {
return `...${a.place.identifier.id}`;
}
})
.join(','),
effect.into.identifier.id,
].join(':');
}
case 'CreateFrom':
case 'ImmutableCapture':
case 'Assign':
case 'Alias':
case 'Capture':
case 'MaybeAlias': {
return [
effect.kind,
effect.from.identifier.id,
effect.into.identifier.id,
].join(':');
}
case 'Create': {
return [
effect.kind,
effect.into.identifier.id,
effect.value,
effect.reason,
].join(':');
}
case 'Freeze': {
return [effect.kind, effect.value.identifier.id, effect.reason].join(':');
}
case 'Impure':
case 'Render': {
return [effect.kind, effect.place.identifier.id].join(':');
}
case 'MutateFrozen':
case 'MutateGlobal': {
return [
effect.kind,
effect.place.identifier.id,
effect.error.severity,
effect.error.reason,
effect.error.description,
printSourceLocation(effect.error.primaryLocation() ?? GeneratedSource),
].join(':');
}
case 'Mutate':
case 'MutateConditionally':
case 'MutateTransitive':
case 'MutateTransitiveConditionally': {
return [effect.kind, effect.value.identifier.id].join(':');
}
case 'CreateFunction': {
return [
effect.kind,
effect.into.identifier.id,
// return places are a unique way to identify functions themselves
effect.function.loweredFunc.func.returns.identifier.id,
effect.captures.map(p => p.identifier.id).join(','),
].join(':');
}
}
}
export type AliasingSignature = {
receiver: IdentifierId;
params: Array<IdentifierId>;
rest: IdentifierId | null;
returns: IdentifierId;
effects: Array<AliasingEffect>;
temporaries: Array<Place>;
};

View File

@@ -6,19 +6,13 @@
*/
import {CompilerError} from '../CompilerError';
import {
Effect,
HIRFunction,
Identifier,
LoweredFunction,
isRefOrRefValue,
makeInstructionId,
} from '../HIR';
import {Effect, HIRFunction, IdentifierId, makeInstructionId} from '../HIR';
import {deadCodeElimination} from '../Optimization';
import {inferReactiveScopeVariables} from '../ReactiveScopes';
import {rewriteInstructionKindsBasedOnReassignment} from '../SSA';
import {inferMutableRanges} from './InferMutableRanges';
import inferReferenceEffects from './InferReferenceEffects';
import {assertExhaustive} from '../Utils/utils';
import {inferMutationAliasingEffects} from './InferMutationAliasingEffects';
import {inferMutationAliasingRanges} from './InferMutationAliasingRanges';
export default function analyseFunctions(func: HIRFunction): void {
for (const [_, block] of func.body.blocks) {
@@ -26,15 +20,22 @@ export default function analyseFunctions(func: HIRFunction): void {
switch (instr.value.kind) {
case 'ObjectMethod':
case 'FunctionExpression': {
lower(instr.value.loweredFunc.func);
infer(instr.value.loweredFunc);
lowerWithMutationAliasing(instr.value.loweredFunc.func);
/**
* Reset mutable range for outer inferReferenceEffects
*/
for (const operand of instr.value.loweredFunc.func.context) {
operand.identifier.mutableRange.start = makeInstructionId(0);
operand.identifier.mutableRange.end = makeInstructionId(0);
/**
* NOTE: inferReactiveScopeVariables makes identifiers in the scope
* point to the *same* mutableRange instance. Resetting start/end
* here is insufficient, because a later mutation of the range
* for any one identifier could affect the range for other identifiers.
*/
operand.identifier.mutableRange = {
start: makeInstructionId(0),
end: makeInstructionId(0),
};
operand.identifier.scope = null;
}
break;
@@ -44,57 +45,83 @@ export default function analyseFunctions(func: HIRFunction): void {
}
}
function lower(func: HIRFunction): void {
analyseFunctions(func);
inferReferenceEffects(func, {isFunctionExpression: true});
deadCodeElimination(func);
inferMutableRanges(func);
rewriteInstructionKindsBasedOnReassignment(func);
inferReactiveScopeVariables(func);
func.env.logger?.debugLogIRs?.({
kind: 'hir',
name: 'AnalyseFunction (inner)',
value: func,
});
}
function lowerWithMutationAliasing(fn: HIRFunction): void {
/**
* Phase 1: similar to lower(), but using the new mutation/aliasing inference
*/
analyseFunctions(fn);
inferMutationAliasingEffects(fn, {isFunctionExpression: true});
deadCodeElimination(fn);
const functionEffects = inferMutationAliasingRanges(fn, {
isFunctionExpression: true,
}).unwrap();
rewriteInstructionKindsBasedOnReassignment(fn);
inferReactiveScopeVariables(fn);
fn.aliasingEffects = functionEffects;
function infer(loweredFunc: LoweredFunction): void {
for (const operand of loweredFunc.func.context) {
const identifier = operand.identifier;
CompilerError.invariant(operand.effect === Effect.Unknown, {
reason:
'[AnalyseFunctions] Expected Function context effects to not have been set',
loc: operand.loc,
});
if (isRefOrRefValue(identifier)) {
/*
* TODO: this is a hack to ensure we treat functions which reference refs
* as having a capture and therefore being considered mutable. this ensures
* the function gets a mutable range which accounts for anywhere that it
* could be called, and allows us to help ensure it isn't called during
* render
*/
operand.effect = Effect.Capture;
} else if (isMutatedOrReassigned(identifier)) {
/**
* Reflects direct reassignments, PropertyStores, and ConditionallyMutate
* (directly or through maybe-aliases)
*/
/**
* Phase 2: populate the Effect of each context variable to use in inferring
* the outer function. For example, InferMutationAliasingEffects uses context variable
* effects to decide if the function may be mutable or not.
*/
const capturedOrMutated = new Set<IdentifierId>();
for (const effect of functionEffects) {
switch (effect.kind) {
case 'Assign':
case 'Alias':
case 'Capture':
case 'CreateFrom':
case 'MaybeAlias': {
capturedOrMutated.add(effect.from.identifier.id);
break;
}
case 'Apply': {
CompilerError.invariant(false, {
reason: `[AnalyzeFunctions] Expected Apply effects to be replaced with more precise effects`,
loc: effect.function.loc,
});
}
case 'Mutate':
case 'MutateConditionally':
case 'MutateTransitive':
case 'MutateTransitiveConditionally': {
capturedOrMutated.add(effect.value.identifier.id);
break;
}
case 'Impure':
case 'Render':
case 'MutateFrozen':
case 'MutateGlobal':
case 'CreateFunction':
case 'Create':
case 'Freeze':
case 'ImmutableCapture': {
// no-op
break;
}
default: {
assertExhaustive(
effect,
`Unexpected effect kind ${(effect as any).kind}`,
);
}
}
}
for (const operand of fn.context) {
if (
capturedOrMutated.has(operand.identifier.id) ||
operand.effect === Effect.Capture
) {
operand.effect = Effect.Capture;
} else {
operand.effect = Effect.Read;
}
}
}
function isMutatedOrReassigned(id: Identifier): boolean {
/*
* This check checks for mutation and reassingnment, so the usual check for
* mutation (ie, `mutableRange.end - mutableRange.start > 1`) isn't quite
* enough.
*
* We need to track re-assignments in context refs as we need to reflect the
* re-assignment back to the captured refs.
*/
return id.mutableRange.end > id.mutableRange.start;
fn.env.logger?.debugLogIRs?.({
kind: 'hir',
name: 'AnalyseFunction (inner)',
value: fn,
});
}

View File

@@ -5,7 +5,13 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError, SourceLocation} from '..';
import {
CompilerDiagnostic,
CompilerError,
ErrorSeverity,
SourceLocation,
} from '..';
import {ErrorCategory} from '../CompilerError';
import {
CallExpression,
Effect,
@@ -30,6 +36,7 @@ import {
makeInstructionId,
} from '../HIR';
import {createTemporaryPlace, markInstructionIds} from '../HIR/HIRBuilder';
import {Result} from '../Utils/Result';
type ManualMemoCallee = {
kind: 'useMemo' | 'useCallback';
@@ -197,6 +204,7 @@ function makeManualMemoizationMarkers(
deps: depsList,
loc: fnExpr.loc,
},
effects: null,
loc: fnExpr.loc,
},
{
@@ -208,6 +216,7 @@ function makeManualMemoizationMarkers(
decl: {...memoDecl},
loc: fnExpr.loc,
},
effects: null,
loc: fnExpr.loc,
},
];
@@ -281,26 +290,45 @@ function extractManualMemoizationArgs(
instr: TInstruction<CallExpression> | TInstruction<MethodCall>,
kind: 'useCallback' | 'useMemo',
sidemap: IdentifierSidemap,
errors: CompilerError,
): {
fnPlace: Place;
fnPlace: Place | null;
depsList: Array<ManualMemoDependency> | null;
} {
const [fnPlace, depsListPlace] = instr.value.args as Array<
Place | SpreadPattern | undefined
>;
if (fnPlace == null) {
CompilerError.throwInvalidReact({
reason: `Expected a callback function to be passed to ${kind}`,
loc: instr.value.loc,
suggestions: null,
});
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
severity: ErrorSeverity.InvalidReact,
reason: `Expected a callback function to be passed to ${kind}`,
description: `Expected a callback function to be passed to ${kind}`,
suggestions: null,
}).withDetail({
kind: 'error',
loc: instr.value.loc,
message: `Expected a callback function to be passed to ${kind}`,
}),
);
return {fnPlace: null, depsList: null};
}
if (fnPlace.kind === 'Spread' || depsListPlace?.kind === 'Spread') {
CompilerError.throwInvalidReact({
reason: `Unexpected spread argument to ${kind}`,
loc: instr.value.loc,
suggestions: null,
});
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
severity: ErrorSeverity.InvalidReact,
reason: `Unexpected spread argument to ${kind}`,
description: `Unexpected spread argument to ${kind}`,
suggestions: null,
}).withDetail({
kind: 'error',
loc: instr.value.loc,
message: `Unexpected spread argument to ${kind}`,
}),
);
return {fnPlace: null, depsList: null};
}
let depsList: Array<ManualMemoDependency> | null = null;
if (depsListPlace != null) {
@@ -308,23 +336,42 @@ function extractManualMemoizationArgs(
depsListPlace.identifier.id,
);
if (maybeDepsList == null) {
CompilerError.throwInvalidReact({
reason: `Expected the dependency list for ${kind} to be an array literal`,
suggestions: null,
loc: depsListPlace.loc,
});
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
severity: ErrorSeverity.InvalidReact,
reason: `Expected the dependency list for ${kind} to be an array literal`,
description: `Expected the dependency list for ${kind} to be an array literal`,
suggestions: null,
}).withDetail({
kind: 'error',
loc: depsListPlace.loc,
message: `Expected the dependency list for ${kind} to be an array literal`,
}),
);
return {fnPlace, depsList: null};
}
depsList = maybeDepsList.map(dep => {
depsList = [];
for (const dep of maybeDepsList) {
const maybeDep = sidemap.maybeDeps.get(dep.identifier.id);
if (maybeDep == null) {
CompilerError.throwInvalidReact({
reason: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
suggestions: null,
loc: dep.loc,
});
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
severity: ErrorSeverity.InvalidReact,
reason: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
description: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
suggestions: null,
}).withDetail({
kind: 'error',
loc: dep.loc,
message: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
}),
);
} else {
depsList.push(maybeDep);
}
return maybeDep;
});
}
}
return {
fnPlace,
@@ -339,8 +386,14 @@ function extractManualMemoizationArgs(
* rely on type inference to find useMemo/useCallback invocations, and instead does basic tracking
* of globals and property loads to find both direct calls as well as usage via the React namespace,
* eg `React.useMemo()`.
*
* This pass also validates that useMemo callbacks return a value (not void), ensuring that useMemo
* is only used for memoizing values and not for running arbitrary side effects.
*/
export function dropManualMemoization(func: HIRFunction): void {
export function dropManualMemoization(
func: HIRFunction,
): Result<void, CompilerError> {
const errors = new CompilerError();
const isValidationEnabled =
func.env.config.validatePreserveExistingMemoizationGuarantees ||
func.env.config.validateNoSetStateInRender ||
@@ -387,7 +440,48 @@ export function dropManualMemoization(func: HIRFunction): void {
instr as TInstruction<CallExpression> | TInstruction<MethodCall>,
manualMemo.kind,
sidemap,
errors,
);
if (fnPlace == null) {
continue;
}
/**
* Bailout on void return useMemos. This is an anti-pattern where code might be using
* useMemo like useEffect: running arbirtary side-effects synced to changes in specific
* values.
*/
if (
func.env.config.validateNoVoidUseMemo &&
manualMemo.kind === 'useMemo'
) {
const funcToCheck = sidemap.functions.get(
fnPlace.identifier.id,
)?.value;
if (funcToCheck !== undefined && funcToCheck.loweredFunc.func) {
if (!hasNonVoidReturn(funcToCheck.loweredFunc.func)) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
severity: ErrorSeverity.InvalidReact,
category: ErrorCategory.UseMemo,
reason: 'useMemo() callbacks must return a value',
description: `This ${
manualMemo.loadInstr.value.kind === 'PropertyLoad'
? 'React.useMemo'
: 'useMemo'
} callback doesn't return a value. useMemo is for computing and caching values, not for arbitrary side effects.`,
suggestions: null,
}).withDetail({
kind: 'error',
loc: instr.value.loc,
message: 'useMemo() callbacks must return a value',
}),
);
}
}
}
instr.value = getManualMemoizationReplacement(
fnPlace,
instr.value.loc,
@@ -408,11 +502,20 @@ export function dropManualMemoization(func: HIRFunction): void {
* is rare and likely sketchy.
*/
if (!sidemap.functions.has(fnPlace.identifier.id)) {
CompilerError.throwInvalidReact({
reason: `Expected the first argument to be an inline function expression`,
suggestions: [],
loc: fnPlace.loc,
});
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
severity: ErrorSeverity.InvalidReact,
reason: `Expected the first argument to be an inline function expression`,
description: `Expected the first argument to be an inline function expression`,
suggestions: [],
}).withDetail({
kind: 'error',
loc: fnPlace.loc,
message: `Expected the first argument to be an inline function expression`,
}),
);
continue;
}
const memoDecl: Place =
manualMemo.kind === 'useMemo'
@@ -484,6 +587,8 @@ export function dropManualMemoization(func: HIRFunction): void {
markInstructionIds(func.body);
}
}
return errors.asResult();
}
function findOptionalPlaces(fn: HIRFunction): Set<IdentifierId> {
@@ -528,3 +633,17 @@ function findOptionalPlaces(fn: HIRFunction): Set<IdentifierId> {
}
return optionals;
}
function hasNonVoidReturn(func: HIRFunction): boolean {
for (const [, block] of func.body.blocks) {
if (block.terminal.kind === 'return') {
if (
block.terminal.returnVariant === 'Explicit' ||
block.terminal.returnVariant === 'Implicit'
) {
return true;
}
}
}
return false;
}

View File

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

View File

@@ -1,68 +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,
Identifier,
Instruction,
isPrimitiveType,
Place,
} from '../HIR/HIR';
import DisjointSet from '../Utils/DisjointSet';
export type AliasSet = Set<Identifier>;
export function inferAliases(func: HIRFunction): DisjointSet<Identifier> {
const aliases = new DisjointSet<Identifier>();
for (const [_, block] of func.body.blocks) {
for (const instr of block.instructions) {
inferInstr(instr, aliases);
}
}
return aliases;
}
function inferInstr(
instr: Instruction,
aliases: DisjointSet<Identifier>,
): void {
const {lvalue, value: instrValue} = instr;
let alias: Place | null = null;
switch (instrValue.kind) {
case 'LoadLocal':
case 'LoadContext': {
if (isPrimitiveType(instrValue.place.identifier)) {
return;
}
alias = instrValue.place;
break;
}
case 'StoreLocal':
case 'StoreContext': {
alias = instrValue.value;
break;
}
case 'Destructure': {
alias = instrValue.value;
break;
}
case 'ComputedLoad':
case 'PropertyLoad': {
alias = instrValue.object;
break;
}
case 'TypeCastExpression': {
alias = instrValue.value;
break;
}
default:
return;
}
aliases.union([lvalue.identifier, alias.identifier]);
}

View File

@@ -1,27 +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, Identifier} from '../HIR/HIR';
import DisjointSet from '../Utils/DisjointSet';
export function inferAliasForPhis(
func: HIRFunction,
aliases: DisjointSet<Identifier>,
): void {
for (const [_, block] of func.body.blocks) {
for (const phi of block.phis) {
const isPhiMutatedAfterCreation: boolean =
phi.place.identifier.mutableRange.end >
(block.instructions.at(0)?.id ?? block.terminal.id);
if (isPhiMutatedAfterCreation) {
for (const [, operand] of phi.operands) {
aliases.union([phi.place.identifier, operand.identifier]);
}
}
}
}
}

View File

@@ -1,68 +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 {
Effect,
HIRFunction,
Identifier,
InstructionId,
Place,
} from '../HIR/HIR';
import {
eachInstructionLValue,
eachInstructionValueOperand,
} from '../HIR/visitors';
import DisjointSet from '../Utils/DisjointSet';
export function inferAliasForStores(
func: HIRFunction,
aliases: DisjointSet<Identifier>,
): void {
for (const [_, block] of func.body.blocks) {
for (const instr of block.instructions) {
const {value, lvalue} = instr;
const isStore =
lvalue.effect === Effect.Store ||
/*
* Some typed functions annotate callees or arguments
* as Effect.Store.
*/
![...eachInstructionValueOperand(value)].every(
operand => operand.effect !== Effect.Store,
);
if (!isStore) {
continue;
}
for (const operand of eachInstructionLValue(instr)) {
maybeAlias(aliases, lvalue, operand, instr.id);
}
for (const operand of eachInstructionValueOperand(value)) {
if (
operand.effect === Effect.Capture ||
operand.effect === Effect.Store
) {
maybeAlias(aliases, lvalue, operand, instr.id);
}
}
}
}
}
function maybeAlias(
aliases: DisjointSet<Identifier>,
lvalue: Place,
rvalue: Place,
id: InstructionId,
): void {
if (
lvalue.identifier.mutableRange.end > id + 1 ||
rvalue.identifier.mutableRange.end > id
) {
aliases.union([lvalue.identifier, rvalue.identifier]);
}
}

View File

@@ -10,7 +10,6 @@ import {CompilerError, SourceLocation} from '..';
import {
ArrayExpression,
Effect,
Environment,
FunctionExpression,
GeneratedSource,
HIRFunction,
@@ -29,6 +28,10 @@ import {
isSetStateType,
isFireFunctionType,
makeScopeId,
HIR,
BasicBlock,
BlockId,
isEffectEventFunctionType,
} from '../HIR';
import {collectHoistablePropertyLoadsInInnerFn} from '../HIR/CollectHoistablePropertyLoads';
import {collectOptionalChainSidemap} from '../HIR/CollectOptionalChainDependencies';
@@ -38,22 +41,30 @@ import {
createTemporaryPlace,
fixScopeAndIdentifierRanges,
markInstructionIds,
markPredecessors,
reversePostorderBlocks,
} from '../HIR/HIRBuilder';
import {
collectTemporariesSidemap,
DependencyCollectionContext,
handleInstruction,
} from '../HIR/PropagateScopeDependenciesHIR';
import {eachInstructionOperand, eachTerminalOperand} from '../HIR/visitors';
import {buildDependencyInstructions} from '../HIR/ScopeDependencyUtils';
import {
eachInstructionOperand,
eachTerminalOperand,
terminalFallthrough,
} from '../HIR/visitors';
import {empty} from '../Utils/Stack';
import {getOrInsertWith} from '../Utils/utils';
import {deadCodeElimination} from '../Optimization';
import {BuiltInAutodepsId} from '../HIR/ObjectShape';
/**
* Infers reactive dependencies captured by useEffect lambdas and adds them as
* a second argument to the useEffect call if no dependency array is provided.
*/
export function inferEffectDependencies(fn: HIRFunction): void {
let hasRewrite = false;
const fnExpressions = new Map<
IdentifierId,
TInstruction<FunctionExpression>
@@ -68,7 +79,7 @@ export function inferEffectDependencies(fn: HIRFunction): void {
);
moduleTargets.set(
effectTarget.function.importSpecifierName,
effectTarget.numRequiredArgs,
effectTarget.autodepsIndex,
);
}
const autodepFnLoads = new Map<IdentifierId, number>();
@@ -86,6 +97,7 @@ export function inferEffectDependencies(fn: HIRFunction): void {
* reactive(Identifier i) = Union_{reference of i}(reactive(reference))
*/
const reactiveIds = inferReactiveIdentifiers(fn);
const rewriteBlocks: Array<BasicBlock> = [];
for (const [, block] of fn.body.blocks) {
if (block.terminal.kind === 'scope') {
@@ -101,7 +113,7 @@ export function inferEffectDependencies(fn: HIRFunction): void {
);
}
}
const rewriteInstrs = new Map<InstructionId, Array<Instruction>>();
const rewriteInstrs: Array<SpliceInfo> = [];
for (const instr of block.instructions) {
const {value, lvalue} = instr;
if (value.kind === 'FunctionExpression') {
@@ -125,7 +137,6 @@ export function inferEffectDependencies(fn: HIRFunction): void {
}
} else if (value.kind === 'LoadGlobal') {
loadGlobals.add(lvalue.identifier.id);
/*
* TODO: Handle properties on default exports, like
* import React from 'react';
@@ -159,13 +170,26 @@ export function inferEffectDependencies(fn: HIRFunction): void {
) {
const callee =
value.kind === 'CallExpression' ? value.callee : value.property;
const autodepsArgIndex = value.args.findIndex(
arg =>
arg.kind === 'Identifier' &&
arg.identifier.type.kind === 'Object' &&
arg.identifier.type.shapeId === BuiltInAutodepsId,
);
const autodepsArgExpectedIndex = autodepFnLoads.get(
callee.identifier.id,
);
if (
value.args.length === autodepFnLoads.get(callee.identifier.id) &&
value.args.length > 0 &&
autodepsArgExpectedIndex != null &&
autodepsArgIndex === autodepsArgExpectedIndex &&
autodepFnLoads.has(callee.identifier.id) &&
value.args[0].kind === 'Identifier'
) {
// We have a useEffect call with no deps array, so we need to infer the deps
const effectDeps: Array<Place> = [];
const newInstructions: Array<Instruction> = [];
const deps: ArrayExpression = {
kind: 'ArrayExpression',
elements: effectDeps,
@@ -196,24 +220,29 @@ export function inferEffectDependencies(fn: HIRFunction): void {
*/
const usedDeps = [];
for (const dep of minimalDeps) {
for (const maybeDep of minimalDeps) {
if (
((isUseRefType(dep.identifier) ||
isSetStateType(dep.identifier)) &&
!reactiveIds.has(dep.identifier.id)) ||
isFireFunctionType(dep.identifier)
((isUseRefType(maybeDep.identifier) ||
isSetStateType(maybeDep.identifier)) &&
!reactiveIds.has(maybeDep.identifier.id)) ||
isFireFunctionType(maybeDep.identifier) ||
isEffectEventFunctionType(maybeDep.identifier)
) {
// exclude non-reactive hook results, which will never be in a memo block
continue;
}
const {place, instructions} = writeDependencyToInstructions(
const dep = truncateDepAtCurrent(maybeDep);
const {place, value, exitBlockId} = buildDependencyInstructions(
dep,
reactiveIds.has(dep.identifier.id),
fn.env,
fnExpr.loc,
);
newInstructions.push(...instructions);
rewriteInstrs.push({
kind: 'block',
location: instr.id,
value,
exitBlockId: exitBlockId,
});
effectDeps.push(place);
usedDeps.push(dep);
}
@@ -234,27 +263,40 @@ export function inferEffectDependencies(fn: HIRFunction): void {
});
}
newInstructions.push({
id: makeInstructionId(0),
loc: GeneratedSource,
lvalue: {...depsPlace, effect: Effect.Mutate},
value: deps,
});
// Step 2: push the inferred deps array as an argument of the useEffect
value.args.push({...depsPlace, effect: Effect.Freeze});
rewriteInstrs.set(instr.id, newInstructions);
rewriteInstrs.push({
kind: 'instr',
location: instr.id,
value: {
id: makeInstructionId(0),
loc: GeneratedSource,
lvalue: {...depsPlace, effect: Effect.Mutate},
value: deps,
effects: null,
},
});
value.args[autodepsArgIndex] = {
...depsPlace,
effect: Effect.Freeze,
};
fn.env.inferredEffectLocations.add(callee.loc);
} else if (loadGlobals.has(value.args[0].identifier.id)) {
// Global functions have no reactive dependencies, so we can insert an empty array
newInstructions.push({
id: makeInstructionId(0),
loc: GeneratedSource,
lvalue: {...depsPlace, effect: Effect.Mutate},
value: deps,
rewriteInstrs.push({
kind: 'instr',
location: instr.id,
value: {
id: makeInstructionId(0),
loc: GeneratedSource,
lvalue: {...depsPlace, effect: Effect.Mutate},
value: deps,
effects: null,
},
});
value.args.push({...depsPlace, effect: Effect.Freeze});
rewriteInstrs.set(instr.id, newInstructions);
value.args[autodepsArgIndex] = {
...depsPlace,
effect: Effect.Freeze,
};
fn.env.inferredEffectLocations.add(callee.loc);
}
} else if (
@@ -285,85 +327,166 @@ export function inferEffectDependencies(fn: HIRFunction): void {
}
}
}
if (rewriteInstrs.size > 0) {
hasRewrite = true;
const newInstrs = [];
for (const instr of block.instructions) {
const newInstr = rewriteInstrs.get(instr.id);
if (newInstr != null) {
newInstrs.push(...newInstr, instr);
} else {
newInstrs.push(instr);
}
}
block.instructions = newInstrs;
}
rewriteSplices(block, rewriteInstrs, rewriteBlocks);
}
if (hasRewrite) {
if (rewriteBlocks.length > 0) {
for (const block of rewriteBlocks) {
fn.body.blocks.set(block.id, block);
}
/**
* Fixup the HIR to restore RPO, ensure correct predecessors, and renumber
* instructions.
*/
reversePostorderBlocks(fn.body);
markPredecessors(fn.body);
// Renumber instructions and fix scope ranges
markInstructionIds(fn.body);
fixScopeAndIdentifierRanges(fn.body);
deadCodeElimination(fn);
fn.env.hasInferredEffect = true;
}
}
function writeDependencyToInstructions(
function truncateDepAtCurrent(
dep: ReactiveScopeDependency,
reactive: boolean,
env: Environment,
loc: SourceLocation,
): {place: Place; instructions: Array<Instruction>} {
const instructions: Array<Instruction> = [];
let currValue = createTemporaryPlace(env, GeneratedSource);
currValue.reactive = reactive;
instructions.push({
id: makeInstructionId(0),
loc: GeneratedSource,
lvalue: {...currValue, effect: Effect.Mutate},
value: {
kind: 'LoadLocal',
place: {
kind: 'Identifier',
identifier: dep.identifier,
effect: Effect.Capture,
reactive,
loc: loc,
},
loc: loc,
},
});
for (const path of dep.path) {
if (path.optional) {
/**
* TODO: instead of truncating optional paths, reuse
* instructions from hoisted dependencies block(s)
*/
break;
}
if (path.property === 'current') {
/*
* Prune ref.current accesses. This may over-capture for non-ref values with
* a current property, but that's fine.
*/
break;
}
const nextValue = createTemporaryPlace(env, GeneratedSource);
nextValue.reactive = reactive;
instructions.push({
id: makeInstructionId(0),
loc: GeneratedSource,
lvalue: {...nextValue, effect: Effect.Mutate},
value: {
kind: 'PropertyLoad',
object: {...currValue, effect: Effect.Capture},
property: path.property,
loc: loc,
},
});
currValue = nextValue;
): ReactiveScopeDependency {
const idx = dep.path.findIndex(path => path.property === 'current');
if (idx === -1) {
return dep;
} else {
return {...dep, path: dep.path.slice(0, idx)};
}
currValue.effect = Effect.Freeze;
return {place: currValue, instructions};
}
type SpliceInfo =
| {kind: 'instr'; location: InstructionId; value: Instruction}
| {
kind: 'block';
location: InstructionId;
value: HIR;
exitBlockId: BlockId;
};
function rewriteSplices(
originalBlock: BasicBlock,
splices: Array<SpliceInfo>,
rewriteBlocks: Array<BasicBlock>,
): void {
if (splices.length === 0) {
return;
}
/**
* Splice instructions or value blocks into the original block.
* --- original block ---
* bb_original
* instr1
* ...
* instr2 <-- splice location
* instr3
* ...
* <original terminal>
*
* If there is more than one block in the splice, this means that we're
* splicing in a set of value-blocks of the following structure:
* --- blocks we're splicing in ---
* bb_entry:
* instrEntry
* ...
* <splice terminal> fallthrough=bb_exit
*
* bb1(value):
* ...
*
* bb_exit:
* instrExit
* ...
* <synthetic terminal>
*
*
* --- rewritten blocks ---
* bb_original
* instr1
* ... (original instructions)
* instr2
* instrEntry
* ... (spliced instructions)
* <splice terminal> fallthrough=bb_exit
*
* bb1(value):
* ...
*
* bb_exit:
* instrExit
* ... (spliced instructions)
* instr3
* ... (original instructions)
* <original terminal>
*/
const originalInstrs = originalBlock.instructions;
let currBlock: BasicBlock = {...originalBlock, instructions: []};
rewriteBlocks.push(currBlock);
let cursor = 0;
for (const rewrite of splices) {
while (originalInstrs[cursor].id < rewrite.location) {
CompilerError.invariant(
originalInstrs[cursor].id < originalInstrs[cursor + 1].id,
{
reason:
'[InferEffectDependencies] Internal invariant broken: expected block instructions to be sorted',
loc: originalInstrs[cursor].loc,
},
);
currBlock.instructions.push(originalInstrs[cursor]);
cursor++;
}
CompilerError.invariant(originalInstrs[cursor].id === rewrite.location, {
reason:
'[InferEffectDependencies] Internal invariant broken: splice location not found',
loc: originalInstrs[cursor].loc,
});
if (rewrite.kind === 'instr') {
currBlock.instructions.push(rewrite.value);
} else if (rewrite.kind === 'block') {
const {entry, blocks} = rewrite.value;
const entryBlock = blocks.get(entry)!;
// splice in all instructions from the entry block
currBlock.instructions.push(...entryBlock.instructions);
if (blocks.size > 1) {
/**
* We're splicing in a set of value-blocks, which means we need
* to push new blocks and update terminals.
*/
CompilerError.invariant(
terminalFallthrough(entryBlock.terminal) === rewrite.exitBlockId,
{
reason:
'[InferEffectDependencies] Internal invariant broken: expected entry block to have a fallthrough',
loc: entryBlock.terminal.loc,
},
);
const originalTerminal = currBlock.terminal;
currBlock.terminal = entryBlock.terminal;
for (const [id, block] of blocks) {
if (id === entry) {
continue;
}
if (id === rewrite.exitBlockId) {
block.terminal = originalTerminal;
currBlock = block;
}
rewriteBlocks.push(block);
}
}
}
}
currBlock.instructions.push(...originalInstrs.slice(cursor));
}
function inferReactiveIdentifiers(fn: HIRFunction): Set<IdentifierId> {

View File

@@ -1,345 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {
CompilerError,
CompilerErrorDetailOptions,
ErrorSeverity,
ValueKind,
} from '..';
import {
AbstractValue,
BasicBlock,
Effect,
Environment,
FunctionEffect,
Instruction,
InstructionValue,
Place,
ValueReason,
getHookKind,
isRefOrRefValue,
} from '../HIR';
import {eachInstructionOperand, eachTerminalOperand} from '../HIR/visitors';
import {assertExhaustive} from '../Utils/utils';
interface State {
kind(place: Place): AbstractValue;
values(place: Place): Array<InstructionValue>;
isDefined(place: Place): boolean;
}
function inferOperandEffect(state: State, place: Place): null | FunctionEffect {
const value = state.kind(place);
CompilerError.invariant(value != null, {
reason: 'Expected operand to have a kind',
loc: null,
});
switch (place.effect) {
case Effect.Store:
case Effect.Mutate: {
if (isRefOrRefValue(place.identifier)) {
break;
} else if (value.kind === ValueKind.Context) {
CompilerError.invariant(value.context.size > 0, {
reason:
"[InferFunctionEffects] Expected Context-kind value's capture list to be non-empty.",
loc: place.loc,
});
return {
kind: 'ContextMutation',
loc: place.loc,
effect: place.effect,
places: value.context,
};
} else if (
value.kind !== ValueKind.Mutable &&
// We ignore mutations of primitives since this is not a React-specific problem
value.kind !== ValueKind.Primitive
) {
let reason = getWriteErrorReason(value);
return {
kind:
value.reason.size === 1 && value.reason.has(ValueReason.Global)
? 'GlobalMutation'
: 'ReactMutation',
error: {
reason,
description:
place.identifier.name !== null &&
place.identifier.name.kind === 'named'
? `Found mutation of \`${place.identifier.name.value}\``
: null,
loc: place.loc,
suggestions: null,
severity: ErrorSeverity.InvalidReact,
},
};
}
break;
}
}
return null;
}
function inheritFunctionEffects(
state: State,
place: Place,
): Array<FunctionEffect> {
const effects = inferFunctionInstrEffects(state, place);
return effects
.flatMap(effect => {
if (effect.kind === 'GlobalMutation' || effect.kind === 'ReactMutation') {
return [effect];
} else {
const effects: Array<FunctionEffect | null> = [];
CompilerError.invariant(effect.kind === 'ContextMutation', {
reason: 'Expected ContextMutation',
loc: null,
});
/**
* Contextual effects need to be replayed against the current inference
* state, which may know more about the value to which the effect applied.
* The main cases are:
* 1. The mutated context value is _still_ a context value in the current scope,
* so we have to continue propagating the original context mutation.
* 2. The mutated context value is a mutable value in the current scope,
* so the context mutation was fine and we can skip propagating the effect.
* 3. The mutated context value is an immutable value in the current scope,
* resulting in a non-ContextMutation FunctionEffect. We propagate that new,
* more detailed effect to the current function context.
*/
for (const place of effect.places) {
if (state.isDefined(place)) {
const replayedEffect = inferOperandEffect(state, {
...place,
loc: effect.loc,
effect: effect.effect,
});
if (replayedEffect != null) {
if (replayedEffect.kind === 'ContextMutation') {
// Case 1, still a context value so propagate the original effect
effects.push(effect);
} else {
// Case 3, immutable value so propagate the more precise effect
effects.push(replayedEffect);
}
} // else case 2, local mutable value so this effect was fine
}
}
return effects;
}
})
.filter((effect): effect is FunctionEffect => effect != null);
}
function inferFunctionInstrEffects(
state: State,
place: Place,
): Array<FunctionEffect> {
const effects: Array<FunctionEffect> = [];
const instrs = state.values(place);
CompilerError.invariant(instrs != null, {
reason: 'Expected operand to have instructions',
loc: null,
});
for (const instr of instrs) {
if (
(instr.kind === 'FunctionExpression' || instr.kind === 'ObjectMethod') &&
instr.loweredFunc.func.effects != null
) {
effects.push(...instr.loweredFunc.func.effects);
}
}
return effects;
}
function operandEffects(
state: State,
place: Place,
filterRenderSafe: boolean,
): Array<FunctionEffect> {
const functionEffects: Array<FunctionEffect> = [];
const effect = inferOperandEffect(state, place);
effect && functionEffects.push(effect);
functionEffects.push(...inheritFunctionEffects(state, place));
if (filterRenderSafe) {
return functionEffects.filter(effect => !isEffectSafeOutsideRender(effect));
} else {
return functionEffects;
}
}
export function inferInstructionFunctionEffects(
env: Environment,
state: State,
instr: Instruction,
): Array<FunctionEffect> {
const functionEffects: Array<FunctionEffect> = [];
switch (instr.value.kind) {
case 'JsxExpression': {
if (instr.value.tag.kind === 'Identifier') {
functionEffects.push(...operandEffects(state, instr.value.tag, false));
}
instr.value.children?.forEach(child =>
functionEffects.push(...operandEffects(state, child, false)),
);
for (const attr of instr.value.props) {
if (attr.kind === 'JsxSpreadAttribute') {
functionEffects.push(...operandEffects(state, attr.argument, false));
} else {
functionEffects.push(...operandEffects(state, attr.place, true));
}
}
break;
}
case 'ObjectMethod':
case 'FunctionExpression': {
/**
* If this function references other functions, propagate the referenced function's
* effects to this function.
*
* ```
* let f = () => global = true;
* let g = () => f();
* g();
* ```
*
* In this example, because `g` references `f`, we propagate the GlobalMutation from
* `f` to `g`. Thus, referencing `g` in `g()` will evaluate the GlobalMutation in the outer
* function effect context and report an error. But if instead we do:
*
* ```
* let f = () => global = true;
* let g = () => f();
* useEffect(() => g(), [g])
* ```
*
* Now `g`'s effects will be discarded since they're in a useEffect.
*/
for (const operand of eachInstructionOperand(instr)) {
instr.value.loweredFunc.func.effects ??= [];
instr.value.loweredFunc.func.effects.push(
...inferFunctionInstrEffects(state, operand),
);
}
break;
}
case 'MethodCall':
case 'CallExpression': {
let callee;
if (instr.value.kind === 'MethodCall') {
callee = instr.value.property;
functionEffects.push(
...operandEffects(state, instr.value.receiver, false),
);
} else {
callee = instr.value.callee;
}
functionEffects.push(...operandEffects(state, callee, false));
let isHook = getHookKind(env, callee.identifier) != null;
for (const arg of instr.value.args) {
const place = arg.kind === 'Identifier' ? arg : arg.place;
/*
* Join the effects of the argument with the effects of the enclosing function,
* unless the we're detecting a global mutation inside a useEffect hook
*/
functionEffects.push(...operandEffects(state, place, isHook));
}
break;
}
case 'StartMemoize':
case 'FinishMemoize':
case 'LoadLocal':
case 'StoreLocal': {
break;
}
case 'StoreGlobal': {
functionEffects.push({
kind: 'GlobalMutation',
error: {
reason:
'Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)',
loc: instr.loc,
suggestions: null,
severity: ErrorSeverity.InvalidReact,
},
});
break;
}
default: {
for (const operand of eachInstructionOperand(instr)) {
functionEffects.push(...operandEffects(state, operand, false));
}
}
}
return functionEffects;
}
export function inferTerminalFunctionEffects(
state: State,
block: BasicBlock,
): Array<FunctionEffect> {
const functionEffects: Array<FunctionEffect> = [];
for (const operand of eachTerminalOperand(block.terminal)) {
functionEffects.push(...operandEffects(state, operand, true));
}
return functionEffects;
}
export function transformFunctionEffectErrors(
functionEffects: Array<FunctionEffect>,
): Array<CompilerErrorDetailOptions> {
return functionEffects.map(eff => {
switch (eff.kind) {
case 'ReactMutation':
case 'GlobalMutation': {
return eff.error;
}
case 'ContextMutation': {
return {
severity: ErrorSeverity.Invariant,
reason: `Unexpected ContextMutation in top-level function effects`,
loc: eff.loc,
};
}
default:
assertExhaustive(
eff,
`Unexpected function effect kind \`${(eff as any).kind}\``,
);
}
});
}
function isEffectSafeOutsideRender(effect: FunctionEffect): boolean {
return effect.kind === 'GlobalMutation';
}
function getWriteErrorReason(abstractValue: AbstractValue): string {
if (abstractValue.reason.has(ValueReason.Global)) {
return 'Writing to a variable defined outside a component or hook is not allowed. Consider using an effect';
} else if (abstractValue.reason.has(ValueReason.JsxCaptured)) {
return 'Updating a value used previously in JSX is not allowed. Consider moving the mutation before the JSX';
} else if (abstractValue.reason.has(ValueReason.Context)) {
return `Mutating a value returned from 'useContext()', which should not be mutated`;
} else if (abstractValue.reason.has(ValueReason.KnownReturnSignature)) {
return 'Mutating a value returned from a function whose return value should not be mutated';
} else if (abstractValue.reason.has(ValueReason.ReactiveFunctionArgument)) {
return 'Mutating component props or hook arguments is not allowed. Consider using a local variable instead';
} else if (abstractValue.reason.has(ValueReason.State)) {
return "Mutating a value returned from 'useState()', which should not be mutated. Use the setter function to update instead";
} else if (abstractValue.reason.has(ValueReason.ReducerState)) {
return "Mutating a value returned from 'useReducer()', which should not be mutated. Use the dispatch function to update instead";
} else {
return 'This mutates a variable that React considers immutable';
}
}

View File

@@ -1,218 +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 {
Effect,
HIRFunction,
Identifier,
InstructionId,
InstructionKind,
isArrayType,
isMapType,
isRefOrRefValue,
isSetType,
makeInstructionId,
Place,
} from '../HIR/HIR';
import {printPlace} from '../HIR/PrintHIR';
import {
eachInstructionLValue,
eachInstructionOperand,
eachTerminalOperand,
} from '../HIR/visitors';
import {assertExhaustive} from '../Utils/utils';
/*
* For each usage of a value in the given function, determines if the usage
* may be succeeded by a mutable usage of that same value and if so updates
* the usage to be mutable.
*
* Stated differently, this inference ensures that inferred capabilities of
* each reference are as follows:
* - freeze: the value is frozen at this point
* - readonly: the value is not modified at this point *or any subsequent
* point*
* - mutable: the value is modified at this point *or some subsequent point*.
*
* Note that this refines the capabilities inferered by InferReferenceCapability,
* which looks at individual references and not the lifetime of a value's mutability.
*
* == Algorithm
*
* TODO:
* 1. Forward data-flow analysis to determine aliasing. Unlike InferReferenceCapability
* which only tracks aliasing of top-level variables (`y = x`), this analysis needs
* to know if a value is aliased anywhere (`y.x = x`). The forward data flow tracks
* all possible locations which may have aliased a value. The concrete result is
* a mapping of each Place to the set of possibly-mutable values it may alias.
*
* ```
* const x = []; // {x: v0; v0: mutable []}
* const y = {}; // {x: v0, y: v1; v0: mutable [], v1: mutable []}
* y.x = x; // {x: v0, y: v1; v0: mutable [v1], v1: mutable [v0]}
* read(x); // {x: v0, y: v1; v0: mutable [v1], v1: mutable [v0]}
* mutate(y); // can infer that y mutates v0 and v1
* ```
*
* DONE:
* 2. Forward data-flow analysis to compute mutability liveness. Walk forwards over
* the CFG and track which values are mutated in a successor.
*
* ```
* mutate(y); // mutable y => v0, v1 mutated
* read(x); // x maps to v0, v1, those are in the mutated-later set, so x is mutable here
* ...
* ```
*/
function infer(place: Place, instrId: InstructionId): void {
if (!isRefOrRefValue(place.identifier)) {
place.identifier.mutableRange.end = makeInstructionId(instrId + 1);
}
}
function inferPlace(
place: Place,
instrId: InstructionId,
inferMutableRangeForStores: boolean,
): void {
switch (place.effect) {
case Effect.Unknown: {
throw new Error(`Found an unknown place ${printPlace(place)}}!`);
}
case Effect.Capture:
case Effect.Read:
case Effect.Freeze:
return;
case Effect.Store:
if (inferMutableRangeForStores) {
infer(place, instrId);
}
return;
case Effect.ConditionallyMutateIterator: {
const identifier = place.identifier;
if (
!isArrayType(identifier) &&
!isSetType(identifier) &&
!isMapType(identifier)
) {
infer(place, instrId);
}
return;
}
case Effect.ConditionallyMutate:
case Effect.Mutate: {
infer(place, instrId);
return;
}
default:
assertExhaustive(place.effect, `Unexpected ${printPlace(place)} effect`);
}
}
export function inferMutableLifetimes(
func: HIRFunction,
inferMutableRangeForStores: boolean,
): void {
/*
* Context variables only appear to mutate where they are assigned, but we need
* to force their range to start at their declaration. Track the declaring instruction
* id so that the ranges can be extended if/when they are reassigned
*/
const contextVariableDeclarationInstructions = new Map<
Identifier,
InstructionId
>();
for (const [_, block] of func.body.blocks) {
for (const phi of block.phis) {
const isPhiMutatedAfterCreation: boolean =
phi.place.identifier.mutableRange.end >
(block.instructions.at(0)?.id ?? block.terminal.id);
if (
inferMutableRangeForStores &&
isPhiMutatedAfterCreation &&
phi.place.identifier.mutableRange.start === 0
) {
for (const [, operand] of phi.operands) {
if (phi.place.identifier.mutableRange.start === 0) {
phi.place.identifier.mutableRange.start =
operand.identifier.mutableRange.start;
} else {
phi.place.identifier.mutableRange.start = makeInstructionId(
Math.min(
phi.place.identifier.mutableRange.start,
operand.identifier.mutableRange.start,
),
);
}
}
}
}
for (const instr of block.instructions) {
for (const operand of eachInstructionLValue(instr)) {
const lvalueId = operand.identifier;
/*
* lvalue start being mutable when they're initially assigned a
* value.
*/
lvalueId.mutableRange.start = instr.id;
/*
* Let's be optimistic and assume this lvalue is not mutable by
* default.
*/
lvalueId.mutableRange.end = makeInstructionId(instr.id + 1);
}
for (const operand of eachInstructionOperand(instr)) {
inferPlace(operand, instr.id, inferMutableRangeForStores);
}
if (
instr.value.kind === 'DeclareContext' ||
(instr.value.kind === 'StoreContext' &&
instr.value.lvalue.kind !== InstructionKind.Reassign &&
!contextVariableDeclarationInstructions.has(
instr.value.lvalue.place.identifier,
))
) {
/**
* Save declarations of context variables if they hasn't already been
* declared (due to hoisted declarations).
*/
contextVariableDeclarationInstructions.set(
instr.value.lvalue.place.identifier,
instr.id,
);
} else if (instr.value.kind === 'StoreContext') {
/*
* Else this is a reassignment, extend the range from the declaration (if present).
* Note that declarations may not be present for context variables that are reassigned
* within a function expression before (or without) a read of the same variable
*/
const declaration = contextVariableDeclarationInstructions.get(
instr.value.lvalue.place.identifier,
);
if (
declaration != null &&
!isRefOrRefValue(instr.value.lvalue.place.identifier)
) {
const range = instr.value.lvalue.place.identifier.mutableRange;
if (range.start === 0) {
range.start = declaration;
} else {
range.start = makeInstructionId(Math.min(range.start, declaration));
}
}
}
}
for (const operand of eachTerminalOperand(block.terminal)) {
inferPlace(operand, block.terminal.id, inferMutableRangeForStores);
}
}
}

View File

@@ -1,102 +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, Identifier} from '../HIR/HIR';
import {inferAliasForUncalledFunctions} from './InerAliasForUncalledFunctions';
import {inferAliases} from './InferAlias';
import {inferAliasForPhis} from './InferAliasForPhis';
import {inferAliasForStores} from './InferAliasForStores';
import {inferMutableLifetimes} from './InferMutableLifetimes';
import {inferMutableRangesForAlias} from './InferMutableRangesForAlias';
import {inferTryCatchAliases} from './InferTryCatchAliases';
export function inferMutableRanges(ir: HIRFunction): void {
// Infer mutable ranges for non fields
inferMutableLifetimes(ir, false);
// Calculate aliases
const aliases = inferAliases(ir);
/*
* Calculate aliases for try/catch, where any value created
* in the try block could be aliased to the catch param
*/
inferTryCatchAliases(ir, aliases);
/*
* Eagerly canonicalize so that if nothing changes we can bail out
* after a single iteration
*/
let prevAliases: Map<Identifier, Identifier> = aliases.canonicalize();
while (true) {
// Infer mutable ranges for aliases that are not fields
inferMutableRangesForAlias(ir, aliases);
// Update aliasing information of fields
inferAliasForStores(ir, aliases);
// Update aliasing information of phis
inferAliasForPhis(ir, aliases);
const nextAliases = aliases.canonicalize();
if (areEqualMaps(prevAliases, nextAliases)) {
break;
}
prevAliases = nextAliases;
}
// Re-infer mutable ranges for all values
inferMutableLifetimes(ir, true);
/**
* The second inferMutableLifetimes() call updates mutable ranges
* of values to account for Store effects. Now we need to update
* all aliases of such values to extend their ranges as well. Note
* that the store only mutates the the directly aliased value and
* not any of its inner captured references. For example:
*
* ```
* let y;
* if (cond) {
* y = [];
* } else {
* y = [{}];
* }
* y.push(z);
* ```
*
* The Store effect from the `y.push` modifies the values that `y`
* directly aliases - the two arrays from the if/else branches -
* but does not modify values that `y` "contains" such as the
* object literal or `z`.
*/
prevAliases = aliases.canonicalize();
while (true) {
inferMutableRangesForAlias(ir, aliases);
inferAliasForPhis(ir, aliases);
inferAliasForUncalledFunctions(ir, aliases);
const nextAliases = aliases.canonicalize();
if (areEqualMaps(prevAliases, nextAliases)) {
break;
}
prevAliases = nextAliases;
}
}
function areEqualMaps<T>(a: Map<T, T>, b: Map<T, T>): boolean {
if (a.size !== b.size) {
return false;
}
for (const [key, value] of a) {
if (!b.has(key)) {
return false;
}
if (b.get(key) !== value) {
return false;
}
}
return true;
}

View File

@@ -1,54 +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,
Identifier,
InstructionId,
isRefOrRefValue,
} from '../HIR/HIR';
import DisjointSet from '../Utils/DisjointSet';
export function inferMutableRangesForAlias(
_fn: HIRFunction,
aliases: DisjointSet<Identifier>,
): void {
const aliasSets = aliases.buildSets();
for (const aliasSet of aliasSets) {
/*
* Update mutableRange.end only if the identifiers have actually been
* mutated.
*/
const mutatingIdentifiers = [...aliasSet].filter(
id =>
id.mutableRange.end - id.mutableRange.start > 1 && !isRefOrRefValue(id),
);
if (mutatingIdentifiers.length > 0) {
// Find final instruction which mutates this alias set.
let lastMutatingInstructionId = 0;
for (const id of mutatingIdentifiers) {
if (id.mutableRange.end > lastMutatingInstructionId) {
lastMutatingInstructionId = id.mutableRange.end;
}
}
/*
* Update mutableRange.end for all aliases in this set ending before the
* last mutation.
*/
for (const alias of aliasSet) {
if (
alias.mutableRange.end < lastMutatingInstructionId &&
!isRefOrRefValue(alias)
) {
alias.mutableRange.end = lastMutatingInstructionId as InstructionId;
}
}
}
}
}

View File

@@ -0,0 +1,818 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError, SourceLocation} from '..';
import {
BlockId,
Effect,
HIRFunction,
Identifier,
IdentifierId,
InstructionId,
isJsxType,
makeInstructionId,
ValueKind,
ValueReason,
Place,
isPrimitiveType,
} from '../HIR/HIR';
import {
eachInstructionLValue,
eachInstructionValueOperand,
eachTerminalOperand,
} from '../HIR/visitors';
import {assertExhaustive, getOrInsertWith} from '../Utils/utils';
import {Err, Ok, Result} from '../Utils/Result';
import {AliasingEffect} from './AliasingEffects';
/**
* 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.
*
* ## 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<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.
* Edges and mutations are ordered, with mutations processed against the
* abstract model only after it is fully constructed by visiting all blocks
* _and_ connecting phis. Phis are considered ordered at the time of the
* phi node.
*
* This should (may?) mean that mutations are able to see the full state
* of the graph and mark all the appropriate identifiers as mutated at
* the correct point, accounting for both backward and forward edges.
* Ie a mutation of x accounts for both values that flowed into x,
* and values that x flowed into.
*/
const state = new AliasingState();
type PendingPhiOperand = {from: Place; into: Place; index: number};
const pendingPhis = new Map<BlockId, Array<PendingPhiOperand>>();
const mutations: Array<{
index: number;
id: InstructionId;
transitive: boolean;
kind: MutationKind;
place: Place;
}> = [];
const renders: Array<{index: number; place: Place}> = [];
let index = 0;
const errors = new CompilerError();
for (const param of [...fn.params, ...fn.context, fn.returns]) {
const place = param.kind === 'Identifier' ? param : param.place;
state.create(place, {kind: 'Object'});
}
const seenBlocks = new Set<BlockId>();
for (const block of fn.body.blocks.values()) {
for (const phi of block.phis) {
state.create(phi.place, {kind: 'Phi'});
for (const [pred, operand] of phi.operands) {
if (!seenBlocks.has(pred)) {
// NOTE: annotation required to actually typecheck and not silently infer `any`
const blockPhis = getOrInsertWith<BlockId, Array<PendingPhiOperand>>(
pendingPhis,
pred,
() => [],
);
blockPhis.push({from: operand, into: phi.place, index: index++});
} else {
state.assign(index++, operand, phi.place);
}
}
}
seenBlocks.add(block.id);
for (const instr of block.instructions) {
if (instr.effects == null) continue;
for (const effect of instr.effects) {
if (effect.kind === 'Create') {
state.create(effect.into, {kind: 'Object'});
} else if (effect.kind === 'CreateFunction') {
state.create(effect.into, {
kind: 'Function',
function: effect.function.loweredFunc.func,
});
} 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'});
}
state.assign(index++, effect.from, effect.into);
} else if (effect.kind === 'Alias') {
state.assign(index++, effect.from, effect.into);
} else if (effect.kind === 'MaybeAlias') {
state.maybeAlias(index++, effect.from, effect.into);
} else if (effect.kind === 'Capture') {
state.capture(index++, effect.from, effect.into);
} else if (
effect.kind === 'MutateTransitive' ||
effect.kind === 'MutateTransitiveConditionally'
) {
mutations.push({
index: index++,
id: instr.id,
transitive: true,
kind:
effect.kind === 'MutateTransitive'
? MutationKind.Definite
: MutationKind.Conditional,
place: effect.value,
});
} else if (
effect.kind === 'Mutate' ||
effect.kind === 'MutateConditionally'
) {
mutations.push({
index: index++,
id: instr.id,
transitive: false,
kind:
effect.kind === 'Mutate'
? MutationKind.Definite
: MutationKind.Conditional,
place: effect.value,
});
} else if (
effect.kind === 'MutateFrozen' ||
effect.kind === 'MutateGlobal' ||
effect.kind === 'Impure'
) {
errors.pushDiagnostic(effect.error);
functionEffects.push(effect);
} else if (effect.kind === 'Render') {
renders.push({index: index++, place: effect.place});
functionEffects.push(effect);
}
}
}
const blockPhis = pendingPhis.get(block.id);
if (blockPhis != null) {
for (const {from, into, index} of blockPhis) {
state.assign(index, from, into);
}
}
if (block.terminal.kind === 'return') {
state.assign(index++, block.terminal.value, fn.returns);
}
if (
(block.terminal.kind === 'maybe-throw' ||
block.terminal.kind === 'return') &&
block.terminal.effects != null
) {
for (const effect of block.terminal.effects) {
if (effect.kind === 'Alias') {
state.assign(index++, effect.from, effect.into);
} else {
CompilerError.invariant(effect.kind === 'Freeze', {
reason: `Unexpected '${effect.kind}' effect for MaybeThrow terminal`,
loc: block.terminal.loc,
});
}
}
}
}
for (const mutation of mutations) {
state.mutate(
mutation.index,
mutation.place.identifier,
makeInstructionId(mutation.id + 1),
mutation.transitive,
mutation.kind,
mutation.place.loc,
errors,
);
}
for (const render of renders) {
state.render(render.index, render.place.identifier, errors);
}
for (const param of [...fn.context, ...fn.params]) {
const place = param.kind === 'Identifier' ? param : param.place;
const node = state.nodes.get(place.identifier);
if (node == null) {
continue;
}
let mutated = false;
if (node.local != null) {
if (node.local.kind === MutationKind.Conditional) {
mutated = true;
functionEffects.push({
kind: 'MutateConditionally',
value: {...place, loc: node.local.loc},
});
} else if (node.local.kind === MutationKind.Definite) {
mutated = true;
functionEffects.push({
kind: 'Mutate',
value: {...place, loc: node.local.loc},
});
}
}
if (node.transitive != null) {
if (node.transitive.kind === MutationKind.Conditional) {
mutated = true;
functionEffects.push({
kind: 'MutateTransitiveConditionally',
value: {...place, loc: node.transitive.loc},
});
} else if (node.transitive.kind === MutationKind.Definite) {
mutated = true;
functionEffects.push({
kind: 'MutateTransitive',
value: {...place, loc: node.transitive.loc},
});
}
}
if (mutated) {
place.effect = Effect.Capture;
}
}
/**
* Part 2
* Add legacy operand-specific effects based on instruction effects and mutable ranges.
* Also fixes up operand mutable ranges, making sure that start is non-zero if the value
* is mutated (depended on by later passes like InferReactiveScopeVariables which uses this
* to filter spurious mutations of globals, which we now guard against more precisely)
*/
for (const block of fn.body.blocks.values()) {
for (const phi of block.phis) {
// TODO: we don't actually set these effects today!
phi.place.effect = Effect.Store;
const isPhiMutatedAfterCreation: boolean =
phi.place.identifier.mutableRange.end >
(block.instructions.at(0)?.id ?? block.terminal.id);
for (const operand of phi.operands.values()) {
operand.effect = isPhiMutatedAfterCreation
? Effect.Capture
: Effect.Read;
}
if (
isPhiMutatedAfterCreation &&
phi.place.identifier.mutableRange.start === 0
) {
/*
* TODO: ideally we'd construct a precise start range, but what really
* matters is that the phi's range appears mutable (end > start + 1)
* so we just set the start to the previous instruction before this block
*/
const firstInstructionIdOfBlock =
block.instructions.at(0)?.id ?? block.terminal.id;
phi.place.identifier.mutableRange.start = makeInstructionId(
firstInstructionIdOfBlock - 1,
);
}
}
for (const instr of block.instructions) {
for (const lvalue of eachInstructionLValue(instr)) {
lvalue.effect = Effect.ConditionallyMutate;
if (lvalue.identifier.mutableRange.start === 0) {
lvalue.identifier.mutableRange.start = instr.id;
}
if (lvalue.identifier.mutableRange.end === 0) {
lvalue.identifier.mutableRange.end = makeInstructionId(
Math.max(instr.id + 1, lvalue.identifier.mutableRange.end),
);
}
}
for (const operand of eachInstructionValueOperand(instr.value)) {
operand.effect = Effect.Read;
}
if (instr.effects == null) {
continue;
}
const operandEffects = new Map<IdentifierId, Effect>();
for (const effect of instr.effects) {
switch (effect.kind) {
case 'Assign':
case 'Alias':
case 'Capture':
case 'CreateFrom':
case 'MaybeAlias': {
const isMutatedOrReassigned =
effect.into.identifier.mutableRange.end > instr.id;
if (isMutatedOrReassigned) {
operandEffects.set(effect.from.identifier.id, Effect.Capture);
operandEffects.set(effect.into.identifier.id, Effect.Store);
} else {
operandEffects.set(effect.from.identifier.id, Effect.Read);
operandEffects.set(effect.into.identifier.id, Effect.Store);
}
break;
}
case 'CreateFunction':
case 'Create': {
break;
}
case 'Mutate': {
operandEffects.set(effect.value.identifier.id, Effect.Store);
break;
}
case 'Apply': {
CompilerError.invariant(false, {
reason: `[AnalyzeFunctions] Expected Apply effects to be replaced with more precise effects`,
loc: effect.function.loc,
});
}
case 'MutateTransitive':
case 'MutateConditionally':
case 'MutateTransitiveConditionally': {
operandEffects.set(
effect.value.identifier.id,
Effect.ConditionallyMutate,
);
break;
}
case 'Freeze': {
operandEffects.set(effect.value.identifier.id, Effect.Freeze);
break;
}
case 'ImmutableCapture': {
// no-op, Read is the default
break;
}
case 'Impure':
case 'Render':
case 'MutateFrozen':
case 'MutateGlobal': {
// no-op
break;
}
default: {
assertExhaustive(
effect,
`Unexpected effect kind ${(effect as any).kind}`,
);
}
}
}
for (const lvalue of eachInstructionLValue(instr)) {
const effect =
operandEffects.get(lvalue.identifier.id) ??
Effect.ConditionallyMutate;
lvalue.effect = effect;
}
for (const operand of eachInstructionValueOperand(instr.value)) {
if (
operand.identifier.mutableRange.end > instr.id &&
operand.identifier.mutableRange.start === 0
) {
operand.identifier.mutableRange.start = instr.id;
}
const effect = operandEffects.get(operand.identifier.id) ?? Effect.Read;
operand.effect = effect;
}
/**
* This case is targeted at hoisted functions like:
*
* ```
* x();
* function x() { ... }
* ```
*
* Which turns into:
*
* t0 = DeclareContext HoistedFunction x
* t1 = LoadContext x
* t2 = CallExpression t1 ( )
* t3 = FunctionExpression ...
* t4 = StoreContext Function x = t3
*
* If the function had captured mutable values, it would already have its
* range extended to include the StoreContext. But if the function doesn't
* capture any mutable values its range won't have been extended yet. We
* want to ensure that the value is memoized along with the context variable,
* not independently of it (bc of the way we do codegen for hoisted functions).
* So here we check for StoreContext rvalues and if they haven't already had
* their range extended to at least this instruction, we extend it.
*/
if (
instr.value.kind === 'StoreContext' &&
instr.value.value.identifier.mutableRange.end <= instr.id
) {
instr.value.value.identifier.mutableRange.end = makeInstructionId(
instr.id + 1,
);
}
}
if (block.terminal.kind === 'return') {
block.terminal.value.effect = isFunctionExpression
? Effect.Read
: Effect.Freeze;
} else {
for (const operand of eachTerminalOperand(block.terminal)) {
operand.effect = Effect.Read;
}
}
}
/**
* 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);
}
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 {
for (const effect of fn.aliasingEffects ?? []) {
switch (effect.kind) {
case 'Impure':
case 'MutateFrozen':
case 'MutateGlobal': {
errors.pushDiagnostic(effect.error);
break;
}
}
}
}
export enum MutationKind {
None = 0,
Conditional = 1,
Definite = 2,
}
type Node = {
id: Identifier;
createdFrom: Map<Identifier, number>;
captures: Map<Identifier, number>;
aliases: Map<Identifier, number>;
maybeAliases: Map<Identifier, number>;
edges: Array<{
index: number;
node: Identifier;
kind: 'capture' | 'alias' | 'maybeAlias';
}>;
transitive: {kind: MutationKind; loc: SourceLocation} | null;
local: {kind: MutationKind; loc: SourceLocation} | null;
lastMutated: number;
value:
| {kind: 'Object'}
| {kind: 'Phi'}
| {kind: 'Function'; function: HIRFunction};
};
class AliasingState {
nodes: Map<Identifier, Node> = new Map();
create(place: Place, value: Node['value']): void {
this.nodes.set(place.identifier, {
id: place.identifier,
createdFrom: new Map(),
captures: new Map(),
aliases: new Map(),
maybeAliases: new Map(),
edges: [],
transitive: null,
local: null,
lastMutated: 0,
value,
});
}
createFrom(index: number, from: Place, into: Place): void {
this.create(into, {kind: 'Object'});
const fromNode = this.nodes.get(from.identifier);
const toNode = this.nodes.get(into.identifier);
if (fromNode == null || toNode == null) {
return;
}
fromNode.edges.push({index, node: into.identifier, kind: 'alias'});
if (!toNode.createdFrom.has(from.identifier)) {
toNode.createdFrom.set(from.identifier, index);
}
}
capture(index: number, from: Place, into: Place): void {
const fromNode = this.nodes.get(from.identifier);
const toNode = this.nodes.get(into.identifier);
if (fromNode == null || toNode == null) {
return;
}
fromNode.edges.push({index, node: into.identifier, kind: 'capture'});
if (!toNode.captures.has(from.identifier)) {
toNode.captures.set(from.identifier, index);
}
}
assign(index: number, from: Place, into: Place): void {
const fromNode = this.nodes.get(from.identifier);
const toNode = this.nodes.get(into.identifier);
if (fromNode == null || toNode == null) {
return;
}
fromNode.edges.push({index, node: into.identifier, kind: 'alias'});
if (!toNode.aliases.has(from.identifier)) {
toNode.aliases.set(from.identifier, index);
}
}
maybeAlias(index: number, from: Place, into: Place): void {
const fromNode = this.nodes.get(from.identifier);
const toNode = this.nodes.get(into.identifier);
if (fromNode == null || toNode == null) {
return;
}
fromNode.edges.push({index, node: into.identifier, kind: 'maybeAlias'});
if (!toNode.maybeAliases.has(from.identifier)) {
toNode.maybeAliases.set(from.identifier, index);
}
}
render(index: number, start: Identifier, errors: CompilerError): void {
const seen = new Set<Identifier>();
const queue: Array<Identifier> = [start];
while (queue.length !== 0) {
const current = queue.pop()!;
if (seen.has(current)) {
continue;
}
seen.add(current);
const node = this.nodes.get(current);
if (node == null || node.transitive != null || node.local != null) {
continue;
}
if (node.value.kind === 'Function') {
appendFunctionErrors(errors, node.value.function);
}
for (const [alias, when] of node.createdFrom) {
if (when >= index) {
continue;
}
queue.push(alias);
}
for (const [alias, when] of node.aliases) {
if (when >= index) {
continue;
}
queue.push(alias);
}
for (const [capture, when] of node.captures) {
if (when >= index) {
continue;
}
queue.push(capture);
}
}
}
mutate(
index: number,
start: Identifier,
// Null is used for simulated mutations
end: InstructionId | null,
transitive: boolean,
startKind: MutationKind,
loc: SourceLocation,
errors: CompilerError,
): void {
const seen = new Map<Identifier, MutationKind>();
const queue: Array<{
place: Identifier;
transitive: boolean;
direction: 'backwards' | 'forwards';
kind: MutationKind;
}> = [{place: start, transitive, direction: 'backwards', kind: startKind}];
while (queue.length !== 0) {
const {place: current, transitive, direction, kind} = queue.pop()!;
const previousKind = seen.get(current);
if (previousKind != null && previousKind >= kind) {
continue;
}
seen.set(current, kind);
const node = this.nodes.get(current);
if (node == null) {
continue;
}
node.lastMutated = Math.max(node.lastMutated, index);
if (end != null) {
node.id.mutableRange.end = makeInstructionId(
Math.max(node.id.mutableRange.end, end),
);
}
if (
node.value.kind === 'Function' &&
node.transitive == null &&
node.local == null
) {
appendFunctionErrors(errors, node.value.function);
}
if (transitive) {
if (node.transitive == null || node.transitive.kind < kind) {
node.transitive = {kind, loc};
}
} else {
if (node.local == null || node.local.kind < kind) {
node.local = {kind, loc};
}
}
/**
* all mutations affect "forward" edges by the rules:
* - Capture a -> b, mutate(a) => mutate(b)
* - Alias a -> b, mutate(a) => mutate(b)
*/
for (const edge of node.edges) {
if (edge.index >= index) {
break;
}
queue.push({place: edge.node, transitive, direction: 'forwards', kind});
}
for (const [alias, when] of node.createdFrom) {
if (when >= index) {
continue;
}
queue.push({
place: alias,
transitive: true,
direction: 'backwards',
kind,
});
}
if (direction === 'backwards' || node.value.kind !== 'Phi') {
/**
* all mutations affect backward alias edges by the rules:
* - Alias a -> b, mutate(b) => mutate(a)
* - Alias a -> b, mutateTransitive(b) => mutate(a)
*
* However, if we reached a phi because one of its inputs was mutated
* (and we're advancing "forwards" through that node's edges), then
* we know we've already processed the mutation at its source. The
* phi's other inputs can't be affected.
*/
for (const [alias, when] of node.aliases) {
if (when >= index) {
continue;
}
queue.push({place: alias, transitive, direction: 'backwards', kind});
}
/**
* MaybeAlias indicates potential data flow from unknown function calls,
* so we downgrade mutations through these aliases to consider them
* conditional. This means we'll consider them for mutation *range*
* purposes but not report validation errors for mutations, since
* we aren't sure that the `from` value could actually be aliased.
*/
for (const [alias, when] of node.maybeAliases) {
if (when >= index) {
continue;
}
queue.push({
place: alias,
transitive,
direction: 'backwards',
kind: MutationKind.Conditional,
});
}
}
/**
* but only transitive mutations affect captures
*/
if (transitive) {
for (const [capture, when] of node.captures) {
if (when >= index) {
continue;
}
queue.push({
place: capture,
transitive,
direction: 'backwards',
kind,
});
}
}
}
}
}

View File

@@ -21,11 +21,11 @@ import {
isStableType,
isStableTypeContainer,
isUseOperator,
isUseRefType,
} from '../HIR';
import {PostDominator} from '../HIR/Dominator';
import {
eachInstructionLValue,
eachInstructionOperand,
eachInstructionValueOperand,
eachTerminalOperand,
} from '../HIR/visitors';
@@ -69,13 +69,6 @@ class StableSidemap {
isStable: false,
});
}
} else if (
this.env.config.enableTreatRefLikeIdentifiersAsRefs &&
isUseRefType(lvalue.identifier)
) {
this.map.set(lvalue.identifier.id, {
isStable: true,
});
}
break;
}
@@ -292,7 +285,7 @@ export function inferReactivePlaces(fn: HIRFunction): void {
let hasReactiveInput = false;
/*
* NOTE: we want to mark all operands as reactive or not, so we
* avoid short-circuting here
* avoid short-circuiting here
*/
for (const operand of eachInstructionValueOperand(value)) {
const reactive = reactiveIdentifiers.isReactive(operand);
@@ -375,6 +368,41 @@ export function inferReactivePlaces(fn: HIRFunction): void {
}
}
} while (reactiveIdentifiers.snapshot());
function propagateReactivityToInnerFunctions(
fn: HIRFunction,
isOutermost: boolean,
): void {
for (const [, block] of fn.body.blocks) {
for (const instr of block.instructions) {
if (!isOutermost) {
for (const operand of eachInstructionOperand(instr)) {
reactiveIdentifiers.isReactive(operand);
}
}
if (
instr.value.kind === 'ObjectMethod' ||
instr.value.kind === 'FunctionExpression'
) {
propagateReactivityToInnerFunctions(
instr.value.loweredFunc.func,
false,
);
}
}
if (!isOutermost) {
for (const operand of eachTerminalOperand(block.terminal)) {
reactiveIdentifiers.isReactive(operand);
}
}
}
}
/**
* Propagate reactivity for inner functions, as we eventually hoist and dedupe
* dependency instructions for scopes.
*/
propagateReactivityToInnerFunctions(fn, true);
}
/*

View File

@@ -1,49 +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 {BlockId, HIRFunction, Identifier} from '../HIR';
import DisjointSet from '../Utils/DisjointSet';
/*
* Any values created within a try/catch block could be aliased to the try handler.
* Our lowering ensures that every instruction within a try block will be lowered into a
* basic block ending in a maybe-throw terminal that points to its catch block, so we can
* iterate such blocks and alias their instruction lvalues to the handler's param (if present).
*/
export function inferTryCatchAliases(
fn: HIRFunction,
aliases: DisjointSet<Identifier>,
): void {
const handlerParams: Map<BlockId, Identifier> = new Map();
for (const [_, block] of fn.body.blocks) {
if (
block.terminal.kind === 'try' &&
block.terminal.handlerBinding !== null
) {
handlerParams.set(
block.terminal.handler,
block.terminal.handlerBinding.identifier,
);
} else if (block.terminal.kind === 'maybe-throw') {
const handlerParam = handlerParams.get(block.terminal.handler);
if (handlerParam === undefined) {
/*
* There's no catch clause param, nothing to alias to so
* skip this block
*/
continue;
}
/*
* Otherwise alias all values created in this block to the
* catch clause param
*/
for (const instr of block.instructions) {
aliases.union([handlerParam, instr.lvalue.identifier]);
}
}
}
}

View File

@@ -11,13 +11,16 @@ import {
Environment,
FunctionExpression,
GeneratedSource,
GotoTerminal,
GotoVariant,
HIRFunction,
IdentifierId,
InstructionKind,
LabelTerminal,
Place,
isStatementBlockKind,
makeInstructionId,
mergeConsecutiveBlocks,
promoteTemporary,
reversePostorderBlocks,
} from '../HIR';
@@ -72,6 +75,10 @@ import {retainWhere} from '../Utils/utils';
* - All return statements in the original function expression are replaced with a
* StoreLocal to the temporary we allocated before plus a Goto to the fallthrough
* block (code following the CallExpression).
*
* Note that if the inliined function has only one return, we avoid the labeled block
* and fully inline the code. The original return is replaced with an assignmen to the
* IIFE's call expression lvalue.
*/
export function inlineImmediatelyInvokedFunctionExpressions(
fn: HIRFunction,
@@ -90,100 +97,144 @@ export function inlineImmediatelyInvokedFunctionExpressions(
*/
const queue = Array.from(fn.body.blocks.values());
queue: for (const block of queue) {
for (let ii = 0; ii < block.instructions.length; ii++) {
const instr = block.instructions[ii]!;
switch (instr.value.kind) {
case 'FunctionExpression': {
if (instr.lvalue.identifier.name === null) {
functions.set(instr.lvalue.identifier.id, instr.value);
/*
* We can't handle labels inside expressions yet, so we don't inline IIFEs if they are in an
* expression block.
*/
if (isStatementBlockKind(block.kind)) {
for (let ii = 0; ii < block.instructions.length; ii++) {
const instr = block.instructions[ii]!;
switch (instr.value.kind) {
case 'FunctionExpression': {
if (instr.lvalue.identifier.name === null) {
functions.set(instr.lvalue.identifier.id, instr.value);
}
break;
}
break;
}
case 'CallExpression': {
if (instr.value.args.length !== 0) {
// We don't support inlining when there are arguments
continue;
case 'CallExpression': {
if (instr.value.args.length !== 0) {
// We don't support inlining when there are arguments
continue;
}
const body = functions.get(instr.value.callee.identifier.id);
if (body === undefined) {
// Not invoking a local function expression, can't inline
continue;
}
if (
body.loweredFunc.func.params.length > 0 ||
body.loweredFunc.func.async ||
body.loweredFunc.func.generator
) {
// Can't inline functions with params, or async/generator functions
continue;
}
// We know this function is used for an IIFE and can prune it later
inlinedFunctions.add(instr.value.callee.identifier.id);
// Create a new block which will contain code following the IIFE call
const continuationBlockId = fn.env.nextBlockId;
const continuationBlock: BasicBlock = {
id: continuationBlockId,
instructions: block.instructions.slice(ii + 1),
kind: block.kind,
phis: new Set(),
preds: new Set(),
terminal: block.terminal,
};
fn.body.blocks.set(continuationBlockId, continuationBlock);
/*
* Trim the original block to contain instructions up to (but not including)
* the IIFE
*/
block.instructions.length = ii;
if (hasSingleExitReturnTerminal(body.loweredFunc.func)) {
block.terminal = {
kind: 'goto',
block: body.loweredFunc.func.body.entry,
id: block.terminal.id,
loc: block.terminal.loc,
variant: GotoVariant.Break,
} as GotoTerminal;
for (const block of body.loweredFunc.func.body.blocks.values()) {
if (block.terminal.kind === 'return') {
block.instructions.push({
id: makeInstructionId(0),
loc: block.terminal.loc,
lvalue: instr.lvalue,
value: {
kind: 'LoadLocal',
loc: block.terminal.loc,
place: block.terminal.value,
},
effects: null,
});
block.terminal = {
kind: 'goto',
block: continuationBlockId,
id: block.terminal.id,
loc: block.terminal.loc,
variant: GotoVariant.Break,
} as GotoTerminal;
}
}
for (const [id, block] of body.loweredFunc.func.body.blocks) {
block.preds.clear();
fn.body.blocks.set(id, block);
}
} else {
/*
* To account for multiple returns within the lambda, we treat the lambda
* as if it were a single labeled statement, and replace all returns with gotos
* to the label fallthrough.
*/
const newTerminal: LabelTerminal = {
block: body.loweredFunc.func.body.entry,
id: makeInstructionId(0),
kind: 'label',
fallthrough: continuationBlockId,
loc: block.terminal.loc,
};
block.terminal = newTerminal;
// We store the result in the IIFE temporary
const result = instr.lvalue;
// Declare the IIFE temporary
declareTemporary(fn.env, block, result);
// Promote the temporary with a name as we require this to persist
if (result.identifier.name == null) {
promoteTemporary(result.identifier);
}
/*
* Rewrite blocks from the lambda to replace any `return` with a
* store to the result and `goto` the continuation block
*/
for (const [id, block] of body.loweredFunc.func.body.blocks) {
block.preds.clear();
rewriteBlock(fn.env, block, continuationBlockId, result);
fn.body.blocks.set(id, block);
}
}
/*
* Ensure we visit the continuation block, since there may have been
* sequential IIFEs that need to be visited.
*/
queue.push(continuationBlock);
continue queue;
}
const body = functions.get(instr.value.callee.identifier.id);
if (body === undefined) {
// Not invoking a local function expression, can't inline
continue;
}
if (
body.loweredFunc.func.params.length > 0 ||
body.loweredFunc.func.async ||
body.loweredFunc.func.generator
) {
// Can't inline functions with params, or async/generator functions
continue;
}
// We know this function is used for an IIFE and can prune it later
inlinedFunctions.add(instr.value.callee.identifier.id);
// Create a new block which will contain code following the IIFE call
const continuationBlockId = fn.env.nextBlockId;
const continuationBlock: BasicBlock = {
id: continuationBlockId,
instructions: block.instructions.slice(ii + 1),
kind: block.kind,
phis: new Set(),
preds: new Set(),
terminal: block.terminal,
};
fn.body.blocks.set(continuationBlockId, continuationBlock);
/*
* Trim the original block to contain instructions up to (but not including)
* the IIFE
*/
block.instructions.length = ii;
/*
* To account for complex control flow within the lambda, we treat the lambda
* as if it were a single labeled statement, and replace all returns with gotos
* to the label fallthrough.
*/
const newTerminal: LabelTerminal = {
block: body.loweredFunc.func.body.entry,
id: makeInstructionId(0),
kind: 'label',
fallthrough: continuationBlockId,
loc: block.terminal.loc,
};
block.terminal = newTerminal;
// We store the result in the IIFE temporary
const result = instr.lvalue;
// Declare the IIFE temporary
declareTemporary(fn.env, block, result);
// Promote the temporary with a name as we require this to persist
promoteTemporary(result.identifier);
/*
* Rewrite blocks from the lambda to replace any `return` with a
* store to the result and `goto` the continuation block
*/
for (const [id, block] of body.loweredFunc.func.body.blocks) {
block.preds.clear();
rewriteBlock(fn.env, block, continuationBlockId, result);
fn.body.blocks.set(id, block);
}
/*
* Ensure we visit the continuation block, since there may have been
* sequential IIFEs that need to be visited.
*/
queue.push(continuationBlock);
continue queue;
}
default: {
for (const place of eachInstructionValueOperand(instr.value)) {
// Any other use of a function expression means it isn't an IIFE
functions.delete(place.identifier.id);
default: {
for (const place of eachInstructionValueOperand(instr.value)) {
// Any other use of a function expression means it isn't an IIFE
functions.delete(place.identifier.id);
}
}
}
}
@@ -192,7 +243,7 @@ export function inlineImmediatelyInvokedFunctionExpressions(
if (inlinedFunctions.size !== 0) {
// Remove instructions that define lambdas which we inlined
for (const [, block] of fn.body.blocks) {
for (const block of fn.body.blocks.values()) {
retainWhere(
block.instructions,
instr => !inlinedFunctions.has(instr.lvalue.identifier.id),
@@ -206,9 +257,25 @@ export function inlineImmediatelyInvokedFunctionExpressions(
reversePostorderBlocks(fn.body);
markInstructionIds(fn.body);
markPredecessors(fn.body);
mergeConsecutiveBlocks(fn);
}
}
/**
* Returns true if the function has a single exit terminal (throw/return) which is a return
*/
function hasSingleExitReturnTerminal(fn: HIRFunction): boolean {
let hasReturn = false;
let exitCount = 0;
for (const [, block] of fn.body.blocks) {
if (block.terminal.kind === 'return' || block.terminal.kind === 'throw') {
hasReturn ||= block.terminal.kind === 'return';
exitCount++;
}
}
return exitCount === 1 && hasReturn;
}
/*
* Rewrites the block so that all `return` terminals are replaced:
* * Add a StoreLocal <returnValue> = <terminal.value>
@@ -235,6 +302,7 @@ function rewriteBlock(
type: null,
loc: terminal.loc,
},
effects: null,
});
block.terminal = {
kind: 'goto',
@@ -263,5 +331,6 @@ function declareTemporary(
type: null,
loc: result.loc,
},
effects: null,
});
}

View File

@@ -0,0 +1,559 @@
# 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.
### MaybeAlias
Describes potential data flow that the compiler knows may occur behind a function call, but cannot be sure about. For example, `foo(x)` _may_ be the identity function and return `x`, or `cond(a, b, c)` may conditionally return `b` or `c` depending on the value of `a`, but those functions could just as easily return new mutable values and not capture any information from their arguments. MaybeAlias represents that we have to consider the potential for data flow when deciding mutable ranges, but should be conservative about reporting errors. For example, `foo(someFrozenValue).property = true` should not error since we don't know for certain that foo returns its input.
### 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
```
### MaybeAlias makes mutation conditional
Because we don't know for certain that the aliasing occurs, we consider the mutation conditional against the source.
```
MaybeAlias a <- b
Mutate a
=>
MutateConditional 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

@@ -7,8 +7,6 @@
export {default as analyseFunctions} from './AnalyseFunctions';
export {dropManualMemoization} from './DropManualMemoization';
export {inferMutableRanges} from './InferMutableRanges';
export {inferReactivePlaces} from './InferReactivePlaces';
export {default as inferReferenceEffects} from './InferReferenceEffects';
export {inlineImmediatelyInvokedFunctionExpressions} from './InlineImmediatelyInvokedFunctionExpressions';
export {inferEffectDependencies} from './InferEffectDependencies';

View File

@@ -42,6 +42,7 @@ import {
mapInstructionValueOperands,
mapTerminalOperands,
} from '../HIR/visitors';
import {ErrorCategory} from '../CompilerError';
type InlinedJsxDeclarationMap = Map<
DeclarationId,
@@ -83,6 +84,7 @@ export function inlineJsxTransform(
kind: 'CompileDiagnostic',
fnLoc: null,
detail: {
category: ErrorCategory.Todo,
reason: 'JSX Inlining is not supported on value blocks',
loc: instr.loc,
},
@@ -151,6 +153,7 @@ export function inlineJsxTransform(
type: null,
loc: instr.value.loc,
},
effects: null,
loc: instr.loc,
};
currentBlockInstructions.push(varInstruction);
@@ -167,6 +170,7 @@ export function inlineJsxTransform(
},
loc: instr.value.loc,
},
effects: null,
loc: instr.loc,
};
currentBlockInstructions.push(devGlobalInstruction);
@@ -220,6 +224,7 @@ export function inlineJsxTransform(
type: null,
loc: instr.value.loc,
},
effects: null,
loc: instr.loc,
};
thenBlockInstructions.push(reassignElseInstruction);
@@ -292,6 +297,7 @@ export function inlineJsxTransform(
],
loc: instr.value.loc,
},
effects: null,
loc: instr.loc,
};
elseBlockInstructions.push(reactElementInstruction);
@@ -309,6 +315,7 @@ export function inlineJsxTransform(
type: null,
loc: instr.value.loc,
},
effects: null,
loc: instr.loc,
};
elseBlockInstructions.push(reassignConditionalInstruction);
@@ -436,6 +443,7 @@ function createSymbolProperty(
binding: {kind: 'Global', name: 'Symbol'},
loc: instr.value.loc,
},
effects: null,
loc: instr.loc,
};
nextInstructions.push(symbolInstruction);
@@ -450,6 +458,7 @@ function createSymbolProperty(
property: makePropertyLiteral('for'),
loc: instr.value.loc,
},
effects: null,
loc: instr.loc,
};
nextInstructions.push(symbolForInstruction);
@@ -463,6 +472,7 @@ function createSymbolProperty(
value: symbolName,
loc: instr.value.loc,
},
effects: null,
loc: instr.loc,
};
nextInstructions.push(symbolValueInstruction);
@@ -478,6 +488,7 @@ function createSymbolProperty(
args: [symbolValueInstruction.lvalue],
loc: instr.value.loc,
},
effects: null,
loc: instr.loc,
};
const $$typeofProperty: ObjectProperty = {
@@ -508,6 +519,7 @@ function createTagProperty(
value: componentTag.name,
loc: instr.value.loc,
},
effects: null,
loc: instr.loc,
};
tagProperty = {
@@ -634,6 +646,7 @@ function createPropsProperties(
elements: [...children],
loc: instr.value.loc,
},
effects: null,
loc: instr.loc,
};
nextInstructions.push(childrenPropInstruction);
@@ -657,6 +670,7 @@ function createPropsProperties(
value: null,
loc: instr.value.loc,
},
effects: null,
loc: instr.loc,
};
refProperty = {
@@ -678,6 +692,7 @@ function createPropsProperties(
value: null,
loc: instr.value.loc,
},
effects: null,
loc: instr.loc,
};
keyProperty = {
@@ -711,6 +726,7 @@ function createPropsProperties(
properties: props,
loc: instr.value.loc,
},
effects: null,
loc: instr.loc,
};
propsProperty = {

View File

@@ -25,7 +25,6 @@ import {
makeBlockId,
makeInstructionId,
makePropertyLiteral,
makeType,
markInstructionIds,
promoteTemporary,
reversePostorderBlocks,
@@ -146,6 +145,7 @@ function emitLoadLoweredContextCallee(
id: makeInstructionId(0),
loc: GeneratedSource,
lvalue: createTemporaryPlace(env, GeneratedSource),
effects: null,
value: loadGlobal,
};
}
@@ -192,6 +192,7 @@ function emitPropertyLoad(
lvalue: object,
value: loadObj,
id: makeInstructionId(0),
effects: null,
loc: GeneratedSource,
};
@@ -206,6 +207,7 @@ function emitPropertyLoad(
lvalue: element,
value: loadProp,
id: makeInstructionId(0),
effects: null,
loc: GeneratedSource,
};
return {
@@ -235,8 +237,10 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
terminal: {
id: makeInstructionId(0),
kind: 'return',
returnVariant: 'Explicit',
loc: GeneratedSource,
value: arrayInstr.lvalue,
effects: null,
},
preds: new Set(),
phis: new Set(),
@@ -249,9 +253,8 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
env,
params: [obj],
returnTypeAnnotation: null,
returnType: makeType(),
returns: createTemporaryPlace(env, GeneratedSource),
context: [],
effects: null,
body: {
entry: block.id,
blocks: new Map([[block.id, block]]),
@@ -259,6 +262,7 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
generator: false,
async: false,
directives: [],
aliasingEffects: [],
};
reversePostorderBlocks(fn.body);
@@ -278,6 +282,7 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
loc: GeneratedSource,
},
lvalue: createTemporaryPlace(env, GeneratedSource),
effects: null,
loc: GeneratedSource,
};
return fnInstr;
@@ -294,6 +299,7 @@ function emitArrayInstr(elements: Array<Place>, env: Environment): Instruction {
id: makeInstructionId(0),
value: array,
lvalue: arrayLvalue,
effects: null,
loc: GeneratedSource,
};
return arrayInstr;

View File

@@ -21,7 +21,6 @@ import {
makeBlockId,
makeIdentifierName,
makeInstructionId,
makeType,
ObjectProperty,
Place,
promoteTemporary,
@@ -297,6 +296,7 @@ function emitOutlinedJsx(
},
loc: GeneratedSource,
},
effects: null,
};
promoteTemporaryJsxTag(loadJsx.lvalue.identifier);
const jsxExpr: Instruction = {
@@ -312,6 +312,7 @@ function emitOutlinedJsx(
openingLoc: GeneratedSource,
closingLoc: GeneratedSource,
},
effects: null,
};
return [loadJsx, jsxExpr];
@@ -351,8 +352,10 @@ function emitOutlinedFn(
terminal: {
id: makeInstructionId(0),
kind: 'return',
returnVariant: 'Explicit',
loc: GeneratedSource,
value: instructions.at(-1)!.lvalue,
effects: null,
},
preds: new Set(),
phis: new Set(),
@@ -365,9 +368,8 @@ function emitOutlinedFn(
env,
params: [propsObj],
returnTypeAnnotation: null,
returnType: makeType(),
returns: createTemporaryPlace(env, GeneratedSource),
context: [],
effects: null,
body: {
entry: block.id,
blocks: new Map([[block.id, block]]),
@@ -375,6 +377,7 @@ function emitOutlinedFn(
generator: false,
async: false,
directives: [],
aliasingEffects: [],
};
return fn;
}
@@ -517,6 +520,7 @@ function emitDestructureProps(
loc: GeneratedSource,
value: propsObj,
},
effects: null,
};
return destructurePropsInstr;
}

View File

@@ -13,7 +13,7 @@ import {
pruneUnusedLabels,
renameVariables,
} from '.';
import {CompilerError, ErrorSeverity} from '../CompilerError';
import {CompilerError, ErrorCategory, ErrorSeverity} from '../CompilerError';
import {Environment, ExternalFunction} from '../HIR';
import {
ArrayPattern,
@@ -44,7 +44,7 @@ import {
getHookKind,
makeIdentifierName,
} from '../HIR/HIR';
import {printIdentifier, printPlace} from '../HIR/PrintHIR';
import {printIdentifier, printInstruction, printPlace} from '../HIR/PrintHIR';
import {eachPatternOperand} from '../HIR/visitors';
import {Err, Ok, Result} from '../Utils/Result';
import {GuardKind} from '../Utils/RuntimeDiagnosticConstants';
@@ -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]);
}),
);
}
@@ -1310,7 +1308,7 @@ function codegenInstructionNullable(
});
CompilerError.invariant(value?.type === 'FunctionExpression', {
reason: 'Expected a function as a function declaration value',
description: null,
description: `Got ${value == null ? String(value) : value.type} at ${printInstruction(instr)}`,
loc: instr.value.loc,
suggestions: null,
});
@@ -1726,7 +1724,7 @@ function codegenInstructionValue(
}
case 'UnaryExpression': {
value = t.unaryExpression(
instrValue.operator as 'throw', // todo
instrValue.operator,
codegenPlaceToExpression(cx, instrValue.value),
);
break;
@@ -2187,6 +2185,7 @@ function codegenInstructionValue(
(declarator.id as t.Identifier).name
}'`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: declarator.loc ?? null,
suggestions: null,
});
@@ -2195,6 +2194,7 @@ function codegenInstructionValue(
cx.errors.push({
reason: `(CodegenReactiveFunction::codegenInstructionValue) Handle conversion of ${stmt.type} to expression`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: stmt.loc ?? null,
suggestions: null,
});
@@ -2582,7 +2582,16 @@ function codegenValue(
value: boolean | number | string | null | undefined,
): t.Expression {
if (typeof value === 'number') {
return t.numericLiteral(value);
if (value < 0) {
/**
* Babel's code generator produces invalid JS for negative numbers when
* run with { compact: true }.
* See repro https://codesandbox.io/p/devbox/5d47fr
*/
return t.unaryExpression('-', t.numericLiteral(-value), false);
} else {
return t.numericLiteral(value);
}
} else if (typeof value === 'boolean') {
return t.booleanLiteral(value);
} else if (typeof value === 'string') {

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

@@ -119,6 +119,7 @@ class FindLastUsageVisitor extends ReactiveFunctionVisitor<void> {
class Transform extends ReactiveFunctionTransform<ReactiveScopeDependencies | null> {
lastUsage: Map<DeclarationId, InstructionId>;
temporaries: Map<DeclarationId, DeclarationId> = new Map();
constructor(lastUsage: Map<DeclarationId, InstructionId>) {
super();
@@ -215,6 +216,12 @@ class Transform extends ReactiveFunctionTransform<ReactiveScopeDependencies | nu
current.lvalues.add(
instr.instruction.lvalue.identifier.declarationId,
);
if (instr.instruction.value.kind === 'LoadLocal') {
this.temporaries.set(
instr.instruction.lvalue.identifier.declarationId,
instr.instruction.value.place.identifier.declarationId,
);
}
}
break;
}
@@ -236,6 +243,13 @@ class Transform extends ReactiveFunctionTransform<ReactiveScopeDependencies | nu
)) {
current.lvalues.add(lvalue.identifier.declarationId);
}
this.temporaries.set(
instr.instruction.value.lvalue.place.identifier
.declarationId,
this.temporaries.get(
instr.instruction.value.value.identifier.declarationId,
) ?? instr.instruction.value.value.identifier.declarationId,
);
} else {
log(
`Reset scope @${current.block.scope.id} from StoreLocal in [${instr.instruction.id}]`,
@@ -260,7 +274,7 @@ class Transform extends ReactiveFunctionTransform<ReactiveScopeDependencies | nu
case 'scope': {
if (
current !== null &&
canMergeScopes(current.block, instr) &&
canMergeScopes(current.block, instr, this.temporaries) &&
areLValuesLastUsedByScope(
instr.scope,
current.lvalues,
@@ -426,6 +440,7 @@ function areLValuesLastUsedByScope(
function canMergeScopes(
current: ReactiveScopeBlock,
next: ReactiveScopeBlock,
temporaries: Map<DeclarationId, DeclarationId>,
): boolean {
// Don't merge scopes with reassignments
if (
@@ -456,6 +471,7 @@ function canMergeScopes(
new Set(
[...current.scope.declarations.values()].map(declaration => ({
identifier: declaration.identifier,
reactive: true,
path: [],
})),
),
@@ -464,11 +480,14 @@ function canMergeScopes(
(next.scope.dependencies.size !== 0 &&
[...next.scope.dependencies].every(
dep =>
dep.path.length === 0 &&
isAlwaysInvalidatingType(dep.identifier.type) &&
Iterable_some(
current.scope.declarations.values(),
decl =>
decl.identifier.declarationId === dep.identifier.declarationId,
decl.identifier.declarationId === dep.identifier.declarationId ||
decl.identifier.declarationId ===
temporaries.get(dep.identifier.declarationId),
),
))
) {
@@ -476,12 +495,16 @@ function canMergeScopes(
return true;
}
log(` cannot merge scopes:`);
log(` ${printReactiveScopeSummary(current.scope)}`);
log(` ${printReactiveScopeSummary(next.scope)}`);
log(
` ${printReactiveScopeSummary(current.scope)} ${[...current.scope.declarations.values()].map(decl => decl.identifier.declarationId)}`,
);
log(
` ${printReactiveScopeSummary(next.scope)} ${[...next.scope.dependencies].map(dep => `${dep.identifier.declarationId} ${temporaries.get(dep.identifier.declarationId) ?? dep.identifier.declarationId}`)}`,
);
return false;
}
function isAlwaysInvalidatingType(type: Type): boolean {
export function isAlwaysInvalidatingType(type: Type): boolean {
switch (type.kind) {
case 'Object': {
switch (type.shapeId) {

View File

@@ -24,7 +24,6 @@ import {
getHookKind,
isMutableEffect,
} from '../HIR';
import {getFunctionCallSignature} from '../Inference/InferReferenceEffects';
import {assertExhaustive, getOrInsertDefault} from '../Utils/utils';
import {getPlaceScope, ReactiveScope} from '../HIR/HIR';
import {
@@ -35,6 +34,7 @@ import {
visitReactiveFunction,
} from './visitors';
import {printPlace} from '../HIR/PrintHIR';
import {getFunctionCallSignature} from '../Inference/InferMutationAliasingEffects';
/*
* This pass prunes reactive scopes that are not necessary to bound downstream computation.
@@ -829,12 +829,14 @@ class CollectDependenciesVisitor extends ReactiveFunctionVisitor<
};
}
case 'UnsupportedNode': {
CompilerError.invariant(false, {
reason: `Unexpected unsupported node`,
description: null,
loc: value.loc,
suggestions: null,
});
const lvalues = [];
if (lvalue !== null) {
lvalues.push({place: lvalue, level: MemoizationLevel.Never});
}
return {
lvalues,
rvalues: [],
};
}
default: {
assertExhaustive(
@@ -1064,12 +1066,29 @@ class PruneScopesTransform extends ReactiveFunctionTransform<
const value = instruction.value;
if (value.kind === 'StoreLocal' && value.lvalue.kind === 'Reassign') {
// Complex cases of useMemo inlining result in a temporary that is reassigned
const ids = getOrInsertDefault(
this.reassignments,
value.lvalue.place.identifier.declarationId,
new Set(),
);
ids.add(value.value.identifier);
} else if (
value.kind === 'LoadLocal' &&
value.place.identifier.scope != null &&
instruction.lvalue != null &&
instruction.lvalue.identifier.scope == null
) {
/*
* Simpler cases result in a direct assignment to the original lvalue, with a
* LoadLocal
*/
const ids = getOrInsertDefault(
this.reassignments,
instruction.lvalue.identifier.declarationId,
new Set(),
);
ids.add(value.place.identifier);
} else if (value.kind === 'FinishMemoize') {
let decls;
if (value.decl.identifier.scope == null) {

View File

@@ -42,6 +42,7 @@ import {
import {eachInstructionOperand} from '../HIR/visitors';
import {printSourceLocationLine} from '../HIR/PrintHIR';
import {USE_FIRE_FUNCTION_NAME} from '../HIR/Environment';
import {ErrorCategory} from '../CompilerError';
/*
* TODO(jmbrown):
@@ -133,6 +134,7 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void {
loc: value.loc,
description: null,
severity: ErrorSeverity.Invariant,
category: ErrorCategory.Invariant,
reason: '[InsertFire] No LoadGlobal found for useEffect call',
suggestions: null,
});
@@ -179,6 +181,7 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void {
description:
'You must use an array literal for an effect dependency array when that effect uses `fire()`',
severity: ErrorSeverity.Invariant,
category: ErrorCategory.Fire,
reason: CANNOT_COMPILE_FIRE,
suggestions: null,
});
@@ -189,6 +192,7 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void {
description:
'You must use an array literal for an effect dependency array when that effect uses `fire()`',
severity: ErrorSeverity.Invariant,
category: ErrorCategory.Fire,
reason: CANNOT_COMPILE_FIRE,
suggestions: null,
});
@@ -223,6 +227,7 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void {
loc: value.loc,
description: null,
severity: ErrorSeverity.Invariant,
category: ErrorCategory.Invariant,
reason:
'[InsertFire] No loadLocal found for fire call argument',
suggestions: null,
@@ -246,6 +251,7 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void {
description:
'`fire()` can only receive a function call such as `fire(fn(a,b)). Method calls and other expressions are not allowed',
severity: ErrorSeverity.InvalidReact,
category: ErrorCategory.Fire,
reason: CANNOT_COMPILE_FIRE,
suggestions: null,
});
@@ -264,6 +270,7 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void {
loc: value.loc,
description,
severity: ErrorSeverity.InvalidReact,
category: ErrorCategory.Fire,
reason: CANNOT_COMPILE_FIRE,
suggestions: null,
});
@@ -395,6 +402,7 @@ function ensureNoRemainingCalleeCaptures(
this effect or not used with a fire() call at all. ${calleeName} was used with fire() on line \
${printSourceLocationLine(calleeInfo.fireLoc)} in this effect`,
severity: ErrorSeverity.InvalidReact,
category: ErrorCategory.Fire,
reason: CANNOT_COMPILE_FIRE,
suggestions: null,
});
@@ -411,6 +419,7 @@ function ensureNoMoreFireUses(fn: HIRFunction, context: Context): void {
context.pushError({
loc: place.identifier.loc,
description: 'Cannot use `fire` outside of a useEffect function',
category: ErrorCategory.Fire,
severity: ErrorSeverity.Invariant,
reason: CANNOT_COMPILE_FIRE,
suggestions: null,
@@ -436,6 +445,7 @@ function makeLoadUseFireInstruction(
value: instrValue,
lvalue: {...useFirePlace},
loc: GeneratedSource,
effects: null,
};
}
@@ -460,6 +470,7 @@ function makeLoadFireCalleeInstruction(
},
lvalue: {...loadedFireCallee},
loc: GeneratedSource,
effects: null,
};
}
@@ -483,6 +494,7 @@ function makeCallUseFireInstruction(
value: useFireCall,
lvalue: {...useFireCallResultPlace},
loc: GeneratedSource,
effects: null,
};
}
@@ -511,6 +523,7 @@ function makeStoreUseFireInstruction(
},
lvalue: fireFunctionBindingLValuePlace,
loc: GeneratedSource,
effects: null,
};
}

View File

@@ -14,6 +14,7 @@ import {
Identifier,
IdentifierId,
Instruction,
InstructionKind,
makePropertyLiteral,
makeType,
PropType,
@@ -90,7 +91,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 +145,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]!);
}
}
@@ -193,12 +195,29 @@ function* generateInstructionTypes(
break;
}
// We intentionally do not infer types for context variables
// We intentionally do not infer types for most context variables
case 'DeclareContext':
case 'StoreContext':
case 'LoadContext': {
break;
}
case 'StoreContext': {
/**
* The caveat is StoreContext const, where we know the value is
* assigned once such that everywhere the value is accessed, it
* must have the same type from the rvalue.
*
* A concrete example where this is useful is `const ref = useRef()`
* where the ref is referenced before its declaration in a function
* expression, causing it to be converted to a const context variable.
*/
if (value.lvalue.kind === InstructionKind.Const) {
yield equation(
value.lvalue.place.identifier.type,
value.value.identifier.type,
);
}
break;
}
case 'StoreLocal': {
if (env.config.enableUseTypeAnnotations) {
@@ -359,6 +378,12 @@ function* generateInstructionTypes(
value: makePropertyLiteral(propertyName),
},
});
} else if (item.kind === 'Spread') {
// Array pattern spread always creates an array
yield equation(item.place.identifier.type, {
kind: 'Object',
shapeId: BuiltInArrayId,
});
} else {
break;
}
@@ -407,7 +432,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;
@@ -426,6 +451,18 @@ function* generateInstructionTypes(
case 'JsxExpression':
case 'JsxFragment': {
if (env.config.enableTreatRefLikeIdentifiersAsRefs) {
if (value.kind === 'JsxExpression') {
for (const prop of value.props) {
if (prop.kind === 'JsxAttribute' && prop.name === 'ref') {
yield equation(prop.place.identifier.type, {
kind: 'Object',
shapeId: BuiltInUseRefId,
});
}
}
}
}
yield equation(left, {kind: 'Object', shapeId: BuiltInJsxId});
break;
}
@@ -441,7 +478,36 @@ function* generateInstructionTypes(
yield equation(left, returnType);
break;
}
case 'PropertyStore':
case 'PropertyStore': {
/**
* Infer types based on assignments to known object properties
* This is important for refs, where assignment to `<maybeRef>.current`
* can help us infer that an object itself is a ref
*/
yield equation(
/**
* Our property type declarations are best-effort and we haven't tested
* using them to drive inference of rvalues from lvalues. We want to emit
* a Property type in order to infer refs from `.current` accesses, but
* stay conservative by not otherwise inferring anything about rvalues.
* So we use a dummy type here.
*
* TODO: consider using the rvalue type here
*/
makeType(),
// unify() only handles properties in the second position
{
kind: 'Property',
objectType: value.object.identifier.type,
objectName: getName(names, value.object.identifier.id),
propertyName: {
kind: 'literal',
value: value.property,
},
},
);
break;
}
case 'DeclareLocal':
case 'RegExpLiteral':
case 'MetaProperty':

View File

@@ -1,110 +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, Type, typeEquals} from '../HIR';
/**
* Temporary workaround for InferTypes not propagating the types of phis.
* Previously, LeaveSSA would replace all the identifiers for each phi (operands and
* the phi itself) with a single "canonical" identifier, generally chosen as the first
* operand to flow into the phi. In case of a phi whose operand was a phi, this could
* sometimes be an operand from the earlier phi.
*
* As a result, even though InferTypes did not propagate types for phis, LeaveSSA
* could end up replacing the phi Identifier with another identifer from an operand,
* which _did_ have a type inferred.
*
* This didn't affect the initial construction of mutable ranges because InferMutableRanges
* runs before LeaveSSA - thus, the types propagated by LeaveSSA only affected later optimizations,
* notably MergeScopesThatInvalidateTogether which uses type to determine if a scope's output
* will always invalidate with its input.
*
* The long-term correct approach is to update InferTypes to infer the types of phis,
* but this is complicated because InferMutableRanges inadvertently depends on phis
* never having a known type, such that a Store effect cannot occur on a phi value.
* Once we fix InferTypes to infer phi types, then we'll also have to update InferMutableRanges
* to handle this case.
*
* As a temporary workaround, this pass propagates the type of phis and can be called
* safely *after* InferMutableRanges. Unlike LeaveSSA, this pass only propagates the
* type if all operands have the same type, it's its more correct.
*/
export function propagatePhiTypes(fn: HIRFunction): void {
/**
* We track which SSA ids have had their types propagated to handle nested ternaries,
* see the StoreLocal handling below
*/
const propagated = new Set<IdentifierId>();
for (const [, block] of fn.body.blocks) {
for (const phi of block.phis) {
/*
* We replicate the previous LeaveSSA behavior and only propagate types for
* unnamed variables. LeaveSSA would have chosen one of the operands as the
* canonical id and taken its type as the type of all identifiers. We're
* more conservative and only propagate if the types are the same and the
* phi didn't have a type inferred.
*
* Note that this can change output slightly in cases such as
* `cond ? <div /> : null`.
*
* Previously the first operand's type (BuiltInJsx) would have been propagated,
* and this expression may have been merged with subsequent reactive scopes
* since it appears (based on that type) to always invalidate.
*
* But the correct type is `BuiltInJsx | null`, which we can't express and
* so leave as a generic `Type`, which does not always invalidate and therefore
* does not merge with subsequent scopes.
*
* We also don't propagate scopes for named variables, to preserve compatibility
* with previous LeaveSSA behavior.
*/
if (
phi.place.identifier.type.kind !== 'Type' ||
phi.place.identifier.name !== null
) {
continue;
}
let type: Type | null = null;
for (const [, operand] of phi.operands) {
if (type === null) {
type = operand.identifier.type;
} else if (!typeEquals(type, operand.identifier.type)) {
type = null;
break;
}
}
if (type !== null) {
phi.place.identifier.type = type;
propagated.add(phi.place.identifier.id);
}
}
for (const instr of block.instructions) {
const {value} = instr;
switch (value.kind) {
case 'StoreLocal': {
/**
* Nested ternaries can lower to a form with an intermediate StoreLocal where
* the value.lvalue is the temporary of the outer ternary, and the value.value
* is the result of the inner ternary.
*
* This is a common pattern in practice and easy enough to support. Again, the
* long-term approach is to update InferTypes and InferMutableRanges.
*/
const lvalue = value.lvalue.place;
if (
propagated.has(value.value.identifier.id) &&
lvalue.identifier.type.kind === 'Type' &&
lvalue.identifier.name === null
) {
lvalue.identifier.type = value.value.identifier.type;
propagated.add(lvalue.identifier.id);
}
}
}
}
}
}

View File

@@ -78,6 +78,10 @@ export default class DisjointSet<T> {
return root;
}
has(item: T): boolean {
return this.#entries.has(item);
}
/*
* Forces the set into canonical form, ie with all items pointing directly to
* their root, and returns a Map representing the mapping of items to their roots.

View File

@@ -0,0 +1,87 @@
/**
* 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.
*/
/**
* https://tc39.es/ecma262/multipage/ecmascript-language-lexical-grammar.html#sec-keywords-and-reserved-words
*/
/**
* Note: `await` and `yield` are contextually allowed as identifiers.
* await: reserved inside async functions and modules
* yield: reserved inside generator functions
*
* Note: `async` is not reserved.
*/
const RESERVED_WORDS = new Set([
'break',
'case',
'catch',
'class',
'const',
'continue',
'debugger',
'default',
'delete',
'do',
'else',
'enum',
'export',
'extends',
'false',
'finally',
'for',
'function',
'if',
'import',
'in',
'instanceof',
'new',
'null',
'return',
'super',
'switch',
'this',
'throw',
'true',
'try',
'typeof',
'var',
'void',
'while',
'with',
]);
/**
* Reserved when a module has a 'use strict' directive.
*/
const STRICT_MODE_RESERVED_WORDS = new Set([
'let',
'static',
'implements',
'interface',
'package',
'private',
'protected',
'public',
]);
/**
* The names arguments and eval are not keywords, but they are subject to some restrictions in
* strict mode code.
*/
const STRICT_MODE_RESTRICTED_WORDS = new Set(['eval', 'arguments']);
/**
* Conservative check for whether an identifer name is reserved or not. We assume that code is
* written with strict mode.
*/
export function isReservedWord(identifierName: string): boolean {
return (
RESERVED_WORDS.has(identifierName) ||
STRICT_MODE_RESERVED_WORDS.has(identifierName) ||
STRICT_MODE_RESTRICTED_WORDS.has(identifierName)
);
}

View File

@@ -90,10 +90,13 @@ export function Ok<T>(val: T): OkImpl<T> {
}
class OkImpl<T> implements Result<T, never> {
constructor(private val: T) {}
#val: T;
constructor(val: T) {
this.#val = val;
}
map<U>(fn: (val: T) => U): Result<U, never> {
return new OkImpl(fn(this.val));
return new OkImpl(fn(this.#val));
}
mapErr<F>(_fn: (val: never) => F): Result<T, F> {
@@ -101,15 +104,15 @@ class OkImpl<T> implements Result<T, never> {
}
mapOr<U>(_fallback: U, fn: (val: T) => U): U {
return fn(this.val);
return fn(this.#val);
}
mapOrElse<U>(_fallback: () => U, fn: (val: T) => U): U {
return fn(this.val);
return fn(this.#val);
}
andThen<U>(fn: (val: T) => Result<U, never>): Result<U, never> {
return fn(this.val);
return fn(this.#val);
}
and<U>(res: Result<U, never>): Result<U, never> {
@@ -133,30 +136,30 @@ class OkImpl<T> implements Result<T, never> {
}
expect(_msg: string): T {
return this.val;
return this.#val;
}
expectErr(msg: string): never {
throw new Error(`${msg}: ${this.val}`);
throw new Error(`${msg}: ${this.#val}`);
}
unwrap(): T {
return this.val;
return this.#val;
}
unwrapOr(_fallback: T): T {
return this.val;
return this.#val;
}
unwrapOrElse(_fallback: (val: never) => T): T {
return this.val;
return this.#val;
}
unwrapErr(): never {
if (this.val instanceof Error) {
throw this.val;
if (this.#val instanceof Error) {
throw this.#val;
}
throw new Error(`Can't unwrap \`Ok\` to \`Err\`: ${this.val}`);
throw new Error(`Can't unwrap \`Ok\` to \`Err\`: ${this.#val}`);
}
}
@@ -165,14 +168,17 @@ export function Err<E>(val: E): ErrImpl<E> {
}
class ErrImpl<E> implements Result<never, E> {
constructor(private val: E) {}
#val: E;
constructor(val: E) {
this.#val = val;
}
map<U>(_fn: (val: never) => U): Result<U, E> {
return this;
}
mapErr<F>(fn: (val: E) => F): Result<never, F> {
return new ErrImpl(fn(this.val));
return new ErrImpl(fn(this.#val));
}
mapOr<U>(fallback: U, _fn: (val: never) => U): U {
@@ -196,7 +202,7 @@ class ErrImpl<E> implements Result<never, E> {
}
orElse<F>(fn: (val: E) => ErrImpl<F>): Result<never, F> {
return fn(this.val);
return fn(this.#val);
}
isOk(): this is OkImpl<never> {
@@ -208,18 +214,18 @@ class ErrImpl<E> implements Result<never, E> {
}
expect(msg: string): never {
throw new Error(`${msg}: ${this.val}`);
throw new Error(`${msg}: ${this.#val}`);
}
expectErr(_msg: string): E {
return this.val;
return this.#val;
}
unwrap(): never {
if (this.val instanceof Error) {
throw this.val;
if (this.#val instanceof Error) {
throw this.#val;
}
throw new Error(`Can't unwrap \`Err\` to \`Ok\`: ${this.val}`);
throw new Error(`Can't unwrap \`Err\` to \`Ok\`: ${this.#val}`);
}
unwrapOr<T>(fallback: T): T {
@@ -227,10 +233,10 @@ class ErrImpl<E> implements Result<never, E> {
}
unwrapOrElse<T>(fallback: (val: E) => T): T {
return fallback(this.val);
return fallback(this.#val);
}
unwrapErr(): E {
return this.val;
return this.#val;
}
}

View File

@@ -75,45 +75,57 @@ const testComplexConfigDefaults: PartialEnvironmentConfig = {
source: 'react',
importSpecifierName: 'useEffect',
},
numRequiredArgs: 1,
autodepsIndex: 1,
},
{
function: {
source: 'shared-runtime',
importSpecifierName: 'useSpecialEffect',
},
numRequiredArgs: 2,
autodepsIndex: 2,
},
{
function: {
source: 'useEffectWrapper',
importSpecifierName: 'default',
},
numRequiredArgs: 1,
autodepsIndex: 1,
},
],
};
function* splitPragma(
pragma: string,
): Generator<{key: string; value: string | null}> {
for (const entry of pragma.split('@')) {
const keyVal = entry.trim();
const valIdx = keyVal.indexOf(':');
if (valIdx === -1) {
yield {key: keyVal.split(' ', 1)[0], value: null};
} else {
yield {key: keyVal.slice(0, valIdx), value: keyVal.slice(valIdx + 1)};
}
}
}
/**
* For snap test fixtures and playground only.
*/
function parseConfigPragmaEnvironmentForTest(
pragma: string,
defaultConfig: PartialEnvironmentConfig,
): EnvironmentConfig {
const maybeConfig: Partial<Record<keyof EnvironmentConfig, unknown>> = {};
// throw early if the defaults are invalid
EnvironmentConfigSchema.parse(defaultConfig);
for (const token of pragma.split(' ')) {
if (!token.startsWith('@')) {
continue;
}
const keyVal = token.slice(1);
const valIdx = keyVal.indexOf(':');
const key = valIdx === -1 ? keyVal : keyVal.slice(0, valIdx);
const val = valIdx === -1 ? undefined : keyVal.slice(valIdx + 1);
const isSet = val === undefined || val === 'true';
const maybeConfig: Partial<Record<keyof EnvironmentConfig, unknown>> =
defaultConfig;
for (const {key, value: val} of splitPragma(pragma)) {
if (!hasOwnProperty(EnvironmentConfigSchema.shape, key)) {
continue;
}
const isSet = val == null || val === 'true';
if (isSet && key in testComplexConfigDefaults) {
maybeConfig[key] = testComplexConfigDefaults[key];
} else if (isSet) {
@@ -167,27 +179,24 @@ export function parseConfigPragmaForTests(
pragma: string,
defaults: {
compilationMode: CompilationMode;
environment?: PartialEnvironmentConfig;
},
): PluginOptions {
const environment = parseConfigPragmaEnvironmentForTest(pragma);
const environment = parseConfigPragmaEnvironmentForTest(
pragma,
defaults.environment ?? {},
);
const options: Record<keyof PluginOptions, unknown> = {
...defaultOptions,
panicThreshold: 'all_errors',
compilationMode: defaults.compilationMode,
environment,
};
for (const token of pragma.split(' ')) {
if (!token.startsWith('@')) {
continue;
}
const keyVal = token.slice(1);
const idx = keyVal.indexOf(':');
const key = idx === -1 ? keyVal : keyVal.slice(0, idx);
const val = idx === -1 ? undefined : keyVal.slice(idx + 1);
for (const {key, value: val} of splitPragma(pragma)) {
if (!hasOwnProperty(defaultOptions, key)) {
continue;
}
const isSet = val === undefined || val === 'true';
const isSet = val == null || val === 'true';
if (isSet && key in testComplexPluginOptionDefaults) {
options[key] = testComplexPluginOptionDefaults[key];
} else if (isSet) {

View File

@@ -33,12 +33,12 @@ export function assertExhaustive(_: never, errorMsg: string): never {
// Modifies @param array in place, retaining only the items where the predicate returns true.
export function retainWhere<T>(
array: Array<T>,
predicate: (item: T) => boolean,
predicate: (item: T, index: number) => boolean,
): void {
let writeIndex = 0;
for (let readIndex = 0; readIndex < array.length; readIndex++) {
const item = array[readIndex];
if (predicate(item) === true) {
if (predicate(item, readIndex) === true) {
array[writeIndex++] = item;
}
}
@@ -121,6 +121,21 @@ export function Set_intersect<T>(sets: Array<ReadonlySet<T>>): Set<T> {
return result;
}
/**
* @returns `true` if `a` is a superset of `b`.
*/
export function Set_isSuperset<T>(
a: ReadonlySet<T>,
b: ReadonlySet<T>,
): boolean {
for (const v of b) {
if (!a.has(v)) {
return false;
}
}
return true;
}
export function Iterable_some<T>(
iter: Iterable<T>,
pred: (item: T) => boolean,

View File

@@ -9,6 +9,7 @@ import * as t from '@babel/types';
import {
CompilerError,
CompilerErrorDetail,
ErrorCategory,
ErrorSeverity,
} from '../CompilerError';
import {computeUnconditionalBlocks} from '../HIR/ComputeUnconditionalBlocks';
@@ -124,6 +125,7 @@ export function validateHooksUsage(
recordError(
place.loc,
new CompilerErrorDetail({
category: ErrorCategory.Hooks,
description: null,
reason,
loc: place.loc,
@@ -140,6 +142,7 @@ export function validateHooksUsage(
recordError(
place.loc,
new CompilerErrorDetail({
category: ErrorCategory.Hooks,
description: null,
reason:
'Hooks may not be referenced as normal values, they must be called. See https://react.dev/reference/rules/react-calls-components-and-hooks#never-pass-around-hooks-as-regular-values',
@@ -157,6 +160,7 @@ export function validateHooksUsage(
recordError(
place.loc,
new CompilerErrorDetail({
category: ErrorCategory.Hooks,
description: null,
reason:
'Hooks must be the same function on every render, but this value may change over time to a different function. See https://react.dev/reference/rules/react-calls-components-and-hooks#dont-dynamically-use-hooks',
@@ -424,7 +428,7 @@ export function validateHooksUsage(
}
for (const [, error] of errorsByPlace) {
errors.push(error);
errors.pushErrorDetail(error);
}
return errors.asResult();
}
@@ -448,11 +452,12 @@ function visitFunctionExpression(errors: CompilerError, fn: HIRFunction): void {
if (hookKind != null) {
errors.pushErrorDetail(
new CompilerErrorDetail({
category: ErrorCategory.Hooks,
severity: ErrorSeverity.InvalidReact,
reason:
'Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)',
loc: callee.loc,
description: `Cannot call ${hookKind} within a function component`,
description: `Cannot call ${hookKind === 'Custom' ? 'hook' : hookKind} within a function expression`,
suggestions: null,
}),
);

View File

@@ -5,14 +5,15 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError, Effect} from '..';
import {CompilerDiagnostic, CompilerError, Effect, ErrorSeverity} from '..';
import {ErrorCategory} from '../CompilerError';
import {HIRFunction, IdentifierId, Place} from '../HIR';
import {
eachInstructionLValue,
eachInstructionValueOperand,
eachTerminalOperand,
} from '../HIR/visitors';
import {getFunctionCallSignature} from '../Inference/InferReferenceEffects';
import {getFunctionCallSignature} from '../Inference/InferMutationAliasingEffects';
/**
* Validates that local variables cannot be reassigned after render.
@@ -28,16 +29,25 @@ export function validateLocalsNotReassignedAfterRender(fn: HIRFunction): void {
false,
);
if (reassignment !== null) {
CompilerError.throwInvalidReact({
reason:
'Reassigning a variable after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead',
description:
reassignment.identifier.name !== null &&
reassignment.identifier.name.kind === 'named'
? `Variable \`${reassignment.identifier.name.value}\` cannot be reassigned after render`
: '',
loc: reassignment.loc,
});
const errors = new CompilerError();
const variable =
reassignment.identifier.name != null &&
reassignment.identifier.name.kind === 'named'
? `\`${reassignment.identifier.name.value}\``
: 'variable';
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Immutability,
severity: ErrorSeverity.InvalidReact,
reason: 'Cannot reassign variable after render completes',
description: `Reassigning ${variable} after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead.`,
}).withDetail({
kind: 'error',
loc: reassignment.loc,
message: `Cannot reassign ${variable} after render completes`,
}),
);
throw errors;
}
}
@@ -75,16 +85,26 @@ function getContextReassignment(
// if the function or its depends reassign, propagate that fact on the lvalue
if (reassignment !== null) {
if (isAsync || value.loweredFunc.func.async) {
CompilerError.throwInvalidReact({
reason:
'Reassigning a variable in an async function can cause inconsistent behavior on subsequent renders. Consider using state instead',
description:
reassignment.identifier.name !== null &&
reassignment.identifier.name.kind === 'named'
? `Variable \`${reassignment.identifier.name.value}\` cannot be reassigned after render`
: '',
loc: reassignment.loc,
});
const errors = new CompilerError();
const variable =
reassignment.identifier.name !== null &&
reassignment.identifier.name.kind === 'named'
? `\`${reassignment.identifier.name.value}\``
: 'variable';
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Immutability,
severity: ErrorSeverity.InvalidReact,
reason: 'Cannot reassign variable in async function',
description:
'Reassigning a variable in an async function can cause inconsistent behavior on subsequent renders. Consider using state instead',
}).withDetail({
kind: 'error',
loc: reassignment.loc,
message: `Cannot reassign ${variable}`,
}),
);
throw errors;
}
reassigningFunctions.set(lvalue.identifier.id, reassignment);
}

View File

@@ -6,6 +6,7 @@
*/
import {CompilerError, ErrorSeverity} from '..';
import {ErrorCategory} from '../CompilerError';
import {
Identifier,
Instruction,
@@ -108,6 +109,7 @@ class Visitor extends ReactiveFunctionVisitor<CompilerError> {
isUnmemoized(deps.identifier, this.scopes))
) {
state.push({
category: ErrorCategory.EffectDependencies,
reason:
'React Compiler has skipped optimizing this component because the effect dependencies could not be memoized. Unmemoized effect dependencies can trigger an infinite loop or other unexpected behavior',
description: null,

View File

@@ -6,6 +6,7 @@
*/
import {CompilerError, EnvironmentConfig, ErrorSeverity} from '..';
import {ErrorCategory} from '../CompilerError';
import {HIRFunction, IdentifierId} from '../HIR';
import {DEFAULT_GLOBALS} from '../HIR/Globals';
import {Result} from '../Utils/Result';
@@ -56,6 +57,7 @@ export function validateNoCapitalizedCalls(
const calleeName = capitalLoadGlobals.get(calleeIdentifier);
if (calleeName != null) {
CompilerError.throwInvalidReact({
category: ErrorCategory.CapitalizedCalls,
reason,
description: `${calleeName} may be a component.`,
loc: value.loc,
@@ -79,6 +81,7 @@ export function validateNoCapitalizedCalls(
const propertyName = capitalizedProperties.get(propertyIdentifier);
if (propertyName != null) {
errors.push({
category: ErrorCategory.CapitalizedCalls,
severity: ErrorSeverity.InvalidReact,
reason,
description: `${propertyName} may be a component.`,

View File

@@ -0,0 +1,232 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError, ErrorSeverity, SourceLocation} from '..';
import {ErrorCategory} from '../CompilerError';
import {
ArrayExpression,
BlockId,
FunctionExpression,
HIRFunction,
IdentifierId,
isSetStateType,
isUseEffectHookType,
} from '../HIR';
import {
eachInstructionValueOperand,
eachTerminalOperand,
} from '../HIR/visitors';
/**
* Validates that useEffect is not used for derived computations which could/should
* be performed in render.
*
* See https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state
*
* Example:
*
* ```
* // 🔴 Avoid: redundant state and unnecessary Effect
* const [fullName, setFullName] = useState('');
* useEffect(() => {
* setFullName(firstName + ' ' + lastName);
* }, [firstName, lastName]);
* ```
*
* Instead use:
*
* ```
* // ✅ Good: calculated during rendering
* const fullName = firstName + ' ' + lastName;
* ```
*/
export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
const candidateDependencies: Map<IdentifierId, ArrayExpression> = new Map();
const functions: Map<IdentifierId, FunctionExpression> = new Map();
const locals: Map<IdentifierId, IdentifierId> = new Map();
const errors = new CompilerError();
for (const block of fn.body.blocks.values()) {
for (const instr of block.instructions) {
const {lvalue, value} = instr;
if (value.kind === 'LoadLocal') {
locals.set(lvalue.identifier.id, value.place.identifier.id);
} else if (value.kind === 'ArrayExpression') {
candidateDependencies.set(lvalue.identifier.id, value);
} else if (value.kind === 'FunctionExpression') {
functions.set(lvalue.identifier.id, value);
} else if (
value.kind === 'CallExpression' ||
value.kind === 'MethodCall'
) {
const callee =
value.kind === 'CallExpression' ? value.callee : value.property;
if (
isUseEffectHookType(callee.identifier) &&
value.args.length === 2 &&
value.args[0].kind === 'Identifier' &&
value.args[1].kind === 'Identifier'
) {
const effectFunction = functions.get(value.args[0].identifier.id);
const deps = candidateDependencies.get(value.args[1].identifier.id);
if (
effectFunction != null &&
deps != null &&
deps.elements.length !== 0 &&
deps.elements.every(element => element.kind === 'Identifier')
) {
const dependencies: Array<IdentifierId> = deps.elements.map(dep => {
CompilerError.invariant(dep.kind === 'Identifier', {
reason: `Dependency is checked as a place above`,
loc: value.loc,
});
return locals.get(dep.identifier.id) ?? dep.identifier.id;
});
validateEffect(
effectFunction.loweredFunc.func,
dependencies,
errors,
);
}
}
}
}
}
if (errors.hasErrors()) {
throw errors;
}
}
function validateEffect(
effectFunction: HIRFunction,
effectDeps: Array<IdentifierId>,
errors: CompilerError,
): void {
for (const operand of effectFunction.context) {
if (isSetStateType(operand.identifier)) {
continue;
} else if (effectDeps.find(dep => dep === operand.identifier.id) != null) {
continue;
} else {
// Captured something other than the effect dep or setState
return;
}
}
for (const dep of effectDeps) {
if (
effectFunction.context.find(operand => operand.identifier.id === dep) ==
null
) {
// effect dep wasn't actually used in the function
return;
}
}
const seenBlocks: Set<BlockId> = new Set();
const values: Map<IdentifierId, Array<IdentifierId>> = new Map();
for (const dep of effectDeps) {
values.set(dep, [dep]);
}
const setStateLocations: Array<SourceLocation> = [];
for (const block of effectFunction.body.blocks.values()) {
for (const pred of block.preds) {
if (!seenBlocks.has(pred)) {
// skip if block has a back edge
return;
}
}
for (const phi of block.phis) {
const aggregateDeps: Set<IdentifierId> = new Set();
for (const operand of phi.operands.values()) {
const deps = values.get(operand.identifier.id);
if (deps != null) {
for (const dep of deps) {
aggregateDeps.add(dep);
}
}
}
if (aggregateDeps.size !== 0) {
values.set(phi.place.identifier.id, Array.from(aggregateDeps));
}
}
for (const instr of block.instructions) {
switch (instr.value.kind) {
case 'Primitive':
case 'JSXText':
case 'LoadGlobal': {
break;
}
case 'LoadLocal': {
const deps = values.get(instr.value.place.identifier.id);
if (deps != null) {
values.set(instr.lvalue.identifier.id, deps);
}
break;
}
case 'ComputedLoad':
case 'PropertyLoad':
case 'BinaryExpression':
case 'TemplateLiteral':
case 'CallExpression':
case 'MethodCall': {
const aggregateDeps: Set<IdentifierId> = new Set();
for (const operand of eachInstructionValueOperand(instr.value)) {
const deps = values.get(operand.identifier.id);
if (deps != null) {
for (const dep of deps) {
aggregateDeps.add(dep);
}
}
}
if (aggregateDeps.size !== 0) {
values.set(instr.lvalue.identifier.id, Array.from(aggregateDeps));
}
if (
instr.value.kind === 'CallExpression' &&
isSetStateType(instr.value.callee.identifier) &&
instr.value.args.length === 1 &&
instr.value.args[0].kind === 'Identifier'
) {
const deps = values.get(instr.value.args[0].identifier.id);
if (deps != null && new Set(deps).size === effectDeps.length) {
setStateLocations.push(instr.value.callee.loc);
} else {
// doesn't depend on any deps
return;
}
}
break;
}
default: {
return;
}
}
}
for (const operand of eachTerminalOperand(block.terminal)) {
if (values.has(operand.identifier.id)) {
//
return;
}
}
seenBlocks.add(block.id);
}
for (const loc of setStateLocations) {
errors.push({
category: ErrorCategory.EffectDerivationsOfState,
reason:
'Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)',
description: null,
severity: ErrorSeverity.InvalidReact,
loc,
suggestions: null,
});
}
}

View File

@@ -5,12 +5,11 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError, Effect, ErrorSeverity} from '..';
import {CompilerDiagnostic, CompilerError, Effect, ErrorSeverity} from '..';
import {ErrorCategory} from '../CompilerError';
import {
FunctionEffect,
HIRFunction,
IdentifierId,
isMutableEffect,
isRefOrRefLikeMutableType,
Place,
} from '../HIR';
@@ -18,8 +17,8 @@ import {
eachInstructionValueOperand,
eachTerminalOperand,
} from '../HIR/visitors';
import {AliasingEffect} from '../Inference/AliasingEffects';
import {Result} from '../Utils/Result';
import {Iterable_some} from '../Utils/utils';
/**
* Validates that functions with known mutations (ie due to types) cannot be passed
@@ -50,24 +49,38 @@ export function validateNoFreezingKnownMutableFunctions(
const errors = new CompilerError();
const contextMutationEffects: Map<
IdentifierId,
Extract<FunctionEffect, {kind: 'ContextMutation'}>
Extract<AliasingEffect, {kind: 'Mutate'} | {kind: 'MutateTransitive'}>
> = new Map();
function visitOperand(operand: Place): void {
if (operand.effect === Effect.Freeze) {
const effect = contextMutationEffects.get(operand.identifier.id);
if (effect != null) {
errors.push({
reason: `This argument is a function which modifies local variables when called, which can bypass memoization and cause the UI not to update`,
description: `Functions that are returned from hooks, passed as arguments to hooks, or passed as props to components may not mutate local variables`,
loc: operand.loc,
severity: ErrorSeverity.InvalidReact,
});
errors.push({
reason: `The function modifies a local variable here`,
loc: effect.loc,
severity: ErrorSeverity.InvalidReact,
});
const place = effect.value;
const variable =
place != null &&
place.identifier.name != null &&
place.identifier.name.kind === 'named'
? `\`${place.identifier.name.value}\``
: 'a local variable';
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Immutability,
severity: ErrorSeverity.InvalidReact,
reason: 'Cannot modify local variables after render completes',
description: `This argument is a function which may reassign or mutate ${variable} after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead.`,
})
.withDetail({
kind: 'error',
loc: operand.loc,
message: `This function may (indirectly) reassign or modify ${variable} after render`,
})
.withDetail({
kind: 'error',
loc: effect.value.loc,
message: `This modifies ${variable}`,
}),
);
}
}
}
@@ -95,23 +108,47 @@ export function validateNoFreezingKnownMutableFunctions(
break;
}
case 'FunctionExpression': {
const knownMutation = (value.loweredFunc.func.effects ?? []).find(
effect => {
return (
effect.kind === 'ContextMutation' &&
(effect.effect === Effect.Store ||
effect.effect === Effect.Mutate) &&
Iterable_some(effect.places, place => {
return (
isMutableEffect(place.effect, place.loc) &&
!isRefOrRefLikeMutableType(place.identifier.type)
if (value.loweredFunc.func.aliasingEffects != null) {
const context = new Set(
value.loweredFunc.func.context.map(p => p.identifier.id),
);
effects: for (const effect of value.loweredFunc.func
.aliasingEffects) {
switch (effect.kind) {
case 'Mutate':
case 'MutateTransitive': {
const knownMutation = contextMutationEffects.get(
effect.value.identifier.id,
);
})
);
},
);
if (knownMutation && knownMutation.kind === 'ContextMutation') {
contextMutationEffects.set(lvalue.identifier.id, knownMutation);
if (knownMutation != null) {
contextMutationEffects.set(
lvalue.identifier.id,
knownMutation,
);
} else if (
context.has(effect.value.identifier.id) &&
!isRefOrRefLikeMutableType(effect.value.identifier.type)
) {
contextMutationEffects.set(lvalue.identifier.id, effect);
break effects;
}
break;
}
case 'MutateConditionally':
case 'MutateTransitiveConditionally': {
const knownMutation = contextMutationEffects.get(
effect.value.identifier.id,
);
if (knownMutation != null) {
contextMutationEffects.set(
lvalue.identifier.id,
knownMutation,
);
}
break;
}
}
}
}
break;
}

View File

@@ -5,9 +5,10 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError, ErrorSeverity} from '..';
import {CompilerDiagnostic, CompilerError, ErrorSeverity} from '..';
import {ErrorCategory} from '../CompilerError';
import {HIRFunction} from '../HIR';
import {getFunctionCallSignature} from '../Inference/InferReferenceEffects';
import {getFunctionCallSignature} from '../Inference/InferMutationAliasingEffects';
import {Result} from '../Utils/Result';
/**
@@ -34,17 +35,23 @@ export function validateNoImpureFunctionsInRender(
callee.identifier.type,
);
if (signature != null && signature.impure === true) {
errors.push({
reason:
'Calling an impure function can produce unstable results. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent)',
description:
signature.canonicalName != null
? `\`${signature.canonicalName}\` is an impure function whose results may change on every call`
: null,
severity: ErrorSeverity.InvalidReact,
loc: callee.loc,
suggestions: null,
});
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Purity,
reason: 'Cannot call impure function during render',
description:
(signature.canonicalName != null
? `\`${signature.canonicalName}\` is an impure function. `
: '') +
'Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent)',
severity: ErrorSeverity.InvalidReact,
suggestions: null,
}).withDetail({
kind: 'error',
loc: callee.loc,
message: 'Cannot call impure function',
}),
);
}
}
}

View File

@@ -5,7 +5,8 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError, ErrorSeverity} from '..';
import {CompilerDiagnostic, CompilerError, ErrorSeverity} from '..';
import {ErrorCategory} from '../CompilerError';
import {BlockId, HIRFunction} from '../HIR';
import {Result} from '../Utils/Result';
import {retainWhere} from '../Utils/utils';
@@ -34,11 +35,18 @@ export function validateNoJSXInTryStatement(
switch (value.kind) {
case 'JsxExpression':
case 'JsxFragment': {
errors.push({
severity: ErrorSeverity.InvalidReact,
reason: `Unexpected JSX element within a try statement. To catch errors in rendering a given component, wrap that component in an error boundary. (https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary)`,
loc: value.loc,
});
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.ErrorBoundaries,
severity: ErrorSeverity.InvalidReact,
reason: 'Avoid constructing JSX within try/catch',
description: `React does not immediately render components when JSX is rendered, so any errors from this component will not be caught by the try/catch. To catch errors in rendering a given component, wrap that component in an error boundary. (https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary)`,
}).withDetail({
kind: 'error',
loc: value.loc,
message: 'Avoid constructing JSX within try/catch',
}),
);
break;
}
}

View File

@@ -5,7 +5,12 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError, ErrorSeverity} from '../CompilerError';
import {
CompilerDiagnostic,
CompilerError,
ErrorCategory,
ErrorSeverity,
} from '../CompilerError';
import {
BlockId,
HIRFunction,
@@ -23,6 +28,7 @@ import {
eachTerminalOperand,
} from '../HIR/visitors';
import {Err, Ok, Result} from '../Utils/Result';
import {retainWhere} from '../Utils/utils';
/**
* Validates that a function does not access a ref value during render. This includes a partial check
@@ -75,8 +81,18 @@ type RefAccessRefType =
type RefFnType = {readRefEffect: boolean; returnType: RefAccessType};
class Env extends Map<IdentifierId, RefAccessType> {
class Env {
#changed = false;
#data: Map<IdentifierId, RefAccessType> = new Map();
#temporaries: Map<IdentifierId, Place> = new Map();
lookup(place: Place): Place {
return this.#temporaries.get(place.identifier.id) ?? place;
}
define(place: Place, value: Place): void {
this.#temporaries.set(place.identifier.id, value);
}
resetChanged(): void {
this.#changed = false;
@@ -86,8 +102,14 @@ class Env extends Map<IdentifierId, RefAccessType> {
return this.#changed;
}
override set(key: IdentifierId, value: RefAccessType): this {
const cur = this.get(key);
get(key: IdentifierId): RefAccessType | undefined {
const operandId = this.#temporaries.get(key)?.identifier.id ?? key;
return this.#data.get(operandId);
}
set(key: IdentifierId, value: RefAccessType): this {
const operandId = this.#temporaries.get(key)?.identifier.id ?? key;
const cur = this.#data.get(operandId);
const widenedValue = joinRefAccessTypes(value, cur ?? {kind: 'None'});
if (
!(cur == null && widenedValue.kind === 'None') &&
@@ -95,7 +117,8 @@ class Env extends Map<IdentifierId, RefAccessType> {
) {
this.#changed = true;
}
return super.set(key, widenedValue);
this.#data.set(operandId, widenedValue);
return this;
}
}
@@ -103,9 +126,48 @@ export function validateNoRefAccessInRender(
fn: HIRFunction,
): Result<void, CompilerError> {
const env = new Env();
collectTemporariesSidemap(fn, env);
return validateNoRefAccessInRenderImpl(fn, env).map(_ => undefined);
}
function collectTemporariesSidemap(fn: HIRFunction, env: Env): void {
for (const block of fn.body.blocks.values()) {
for (const instr of block.instructions) {
const {lvalue, value} = instr;
switch (value.kind) {
case 'LoadLocal': {
const temp = env.lookup(value.place);
if (temp != null) {
env.define(lvalue, temp);
}
break;
}
case 'StoreLocal': {
const temp = env.lookup(value.value);
if (temp != null) {
env.define(lvalue, temp);
env.define(value.lvalue.place, temp);
}
break;
}
case 'PropertyLoad': {
if (
isUseRefType(value.object.identifier) &&
value.property === 'current'
) {
continue;
}
const temp = env.lookup(value.object);
if (temp != null) {
env.define(lvalue, temp);
}
break;
}
}
}
}
}
function refTypeOfType(place: Place): RefAccessType {
if (isRefValueType(place.identifier)) {
return {kind: 'RefValue'};
@@ -258,12 +320,27 @@ function validateNoRefAccessInRenderImpl(
env.set(place.identifier.id, type);
}
const interpolatedAsJsx = new Set<IdentifierId>();
for (const block of fn.body.blocks.values()) {
for (const instr of block.instructions) {
const {value} = instr;
if (value.kind === 'JsxExpression' || value.kind === 'JsxFragment') {
if (value.children != null) {
for (const child of value.children) {
interpolatedAsJsx.add(child.identifier.id);
}
}
}
}
}
for (let i = 0; (i == 0 || env.hasChanged()) && i < 10; i++) {
env.resetChanged();
returnValues = [];
const safeBlocks = new Map<BlockId, RefId>();
const safeBlocks: Array<{block: BlockId; ref: RefId}> = [];
const errors = new CompilerError();
for (const [, block] of fn.body.blocks) {
retainWhere(safeBlocks, entry => entry.block !== block.id);
for (const phi of block.phis) {
env.set(
phi.place.identifier.id,
@@ -385,28 +462,80 @@ function validateNoRefAccessInRenderImpl(
const hookKind = getHookKindForType(fn.env, callee.identifier.type);
let returnType: RefAccessType = {kind: 'None'};
const fnType = env.get(callee.identifier.id);
let didError = false;
if (fnType?.kind === 'Structure' && fnType.fn !== null) {
returnType = fnType.fn.returnType;
if (fnType.fn.readRefEffect) {
errors.push({
severity: ErrorSeverity.InvalidReact,
reason:
'This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef)',
loc: callee.loc,
description:
callee.identifier.name !== null &&
callee.identifier.name.kind === 'named'
? `Function \`${callee.identifier.name.value}\` accesses a ref`
: null,
suggestions: null,
});
didError = true;
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Refs,
severity: ErrorSeverity.InvalidReact,
reason: 'Cannot access refs during render',
description: ERROR_DESCRIPTION,
}).withDetail({
kind: 'error',
loc: callee.loc,
message: `This function accesses a ref value`,
}),
);
}
}
for (const operand of eachInstructionValueOperand(instr.value)) {
if (hookKind != null) {
validateNoDirectRefValueAccess(errors, operand, env);
} else {
validateNoRefAccess(errors, env, operand, operand.loc);
/*
* If we already reported an error on this instruction, don't report
* duplicate errors
*/
if (!didError) {
const isRefLValue = isUseRefType(instr.lvalue.identifier);
for (const operand of eachInstructionValueOperand(instr.value)) {
/**
* By default we check that function call operands are not refs,
* ref values, or functions that can access refs.
*/
if (
isRefLValue ||
(hookKind != null &&
hookKind !== 'useState' &&
hookKind !== 'useReducer')
) {
/**
* Special cases:
*
* 1. the lvalue is a ref
* In general passing a ref to a function may access that ref
* value during render, so we disallow it.
*
* The main exception is the "mergeRefs" pattern, ie a function
* that accepts multiple refs as arguments (or an array of refs)
* and returns a new, aggregated ref. If the lvalue is a ref,
* we assume that the user is doing this pattern and allow passing
* refs.
*
* Eg `const mergedRef = mergeRefs(ref1, ref2)`
*
* 2. calling hooks
*
* Hooks are independently checked to ensure they don't access refs
* during render.
*/
validateNoDirectRefValueAccess(errors, operand, env);
} else if (interpolatedAsJsx.has(instr.lvalue.identifier.id)) {
/**
* Special case: the lvalue is passed as a jsx child
*
* For example `<Foo>{renderHelper(ref)}</Foo>`. Here we have more
* context and infer that the ref is being passed to a component-like
* render function which attempts to obey the rules.
*/
validateNoRefValueAccess(errors, env, operand);
} else {
validateNoRefPassedToFunction(
errors,
env,
operand,
operand.loc,
);
}
}
}
env.set(instr.lvalue.identifier.id, returnType);
@@ -439,23 +568,39 @@ function validateNoRefAccessInRenderImpl(
case 'PropertyStore':
case 'ComputedDelete':
case 'ComputedStore': {
const safe = safeBlocks.get(block.id);
const target = env.get(instr.value.object.identifier.id);
let safe: (typeof safeBlocks)['0'] | null | undefined = null;
if (
instr.value.kind === 'PropertyStore' &&
safe != null &&
target?.kind === 'Ref' &&
target.refId === safe
target != null &&
target.kind === 'Ref'
) {
safeBlocks.delete(block.id);
} else {
validateNoRefAccess(errors, env, instr.value.object, instr.loc);
safe = safeBlocks.find(entry => entry.ref === target.refId);
}
for (const operand of eachInstructionValueOperand(instr.value)) {
if (operand === instr.value.object) {
continue;
if (safe != null) {
retainWhere(safeBlocks, entry => entry !== safe);
} else {
validateNoRefUpdate(errors, env, instr.value.object, instr.loc);
}
if (
instr.value.kind === 'ComputedDelete' ||
instr.value.kind === 'ComputedStore'
) {
validateNoRefValueAccess(errors, env, instr.value.property);
}
if (
instr.value.kind === 'ComputedStore' ||
instr.value.kind === 'PropertyStore'
) {
validateNoDirectRefValueAccess(errors, instr.value.value, env);
const type = env.get(instr.value.value.identifier.id);
if (type != null && type.kind === 'Structure') {
let objectType: RefAccessType = type;
if (target != null) {
objectType = joinRefAccessTypes(objectType, target);
}
env.set(instr.value.object.identifier.id, objectType);
}
validateNoRefValueAccess(errors, env, operand);
}
break;
}
@@ -535,8 +680,11 @@ function validateNoRefAccessInRenderImpl(
if (block.terminal.kind === 'if') {
const test = env.get(block.terminal.test.identifier.id);
if (test?.kind === 'Guard') {
safeBlocks.set(block.terminal.consequent, test.refId);
if (
test?.kind === 'Guard' &&
safeBlocks.find(entry => entry.ref === test.refId) == null
) {
safeBlocks.push({block: block.terminal.fallthrough, ref: test.refId});
}
}
@@ -583,18 +731,18 @@ function destructure(
function guardCheck(errors: CompilerError, operand: Place, env: Env): void {
if (env.get(operand.identifier.id)?.kind === 'Guard') {
errors.push({
severity: ErrorSeverity.InvalidReact,
reason:
'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)',
loc: operand.loc,
description:
operand.identifier.name !== null &&
operand.identifier.name.kind === 'named'
? `Cannot access ref value \`${operand.identifier.name.value}\``
: null,
suggestions: null,
});
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Refs,
severity: ErrorSeverity.InvalidReact,
reason: 'Cannot access refs during render',
description: ERROR_DESCRIPTION,
}).withDetail({
kind: 'error',
loc: operand.loc,
message: `Cannot access ref value during render`,
}),
);
}
}
@@ -608,22 +756,22 @@ function validateNoRefValueAccess(
type?.kind === 'RefValue' ||
(type?.kind === 'Structure' && type.fn?.readRefEffect)
) {
errors.push({
severity: ErrorSeverity.InvalidReact,
reason:
'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)',
loc: (type.kind === 'RefValue' && type.loc) || operand.loc,
description:
operand.identifier.name !== null &&
operand.identifier.name.kind === 'named'
? `Cannot access ref value \`${operand.identifier.name.value}\``
: null,
suggestions: null,
});
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Refs,
severity: ErrorSeverity.InvalidReact,
reason: 'Cannot access refs during render',
description: ERROR_DESCRIPTION,
}).withDetail({
kind: 'error',
loc: (type.kind === 'RefValue' && type.loc) || operand.loc,
message: `Cannot access ref value during render`,
}),
);
}
}
function validateNoRefAccess(
function validateNoRefPassedToFunction(
errors: CompilerError,
env: Env,
operand: Place,
@@ -635,18 +783,41 @@ function validateNoRefAccess(
type?.kind === 'RefValue' ||
(type?.kind === 'Structure' && type.fn?.readRefEffect)
) {
errors.push({
severity: ErrorSeverity.InvalidReact,
reason:
'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)',
loc: (type.kind === 'RefValue' && type.loc) || loc,
description:
operand.identifier.name !== null &&
operand.identifier.name.kind === 'named'
? `Cannot access ref value \`${operand.identifier.name.value}\``
: null,
suggestions: null,
});
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Refs,
severity: ErrorSeverity.InvalidReact,
reason: 'Cannot access refs during render',
description: ERROR_DESCRIPTION,
}).withDetail({
kind: 'error',
loc: (type.kind === 'RefValue' && type.loc) || loc,
message: `Passing a ref to a function may read its value during render`,
}),
);
}
}
function validateNoRefUpdate(
errors: CompilerError,
env: Env,
operand: Place,
loc: SourceLocation,
): void {
const type = destructure(env.get(operand.identifier.id));
if (type?.kind === 'Ref' || type?.kind === 'RefValue') {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Refs,
severity: ErrorSeverity.InvalidReact,
reason: 'Cannot access refs during render',
description: ERROR_DESCRIPTION,
}).withDetail({
kind: 'error',
loc: (type.kind === 'RefValue' && type.loc) || loc,
message: `Cannot update ref during render`,
}),
);
}
}
@@ -657,17 +828,23 @@ function validateNoDirectRefValueAccess(
): void {
const type = destructure(env.get(operand.identifier.id));
if (type?.kind === 'RefValue') {
errors.push({
severity: ErrorSeverity.InvalidReact,
reason:
'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)',
loc: type.loc ?? operand.loc,
description:
operand.identifier.name !== null &&
operand.identifier.name.kind === 'named'
? `Cannot access ref value \`${operand.identifier.name.value}\``
: null,
suggestions: null,
});
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Refs,
severity: ErrorSeverity.InvalidReact,
reason: 'Cannot access refs during render',
description: ERROR_DESCRIPTION,
}).withDetail({
kind: 'error',
loc: type.loc ?? operand.loc,
message: `Cannot access ref value during render`,
}),
);
}
}
const ERROR_DESCRIPTION =
'React refs are values that are not needed for rendering. Refs should only be accessed ' +
'outside of render, such as in event handlers or effects. ' +
'Accessing a ref value (the `current` property) during render can cause your component ' +
'not to update as expected (https://react.dev/reference/react/useRef)';

View File

@@ -5,26 +5,33 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError, ErrorSeverity} from '../CompilerError';
import {
CompilerDiagnostic,
CompilerError,
ErrorCategory,
ErrorSeverity,
} from '../CompilerError';
import {
HIRFunction,
IdentifierId,
isSetStateType,
isUseEffectHookType,
isUseInsertionEffectHookType,
isUseLayoutEffectHookType,
Place,
} from '../HIR';
import {eachInstructionValueOperand} from '../HIR/visitors';
import {Result} from '../Utils/Result';
/**
* Validates against calling setState in the body of a *passive* effect (useEffect),
* Validates against calling setState in the body of an effect (useEffect and friends),
* while allowing calling setState in callbacks scheduled by the effect.
*
* Calling setState during execution of a useEffect triggers a re-render, which is
* often bad for performance and frequently has more efficient and straightforward
* alternatives. See https://react.dev/learn/you-might-not-need-an-effect for examples.
*/
export function validateNoSetStateInPassiveEffects(
export function validateNoSetStateInEffects(
fn: HIRFunction,
): Result<void, CompilerError> {
const setStateFunctions: Map<IdentifierId, Place> = new Map();
@@ -79,19 +86,36 @@ export function validateNoSetStateInPassiveEffects(
instr.value.kind === 'MethodCall'
? instr.value.receiver
: instr.value.callee;
if (isUseEffectHookType(callee.identifier)) {
if (
isUseEffectHookType(callee.identifier) ||
isUseLayoutEffectHookType(callee.identifier) ||
isUseInsertionEffectHookType(callee.identifier)
) {
const arg = instr.value.args[0];
if (arg !== undefined && arg.kind === 'Identifier') {
const setState = setStateFunctions.get(arg.identifier.id);
if (setState !== undefined) {
errors.push({
reason:
'Calling setState directly within a useEffect causes cascading renders and is not recommended. Consider alternatives to useEffect. (https://react.dev/learn/you-might-not-need-an-effect)',
description: null,
severity: ErrorSeverity.InvalidReact,
loc: setState.loc,
suggestions: null,
});
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.EffectSetState,
reason:
'Calling setState synchronously within an effect can trigger cascading renders',
description:
'Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. ' +
'In general, the body of an effect should do one or both of the following:\n' +
'* Update external systems with the latest state from React.\n' +
'* Subscribe for updates from some external system, calling setState in a callback function when external state changes.\n\n' +
'Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. ' +
'(https://react.dev/learn/you-might-not-need-an-effect)',
severity: ErrorSeverity.InvalidReact,
suggestions: null,
}).withDetail({
kind: 'error',
loc: setState.loc,
message:
'Avoid calling setState() directly within an effect',
}),
);
}
}
}

View File

@@ -5,7 +5,12 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError, ErrorSeverity} from '../CompilerError';
import {
CompilerDiagnostic,
CompilerError,
ErrorCategory,
ErrorSeverity,
} from '../CompilerError';
import {HIRFunction, IdentifierId, isSetStateType} from '../HIR';
import {computeUnconditionalBlocks} from '../HIR/ComputeUnconditionalBlocks';
import {eachInstructionValueOperand} from '../HIR/visitors';
@@ -122,23 +127,37 @@ function validateNoSetStateInRenderImpl(
unconditionalSetStateFunctions.has(callee.identifier.id)
) {
if (activeManualMemoId !== null) {
errors.push({
reason:
'Calling setState from useMemo may trigger an infinite loop. (https://react.dev/reference/react/useState)',
description: null,
severity: ErrorSeverity.InvalidReact,
loc: callee.loc,
suggestions: null,
});
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.RenderSetState,
reason:
'Calling setState from useMemo may trigger an infinite loop',
description:
'Each time the memo callback is evaluated it will change state. This can cause a memoization dependency to change, running the memo function again and causing an infinite loop. Instead of setting state in useMemo(), prefer deriving the value during render. (https://react.dev/reference/react/useState)',
severity: ErrorSeverity.InvalidReact,
suggestions: null,
}).withDetail({
kind: 'error',
loc: callee.loc,
message: 'Found setState() within useMemo()',
}),
);
} else if (unconditionalBlocks.has(block.id)) {
errors.push({
reason:
'This is an unconditional set state during render, which will trigger an infinite loop. (https://react.dev/reference/react/useState)',
description: null,
severity: ErrorSeverity.InvalidReact,
loc: callee.loc,
suggestions: null,
});
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.RenderSetState,
reason:
'Calling setState during render may trigger an infinite loop',
description:
'Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState)',
severity: ErrorSeverity.InvalidReact,
suggestions: null,
}).withDetail({
kind: 'error',
loc: callee.loc,
message: 'Found setState() in render',
}),
);
}
}
break;

View File

@@ -5,7 +5,12 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError, ErrorSeverity} from '../CompilerError';
import {
CompilerDiagnostic,
CompilerError,
ErrorCategory,
ErrorSeverity,
} from '../CompilerError';
import {
DeclarationId,
Effect,
@@ -275,27 +280,38 @@ function validateInferredDep(
errorDiagnostic = merge(errorDiagnostic ?? compareResult, compareResult);
}
}
errorState.push({
severity: ErrorSeverity.CannotPreserveMemoization,
reason:
'React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected',
description:
DEBUG ||
// If the dependency is a named variable then we can report it. Otherwise only print in debug mode
(dep.identifier.name != null && dep.identifier.name.kind === 'named')
? `The inferred dependency was \`${prettyPrintScopeDependency(
dep,
)}\`, but the source dependencies were [${validDepsInMemoBlock
.map(dep => printManualMemoDependency(dep, true))
.join(', ')}]. ${
errorDiagnostic
? getCompareDependencyResultDescription(errorDiagnostic)
: 'Inferred dependency not present in source'
}`
: null,
loc: memoLocation,
suggestions: null,
});
errorState.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.PreserveManualMemo,
severity: ErrorSeverity.CannotPreserveMemoization,
reason:
'Compilation skipped because existing memoization could not be preserved',
description: [
'React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. ',
'The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. ',
DEBUG ||
// If the dependency is a named variable then we can report it. Otherwise only print in debug mode
(dep.identifier.name != null && dep.identifier.name.kind === 'named')
? `The inferred dependency was \`${prettyPrintScopeDependency(
dep,
)}\`, but the source dependencies were [${validDepsInMemoBlock
.map(dep => printManualMemoDependency(dep, true))
.join(', ')}]. ${
errorDiagnostic
? getCompareDependencyResultDescription(errorDiagnostic)
: 'Inferred dependency not present in source'
}.`
: '',
]
.join('')
.trim(),
suggestions: null,
}).withDetail({
kind: 'error',
loc: memoLocation,
message: 'Could not preserve existing manual memoization',
}),
);
}
class Visitor extends ReactiveFunctionVisitor<VisitorState> {
@@ -445,11 +461,13 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
*/
this.recordTemporaries(instruction, state);
const value = instruction.value;
// Track reassignments from inlining of manual memo
if (
value.kind === 'StoreLocal' &&
value.lvalue.kind === 'Reassign' &&
state.manualMemoState != null
) {
// Complex cases of inlining end up with a temporary that is reassigned
const ids = getOrInsertDefault(
state.manualMemoState.reassignments,
value.lvalue.place.identifier.declarationId,
@@ -457,6 +475,21 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
);
ids.add(value.value.identifier);
}
if (
value.kind === 'LoadLocal' &&
value.place.identifier.scope != null &&
instruction.lvalue != null &&
instruction.lvalue.identifier.scope == null &&
state.manualMemoState != null
) {
// Simpler cases of inlining assign to the original IIFE lvalue
const ids = getOrInsertDefault(
state.manualMemoState.reassignments,
instruction.lvalue.identifier.declarationId,
new Set(),
);
ids.add(value.place.identifier);
}
if (value.kind === 'StartMemoize') {
let depsFromSource: Array<ManualMemoDependency> | null = null;
if (value.deps != null) {
@@ -502,14 +535,22 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
!this.scopes.has(identifier.scope.id) &&
!this.prunedScopes.has(identifier.scope.id)
) {
state.errors.push({
reason:
'React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This dependency may be mutated later, which could cause the value to change unexpectedly',
description: null,
severity: ErrorSeverity.CannotPreserveMemoization,
loc,
suggestions: null,
});
state.errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.PreserveManualMemo,
severity: ErrorSeverity.CannotPreserveMemoization,
reason:
'Compilation skipped because existing memoization could not be preserved',
description: [
'React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. ',
'This dependency may be mutated later, which could cause the value to change unexpectedly.',
].join(''),
}).withDetail({
kind: 'error',
loc,
message: 'This dependency may be modified later',
}),
);
}
}
}
@@ -543,16 +584,26 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
for (const identifier of decls) {
if (isUnmemoized(identifier, this.scopes)) {
state.errors.push({
reason:
'React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output.',
description: DEBUG
? `${printIdentifier(identifier)} was not memoized`
: null,
severity: ErrorSeverity.CannotPreserveMemoization,
loc,
suggestions: null,
});
state.errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.PreserveManualMemo,
severity: ErrorSeverity.CannotPreserveMemoization,
reason:
'Compilation skipped because existing memoization could not be preserved',
description: [
'React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output. ',
DEBUG
? `${printIdentifier(identifier)} was not memoized.`
: '',
]
.join('')
.trim(),
}).withDetail({
kind: 'error',
loc,
message: 'Could not preserve existing memoization',
}),
);
}
}
}

View File

@@ -5,7 +5,12 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError, ErrorSeverity} from '../CompilerError';
import {
CompilerDiagnostic,
CompilerError,
ErrorCategory,
ErrorSeverity,
} from '../CompilerError';
import {HIRFunction, IdentifierId, SourceLocation} from '../HIR';
import {Result} from '../Utils/Result';
@@ -59,20 +64,24 @@ export function validateStaticComponents(
value.tag.identifier.id,
);
if (location != null) {
error.push({
reason: `Components created during render will reset their state each time they are created. Declare components outside of render. `,
severity: ErrorSeverity.InvalidReact,
loc: value.tag.loc,
description: null,
suggestions: null,
});
error.push({
reason: `The component may be created during render`,
severity: ErrorSeverity.InvalidReact,
loc: location,
description: null,
suggestions: null,
});
error.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.StaticComponents,
severity: ErrorSeverity.InvalidReact,
reason: 'Cannot create components during render',
description: `Components created during render will reset their state each time they are created. Declare components outside of render. `,
})
.withDetail({
kind: 'error',
loc: value.tag.loc,
message: 'This component is created during render',
})
.withDetail({
kind: 'error',
loc: location,
message: 'The component is created during render here',
}),
);
}
}
}

View File

@@ -5,7 +5,12 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError, ErrorSeverity} from '..';
import {
CompilerDiagnostic,
CompilerError,
ErrorCategory,
ErrorSeverity,
} from '../CompilerError';
import {FunctionExpression, HIRFunction, IdentifierId} from '../HIR';
import {Result} from '../Utils/Result';
@@ -63,24 +68,43 @@ export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
}
if (body.loweredFunc.func.params.length > 0) {
errors.push({
severity: ErrorSeverity.InvalidReact,
reason: 'useMemo callbacks may not accept any arguments',
description: null,
loc: body.loc,
suggestions: null,
});
const firstParam = body.loweredFunc.func.params[0];
const loc =
firstParam.kind === 'Identifier'
? firstParam.loc
: firstParam.place.loc;
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
severity: ErrorSeverity.InvalidReact,
reason: 'useMemo() callbacks may not accept parameters',
description:
'useMemo() callbacks are called by React to cache calculations across re-renders. They should not take parameters. Instead, directly reference the props, state, or local variables needed for the computation.',
suggestions: null,
}).withDetail({
kind: 'error',
loc,
message: 'Callbacks with parameters are not supported',
}),
);
}
if (body.loweredFunc.func.async || body.loweredFunc.func.generator) {
errors.push({
severity: ErrorSeverity.InvalidReact,
reason:
'useMemo callbacks may not be async or generator functions',
description: null,
loc: body.loc,
suggestions: null,
});
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
severity: ErrorSeverity.InvalidReact,
reason:
'useMemo() callbacks may not be async or generator functions',
description:
'useMemo() callbacks are called once and must synchronously return a value.',
suggestions: null,
}).withDetail({
kind: 'error',
loc: body.loc,
message: 'Async and generator functions are not supported',
}),
);
}
break;

View File

@@ -9,7 +9,7 @@ export {validateContextVariableLValues} from './ValidateContextVariableLValues';
export {validateHooksUsage} from './ValidateHooksUsage';
export {validateMemoizedEffectDependencies} from './ValidateMemoizedEffectDependencies';
export {validateNoCapitalizedCalls} from './ValidateNoCapitalizedCalls';
export {validateNoRefAccessInRender} from './ValidateNoRefAccesInRender';
export {validateNoRefAccessInRender} from './ValidateNoRefAccessInRender';
export {validateNoSetStateInRender} from './ValidateNoSetStateInRender';
export {validatePreservedManualMemoization} from './ValidatePreservedManualMemoization';
export {validateUseMemo} from './ValidateUseMemo';

View File

@@ -58,7 +58,8 @@ it('logs failed compilation', () => {
expect(event.detail.severity).toEqual('InvalidReact');
//@ts-ignore
const {start, end, identifierName} = event.detail.loc as t.SourceLocation;
const {start, end, identifierName} =
event.detail.primaryLocation() as t.SourceLocation;
expect(start).toEqual({column: 28, index: 28, line: 1});
expect(end).toEqual({column: 33, index: 33, line: 1});
expect(identifierName).toEqual('props');

View File

@@ -20,7 +20,7 @@ describe('parseConfigPragma()', () => {
validateHooksUsage: 1,
} as any);
}).toThrowErrorMatchingInlineSnapshot(
`"InvalidConfig: Could not validate environment config. Update React Compiler config to fix the error. Validation error: Expected boolean, received number at "validateHooksUsage""`,
`"Error: Could not validate environment config. Update React Compiler config to fix the error. Validation error: Expected boolean, received number at "validateHooksUsage"."`,
);
});
@@ -33,12 +33,12 @@ describe('parseConfigPragma()', () => {
source: 'react',
importSpecifierName: 'useEffect',
},
numRequiredArgs: 0,
autodepsIndex: 0,
},
],
} as any);
}).toThrowErrorMatchingInlineSnapshot(
`"InvalidConfig: Could not validate environment config. Update React Compiler config to fix the error. Validation error: numRequiredArgs must be > 0 at "inferEffectDependencies[0].numRequiredArgs""`,
`"Error: Could not validate environment config. Update React Compiler config to fix the error. Validation error: autodepsIndex must be > 0 at "inferEffectDependencies[0].autodepsIndex"."`,
);
});

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