mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
If a function captures a mutable value but never gets called, we don't infer a mutable range for that function. This means that we also don't alias the function with its mutable captures.
This case is tricky, because we don't generally know for sure what is a mutation and what may just be a normal function call. For example:
```js
hook useFoo() {
const x = makeObject();
return () => {
return readObject(x); // could be a mutation!
}
}
```
If we pessimistically assume that all such cases are mutations, we'd have to group lots of memo scopes together unnecessarily. However, if there is definitely a mutation:
```js
hook useFoo(createEntryForKey) {
const cache = new WeakMap();
return (key) => {
let entry = cache.get(key);
if (entry == null) {
entry = createEntryForKey(key);
cache.set(key, entry); // known mutation!
}
return entry;
}
}
```
Then we have to ensure that the function and its mutable captures alias together and end up in the same scope. However, aliasing together isn't enough if the function and operands all have empty mutable ranges (end = start + 1).
This pass finds function expressions and object methods that have an empty mutable range and known-mutable operands which also don't have a mutable range, and ensures that the function and those operands are aliased together *and* that their ranges are updated to end after the function expression. This is sufficient to ensure that a reactive scope is created for the alias set.
NOTE: The alternative is to reject these cases. If we do that we'd also want to similarly disallow cases like passing a mutable function to a hook.
ghstack-source-id: 5d8158246a
Pull Request resolved: https://github.com/facebook/react/pull/33078
103 lines
3.0 KiB
TypeScript
103 lines
3.0 KiB
TypeScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*/
|
|
|
|
import {HIRFunction, Identifier} from '../HIR/HIR';
|
|
import {inferAliasForUncalledFunctions} from './InerAliasForUncalledFunctions';
|
|
import {inferAliases} from './InferAlias';
|
|
import {inferAliasForPhis} from './InferAliasForPhis';
|
|
import {inferAliasForStores} from './InferAliasForStores';
|
|
import {inferMutableLifetimes} from './InferMutableLifetimes';
|
|
import {inferMutableRangesForAlias} from './InferMutableRangesForAlias';
|
|
import {inferTryCatchAliases} from './InferTryCatchAliases';
|
|
|
|
export function inferMutableRanges(ir: HIRFunction): void {
|
|
// Infer mutable ranges for non fields
|
|
inferMutableLifetimes(ir, false);
|
|
|
|
// Calculate aliases
|
|
const aliases = inferAliases(ir);
|
|
/*
|
|
* Calculate aliases for try/catch, where any value created
|
|
* in the try block could be aliased to the catch param
|
|
*/
|
|
inferTryCatchAliases(ir, aliases);
|
|
|
|
/*
|
|
* Eagerly canonicalize so that if nothing changes we can bail out
|
|
* after a single iteration
|
|
*/
|
|
let prevAliases: Map<Identifier, Identifier> = aliases.canonicalize();
|
|
while (true) {
|
|
// Infer mutable ranges for aliases that are not fields
|
|
inferMutableRangesForAlias(ir, aliases);
|
|
|
|
// Update aliasing information of fields
|
|
inferAliasForStores(ir, aliases);
|
|
|
|
// Update aliasing information of phis
|
|
inferAliasForPhis(ir, aliases);
|
|
|
|
const nextAliases = aliases.canonicalize();
|
|
if (areEqualMaps(prevAliases, nextAliases)) {
|
|
break;
|
|
}
|
|
prevAliases = nextAliases;
|
|
}
|
|
|
|
// Re-infer mutable ranges for all values
|
|
inferMutableLifetimes(ir, true);
|
|
|
|
/**
|
|
* The second inferMutableLifetimes() call updates mutable ranges
|
|
* of values to account for Store effects. Now we need to update
|
|
* all aliases of such values to extend their ranges as well. Note
|
|
* that the store only mutates the the directly aliased value and
|
|
* not any of its inner captured references. For example:
|
|
*
|
|
* ```
|
|
* let y;
|
|
* if (cond) {
|
|
* y = [];
|
|
* } else {
|
|
* y = [{}];
|
|
* }
|
|
* y.push(z);
|
|
* ```
|
|
*
|
|
* The Store effect from the `y.push` modifies the values that `y`
|
|
* directly aliases - the two arrays from the if/else branches -
|
|
* but does not modify values that `y` "contains" such as the
|
|
* object literal or `z`.
|
|
*/
|
|
prevAliases = aliases.canonicalize();
|
|
while (true) {
|
|
inferMutableRangesForAlias(ir, aliases);
|
|
inferAliasForPhis(ir, aliases);
|
|
inferAliasForUncalledFunctions(ir, aliases);
|
|
const nextAliases = aliases.canonicalize();
|
|
if (areEqualMaps(prevAliases, nextAliases)) {
|
|
break;
|
|
}
|
|
prevAliases = nextAliases;
|
|
}
|
|
}
|
|
|
|
function areEqualMaps<T>(a: Map<T, T>, b: Map<T, T>): boolean {
|
|
if (a.size !== b.size) {
|
|
return false;
|
|
}
|
|
for (const [key, value] of a) {
|
|
if (!b.has(key)) {
|
|
return false;
|
|
}
|
|
if (b.get(key) !== value) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|