The current error message "This mutates a global or a variable after it
was passed to React" no longer makes sense since we now have more
specific error messages for different kinds of Effect.Mutate or
Effect.Stores. This replaces the fallthrough "Other" case with a
more generic message. It's not perfect, but it's a little more accurate
than what is currently emitted
The proper fix might be to treat functions as mutable objects and allow
the mutation, or special case `Function.displayName`. For now though
this PR just updates the message in the meantime so it's less
confusing.
We're doing some internal benchmarking using a lightweight bundler that @pieterv
wrote for experimentation purposes. It's designed to fully preserve Flow type
annotations so we can experiment with type-driven compilation and test out what
benefits we might get from "cross-module" compilation more easily (ie by just
bundling together a few modules so we can see them all as one).
However, the bundler renames local variables and imports, so that a reference to
`useMemo()` might end up as `React$useMemo()` or similar. This PR adds a flag to
tell the compiler that builtin hooks might be prefixed and resolve them
appropriately.
I found this by adding logic to reject inputs where reactivity gets newly
propagated in PruneNonReactiveDependencies. It's possible to create a readonly
alias to a mutable value such that we don't know the value is reactive yet when
the alias is created. Thus we need to do a fixpoint iteration even if there are
no loops in order to be able to revisit such aliases and reflow the reactivity
forward. Example:
```javascript
const x = [];
const y = x;
const z = [y]; // y isn't reactive yet when we first visit this, so z is
initially non-reactive
y.push(props.value); // then we realize y is reactive. we need a fixpoint to
propagate this back to z
const a = [z]; // need an indirection to get past the partial propagation in
PruneNonReactiveDependencies
let b = 0;
if (a[0][0]) {
b = 1;
}
return [b];
```
Existing fixtures don't change because the basic reactivity propagation in
PruneNonReactiveDependencies is enough to make common cases work. I confirmed
that the new fixture does not work on previous PR in the stack.
Fixes T175227223. When inferring reactivity, mutation of a value with a reactive
input marks the mutable value as reactive. However, we also need to account for
aliases:
```javascript
const x = [];
const y = x;
y.push(props.value);
```
Previously we would have only considered `y` reactive here, but `x` also becomes
reactive.
The implementation extracts out a helper from InferReactiveScopeVariables that
builds a `DisjointSet<Identifier>` of disjoint sets of mutably aliased values.
InferReactivePlaces then treats all instances of each mutable alias group as
equivalent for reactivity purposes.
In InferReactivePlaces, we already account for reactively controlled values:
where a value is never assigned a non-reactive value, but _which_ value is
assigned is based on a reactive condition (the test conditions of an if, switch,
loop, etc).
This PR extends that reactively-controlled inference to mutation that is
conditioned upon a reactive value. From the test case:
```javascript
let x = [];
if (props.cond) {
// This mutation has no reactive inputs.
// *But* the mutation conditionally occurs based on props.cond which is reactive
x.push(1);
}
let y = false;
if (x[0]) { // therefore the value observed here is reactive
y = true;
}
// so the value of y here is reactive via the reactive control dependency x[0]
return [y];
```
Add frozen reason for props and hook arguments
Improves the error message when mutating props or hook arguments.
Previously, this would print a generic error about mutating global variables.
This was an oversight in the original definition of useContext (oops my bad).
Context values are owned by React and should not be modified. I found this
because some cases of existing useMemo were not preserved (tested via the
validatePreserveExistingManualMemo flag) due to function calls referencing
context being assumed to mutate.
This change will allow more memoization, it's also just more correct for the
rules of React. Note the new ValueReason variant so that we can provide a
precise error message about mutating context values.
Adds back a mode to transitively freeze function expressions, independently from
the mode to preserve existing manual memoization. This lets us experiment with a
few variants:
* Preserve existing memoization
* Validate existing memoization with:
* `enableAssumeHooksFollowRulesOfReact` &&
`enableTransitivelyFreezeFunctionExpressions`
* `enableAssumeHooksFollowRulesOfReact` only
* neither of those flags
Note that `enableTransitivelyFreezeFunctionExpressions` alone probably doesn't
make sense, it's more aggressive than
`enableAssumeHooksFollowRulesOfReact` so we might as well try them together.
Adds a new mode which validates that existing manual memoization is preserved
_without_ using information from the manual memoization to affect compilation.
This gives us a way to try out the more aggressive version of Forget — ignoring
manual memoization — first and see how much code bails out and what patterns
cause this.
We can then proceed to enable the mode to actually _preserve_ existing memo
guarantees only where necessary.
Merges `@enableTransitivelyFreezeFunctionExpressions` into the new
`@enablePreserveExistingMemoizationGuarantees` mode, since they are both
motivated by the same use case of preserving effect behavior by preserving
existing memoization behavior.
The idea is that `useCallback` has an implicit assumption: that the variables
captured by the callback aren't subsequently modified. Previous PRs treated the
values directly captured by the callback as frozen. But if those variables were
themselves another function expression, and that expression captured a mutable
value, then we wouldn't consider the freeze to be transitive:
```javascript
const object = makeObject();
useHook(); // oops, hook call inside `object`'s mutable range, can't memoize
object, log, or onClick!
const log = () => { console.log(object) };
const onClick = useCallback(() => { log() });
maybeMutate(object);
```
However, the assumption of such code is that it _doesn't_ modify such
transitively captured values. So here we merge
`@enableTransitivelyFreezeFunctionExpressions` mode into the
memoization-preserving mode. Now, the memoize instructions emitted for
useCallback (and useMemo) will transitively freeze captured function
expressions, allowing us to memoize.
The flip side of this is that some code may be violating these rules. We'll rely
on runtime validation to detect such cases.
Improves `@enablePreserveExistingMemoizationGuarantees` for the useCallback
case. Similar to useMemo, we add an explicit `Memoize` instruction for the
callback function itself _and_ for its dependencies. This means we'll assume the
callback doesn't mutate any captured variables.
TODO: check this with cases involving refs (should be allowed, but also not
accidentally freeze the ref) and reassignment of locals (should be disallowed,
though that might just be a validation we're missing today)
The previous PR introduced `memoize` instructions whose lvalues aren't used, but
which can't be pruned by DCE due to pipeline ordering. Here we change to make
memoize an instruction intended for its side effects only, and prune during
codegen.
See discussion on #2448 for full context. In the new
`@enablePreserveExistingMemoizationGuarantees` mode, the goal is to preserve the
existing referential equality guarantees from the original code. #2448 lays the
groundwork by explicitly marking the _output_ of each useMemo block as memoized,
hinting to the compiler that the value cannot subsequently change. This ensures
the mutable range doesn't extend _later_, possibly overlapping a hook call and
causing memoization to gett pruned.
This PR fixes the other direction. There are cases where free variables
referenced in the useMemo block could have been inferred as mutated, which could
then extend the _start_ of the range earlier past a hook:
```javascript
const foo = createObject();
useBar();
const baz = useMemo(() => {
const baz = createObject();
maybeMutate(foo, baz);
return baz;
}, [foo]);
```
Here the compiler would infer that both `baz` and `foo` are mutable at the
`maybeMutate()` call, grouping them in the same scope. But that scope would span
the `useBar()` call, and be pruned, meaning that `baz` went unmemoized.
However, useMemo blocks shouldn't be mutating free variables. Only variables
newly created within the useMemo block should be mutable. So this PR extends the
feature to treat all free variables referenced in a useMemo block as frozen as
of the block itself.
Adds an option to preserve existing memoization guarantees for values produced
with useMemo and useCallback. We still discard the calls to these hooks, but we
preserve the information that the value is frozen at that point in the program.
Because these values are produced solely within the useMemo/useCallback
callback, their mutation cannot have any interspersed hook calls. This means
that the values mutable range will never span a hook and end at the point of the
useMemo, ensuring that they are memoized at the same point.
The main things that can change (relative to the orignal code) are:
* Forget will infer a precise set of dependencies, ignoring the user-provided
values. In practice this should only occur if the original code had a lint
violation, which Forget would bail out on. So in practice this shouldn't happen
unless the code doesn't use the React linter.
* Forget may start the memoization block earlier than the developer did if other
values are mutated along with the value being produced. This can cause
memoization to fail, but only in situations where it would have failed
previously:
```javascript
const a = [];
useFoo();
const b = useMemo(() => {
const c = a;
c.push(1);
return c;
}, [a]);
```
In this example (sans Forget) the useMemo will invalidate on every render
because `a` will always be a new array and its listed as a dependency of the
useMemo. Forget would correctly determine that the memoization would have to
work as follows:
```javascript
let c;
if (...) {
const a = []
useFoo(); // OOPS we made a hook call conditional
const t0 = a;
t0.push(1);
c = t0;
...
} else {
c = $[...]
}
```
Because this is invalid, Forget would (later in the pipeline) strip out this
memoization block and (as with the original) leave `c` un-memoized.
In this same example, removing the hook would cause Forget to be able to memoize
a value that wasn't memoized before:
```javascript
const a = [];
const b = useMemo(() => {
const c = a;
c.push(1);
return c;
}, [a]);
```
This invalidates every render without Forget, but would memoize correctly with
Forget (it would expand the memoization block to include the declaration of
`a`).
Builds on the utilities added previously to infer types from type annotations on
variable declarations. This is a limited form, where currently we only infer for
local identifiers (not function parameters) and only infer a type for the
variable initializer and not subsequent reassignments.
I realized this while working on Forest. When computing the dependencies of a
reactive scope we can omit setState functions in the general case (exception
described below). Currently that's implemented in PruneNonReactiveDependencies.
However, this causes us to miss some optimizations — a value isn't reactive if
its only dependency is a setState, and that may allow further downstreams values
to become non-reactive. We lose out on that by only filtering out setStates in
PruneNonReactiveDependencies — this logic really belongs in InferReactivePlaces.
So this PR moves the check for setState types to that pass. The updated fixtures
show that this already uncovers some wins. The _new_ fixtures covers the
exception. It's possible for a value to be typed as being a setState function,
but to still be reactive: if its a local that is conditionally assigned
different setState function values. Currently this test happens to work because
our phi type inference is incomplete (see #2296). I'm adding the test now though
to prevent regressions when we fix phi type inference.
The previous PR helped me realize we weren't handling Array#at correctly. If the
receiver is a mutable value its effect should be Capture and the lvalue effect
needs to be Store. This PR updates the definition for Array#at to make the
receiver Capture, and then updates inference to automatically set the lvalue
effect to Store if _any_ argument (or the receiver) was Capture.
There was one missing piece to the optimization from the previous PR: Array#map
can return an alias to the receiver in its output, which means that mutations of
the result have to be treated as mutations of the receiver. This means we need
to use a Capture effect on the receiver. If that doesn't get downgraded to a
Read bc the value was immutable, we then also need to make the lvalue effect a
Store (so that InferMutableRanges actually looks at it for aliasing).
Improves memoization for cases such as #2409:
```javascript
const x = [];
useEffect(...);
return <div>{x.map(item => <span>{item}</span>)}</div>;
```
We previously thought that the `x.map(...)` call mutated `x` since its kind was
Mutable. However, in this case we can determine that the map call cannot mutate
`x` (or anything else): the lambda does not mutate any free variables and does
not mutate its arguments.
This PR adds a new flag to function signatures, used for method calls only, that
checks for such cases. The idea is that if the receiver is the only thing that
is mutable — including that there are no args which are function expressions
which mutate their parameters — then we can infer the effect as a read. See
tests which confirm that function expressions which capture or mutate their
params bypass the optimization.
We were using `returnValueKind` from function signatures for CallExpression but
not MethodCall; this PR changes to use this signature information for both
instruction kinds.
---
Going to hold off on landing until after codefreeze, it's not urgent as we
already fixed playground in #2404. All other internal pipelines do error
handling through Entrypoint, which catches and creates UnexpectedErrors as
needed.
This is non ideal but at least it's a step in the right direction.
Getting the correct error requires us to track every identifier and global,
which seems a bit excessive for now.
We can revisit and improve this error if this is starting to confuse folks.
Noticed from our paste that we weren't correctly rolling up hoisting related
errors due to specific information being in the error title, so this PR moves
them into description instead.
I did a double take when I thought we didn't handle returning the
error when reading the code and when I edited the code, typescript told
me that there's no need to return as creating the error will throw.
This PR makes it clear from the name of the function that we will throw.
This PR adds a feature flag to model a potential new-in-practice rule in React:
that freezing a function expression also freezes its closed-over values,
transitively. For example, in the following code `data` is frozen when the
lambda that captures it is is passed to useEffect:
```javascript
const data = [];
// useEffect freezes its argument (the function expr), which transitively
freezes its captured value data
useEffect(() => {
foo(data);
}, [data]);
data.push(true); // ERROR: mutating a frozen value
mutate(data); // we conservatively assume this doesn't mutate but could be wrong
```
Note that this rule has never been written down or enforced. It is theoretically
equivalent to the rule (already implemented in Forget) that values captured by
JSX are frozen:
```javascript
const style = {...};
<div style={style}>...</div>
style.width = 10; // ERROR: mutating a frozen value
mutate(style); // we conservatively assume this doesn't mutate but could be
wrong
```
However, JSX is typically constructed toward the very end of a render function.
Thus in practice there isn't much subsequent code that could even modify such a
captured value. But for the useEffect case (and other hooks that take closures
as arguments), they tend to occur much earlier in a render function. There's
more code that can run later and still modify the captured values, without
causing issues in practice. The _practical_ rule today is that you can't modify
values captured by frozen lambdas _after the component returns_: it's fine in
practice to modify captured values between calling eg useEffect and returning
from render.
Thus this feature flag is fairly likely to break some percent of real product
code. I'm adding this so that we can experiment and see how unsafe it actually
is.
Fixes for the previous PR. What was happening is that our inference was
inferring the correct mutable ranges and reactive scopes, but the inlining
process left the instructions from the IIFEs inside a separate block, with a
'label' terminal preceding it. When we converted to ReactiveFunction this was
preserved as a ReactiveLabelTerminal, which meant that the first instruction for
the mutable range could be nested inside one LabelTerminal, while more would be
in a subsequent LabelTerminal. But we close blocks based on the block scope!
This meant that we'd have leftover instructions (in the second LabelTerminal)
that got left out of the block.
Furthermore, because inlining was happening after EnterSSA we weren't creating
phis correctly. This PR fixes a bunch of these issues, and a subsequent PR
handles the remaining cases:
* We move DropManualMemo and InlineIIFEs before EnterSSA. This means we lose the
ability to use type information, but we ensure that we create proper SSA ids and
phis for any reassignments within the IIFE
* We also update PruneUnusedLabels to not just remove the unused labels, but to
actually remove LabelTerminals that don't need them.
Previously if any operand was reactive, we transferred that reactivity to other
operands that had a mutable effect (capture, conditionally mutate, mutate, or
store). But a value can be captured without ever being modified again. This PR
updates the logic to only transfer reactivity among operands that are actually
mutable at the given instruction, based on the mutable range. This is strictly
more precise.
This PR adds one remaining feature to InferReactivePlaces: tracking indirections
like LoadLocal, PropertyLoad, and similar. Consider something like:
```
// INPUT
x.push(reactiveValue);
// HIR
t0 = LoadLocal 'x'
t1 = PropertyLoad t0, 'push'
t2 = LoadLocal 'reactiveValue' // reactive
t3 = CallExpression mutate t0 . read t1 ( read t2 )
```
Because a reactive value (`t2`) flows into `t0`, we want to record t0 as
reactive as well. But that's just the temporary for `LoadLocal 'x'` - what's
really happening is that from this point, `x` is reactive.
InferReactiveIdentifiers tracked this, and now that logic is ported into
InferReactivePlaces as well. That lets us remove all the actual inference from
InferReactiveIdentifiers.
Updates `InferReactivePlaces` to infer control dependencies. We build on the
formal definition of control dependencies, which is that statement S2 is
control-dependent on statement S1 if S1 is in the post-dominance-frontier of S2.
Intuitively, if S1 decides whether S2 is reached or not, then S1 is a control
dependency of S2. The post dominance frontier of a given statement S is the set
of statements which may or may not reach S, and captures the intuitive notion.
We take advantage of phis: phis are the point where a variable may have multiple
values depending on the path we took. If a phi is not already known to be
reactive from data dependencies we check for control dependencies. Specifically
we look at each phi operand. We check if the block that the operand came from
has any reactive control dependencies, and if so we mark the phi itself as
reactive.
The post-dominance-frontier (PDF) algorithm requires walking the post-dominator
tree a bunch, so we cache the PDF of blocks so that we don't have to recalculate
on subsequent iterations.
In addition, `InferReactiveIdentifiers` now uses the _union_ of its own
inference plus the new `InferReactivePlaces` output when deciding what
identifiers are reactive. This ensures that control dependencies are recorded
correctly, fixing the previous test cases. The next diff adds the remaining
features to InferReactivePlaces so that it can fully replace
InferReactiveIdentifiers.
See context from #2187 for background about control dependencies.
Our current `PruneNonReactiveIdentifiers` pass runs on ReactiveFunction, after
scope construction, and removes scope dependencies that aren't reactive. It
works by first building up a set of reactive identifiers in
`InferReactiveIdentifiers`, then walking the ReactiveFunction and pruning any
scope dependencies that aren't in that set.
The challenge is control variables, as demonstrated by the test cases in #2184.
`InferReactiveIdentifiers` runs against ReactiveFunction, and when we initially
wrote it we didn't consider control variables. To handle control variables we
really need to use precise control- & data-flow analysis, which is much easier
with HIR.
This PR adds the start of `InferReactivePlaces`, which annotates each `Place`
with whether it is reactive or not. This allows the annotation to survive
LeaveSSA, which swaps out the identifiers of places but leaves other properties
as-is. This version does _not_ yet handle control variables, but it's already
more precise than our existing inference. In our current inference, if `x` is
ever assigned a reactive value, then all `x`s are marked reactive. In our new
inference, each instance of `x` (each Place) gets a separate flag based on
whether x can actually be reactive at that point in the program.
There are two main next steps (in follow-up PRs):
* Update the mechanism by which we prune non-reactive dependencies from scopes.
* Handle control variables. I think we may be able to use dominator trees to
figure out the set of basic blocks whose reachability is gated by the control
variables. This should clearly work for if/else and switch, as for loops i'm not
sure but intuitively it seems right.
This is part of a stack for inferring variables which are reactive via *control
dependencies* as opposed to a data dependency. In compiler engineering, a
statement S2 is control-dependent on statement S1 if S1 is in the post-dominance
frontier of S2. Stated more intuitively: if S1 decides whether or not S2 is
reached, then S1 is a control dependency of S2.
As a start, we add `Place.reactive: boolean` so that individual places can track
whether they are reactive or not. This lets us do fine-grained reactivity
inference on the control-flow graph, even taking into account different SSA
instances of a variable, so that we can say that a particular SSA version of `x`
is reactive, while other "versions" of x (due to reassignment) are not.
This PR completes the refactor. We now do the following sequence:
* ValidateUseMemo. This is a new pass that extracts just the validation logic
from the existing InlineUseMemo. This was always being run before, so this pass
also always runs.
* DropManualMemoization. As before, this converts useMemo calls into an IIFE
(immediately invoked function expression).
* InlineImmediatelyInvokedFunctionExpressions (prev InlineUseMemo). This pass
now inlines _all_ IIFEs, including both useMemo calls that were dropped as well
as IIFEs that the user wrote.
The motivation for this change is that some codebases use IIFEs as a workaround
for lack of if expressions, but we're unable to optimize within function
expressions. This is the reason we originally added inlining for useMemo, but
given that IIFEs are common it makes sense to generalize the inlining.
## Test Plan
* Manually checked changes in output
* Synced internally and tested on profile page, no issues observed. Also
spot-checked some of the changes in ouput and it looks as expected.
The goal of this stack is to generalize `InlineUseMemo` into a pass that inlines
all immediately invoked function expressions (IIFEs). Rather than specialize
just useMemo calls, we'll rely on DropManualMemoization running first and
turning useMemo calls into IIFEs. Then the generalized inlining pass can handle
those IIFEs as well as others present in the source.
For now, moving the order of the pass makes the output closer to what it will
eventually be after this stack is complete.
#2127 introduced a special type for the result of `useContext()` that was sort
of ref-like. The intent was to allow code like this:
```
function Foo() {
const cx = useContext(...);
function onEvent() {
cx.foo = true;
};
return <Bar onEvent={onEvent} />;
}
```
However, that code actually is allowed by the compiler by default. It's only a
bailout when `@validateFrozenLambdas` is enabled. The "fix" in #2127 therefore
wasn't strictly necessary to unblock rollout, and it's also flawed in a few
ways:
* First, `useContext(FooContext)` should have equivalent behavior to a custom
hooks which does the same thing, ie `function useFooContext() { return
useContext(FooContext) }`. Specializing the type of useContext makes the
behavior different.
* Second, it meant that even readonly accesses of the context inside a callback
marked the function as capturing, which in turn prevented those callbacks from
being memoized.
So i'm reverting this and we'll have to think a bit more about this case.
Updates `Environment` to store all feature flags on a single `config` object. We
now also define an object with all the default config values, and use this to
populate defaults for any missing values in the user-provided config.
InferReferenceEffects uses object identity to merge states, which breaks when we
create a new object to model `undefined`.
Two value objects representing `undefined` are not equal due to referential
equality.
Instead, let's use a singleton to represent `undefined` value.
Adds a new type for representing context values, which is transitive. So
`useContext(a).b.c` also gets inferred as a context type. This allows us to
refine our inference, and allow passing callbacks that modify context where a
"frozen" lambda is exepcted.