Compare commits

...

1 Commits

Author SHA1 Message Date
Mofei Zhang
daab9e33b2 [compiler][rfc] enableTreatFunctionDepsAsConditional by default
See https://github.com/facebook/react/issues/31551 for context

We've rolled out internally with `enableTreatFunctionDepsAsConditional: false` and encountered no issues, but this is technically an unsound optimization as both flow and typescript have sources of unsoundness:
- typing array accesses / other unknown keys as `TValue | undefined`  (typescript's noUncheckedIndexedAccess)
- explicit and inferred `any`
- unsound typecasts

Note that removing this optimization results in less granular dependencies for ~3% of files on a large Meta project ([link](https://www.internalfb.com/phabricator/paste/view/P1681742214)).
2024-11-19 10:10:39 -05:00
37 changed files with 165 additions and 149 deletions

View File

@@ -457,9 +457,10 @@ const EnvironmentConfigSchema = z.object({
throwUnknownException__testonly: z.boolean().default(false),
/**
* Enables deps of a function epxression to be treated as conditional. This
* makes sure we don't load a dep when it's a property (to check if it has
* changed) and instead check the receiver.
* Enables deps of a function expression to be treated as conditional. This
* makes sure we don't hoist property loads from function expressions when we
* don't know that the property load source object is safe to access in the
* outer context
*
* This makes sure we don't end up throwing when the reciver is null. Consider
* this code:
@@ -475,8 +476,14 @@ const EnvironmentConfigSchema = z.object({
*
* This does cause the memoization to now be coarse grained, which is
* non-ideal.
*
* This is safe to toggle off for a codebase that has no sources of
* unsoundness. This includes:
* - typing array accesses / other unknown keys as TValue | undefined
* (typescript's noUncheckedIndexedAccess)
* - enabling and monitoring warnings for explicit and inferred `any`
*/
enableTreatFunctionDepsAsConditional: z.boolean().default(false),
enableTreatFunctionDepsAsConditional: z.boolean().default(true),
/**
* When true, always act as though the dependencies of a memoized value

View File

@@ -75,9 +75,9 @@ function useFoo(t0) {
arrayPush(x, y);
const y_alias = y;
let t2;
if ($[3] !== y_alias.value) {
if ($[3] !== y_alias) {
t2 = () => y_alias.value;
$[3] = y_alias.value;
$[3] = y_alias;
$[4] = t2;
} else {
t2 = $[4];

View File

@@ -56,9 +56,9 @@ function useFoo(t0) {
setPropertyByKey(obj, "arr", arr);
const obj_alias = obj;
let t2;
if ($[2] !== obj_alias.arr.length) {
if ($[2] !== obj_alias) {
t2 = () => obj_alias.arr.length;
$[2] = obj_alias.arr.length;
$[2] = obj_alias;
$[3] = t2;
} else {
t2 = $[3];

View File

@@ -22,11 +22,11 @@ import { c as _c } from "react/compiler-runtime";
function Foo(props) {
const $ = _c(2);
let t0;
if ($[0] !== props.router.location) {
if ($[0] !== props.router) {
t0 = (reason) => {
log(props.router.location);
};
$[0] = props.router.location;
$[0] = props.router;
$[1] = t0;
} else {
t0 = $[1];

View File

@@ -35,11 +35,11 @@ function component(t0) {
}
const poke = t1;
let t2;
if ($[2] !== mutator.user) {
if ($[2] !== mutator) {
t2 = () => {
mutator.user.hide();
};
$[2] = mutator.user;
$[2] = mutator;
$[3] = t2;
} else {
t2 = $[3];

View File

@@ -34,11 +34,11 @@ function component(a) {
}
const z = t0;
let t1;
if ($[2] !== z.a) {
if ($[2] !== z) {
t1 = function () {
console.log(z.a);
};
$[2] = z.a;
$[2] = z;
$[3] = t1;
} else {
t1 = $[3];

View File

@@ -36,13 +36,13 @@ function component(a) {
}
const z = t0;
let t1;
if ($[2] !== z.a.a) {
if ($[2] !== z) {
t1 = function () {
(function () {
console.log(z.a.a);
})();
};
$[2] = z.a.a;
$[2] = z;
$[3] = t1;
} else {
t1 = $[3];

View File

@@ -34,11 +34,11 @@ function component(a) {
}
const z = t0;
let t1;
if ($[2] !== z.a.a) {
if ($[2] !== z) {
t1 = function () {
console.log(z.a.a);
};
$[2] = z.a.a;
$[2] = z;
$[3] = t1;
} else {
t1 = $[3];

View File

@@ -97,7 +97,7 @@ function Component(props) {
}
const urls = t5;
let t6;
if ($[6] !== comments.length) {
if ($[6] !== comments) {
t6 = (e) => {
if (!comments.length) {
return;
@@ -105,7 +105,7 @@ function Component(props) {
console.log(comments.length);
};
$[6] = comments.length;
$[6] = comments;
$[7] = t6;
} else {
t6 = $[7];

View File

@@ -56,7 +56,7 @@ function Component(props) {
const { media, comments, urls } = post;
let t1;
if ($[2] !== comments.length) {
if ($[2] !== comments) {
t1 = (e) => {
if (!comments.length) {
return;
@@ -64,7 +64,7 @@ function Component(props) {
console.log(comments.length);
};
$[2] = comments.length;
$[2] = comments;
$[3] = t1;
} else {
t1 = $[3];

View File

@@ -23,11 +23,11 @@ import { c as _c } from "react/compiler-runtime";
function Component(props) {
const $ = _c(4);
let t0;
if ($[0] !== props.name) {
if ($[0] !== props) {
t0 = function () {
return <div>{props.name}</div>;
};
$[0] = props.name;
$[0] = props;
$[1] = t0;
} else {
t0 = $[1];

View File

@@ -1,32 +0,0 @@
## Input
```javascript
// @validatePreserveExistingMemoizationGuarantees
import {useCallback} from 'react';
function useHook(maybeRef) {
return useCallback(() => {
return [maybeRef.current];
}, [maybeRef]);
}
```
## Error
```
3 |
4 | function useHook(maybeRef) {
> 5 | return useCallback(() => {
| ^^^^^^^
> 6 | return [maybeRef.current];
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 7 | }, [maybeRef]);
| ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected (5:7)
8 | }
9 |
```

View File

@@ -1,32 +0,0 @@
## Input
```javascript
// @validatePreserveExistingMemoizationGuarantees
import {useMemo} from 'react';
function useHook(maybeRef, shouldRead) {
return useMemo(() => {
return () => [maybeRef.current];
}, [shouldRead, maybeRef]);
}
```
## Error
```
3 |
4 | function useHook(maybeRef, shouldRead) {
> 5 | return useMemo(() => {
| ^^^^^^^
> 6 | return () => [maybeRef.current];
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 7 | }, [shouldRead, maybeRef]);
| ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected (5:7)
8 | }
9 |
```

View File

@@ -0,0 +1,38 @@
## Input
```javascript
// @validatePreserveExistingMemoizationGuarantees
import {useCallback} from 'react';
function useHook(maybeRef) {
return useCallback(() => {
return [maybeRef.current];
}, [maybeRef]);
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees
import { useCallback } from "react";
function useHook(maybeRef) {
const $ = _c(2);
let t0;
if ($[0] !== maybeRef) {
t0 = () => [maybeRef.current];
$[0] = maybeRef;
$[1] = t0;
} else {
t0 = $[1];
}
return t0;
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -0,0 +1,40 @@
## Input
```javascript
// @validatePreserveExistingMemoizationGuarantees
import {useMemo} from 'react';
function useHook(maybeRef) {
return useMemo(() => {
return () => [maybeRef.current];
}, [maybeRef]);
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees
import { useMemo } from "react";
function useHook(maybeRef) {
const $ = _c(2);
let t0;
let t1;
if ($[0] !== maybeRef) {
t1 = () => [maybeRef.current];
$[0] = maybeRef;
$[1] = t1;
} else {
t1 = $[1];
}
t0 = t1;
return t0;
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -1,8 +1,8 @@
// @validatePreserveExistingMemoizationGuarantees
import {useMemo} from 'react';
function useHook(maybeRef, shouldRead) {
function useHook(maybeRef) {
return useMemo(() => {
return () => [maybeRef.current];
}, [shouldRead, maybeRef]);
}, [maybeRef]);
}

View File

@@ -9,13 +9,13 @@ import {sum} from 'shared-runtime';
function Component({propA, propB}) {
const x = propB.x.y;
return useCallback(() => {
return sum(propA.x, x);
}, [propA.x, x]);
return sum(propA, x);
}, [propA, x]);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{propA: {x: 2}, propB: {x: {y: 3}}}],
params: [{propA: 2, propB: {x: {y: 3}}}],
};
```
@@ -32,9 +32,9 @@ function Component(t0) {
const { propA, propB } = t0;
const x = propB.x.y;
let t1;
if ($[0] !== propA.x || $[1] !== x) {
t1 = () => sum(propA.x, x);
$[0] = propA.x;
if ($[0] !== propA || $[1] !== x) {
t1 = () => sum(propA, x);
$[0] = propA;
$[1] = x;
$[2] = t1;
} else {
@@ -45,7 +45,7 @@ function Component(t0) {
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ propA: { x: 2 }, propB: { x: { y: 3 } } }],
params: [{ propA: 2, propB: { x: { y: 3 } } }],
};
```

View File

@@ -5,11 +5,11 @@ import {sum} from 'shared-runtime';
function Component({propA, propB}) {
const x = propB.x.y;
return useCallback(() => {
return sum(propA.x, x);
}, [propA.x, x]);
return sum(propA, x);
}, [propA, x]);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{propA: {x: 2}, propB: {x: {y: 3}}}],
params: [{propA: 2, propB: {x: {y: 3}}}],
};

View File

@@ -37,9 +37,9 @@ import { useCallback } from "react";
function useHook(x) {
const $ = _c(2);
let t0;
if ($[0] !== x.y.z) {
if ($[0] !== x) {
t0 = () => [x.y.z];
$[0] = x.y.z;
$[0] = x;
$[1] = t0;
} else {
t0 = $[1];

View File

@@ -29,9 +29,9 @@ function useFoo(t0) {
const $ = _c(2);
const { a } = t0;
let t1;
if ($[0] !== a.b.c) {
if ($[0] !== a) {
t1 = <Stringify fn={() => a.b.c} shouldInvokeFns={true} />;
$[0] = a.b.c;
$[0] = a;
$[1] = t1;
} else {
t1 = $[1];

View File

@@ -51,12 +51,12 @@ function Foo(t0) {
const fn = t1;
useIdentity(null);
let x;
if ($[2] !== a.b.c || $[3] !== cond) {
if ($[2] !== a || $[3] !== cond) {
x = makeArray();
if (cond) {
x.push(identity(a.b.c));
}
$[2] = a.b.c;
$[2] = a;
$[3] = cond;
$[4] = x;
} else {

View File

@@ -41,9 +41,9 @@ function useFoo(t0) {
local = $[1];
}
let t1;
if ($[2] !== local.b.c) {
if ($[2] !== local) {
t1 = () => local.b.c;
$[2] = local.b.c;
$[2] = local;
$[3] = t1;
} else {
t1 = $[3];

View File

@@ -50,12 +50,12 @@ function Foo(t0) {
const fn = t1;
useIdentity(null);
let arr;
if ($[2] !== a.b?.c.e || $[3] !== cond) {
if ($[2] !== a || $[3] !== cond) {
arr = makeArray();
if (cond) {
arr.push(identity(a.b?.c.e));
}
$[2] = a.b?.c.e;
$[2] = a;
$[3] = cond;
$[4] = arr;
} else {

View File

@@ -41,9 +41,9 @@ function useFoo(t0) {
local = $[1];
}
let t1;
if ($[2] !== local.b.c) {
if ($[2] !== local) {
t1 = () => [() => local.b.c];
$[2] = local.b.c;
$[2] = local;
$[3] = t1;
} else {
t1 = $[3];

View File

@@ -34,9 +34,9 @@ function useFoo(t0) {
const $ = _c(4);
const { a } = t0;
let t1;
if ($[0] !== a.b.c) {
if ($[0] !== a) {
t1 = () => () => ({ value: a.b.c });
$[0] = a.b.c;
$[0] = a;
$[1] = t1;
} else {
t1 = $[1];

View File

@@ -34,13 +34,13 @@ function useFoo(t0) {
const $ = _c(4);
const { a } = t0;
let t1;
if ($[0] !== a.b.c) {
if ($[0] !== a) {
t1 = {
fn() {
return identity(a.b.c);
},
};
$[0] = a.b.c;
$[0] = a;
$[1] = t1;
} else {
t1 = $[1];

View File

@@ -34,9 +34,9 @@ function useFoo(t0) {
const $ = _c(2);
const { a } = t0;
let t1;
if ($[0] !== a.b?.c.d?.e) {
if ($[0] !== a) {
t1 = <Stringify fn={() => a.b?.c.d?.e} shouldInvokeFns={true} />;
$[0] = a.b?.c.d?.e;
$[0] = a;
$[1] = t1;
} else {
t1 = $[1];

View File

@@ -29,9 +29,9 @@ function Foo(t0) {
const $ = _c(5);
const { data } = t0;
let t1;
if ($[0] !== data.a.d) {
if ($[0] !== data.a) {
t1 = () => data.a.d;
$[0] = data.a.d;
$[0] = data.a;
$[1] = t1;
} else {
t1 = $[1];

View File

@@ -51,7 +51,7 @@ import { identity } from "shared-runtime";
function Component(props) {
const $ = _c(4);
let x;
if ($[0] !== props.cond) {
if ($[0] !== props) {
const f = () => {
if (props.cond) {
x = 1;
@@ -62,7 +62,7 @@ function Component(props) {
const f2 = identity(f);
f2();
$[0] = props.cond;
$[0] = props;
$[1] = x;
} else {
x = $[1];

View File

@@ -31,9 +31,9 @@ function useFoo(t0) {
const $ = _c(2);
const { a } = t0;
let t1;
if ($[0] !== a.b?.c.d?.e) {
if ($[0] !== a) {
t1 = <Stringify fn={() => a.b?.c.d?.e} shouldInvokeFns={true} />;
$[0] = a.b?.c.d?.e;
$[0] = a;
$[1] = t1;
} else {
t1 = $[1];

View File

@@ -42,7 +42,7 @@ const t0 = "module_t0";
const c_0 = "module_c_0";
function useFoo(props) {
const $0 = _c(4);
const c_00 = $0[0] !== props.value;
const c_00 = $0[0] !== props;
let t1;
if (c_00) {
t1 = () => {
@@ -57,7 +57,7 @@ function useFoo(props) {
};
return b;
};
$0[0] = props.value;
$0[0] = props;
$0[1] = t1;
} else {
t1 = $0[1];

View File

@@ -43,7 +43,7 @@ const t0 = "module_t0";
const c_0 = "module_c_0";
function useFoo(props) {
const $0 = _c(2);
const c_00 = $0[0] !== props.value;
const c_00 = $0[0] !== props;
let t1;
if (c_00) {
const a = {
@@ -61,7 +61,7 @@ function useFoo(props) {
};
t1 = a.foo().bar();
$0[0] = props.value;
$0[0] = props;
$0[1] = t1;
} else {
t1 = $0[1];

View File

@@ -41,11 +41,11 @@ function Component(props) {
const $ = _c(5);
const Foo = useContext(FooContext);
let t0;
if ($[0] !== Foo.current) {
if ($[0] !== Foo) {
t0 = () => {
mutate(Foo.current);
};
$[0] = Foo.current;
$[0] = Foo;
$[1] = t0;
} else {
t0 = $[1];

View File

@@ -42,7 +42,7 @@ function Component(props) {
const $ = _c(6);
const foo = useContext(FooContext);
let t0;
if ($[0] !== foo.current) {
if ($[0] !== foo) {
t0 = () => {
if (foo.current) {
return {};
@@ -50,7 +50,7 @@ function Component(props) {
return null;
}
};
$[0] = foo.current;
$[0] = foo;
$[1] = t0;
} else {
t0 = $[1];

View File

@@ -34,11 +34,11 @@ function Component(props) {
const $ = _c(5);
const foo = useContext(FooContext);
let t0;
if ($[0] !== foo.current) {
if ($[0] !== foo) {
t0 = () => {
console.log(foo.current);
};
$[0] = foo.current;
$[0] = foo;
$[1] = t0;
} else {
t0 = $[1];

View File

@@ -35,7 +35,7 @@ function Component(props) {
import { c as _c } from "react/compiler-runtime"; // @enableTransitivelyFreezeFunctionExpressions:false
function Component(props) {
const $ = _c(9);
const $ = _c(7);
const item = useMutable(props.itemId);
const dispatch = useDispatch();
useFreeze(dispatch);
@@ -51,7 +51,8 @@ function Component(props) {
}
const exit = t0;
let t1;
if ($[2] !== exit || $[3] !== item.value) {
let t2;
if ($[2] !== exit || $[3] !== item) {
t1 = () => {
const cleanup = GlobalEventEmitter.addListener("onInput", () => {
if (item.value) {
@@ -60,30 +61,24 @@ function Component(props) {
});
return () => cleanup.remove();
};
t2 = [exit, item];
$[2] = exit;
$[3] = item.value;
$[3] = item;
$[4] = t1;
$[5] = t2;
} else {
t1 = $[4];
}
let t2;
if ($[5] !== exit || $[6] !== item) {
t2 = [exit, item];
$[5] = exit;
$[6] = item;
$[7] = t2;
} else {
t2 = $[7];
t2 = $[5];
}
useEffect(t1, t2);
maybeMutate(item);
let t3;
if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
t3 = <div />;
$[8] = t3;
$[6] = t3;
} else {
t3 = $[8];
t3 = $[6];
}
return t3;
}