Skip to content

Commit 842865e

Browse files
committed
[compiler] Distingush optional/extraneous deps
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.
1 parent 16e16ec commit 842865e

12 files changed

+64
-40
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: 37 additions & 32 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,
@@ -39,6 +40,7 @@ import {
3940
} from '../HIR/visitors';
4041
import {Result} from '../Utils/Result';
4142
import {retainWhere} from '../Utils/utils';
43+
import {printType} from '../HIR/PrintHIR';
4244

4345
const DEBUG = false;
4446

@@ -53,20 +55,18 @@ const DEBUG = false;
5355
* - If the manual dependencies had extraneous deps, then auto memoization
5456
* will remove them and cause the value to update *less* frequently.
5557
*
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.
58+
* The implementation compares the manual dependencies against the values
59+
* actually used within the memoization function
60+
* - For each value V referenced in the memo function, either:
61+
* - If the value is non-reactive *and* a known stable type, then the
62+
* value may optionally be specified as an exact dependency.
63+
* - Otherwise, report an error unless there is a manual dependency that will
64+
* invalidate whenever V invalidates. If `x.y.z` is referenced, there must
65+
* be a manual dependency for `x.y.z`, `x.y`, or `x`. Note that we assume
66+
* no interior mutability, ie we assume that any changes to inner paths must
67+
* always cause the other path to change as well.
68+
* - Any dependencies that do not correspond to a value referenced in the memo
69+
* function are considered extraneous and throw an error
7070
*
7171
* ## TODO: Invalid, Complex Deps
7272
*
@@ -226,9 +226,6 @@ export function validateExhaustiveDependencies(
226226
reason: 'Unexpected function dependency',
227227
loc: value.loc,
228228
});
229-
const isRequiredDependency = reactive.has(
230-
inferredDependency.identifier.id,
231-
);
232229
let hasMatchingManualDependency = false;
233230
for (const manualDependency of manualDependencies) {
234231
if (
@@ -243,32 +240,40 @@ export function validateExhaustiveDependencies(
243240
) {
244241
hasMatchingManualDependency = true;
245242
matched.add(manualDependency);
246-
if (!isRequiredDependency) {
247-
extra.push(manualDependency);
248-
}
249243
}
250244
}
251-
if (isRequiredDependency && !hasMatchingManualDependency) {
252-
missing.push(inferredDependency);
245+
const isOptionalDependency =
246+
!reactive.has(inferredDependency.identifier.id) &&
247+
(isStableType(inferredDependency.identifier) ||
248+
isPrimitiveType(inferredDependency.identifier));
249+
if (hasMatchingManualDependency || isOptionalDependency) {
250+
continue;
253251
}
252+
missing.push(inferredDependency);
254253
}
255254

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

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-
272277
if (missing.length !== 0 || extra.length !== 0) {
273278
let suggestions: Array<CompilerSuggestion> | null = null;
274279
if (startMemo.depsLoc != null && typeof startMemo.depsLoc !== 'symbol') {
@@ -313,7 +318,7 @@ export function validateExhaustiveDependencies(
313318
});
314319
diagnostic.withDetails({
315320
kind: 'error',
316-
message: `Unnecessary dependencies ${extra.map(dep => `\`${printManualMemoDependency(dep)}\``).join(', ')}`,
321+
message: `Unnecessary dependencies ${extra.map(dep => `\`${printManualMemoDependency(dep)}\` (${dep.root.kind === 'NamedLocal' ? printType(dep.root.value.identifier.type) : ''})`).join(', ')}`,
317322
loc: startMemo.depsLoc ?? value.loc,
318323
});
319324
error.pushDiagnostic(diagnostic);

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
}

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-exhaustive-deps.expect.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ error.invalid-exhaustive-deps.ts:31:5
8787
29 | return [];
8888
30 | // error: unnecessary
8989
> 31 | }, [x, y.z, z?.y?.a, UNUSED_GLOBAL]);
90-
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Unnecessary dependencies `x`, `y.z`, `z?.y?.a`, `UNUSED_GLOBAL`
90+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Unnecessary dependencies `x` (), `y.z` (), `z?.y?.a` (), `UNUSED_GLOBAL` ()
9191
32 | const ref1 = useRef(null);
9292
33 | const ref2 = useRef(null);
9393
34 | const ref = z ? ref1 : ref2;

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/todo-ensure-constant-prop-decls-get-removed.expect.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
## Input
33

44
```javascript
5-
// @validatePreserveExistingMemoizationGuarantees
5+
// @validatePreserveExistingMemoizationGuarantees @validateExhaustiveMemoizationDependencies:false
66

77
import {useMemo} from 'react';
88

@@ -27,7 +27,7 @@ export const FIXTURE_ENTRYPOINT = {
2727
## Code
2828

2929
```javascript
30-
import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees
30+
import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees @validateExhaustiveMemoizationDependencies:false
3131

3232
import { useMemo } from "react";
3333

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/todo-ensure-constant-prop-decls-get-removed.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// @validatePreserveExistingMemoizationGuarantees
1+
// @validatePreserveExistingMemoizationGuarantees @validateExhaustiveMemoizationDependencies:false
22

33
import {useMemo} from 'react';
44

0 commit comments

Comments
 (0)