diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts
index e6883250705d4..e2e2d4b7c1b66 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts
+++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts
@@ -872,11 +872,31 @@ function inferBlock(
reason: new Set([ValueReason.Other]),
context: new Set(),
};
+
+ for (const element of instrValue.elements) {
+ if (element.kind === 'Spread') {
+ state.referenceAndRecordEffects(
+ freezeActions,
+ element.place,
+ Effect.ConditionallyMutate,
+ ValueReason.Other,
+ );
+ } else if (element.kind === 'Identifier') {
+ state.referenceAndRecordEffects(
+ freezeActions,
+ element,
+ Effect.Capture,
+ ValueReason.Other,
+ );
+ } else {
+ let _: 'Hole' = element.kind;
+ }
+ }
+ state.initialize(instrValue, valueKind);
+ state.define(instr.lvalue, instrValue);
+ instr.lvalue.effect = Effect.Store;
continuation = {
- kind: 'initialize',
- valueKind,
- effect: {kind: Effect.Capture, reason: ValueReason.Other},
- lvalueEffect: Effect.Store,
+ kind: 'funeffects',
};
break;
}
@@ -1241,21 +1261,12 @@ function inferBlock(
for (let i = 0; i < instrValue.args.length; i++) {
const arg = instrValue.args[i];
const place = arg.kind === 'Identifier' ? arg : arg.place;
- if (effects !== null) {
- state.referenceAndRecordEffects(
- freezeActions,
- place,
- effects[i],
- ValueReason.Other,
- );
- } else {
- state.referenceAndRecordEffects(
- freezeActions,
- place,
- Effect.ConditionallyMutate,
- ValueReason.Other,
- );
- }
+ state.referenceAndRecordEffects(
+ freezeActions,
+ place,
+ getArgumentEffect(effects != null ? effects[i] : null, arg),
+ ValueReason.Other,
+ );
hasCaptureArgument ||= place.effect === Effect.Capture;
}
if (signature !== null) {
@@ -1307,7 +1318,10 @@ function inferBlock(
signature !== null
? {
kind: signature.returnValueKind,
- reason: new Set([ValueReason.Other]),
+ reason: new Set([
+ signature.returnValueReason ??
+ ValueReason.KnownReturnSignature,
+ ]),
context: new Set(),
}
: {
@@ -1330,7 +1344,8 @@ function inferBlock(
state.referenceAndRecordEffects(
freezeActions,
place,
- Effect.Read,
+ // see call-spread-argument-mutable-iterator test fixture
+ arg.kind === 'Spread' ? Effect.ConditionallyMutate : Effect.Read,
ValueReason.Other,
);
}
@@ -1356,25 +1371,16 @@ function inferBlock(
for (let i = 0; i < instrValue.args.length; i++) {
const arg = instrValue.args[i];
const place = arg.kind === 'Identifier' ? arg : arg.place;
- if (effects !== null) {
- /*
- * If effects are inferred for an argument, we should fail invalid
- * mutating effects
- */
- state.referenceAndRecordEffects(
- freezeActions,
- place,
- effects[i],
- ValueReason.Other,
- );
- } else {
- state.referenceAndRecordEffects(
- freezeActions,
- place,
- Effect.ConditionallyMutate,
- ValueReason.Other,
- );
- }
+ /*
+ * If effects are inferred for an argument, we should fail invalid
+ * mutating effects
+ */
+ state.referenceAndRecordEffects(
+ freezeActions,
+ place,
+ getArgumentEffect(effects != null ? effects[i] : null, arg),
+ ValueReason.Other,
+ );
hasCaptureArgument ||= place.effect === Effect.Capture;
}
if (signature !== null) {
@@ -2049,3 +2055,31 @@ function areArgumentsImmutableAndNonMutating(
}
return true;
}
+
+function getArgumentEffect(
+ signatureEffect: Effect | null,
+ arg: Place | SpreadPattern,
+): Effect {
+ if (signatureEffect != null) {
+ if (arg.kind === 'Identifier') {
+ return signatureEffect;
+ } else if (
+ signatureEffect === Effect.Mutate ||
+ signatureEffect === Effect.ConditionallyMutate
+ ) {
+ return signatureEffect;
+ } else {
+ // see call-spread-argument-mutable-iterator test fixture
+ if (signatureEffect === Effect.Freeze) {
+ CompilerError.throwTodo({
+ reason: 'Support spread syntax for hook arguments',
+ loc: arg.place.loc,
+ });
+ }
+ // effects[i] is Effect.Capture | Effect.Read | Effect.Store
+ return Effect.ConditionallyMutate;
+ }
+ } else {
+ return Effect.ConditionallyMutate;
+ }
+}
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-spread-later-mutated.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-spread-later-mutated.expect.md
new file mode 100644
index 0000000000000..a317e22faf92e
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-spread-later-mutated.expect.md
@@ -0,0 +1,62 @@
+
+## Input
+
+```javascript
+function useBar({arg}) {
+ /**
+ * Note that mutableIterator is mutated by the later object spread. Therefore,
+ * `s.values()` should be memoized within the same block as the object spread.
+ * In terms of compiler internals, they should have the same reactive scope.
+ */
+ const obj = {};
+ const s = new Set([obj, 5, 4]);
+ const mutableIterator = s.values();
+ const arr = [...mutableIterator];
+
+ obj.x = arg;
+ return arr;
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: useBar,
+ params: [{arg: 3}],
+ sequentialRenders: [{arg: 3}, {arg: 3}, {arg: 4}],
+};
+
+```
+
+## Code
+
+```javascript
+import { c as _c } from "react/compiler-runtime";
+function useBar(t0) {
+ const $ = _c(2);
+ const { arg } = t0;
+ let arr;
+ if ($[0] !== arg) {
+ const obj = {};
+ const s = new Set([obj, 5, 4]);
+ const mutableIterator = s.values();
+ arr = [...mutableIterator];
+
+ obj.x = arg;
+ $[0] = arg;
+ $[1] = arr;
+ } else {
+ arr = $[1];
+ }
+ return arr;
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: useBar,
+ params: [{ arg: 3 }],
+ sequentialRenders: [{ arg: 3 }, { arg: 3 }, { arg: 4 }],
+};
+
+```
+
+### Eval output
+(kind: ok) [{"x":3},5,4]
+[{"x":3},5,4]
+[{"x":4},5,4]
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-spread-later-mutated.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-spread-later-mutated.js
new file mode 100644
index 0000000000000..036ce2ddfc681
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-spread-later-mutated.js
@@ -0,0 +1,20 @@
+function useBar({arg}) {
+ /**
+ * Note that mutableIterator is mutated by the later object spread. Therefore,
+ * `s.values()` should be memoized within the same block as the object spread.
+ * In terms of compiler internals, they should have the same reactive scope.
+ */
+ const obj = {};
+ const s = new Set([obj, 5, 4]);
+ const mutableIterator = s.values();
+ const arr = [...mutableIterator];
+
+ obj.x = arg;
+ return arr;
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: useBar,
+ params: [{arg: 3}],
+ sequentialRenders: [{arg: 3}, {arg: 3}, {arg: 4}],
+};
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-spread-mutable-iterator.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-spread-mutable-iterator.expect.md
new file mode 100644
index 0000000000000..25499af1b0202
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-spread-mutable-iterator.expect.md
@@ -0,0 +1,85 @@
+
+## Input
+
+```javascript
+/**
+ * TODO: object spreads should have conditionally mutate semantics
+ * Found differences in evaluator results
+ * Non-forget (expected):
+ * (kind: ok) [3,1,5,4]
+ * [3,1,5,4]
+ * [4,1,5,4]
+ * Forget:
+ * (kind: ok) [3,1,5,4]
+ * [3,1,5,4]
+ * [4]
+ */
+
+function useBar({arg}) {
+ 'use memo';
+
+ /**
+ * Note that mutableIterator is mutated by the later object spread. Therefore,
+ * `s.values()` should be memoized within the same block as the object spread.
+ * In terms of compiler internals, they should have the same reactive scope.
+ */
+ const s = new Set([1, 5, 4]);
+ const mutableIterator = s.values();
+
+ return [arg, ...mutableIterator];
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: useBar,
+ params: [{arg: 3}],
+ sequentialRenders: [{arg: 3}, {arg: 3}, {arg: 4}],
+};
+
+```
+
+## Code
+
+```javascript
+import { c as _c } from "react/compiler-runtime"; /**
+ * TODO: object spreads should have conditionally mutate semantics
+ * Found differences in evaluator results
+ * Non-forget (expected):
+ * (kind: ok) [3,1,5,4]
+ * [3,1,5,4]
+ * [4,1,5,4]
+ * Forget:
+ * (kind: ok) [3,1,5,4]
+ * [3,1,5,4]
+ * [4]
+ */
+
+function useBar(t0) {
+ "use memo";
+ const $ = _c(2);
+ const { arg } = t0;
+ let t1;
+ if ($[0] !== arg) {
+ const s = new Set([1, 5, 4]);
+ const mutableIterator = s.values();
+
+ t1 = [arg, ...mutableIterator];
+ $[0] = arg;
+ $[1] = t1;
+ } else {
+ t1 = $[1];
+ }
+ return t1;
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: useBar,
+ params: [{ arg: 3 }],
+ sequentialRenders: [{ arg: 3 }, { arg: 3 }, { arg: 4 }],
+};
+
+```
+
+### Eval output
+(kind: ok) [3,1,5,4]
+[3,1,5,4]
+[4,1,5,4]
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-spread-mutable-iterator.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-spread-mutable-iterator.js
new file mode 100644
index 0000000000000..c83a9e53e6acc
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-spread-mutable-iterator.js
@@ -0,0 +1,32 @@
+/**
+ * TODO: object spreads should have conditionally mutate semantics
+ * Found differences in evaluator results
+ * Non-forget (expected):
+ * (kind: ok) [3,1,5,4]
+ * [3,1,5,4]
+ * [4,1,5,4]
+ * Forget:
+ * (kind: ok) [3,1,5,4]
+ * [3,1,5,4]
+ * [4]
+ */
+
+function useBar({arg}) {
+ 'use memo';
+
+ /**
+ * Note that mutableIterator is mutated by the later object spread. Therefore,
+ * `s.values()` should be memoized within the same block as the object spread.
+ * In terms of compiler internals, they should have the same reactive scope.
+ */
+ const s = new Set([1, 5, 4]);
+ const mutableIterator = s.values();
+
+ return [arg, ...mutableIterator];
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: useBar,
+ params: [{arg: 3}],
+ sequentialRenders: [{arg: 3}, {arg: 3}, {arg: 4}],
+};
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/call-spread-argument-mutable-iterator.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/call-spread-argument-mutable-iterator.expect.md
new file mode 100644
index 0000000000000..74fb57b6bc2fe
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/call-spread-argument-mutable-iterator.expect.md
@@ -0,0 +1,42 @@
+
+## Input
+
+```javascript
+import {useIdentity} from 'shared-runtime';
+
+function useFoo() {
+ const it = new Set([1, 2]).values();
+ useIdentity();
+ return Math.max(...it);
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: useFoo,
+ params: [{}],
+ sequentialRenders: [{}, {}],
+};
+
+```
+
+## Code
+
+```javascript
+import { useIdentity } from "shared-runtime";
+
+function useFoo() {
+ const it = new Set([1, 2]).values();
+ useIdentity();
+ return Math.max(...it);
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: useFoo,
+ params: [{}],
+ sequentialRenders: [{}, {}],
+};
+
+```
+
+### Eval output
+(kind: ok) 2
+2
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/call-spread-argument-mutable-iterator.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/call-spread-argument-mutable-iterator.js
new file mode 100644
index 0000000000000..1b30f0a46f7c7
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/call-spread-argument-mutable-iterator.js
@@ -0,0 +1,13 @@
+import {useIdentity} from 'shared-runtime';
+
+function useFoo() {
+ const it = new Set([1, 2]).values();
+ useIdentity();
+ return Math.max(...it);
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: useFoo,
+ params: [{}],
+ sequentialRenders: [{}, {}],
+};
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-hook-call-spreads-mutable-iterator.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-hook-call-spreads-mutable-iterator.expect.md
new file mode 100644
index 0000000000000..5d0af122f2146
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-hook-call-spreads-mutable-iterator.expect.md
@@ -0,0 +1,33 @@
+
+## Input
+
+```javascript
+import {useIdentity} from 'shared-runtime';
+
+function Component() {
+ const items = makeArray(0, 1, 2, null, 4, false, 6);
+ return useIdentity(...items.values());
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [],
+ sequentialRenders: [{}, {}],
+};
+
+```
+
+
+## Error
+
+```
+ 3 | function Component() {
+ 4 | const items = makeArray(0, 1, 2, null, 4, false, 6);
+> 5 | return useIdentity(...items.values());
+ | ^^^^^^^^^^^^^^ Todo: Support spread syntax for hook arguments (5:5)
+ 6 | }
+ 7 |
+ 8 | export const FIXTURE_ENTRYPOINT = {
+```
+
+
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-hook-call-spreads-mutable-iterator.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-hook-call-spreads-mutable-iterator.js
new file mode 100644
index 0000000000000..982fd83dc8a20
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-hook-call-spreads-mutable-iterator.js
@@ -0,0 +1,12 @@
+import {useIdentity} from 'shared-runtime';
+
+function Component() {
+ const items = makeArray(0, 1, 2, null, 4, false, 6);
+ return useIdentity(...items.values());
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [],
+ sequentialRenders: [{}, {}],
+};
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-nested-method-calls-lower-property-load-into-temporary.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-nested-method-calls-lower-property-load-into-temporary.expect.md
index 877c15e883ffa..56bf9e3d62148 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-nested-method-calls-lower-property-load-into-temporary.expect.md
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-nested-method-calls-lower-property-load-into-temporary.expect.md
@@ -4,9 +4,10 @@
```javascript
import {makeArray} from 'shared-runtime';
-function Component(props) {
+const other = [0, 1];
+function Component({}) {
const items = makeArray(0, 1, 2, null, 4, false, 6);
- const max = Math.max(...items.filter(Boolean));
+ const max = Math.max(2, items.push(5), ...other);
return max;
}
@@ -21,13 +22,13 @@ export const FIXTURE_ENTRYPOINT = {
## Error
```
- 3 | function Component(props) {
- 4 | const items = makeArray(0, 1, 2, null, 4, false, 6);
-> 5 | const max = Math.max(...items.filter(Boolean));
- | ^^^^^^^^ Invariant: [Codegen] Internal error: MethodCall::property must be an unpromoted + unmemoized MemberExpression. Got a `Identifier` (5:5)
- 6 | return max;
- 7 | }
- 8 |
+ 4 | function Component({}) {
+ 5 | const items = makeArray(0, 1, 2, null, 4, false, 6);
+> 6 | const max = Math.max(2, items.push(5), ...other);
+ | ^^^^^^^^ Invariant: [Codegen] Internal error: MethodCall::property must be an unpromoted + unmemoized MemberExpression. Got a `Identifier` (6:6)
+ 7 | return max;
+ 8 | }
+ 9 |
```
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-nested-method-calls-lower-property-load-into-temporary.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-nested-method-calls-lower-property-load-into-temporary.js
index 475cf383cf1e4..b2883c3303831 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-nested-method-calls-lower-property-load-into-temporary.js
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-nested-method-calls-lower-property-load-into-temporary.js
@@ -1,8 +1,9 @@
import {makeArray} from 'shared-runtime';
-function Component(props) {
+const other = [0, 1];
+function Component({}) {
const items = makeArray(0, 1, 2, null, 4, false, 6);
- const max = Math.max(...items.filter(Boolean));
+ const max = Math.max(2, items.push(5), ...other);
return max;
}
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo-granular-iterator-semantics.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo-granular-iterator-semantics.expect.md
new file mode 100644
index 0000000000000..1ba01dc5bf4df
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo-granular-iterator-semantics.expect.md
@@ -0,0 +1,96 @@
+
+## Input
+
+```javascript
+import {useIdentity, ValidateMemoization} from 'shared-runtime';
+
+/**
+ * TODO fixture for granular iterator semantics:
+ * 1. ConditionallyMutate the iterator itself, depending on whether the iterator
+ * is a mutable iterator.
+ * 2. Capture effect on elements within the iterator.
+ */
+function Validate({x, input}) {
+ 'use no memo';
+ return (
+ <>
+