@samitouri / QOS-React / commits / 21b2af8ba0

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",