Skip to content

Commit 2162d08

Browse files
committed
[compiler][draft] Patch logic for aligning scopes to non-value blocks
ghstack-source-id: 18fa632 Pull Request resolved: #29891
1 parent 546b4bf commit 2162d08

28 files changed

+826
-411
lines changed

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

Lines changed: 92 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,17 @@ import {
2424
} from "../HIR/visitors";
2525
import { getPlaceScope } from "./BuildReactiveBlocks";
2626

27+
type InstructionRange = MutableRange;
28+
function retainWhere_Set<T>(
29+
items: Set<T>,
30+
predicate: (item: T) => boolean
31+
): void {
32+
for (const item of items) {
33+
if (!predicate(item)) {
34+
items.delete(item);
35+
}
36+
}
37+
}
2738
/*
2839
* Note: this is the 2nd of 4 passes that determine how to break a function into discrete
2940
* reactive scopes (independently memoizeable units of code):
@@ -66,18 +77,20 @@ import { getPlaceScope } from "./BuildReactiveBlocks";
6677
* will be the updated end for that scope).
6778
*/
6879
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);
80+
const activeInnerBlockRanges: Array<{
81+
range: InstructionRange;
82+
fallthrough: BlockId;
83+
}> = [];
84+
const activeScopes = new Set<ReactiveScope>();
7785
const seen = new Set<ReactiveScope>();
86+
const valueBlockNodes = new Map<BlockId, ValueBlockNode>();
7887
const placeScopes = new Map<Place, ReactiveScope>();
7988

80-
function recordPlace(id: InstructionId, place: Place, node: BlockNode): void {
89+
function recordPlace(
90+
id: InstructionId,
91+
place: Place,
92+
node: ValueBlockNode | null
93+
): void {
8194
if (place.identifier.scope !== null) {
8295
placeScopes.set(place, place.identifier.scope);
8396
}
@@ -86,13 +99,14 @@ export function alignReactiveScopesToBlockScopesHIR(fn: HIRFunction): void {
8699
if (scope == null) {
87100
return;
88101
}
89-
node.children.push({ kind: "scope", scope, id });
102+
activeScopes.add(scope);
103+
node?.children.push({ kind: "scope", scope, id });
90104

91105
if (seen.has(scope)) {
92106
return;
93107
}
94108
seen.add(scope);
95-
if (node.valueRange !== null) {
109+
if (node != null && node.valueRange !== null) {
96110
scope.range.start = makeInstructionId(
97111
Math.min(node.valueRange.start, scope.range.start)
98112
);
@@ -103,16 +117,23 @@ export function alignReactiveScopesToBlockScopesHIR(fn: HIRFunction): void {
103117
}
104118

105119
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-
});
120+
const startingId = block.instructions[0]?.id ?? block.terminal.id;
121+
retainWhere_Set(activeScopes, (scope) => scope.range.end >= startingId);
122+
const top = activeInnerBlockRanges.at(-1);
123+
if (top?.fallthrough === block.id) {
124+
activeInnerBlockRanges.pop();
125+
// All active scopes must have either started before or within the last
126+
// block-fallthrough range. In either case, they overlap this block-
127+
// fallthrough range and can have their ranges extended.
128+
for (const scope of activeScopes) {
129+
scope.range.start = makeInstructionId(
130+
Math.min(scope.range.start, top.range.start)
131+
);
132+
}
114133
}
115134

135+
const { instructions, terminal } = block;
136+
const node = valueBlockNodes.get(block.id) ?? null;
116137
for (const instr of instructions) {
117138
for (const lvalue of eachInstructionLValue(instr)) {
118139
recordPlace(instr.id, lvalue, node);
@@ -125,36 +146,42 @@ export function alignReactiveScopesToBlockScopesHIR(fn: HIRFunction): void {
125146
recordPlace(terminal.id, operand, node);
126147
}
127148

128-
// Save the current node for the fallback block, where this block scope continues
129149
const fallthrough = terminalFallthrough(terminal);
130-
if (fallthrough !== null && !blockNodes.has(fallthrough)) {
150+
if (fallthrough !== null) {
131151
/*
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.
152+
* Any currently active scopes that overlaps the block-fallthrough range
153+
* need their range extended to at least the first instruction of the
154+
* fallthrough
142155
*/
143156
const fallthroughBlock = fn.body.blocks.get(fallthrough)!;
144157
const nextId =
145158
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;
159+
for (const scope of activeScopes) {
151160
if (scope.range.end > terminal.id) {
152161
scope.range.end = makeInstructionId(
153162
Math.max(scope.range.end, nextId)
154163
);
155164
}
156165
}
157-
blockNodes.set(fallthrough, node);
166+
/**
167+
* We also record the block-fallthrough range for future scopes that begin
168+
* within the range (and overlap with the range end).
169+
*/
170+
activeInnerBlockRanges.push({
171+
fallthrough,
172+
range: {
173+
start: terminal.id,
174+
end: nextId,
175+
},
176+
});
177+
178+
CompilerError.invariant(!valueBlockNodes.has(fallthrough), {
179+
reason: "Expect hir blocks to have unique fallthroughs",
180+
loc: terminal.loc,
181+
});
182+
if (node != null) {
183+
valueBlockNodes.set(fallthrough, node);
184+
}
158185
}
159186

160187
/*
@@ -166,48 +193,35 @@ export function alignReactiveScopesToBlockScopesHIR(fn: HIRFunction): void {
166193
* just those that are direct successors for normal control-flow ordering.
167194
*/
168195
mapTerminalSuccessors(terminal, (successor) => {
169-
if (blockNodes.has(successor)) {
196+
if (valueBlockNodes.has(successor)) {
170197
return successor;
171198
}
172199

173200
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-
*/
178201
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);
202+
/*
203+
* we need the block kind check here because the do..while terminal's
204+
* successor is a block, and try's successor is a catch block
205+
*/
187206
} else if (
188-
node.valueRange === null ||
207+
node == null ||
189208
terminal.kind === "ternary" ||
190209
terminal.kind === "logical" ||
191210
terminal.kind === "optional"
192211
) {
193212
/**
194-
* Create a new scope node whenever we transition from block scope -> value scope.
213+
* Create a new node whenever we transition from non-value -> value block.
195214
*
196215
* For compatibility with the previous ReactiveFunction-based scope merging logic,
197216
* 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
217+
* Inside value blocks we always store a range (valueRange) that is the
199218
* start/end instruction ids at the nearest parent block scope level, so that
200219
* scopes inside the value blocks can be extended to align with block scope
201220
* instructions.
202221
*/
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
222+
let valueRange: MutableRange;
223+
if (node == null) {
224+
// Transition from block->value block, derive the outer block range
211225
CompilerError.invariant(fallthrough !== null, {
212226
reason: `Expected a fallthrough for value block`,
213227
loc: terminal.loc,
@@ -216,46 +230,50 @@ export function alignReactiveScopesToBlockScopesHIR(fn: HIRFunction): void {
216230
const nextId =
217231
fallthroughBlock.instructions[0]?.id ??
218232
fallthroughBlock.terminal.id;
219-
childNode.valueRange = {
233+
valueRange = {
220234
start: terminal.id,
221235
end: nextId,
222236
};
223237
} else {
224238
// else value->value transition, reuse the range
225-
childNode.valueRange = node.valueRange;
239+
valueRange = node.valueRange;
226240
}
227-
node.children.push(childNode);
228-
blockNodes.set(successor, childNode);
241+
const childNode: ValueBlockNode = {
242+
kind: "node",
243+
id: terminal.id,
244+
children: [],
245+
valueRange,
246+
};
247+
node?.children.push(childNode);
248+
valueBlockNodes.set(successor, childNode);
229249
} else {
230250
// this is a value -> value block transition, reuse the node
231-
blockNodes.set(successor, node);
251+
valueBlockNodes.set(successor, node);
232252
}
233253
return successor;
234254
});
235255
}
236-
237-
// console.log(_debug(rootNode));
238256
}
239257

240-
type BlockNode = {
258+
type ValueBlockNode = {
241259
kind: "node";
242260
id: InstructionId;
243-
valueRange: MutableRange | null;
244-
children: Array<BlockNode | ReactiveScopeNode>;
261+
valueRange: MutableRange;
262+
children: Array<ValueBlockNode | ReactiveScopeNode>;
245263
};
246264
type ReactiveScopeNode = {
247265
kind: "scope";
248266
id: InstructionId;
249267
scope: ReactiveScope;
250268
};
251269

252-
function _debug(node: BlockNode): string {
270+
function _debug(node: ValueBlockNode): string {
253271
const buf: Array<string> = [];
254272
_printNode(node, buf, 0);
255273
return buf.join("\n");
256274
}
257275
function _printNode(
258-
node: BlockNode | ReactiveScopeNode,
276+
node: ValueBlockNode | ReactiveScopeNode,
259277
out: Array<string>,
260278
depth: number = 0
261279
): void {
@@ -265,10 +283,7 @@ function _printNode(
265283
`${prefix}[${node.id}] @${node.scope.id} [${node.scope.range.start}:${node.scope.range.end}]`
266284
);
267285
} else {
268-
let range =
269-
node.valueRange !== null
270-
? ` [${node.valueRange.start}:${node.valueRange.end}]`
271-
: "";
286+
let range = ` (range=[${node.valueRange.start}:${node.valueRange.end}])`;
272287
out.push(`${prefix}[${node.id}] node${range} [`);
273288
for (const child of node.children) {
274289
_printNode(child, out, depth + 1);
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"}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { mutate } from "shared-runtime";
2+
3+
/**
4+
* Similar fixture to `align-scopes-nested-block-structure`, but
5+
* a simpler case.
6+
*/
7+
function useFoo(cond) {
8+
let s = null;
9+
if (cond) {
10+
s = {};
11+
} else {
12+
return null;
13+
}
14+
mutate(s);
15+
return s;
16+
}
17+
18+
export const FIXTURE_ENTRYPOINT = {
19+
fn: useFoo,
20+
params: [true],
21+
};

0 commit comments

Comments
 (0)