-
Notifications
You must be signed in to change notification settings - Fork 49.6k
[compiler] Improve ShallowMutable detection for frozen sources #34341
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
let isFromProps = false; | ||
if (context.fn.fnType === 'Component' && context.fn.params.length > 0) { | ||
const props = context.fn.params[0]; | ||
const propsPlace = props.kind === 'Identifier' ? props : props.place; | ||
isFromProps = propsPlace.identifier.id === value.value.identifier.id; | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this might be overly specific, but i thought i'd err on the side of specificity for now before generalizing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we need to limit it to just props. If it's a rest spread of a frozen object, we can treat that as shallow mutable
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also it should work for array spreads too: const [x, ...rest] = useHook()
and for object spread of hook returns, or hook arguments.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a great start! See comments for some suggestions and things to make sure we test
compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts
Outdated
Show resolved
Hide resolved
compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts
Show resolved
Hide resolved
...gin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/spread-props.tsx
Outdated
Show resolved
Hide resolved
let isFromProps = false; | ||
if (context.fn.fnType === 'Component' && context.fn.params.length > 0) { | ||
const props = context.fn.params[0]; | ||
const propsPlace = props.kind === 'Identifier' ? props : props.place; | ||
isFromProps = propsPlace.identifier.id === value.value.identifier.id; | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we need to limit it to just props. If it's a rest spread of a frozen object, we can treat that as shallow mutable
compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts
Outdated
Show resolved
Hide resolved
compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts
Outdated
Show resolved
Hide resolved
compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts
Show resolved
Hide resolved
Note that object and array literals can also be treated as shallow mutable if all of their initial elements are frozen. |
The compiler currently fails to preserve manual memoization when components use rest spread destructuring for props: ```js // OK function Component(props) { const value = useMemo(() => compute(props.foo), [props.foo]); } // Manual memo could not be preserved function Component({...props}) { const value = useMemo(() => compute(props.foo), [props.foo]); } ``` The issue is that property accesses on rest-spread objects are treated as fully mutable. This PR introduces a new `ShallowMutable` ValueKind for rest spreads that come specifically from props. When we access properties from a `ShallowMutable` value, they're treated as Frozen. Closes #34313
const fromValue = state.kind(effect.from); | ||
const fromKind = fromValue.kind; | ||
switch (fromKind) { | ||
case ValueKind.ShallowMutable: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm this doesn't seem right, we should keep this as an Assign. The new rule for shallow mutable should be[1]:
Given
x is ShallowMutable
CreateFrom y <- x
Then
y is Frozen
Whereas this basically says
Given
x is ShallowMutable
Assign y = x
Then
y is Frozen (bc immutable capture)
[1] would be nice to update MUTABILITY_ALIASING_MODEL for this, btw
Primitive = 'primitive', | ||
Global = 'global', | ||
Mutable = 'mutable', | ||
ShallowMutable = 'shallowmutable', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you'll need to update mergeValueKinds()
in InferMutationAliasingEffects to account for the new type
if (valueInfo && valueInfo.kind === ValueKind.ShallowMutable) { | ||
this.#values.set(value, { | ||
kind: ValueKind.Mutable, | ||
reason: valueInfo.reason, | ||
}); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks right. See my comment elsewhere about updating mergeValueKinds()
to account for ShallowMutability. The logic there should basically be to treat ShallowMutable like Mutable when it joins with other things (if Mutable | x => Mutable
, then ShallowMutable | x => ShallowMutable
). Except ShallowMutable | Mutable => Mutable
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The fact that you didn't hit the invariant in mergeValueKinds() also suggests we need some more tests. Something like
function Component(props) {
let x;
if (props.cond) {
x = [props.item]; // shallow mutable
} else {
x = []; // regular mutable
}
const z = x[0]; // z is mutable
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I must be overlooking things — where do we actually create new instances that are ShallowMutable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, the bug from the PR description still occurs on the latest version of the PR, and the fixtures don't seem different than before the change. We don't actually create ShallowMutable instances anywhere afaict
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe i'm looking too early, this is just exciting!!!!
As part of the new inference model we updated to (correctly) treat destructuring spread as creating a new mutable object. This had the unfortunate side-effect of reducing precision on destructuring of props, though: ```js function Component({x, ...rest}) { const z = rest.z; identity(z); return <Stringify x={x} z={z} />; } ``` Memoized as the following, where we don't realize that `z` is actually frozen: ```js function Component(t0) { const $ = _c(6); let x; let z; if ($[0] !== t0) { const { x: t1, ...rest } = t0; x = t1; z = rest.z; identity(z); ... ``` #34341 was our first thought of how to do this (thanks @poteto for exploring this idea!). But during review it became clear that it was a bit more complicated than I had thought. So this PR explores a more conservative alternative. The idea is: * Track known sources of frozen values: component props, hook params, and hook return values. * Find all object spreads where the rvalue is a known frozen value. * Look at how such objects are used, and if they are only used to access properties (PropertyLoad/Destructure), pass to hooks, or pass to jsx then we can be very confident the object is not mutated. We consider any such objects to be frozen, even though technically spread creates a new object. See new fixtures for more examples.
As part of the new inference model we updated to (correctly) treat destructuring spread as creating a new mutable object. This had the unfortunate side-effect of reducing precision on destructuring of props, though: ```js function Component({x, ...rest}) { const z = rest.z; identity(z); return <Stringify x={x} z={z} />; } ``` Memoized as the following, where we don't realize that `z` is actually frozen: ```js function Component(t0) { const $ = _c(6); let x; let z; if ($[0] !== t0) { const { x: t1, ...rest } = t0; x = t1; z = rest.z; identity(z); ... ``` #34341 was our first thought of how to do this (thanks @poteto for exploring this idea!). But during review it became clear that it was a bit more complicated than I had thought. So this PR explores a more conservative alternative. The idea is: * Track known sources of frozen values: component props, hook params, and hook return values. * Find all object spreads where the rvalue is a known frozen value. * Look at how such objects are used, and if they are only used to access properties (PropertyLoad/Destructure), pass to hooks, or pass to jsx then we can be very confident the object is not mutated. We consider any such objects to be frozen, even though technically spread creates a new object. See new fixtures for more examples.
As part of the new inference model we updated to (correctly) treat destructuring spread as creating a new mutable object. This had the unfortunate side-effect of reducing precision on destructuring of props, though: ```js function Component({x, ...rest}) { const z = rest.z; identity(z); return <Stringify x={x} z={z} />; } ``` Memoized as the following, where we don't realize that `z` is actually frozen: ```js function Component(t0) { const $ = _c(6); let x; let z; if ($[0] !== t0) { const { x: t1, ...rest } = t0; x = t1; z = rest.z; identity(z); ... ``` #34341 was our first thought of how to do this (thanks @poteto for exploring this idea!). But during review it became clear that it was a bit more complicated than I had thought. So this PR explores a more conservative alternative. The idea is: * Track known sources of frozen values: component props, hook params, and hook return values. * Find all object spreads where the rvalue is a known frozen value. * Look at how such objects are used, and if they are only used to access properties (PropertyLoad/Destructure), pass to hooks, or pass to jsx then we can be very confident the object is not mutated. We consider any such objects to be frozen, even though technically spread creates a new object. See new fixtures for more examples.
I tried something simpler in #34900 |
As part of the new inference model we updated to (correctly) treat destructuring spread as creating a new mutable object. This had the unfortunate side-effect of reducing precision on destructuring of props, though: ```js function Component({x, ...rest}) { const z = rest.z; identity(z); return <Stringify x={x} z={z} />; } ``` Memoized as the following, where we don't realize that `z` is actually frozen: ```js function Component(t0) { const $ = _c(6); let x; let z; if ($[0] !== t0) { const { x: t1, ...rest } = t0; x = t1; z = rest.z; identity(z); ... ``` #34341 was our first thought of how to do this (thanks @poteto for exploring this idea!). But during review it became clear that it was a bit more complicated than I had thought. So this PR explores a more conservative alternative. The idea is: * Track known sources of frozen values: component props, hook params, and hook return values. * Find all object spreads where the rvalue is a known frozen value. * Look at how such objects are used, and if they are only used to access properties (PropertyLoad/Destructure), pass to hooks, or pass to jsx then we can be very confident the object is not mutated. We consider any such objects to be frozen, even though technically spread creates a new object. See new fixtures for more examples. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34900). * __->__ #34900 * #34887
As part of the new inference model we updated to (correctly) treat destructuring spread as creating a new mutable object. This had the unfortunate side-effect of reducing precision on destructuring of props, though: ```js function Component({x, ...rest}) { const z = rest.z; identity(z); return <Stringify x={x} z={z} />; } ``` Memoized as the following, where we don't realize that `z` is actually frozen: ```js function Component(t0) { const $ = _c(6); let x; let z; if ($[0] !== t0) { const { x: t1, ...rest } = t0; x = t1; z = rest.z; identity(z); ... ``` #34341 was our first thought of how to do this (thanks @poteto for exploring this idea!). But during review it became clear that it was a bit more complicated than I had thought. So this PR explores a more conservative alternative. The idea is: * Track known sources of frozen values: component props, hook params, and hook return values. * Find all object spreads where the rvalue is a known frozen value. * Look at how such objects are used, and if they are only used to access properties (PropertyLoad/Destructure), pass to hooks, or pass to jsx then we can be very confident the object is not mutated. We consider any such objects to be frozen, even though technically spread creates a new object. See new fixtures for more examples. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34900). * __->__ #34900 * #34887 DiffTrain build for [c35f6a3](c35f6a3)
As part of the new inference model we updated to (correctly) treat destructuring spread as creating a new mutable object. This had the unfortunate side-effect of reducing precision on destructuring of props, though: ```js function Component({x, ...rest}) { const z = rest.z; identity(z); return <Stringify x={x} z={z} />; } ``` Memoized as the following, where we don't realize that `z` is actually frozen: ```js function Component(t0) { const $ = _c(6); let x; let z; if ($[0] !== t0) { const { x: t1, ...rest } = t0; x = t1; z = rest.z; identity(z); ... ``` #34341 was our first thought of how to do this (thanks @poteto for exploring this idea!). But during review it became clear that it was a bit more complicated than I had thought. So this PR explores a more conservative alternative. The idea is: * Track known sources of frozen values: component props, hook params, and hook return values. * Find all object spreads where the rvalue is a known frozen value. * Look at how such objects are used, and if they are only used to access properties (PropertyLoad/Destructure), pass to hooks, or pass to jsx then we can be very confident the object is not mutated. We consider any such objects to be frozen, even though technically spread creates a new object. See new fixtures for more examples. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34900). * __->__ #34900 * #34887 DiffTrain build for [c35f6a3](c35f6a3)
The compiler currently fails to preserve manual memoization when components use rest spread destructuring for props:
The issue is that property accesses on rest-spread objects are treated as fully mutable.
This PR introduces a new
ShallowMutable
ValueKind for rest spreads that come specifically from props. When we access properties from aShallowMutable
value, they're treated as Frozen.Closes #34313