Compare commits

..

77 Commits

Author SHA1 Message Date
Joe Savona
2145ea7b59 [compiler] Remove now-unused FunctionEffect type
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:23:06 -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
232 changed files with 11614 additions and 6327 deletions

View File

@@ -58,11 +58,15 @@ export type CompilerDiagnosticDetail =
/**
* A/the source of the error
*/
{
kind: 'error';
loc: SourceLocation | null;
message: string;
};
| {
kind: 'error';
loc: SourceLocation | null;
message: string;
}
| {
kind: 'hint';
message: string;
};
export enum CompilerSuggestionOperation {
InsertBefore,
@@ -134,7 +138,12 @@ export class CompilerDiagnostic {
}
primaryLocation(): SourceLocation | null {
return this.options.details.filter(d => d.kind === 'error')[0]?.loc ?? 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 {
@@ -167,9 +176,14 @@ export class CompilerDiagnostic {
buffer.push(codeFrame);
break;
}
case 'hint': {
buffer.push('\n\n');
buffer.push(detail.message);
break;
}
default: {
assertExhaustive(
detail.kind,
detail,
`Unexpected detail kind ${(detail as any).kind}`,
);
}

View File

@@ -33,9 +33,7 @@ import {findContextIdentifiers} from '../HIR/FindContextIdentifiers';
import {
analyseFunctions,
dropManualMemoization,
inferMutableRanges,
inferReactivePlaces,
inferReferenceEffects,
inlineImmediatelyInvokedFunctionExpressions,
inferEffectDependencies,
} from '../Inference';
@@ -92,7 +90,6 @@ import {
} from '../Validation';
import {validateLocalsNotReassignedAfterRender} from '../Validation/ValidateLocalsNotReassignedAfterRender';
import {outlineFunctions} from '../Optimization/OutlineFunctions';
import {propagatePhiTypes} from '../TypeInference/PropagatePhiTypes';
import {lowerContextAccess} from '../Optimization/LowerContextAccess';
import {validateNoSetStateInEffects} from '../Validation/ValidateNoSetStateInEffects';
import {validateNoJSXInTryStatement} from '../Validation/ValidateNoJSXInTryStatement';
@@ -101,11 +98,11 @@ 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}
@@ -229,26 +226,12 @@ function runWithEnvironment(
analyseFunctions(hir);
log({kind: 'hir', name: 'AnalyseFunctions', value: hir});
if (!env.config.enableNewMutationAliasingModel) {
const fnEffectErrors = inferReferenceEffects(hir);
if (env.isInferredMemoEnabled) {
if (fnEffectErrors.length > 0) {
CompilerError.throw(fnEffectErrors[0]);
}
const mutabilityAliasingErrors = inferMutationAliasingEffects(hir);
log({kind: 'hir', name: 'InferMutationAliasingEffects', value: hir});
if (env.isInferredMemoEnabled) {
if (mutabilityAliasingErrors.isErr()) {
throw mutabilityAliasingErrors.unwrapErr();
}
log({kind: 'hir', name: 'InferReferenceEffects', value: hir});
} else {
const mutabilityAliasingErrors = inferMutationAliasingEffects(hir);
log({kind: 'hir', name: 'InferMutationAliasingEffects', value: hir});
if (env.isInferredMemoEnabled) {
if (mutabilityAliasingErrors.isErr()) {
throw mutabilityAliasingErrors.unwrapErr();
}
}
}
if (!env.config.enableNewMutationAliasingModel) {
validateLocalsNotReassignedAfterRender(hir);
}
// Note: Has to come after infer reference effects because "dead" code may still affect inference
@@ -263,20 +246,15 @@ function runWithEnvironment(
pruneMaybeThrows(hir);
log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
if (!env.config.enableNewMutationAliasingModel) {
inferMutableRanges(hir);
log({kind: 'hir', name: 'InferMutableRanges', value: hir});
} else {
const mutabilityAliasingErrors = inferMutationAliasingRanges(hir, {
isFunctionExpression: false,
});
log({kind: 'hir', name: 'InferMutationAliasingRanges', value: hir});
if (env.isInferredMemoEnabled) {
if (mutabilityAliasingErrors.isErr()) {
throw mutabilityAliasingErrors.unwrapErr();
}
validateLocalsNotReassignedAfterRender(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) {
@@ -292,6 +270,10 @@ function runWithEnvironment(
validateNoSetStateInRender(hir).unwrap();
}
if (env.config.validateNoDerivedComputationsInEffects) {
validateNoDerivedComputationsInEffects(hir);
}
if (env.config.validateNoSetStateInEffects) {
env.logErrors(validateNoSetStateInEffects(hir));
}
@@ -304,12 +286,7 @@ function runWithEnvironment(
validateNoImpureFunctionsInRender(hir).unwrap();
}
if (
env.config.validateNoFreezingKnownMutableFunctions ||
env.config.enableNewMutationAliasingModel
) {
validateNoFreezingKnownMutableFunctions(hir).unwrap();
}
validateNoFreezingKnownMutableFunctions(hir).unwrap();
}
inferReactivePlaces(hir);
@@ -322,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

@@ -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

@@ -49,6 +49,7 @@ import {
} from './ObjectShape';
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([
@@ -244,9 +245,10 @@ export const EnvironmentConfigSchema = z.object({
enableUseTypeAnnotations: z.boolean().default(false),
/**
* Enable a new model for mutability and aliasing inference
* Allows specifying a function that can populate HIR with type information from
* Flow
*/
enableNewMutationAliasingModel: z.boolean().default(true),
flowTypeProvider: z.nullable(z.function().args(z.string())).default(null),
/**
* Enables inference of optional dependency chains. Without this flag
@@ -323,6 +325,12 @@ export const EnvironmentConfigSchema = z.object({
*/
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
* instead.
@@ -691,6 +699,8 @@ export class Environment {
#hoistedIdentifiers: Set<t.Identifier>;
parentFunction: NodePath<t.Function>;
#flowTypeEnvironment: FlowTypeEnv | null;
constructor(
scope: BabelScope,
fnType: ReactFunctionType,
@@ -759,6 +769,26 @@ export class Environment {
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

@@ -114,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',
},
],
},
}),
],
]),
],
[

View File

@@ -7,13 +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';
/*
* *******************************************************************************************
@@ -281,30 +282,13 @@ export type HIRFunction = {
returnTypeAnnotation: t.FlowType | t.TSType | null;
returns: Place;
context: Array<Place>;
effects: Array<FunctionEffect> | null;
body: HIR;
generator: boolean;
async: boolean;
directives: Array<string>;
aliasingEffects?: Array<AliasingEffect> | null;
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
@@ -1320,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,

View File

@@ -142,6 +142,7 @@ function parseAliasingSignatureConfig(
const effects = typeConfig.effects.map(
(effect: AliasingEffectConfig): AliasingEffect => {
switch (effect.kind) {
case 'ImmutableCapture':
case 'CreateFrom':
case 'Capture':
case 'Alias':

View File

@@ -554,23 +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 aliasingEffects =
instrValue.loweredFunc.func.aliasingEffects
?.map(printAliasingEffect)
?.join(', ') ?? '';
value = `${kind} ${name} @context[${context}] @effects[${effects}] @aliasingEffects=[${aliasingEffects}]\n${fn}`;
value = `${kind} ${name} @context[${context}] @aliasingEffects=[${aliasingEffects}]\n${fn}`;
break;
}
case 'TaggedTemplateExpression': {

View File

@@ -111,6 +111,19 @@ export const AliasEffectSchema: z.ZodType<AliasEffectConfig> = z.object({
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;
@@ -187,6 +200,7 @@ export type AliasingEffectConfig =
| AssignEffectConfig
| AliasEffectConfig
| CaptureEffectConfig
| ImmutableCaptureEffectConfig
| ImpureEffectConfig
| MutateEffectConfig
| MutateTransitiveConditionallyConfig
@@ -199,6 +213,7 @@ export const AliasingEffectSchema: z.ZodType<AliasingEffectConfig> = z.union([
AssignEffectSchema,
AliasEffectSchema,
CaptureEffectSchema,
ImmutableCaptureEffectSchema,
ImpureEffectSchema,
MutateEffectSchema,
MutateTransitiveConditionallySchema,

View File

@@ -50,7 +50,7 @@ export type AliasingEffect =
/**
* Mutate the value and any direct aliases (not captures). Errors if the value is not mutable.
*/
| {kind: 'Mutate'; value: Place}
| {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.
@@ -174,6 +174,8 @@ export type AliasingEffect =
place: Place;
};
export type MutationReason = {kind: 'AssignCurrentProperty'};
export function hashEffect(effect: AliasingEffect): string {
switch (effect.kind) {
case 'Apply': {

View File

@@ -6,20 +6,10 @@
*/
import {CompilerError} from '../CompilerError';
import {
Effect,
HIRFunction,
Identifier,
IdentifierId,
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';
@@ -30,12 +20,7 @@ export default function analyseFunctions(func: HIRFunction): void {
switch (instr.value.kind) {
case 'ObjectMethod':
case 'FunctionExpression': {
if (!func.env.config.enableNewMutationAliasingModel) {
lower(instr.value.loweredFunc.func);
infer(instr.value.loweredFunc);
} else {
lowerWithMutationAliasing(instr.value.loweredFunc.func);
}
lowerWithMutationAliasing(instr.value.loweredFunc.func);
/**
* Reset mutable range for outer inferReferenceEffects
@@ -140,58 +125,3 @@ function lowerWithMutationAliasing(fn: HIRFunction): void {
value: fn,
});
}
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 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)
*/
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;
}

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

@@ -1,351 +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';
}
export function getWriteErrorReason(abstractValue: AbstractValue): string {
if (abstractValue.reason.has(ValueReason.Global)) {
return 'Modifying a variable defined outside a component or hook is not allowed. Consider using an effect';
} else if (abstractValue.reason.has(ValueReason.JsxCaptured)) {
return 'Modifying a value used previously in JSX is not allowed. Consider moving the modification before the JSX';
} else if (abstractValue.reason.has(ValueReason.Context)) {
return `Modifying a value returned from 'useContext()' is not allowed.`;
} else if (abstractValue.reason.has(ValueReason.KnownReturnSignature)) {
return 'Modifying a value returned from a function whose return value should not be mutated';
} else if (abstractValue.reason.has(ValueReason.ReactiveFunctionArgument)) {
return 'Modifying component props or hook arguments is not allowed. Consider using a local variable instead';
} else if (abstractValue.reason.has(ValueReason.State)) {
return "Modifying a value returned from 'useState()', which should not be modified directly. Use the setter function to update instead";
} else if (abstractValue.reason.has(ValueReason.ReducerState)) {
return "Modifying a value returned from 'useReducer()', which should not be modified directly. Use the dispatch function to update instead";
} else if (abstractValue.reason.has(ValueReason.Effect)) {
return 'Modifying a value used previously in an effect function or as an effect dependency is not allowed. Consider moving the modification before calling useEffect()';
} else if (abstractValue.reason.has(ValueReason.HookCaptured)) {
return 'Modifying a value previously passed as an argument to a hook is not allowed. Consider moving the modification before calling the hook';
} else if (abstractValue.reason.has(ValueReason.HookReturn)) {
return 'Modifying a value returned from a hook is not allowed. Consider moving the modification into the hook where the value is constructed';
} else {
return 'This modifies 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, U>(a: Map<T, U>, b: Map<T, U>): 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

@@ -19,6 +19,7 @@ import {
DeclarationId,
Environment,
FunctionExpression,
GeneratedSource,
HIRFunction,
Hole,
IdentifierId,
@@ -34,6 +35,7 @@ import {
Phi,
Place,
SpreadPattern,
Type,
ValueReason,
} from '../HIR';
import {
@@ -43,12 +45,6 @@ import {
eachTerminalSuccessor,
} from '../HIR/visitors';
import {Ok, Result} from '../Utils/Result';
import {
getArgumentEffect,
getFunctionCallSignature,
isKnownMutableEffect,
mergeValueKinds,
} from './InferReferenceEffects';
import {
assertExhaustive,
getOrInsertDefault,
@@ -65,10 +61,14 @@ import {
printSourceLocation,
} from '../HIR/PrintHIR';
import {FunctionSignature} from '../HIR/ObjectShape';
import {getWriteErrorReason} from './InferFunctionEffects';
import prettyFormat from 'pretty-format';
import {createTemporaryPlace} from '../HIR/HIRBuilder';
import {AliasingEffect, AliasingSignature, hashEffect} from './AliasingEffects';
import {
AliasingEffect,
AliasingSignature,
hashEffect,
MutationReason,
} from './AliasingEffects';
const DEBUG = false;
@@ -445,25 +445,34 @@ function applySignature(
const reason = getWriteErrorReason({
kind: value.kind,
reason: value.reason,
context: new Set(),
});
const variable =
effect.value.identifier.name !== null &&
effect.value.identifier.name.kind === 'named'
? `\`${effect.value.identifier.name.value}\``
: 'value';
const diagnostic = CompilerDiagnostic.create({
severity: ErrorSeverity.InvalidReact,
category: 'This value cannot be modified',
description: `${reason}.`,
}).withDetail({
kind: 'error',
loc: effect.value.loc,
message: `${variable} cannot be modified`,
});
if (
effect.kind === 'Mutate' &&
effect.reason?.kind === 'AssignCurrentProperty'
) {
diagnostic.withDetail({
kind: 'hint',
message: `Hint: If this value is a Ref (value returned by \`useRef()\`), rename the variable to end in "Ref".`,
});
}
effects.push({
kind: 'MutateFrozen',
place: effect.value,
error: CompilerDiagnostic.create({
severity: ErrorSeverity.InvalidReact,
category: 'This value cannot be modified',
description: `${reason}.`,
}).withDetail({
kind: 'error',
loc: effect.value.loc,
message: `${variable} cannot be modified`,
}),
error: diagnostic,
});
}
}
@@ -1059,13 +1068,30 @@ function applyEffect(
const reason = getWriteErrorReason({
kind: value.kind,
reason: value.reason,
context: new Set(),
});
const variable =
effect.value.identifier.name !== null &&
effect.value.identifier.name.kind === 'named'
? `\`${effect.value.identifier.name.value}\``
: 'value';
const diagnostic = CompilerDiagnostic.create({
severity: ErrorSeverity.InvalidReact,
category: 'This value cannot be modified',
description: `${reason}.`,
}).withDetail({
kind: 'error',
loc: effect.value.loc,
message: `${variable} cannot be modified`,
});
if (
effect.kind === 'Mutate' &&
effect.reason?.kind === 'AssignCurrentProperty'
) {
diagnostic.withDetail({
kind: 'hint',
message: `Hint: If this value is a Ref (value returned by \`useRef()\`), rename the variable to end in "Ref".`,
});
}
applyEffect(
context,
state,
@@ -1075,15 +1101,7 @@ function applyEffect(
? 'MutateFrozen'
: 'MutateGlobal',
place: effect.value,
error: CompilerDiagnostic.create({
severity: ErrorSeverity.InvalidReact,
category: 'This value cannot be modified',
description: `${reason}.`,
}).withDetail({
kind: 'error',
loc: effect.value.loc,
message: `${variable} cannot be modified`,
}),
error: diagnostic,
},
initialized,
effects,
@@ -1680,7 +1698,15 @@ function computeSignatureForInstruction(
}
case 'PropertyStore':
case 'ComputedStore': {
effects.push({kind: 'Mutate', value: value.object});
const mutationReason: MutationReason | null =
value.kind === 'PropertyStore' && value.property === 'current'
? {kind: 'AssignCurrentProperty'}
: null;
effects.push({
kind: 'Mutate',
value: value.object,
reason: mutationReason,
});
effects.push({
kind: 'Capture',
from: value.value,
@@ -2534,3 +2560,196 @@ export type AbstractValue = {
kind: ValueKind;
reason: ReadonlySet<ValueReason>;
};
export function getWriteErrorReason(abstractValue: AbstractValue): string {
if (abstractValue.reason.has(ValueReason.Global)) {
return 'Modifying a variable defined outside a component or hook is not allowed. Consider using an effect';
} else if (abstractValue.reason.has(ValueReason.JsxCaptured)) {
return 'Modifying a value used previously in JSX is not allowed. Consider moving the modification before the JSX';
} else if (abstractValue.reason.has(ValueReason.Context)) {
return `Modifying a value returned from 'useContext()' is not allowed.`;
} else if (abstractValue.reason.has(ValueReason.KnownReturnSignature)) {
return 'Modifying a value returned from a function whose return value should not be mutated';
} else if (abstractValue.reason.has(ValueReason.ReactiveFunctionArgument)) {
return 'Modifying component props or hook arguments is not allowed. Consider using a local variable instead';
} else if (abstractValue.reason.has(ValueReason.State)) {
return "Modifying a value returned from 'useState()', which should not be modified directly. Use the setter function to update instead";
} else if (abstractValue.reason.has(ValueReason.ReducerState)) {
return "Modifying a value returned from 'useReducer()', which should not be modified directly. Use the dispatch function to update instead";
} else if (abstractValue.reason.has(ValueReason.Effect)) {
return 'Modifying a value used previously in an effect function or as an effect dependency is not allowed. Consider moving the modification before calling useEffect()';
} else if (abstractValue.reason.has(ValueReason.HookCaptured)) {
return 'Modifying a value previously passed as an argument to a hook is not allowed. Consider moving the modification before calling the hook';
} else if (abstractValue.reason.has(ValueReason.HookReturn)) {
return 'Modifying a value returned from a hook is not allowed. Consider moving the modification into the hook where the value is constructed';
} else {
return 'This modifies a variable that React considers immutable';
}
}
function getArgumentEffect(
signatureEffect: Effect | null,
arg: Place | SpreadPattern,
): Effect {
if (signatureEffect != null) {
if (arg.kind === 'Identifier') {
return signatureEffect;
} else if (
signatureEffect === Effect.Mutate ||
signatureEffect === Effect.ConditionallyMutate
) {
return signatureEffect;
} else {
// see call-spread-argument-mutable-iterator test fixture
if (signatureEffect === Effect.Freeze) {
CompilerError.throwTodo({
reason: 'Support spread syntax for hook arguments',
loc: arg.place.loc,
});
}
// effects[i] is Effect.Capture | Effect.Read | Effect.Store
return Effect.ConditionallyMutateIterator;
}
} else {
return Effect.ConditionallyMutate;
}
}
export function getFunctionCallSignature(
env: Environment,
type: Type,
): FunctionSignature | null {
if (type.kind !== 'Function') {
return null;
}
return env.getFunctionSignature(type);
}
export function isKnownMutableEffect(effect: Effect): boolean {
switch (effect) {
case Effect.Store:
case Effect.ConditionallyMutate:
case Effect.ConditionallyMutateIterator:
case Effect.Mutate: {
return true;
}
case Effect.Unknown: {
CompilerError.invariant(false, {
reason: 'Unexpected unknown effect',
description: null,
loc: GeneratedSource,
suggestions: null,
});
}
case Effect.Read:
case Effect.Capture:
case Effect.Freeze: {
return false;
}
default: {
assertExhaustive(effect, `Unexpected effect \`${effect}\``);
}
}
}
/**
* Joins two values using the following rules:
* == Effect Transitions ==
*
* Freezing an immutable value has not effect:
* ┌───────────────┐
* │ │
* ▼ │ Freeze
* ┌──────────────────────────┐ │
* │ Immutable │──┘
* └──────────────────────────┘
*
* Freezing a mutable or maybe-frozen value makes it frozen. Freezing a frozen
* value has no effect:
* ┌───────────────┐
* ┌─────────────────────────┐ Freeze │ │
* │ MaybeFrozen │────┐ ▼ │ Freeze
* └─────────────────────────┘ │ ┌──────────────────────────┐ │
* ├────▶│ Frozen │──┘
* │ └──────────────────────────┘
* ┌─────────────────────────┐ │
* │ Mutable │────┘
* └─────────────────────────┘
*
* == Join Lattice ==
* - immutable | mutable => mutable
* The justification is that immutable and mutable values are different types,
* and functions can introspect them to tell the difference (if the argument
* is null return early, else if its an object mutate it).
* - frozen | mutable => maybe-frozen
* Frozen values are indistinguishable from mutable values at runtime, so callers
* cannot dynamically avoid mutation of "frozen" values. If a value could be
* frozen we have to distinguish it from a mutable value. But it also isn't known
* frozen yet, so we distinguish as maybe-frozen.
* - immutable | frozen => frozen
* This is subtle and falls out of the above rules. If a value could be any of
* immutable, mutable, or frozen, then at runtime it could either be a primitive
* or a reference type, and callers can't distinguish frozen or not for reference
* types. To ensure that any sequence of joins btw those three states yields the
* correct maybe-frozen, these two have to produce a frozen value.
* - <any> | maybe-frozen => maybe-frozen
* - immutable | context => context
* - mutable | context => context
* - frozen | context => maybe-frozen
*
* ┌──────────────────────────┐
* │ Immutable │───┐
* └──────────────────────────┘ │
* │ ┌─────────────────────────┐
* ├───▶│ Frozen │──┐
* ┌──────────────────────────┐ │ └─────────────────────────┘ │
* │ Frozen │───┤ │ ┌─────────────────────────┐
* └──────────────────────────┘ │ ├─▶│ MaybeFrozen │
* │ ┌─────────────────────────┐ │ └─────────────────────────┘
* ├───▶│ MaybeFrozen │──┘
* ┌──────────────────────────┐ │ └─────────────────────────┘
* │ Mutable │───┘
* └──────────────────────────┘
*/
function mergeValueKinds(a: ValueKind, b: ValueKind): ValueKind {
if (a === b) {
return a;
} else if (a === ValueKind.MaybeFrozen || b === ValueKind.MaybeFrozen) {
return ValueKind.MaybeFrozen;
// after this a and b differ and neither are MaybeFrozen
} else if (a === ValueKind.Mutable || b === ValueKind.Mutable) {
if (a === ValueKind.Frozen || b === ValueKind.Frozen) {
// frozen | mutable
return ValueKind.MaybeFrozen;
} else if (a === ValueKind.Context || b === ValueKind.Context) {
// context | mutable
return ValueKind.Context;
} else {
// mutable | immutable
return ValueKind.Mutable;
}
} else if (a === ValueKind.Context || b === ValueKind.Context) {
if (a === ValueKind.Frozen || b === ValueKind.Frozen) {
// frozen | context
return ValueKind.MaybeFrozen;
} else {
// context | immutable
return ValueKind.Context;
}
} else if (a === ValueKind.Frozen || b === ValueKind.Frozen) {
return ValueKind.Frozen;
} else if (a === ValueKind.Global || b === ValueKind.Global) {
return ValueKind.Global;
} else {
CompilerError.invariant(
a === ValueKind.Primitive && b == ValueKind.Primitive,
{
reason: `Unexpected value kind in mergeValues()`,
description: `Found kinds ${a} and ${b}`,
loc: GeneratedSource,
},
);
return ValueKind.Primitive;
}
}

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

@@ -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

@@ -255,7 +255,6 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
returnTypeAnnotation: null,
returns: createTemporaryPlace(env, GeneratedSource),
context: [],
effects: null,
body: {
entry: block.id,
blocks: new Map([[block.id, block]]),
@@ -263,6 +262,7 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
generator: false,
async: false,
directives: [],
aliasingEffects: [],
};
reversePostorderBlocks(fn.body);

View File

@@ -370,7 +370,6 @@ function emitOutlinedFn(
returnTypeAnnotation: null,
returns: createTemporaryPlace(env, GeneratedSource),
context: [],
effects: null,
body: {
entry: block.id,
blocks: new Map([[block.id, block]]),
@@ -378,6 +377,7 @@ function emitOutlinedFn(
generator: false,
async: false,
directives: [],
aliasingEffects: [],
};
return fn;
}

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 (
@@ -465,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),
),
))
) {
@@ -477,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.

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

@@ -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;
}
}

View File

@@ -12,7 +12,7 @@ import {
eachInstructionValueOperand,
eachTerminalOperand,
} from '../HIR/visitors';
import {getFunctionCallSignature} from '../Inference/InferReferenceEffects';
import {getFunctionCallSignature} from '../Inference/InferMutationAliasingEffects';
/**
* Validates that local variables cannot be reassigned after render.

View File

@@ -0,0 +1,230 @@
/**
* 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 {
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({
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

@@ -7,10 +7,8 @@
import {CompilerDiagnostic, CompilerError, Effect, ErrorSeverity} from '..';
import {
FunctionEffect,
HIRFunction,
IdentifierId,
isMutableEffect,
isRefOrRefLikeMutableType,
Place,
} from '../HIR';
@@ -18,8 +16,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,14 +48,14 @@ 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) {
const place = [...effect.places][0];
const place = effect.value;
const variable =
place != null &&
place.identifier.name != null &&
@@ -77,7 +75,7 @@ export function validateNoFreezingKnownMutableFunctions(
})
.withDetail({
kind: 'error',
loc: effect.loc,
loc: effect.value.loc,
message: `This modifies ${variable}`,
}),
);
@@ -108,27 +106,7 @@ 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 (knownMutation && knownMutation.kind === 'ContextMutation') {
contextMutationEffects.set(lvalue.identifier.id, knownMutation);
} else if (
fn.env.config.enableNewMutationAliasingModel &&
value.loweredFunc.func.aliasingEffects != null
) {
if (value.loweredFunc.func.aliasingEffects != null) {
const context = new Set(
value.loweredFunc.func.context.map(p => p.identifier.id),
);
@@ -149,12 +127,7 @@ export function validateNoFreezingKnownMutableFunctions(
context.has(effect.value.identifier.id) &&
!isRefOrRefLikeMutableType(effect.value.identifier.type)
) {
contextMutationEffects.set(lvalue.identifier.id, {
kind: 'ContextMutation',
effect: Effect.Mutate,
loc: effect.value.loc,
places: new Set([effect.value]),
});
contextMutationEffects.set(lvalue.identifier.id, effect);
break effects;
}
break;

View File

@@ -7,7 +7,7 @@
import {CompilerDiagnostic, CompilerError, ErrorSeverity} from '..';
import {HIRFunction} from '../HIR';
import {getFunctionCallSignature} from '../Inference/InferReferenceEffects';
import {getFunctionCallSignature} from '../Inference/InferMutationAliasingEffects';
import {Result} from '../Utils/Result';
/**

View File

@@ -0,0 +1,39 @@
## Input
```javascript
// @enableNewMutationAliasingModel:false
function Component() {
const foo = () => {
someGlobal = true;
};
// spreading a function is weird, but it doesn't call the function so this is allowed
return <div {...foo} />;
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel:false
function Component() {
const $ = _c(1);
const foo = _temp;
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = <div {...foo} />;
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}
function _temp() {
someGlobal = true;
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -3,5 +3,6 @@ function Component() {
const foo = () => {
someGlobal = true;
};
// spreading a function is weird, but it doesn't call the function so this is allowed
return <div {...foo} />;
}

View File

@@ -19,7 +19,7 @@ function Component(props) {
```javascript
import { c as _c } from "react/compiler-runtime";
function Component(props) {
const $ = _c(7);
const $ = _c(5);
let t0;
if ($[0] !== props.x) {
t0 = foo(props.x);
@@ -31,26 +31,19 @@ function Component(props) {
const x = t0;
let t1;
if ($[2] !== props || $[3] !== x) {
t1 = function () {
const fn = function () {
const arr = [...bar(props)];
return arr.at(x);
};
t1 = fn();
$[2] = props;
$[3] = x;
$[4] = t1;
} else {
t1 = $[4];
}
const fn = t1;
let t2;
if ($[5] !== fn) {
t2 = fn();
$[5] = fn;
$[6] = t2;
} else {
t2 = $[6];
}
const fnResult = t2;
const fnResult = t1;
return fnResult;
}

View File

@@ -23,34 +23,18 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
function Component(props) {
const $ = _c(6);
const $ = _c(2);
let t0;
if ($[0] !== props.a) {
t0 = { a: props.a };
const item = { a: props.a };
const items = [item];
t0 = items.map(_temp);
$[0] = props.a;
$[1] = t0;
} else {
t0 = $[1];
}
const item = t0;
let t1;
if ($[2] !== item) {
t1 = [item];
$[2] = item;
$[3] = t1;
} else {
t1 = $[3];
}
const items = t1;
let t2;
if ($[4] !== items) {
t2 = items.map(_temp);
$[4] = items;
$[5] = t2;
} else {
t2 = $[5];
}
const mapped = t2;
const mapped = t0;
return mapped;
}
function _temp(item_0) {

View File

@@ -21,26 +21,18 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
function Component(props) {
const $ = _c(4);
const $ = _c(2);
const f = _temp;
let t0;
if ($[0] !== props.items) {
t0 = [...props.items].map(f);
const x = [...props.items].map(f);
t0 = [x, f];
$[0] = props.items;
$[1] = t0;
} else {
t0 = $[1];
}
const x = t0;
let t1;
if ($[2] !== x) {
t1 = [x, f];
$[2] = x;
$[3] = t1;
} else {
t1 = $[3];
}
return t1;
return t0;
}
function _temp(item) {
return item;

View File

@@ -1,107 +0,0 @@
## Input
```javascript
// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel:false
import {arrayPush, setPropertyByKey, Stringify} from 'shared-runtime';
/**
* 1. `InferMutableRanges` derives the mutable range of identifiers and their
* aliases from `LoadLocal`, `PropertyLoad`, etc
* - After this pass, y's mutable range only extends to `arrayPush(x, y)`
* - We avoid assigning mutable ranges to loads after y's mutable range, as
* these are working with an immutable value. As a result, `LoadLocal y` and
* `PropertyLoad y` do not get mutable ranges
* 2. `InferReactiveScopeVariables` extends mutable ranges and creates scopes,
* as according to the 'co-mutation' of different values
* - Here, we infer that
* - `arrayPush(y, x)` might alias `x` and `y` to each other
* - `setPropertyKey(x, ...)` may mutate both `x` and `y`
* - This pass correctly extends the mutable range of `y`
* - Since we didn't run `InferMutableRange` logic again, the LoadLocal /
* PropertyLoads still don't have a mutable range
*
* Note that the this bug is an edge case. Compiler output is only invalid for:
* - function expressions with
* `enableTransitivelyFreezeFunctionExpressions:false`
* - functions that throw and get retried without clearing the memocache
*
* Found differences in evaluator results
* Non-forget (expected):
* (kind: ok)
* <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
* <div>{"cb":{"kind":"Function","result":11},"shouldInvokeFns":true}</div>
* Forget:
* (kind: ok)
* <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
* <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
*/
function useFoo({a, b}: {a: number, b: number}) {
const x = [];
const y = {value: a};
arrayPush(x, y); // x and y co-mutate
const y_alias = y;
const cb = () => y_alias.value;
setPropertyByKey(x[0], 'value', b); // might overwrite y.value
return <Stringify cb={cb} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{a: 2, b: 10}],
sequentialRenders: [
{a: 2, b: 10},
{a: 2, b: 11},
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { arrayPush, setPropertyByKey, Stringify } from "shared-runtime";
function useFoo(t0) {
const $ = _c(5);
const { a, b } = t0;
let t1;
if ($[0] !== a || $[1] !== b) {
const x = [];
const y = { value: a };
arrayPush(x, y);
const y_alias = y;
let t2;
if ($[3] !== y_alias.value) {
t2 = () => y_alias.value;
$[3] = y_alias.value;
$[4] = t2;
} else {
t2 = $[4];
}
const cb = t2;
setPropertyByKey(x[0], "value", b);
t1 = <Stringify cb={cb} shouldInvokeFns={true} />;
$[0] = a;
$[1] = b;
$[2] = t1;
} else {
t1 = $[2];
}
return t1;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{ a: 2, b: 10 }],
sequentialRenders: [
{ a: 2, b: 10 },
{ a: 2, b: 11 },
],
};
```

View File

@@ -1,53 +0,0 @@
// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel:false
import {arrayPush, setPropertyByKey, Stringify} from 'shared-runtime';
/**
* 1. `InferMutableRanges` derives the mutable range of identifiers and their
* aliases from `LoadLocal`, `PropertyLoad`, etc
* - After this pass, y's mutable range only extends to `arrayPush(x, y)`
* - We avoid assigning mutable ranges to loads after y's mutable range, as
* these are working with an immutable value. As a result, `LoadLocal y` and
* `PropertyLoad y` do not get mutable ranges
* 2. `InferReactiveScopeVariables` extends mutable ranges and creates scopes,
* as according to the 'co-mutation' of different values
* - Here, we infer that
* - `arrayPush(y, x)` might alias `x` and `y` to each other
* - `setPropertyKey(x, ...)` may mutate both `x` and `y`
* - This pass correctly extends the mutable range of `y`
* - Since we didn't run `InferMutableRange` logic again, the LoadLocal /
* PropertyLoads still don't have a mutable range
*
* Note that the this bug is an edge case. Compiler output is only invalid for:
* - function expressions with
* `enableTransitivelyFreezeFunctionExpressions:false`
* - functions that throw and get retried without clearing the memocache
*
* Found differences in evaluator results
* Non-forget (expected):
* (kind: ok)
* <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
* <div>{"cb":{"kind":"Function","result":11},"shouldInvokeFns":true}</div>
* Forget:
* (kind: ok)
* <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
* <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
*/
function useFoo({a, b}: {a: number, b: number}) {
const x = [];
const y = {value: a};
arrayPush(x, y); // x and y co-mutate
const y_alias = y;
const cb = () => y_alias.value;
setPropertyByKey(x[0], 'value', b); // might overwrite y.value
return <Stringify cb={cb} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{a: 2, b: 10}],
sequentialRenders: [
{a: 2, b: 10},
{a: 2, b: 11},
],
};

View File

@@ -1,87 +0,0 @@
## Input
```javascript
// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel:false
import {setPropertyByKey, Stringify} from 'shared-runtime';
/**
* Variation of bug in `bug-aliased-capture-aliased-mutate`
* Found differences in evaluator results
* Non-forget (expected):
* (kind: ok)
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
* <div>{"cb":{"kind":"Function","result":3},"shouldInvokeFns":true}</div>
* Forget:
* (kind: ok)
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
*/
function useFoo({a}: {a: number, b: number}) {
const arr = [];
const obj = {value: a};
setPropertyByKey(obj, 'arr', arr);
const obj_alias = obj;
const cb = () => obj_alias.arr.length;
for (let i = 0; i < a; i++) {
arr.push(i);
}
return <Stringify cb={cb} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{a: 2}],
sequentialRenders: [{a: 2}, {a: 3}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { setPropertyByKey, Stringify } from "shared-runtime";
function useFoo(t0) {
const $ = _c(4);
const { a } = t0;
let t1;
if ($[0] !== a) {
const arr = [];
const obj = { value: a };
setPropertyByKey(obj, "arr", arr);
const obj_alias = obj;
let t2;
if ($[2] !== obj_alias.arr.length) {
t2 = () => obj_alias.arr.length;
$[2] = obj_alias.arr.length;
$[3] = t2;
} else {
t2 = $[3];
}
const cb = t2;
for (let i = 0; i < a; i++) {
arr.push(i);
}
t1 = <Stringify cb={cb} shouldInvokeFns={true} />;
$[0] = a;
$[1] = t1;
} else {
t1 = $[1];
}
return t1;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{ a: 2 }],
sequentialRenders: [{ a: 2 }, { a: 3 }],
};
```

View File

@@ -1,34 +0,0 @@
// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel:false
import {setPropertyByKey, Stringify} from 'shared-runtime';
/**
* Variation of bug in `bug-aliased-capture-aliased-mutate`
* Found differences in evaluator results
* Non-forget (expected):
* (kind: ok)
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
* <div>{"cb":{"kind":"Function","result":3},"shouldInvokeFns":true}</div>
* Forget:
* (kind: ok)
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
*/
function useFoo({a}: {a: number, b: number}) {
const arr = [];
const obj = {value: a};
setPropertyByKey(obj, 'arr', arr);
const obj_alias = obj;
const cb = () => obj_alias.arr.length;
for (let i = 0; i < a; i++) {
arr.push(i);
}
return <Stringify cb={cb} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{a: 2}],
sequentialRenders: [{a: 2}, {a: 3}],
};

View File

@@ -85,19 +85,11 @@ import { makeArray, mutate } from "shared-runtime";
* used when we analyze CallExpressions.
*/
function Component(t0) {
const $ = _c(5);
const $ = _c(3);
const { foo, bar } = t0;
let t1;
if ($[0] !== foo) {
t1 = { foo };
$[0] = foo;
$[1] = t1;
} else {
t1 = $[1];
}
const x = t1;
let y;
if ($[2] !== bar || $[3] !== x) {
if ($[0] !== bar || $[1] !== foo) {
const x = { foo };
y = { bar };
const f0 = function () {
const a = makeArray(y);
@@ -108,11 +100,11 @@ function Component(t0) {
f0();
mutate(y.x);
$[2] = bar;
$[3] = x;
$[4] = y;
$[0] = bar;
$[1] = foo;
$[2] = y;
} else {
y = $[4];
y = $[2];
}
return y;
}

View File

@@ -1,92 +0,0 @@
## Input
```javascript
// @enableNewMutationAliasingModel:false
import {CONST_TRUE, Stringify, mutate, useIdentity} from 'shared-runtime';
/**
* Fixture showing an edge case for ReactiveScope variable propagation.
*
* Found differences in evaluator results
* Non-forget (expected):
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
* Forget:
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
* [[ (exception in render) Error: invariant broken ]]
*
*/
function Component() {
const obj = CONST_TRUE ? {inner: {value: 'hello'}} : null;
const boxedInner = [obj?.inner];
useIdentity(null);
mutate(obj);
if (boxedInner[0] !== obj?.inner) {
throw new Error('invariant broken');
}
return <Stringify obj={obj} inner={boxedInner} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{arg: 0}],
sequentialRenders: [{arg: 0}, {arg: 1}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel:false
import { CONST_TRUE, Stringify, mutate, useIdentity } from "shared-runtime";
/**
* Fixture showing an edge case for ReactiveScope variable propagation.
*
* Found differences in evaluator results
* Non-forget (expected):
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
* Forget:
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
* [[ (exception in render) Error: invariant broken ]]
*
*/
function Component() {
const $ = _c(4);
const obj = CONST_TRUE ? { inner: { value: "hello" } } : null;
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = [obj?.inner];
$[0] = t0;
} else {
t0 = $[0];
}
const boxedInner = t0;
useIdentity(null);
mutate(obj);
if (boxedInner[0] !== obj?.inner) {
throw new Error("invariant broken");
}
let t1;
if ($[1] !== boxedInner || $[2] !== obj) {
t1 = <Stringify obj={obj} inner={boxedInner} />;
$[1] = boxedInner;
$[2] = obj;
$[3] = t1;
} else {
t1 = $[3];
}
return t1;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ arg: 0 }],
sequentialRenders: [{ arg: 0 }, { arg: 1 }],
};
```

View File

@@ -1,31 +0,0 @@
// @enableNewMutationAliasingModel:false
import {CONST_TRUE, Stringify, mutate, useIdentity} from 'shared-runtime';
/**
* Fixture showing an edge case for ReactiveScope variable propagation.
*
* Found differences in evaluator results
* Non-forget (expected):
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
* Forget:
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
* [[ (exception in render) Error: invariant broken ]]
*
*/
function Component() {
const obj = CONST_TRUE ? {inner: {value: 'hello'}} : null;
const boxedInner = [obj?.inner];
useIdentity(null);
mutate(obj);
if (boxedInner[0] !== obj?.inner) {
throw new Error('invariant broken');
}
return <Stringify obj={obj} inner={boxedInner} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{arg: 0}],
sequentialRenders: [{arg: 0}, {arg: 1}],
};

View File

@@ -1,110 +0,0 @@
## Input
```javascript
// @enableNewMutationAliasingModel:false
import {identity, mutate} from 'shared-runtime';
/**
* Bug: copy of error.todo-object-expression-computed-key-modified-during-after-construction-sequence-expr
* with the mutation hoisted to a named variable instead of being directly
* inlined into the Object key.
*
* Found differences in evaluator results
* Non-forget (expected):
* (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
* [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
* Forget:
* (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
* [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe","wat2":"joe"}]
*/
function Component(props) {
const key = {};
const tmp = (mutate(key), key);
const context = {
// Here, `tmp` is frozen (as it's inferred to be a primitive/string)
[tmp]: identity([props.value]),
};
mutate(key);
return [context, key];
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 42}],
sequentialRenders: [{value: 42}, {value: 42}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel:false
import { identity, mutate } from "shared-runtime";
/**
* Bug: copy of error.todo-object-expression-computed-key-modified-during-after-construction-sequence-expr
* with the mutation hoisted to a named variable instead of being directly
* inlined into the Object key.
*
* Found differences in evaluator results
* Non-forget (expected):
* (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
* [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
* Forget:
* (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
* [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe","wat2":"joe"}]
*/
function Component(props) {
const $ = _c(8);
let key;
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
key = {};
t0 = (mutate(key), key);
$[0] = key;
$[1] = t0;
} else {
key = $[0];
t0 = $[1];
}
const tmp = t0;
let t1;
if ($[2] !== props.value) {
t1 = identity([props.value]);
$[2] = props.value;
$[3] = t1;
} else {
t1 = $[3];
}
let t2;
if ($[4] !== t1) {
t2 = { [tmp]: t1 };
$[4] = t1;
$[5] = t2;
} else {
t2 = $[5];
}
const context = t2;
mutate(key);
let t3;
if ($[6] !== context) {
t3 = [context, key];
$[6] = context;
$[7] = t3;
} else {
t3 = $[7];
}
return t3;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ value: 42 }],
sequentialRenders: [{ value: 42 }, { value: 42 }],
};
```

View File

@@ -1,32 +0,0 @@
// @enableNewMutationAliasingModel:false
import {identity, mutate} from 'shared-runtime';
/**
* Bug: copy of error.todo-object-expression-computed-key-modified-during-after-construction-sequence-expr
* with the mutation hoisted to a named variable instead of being directly
* inlined into the Object key.
*
* Found differences in evaluator results
* Non-forget (expected):
* (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
* [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
* Forget:
* (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
* [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe","wat2":"joe"}]
*/
function Component(props) {
const key = {};
const tmp = (mutate(key), key);
const context = {
// Here, `tmp` is frozen (as it's inferred to be a primitive/string)
[tmp]: identity([props.value]),
};
mutate(key);
return [context, key];
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 42}],
sequentialRenders: [{value: 42}, {value: 42}],
};

View File

@@ -23,27 +23,19 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
function component(a) {
const $ = _c(4);
const $ = _c(2);
let t0;
if ($[0] !== a) {
t0 = { a };
const z = { a };
t0 = () => {
console.log(z);
};
$[0] = a;
$[1] = t0;
} else {
t0 = $[1];
}
const z = t0;
let t1;
if ($[2] !== z) {
t1 = () => {
console.log(z);
};
$[2] = z;
$[3] = t1;
} else {
t1 = $[3];
}
const x = t1;
const x = t0;
return x;
}

View File

@@ -23,27 +23,19 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
function component(a) {
const $ = _c(4);
const $ = _c(2);
let t0;
if ($[0] !== a) {
t0 = { a };
const z = { a };
t0 = function () {
console.log(z);
};
$[0] = a;
$[1] = t0;
} else {
t0 = $[1];
}
const z = t0;
let t1;
if ($[2] !== z) {
t1 = function () {
console.log(z);
};
$[2] = z;
$[3] = t1;
} else {
t1 = $[3];
}
const x = t1;
const x = t0;
return x;
}

View File

@@ -22,35 +22,19 @@ export const FIXTURE_ENTRYPOINT = {
import { c as _c } from "react/compiler-runtime";
import { Stringify } from "shared-runtime";
function Component(t0) {
const $ = _c(6);
const $ = _c(2);
const { a } = t0;
let t1;
if ($[0] !== a) {
t1 = { a };
const z = { a };
const p = () => <Stringify>{z}</Stringify>;
t1 = p();
$[0] = a;
$[1] = t1;
} else {
t1 = $[1];
}
const z = t1;
let t2;
if ($[2] !== z) {
t2 = () => <Stringify>{z}</Stringify>;
$[2] = z;
$[3] = t2;
} else {
t2 = $[3];
}
const p = t2;
let t3;
if ($[4] !== p) {
t3 = p();
$[4] = p;
$[5] = t3;
} else {
t3 = $[5];
}
return t3;
return t1;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -25,27 +25,19 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
function component(a) {
const $ = _c(4);
const $ = _c(2);
let t0;
if ($[0] !== a) {
t0 = { a };
const z = { a };
t0 = function () {
console.log(z);
};
$[0] = a;
$[1] = t0;
} else {
t0 = $[1];
}
const z = t0;
let t1;
if ($[2] !== z) {
t1 = function () {
console.log(z);
};
$[2] = z;
$[3] = t1;
} else {
t1 = $[3];
}
const x = t1;
const x = t0;
return x;
}

View File

@@ -25,29 +25,21 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
function component(a) {
const $ = _c(4);
const $ = _c(2);
let t0;
if ($[0] !== a) {
t0 = { a };
const z = { a };
t0 = function () {
(function () {
console.log(z);
})();
};
$[0] = a;
$[1] = t0;
} else {
t0 = $[1];
}
const z = t0;
let t1;
if ($[2] !== z) {
t1 = function () {
(function () {
console.log(z);
})();
};
$[2] = z;
$[3] = t1;
} else {
t1 = $[3];
}
const x = t1;
const x = t0;
return x;
}

View File

@@ -0,0 +1,32 @@
## Input
```javascript
import {useRef} from 'react';
function useThing(fn) {
const fnRef = useRef(fn);
const ref = useRef(null);
if (ref.current === null) {
ref.current = function (this: unknown, ...args) {
return fnRef.current.call(this, ...args);
};
}
return ref.current;
}
```
## Error
```
Found 1 error:
Error: Expected a non-reserved identifier name
`this` is a reserved word in JavaScript and cannot be used as an identifier name.
```

View File

@@ -0,0 +1,13 @@
import {useRef} from 'react';
function useThing(fn) {
const fnRef = useRef(fn);
const ref = useRef(null);
if (ref.current === null) {
ref.current = function (this: unknown, ...args) {
return fnRef.current.call(this, ...args);
};
}
return ref.current;
}

View File

@@ -1,33 +0,0 @@
## Input
```javascript
// @enableNewMutationAliasingModel:false
function Component() {
const foo = () => {
someGlobal = true;
};
return <div {...foo} />;
}
```
## Error
```
Found 1 error:
Error: 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)
error.assign-global-in-jsx-spread-attribute.ts:4:4
2 | function Component() {
3 | const foo = () => {
> 4 | someGlobal = true;
| ^^^^^^^^^^ 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)
5 | };
6 | return <div {...foo} />;
7 | }
```

View File

@@ -0,0 +1,46 @@
## Input
```javascript
import {useCallback, useRef} from 'react';
export default function useThunkDispatch(state, dispatch, extraArg) {
const stateRef = useRef(state);
stateRef.current = state;
return useCallback(
function thunk(action) {
if (typeof action === 'function') {
return action(thunk, () => stateRef.current, extraArg);
} else {
dispatch(action);
return undefined;
}
},
[dispatch, extraArg]
);
}
```
## Error
```
Found 1 error:
Invariant: [InferMutationAliasingEffects] Expected value kind to be initialized
<unknown> thunk$14.
error.bug-infer-mutation-aliasing-effects.ts:10:22
8 | function thunk(action) {
9 | if (typeof action === 'function') {
> 10 | return action(thunk, () => stateRef.current, extraArg);
| ^^^^^ [InferMutationAliasingEffects] Expected value kind to be initialized
11 | } else {
12 | dispatch(action);
13 | return undefined;
```

View File

@@ -0,0 +1,18 @@
import {useCallback, useRef} from 'react';
export default function useThunkDispatch(state, dispatch, extraArg) {
const stateRef = useRef(state);
stateRef.current = state;
return useCallback(
function thunk(action) {
if (typeof action === 'function') {
return action(thunk, () => stateRef.current, extraArg);
} else {
dispatch(action);
return undefined;
}
},
[dispatch, extraArg]
);
}

View File

@@ -0,0 +1,31 @@
## Input
```javascript
const YearsAndMonthsSince = () => {
const diff = foo();
const months = Math.floor(diff.bar());
return <>{months}</>;
};
```
## Error
```
Found 1 error:
Invariant: [Codegen] Internal error: MethodCall::property must be an unpromoted + unmemoized MemberExpression. Got a `Identifier`
error.bug-invariant-codegen-methodcall.ts:3:17
1 | const YearsAndMonthsSince = () => {
2 | const diff = foo();
> 3 | const months = Math.floor(diff.bar());
| ^^^^^^^^^^ [Codegen] Internal error: MethodCall::property must be an unpromoted + unmemoized MemberExpression. Got a `Identifier`
4 | return <>{months}</>;
5 | };
6 |
```

View File

@@ -0,0 +1,5 @@
const YearsAndMonthsSince = () => {
const diff = foo();
const months = Math.floor(diff.bar());
return <>{months}</>;
};

View File

@@ -0,0 +1,37 @@
## Input
```javascript
import {useEffect} from 'react';
export function Foo() {
useEffect(() => {
try {
// do something
} catch ({status}) {
// do something
}
}, []);
}
```
## Error
```
Found 1 error:
Invariant: (BuildHIR::lowerAssignment) Could not find binding for declaration.
error.bug-invariant-couldnt-find-binding-for-decl.ts:7:14
5 | try {
6 | // do something
> 7 | } catch ({status}) {
| ^^^^^^ (BuildHIR::lowerAssignment) Could not find binding for declaration.
8 | // do something
9 | }
10 | }, []);
```

View File

@@ -0,0 +1,11 @@
import {useEffect} from 'react';
export function Foo() {
useEffect(() => {
try {
// do something
} catch ({status}) {
// do something
}
}, []);
}

View File

@@ -0,0 +1,32 @@
## Input
```javascript
import {useMemo} from 'react';
export default function useFoo(text) {
return useMemo(() => {
try {
let formattedText = '';
try {
formattedText = format(text);
} catch {
console.log('error');
}
return formattedText || '';
} catch (e) {}
}, [text]);
}
```
## Error
```
Found 1 error:
Invariant: Expected a break target
```

View File

@@ -0,0 +1,15 @@
import {useMemo} from 'react';
export default function useFoo(text) {
return useMemo(() => {
try {
let formattedText = '';
try {
formattedText = format(text);
} catch {
console.log('error');
}
return formattedText || '';
} catch (e) {}
}, [text]);
}

View File

@@ -0,0 +1,44 @@
## Input
```javascript
import {useMemo} from 'react';
import {useFoo, formatB, Baz} from './lib';
export const Example = ({data}) => {
let a;
let b;
if (data) {
({a, b} = data);
}
const foo = useFoo(a);
const bar = useMemo(() => formatB(b), [b]);
return <Baz foo={foo} bar={bar} />;
};
```
## Error
```
Found 1 error:
Invariant: Expected consistent kind for destructuring
Other places were `Reassign` but 'mutate? #t8$46[7:9]{reactive}' is const.
error.bug-invariant-expected-consistent-destructuring.ts:9:9
7 |
8 | if (data) {
> 9 | ({a, b} = data);
| ^ Expected consistent kind for destructuring
10 | }
11 |
12 | const foo = useFoo(a);
```

View File

@@ -0,0 +1,16 @@
import {useMemo} from 'react';
import {useFoo, formatB, Baz} from './lib';
export const Example = ({data}) => {
let a;
let b;
if (data) {
({a, b} = data);
}
const foo = useFoo(a);
const bar = useMemo(() => formatB(b), [b]);
return <Baz foo={foo} bar={bar} />;
};

View File

@@ -0,0 +1,46 @@
## Input
```javascript
import {useState} from 'react';
import {bar} from './bar';
export const useFoot = () => {
const [, setState] = useState(null);
try {
const {data} = bar();
setState({
data,
error: null,
});
} catch (err) {
setState(_prevState => ({
loading: false,
error: err,
}));
}
};
```
## Error
```
Found 1 error:
Invariant: Expected all references to a variable to be consistently local or context references
Identifier <unknown> err$7 is referenced as a context variable, but was previously referenced as a [object Object] variable.
error.bug-invariant-local-or-context-references.ts:15:13
13 | setState(_prevState => ({
14 | loading: false,
> 15 | error: err,
| ^^^ Expected all references to a variable to be consistently local or context references
16 | }));
17 | }
18 | };
```

View File

@@ -0,0 +1,18 @@
import {useState} from 'react';
import {bar} from './bar';
export const useFoot = () => {
const [, setState] = useState(null);
try {
const {data} = bar();
setState({
data,
error: null,
});
} catch (err) {
setState(_prevState => ({
loading: false,
error: err,
}));
}
};

View File

@@ -0,0 +1,34 @@
## Input
```javascript
const Foo = ({json}) => {
try {
const foo = JSON.parse(json)?.foo;
return <span>{foo}</span>;
} catch {
return null;
}
};
```
## Error
```
Found 1 error:
Invariant: Unexpected terminal in optional
error.bug-invariant-unexpected-terminal-in-optional.ts:3:16
1 | const Foo = ({json}) => {
2 | try {
> 3 | const foo = JSON.parse(json)?.foo;
| ^^^^ Unexpected terminal in optional
4 | return <span>{foo}</span>;
5 | } catch {
6 | return null;
```

View File

@@ -0,0 +1,8 @@
const Foo = ({json}) => {
try {
const foo = JSON.parse(json)?.foo;
return <span>{foo}</span>;
} catch {
return null;
}
};

View File

@@ -0,0 +1,30 @@
## Input
```javascript
import Bar from './Bar';
export function Foo() {
return (
<Bar
renderer={(...props) => {
return <span {...props}>{displayValue}</span>;
}}
/>
);
}
```
## Error
```
Found 1 error:
Invariant: Expected temporaries to be promoted to named identifiers in an earlier pass
identifier 15 is unnamed.
```

View File

@@ -0,0 +1,11 @@
import Bar from './Bar';
export function Foo() {
return (
<Bar
renderer={(...props) => {
return <span {...props}>{displayValue}</span>;
}}
/>
);
}

View File

@@ -1,72 +0,0 @@
## Input
```javascript
// @validateNoFreezingKnownMutableFunctions @enableNewMutationAliasingModel:false
import {useCallback, useEffect, useRef} from 'react';
import {useHook} from 'shared-runtime';
function Component() {
const params = useHook();
const update = useCallback(
partialParams => {
const nextParams = {
...params,
...partialParams,
};
nextParams.param = 'value';
console.log(nextParams);
},
[params]
);
const ref = useRef(null);
useEffect(() => {
if (ref.current === null) {
update();
}
}, [update]);
return 'ok';
}
```
## Error
```
Found 1 error:
Error: Cannot modify local variables after render completes
This argument is a function which may reassign or mutate a local variable after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead.
error.bug-old-inference-false-positive-ref-validation-in-use-effect.ts:20:12
18 | );
19 | const ref = useRef(null);
> 20 | useEffect(() => {
| ^^^^^^^
> 21 | if (ref.current === null) {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 22 | update();
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 23 | }
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 24 | }, [update]);
| ^^^^ This function may (indirectly) reassign or modify a local variable after render
25 |
26 | return 'ok';
27 | }
error.bug-old-inference-false-positive-ref-validation-in-use-effect.ts:14:6
12 | ...partialParams,
13 | };
> 14 | nextParams.param = 'value';
| ^^^^^^^^^^ This modifies a local variable
15 | console.log(nextParams);
16 | },
17 | [params]
```

View File

@@ -1,27 +0,0 @@
// @validateNoFreezingKnownMutableFunctions @enableNewMutationAliasingModel:false
import {useCallback, useEffect, useRef} from 'react';
import {useHook} from 'shared-runtime';
function Component() {
const params = useHook();
const update = useCallback(
partialParams => {
const nextParams = {
...params,
...partialParams,
};
nextParams.param = 'value';
console.log(nextParams);
},
[params]
);
const ref = useRef(null);
useEffect(() => {
if (ref.current === null) {
update();
}
}, [update]);
return 'ok';
}

View File

@@ -0,0 +1,36 @@
## Input
```javascript
// @flow
component Foo() {
const foo = useFoo();
foo.current = true;
return <div />;
}
```
## Error
```
Found 1 error:
Error: This value cannot be modified
Modifying a value returned from a hook is not allowed. Consider moving the modification into the hook where the value is constructed.
3 | component Foo() {
4 | const foo = useFoo();
> 5 | foo.current = true;
| ^^^ value cannot be modified
6 | return <div />;
7 | }
8 |
Hint: If this value is a Ref (value returned by `useRef()`), rename the variable to end in "Ref".
```

View File

@@ -0,0 +1,7 @@
// @flow
component Foo() {
const foo = useFoo();
foo.current = true;
return <div />;
}

View File

@@ -0,0 +1,39 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects
function BadExample() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
// 🔴 Avoid: redundant state and unnecessary Effect
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(capitalize(firstName + ' ' + lastName));
}, [firstName, lastName]);
return <div>{fullName}</div>;
}
```
## Error
```
Found 1 error:
Error: 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)
error.invalid-derived-computation-in-effect.ts:9:4
7 | const [fullName, setFullName] = useState('');
8 | useEffect(() => {
> 9 | setFullName(capitalize(firstName + ' ' + lastName));
| ^^^^^^^^^^^ 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)
10 | }, [firstName, lastName]);
11 |
12 | return <div>{fullName}</div>;
```

View File

@@ -0,0 +1,13 @@
// @validateNoDerivedComputationsInEffects
function BadExample() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
// 🔴 Avoid: redundant state and unnecessary Effect
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(capitalize(firstName + ' ' + lastName));
}, [firstName, lastName]);
return <div>{fullName}</div>;
}

View File

@@ -1,39 +0,0 @@
## Input
```javascript
// @enableNewMutationAliasingModel:false
function Foo() {
const x = () => {
window.href = 'foo';
};
const y = {x};
return <Bar y={y} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [],
};
```
## Error
```
Found 1 error:
Error: Modifying a variable defined outside a component or hook is not allowed. Consider using an effect
error.object-capture-global-mutation.ts:4:4
2 | function Foo() {
3 | const x = () => {
> 4 | window.href = 'foo';
| ^^^^^^ Modifying a variable defined outside a component or hook is not allowed. Consider using an effect
5 | };
6 | const y = {x};
7 | return <Bar y={y} />;
```

View File

@@ -0,0 +1,57 @@
## Input
```javascript
// @validatePreserveExistingMemoizationGuarantees
import {makeObject_Primitives, Stringify} from 'shared-runtime';
function Component(props) {
const object = {object: props.object};
const entries = useMemo(() => Object.entries(object), [object]);
entries.map(([, value]) => {
value.updated = true;
});
return <Stringify entries={entries} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{object: {key: makeObject_Primitives()}}],
};
```
## Error
```
Found 2 errors:
Memoization: Compilation skipped because existing memoization could not be preserved
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.
error.validate-object-entries-mutation.ts:6:57
4 | function Component(props) {
5 | const object = {object: props.object};
> 6 | const entries = useMemo(() => Object.entries(object), [object]);
| ^^^^^^ This dependency may be modified later
7 | entries.map(([, value]) => {
8 | value.updated = true;
9 | });
Memoization: Compilation skipped because existing memoization could not be preserved
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.
error.validate-object-entries-mutation.ts:6:18
4 | function Component(props) {
5 | const object = {object: props.object};
> 6 | const entries = useMemo(() => Object.entries(object), [object]);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Could not preserve existing memoization
7 | entries.map(([, value]) => {
8 | value.updated = true;
9 | });
```

View File

@@ -0,0 +1,16 @@
// @validatePreserveExistingMemoizationGuarantees
import {makeObject_Primitives, Stringify} from 'shared-runtime';
function Component(props) {
const object = {object: props.object};
const entries = useMemo(() => Object.entries(object), [object]);
entries.map(([, value]) => {
value.updated = true;
});
return <Stringify entries={entries} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{object: {key: makeObject_Primitives()}}],
};

View File

@@ -0,0 +1,57 @@
## Input
```javascript
// @validatePreserveExistingMemoizationGuarantees
import {makeObject_Primitives, Stringify} from 'shared-runtime';
function Component(props) {
const object = {object: props.object};
const values = useMemo(() => Object.values(object), [object]);
values.map(value => {
value.updated = true;
});
return <Stringify values={values} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{object: {key: makeObject_Primitives()}}],
};
```
## Error
```
Found 2 errors:
Memoization: Compilation skipped because existing memoization could not be preserved
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.
error.validate-object-values-mutation.ts:6:55
4 | function Component(props) {
5 | const object = {object: props.object};
> 6 | const values = useMemo(() => Object.values(object), [object]);
| ^^^^^^ This dependency may be modified later
7 | values.map(value => {
8 | value.updated = true;
9 | });
Memoization: Compilation skipped because existing memoization could not be preserved
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.
error.validate-object-values-mutation.ts:6:17
4 | function Component(props) {
5 | const object = {object: props.object};
> 6 | const values = useMemo(() => Object.values(object), [object]);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Could not preserve existing memoization
7 | values.map(value => {
8 | value.updated = true;
9 | });
```

View File

@@ -0,0 +1,16 @@
// @validatePreserveExistingMemoizationGuarantees
import {makeObject_Primitives, Stringify} from 'shared-runtime';
function Component(props) {
const object = {object: props.object};
const values = useMemo(() => Object.values(object), [object]);
values.map(value => {
value.updated = true;
});
return <Stringify values={values} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{object: {key: makeObject_Primitives()}}],
};

View File

@@ -40,36 +40,29 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
function Component(props) {
const $ = _c(7);
const $ = _c(5);
let t0;
if ($[0] !== props.a) {
t0 = [props.a];
const a = [props.a];
t0 = [a];
$[0] = props.a;
$[1] = t0;
} else {
t0 = $[1];
}
const a = t0;
let t1;
if ($[2] !== a) {
t1 = [a];
$[2] = a;
$[3] = t1;
} else {
t1 = $[3];
}
const b = t1;
const b = t0;
let c;
if ($[4] !== b || $[5] !== props.b) {
if ($[2] !== b || $[3] !== props.b) {
c = [];
const d = {};
d.b = b;
c.push(props.b);
$[4] = b;
$[5] = props.b;
$[6] = c;
$[2] = b;
$[3] = props.b;
$[4] = c;
} else {
c = $[6];
c = $[4];
}
return c;
}

View File

@@ -32,10 +32,10 @@ import { c as _c } from "react/compiler-runtime";
import fbt from "fbt";
function Component(props) {
const $ = _c(4);
const $ = _c(2);
let t0;
if ($[0] !== props.name) {
t0 = fbt._(
const element = fbt._(
"Hello {a really long description that got split into multiple lines}",
[
fbt._param(
@@ -46,21 +46,14 @@ function Component(props) {
],
{ hk: "1euPUp" },
);
t0 = element.toString();
$[0] = props.name;
$[1] = t0;
} else {
t0 = $[1];
}
const element = t0;
let t1;
if ($[2] !== element) {
t1 = element.toString();
$[2] = element;
$[3] = t1;
} else {
t1 = $[3];
}
return t1;
return t0;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -27,27 +27,28 @@ import { c as _c } from "react/compiler-runtime";
import fbt from "fbt";
function Component(props) {
const $ = _c(4);
const $ = _c(2);
let t0;
if ($[0] !== props.name) {
t0 = fbt._('Hello {"user" name}', [fbt._param('"user" name', props.name)], {
hk: "S0vMe",
});
const element = fbt._(
'Hello {"user" name}',
[
fbt._param(
'"user" name',
props.name,
),
],
{ hk: "S0vMe" },
);
t0 = element.toString();
$[0] = props.name;
$[1] = t0;
} else {
t0 = $[1];
}
const element = t0;
let t1;
if ($[2] !== element) {
t1 = element.toString();
$[2] = element;
$[3] = t1;
} else {
t1 = $[3];
}
return t1;
return t0;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -27,29 +27,28 @@ import { c as _c } from "react/compiler-runtime";
import fbt from "fbt";
function Component(props) {
const $ = _c(4);
const $ = _c(2);
let t0;
if ($[0] !== props.name) {
t0 = fbt._(
const element = fbt._(
"Hello {user name ☺}",
[fbt._param("user name \u263A", props.name)],
[
fbt._param(
"user name \u263A",
props.name,
),
],
{ hk: "1En1lp" },
);
t0 = element.toString();
$[0] = props.name;
$[1] = t0;
} else {
t0 = $[1];
}
const element = t0;
let t1;
if ($[2] !== element) {
t1 = element.toString();
$[2] = element;
$[3] = t1;
} else {
t1 = $[3];
}
return t1;
return t0;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -27,27 +27,28 @@ import { c as _c } from "react/compiler-runtime";
import fbt from "fbt";
function Component(props) {
const $ = _c(4);
const $ = _c(2);
let t0;
if ($[0] !== props.name) {
t0 = fbt._("Hello {user name}", [fbt._param("user name", props.name)], {
hk: "2zEDKF",
});
const element = fbt._(
"Hello {user name}",
[
fbt._param(
"user name",
props.name,
),
],
{ hk: "2zEDKF" },
);
t0 = element.toString();
$[0] = props.name;
$[1] = t0;
} else {
t0 = $[1];
}
const element = t0;
let t1;
if ($[2] !== element) {
t1 = element.toString();
$[2] = element;
$[3] = t1;
} else {
t1 = $[3];
}
return t1;
return t0;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -22,28 +22,21 @@ function Component(props) {
```javascript
import { c as _c } from "react/compiler-runtime";
function Component(props) {
const $ = _c(4);
const $ = _c(2);
const id = useSelectedEntitytId();
let t0;
if ($[0] !== id) {
t0 = () => {
const onLoad = () => {
log(id);
};
t0 = <Foo onLoad={onLoad} />;
$[0] = id;
$[1] = t0;
} else {
t0 = $[1];
}
const onLoad = t0;
let t1;
if ($[2] !== onLoad) {
t1 = <Foo onLoad={onLoad} />;
$[2] = onLoad;
$[3] = t1;
} else {
t1 = $[3];
}
return t1;
return t0;
}
```

View File

@@ -21,27 +21,20 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
function Component(props) {
const $ = _c(4);
const $ = _c(2);
let t0;
if ($[0] !== props) {
t0 = function () {
const f = function () {
return <div>{props.name}</div>;
};
t0 = f.call();
$[0] = props;
$[1] = t0;
} else {
t0 = $[1];
}
const f = t0;
let t1;
if ($[2] !== f) {
t1 = f.call();
$[2] = f;
$[3] = t1;
} else {
t1 = $[3];
}
return t1;
return t0;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -55,33 +55,26 @@ import { Stringify } from "shared-runtime";
* (kind: exception) Cannot read properties of null (reading 'prop')
*/
function Component(t0) {
const $ = _c(5);
const $ = _c(3);
const { obj, isObjNull } = t0;
let t1;
if ($[0] !== isObjNull || $[1] !== obj) {
t1 = () => {
const callback = () => {
if (!isObjNull) {
return obj.prop;
} else {
return null;
}
};
t1 = <Stringify shouldInvokeFns={true} callback={callback} />;
$[0] = isObjNull;
$[1] = obj;
$[2] = t1;
} else {
t1 = $[2];
}
const callback = t1;
let t2;
if ($[3] !== callback) {
t2 = <Stringify shouldInvokeFns={true} callback={callback} />;
$[3] = callback;
$[4] = t2;
} else {
t2 = $[4];
}
return t2;
return t1;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -27,7 +27,7 @@ function useFoo() {
```javascript
import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees
function useFoo() {
const $ = _c(9);
const $ = _c(7);
const onClick = (response) => {
setState(DISABLED_FORM);
};
@@ -48,31 +48,24 @@ function useFoo() {
const handleLogout = t1;
let t2;
if ($[2] !== handleLogout) {
t2 = () => <ColumnItem onPress={() => handleLogout()} />;
const getComponent = () => <ColumnItem onPress={() => handleLogout()} />;
t2 = getComponent();
$[2] = handleLogout;
$[3] = t2;
} else {
t2 = $[3];
}
const getComponent = t2;
let t3;
if ($[4] !== getComponent) {
t3 = getComponent();
$[4] = getComponent;
$[5] = t3;
if ($[4] !== onClick || $[5] !== t2) {
t3 = [t2, onClick];
$[4] = onClick;
$[5] = t2;
$[6] = t3;
} else {
t3 = $[5];
t3 = $[6];
}
let t4;
if ($[6] !== onClick || $[7] !== t3) {
t4 = [t3, onClick];
$[6] = onClick;
$[7] = t3;
$[8] = t4;
} else {
t4 = $[8];
}
return t4;
return t3;
}
```

View File

@@ -42,74 +42,58 @@ import { c as _c } from "react/compiler-runtime"; /**
* conservative and assume that all named lambdas are conditionally called.
*/
function useFoo(t0) {
const $ = _c(17);
const $ = _c(13);
const { arr1, arr2 } = t0;
let t1;
if ($[0] !== arr1[0]) {
t1 = () => arr1[0].value;
const getVal1 = () => arr1[0].value;
t1 = (e) => getVal1() + e.value;
$[0] = arr1[0];
$[1] = t1;
} else {
t1 = $[1];
}
const getVal1 = t1;
const cb1 = t1;
let t2;
if ($[2] !== getVal1) {
t2 = (e) => getVal1() + e.value;
$[2] = getVal1;
$[3] = t2;
if ($[2] !== arr1 || $[3] !== cb1) {
t2 = arr1.map(cb1);
$[2] = arr1;
$[3] = cb1;
$[4] = t2;
} else {
t2 = $[3];
t2 = $[4];
}
const cb1 = t2;
const x = t2;
let t3;
if ($[4] !== arr1 || $[5] !== cb1) {
t3 = arr1.map(cb1);
$[4] = arr1;
$[5] = cb1;
if ($[5] !== arr2) {
const getVal2 = () => arr2[0].value;
t3 = (e_0) => getVal2() + e_0.value;
$[5] = arr2;
$[6] = t3;
} else {
t3 = $[6];
}
const x = t3;
const cb2 = t3;
let t4;
if ($[7] !== arr2) {
t4 = () => arr2[0].value;
$[7] = arr2;
$[8] = t4;
if ($[7] !== arr1 || $[8] !== cb2) {
t4 = arr1.map(cb2);
$[7] = arr1;
$[8] = cb2;
$[9] = t4;
} else {
t4 = $[8];
t4 = $[9];
}
const getVal2 = t4;
const y = t4;
let t5;
if ($[9] !== getVal2) {
t5 = (e_0) => getVal2() + e_0.value;
$[9] = getVal2;
$[10] = t5;
if ($[10] !== x || $[11] !== y) {
t5 = [x, y];
$[10] = x;
$[11] = y;
$[12] = t5;
} else {
t5 = $[10];
t5 = $[12];
}
const cb2 = t5;
let t6;
if ($[11] !== arr1 || $[12] !== cb2) {
t6 = arr1.map(cb2);
$[11] = arr1;
$[12] = cb2;
$[13] = t6;
} else {
t6 = $[13];
}
const y = t6;
let t7;
if ($[14] !== x || $[15] !== y) {
t7 = [x, y];
$[14] = x;
$[15] = y;
$[16] = t7;
} else {
t7 = $[16];
}
return t7;
return t5;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -42,7 +42,7 @@ import { useRef } from "react";
import { Stringify } from "shared-runtime";
function Component(t0) {
const $ = _c(9);
const $ = _c(7);
const { a, b } = t0;
let t1;
if ($[0] !== a.value) {
@@ -70,29 +70,22 @@ function Component(t0) {
const hasLogged = useRef(false);
let t3;
if ($[4] !== logA || $[5] !== logB) {
t3 = () => {
const log = () => {
if (!hasLogged.current) {
logA();
logB();
hasLogged.current = true;
}
};
t3 = <Stringify log={log} shouldInvokeFns={true} />;
$[4] = logA;
$[5] = logB;
$[6] = t3;
} else {
t3 = $[6];
}
const log = t3;
let t4;
if ($[7] !== log) {
t4 = <Stringify log={log} shouldInvokeFns={true} />;
$[7] = log;
$[8] = t4;
} else {
t4 = $[8];
}
return t4;
return t3;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -24,28 +24,20 @@ import { c as _c } from "react/compiler-runtime";
import * as SharedRuntime from "shared-runtime";
import { invoke } from "shared-runtime";
function useComponentFactory(t0) {
const $ = _c(4);
const $ = _c(2);
const { name } = t0;
let t1;
if ($[0] !== name) {
t1 = () => (
const cb = () => (
<SharedRuntime.Stringify>hello world {name}</SharedRuntime.Stringify>
);
t1 = invoke(cb);
$[0] = name;
$[1] = t1;
} else {
t1 = $[1];
}
const cb = t1;
let t2;
if ($[2] !== cb) {
t2 = invoke(cb);
$[2] = cb;
$[3] = t2;
} else {
t2 = $[3];
}
return t2;
return t1;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -25,34 +25,18 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
function Component(props) {
const $ = _c(6);
const $ = _c(2);
let t0;
if ($[0] !== props.a) {
t0 = { a: props.a };
const item = { a: props.a };
const items = [item];
t0 = items.map(_temp);
$[0] = props.a;
$[1] = t0;
} else {
t0 = $[1];
}
const item = t0;
let t1;
if ($[2] !== item) {
t1 = [item];
$[2] = item;
$[3] = t1;
} else {
t1 = $[3];
}
const items = t1;
let t2;
if ($[4] !== items) {
t2 = items.map(_temp);
$[4] = items;
$[5] = t2;
} else {
t2 = $[5];
}
const mapped = t2;
const mapped = t0;
return mapped;
}
function _temp(item_0) {

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