Skip to content

Commit de8fb89

Browse files
committed
[compiler] Patch array and argument spread mutability
Array and argument spreads may mutate stateful iterables. Spread sites should have `ConditionallyMutate` effects (e.g. mutate if the ValueKind is mutable, otherwise read). See - [ecma spec (13.2.4.1 Runtime Semantics: ArrayAccumulation. SpreadElement : ... AssignmentExpression)](https://tc39.es/ecma262/multipage/ecmascript-language-expressions.html#sec-runtime-semantics-arrayaccumulation). - [ecma spec 13.3.8.1 Runtime Semantics: ArgumentListEvaluation](https://tc39.es/ecma262/multipage/ecmascript-language-expressions.html#sec-runtime-semantics-argumentlistevaluation) Note that - Object and JSX Attribute spreads do not evaluate iterables (srcs [mozilla](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax#description), [ecma](https://tc39.es/ecma262/multipage/ecmascript-language-expressions.html#sec-runtime-semantics-propertydefinitionevaluation)) - An ideal mutability inference system could model known collections (i.e. Arrays or Sets) as a "mutated collection of non-mutable objects" (see `todo-granular-iterator-semantics`), but this is not what we do today. As such, an array / argument spread will always extend the range of built-in arrays, sets, etc - Due to HIR limitations, call expressions with argument spreads may cause unnecessary bailouts and/or scope merging when we know the call itself has `freeze`, `capture`, or `read` semantics (e.g. `useHook(...mutableValue)`) We can deal with this by rewriting these call instructions to (1) create an intermediate array to consume the iterator and (2) capture and spread the array at the callsite
1 parent 464a6c6 commit de8fb89

12 files changed

+283
-68
lines changed

compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts

+74-40
Original file line numberDiff line numberDiff line change
@@ -872,11 +872,31 @@ function inferBlock(
872872
reason: new Set([ValueReason.Other]),
873873
context: new Set(),
874874
};
875+
876+
for (const element of instrValue.elements) {
877+
if (element.kind === 'Spread') {
878+
state.referenceAndRecordEffects(
879+
freezeActions,
880+
element.place,
881+
Effect.ConditionallyMutate,
882+
ValueReason.Other,
883+
);
884+
} else if (element.kind === 'Identifier') {
885+
state.referenceAndRecordEffects(
886+
freezeActions,
887+
element,
888+
Effect.Capture,
889+
ValueReason.Other,
890+
);
891+
} else {
892+
let _: 'Hole' = element.kind;
893+
}
894+
}
895+
state.initialize(instrValue, valueKind);
896+
state.define(instr.lvalue, instrValue);
897+
instr.lvalue.effect = Effect.Store;
875898
continuation = {
876-
kind: 'initialize',
877-
valueKind,
878-
effect: {kind: Effect.Capture, reason: ValueReason.Other},
879-
lvalueEffect: Effect.Store,
899+
kind: 'funeffects',
880900
};
881901
break;
882902
}
@@ -1241,21 +1261,12 @@ function inferBlock(
12411261
for (let i = 0; i < instrValue.args.length; i++) {
12421262
const arg = instrValue.args[i];
12431263
const place = arg.kind === 'Identifier' ? arg : arg.place;
1244-
if (effects !== null) {
1245-
state.referenceAndRecordEffects(
1246-
freezeActions,
1247-
place,
1248-
effects[i],
1249-
ValueReason.Other,
1250-
);
1251-
} else {
1252-
state.referenceAndRecordEffects(
1253-
freezeActions,
1254-
place,
1255-
Effect.ConditionallyMutate,
1256-
ValueReason.Other,
1257-
);
1258-
}
1264+
state.referenceAndRecordEffects(
1265+
freezeActions,
1266+
place,
1267+
getArgumentEffect(effects != null ? effects[i] : null, arg),
1268+
ValueReason.Other,
1269+
);
12591270
hasCaptureArgument ||= place.effect === Effect.Capture;
12601271
}
12611272
if (signature !== null) {
@@ -1307,7 +1318,10 @@ function inferBlock(
13071318
signature !== null
13081319
? {
13091320
kind: signature.returnValueKind,
1310-
reason: new Set([ValueReason.Other]),
1321+
reason: new Set([
1322+
signature.returnValueReason ??
1323+
ValueReason.KnownReturnSignature,
1324+
]),
13111325
context: new Set(),
13121326
}
13131327
: {
@@ -1330,7 +1344,8 @@ function inferBlock(
13301344
state.referenceAndRecordEffects(
13311345
freezeActions,
13321346
place,
1333-
Effect.Read,
1347+
// see call-spread-argument-mutable-iterator test fixture
1348+
arg.kind === 'Spread' ? Effect.ConditionallyMutate : Effect.Read,
13341349
ValueReason.Other,
13351350
);
13361351
}
@@ -1356,25 +1371,16 @@ function inferBlock(
13561371
for (let i = 0; i < instrValue.args.length; i++) {
13571372
const arg = instrValue.args[i];
13581373
const place = arg.kind === 'Identifier' ? arg : arg.place;
1359-
if (effects !== null) {
1360-
/*
1361-
* If effects are inferred for an argument, we should fail invalid
1362-
* mutating effects
1363-
*/
1364-
state.referenceAndRecordEffects(
1365-
freezeActions,
1366-
place,
1367-
effects[i],
1368-
ValueReason.Other,
1369-
);
1370-
} else {
1371-
state.referenceAndRecordEffects(
1372-
freezeActions,
1373-
place,
1374-
Effect.ConditionallyMutate,
1375-
ValueReason.Other,
1376-
);
1377-
}
1374+
/*
1375+
* If effects are inferred for an argument, we should fail invalid
1376+
* mutating effects
1377+
*/
1378+
state.referenceAndRecordEffects(
1379+
freezeActions,
1380+
place,
1381+
getArgumentEffect(effects != null ? effects[i] : null, arg),
1382+
ValueReason.Other,
1383+
);
13781384
hasCaptureArgument ||= place.effect === Effect.Capture;
13791385
}
13801386
if (signature !== null) {
@@ -2049,3 +2055,31 @@ function areArgumentsImmutableAndNonMutating(
20492055
}
20502056
return true;
20512057
}
2058+
2059+
function getArgumentEffect(
2060+
signatureEffect: Effect | null,
2061+
arg: Place | SpreadPattern,
2062+
): Effect {
2063+
if (signatureEffect != null) {
2064+
if (arg.kind === 'Identifier') {
2065+
return signatureEffect;
2066+
} else if (
2067+
signatureEffect === Effect.Mutate ||
2068+
signatureEffect === Effect.ConditionallyMutate
2069+
) {
2070+
return signatureEffect;
2071+
} else {
2072+
// see call-spread-argument-mutable-iterator test fixture
2073+
if (signatureEffect === Effect.Freeze) {
2074+
CompilerError.throwTodo({
2075+
reason: 'Support spread syntax for hook arguments',
2076+
loc: arg.place.loc,
2077+
});
2078+
}
2079+
// effects[i] is Effect.Capture | Effect.Read | Effect.Store
2080+
return Effect.ConditionallyMutate;
2081+
}
2082+
} else {
2083+
return Effect.ConditionallyMutate;
2084+
}
2085+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
2+
## Input
3+
4+
```javascript
5+
function useBar({arg}) {
6+
/**
7+
* Note that mutableIterator is mutated by the later object spread. Therefore,
8+
* `s.values()` should be memoized within the same block as the object spread.
9+
* In terms of compiler internals, they should have the same reactive scope.
10+
*/
11+
const obj = {};
12+
const s = new Set([obj, 5, 4]);
13+
const mutableIterator = s.values();
14+
const arr = [...mutableIterator];
15+
16+
obj.x = arg;
17+
return arr;
18+
}
19+
20+
export const FIXTURE_ENTRYPOINT = {
21+
fn: useBar,
22+
params: [{arg: 3}],
23+
sequentialRenders: [{arg: 3}, {arg: 3}, {arg: 4}],
24+
};
25+
26+
```
27+
28+
## Code
29+
30+
```javascript
31+
import { c as _c } from "react/compiler-runtime";
32+
function useBar(t0) {
33+
const $ = _c(2);
34+
const { arg } = t0;
35+
let arr;
36+
if ($[0] !== arg) {
37+
const obj = {};
38+
const s = new Set([obj, 5, 4]);
39+
const mutableIterator = s.values();
40+
arr = [...mutableIterator];
41+
42+
obj.x = arg;
43+
$[0] = arg;
44+
$[1] = arr;
45+
} else {
46+
arr = $[1];
47+
}
48+
return arr;
49+
}
50+
51+
export const FIXTURE_ENTRYPOINT = {
52+
fn: useBar,
53+
params: [{ arg: 3 }],
54+
sequentialRenders: [{ arg: 3 }, { arg: 3 }, { arg: 4 }],
55+
};
56+
57+
```
58+
59+
### Eval output
60+
(kind: ok) [{"x":3},5,4]
61+
[{"x":3},5,4]
62+
[{"x":4},5,4]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
function useBar({arg}) {
2+
/**
3+
* Note that mutableIterator is mutated by the later object spread. Therefore,
4+
* `s.values()` should be memoized within the same block as the object spread.
5+
* In terms of compiler internals, they should have the same reactive scope.
6+
*/
7+
const obj = {};
8+
const s = new Set([obj, 5, 4]);
9+
const mutableIterator = s.values();
10+
const arr = [...mutableIterator];
11+
12+
obj.x = arg;
13+
return arr;
14+
}
15+
16+
export const FIXTURE_ENTRYPOINT = {
17+
fn: useBar,
18+
params: [{arg: 3}],
19+
sequentialRenders: [{arg: 3}, {arg: 3}, {arg: 4}],
20+
};
+14-16
Original file line numberDiff line numberDiff line change
@@ -55,26 +55,20 @@ import { c as _c } from "react/compiler-runtime"; /**
5555

5656
function useBar(t0) {
5757
"use memo";
58-
const $ = _c(3);
58+
const $ = _c(2);
5959
const { arg } = t0;
6060
let t1;
61-
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
61+
if ($[0] !== arg) {
6262
const s = new Set([1, 5, 4]);
63-
t1 = s.values();
64-
$[0] = t1;
65-
} else {
66-
t1 = $[0];
67-
}
68-
const mutableIterator = t1;
69-
let t2;
70-
if ($[1] !== arg) {
71-
t2 = [arg, ...mutableIterator];
72-
$[1] = arg;
73-
$[2] = t2;
63+
const mutableIterator = s.values();
64+
65+
t1 = [arg, ...mutableIterator];
66+
$[0] = arg;
67+
$[1] = t1;
7468
} else {
75-
t2 = $[2];
69+
t1 = $[1];
7670
}
77-
return t2;
71+
return t1;
7872
}
7973

8074
export const FIXTURE_ENTRYPOINT = {
@@ -84,4 +78,8 @@ export const FIXTURE_ENTRYPOINT = {
8478
};
8579

8680
```
87-
81+
82+
### Eval output
83+
(kind: ok) [3,1,5,4]
84+
[3,1,5,4]
85+
[4,1,5,4]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
2+
## Input
3+
4+
```javascript
5+
import {useIdentity} from 'shared-runtime';
6+
7+
function useFoo() {
8+
const it = new Set([1, 2]).values();
9+
useIdentity();
10+
return Math.max(...it);
11+
}
12+
13+
export const FIXTURE_ENTRYPOINT = {
14+
fn: useFoo,
15+
params: [{}],
16+
sequentialRenders: [{}, {}],
17+
};
18+
19+
```
20+
21+
## Code
22+
23+
```javascript
24+
import { useIdentity } from "shared-runtime";
25+
26+
function useFoo() {
27+
const it = new Set([1, 2]).values();
28+
useIdentity();
29+
return Math.max(...it);
30+
}
31+
32+
export const FIXTURE_ENTRYPOINT = {
33+
fn: useFoo,
34+
params: [{}],
35+
sequentialRenders: [{}, {}],
36+
};
37+
38+
```
39+
40+
### Eval output
41+
(kind: ok) 2
42+
2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import {useIdentity} from 'shared-runtime';
2+
3+
function useFoo() {
4+
const it = new Set([1, 2]).values();
5+
useIdentity();
6+
return Math.max(...it);
7+
}
8+
9+
export const FIXTURE_ENTRYPOINT = {
10+
fn: useFoo,
11+
params: [{}],
12+
sequentialRenders: [{}, {}],
13+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
2+
## Input
3+
4+
```javascript
5+
import {useIdentity} from 'shared-runtime';
6+
7+
function Component() {
8+
const items = makeArray(0, 1, 2, null, 4, false, 6);
9+
return useIdentity(...items.values());
10+
}
11+
12+
export const FIXTURE_ENTRYPOINT = {
13+
fn: Component,
14+
params: [],
15+
sequentialRenders: [{}, {}],
16+
};
17+
18+
```
19+
20+
21+
## Error
22+
23+
```
24+
3 | function Component() {
25+
4 | const items = makeArray(0, 1, 2, null, 4, false, 6);
26+
> 5 | return useIdentity(...items.values());
27+
| ^^^^^^^^^^^^^^ Todo: Support spread syntax for hook arguments (5:5)
28+
6 | }
29+
7 |
30+
8 | export const FIXTURE_ENTRYPOINT = {
31+
```
32+
33+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import {useIdentity} from 'shared-runtime';
2+
3+
function Component() {
4+
const items = makeArray(0, 1, 2, null, 4, false, 6);
5+
return useIdentity(...items.values());
6+
}
7+
8+
export const FIXTURE_ENTRYPOINT = {
9+
fn: Component,
10+
params: [],
11+
sequentialRenders: [{}, {}],
12+
};

0 commit comments

Comments
 (0)