Allow context values to be mutated in callbacks

Adds a new type for representing context values, which is transitive. So 
`useContext(a).b.c` also gets inferred as a context type. This allows us to 
refine our inference, and allow passing callbacks that modify context where a 
"frozen" lambda is exepcted.
This commit is contained in:
Joe Savona
2023-10-03 08:47:04 -07:00
parent 41cb3c722e
commit 216696dd88
7 changed files with 71 additions and 2 deletions
@@ -9,6 +9,7 @@ import { Effect, ValueKind } from "./HIR";
import {
BUILTIN_SHAPES,
BuiltInArrayId,
BuiltInContextId,
BuiltInUseRefId,
BuiltInUseStateId,
ShapeRegistry,
@@ -239,7 +240,7 @@ const BUILTIN_HOOKS: Array<[string, FunctionType]> = [
addHook(DEFAULT_SHAPES, [], {
positionalParams: [],
restParam: Effect.Read,
returnType: { kind: "Poly" },
returnType: { kind: "Object", shapeId: BuiltInContextId },
calleeEffect: Effect.Read,
hookKind: "useContext",
returnValueKind: ValueKind.Mutable,
@@ -1111,6 +1111,10 @@ export function isSetStateType(id: Identifier): boolean {
return id.type.kind === "Function" && id.type.shapeId === "BuiltInSetState";
}
export function isContextType(id: Identifier): boolean {
return id.type.kind === "Object" && id.type.shapeId === "BuiltInContext";
}
export function getHookKind(env: Environment, id: Identifier): HookKind | null {
const idType = id.type;
if (idType.kind === "Function") {
@@ -170,6 +170,7 @@ export const BuiltInUseStateId = "BuiltInUseState";
export const BuiltInSetStateId = "BuiltInSetState";
export const BuiltInUseRefId = "BuiltInUseRefId";
export const BuiltInRefValueId = "BuiltInRefValue";
export const BuiltInContextId = "BuiltInContext";
export const BuiltInMixedReadonlyId = "BuiltInMixedReadonly";
/**
@@ -293,6 +294,10 @@ addObject(BUILTIN_SHAPES, BuiltInUseRefId, [
["current", { kind: "Object", shapeId: BuiltInRefValueId }],
]);
addObject(BUILTIN_SHAPES, BuiltInContextId, [
["*", { kind: "Object", shapeId: BuiltInContextId }],
]);
addObject(BUILTIN_SHAPES, BuiltInRefValueId, []);
addObject(BUILTIN_SHAPES, BuiltInMixedReadonlyId, [
@@ -10,6 +10,7 @@ import {
Effect,
HIRFunction,
Identifier,
isContextType,
isRefValueType,
isSetStateType,
isUseRefType,
@@ -142,7 +143,8 @@ function infer(
if (
isUseRefType(dep.identifier) ||
isRefValueType(dep.identifier) ||
isSetStateType(dep.identifier)
isSetStateType(dep.identifier) ||
isContextType(dep.identifier)
) {
// TODO: this is a hack to ensure we treat functions which reference refs
// as having a capture and therefore being considered mutable. this ensures
@@ -0,0 +1,48 @@
## Input
```javascript
// @debug
function Component(props) {
const FooContext = useContext(Foo);
const onClick = () => {
FooContext.current = true;
};
return <div onClick={onClick} />;
}
```
## Code
```javascript
import { unstable_useMemoCache as useMemoCache } from "react"; // @debug
function Component(props) {
const $ = useMemoCache(4);
const FooContext = useContext(Foo);
const c_0 = $[0] !== FooContext.current;
let t0;
if (c_0) {
t0 = () => {
FooContext.current = true;
};
$[0] = FooContext.current;
$[1] = t0;
} else {
t0 = $[1];
}
const onClick = t0;
const c_2 = $[2] !== onClick;
let t1;
if (c_2) {
t1 = <div onClick={onClick} />;
$[2] = onClick;
$[3] = t1;
} else {
t1 = $[3];
}
return t1;
}
```
@@ -0,0 +1,8 @@
// @debug
function Component(props) {
const FooContext = useContext(Foo);
const onClick = () => {
FooContext.current = true;
};
return <div onClick={onClick} />;
}
@@ -460,6 +460,7 @@ const skipFilter = new Set([
"fbtparam-text-must-use-expression-container",
"fbtparam-with-jsx-fragment-value",
"fbt-preserve-jsxtext",
"useContext-mutable-value",
]);
export default skipFilter;