From 0a3d1c3d31ce83d18906357c29381e722cbec8b4 Mon Sep 17 00:00:00 2001 From: Sathya Gunasekaran Date: Thu, 28 Mar 2024 12:26:22 +0000 Subject: [PATCH] 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. --- .../src/HIR/Environment.ts | 15 ++++ .../src/HIR/Globals.ts | 74 ++++++++++++++++++ .../compiler/reanimated-no-memo-arg.expect.md | 76 +++++++++++++++++++ .../compiler/reanimated-no-memo-arg.js | 29 +++++++ .../packages/snap/src/SproutTodoFilter.ts | 1 + 5 files changed, 195 insertions(+) create mode 100644 compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reanimated-no-memo-arg.expect.md create mode 100644 compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reanimated-no-memo-arg.js diff --git a/compiler/packages/babel-plugin-react-forget/src/HIR/Environment.ts b/compiler/packages/babel-plugin-react-forget/src/HIR/Environment.ts index 901dfd6ff6..676e9ecd0b 100644 --- a/compiler/packages/babel-plugin-react-forget/src/HIR/Environment.ts +++ b/compiler/packages/babel-plugin-react-forget/src/HIR/Environment.ts @@ -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(); } diff --git a/compiler/packages/babel-plugin-react-forget/src/HIR/Globals.ts b/compiler/packages/babel-plugin-react-forget/src/HIR/Globals.ts index 899bb64108..2679833c0b 100644 --- a/compiler/packages/babel-plugin-react-forget/src/HIR/Globals.ts +++ b/compiler/packages/babel-plugin-react-forget/src/HIR/Globals.ts @@ -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, + }) + ); + } +} diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reanimated-no-memo-arg.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reanimated-no-memo-arg.expect.md new file mode 100644 index 0000000000..94e4ba4d07 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reanimated-no-memo-arg.expect.md @@ -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 ( + + + + ); +} +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 = ( + + + + ); + $[0] = animatedProps; + $[1] = t0; + } else { + t0 = $[1]; + } + return t0; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], + isComponent: false, +}; + +``` + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reanimated-no-memo-arg.js b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reanimated-no-memo-arg.js new file mode 100644 index 0000000000..eca548f748 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reanimated-no-memo-arg.js @@ -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 ( + + + + ); +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], + isComponent: false, +}; diff --git a/compiler/packages/snap/src/SproutTodoFilter.ts b/compiler/packages/snap/src/SproutTodoFilter.ts index fef339c363..2faaf15443 100644 --- a/compiler/packages/snap/src/SproutTodoFilter.ts +++ b/compiler/packages/snap/src/SproutTodoFilter.ts @@ -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",