@samitouri / QOS-React-1 / commits / d5c3fb87e6

HIR-based reactive identifier analysis

See context from #2187 for background about control dependencies. Our current `PruneNonReactiveIdentifiers` pass runs on ReactiveFunction, after scope construction, and removes scope dependencies that aren't reactive. It works by first building up a set of reactive identifiers in `InferReactiveIdentifiers`, then walking the ReactiveFunction and pruning any scope dependencies that aren't in that set. The challenge is control variables, as demonstrated by the test cases in #2184. `InferReactiveIdentifiers` runs against ReactiveFunction, and when we initially wrote it we didn't consider control variables. To handle control variables we really need to use precise control- & data-flow analysis, which is much easier with HIR. This PR adds the start of `InferReactivePlaces`, which annotates each `Place` with whether it is reactive or not. This allows the annotation to survive LeaveSSA, which swaps out the identifiers of places but leaves other properties as-is. This version does _not_ yet handle control variables, but it's already more precise than our existing inference. In our current inference, if `x` is ever assigned a reactive value, then all `x`s are marked reactive. In our new inference, each instance of `x` (each Place) gets a separate flag based on whether x can actually be reactive at that point in the program. There are two main next steps (in follow-up PRs): * Update the mechanism by which we prune non-reactive dependencies from scopes. * Handle control variables. I think we may be able to use dominator trees to figure out the set of basic blocks whose reachability is gated by the control variables. This should clearly work for if/else and switch, as for loops i'm not sure but intuitively it seems right.

Joe Savona committed Nov 1, 2023 at 17:13 UTC d5c3fb87e6861dfe1bfdcb9353f71275172a3816
3 files changed +208
compiler/packages/babel-plugin-react-forget/src/Entrypoint/Pipeline.ts
+4
@@ -24,6 +24,7 @@ import {
24 analyseFunctions,
25 dropManualMemoization,
26 inferMutableRanges,
27 + inferReactivePlaces,
28 inferReferenceEffects,
29 inlineImmediatelyInvokedFunctionExpressions,
30 } from "../Inference";
@@ -193,6 +194,9 @@ function* runWithEnvironment(
194 });
195 }
196
197 + inferReactivePlaces(hir);
198 + yield log({ kind: "hir", name: "InferReactivePlaces", value: hir });
199 +
200 leaveSSA(hir);
201 yield log({ kind: "hir", name: "LeaveSSA", value: hir });
202
compiler/packages/babel-plugin-react-forget/src/Inference/InferReactivePlaces.ts new
+203
@@ -0,0 +1,203 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + */
7 +
8 +import { CompilerError } from "..";
9 +import {
10 + Effect,
11 + HIRFunction,
12 + Identifier,
13 + IdentifierId,
14 + Place,
15 + getHookKind,
16 +} from "../HIR";
17 +import {
18 + eachInstructionLValue,
19 + eachInstructionValueOperand,
20 + eachTerminalOperand,
21 +} from "../HIR/visitors";
22 +import { hasBackEdge } from "../Optimization/DeadCodeElimination";
23 +import { assertExhaustive } from "../Utils/utils";
24 +
25 +/**
26 + * Infers which `Place`s are reactive, ie may *semantically* change
27 + * over the course of the component/hook's lifetime. Places are reactive
28 + * if they derive from source source of reactivity, which includes the
29 + * following categories.
30 + *
31 + * ## Props
32 + *
33 + * Props may change so they're reactive:
34 + *
35 + * ## Hooks
36 + *
37 + * Hooks may access state or context, which can change so they're reactive.
38 + *
39 + * ## Mutation with reactive operands
40 + *
41 + * Any value that is mutated in an instruction that also has reactive operands
42 + * could cause the modified value to capture a reference to the reactive value,
43 + * making the mutated value reactive.
44 + *
45 + * Ex:
46 + * ```
47 + * function Component(props) {
48 + * const x = {}; // not yet reactive
49 + * x.y = props.y;
50 + * }
51 + * ```
52 + *
53 + * Here `x` is modified in an instruction that has a reactive operand (`props.y`)
54 + * so x becomes reactive.
55 + *
56 + * ## Conditional assignment based on a reactive condition
57 + *
58 + * Conditionally reassigning a variable based on a condition which is reactive means
59 + * that the value being assigned could change, hence that variable also becomes
60 + * reactive.
61 + *
62 + * ```
63 + * function Component(props) {
64 + * let x;
65 + * if (props.cond) {
66 + * x = 1;
67 + * } else {
68 + * x = 2;
69 + * }
70 + * return x;
71 + * }
72 + * ```
73 + *
74 + * Here `x` is never assigned a reactive value (it is assigned the constant 1 or 2) but
75 + * the condition, `props.cond`, is reactive, and therefore `x` could change reactively too.
76 + *
77 + *
78 + * # Algorithm
79 + *
80 + * The algorithm uses a fixpoint iteration in order to propagate reactivity "forward" through
81 + * the control-flow graph. We track whether each IdentifierId is reactive and terminate when
82 + * there are no changes after a given pass over the CFG.
83 + */
84 +export function inferReactivePlaces(fn: HIRFunction): void {
85 + const reactiveIdentifiers = new ReactivityMap();
86 + for (const param of fn.params) {
87 + const place = param.kind === "Identifier" ? param : param.place;
88 + reactiveIdentifiers.markReactive(place);
89 + }
90 +
91 + const hasLoop = hasBackEdge(fn);
92 + do {
93 + for (const [, block] of fn.body.blocks) {
94 + for (const phi of block.phis) {
95 + for (const [, operand] of phi.operands) {
96 + if (reactiveIdentifiers.isReactiveIdentifier(operand)) {
97 + reactiveIdentifiers.markReactiveIdentifier(phi.id);
98 + break;
99 + }
100 + }
101 + }
102 + for (const instruction of block.instructions) {
103 + const { value } = instruction;
104 + let hasReactiveInput = false;
105 + // NOTE: we want to mark all operands as reactive or not, so we
106 + // avoid short-circuting here
107 + for (const operand of eachInstructionValueOperand(value)) {
108 + const reactive = reactiveIdentifiers.isReactive(operand);
109 + hasReactiveInput ||= reactive;
110 + }
111 +
112 + // Hooks may always return a reactive variable, even if their inputs are
113 + // non-reactive, because they can access state or context.
114 + if (
115 + value.kind === "CallExpression" &&
116 + getHookKind(fn.env, value.callee.identifier) != null
117 + ) {
118 + hasReactiveInput = true;
119 + } else if (
120 + value.kind === "MethodCall" &&
121 + getHookKind(fn.env, value.property.identifier) != null
122 + ) {
123 + hasReactiveInput = true;
124 + }
125 +
126 + if (hasReactiveInput) {
127 + for (const lvalue of eachInstructionLValue(instruction)) {
128 + reactiveIdentifiers.markReactive(lvalue);
129 + }
130 +
131 + for (const operand of eachInstructionValueOperand(value)) {
132 + switch (operand.effect) {
133 + case Effect.Capture:
134 + case Effect.Store:
135 + case Effect.ConditionallyMutate:
136 + case Effect.Mutate: {
137 + reactiveIdentifiers.markReactive(operand);
138 + break;
139 + }
140 + case Effect.Freeze:
141 + case Effect.Read: {
142 + // no-op
143 + break;
144 + }
145 + case Effect.Unknown: {
146 + CompilerError.invariant(false, {
147 + reason: "Unexpected unknown effect",
148 + description: null,
149 + loc: operand.loc,
150 + suggestions: null,
151 + });
152 + }
153 + default: {
154 + assertExhaustive(
155 + operand.effect,
156 + `Unexpected effect kind '${operand.effect}'`
157 + );
158 + }
159 + }
160 + }
161 + }
162 + }
163 + for (const operand of eachTerminalOperand(block.terminal)) {
164 + reactiveIdentifiers.isReactive(operand);
165 + }
166 + }
167 + } while (reactiveIdentifiers.snapshot() && hasLoop);
168 +}
169 +
170 +class ReactivityMap {
171 + hasChanges: boolean = false;
172 + reactive: Set<IdentifierId> = new Set();
173 +
174 + isReactive(place: Place): boolean {
175 + const reactive = this.reactive.has(place.identifier.id);
176 + if (reactive) {
177 + place.reactive = true;
178 + }
179 + return reactive;
180 + }
181 +
182 + isReactiveIdentifier(identifier: Identifier): boolean {
183 + return this.reactive.has(identifier.id);
184 + }
185 +
186 + markReactive(place: Place): void {
187 + place.reactive = true;
188 + this.markReactiveIdentifier(place.identifier);
189 + }
190 +
191 + markReactiveIdentifier(identifier: Identifier): void {
192 + if (!this.reactive.has(identifier.id)) {
193 + this.hasChanges = true;
194 + this.reactive.add(identifier.id);
195 + }
196 + }
197 +
198 + snapshot(): boolean {
199 + const hasChanges = this.hasChanges;
200 + this.hasChanges = false;
201 + return hasChanges;
202 + }
203 +}
compiler/packages/babel-plugin-react-forget/src/Inference/index.ts
+1
@@ -8,5 +8,6 @@
8 export { default as analyseFunctions } from "./AnalyseFunctions";
9 export { dropManualMemoization } from "./DropManualMemoization";
10 export { inferMutableRanges } from "./InferMutableRanges";
11 +export { inferReactivePlaces } from "./InferReactivePlaces";
12 export { default as inferReferenceEffects } from "./InferReferenceEffects";
13 export { inlineImmediatelyInvokedFunctionExpressions } from "./InlineImmediatelyInvokedFunctionExpressions";