@samitouri / QOS-React-2 / commits / 32569d2113

noAlias support for CallExpr incl hooks

Adds `noAlias` support for CallExpression, including hooks. Note that we treat hook arguments as escaping by default — ie we assume that they don't just flow into the hook return value, but are just outright escape points equivalent to a return. A `noAlias` annotation on a hook definition disables both: this will allow us to avoid memoizing the `graphql` tag arguments to `useFragment`, for example.

Joe Savona committed Sep 18, 2023 at 14:38 UTC 32569d21139958f93a4b87d7c0ae1c068dfc5b78
6 files changed +165 -5
compiler/packages/babel-plugin-react-forget/src/HIR/Environment.ts
+18
@@ -36,8 +36,25 @@ import {
36 } from "./ObjectShape";
37
38 export type Hook = {
39 + /**
40 + * The effect of arguments to this hook. Describes whether the hook may or may
41 + * not mutate arguments, etc.
42 + */
43 effectKind: Effect;
44 +
45 + /**
46 + * The kind of value returned by the hook. Allows indicating that a hook returns
47 + * a primitive or already-frozen value, which can allow more precise memoization
48 + * of callers.
49 + */
50 valueKind: ValueKind;
51 +
52 + /**
53 + * Specifies whether hook arguments may be aliased by other arguments or by the
54 + * return value of the function. Defaults to false. When enabled, this allows the
55 + * compiler to avoid memoizing arguments.
56 + */
57 + noAlias?: boolean;
58 };
59
60 // TODO(mofeiZ): User defined global types (with corresponding shapes).
@@ -223,6 +240,7 @@ export class Environment {
240 returnValueKind: hook.valueKind,
241 calleeEffect: Effect.Read,
242 hookKind: "Custom",
243 + noAlias: hook.noAlias ?? false,
244 })
245 );
246 }
compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/PruneNonEscapingScopes.ts
+40 -5
@@ -28,7 +28,6 @@ import { getFunctionCallSignature } from "../Inference/InferReferenceEffects";
28 import { log } from "../Utils/logger";
29 import { assertExhaustive } from "../Utils/utils";
30 import { getPlaceScope } from "./BuildReactiveBlocks";
31 -import { printReactiveFunction } from "./PrintReactiveFunction";
31 import {
32 ReactiveFunctionTransform,
33 ReactiveFunctionVisitor,
@@ -133,8 +132,7 @@ export function pruneNonEscapingScopes(
132 const memoized = computeMemoizedIdentifiers(state);
133
134 log(() => prettyFormat(memoized));
136 -
137 - log(() => printReactiveFunction(fn));
135 + // log(() => printReactiveFunction(fn));
136
137 // Prune scopes that do not declare/reassign any escaping values
138 visitReactiveFunction(fn, new PruneScopesTransform(), memoized);
@@ -608,6 +606,31 @@ function computeMemoizationInputs(
606 ],
607 };
608 }
609 + case "CallExpression": {
610 + const signature = env.enableNoAliasOptimizations
611 + ? getFunctionCallSignature(env, value.callee.identifier.type)
612 + : null;
613 + const operands = [...eachReactiveValueOperand(value)];
614 + let lvalues = [];
615 + if (lvalue !== null) {
616 + lvalues.push({ place: lvalue, level: MemoizationLevel.Memoized });
617 + }
618 + if (signature?.noAlias === true) {
619 + return {
620 + lvalues,
621 + rvalues: [],
622 + };
623 + }
624 + lvalues.push(
625 + ...operands
626 + .filter((operand) => isMutableEffect(operand.effect, operand.loc))
627 + .map((place) => ({ place, level: MemoizationLevel.Memoized }))
628 + );
629 + return {
630 + lvalues,
631 + rvalues: operands,
632 + };
633 + }
634 case "MethodCall": {
635 const signature = env.enableNoAliasOptimizations
636 ? getFunctionCallSignature(env, value.property.identifier.type)
@@ -639,7 +662,6 @@ function computeMemoizationInputs(
662 case "ArrayExpression":
663 case "NewExpression":
664 case "ObjectExpression":
642 - case "CallExpression":
665 case "PropertyStore": {
666 // All of these instructions may produce new values which must be memoized if
667 // reachable from a return value. Any mutable rvalue may alias any other rvalue
@@ -780,7 +802,20 @@ class CollectDependenciesVisitor extends ReactiveFunctionVisitor<State> {
802 );
803 } else if (instruction.value.kind === "CallExpression") {
804 const callee = instruction.value.callee;
783 - if (getHookKind(state.env, callee.identifier)) {
805 + if (getHookKind(state.env, callee.identifier) != null) {
806 + const signature = this.env.enableNoAliasOptimizations
807 + ? getFunctionCallSignature(
808 + this.env,
809 + instruction.value.callee.identifier.type
810 + )
811 + : null;
812 + // Hook values are assumed to escape by default since they can be inputs
813 + // to reactive scopes in the hook. However if the hook is annotated as
814 + // noAlias we know that the arguments cannot escape and don't need to
815 + // be memoized.
816 + if (signature && signature.noAlias === true) {
817 + return;
818 + }
819 for (const operand of instruction.value.args) {
820 const place = operand.kind === "Spread" ? operand.place : operand;
821 state.escapingValues.add(place.identifier.id);
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/hook-noAlias.expect.md new
+74
@@ -0,0 +1,74 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @enableNoAliasOptimizations
6 +import { useNoAlias } from "shared-runtime";
7 +
8 +function Component(props) {
9 + const item = { a: props.a };
10 + const x = useNoAlias(
11 + item,
12 + () => {
13 + console.log(props);
14 + },
15 + [props.a]
16 + );
17 + return [x, item];
18 +}
19 +
20 +export const FIXTURE_ENTRYPOINT = {
21 + fn: Component,
22 + params: [{ a: { id: 42 } }],
23 + isComponent: true,
24 +};
25 +
26 +```
27 +
28 +## Code
29 +
30 +```javascript
31 +import { unstable_useMemoCache as useMemoCache } from "react"; // @enableNoAliasOptimizations
32 +import { useNoAlias } from "shared-runtime";
33 +
34 +function Component(props) {
35 + const $ = useMemoCache(5);
36 + const c_0 = $[0] !== props.a;
37 + let t0;
38 + if (c_0) {
39 + t0 = { a: props.a };
40 + $[0] = props.a;
41 + $[1] = t0;
42 + } else {
43 + t0 = $[1];
44 + }
45 + const item = t0;
46 + const x = useNoAlias(
47 + item,
48 + () => {
49 + console.log(props);
50 + },
51 + [props.a]
52 + );
53 + const c_2 = $[2] !== x;
54 + const c_3 = $[3] !== item;
55 + let t1;
56 + if (c_2 || c_3) {
57 + t1 = [x, item];
58 + $[2] = x;
59 + $[3] = item;
60 + $[4] = t1;
61 + } else {
62 + t1 = $[4];
63 + }
64 + return t1;
65 +}
66 +
67 +export const FIXTURE_ENTRYPOINT = {
68 + fn: Component,
69 + params: [{ a: { id: 42 } }],
70 + isComponent: true,
71 +};
72 +
73 +```
74 +
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/hook-noAlias.js new
+20
@@ -0,0 +1,20 @@
1 +// @enableNoAliasOptimizations
2 +import { useNoAlias } from "shared-runtime";
3 +
4 +function Component(props) {
5 + const item = { a: props.a };
6 + const x = useNoAlias(
7 + item,
8 + () => {
9 + console.log(props);
10 + },
11 + [props.a]
12 + );
13 + return [x, item];
14 +}
15 +
16 +export const FIXTURE_ENTRYPOINT = {
17 + fn: Component,
18 + params: [{ a: { id: 42 } }],
19 + isComponent: true,
20 +};
compiler/packages/fixture-test-utils/src/compiler-utils.ts
+8
@@ -102,6 +102,14 @@ export function transformFixtureInput(
102 effectKind: "freeze" as Effect,
103 },
104 ],
105 + [
106 + "useNoAlias",
107 + {
108 + valueKind: "mutable" as ValueKind,
109 + effectKind: "read" as Effect,
110 + noAlias: true,
111 + },
112 + ],
113 ]),
114 enableAssumeHooksFollowRulesOfReact,
115 enableFunctionCallSignatureOptimizations: true,
compiler/packages/sprout/src/shared-runtime.ts
+5
@@ -81,6 +81,11 @@ export function throwInput(x: Object): never {
81 throw x;
82 }
83
84 +const noAliasObject = Object.freeze({});
85 +export function useNoAlias(...args: Array<any>): object {
86 + return noAliasObject;
87 +}
88 +
89 /**
90 * React Components
91 */