@samitouri / QOS-React-1 / commits / a78bbf9dbc

[compiler] Context variables as dependencies (#31582)

We previously didn't track context variables in the hoistable values sidemap of `propagateScopeDependencies`. This was overly conservative as we *do* track the mutable range of context variables, and it is safe to hoist accesses to context variables after their last direct / aliased maybe-assignment. ```js function Component({value}) { // start of mutable range for `x` let x = DEFAULT; const setX = () => x = value; const aliasedSet = maybeAlias(setX); maybeCall(aliasedSet); // end of mutable range for `x` // here, we should be able to take x (and property reads // off of x) as dependencies return <Jsx value={x} /> } ``` --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/31582). * #31583 * __->__ #31582

mofeiZ committed Dec 16, 2024 at 16:45 UTC a78bbf9dbcf92434c902f9265ddfee34eae51a54
20 files changed +447 -194
compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts
+6 -5
@@ -840,6 +840,11 @@ export type LoadLocal = {
840 place: Place;
841 loc: SourceLocation;
842 };
843 +export type LoadContext = {
844 + kind: 'LoadContext';
845 + place: Place;
846 + loc: SourceLocation;
847 +};
848
849 /*
850 * The value of a given instruction. Note that values are not recursive: complex
@@ -852,11 +857,7 @@ export type LoadLocal = {
857
858 export type InstructionValue =
859 | LoadLocal
855 - | {
856 - kind: 'LoadContext';
857 - place: Place;
858 - loc: SourceLocation;
859 - }
860 + | LoadContext
861 | {
862 kind: 'DeclareLocal';
863 lvalue: LValue;
compiler/packages/babel-plugin-react-compiler/src/HIR/PropagateScopeDependenciesHIR.ts
+48 -17
@@ -17,6 +17,11 @@ import {
17 areEqualPaths,
18 IdentifierId,
19 Terminal,
20 + InstructionValue,
21 + LoadContext,
22 + TInstruction,
23 + FunctionExpression,
24 + ObjectMethod,
25 } from './HIR';
26 import {
27 collectHoistablePropertyLoads,
@@ -223,11 +228,25 @@ export function collectTemporariesSidemap(
228 fn,
229 usedOutsideDeclaringScope,
230 temporaries,
226 - false,
231 + null,
232 );
233 return temporaries;
234 }
235
236 +function isLoadContextMutable(
237 + instrValue: InstructionValue,
238 + id: InstructionId,
239 +): instrValue is LoadContext {
240 + if (instrValue.kind === 'LoadContext') {
241 + CompilerError.invariant(instrValue.place.identifier.scope != null, {
242 + reason:
243 + '[PropagateScopeDependencies] Expected all context variables to be assigned a scope',
244 + loc: instrValue.loc,
245 + });
246 + return id >= instrValue.place.identifier.scope.range.end;
247 + }
248 + return false;
249 +}
250 /**
251 * Recursive collect a sidemap of all `LoadLocal` and `PropertyLoads` with a
252 * function and all nested functions.
@@ -239,17 +258,21 @@ function collectTemporariesSidemapImpl(
258 fn: HIRFunction,
259 usedOutsideDeclaringScope: ReadonlySet<DeclarationId>,
260 temporaries: Map<IdentifierId, ReactiveScopeDependency>,
242 - isInnerFn: boolean,
261 + innerFnContext: {instrId: InstructionId} | null,
262 ): void {
263 for (const [_, block] of fn.body.blocks) {
245 - for (const instr of block.instructions) {
246 - const {value, lvalue} = instr;
264 + for (const {value, lvalue, id: origInstrId} of block.instructions) {
265 + const instrId =
266 + innerFnContext != null ? innerFnContext.instrId : origInstrId;
267 const usedOutside = usedOutsideDeclaringScope.has(
268 lvalue.identifier.declarationId,
269 );
270
271 if (value.kind === 'PropertyLoad' && !usedOutside) {
252 - if (!isInnerFn || temporaries.has(value.object.identifier.id)) {
272 + if (
273 + innerFnContext == null ||
274 + temporaries.has(value.object.identifier.id)
275 + ) {
276 /**
277 * All dependencies of a inner / nested function must have a base
278 * identifier from the outermost component / hook. This is because the
@@ -265,13 +288,13 @@ function collectTemporariesSidemapImpl(
288 temporaries.set(lvalue.identifier.id, property);
289 }
290 } else if (
268 - value.kind === 'LoadLocal' &&
291 + (value.kind === 'LoadLocal' || isLoadContextMutable(value, instrId)) &&
292 lvalue.identifier.name == null &&
293 value.place.identifier.name !== null &&
294 !usedOutside
295 ) {
296 if (
274 - !isInnerFn ||
297 + innerFnContext == null ||
298 fn.context.some(
299 context => context.identifier.id === value.place.identifier.id,
300 )
@@ -289,7 +312,7 @@ function collectTemporariesSidemapImpl(
312 value.loweredFunc.func,
313 usedOutsideDeclaringScope,
314 temporaries,
292 - true,
315 + innerFnContext ?? {instrId},
316 );
317 }
318 }
@@ -364,7 +387,7 @@ class Context {
387 * Tracks the traversal state. See Context.declare for explanation of why this
388 * is needed.
389 */
367 - inInnerFn: boolean = false;
390 + #innerFnContext: {outerInstrId: InstructionId} | null = null;
391
392 constructor(
393 temporariesUsedOutsideScope: ReadonlySet<DeclarationId>,
@@ -434,7 +457,7 @@ class Context {
457 * by root identifier mutable ranges).
458 */
459 declare(identifier: Identifier, decl: Decl): void {
437 - if (this.inInnerFn) return;
460 + if (this.#innerFnContext != null) return;
461 if (!this.#declarations.has(identifier.declarationId)) {
462 this.#declarations.set(identifier.declarationId, decl);
463 }
@@ -577,11 +600,14 @@ class Context {
600 currentScope.reassignments.add(place.identifier);
601 }
602 }
580 - enterInnerFn<T>(cb: () => T): T {
581 - const wasInInnerFn = this.inInnerFn;
582 - this.inInnerFn = true;
603 + enterInnerFn<T>(
604 + innerFn: TInstruction<FunctionExpression> | TInstruction<ObjectMethod>,
605 + cb: () => T,
606 + ): T {
607 + const prevContext = this.#innerFnContext;
608 + this.#innerFnContext = this.#innerFnContext ?? {outerInstrId: innerFn.id};
609 const result = cb();
584 - this.inInnerFn = wasInInnerFn;
610 + this.#innerFnContext = prevContext;
611 return result;
612 }
613
@@ -724,9 +750,14 @@ function collectDependencies(
750 * Recursively visit the inner function to extract dependencies there
751 */
752 const innerFn = instr.value.loweredFunc.func;
727 - context.enterInnerFn(() => {
728 - handleFunction(innerFn);
729 - });
753 + context.enterInnerFn(
754 + instr as
755 + | TInstruction<FunctionExpression>
756 + | TInstruction<ObjectMethod>,
757 + () => {
758 + handleFunction(innerFn);
759 + },
760 + );
761 } else {
762 handleInstruction(instr, context);
763 }
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-functiondecl-hoisting.expect.md
+8 -10
@@ -58,18 +58,16 @@ function Foo(t0) {
58 bar = $[1];
59 result = $[2];
60 }
61 -
62 - const t1 = bar;
63 - let t2;
64 - if ($[3] !== result || $[4] !== t1) {
65 - t2 = <Stringify result={result} fn={t1} shouldInvokeFns={true} />;
66 - $[3] = result;
67 - $[4] = t1;
68 - $[5] = t2;
61 + let t1;
62 + if ($[3] !== bar || $[4] !== result) {
63 + t1 = <Stringify result={result} fn={bar} shouldInvokeFns={true} />;
64 + $[3] = bar;
65 + $[4] = result;
66 + $[5] = t1;
67 } else {
70 - t2 = $[5];
68 + t1 = $[5];
69 }
72 - return t2;
70 + return t1;
71 }
72
73 export const FIXTURE_ENTRYPOINT = {
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/destructure-array-assignment-to-context-var.expect.md
+7 -8
@@ -43,16 +43,15 @@ function Component(props) {
43 } else {
44 x = $[1];
45 }
46 - const t0 = x;
47 - let t1;
48 - if ($[2] !== t0) {
49 - t1 = { x: t0 };
50 - $[2] = t0;
51 - $[3] = t1;
46 + let t0;
47 + if ($[2] !== x) {
48 + t0 = { x };
49 + $[2] = x;
50 + $[3] = t0;
51 } else {
53 - t1 = $[3];
52 + t0 = $[3];
53 }
55 - return t1;
54 + return t0;
55 }
56
57 export const FIXTURE_ENTRYPOINT = {
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/destructure-array-declaration-to-context-var.expect.md
+7 -8
@@ -42,16 +42,15 @@ function Component(props) {
42 } else {
43 x = $[1];
44 }
45 - const t0 = x;
46 - let t1;
47 - if ($[2] !== t0) {
48 - t1 = <div>{t0}</div>;
49 - $[2] = t0;
50 - $[3] = t1;
45 + let t0;
46 + if ($[2] !== x) {
47 + t0 = <div>{x}</div>;
48 + $[2] = x;
49 + $[3] = t0;
50 } else {
52 - t1 = $[3];
51 + t0 = $[3];
52 }
54 - return t1;
53 + return t0;
54 }
55
56 export const FIXTURE_ENTRYPOINT = {
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/destructure-object-assignment-to-context-var.expect.md
+7 -8
@@ -43,16 +43,15 @@ function Component(props) {
43 } else {
44 x = $[1];
45 }
46 - const t0 = x;
47 - let t1;
48 - if ($[2] !== t0) {
49 - t1 = { x: t0 };
50 - $[2] = t0;
51 - $[3] = t1;
46 + let t0;
47 + if ($[2] !== x) {
48 + t0 = { x };
49 + $[2] = x;
50 + $[3] = t0;
51 } else {
53 - t1 = $[3];
52 + t0 = $[3];
53 }
55 - return t1;
54 + return t0;
55 }
56
57 export const FIXTURE_ENTRYPOINT = {
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/destructure-object-declaration-to-context-var.expect.md
+7 -8
@@ -42,16 +42,15 @@ function Component(props) {
42 } else {
43 x = $[1];
44 }
45 - const t0 = x;
46 - let t1;
47 - if ($[2] !== t0) {
48 - t1 = { x: t0 };
49 - $[2] = t0;
50 - $[3] = t1;
45 + let t0;
46 + if ($[2] !== x) {
47 + t0 = { x };
48 + $[2] = x;
49 + $[3] = t0;
50 } else {
52 - t1 = $[3];
51 + t0 = $[3];
52 }
54 - return t1;
53 + return t0;
54 }
55
56 export const FIXTURE_ENTRYPOINT = {
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/lambda-mutated-non-reactive-to-reactive.expect.md
+7 -9
@@ -33,17 +33,15 @@ function f(a) {
33 } else {
34 x = $[1];
35 }
36 -
37 - const t0 = x;
38 - let t1;
39 - if ($[2] !== t0) {
40 - t1 = <div x={t0} />;
41 - $[2] = t0;
42 - $[3] = t1;
36 + let t0;
37 + if ($[2] !== x) {
38 + t0 = <div x={x} />;
39 + $[2] = x;
40 + $[3] = t0;
41 } else {
44 - t1 = $[3];
42 + t0 = $[3];
43 }
46 - return t1;
44 + return t0;
45 }
46
47 export const FIXTURE_ENTRYPOINT = {
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.todo-useCallback-captures-reassigned-context-property.expect.md deleted
-53
@@ -1,53 +0,0 @@
1 -
2 -## Input
3 -
4 -```javascript
5 -// @validatePreserveExistingMemoizationGuarantees
6 -import {useCallback} from 'react';
7 -import {Stringify} from 'shared-runtime';
8 -
9 -/**
10 - * TODO: we're currently bailing out because `contextVar` is a context variable
11 - * and not recorded into the PropagateScopeDeps LoadLocal / PropertyLoad
12 - * sidemap. Previously, we were able to avoid this as `BuildHIR` hoisted
13 - * `LoadContext` and `PropertyLoad` instructions into the outer function, which
14 - * we took as eligible dependencies.
15 - *
16 - * One solution is to simply record `LoadContext` identifiers into the
17 - * temporaries sidemap when the instruction occurs *after* the context
18 - * variable's mutable range.
19 - */
20 -function Foo(props) {
21 - let contextVar;
22 - if (props.cond) {
23 - contextVar = {val: 2};
24 - } else {
25 - contextVar = {};
26 - }
27 -
28 - const cb = useCallback(() => [contextVar.val], [contextVar.val]);
29 -
30 - return <Stringify cb={cb} shouldInvokeFns={true} />;
31 -}
32 -
33 -export const FIXTURE_ENTRYPOINT = {
34 - fn: Foo,
35 - params: [{cond: true}],
36 -};
37 -
38 -```
39 -
40 -
41 -## Error
42 -
43 -```
44 - 22 | }
45 - 23 |
46 -> 24 | const cb = useCallback(() => [contextVar.val], [contextVar.val]);
47 - | ^^^^^^^^^^^^^^^^^^^^^^ 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 (24:24)
48 - 25 |
49 - 26 | return <Stringify cb={cb} shouldInvokeFns={true} />;
50 - 27 | }
51 -```
52 -
53 -
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-captures-reassigned-context-property.expect.md new
+101
@@ -0,0 +1,101 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validatePreserveExistingMemoizationGuarantees
6 +import {useCallback} from 'react';
7 +import {Stringify} from 'shared-runtime';
8 +
9 +/**
10 + * TODO: we're currently bailing out because `contextVar` is a context variable
11 + * and not recorded into the PropagateScopeDeps LoadLocal / PropertyLoad
12 + * sidemap. Previously, we were able to avoid this as `BuildHIR` hoisted
13 + * `LoadContext` and `PropertyLoad` instructions into the outer function, which
14 + * we took as eligible dependencies.
15 + *
16 + * One solution is to simply record `LoadContext` identifiers into the
17 + * temporaries sidemap when the instruction occurs *after* the context
18 + * variable's mutable range.
19 + */
20 +function Foo(props) {
21 + let contextVar;
22 + if (props.cond) {
23 + contextVar = {val: 2};
24 + } else {
25 + contextVar = {};
26 + }
27 +
28 + const cb = useCallback(() => [contextVar.val], [contextVar.val]);
29 +
30 + return <Stringify cb={cb} shouldInvokeFns={true} />;
31 +}
32 +
33 +export const FIXTURE_ENTRYPOINT = {
34 + fn: Foo,
35 + params: [{cond: true}],
36 +};
37 +
38 +```
39 +
40 +## Code
41 +
42 +```javascript
43 +import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees
44 +import { useCallback } from "react";
45 +import { Stringify } from "shared-runtime";
46 +
47 +/**
48 + * TODO: we're currently bailing out because `contextVar` is a context variable
49 + * and not recorded into the PropagateScopeDeps LoadLocal / PropertyLoad
50 + * sidemap. Previously, we were able to avoid this as `BuildHIR` hoisted
51 + * `LoadContext` and `PropertyLoad` instructions into the outer function, which
52 + * we took as eligible dependencies.
53 + *
54 + * One solution is to simply record `LoadContext` identifiers into the
55 + * temporaries sidemap when the instruction occurs *after* the context
56 + * variable's mutable range.
57 + */
58 +function Foo(props) {
59 + const $ = _c(6);
60 + let contextVar;
61 + if ($[0] !== props.cond) {
62 + if (props.cond) {
63 + contextVar = { val: 2 };
64 + } else {
65 + contextVar = {};
66 + }
67 + $[0] = props.cond;
68 + $[1] = contextVar;
69 + } else {
70 + contextVar = $[1];
71 + }
72 + let t0;
73 + if ($[2] !== contextVar.val) {
74 + t0 = () => [contextVar.val];
75 + $[2] = contextVar.val;
76 + $[3] = t0;
77 + } else {
78 + t0 = $[3];
79 + }
80 + contextVar;
81 + const cb = t0;
82 + let t1;
83 + if ($[4] !== cb) {
84 + t1 = <Stringify cb={cb} shouldInvokeFns={true} />;
85 + $[4] = cb;
86 + $[5] = t1;
87 + } else {
88 + t1 = $[5];
89 + }
90 + return t1;
91 +}
92 +
93 +export const FIXTURE_ENTRYPOINT = {
94 + fn: Foo,
95 + params: [{ cond: true }],
96 +};
97 +
98 +```
99 +
100 +### Eval output
101 +(kind: ok) <div>{"cb":{"kind":"Function","result":[2]},"shouldInvokeFns":true}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-captures-reassigned-context-property.tsx renamed
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-reordering-depslist-assignment.expect.md
+7 -8
@@ -44,16 +44,15 @@ function useFoo(arr1, arr2) {
44 y = $[2];
45 }
46 let t0;
47 - const t1 = y;
48 - let t2;
49 - if ($[3] !== t1) {
50 - t2 = { y: t1 };
51 - $[3] = t1;
52 - $[4] = t2;
47 + let t1;
48 + if ($[3] !== y) {
49 + t1 = { y };
50 + $[3] = y;
51 + $[4] = t1;
52 } else {
54 - t2 = $[4];
53 + t1 = $[4];
54 }
56 - t0 = t2;
55 + t0 = t1;
56 return t0;
57 }
58
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/repro-scope-missing-mutable-range.expect.md
+7 -9
@@ -36,17 +36,15 @@ function HomeDiscoStoreItemTileRating(props) {
36 } else {
37 count = $[1];
38 }
39 -
40 - const t0 = count;
41 - let t1;
42 - if ($[2] !== t0) {
43 - t1 = <Text>{t0}</Text>;
44 - $[2] = t0;
45 - $[3] = t1;
39 + let t0;
40 + if ($[2] !== count) {
41 + t0 = <Text>{count}</Text>;
42 + $[2] = count;
43 + $[3] = t0;
44 } else {
47 - t1 = $[3];
45 + t0 = $[3];
46 }
49 - return t1;
47 + return t0;
48 }
49
50 ```
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reactive-control-dependency-on-context-variable.expect.md
+7 -9
@@ -67,17 +67,15 @@ function Component(props) {
67 } else {
68 x = $[1];
69 }
70 -
71 - const t0 = x;
72 - let t1;
73 - if ($[2] !== t0) {
74 - t1 = [t0];
75 - $[2] = t0;
76 - $[3] = t1;
70 + let t0;
71 + if ($[2] !== x) {
72 + t0 = [x];
73 + $[2] = x;
74 + $[3] = t0;
75 } else {
78 - t1 = $[3];
76 + t0 = $[3];
77 }
80 - return t1;
78 + return t0;
79 }
80
81 export const FIXTURE_ENTRYPOINT = {
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/context-var-granular-dep.expect.md new
+130
@@ -0,0 +1,130 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import {throwErrorWithMessage, ValidateMemoization} from 'shared-runtime';
6 +
7 +/**
8 + * Context variables are local variables that (1) have at least one reassignment
9 + * and (2) are captured into a function expression. These have a known mutable
10 + * range: from first declaration / assignment to the last direct or aliased,
11 + * mutable reference.
12 + *
13 + * This fixture validates that forget can take granular dependencies on context
14 + * variables when the reference to a context var happens *after* the end of its
15 + * mutable range.
16 + */
17 +function Component({cond, a}) {
18 + let contextVar;
19 + if (cond) {
20 + contextVar = {val: a};
21 + } else {
22 + contextVar = {};
23 + throwErrorWithMessage('');
24 + }
25 + const cb = {cb: () => contextVar.val * 4};
26 +
27 + /**
28 + * manually specify input to avoid adding a `PropertyLoad` from contextVar,
29 + * which might affect hoistable-objects analysis.
30 + */
31 + return (
32 + <ValidateMemoization
33 + inputs={[cond ? a : undefined]}
34 + output={cb}
35 + onlyCheckCompiled={true}
36 + />
37 + );
38 +}
39 +
40 +export const FIXTURE_ENTRYPOINT = {
41 + fn: Component,
42 + params: [{cond: false, a: undefined}],
43 + sequentialRenders: [
44 + {cond: true, a: 2},
45 + {cond: true, a: 2},
46 + ],
47 +};
48 +
49 +```
50 +
51 +## Code
52 +
53 +```javascript
54 +import { c as _c } from "react/compiler-runtime";
55 +import { throwErrorWithMessage, ValidateMemoization } from "shared-runtime";
56 +
57 +/**
58 + * Context variables are local variables that (1) have at least one reassignment
59 + * and (2) are captured into a function expression. These have a known mutable
60 + * range: from first declaration / assignment to the last direct or aliased,
61 + * mutable reference.
62 + *
63 + * This fixture validates that forget can take granular dependencies on context
64 + * variables when the reference to a context var happens *after* the end of its
65 + * mutable range.
66 + */
67 +function Component(t0) {
68 + const $ = _c(10);
69 + const { cond, a } = t0;
70 + let contextVar;
71 + if ($[0] !== a || $[1] !== cond) {
72 + if (cond) {
73 + contextVar = { val: a };
74 + } else {
75 + contextVar = {};
76 + throwErrorWithMessage("");
77 + }
78 + $[0] = a;
79 + $[1] = cond;
80 + $[2] = contextVar;
81 + } else {
82 + contextVar = $[2];
83 + }
84 + let t1;
85 + if ($[3] !== contextVar.val) {
86 + t1 = { cb: () => contextVar.val * 4 };
87 + $[3] = contextVar.val;
88 + $[4] = t1;
89 + } else {
90 + t1 = $[4];
91 + }
92 + const cb = t1;
93 +
94 + const t2 = cond ? a : undefined;
95 + let t3;
96 + if ($[5] !== t2) {
97 + t3 = [t2];
98 + $[5] = t2;
99 + $[6] = t3;
100 + } else {
101 + t3 = $[6];
102 + }
103 + let t4;
104 + if ($[7] !== cb || $[8] !== t3) {
105 + t4 = (
106 + <ValidateMemoization inputs={t3} output={cb} onlyCheckCompiled={true} />
107 + );
108 + $[7] = cb;
109 + $[8] = t3;
110 + $[9] = t4;
111 + } else {
112 + t4 = $[9];
113 + }
114 + return t4;
115 +}
116 +
117 +export const FIXTURE_ENTRYPOINT = {
118 + fn: Component,
119 + params: [{ cond: false, a: undefined }],
120 + sequentialRenders: [
121 + { cond: true, a: 2 },
122 + { cond: true, a: 2 },
123 + ],
124 +};
125 +
126 +```
127 +
128 +### Eval output
129 +(kind: ok) <div>{"inputs":[2],"output":{"cb":"[[ function params=0 ]]"}}</div>
130 +<div>{"inputs":[2],"output":{"cb":"[[ function params=0 ]]"}}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/context-var-granular-dep.js new
+43
@@ -0,0 +1,43 @@
1 +import {throwErrorWithMessage, ValidateMemoization} from 'shared-runtime';
2 +
3 +/**
4 + * Context variables are local variables that (1) have at least one reassignment
5 + * and (2) are captured into a function expression. These have a known mutable
6 + * range: from first declaration / assignment to the last direct or aliased,
7 + * mutable reference.
8 + *
9 + * This fixture validates that forget can take granular dependencies on context
10 + * variables when the reference to a context var happens *after* the end of its
11 + * mutable range.
12 + */
13 +function Component({cond, a}) {
14 + let contextVar;
15 + if (cond) {
16 + contextVar = {val: a};
17 + } else {
18 + contextVar = {};
19 + throwErrorWithMessage('');
20 + }
21 + const cb = {cb: () => contextVar.val * 4};
22 +
23 + /**
24 + * manually specify input to avoid adding a `PropertyLoad` from contextVar,
25 + * which might affect hoistable-objects analysis.
26 + */
27 + return (
28 + <ValidateMemoization
29 + inputs={[cond ? a : undefined]}
30 + output={cb}
31 + onlyCheckCompiled={true}
32 + />
33 + );
34 +}
35 +
36 +export const FIXTURE_ENTRYPOINT = {
37 + fn: Component,
38 + params: [{cond: false, a: undefined}],
39 + sequentialRenders: [
40 + {cond: true, a: 2},
41 + {cond: true, a: 2},
42 + ],
43 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-scope-missing-mutable-range.expect.md
+7 -9
@@ -35,17 +35,15 @@ function HomeDiscoStoreItemTileRating(props) {
35 } else {
36 count = $[1];
37 }
38 -
39 - const t0 = count;
40 - let t1;
41 - if ($[2] !== t0) {
42 - t1 = <Text>{t0}</Text>;
43 - $[2] = t0;
44 - $[3] = t1;
38 + let t0;
39 + if ($[2] !== count) {
40 + t0 = <Text>{count}</Text>;
41 + $[2] = count;
42 + $[3] = t0;
43 } else {
46 - t1 = $[3];
44 + t0 = $[3];
45 }
48 - return t1;
46 + return t0;
47 }
48
49 ```
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-operator-conditional.expect.md
+20 -22
@@ -88,36 +88,34 @@ function Inner(props) {
88 input;
89 input;
90 let t0;
91 - const t1 = input;
92 - let t2;
93 - if ($[0] !== t1) {
94 - t2 = [t1];
95 - $[0] = t1;
96 - $[1] = t2;
91 + let t1;
92 + if ($[0] !== input) {
93 + t1 = [input];
94 + $[0] = input;
95 + $[1] = t1;
96 } else {
98 - t2 = $[1];
97 + t1 = $[1];
98 }
100 - t0 = t2;
99 + t0 = t1;
100 const output = t0;
102 - const t3 = input;
103 - let t4;
104 - if ($[2] !== t3) {
105 - t4 = [t3];
106 - $[2] = t3;
107 - $[3] = t4;
101 + let t2;
102 + if ($[2] !== input) {
103 + t2 = [input];
104 + $[2] = input;
105 + $[3] = t2;
106 } else {
109 - t4 = $[3];
107 + t2 = $[3];
108 }
111 - let t5;
112 - if ($[4] !== output || $[5] !== t4) {
113 - t5 = <ValidateMemoization inputs={t4} output={output} />;
109 + let t3;
110 + if ($[4] !== output || $[5] !== t2) {
111 + t3 = <ValidateMemoization inputs={t2} output={output} />;
112 $[4] = output;
115 - $[5] = t4;
116 - $[6] = t5;
113 + $[5] = t2;
114 + $[6] = t3;
115 } else {
118 - t5 = $[6];
116 + t3 = $[6];
117 }
120 - return t5;
118 + return t3;
119 }
120
121 export const FIXTURE_ENTRYPOINT = {
compiler/packages/snap/src/sprout/index.ts
+9 -1
@@ -32,7 +32,15 @@ export function runSprout(
32 originalCode: string,
33 forgetCode: string,
34 ): SproutResult {
35 - const forgetResult = doEval(forgetCode);
35 + let forgetResult;
36 + try {
37 + (globalThis as any).__SNAP_EVALUATOR_MODE = 'forget';
38 + forgetResult = doEval(forgetCode);
39 + } catch (e) {
40 + throw e;
41 + } finally {
42 + (globalThis as any).__SNAP_EVALUATOR_MODE = undefined;
43 + }
44 if (forgetResult.kind === 'UnexpectedError') {
45 return makeError('Unexpected error in Forget runner', forgetResult.value);
46 }
compiler/packages/snap/src/sprout/shared-runtime.ts
+19 -10
@@ -259,26 +259,35 @@ export function Throw() {
259
260 export function ValidateMemoization({
261 inputs,
262 - output,
262 + output: rawOutput,
263 + onlyCheckCompiled = false,
264 }: {
265 inputs: Array<any>;
266 output: any;
267 + onlyCheckCompiled: boolean;
268 }): React.ReactElement {
269 'use no forget';
270 + // Wrap rawOutput as it might be a function, which useState would invoke.
271 + const output = {value: rawOutput};
272 const [previousInputs, setPreviousInputs] = React.useState(inputs);
273 const [previousOutput, setPreviousOutput] = React.useState(output);
274 if (
271 - inputs.length !== previousInputs.length ||
272 - inputs.some((item, i) => item !== previousInputs[i])
275 + onlyCheckCompiled &&
276 + (globalThis as any).__SNAP_EVALUATOR_MODE === 'forget'
277 ) {
274 - // Some input changed, we expect the output to change
275 - setPreviousInputs(inputs);
276 - setPreviousOutput(output);
277 - } else if (output !== previousOutput) {
278 - // Else output should be stable
279 - throw new Error('Output identity changed but inputs did not');
278 + if (
279 + inputs.length !== previousInputs.length ||
280 + inputs.some((item, i) => item !== previousInputs[i])
281 + ) {
282 + // Some input changed, we expect the output to change
283 + setPreviousInputs(inputs);
284 + setPreviousOutput(output);
285 + } else if (output.value !== previousOutput.value) {
286 + // Else output should be stable
287 + throw new Error('Output identity changed but inputs did not');
288 + }
289 }
281 - return React.createElement(Stringify, {inputs, output});
290 + return React.createElement(Stringify, {inputs, output: rawOutput});
291 }
292
293 export function createHookWrapper<TProps, TRet>(