@samitouri / QOS-React-1 / commits / 86d1a6f54a

[compiler][rewrite] Patch logic for aligning scopes to non-value blocks

Our previous logic for aligning scopes to block scopes constructs a tree of block and scope nodes. We ensured that blocks always mapped to the same node as their fallthroughs. e.g. ```js // source a(); if (...) { b(); } c(); // HIR bb0: a() if test=... consequent=bb1 fallthrough=bb2 bb1: b() goto bb2 bb2: c() // AlignReactiveScopesToBlockScopesHIR nodes Root node (maps to both bb0 and bb2) |- bb1 |- ... ``` There are two issues with the existing implementation: 1. Only scopes that overlap with the beginning of a block are aligned correctly. This is because the traversal does not store information about the block-fallthrough pair for scopes that begin *within* the block-fallthrough range. ``` \# This case gets handled correctly ┌──────────────┐ │ │ block start block end scope start scope end │ │ └───────────────┘ \# But not this one! ┌──────────────┐ │ │ block start block end scope start scope end │ │ └───────────────┘ ``` 2. Only scopes that are directly used by a block is considered. See the `align-scopes-nested-block-structure` fixture for details. ghstack-source-id: 327dec5019483666f81c8156ac0c666ccad511b3 Pull Request resolved: https://github.com/facebook/react/pull/29891

Mofei Zhang committed Jun 25, 2024 at 16:03 UTC 86d1a6f54aebb2854aab0033bb57865add9f440d
29 files changed +841 -411
compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/AlignReactiveScopesToBlockScopesHIR.ts
+85 -77
@@ -22,8 +22,10 @@ import {
22 mapTerminalSuccessors,
23 terminalFallthrough,
24 } from "../HIR/visitors";
25 +import { retainWhere_Set } from "../Utils/utils";
26 import { getPlaceScope } from "./BuildReactiveBlocks";
27
28 +type InstructionRange = MutableRange;
29 /*
30 * Note: this is the 2nd of 4 passes that determine how to break a function into discrete
31 * reactive scopes (independently memoizeable units of code):
@@ -66,18 +68,20 @@ import { getPlaceScope } from "./BuildReactiveBlocks";
68 * will be the updated end for that scope).
69 */
70 export function alignReactiveScopesToBlockScopesHIR(fn: HIRFunction): void {
69 - const blockNodes = new Map<BlockId, BlockNode>();
70 - const rootNode: BlockNode = {
71 - kind: "node",
72 - valueRange: null,
73 - children: [],
74 - id: makeInstructionId(0),
75 - };
76 - blockNodes.set(fn.body.entry, rootNode);
71 + const activeBlockFallthroughRanges: Array<{
72 + range: InstructionRange;
73 + fallthrough: BlockId;
74 + }> = [];
75 + const activeScopes = new Set<ReactiveScope>();
76 const seen = new Set<ReactiveScope>();
77 + const valueBlockNodes = new Map<BlockId, ValueBlockNode>();
78 const placeScopes = new Map<Place, ReactiveScope>();
79
80 - function recordPlace(id: InstructionId, place: Place, node: BlockNode): void {
80 + function recordPlace(
81 + id: InstructionId,
82 + place: Place,
83 + node: ValueBlockNode | null
84 + ): void {
85 if (place.identifier.scope !== null) {
86 placeScopes.set(place, place.identifier.scope);
87 }
@@ -86,13 +90,14 @@ export function alignReactiveScopesToBlockScopesHIR(fn: HIRFunction): void {
90 if (scope == null) {
91 return;
92 }
89 - node.children.push({ kind: "scope", scope, id });
93 + activeScopes.add(scope);
94 + node?.children.push({ kind: "scope", scope, id });
95
96 if (seen.has(scope)) {
97 return;
98 }
99 seen.add(scope);
95 - if (node.valueRange !== null) {
100 + if (node != null && node.valueRange !== null) {
101 scope.range.start = makeInstructionId(
102 Math.min(node.valueRange.start, scope.range.start)
103 );
@@ -103,16 +108,25 @@ export function alignReactiveScopesToBlockScopesHIR(fn: HIRFunction): void {
108 }
109
110 for (const [, block] of fn.body.blocks) {
106 - const { instructions, terminal } = block;
107 - const node = blockNodes.get(block.id);
108 - if (node === undefined) {
109 - CompilerError.invariant(false, {
110 - reason: `Expected a node to be initialized for block`,
111 - loc: instructions[0]?.loc ?? terminal.loc,
112 - description: `No node for block bb${block.id} (${block.kind})`,
113 - });
111 + const startingId = block.instructions[0]?.id ?? block.terminal.id;
112 + retainWhere_Set(activeScopes, (scope) => scope.range.end > startingId);
113 + const top = activeBlockFallthroughRanges.at(-1);
114 + if (top?.fallthrough === block.id) {
115 + activeBlockFallthroughRanges.pop();
116 + /*
117 + * All active scopes must have either started before or within the last
118 + * block-fallthrough range. In either case, they overlap this block-
119 + * fallthrough range and can have their ranges extended.
120 + */
121 + for (const scope of activeScopes) {
122 + scope.range.start = makeInstructionId(
123 + Math.min(scope.range.start, top.range.start)
124 + );
125 + }
126 }
127
128 + const { instructions, terminal } = block;
129 + const node = valueBlockNodes.get(block.id) ?? null;
130 for (const instr of instructions) {
131 for (const lvalue of eachInstructionLValue(instr)) {
132 recordPlace(instr.id, lvalue, node);
@@ -125,36 +139,42 @@ export function alignReactiveScopesToBlockScopesHIR(fn: HIRFunction): void {
139 recordPlace(terminal.id, operand, node);
140 }
141
128 - // Save the current node for the fallback block, where this block scope continues
142 const fallthrough = terminalFallthrough(terminal);
130 - if (fallthrough !== null && !blockNodes.has(fallthrough)) {
143 + if (fallthrough !== null) {
144 /*
132 - * Any scopes that carried over across a terminal->fallback need their range extended
133 - * to at least the first instruction of the fallback
134 - *
135 - * Note that it's possible for a terminal such as an if or switch to have a null fallback,
136 - * indicating that all control-flow paths diverge instead of reaching the fallthrough.
137 - * In this case there isn't an instruction id in the program that we can point to for the
138 - * updated range. Since the output is correct in this case we leave it, but it would be
139 - * more correct to find the maximum instuction id in the whole program and set the range.end
140 - * to one greater. Alternatively, we could leave in an unreachable fallthrough (with a new
141 - * "unreachable" terminal variant, perhaps) and use that instruction id.
145 + * Any currently active scopes that overlaps the block-fallthrough range
146 + * need their range extended to at least the first instruction of the
147 + * fallthrough
148 */
149 const fallthroughBlock = fn.body.blocks.get(fallthrough)!;
150 const nextId =
151 fallthroughBlock.instructions[0]?.id ?? fallthroughBlock.terminal.id;
146 - for (const child of node.children) {
147 - if (child.kind !== "scope") {
148 - continue;
149 - }
150 - const scope = child.scope;
152 + for (const scope of activeScopes) {
153 if (scope.range.end > terminal.id) {
154 scope.range.end = makeInstructionId(
155 Math.max(scope.range.end, nextId)
156 );
157 }
158 }
157 - blockNodes.set(fallthrough, node);
159 + /**
160 + * We also record the block-fallthrough range for future scopes that begin
161 + * within the range (and overlap with the range end).
162 + */
163 + activeBlockFallthroughRanges.push({
164 + fallthrough,
165 + range: {
166 + start: terminal.id,
167 + end: nextId,
168 + },
169 + });
170 +
171 + CompilerError.invariant(!valueBlockNodes.has(fallthrough), {
172 + reason: "Expect hir blocks to have unique fallthroughs",
173 + loc: terminal.loc,
174 + });
175 + if (node != null) {
176 + valueBlockNodes.set(fallthrough, node);
177 + }
178 }
179
180 /*
@@ -166,48 +186,35 @@ export function alignReactiveScopesToBlockScopesHIR(fn: HIRFunction): void {
186 * just those that are direct successors for normal control-flow ordering.
187 */
188 mapTerminalSuccessors(terminal, (successor) => {
169 - if (blockNodes.has(successor)) {
189 + if (valueBlockNodes.has(successor)) {
190 return successor;
191 }
192
193 const successorBlock = fn.body.blocks.get(successor)!;
174 - /*
175 - * we need the block kind check here because the do..while terminal's successor
176 - * is a block, and try's successor is a catch block
177 - */
194 if (successorBlock.kind === "block" || successorBlock.kind === "catch") {
179 - const childNode: BlockNode = {
180 - kind: "node",
181 - id: terminal.id,
182 - children: [],
183 - valueRange: null,
184 - };
185 - node.children.push(childNode);
186 - blockNodes.set(successor, childNode);
195 + /*
196 + * we need the block kind check here because the do..while terminal's
197 + * successor is a block, and try's successor is a catch block
198 + */
199 } else if (
188 - node.valueRange === null ||
200 + node == null ||
201 terminal.kind === "ternary" ||
202 terminal.kind === "logical" ||
203 terminal.kind === "optional"
204 ) {
205 /**
194 - * Create a new scope node whenever we transition from block scope -> value scope.
206 + * Create a new node whenever we transition from non-value -> value block.
207 *
208 * For compatibility with the previous ReactiveFunction-based scope merging logic,
209 * we also create new scope nodes for ternary, logical, and optional terminals.
198 - * However, inside value blocks we always store a range (valueRange) that is the
210 + * Inside value blocks we always store a range (valueRange) that is the
211 * start/end instruction ids at the nearest parent block scope level, so that
212 * scopes inside the value blocks can be extended to align with block scope
213 * instructions.
214 */
203 - const childNode = {
204 - kind: "node",
205 - id: terminal.id,
206 - children: [],
207 - valueRange: null,
208 - } as BlockNode;
209 - if (node.valueRange === null) {
210 - // Transition from block->value scope, derive the outer block scope range
215 + let valueRange: MutableRange;
216 + if (node == null) {
217 + // Transition from block->value block, derive the outer block range
218 CompilerError.invariant(fallthrough !== null, {
219 reason: `Expected a fallthrough for value block`,
220 loc: terminal.loc,
@@ -216,32 +223,36 @@ export function alignReactiveScopesToBlockScopesHIR(fn: HIRFunction): void {
223 const nextId =
224 fallthroughBlock.instructions[0]?.id ??
225 fallthroughBlock.terminal.id;
219 - childNode.valueRange = {
226 + valueRange = {
227 start: terminal.id,
228 end: nextId,
229 };
230 } else {
231 // else value->value transition, reuse the range
225 - childNode.valueRange = node.valueRange;
232 + valueRange = node.valueRange;
233 }
227 - node.children.push(childNode);
228 - blockNodes.set(successor, childNode);
234 + const childNode: ValueBlockNode = {
235 + kind: "node",
236 + id: terminal.id,
237 + children: [],
238 + valueRange,
239 + };
240 + node?.children.push(childNode);
241 + valueBlockNodes.set(successor, childNode);
242 } else {
243 // this is a value -> value block transition, reuse the node
231 - blockNodes.set(successor, node);
244 + valueBlockNodes.set(successor, node);
245 }
246 return successor;
247 });
248 }
236 -
237 - // console.log(_debug(rootNode));
249 }
250
240 -type BlockNode = {
251 +type ValueBlockNode = {
252 kind: "node";
253 id: InstructionId;
243 - valueRange: MutableRange | null;
244 - children: Array<BlockNode | ReactiveScopeNode>;
254 + valueRange: MutableRange;
255 + children: Array<ValueBlockNode | ReactiveScopeNode>;
256 };
257 type ReactiveScopeNode = {
258 kind: "scope";
@@ -249,13 +260,13 @@ type ReactiveScopeNode = {
260 scope: ReactiveScope;
261 };
262
252 -function _debug(node: BlockNode): string {
263 +function _debug(node: ValueBlockNode): string {
264 const buf: Array<string> = [];
265 _printNode(node, buf, 0);
266 return buf.join("\n");
267 }
268 function _printNode(
258 - node: BlockNode | ReactiveScopeNode,
269 + node: ValueBlockNode | ReactiveScopeNode,
270 out: Array<string>,
271 depth: number = 0
272 ): void {
@@ -265,10 +276,7 @@ function _printNode(
276 `${prefix}[${node.id}] @${node.scope.id} [${node.scope.range.start}:${node.scope.range.end}]`
277 );
278 } else {
268 - let range =
269 - node.valueRange !== null
270 - ? ` [${node.valueRange.start}:${node.valueRange.end}]`
271 - : "";
279 + let range = ` (range=[${node.valueRange.start}:${node.valueRange.end}])`;
280 out.push(`${prefix}[${node.id}] node${range} [`);
281 for (const child of node.children) {
282 _printNode(child, out, depth + 1);
compiler/packages/babel-plugin-react-compiler/src/Utils/utils.ts
+11
@@ -45,6 +45,17 @@ export function retainWhere<T>(
45 array.length = writeIndex;
46 }
47
48 +export function retainWhere_Set<T>(
49 + items: Set<T>,
50 + predicate: (item: T) => boolean
51 +): void {
52 + for (const item of items) {
53 + if (!predicate(item)) {
54 + items.delete(item);
55 + }
56 + }
57 +}
58 +
59 export function getOrInsertWith<U, V>(
60 m: Map<U, V>,
61 key: U,
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scope-starts-within-cond.expect.md new
+76
@@ -0,0 +1,76 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import { mutate } from "shared-runtime";
6 +
7 +/**
8 + * Similar fixture to `align-scopes-nested-block-structure`, but
9 + * a simpler case.
10 + */
11 +function useFoo(cond) {
12 + let s = null;
13 + if (cond) {
14 + s = {};
15 + } else {
16 + return null;
17 + }
18 + mutate(s);
19 + return s;
20 +}
21 +
22 +export const FIXTURE_ENTRYPOINT = {
23 + fn: useFoo,
24 + params: [true],
25 +};
26 +
27 +```
28 +
29 +## Code
30 +
31 +```javascript
32 +import { c as _c } from "react/compiler-runtime";
33 +import { mutate } from "shared-runtime";
34 +
35 +/**
36 + * Similar fixture to `align-scopes-nested-block-structure`, but
37 + * a simpler case.
38 + */
39 +function useFoo(cond) {
40 + const $ = _c(3);
41 + let s;
42 + let t0;
43 + if ($[0] !== cond) {
44 + t0 = Symbol.for("react.early_return_sentinel");
45 + bb0: {
46 + if (cond) {
47 + s = {};
48 + } else {
49 + t0 = null;
50 + break bb0;
51 + }
52 +
53 + mutate(s);
54 + }
55 + $[0] = cond;
56 + $[1] = t0;
57 + $[2] = s;
58 + } else {
59 + t0 = $[1];
60 + s = $[2];
61 + }
62 + if (t0 !== Symbol.for("react.early_return_sentinel")) {
63 + return t0;
64 + }
65 + return s;
66 +}
67 +
68 +export const FIXTURE_ENTRYPOINT = {
69 + fn: useFoo,
70 + params: [true],
71 +};
72 +
73 +```
74 +
75 +### Eval output
76 +(kind: ok) {"wat0":"joe"}
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scope-starts-within-cond.ts new
+21
@@ -0,0 +1,21 @@
1 +import { mutate } from "shared-runtime";
2 +
3 +/**
4 + * Similar fixture to `align-scopes-nested-block-structure`, but
5 + * a simpler case.
6 + */
7 +function useFoo(cond) {
8 + let s = null;
9 + if (cond) {
10 + s = {};
11 + } else {
12 + return null;
13 + }
14 + mutate(s);
15 + return s;
16 +}
17 +
18 +export const FIXTURE_ENTRYPOINT = {
19 + fn: useFoo,
20 + params: [true],
21 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scopes-iife-return-modified-later-logical.expect.md new
+55
@@ -0,0 +1,55 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import { getNull } from "shared-runtime";
6 +
7 +function Component(props) {
8 + const items = (() => {
9 + return getNull() ?? [];
10 + })();
11 + items.push(props.a);
12 + return items;
13 +}
14 +
15 +export const FIXTURE_ENTRYPOINT = {
16 + fn: Component,
17 + params: [{ a: {} }],
18 +};
19 +
20 +```
21 +
22 +## Code
23 +
24 +```javascript
25 +import { c as _c } from "react/compiler-runtime";
26 +import { getNull } from "shared-runtime";
27 +
28 +function Component(props) {
29 + const $ = _c(3);
30 + let t0;
31 + let items;
32 + if ($[0] !== props.a) {
33 + t0 = getNull() ?? [];
34 + items = t0;
35 +
36 + items.push(props.a);
37 + $[0] = props.a;
38 + $[1] = items;
39 + $[2] = t0;
40 + } else {
41 + items = $[1];
42 + t0 = $[2];
43 + }
44 + return items;
45 +}
46 +
47 +export const FIXTURE_ENTRYPOINT = {
48 + fn: Component,
49 + params: [{ a: {} }],
50 +};
51 +
52 +```
53 +
54 +### Eval output
55 +(kind: ok) [{}]
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scopes-iife-return-modified-later-logical.ts renamed
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scopes-nested-block-structure.expect.md new
+169
@@ -0,0 +1,169 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import { mutate } from "shared-runtime";
6 +/**
7 + * Fixture showing that it's not sufficient to only align direct scoped
8 + * accesses of a block-fallthrough pair.
9 + * Below is a simplified view of HIR blocks in this fixture.
10 + * Note that here, s is mutated in both bb1 and bb4. However, neither
11 + * bb1 nor bb4 have terminal fallthroughs or are fallthroughs themselves.
12 + *
13 + * This means that we need to recursively visit all scopes accessed between
14 + * a block and its fallthrough and extend the range of those scopes which overlap
15 + * with an active block/fallthrough pair,
16 + *
17 + * bb0
18 + * ┌──────────────┐
19 + * │let s = null │
20 + * │test cond1 │
21 + * │ <fallthr=bb3>│
22 + * └┬─────────────┘
23 + * │ bb1
24 + * ├─►┌───────┐
25 + * │ │s = {} ├────┐
26 + * │ └───────┘ │
27 + * │ bb2 │
28 + * └─►┌───────┐ │
29 + * │return;│ │
30 + * └───────┘ │
31 + * bb3 │
32 + * ┌──────────────┐◄┘
33 + * │test cond2 │
34 + * │ <fallthr=bb5>│
35 + * └┬─────────────┘
36 + * │ bb4
37 + * ├─►┌─────────┐
38 + * │ │mutate(s)├─┐
39 + * ▼ └─────────┘ │
40 + * bb5 │
41 + * ┌───────────┐ │
42 + * │return s; │◄──┘
43 + * └───────────┘
44 + */
45 +function useFoo({ cond1, cond2 }) {
46 + let s = null;
47 + if (cond1) {
48 + s = {};
49 + } else {
50 + return null;
51 + }
52 +
53 + if (cond2) {
54 + mutate(s);
55 + }
56 +
57 + return s;
58 +}
59 +
60 +export const FIXTURE_ENTRYPOINT = {
61 + fn: useFoo,
62 + params: [{ cond1: true, cond2: false }],
63 + sequentialRenders: [
64 + { cond1: true, cond2: false },
65 + { cond1: true, cond2: false },
66 + { cond1: true, cond2: true },
67 + { cond1: true, cond2: true },
68 + { cond1: false, cond2: true },
69 + ],
70 +};
71 +
72 +```
73 +
74 +## Code
75 +
76 +```javascript
77 +import { c as _c } from "react/compiler-runtime";
78 +import { mutate } from "shared-runtime";
79 +/**
80 + * Fixture showing that it's not sufficient to only align direct scoped
81 + * accesses of a block-fallthrough pair.
82 + * Below is a simplified view of HIR blocks in this fixture.
83 + * Note that here, s is mutated in both bb1 and bb4. However, neither
84 + * bb1 nor bb4 have terminal fallthroughs or are fallthroughs themselves.
85 + *
86 + * This means that we need to recursively visit all scopes accessed between
87 + * a block and its fallthrough and extend the range of those scopes which overlap
88 + * with an active block/fallthrough pair,
89 + *
90 + * bb0
91 + * ┌──────────────┐
92 + * │let s = null │
93 + * │test cond1 │
94 + * │ <fallthr=bb3>│
95 + * └┬─────────────┘
96 + * │ bb1
97 + * ├─►┌───────┐
98 + * │ │s = {} ├────┐
99 + * │ └───────┘ │
100 + * │ bb2 │
101 + * └─►┌───────┐ │
102 + * │return;│ │
103 + * └───────┘ │
104 + * bb3 │
105 + * ┌──────────────┐◄┘
106 + * │test cond2 │
107 + * │ <fallthr=bb5>│
108 + * └┬─────────────┘
109 + * │ bb4
110 + * ├─►┌─────────┐
111 + * │ │mutate(s)├─┐
112 + * ▼ └─────────┘ │
113 + * bb5 │
114 + * ┌───────────┐ │
115 + * │return s; │◄──┘
116 + * └───────────┘
117 + */
118 +function useFoo(t0) {
119 + const $ = _c(4);
120 + const { cond1, cond2 } = t0;
121 + let s;
122 + let t1;
123 + if ($[0] !== cond1 || $[1] !== cond2) {
124 + t1 = Symbol.for("react.early_return_sentinel");
125 + bb0: {
126 + if (cond1) {
127 + s = {};
128 + } else {
129 + t1 = null;
130 + break bb0;
131 + }
132 + if (cond2) {
133 + mutate(s);
134 + }
135 + }
136 + $[0] = cond1;
137 + $[1] = cond2;
138 + $[2] = t1;
139 + $[3] = s;
140 + } else {
141 + t1 = $[2];
142 + s = $[3];
143 + }
144 + if (t1 !== Symbol.for("react.early_return_sentinel")) {
145 + return t1;
146 + }
147 + return s;
148 +}
149 +
150 +export const FIXTURE_ENTRYPOINT = {
151 + fn: useFoo,
152 + params: [{ cond1: true, cond2: false }],
153 + sequentialRenders: [
154 + { cond1: true, cond2: false },
155 + { cond1: true, cond2: false },
156 + { cond1: true, cond2: true },
157 + { cond1: true, cond2: true },
158 + { cond1: false, cond2: true },
159 + ],
160 +};
161 +
162 +```
163 +
164 +### Eval output
165 +(kind: ok) {}
166 +{}
167 +{"wat0":"joe"}
168 +{"wat0":"joe"}
169 +null
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scopes-nested-block-structure.ts renamed
+13 -15
@@ -1,7 +1,4 @@
1 -
2 -## Input
3 -
4 -```javascript
1 +import { mutate } from "shared-runtime";
2 /**
3 * Fixture showing that it's not sufficient to only align direct scoped
4 * accesses of a block-fallthrough pair.
@@ -41,7 +38,7 @@
38 * │return s; │◄──┘
39 * └───────────┘
40 */
44 -function useFoo(cond1, cond2) {
41 +function useFoo({ cond1, cond2 }) {
42 let s = null;
43 if (cond1) {
44 s = {};
@@ -56,13 +53,14 @@ function useFoo(cond1, cond2) {
53 return s;
54 }
55
59 -```
60 -
61 -
62 -## Error
63 -
64 -```
65 -Invariant: Invalid nesting in program blocks or scopes. Items overlap but are not nested: 4:10(5:15)
66 -```
67 -
68 -
\ No newline at end of file
56 +export const FIXTURE_ENTRYPOINT = {
57 + fn: useFoo,
58 + params: [{ cond1: true, cond2: false }],
59 + sequentialRenders: [
60 + { cond1: true, cond2: false },
61 + { cond1: true, cond2: false },
62 + { cond1: true, cond2: true },
63 + { cond1: true, cond2: true },
64 + { cond1: false, cond2: true },
65 + ],
66 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scopes-reactive-scope-overlaps-if.expect.md new
+84
@@ -0,0 +1,84 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function useFoo({ cond }) {
6 + let items: any = {};
7 + b0: {
8 + if (cond) {
9 + // Mutable range of `items` begins here, but its reactive scope block
10 + // should be aligned to above the if-branch
11 + items = [];
12 + } else {
13 + break b0;
14 + }
15 + items.push(2);
16 + }
17 + return items;
18 +}
19 +
20 +export const FIXTURE_ENTRYPOINT = {
21 + fn: useFoo,
22 + params: [{ cond: true }],
23 + sequentialRenders: [
24 + { cond: true },
25 + { cond: true },
26 + { cond: false },
27 + { cond: false },
28 + { cond: true },
29 + ],
30 +};
31 +
32 +```
33 +
34 +## Code
35 +
36 +```javascript
37 +import { c as _c } from "react/compiler-runtime";
38 +function useFoo(t0) {
39 + const $ = _c(3);
40 + const { cond } = t0;
41 + let t1;
42 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
43 + t1 = {};
44 + $[0] = t1;
45 + } else {
46 + t1 = $[0];
47 + }
48 + let items = t1;
49 + bb0: if ($[1] !== cond) {
50 + if (cond) {
51 + items = [];
52 + } else {
53 + break bb0;
54 + }
55 +
56 + items.push(2);
57 + $[1] = cond;
58 + $[2] = items;
59 + } else {
60 + items = $[2];
61 + }
62 + return items;
63 +}
64 +
65 +export const FIXTURE_ENTRYPOINT = {
66 + fn: useFoo,
67 + params: [{ cond: true }],
68 + sequentialRenders: [
69 + { cond: true },
70 + { cond: true },
71 + { cond: false },
72 + { cond: false },
73 + { cond: true },
74 + ],
75 +};
76 +
77 +```
78 +
79 +### Eval output
80 +(kind: ok) [2]
81 +[2]
82 +{}
83 +{}
84 +[2]
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scopes-reactive-scope-overlaps-if.ts renamed
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scopes-reactive-scope-overlaps-label.expect.md new
+79
@@ -0,0 +1,79 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import { arrayPush } from "shared-runtime";
6 +
7 +function useFoo({ cond, value }) {
8 + let items;
9 + label: {
10 + items = [];
11 + // Mutable range of `items` begins here, but its reactive scope block
12 + // should be aligned to above the label-block
13 + if (cond) break label;
14 + arrayPush(items, value);
15 + }
16 + arrayPush(items, value);
17 + return items;
18 +}
19 +
20 +export const FIXTURE_ENTRYPOINT = {
21 + fn: useFoo,
22 + params: [{ cond: true, value: 2 }],
23 + sequentialRenders: [
24 + { cond: true, value: 2 },
25 + { cond: true, value: 2 },
26 + { cond: true, value: 3 },
27 + { cond: false, value: 3 },
28 + ],
29 +};
30 +
31 +```
32 +
33 +## Code
34 +
35 +```javascript
36 +import { c as _c } from "react/compiler-runtime";
37 +import { arrayPush } from "shared-runtime";
38 +
39 +function useFoo(t0) {
40 + const $ = _c(3);
41 + const { cond, value } = t0;
42 + let items;
43 + if ($[0] !== cond || $[1] !== value) {
44 + bb0: {
45 + items = [];
46 + if (cond) {
47 + break bb0;
48 + }
49 + arrayPush(items, value);
50 + }
51 +
52 + arrayPush(items, value);
53 + $[0] = cond;
54 + $[1] = value;
55 + $[2] = items;
56 + } else {
57 + items = $[2];
58 + }
59 + return items;
60 +}
61 +
62 +export const FIXTURE_ENTRYPOINT = {
63 + fn: useFoo,
64 + params: [{ cond: true, value: 2 }],
65 + sequentialRenders: [
66 + { cond: true, value: 2 },
67 + { cond: true, value: 2 },
68 + { cond: true, value: 3 },
69 + { cond: false, value: 3 },
70 + ],
71 +};
72 +
73 +```
74 +
75 +### Eval output
76 +(kind: ok) [2]
77 +[2]
78 +[3]
79 +[3,3]
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scopes-reactive-scope-overlaps-label.ts renamed
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scopes-reactive-scope-overlaps-try.expect.md new
+65
@@ -0,0 +1,65 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import { arrayPush, mutate } from "shared-runtime";
6 +
7 +function useFoo({ value }) {
8 + let items = null;
9 + try {
10 + // Mutable range of `items` begins here, but its reactive scope block
11 + // should be aligned to above the try-block
12 + items = [];
13 + arrayPush(items, value);
14 + } catch {
15 + // ignore
16 + }
17 + mutate(items);
18 + return items;
19 +}
20 +
21 +export const FIXTURE_ENTRYPOINT = {
22 + fn: useFoo,
23 + params: [{ value: 2 }],
24 + sequentialRenders: [{ value: 2 }, { value: 2 }, { value: 3 }],
25 +};
26 +
27 +```
28 +
29 +## Code
30 +
31 +```javascript
32 +import { c as _c } from "react/compiler-runtime";
33 +import { arrayPush, mutate } from "shared-runtime";
34 +
35 +function useFoo(t0) {
36 + const $ = _c(2);
37 + const { value } = t0;
38 + let items;
39 + if ($[0] !== value) {
40 + try {
41 + items = [];
42 + arrayPush(items, value);
43 + } catch {}
44 +
45 + mutate(items);
46 + $[0] = value;
47 + $[1] = items;
48 + } else {
49 + items = $[1];
50 + }
51 + return items;
52 +}
53 +
54 +export const FIXTURE_ENTRYPOINT = {
55 + fn: useFoo,
56 + params: [{ value: 2 }],
57 + sequentialRenders: [{ value: 2 }, { value: 2 }, { value: 3 }],
58 +};
59 +
60 +```
61 +
62 +### Eval output
63 +(kind: ok) [2]
64 +[2]
65 +[3]
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scopes-reactive-scope-overlaps-try.ts renamed
+1 -1
@@ -1,4 +1,4 @@
1 -import { arrayPush } from "shared-runtime";
1 +import { arrayPush, mutate } from "shared-runtime";
2
3 function useFoo({ value }) {
4 let items = null;
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scopes-trycatch-nested-overlapping-range.expect.md new
+61
@@ -0,0 +1,61 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import { CONST_TRUE, makeObject_Primitives } from "shared-runtime";
6 +
7 +function Foo() {
8 + try {
9 + let thing = null;
10 + if (cond) {
11 + thing = makeObject_Primitives();
12 + }
13 + if (CONST_TRUE) {
14 + mutate(thing);
15 + }
16 + return thing;
17 + } catch {}
18 +}
19 +
20 +export const FIXTURE_ENTRYPOINT = {
21 + fn: Foo,
22 + params: [{}],
23 +};
24 +
25 +```
26 +
27 +## Code
28 +
29 +```javascript
30 +import { c as _c } from "react/compiler-runtime";
31 +import { CONST_TRUE, makeObject_Primitives } from "shared-runtime";
32 +
33 +function Foo() {
34 + const $ = _c(1);
35 + try {
36 + let thing;
37 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
38 + thing = null;
39 + if (cond) {
40 + thing = makeObject_Primitives();
41 + }
42 + if (CONST_TRUE) {
43 + mutate(thing);
44 + }
45 + $[0] = thing;
46 + } else {
47 + thing = $[0];
48 + }
49 + return thing;
50 + } catch {}
51 +}
52 +
53 +export const FIXTURE_ENTRYPOINT = {
54 + fn: Foo,
55 + params: [{}],
56 +};
57 +
58 +```
59 +
60 +### Eval output
61 +(kind: ok)
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scopes-trycatch-nested-overlapping-range.ts new
+19
@@ -0,0 +1,19 @@
1 +import { CONST_TRUE, makeObject_Primitives } from "shared-runtime";
2 +
3 +function Foo() {
4 + try {
5 + let thing = null;
6 + if (cond) {
7 + thing = makeObject_Primitives();
8 + }
9 + if (CONST_TRUE) {
10 + mutate(thing);
11 + }
12 + return thing;
13 + } catch {}
14 +}
15 +
16 +export const FIXTURE_ENTRYPOINT = {
17 + fn: Foo,
18 + params: [{}],
19 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-repro-trycatch-nested-overlapping-range.expect.md deleted
-26
@@ -1,26 +0,0 @@
1 -
2 -## Input
3 -
4 -```javascript
5 -function Foo() {
6 - try {
7 - let thing = null;
8 - if (cond) {
9 - thing = makeObject();
10 - }
11 - if (otherCond) {
12 - mutate(thing);
13 - }
14 - } catch {}
15 -}
16 -
17 -```
18 -
19 -
20 -## Error
21 -
22 -```
23 -Invariant: Invalid nesting in program blocks or scopes. Items overlap but are not nested: 2:24(18:26)
24 -```
25 -
26 -
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-repro-trycatch-nested-overlapping-range.js deleted
-11
@@ -1,11 +0,0 @@
1 -function Foo() {
2 - try {
3 - let thing = null;
4 - if (cond) {
5 - thing = makeObject();
6 - }
7 - if (otherCond) {
8 - mutate(thing);
9 - }
10 - } catch {}
11 -}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.repro-bug-ref-mutable-range.expect.md deleted
-27
@@ -1,27 +0,0 @@
1 -
2 -## Input
3 -
4 -```javascript
5 -function Foo(props, ref) {
6 - const value = {};
7 - if (cond1) {
8 - mutate(value);
9 - return <Child ref={ref} />;
10 - }
11 - mutate(value);
12 - if (cond2) {
13 - return <Child ref={identity(ref)} />;
14 - }
15 - return value;
16 -}
17 -
18 -```
19 -
20 -
21 -## Error
22 -
23 -```
24 -Invariant: Invalid nesting in program blocks or scopes. Items overlap but are not nested: 1:21(16:23)
25 -```
26 -
27 -
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.repro-bug-ref-mutable-range.js deleted
-12
@@ -1,12 +0,0 @@
1 -function Foo(props, ref) {
2 - const value = {};
3 - if (cond1) {
4 - mutate(value);
5 - return <Child ref={ref} />;
6 - }
7 - mutate(value);
8 - if (cond2) {
9 - return <Child ref={identity(ref)} />;
10 - }
11 - return value;
12 -}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-align-scope-starts-within-cond.expect.md deleted
-29
@@ -1,29 +0,0 @@
1 -
2 -## Input
3 -
4 -```javascript
5 -/**
6 - * Similar fixture to `error.todo-align-scopes-nested-block-structure`, but
7 - * a simpler case.
8 - */
9 -function useFoo(cond) {
10 - let s = null;
11 - if (cond) {
12 - s = {};
13 - } else {
14 - return null;
15 - }
16 - mutate(s);
17 - return s;
18 -}
19 -
20 -```
21 -
22 -
23 -## Error
24 -
25 -```
26 -Invariant: Invalid nesting in program blocks or scopes. Items overlap but are not nested: 4:10(5:13)
27 -```
28 -
29 -
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-align-scope-starts-within-cond.ts deleted
-14
@@ -1,14 +0,0 @@
1 -/**
2 - * Similar fixture to `error.todo-align-scopes-nested-block-structure`, but
3 - * a simpler case.
4 - */
5 -function useFoo(cond) {
6 - let s = null;
7 - if (cond) {
8 - s = {};
9 - } else {
10 - return null;
11 - }
12 - mutate(s);
13 - return s;
14 -}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-align-scopes-nested-block-structure.ts deleted
-53
@@ -1,53 +0,0 @@
1 -/**
2 - * Fixture showing that it's not sufficient to only align direct scoped
3 - * accesses of a block-fallthrough pair.
4 - * Below is a simplified view of HIR blocks in this fixture.
5 - * Note that here, s is mutated in both bb1 and bb4. However, neither
6 - * bb1 nor bb4 have terminal fallthroughs or are fallthroughs themselves.
7 - *
8 - * This means that we need to recursively visit all scopes accessed between
9 - * a block and its fallthrough and extend the range of those scopes which overlap
10 - * with an active block/fallthrough pair,
11 - *
12 - * bb0
13 - * ┌──────────────┐
14 - * │let s = null │
15 - * │test cond1 │
16 - * │ <fallthr=bb3>│
17 - * └┬─────────────┘
18 - * │ bb1
19 - * ├─►┌───────┐
20 - * │ │s = {} ├────┐
21 - * │ └───────┘ │
22 - * │ bb2 │
23 - * └─►┌───────┐ │
24 - * │return;│ │
25 - * └───────┘ │
26 - * bb3 │
27 - * ┌──────────────┐◄┘
28 - * │test cond2 │
29 - * │ <fallthr=bb5>│
30 - * └┬─────────────┘
31 - * │ bb4
32 - * ├─►┌─────────┐
33 - * │ │mutate(s)├─┐
34 - * ▼ └─────────┘ │
35 - * bb5 │
36 - * ┌───────────┐ │
37 - * │return s; │◄──┘
38 - * └───────────┘
39 - */
40 -function useFoo(cond1, cond2) {
41 - let s = null;
42 - if (cond1) {
43 - s = {};
44 - } else {
45 - return null;
46 - }
47 -
48 - if (cond2) {
49 - mutate(s);
50 - }
51 -
52 - return s;
53 -}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-iife-return-modified-later-logical.expect.md deleted
-29
@@ -1,29 +0,0 @@
1 -
2 -## Input
3 -
4 -```javascript
5 -import { getNull } from "shared-runtime";
6 -
7 -function Component(props) {
8 - const items = (() => {
9 - return getNull() ?? [];
10 - })();
11 - items.push(props.a);
12 - return items;
13 -}
14 -
15 -export const FIXTURE_ENTRYPOINT = {
16 - fn: Component,
17 - params: [{ a: {} }],
18 -};
19 -
20 -```
21 -
22 -
23 -## Error
24 -
25 -```
26 -Invariant: Invalid nesting in program blocks or scopes. Items overlap but are not nested: 2:15(3:21)
27 -```
28 -
29 -
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-reactive-scope-overlaps-if.expect.md deleted
-41
@@ -1,41 +0,0 @@
1 -
2 -## Input
3 -
4 -```javascript
5 -function useFoo({ cond }) {
6 - let items: any = {};
7 - b0: {
8 - if (cond) {
9 - // Mutable range of `items` begins here, but its reactive scope block
10 - // should be aligned to above the if-branch
11 - items = [];
12 - } else {
13 - break b0;
14 - }
15 - items.push(2);
16 - }
17 - return items;
18 -}
19 -
20 -export const FIXTURE_ENTRYPOINT = {
21 - fn: useFoo,
22 - params: [{ cond: true }],
23 - sequentialRenders: [
24 - { cond: true },
25 - { cond: true },
26 - { cond: false },
27 - { cond: false },
28 - { cond: true },
29 - ],
30 -};
31 -
32 -```
33 -
34 -
35 -## Error
36 -
37 -```
38 -Invariant: Invalid nesting in program blocks or scopes. Items overlap but are not nested: 6:11(7:15)
39 -```
40 -
41 -
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-reactive-scope-overlaps-label.expect.md deleted
-40
@@ -1,40 +0,0 @@
1 -
2 -## Input
3 -
4 -```javascript
5 -import { arrayPush } from "shared-runtime";
6 -
7 -function useFoo({ cond, value }) {
8 - let items;
9 - label: {
10 - items = [];
11 - // Mutable range of `items` begins here, but its reactive scope block
12 - // should be aligned to above the label-block
13 - if (cond) break label;
14 - arrayPush(items, value);
15 - }
16 - arrayPush(items, value);
17 - return items;
18 -}
19 -
20 -export const FIXTURE_ENTRYPOINT = {
21 - fn: useFoo,
22 - params: [{ cond: true, value: 2 }],
23 - sequentialRenders: [
24 - { cond: true, value: 2 },
25 - { cond: true, value: 2 },
26 - { cond: true, value: 3 },
27 - { cond: false, value: 3 },
28 - ],
29 -};
30 -
31 -```
32 -
33 -
34 -## Error
35 -
36 -```
37 -Invariant: Invalid nesting in program blocks or scopes. Items overlap but are not nested: 3:14(4:18)
38 -```
39 -
40 -
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-reactive-scope-overlaps-try.expect.md deleted
-36
@@ -1,36 +0,0 @@
1 -
2 -## Input
3 -
4 -```javascript
5 -import { arrayPush } from "shared-runtime";
6 -
7 -function useFoo({ value }) {
8 - let items = null;
9 - try {
10 - // Mutable range of `items` begins here, but its reactive scope block
11 - // should be aligned to above the try-block
12 - items = [];
13 - arrayPush(items, value);
14 - } catch {
15 - // ignore
16 - }
17 - mutate(items);
18 - return items;
19 -}
20 -
21 -export const FIXTURE_ENTRYPOINT = {
22 - fn: useFoo,
23 - params: [{ value: 2 }],
24 - sequentialRenders: [{ value: 2 }, { value: 2 }, { value: 3 }],
25 -};
26 -
27 -```
28 -
29 -
30 -## Error
31 -
32 -```
33 -Invariant: Invalid nesting in program blocks or scopes. Items overlap but are not nested: 4:19(5:22)
34 -```
35 -
36 -
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-ref-mutable-range.expect.md new
+83
@@ -0,0 +1,83 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import { Stringify, identity, mutate, CONST_TRUE } from "shared-runtime";
6 +
7 +function Foo(props, ref) {
8 + const value = {};
9 + if (CONST_TRUE) {
10 + mutate(value);
11 + return <Stringify ref={ref} />;
12 + }
13 + mutate(value);
14 + if (CONST_TRUE) {
15 + return <Stringify ref={identity(ref)} />;
16 + }
17 + return value;
18 +}
19 +
20 +export const FIXTURE_ENTRYPOINT = {
21 + fn: Foo,
22 + params: [{}, { current: "fake-ref-object" }],
23 +};
24 +
25 +```
26 +
27 +## Code
28 +
29 +```javascript
30 +import { c as _c } from "react/compiler-runtime";
31 +import { Stringify, identity, mutate, CONST_TRUE } from "shared-runtime";
32 +
33 +function Foo(props, ref) {
34 + const $ = _c(5);
35 + let value;
36 + let t0;
37 + if ($[0] !== ref) {
38 + t0 = Symbol.for("react.early_return_sentinel");
39 + bb0: {
40 + value = {};
41 + if (CONST_TRUE) {
42 + mutate(value);
43 + t0 = <Stringify ref={ref} />;
44 + break bb0;
45 + }
46 +
47 + mutate(value);
48 + if (CONST_TRUE) {
49 + const t1 = identity(ref);
50 + let t2;
51 + if ($[3] !== t1) {
52 + t2 = <Stringify ref={t1} />;
53 + $[3] = t1;
54 + $[4] = t2;
55 + } else {
56 + t2 = $[4];
57 + }
58 + t0 = t2;
59 + break bb0;
60 + }
61 + }
62 + $[0] = ref;
63 + $[1] = value;
64 + $[2] = t0;
65 + } else {
66 + value = $[1];
67 + t0 = $[2];
68 + }
69 + if (t0 !== Symbol.for("react.early_return_sentinel")) {
70 + return t0;
71 + }
72 + return value;
73 +}
74 +
75 +export const FIXTURE_ENTRYPOINT = {
76 + fn: Foo,
77 + params: [{}, { current: "fake-ref-object" }],
78 +};
79 +
80 +```
81 +
82 +### Eval output
83 +(kind: ok) <div>{"ref":{"current":"fake-ref-object"}}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-ref-mutable-range.tsx new
+19
@@ -0,0 +1,19 @@
1 +import { Stringify, identity, mutate, CONST_TRUE } from "shared-runtime";
2 +
3 +function Foo(props, ref) {
4 + const value = {};
5 + if (CONST_TRUE) {
6 + mutate(value);
7 + return <Stringify ref={ref} />;
8 + }
9 + mutate(value);
10 + if (CONST_TRUE) {
11 + return <Stringify ref={identity(ref)} />;
12 + }
13 + return value;
14 +}
15 +
16 +export const FIXTURE_ENTRYPOINT = {
17 + fn: Foo,
18 + params: [{}, { current: "fake-ref-object" }],
19 +};