@samitouri / QOS-React-1 / commits / 91f7bc8be7

Ensure valid mutable ranges for all scopes; fix ranges for context vars

Our current validation fails to detect some invalid cases of mutable ranges — namely, ranges that are fully or partially uninitialized, with start or start+end still set to zero. This PR fixes these cases, starting by validating that the ranges for _all_ reactive scopes are valid: start >= 1, and end <= (last instr id + 1). This exposed the invalid cases, which are also fixed here: * During AnalyzeFunctions, we need to reset identifier ranges and scopes when exiting an inner function. This has to happen *after* the effects have been translated to the function deps/context operands, in order for InferRefenceEffects to continue working on the outer function. Previously I did this at the start of InferReactiveScopeVariables, but that's insufficient bc the incorrect ranges could also influence InferMutableRanges. AnalyzeFunctions is the point at which we compute the ranges for identifiers in the inner function, so it's the most ideal place to clean those up so they don't influence the outer function. * In InferMutableLifetimes, we need to ensure that context variable identifiers end up with a mutable range starting where they are declared, and ending with their last assignment. We now track declarations and extend their mutable range to account for each reassignment.

Joe Savona committed Mar 26, 2024 at 13:29 UTC 91f7bc8be7837ded7664220a30e447a8aae5b87b
8 files changed +122 -67
compiler/packages/babel-plugin-react-forget/src/HIR/PrintHIR.ts
+8 -4
@@ -633,10 +633,14 @@ function isMutable(range: MutableRange): boolean {
633 }
634
635 function printMutableRange(identifier: Identifier): string {
636 - const range =
637 - identifier.scope !== null
638 - ? identifier.scope.range
639 - : identifier.mutableRange;
636 + const range = identifier.mutableRange;
637 + const scopeRange = identifier.scope?.range;
638 + if (
639 + scopeRange != null &&
640 + (scopeRange.start !== range.start || scopeRange.end !== range.end)
641 + ) {
642 + return `[${range.start}:${range.end}] scope=[${scopeRange.start}:${scopeRange.end}]`;
643 + }
644 return isMutable(range) ? `[${range.start}:${range.end}]` : "";
645 }
646
compiler/packages/babel-plugin-react-forget/src/HIR/visitors.ts
+2 -1
@@ -32,8 +32,9 @@ export function* eachInstructionValueLValue(
32 value: ReactiveValue
33 ): Iterable<Place> {
34 switch (value.kind) {
35 - case "DeclareLocal":
35 case "DeclareContext":
36 + case "StoreContext":
37 + case "DeclareLocal":
38 case "StoreLocal": {
39 yield value.lvalue.place;
40 break;
compiler/packages/babel-plugin-react-forget/src/Inference/AnalyseFunctions.ts
+7 -1
@@ -16,6 +16,7 @@ import {
16 ReactiveScopeDependency,
17 isRefValueType,
18 isUseRefType,
19 + makeInstructionId,
20 } from "../HIR";
21 import { deadCodeElimination } from "../Optimization";
22 import { inferReactiveScopeVariables } from "../ReactiveScopes";
@@ -126,7 +127,6 @@ function infer(
127 ) {
128 mutations.set(operand.identifier.name.value, operand.effect);
129 }
129 - operand.identifier.mutableRange.end = operand.identifier.mutableRange.start;
130 }
131
132 for (const dep of loweredFunc.dependencies) {
@@ -178,6 +178,12 @@ function infer(
178 loweredFunc.dependencies.push(place);
179 }
180 }
181 +
182 + for (const operand of loweredFunc.func.context) {
183 + operand.identifier.mutableRange.start = makeInstructionId(0);
184 + operand.identifier.mutableRange.end = makeInstructionId(0);
185 + operand.identifier.scope = null;
186 + }
187 }
188
189 function isMutatedOrReassigned(id: Identifier): boolean {
compiler/packages/babel-plugin-react-forget/src/Inference/InferMutableLifetimes.ts
+40
@@ -8,7 +8,9 @@
8 import {
9 Effect,
10 HIRFunction,
11 + Identifier,
12 InstructionId,
13 + InstructionKind,
14 makeInstructionId,
15 Place,
16 } from "../HIR/HIR";
@@ -99,6 +101,15 @@ export function inferMutableLifetimes(
101 func: HIRFunction,
102 inferMutableRangeForStores: boolean
103 ): void {
104 + /*
105 + * Context variables only appear to mutate where they are assigned, but we need
106 + * to force their range to start at their declaration. Track the declaring instruction
107 + * id so that the ranges can be extended if/when they are reassigned
108 + */
109 + const contextVariableDeclarationInstructions = new Map<
110 + Identifier,
111 + InstructionId
112 + >();
113 for (const [_, block] of func.body.blocks) {
114 for (const phi of block.phis) {
115 for (const [_, operand] of phi.operands) {
@@ -146,6 +157,35 @@ export function inferMutableLifetimes(
157 for (const operand of eachInstructionOperand(instr)) {
158 inferPlace(operand, instr.id, inferMutableRangeForStores);
159 }
160 +
161 + if (
162 + instr.value.kind === "DeclareContext" ||
163 + (instr.value.kind === "StoreContext" &&
164 + instr.value.lvalue.kind !== InstructionKind.Reassign)
165 + ) {
166 + // Save declarations of context variables
167 + contextVariableDeclarationInstructions.set(
168 + instr.value.lvalue.place.identifier,
169 + instr.id
170 + );
171 + } else if (instr.value.kind === "StoreContext") {
172 + /*
173 + * Else this is a reassignment, extend the range from the declaration (if present).
174 + * Note that declarations may not be present for context variables that are reassigned
175 + * within a function expression before (or without) a read of the same variable
176 + */
177 + const declaration = contextVariableDeclarationInstructions.get(
178 + instr.value.lvalue.place.identifier
179 + );
180 + if (declaration != null) {
181 + const range = instr.value.lvalue.place.identifier.mutableRange;
182 + if (range.start === 0) {
183 + range.start = declaration;
184 + } else {
185 + range.start = makeInstructionId(Math.min(range.start, declaration));
186 + }
187 + }
188 + }
189 }
190 for (const operand of eachTerminalOperand(block.terminal)) {
191 inferPlace(operand, block.terminal.id, inferMutableRangeForStores);
compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/InferReactiveScopeVariables.ts
+36 -24
@@ -5,21 +5,21 @@
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 +import { CompilerError } from "..";
9 import { Environment } from "../HIR";
10 import {
11 + GeneratedSource,
12 HIRFunction,
13 Identifier,
14 Instruction,
13 - makeInstructionId,
15 Place,
16 ReactiveScope,
17 + makeInstructionId,
18 } from "../HIR/HIR";
19 import {
20 doesPatternContainSpreadElement,
19 - eachInstructionLValue,
21 eachInstructionOperand,
22 eachPatternOperand,
22 - eachTerminalOperand,
23 } from "../HIR/visitors";
24 import DisjointSet from "../Utils/DisjointSet";
25 import { assertExhaustive } from "../Utils/utils";
@@ -81,27 +81,6 @@ import { assertExhaustive } from "../Utils/utils";
81 * ```
82 */
83 export function inferReactiveScopeVariables(fn: HIRFunction): void {
84 - // First reset any scopes that may have been created from inner functions
85 - for (const [, block] of fn.body.blocks) {
86 - for (const phi of block.phis) {
87 - phi.id.scope = null;
88 - for (const [, operand] of phi.operands) {
89 - operand.scope = null;
90 - }
91 - }
92 - for (const instr of block.instructions) {
93 - for (const lvalue of eachInstructionLValue(instr)) {
94 - lvalue.identifier.scope = null;
95 - }
96 - for (const operand of eachInstructionOperand(instr)) {
97 - operand.identifier.scope = null;
98 - }
99 - }
100 - for (const operand of eachTerminalOperand(block.terminal)) {
101 - operand.identifier.scope = null;
102 - }
103 - }
104 -
84 /*
85 * Represents the set of reactive scopes as disjoint sets of identifiers
86 * that mutate together.
@@ -141,7 +120,40 @@ export function inferReactiveScopeVariables(fn: HIRFunction): void {
120 );
121 }
122 identifier.scope = scope;
123 + identifier.mutableRange = scope.range;
124 });
125 +
126 + let maxInstruction = 0;
127 + for (const [, block] of fn.body.blocks) {
128 + for (const instr of block.instructions) {
129 + maxInstruction = makeInstructionId(Math.max(maxInstruction, instr.id));
130 + }
131 + maxInstruction = makeInstructionId(
132 + Math.max(maxInstruction, block.terminal.id)
133 + );
134 + }
135 +
136 + /*
137 + * Validate that all scopes have properly intialized, valid mutable ranges
138 + * within the span of instructions for this function, ie from 1 to 1 past
139 + * the last instruction id.
140 + */
141 + for (const [, scope] of scopes) {
142 + if (
143 + scope.range.start === 0 ||
144 + scope.range.end === 0 ||
145 + maxInstruction === 0 ||
146 + scope.range.end > maxInstruction + 1
147 + ) {
148 + CompilerError.invariant(false, {
149 + reason: `Invalid mutable range for scope`,
150 + loc: GeneratedSource,
151 + description: `Scope @${scope.id} has range [${scope.range.start}:${
152 + scope.range.end
153 + }] but the valid range is [1:${maxInstruction + 1}]`,
154 + });
155 + }
156 + }
157 }
158
159 // Is the operand mutable at this given instruction
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/destructure-array-declaration-to-context-var.expect.md
+13 -14
@@ -27,32 +27,31 @@ import { unstable_useMemoCache as useMemoCache } from "react";
27 import { identity } from "shared-runtime";
28
29 function Component(props) {
30 - const $ = useMemoCache(5);
31 - const [t0] = props.value;
30 + const $ = useMemoCache(4);
31 let x;
33 - if ($[0] !== t0 || $[1] !== props.value) {
32 + if ($[0] !== props.value) {
33 + const [t0] = props.value;
34 x = t0;
35 const foo = () => {
36 x = identity(props.value[0]);
37 };
38
39 foo();
40 - $[0] = t0;
41 - $[1] = props.value;
42 - $[2] = x;
40 + $[0] = props.value;
41 + $[1] = x;
42 } else {
44 - x = $[2];
43 + x = $[1];
44 }
46 - const t1 = x;
47 - let t2;
48 - if ($[3] !== t1) {
49 - t2 = { x: t1 };
45 + const t0 = x;
46 + let t1;
47 + if ($[2] !== t0) {
48 + t1 = { x: t0 };
49 + $[2] = t0;
50 $[3] = t1;
51 - $[4] = t2;
51 } else {
53 - t2 = $[4];
52 + t1 = $[3];
53 }
55 - return t2;
54 + return t1;
55 }
56
57 export const FIXTURE_ENTRYPOINT = {
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/destructure-object-declaration-to-context-var.expect.md
+13 -14
@@ -27,32 +27,31 @@ import { unstable_useMemoCache as useMemoCache } from "react";
27 import { identity } from "shared-runtime";
28
29 function Component(props) {
30 - const $ = useMemoCache(5);
31 - const { x: t0 } = props;
30 + const $ = useMemoCache(4);
31 let x;
33 - if ($[0] !== t0 || $[1] !== props.x) {
32 + if ($[0] !== props) {
33 + const { x: t0 } = props;
34 x = t0;
35 const foo = () => {
36 x = identity(props.x);
37 };
38
39 foo();
40 - $[0] = t0;
41 - $[1] = props.x;
42 - $[2] = x;
40 + $[0] = props;
41 + $[1] = x;
42 } else {
44 - x = $[2];
43 + x = $[1];
44 }
46 - const t1 = x;
47 - let t2;
48 - if ($[3] !== t1) {
49 - t2 = { x: t1 };
45 + const t0 = x;
46 + let t1;
47 + if ($[2] !== t0) {
48 + t1 = { x: t0 };
49 + $[2] = t0;
50 $[3] = t1;
51 - $[4] = t2;
51 } else {
53 - t2 = $[4];
52 + t1 = $[3];
53 }
55 - return t2;
54 + return t1;
55 }
56
57 export const FIXTURE_ENTRYPOINT = {
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/hoisting-nested-const-declaration-2.expect.md
+3 -9
@@ -27,7 +27,7 @@ export const FIXTURE_ENTRYPOINT = {
27 ```javascript
28 import { unstable_useMemoCache as useMemoCache } from "react";
29 function hoisting(cond) {
30 - const $ = useMemoCache(3);
30 + const $ = useMemoCache(2);
31 let items;
32 if ($[0] !== cond) {
33 items = [];
@@ -35,14 +35,8 @@ function hoisting(cond) {
35 const foo = () => {
36 items.push(bar());
37 };
38 - let t0;
39 - if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
40 - t0 = () => true;
41 - $[2] = t0;
42 - } else {
43 - t0 = $[2];
44 - }
45 - const bar = t0;
38 +
39 + const bar = () => true;
40 foo();
41 }
42 $[0] = cond;