Skip to content

Commit d39a1d6

Browse files
authored
[compiler] Distingush optional/extraneous deps (facebook#35204)
In ValidateExhaustiveDependencies, I previously changed to allow extraneous dependencies as long as they were non-reactive. Here we make that more precise, and distinguish between values that are definitely referenced in the memo function but optional as dependencies vs values that are not even referenced in the memo function. The latter now error as extraneous even if they're non-reactive. This also turned up a case where constant-folded primitives could show up as false positives of the latter category, so now we track manual deps which quality for constant folding and don't error on them. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35204). * facebook#35213 * facebook#35201 * __->__ facebook#35204
1 parent 16e16ec commit d39a1d6

15 files changed

+169
-38
lines changed

compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -803,6 +803,7 @@ export type ManualMemoDependency = {
803803
| {
804804
kind: 'NamedLocal';
805805
value: Place;
806+
constant: boolean;
806807
}
807808
| {kind: 'Global'; identifierName: string};
808809
path: DependencyPath;

compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ export function collectMaybeMemoDependencies(
9292
root: {
9393
kind: 'NamedLocal',
9494
value: {...value.place},
95+
constant: false,
9596
},
9697
path: [],
9798
};

compiler/packages/babel-plugin-react-compiler/src/Optimization/ConstantPropagation.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -609,6 +609,19 @@ function evaluateInstruction(
609609
constantPropagationImpl(value.loweredFunc.func, constants);
610610
return null;
611611
}
612+
case 'StartMemoize': {
613+
if (value.deps != null) {
614+
for (const dep of value.deps) {
615+
if (dep.root.kind === 'NamedLocal') {
616+
const placeValue = read(constants, dep.root.value);
617+
if (placeValue != null && placeValue.kind === 'Primitive') {
618+
dep.root.constant = true;
619+
}
620+
}
621+
}
622+
}
623+
return null;
624+
}
612625
default: {
613626
// TODO: handle more cases
614627
return null;

compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateExhaustiveDependencies.ts

Lines changed: 35 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
Identifier,
2323
IdentifierId,
2424
InstructionKind,
25+
isPrimitiveType,
2526
isStableType,
2627
isSubPath,
2728
isSubPathIgnoringOptionals,
@@ -53,20 +54,18 @@ const DEBUG = false;
5354
* - If the manual dependencies had extraneous deps, then auto memoization
5455
* will remove them and cause the value to update *less* frequently.
5556
*
56-
* We consider a value V as missing if ALL of the following conditions are met:
57-
* - V is reactive
58-
* - There is no manual dependency path P such that whenever V would change,
59-
* P would also change. If V is `x.y.z`, this means there must be some
60-
* path P that is either `x.y.z`, `x.y`, or `x`. Note that we assume no
61-
* interior mutability, such that a shorter path "covers" changes to longer
62-
* more precise paths.
63-
*
64-
* We consider a value V extraneous if either of the folowing are true:
65-
* - V is a reactive local that is unreferenced
66-
* - V is a global that is unreferenced
67-
*
68-
* In other words, we allow extraneous non-reactive values since we know they cannot
69-
* impact how often the memoization would run.
57+
* The implementation compares the manual dependencies against the values
58+
* actually used within the memoization function
59+
* - For each value V referenced in the memo function, either:
60+
* - If the value is non-reactive *and* a known stable type, then the
61+
* value may optionally be specified as an exact dependency.
62+
* - Otherwise, report an error unless there is a manual dependency that will
63+
* invalidate whenever V invalidates. If `x.y.z` is referenced, there must
64+
* be a manual dependency for `x.y.z`, `x.y`, or `x`. Note that we assume
65+
* no interior mutability, ie we assume that any changes to inner paths must
66+
* always cause the other path to change as well.
67+
* - Any dependencies that do not correspond to a value referenced in the memo
68+
* function are considered extraneous and throw an error
7069
*
7170
* ## TODO: Invalid, Complex Deps
7271
*
@@ -226,9 +225,6 @@ export function validateExhaustiveDependencies(
226225
reason: 'Unexpected function dependency',
227226
loc: value.loc,
228227
});
229-
const isRequiredDependency = reactive.has(
230-
inferredDependency.identifier.id,
231-
);
232228
let hasMatchingManualDependency = false;
233229
for (const manualDependency of manualDependencies) {
234230
if (
@@ -243,32 +239,40 @@ export function validateExhaustiveDependencies(
243239
) {
244240
hasMatchingManualDependency = true;
245241
matched.add(manualDependency);
246-
if (!isRequiredDependency) {
247-
extra.push(manualDependency);
248-
}
249242
}
250243
}
251-
if (isRequiredDependency && !hasMatchingManualDependency) {
252-
missing.push(inferredDependency);
244+
const isOptionalDependency =
245+
!reactive.has(inferredDependency.identifier.id) &&
246+
(isStableType(inferredDependency.identifier) ||
247+
isPrimitiveType(inferredDependency.identifier));
248+
if (hasMatchingManualDependency || isOptionalDependency) {
249+
continue;
253250
}
251+
missing.push(inferredDependency);
254252
}
255253

256254
for (const dep of startMemo.deps ?? []) {
257255
if (matched.has(dep)) {
258256
continue;
259257
}
258+
if (dep.root.kind === 'NamedLocal' && dep.root.constant) {
259+
CompilerError.simpleInvariant(
260+
!dep.root.value.reactive &&
261+
isPrimitiveType(dep.root.value.identifier),
262+
{
263+
reason: 'Expected constant-folded dependency to be non-reactive',
264+
loc: dep.root.value.loc,
265+
},
266+
);
267+
/*
268+
* Constant primitives can get constant-folded, which means we won't
269+
* see a LoadLocal for the value within the memo function.
270+
*/
271+
continue;
272+
}
260273
extra.push(dep);
261274
}
262275

263-
/**
264-
* Per docblock, we only consider dependencies as extraneous if
265-
* they are unused globals or reactive locals. Notably, this allows
266-
* non-reactive locals.
267-
*/
268-
retainWhere(extra, dep => {
269-
return dep.root.kind === 'Global' || dep.root.value.reactive;
270-
});
271-
272276
if (missing.length !== 0 || extra.length !== 0) {
273277
let suggestions: Array<CompilerSuggestion> | null = null;
274278
if (startMemo.depsLoc != null && typeof startMemo.depsLoc !== 'symbol') {

compiler/packages/babel-plugin-react-compiler/src/Validation/ValidatePreservedManualMemoization.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@ function validateInferredDep(
267267
effect: Effect.Read,
268268
reactive: false,
269269
},
270+
constant: false,
270271
},
271272
path: [...dep.path],
272273
};
@@ -379,6 +380,7 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
379380
root: {
380381
kind: 'NamedLocal',
381382
value: storeTarget,
383+
constant: false,
382384
},
383385
path: [],
384386
});
@@ -408,6 +410,7 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
408410
root: {
409411
kind: 'NamedLocal',
410412
value: {...lvalue},
413+
constant: false,
411414
},
412415
path: [],
413416
});

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/context-variable-as-jsx-element-tag.expect.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ function Component(props) {
1111

1212
Component = useMemo(() => {
1313
return Component;
14-
});
14+
}, [Component]);
1515

1616
return <Component {...props} />;
1717
}
@@ -36,6 +36,7 @@ function Component(props) {
3636
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
3737
Component = Stringify;
3838

39+
Component;
3940
Component = Component;
4041
$[0] = Component;
4142
} else {

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/context-variable-as-jsx-element-tag.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ function Component(props) {
77

88
Component = useMemo(() => {
99
return Component;
10-
});
10+
}, [Component]);
1111

1212
return <Component {...props} />;
1313
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
2+
## Input
3+
4+
```javascript
5+
// @validateExhaustiveMemoizationDependencies
6+
7+
import {useState} from 'react';
8+
import {Stringify} from 'shared-runtime';
9+
10+
function Component() {
11+
const [state, setState] = useState(0);
12+
const x = useMemo(() => {
13+
return [state];
14+
// error: `setState` is a stable type, but not actually referenced
15+
}, [state, setState]);
16+
17+
return 'oops';
18+
}
19+
20+
```
21+
22+
23+
## Error
24+
25+
```
26+
Found 1 error:
27+
28+
Error: Found unnecessary memoization dependencies
29+
30+
Unnecessary dependencies can cause a value to update more often than necessary, causing performance regressions and effects to fire more often than expected.
31+
32+
error.invalid-exhaustive-deps-disallow-unused-stable-types.ts:11:5
33+
9 | return [state];
34+
10 | // error: `setState` is a stable type, but not actually referenced
35+
> 11 | }, [state, setState]);
36+
| ^^^^^^^^^^^^^^^^^ Unnecessary dependencies `setState`
37+
12 |
38+
13 | return 'oops';
39+
14 | }
40+
```
41+
42+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// @validateExhaustiveMemoizationDependencies
2+
3+
import {useState} from 'react';
4+
import {Stringify} from 'shared-runtime';
5+
6+
function Component() {
7+
const [state, setState] = useState(0);
8+
const x = useMemo(() => {
9+
return [state];
10+
// error: `setState` is a stable type, but not actually referenced
11+
}, [state, setState]);
12+
13+
return 'oops';
14+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
2+
## Input
3+
4+
```javascript
5+
// @validateExhaustiveMemoizationDependencies
6+
7+
function Component() {
8+
const x = 0;
9+
const y = useMemo(() => {
10+
return [x];
11+
// x gets constant-folded but shouldn't count as extraneous,
12+
// it was referenced in the memo block
13+
}, [x]);
14+
return y;
15+
}
16+
17+
```
18+
19+
## Code
20+
21+
```javascript
22+
import { c as _c } from "react/compiler-runtime"; // @validateExhaustiveMemoizationDependencies
23+
24+
function Component() {
25+
const $ = _c(1);
26+
const x = 0;
27+
let t0;
28+
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
29+
t0 = [0];
30+
$[0] = t0;
31+
} else {
32+
t0 = $[0];
33+
}
34+
const y = t0;
35+
return y;
36+
}
37+
38+
```
39+
40+
### Eval output
41+
(kind: exception) Fixture not implemented

0 commit comments

Comments
 (0)