Add type defs for reanimated

The reanimated babel plugin specifically looks for args to their hooks that are 
callbacks, then it workletizes the body of that callback so it can run on the 
main thread. 

But, forget extracts that callback into a temporary variable and then replaces 
the previously inlined callback as an identifier, so that breaks reanimated's 
babel plugin. so what happens is some of the previously workletized functions no 
longer do after forget runs, which throws a runtime error about a non-worklet 
function running on the main thread. 

Reanimated expects this: ``` const animatedGProps = useAnimatedProp(function () 
{ ... }) ``` 

But forget does this: ``` const t0 =function () { ... } const animatedGProps = 
useAnimatedProp(t0) ``` 

With the type definitions, Forget no longer assumes the args to reanimated APIs 
escape so Forget does not memoize and they stay as is.
This commit is contained in:
Sathya Gunasekaran
2024-03-28 12:26:22 +00:00
parent a6c6855dd1
commit 0a3d1c3d31
5 changed files with 195 additions and 0 deletions
@@ -17,6 +17,7 @@ import {
DEFAULT_SHAPES,
Global,
GlobalRegistry,
installReAnimatedTypes,
} from "./Globals";
import {
BlockId,
@@ -357,6 +358,16 @@ const EnvironmentConfigSchema = z.object({
*/
enableTreatFunctionDepsAsConditional: z.boolean().default(false),
/**
* The react native re-animated library uses custom Babel transforms that
* requires the calls to library API remain unmodified.
*
* If this flag is turned on, the React compiler will use custom type
* definitions for reanimated library to make it's Babel plugin work
* with the compiler.
*/
enableCustomTypeDefinitionForReAnimated: z.boolean().default(false),
/**
* If specified, this value is used as a pattern for determing which global values should be
* treated as hooks. The pattern should have a single capture group, which will be used as
@@ -469,6 +480,10 @@ export class Environment {
);
}
if (config.enableCustomTypeDefinitionForReAnimated) {
installReAnimatedTypes(this.#globals, this.#shapes);
}
this.#contextIdentifiers = contextIdentifiers;
this.#hoistedIdentifiers = new Set();
}
@@ -9,6 +9,7 @@ import { Effect, ValueKind, ValueReason } from "./HIR";
import {
BUILTIN_SHAPES,
BuiltInArrayId,
BuiltInMixedReadonlyId,
BuiltInUseEffectHookId,
BuiltInUseInsertionEffectHookId,
BuiltInUseLayoutEffectHookId,
@@ -413,3 +414,76 @@ DEFAULT_GLOBALS.set(
"globalThis",
addObject(DEFAULT_SHAPES, "globalThis", TYPED_GLOBALS)
);
export function installReAnimatedTypes(
globals: GlobalRegistry,
registry: ShapeRegistry
): void {
// hooks that freeze args and return frozen value
const frozenHooks = [
"useFrameCallback",
"useAnimatedStyle",
"useAnimatedProps",
"useAnimatedScrollHandler",
"useAnimatedReaction",
"useWorkletCallback",
];
for (const hook of frozenHooks) {
globals.set(
hook,
addHook(registry, {
positionalParams: [],
restParam: Effect.Freeze,
returnType: { kind: "Object", shapeId: BuiltInMixedReadonlyId },
returnValueKind: ValueKind.Frozen,
noAlias: true,
calleeEffect: Effect.Read,
hookKind: "Custom",
})
);
}
/**
* hooks that return a mutable value. ideally these should be modelled as a
* ref, but this works for now.
*/
const mutableHooks = ["useSharedValue", "useDerivedValue"];
for (const hook of mutableHooks) {
globals.set(
hook,
addHook(registry, {
positionalParams: [],
restParam: Effect.Freeze,
returnType: { kind: "Poly" },
returnValueKind: ValueKind.Mutable,
noAlias: true,
calleeEffect: Effect.Read,
hookKind: "Custom",
})
);
}
// functions that return mutable value
const funcs = [
"withTiming",
"withSpring",
"createAnimatedPropAdapter",
"withDecay",
"withRepeat",
"runOnUI",
"executeOnUIRuntimeSync",
];
for (const fn of funcs) {
globals.set(
fn,
addFunction(registry, [], {
positionalParams: [],
restParam: Effect.Read,
returnType: { kind: "Poly" },
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Mutable,
noAlias: true,
})
);
}
}
@@ -0,0 +1,76 @@
## Input
```javascript
// @enableCustomTypeDefinitionForReAnimated
function Component() {
const radius = useSharedValue(50);
const animatedProps = useAnimatedProps(() => {
// draw a circle
const path = `
M 100, 100
m -${radius.value}, 0
a ${radius.value},${radius.value} 0 1,0 ${radius.value * 2},0
a ${radius.value},${radius.value} 0 1,0 ${-radius.value * 2},0
`;
return {
d: path,
};
});
// attach animated props to an SVG path using animatedProps
return (
<Svg>
<AnimatedPath animatedProps={animatedProps} fill="black" />
</Svg>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [],
isComponent: false,
};
```
## Code
```javascript
import { unstable_useMemoCache as useMemoCache } from "react"; // @enableCustomTypeDefinitionForReAnimated
function Component() {
const $ = useMemoCache(2);
const radius = useSharedValue(50);
const animatedProps = useAnimatedProps(() => {
const path = `
M 100, 100
m -${radius.value}, 0
a ${radius.value},${radius.value} 0 1,0 ${radius.value * 2},0
a ${radius.value},${radius.value} 0 1,0 ${-radius.value * 2},0
`;
return { d: path };
});
let t0;
if ($[0] !== animatedProps) {
t0 = (
<Svg>
<AnimatedPath animatedProps={animatedProps} fill="black" />
</Svg>
);
$[0] = animatedProps;
$[1] = t0;
} else {
t0 = $[1];
}
return t0;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [],
isComponent: false,
};
```
@@ -0,0 +1,29 @@
// @enableCustomTypeDefinitionForReAnimated
function Component() {
const radius = useSharedValue(50);
const animatedProps = useAnimatedProps(() => {
// draw a circle
const path = `
M 100, 100
m -${radius.value}, 0
a ${radius.value},${radius.value} 0 1,0 ${radius.value * 2},0
a ${radius.value},${radius.value} 0 1,0 ${-radius.value * 2},0
`;
return {
d: path,
};
});
// attach animated props to an SVG path using animatedProps
return (
<Svg>
<AnimatedPath animatedProps={animatedProps} fill="black" />
</Svg>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [],
isComponent: false,
};
@@ -490,6 +490,7 @@ const skipFilter = new Set([
"fbt/fbt-preserve-jsxtext",
"todo.useContext-mutate-context-in-callback",
"loop-unused-let",
"reanimated-no-memo-arg",
// Tested e2e in forget-feedback repo
"userspace-use-memo-cache",