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

HIR-based MergeOverlappingScopes

ghstack-source-id: 93ac68114683044d7e6332fe7898b0dcc5dc6685 Pull Request resolved: https://github.com/facebook/react-forget/pull/2828

Joe Savona committed Apr 8, 2024 at 17:09 UTC f707cb5ff321d1e5c66068274c224d5e7f3bb079
10 files changed +737 -75
compiler/packages/babel-plugin-react-forget/src/Entrypoint/Pipeline.ts
+7 -7
@@ -264,14 +264,14 @@ function* runWithEnvironment(
264 name: "AlignReactiveScopesToBlockScopes",
265 value: reactiveFunction,
266 });
267 - }
267
269 - mergeOverlappingReactiveScopes(reactiveFunction);
270 - yield log({
271 - kind: "reactive",
272 - name: "MergeOverlappingReactiveScopes",
273 - value: reactiveFunction,
274 - });
268 + mergeOverlappingReactiveScopes(reactiveFunction);
269 + yield log({
270 + kind: "reactive",
271 + name: "MergeOverlappingReactiveScopes",
272 + value: reactiveFunction,
273 + });
274 + }
275
276 buildReactiveBlocks(reactiveFunction);
277 yield log({
compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/AlignReactiveScopesToBlockScopesHIR.ts
+257 -68
@@ -10,6 +10,7 @@ import {
10 BlockId,
11 HIRFunction,
12 InstructionId,
13 + MutableRange,
14 Place,
15 ReactiveScope,
16 makeInstructionId,
@@ -21,7 +22,9 @@ import {
22 mapTerminalSuccessors,
23 terminalFallthrough,
24 } from "../HIR/visitors";
25 +import DisjointSet from "../Utils/DisjointSet";
26 import { retainWhere } from "../Utils/utils";
27 +import { getPlaceScope } from "./BuildReactiveBlocks";
28
29 /*
30 * Note: this is the 2nd of 4 passes that determine how to break a function into discrete
@@ -64,119 +67,149 @@ import { retainWhere } from "../Utils/utils";
67 * finds the first instruction after the scope's mutable range in that same block scope (which
68 * will be the updated end for that scope).
69 */
67 -
70 export function alignReactiveScopesToBlockScopesHIR(fn: HIRFunction): void {
69 - type BlockContext =
70 - | { kind: "block"; block: BlockId; scopes: Array<ReactiveScope> }
71 - | {
72 - kind: "value";
73 - start: InstructionId;
74 - end: InstructionId;
75 - scopes: Array<ReactiveScope>;
76 - };
77 - const blockContexts = new Map<BlockId, BlockContext>();
71 + const blockNodes = new Map<BlockId, BlockNode>();
72 + const rootNode: BlockNode = {
73 + kind: "node",
74 + valueRange: null,
75 + children: [],
76 + id: makeInstructionId(0),
77 + };
78 + blockNodes.set(fn.body.entry, rootNode);
79 const seen = new Set<ReactiveScope>();
80 + const placeScopes = new Map<Place, ReactiveScope>();
81 +
82 + function recordPlace(id: InstructionId, place: Place, node: BlockNode): void {
83 + if (place.identifier.scope !== null) {
84 + placeScopes.set(place, place.identifier.scope);
85 + }
86
80 - function recordPlace(place: Place, context: BlockContext): void {
81 - const scope = place.identifier.scope;
87 + const scope = getPlaceScope(id, place);
88 if (scope == null) {
89 return;
90 }
91 + node.children.push({ kind: "scope", scope, id });
92
93 if (seen.has(scope)) {
94 return;
95 }
89 - if (context.kind === "value") {
96 + seen.add(scope);
97 + if (node.valueRange !== null) {
98 scope.range.start = makeInstructionId(
91 - Math.min(context.start, scope.range.start)
99 + Math.min(node.valueRange.start, scope.range.start)
100 );
101 scope.range.end = makeInstructionId(
94 - Math.max(context.end, scope.range.end)
102 + Math.max(node.valueRange.end, scope.range.end)
103 );
104 }
97 - seen.add(scope);
98 - context.scopes.push(scope);
105 }
106
107 for (const [, block] of fn.body.blocks) {
108 const { instructions, terminal } = block;
103 - let context = blockContexts.get(block.id);
104 - if (context === undefined) {
105 - if (block.kind === "block" || block.kind === "catch") {
106 - context = { kind: "block", block: block.id, scopes: [] };
107 - } else {
108 - CompilerError.invariant(false, {
109 - reason: `Expected a context to be initialized for value block`,
110 - loc: instructions[0]?.loc ?? terminal.loc,
111 - description: `No value for block bb${block.id}`,
112 - });
113 - }
114 - } else if (block.kind === "block" && context.kind !== "block") {
109 + const node = blockNodes.get(block.id);
110 + if (node === undefined) {
111 CompilerError.invariant(false, {
116 - reason: `Expected a block context for block`,
112 + reason: `Expected a node to be initialized for block`,
113 loc: instructions[0]?.loc ?? terminal.loc,
118 - description: `Got value block for bb${block.id}`,
114 + description: `No node for block bb${block.id} (${block.kind})`,
115 });
116 }
117
122 - /*
123 - * Any scopes that carried over across a terminal->fallback need their range extended
124 - * to at least the first instruction of the fallback
125 - */
126 - const startId = instructions.at(0)?.id ?? terminal.id;
127 - for (const scope of context.scopes) {
128 - scope.range.end = makeInstructionId(Math.max(scope.range.end, startId));
129 - }
130 -
131 - /*
132 - * Visit instructions, pruning scopes that end and recording new scopes that appear
133 - * on operands
134 - */
118 for (const instr of instructions) {
136 - retainWhere(context.scopes, (scope) => scope.range.end > instr.id);
119 for (const lvalue of eachInstructionLValue(instr)) {
138 - recordPlace(lvalue, context);
120 + recordPlace(instr.id, lvalue, node);
121 }
122 for (const operand of eachInstructionValueOperand(instr.value)) {
141 - recordPlace(operand, context);
123 + recordPlace(instr.id, operand, node);
124 }
125 }
144 -
145 - // Close scopes that complete at the terminal, and visit scopes of operands
146 - retainWhere(context.scopes, (scope) => scope.range.end > terminal.id);
126 for (const operand of eachTerminalOperand(terminal)) {
148 - recordPlace(operand, context);
127 + recordPlace(terminal.id, operand, node);
128 }
129
151 - // Save the current context for the fallback block, where this block scope continues
130 + // Save the current node for the fallback block, where this block scope continues
131 const fallthrough = terminalFallthrough(terminal);
153 - if (fallthrough !== null && !blockContexts.has(fallthrough)) {
154 - blockContexts.set(fallthrough, context);
132 + if (fallthrough !== null && !blockNodes.has(fallthrough)) {
133 + /*
134 + * Any scopes that carried over across a terminal->fallback need their range extended
135 + * to at least the first instruction of the fallback
136 + *
137 + * Note that it's possible for a terminal such as an if or switch to have a null fallback,
138 + * indicating that all control-flow paths diverge instead of reaching the fallthrough.
139 + * In this case there isn't an instruction id in the program that we can point to for the
140 + * updated range. Since the output is correct in this case we leave it, but it would be
141 + * more correct to find the maximum instuction id in the whole program and set the range.end
142 + * to one greater. Alternatively, we could leave in an unreachable fallthrough (with a new
143 + * "unreachable" terminal variant, perhaps) and use that instruction id.
144 + */
145 + const fallthroughBlock = fn.body.blocks.get(fallthrough)!;
146 + const nextId =
147 + fallthroughBlock.instructions[0]?.id ?? fallthroughBlock.terminal.id;
148 + for (const child of node.children) {
149 + if (child.kind !== "scope") {
150 + continue;
151 + }
152 + const scope = child.scope;
153 + if (scope.range.end > terminal.id) {
154 + scope.range.end = makeInstructionId(
155 + Math.max(scope.range.end, nextId)
156 + );
157 + }
158 + }
159 + blockNodes.set(fallthrough, node);
160 }
161
162 /*
163 * Visit all successors (not just direct successors for control-flow ordering) to
159 - * set a value block context where necessary to align the value block start/end
164 + * set a value block node where necessary to align the value block start/end
165 * back to the outer block scope.
166 *
167 * TODO: add a variant of eachTerminalSuccessor() that visits _all_ successors, not
168 * just those that are direct successors for normal control-flow ordering.
169 */
170 mapTerminalSuccessors(terminal, (successor) => {
171 + if (blockNodes.has(successor)) {
172 + return successor;
173 + }
174 +
175 const successorBlock = fn.body.blocks.get(successor)!;
176 /*
177 * we need the block kind check here because the do..while terminal's successor
178 * is a block, and try's successor is a catch block
179 */
171 - if (
172 - !blockContexts.has(successor) &&
173 - successorBlock.kind !== "block" &&
174 - successorBlock.kind !== "catch"
180 + if (successorBlock.kind === "block" || successorBlock.kind === "catch") {
181 + const childNode: BlockNode = {
182 + kind: "node",
183 + id: terminal.id,
184 + children: [],
185 + valueRange: null,
186 + };
187 + node.children.push(childNode);
188 + blockNodes.set(successor, childNode);
189 + } else if (
190 + node.valueRange === null ||
191 + terminal.kind === "ternary" ||
192 + terminal.kind === "logical" ||
193 + terminal.kind === "optional"
194 ) {
176 - let valueContext: BlockContext;
177 - if (context!.kind === "value") {
178 - valueContext = context!;
179 - } else {
195 + /**
196 + * Create a new scope node whenever we transition from block scope -> value scope.
197 + *
198 + * For compatibility with the previous ReactiveFunction-based scope merging logic,
199 + * we also create new scope nodes for ternary, logical, and optional terminals.
200 + * However, inside value blocks we always store a range (valueRange) that is the
201 + * start/end instruction ids at the nearest parent block scope level, so that
202 + * scopes inside the value blocks can be extended to align with block scope
203 + * instructions.
204 + */
205 + const childNode = {
206 + kind: "node",
207 + id: terminal.id,
208 + children: [],
209 + valueRange: null,
210 + } as BlockNode;
211 + if (node.valueRange === null) {
212 + // Transition from block->value scope, derive the outer block scope range
213 CompilerError.invariant(fallthrough !== null, {
214 reason: `Expected a fallthrough for value block`,
215 loc: terminal.loc,
@@ -185,16 +218,172 @@ export function alignReactiveScopesToBlockScopesHIR(fn: HIRFunction): void {
218 const nextId =
219 fallthroughBlock.instructions[0]?.id ??
220 fallthroughBlock.terminal.id;
188 - valueContext = {
189 - kind: "value",
221 + childNode.valueRange = {
222 start: terminal.id,
223 end: nextId,
192 - scopes: [],
193 - } as BlockContext;
224 + };
225 + } else {
226 + // else value->value transition, reuse the range
227 + childNode.valueRange = node.valueRange;
228 }
195 - blockContexts.set(successor, valueContext);
229 + node.children.push(childNode);
230 + blockNodes.set(successor, childNode);
231 + } else {
232 + // this is a value -> value block transition, reuse the node
233 + blockNodes.set(successor, node);
234 }
235 return successor;
236 });
237 }
238 +
239 + // console.log(_debug(rootNode));
240 +
241 + const joinedScopes: DisjointSet<ReactiveScope> =
242 + mergeOverlappingScopes(rootNode);
243 +
244 + joinedScopes.forEach((scope, groupScope) => {
245 + if (scope !== groupScope) {
246 + groupScope.range.start = makeInstructionId(
247 + Math.min(groupScope.range.start, scope.range.start)
248 + );
249 + groupScope.range.end = makeInstructionId(
250 + Math.max(groupScope.range.end, scope.range.end)
251 + );
252 + }
253 + });
254 + for (const [place, originalScope] of placeScopes) {
255 + const nextScope = joinedScopes.find(originalScope);
256 + if (nextScope !== null && nextScope !== originalScope) {
257 + place.identifier.scope = nextScope;
258 + }
259 + }
260 +}
261 +
262 +type BlockNode = {
263 + kind: "node";
264 + id: InstructionId;
265 + valueRange: MutableRange | null;
266 + children: Array<BlockNode | ReactiveScopeNode>;
267 +};
268 +type ReactiveScopeNode = {
269 + kind: "scope";
270 + id: InstructionId;
271 + scope: ReactiveScope;
272 +};
273 +
274 +function _debug(node: BlockNode): string {
275 + const buf: Array<string> = [];
276 + _printNode(node, buf, 0);
277 + return buf.join("\n");
278 +}
279 +function _printNode(
280 + node: BlockNode | ReactiveScopeNode,
281 + out: Array<string>,
282 + depth: number = 0
283 +): void {
284 + let prefix = " ".repeat(depth);
285 + if (node.kind === "scope") {
286 + out.push(
287 + `${prefix}[${node.id}] @${node.scope.id} [${node.scope.range.start}:${node.scope.range.end}]`
288 + );
289 + } else {
290 + let range =
291 + node.valueRange !== null
292 + ? ` [${node.valueRange.start}:${node.valueRange.end}]`
293 + : "";
294 + out.push(`${prefix}[${node.id}] node${range} [`);
295 + for (const child of node.children) {
296 + _printNode(child, out, depth + 1);
297 + }
298 + out.push(`${prefix}]`);
299 + }
300 +}
301 +
302 +type ScopeItem = {
303 + scope: ReactiveScope;
304 + shadowedBy: ReactiveScope | null;
305 +};
306 +class BlockItem {
307 + seen: Set<ReactiveScope> = new Set();
308 + scopes: Array<ScopeItem> = [];
309 +}
310 +
311 +function mergeOverlappingScopes(root: BlockNode): DisjointSet<ReactiveScope> {
312 + const seen = new Set<ReactiveScope>();
313 + const joined = new DisjointSet<ReactiveScope>();
314 +
315 + function visit(node: BlockNode, stack: Array<BlockItem>): void {
316 + const currentBlock = stack.at(-1)!;
317 + child: for (const child of node.children) {
318 + retainWhere(currentBlock.scopes, (item) => {
319 + if (item.scope.range.end > child.id) {
320 + return true;
321 + } else {
322 + currentBlock.seen.delete(item.scope);
323 + return false;
324 + }
325 + });
326 + if (child.kind === "node") {
327 + visit(child, [...stack, new BlockItem()]);
328 + } else {
329 + const scope = child.scope;
330 + if (!seen.has(scope)) {
331 + seen.add(scope);
332 + currentBlock.seen.add(scope);
333 + currentBlock.scopes.push({ shadowedBy: null, scope });
334 + continue;
335 + }
336 +
337 + let index = stack.length - 1;
338 + let nextBlock = currentBlock;
339 + while (!nextBlock.seen.has(scope)) {
340 + joined.union([scope, ...nextBlock.scopes.map((s) => s.scope)]);
341 + index--;
342 + if (index < 0) {
343 + currentBlock.seen.add(scope);
344 + currentBlock.scopes.push({ shadowedBy: null, scope });
345 + continue child;
346 + }
347 + nextBlock = stack[index]!;
348 + }
349 +
350 + // Handle interleaving within a given block scope
351 + let found = false;
352 + for (let i = 0; i < nextBlock.scopes.length; i++) {
353 + const current = nextBlock.scopes[i]!;
354 + if (current.scope.id === scope.id) {
355 + found = true;
356 + if (current.shadowedBy !== null) {
357 + joined.union([current.shadowedBy, current.scope]);
358 + }
359 + } else if (found && current.shadowedBy === null) {
360 + // `scope` is shadowing `current` and may interleave
361 + current.shadowedBy = scope;
362 + if (current.scope.range.end > scope.range.end) {
363 + /*
364 + * Current is shadowed by `scope`, and we know that `current` will mutate
365 + * again (per its range), so the scopes are already known to interleave.
366 + *
367 + * Eagerly extend the ranges of the scopes so that we don't prematurely end
368 + * a scope relative to its eventual post-merge mutable range
369 + */
370 + const end = makeInstructionId(
371 + Math.max(current.scope.range.end, scope.range.end)
372 + );
373 + current.scope.range.end = end;
374 + scope.range.end = end;
375 + joined.union([current.scope, scope]);
376 + }
377 + }
378 + }
379 + if (!currentBlock.seen.has(scope)) {
380 + currentBlock.seen.add(scope);
381 + currentBlock.scopes.push({ shadowedBy: null, scope });
382 + }
383 + }
384 + }
385 + }
386 +
387 + visit(root, [new BlockItem()]);
388 + return joined;
389 }
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/object-mutated-in-consequent-alternate-both-return.expect.md new
+66
@@ -0,0 +1,66 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import { makeObject_Primitives } from "shared-runtime";
6 +
7 +function Component(props) {
8 + const object = makeObject_Primitives();
9 + if (props.cond) {
10 + object.value = 1;
11 + return object;
12 + } else {
13 + object.value = props.value;
14 + return object;
15 + }
16 +}
17 +
18 +export const FIXTURE_ENTRYPOINT = {
19 + fn: Component,
20 + params: [{ cond: false, value: [0, 1, 2] }],
21 +};
22 +
23 +```
24 +
25 +## Code
26 +
27 +```javascript
28 +import { unstable_useMemoCache as useMemoCache } from "react";
29 +import { makeObject_Primitives } from "shared-runtime";
30 +
31 +function Component(props) {
32 + const $ = useMemoCache(2);
33 + let t0;
34 + if ($[0] !== props) {
35 + t0 = Symbol.for("react.early_return_sentinel");
36 + bb9: {
37 + const object = makeObject_Primitives();
38 + if (props.cond) {
39 + object.value = 1;
40 + t0 = object;
41 + break bb9;
42 + } else {
43 + object.value = props.value;
44 + t0 = object;
45 + break bb9;
46 + }
47 + }
48 + $[0] = props;
49 + $[1] = t0;
50 + } else {
51 + t0 = $[1];
52 + }
53 + if (t0 !== Symbol.for("react.early_return_sentinel")) {
54 + return t0;
55 + }
56 +}
57 +
58 +export const FIXTURE_ENTRYPOINT = {
59 + fn: Component,
60 + params: [{ cond: false, value: [0, 1, 2] }],
61 +};
62 +
63 +```
64 +
65 +### Eval output
66 +(kind: ok) {"a":0,"b":"value1","c":true,"value":[0,1,2]}
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/object-mutated-in-consequent-alternate-both-return.js new
+17
@@ -0,0 +1,17 @@
1 +import { makeObject_Primitives } from "shared-runtime";
2 +
3 +function Component(props) {
4 + const object = makeObject_Primitives();
5 + if (props.cond) {
6 + object.value = 1;
7 + return object;
8 + } else {
9 + object.value = props.value;
10 + return object;
11 + }
12 +}
13 +
14 +export const FIXTURE_ENTRYPOINT = {
15 + fn: Component,
16 + params: [{ cond: false, value: [0, 1, 2] }],
17 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-mutable-range-extending-into-ternary.expect.md new
+106
@@ -0,0 +1,106 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import { useState } from "react";
6 +
7 +function Component(props) {
8 + const items = props.items ? props.items.slice() : [];
9 + const [state] = useState("");
10 + return props.cond ? (
11 + <div>{state}</div>
12 + ) : (
13 + <div>
14 + {items.map((item) => (
15 + <div key={item.id}>{item.name}</div>
16 + ))}
17 + </div>
18 + );
19 +}
20 +
21 +export const FIXTURE_ENTRYPOINT = {
22 + fn: Component,
23 + params: [{ cond: false, items: [{ id: 0, name: "Alice" }] }],
24 + sequentialRenders: [
25 + { cond: false, items: [{ id: 0, name: "Alice" }] },
26 + {
27 + cond: false,
28 + items: [
29 + { id: 0, name: "Alice" },
30 + { id: 1, name: "Bob" },
31 + ],
32 + },
33 + {
34 + cond: true,
35 + items: [
36 + { id: 0, name: "Alice" },
37 + { id: 1, name: "Bob" },
38 + ],
39 + },
40 + {
41 + cond: false,
42 + items: [
43 + { id: 1, name: "Bob" },
44 + { id: 2, name: "Claire" },
45 + ],
46 + },
47 + ],
48 +};
49 +
50 +```
51 +
52 +## Code
53 +
54 +```javascript
55 +import { useState } from "react";
56 +
57 +function Component(props) {
58 + const items = props.items ? props.items.slice() : [];
59 + const [state] = useState("");
60 + return props.cond ? (
61 + <div>{state}</div>
62 + ) : (
63 + <div>
64 + {items.map((item) => (
65 + <div key={item.id}>{item.name}</div>
66 + ))}
67 + </div>
68 + );
69 +}
70 +
71 +export const FIXTURE_ENTRYPOINT = {
72 + fn: Component,
73 + params: [{ cond: false, items: [{ id: 0, name: "Alice" }] }],
74 + sequentialRenders: [
75 + { cond: false, items: [{ id: 0, name: "Alice" }] },
76 + {
77 + cond: false,
78 + items: [
79 + { id: 0, name: "Alice" },
80 + { id: 1, name: "Bob" },
81 + ],
82 + },
83 + {
84 + cond: true,
85 + items: [
86 + { id: 0, name: "Alice" },
87 + { id: 1, name: "Bob" },
88 + ],
89 + },
90 + {
91 + cond: false,
92 + items: [
93 + { id: 1, name: "Bob" },
94 + { id: 2, name: "Claire" },
95 + ],
96 + },
97 + ],
98 +};
99 +
100 +```
101 +
102 +### Eval output
103 +(kind: ok) <div><div>Alice</div></div>
104 +<div><div>Alice</div><div>Bob</div></div>
105 +<div></div>
106 +<div><div>Bob</div><div>Claire</div></div>
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-mutable-range-extending-into-ternary.js new
+44
@@ -0,0 +1,44 @@
1 +import { useState } from "react";
2 +
3 +function Component(props) {
4 + const items = props.items ? props.items.slice() : [];
5 + const [state] = useState("");
6 + return props.cond ? (
7 + <div>{state}</div>
8 + ) : (
9 + <div>
10 + {items.map((item) => (
11 + <div key={item.id}>{item.name}</div>
12 + ))}
13 + </div>
14 + );
15 +}
16 +
17 +export const FIXTURE_ENTRYPOINT = {
18 + fn: Component,
19 + params: [{ cond: false, items: [{ id: 0, name: "Alice" }] }],
20 + sequentialRenders: [
21 + { cond: false, items: [{ id: 0, name: "Alice" }] },
22 + {
23 + cond: false,
24 + items: [
25 + { id: 0, name: "Alice" },
26 + { id: 1, name: "Bob" },
27 + ],
28 + },
29 + {
30 + cond: true,
31 + items: [
32 + { id: 0, name: "Alice" },
33 + { id: 1, name: "Bob" },
34 + ],
35 + },
36 + {
37 + cond: false,
38 + items: [
39 + { id: 1, name: "Bob" },
40 + { id: 2, name: "Claire" },
41 + ],
42 + },
43 + ],
44 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-separate-scopes-for-divs.expect.md new
+115
@@ -0,0 +1,115 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import { identity } from "shared-runtime";
6 +
7 +const DISPLAY = true;
8 +function Component({ cond = false, id }) {
9 + return (
10 + <>
11 + <div className={identity(styles.a, id !== null ? styles.b : {})}></div>
12 +
13 + {cond === false && (
14 + <div className={identity(styles.c, DISPLAY ? styles.d : {})} />
15 + )}
16 + </>
17 + );
18 +}
19 +
20 +export const FIXTURE_ENTRYPOINT = {
21 + fn: Component,
22 + params: [{ cond: false, id: 42 }],
23 + sequentialRenders: [
24 + { cond: false, id: 4 },
25 + { cond: true, id: 4 },
26 + { cond: true, id: 42 },
27 + ],
28 +};
29 +
30 +const styles = {
31 + a: "a",
32 + b: "b",
33 + c: "c",
34 + d: "d",
35 +};
36 +
37 +```
38 +
39 +## Code
40 +
41 +```javascript
42 +import { unstable_useMemoCache as useMemoCache } from "react";
43 +import { identity } from "shared-runtime";
44 +
45 +const DISPLAY = true;
46 +function Component(t0) {
47 + const $ = useMemoCache(9);
48 + const { cond: t1, id } = t0;
49 + const cond = t1 === undefined ? false : t1;
50 + let t2;
51 + if ($[0] !== id) {
52 + t2 = identity(styles.a, id !== null ? styles.b : {});
53 + $[0] = id;
54 + $[1] = t2;
55 + } else {
56 + t2 = $[1];
57 + }
58 + let t3;
59 + if ($[2] !== t2) {
60 + t3 = <div className={t2} />;
61 + $[2] = t2;
62 + $[3] = t3;
63 + } else {
64 + t3 = $[3];
65 + }
66 + let t4;
67 + if ($[4] !== cond) {
68 + t4 = cond === false && (
69 + <div className={identity(styles.c, DISPLAY ? styles.d : {})} />
70 + );
71 + $[4] = cond;
72 + $[5] = t4;
73 + } else {
74 + t4 = $[5];
75 + }
76 + let t5;
77 + if ($[6] !== t3 || $[7] !== t4) {
78 + t5 = (
79 + <>
80 + {t3}
81 + {t4}
82 + </>
83 + );
84 + $[6] = t3;
85 + $[7] = t4;
86 + $[8] = t5;
87 + } else {
88 + t5 = $[8];
89 + }
90 + return t5;
91 +}
92 +
93 +export const FIXTURE_ENTRYPOINT = {
94 + fn: Component,
95 + params: [{ cond: false, id: 42 }],
96 + sequentialRenders: [
97 + { cond: false, id: 4 },
98 + { cond: true, id: 4 },
99 + { cond: true, id: 42 },
100 + ],
101 +};
102 +
103 +const styles = {
104 + a: "a",
105 + b: "b",
106 + c: "c",
107 + d: "d",
108 +};
109 +
110 +```
111 +
112 +### Eval output
113 +(kind: ok) <div class="a"></div><div class="c"></div>
114 +<div class="a"></div>
115 +<div class="a"></div>
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-separate-scopes-for-divs.js new
+31
@@ -0,0 +1,31 @@
1 +import { identity } from "shared-runtime";
2 +
3 +const DISPLAY = true;
4 +function Component({ cond = false, id }) {
5 + return (
6 + <>
7 + <div className={identity(styles.a, id !== null ? styles.b : {})}></div>
8 +
9 + {cond === false && (
10 + <div className={identity(styles.c, DISPLAY ? styles.d : {})} />
11 + )}
12 + </>
13 + );
14 +}
15 +
16 +export const FIXTURE_ENTRYPOINT = {
17 + fn: Component,
18 + params: [{ cond: false, id: 42 }],
19 + sequentialRenders: [
20 + { cond: false, id: 4 },
21 + { cond: true, id: 4 },
22 + { cond: true, id: 42 },
23 + ],
24 +};
25 +
26 +const styles = {
27 + a: "a",
28 + b: "b",
29 + c: "c",
30 + d: "d",
31 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-unmerged-fbt-call-merge-overlapping-reactive-scopes.expect.md new
+71
@@ -0,0 +1,71 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import fbt from "fbt";
6 +import { Stringify } from "shared-runtime";
7 +
8 +function Component(props) {
9 + const label = fbt(
10 + fbt.plural("bar", props.value.length, {
11 + many: "bars",
12 + showCount: "yes",
13 + }),
14 + "The label text"
15 + );
16 + return props.cond ? (
17 + <Stringify
18 + description={<fbt desc="Some text">Text here</fbt>}
19 + label={label.toString()}
20 + />
21 + ) : null;
22 +}
23 +
24 +export const FIXTURE_ENTRYPOINT = {
25 + fn: Component,
26 + params: [{ cond: true, value: [0, 1, 2] }],
27 +};
28 +
29 +```
30 +
31 +## Code
32 +
33 +```javascript
34 +import { unstable_useMemoCache as useMemoCache } from "react";
35 +import fbt from "fbt";
36 +import { Stringify } from "shared-runtime";
37 +
38 +function Component(props) {
39 + const $ = useMemoCache(3);
40 + let t0;
41 + if ($[0] !== props.value.length || $[1] !== props.cond) {
42 + const label = fbt._(
43 + { "*": "{number} bars", _1: "1 bar" },
44 + [fbt._plural(props.value.length, "number")],
45 + { hk: "4mUen7" }
46 + );
47 +
48 + t0 = props.cond ? (
49 + <Stringify
50 + description={fbt._("Text here", null, { hk: "21YpZs" })}
51 + label={label.toString()}
52 + />
53 + ) : null;
54 + $[0] = props.value.length;
55 + $[1] = props.cond;
56 + $[2] = t0;
57 + } else {
58 + t0 = $[2];
59 + }
60 + return t0;
61 +}
62 +
63 +export const FIXTURE_ENTRYPOINT = {
64 + fn: Component,
65 + params: [{ cond: true, value: [0, 1, 2] }],
66 +};
67 +
68 +```
69 +
70 +### Eval output
71 +(kind: ok) <div>{"description":"Text here","label":"3 bars"}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-unmerged-fbt-call-merge-overlapping-reactive-scopes.js new
+23
@@ -0,0 +1,23 @@
1 +import fbt from "fbt";
2 +import { Stringify } from "shared-runtime";
3 +
4 +function Component(props) {
5 + const label = fbt(
6 + fbt.plural("bar", props.value.length, {
7 + many: "bars",
8 + showCount: "yes",
9 + }),
10 + "The label text"
11 + );
12 + return props.cond ? (
13 + <Stringify
14 + description={<fbt desc="Some text">Text here</fbt>}
15 + label={label.toString()}
16 + />
17 + ) : null;
18 +}
19 +
20 +export const FIXTURE_ENTRYPOINT = {
21 + fn: Component,
22 + params: [{ cond: true, value: [0, 1, 2] }],
23 +};