Support use operator
Implements support for use: * Teaches InferReactivePlaces to treat use() result as reactive * Teaches FlattenScopesWithHooks to also flatten scopes with use() Handles both `use()` and `React.use()`.
Joe Savona committed
Apr 2, 2024 at 11:28 UTC
8b9edafa95f5657cbccb11e4c226abd7953df8f0
12 files changed
+393
-25
compiler/packages/babel-plugin-react-forget/src/Entrypoint/Pipeline.ts
+2
-2
@@ -47,7 +47,7 @@ import {
47
codegenFunction,
48
extractScopeDeclarationsFromDestructuring,
49
flattenReactiveLoops,
50
- flattenScopesWithHooks,
50
+ flattenScopesWithHooksOrUse,
51
inferReactiveScopeVariables,
52
memoizeFbtOperandsInSameScope,
53
mergeOverlappingReactiveScopes,
@@ -277,7 +277,7 @@ function* runWithEnvironment(
277
278
assertScopeInstructionsWithinScopes(reactiveFunction);
279
280
- flattenScopesWithHooks(reactiveFunction);
280
+ flattenScopesWithHooksOrUse(reactiveFunction);
281
yield log({
282
kind: "reactive",
283
name: "FlattenScopesWithHooks",
compiler/packages/babel-plugin-react-forget/src/HIR/Environment.ts
-5
@@ -572,11 +572,6 @@ export class Environment {
572
573
// From https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/src/RulesOfHooks.js#LL18C1-L23C2
574
export function isHookName(name: string): boolean {
575
- /*
576
- * if (__EXPERIMENTAL__) {
577
- * return name === 'use' || /^use[A-Z0-9]/.test(name);
578
- * }
579
- */
575
return /^use[A-Z0-9]/.test(name);
576
}
577
compiler/packages/babel-plugin-react-forget/src/HIR/Globals.ts
+19
-3
@@ -12,6 +12,7 @@ import {
12
BuiltInUseEffectHookId,
13
BuiltInUseInsertionEffectHookId,
14
BuiltInUseLayoutEffectHookId,
15
+ BuiltInUseOperatorId,
16
BuiltInUseRefId,
17
BuiltInUseStateId,
18
ShapeRegistry,
@@ -239,7 +240,7 @@ const TYPED_GLOBALS: Array<[string, BuiltInType]> = [
240
* now that FeatureFlag `enableTreatHooksAsFunctions` is removed we can
241
* use positional params too (?)
242
*/
242
-const BUILTIN_HOOKS: Array<[string, BuiltInType]> = [
243
+const REACT_APIS: Array<[string, BuiltInType]> = [
244
[
245
"useContext",
246
addHook(DEFAULT_SHAPES, {
@@ -342,13 +343,28 @@ const BUILTIN_HOOKS: Array<[string, BuiltInType]> = [
343
BuiltInUseInsertionEffectHookId
344
),
345
],
346
+ [
347
+ "use",
348
+ addFunction(
349
+ DEFAULT_SHAPES,
350
+ [],
351
+ {
352
+ positionalParams: [],
353
+ restParam: Effect.Freeze,
354
+ returnType: { kind: "Poly" },
355
+ calleeEffect: Effect.Read,
356
+ returnValueKind: ValueKind.Frozen,
357
+ },
358
+ BuiltInUseOperatorId
359
+ ),
360
+ ],
361
];
362
363
TYPED_GLOBALS.push(
364
[
365
"React",
366
addObject(DEFAULT_SHAPES, null, [
351
- ...BUILTIN_HOOKS,
367
+ ...REACT_APIS,
368
[
369
"createElement",
370
addFunction(DEFAULT_SHAPES, [], {
@@ -395,7 +411,7 @@ TYPED_GLOBALS.push(
411
412
export type Global = BuiltInType | PolyType;
413
export type GlobalRegistry = Map<string, Global>;
398
-export const DEFAULT_GLOBALS: GlobalRegistry = new Map(BUILTIN_HOOKS);
414
+export const DEFAULT_GLOBALS: GlobalRegistry = new Map(REACT_APIS);
415
416
// Hack until we add ObjectShapes for all globals
417
for (const name of UNTYPED_GLOBALS) {
compiler/packages/babel-plugin-react-forget/src/HIR/HIR.ts
+6
@@ -1416,6 +1416,12 @@ export function getHookKind(env: Environment, id: Identifier): HookKind | null {
1416
return getHookKindForType(env, id.type);
1417
}
1418
1419
+export function isUseOperator(id: Identifier): boolean {
1420
+ return (
1421
+ id.type.kind === "Function" && id.type.shapeId === "BuiltInUseOperator"
1422
+ );
1423
+}
1424
+
1425
export function getHookKindForType(
1426
env: Environment,
1427
type: Type
compiler/packages/babel-plugin-react-forget/src/HIR/ObjectShape.ts
+1
@@ -197,6 +197,7 @@ export const BuiltInMixedReadonlyId = "BuiltInMixedReadonly";
197
export const BuiltInUseEffectHookId = "BuiltInUseEffectHook";
198
export const BuiltInUseLayoutEffectHookId = "BuiltInUseLayoutEffectHook";
199
export const BuiltInUseInsertionEffectHookId = "BuiltInUseInsertionEffectHook";
200
+export const BuiltInUseOperatorId = "BuiltInUseOperator";
201
202
// ShapeRegistry with default definitions for built-ins.
203
export const BUILTIN_SHAPES: ShapeRegistry = new Map();
compiler/packages/babel-plugin-react-forget/src/Inference/InferReactivePlaces.ts
+11
-5
@@ -16,6 +16,7 @@ import {
16
computePostDominatorTree,
17
getHookKind,
18
isSetStateType,
19
+ isUseOperator,
20
} from "../HIR";
21
import { PostDominator } from "../HIR/Dominator";
22
import {
@@ -195,18 +196,23 @@ export function inferReactivePlaces(fn: HIRFunction): void {
196
hasReactiveInput ||= reactive;
197
}
198
198
- /*
199
- * Hooks may always return a reactive variable, even if their inputs are
200
- * non-reactive, because they can access state or context.
199
+ /**
200
+ * Hooks and the 'use' operator are sources of reactivity because
201
+ * they can access state (for hooks) or context (for hooks/use).
202
+ *
203
+ * Technically, `use` could be used to await a non-reactive promise,
204
+ * but we are conservative and assume that the value could be reactive.
205
*/
206
if (
207
value.kind === "CallExpression" &&
204
- getHookKind(fn.env, value.callee.identifier) != null
208
+ (getHookKind(fn.env, value.callee.identifier) != null ||
209
+ isUseOperator(value.callee.identifier))
210
) {
211
hasReactiveInput = true;
212
} else if (
213
value.kind === "MethodCall" &&
209
- getHookKind(fn.env, value.property.identifier) != null
214
+ (getHookKind(fn.env, value.property.identifier) != null ||
215
+ isUseOperator(value.property.identifier))
216
) {
217
hasReactiveInput = true;
218
}
compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/FlattenScopesWithHooksOrUse.ts
renamed
+26
-9
@@ -13,6 +13,7 @@ import {
13
ReactiveStatement,
14
ReactiveValue,
15
getHookKind,
16
+ isUseOperator,
17
} from "../HIR";
18
import {
19
ReactiveFunctionTransform,
@@ -20,18 +21,28 @@ import {
21
visitReactiveFunction,
22
} from "./visitors";
23
23
-/*
24
- * Most parts of compilation do not treat hooks specially, because there is no guarantee that custom
25
- * hooks obey any particular contract. For example, we can't assume that custom hooks won't modify
26
- * their arguments, and we can't assume that hooks return immutable or memoized values. Therefore
27
- * earlier passes largely ignore hooks, and may end up creating reactive scopes that contain hook calls.
24
+/**
25
+ * For simplicity the majority of compiler passes do not treat hooks specially. However, hooks are different
26
+ * from regular functions in two key ways:
27
+ * - They can introduce reactivity even when their arguments are non-reactive (accounted for in InferReactivePlaces)
28
+ * - They cannot be called conditionally
29
+ *
30
+ * The `use` operator is similar:
31
+ * - It can access context, and therefore introduce reactivity
32
+ * - It can be called conditionally, but _it must be called if the component needs the return value_. This is because
33
+ * React uses the fact that use was called to remember that the component needs the value, and that changes to the
34
+ * input should invalidate the component itself.
35
+ *
36
+ * This pass accounts for the "can't call conditionally" aspect of both hooks and use. Though the reasoning is slightly
37
+ * different for reach, the result is that we can't memoize scopes that call hooks or use since this would make them
38
+ * called conditionally in the output.
39
*
29
- * This pass then finds and removes any scopes that transitively contain a hook call. By running all
40
+ * The pass finds and removes any scopes that transitively contain a hook or use call. By running all
41
* the reactive scope inference first, agnostic of hooks, we know that the reactive scopes accurately
42
* describe the set of values which "construct together", and remove _all_ that memoization in order
43
* to ensure the hook call does not inadvertently become conditional.
44
*/
34
-export function flattenScopesWithHooks(fn: ReactiveFunction): void {
45
+export function flattenScopesWithHooksOrUse(fn: ReactiveFunction): void {
46
visitReactiveFunction(fn, new Transform(), {
47
env: fn.env,
48
hasHook: false,
@@ -69,13 +80,19 @@ class Transform extends ReactiveFunctionTransform<State> {
80
this.traverseValue(id, value, state);
81
switch (value.kind) {
82
case "CallExpression": {
72
- if (getHookKind(state.env, value.callee.identifier) != null) {
83
+ if (
84
+ getHookKind(state.env, value.callee.identifier) != null ||
85
+ isUseOperator(value.callee.identifier)
86
+ ) {
87
state.hasHook = true;
88
}
89
break;
90
}
91
case "MethodCall": {
78
- if (getHookKind(state.env, value.property.identifier) != null) {
92
+ if (
93
+ getHookKind(state.env, value.property.identifier) != null ||
94
+ isUseOperator(value.property.identifier)
95
+ ) {
96
state.hasHook = true;
97
}
98
break;
compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/index.ts
+1
-1
@@ -16,7 +16,7 @@ export {
16
} from "./CodegenReactiveFunction";
17
export { extractScopeDeclarationsFromDestructuring } from "./ExtractScopeDeclarationsFromDestructuring";
18
export { flattenReactiveLoops } from "./FlattenReactiveLoops";
19
-export { flattenScopesWithHooks } from "./FlattenScopesWithHooks";
19
+export { flattenScopesWithHooksOrUse } from "./FlattenScopesWithHooksOrUse";
20
export { inferReactiveScopeVariables } from "./InferReactiveScopeVariables";
21
export { memoizeFbtOperandsInSameScope } from "./MemoizeFbtOperandsInSameScope";
22
export { mergeOverlappingReactiveScopes } from "./MergeOverlappingReactiveScopes";
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/use-operator-call-expression.expect.md
new
+129
@@ -0,0 +1,129 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+import { ValidateMemoization } from "shared-runtime";
6
+import { use, useMemo } from "react";
7
+
8
+const FooContext = React.createContext(null);
9
+function Component(props) {
10
+ return (
11
+ <FooContext.Provider value={props.value}>
12
+ <Inner />
13
+ </FooContext.Provider>
14
+ );
15
+}
16
+
17
+function Inner(props) {
18
+ const input = use(FooContext);
19
+ const output = useMemo(() => [input], [input]);
20
+ return <ValidateMemoization inputs={[input]} output={output} />;
21
+}
22
+
23
+export const FIXTURE_ENTRYPOINT = {
24
+ fn: Component,
25
+ params: [{ value: 42 }],
26
+ sequentialRenders: [
27
+ { value: null },
28
+ { value: 42 },
29
+ { value: 42 },
30
+ { value: null },
31
+ { value: null },
32
+ { value: 42 },
33
+ { value: null },
34
+ { value: 42 },
35
+ { value: null },
36
+ ],
37
+};
38
+
39
+```
40
+
41
+## Code
42
+
43
+```javascript
44
+import { ValidateMemoization } from "shared-runtime";
45
+import { use, useMemo, unstable_useMemoCache as useMemoCache } from "react";
46
+
47
+const FooContext = React.createContext(null);
48
+function Component(props) {
49
+ const $ = useMemoCache(3);
50
+ let t0;
51
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
52
+ t0 = <Inner />;
53
+ $[0] = t0;
54
+ } else {
55
+ t0 = $[0];
56
+ }
57
+ let t1;
58
+ if ($[1] !== props.value) {
59
+ t1 = <FooContext.Provider value={props.value}>{t0}</FooContext.Provider>;
60
+ $[1] = props.value;
61
+ $[2] = t1;
62
+ } else {
63
+ t1 = $[2];
64
+ }
65
+ return t1;
66
+}
67
+
68
+function Inner(props) {
69
+ const $ = useMemoCache(7);
70
+ const input = use(FooContext);
71
+ let t0;
72
+ let t1;
73
+ if ($[0] !== input) {
74
+ t1 = [input];
75
+ $[0] = input;
76
+ $[1] = t1;
77
+ } else {
78
+ t1 = $[1];
79
+ }
80
+ t0 = t1;
81
+ const output = t0;
82
+ let t2;
83
+ if ($[2] !== input) {
84
+ t2 = [input];
85
+ $[2] = input;
86
+ $[3] = t2;
87
+ } else {
88
+ t2 = $[3];
89
+ }
90
+ let t3;
91
+ if ($[4] !== t2 || $[5] !== output) {
92
+ t3 = <ValidateMemoization inputs={t2} output={output} />;
93
+ $[4] = t2;
94
+ $[5] = output;
95
+ $[6] = t3;
96
+ } else {
97
+ t3 = $[6];
98
+ }
99
+ return t3;
100
+}
101
+
102
+export const FIXTURE_ENTRYPOINT = {
103
+ fn: Component,
104
+ params: [{ value: 42 }],
105
+ sequentialRenders: [
106
+ { value: null },
107
+ { value: 42 },
108
+ { value: 42 },
109
+ { value: null },
110
+ { value: null },
111
+ { value: 42 },
112
+ { value: null },
113
+ { value: 42 },
114
+ { value: null },
115
+ ],
116
+};
117
+
118
+```
119
+
120
+### Eval output
121
+(kind: ok) <div>{"inputs":[null],"output":["[[ cyclic ref *2 ]]"]}</div>
122
+<div>{"inputs":[42],"output":[42]}</div>
123
+<div>{"inputs":[42],"output":[42]}</div>
124
+<div>{"inputs":[null],"output":["[[ cyclic ref *2 ]]"]}</div>
125
+<div>{"inputs":[null],"output":["[[ cyclic ref *2 ]]"]}</div>
126
+<div>{"inputs":[42],"output":[42]}</div>
127
+<div>{"inputs":[null],"output":["[[ cyclic ref *2 ]]"]}</div>
128
+<div>{"inputs":[42],"output":[42]}</div>
129
+<div>{"inputs":[null],"output":["[[ cyclic ref *2 ]]"]}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/use-operator-call-expression.js
new
+33
@@ -0,0 +1,33 @@
1
+import { ValidateMemoization } from "shared-runtime";
2
+import { use, useMemo } from "react";
3
+
4
+const FooContext = React.createContext(null);
5
+function Component(props) {
6
+ return (
7
+ <FooContext.Provider value={props.value}>
8
+ <Inner />
9
+ </FooContext.Provider>
10
+ );
11
+}
12
+
13
+function Inner(props) {
14
+ const input = use(FooContext);
15
+ const output = useMemo(() => [input], [input]);
16
+ return <ValidateMemoization inputs={[input]} output={output} />;
17
+}
18
+
19
+export const FIXTURE_ENTRYPOINT = {
20
+ fn: Component,
21
+ params: [{ value: 42 }],
22
+ sequentialRenders: [
23
+ { value: null },
24
+ { value: 42 },
25
+ { value: 42 },
26
+ { value: null },
27
+ { value: null },
28
+ { value: 42 },
29
+ { value: null },
30
+ { value: 42 },
31
+ { value: null },
32
+ ],
33
+};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/use-operator-method-call.expect.md
new
+131
@@ -0,0 +1,131 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+import { ValidateMemoization } from "shared-runtime";
6
+import { useMemo } from "react";
7
+import * as React from "react";
8
+
9
+const FooContext = React.createContext(null);
10
+function Component(props) {
11
+ return (
12
+ <FooContext.Provider value={props.value}>
13
+ <Inner />
14
+ </FooContext.Provider>
15
+ );
16
+}
17
+
18
+function Inner(props) {
19
+ const input = React.use(FooContext);
20
+ const output = useMemo(() => [input], [input]);
21
+ return <ValidateMemoization inputs={[input]} output={output} />;
22
+}
23
+
24
+export const FIXTURE_ENTRYPOINT = {
25
+ fn: Component,
26
+ params: [{ value: 42 }],
27
+ sequentialRenders: [
28
+ { value: null },
29
+ { value: 42 },
30
+ { value: 42 },
31
+ { value: null },
32
+ { value: null },
33
+ { value: 42 },
34
+ { value: null },
35
+ { value: 42 },
36
+ { value: null },
37
+ ],
38
+};
39
+
40
+```
41
+
42
+## Code
43
+
44
+```javascript
45
+import { ValidateMemoization } from "shared-runtime";
46
+import { useMemo, unstable_useMemoCache as useMemoCache } from "react";
47
+import * as React from "react";
48
+
49
+const FooContext = React.createContext(null);
50
+function Component(props) {
51
+ const $ = useMemoCache(3);
52
+ let t0;
53
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
54
+ t0 = <Inner />;
55
+ $[0] = t0;
56
+ } else {
57
+ t0 = $[0];
58
+ }
59
+ let t1;
60
+ if ($[1] !== props.value) {
61
+ t1 = <FooContext.Provider value={props.value}>{t0}</FooContext.Provider>;
62
+ $[1] = props.value;
63
+ $[2] = t1;
64
+ } else {
65
+ t1 = $[2];
66
+ }
67
+ return t1;
68
+}
69
+
70
+function Inner(props) {
71
+ const $ = useMemoCache(7);
72
+ const input = React.use(FooContext);
73
+ let t0;
74
+ let t1;
75
+ if ($[0] !== input) {
76
+ t1 = [input];
77
+ $[0] = input;
78
+ $[1] = t1;
79
+ } else {
80
+ t1 = $[1];
81
+ }
82
+ t0 = t1;
83
+ const output = t0;
84
+ let t2;
85
+ if ($[2] !== input) {
86
+ t2 = [input];
87
+ $[2] = input;
88
+ $[3] = t2;
89
+ } else {
90
+ t2 = $[3];
91
+ }
92
+ let t3;
93
+ if ($[4] !== t2 || $[5] !== output) {
94
+ t3 = <ValidateMemoization inputs={t2} output={output} />;
95
+ $[4] = t2;
96
+ $[5] = output;
97
+ $[6] = t3;
98
+ } else {
99
+ t3 = $[6];
100
+ }
101
+ return t3;
102
+}
103
+
104
+export const FIXTURE_ENTRYPOINT = {
105
+ fn: Component,
106
+ params: [{ value: 42 }],
107
+ sequentialRenders: [
108
+ { value: null },
109
+ { value: 42 },
110
+ { value: 42 },
111
+ { value: null },
112
+ { value: null },
113
+ { value: 42 },
114
+ { value: null },
115
+ { value: 42 },
116
+ { value: null },
117
+ ],
118
+};
119
+
120
+```
121
+
122
+### Eval output
123
+(kind: ok) <div>{"inputs":[null],"output":["[[ cyclic ref *2 ]]"]}</div>
124
+<div>{"inputs":[42],"output":[42]}</div>
125
+<div>{"inputs":[42],"output":[42]}</div>
126
+<div>{"inputs":[null],"output":["[[ cyclic ref *2 ]]"]}</div>
127
+<div>{"inputs":[null],"output":["[[ cyclic ref *2 ]]"]}</div>
128
+<div>{"inputs":[42],"output":[42]}</div>
129
+<div>{"inputs":[null],"output":["[[ cyclic ref *2 ]]"]}</div>
130
+<div>{"inputs":[42],"output":[42]}</div>
131
+<div>{"inputs":[null],"output":["[[ cyclic ref *2 ]]"]}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/use-operator-method-call.js
new
+34
@@ -0,0 +1,34 @@
1
+import { ValidateMemoization } from "shared-runtime";
2
+import { useMemo } from "react";
3
+import * as React from "react";
4
+
5
+const FooContext = React.createContext(null);
6
+function Component(props) {
7
+ return (
8
+ <FooContext.Provider value={props.value}>
9
+ <Inner />
10
+ </FooContext.Provider>
11
+ );
12
+}
13
+
14
+function Inner(props) {
15
+ const input = React.use(FooContext);
16
+ const output = useMemo(() => [input], [input]);
17
+ return <ValidateMemoization inputs={[input]} output={output} />;
18
+}
19
+
20
+export const FIXTURE_ENTRYPOINT = {
21
+ fn: Component,
22
+ params: [{ value: 42 }],
23
+ sequentialRenders: [
24
+ { value: null },
25
+ { value: 42 },
26
+ { value: 42 },
27
+ { value: null },
28
+ { value: null },
29
+ { value: 42 },
30
+ { value: null },
31
+ { value: 42 },
32
+ { value: null },
33
+ ],
34
+};