[compiler][optim] Add Effect.ConditionallyMutateIterator

Adds Effect.ConditionallyMutateIterator, which has the following effects:
- capture for known array, map, and sets
- mutate for all other values

An alternative to this approach could be to add polymorphic shape definitions
This commit is contained in:
Mofei Zhang
2025-03-20 20:16:57 -04:00
parent 8e6550a239
commit 76a1cbfbaa
18 changed files with 410 additions and 130 deletions
@@ -65,8 +65,6 @@ const UNTYPED_GLOBALS: Set<string> = new Set([
'Int8Array',
'Int16Array',
'Int32Array',
'Map',
'Set',
'WeakMap',
'Uint8Array',
'Uint8ClampedArray',
@@ -140,7 +138,7 @@ const TYPED_GLOBALS: Array<[string, BuiltInType]> = [
'from',
addFunction(DEFAULT_SHAPES, [], {
positionalParams: [
Effect.ConditionallyMutate,
Effect.ConditionallyMutateIterator,
Effect.ConditionallyMutate,
Effect.ConditionallyMutate,
],
@@ -466,7 +464,7 @@ const TYPED_GLOBALS: Array<[string, BuiltInType]> = [
DEFAULT_SHAPES,
[],
{
positionalParams: [Effect.ConditionallyMutate],
positionalParams: [Effect.ConditionallyMutateIterator],
restParam: null,
returnType: {kind: 'Object', shapeId: BuiltInMapId},
calleeEffect: Effect.Read,
@@ -482,7 +480,7 @@ const TYPED_GLOBALS: Array<[string, BuiltInType]> = [
DEFAULT_SHAPES,
[],
{
positionalParams: [Effect.ConditionallyMutate],
positionalParams: [Effect.ConditionallyMutateIterator],
restParam: null,
returnType: {kind: 'Object', shapeId: BuiltInSetId},
calleeEffect: Effect.Read,
@@ -1396,6 +1396,7 @@ export enum Effect {
Read = 'read',
// This reference reads and stores the value
Capture = 'capture',
ConditionallyMutateIterator = 'mutate-iterator?',
/*
* This reference *may* write to (mutate) the value. This covers two similar cases:
* - The compiler is being conservative and assuming that a value *may* be mutated
@@ -1414,11 +1415,11 @@ export enum Effect {
// This reference may alias to (mutate) the value
Store = 'store',
}
export const EffectSchema = z.enum([
Effect.Read,
Effect.Mutate,
Effect.ConditionallyMutate,
Effect.ConditionallyMutateIterator,
Effect.Capture,
Effect.Store,
Effect.Freeze,
@@ -1432,6 +1433,7 @@ export function isMutableEffect(
case Effect.Capture:
case Effect.Store:
case Effect.ConditionallyMutate:
case Effect.ConditionallyMutateIterator:
case Effect.Mutate: {
return true;
}
@@ -11,7 +11,10 @@ import {
Identifier,
InstructionId,
InstructionKind,
isArrayType,
isMapType,
isRefOrRefValue,
isSetType,
makeInstructionId,
Place,
} from '../HIR/HIR';
@@ -90,6 +93,17 @@ function inferPlace(
infer(place, instrId);
}
return;
case Effect.ConditionallyMutateIterator: {
const identifier = place.identifier;
if (
!isArrayType(identifier) &&
!isSetType(identifier) &&
!isMapType(identifier)
) {
infer(place, instrId);
}
return;
}
case Effect.ConditionallyMutate:
case Effect.Mutate: {
infer(place, instrId);
@@ -230,6 +230,7 @@ export function inferReactivePlaces(fn: HIRFunction): void {
case Effect.Capture:
case Effect.Store:
case Effect.ConditionallyMutate:
case Effect.ConditionallyMutateIterator:
case Effect.Mutate: {
if (isMutable(instruction, operand)) {
reactiveIdentifiers.markReactive(operand);
@@ -29,8 +29,10 @@ import {
ValueKind,
ValueReason,
isArrayType,
isMapType,
isMutableEffect,
isObjectType,
isSetType,
} from '../HIR/HIR';
import {FunctionSignature} from '../HIR/ObjectShape';
import {
@@ -470,6 +472,25 @@ class InferenceState {
}
break;
}
case Effect.ConditionallyMutateIterator: {
if (
valueKind.kind === ValueKind.Mutable ||
valueKind.kind === ValueKind.Context
) {
if (
isArrayType(place.identifier) ||
isSetType(place.identifier) ||
isMapType(place.identifier)
) {
effect = Effect.Capture;
} else {
effect = Effect.ConditionallyMutate;
}
} else {
effect = Effect.Read;
}
break;
}
case Effect.Mutate: {
effect = Effect.Mutate;
break;
@@ -881,9 +902,7 @@ function inferBlock(
state.referenceAndRecordEffects(
freezeActions,
element.place,
isArrayType(element.place.identifier)
? Effect.Capture
: Effect.ConditionallyMutate,
Effect.ConditionallyMutateIterator,
ValueReason.Other,
);
} else if (element.kind === 'Identifier') {
@@ -1644,7 +1663,13 @@ function inferBlock(
kind === ValueKind.Mutable || kind === ValueKind.Context;
let effect;
let valueKind: AbstractValue;
if (!isMutable || isArrayType(instrValue.collection.identifier)) {
const iterator = instrValue.collection.identifier;
if (
!isMutable ||
isArrayType(iterator) ||
isMapType(iterator) ||
isSetType(iterator)
) {
// Case 1, assume iterator is a separate mutable object
effect = {
kind: Effect.Read,
@@ -1685,7 +1710,7 @@ function inferBlock(
state.referenceAndRecordEffects(
freezeActions,
instrValue.iterator,
Effect.ConditionallyMutate,
Effect.ConditionallyMutateIterator,
ValueReason.Other,
);
/**
@@ -1847,6 +1872,7 @@ export function isKnownMutableEffect(effect: Effect): boolean {
switch (effect) {
case Effect.Store:
case Effect.ConditionallyMutate:
case Effect.ConditionallyMutateIterator:
case Effect.Mutate: {
return true;
}
@@ -1950,7 +1976,7 @@ function getArgumentEffect(
});
}
// effects[i] is Effect.Capture | Effect.Read | Effect.Store
return Effect.ConditionallyMutate;
return Effect.ConditionallyMutateIterator;
}
} else {
return Effect.ConditionallyMutate;
@@ -50,28 +50,55 @@ import { useIdentity, Stringify } from "shared-runtime";
* (2) the 1st argument might mutate its callee
*/
function Component(t0) {
const $ = _c(4);
const $ = _c(10);
const { value } = t0;
const arr = [{ value: "foo" }, { value: "bar" }, { value }];
useIdentity();
const derived = Array.from(arr, _temp);
let t1;
if ($[0] !== derived) {
t1 = derived.at(-1);
$[0] = derived;
$[1] = t1;
} else {
t1 = $[1];
}
let t2;
if ($[2] !== t1) {
t2 = <Stringify>{t1}</Stringify>;
$[2] = t1;
$[3] = t2;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = { value: "foo" };
t2 = { value: "bar" };
$[0] = t1;
$[1] = t2;
} else {
t2 = $[3];
t1 = $[0];
t2 = $[1];
}
return t2;
let t3;
if ($[2] !== value) {
t3 = [t1, t2, { value }];
$[2] = value;
$[3] = t3;
} else {
t3 = $[3];
}
const arr = t3;
useIdentity();
let t4;
if ($[4] !== arr) {
t4 = Array.from(arr, _temp);
$[4] = arr;
$[5] = t4;
} else {
t4 = $[5];
}
const derived = t4;
let t5;
if ($[6] !== derived) {
t5 = derived.at(-1);
$[6] = derived;
$[7] = t5;
} else {
t5 = $[7];
}
let t6;
if ($[8] !== t5) {
t6 = <Stringify>{t5}</Stringify>;
$[8] = t5;
$[9] = t6;
} else {
t6 = $[9];
}
return t6;
}
function _temp(x, idx) {
return { ...x, id: idx };
@@ -50,28 +50,55 @@ import { useIdentity, Stringify } from "shared-runtime";
* (2) the 1st argument might mutate its callee
*/
function Component(t0) {
const $ = _c(4);
const $ = _c(10);
const { value } = t0;
const arr = [{ value: "foo" }, { value: "bar" }, { value }];
useIdentity();
const derived = Array.from(arr);
let t1;
if ($[0] !== derived) {
t1 = derived.at(-1);
$[0] = derived;
$[1] = t1;
} else {
t1 = $[1];
}
let t2;
if ($[2] !== t1) {
t2 = <Stringify>{t1}</Stringify>;
$[2] = t1;
$[3] = t2;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = { value: "foo" };
t2 = { value: "bar" };
$[0] = t1;
$[1] = t2;
} else {
t2 = $[3];
t1 = $[0];
t2 = $[1];
}
return t2;
let t3;
if ($[2] !== value) {
t3 = [t1, t2, { value }];
$[2] = value;
$[3] = t3;
} else {
t3 = $[3];
}
const arr = t3;
useIdentity();
let t4;
if ($[4] !== arr) {
t4 = Array.from(arr);
$[4] = arr;
$[5] = t4;
} else {
t4 = $[5];
}
const derived = t4;
let t5;
if ($[6] !== derived) {
t5 = derived.at(-1);
$[6] = derived;
$[7] = t5;
} else {
t5 = $[7];
}
let t6;
if ($[8] !== t5) {
t6 = <Stringify>{t5}</Stringify>;
$[8] = t5;
$[9] = t6;
} else {
t6 = $[9];
}
return t6;
}
export const FIXTURE_ENTRYPOINT = {
@@ -7,7 +7,7 @@ import {mutateAndReturn, Stringify, useIdentity} from 'shared-runtime';
function Component({value}) {
const arr = [{value: 'foo'}, {value: 'bar'}, {value}];
useIdentity();
const derived = Array.from(arr, mutateAndReturn);
const derived = Array.from(arr).map(mutateAndReturn);
return (
<Stringify>
{derived.at(0)}
@@ -19,7 +19,7 @@ function Component({value}) {
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 5}],
sequentialRenders: [{value: 5}, {value: 6}, {value: 6}],
sequentialRenders: [{value: 5}, {value: 6}, {value: 6}, {value: 7}],
};
```
@@ -35,7 +35,7 @@ function Component(t0) {
const { value } = t0;
const arr = [{ value: "foo" }, { value: "bar" }, { value }];
useIdentity();
const derived = Array.from(arr, mutateAndReturn);
const derived = Array.from(arr).map(mutateAndReturn);
let t1;
if ($[0] !== derived) {
t1 = derived.at(0);
@@ -72,7 +72,7 @@ function Component(t0) {
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ value: 5 }],
sequentialRenders: [{ value: 5 }, { value: 6 }, { value: 6 }],
sequentialRenders: [{ value: 5 }, { value: 6 }, { value: 6 }, { value: 7 }],
};
```
@@ -80,4 +80,5 @@ export const FIXTURE_ENTRYPOINT = {
### Eval output
(kind: ok) <div>{"children":[{"value":"foo","wat0":"joe"},{"value":5,"wat0":"joe"}]}</div>
<div>{"children":[{"value":"foo","wat0":"joe"},{"value":6,"wat0":"joe"}]}</div>
<div>{"children":[{"value":"foo","wat0":"joe"},{"value":6,"wat0":"joe"}]}</div>
<div>{"children":[{"value":"foo","wat0":"joe"},{"value":6,"wat0":"joe"}]}</div>
<div>{"children":[{"value":"foo","wat0":"joe"},{"value":7,"wat0":"joe"}]}</div>
@@ -3,7 +3,7 @@ import {mutateAndReturn, Stringify, useIdentity} from 'shared-runtime';
function Component({value}) {
const arr = [{value: 'foo'}, {value: 'bar'}, {value}];
useIdentity();
const derived = Array.from(arr, mutateAndReturn);
const derived = Array.from(arr).map(mutateAndReturn);
return (
<Stringify>
{derived.at(0)}
@@ -15,5 +15,5 @@ function Component({value}) {
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 5}],
sequentialRenders: [{value: 5}, {value: 6}, {value: 6}],
sequentialRenders: [{value: 5}, {value: 6}, {value: 6}, {value: 7}],
};
@@ -0,0 +1,66 @@
## Input
```javascript
import {useIdentity} from 'shared-runtime';
/**
* Forked version of call-spread-argument-mutable-iterator that is known to not mutate
* the spread argument since it is a Set
*/
function useFoo() {
const s = new Set([1, 2]);
useIdentity(null);
return [Math.max(...s), s];
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{}],
sequentialRenders: [{}, {}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { useIdentity } from "shared-runtime";
/**
* Forked version of call-spread-argument-mutable-iterator that is known to not mutate
* the spread argument since it is a Set
*/
function useFoo() {
const $ = _c(2);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = new Set([1, 2]);
$[0] = t0;
} else {
t0 = $[0];
}
const s = t0;
useIdentity(null);
let t1;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
t1 = [Math.max(...s), s];
$[1] = t1;
} else {
t1 = $[1];
}
return t1;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{}],
sequentialRenders: [{}, {}],
};
```
### Eval output
(kind: ok) [2,{"kind":"Set","value":[1,2]}]
[2,{"kind":"Set","value":[1,2]}]
@@ -0,0 +1,17 @@
import {useIdentity} from 'shared-runtime';
/**
* Forked version of call-spread-argument-mutable-iterator that is known to not mutate
* the spread argument since it is a Set
*/
function useFoo() {
const s = new Set([1, 2]);
useIdentity(null);
return [Math.max(...s), s];
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{}],
sequentialRenders: [{}, {}],
};
@@ -4,7 +4,7 @@
```javascript
const MODULE_LOCAL = new Set([4, 5, 6]);
function useFoo({propArr}: {propArr: Array<number>}) {
/* TODO: Array can be memoized separately of the Set */
/* Array can be memoized separately of the Set */
const s1 = new Set([1, 2, 3]);
s1.add(propArr[0]);
@@ -16,7 +16,7 @@ function useFoo({propArr}: {propArr: Array<number>}) {
s3.add(propArr[2]);
/**
* TODO: s3 should be memoized separately of s4
* s4 should be memoized separately from s3
*/
const s4 = new Set(s3);
s4.add(propArr[3]);
@@ -37,52 +37,62 @@ export const FIXTURE_ENTRYPOINT = {
import { c as _c } from "react/compiler-runtime";
const MODULE_LOCAL = new Set([4, 5, 6]);
function useFoo(t0) {
const $ = _c(13);
const $ = _c(15);
const { propArr } = t0;
let s1;
if ($[0] !== propArr[0]) {
s1 = new Set([1, 2, 3]);
s1.add(propArr[0]);
$[0] = propArr[0];
$[1] = s1;
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = [1, 2, 3];
$[0] = t1;
} else {
s1 = $[1];
t1 = $[0];
}
let s1;
if ($[1] !== propArr[0]) {
s1 = new Set(t1);
s1.add(propArr[0]);
$[1] = propArr[0];
$[2] = s1;
} else {
s1 = $[2];
}
let s2;
let s3;
let s4;
if ($[2] !== propArr[1] || $[3] !== propArr[2] || $[4] !== propArr[3]) {
if ($[3] !== propArr[1] || $[4] !== propArr[2]) {
s2 = new Set(MODULE_LOCAL.values());
s2.add(propArr[1]);
s3 = new Set(s2.values());
s3.add(propArr[2]);
s4 = new Set(s3);
s4.add(propArr[3]);
$[2] = propArr[1];
$[3] = propArr[2];
$[4] = propArr[3];
$[3] = propArr[1];
$[4] = propArr[2];
$[5] = s2;
$[6] = s3;
$[7] = s4;
} else {
s2 = $[5];
s3 = $[6];
s4 = $[7];
}
let t1;
if ($[8] !== s1 || $[9] !== s2 || $[10] !== s3 || $[11] !== s4) {
t1 = [s1, s2, s3, s4];
$[8] = s1;
$[9] = s2;
$[10] = s3;
$[11] = s4;
$[12] = t1;
let s4;
if ($[7] !== propArr[3] || $[8] !== s3) {
s4 = new Set(s3);
s4.add(propArr[3]);
$[7] = propArr[3];
$[8] = s3;
$[9] = s4;
} else {
t1 = $[12];
s4 = $[9];
}
return t1;
let t2;
if ($[10] !== s1 || $[11] !== s2 || $[12] !== s3 || $[13] !== s4) {
t2 = [s1, s2, s3, s4];
$[10] = s1;
$[11] = s2;
$[12] = s3;
$[13] = s4;
$[14] = t2;
} else {
t2 = $[14];
}
return t2;
}
export const FIXTURE_ENTRYPOINT = {
@@ -1,6 +1,6 @@
const MODULE_LOCAL = new Set([4, 5, 6]);
function useFoo({propArr}: {propArr: Array<number>}) {
/* TODO: Array can be memoized separately of the Set */
/* Array can be memoized separately of the Set */
const s1 = new Set([1, 2, 3]);
s1.add(propArr[0]);
@@ -12,7 +12,7 @@ function useFoo({propArr}: {propArr: Array<number>}) {
s3.add(propArr[2]);
/**
* TODO: s3 should be memoized separately of s4
* s4 should be memoized separately from s3
*/
const s4 = new Set(s3);
s4.add(propArr[3]);
@@ -0,0 +1,61 @@
## Input
```javascript
import {mutateAndReturn, Stringify, useIdentity} from 'shared-runtime';
function Component({value}) {
const arr = [{value: 'foo'}, {value: 'bar'}, {value}];
useIdentity();
const derived = new Set(arr).forEach(mutateAndReturn);
return (
<Stringify>
{[...derived]}
</Stringify>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 5}],
sequentialRenders: [{value: 5}, {value: 6}, {value: 6}, {value: 7}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { mutateAndReturn, Stringify, useIdentity } from "shared-runtime";
function Component(t0) {
const $ = _c(2);
const { value } = t0;
const arr = [{ value: "foo" }, { value: "bar" }, { value }];
useIdentity();
const derived = new Set(arr).forEach(mutateAndReturn);
let t1;
if ($[0] !== derived) {
t1 = <Stringify>{[...derived]}</Stringify>;
$[0] = derived;
$[1] = t1;
} else {
t1 = $[1];
}
return t1;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ value: 5 }],
sequentialRenders: [{ value: 5 }, { value: 6 }, { value: 6 }, { value: 7 }],
};
```
### Eval output
(kind: ok) [[ (exception in render) TypeError: derived is not iterable ]]
[[ (exception in render) TypeError: derived is not iterable ]]
[[ (exception in render) TypeError: derived is not iterable ]]
[[ (exception in render) TypeError: derived is not iterable ]]
@@ -0,0 +1,18 @@
import {mutateAndReturn, Stringify, useIdentity} from 'shared-runtime';
function Component({value}) {
const arr = [{value: 'foo'}, {value: 'bar'}, {value}];
useIdentity();
const derived = new Set(arr).forEach(mutateAndReturn);
return (
<Stringify>
{[...derived]}
</Stringify>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 5}],
sequentialRenders: [{value: 5}, {value: 6}, {value: 6}, {value: 7}],
};
@@ -5,7 +5,7 @@
import {useIdentity, ValidateMemoization} from 'shared-runtime';
/**
* TODO fixture for granular iterator semantics:
* 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.
@@ -26,7 +26,7 @@ function Validate({x, input}) {
function useFoo(input) {
'use memo';
/**
* TODO: We should be able to memoize {} separately from `x`.
* We should be able to memoize {} separately from `x`.
*/
const x = Array.from([{}]);
useIdentity();
@@ -48,7 +48,7 @@ import { c as _c } from "react/compiler-runtime";
import { useIdentity, ValidateMemoization } from "shared-runtime";
/**
* TODO fixture for granular iterator semantics:
* 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.
@@ -68,29 +68,35 @@ function Validate({ x, input }) {
}
function useFoo(input) {
"use memo";
const $ = _c(5);
const x = Array.from([{}]);
useIdentity();
const $ = _c(6);
let t0;
if ($[0] !== input) {
t0 = [input];
$[0] = input;
$[1] = t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = [{}];
$[0] = t0;
} else {
t0 = $[1];
t0 = $[0];
}
x.push(t0);
const x = Array.from(t0);
useIdentity();
let t1;
if ($[2] !== input || $[3] !== x) {
t1 = <Validate x={x} input={input} />;
$[2] = input;
$[3] = x;
$[4] = t1;
if ($[1] !== input) {
t1 = [input];
$[1] = input;
$[2] = t1;
} else {
t1 = $[4];
t1 = $[2];
}
return t1;
x.push(t1);
let t2;
if ($[3] !== input || $[4] !== x) {
t2 = <Validate x={x} input={input} />;
$[3] = input;
$[4] = x;
$[5] = t2;
} else {
t2 = $[5];
}
return t2;
}
export const FIXTURE_ENTRYPOINT = {
@@ -1,7 +1,7 @@
import {useIdentity, ValidateMemoization} from 'shared-runtime';
/**
* TODO fixture for granular iterator semantics:
* 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.
@@ -22,7 +22,7 @@ function Validate({x, input}) {
function useFoo(input) {
'use memo';
/**
* TODO: We should be able to memoize {} separately from `x`.
* We should be able to memoize {} separately from `x`.
*/
const x = Array.from([{}]);
useIdentity();
@@ -77,40 +77,46 @@ function Validate({ x, val1, val2 }) {
}
function useFoo(t0) {
"use memo";
const $ = _c(8);
const $ = _c(9);
const { val1, val2 } = t0;
const x = Array.from([]);
useIdentity();
let t1;
if ($[0] !== val1) {
t1 = [val1];
$[0] = val1;
$[1] = t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = [];
$[0] = t1;
} else {
t1 = $[1];
t1 = $[0];
}
x.push(t1);
const x = Array.from(t1);
useIdentity();
let t2;
if ($[2] !== val2) {
t2 = [val2];
$[2] = val2;
$[3] = t2;
if ($[1] !== val1) {
t2 = [val1];
$[1] = val1;
$[2] = t2;
} else {
t2 = $[3];
t2 = $[2];
}
x.push(t2);
let t3;
if ($[4] !== val1 || $[5] !== val2 || $[6] !== x) {
t3 = <Validate x={x} val1={val1} val2={val2} />;
$[4] = val1;
$[5] = val2;
$[6] = x;
$[7] = t3;
if ($[3] !== val2) {
t3 = [val2];
$[3] = val2;
$[4] = t3;
} else {
t3 = $[7];
t3 = $[4];
}
return t3;
x.push(t3);
let t4;
if ($[5] !== val1 || $[6] !== val2 || $[7] !== x) {
t4 = <Validate x={x} val1={val1} val2={val2} />;
$[5] = val1;
$[6] = val2;
$[7] = x;
$[8] = t4;
} else {
t4 = $[8];
}
return t4;
}
export const FIXTURE_ENTRYPOINT = {