More precisely model for..of semantics with separate init/test blocks
ghstack-source-id: 827c35466d04f7b7143ddf1adefeff926d4b22cd Pull Request resolved: https://github.com/facebook/react-forget/pull/2893
Joe Savona committed
Apr 24, 2024 at 14:59 UTC
21b2af8ba0b3b81e88887ac111be64041e7783ae
24 files changed
+699
-76
compiler/packages/babel-plugin-react-forget/src/HIR/BuildHIR.ts
+20
-1
@@ -1006,6 +1006,7 @@ function lowerStatement(
1006
const stmt = stmtPath as NodePath<t.ForOfStatement>;
1007
const continuationBlock = builder.reserve("block");
1008
const initBlock = builder.reserve("loop");
1009
+ const testBlock = builder.reserve("loop");
1010
1011
const loopBlock = builder.enter("block", (_blockId) => {
1012
return builder.loop(label, initBlock.id, continuationBlock.id, () => {
@@ -1028,6 +1029,7 @@ function lowerStatement(
1029
kind: "for-of",
1030
loc,
1031
init: initBlock.id,
1032
+ test: testBlock.id,
1033
loop: loopBlock,
1034
fallthrough: continuationBlock.id,
1035
id: makeInstructionId(0),
@@ -1040,6 +1042,22 @@ function lowerStatement(
1042
* right (Expression), so we synthesize a new InstrValue and assignment (potentially multiple
1043
* instructions when we handle other syntax like Patterns)
1044
*/
1045
+ const iterator = lowerValueToTemporary(builder, {
1046
+ kind: "GetIterator",
1047
+ loc: value.loc,
1048
+ collection: { ...value },
1049
+ });
1050
+ builder.terminateWithContinuation(
1051
+ {
1052
+ id: makeInstructionId(0),
1053
+ kind: "goto",
1054
+ block: testBlock.id,
1055
+ variant: GotoVariant.Break,
1056
+ loc: stmt.node.loc ?? GeneratedSource,
1057
+ },
1058
+ testBlock
1059
+ );
1060
+
1061
const left = stmt.get("left");
1062
const leftLoc = left.node.loc ?? GeneratedSource;
1063
let test: Place;
@@ -1055,7 +1073,8 @@ function lowerStatement(
1073
const nextIterableOf = lowerValueToTemporary(builder, {
1074
kind: "NextIterableOf",
1075
loc: leftLoc,
1058
- value,
1076
+ iterator: { ...iterator },
1077
+ collection: { ...value },
1078
});
1079
const assign = lowerAssignment(
1080
builder,
compiler/packages/babel-plugin-react-forget/src/HIR/HIR.ts
+9
-2
@@ -234,6 +234,7 @@ export type ReactiveForTerminal = {
234
export type ReactiveForOfTerminal = {
235
kind: "for-of";
236
init: ReactiveValue;
237
+ test: ReactiveValue;
238
loop: ReactiveBlock;
239
id: InstructionId;
240
loc: SourceLocation;
@@ -502,6 +503,7 @@ export type ForOfTerminal = {
503
kind: "for-of";
504
loc: SourceLocation;
505
init: BlockId;
506
+ test: BlockId;
507
loop: BlockId;
508
fallthrough: BlockId;
509
id: InstructionId;
@@ -958,12 +960,13 @@ export type InstructionValue =
960
}
961
| {
962
kind: "GetIterator";
961
- value: Place; // the collection
963
+ collection: Place; // the collection
964
loc: SourceLocation;
965
}
966
| {
967
kind: "NextIterableOf";
966
- value: Place; // the iterator created with GetIterator
968
+ iterator: Place; // the iterator created with GetIterator
969
+ collection: Place; // the collection being iterated over (which may be an iterable or iterator)
970
loc: SourceLocation;
971
}
972
| {
@@ -1442,6 +1445,10 @@ export function isPrimitiveType(id: Identifier): boolean {
1445
return id.type.kind === "Primitive";
1446
}
1447
1448
+export function isArrayType(id: Identifier): boolean {
1449
+ return id.type.kind === "Object" && id.type.shapeId === "BuiltInArray";
1450
+}
1451
+
1452
export function isRefValueType(id: Identifier): boolean {
1453
return id.type.kind === "Object" && id.type.shapeId === "BuiltInRefValue";
1454
}
compiler/packages/babel-plugin-react-forget/src/HIR/PrintHIR.ts
+5
-3
@@ -245,7 +245,7 @@ export function printTerminal(terminal: Terminal): Array<string> | string {
245
break;
246
}
247
case "for-of": {
248
- value = `[${terminal.id}] ForOf init=bb${terminal.init} loop=bb${terminal.loop} fallthrough=bb${terminal.fallthrough}`;
248
+ value = `[${terminal.id}] ForOf init=bb${terminal.init} test=bb${terminal.test} loop=bb${terminal.loop} fallthrough=bb${terminal.fallthrough}`;
249
break;
250
}
251
case "for-in": {
@@ -610,11 +610,13 @@ export function printInstructionValue(instrValue: ReactiveValue): string {
610
break;
611
}
612
case "GetIterator": {
613
- value = `GetIterator ${printPlace(instrValue.value)}`;
613
+ value = `GetIterator collection=${printPlace(instrValue.collection)}`;
614
break;
615
}
616
case "NextIterableOf": {
617
- value = `NextIterableOf ${printPlace(instrValue.value)}`;
617
+ value = `NextIterableOf iterator=${printPlace(
618
+ instrValue.iterator
619
+ )} collection=${printPlace(instrValue.collection)}`;
620
break;
621
}
622
case "NextPropertyOf": {
compiler/packages/babel-plugin-react-forget/src/HIR/visitors.ts
+14
-4
@@ -208,9 +208,13 @@ export function* eachInstructionValueOperand(
208
yield instrValue.value;
209
break;
210
}
211
- case "GetIterator":
211
+ case "GetIterator": {
212
+ yield instrValue.collection;
213
+ break;
214
+ }
215
case "NextIterableOf": {
213
- yield instrValue.value;
216
+ yield instrValue.iterator;
217
+ yield instrValue.collection;
218
break;
219
}
220
case "NextPropertyOf": {
@@ -528,9 +532,13 @@ export function mapInstructionValueOperands(
532
instrValue.value = fn(instrValue.value);
533
break;
534
}
531
- case "GetIterator":
535
+ case "GetIterator": {
536
+ instrValue.collection = fn(instrValue.collection);
537
+ break;
538
+ }
539
case "NextIterableOf": {
533
- instrValue.value = fn(instrValue.value);
540
+ instrValue.iterator = fn(instrValue.iterator);
541
+ instrValue.collection = fn(instrValue.collection);
542
break;
543
}
544
case "NextPropertyOf": {
@@ -769,11 +777,13 @@ export function mapTerminalSuccessors(
777
case "for-of": {
778
const init = fn(terminal.init);
779
const loop = fn(terminal.loop);
780
+ const test = fn(terminal.test);
781
const fallthrough = fn(terminal.fallthrough);
782
return {
783
kind: "for-of",
784
loc: terminal.loc,
785
init,
786
+ test,
787
loop,
788
fallthrough,
789
id: makeInstructionId(0),
compiler/packages/babel-plugin-react-forget/src/Inference/InferReferenceEffects.ts
+69
-17
@@ -26,6 +26,7 @@ import {
26
Type,
27
ValueKind,
28
ValueReason,
29
+ isArrayType,
30
isMutableEffect,
31
isObjectType,
32
} from "../HIR/HIR";
@@ -1811,27 +1812,78 @@ function inferBlock(
1812
continue;
1813
}
1814
case "GetIterator": {
1814
- effect = { kind: Effect.Capture, reason: ValueReason.Other };
1815
+ /**
1816
+ * This instruction represents the step of retrieving an iterator from the collection
1817
+ * in `for (... of <collection>)` syntax. We model two cases:
1818
+ *
1819
+ * 1. The collection is immutable or a known collection type (e.g. Array). In this case
1820
+ * we infer that the iterator produced won't be the same as the collection itself.
1821
+ * If the collection is an Array, this is because it will produce a native Array
1822
+ * iterator. If the collection is already frozen, we assume it must be of some
1823
+ * type that returns a separate iterator. In theory you could pass an Iterator
1824
+ * as props to a component and then for..of over that in the component body, but
1825
+ * this already violates React's rules so we assume you're not doing this.
1826
+ * 2. The collection could be an Iterator itself, such that advancing the iterator
1827
+ * (modeled with NextIterableOf) mutates the collection itself.
1828
+ */
1829
+ const kind = state.kind(instrValue.collection).kind;
1830
+ const isMutable =
1831
+ kind === ValueKind.Mutable || kind === ValueKind.Context;
1832
+ if (!isMutable || isArrayType(instrValue.collection.identifier)) {
1833
+ // Case 1, assume iterator is a separate mutable object
1834
+ effect = {
1835
+ kind: Effect.Read,
1836
+ reason: ValueReason.Other,
1837
+ };
1838
+ valueKind = {
1839
+ kind: ValueKind.Mutable,
1840
+ reason: new Set([ValueReason.Other]),
1841
+ context: new Set(),
1842
+ };
1843
+ } else {
1844
+ // Case 2, assume that the iterator could be the (mutable) collection itself
1845
+ effect = {
1846
+ kind: Effect.Capture,
1847
+ reason: ValueReason.Other,
1848
+ };
1849
+ valueKind = state.kind(instrValue.collection);
1850
+ }
1851
lvalueEffect = Effect.Store;
1816
- valueKind = {
1817
- kind: ValueKind.Mutable,
1818
- reason: new Set([ValueReason.Other]),
1819
- context: new Set(),
1820
- };
1852
break;
1853
}
1854
case "NextIterableOf": {
1824
- effect = {
1825
- kind: Effect.Capture,
1826
- reason: ValueReason.Other,
1827
- };
1828
- lvalueEffect = Effect.Store;
1829
- valueKind = {
1830
- kind: ValueKind.Mutable,
1831
- reason: new Set([ValueReason.Other]),
1832
- context: new Set(),
1833
- };
1834
- break;
1855
+ /**
1856
+ * This instruction represents advancing an iterator with .next(). We use a
1857
+ * conditional mutate to model the two cases for GetIterator:
1858
+ * - If the collection is a mutable iterator, we want to model the fact that
1859
+ * advancing the iterator will mutate it
1860
+ * - If the iterator may be different from the collection and the collection
1861
+ * is frozen, we don't want to report a false positive "cannot mutate" error.
1862
+ *
1863
+ * ConditionallyMutate reflects this "mutate if mutable" semantic.
1864
+ */
1865
+ state.referenceAndRecordEffects(
1866
+ instrValue.iterator,
1867
+ Effect.ConditionallyMutate,
1868
+ ValueReason.Other,
1869
+ functionEffects
1870
+ );
1871
+ /**
1872
+ * Regardless of the effect on the iterator, the *result* of advancing the iterator
1873
+ * is to extract a value from the collection. We use a Capture effect to reflect this
1874
+ * aliasing, and then initialize() the lvalue to the same kind as the colleciton to
1875
+ * ensure that the item is mutable or frozen if the collection is mutable/frozen.
1876
+ */
1877
+ state.referenceAndRecordEffects(
1878
+ instrValue.collection,
1879
+ Effect.Capture,
1880
+ ValueReason.Other,
1881
+ functionEffects
1882
+ );
1883
+ state.initialize(instrValue, state.kind(instrValue.collection));
1884
+ state.define(instr.lvalue, instrValue);
1885
+ instr.lvalue.effect = Effect.Store;
1886
+ continue;
1887
}
1888
case "NextPropertyOf": {
1889
effect = { kind: Effect.Read, reason: ValueReason.Other };
compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/BuildReactiveFunction.ts
+26
@@ -471,6 +471,31 @@ class Driver {
471
};
472
}
473
474
+ const test = this.visitValueBlock(terminal.test, terminal.loc);
475
+ const testBlock = this.cx.ir.blocks.get(test.block)!;
476
+ let testValue = test.value;
477
+ if (testValue.kind === "SequenceExpression") {
478
+ const last = testBlock.instructions.at(-1)!;
479
+ testValue.instructions.push(last);
480
+ testValue.value = {
481
+ kind: "Primitive",
482
+ value: undefined,
483
+ loc: terminal.loc,
484
+ };
485
+ } else {
486
+ testValue = {
487
+ kind: "SequenceExpression",
488
+ instructions: [testBlock.instructions.at(-1)!],
489
+ id: terminal.id,
490
+ loc: terminal.loc,
491
+ value: {
492
+ kind: "Primitive",
493
+ value: undefined,
494
+ loc: terminal.loc,
495
+ },
496
+ };
497
+ }
498
+
499
let loopBody: ReactiveBlock;
500
if (loopId) {
501
loopBody = this.traverseBlock(this.cx.ir.blocks.get(loopId)!);
@@ -488,6 +513,7 @@ class Driver {
513
kind: "for-of",
514
loc: terminal.loc,
515
init: initValue,
516
+ test: testValue,
517
loop: loopBody,
518
id: terminal.id,
519
},
compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/CodegenReactiveFunction.ts
+112
-32
@@ -650,17 +650,16 @@ function codegenTerminal(
650
codegenBlock(cx, terminal.loop)
651
);
652
}
653
- case "for-in":
654
- case "for-of": {
653
+ case "for-in": {
654
CompilerError.invariant(terminal.init.kind === "SequenceExpression", {
656
- reason: `Expected a sequence expression init for ForOf`,
655
+ reason: `Expected a sequence expression init for for..in`,
656
description: `Got \`${terminal.init.kind}\` expression instead`,
657
loc: terminal.init.loc,
658
suggestions: null,
659
});
660
if (terminal.init.instructions.length !== 2) {
661
CompilerError.throwTodo({
663
- reason: "Support non-trivial ForOf inits",
662
+ reason: "Support non-trivial for..in inits",
663
description: null,
664
loc: terminal.init.loc,
665
suggestions: null,
@@ -704,14 +703,14 @@ function codegenTerminal(
703
});
704
case InstructionKind.Catch:
705
CompilerError.invariant(false, {
707
- reason: "Unexpected catch variable as for-of collection",
706
+ reason: "Unexpected catch variable as for..in collection",
707
description: null,
708
loc: iterableItem.loc,
709
suggestions: null,
710
});
711
case InstructionKind.HoistedConst:
712
CompilerError.invariant(false, {
714
- reason: "Unexpected HoistedConst variable in for-of collection",
713
+ reason: "Unexpected HoistedConst variable in for..in collection",
714
description: null,
715
loc: iterableItem.loc,
716
suggestions: null,
@@ -722,31 +721,112 @@ function codegenTerminal(
721
`Unhandled lvalue kind: ${iterableItem.value.lvalue.kind}`
722
);
723
}
725
- if (terminal.kind === "for-of") {
726
- return t.forOfStatement(
727
- /*
728
- * Special handling here since we only want the VariableDeclarators without any inits
729
- * This needs to be updated when we handle non-trivial ForOf inits
730
- */
731
- createVariableDeclaration(iterableItem.value.loc, varDeclKind, [
732
- t.variableDeclarator(lval, null),
733
- ]),
734
- codegenInstructionValueToExpression(cx, iterableCollection.value),
735
- codegenBlock(cx, terminal.loop)
736
- );
737
- } else {
738
- return t.forInStatement(
739
- /*
740
- * Special handling here since we only want the VariableDeclarators without any inits
741
- * This needs to be updated when we handle non-trivial ForOf inits
742
- */
743
- createVariableDeclaration(iterableItem.value.loc, varDeclKind, [
744
- t.variableDeclarator(lval, null),
745
- ]),
746
- codegenInstructionValueToExpression(cx, iterableCollection.value),
747
- codegenBlock(cx, terminal.loop)
748
- );
724
+ return t.forInStatement(
725
+ /*
726
+ * Special handling here since we only want the VariableDeclarators without any inits
727
+ * This needs to be updated when we handle non-trivial ForOf inits
728
+ */
729
+ createVariableDeclaration(iterableItem.value.loc, varDeclKind, [
730
+ t.variableDeclarator(lval, null),
731
+ ]),
732
+ codegenInstructionValueToExpression(cx, iterableCollection.value),
733
+ codegenBlock(cx, terminal.loop)
734
+ );
735
+ }
736
+ case "for-of": {
737
+ CompilerError.invariant(
738
+ terminal.init.kind === "SequenceExpression" &&
739
+ terminal.init.instructions.length === 1 &&
740
+ terminal.init.instructions[0].value.kind === "GetIterator",
741
+ {
742
+ reason: `Expected a single-expression sequence expression init for for..of`,
743
+ description: `Got \`${terminal.init.kind}\` expression instead`,
744
+ loc: terminal.init.loc,
745
+ suggestions: null,
746
+ }
747
+ );
748
+ const iterableCollection = terminal.init.instructions[0].value;
749
+
750
+ CompilerError.invariant(terminal.test.kind === "SequenceExpression", {
751
+ reason: `Expected a sequence expression test for for..of`,
752
+ description: `Got \`${terminal.init.kind}\` expression instead`,
753
+ loc: terminal.test.loc,
754
+ suggestions: null,
755
+ });
756
+ if (terminal.test.instructions.length !== 2) {
757
+ CompilerError.throwTodo({
758
+ reason: "Support non-trivial for..of inits",
759
+ description: null,
760
+ loc: terminal.init.loc,
761
+ suggestions: null,
762
+ });
763
}
764
+ const iterableItem = terminal.test.instructions[1];
765
+ let lval: t.LVal;
766
+ switch (iterableItem.value.kind) {
767
+ case "StoreLocal": {
768
+ lval = codegenLValue(cx, iterableItem.value.lvalue.place);
769
+ break;
770
+ }
771
+ case "Destructure": {
772
+ lval = codegenLValue(cx, iterableItem.value.lvalue.pattern);
773
+ break;
774
+ }
775
+ default:
776
+ CompilerError.invariant(false, {
777
+ reason: `Expected a StoreLocal or Destructure to be assigned to the collection`,
778
+ description: `Found ${iterableItem.value.kind}`,
779
+ loc: iterableItem.value.loc,
780
+ suggestions: null,
781
+ });
782
+ }
783
+ let varDeclKind: "const" | "let";
784
+ switch (iterableItem.value.lvalue.kind) {
785
+ case InstructionKind.Const:
786
+ varDeclKind = "const" as const;
787
+ break;
788
+ case InstructionKind.Let:
789
+ varDeclKind = "let" as const;
790
+ break;
791
+ case InstructionKind.Reassign:
792
+ CompilerError.invariant(false, {
793
+ reason:
794
+ "Destructure should never be Reassign as it would be an Object/ArrayPattern",
795
+ description: null,
796
+ loc: iterableItem.loc,
797
+ suggestions: null,
798
+ });
799
+ case InstructionKind.Catch:
800
+ CompilerError.invariant(false, {
801
+ reason: "Unexpected catch variable as for..of collection",
802
+ description: null,
803
+ loc: iterableItem.loc,
804
+ suggestions: null,
805
+ });
806
+ case InstructionKind.HoistedConst:
807
+ CompilerError.invariant(false, {
808
+ reason: "Unexpected HoistedConst variable in for..of collection",
809
+ description: null,
810
+ loc: iterableItem.loc,
811
+ suggestions: null,
812
+ });
813
+ default:
814
+ assertExhaustive(
815
+ iterableItem.value.lvalue.kind,
816
+ `Unhandled lvalue kind: ${iterableItem.value.lvalue.kind}`
817
+ );
818
+ }
819
+ return t.forOfStatement(
820
+ /*
821
+ * Special handling here since we only want the VariableDeclarators without any inits
822
+ * This needs to be updated when we handle non-trivial ForOf inits
823
+ */
824
+ createVariableDeclaration(iterableItem.value.loc, varDeclKind, [
825
+ t.variableDeclarator(lval, null),
826
+ ]),
827
+ codegenInstructionValueToExpression(cx, iterableCollection),
828
+ codegenBlock(cx, terminal.loop)
829
+ );
830
}
831
case "if": {
832
const test = codegenPlaceToExpression(cx, terminal.test);
@@ -1773,11 +1853,11 @@ function codegenInstructionValue(
1853
break;
1854
}
1855
case "GetIterator": {
1776
- value = codegenPlaceToExpression(cx, instrValue.value);
1856
+ value = codegenPlaceToExpression(cx, instrValue.collection);
1857
break;
1858
}
1859
case "NextIterableOf": {
1780
- value = codegenPlaceToExpression(cx, instrValue.value);
1860
+ value = codegenPlaceToExpression(cx, instrValue.iterator);
1861
break;
1862
}
1863
case "NextPropertyOf": {
compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/PrintReactiveFunction.ts
+2
@@ -318,6 +318,8 @@ function writeTerminal(writer: Writer, terminal: ReactiveTerminal): void {
318
writer.writeLine(`[${terminal.id}] for-of (`);
319
writer.indented(() => {
320
writeReactiveValue(writer, terminal.init);
321
+ writer.writeLine(";");
322
+ writeReactiveValue(writer, terminal.test);
323
});
324
writer.writeLine(") {");
325
writeReactiveInstructions(writer, terminal.loop);
compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/PruneNonEscapingScopes.ts
+21
-3
@@ -474,9 +474,7 @@ function computeMemoizationInputs(
474
};
475
}
476
case "Await":
477
- case "TypeCastExpression":
478
- case "GetIterator":
479
- case "NextIterableOf": {
477
+ case "TypeCastExpression": {
478
return {
479
// Indirection for the inner value, memoized if the value is
480
lvalues:
@@ -486,6 +484,26 @@ function computeMemoizationInputs(
484
rvalues: [value.value],
485
};
486
}
487
+ case "NextIterableOf": {
488
+ return {
489
+ // Indirection for the inner value, memoized if the value is
490
+ lvalues:
491
+ lvalue !== null
492
+ ? [{ place: lvalue, level: MemoizationLevel.Conditional }]
493
+ : [],
494
+ rvalues: [value.iterator, value.collection],
495
+ };
496
+ }
497
+ case "GetIterator": {
498
+ return {
499
+ // Indirection for the inner value, memoized if the value is
500
+ lvalues:
501
+ lvalue !== null
502
+ ? [{ place: lvalue, level: MemoizationLevel.Conditional }]
503
+ : [],
504
+ rvalues: [value.collection],
505
+ };
506
+ }
507
case "LoadLocal": {
508
return {
509
// Indirection for the inner value, memoized if the value is
compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/visitors.ts
+5
@@ -132,6 +132,7 @@ export class ReactiveFunctionVisitor<TState = void> {
132
}
133
case "for-of": {
134
this.visitValue(terminal.id, terminal.init, state);
135
+ this.visitValue(terminal.id, terminal.test, state);
136
this.visitBlock(terminal.loop, state);
137
break;
138
}
@@ -492,6 +493,10 @@ export class ReactiveFunctionTransform<
493
if (init.kind === "replace") {
494
terminal.init = init.value;
495
}
496
+ const test = this.transformValue(terminal.id, terminal.test, state);
497
+ if (test.kind === "replace") {
498
+ terminal.test = test.value;
499
+ }
500
this.visitBlock(terminal.loop, state);
501
break;
502
}
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.invalid-mutate-props-via-for-of-iterator.expect.md
new
+29
@@ -0,0 +1,29 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+function Component(props) {
6
+ const items = [];
7
+ for (const x of props.items) {
8
+ x.modified = true;
9
+ items.push(x);
10
+ }
11
+ return items;
12
+}
13
+
14
+```
15
+
16
+
17
+## Error
18
+
19
+```
20
+ 2 | const items = [];
21
+ 3 | for (const x of props.items) {
22
+> 4 | x.modified = true;
23
+ | ^ InvalidReact: Mutating component props or hook arguments is not allowed. Consider using a local variable instead (4:4)
24
+ 5 | items.push(x);
25
+ 6 | }
26
+ 7 | return items;
27
+```
28
+
29
+
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.invalid-mutate-props-via-for-of-iterator.js
new
+8
@@ -0,0 +1,8 @@
1
+function Component(props) {
2
+ const items = [];
3
+ for (const x of props.items) {
4
+ x.modified = true;
5
+ items.push(x);
6
+ }
7
+ return items;
8
+}
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.todo-for-in-loop-with-context-variable-iterator.expect.md
+1
-1
@@ -48,7 +48,7 @@ export const FIXTURE_ENTRYPOINT = {
48
> 14 | );
49
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
50
> 15 | }
51
- | ^^^^ Todo: Support non-trivial ForOf inits (8:15)
51
+ | ^^^^ Todo: Support non-trivial for..in inits (8:15)
52
16 | return <div>{items}</div>;
53
17 | }
54
18 |
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.todo-for-of-capture-item-of-local-collection-mutate-later-value-initially-null.expect.md
new
+34
@@ -0,0 +1,34 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+import { makeObject_Primitives } from "shared-runtime";
6
+
7
+function Component(props) {
8
+ let lastItem = null; // we reject this code bc `lastItem` could be null and you can't mutate null
9
+ const items = [makeObject_Primitives(), makeObject_Primitives()];
10
+ for (const x of items) {
11
+ lastItem = x;
12
+ }
13
+ if (lastItem != null) {
14
+ lastItem.mutated = true;
15
+ }
16
+ return items;
17
+}
18
+
19
+```
20
+
21
+
22
+## Error
23
+
24
+```
25
+ 8 | }
26
+ 9 | if (lastItem != null) {
27
+> 10 | lastItem.mutated = true;
28
+ | ^^^^^^^^ InvalidReact: This mutates a variable that React considers immutable (10:10)
29
+ 11 | }
30
+ 12 | return items;
31
+ 13 | }
32
+```
33
+
34
+
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.todo-for-of-capture-item-of-local-collection-mutate-later-value-initially-null.js
new
+13
@@ -0,0 +1,13 @@
1
+import { makeObject_Primitives } from "shared-runtime";
2
+
3
+function Component(props) {
4
+ let lastItem = null; // we reject this code bc `lastItem` could be null and you can't mutate null
5
+ const items = [makeObject_Primitives(), makeObject_Primitives()];
6
+ for (const x of items) {
7
+ lastItem = x;
8
+ }
9
+ if (lastItem != null) {
10
+ lastItem.mutated = true;
11
+ }
12
+ return items;
13
+}
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.todo-for-of-loop-with-context-variable-iterator.expect.md
+1
-1
@@ -48,7 +48,7 @@ export const FIXTURE_ENTRYPOINT = {
48
> 14 | );
49
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
50
> 15 | }
51
- | ^^^^ Todo: Support non-trivial ForOf inits (8:15)
51
+ | ^^^^ Todo: Support non-trivial for..of inits (8:15)
52
16 | return <div>{items}</div>;
53
17 | }
54
18 |
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/for-of-capture-item-of-local-collection-mutate-later.expect.md
new
+62
@@ -0,0 +1,62 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+import { makeObject_Primitives } from "shared-runtime";
6
+
7
+function Component(props) {
8
+ let lastItem = {};
9
+ const items = [makeObject_Primitives(), makeObject_Primitives()];
10
+ for (const x of items) {
11
+ lastItem = x;
12
+ }
13
+ if (lastItem != null) {
14
+ lastItem.a += 1;
15
+ }
16
+ return items;
17
+}
18
+
19
+export const FIXTURE_ENTRYPOINT = {
20
+ fn: Component,
21
+ params: [{}],
22
+ sequentialRenders: [{}, {}],
23
+};
24
+
25
+```
26
+
27
+## Code
28
+
29
+```javascript
30
+import { unstable_useMemoCache as useMemoCache } from "react";
31
+import { makeObject_Primitives } from "shared-runtime";
32
+
33
+function Component(props) {
34
+ const $ = useMemoCache(1);
35
+ let items;
36
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
37
+ let lastItem = {};
38
+ items = [makeObject_Primitives(), makeObject_Primitives()];
39
+ for (const x of items) {
40
+ lastItem = x;
41
+ }
42
+ if (lastItem != null) {
43
+ lastItem.a = lastItem.a + 1;
44
+ }
45
+ $[0] = items;
46
+ } else {
47
+ items = $[0];
48
+ }
49
+ return items;
50
+}
51
+
52
+export const FIXTURE_ENTRYPOINT = {
53
+ fn: Component,
54
+ params: [{}],
55
+ sequentialRenders: [{}, {}],
56
+};
57
+
58
+```
59
+
60
+### Eval output
61
+(kind: ok) [{"a":0,"b":"value1","c":true},{"a":1,"b":"value1","c":true}]
62
+[{"a":0,"b":"value1","c":true},{"a":1,"b":"value1","c":true}]
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/for-of-capture-item-of-local-collection-mutate-later.js
new
+19
@@ -0,0 +1,19 @@
1
+import { makeObject_Primitives } from "shared-runtime";
2
+
3
+function Component(props) {
4
+ let lastItem = {};
5
+ const items = [makeObject_Primitives(), makeObject_Primitives()];
6
+ for (const x of items) {
7
+ lastItem = x;
8
+ }
9
+ if (lastItem != null) {
10
+ lastItem.a += 1;
11
+ }
12
+ return items;
13
+}
14
+
15
+export const FIXTURE_ENTRYPOINT = {
16
+ fn: Component,
17
+ params: [{}],
18
+ sequentialRenders: [{}, {}],
19
+};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/for-of-iterator-of-immutable-collection.expect.md
+6
-11
@@ -37,20 +37,12 @@ export const FIXTURE_ENTRYPOINT = {
37
```javascript
38
import { unstable_useMemoCache as useMemoCache } from "react";
39
function Router(t0) {
40
- const $ = useMemoCache(5);
40
+ const $ = useMemoCache(3);
41
const { title, mapping } = t0;
42
let array;
43
if ($[0] !== mapping || $[1] !== title) {
44
array = [];
45
- let t1;
46
- if ($[3] !== mapping) {
47
- t1 = mapping.values();
48
- $[3] = mapping;
49
- $[4] = t1;
50
- } else {
51
- t1 = $[4];
52
- }
53
- for (const entry of t1) {
45
+ for (const entry of mapping.values()) {
46
array.push([title, entry]);
47
}
48
$[0] = mapping;
@@ -83,4 +75,7 @@ export const FIXTURE_ENTRYPOINT = {
75
};
76
77
```
86
-
\ No newline at end of file
78
+
79
+### Eval output
80
+(kind: ok) [["Foo","/about"],["Foo","/contact"]]
81
+[["Bar","/about"],["Bar","/contact"]]
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/for-of-mutate-item-of-local-collection.expect.md
new
+55
@@ -0,0 +1,55 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+import { makeObject_Primitives } from "shared-runtime";
6
+
7
+function Component(props) {
8
+ const items = [makeObject_Primitives(), makeObject_Primitives()];
9
+ for (const x of items) {
10
+ x.a += 1;
11
+ }
12
+ return items;
13
+}
14
+
15
+export const FIXTURE_ENTRYPOINT = {
16
+ fn: Component,
17
+ params: [{}],
18
+ sequentialRenders: [{}, {}, {}],
19
+};
20
+
21
+```
22
+
23
+## Code
24
+
25
+```javascript
26
+import { unstable_useMemoCache as useMemoCache } from "react";
27
+import { makeObject_Primitives } from "shared-runtime";
28
+
29
+function Component(props) {
30
+ const $ = useMemoCache(1);
31
+ let items;
32
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
33
+ items = [makeObject_Primitives(), makeObject_Primitives()];
34
+ for (const x of items) {
35
+ x.a = x.a + 1;
36
+ }
37
+ $[0] = items;
38
+ } else {
39
+ items = $[0];
40
+ }
41
+ return items;
42
+}
43
+
44
+export const FIXTURE_ENTRYPOINT = {
45
+ fn: Component,
46
+ params: [{}],
47
+ sequentialRenders: [{}, {}, {}],
48
+};
49
+
50
+```
51
+
52
+### Eval output
53
+(kind: ok) [{"a":1,"b":"value1","c":true},{"a":1,"b":"value1","c":true}]
54
+[{"a":1,"b":"value1","c":true},{"a":1,"b":"value1","c":true}]
55
+[{"a":1,"b":"value1","c":true},{"a":1,"b":"value1","c":true}]
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/for-of-mutate-item-of-local-collection.js
new
+15
@@ -0,0 +1,15 @@
1
+import { makeObject_Primitives } from "shared-runtime";
2
+
3
+function Component(props) {
4
+ const items = [makeObject_Primitives(), makeObject_Primitives()];
5
+ for (const x of items) {
6
+ x.a += 1;
7
+ }
8
+ return items;
9
+}
10
+
11
+export const FIXTURE_ENTRYPOINT = {
12
+ fn: Component,
13
+ params: [{}],
14
+ sequentialRenders: [{}, {}, {}],
15
+};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/for-of-nonmutating-loop-local-collection.expect.md
new
+142
@@ -0,0 +1,142 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+import { useMemo } from "react";
6
+import { ValidateMemoization } from "shared-runtime";
7
+
8
+function Component({ a, b }) {
9
+ const x = useMemo(() => {
10
+ return [a];
11
+ }, [a]);
12
+ const y = useMemo(() => {
13
+ const items = [b];
14
+ for (const i of x) {
15
+ items.push(i);
16
+ }
17
+ return items;
18
+ }, [x, b]);
19
+ return (
20
+ <>
21
+ <ValidateMemoization inputs={[a]} output={x} />
22
+ <ValidateMemoization inputs={[x, b]} output={y} />
23
+ </>
24
+ );
25
+}
26
+
27
+export const FIXTURE_ENTRYPOINT = {
28
+ fn: Component,
29
+ params: [{ a: 0, b: 0 }],
30
+ sequentialRenders: [
31
+ { a: 1, b: 0 },
32
+ { a: 1, b: 1 },
33
+ { a: 0, b: 1 },
34
+ ],
35
+};
36
+
37
+```
38
+
39
+## Code
40
+
41
+```javascript
42
+import { useMemo, unstable_useMemoCache as useMemoCache } from "react";
43
+import { ValidateMemoization } from "shared-runtime";
44
+
45
+function Component(t0) {
46
+ const $ = useMemoCache(19);
47
+ const { a, b } = t0;
48
+ let t1;
49
+ let t2;
50
+ if ($[0] !== a) {
51
+ t2 = [a];
52
+ $[0] = a;
53
+ $[1] = t2;
54
+ } else {
55
+ t2 = $[1];
56
+ }
57
+ t1 = t2;
58
+ const x = t1;
59
+ let t3;
60
+ let items;
61
+ if ($[2] !== b || $[3] !== x) {
62
+ items = [b];
63
+ for (const i of x) {
64
+ items.push(i);
65
+ }
66
+ $[2] = b;
67
+ $[3] = x;
68
+ $[4] = items;
69
+ } else {
70
+ items = $[4];
71
+ }
72
+
73
+ t3 = items;
74
+ const y = t3;
75
+ let t4;
76
+ if ($[5] !== a) {
77
+ t4 = [a];
78
+ $[5] = a;
79
+ $[6] = t4;
80
+ } else {
81
+ t4 = $[6];
82
+ }
83
+ let t5;
84
+ if ($[7] !== t4 || $[8] !== x) {
85
+ t5 = <ValidateMemoization inputs={t4} output={x} />;
86
+ $[7] = t4;
87
+ $[8] = x;
88
+ $[9] = t5;
89
+ } else {
90
+ t5 = $[9];
91
+ }
92
+ let t6;
93
+ if ($[10] !== x || $[11] !== b) {
94
+ t6 = [x, b];
95
+ $[10] = x;
96
+ $[11] = b;
97
+ $[12] = t6;
98
+ } else {
99
+ t6 = $[12];
100
+ }
101
+ let t7;
102
+ if ($[13] !== t6 || $[14] !== y) {
103
+ t7 = <ValidateMemoization inputs={t6} output={y} />;
104
+ $[13] = t6;
105
+ $[14] = y;
106
+ $[15] = t7;
107
+ } else {
108
+ t7 = $[15];
109
+ }
110
+ let t8;
111
+ if ($[16] !== t5 || $[17] !== t7) {
112
+ t8 = (
113
+ <>
114
+ {t5}
115
+ {t7}
116
+ </>
117
+ );
118
+ $[16] = t5;
119
+ $[17] = t7;
120
+ $[18] = t8;
121
+ } else {
122
+ t8 = $[18];
123
+ }
124
+ return t8;
125
+}
126
+
127
+export const FIXTURE_ENTRYPOINT = {
128
+ fn: Component,
129
+ params: [{ a: 0, b: 0 }],
130
+ sequentialRenders: [
131
+ { a: 1, b: 0 },
132
+ { a: 1, b: 1 },
133
+ { a: 0, b: 1 },
134
+ ],
135
+};
136
+
137
+```
138
+
139
+### Eval output
140
+(kind: ok) <div>{"inputs":[1],"output":[1]}</div><div>{"inputs":[[1],0],"output":[0,1]}</div>
141
+<div>{"inputs":[1],"output":[1]}</div><div>{"inputs":[[1],1],"output":[1,1]}</div>
142
+<div>{"inputs":[0],"output":[0]}</div><div>{"inputs":[[0],1],"output":[1,0]}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/for-of-nonmutating-loop-local-collection.js
new
+31
@@ -0,0 +1,31 @@
1
+import { useMemo } from "react";
2
+import { ValidateMemoization } from "shared-runtime";
3
+
4
+function Component({ a, b }) {
5
+ const x = useMemo(() => {
6
+ return [a];
7
+ }, [a]);
8
+ const y = useMemo(() => {
9
+ const items = [b];
10
+ for (const i of x) {
11
+ items.push(i);
12
+ }
13
+ return items;
14
+ }, [x, b]);
15
+ return (
16
+ <>
17
+ <ValidateMemoization inputs={[a]} output={x} />
18
+ <ValidateMemoization inputs={[x, b]} output={y} />
19
+ </>
20
+ );
21
+}
22
+
23
+export const FIXTURE_ENTRYPOINT = {
24
+ fn: Component,
25
+ params: [{ a: 0, b: 0 }],
26
+ sequentialRenders: [
27
+ { a: 1, b: 0 },
28
+ { a: 1, b: 1 },
29
+ { a: 0, b: 1 },
30
+ ],
31
+};
compiler/packages/snap/src/SproutTodoFilter.ts
-1
@@ -489,7 +489,6 @@ const skipFilter = new Set([
489
490
// bugs
491
"bug-invalid-reactivity-value-block",
492
- "for-of-iterator-of-immutable-collection",
492
493
// 'react-forget-runtime' not yet supported
494
"flag-enable-emit-hook-guards",