Skip to content

[compiler] Fix false negative: independently memoize values used as method call receivers#35976

Closed
Felipeness wants to merge 1 commit intofacebook:mainfrom
Felipeness:fix/compiler-memoize-unknown-receiver-methods
Closed

[compiler] Fix false negative: independently memoize values used as method call receivers#35976
Felipeness wants to merge 1 commit intofacebook:mainfrom
Felipeness:fix/compiler-memoize-unknown-receiver-methods

Conversation

@Felipeness
Copy link
Copy Markdown

Summary

Fixes #35902

When a method like .map() is called on a value with an unknown type (e.g. the return value of an unknown function like expensiveProcessing(data)), the compiler had no function signature available. This caused the fallback in InferMutationAliasingEffects to apply MutateTransitiveConditionally on the receiver, over-extending its mutable range and merging it into the same reactive scope as the method call result.

Before (bug): expensiveProcessing(data) re-executes when onClick changes:

if ($[0] !== data || $[1] !== onClick) {
  const processedData = expensiveProcessing(data); // unnecessary!
  t1 = processedData.map(t2);
}

After (fix): expensiveProcessing(data) has its own scope depending only on data:

if ($[0] !== data) {
  t1 = expensiveProcessing(data);
}
const processedData = t1;
if ($[2] !== onClick || $[3] !== processedData) {
  t2 = <ul>{processedData.map(...)}</ul>;
}

Root cause

In getPropertyType(), when the receiver has an unresolved type (Type kind), property lookups returned null — meaning no function signature was available for the method call. Without a signature, InferMutationAliasingEffects falls back to MutateTransitiveConditionally on all operands including the receiver, which extends its mutable range through the method call. This causes InferReactiveScopeVariables (union-find) to group the receiver's creation and the method call into the same reactive scope.

Fix

Added a fallback in getPropertyType: when the receiver has an unresolved type, try to resolve the property from the BuiltInArray shape — but only if the method also exists in BuiltInMixedReadonly (the safe, non-mutating subset). This follows the same reasoning already documented in ObjectShape.ts for BuiltInMixedReadonlyId:

"if .map() is called on an unknown type, the value must be array-like"

We use the BuiltInArray shape (not BuiltInMixedReadonly) because it has aliasing signatures that correctly model data flow without over-extending mutable ranges. The MixedReadonly guard ensures that mutating methods like .push() and .pop() — which only exist in BuiltInArray — are NOT resolved through this fallback, preserving the conservative behavior for those cases.

Test plan

  • Added fixture bug-independent-memoization-for-unknown-receiver-method-call demonstrating the fix
  • Verified 1090 existing fixtures produce identical output (zero regressions)
  • Manually tested: .push() (mutating, not in MixedReadonly) still uses conservative fallback
  • Manually tested: .filter(), .map() on unknown types now correctly separate scopes
  • Manually tested: custom methods (not in any shape) still use conservative fallback

When a method like `.map()` is called on a value with an unknown type
(e.g., the return value of an unknown function), the compiler previously
had no function signature available. This caused the fallback in
`InferMutationAliasingEffects` to apply `MutateTransitiveConditionally`
on the receiver, over-extending its mutable range and merging it into
the same reactive scope as the method call result.

This meant that `expensiveProcessing(data)` would be re-executed whenever
`onClick` changed, even though it only depends on `data`.

The fix adds a fallback in `getPropertyType`: when the receiver has an
unresolved type (`Type` kind), try to resolve the property from the
`BuiltInArray` shape — but only if the method also exists in the
`BuiltInMixedReadonly` shape (the safe, non-mutating subset). This gives
the compiler access to Array's aliasing signatures which correctly model
data flow without over-extending mutable ranges.

Fixes facebook#35902
@meta-cla
Copy link
Copy Markdown

meta-cla bot commented Mar 8, 2026

Hi @Felipeness!

Thank you for your pull request and welcome to our community.

Action Required

In order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you.

Process

In order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA.

Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with CLA signed. The tagging process may take up to 1 hour after signing. Please give it that time before contacting us about it.

If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks!

@meta-cla meta-cla bot added the CLA Signed label Mar 8, 2026
@meta-cla
Copy link
Copy Markdown

meta-cla bot commented Mar 8, 2026

Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks!

Copy link
Copy Markdown
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.

This isn’t safe. I believe in commented on a similar PR to this one, but tldr is that if we don’t know the type, then we don’t know that it’s map/filter/etc method is actually non-mutating.

@josephsavona
Copy link
Copy Markdown
Member

Closing since there is no variant of this that works safely. The long-term solution that we’re exploring is using information from type systems (Flow, hopefully also TS) in order to have more information to guide compilation.

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.

[Compiler Bug]: false negative, compiler skips memoizing a variable that should be memoized

2 participants