@samitouri / QOS-React / commits / a44559b0a5

[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",