@samitouri / QOS-React / commits / 8b9edafa95

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 +};