22
mapTerminalSuccessors,
23
terminalFallthrough,
24
} from "../HIR/visitors";
25
+import { retainWhere_Set } from "../Utils/utils";
26
import { getPlaceScope } from "./BuildReactiveBlocks";
27
28
+type InstructionRange = MutableRange;
29
/*
30
* Note: this is the 2nd of 4 passes that determine how to break a function into discrete
31
* reactive scopes (independently memoizeable units of code):
68
* will be the updated end for that scope).
69
*/
70
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>();
76
const seen = new Set<ReactiveScope>();
77
+ const valueBlockNodes = new Map<BlockId, ValueBlockNode>();
78
const placeScopes = new Map<Place, ReactiveScope>();
79
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 {
85
if (place.identifier.scope !== null) {
86
placeScopes.set(place, place.identifier.scope);
87
}
90
if (scope == null) {
91
return;
92
}
89
- node.children.push({ kind: "scope", scope, id });
93
+ activeScopes.add(scope);
94
+ node?.children.push({ kind: "scope", scope, id });
95
96
if (seen.has(scope)) {
97
return;
98
}
99
seen.add(scope);
95
- if (node.valueRange !== null) {
100
+ if (node != null && node.valueRange !== null) {
101
scope.range.start = makeInstructionId(
102
Math.min(node.valueRange.start, scope.range.start)
103
);
108
}
109
110
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
+ }
126
}
127
128
+ const { instructions, terminal } = block;
129
+ const node = valueBlockNodes.get(block.id) ?? null;
130
for (const instr of instructions) {
131
for (const lvalue of eachInstructionLValue(instr)) {
132
recordPlace(instr.id, lvalue, node);
139
recordPlace(terminal.id, operand, node);
140
}
141
128
- // Save the current node for the fallback block, where this block scope continues
142
const fallthrough = terminalFallthrough(terminal);
130
- if (fallthrough !== null && !blockNodes.has(fallthrough)) {
143
+ if (fallthrough !== null) {
144
/*
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
148
*/
149
const fallthroughBlock = fn.body.blocks.get(fallthrough)!;
150
const nextId =
151
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) {
153
if (scope.range.end > terminal.id) {
154
scope.range.end = makeInstructionId(
155
Math.max(scope.range.end, nextId)
156
);
157
}
158
}
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
+ }
178
}
179
180
/*
186
* just those that are direct successors for normal control-flow ordering.
187
*/
188
mapTerminalSuccessors(terminal, (successor) => {
169
- if (blockNodes.has(successor)) {
189
+ if (valueBlockNodes.has(successor)) {
190
return successor;
191
}
192
193
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
- */
194
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
+ */
199
} else if (
188
- node.valueRange === null ||
200
+ node == null ||
201
terminal.kind === "ternary" ||
202
terminal.kind === "logical" ||
203
terminal.kind === "optional"
204
) {
205
/**
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.
207
*
208
* For compatibility with the previous ReactiveFunction-based scope merging logic,
209
* 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
211
* start/end instruction ids at the nearest parent block scope level, so that
212
* scopes inside the value blocks can be extended to align with block scope
213
* instructions.
214
*/
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
218
CompilerError.invariant(fallthrough !== null, {
219
reason: `Expected a fallthrough for value block`,
220
loc: terminal.loc,
223
const nextId =
224
fallthroughBlock.instructions[0]?.id ??
225
fallthroughBlock.terminal.id;
219
- childNode.valueRange = {
226
+ valueRange = {
227
start: terminal.id,
228
end: nextId,
229
};
230
} else {
231
// else value->value transition, reuse the range
225
- childNode.valueRange = node.valueRange;
232
+ valueRange = node.valueRange;
233
}
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);
242
} else {
243
// this is a value -> value block transition, reuse the node
231
- blockNodes.set(successor, node);
244
+ valueBlockNodes.set(successor, node);
245
}
246
return successor;
247
});
248
}
236
-
237
- // console.log(_debug(rootNode));
249
}
250
240
-type BlockNode = {
251
+type ValueBlockNode = {
252
kind: "node";
253
id: InstructionId;
243
- valueRange: MutableRange | null;
244
- children: Array<BlockNode | ReactiveScopeNode>;
254
+ valueRange: MutableRange;
255
+ children: Array<ValueBlockNode | ReactiveScopeNode>;
256
};
257
type ReactiveScopeNode = {
258
kind: "scope";
260
scope: ReactiveScope;
261
};
262
252
-function _debug(node: BlockNode): string {
263
+function _debug(node: ValueBlockNode): string {
264
const buf: Array<string> = [];
265
_printNode(node, buf, 0);
266
return buf.join("\n");
267
}
268
function _printNode(
258
- node: BlockNode | ReactiveScopeNode,
269
+ node: ValueBlockNode | ReactiveScopeNode,
270
out: Array<string>,
271
depth: number = 0
272
): void {
276
`${prefix}[${node.id}] @${node.scope.id} [${node.scope.range.start}:${node.scope.range.end}]`
277
);
278
} 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}])`;
280
out.push(`${prefix}[${node.id}] node${range} [`);
281
for (const child of node.children) {
282
_printNode(child, out, depth + 1);