mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
[wip] Early exploration of optional deps
[ghstack-poisoned]
This commit is contained in:
@@ -1494,7 +1494,7 @@ export type ReactiveScopeDeclaration = {
|
||||
|
||||
export type ReactiveScopeDependency = {
|
||||
identifier: Identifier;
|
||||
path: Array<string>;
|
||||
path: Array<{property: string; optional: boolean}>;
|
||||
};
|
||||
|
||||
/*
|
||||
|
||||
+99
-9
@@ -5,10 +5,12 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {PATTERNLIKE_TYPES} from '@babel/types';
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {Identifier, ReactiveScopeDependency} from '../HIR';
|
||||
import {printIdentifier} from '../HIR/PrintHIR';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
import {printReactiveScopeSummary} from './PrintReactiveFunction';
|
||||
|
||||
/*
|
||||
* We need to understand optional member expressions only when determining
|
||||
@@ -51,7 +53,9 @@ export type ReactiveScopePropertyDependency = ReactiveScopeDependency & {
|
||||
* @param initialDeps
|
||||
* @returns
|
||||
*/
|
||||
let nextId = 0;
|
||||
export class ReactiveScopeDependencyTree {
|
||||
#inst: number = nextId++;
|
||||
#roots: Map<Identifier, DependencyNode> = new Map();
|
||||
|
||||
#getOrCreateRoot(identifier: Identifier): DependencyNode {
|
||||
@@ -69,6 +73,11 @@ export class ReactiveScopeDependencyTree {
|
||||
}
|
||||
|
||||
add(dep: ReactiveScopePropertyDependency, inConditional: boolean): void {
|
||||
// console.log(
|
||||
// `add(${this.#inst}): ${printIdentifier(dep.identifier)}${dep.path.length ? `.${dep.path.join('.')}` : ''}${dep.optionalPath.length ? `?.${dep.optionalPath.join('?.')}` : ''} inConditional=${inConditional}`,
|
||||
// );
|
||||
// console.log(this.debug());
|
||||
// console.log();
|
||||
const {path, optionalPath} = dep;
|
||||
let currNode = this.#getOrCreateRoot(dep.identifier);
|
||||
|
||||
@@ -76,7 +85,7 @@ export class ReactiveScopeDependencyTree {
|
||||
? PropertyAccessType.ConditionalAccess
|
||||
: PropertyAccessType.UnconditionalAccess;
|
||||
|
||||
for (const property of path) {
|
||||
for (const {property} of path) {
|
||||
// all properties read 'on the way' to a dependency are marked as 'access'
|
||||
let currChild = getOrMakeProperty(currNode, property);
|
||||
currChild.accessType = merge(currChild.accessType, accessType);
|
||||
@@ -111,7 +120,10 @@ export class ReactiveScopeDependencyTree {
|
||||
let currChild = getOrMakeProperty(currNode, property);
|
||||
currChild.accessType = merge(
|
||||
currChild.accessType,
|
||||
PropertyAccessType.ConditionalAccess,
|
||||
// Conditional access takes precedence over optional access
|
||||
inConditional
|
||||
? PropertyAccessType.ConditionalAccess
|
||||
: PropertyAccessType.OptionalAccess,
|
||||
);
|
||||
currNode = currChild;
|
||||
}
|
||||
@@ -119,18 +131,24 @@ export class ReactiveScopeDependencyTree {
|
||||
// The final node should be marked as a conditional dependency.
|
||||
currNode.accessType = merge(
|
||||
currNode.accessType,
|
||||
PropertyAccessType.ConditionalDependency,
|
||||
// Conditional access takes precedence over optional access
|
||||
inConditional
|
||||
? PropertyAccessType.ConditionalDependency
|
||||
: PropertyAccessType.OptionalDependency,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
deriveMinimalDependencies(): Set<ReactiveScopeDependency> {
|
||||
console.log(this.debug());
|
||||
const results = new Set<ReactiveScopeDependency>();
|
||||
for (const [rootId, rootNode] of this.#roots.entries()) {
|
||||
const deps = deriveMinimalDependenciesInSubtree(rootNode);
|
||||
CompilerError.invariant(
|
||||
deps.every(
|
||||
dep => dep.accessType === PropertyAccessType.UnconditionalDependency,
|
||||
dep =>
|
||||
dep.accessType === PropertyAccessType.UnconditionalDependency ||
|
||||
dep.accessType === PropertyAccessType.OptionalDependency,
|
||||
),
|
||||
{
|
||||
reason:
|
||||
@@ -215,6 +233,27 @@ export class ReactiveScopeDependencyTree {
|
||||
}
|
||||
return res.flat().join('\n');
|
||||
}
|
||||
|
||||
debug(): string {
|
||||
const buf: Array<string> = [`tree(${this.#inst}) [`];
|
||||
for (const [rootId, rootNode] of this.#roots) {
|
||||
buf.push(`${printIdentifier(rootId)} (${rootNode.accessType}):`);
|
||||
this.#debugImpl(buf, rootNode, 1);
|
||||
}
|
||||
buf.push(']');
|
||||
return buf.length > 2 ? buf.join('\n') : buf.join('');
|
||||
}
|
||||
|
||||
#debugImpl(
|
||||
buf: Array<string>,
|
||||
node: DependencyNode,
|
||||
depth: number = 0,
|
||||
): void {
|
||||
for (const [property, childNode] of node.properties) {
|
||||
buf.push(`${' '.repeat(depth)}.${property} (${childNode.accessType}):`);
|
||||
this.#debugImpl(buf, childNode, depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -237,6 +276,8 @@ export class ReactiveScopeDependencyTree {
|
||||
* ```
|
||||
*/
|
||||
enum PropertyAccessType {
|
||||
OptionalAccess = 'OptionalAccess',
|
||||
OptionalDependency = 'OptionalDependency',
|
||||
ConditionalAccess = 'ConditionalAccess',
|
||||
UnconditionalAccess = 'UnconditionalAccess',
|
||||
ConditionalDependency = 'ConditionalDependency',
|
||||
@@ -253,7 +294,20 @@ function isUnconditional(access: PropertyAccessType): boolean {
|
||||
function isDependency(access: PropertyAccessType): boolean {
|
||||
return (
|
||||
access === PropertyAccessType.ConditionalDependency ||
|
||||
access === PropertyAccessType.UnconditionalDependency
|
||||
access === PropertyAccessType.UnconditionalDependency ||
|
||||
access === PropertyAccessType.OptionalDependency
|
||||
);
|
||||
}
|
||||
function isOptional(access: PropertyAccessType): boolean {
|
||||
return (
|
||||
access === PropertyAccessType.OptionalAccess ||
|
||||
access == PropertyAccessType.OptionalDependency
|
||||
);
|
||||
}
|
||||
function _isConditional(access: PropertyAccessType): boolean {
|
||||
return (
|
||||
access === PropertyAccessType.ConditionalAccess ||
|
||||
access === PropertyAccessType.ConditionalDependency
|
||||
);
|
||||
}
|
||||
|
||||
@@ -264,6 +318,7 @@ function merge(
|
||||
const resultIsUnconditional =
|
||||
isUnconditional(access1) || isUnconditional(access2);
|
||||
const resultIsDependency = isDependency(access1) || isDependency(access2);
|
||||
const resultIsOptional = isOptional(access1) || isOptional(access2);
|
||||
|
||||
/*
|
||||
* Straightforward merge.
|
||||
@@ -279,6 +334,12 @@ function merge(
|
||||
} else {
|
||||
return PropertyAccessType.UnconditionalAccess;
|
||||
}
|
||||
} else if (resultIsOptional) {
|
||||
if (resultIsDependency) {
|
||||
return PropertyAccessType.OptionalDependency;
|
||||
} else {
|
||||
return PropertyAccessType.OptionalAccess;
|
||||
}
|
||||
} else {
|
||||
if (resultIsDependency) {
|
||||
return PropertyAccessType.ConditionalDependency;
|
||||
@@ -294,24 +355,31 @@ type DependencyNode = {
|
||||
};
|
||||
|
||||
type ReduceResultNode = {
|
||||
relativePath: Array<string>;
|
||||
relativePath: Array<{property: string; optional: boolean}>;
|
||||
accessType: PropertyAccessType;
|
||||
};
|
||||
|
||||
const promoteUncondResult = [
|
||||
const promoteUncondResult: Array<ReduceResultNode> = [
|
||||
{
|
||||
relativePath: [],
|
||||
accessType: PropertyAccessType.UnconditionalDependency,
|
||||
},
|
||||
];
|
||||
|
||||
const promoteCondResult = [
|
||||
const promoteCondResult: Array<ReduceResultNode> = [
|
||||
{
|
||||
relativePath: [],
|
||||
accessType: PropertyAccessType.ConditionalDependency,
|
||||
},
|
||||
];
|
||||
|
||||
const promoteOptionalResult: Array<ReduceResultNode> = [
|
||||
{
|
||||
relativePath: [],
|
||||
accessType: PropertyAccessType.OptionalDependency,
|
||||
},
|
||||
];
|
||||
|
||||
/*
|
||||
* Recursively calculates minimal dependencies in a subtree.
|
||||
* @param dep DependencyNode representing a dependency subtree.
|
||||
@@ -337,11 +405,33 @@ function deriveMinimalDependenciesInSubtree(
|
||||
case PropertyAccessType.UnconditionalDependency: {
|
||||
return promoteUncondResult;
|
||||
}
|
||||
case PropertyAccessType.OptionalDependency: {
|
||||
if (results.length === 0) {
|
||||
return promoteOptionalResult;
|
||||
} else if (
|
||||
results.every(
|
||||
({accessType}) =>
|
||||
accessType === PropertyAccessType.UnconditionalDependency ||
|
||||
accessType === PropertyAccessType.OptionalDependency,
|
||||
)
|
||||
) {
|
||||
// all children are unconditional dependencies, return them to preserve granularity
|
||||
return results;
|
||||
} else {
|
||||
/*
|
||||
* at least one child is accessed conditionally, so this node needs to be promoted to
|
||||
* unconditional dependency
|
||||
*/
|
||||
return promoteUncondResult;
|
||||
}
|
||||
}
|
||||
case PropertyAccessType.OptionalAccess:
|
||||
case PropertyAccessType.UnconditionalAccess: {
|
||||
if (
|
||||
results.every(
|
||||
({accessType}) =>
|
||||
accessType === PropertyAccessType.UnconditionalDependency,
|
||||
accessType === PropertyAccessType.UnconditionalDependency ||
|
||||
accessType === PropertyAccessType.OptionalDependency,
|
||||
)
|
||||
) {
|
||||
// all children are unconditional dependencies, return them to preserve granularity
|
||||
|
||||
+11
-2
@@ -525,8 +525,17 @@ function areEqualDependencies(
|
||||
return true;
|
||||
}
|
||||
|
||||
export function areEqualPaths(a: Array<string>, b: Array<string>): boolean {
|
||||
return a.length === b.length && a.every((item, ix) => item === b[ix]);
|
||||
export function areEqualPaths(
|
||||
a: Array<{property: string; optional: boolean}>,
|
||||
b: Array<{property: string; optional: boolean}>,
|
||||
): boolean {
|
||||
return (
|
||||
a.length === b.length &&
|
||||
a.every(
|
||||
(item, ix) =>
|
||||
item.property === b[ix].property && item.optional === b[ix].optional,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+92
-7
@@ -5,12 +5,14 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import prettyFormat from 'pretty-format';
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {
|
||||
BlockId,
|
||||
DeclarationId,
|
||||
GeneratedSource,
|
||||
Identifier,
|
||||
IdentifierId,
|
||||
InstructionId,
|
||||
InstructionKind,
|
||||
isObjectMethodType,
|
||||
@@ -28,6 +30,7 @@ import {
|
||||
ReactiveValue,
|
||||
ScopeId,
|
||||
} from '../HIR/HIR';
|
||||
import {printIdentifier, printInstruction, printPlace} from '../HIR/PrintHIR';
|
||||
import {eachInstructionValueOperand, eachPatternOperand} from '../HIR/visitors';
|
||||
import {empty, Stack} from '../Utils/Stack';
|
||||
import {assertExhaustive, Iterable_some} from '../Utils/utils';
|
||||
@@ -36,6 +39,11 @@ import {
|
||||
ReactiveScopePropertyDependency,
|
||||
} from './DeriveMinimalDependencies';
|
||||
import {areEqualPaths} from './MergeReactiveScopesThatInvalidateTogether';
|
||||
import {
|
||||
printDependency,
|
||||
printReactiveFunction,
|
||||
printReactiveValue,
|
||||
} from './PrintReactiveFunction';
|
||||
import {ReactiveFunctionVisitor, visitReactiveFunction} from './visitors';
|
||||
|
||||
/*
|
||||
@@ -45,6 +53,7 @@ import {ReactiveFunctionVisitor, visitReactiveFunction} from './visitors';
|
||||
* their direct dependencies and those of their child scopes.
|
||||
*/
|
||||
export function propagateScopeDependencies(fn: ReactiveFunction): void {
|
||||
console.log();
|
||||
const escapingTemporaries: TemporariesUsedOutsideDefiningScope = {
|
||||
declarations: new Map(),
|
||||
usedOutsideDeclaringScope: new Set(),
|
||||
@@ -70,6 +79,7 @@ export function propagateScopeDependencies(fn: ReactiveFunction): void {
|
||||
new PropagationVisitor(fn.env.config.enableTreatFunctionDepsAsConditional),
|
||||
context,
|
||||
);
|
||||
console.log(printReactiveFunction(fn));
|
||||
}
|
||||
|
||||
type TemporariesUsedOutsideDefiningScope = {
|
||||
@@ -302,6 +312,8 @@ class Context {
|
||||
#properties: Map<Identifier, ReactiveScopePropertyDependency> = new Map();
|
||||
#temporaries: Map<Identifier, Place> = new Map();
|
||||
#inConditionalWithinScope: boolean = false;
|
||||
#optionalValues: Set<IdentifierId> = new Set();
|
||||
|
||||
/*
|
||||
* Reactive dependencies used unconditionally in the current conditional.
|
||||
* Composed of dependencies:
|
||||
@@ -354,8 +366,12 @@ class Context {
|
||||
this.#inConditionalWithinScope = prevInConditional;
|
||||
|
||||
// Derive minimal dependencies now, since next line may mutate scopedDependencies
|
||||
console.log(`deps for @${scope.id}`);
|
||||
const minInnerScopeDependencies =
|
||||
scopedDependencies.deriveMinimalDependencies();
|
||||
for (const dep of minInnerScopeDependencies) {
|
||||
console.log(printDependency(dep));
|
||||
}
|
||||
|
||||
/*
|
||||
* propagate dependencies upward using the same rules as normal dependency
|
||||
@@ -424,6 +440,23 @@ class Context {
|
||||
return result;
|
||||
}
|
||||
|
||||
enterOptional(
|
||||
lhs: Place,
|
||||
optional: boolean,
|
||||
fn: () => void,
|
||||
): ReactiveScopeDependencyTree {
|
||||
if (!optional) {
|
||||
return this.enterConditional(fn);
|
||||
}
|
||||
const previousOptionals = this.#optionalValues;
|
||||
this.#optionalValues = new Set(this.#optionalValues);
|
||||
this.#optionalValues.add(lhs.identifier.id);
|
||||
fn();
|
||||
const result = this.#depsInCurrentConditional;
|
||||
this.#optionalValues = previousOptionals;
|
||||
return result;
|
||||
}
|
||||
|
||||
/*
|
||||
* Add dependencies from exhaustive CFG paths into the current ReactiveDeps
|
||||
* tree. If a property is used in every CFG path, it is promoted to an
|
||||
@@ -465,7 +498,6 @@ class Context {
|
||||
#getProperty(
|
||||
object: Place,
|
||||
property: string,
|
||||
isConditional: boolean,
|
||||
): ReactiveScopePropertyDependency {
|
||||
const resolvedObject = this.resolveTemporary(object);
|
||||
const resolvedDependency = this.#properties.get(resolvedObject.identifier);
|
||||
@@ -497,17 +529,17 @@ class Context {
|
||||
* e.g. for `a.b?.c.d`, `d` should be added to optionalPath
|
||||
*/
|
||||
objectDependency.optionalPath.push(property);
|
||||
} else if (isConditional) {
|
||||
} else if (this.#optionalValues.has(object.identifier.id)) {
|
||||
objectDependency.optionalPath.push(property);
|
||||
} else {
|
||||
objectDependency.path.push(property);
|
||||
objectDependency.path.push({property, optional: false});
|
||||
}
|
||||
|
||||
return objectDependency;
|
||||
}
|
||||
|
||||
declareProperty(lvalue: Place, object: Place, property: string): void {
|
||||
const nextDependency = this.#getProperty(object, property, false);
|
||||
const nextDependency = this.#getProperty(object, property);
|
||||
this.#properties.set(lvalue.identifier, nextDependency);
|
||||
}
|
||||
|
||||
@@ -516,7 +548,7 @@ class Context {
|
||||
// ref.current access is not a valid dep
|
||||
if (
|
||||
isUseRefType(maybeDependency.identifier) &&
|
||||
maybeDependency.path.at(0) === 'current'
|
||||
maybeDependency.path.at(0)?.property === 'current'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
@@ -589,7 +621,7 @@ class Context {
|
||||
}
|
||||
|
||||
visitProperty(object: Place, property: string): void {
|
||||
const nextDependency = this.#getProperty(object, property, false);
|
||||
const nextDependency = this.#getProperty(object, property);
|
||||
this.visitDependency(nextDependency);
|
||||
}
|
||||
|
||||
@@ -785,8 +817,17 @@ class PropagationVisitor extends ReactiveFunctionVisitor<Context> {
|
||||
for (const instr of inner.instructions) {
|
||||
this.visitInstruction(instr, context);
|
||||
}
|
||||
const lastInstruction = inner.instructions.at(-1);
|
||||
CompilerError.invariant(
|
||||
lastInstruction !== undefined && lastInstruction.lvalue !== null,
|
||||
{
|
||||
reason:
|
||||
'Expected OptionalExpresion to have an instruction with an lvalue',
|
||||
loc: value.loc,
|
||||
},
|
||||
);
|
||||
// The final value is the conditional portion following the `?`
|
||||
context.enterConditional(() => {
|
||||
context.enterOptional(lastInstruction.lvalue, value.optional, () => {
|
||||
this.visitReactiveValue(context, id, inner.value);
|
||||
});
|
||||
break;
|
||||
@@ -912,6 +953,50 @@ class PropagationVisitor extends ReactiveFunctionVisitor<Context> {
|
||||
scope: context.currentScope,
|
||||
});
|
||||
}
|
||||
} else if (value.kind === 'OptionalExpression') {
|
||||
const inner = value.value;
|
||||
/*
|
||||
* OptionalExpression value is a SequenceExpression where the instructions
|
||||
* represent the code prior to the `?` and the final value represents the
|
||||
* conditional code that follows.
|
||||
*/
|
||||
CompilerError.invariant(inner.kind === 'SequenceExpression', {
|
||||
reason: 'Expected OptionalExpression value to be a SequenceExpression',
|
||||
description: `Found a \`${value.kind}\``,
|
||||
loc: value.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
// Instructions are the unconditionally executed portion before the `?`
|
||||
for (const instr of inner.instructions) {
|
||||
this.visitInstruction(instr, context);
|
||||
}
|
||||
const lastInstruction = inner.instructions.at(-1);
|
||||
CompilerError.invariant(
|
||||
lastInstruction !== undefined && lastInstruction.lvalue !== null,
|
||||
{
|
||||
reason:
|
||||
'Expected OptionalExpresion to have an instruction with an lvalue',
|
||||
loc: value.loc,
|
||||
},
|
||||
);
|
||||
// The final value is the conditional portion following the `?`
|
||||
context.enterOptional(lastInstruction.lvalue, value.optional, () => {
|
||||
this.visitReactiveValue(context, id, inner.value);
|
||||
});
|
||||
let innerValue = inner.value;
|
||||
while (innerValue.kind === 'SequenceExpression') {
|
||||
innerValue = innerValue.value;
|
||||
}
|
||||
CompilerError.invariant(innerValue.kind === 'LoadLocal', {
|
||||
reason:
|
||||
'Expected OptionalExpression to end in a SequenceExpression with a LoadLocal',
|
||||
loc: innerValue.loc,
|
||||
});
|
||||
if (lvalue !== null && !context.isUsedOutsideDeclaringScope(lvalue)) {
|
||||
context.declareTemporary(lvalue, innerValue.place);
|
||||
} else {
|
||||
context.visitOperand(innerValue.place);
|
||||
}
|
||||
} else {
|
||||
this.visitReactiveValue(context, id, value);
|
||||
}
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @flow @validatePreserveExistingMemoizationGuarantees
|
||||
import {useMemo} from 'react';
|
||||
import {useFragment} from 'shared-runtime';
|
||||
|
||||
function useData({items}) {
|
||||
const data = useMemo(
|
||||
() => items?.edges?.nodes.map(item => <Item item={item} />),
|
||||
[items?.edges?.nodes]
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
5 | function useData({items}) {
|
||||
6 | const data = useMemo(
|
||||
> 7 | () => items?.edges?.nodes.map(item => <Item item={item} />),
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected (7:7)
|
||||
8 | [items?.edges?.nodes]
|
||||
9 | );
|
||||
10 | return data;
|
||||
```
|
||||
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
// @flow @validatePreserveExistingMemoizationGuarantees
|
||||
import {useMemo} from 'react';
|
||||
import {useFragment} from 'shared-runtime';
|
||||
|
||||
function useData({items}) {
|
||||
const data = useMemo(
|
||||
() => items?.edges?.nodes.map(item => <Item item={item} />),
|
||||
[items?.edges?.nodes]
|
||||
);
|
||||
return data;
|
||||
}
|
||||
Reference in New Issue
Block a user