Skip to content

[compiler] Fix for edge cases of mutation of potentially frozen values #33984

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

Merged
merged 1 commit into from
Jul 25, 2025

Conversation

josephsavona
Copy link
Member

@josephsavona josephsavona commented Jul 24, 2025

Fixes two related cases of mutation of potentially frozen values.

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

This results in cases like this:

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

Within the function we would infer:

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

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

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

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

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

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


Stack created with Sapling. Best reviewed with ReviewStack.

@github-actions github-actions bot added the React Core Team Opened by a member of the React Core Team label Jul 24, 2025
Fixes two related cases of mutation of potentially frozen values.

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

This results in cases like this:

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

Within the function we would infer:

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

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

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


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

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

The second, related fix is for known mutation of phis that may be a frozen value. The previous inference model correctly recorded these as errors, the new model does not. We now correctly report a validation error for this case in the new model.
error.invalid-mutate-phi-which-could-be-frozen.ts:11:2
9 | x = {};
10 | }
> 11 | x.property = true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yesss

// that if the iteration variable is only updated in the updater, the variable
// is effectively const within the body and the "update" acts more like
// a re-initialization than a reassignment.
// Until we model this "new environment" semantic, we allow this case to error
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes sense!

Comment on lines +11 to +13
const obj = identity(object);
obj.property = props.value;
return obj;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having invalid-react fixtures feels a little weird, since compiler semantics is technically UB.

I see what you're trying to express here though!

const object = props.object;
const f = () => {
// The receiver maybe-aliases into the return
const obj = object.makeObject();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice! this is the fixture that inspired this PR right?


function Example(props) {
const obj = props.object.makeObject();
obj.property = props.value;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

very nice

@josephsavona josephsavona merged commit 12483a1 into main Jul 25, 2025
21 checks passed
josephsavona added a commit that referenced this pull request Jul 25, 2025
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33991).
* #33993
* __->__ #33991
* #33984
josephsavona added a commit that referenced this pull request Jul 25, 2025
…33993)

The test case here previously reported a "Cannot modify local variables
after render completes" error (from
ValidateNoFreezingKnownMutableFunctions). This happens because one of
the functions passed to a hook clearly mutates a ref — except that we
try to ignore mutations of refs! The problem in this case is that the
`const ref = ...` was getting converted to a context variable since the
ref is accessed in a function before its declaration. We don't infer
types for context variables at all, and our ref handling is based on
types, so we failed to ignore this ref mutation.

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

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33993).
* __->__ #33993
* #33991
* #33984
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed React Core Team Opened by a member of the React Core Team
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants