Skip to content

compiler: fix interleaved hook chained-call memoization#499

Closed
everettbu wants to merge 1 commit into
mainfrom
fix/repro-hook-interleaved-chained-call-memoization
Closed

compiler: fix interleaved hook chained-call memoization#499
everettbu wants to merge 1 commit into
mainfrom
fix/repro-hook-interleaved-chained-call-memoization

Conversation

@everettbu

Copy link
Copy Markdown

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:

  const b = transform(a);
  useState(); // unrelated hook
  const c = transform(b);

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:

  1. The call result is consumed downstream
  2. The arg is used exclusively as call arguments
  3. The arg root is produced by a call/new/method-call
  4. A hook call exists between the arg root definition and the current call
  5. 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

@greptile-apps

greptile-apps Bot commented Feb 10, 2026

Copy link
Copy Markdown

Greptile Overview

Greptile Summary

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.
  • compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts

Important Files Changed

Filename Overview
compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts Adds an operand-usage prepass to selectively skip conservative transitive-mutation marking for certain call args across interleaved hook calls (#35237).
compiler/packages/babel-plugin-react-compiler/src/tests/fixtures/compiler/repro-hook-interleaved-chained-call-memoization.expect.md Updates fixture expected output to show both chained calls memoized across an interleaved hook call.
compiler/packages/babel-plugin-react-compiler/src/tests/fixtures/compiler/repro-hook-interleaved-chained-call-memoization.ts Adds a repro fixture demonstrating memoization across an interleaved hook call.
compiler/packages/babel-plugin-react-compiler/src/tests/fixtures/compiler/repro-hook-interleaved-chained-call-memoization-arg-in-hook.expect.md Adds expected output fixture where the intermediate value is passed into the hook call and memoization still occurs.
compiler/packages/babel-plugin-react-compiler/src/tests/fixtures/compiler/repro-hook-interleaved-chained-call-memoization-arg-in-hook.ts Adds fixture where arg flows into the hook itself.
compiler/packages/babel-plugin-react-compiler/src/tests/fixtures/compiler/repro-hook-interleaved-chained-call-memoization-arg-used-after.expect.md Adds expected output for boundary case where arg is used after second call (optimization should not apply).
compiler/packages/babel-plugin-react-compiler/src/tests/fixtures/compiler/repro-hook-interleaved-chained-call-memoization-arg-used-after.ts Adds boundary fixture where intermediate arg is used after second call.
compiler/packages/babel-plugin-react-compiler/src/tests/fixtures/compiler/repro-hook-interleaved-chained-call-memoization-non-call-arg.expect.md Adds expected output fixture for case where arg root is not produced by a call (function parameter).
compiler/packages/babel-plugin-react-compiler/src/tests/fixtures/compiler/repro-hook-interleaved-chained-call-memoization-non-call-arg.ts Adds fixture where arg is a parameter, ensuring optimization guard doesn't trigger.
compiler/packages/babel-plugin-react-compiler/src/tests/fixtures/compiler/repro-hook-interleaved-chained-call-memoization-non-call-use.expect.md Adds expected output fixture for case where arg is used in non-call context (array literal).
compiler/packages/babel-plugin-react-compiler/src/tests/fixtures/compiler/repro-hook-interleaved-chained-call-memoization-non-call-use.ts Adds fixture where arg is used in non-call context, avoiding the unknown-call conservative path.
compiler/packages/babel-plugin-react-compiler/src/tests/fixtures/compiler/repro-hook-interleaved-chained-call-memoization-triple-chain.expect.md Adds expected output fixture for three chained calls with hooks between each, all memoized.
compiler/packages/babel-plugin-react-compiler/src/tests/fixtures/compiler/repro-hook-interleaved-chained-call-memoization-triple-chain.ts Adds triple-chain fixture exercising multiple interleaved hooks.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

13 files reviewed, no comments

Edit Code Review Agent Settings | Greptile

@everettbu

Copy link
Copy Markdown
Author

Upstream PR was closed or merged. Code is synced via branch mirror.

@everettbu everettbu closed this Feb 10, 2026
@everettbu
everettbu deleted the fix/repro-hook-interleaved-chained-call-memoization branch February 10, 2026 17:33
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.

1 participant