Allow ref argument to be mutated

Previously, Forget would throw if _any_ of the arguments to a component are 
modified. This isn't quite right as a ref argument can be modified. 

This PR assumes the second argument of a component to be a ref and allows it to 
be mutable. 

A future PR will add types to this argument so the validateRefAccessDuringRender 
can catch if ref is mutated in render. This PR contains a todo test for this.
This commit is contained in:
Sathya Gunasekaran
2024-02-29 14:47:49 -08:00
parent 9d25ced018
commit 19e44a4a69
13 changed files with 400 additions and 28 deletions
@@ -21,6 +21,7 @@ import {
MethodCall,
Phi,
Place,
SpreadPattern,
Type,
ValueKind,
ValueReason,
@@ -133,26 +134,48 @@ export default function inferReferenceEffects(
kind: ValueKind.Frozen,
reason: new Set([ValueReason.ReactiveFunctionArgument]),
};
for (const param of fn.params) {
const isComponent = isComponentName(fn.id);
if (isComponent) {
CompilerError.invariant(fn.params.length <= 2, {
reason:
"Expected React component to have not more than two parameters: one for props and for ref",
description: null,
loc: fn.loc,
suggestions: null,
});
const [props, ref] = fn.params;
let value: InstructionValue;
let place: Place;
if (param.kind === "Identifier") {
place = param;
value = {
kind: "Primitive",
loc: param.loc,
value: undefined,
};
} else {
place = param.place;
value = {
kind: "Primitive",
loc: param.place.loc,
value: undefined,
};
if (props) {
inferParam(props, initialState, paramKind);
}
if (ref) {
if (ref.kind === "Identifier") {
place = ref;
value = {
kind: "ObjectExpression",
properties: [],
loc: ref.loc,
};
} else {
place = ref.place;
value = {
kind: "ObjectExpression",
properties: [],
loc: ref.place.loc,
};
}
initialState.initialize(value, {
kind: ValueKind.Mutable,
reason: new Set([ValueReason.Other]),
});
initialState.define(place, value);
}
} else {
for (const param of fn.params) {
inferParam(param, initialState, paramKind);
}
initialState.initialize(value, paramKind);
initialState.define(place, value);
}
// Map of blocks to the last (merged) incoming state that was processed
@@ -596,6 +619,36 @@ class InferenceState {
}
}
function isComponentName(name: string | null): boolean {
return name !== null && /^[A-Z]/.test(name);
}
function inferParam(
param: Place | SpreadPattern,
initialState: InferenceState,
paramKind: AbstractValue
): void {
let value: InstructionValue;
let place: Place;
if (param.kind === "Identifier") {
place = param;
value = {
kind: "Primitive",
loc: param.loc,
value: undefined,
};
} else {
place = param.place;
value = {
kind: "Primitive",
loc: param.place.loc,
value: undefined,
};
}
initialState.initialize(value, paramKind);
initialState.define(place, value);
}
/*
* Joins two values using the following rules:
* == Effect Transitions ==
@@ -0,0 +1,67 @@
## Input
```javascript
import { useEffect } from "react";
function Foo(props, ref) {
useEffect(() => {
ref.current = 2;
}, []);
return <div>{props.bar}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{ bar: "foo" }, { ref: { cuurrent: 1 } }],
isComponent: true,
};
```
## Code
```javascript
import { useEffect, unstable_useMemoCache as useMemoCache } from "react";
function Foo(props, ref) {
const $ = useMemoCache(5);
let t0;
if ($[0] !== ref.current) {
t0 = () => {
ref.current = 2;
};
$[0] = ref.current;
$[1] = t0;
} else {
t0 = $[1];
}
let t1;
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
t1 = [];
$[2] = t1;
} else {
t1 = $[2];
}
useEffect(t0, t1);
let t2;
if ($[3] !== props.bar) {
t2 = <div>{props.bar}</div>;
$[3] = props.bar;
$[4] = t2;
} else {
t2 = $[4];
}
return t2;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{ bar: "foo" }, { ref: { cuurrent: 1 } }],
isComponent: true,
};
```
### Eval output
(kind: ok) <div>foo</div>
@@ -0,0 +1,14 @@
import { useEffect } from "react";
function Foo(props, ref) {
useEffect(() => {
ref.current = 2;
}, []);
return <div>{props.bar}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{ bar: "foo" }, { ref: { cuurrent: 1 } }],
isComponent: true,
};
@@ -0,0 +1,46 @@
## Input
```javascript
// @validateRefAccessDuringRender: true
function Foo(props, ref) {
ref.current = 2;
return <div>{props.bar}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{ bar: "foo" }, { ref: { cuurrent: 1 } }],
isComponent: true,
};
```
## Code
```javascript
import { unstable_useMemoCache as useMemoCache } from "react"; // @validateRefAccessDuringRender: true
function Foo(props, ref) {
const $ = useMemoCache(2);
ref.current = 2;
let t0;
if ($[0] !== props.bar) {
t0 = <div>{props.bar}</div>;
$[0] = props.bar;
$[1] = t0;
} else {
t0 = $[1];
}
return t0;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{ bar: "foo" }, { ref: { cuurrent: 1 } }],
isComponent: true,
};
```
### Eval output
(kind: ok) <div>foo</div>
@@ -0,0 +1,11 @@
// @validateRefAccessDuringRender: true
function Foo(props, ref) {
ref.current = 2;
return <div>{props.bar}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{ bar: "foo" }, { ref: { cuurrent: 1 } }],
isComponent: true,
};
@@ -2,8 +2,7 @@
## Input
```javascript
// @debug
function Component(a, [b], { c }) {
function Component({ a: a, b: [b], c: { c } }) {
let d = a++;
let e = ++a;
let f = b--;
@@ -15,7 +14,7 @@ function Component(a, [b], { c }) {
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [2, [3], { c: 4 }],
params: [{ a: 2, b: [3], c: { c: 4 } }],
isComponent: false,
};
@@ -24,11 +23,12 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
import { unstable_useMemoCache as useMemoCache } from "react"; // @debug
function Component(a, t37, t38) {
import { unstable_useMemoCache as useMemoCache } from "react";
function Component(t38) {
const $ = useMemoCache(10);
let [b] = t37;
let { c } = t38;
let { a, b: t40, c: t41 } = t38;
let [b] = t40;
let { c } = t41;
const d = a++;
const e = ++a;
const f = b--;
@@ -66,7 +66,7 @@ function Component(a, t37, t38) {
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [2, [3], { c: 4 }],
params: [{ a: 2, b: [3], c: { c: 4 } }],
isComponent: false,
};
@@ -1,5 +1,4 @@
// @debug
function Component(a, [b], { c }) {
function Component({ a: a, b: [b], c: { c } }) {
let d = a++;
let e = ++a;
let f = b--;
@@ -11,6 +10,6 @@ function Component(a, [b], { c }) {
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [2, [3], { c: 4 }],
params: [{ a: 2, b: [3], c: { c: 4 } }],
isComponent: false,
};
@@ -0,0 +1,49 @@
## Input
```javascript
function Component(a) {
let d = a++;
let e = ++a;
return [a, d, e];
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [2],
isComponent: false,
};
```
## Code
```javascript
import { unstable_useMemoCache as useMemoCache } from "react";
function Component(a) {
const $ = useMemoCache(4);
const d = a++;
const e = ++a;
let t0;
if ($[0] !== a || $[1] !== d || $[2] !== e) {
t0 = [a, d, e];
$[0] = a;
$[1] = d;
$[2] = e;
$[3] = t0;
} else {
t0 = $[3];
}
return t0;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [2],
isComponent: false,
};
```
### Eval output
(kind: ok) [4,2,4]
@@ -0,0 +1,11 @@
function Component(a) {
let d = a++;
let e = ++a;
return [a, d, e];
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [2],
isComponent: false,
};
@@ -0,0 +1,50 @@
## Input
```javascript
function Component({ c }) {
let h = c++;
let i = --c;
return [c, h, i];
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ c: 4 }],
isComponent: false,
};
```
## Code
```javascript
import { unstable_useMemoCache as useMemoCache } from "react";
function Component(t14) {
const $ = useMemoCache(4);
let { c } = t14;
const h = c++;
const i = --c;
let t0;
if ($[0] !== c || $[1] !== h || $[2] !== i) {
t0 = [c, h, i];
$[0] = c;
$[1] = h;
$[2] = i;
$[3] = t0;
} else {
t0 = $[3];
}
return t0;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ c: 4 }],
isComponent: false,
};
```
### Eval output
(kind: ok) [4,4,4]
@@ -0,0 +1,11 @@
function Component({ c }) {
let h = c++;
let i = --c;
return [c, h, i];
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ c: 4 }],
isComponent: false,
};
@@ -0,0 +1,50 @@
## Input
```javascript
function Component([b]) {
let f = b--;
let g = --b;
return [b, f, g];
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [[3]],
isComponent: false,
};
```
## Code
```javascript
import { unstable_useMemoCache as useMemoCache } from "react";
function Component(t14) {
const $ = useMemoCache(4);
let [b] = t14;
const f = b--;
const g = --b;
let t0;
if ($[0] !== b || $[1] !== f || $[2] !== g) {
t0 = [b, f, g];
$[0] = b;
$[1] = f;
$[2] = g;
$[3] = t0;
} else {
t0 = $[3];
}
return t0;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [[3]],
isComponent: false,
};
```
### Eval output
(kind: ok) [1,3,1]
@@ -0,0 +1,11 @@
function Component([b]) {
let f = b--;
let g = --b;
return [b, f, g];
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [[3]],
isComponent: false,
};