You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Root cause: In InferMutationAliasingEffects, the unknown-call fallback conservatively emits MutateTransitiveConditionally for call operands. For chained call results separated by a hook, this over-approximates mutation on the intermediate value and extends its mutable range across the interleaved hook call, preventing scope construction from memoizing either call.
Fix:
Add a pre-pass (collectOperandUsageInfo) that collects declaration usage metadata, then skip the conservative MutateTransitiveConditionally for an arg only when all of the following hold:
The call result is consumed downstream
The arg is used exclusively as call arguments
The arg root is produced by a call/new/method-call
A hook call exists between the arg root definition and the current call
The arg has no uses after the current call
This keeps the conservative behavior everywhere else and only avoids the specific over-approximation that breaks memoization in this pattern.
The pre-pass is skipped entirely for functions with no hook calls, and uses path-compressed union-find and binary search for efficiency (it still has a perf impact tho)
How did you test this change?
Ran the full compiler fixture suite (1844 tests, 0 failures):
yarn snap
# 1844 Tests, 1844 Passed, 0 Failed
Added 6 fixture tests covering the fix and its boundary conditions:
Fixture: repro-hook-interleaved-chained-call-memoization
What it tests: Original repro, both func(bar) and result(data, value) are now memoized
Fixture: ...-arg-used-after
What it tests: Arg used after the second call — optimization correctly does NOT fire, preserving conservative mutation
Fixture: ...-triple-chain
What it tests: Three chained calls with hooks between each, all three memoized independently
Fixture: ...-arg-in-hook
What it tests: Arg flows into the hook itself, both calls memoized
Fixture: ...-non-call-arg
What it tests: Arg is a function parameter (not a call result), memoized without the optimization
Fixture: ...-non-call-use
What it tests: Arg used in non-call context (array literal), memoized, confirms bug only manifests through unknown-call path
This PR fixes a React Compiler memoization regression where two chained calls separated by an unrelated hook call were not memoized due to an overly conservative unknown-call fallback in InferMutationAliasingEffects.
The change adds a function-level prepass (collectOperandUsageInfo) to compute declaration-root/usage metadata and uses it to narrowly skip emitting MutateTransitiveConditionally for certain call operands when (1) the intermediate value is only ever used as call arguments, (2) its root comes from a call/new/method-call, (3) there is at least one hook call between the root definition and the current call, and (4) the value is not used after the current call. Several new compiler fixtures cover the repro and boundary conditions.
Confidence Score: 5/5
This PR is safe to merge with minimal risk.
Change is narrowly scoped behind multiple guards, only activates when hook calls exist, and includes targeted fixtures that cover the repro and boundary cases. No clear correctness regressions were identified in the updated inference logic.
Adds an operand-usage prepass to selectively skip conservative transitive-mutation marking for certain call args across interleaved hook calls (#35237).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Mirror of facebook/react#35756
Original author: glinf
Summary
Fixes memoization instability reported in #35237, where two chained calls separated by an unrelated hook call are not memoized.
The compiler produced non-memoized output for code like:
Root cause: In
InferMutationAliasingEffects, the unknown-call fallback conservatively emitsMutateTransitiveConditionallyfor call operands. For chained call results separated by a hook, this over-approximates mutation on the intermediate value and extends its mutable range across the interleaved hook call, preventing scope construction from memoizing either call.Fix:
Add a pre-pass (
collectOperandUsageInfo) that collects declaration usage metadata, then skip the conservativeMutateTransitiveConditionallyfor an arg only when all of the following hold:This keeps the conservative behavior everywhere else and only avoids the specific over-approximation that breaks memoization in this pattern.
The pre-pass is skipped entirely for functions with no hook calls, and uses path-compressed union-find and binary search for efficiency (it still has a perf impact tho)
How did you test this change?
Ran the full compiler fixture suite (1844 tests, 0 failures):
yarn snap # 1844 Tests, 1844 Passed, 0 FailedAdded 6 fixture tests covering the fix and its boundary conditions:
Fixture: repro-hook-interleaved-chained-call-memoization
What it tests: Original repro, both func(bar) and result(data, value) are now memoized
Fixture: ...-arg-used-after
What it tests: Arg used after the second call — optimization correctly does NOT fire, preserving conservative mutation
Fixture: ...-triple-chain
What it tests: Three chained calls with hooks between each, all three memoized independently
Fixture: ...-arg-in-hook
What it tests: Arg flows into the hook itself, both calls memoized
Fixture: ...-non-call-arg
What it tests: Arg is a function parameter (not a call result), memoized without the optimization
Fixture: ...-non-call-use
What it tests: Arg used in non-call context (array literal), memoized, confirms bug only manifests through unknown-call path