Skip to content

Commit 86d1a6f

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

29 files changed

+841
-411
lines changed

compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/AlignReactiveScopesToBlockScopesHIR.ts

Lines changed: 85 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@ import {
2222
mapTerminalSuccessors,
2323
terminalFallthrough,
2424
} from "../HIR/visitors";
25+
import { retainWhere_Set } from "../Utils/utils";
2526
import { getPlaceScope } from "./BuildReactiveBlocks";
2627

28+
type InstructionRange = MutableRange;
2729
/*
2830
* Note: this is the 2nd of 4 passes that determine how to break a function into discrete
2931
* reactive scopes (independently memoizeable units of code):
@@ -66,18 +68,20 @@ import { getPlaceScope } from "./BuildReactiveBlocks";
6668
* will be the updated end for that scope).
6769
*/
6870
export function alignReactiveScopesToBlockScopesHIR(fn: HIRFunction): void {
69-
const blockNodes = new Map<BlockId, BlockNode>();
70-
const rootNode: BlockNode = {
71-
kind: "node",
72-
valueRange: null,
73-
children: [],
74-
id: makeInstructionId(0),
75-
};
76-
blockNodes.set(fn.body.entry, rootNode);
71+
const activeBlockFallthroughRanges: Array<{
72+
range: InstructionRange;
73+
fallthrough: BlockId;
74+
}> = [];
75+
const activeScopes = new Set<ReactiveScope>();
7776
const seen = new Set<ReactiveScope>();
77+
const valueBlockNodes = new Map<BlockId, ValueBlockNode>();
7878
const placeScopes = new Map<Place, ReactiveScope>();
7979

80-
function recordPlace(id: InstructionId, place: Place, node: BlockNode): void {
80+
function recordPlace(
81+
id: InstructionId,
82+
place: Place,
83+
node: ValueBlockNode | null
84+
): void {
8185
if (place.identifier.scope !== null) {
8286
placeScopes.set(place, place.identifier.scope);
8387
}
@@ -86,13 +90,14 @@ export function alignReactiveScopesToBlockScopesHIR(fn: HIRFunction): void {
8690
if (scope == null) {
8791
return;
8892
}
89-
node.children.push({ kind: "scope", scope, id });
93+
activeScopes.add(scope);
94+
node?.children.push({ kind: "scope", scope, id });
9095

9196
if (seen.has(scope)) {
9297
return;
9398
}
9499
seen.add(scope);
95-
if (node.valueRange !== null) {
100+
if (node != null && node.valueRange !== null) {
96101
scope.range.start = makeInstructionId(
97102
Math.min(node.valueRange.start, scope.range.start)
98103
);
@@ -103,16 +108,25 @@ export function alignReactiveScopesToBlockScopesHIR(fn: HIRFunction): void {
103108
}
104109

105110
for (const [, block] of fn.body.blocks) {
106-
const { instructions, terminal } = block;
107-
const node = blockNodes.get(block.id);
108-
if (node === undefined) {
109-
CompilerError.invariant(false, {
110-
reason: `Expected a node to be initialized for block`,
111-
loc: instructions[0]?.loc ?? terminal.loc,
112-
description: `No node for block bb${block.id} (${block.kind})`,
113-
});
111+
const startingId = block.instructions[0]?.id ?? block.terminal.id;
112+
retainWhere_Set(activeScopes, (scope) => scope.range.end > startingId);
113+
const top = activeBlockFallthroughRanges.at(-1);
114+
if (top?.fallthrough === block.id) {
115+
activeBlockFallthroughRanges.pop();
116+
/*
117+
* All active scopes must have either started before or within the last
118+
* block-fallthrough range. In either case, they overlap this block-
119+
* fallthrough range and can have their ranges extended.
120+
*/
121+
for (const scope of activeScopes) {
122+
scope.range.start = makeInstructionId(
123+
Math.min(scope.range.start, top.range.start)
124+
);
125+
}
114126
}
115127

128+
const { instructions, terminal } = block;
129+
const node = valueBlockNodes.get(block.id) ?? null;
116130
for (const instr of instructions) {
117131
for (const lvalue of eachInstructionLValue(instr)) {
118132
recordPlace(instr.id, lvalue, node);
@@ -125,36 +139,42 @@ export function alignReactiveScopesToBlockScopesHIR(fn: HIRFunction): void {
125139
recordPlace(terminal.id, operand, node);
126140
}
127141

128-
// Save the current node for the fallback block, where this block scope continues
129142
const fallthrough = terminalFallthrough(terminal);
130-
if (fallthrough !== null && !blockNodes.has(fallthrough)) {
143+
if (fallthrough !== null) {
131144
/*
132-
* Any scopes that carried over across a terminal->fallback need their range extended
133-
* to at least the first instruction of the fallback
134-
*
135-
* Note that it's possible for a terminal such as an if or switch to have a null fallback,
136-
* indicating that all control-flow paths diverge instead of reaching the fallthrough.
137-
* In this case there isn't an instruction id in the program that we can point to for the
138-
* updated range. Since the output is correct in this case we leave it, but it would be
139-
* more correct to find the maximum instuction id in the whole program and set the range.end
140-
* to one greater. Alternatively, we could leave in an unreachable fallthrough (with a new
141-
* "unreachable" terminal variant, perhaps) and use that instruction id.
145+
* Any currently active scopes that overlaps the block-fallthrough range
146+
* need their range extended to at least the first instruction of the
147+
* fallthrough
142148
*/
143149
const fallthroughBlock = fn.body.blocks.get(fallthrough)!;
144150
const nextId =
145151
fallthroughBlock.instructions[0]?.id ?? fallthroughBlock.terminal.id;
146-
for (const child of node.children) {
147-
if (child.kind !== "scope") {
148-
continue;
149-
}
150-
const scope = child.scope;
152+
for (const scope of activeScopes) {
151153
if (scope.range.end > terminal.id) {
152154
scope.range.end = makeInstructionId(
153155
Math.max(scope.range.end, nextId)
154156
);
155157
}
156158
}
157-
blockNodes.set(fallthrough, node);
159+
/**
160+
* We also record the block-fallthrough range for future scopes that begin
161+
* within the range (and overlap with the range end).
162+
*/
163+
activeBlockFallthroughRanges.push({
164+
fallthrough,
165+
range: {
166+
start: terminal.id,
167+
end: nextId,
168+
},
169+
});
170+
171+
CompilerError.invariant(!valueBlockNodes.has(fallthrough), {
172+
reason: "Expect hir blocks to have unique fallthroughs",
173+
loc: terminal.loc,
174+
});
175+
if (node != null) {
176+
valueBlockNodes.set(fallthrough, node);
177+
}
158178
}
159179

160180
/*
@@ -166,48 +186,35 @@ export function alignReactiveScopesToBlockScopesHIR(fn: HIRFunction): void {
166186
* just those that are direct successors for normal control-flow ordering.
167187
*/
168188
mapTerminalSuccessors(terminal, (successor) => {
169-
if (blockNodes.has(successor)) {
189+
if (valueBlockNodes.has(successor)) {
170190
return successor;
171191
}
172192

173193
const successorBlock = fn.body.blocks.get(successor)!;
174-
/*
175-
* we need the block kind check here because the do..while terminal's successor
176-
* is a block, and try's successor is a catch block
177-
*/
178194
if (successorBlock.kind === "block" || successorBlock.kind === "catch") {
179-
const childNode: BlockNode = {
180-
kind: "node",
181-
id: terminal.id,
182-
children: [],
183-
valueRange: null,
184-
};
185-
node.children.push(childNode);
186-
blockNodes.set(successor, childNode);
195+
/*
196+
* we need the block kind check here because the do..while terminal's
197+
* successor is a block, and try's successor is a catch block
198+
*/
187199
} else if (
188-
node.valueRange === null ||
200+
node == null ||
189201
terminal.kind === "ternary" ||
190202
terminal.kind === "logical" ||
191203
terminal.kind === "optional"
192204
) {
193205
/**
194-
* Create a new scope node whenever we transition from block scope -> value scope.
206+
* Create a new node whenever we transition from non-value -> value block.
195207
*
196208
* For compatibility with the previous ReactiveFunction-based scope merging logic,
197209
* we also create new scope nodes for ternary, logical, and optional terminals.
198-
* However, inside value blocks we always store a range (valueRange) that is the
210+
* Inside value blocks we always store a range (valueRange) that is the
199211
* start/end instruction ids at the nearest parent block scope level, so that
200212
* scopes inside the value blocks can be extended to align with block scope
201213
* instructions.
202214
*/
203-
const childNode = {
204-
kind: "node",
205-
id: terminal.id,
206-
children: [],
207-
valueRange: null,
208-
} as BlockNode;
209-
if (node.valueRange === null) {
210-
// Transition from block->value scope, derive the outer block scope range
215+
let valueRange: MutableRange;
216+
if (node == null) {
217+
// Transition from block->value block, derive the outer block range
211218
CompilerError.invariant(fallthrough !== null, {
212219
reason: `Expected a fallthrough for value block`,
213220
loc: terminal.loc,
@@ -216,46 +223,50 @@ export function alignReactiveScopesToBlockScopesHIR(fn: HIRFunction): void {
216223
const nextId =
217224
fallthroughBlock.instructions[0]?.id ??
218225
fallthroughBlock.terminal.id;
219-
childNode.valueRange = {
226+
valueRange = {
220227
start: terminal.id,
221228
end: nextId,
222229
};
223230
} else {
224231
// else value->value transition, reuse the range
225-
childNode.valueRange = node.valueRange;
232+
valueRange = node.valueRange;
226233
}
227-
node.children.push(childNode);
228-
blockNodes.set(successor, childNode);
234+
const childNode: ValueBlockNode = {
235+
kind: "node",
236+
id: terminal.id,
237+
children: [],
238+
valueRange,
239+
};
240+
node?.children.push(childNode);
241+
valueBlockNodes.set(successor, childNode);
229242
} else {
230243
// this is a value -> value block transition, reuse the node
231-
blockNodes.set(successor, node);
244+
valueBlockNodes.set(successor, node);
232245
}
233246
return successor;
234247
});
235248
}
236-
237-
// console.log(_debug(rootNode));
238249
}
239250

240-
type BlockNode = {
251+
type ValueBlockNode = {
241252
kind: "node";
242253
id: InstructionId;
243-
valueRange: MutableRange | null;
244-
children: Array<BlockNode | ReactiveScopeNode>;
254+
valueRange: MutableRange;
255+
children: Array<ValueBlockNode | ReactiveScopeNode>;
245256
};
246257
type ReactiveScopeNode = {
247258
kind: "scope";
248259
id: InstructionId;
249260
scope: ReactiveScope;
250261
};
251262

252-
function _debug(node: BlockNode): string {
263+
function _debug(node: ValueBlockNode): string {
253264
const buf: Array<string> = [];
254265
_printNode(node, buf, 0);
255266
return buf.join("\n");
256267
}
257268
function _printNode(
258-
node: BlockNode | ReactiveScopeNode,
269+
node: ValueBlockNode | ReactiveScopeNode,
259270
out: Array<string>,
260271
depth: number = 0
261272
): void {
@@ -265,10 +276,7 @@ function _printNode(
265276
`${prefix}[${node.id}] @${node.scope.id} [${node.scope.range.start}:${node.scope.range.end}]`
266277
);
267278
} else {
268-
let range =
269-
node.valueRange !== null
270-
? ` [${node.valueRange.start}:${node.valueRange.end}]`
271-
: "";
279+
let range = ` (range=[${node.valueRange.start}:${node.valueRange.end}])`;
272280
out.push(`${prefix}[${node.id}] node${range} [`);
273281
for (const child of node.children) {
274282
_printNode(child, out, depth + 1);

compiler/packages/babel-plugin-react-compiler/src/Utils/utils.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,17 @@ export function retainWhere<T>(
4545
array.length = writeIndex;
4646
}
4747

48+
export function retainWhere_Set<T>(
49+
items: Set<T>,
50+
predicate: (item: T) => boolean
51+
): void {
52+
for (const item of items) {
53+
if (!predicate(item)) {
54+
items.delete(item);
55+
}
56+
}
57+
}
58+
4859
export function getOrInsertWith<U, V>(
4960
m: Map<U, V>,
5061
key: U,
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
2+
## Input
3+
4+
```javascript
5+
import { mutate } from "shared-runtime";
6+
7+
/**
8+
* Similar fixture to `align-scopes-nested-block-structure`, but
9+
* a simpler case.
10+
*/
11+
function useFoo(cond) {
12+
let s = null;
13+
if (cond) {
14+
s = {};
15+
} else {
16+
return null;
17+
}
18+
mutate(s);
19+
return s;
20+
}
21+
22+
export const FIXTURE_ENTRYPOINT = {
23+
fn: useFoo,
24+
params: [true],
25+
};
26+
27+
```
28+
29+
## Code
30+
31+
```javascript
32+
import { c as _c } from "react/compiler-runtime";
33+
import { mutate } from "shared-runtime";
34+
35+
/**
36+
* Similar fixture to `align-scopes-nested-block-structure`, but
37+
* a simpler case.
38+
*/
39+
function useFoo(cond) {
40+
const $ = _c(3);
41+
let s;
42+
let t0;
43+
if ($[0] !== cond) {
44+
t0 = Symbol.for("react.early_return_sentinel");
45+
bb0: {
46+
if (cond) {
47+
s = {};
48+
} else {
49+
t0 = null;
50+
break bb0;
51+
}
52+
53+
mutate(s);
54+
}
55+
$[0] = cond;
56+
$[1] = t0;
57+
$[2] = s;
58+
} else {
59+
t0 = $[1];
60+
s = $[2];
61+
}
62+
if (t0 !== Symbol.for("react.early_return_sentinel")) {
63+
return t0;
64+
}
65+
return s;
66+
}
67+
68+
export const FIXTURE_ENTRYPOINT = {
69+
fn: useFoo,
70+
params: [true],
71+
};
72+
73+
```
74+
75+
### Eval output
76+
(kind: ok) {"wat0":"joe"}

0 commit comments

Comments
 (0)