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
*/