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