[hir] Re-implement mergeOverlappingReactiveScopes (+ bugfix)
ghstack-source-id: 06d49edffe5ae3c31eb6ef642078752c056c617c Pull Request resolved: https://github.com/facebook/react-forget/pull/2852
Mofei Zhang committed
Apr 26, 2024 at 12:40 UTC
a44559b0a5f6fc01f9963d2f3b216a2dd7179fd4
11 files changed
+541
-147
compiler/packages/babel-plugin-react-forget/src/Entrypoint/Pipeline.ts
+8
@@ -18,6 +18,7 @@ import {
18
assertValidMutableRanges,
19
lower,
20
mergeConsecutiveBlocks,
21
+ mergeOverlappingReactiveScopesHIR,
22
pruneUnusedLabelsHIR,
23
} from "../HIR";
24
import {
@@ -252,6 +253,13 @@ function* runWithEnvironment(
253
value: hir,
254
});
255
256
+ mergeOverlappingReactiveScopesHIR(hir);
257
+ yield log({
258
+ kind: "hir",
259
+ name: "MergeOverlappingReactiveScopesHIR",
260
+ value: hir,
261
+ });
262
+
263
assertValidBlockNesting(hir);
264
}
265
compiler/packages/babel-plugin-react-forget/src/HIR/MergeOverlappingReactiveScopesHIR.ts
new
+291
@@ -0,0 +1,291 @@
1
+import {
2
+ HIRFunction,
3
+ InstructionId,
4
+ Place,
5
+ ReactiveScope,
6
+ makeInstructionId,
7
+} from ".";
8
+import { getPlaceScope } from "../ReactiveScopes/BuildReactiveBlocks";
9
+import { isMutable } from "../ReactiveScopes/InferReactiveScopeVariables";
10
+import DisjointSet from "../Utils/DisjointSet";
11
+import { getOrInsertDefault } from "../Utils/utils";
12
+import {
13
+ eachInstructionLValue,
14
+ eachInstructionOperand,
15
+ eachTerminalOperand,
16
+} from "./visitors";
17
+
18
+/**
19
+ * While previous passes ensure that reactive scopes span valid sets of program
20
+ * blocks, pairs of reactive scopes may still be inconsistent with respect to
21
+ * each other.
22
+ *
23
+ * (a) Reactive scopes ranges must form valid blocks in the resulting javascript
24
+ * program. Any two scopes must either be entirely disjoint or one scope must be
25
+ * nested within the other.
26
+ * ```js
27
+ * // Scopes 1:3 and 3:5 are valid because they contain no common instructions
28
+ * [1] ⌝
29
+ * [2] ⌟
30
+ * [3] ⌝
31
+ * [4] ⌟
32
+ * // Scopes 1:3 and 1:5 are valid because the former is nested within the other
33
+ * [1] ⌝ ⌝
34
+ * [2] ⌟ |
35
+ * [3] |
36
+ * [4] ⌟
37
+ * // Scopes 1:4 and 2:5 are invalid because we cannot produce if-else memo
38
+ * // blocks representing these scopes in the output program.
39
+ * [1] ⌝
40
+ * [2] | ⌝
41
+ * [3] ⌟ |
42
+ * [4] ⌟
43
+ * ```
44
+ *
45
+ * (b) A scope's own instructions may only mutate that scope.
46
+ * For each reactive scope, we currently produce exactly one if-block which
47
+ * spans the instruction range of the scope. In this simple example, instr [2]
48
+ * does not mutate any values but is included within scope @0.
49
+ * ```js
50
+ * // IR instructions
51
+ * [1] (writes to scope @0's values)
52
+ * [2] (does not mutate anything)
53
+ * [3] (writes to scope @0's values)
54
+ *
55
+ * // javascript output
56
+ * if (( scope @0's dependencies changed )) {
57
+ * [1]
58
+ * [2]
59
+ * [3]
60
+ * }
61
+ * ```
62
+ * Nested scopes may be modeled as a tree in which child scopes are contained
63
+ * within parent scopes. This corresponds to nested if-else memo blocks in the
64
+ * output program). An instruction may only mutate its own "active" scope.
65
+ * ```js
66
+ * // Active scopes for a simple program
67
+ * scope @0 {
68
+ * [0] (active scope=@0)
69
+ * scope @1 {
70
+ * [1] (active scope=@1)
71
+ * [2] (active scope=@1)
72
+ * }
73
+ * [3] (active scope=@0)
74
+ * }
75
+ * [4] (no active scope)
76
+ *
77
+ * // In this example, scopes @0 and @1 must be merged because instr [2]'s
78
+ * // active scope is scope@1 but it mutates scope@0.
79
+ * scope @0, produces x {
80
+ * [0] x = []
81
+ * scope @1, produces y {
82
+ * [1] y = []
83
+ * [2] x.push(2)
84
+ * [3] y.push(3)
85
+ * }
86
+ * [3] x.push(1)
87
+ * }
88
+ * ```
89
+ *
90
+ * As mentioned, these constraints arise entirely from the current design of
91
+ * compiler output.
92
+ * - instruction ordering is preserved (otherwise, disjoint ranges for scopes
93
+ * may be produced by reordering their mutating instructions)
94
+ * - exactly one if-else block per scope, which does not allow the composition
95
+ * of a reactive scope from disconnected instruction ranges.
96
+ */
97
+
98
+export function mergeOverlappingReactiveScopesHIR(fn: HIRFunction): void {
99
+ /**
100
+ * Collect all scopes eagerly because some scopes begin before the first
101
+ * instruction that references them (due to alignReactiveScopesToBlocks)
102
+ */
103
+ const scopesInfo = collectScopeInfo(fn);
104
+
105
+ /**
106
+ * Iterate through scopes and instructions to find which should be merged
107
+ */
108
+ const joinedScopes = getOverlappingReactiveScopes(fn, scopesInfo);
109
+
110
+ /**
111
+ * Merge scopes and rewrite all references
112
+ */
113
+ joinedScopes.forEach((scope, groupScope) => {
114
+ if (scope !== groupScope) {
115
+ groupScope.range.start = makeInstructionId(
116
+ Math.min(groupScope.range.start, scope.range.start)
117
+ );
118
+ groupScope.range.end = makeInstructionId(
119
+ Math.max(groupScope.range.end, scope.range.end)
120
+ );
121
+ }
122
+ });
123
+ for (const [place, originalScope] of scopesInfo.placeScopes) {
124
+ const nextScope = joinedScopes.find(originalScope);
125
+ if (nextScope !== null && nextScope !== originalScope) {
126
+ place.identifier.scope = nextScope;
127
+ }
128
+ }
129
+}
130
+
131
+type ScopeInfo = {
132
+ scopeStarts: Array<{ id: InstructionId; scopes: Set<ReactiveScope> }>;
133
+ scopeEnds: Array<{ id: InstructionId; scopes: Set<ReactiveScope> }>;
134
+ placeScopes: Map<Place, ReactiveScope>;
135
+};
136
+
137
+type TraversalState = {
138
+ joined: DisjointSet<ReactiveScope>;
139
+ activeScopes: Array<ReactiveScope>;
140
+};
141
+
142
+function collectScopeInfo(fn: HIRFunction): ScopeInfo {
143
+ const scopeStarts: Map<InstructionId, Set<ReactiveScope>> = new Map();
144
+ const scopeEnds: Map<InstructionId, Set<ReactiveScope>> = new Map();
145
+ const placeScopes: Map<Place, ReactiveScope> = new Map();
146
+
147
+ function collectPlaceScope(place: Place): void {
148
+ const scope = place.identifier.scope;
149
+ if (scope != null) {
150
+ placeScopes.set(place, scope);
151
+ if (scope.range.start !== scope.range.end) {
152
+ getOrInsertDefault(scopeStarts, scope.range.start, new Set()).add(
153
+ scope
154
+ );
155
+ getOrInsertDefault(scopeEnds, scope.range.end, new Set()).add(scope);
156
+ }
157
+ }
158
+ }
159
+
160
+ for (const [, block] of fn.body.blocks) {
161
+ for (const instr of block.instructions) {
162
+ for (const operand of eachInstructionLValue(instr)) {
163
+ collectPlaceScope(operand);
164
+ }
165
+ for (const operand of eachInstructionOperand(instr)) {
166
+ collectPlaceScope(operand);
167
+ }
168
+ }
169
+ for (const operand of eachTerminalOperand(block.terminal)) {
170
+ collectPlaceScope(operand);
171
+ }
172
+ }
173
+
174
+ return {
175
+ scopeStarts: [...scopeStarts.entries()]
176
+ .map(([id, scopes]) => ({ id, scopes }))
177
+ .sort((a, b) => b.id - a.id),
178
+ scopeEnds: [...scopeEnds.entries()]
179
+ .map(([id, scopes]) => ({ id, scopes }))
180
+ .sort((a, b) => b.id - a.id),
181
+ placeScopes,
182
+ };
183
+}
184
+
185
+function visitInstructionId(
186
+ id: InstructionId,
187
+ { scopeEnds, scopeStarts }: ScopeInfo,
188
+ { activeScopes, joined }: TraversalState
189
+): void {
190
+ /**
191
+ * Handle all scopes that end at this instruction.
192
+ */
193
+ const scopeEndTop = scopeEnds.at(-1);
194
+ if (scopeEndTop != null && scopeEndTop.id <= id) {
195
+ scopeEnds.pop();
196
+
197
+ /**
198
+ * Match scopes that end at this instruction with our stack of active
199
+ * scopes (from traversal state). We need to sort these in descending
200
+ * order of start IDs because the scopes stack is ordered as such
201
+ */
202
+ const scopesSortedStartDescending = [...scopeEndTop.scopes].sort(
203
+ (a, b) => b.range.start - a.range.start
204
+ );
205
+ for (const scope of scopesSortedStartDescending) {
206
+ const idx = activeScopes.indexOf(scope);
207
+ if (idx !== -1) {
208
+ /**
209
+ * Detect and merge all overlapping scopes. `activeScopes` is ordered
210
+ * by scope start, so every active scope between a completed scope s
211
+ * and the top of the stack (1) started later than s and (2) completes after s.
212
+ */
213
+ if (idx !== activeScopes.length - 1) {
214
+ joined.union([scope, ...activeScopes.slice(idx + 1)]);
215
+ }
216
+ activeScopes.splice(idx, 1);
217
+ }
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Handle all scopes that begin at this instruction by adding them
223
+ * to the scopes stack
224
+ */
225
+ const scopeStartTop = scopeStarts.at(-1);
226
+ if (scopeStartTop != null && scopeStartTop.id <= id) {
227
+ scopeStarts.pop();
228
+
229
+ const scopesSortedEndDescending = [...scopeStartTop.scopes].sort(
230
+ (a, b) => b.range.end - a.range.end
231
+ );
232
+ activeScopes.push(...scopesSortedEndDescending);
233
+ /**
234
+ * Merge all identical scopes (ones with the same start and end),
235
+ * as they end up with the same reactive block
236
+ */
237
+ for (let i = 1; i < scopesSortedEndDescending.length; i++) {
238
+ const prev = scopesSortedEndDescending[i - 1];
239
+ const curr = scopesSortedEndDescending[i];
240
+ if (prev.range.end === curr.range.end) {
241
+ joined.union([prev, curr]);
242
+ }
243
+ }
244
+ }
245
+}
246
+
247
+function visitPlace(
248
+ id: InstructionId,
249
+ place: Place,
250
+ { activeScopes, joined }: TraversalState
251
+): void {
252
+ /**
253
+ * If an instruction mutates an outer scope, flatten all scopes from the top
254
+ * of the stack to the mutated outer scope.
255
+ */
256
+ const placeScope = getPlaceScope(id, place);
257
+ if (placeScope != null && isMutable({ id } as any, place)) {
258
+ const placeScopeIdx = activeScopes.indexOf(placeScope);
259
+ if (placeScopeIdx !== -1 && placeScopeIdx !== activeScopes.length - 1) {
260
+ joined.union([placeScope, ...activeScopes.slice(placeScopeIdx + 1)]);
261
+ }
262
+ }
263
+}
264
+
265
+function getOverlappingReactiveScopes(
266
+ fn: HIRFunction,
267
+ context: ScopeInfo
268
+): DisjointSet<ReactiveScope> {
269
+ const state: TraversalState = {
270
+ joined: new DisjointSet<ReactiveScope>(),
271
+ activeScopes: [],
272
+ };
273
+
274
+ for (const [, block] of fn.body.blocks) {
275
+ for (const instr of block.instructions) {
276
+ visitInstructionId(instr.id, context, state);
277
+ for (const place of eachInstructionOperand(instr)) {
278
+ visitPlace(instr.id, place, state);
279
+ }
280
+ for (const place of eachInstructionLValue(instr)) {
281
+ visitPlace(instr.id, place, state);
282
+ }
283
+ }
284
+ visitInstructionId(block.terminal.id, context, state);
285
+ for (const place of eachTerminalOperand(block.terminal)) {
286
+ visitPlace(block.terminal.id, place, state);
287
+ }
288
+ }
289
+
290
+ return state.joined;
291
+}
compiler/packages/babel-plugin-react-forget/src/HIR/index.ts
+1
@@ -27,5 +27,6 @@ export {
27
reversePostorderBlocks,
28
} from "./HIRBuilder";
29
export { mergeConsecutiveBlocks } from "./MergeConsecutiveBlocks";
30
+export { mergeOverlappingReactiveScopesHIR } from "./MergeOverlappingReactiveScopesHIR";
31
export { printFunction, printHIR } from "./PrintHIR";
32
export { pruneUnusedLabelsHIR } from "./PruneUnusedLabelsHIR";
compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/AlignReactiveScopesToBlockScopesHIR.ts
-131
@@ -22,8 +22,6 @@ import {
22
mapTerminalSuccessors,
23
terminalFallthrough,
24
} from "../HIR/visitors";
25
-import DisjointSet from "../Utils/DisjointSet";
26
-import { retainWhere } from "../Utils/utils";
25
import { getPlaceScope } from "./BuildReactiveBlocks";
26
27
/*
@@ -237,46 +235,6 @@ export function alignReactiveScopesToBlockScopesHIR(fn: HIRFunction): void {
235
}
236
237
// console.log(_debug(rootNode));
240
-
241
- const joinedScopes: DisjointSet<ReactiveScope> =
242
- mergeOverlappingScopes(rootNode);
243
-
244
- /**
245
- * Join scopes that begin and end at the same instructions
246
- */
247
- {
248
- const allScopes = [...new Set(placeScopes.values())].sort(
249
- (a, b) => a.range.start - b.range.start
250
- );
251
- for (let i = 1; i < allScopes.length; i++) {
252
- const prev = allScopes[i - 1];
253
- const curr = allScopes[i];
254
- if (
255
- prev.range.start === curr.range.start &&
256
- prev.range.end === curr.range.end
257
- ) {
258
- joinedScopes.union([prev, curr]);
259
- }
260
- }
261
- }
262
-
263
- joinedScopes.forEach((scope, groupScope) => {
264
- if (scope !== groupScope) {
265
- groupScope.range.start = makeInstructionId(
266
- Math.min(groupScope.range.start, scope.range.start)
267
- );
268
- groupScope.range.end = makeInstructionId(
269
- Math.max(groupScope.range.end, scope.range.end)
270
- );
271
- }
272
- });
273
-
274
- for (const [place, originalScope] of placeScopes) {
275
- const nextScope = joinedScopes.find(originalScope);
276
- if (nextScope !== null && nextScope !== originalScope) {
277
- place.identifier.scope = nextScope;
278
- }
279
- }
238
}
239
240
type BlockNode = {
@@ -318,92 +276,3 @@ function _printNode(
276
out.push(`${prefix}]`);
277
}
278
}
321
-
322
-type ScopeItem = {
323
- scope: ReactiveScope;
324
- shadowedBy: ReactiveScope | null;
325
-};
326
-class BlockItem {
327
- seen: Set<ReactiveScope> = new Set();
328
- scopes: Array<ScopeItem> = [];
329
-}
330
-
331
-function mergeOverlappingScopes(root: BlockNode): DisjointSet<ReactiveScope> {
332
- const seen = new Set<ReactiveScope>();
333
- const joined = new DisjointSet<ReactiveScope>();
334
-
335
- function visit(node: BlockNode, stack: Array<BlockItem>): void {
336
- const currentBlock = stack.at(-1)!;
337
- child: for (const child of node.children) {
338
- retainWhere(currentBlock.scopes, (item) => {
339
- if (item.scope.range.end > child.id) {
340
- return true;
341
- } else {
342
- currentBlock.seen.delete(item.scope);
343
- return false;
344
- }
345
- });
346
- if (child.kind === "node") {
347
- visit(child, [...stack, new BlockItem()]);
348
- } else {
349
- const scope = child.scope;
350
- if (!seen.has(scope)) {
351
- seen.add(scope);
352
- currentBlock.seen.add(scope);
353
- currentBlock.scopes.push({ shadowedBy: null, scope });
354
- continue;
355
- }
356
-
357
- let index = stack.length - 1;
358
- let nextBlock = currentBlock;
359
- while (!nextBlock.seen.has(scope)) {
360
- joined.union([scope, ...nextBlock.scopes.map((s) => s.scope)]);
361
- index--;
362
- if (index < 0) {
363
- currentBlock.seen.add(scope);
364
- currentBlock.scopes.push({ shadowedBy: null, scope });
365
- continue child;
366
- }
367
- nextBlock = stack[index]!;
368
- }
369
-
370
- // Handle interleaving within a given block scope
371
- let found = false;
372
- for (let i = 0; i < nextBlock.scopes.length; i++) {
373
- const current = nextBlock.scopes[i]!;
374
- if (current.scope.id === scope.id) {
375
- found = true;
376
- if (current.shadowedBy !== null) {
377
- joined.union([current.shadowedBy, current.scope]);
378
- }
379
- } else if (found && current.shadowedBy === null) {
380
- // `scope` is shadowing `current` and may interleave
381
- current.shadowedBy = scope;
382
- if (current.scope.range.end > scope.range.end) {
383
- /*
384
- * Current is shadowed by `scope`, and we know that `current` will mutate
385
- * again (per its range), so the scopes are already known to interleave.
386
- *
387
- * Eagerly extend the ranges of the scopes so that we don't prematurely end
388
- * a scope relative to its eventual post-merge mutable range
389
- */
390
- const end = makeInstructionId(
391
- Math.max(current.scope.range.end, scope.range.end)
392
- );
393
- current.scope.range.end = end;
394
- scope.range.end = end;
395
- joined.union([current.scope, scope]);
396
- }
397
- }
398
- }
399
- if (!currentBlock.seen.has(scope)) {
400
- currentBlock.seen.add(scope);
401
- currentBlock.scopes.push({ shadowedBy: null, scope });
402
- }
403
- }
404
- }
405
- }
406
-
407
- visit(root, [new BlockItem()]);
408
- return joined;
409
-}
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/nested-scopes-begin-same-instr-valueblock.expect.md
new
+66
@@ -0,0 +1,66 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+import { identity, mutate } from "shared-runtime";
6
+
7
+function Foo({ cond }) {
8
+ const x = identity(identity(cond)) ? { a: 2 } : { b: 2 };
9
+
10
+ mutate(x);
11
+ return x;
12
+}
13
+
14
+export const FIXTURE_ENTRYPOINT = {
15
+ fn: Foo,
16
+ params: [{ cond: false }],
17
+ sequentialRenders: [
18
+ { cond: false },
19
+ { cond: false },
20
+ { cond: true },
21
+ { cond: true },
22
+ ],
23
+};
24
+
25
+```
26
+
27
+## Code
28
+
29
+```javascript
30
+import { unstable_useMemoCache as useMemoCache } from "react";
31
+import { identity, mutate } from "shared-runtime";
32
+
33
+function Foo(t0) {
34
+ const $ = useMemoCache(2);
35
+ const { cond } = t0;
36
+ let x;
37
+ if ($[0] !== cond) {
38
+ x = identity(identity(cond)) ? { a: 2 } : { b: 2 };
39
+
40
+ mutate(x);
41
+ $[0] = cond;
42
+ $[1] = x;
43
+ } else {
44
+ x = $[1];
45
+ }
46
+ return x;
47
+}
48
+
49
+export const FIXTURE_ENTRYPOINT = {
50
+ fn: Foo,
51
+ params: [{ cond: false }],
52
+ sequentialRenders: [
53
+ { cond: false },
54
+ { cond: false },
55
+ { cond: true },
56
+ { cond: true },
57
+ ],
58
+};
59
+
60
+```
61
+
62
+### Eval output
63
+(kind: ok) {"b":2,"wat0":"joe"}
64
+{"b":2,"wat0":"joe"}
65
+{"a":2,"wat0":"joe"}
66
+{"a":2,"wat0":"joe"}
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/nested-scopes-begin-same-instr-valueblock.ts
new
+19
@@ -0,0 +1,19 @@
1
+import { identity, mutate } from "shared-runtime";
2
+
3
+function Foo({ cond }) {
4
+ const x = identity(identity(cond)) ? { a: 2 } : { b: 2 };
5
+
6
+ mutate(x);
7
+ return x;
8
+}
9
+
10
+export const FIXTURE_ENTRYPOINT = {
11
+ fn: Foo,
12
+ params: [{ cond: false }],
13
+ sequentialRenders: [
14
+ { cond: false },
15
+ { cond: false },
16
+ { cond: true },
17
+ { cond: true },
18
+ ],
19
+};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-invalid-scope-merging-value-blocks.expect.md
new
+81
@@ -0,0 +1,81 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+import {
6
+ CONST_TRUE,
7
+ identity,
8
+ makeObject_Primitives,
9
+ mutateAndReturn,
10
+ useHook,
11
+} from "shared-runtime";
12
+
13
+/**
14
+ * value and `mutateAndReturn(value)` should end up in the same reactive scope.
15
+ * (1) `value = makeObject` and `(temporary) = mutateAndReturn(value)` should be assigned
16
+ * the same scope id (on their identifiers)
17
+ * (2) alignScopesToBlockScopes should expand the scopes of both `(temporary) = identity(1)`
18
+ * and `(temporary) = mutateAndReturn(value)` to the outermost value block boundaries
19
+ * (3) mergeOverlappingScopes should merge the scopes of the above two instructions
20
+ */
21
+function Component({}) {
22
+ const value = makeObject_Primitives();
23
+ useHook();
24
+ const mutatedValue =
25
+ identity(1) && CONST_TRUE ? mutateAndReturn(value) : null;
26
+ const result = [];
27
+ useHook();
28
+ result.push(value, mutatedValue);
29
+ return result;
30
+}
31
+
32
+export const FIXTURE_ENTRYPOINT = {
33
+ fn: Component,
34
+ params: [{}],
35
+ sequentialRenders: [{}, {}, {}],
36
+};
37
+
38
+```
39
+
40
+## Code
41
+
42
+```javascript
43
+import {
44
+ CONST_TRUE,
45
+ identity,
46
+ makeObject_Primitives,
47
+ mutateAndReturn,
48
+ useHook,
49
+} from "shared-runtime";
50
+
51
+/**
52
+ * value and `mutateAndReturn(value)` should end up in the same reactive scope.
53
+ * (1) `value = makeObject` and `(temporary) = mutateAndReturn(value)` should be assigned
54
+ * the same scope id (on their identifiers)
55
+ * (2) alignScopesToBlockScopes should expand the scopes of both `(temporary) = identity(1)`
56
+ * and `(temporary) = mutateAndReturn(value)` to the outermost value block boundaries
57
+ * (3) mergeOverlappingScopes should merge the scopes of the above two instructions
58
+ */
59
+function Component(t0) {
60
+ const value = makeObject_Primitives();
61
+ useHook();
62
+ const mutatedValue =
63
+ identity(1) && CONST_TRUE ? mutateAndReturn(value) : null;
64
+ const result = [];
65
+ useHook();
66
+ result.push(value, mutatedValue);
67
+ return result;
68
+}
69
+
70
+export const FIXTURE_ENTRYPOINT = {
71
+ fn: Component,
72
+ params: [{}],
73
+ sequentialRenders: [{}, {}, {}],
74
+};
75
+
76
+```
77
+
78
+### Eval output
79
+(kind: ok) [{"a":0,"b":"value1","c":true,"wat0":"joe"},"[[ cyclic ref *1 ]]"]
80
+[{"a":0,"b":"value1","c":true,"wat0":"joe"},"[[ cyclic ref *1 ]]"]
81
+[{"a":0,"b":"value1","c":true,"wat0":"joe"},"[[ cyclic ref *1 ]]"]
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-invalid-scope-merging-value-blocks.ts
new
+32
@@ -0,0 +1,32 @@
1
+import {
2
+ CONST_TRUE,
3
+ identity,
4
+ makeObject_Primitives,
5
+ mutateAndReturn,
6
+ useHook,
7
+} from "shared-runtime";
8
+
9
+/**
10
+ * value and `mutateAndReturn(value)` should end up in the same reactive scope.
11
+ * (1) `value = makeObject` and `(temporary) = mutateAndReturn(value)` should be assigned
12
+ * the same scope id (on their identifiers)
13
+ * (2) alignScopesToBlockScopes should expand the scopes of both `(temporary) = identity(1)`
14
+ * and `(temporary) = mutateAndReturn(value)` to the outermost value block boundaries
15
+ * (3) mergeOverlappingScopes should merge the scopes of the above two instructions
16
+ */
17
+function Component({}) {
18
+ const value = makeObject_Primitives();
19
+ useHook();
20
+ const mutatedValue =
21
+ identity(1) && CONST_TRUE ? mutateAndReturn(value) : null;
22
+ const result = [];
23
+ useHook();
24
+ result.push(value, mutatedValue);
25
+ return result;
26
+}
27
+
28
+export const FIXTURE_ENTRYPOINT = {
29
+ fn: Component,
30
+ params: [{}],
31
+ sequentialRenders: [{}, {}, {}],
32
+};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/ssa-renaming-unconditional-ternary-with-mutation.expect.md
+31
-14
@@ -2,39 +2,42 @@
2
## Input
3
4
```javascript
5
+import { arrayPush } from "shared-runtime";
6
function foo(props) {
7
let x = [];
8
x.push(props.bar);
9
props.cond
10
? ((x = {}), (x = []), x.push(props.foo))
11
: ((x = []), (x = []), x.push(props.bar));
11
- mut(x);
12
+ arrayPush(x, 4);
13
return x;
14
}
15
16
+export const FIXTURE_ENTRYPOINT = {
17
+ fn: foo,
18
+ params: [{ cond: false, foo: 2, bar: 55 }],
19
+ sequentialRenders: [
20
+ { cond: false, foo: 2, bar: 55 },
21
+ { cond: false, foo: 3, bar: 55 },
22
+ { cond: true, foo: 3, bar: 55 },
23
+ ],
24
+};
25
+
26
```
27
28
## Code
29
30
```javascript
31
import { unstable_useMemoCache as useMemoCache } from "react";
32
+import { arrayPush } from "shared-runtime";
33
function foo(props) {
22
- const $ = useMemoCache(5);
34
+ const $ = useMemoCache(2);
35
let x;
36
if ($[0] !== props) {
37
x = [];
38
x.push(props.bar);
27
- if ($[2] !== props || $[3] !== x) {
28
- props.cond
29
- ? ((x = []), x.push(props.foo))
30
- : ((x = []), x.push(props.bar));
31
- mut(x);
32
- $[2] = props;
33
- $[3] = x;
34
- $[4] = x;
35
- } else {
36
- x = $[4];
37
- }
39
+ props.cond ? ((x = []), x.push(props.foo)) : ((x = []), x.push(props.bar));
40
+ arrayPush(x, 4);
41
$[0] = props;
42
$[1] = x;
43
} else {
@@ -43,5 +46,19 @@ function foo(props) {
46
return x;
47
}
48
49
+export const FIXTURE_ENTRYPOINT = {
50
+ fn: foo,
51
+ params: [{ cond: false, foo: 2, bar: 55 }],
52
+ sequentialRenders: [
53
+ { cond: false, foo: 2, bar: 55 },
54
+ { cond: false, foo: 3, bar: 55 },
55
+ { cond: true, foo: 3, bar: 55 },
56
+ ],
57
+};
58
+
59
```
47
-
\ No newline at end of file
60
+
61
+### Eval output
62
+(kind: ok) [55,4]
63
+[55,4]
64
+[3,4]
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/ssa-renaming-unconditional-ternary-with-mutation.js
+12
-1
@@ -1,9 +1,20 @@
1
+import { arrayPush } from "shared-runtime";
2
function foo(props) {
3
let x = [];
4
x.push(props.bar);
5
props.cond
6
? ((x = {}), (x = []), x.push(props.foo))
7
: ((x = []), (x = []), x.push(props.bar));
7
- mut(x);
8
+ arrayPush(x, 4);
9
return x;
10
}
11
+
12
+export const FIXTURE_ENTRYPOINT = {
13
+ fn: foo,
14
+ params: [{ cond: false, foo: 2, bar: 55 }],
15
+ sequentialRenders: [
16
+ { cond: false, foo: 2, bar: 55 },
17
+ { cond: false, foo: 3, bar: 55 },
18
+ { cond: true, foo: 3, bar: 55 },
19
+ ],
20
+};
compiler/packages/snap/src/SproutTodoFilter.ts
-1
@@ -334,7 +334,6 @@ const skipFilter = new Set([
334
"ssa-property-alias-mutate-inside-if",
335
"ssa-renaming-ternary-destruction-with-mutation",
336
"ssa-renaming-ternary-with-mutation",
337
- "ssa-renaming-unconditional-ternary-with-mutation",
337
"ssa-renaming-unconditional-with-mutation",
338
"ssa-renaming-via-destructuring-with-mutation",
339
"ssa-renaming-with-mutation",