Skip to content

Conversation

mofeiZ
Copy link
Contributor

@mofeiZ mofeiZ commented Aug 27, 2025

Alternative to #34276


(Summary taken from @josephsavona 's #34276)
Partial fix for #34262. Consider this example:

function useInputValue(input) {
  const object = React.useMemo(() => {
    const {value} = transform(input);
    return {value};
  }, [input]);
  return object;
}

React Compiler breaks this code into two reactive scopes:

  • One for transform(input)
  • One for {value}

When we run ValidatePreserveExistingMemo, we see that the scope for {value} has the dependency value, whereas the original memoization had the dependency input, and throw an error that the dependencies didn't match.

In other words, we're flagging the fact that memoized better than the user as a problem. The more complete solution would be to validate that there is a subgraph of reactive scopes with a single input and output node, where the input node has the same dependencies as the original useMemo, and the output has the same outputs. That is true in this case, with the subgraph being the two consecutive scopes mentioned above.

But that's complicated. As a shortcut, this PR checks for any dependencies that are defined after the start of the original useMemo. If we find one, we know that it's a case where we were able to memoize more precisely than the original, and we don't report an error on the dependency. We still check that the original output value is able to be memoized, though. So if the scope of object were extended, eg with a call to mutate(object), then we'd still correctly report an error that we couldn't preserve memoization.

Comment on lines 375 to 382
dep = {
root: {
kind: 'NamedLocal',
value: storeTarget,
},
path: [],
};
this.temporaries.set(storeTarget.identifier.id, dep);
Copy link
Member

Choose a reason for hiding this comment

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

hmm this just means dep will be the last property of the destructure, this should be collecting all of deps, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, this is eventually used by recordTemporaries which maps whatever this function returns to the Instruction lvalue. In the destructure case, I think we want to return the dep of source since x = {a, b} = source has the semantic of assigning source to x

<promoted>t1 = Destructure target={x, y} source=$0

@mofeiZ mofeiZ changed the title [donotcommit] Alternative fix for false positive memo validation [compiler] Fix false positive memo validation (alternative) Sep 8, 2025
@mofeiZ mofeiZ force-pushed the pr34318 branch 2 times, most recently from b2a7d2f to a355b48 Compare September 8, 2025 20:50
@mofeiZ mofeiZ marked this pull request as ready for review September 8, 2025 20:50
@mofeiZ mofeiZ requested a review from josephsavona September 8, 2025 20:50
temporaries.set(lvalId, {
root: {
kind: 'NamedLocal',
value: {...(instr.lvalue as Place)},
Copy link
Member

Choose a reason for hiding this comment

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

nit: above we could refine on lvalue != null and then use lvalue here without as

Copy link
Member

@josephsavona josephsavona left a comment

Choose a reason for hiding this comment

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

makes sense, nice!

Alternative to #34276

---
(Summary taken from @josephsavona 's #34276)
Partial fix for #34262. Consider this example:

```js
function useInputValue(input) {
  const object = React.useMemo(() => {
    const {value} = transform(input);
    return {value};
  }, [input]);
  return object;
}
```

React Compiler breaks this code into two reactive scopes:
* One for `transform(input)`
* One for `{value}`

When we run ValidatePreserveExistingMemo, we see that the scope for `{value}` has the dependency `value`, whereas the original memoization had the dependency `input`, and throw an error that the dependencies didn't match.

In other words, we're flagging the fact that memoized _better than the user_ as a problem. The more complete solution would be to validate that there is a subgraph of reactive scopes with a single input and output node, where the input node has the same dependencies as the original useMemo, and the output has the same outputs. That is true in this case, with the subgraph being the two consecutive scopes mentioned above.

But that's complicated. As a shortcut, this PR checks for any dependencies that are defined after the start of the original useMemo. If we find one, we know that it's a case where we were able to memoize more precisely than the original, and we don't report an error on the dependency. We still check that the original _output_ value is able to be memoized, though. So if the scope of `object` were extended, eg with a call to `mutate(object)`, then we'd still correctly report an error that we couldn't preserve memoization.
@mofeiZ mofeiZ merged commit eda778b into main Sep 9, 2025
21 of 30 checks passed
github-actions bot pushed a commit that referenced this pull request Sep 9, 2025
Alternative to #34276

---
(Summary taken from @josephsavona 's #34276)
Partial fix for #34262. Consider this example:

```js
function useInputValue(input) {
  const object = React.useMemo(() => {
    const {value} = transform(input);
    return {value};
  }, [input]);
  return object;
}
```

React Compiler breaks this code into two reactive scopes:
* One for `transform(input)`
* One for `{value}`

When we run ValidatePreserveExistingMemo, we see that the scope for
`{value}` has the dependency `value`, whereas the original memoization
had the dependency `input`, and throw an error that the dependencies
didn't match.

In other words, we're flagging the fact that memoized _better than the
user_ as a problem. The more complete solution would be to validate that
there is a subgraph of reactive scopes with a single input and output
node, where the input node has the same dependencies as the original
useMemo, and the output has the same outputs. That is true in this case,
with the subgraph being the two consecutive scopes mentioned above.

But that's complicated. As a shortcut, this PR checks for any
dependencies that are defined after the start of the original useMemo.
If we find one, we know that it's a case where we were able to memoize
more precisely than the original, and we don't report an error on the
dependency. We still check that the original _output_ value is able to
be memoized, though. So if the scope of `object` were extended, eg with
a call to `mutate(object)`, then we'd still correctly report an error
that we couldn't preserve memoization.

Co-authored-by: Joe Savona <[email protected]>

DiffTrain build for [eda778b](eda778b)
github-actions bot pushed a commit that referenced this pull request Sep 9, 2025
Alternative to #34276

---
(Summary taken from @josephsavona 's #34276)
Partial fix for #34262. Consider this example:

```js
function useInputValue(input) {
  const object = React.useMemo(() => {
    const {value} = transform(input);
    return {value};
  }, [input]);
  return object;
}
```

React Compiler breaks this code into two reactive scopes:
* One for `transform(input)`
* One for `{value}`

When we run ValidatePreserveExistingMemo, we see that the scope for
`{value}` has the dependency `value`, whereas the original memoization
had the dependency `input`, and throw an error that the dependencies
didn't match.

In other words, we're flagging the fact that memoized _better than the
user_ as a problem. The more complete solution would be to validate that
there is a subgraph of reactive scopes with a single input and output
node, where the input node has the same dependencies as the original
useMemo, and the output has the same outputs. That is true in this case,
with the subgraph being the two consecutive scopes mentioned above.

But that's complicated. As a shortcut, this PR checks for any
dependencies that are defined after the start of the original useMemo.
If we find one, we know that it's a case where we were able to memoize
more precisely than the original, and we don't report an error on the
dependency. We still check that the original _output_ value is able to
be memoized, though. So if the scope of `object` were extended, eg with
a call to `mutate(object)`, then we'd still correctly report an error
that we couldn't preserve memoization.

Co-authored-by: Joe Savona <[email protected]>

DiffTrain build for [eda778b](eda778b)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants