[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
+};