78
#scheduled: Set<BlockId> = new Set();
79
80
/**
81
- * A stack of blocks that are in scope, used to decide whether/how to emit
82
- * break and continue statements. All blocks in the stack must also be
83
- * in 'scheduled'.
81
+ * Represents which control flow operations are currently in scope, with the innermost
82
+ * scope last. Roughly speaking, the last ControlFlowTarget on the stack indicates where
83
+ * control will implicitly transfer, such that gotos to that block can be elided. Gotos
84
+ * targeting items higher up the stack may need labeled break or continue; see
85
+ * getBreakTarget() and getContinueTarget() for more details.
86
*/
85
- #breakTargets: Array<BreakTarget> = [];
87
+ #controlFlowStack: Array<ControlFlowTarget> = [];
88
89
constructor(ir: HIR) {
90
this.ir = ir;
98
const id = this.#nextScheduleId++;
99
invariant(!this.#scheduled.has(block), "Block is already scheduled");
100
this.#scheduled.add(block);
99
- this.#breakTargets.push({ block, id, type });
101
+ this.#controlFlowStack.push({ block, id, type });
102
+ return id;
103
+ }
104
+
105
+ scheduleLoop(
106
+ fallthroughBlock: BlockId,
107
+ continueBlock: BlockId,
108
+ loopBlock: BlockId | null
109
+ ): number {
110
+ const id = this.#nextScheduleId++;
111
+ const ownsBlock = !this.#scheduled.has(fallthroughBlock);
112
+ this.#scheduled.add(fallthroughBlock);
113
+ invariant(
114
+ !this.#scheduled.has(continueBlock),
115
+ "Block is already scheduled"
116
+ );
117
+ this.#scheduled.add(continueBlock);
118
+ if (loopBlock !== null) {
119
+ invariant(!this.#scheduled.has(loopBlock), "Block is already scheduled");
120
+ this.#scheduled.add(loopBlock);
121
+ }
122
+
123
+ this.#controlFlowStack.push({
124
+ block: fallthroughBlock,
125
+ ownsBlock,
126
+ id,
127
+ type: "loop",
128
+ continueBlock,
129
+ loopBlock,
130
+ });
131
return id;
132
}
133
135
* Removes a block that was scheduled; must be called after that block is emitted.
136
*/
137
unschedule(scheduleId: number): void {
107
- const last = this.#breakTargets.pop();
138
+ const last = this.#controlFlowStack.pop();
139
invariant(
140
last !== undefined && last.id === scheduleId,
141
"Can only unschedule the last target"
142
);
112
- this.#scheduled.delete(last.block);
143
+ if (last.type !== "loop" || last.ownsBlock !== null) {
144
+ this.#scheduled.delete(last.block);
145
+ }
146
+ if (last.type === "loop") {
147
+ this.#scheduled.delete(last.continueBlock);
148
+ if (last.loopBlock !== null) {
149
+ this.#scheduled.delete(last.loopBlock);
150
+ }
151
+ }
152
}
153
154
/**
169
}
170
171
/**
133
- * Lookup the break target for the given @param block. This will return non-null
134
- * if and only if isScheduled() returns true for the given @param block. Returns
135
- * the break target and whether this is the most recent target (which can be used
136
- * to elide unnecessary break statemetns).
172
+ * Given the current control flow stack, determines how a `break` to the given @param block
173
+ * must be emitted. Returns as follows:
174
+ * - 'implicit' if control would implicitly transfer to that block
175
+ * - 'labeled' if a labeled break is required to transfer control to that block
176
+ * - 'unlabeled' if an unlabeled break would transfer to that block
177
+ * - null if there is no information for this block
178
+ *
179
+ * The returned 'block' value should be used as the label if necessary.
180
*/
181
getBreakTarget(
182
block: BlockId
140
- ): { target: BreakTarget; last: boolean } | null {
141
- for (let i = this.#breakTargets.length - 1; i >= 0; i--) {
142
- const target = this.#breakTargets[i]!;
183
+ ): { block: BlockId; type: ControlFlowKind } | null {
184
+ let hasPrecedingLoop = false;
185
+ for (let i = this.#controlFlowStack.length - 1; i >= 0; i--) {
186
+ const target = this.#controlFlowStack[i]!;
187
if (target.block === block) {
188
+ let type: ControlFlowKind;
189
+ if (target.type === "loop") {
190
+ // breaking out of a loop requires an explicit break,
191
+ // but only requires a label if breaking past the innermost loop.
192
+ type = hasPrecedingLoop ? "labeled" : "unlabeled";
193
+ } else if (i === this.#controlFlowStack.length - 1) {
194
+ // breaking to the last break point, which is where control will transfer
195
+ // implicitly
196
+ type = "implicit";
197
+ } else {
198
+ // breaking somewhere else requires an explicit break
199
+ type = "labeled";
200
+ }
201
+ return {
202
+ block: target.block,
203
+ type,
204
+ };
205
+ }
206
+ hasPrecedingLoop ||= target.type === "loop";
207
+ }
208
+ return null;
209
+ }
210
+
211
+ /**
212
+ * Given the current control flow stack, determines how a `continue` to the given @param block
213
+ * must be emitted. Returns as follows:
214
+ * - 'implicit' if control would implicitly continue to that block
215
+ * - 'labeled' if a labeled continue is required to continue to that block
216
+ * - 'unlabeled' if an unlabeled continue would transfer to that block
217
+ * - null if there is no information for this block
218
+ *
219
+ * The returned 'block' value should be used as the label if necessary.
220
+ */
221
+ getContinueTarget(
222
+ block: BlockId
223
+ ): { block: BlockId; type: ControlFlowKind } | null {
224
+ let hasPrecedingLoop = false;
225
+ for (let i = this.#controlFlowStack.length - 1; i >= 0; i--) {
226
+ const target = this.#controlFlowStack[i]!;
227
+ if (target.type == "loop" && target.continueBlock === block) {
228
+ let type: ControlFlowKind;
229
+ if (hasPrecedingLoop) {
230
+ // continuing to a loop that is not the innermost loop always requires
231
+ // a label
232
+ type = "labeled";
233
+ } else if (i === this.#controlFlowStack.length - 1) {
234
+ // continuing to the last break point, which is where control will
235
+ // transfer to naturally
236
+ type = "implicit";
237
+ } else {
238
+ // the continue is inside some conditional logic, requires an explicit
239
+ // continue
240
+ type = "unlabeled";
241
+ }
242
return {
145
- target,
146
- last: i === this.#breakTargets.length - 1,
243
+ block: target.block,
244
+ type,
245
};
246
}
247
+ hasPrecedingLoop ||= target.type === "loop";
248
}
249
return null;
250
}
251
+
252
+ debugBreakTargets(): Array<ControlFlowTarget> {
253
+ return this.#controlFlowStack.map((target) => ({ ...target }));
254
+ }
255
}
256
154
-type BreakTarget = {
155
- block: BlockId;
156
- id: number;
157
- type: "if" | "switch" | "case";
158
-};
257
+type ControlFlowKind = "implicit" | "labeled" | "unlabeled";
258
+
259
+type ControlFlowTarget =
260
+ | { type: "if"; block: BlockId; id: number }
261
+ | { type: "switch"; block: BlockId; id: number }
262
+ | { type: "case"; block: BlockId; id: number }
263
+ | {
264
+ type: "loop";
265
+ block: BlockId;
266
+ ownsBlock: boolean;
267
+ continueBlock: BlockId;
268
+ loopBlock: BlockId | null;
269
+ id: number;
270
+ };
271
272
function codegenBlock(cx: Context, block: BasicBlock): t.BlockStatement {
273
invariant(
328
}
329
}
330
331
+ cx.unscheduleAll(scheduleIds);
332
if (fallthroughId !== null) {
333
if (consequent === null && alternate === null) {
334
body.push(t.expressionStatement(test));
399
});
400
cases.reverse();
401
402
+ cx.unscheduleAll(scheduleIds);
403
if (fallthroughId !== null) {
404
body.push(
405
t.labeledStatement(
413
}
414
break;
415
}
416
+ case "while": {
417
+ const testBlock = cx.ir.blocks.get(terminal.test)!;
418
+ const testTerminal = testBlock.terminal;
419
+ invariant(
420
+ testTerminal.kind === "if",
421
+ "Expected while loop test block to end in an if"
422
+ );
423
+ const bodyLength = body.length;
424
+ for (const instr of testBlock.instructions) {
425
+ writeInstr(cx, instr, body);
426
+ }
427
+ invariant(
428
+ body.length === bodyLength,
429
+ "Expected test to produce only temporaries"
430
+ );
431
+ const testValue =
432
+ cx.temp.get(testTerminal.test.identifier.id) ??
433
+ codegenPlace(cx, testTerminal.test);
434
+ invariant(
435
+ testValue != null,
436
+ "Expected test to produce a temporary value"
437
+ );
438
+
439
+ const fallthroughId =
440
+ terminal.fallthrough !== null && !cx.isScheduled(terminal.fallthrough)
441
+ ? terminal.fallthrough
442
+ : null;
443
+ const scheduleId = cx.scheduleLoop(
444
+ terminal.fallthrough,
445
+ terminal.test,
446
+ terminal.loop
447
+ );
448
+ scheduleIds.push(scheduleId);
449
+
450
+ let loopBody: t.Statement;
451
+ if (terminal.loop !== null) {
452
+ loopBody = codegenBlock(cx, cx.ir.blocks.get(terminal.loop)!);
453
+ } else {
454
+ loopBody = t.blockStatement([]);
455
+ }
456
+
457
+ cx.unscheduleAll(scheduleIds);
458
+ if (fallthroughId !== null) {
459
+ body.push(
460
+ t.labeledStatement(
461
+ t.identifier(`bb${fallthroughId}`),
462
+ t.whileStatement(testValue, loopBody)
463
+ )
464
+ );
465
+ writeBlock(cx, cx.ir.blocks.get(fallthroughId)!, body);
466
+ } else {
467
+ body.push(t.whileStatement(testValue, loopBody));
468
+ }
469
+ break;
470
+ }
471
case "goto": {
472
switch (terminal.variant) {
473
case GotoVariant.Break: {
478
break;
479
}
480
case GotoVariant.Continue: {
312
- invariant(
313
- cx.isScheduled(terminal.block),
314
- "Expected continue target to be scheduled"
315
- );
316
- body.push(t.continueStatement(t.identifier(`bb${terminal.block}`)));
481
+ const continue_ = codegenContinue(cx, terminal.block);
482
+ if (continue_ !== null) {
483
+ body.push(continue_);
484
+ }
485
break;
486
}
487
default: {
497
assertExhaustive(terminal, "Unexpected terminal");
498
}
499
}
332
- cx.unscheduleAll(scheduleIds);
500
}
501
502
function codegenBreak(cx: Context, block: BlockId): t.Statement | null {
336
- const breakTarget = cx.getBreakTarget(block);
337
- if (breakTarget === null) {
503
+ const target = cx.getBreakTarget(block);
504
+ if (target === null) {
505
// TODO: we should always have a target
506
return null;
507
}
341
- const { target, last } = breakTarget;
342
- if (target.type === "case") {
343
- // This break is transitioning to the next case statement. JS doesn't allow
344
- // labeling cases, the only option is to emit a plain break.
345
- return null;
346
- } else if (last) {
347
- // This break is to the most recent break target. Control flow will naturally
348
- // transition to this target, so a break is not required.
349
- return null;
350
- } else {
351
- // We're trying to break somewhere else, emit a label
352
- return t.breakStatement(t.identifier(`bb${block}`));
508
+ switch (target.type) {
509
+ case "implicit": {
510
+ return null;
511
+ }
512
+ case "unlabeled": {
513
+ return t.breakStatement();
514
+ }
515
+ case "labeled": {
516
+ return t.breakStatement(t.identifier(`bb${target.block}`));
517
+ }
518
+ }
519
+}
520
+
521
+function codegenContinue(cx: Context, block: BlockId): t.Statement | null {
522
+ const target = cx.getContinueTarget(block);
523
+ invariant(
524
+ target !== null,
525
+ `Expected continue target to be scheduled for bb${block}`
526
+ );
527
+ switch (target.type) {
528
+ case "labeled": {
529
+ return t.continueStatement(t.identifier(`bb${target.block}`));
530
+ }
531
+ case "unlabeled": {
532
+ return t.continueStatement();
533
+ }
534
+ case "implicit": {
535
+ return null;
536
+ }
537
+ default: {
538
+ assertExhaustive(
539
+ target.type,
540
+ `Unexpected continue target kind '${(target as any).type}'`
541
+ );
542
+ }
543
}
544
}
545