@samitouri / QOS-React / commits / 226b85926a

[compiler] Support for context variable loop iterators

Summary: Fixing a compiler todo ghstack-source-id: c4d9226b1745d003dc9945df1ac5c5a01712f909 Pull Request resolved: https://github.com/facebook/react/pull/31709

Mike Vitousek committed Dec 9, 2024 at 10:15 UTC 226b85926a70a9b258583c234bd4e143e6bb42e4
4 files changed +119 -60
compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts
+26 -27
@@ -1354,20 +1354,6 @@ function codegenForInit(
1354 init: ReactiveValue,
1355 ): t.Expression | t.VariableDeclaration | null {
1356 if (init.kind === 'SequenceExpression') {
1357 - for (const instr of init.instructions) {
1358 - if (instr.value.kind === 'DeclareContext') {
1359 - CompilerError.throwTodo({
1360 - reason: `Support for loops where the index variable is a context variable`,
1361 - loc: instr.loc,
1362 - description:
1363 - instr.value.lvalue.place.identifier.name != null
1364 - ? `\`${instr.value.lvalue.place.identifier.name.value}\` is a context variable`
1365 - : null,
1366 - suggestions: null,
1367 - });
1368 - }
1369 - }
1370 -
1357 const body = codegenBlock(
1358 cx,
1359 init.instructions.map(instruction => ({
@@ -1378,20 +1364,33 @@ function codegenForInit(
1364 const declarators: Array<t.VariableDeclarator> = [];
1365 let kind: 'let' | 'const' = 'const';
1366 body.forEach(instr => {
1381 - CompilerError.invariant(
1382 - instr.type === 'VariableDeclaration' &&
1383 - (instr.kind === 'let' || instr.kind === 'const'),
1384 - {
1385 - reason: 'Expected a variable declaration',
1386 - loc: init.loc,
1387 - description: `Got ${instr.type}`,
1388 - suggestions: null,
1389 - },
1390 - );
1391 - if (instr.kind === 'let') {
1392 - kind = 'let';
1367 + let top: undefined | t.VariableDeclarator = undefined;
1368 + if (
1369 + instr.type === 'ExpressionStatement' &&
1370 + instr.expression.type === 'AssignmentExpression' &&
1371 + instr.expression.operator === '=' &&
1372 + instr.expression.left.type === 'Identifier' &&
1373 + (top = declarators.at(-1))?.id.type === 'Identifier' &&
1374 + top?.id.name === instr.expression.left.name &&
1375 + top?.init == null
1376 + ) {
1377 + top.init = instr.expression.right;
1378 + } else {
1379 + CompilerError.invariant(
1380 + instr.type === 'VariableDeclaration' &&
1381 + (instr.kind === 'let' || instr.kind === 'const'),
1382 + {
1383 + reason: 'Expected a variable declaration',
1384 + loc: init.loc,
1385 + description: `Got ${instr.type}`,
1386 + suggestions: null,
1387 + },
1388 + );
1389 + if (instr.kind === 'let') {
1390 + kind = 'let';
1391 + }
1392 + declarators.push(...instr.declarations);
1393 }
1394 - declarators.push(...instr.declarations);
1394 });
1395 CompilerError.invariant(declarators.length > 0, {
1396 reason: 'Expected a variable declaration',
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-for-loop-with-context-variable-iterator.expect.md deleted
-31
@@ -1,31 +0,0 @@
1 -
2 -## Input
3 -
4 -```javascript
5 -function Component() {
6 - const data = useData();
7 - const items = [];
8 - // NOTE: `i` is a context variable because it's reassigned and also referenced
9 - // within a closure, the `onClick` handler of each item
10 - for (let i = MIN; i <= MAX; i += INCREMENT) {
11 - items.push(<Stringify key={i} onClick={() => data.set(i)} />);
12 - }
13 - return items;
14 -}
15 -
16 -```
17 -
18 -
19 -## Error
20 -
21 -```
22 - 4 | // NOTE: `i` is a context variable because it's reassigned and also referenced
23 - 5 | // within a closure, the `onClick` handler of each item
24 -> 6 | for (let i = MIN; i <= MAX; i += INCREMENT) {
25 - | ^^^^^^^^^^^ Todo: Support for loops where the index variable is a context variable. `i` is a context variable (6:6)
26 - 7 | items.push(<Stringify key={i} onClick={() => data.set(i)} />);
27 - 8 | }
28 - 9 | return items;
29 -```
30 -
31 -
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/for-loop-with-context-variable-iterator.expect.md new
+78
@@ -0,0 +1,78 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function Component() {
6 + const data = useData();
7 + const items = [];
8 + // NOTE: `i` is a context variable because it's reassigned and also referenced
9 + // within a closure, the `onClick` handler of each item
10 + for (let i = MIN; i <= MAX; i += INCREMENT) {
11 + items.push(<div key={i} onClick={() => data.set(i)} />);
12 + }
13 + return <>{items}</>;
14 +}
15 +
16 +const MIN = 0;
17 +const MAX = 3;
18 +const INCREMENT = 1;
19 +
20 +function useData() {
21 + return new Map();
22 +}
23 +
24 +export const FIXTURE_ENTRYPOINT = {
25 + params: [],
26 + fn: Component,
27 +};
28 +
29 +```
30 +
31 +## Code
32 +
33 +```javascript
34 +import { c as _c } from "react/compiler-runtime";
35 +function Component() {
36 + const $ = _c(2);
37 + const data = useData();
38 + let t0;
39 + if ($[0] !== data) {
40 + const items = [];
41 + for (let i = MIN; i <= MAX; i = i + INCREMENT, i) {
42 + items.push(<div key={i} onClick={() => data.set(i)} />);
43 + }
44 +
45 + t0 = <>{items}</>;
46 + $[0] = data;
47 + $[1] = t0;
48 + } else {
49 + t0 = $[1];
50 + }
51 + return t0;
52 +}
53 +
54 +const MIN = 0;
55 +const MAX = 3;
56 +const INCREMENT = 1;
57 +
58 +function useData() {
59 + const $ = _c(1);
60 + let t0;
61 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
62 + t0 = new Map();
63 + $[0] = t0;
64 + } else {
65 + t0 = $[0];
66 + }
67 + return t0;
68 +}
69 +
70 +export const FIXTURE_ENTRYPOINT = {
71 + params: [],
72 + fn: Component,
73 +};
74 +
75 +```
76 +
77 +### Eval output
78 +(kind: ok) <div></div><div></div><div></div><div></div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/for-loop-with-context-variable-iterator.js renamed
+15 -2
@@ -4,7 +4,20 @@ function Component() {
4 // NOTE: `i` is a context variable because it's reassigned and also referenced
5 // within a closure, the `onClick` handler of each item
6 for (let i = MIN; i <= MAX; i += INCREMENT) {
7 - items.push(<Stringify key={i} onClick={() => data.set(i)} />);
7 + items.push(<div key={i} onClick={() => data.set(i)} />);
8 }
9 - return items;
9 + return <>{items}</>;
10 }
11 +
12 +const MIN = 0;
13 +const MAX = 3;
14 +const INCREMENT = 1;
15 +
16 +function useData() {
17 + return new Map();
18 +}
19 +
20 +export const FIXTURE_ENTRYPOINT = {
21 + params: [],
22 + fn: Component,
23 +};