Skip to content

[compiler] Fix <ValidateMemoization> #33547

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

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open

[compiler] Fix <ValidateMemoization> #33547

wants to merge 17 commits into from

Conversation

josephsavona
Copy link
Member

@josephsavona josephsavona commented Jun 17, 2025

Squashed, review-friendly version of the stack from #33488.

This is new version of our mutability and inference model, designed to replace the core algorithm for determining the sets of instructions involved in constructing a given value or set of values. The new model replaces InferReferenceEffects, InferMutableRanges (and all of its subcomponents), and parts of AnalyzeFunctions. The new model does not use per-Place effect values, but in order to make this drop-in the end _result_ of the inference adds these per-Place effects.

I'll write up a larger document on the model, first i'm doing some housekeeping to rebase the PR.
AnalyzeFunctions had logic to reset the mutable ranges of context variables after visiting inner function expressions. However, there was a bug in that logic: InferReactiveScopeVariables makes all the identifiers in a scope point to the same mutable range instance. That meant that it was possible for a later function expression to indirectly cause an earlier function expressions' context variables to get a non-zero mutable range.

The fix is to not just reset start/end of context var ranges, but assign a new range instance. Thanks for the help on debugging, @mofeiZ!
We're already tracking which variables are hoisted context variables, so if we see a mutation of a frozen value we can emit a custom error message to help users identify the problem.
This has always been awkward: `FunctionExpression.context` places have locations set to the declaration of the identifier, whereas other references have locations pointing to the reference itself. Here, we update context operands to have their location point to the first reference of that variable within the function.
The previous error message was generic, because the old style function signature didn't support a way to specify a reason alongside a freeze effect. This meant we could only say why a value was frozen for instructions, but not hooks which use function signatures. By defining a new aliasing signature for custom hooks we can specify a reason and provide a better error message.
The previous error for hoisting violations pointed only to the variable declaration, but didn't show where the value was accessed before that declaration. We now track where each hoisted variable is first accessed and report two errors, one for the reference and one for the declaration. When we improve our diagnostic infra to support reporting errors at multiple locations we can merge these into a single conceptual error.
When we apply new aliasing signatures we can generate new temporaries, which causes the abstract memory model to not converge. The fix is to make sure we cache the applications of these signatures.
In comparing compilation output of the old/new inference models I found this case (heavily distilled into a fixture). Roughly speaking the scenario is:

* Create a mutable object `x`
* Extract part of that object and pass it to a hook/jsx so that _part_ becomes frozen
* Mutate `x`, even indirectly.

In the old model we can still independently memoize the value from the middle step, since we assume that part of the larger value is not changing. In the new model, the mutation from the later step effectively overrides the freeze effect in step 2, and considers the value to have changed later anyway.

We've already rolled out and vetted the previous behavior, confirming that the heuristic of "that part of the mutable object is fozen now" is generally safe. I'll fix in a follow-up.
This allows us to type things like `nullthrows()` or `identity()` functions where the return type is polymorphic on the input.
Now that we have support for defining aliasing signatures in moduleTypeProvider, which uses string names for receiver/args/returns/etc, we can reuse that same form for builtin declarations. The declarations are written in the unparsed form and than parsed/validated when registered (in the addFunction/addHook call).

This also required flushing out configs/schemas for more effect types.
Start of docs describing the effects and the inference rules.
…tion expressions

Adds some typed helpers to represent aliasing, assign, capture, createfrom, and mutate effects along with representative runtime behavior, and then adds tests to demonstrate that we model capture->createfrom and createfrom->capture correctly.

There is one case (createfrom->capture in a lambda) where we infer a less precise effect, but in the more conservative direction (we include more code/deps than necesssary rather than fewer).
By accident we were only ever checking the compiled output, but the intention was in general to be able to compare memoization with/without forget.
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.

2 participants