@samitouri / QOS-React-2 / commits / 2c95cdb431

Collapse BuildReactiveFunction/TreeVisitor

Collapses HIRTReeVisitor into BuildReactiveFunction, allowing us to remove the generic interface and simplify the code. Note that because the visitor was already not attempting to group instructions by scope anymore, the visitor code was very straightforward. This is mostly replacing calls to `appendBlock(block, instr)` with `block.push(instr)`.

Joe Savona committed Jan 17, 2023 at 09:40 UTC 2c95cdb431d13057427b1a6d425129114347150f
2 files changed +659 -1136
compiler/forget/src/HIR/HIRTreeVisitor.ts deleted
-966
@@ -1,966 +0,0 @@
1 -/**
2 - * Copyright (c) Facebook, Inc. and its affiliates.
3 - *
4 - * This source code is licensed under the MIT license found in the
5 - * LICENSE file in the root directory of this source tree.
6 - */
7 -
8 -import invariant from "invariant";
9 -import todo from "../Utils/todo";
10 -import { assertExhaustive } from "../Utils/utils";
11 -import {
12 - BasicBlock,
13 - BlockId,
14 - GotoVariant,
15 - HIR,
16 - HIRFunction,
17 - Instruction,
18 - InstructionId,
19 - InstructionValue,
20 - Place,
21 - SourceLocation,
22 -} from "./HIR";
23 -
24 -/**
25 - * Function to visit HIR as a tree of high-level constructs rather than as a sequence
26 - * of lower-level basic blocks. Intended for use in codegen and reactive scope
27 - * construction which need to see the original "shape" of the code.
28 - *
29 - * See the {@link Visitor} interface for more about implementing a visitor.
30 - */
31 -export function visitTree<
32 - TBlockBuilder,
33 - TBlock,
34 - TInit,
35 - TValueBlock,
36 - TValue,
37 - TStatement,
38 - TCase
39 ->(
40 - fn: HIRFunction,
41 - visitor: Visitor<
42 - TBlockBuilder,
43 - TBlock,
44 - TInit,
45 - TValueBlock,
46 - TValue,
47 - TStatement,
48 - TCase
49 - >
50 -): TBlock {
51 - const cx = new Context(fn.body);
52 - const driver = new Driver(cx, visitor);
53 - return driver.traverseBlock(cx.block(fn.body.entry));
54 -}
55 -
56 -class Driver<
57 - TBlockBuilder,
58 - TBlock,
59 - TInit,
60 - TValueBlock,
61 - TValue,
62 - TStatement,
63 - TCase
64 -> {
65 - cx: Context;
66 - visitor: Visitor<
67 - TBlockBuilder,
68 - TBlock,
69 - TInit,
70 - TValueBlock,
71 - TValue,
72 - TStatement,
73 - TCase
74 - >;
75 -
76 - constructor(
77 - cx: Context,
78 - visitor: Visitor<
79 - TBlockBuilder,
80 - TBlock,
81 - TInit,
82 - TValueBlock,
83 - TValue,
84 - TStatement,
85 - TCase
86 - >
87 - ) {
88 - this.cx = cx;
89 - this.visitor = visitor;
90 - }
91 -
92 - traverseBlock(block: BasicBlock): TBlock {
93 - const blockValue = this.visitor.enterBlock();
94 - this.visitBlock(block, blockValue);
95 - return this.visitor.leaveBlock(blockValue);
96 - }
97 -
98 - visitBlock(block: BasicBlock, blockValue: TBlockBuilder): void {
99 - invariant(
100 - !this.cx.emitted.has(block.id),
101 - `Cannot emit the same block twice: bb${block.id}`
102 - );
103 - this.cx.emitted.add(block.id);
104 - for (const instr of block.instructions) {
105 - this.visitInstr(instr, blockValue);
106 - }
107 -
108 - const terminal = block.terminal;
109 - const scheduleIds = [];
110 - switch (terminal.kind) {
111 - case "return": {
112 - const value =
113 - terminal.value != null
114 - ? this.visitPlace(terminal.value, terminal.id)
115 - : null;
116 - this.visitor.visitTerminalId(terminal.id);
117 - this.visitor.appendBlock(
118 - blockValue,
119 - this.visitor.visitTerminal({
120 - kind: "return",
121 - loc: terminal.loc,
122 - value,
123 - id: terminal.id,
124 - })
125 - );
126 - break;
127 - }
128 - case "throw": {
129 - const value = this.visitPlace(terminal.value, terminal.id);
130 - this.visitor.visitTerminalId(terminal.id);
131 - this.visitor.appendBlock(
132 - blockValue,
133 - this.visitor.visitTerminal({
134 - kind: "throw",
135 - value,
136 - id: terminal.id,
137 - })
138 - );
139 - break;
140 - }
141 - case "if": {
142 - const test = this.visitPlace(terminal.test, terminal.id);
143 - const fallthroughId =
144 - terminal.fallthrough !== null &&
145 - !this.cx.isScheduled(terminal.fallthrough)
146 - ? terminal.fallthrough
147 - : null;
148 - const alternateId =
149 - terminal.alternate !== terminal.fallthrough
150 - ? terminal.alternate
151 - : null;
152 -
153 - if (fallthroughId !== null) {
154 - const scheduleId = this.cx.schedule(fallthroughId, "if");
155 - scheduleIds.push(scheduleId);
156 - }
157 -
158 - this.visitor.visitTerminalId(terminal.id);
159 - let consequent: TBlock | null = null;
160 - if (this.cx.isScheduled(terminal.consequent)) {
161 - const break_ = this.visitBreak(terminal.consequent, null);
162 - if (break_ !== null) {
163 - const builder = this.visitor.enterBlock();
164 - this.visitor.appendBlock(builder, break_);
165 - consequent = this.visitor.leaveBlock(builder);
166 - }
167 - } else {
168 - consequent = this.traverseBlock(
169 - this.cx.ir.blocks.get(terminal.consequent)!
170 - );
171 - }
172 -
173 - let alternate: TBlock | null = null;
174 - if (alternateId !== null) {
175 - if (this.cx.isScheduled(alternateId)) {
176 - const break_ = this.visitBreak(alternateId, null);
177 - if (break_ !== null) {
178 - const builder = this.visitor.enterBlock();
179 - this.visitor.appendBlock(builder, break_);
180 - alternate = this.visitor.leaveBlock(builder);
181 - }
182 - } else {
183 - alternate = this.traverseBlock(this.cx.ir.blocks.get(alternateId)!);
184 - }
185 - }
186 -
187 - this.cx.unscheduleAll(scheduleIds);
188 - if (fallthroughId !== null) {
189 - this.visitor.appendBlock(
190 - blockValue,
191 - this.visitor.visitTerminal({
192 - kind: "if",
193 - test,
194 - consequent: consequent ?? this.emptyBlock(),
195 - alternate: alternate,
196 - id: terminal.id,
197 - }),
198 - fallthroughId
199 - );
200 - this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue);
201 - } else {
202 - this.visitor.appendBlock(
203 - blockValue,
204 - this.visitor.visitTerminal({
205 - kind: "if",
206 - test,
207 - consequent: consequent ?? this.emptyBlock(),
208 - alternate: alternate,
209 - id: terminal.id,
210 - })
211 - );
212 - }
213 - break;
214 - }
215 - case "switch": {
216 - const test = this.visitPlace(terminal.test, terminal.id);
217 - const fallthroughId =
218 - terminal.fallthrough !== null &&
219 - !this.cx.isScheduled(terminal.fallthrough)
220 - ? terminal.fallthrough
221 - : null;
222 - if (fallthroughId !== null) {
223 - const scheduleId = this.cx.schedule(fallthroughId, "switch");
224 - scheduleIds.push(scheduleId);
225 - }
226 -
227 - this.visitor.visitTerminalId(terminal.id);
228 - const cases: Array<TCase> = [];
229 - [...terminal.cases].reverse().forEach((case_, index) => {
230 - const test =
231 - case_.test !== null
232 - ? this.visitPlace(case_.test, terminal.id)
233 - : null;
234 -
235 - let consequent;
236 - if (this.cx.isScheduled(case_.block)) {
237 - // cases which are empty or contain only a `break` may point to blocks
238 - // that are already scheduled. emit as follows:
239 - // - if the block is for another case branch, don't emit a break and fall-through
240 - // - else, emit an explicit break.
241 - const break_ = this.visitBreak(case_.block, null);
242 - if (
243 - index === 0 &&
244 - break_ === null &&
245 - case_.block === terminal.fallthrough &&
246 - case_.test === null
247 - ) {
248 - // If the last case statement (first in reverse order) is a default that
249 - // jumps to the fallthrough, then we would emit a useless `default: {}`,
250 - // so instead skip this case.
251 - return;
252 - }
253 - const block = this.visitor.enterBlock();
254 - if (break_ !== null) {
255 - this.visitor.appendBlock(block, break_);
256 - }
257 - consequent = this.visitor.leaveBlock(block);
258 - } else {
259 - consequent = this.traverseBlock(
260 - this.cx.ir.blocks.get(case_.block)!
261 - );
262 - const scheduleId = this.cx.schedule(case_.block, "case");
263 - scheduleIds.push(scheduleId);
264 - }
265 - cases.push(this.visitor.visitCase(test, consequent));
266 - });
267 - cases.reverse();
268 -
269 - this.cx.unscheduleAll(scheduleIds);
270 - if (fallthroughId !== null) {
271 - this.visitor.appendBlock(
272 - blockValue,
273 - this.visitor.visitTerminal({
274 - kind: "switch",
275 - test,
276 - cases,
277 - id: terminal.id,
278 - }),
279 - fallthroughId
280 - );
281 - this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue);
282 - } else {
283 - this.visitor.appendBlock(
284 - blockValue,
285 - this.visitor.visitTerminal({
286 - kind: "switch",
287 - test,
288 - cases,
289 - id: terminal.id,
290 - })
291 - );
292 - }
293 - break;
294 - }
295 - case "while": {
296 - const fallthroughId =
297 - terminal.fallthrough !== null &&
298 - !this.cx.isScheduled(terminal.fallthrough)
299 - ? terminal.fallthrough
300 - : null;
301 - const loopId =
302 - !this.cx.isScheduled(terminal.loop) &&
303 - terminal.loop !== terminal.fallthrough
304 - ? terminal.loop
305 - : null;
306 - const scheduleId = this.cx.scheduleLoop(
307 - terminal.fallthrough,
308 - terminal.test,
309 - terminal.loop
310 - );
311 - scheduleIds.push(scheduleId);
312 -
313 - this.visitor.visitTerminalId(terminal.id);
314 - const testBlock = this.cx.ir.blocks.get(terminal.test)!;
315 - const testTerminal = testBlock.terminal;
316 - invariant(
317 - testTerminal.kind === "if",
318 - "Expected while loop test block to end in an if"
319 - );
320 - const testValue = this.visitValueBlock(blockValue, testBlock, {
321 - value: testTerminal.test,
322 - id: testTerminal.id,
323 - });
324 - let loopBody: TBlock;
325 - if (loopId) {
326 - loopBody = this.traverseBlock(this.cx.ir.blocks.get(loopId)!);
327 - } else {
328 - const break_ = this.visitBreak(terminal.loop, null);
329 - invariant(
330 - break_ !== null,
331 - "If loop body is already scheduled it must be a break"
332 - );
333 - const body = this.visitor.enterBlock();
334 - this.visitor.appendBlock(body, break_);
335 - loopBody = this.visitor.leaveBlock(body);
336 - }
337 -
338 - this.cx.unscheduleAll(scheduleIds);
339 - if (fallthroughId !== null) {
340 - this.visitor.appendBlock(
341 - blockValue,
342 - this.visitor.visitTerminal({
343 - kind: "while",
344 - loc: terminal.loc,
345 - test: testValue,
346 - loop: loopBody,
347 - id: terminal.id,
348 - }),
349 - fallthroughId
350 - );
351 - this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue);
352 - } else {
353 - this.visitor.appendBlock(
354 - blockValue,
355 - this.visitor.visitTerminal({
356 - kind: "while",
357 - loc: terminal.loc,
358 - test: testValue,
359 - loop: loopBody,
360 - id: terminal.id,
361 - })
362 - );
363 - }
364 - break;
365 - }
366 - case "for": {
367 - const loopId =
368 - !this.cx.isScheduled(terminal.loop) &&
369 - terminal.loop !== terminal.fallthrough
370 - ? terminal.loop
371 - : null;
372 -
373 - const fallthroughId =
374 - terminal.fallthrough !== null &&
375 - !this.cx.isScheduled(terminal.fallthrough)
376 - ? terminal.fallthrough
377 - : null;
378 -
379 - const scheduleId = this.cx.scheduleLoop(
380 - terminal.fallthrough,
381 - terminal.update,
382 - terminal.loop
383 - );
384 - scheduleIds.push(scheduleId);
385 -
386 - this.visitor.visitTerminalId(terminal.id);
387 -
388 - const initBlock = this.cx.ir.blocks.get(terminal.init)!;
389 - const initTerminal = initBlock.terminal;
390 - invariant(
391 - initTerminal.kind === "goto",
392 - "Expected for loop init block to end in a goto"
393 - );
394 - const initValue = this.visitInitBlock(blockValue, initBlock);
395 -
396 - const testBlock = this.cx.ir.blocks.get(terminal.test)!;
397 - const testTerminal = testBlock.terminal;
398 - invariant(
399 - testTerminal.kind === "if",
400 - "Expected for loop test block to end in an if"
401 - );
402 - const testValue = this.visitValueBlock(blockValue, testBlock, {
403 - value: testTerminal.test,
404 - id: testTerminal.id,
405 - });
406 -
407 - const updateBlock = this.cx.ir.blocks.get(terminal.update)!;
408 - const updateTerminal = updateBlock.terminal;
409 - invariant(
410 - updateTerminal.kind === "goto",
411 - "Expected for loop update block to end in a goto"
412 - );
413 - const updateValue = this.visitValueBlock(blockValue, updateBlock);
414 -
415 - let loopBody: TBlock;
416 - if (loopId) {
417 - loopBody = this.traverseBlock(this.cx.ir.blocks.get(loopId)!);
418 - } else {
419 - const break_ = this.visitBreak(terminal.loop, null);
420 - invariant(
421 - break_ !== null,
422 - "If loop body is already scheduled it must be a break"
423 - );
424 - const body = this.visitor.enterBlock();
425 - this.visitor.appendBlock(body, break_);
426 - loopBody = this.visitor.leaveBlock(body);
427 - }
428 -
429 - this.cx.unscheduleAll(scheduleIds);
430 - if (fallthroughId !== null) {
431 - this.visitor.appendBlock(
432 - blockValue,
433 - this.visitor.visitTerminal({
434 - kind: "for",
435 - init: initValue,
436 - test: testValue,
437 - update: updateValue,
438 - loop: loopBody,
439 - id: terminal.id,
440 - }),
441 - fallthroughId
442 - );
443 - this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue);
444 - } else {
445 - this.visitor.appendBlock(
446 - blockValue,
447 - this.visitor.visitTerminal({
448 - kind: "for",
449 - init: initValue,
450 - test: testValue,
451 - update: updateValue,
452 - loop: loopBody,
453 - id: terminal.id,
454 - })
455 - );
456 - }
457 - break;
458 - }
459 - case "logical": {
460 - todo("Implement tree visitor for logical terminal");
461 - }
462 - case "goto": {
463 - this.visitor.visitTerminalId(terminal.id);
464 - switch (terminal.variant) {
465 - case GotoVariant.Break: {
466 - const break_ = this.visitBreak(terminal.block, terminal.id);
467 - if (break_ !== null) {
468 - this.visitor.appendBlock(blockValue, break_);
469 - }
470 - break;
471 - }
472 - case GotoVariant.Continue: {
473 - const continue_ = this.visitContinue(terminal.block, terminal.id);
474 - if (continue_ !== null) {
475 - this.visitor.appendBlock(blockValue, continue_);
476 - }
477 - break;
478 - }
479 - default: {
480 - assertExhaustive(
481 - terminal.variant,
482 - `Unexpected goto variant '${terminal.variant}'`
483 - );
484 - }
485 - }
486 - break;
487 - }
488 - case "error":
489 - break;
490 - default: {
491 - assertExhaustive(terminal, "Unexpected terminal");
492 - }
493 - }
494 - }
495 -
496 - visitInitBlock(parent: TBlockBuilder, block: BasicBlock): TInit {
497 - const initBlock = this.visitor.enterInitBlock(parent);
498 - for (const instr of block.instructions) {
499 - const value = this.visitor.visitValue(instr.value, instr.id);
500 - const item = this.visitor.visitInstruction(instr, value);
501 - this.visitor.appendInitBlock(initBlock, item);
502 - }
503 - return this.visitor.leaveInitBlock(initBlock);
504 - }
505 -
506 - visitValueBlock(
507 - parent: TBlockBuilder,
508 - block: BasicBlock,
509 - terminalValue?: { value: InstructionValue; id: InstructionId }
510 - ): TValue {
511 - const valueBlock = this.visitor.enterValueBlock(parent);
512 - const instructions = [...block.instructions];
513 - let lastValue: { value: InstructionValue; id: InstructionId } | null = null;
514 - if (terminalValue != null) {
515 - lastValue = terminalValue;
516 - }
517 - for (const instr of instructions) {
518 - const value = this.visitor.visitValue(instr.value, instr.id);
519 - const item = this.visitor.visitInstruction(instr, value);
520 - this.visitor.appendValueBlock(valueBlock, item);
521 - }
522 - const value =
523 - lastValue !== null
524 - ? {
525 - value: this.visitor.visitValue(lastValue.value, lastValue.id),
526 - id: lastValue.id,
527 - }
528 - : null;
529 - return this.visitor.leaveValueBlock(valueBlock, value);
530 - }
531 -
532 - emptyBlock(): TBlock {
533 - const block = this.visitor.enterBlock();
534 - return this.visitor.leaveBlock(block);
535 - }
536 -
537 - visitBreak(block: BlockId, id: InstructionId | null): TStatement | null {
538 - const target = this.cx.getBreakTarget(block);
539 - if (target === null) {
540 - // TODO: we should always have a target
541 - return null;
542 - }
543 - switch (target.type) {
544 - case "implicit": {
545 - return this.visitor.visitImplicitTerminal();
546 - }
547 - case "unlabeled": {
548 - return this.visitor.visitTerminal({ kind: "break", label: null, id });
549 - }
550 - case "labeled": {
551 - return this.visitor.visitTerminal({
552 - kind: "break",
553 - label: target.block,
554 - id,
555 - });
556 - }
557 - }
558 - }
559 -
560 - visitContinue(block: BlockId, id: InstructionId): TStatement | null {
561 - const target = this.cx.getContinueTarget(block);
562 - invariant(
563 - target !== null,
564 - `Expected continue target to be scheduled for bb${block}`
565 - );
566 - switch (target.type) {
567 - case "labeled": {
568 - return this.visitor.visitTerminal({
569 - kind: "continue",
570 - label: target.block,
571 - id,
572 - });
573 - }
574 - case "unlabeled": {
575 - return this.visitor.visitTerminal({
576 - kind: "continue",
577 - label: null,
578 - id,
579 - });
580 - }
581 - case "implicit": {
582 - return this.visitor.visitImplicitTerminal();
583 - }
584 - default: {
585 - assertExhaustive(
586 - target.type,
587 - `Unexpected continue target kind '${(target as any).type}'`
588 - );
589 - }
590 - }
591 - }
592 -
593 - visitInstr(instr: Instruction, blockValue: TBlockBuilder): void {
594 - const value = this.visitor.visitValue(instr.value, instr.id);
595 - const item = this.visitor.visitInstruction(instr, value);
596 - this.visitor.appendBlock(blockValue, item);
597 - }
598 -
599 - visitPlace(place: Place, id: InstructionId): TValue {
600 - return this.visitor.visitValue(place, id);
601 - }
602 -}
603 -
604 -class Context {
605 - ir: HIR;
606 - #nextScheduleId: number = 0;
607 -
608 - /**
609 - * Used to track which blocks *have been* generated already in order to
610 - * abort if a block is generated a second time. This is an error catching
611 - * mechanism for debugging purposes, and is not used by the codegen algorithm
612 - * to drive decisions about how to emit blocks.
613 - */
614 - emitted: Set<BlockId> = new Set();
615 -
616 - /**
617 - * A set of blocks that are already scheduled to be emitted by eg a parent.
618 - * This allows child nodes to avoid re-emitting the same block and emit eg
619 - * a break instead.
620 - */
621 - #scheduled: Set<BlockId> = new Set();
622 -
623 - /**
624 - * Represents which control flow operations are currently in scope, with the innermost
625 - * scope last. Roughly speaking, the last ControlFlowTarget on the stack indicates where
626 - * control will implicitly transfer, such that gotos to that block can be elided. Gotos
627 - * targeting items higher up the stack may need labeled break or continue; see
628 - * getBreakTarget() and getContinueTarget() for more details.
629 - */
630 - #controlFlowStack: Array<ControlFlowTarget> = [];
631 -
632 - constructor(ir: HIR) {
633 - this.ir = ir;
634 - }
635 -
636 - block(id: BlockId): BasicBlock {
637 - return this.ir.blocks.get(id)!;
638 - }
639 -
640 - /**
641 - * Record that the given block will be emitted (eg by the codegen of a parent node)
642 - * so that child nodes can avoid re-emitting it.
643 - */
644 - schedule(block: BlockId, type: "if" | "switch" | "case"): number {
645 - const id = this.#nextScheduleId++;
646 - invariant(
647 - !this.#scheduled.has(block),
648 - `Break block is already scheduled: bb${block}`
649 - );
650 - this.#scheduled.add(block);
651 - this.#controlFlowStack.push({ block, id, type });
652 - return id;
653 - }
654 -
655 - scheduleLoop(
656 - fallthroughBlock: BlockId,
657 - continueBlock: BlockId,
658 - loopBlock: BlockId | null
659 - ): number {
660 - const id = this.#nextScheduleId++;
661 - const ownsBlock = !this.#scheduled.has(fallthroughBlock);
662 - this.#scheduled.add(fallthroughBlock);
663 - invariant(
664 - !this.#scheduled.has(continueBlock),
665 - `Continue block is already scheduled: bb${continueBlock}`
666 - );
667 - this.#scheduled.add(continueBlock);
668 - let ownsLoop = false;
669 - if (loopBlock !== null) {
670 - ownsLoop = !this.#scheduled.has(loopBlock);
671 - this.#scheduled.add(loopBlock);
672 - }
673 -
674 - this.#controlFlowStack.push({
675 - block: fallthroughBlock,
676 - ownsBlock,
677 - id,
678 - type: "loop",
679 - continueBlock,
680 - loopBlock,
681 - ownsLoop,
682 - });
683 - return id;
684 - }
685 -
686 - /**
687 - * Removes a block that was scheduled; must be called after that block is emitted.
688 - */
689 - unschedule(scheduleId: number): void {
690 - const last = this.#controlFlowStack.pop();
691 - invariant(
692 - last !== undefined && last.id === scheduleId,
693 - "Can only unschedule the last target"
694 - );
695 - if (last.type !== "loop" || last.ownsBlock !== null) {
696 - this.#scheduled.delete(last.block);
697 - }
698 - if (last.type === "loop") {
699 - this.#scheduled.delete(last.continueBlock);
700 - if (last.ownsLoop && last.loopBlock !== null) {
701 - this.#scheduled.delete(last.loopBlock);
702 - }
703 - }
704 - }
705 -
706 - /**
707 - * Helper to unschedule multiple scheduled blocks. The ids should be in
708 - * the order in which they were scheduled, ie most recently scheduled last.
709 - */
710 - unscheduleAll(scheduleIds: Array<number>): void {
711 - for (let i = scheduleIds.length - 1; i >= 0; i--) {
712 - this.unschedule(scheduleIds[i]!);
713 - }
714 - }
715 -
716 - /**
717 - * Check if the given @param block is scheduled or not.
718 - */
719 - isScheduled(block: BlockId): boolean {
720 - return this.#scheduled.has(block);
721 - }
722 -
723 - /**
724 - * Given the current control flow stack, determines how a `break` to the given @param block
725 - * must be emitted. Returns as follows:
726 - * - 'implicit' if control would implicitly transfer to that block
727 - * - 'labeled' if a labeled break is required to transfer control to that block
728 - * - 'unlabeled' if an unlabeled break would transfer to that block
729 - * - null if there is no information for this block
730 - *
731 - * The returned 'block' value should be used as the label if necessary.
732 - */
733 - getBreakTarget(
734 - block: BlockId
735 - ): { block: BlockId; type: ControlFlowKind } | null {
736 - let hasPrecedingLoop = false;
737 - for (let i = this.#controlFlowStack.length - 1; i >= 0; i--) {
738 - const target = this.#controlFlowStack[i]!;
739 - if (target.block === block) {
740 - let type: ControlFlowKind;
741 - if (target.type === "loop") {
742 - // breaking out of a loop requires an explicit break,
743 - // but only requires a label if breaking past the innermost loop.
744 - type = hasPrecedingLoop ? "labeled" : "unlabeled";
745 - } else if (i === this.#controlFlowStack.length - 1) {
746 - // breaking to the last break point, which is where control will transfer
747 - // implicitly
748 - type = "implicit";
749 - } else {
750 - // breaking somewhere else requires an explicit break
751 - type = "labeled";
752 - }
753 - return {
754 - block: target.block,
755 - type,
756 - };
757 - }
758 - hasPrecedingLoop ||= target.type === "loop";
759 - }
760 - return null;
761 - }
762 -
763 - /**
764 - * Given the current control flow stack, determines how a `continue` to the given @param block
765 - * must be emitted. Returns as follows:
766 - * - 'implicit' if control would implicitly continue to that block
767 - * - 'labeled' if a labeled continue is required to continue to that block
768 - * - 'unlabeled' if an unlabeled continue would transfer to that block
769 - * - null if there is no information for this block
770 - *
771 - * The returned 'block' value should be used as the label if necessary.
772 - */
773 - getContinueTarget(
774 - block: BlockId
775 - ): { block: BlockId; type: ControlFlowKind } | null {
776 - let hasPrecedingLoop = false;
777 - for (let i = this.#controlFlowStack.length - 1; i >= 0; i--) {
778 - const target = this.#controlFlowStack[i]!;
779 - if (target.type == "loop" && target.continueBlock === block) {
780 - let type: ControlFlowKind;
781 - if (hasPrecedingLoop) {
782 - // continuing to a loop that is not the innermost loop always requires
783 - // a label
784 - type = "labeled";
785 - } else if (i === this.#controlFlowStack.length - 1) {
786 - // continuing to the last break point, which is where control will
787 - // transfer to naturally
788 - type = "implicit";
789 - } else {
790 - // the continue is inside some conditional logic, requires an explicit
791 - // continue
792 - type = "unlabeled";
793 - }
794 - return {
795 - block: target.block,
796 - type,
797 - };
798 - }
799 - hasPrecedingLoop ||= target.type === "loop";
800 - }
801 - return null;
802 - }
803 -
804 - debugBreakTargets(): Array<ControlFlowTarget> {
805 - return this.#controlFlowStack.map((target) => ({ ...target }));
806 - }
807 -}
808 -
809 -type ControlFlowKind = "implicit" | "labeled" | "unlabeled";
810 -
811 -type ControlFlowTarget =
812 - | { type: "if"; block: BlockId; id: number }
813 - | { type: "switch"; block: BlockId; id: number }
814 - | { type: "case"; block: BlockId; id: number }
815 - | {
816 - type: "loop";
817 - block: BlockId;
818 - ownsBlock: boolean;
819 - continueBlock: BlockId;
820 - loopBlock: BlockId | null;
821 - ownsLoop: boolean;
822 - id: number;
823 - };
824 -
825 -/**
826 - * An object that can receive structured callbacks to visit HIR as a tree,
827 - * and convert it to an alternate format.
828 - *
829 - * TBlock = representation of a list of statements
830 - * TValue = represenation of an InstructionValue
831 - * TStatement = representation of an Instruction
832 - * TCase = representation of a switch case
833 - */
834 -export interface Visitor<
835 - TBlockBuilder,
836 - TBlock,
837 - TInit,
838 - TValueBlock,
839 - TValue,
840 - TStatement,
841 - TCase
842 -> {
843 - /**
844 - * Must create an "empty" instance of the visitor's represenation for
845 - * the contents of a block.
846 - */
847 - enterBlock(): TBlockBuilder;
848 -
849 - /**
850 - * Appends an item onto the given block, with an optional label. The label
851 - * indicates that a break/continue will proceed to code *after* the given item.
852 - */
853 - appendBlock(block: TBlockBuilder, item: TStatement, label?: BlockId): void;
854 -
855 - /**
856 - * Converts the visitor's block representation into the representation of a
857 - * block item, simultaneously "closing" the given block. The block will no
858 - * longer be modified by the visitor driver.
859 - */
860 - leaveBlock(block: TBlockBuilder): TBlock;
861 -
862 - /**
863 - * Must create an "empty" instance of the visitor's representation for a value
864 - * block, which can generally contain only expressions but not statements. The
865 - * currently active parent block is passed as an argument.
866 - *
867 - * The exception is the for initializer, which may contain declarations but not
868 - * other statement types.
869 - */
870 - enterValueBlock(block: TBlockBuilder): TValueBlock;
871 -
872 - /**
873 - * Appends an item onto the given value block.
874 - */
875 - appendValueBlock(block: TValueBlock, item: TStatement): void;
876 -
877 - /**
878 - * Converts the visitor's value block (and final value) to the visitor's
879 - * value representation.
880 - */
881 - leaveValueBlock(
882 - block: TValueBlock,
883 - value: { value: TValue; id: InstructionId } | null
884 - ): TValue;
885 -
886 - enterInitBlock(block: TBlockBuilder): TValueBlock;
887 -
888 - appendInitBlock(block: TValueBlock, item: TStatement): void;
889 -
890 - leaveInitBlock(block: TValueBlock): TInit;
891 -
892 - /**
893 - * Convert an InstructionValue into the visitor's own representation
894 - * of a value.
895 - */
896 - visitValue(value: InstructionValue, id: InstructionId): TValue;
897 -
898 - /**
899 - * Convert an Instruction into the visitor's own representation of
900 - * a block item.
901 - */
902 - visitInstruction(instruction: Instruction, value: TValue): TStatement;
903 -
904 - /**
905 - * Called when a terminal is reached, before processing any of its
906 - * possible branches.
907 - */
908 - visitTerminalId(id: InstructionId): void;
909 -
910 - /**
911 - * Converts a break/continue that is implicit — that does not strictly
912 - * have to be emitted — to the visitor's representation. The visitor
913 - * can choose to return null if this does not need to be represented.
914 - */
915 - visitImplicitTerminal(): TStatement | null;
916 -
917 - /**
918 - * Converts a terminal into the visitor's own representation of a block
919 - * item. Note that the terminal differs from HIR Terminals, because
920 - * values and block ids will have already been converted into the visitor's
921 - * own representations.
922 - */
923 - visitTerminal(
924 - terminal: BlockTerminal<TInit, TValue, TBlock, TCase>
925 - ): TStatement;
926 -
927 - /**
928 - * Visits a switch case statement, which is collected into a switch terminal
929 - * variant.
930 - */
931 - visitCase(test: TValue | null, block: TBlock): TCase;
932 -}
933 -
934 -export type BlockTerminal<TInit, TValue, TBlock, TCase> =
935 - | {
936 - kind: "return";
937 - loc: SourceLocation;
938 - value: TValue | null;
939 - id: InstructionId;
940 - }
941 - | { kind: "throw"; value: TValue; id: InstructionId }
942 - | {
943 - kind: "if";
944 - test: TValue;
945 - consequent: TBlock;
946 - alternate: TBlock | null;
947 - id: InstructionId;
948 - }
949 - | { kind: "switch"; test: TValue; cases: Array<TCase>; id: InstructionId }
950 - | {
951 - kind: "while";
952 - loc: SourceLocation;
953 - test: TValue;
954 - loop: TBlock;
955 - id: InstructionId;
956 - }
957 - | {
958 - kind: "for";
959 - init: TInit;
960 - test: TValue;
961 - update: TValue;
962 - loop: TBlock;
963 - id: InstructionId;
964 - }
965 - | { kind: "break"; label: BlockId | null; id: InstructionId | null }
966 - | { kind: "continue"; label: BlockId | null; id: InstructionId };
compiler/forget/src/ReactiveScopes/BuildReactiveFunction.ts
+659 -170
@@ -7,25 +7,31 @@
7
8 import invariant from "invariant";
9 import {
10 + BasicBlock,
11 BlockId,
11 - HIRFunction,
12 - Instruction,
12 + GotoVariant,
13 + HIR,
14 InstructionId,
15 InstructionValue,
16 Place,
17 ReactiveBlock,
17 - ReactiveFunction,
18 ReactiveStatement,
19 - ReactiveTerminal,
19 ReactiveValueBlock,
21 -} from "../HIR/HIR";
22 -import { BlockTerminal, Visitor, visitTree } from "../HIR/HIRTreeVisitor";
20 +} from "../HIR";
21 +import { HIRFunction, ReactiveFunction } from "../HIR/HIR";
22 +import todo from "../Utils/todo";
23 import { assertExhaustive } from "../Utils/utils";
24
25 +/**
26 + * Converts from HIR (lower-level CFG) to ReactiveFunction, a tree representation
27 + * that is closer to an AST. This pass restores the original control flow constructs,
28 + * including break/continue to labeled statements. Note that this pass naively emits
29 + * labels for *all* terminals: see PruneUnusedLabels which removes unnecessary labels.
30 + */
31 export function buildReactiveFunction(fn: HIRFunction): ReactiveFunction {
26 - const builder = new ReactiveFunctionBuilder();
27 - const body = visitTree(fn, builder);
28 - invariant(body != null, "Expected a root block");
32 + const cx = new Context(fn.body);
33 + const driver = new Driver(cx);
34 + const body = driver.traverseBlock(cx.block(fn.body.entry));
35 return {
36 loc: fn.loc,
37 id: fn.id,
@@ -36,199 +42,682 @@ export function buildReactiveFunction(fn: HIRFunction): ReactiveFunction {
42 };
43 }
44
39 -class Builder {
40 - #instructions: ReactiveBlock = [];
45 +class Driver {
46 + cx: Context;
47
42 - append(item: ReactiveStatement, label: BlockId | undefined): void {
43 - if (label !== undefined) {
44 - invariant(item.kind === "terminal", "Only terminals may have a label");
45 - item.label = label;
46 - }
47 - this.#instructions.push(item);
48 + constructor(cx: Context) {
49 + this.cx = cx;
50 }
51
50 - complete(): ReactiveBlock {
51 - return this.#instructions;
52 + traverseBlock(block: BasicBlock): ReactiveBlock {
53 + const blockValue: ReactiveBlock = [];
54 + this.visitBlock(block, blockValue);
55 + return blockValue;
56 }
53 -}
57
55 -class ReactiveFunctionBuilder
56 - implements
57 - Visitor<
58 - Builder,
59 - ReactiveBlock,
60 - ReactiveValueBlock,
61 - ReactiveValueBlock,
62 - InstructionValue | ReactiveValueBlock,
63 - ReactiveStatement,
64 - { test: InstructionValue | null; block: ReactiveBlock }
65 - >
66 -{
67 - enterBlock(): Builder {
68 - return new Builder();
69 - }
70 - appendBlock(
71 - block: Builder,
72 - item: ReactiveStatement,
73 - label?: BlockId | undefined
74 - ): void {
75 - block.append(item, label);
76 - }
77 - leaveBlock(block: Builder): ReactiveBlock {
78 - return block.complete();
79 - }
80 -
81 - enterValueBlock(block: Builder): ReactiveValueBlock {
82 - return {
58 + visitBlock(block: BasicBlock, blockValue: ReactiveBlock): void {
59 + invariant(
60 + !this.cx.emitted.has(block.id),
61 + `Cannot emit the same block twice: bb${block.id}`
62 + );
63 + this.cx.emitted.add(block.id);
64 + for (const instruction of block.instructions) {
65 + blockValue.push({
66 + kind: "instruction",
67 + instruction,
68 + });
69 + }
70 +
71 + const terminal = block.terminal;
72 + const scheduleIds = [];
73 + switch (terminal.kind) {
74 + case "return": {
75 + blockValue.push({
76 + kind: "terminal",
77 + terminal: {
78 + kind: "return",
79 + // loc: terminal.loc,
80 + value: terminal.value,
81 + id: terminal.id,
82 + },
83 + label: null,
84 + });
85 + break;
86 + }
87 + case "throw": {
88 + blockValue.push({
89 + kind: "terminal",
90 + terminal: {
91 + kind: "throw",
92 + value: terminal.value,
93 + id: terminal.id,
94 + },
95 + label: null,
96 + });
97 + break;
98 + }
99 + case "if": {
100 + const fallthroughId =
101 + terminal.fallthrough !== null &&
102 + !this.cx.isScheduled(terminal.fallthrough)
103 + ? terminal.fallthrough
104 + : null;
105 + const alternateId =
106 + terminal.alternate !== terminal.fallthrough
107 + ? terminal.alternate
108 + : null;
109 +
110 + if (fallthroughId !== null) {
111 + const scheduleId = this.cx.schedule(fallthroughId, "if");
112 + scheduleIds.push(scheduleId);
113 + }
114 +
115 + let consequent: ReactiveBlock | null = null;
116 + if (this.cx.isScheduled(terminal.consequent)) {
117 + const break_ = this.visitBreak(terminal.consequent, null);
118 + if (break_ !== null) {
119 + consequent = [break_];
120 + }
121 + } else {
122 + consequent = this.traverseBlock(
123 + this.cx.ir.blocks.get(terminal.consequent)!
124 + );
125 + }
126 +
127 + let alternate: ReactiveBlock | null = null;
128 + if (alternateId !== null) {
129 + if (this.cx.isScheduled(alternateId)) {
130 + const break_ = this.visitBreak(alternateId, null);
131 + if (break_ !== null) {
132 + alternate = [break_];
133 + }
134 + } else {
135 + alternate = this.traverseBlock(this.cx.ir.blocks.get(alternateId)!);
136 + }
137 + }
138 +
139 + this.cx.unscheduleAll(scheduleIds);
140 + blockValue.push({
141 + kind: "terminal",
142 + terminal: {
143 + kind: "if",
144 + test: terminal.test,
145 + consequent: consequent ?? this.emptyBlock(),
146 + alternate: alternate,
147 + id: terminal.id,
148 + },
149 + label: fallthroughId,
150 + });
151 + if (fallthroughId !== null) {
152 + this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue);
153 + }
154 + break;
155 + }
156 + case "switch": {
157 + const fallthroughId =
158 + terminal.fallthrough !== null &&
159 + !this.cx.isScheduled(terminal.fallthrough)
160 + ? terminal.fallthrough
161 + : null;
162 + if (fallthroughId !== null) {
163 + const scheduleId = this.cx.schedule(fallthroughId, "switch");
164 + scheduleIds.push(scheduleId);
165 + }
166 +
167 + const cases: Array<{
168 + test: Place | null;
169 + block: ReactiveBlock;
170 + }> = [];
171 + [...terminal.cases].reverse().forEach((case_, index) => {
172 + const test = case_.test;
173 +
174 + let consequent: ReactiveBlock;
175 + if (this.cx.isScheduled(case_.block)) {
176 + // cases which are empty or contain only a `break` may point to blocks
177 + // that are already scheduled. emit as follows:
178 + // - if the block is for another case branch, don't emit a break and fall-through
179 + // - else, emit an explicit break.
180 + const break_ = this.visitBreak(case_.block, null);
181 + if (
182 + index === 0 &&
183 + break_ === null &&
184 + case_.block === terminal.fallthrough &&
185 + case_.test === null
186 + ) {
187 + // If the last case statement (first in reverse order) is a default that
188 + // jumps to the fallthrough, then we would emit a useless `default: {}`,
189 + // so instead skip this case.
190 + return;
191 + }
192 + const block = [];
193 + if (break_ !== null) {
194 + block.push(break_);
195 + }
196 + consequent = block;
197 + } else {
198 + consequent = this.traverseBlock(
199 + this.cx.ir.blocks.get(case_.block)!
200 + );
201 + const scheduleId = this.cx.schedule(case_.block, "case");
202 + scheduleIds.push(scheduleId);
203 + }
204 + cases.push({ test, block: consequent });
205 + });
206 + cases.reverse();
207 +
208 + this.cx.unscheduleAll(scheduleIds);
209 + blockValue.push({
210 + kind: "terminal",
211 + terminal: {
212 + kind: "switch",
213 + test: terminal.test,
214 + cases,
215 + id: terminal.id,
216 + },
217 + label: fallthroughId,
218 + });
219 + if (fallthroughId !== null) {
220 + this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue);
221 + }
222 + break;
223 + }
224 + case "while": {
225 + const fallthroughId =
226 + terminal.fallthrough !== null &&
227 + !this.cx.isScheduled(terminal.fallthrough)
228 + ? terminal.fallthrough
229 + : null;
230 + const loopId =
231 + !this.cx.isScheduled(terminal.loop) &&
232 + terminal.loop !== terminal.fallthrough
233 + ? terminal.loop
234 + : null;
235 + const scheduleId = this.cx.scheduleLoop(
236 + terminal.fallthrough,
237 + terminal.test,
238 + terminal.loop
239 + );
240 + scheduleIds.push(scheduleId);
241 +
242 + const testBlock = this.cx.ir.blocks.get(terminal.test)!;
243 + const testTerminal = testBlock.terminal;
244 + invariant(
245 + testTerminal.kind === "if",
246 + "Expected while loop test block to end in an if"
247 + );
248 + const testValue = this.visitValueBlock(blockValue, testBlock, {
249 + value: testTerminal.test,
250 + id: testTerminal.id,
251 + });
252 + let loopBody: ReactiveBlock;
253 + if (loopId) {
254 + loopBody = this.traverseBlock(this.cx.ir.blocks.get(loopId)!);
255 + } else {
256 + const break_ = this.visitBreak(terminal.loop, null);
257 + invariant(
258 + break_ !== null,
259 + "If loop body is already scheduled it must be a break"
260 + );
261 + loopBody = [break_];
262 + }
263 +
264 + this.cx.unscheduleAll(scheduleIds);
265 + blockValue.push({
266 + kind: "terminal",
267 + terminal: {
268 + kind: "while",
269 + // loc: terminal.loc,
270 + test: testValue,
271 + loop: loopBody,
272 + id: terminal.id,
273 + },
274 + label: fallthroughId,
275 + });
276 + if (fallthroughId !== null) {
277 + this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue);
278 + }
279 + break;
280 + }
281 + case "for": {
282 + const loopId =
283 + !this.cx.isScheduled(terminal.loop) &&
284 + terminal.loop !== terminal.fallthrough
285 + ? terminal.loop
286 + : null;
287 +
288 + const fallthroughId =
289 + terminal.fallthrough !== null &&
290 + !this.cx.isScheduled(terminal.fallthrough)
291 + ? terminal.fallthrough
292 + : null;
293 +
294 + const scheduleId = this.cx.scheduleLoop(
295 + terminal.fallthrough,
296 + terminal.update,
297 + terminal.loop
298 + );
299 + scheduleIds.push(scheduleId);
300 +
301 + const initBlock = this.cx.ir.blocks.get(terminal.init)!;
302 + const initTerminal = initBlock.terminal;
303 + invariant(
304 + initTerminal.kind === "goto",
305 + "Expected for loop init block to end in a goto"
306 + );
307 + const initValue = this.visitInitBlock(blockValue, initBlock);
308 +
309 + const testBlock = this.cx.ir.blocks.get(terminal.test)!;
310 + const testTerminal = testBlock.terminal;
311 + invariant(
312 + testTerminal.kind === "if",
313 + "Expected for loop test block to end in an if"
314 + );
315 + const testValue = this.visitValueBlock(blockValue, testBlock, {
316 + value: testTerminal.test,
317 + id: testTerminal.id,
318 + });
319 +
320 + const updateBlock = this.cx.ir.blocks.get(terminal.update)!;
321 + const updateTerminal = updateBlock.terminal;
322 + invariant(
323 + updateTerminal.kind === "goto",
324 + "Expected for loop update block to end in a goto"
325 + );
326 + const updateValue = this.visitValueBlock(blockValue, updateBlock);
327 +
328 + let loopBody: ReactiveBlock;
329 + if (loopId) {
330 + loopBody = this.traverseBlock(this.cx.ir.blocks.get(loopId)!);
331 + } else {
332 + const break_ = this.visitBreak(terminal.loop, null);
333 + invariant(
334 + break_ !== null,
335 + "If loop body is already scheduled it must be a break"
336 + );
337 + loopBody = [break_];
338 + }
339 +
340 + this.cx.unscheduleAll(scheduleIds);
341 + blockValue.push({
342 + kind: "terminal",
343 + terminal: {
344 + kind: "for",
345 + init: initValue,
346 + test: testValue,
347 + update: updateValue,
348 + loop: loopBody,
349 + id: terminal.id,
350 + },
351 + label: fallthroughId,
352 + });
353 + if (fallthroughId !== null) {
354 + this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue);
355 + }
356 + break;
357 + }
358 + case "logical": {
359 + todo("Implement tree visitor for logical terminal");
360 + }
361 + case "goto": {
362 + switch (terminal.variant) {
363 + case GotoVariant.Break: {
364 + const break_ = this.visitBreak(terminal.block, terminal.id);
365 + if (break_ !== null) {
366 + blockValue.push(break_);
367 + }
368 + break;
369 + }
370 + case GotoVariant.Continue: {
371 + const continue_ = this.visitContinue(terminal.block, terminal.id);
372 + if (continue_ !== null) {
373 + blockValue.push(continue_);
374 + }
375 + break;
376 + }
377 + default: {
378 + assertExhaustive(
379 + terminal.variant,
380 + `Unexpected goto variant '${terminal.variant}'`
381 + );
382 + }
383 + }
384 + break;
385 + }
386 + default: {
387 + assertExhaustive(terminal, "Unexpected terminal");
388 + }
389 + }
390 + }
391 +
392 + visitInitBlock(parent: ReactiveBlock, block: BasicBlock): ReactiveValueBlock {
393 + const initBlock: ReactiveValueBlock = {
394 kind: "value-block",
395 instructions: [],
396 last: null,
397 };
87 - }
88 - appendValueBlock(block: ReactiveValueBlock, item: ReactiveStatement): void {
89 - block.instructions.push(item);
90 - }
91 - leaveValueBlock(
92 - block: ReactiveValueBlock,
93 - last: {
94 - value: InstructionValue | ReactiveValueBlock;
95 - id: InstructionId;
96 - } | null
97 - ): InstructionValue | ReactiveValueBlock {
98 - if (last !== null) {
99 - const { id, value } = last;
100 - invariant(
101 - value.kind !== "value-block",
102 - "Expected value block to end in a value"
103 - );
104 - block.last = { id, value };
398 + for (const instruction of block.instructions) {
399 + initBlock.instructions.push({
400 + kind: "instruction",
401 + instruction,
402 + });
403 }
106 - return block;
404 + return initBlock;
405 }
406
109 - enterInitBlock(block: Builder): ReactiveValueBlock {
110 - return this.enterValueBlock(block);
111 - }
112 - appendInitBlock(block: ReactiveValueBlock, item: ReactiveStatement): void {
113 - this.appendValueBlock(block, item);
114 - }
115 - leaveInitBlock(block: ReactiveValueBlock): ReactiveValueBlock {
116 - return block;
407 + visitValueBlock(
408 + parent: ReactiveBlock,
409 + block: BasicBlock,
410 + terminalValue?: { value: InstructionValue; id: InstructionId }
411 + ): ReactiveValueBlock {
412 + const valueBlock: ReactiveValueBlock = {
413 + kind: "value-block",
414 + instructions: [],
415 + last: terminalValue ?? null,
416 + };
417 + let lastValue: { value: InstructionValue; id: InstructionId } | null = null;
418 + if (terminalValue != null) {
419 + lastValue = terminalValue;
420 + }
421 + for (const instruction of block.instructions) {
422 + valueBlock.instructions.push({
423 + kind: "instruction",
424 + instruction,
425 + });
426 + }
427 + return valueBlock;
428 }
429
119 - visitValue(
120 - value: InstructionValue,
121 - id: InstructionId
122 - ): InstructionValue | ReactiveValueBlock {
123 - return value;
124 - }
125 - visitInstruction(
126 - instruction: Instruction,
127 - value: InstructionValue | ReactiveValueBlock
128 - ): ReactiveStatement {
129 - return { kind: "instruction", instruction };
430 + emptyBlock(): ReactiveBlock {
431 + return [];
432 }
131 - visitTerminalId(id: InstructionId): void {}
132 - visitImplicitTerminal(): ReactiveStatement | null {
133 - return null;
134 - }
135 - visitTerminal(
136 - terminal: BlockTerminal<
137 - ReactiveValueBlock,
138 - InstructionValue | ReactiveValueBlock,
139 - ReactiveBlock,
140 - { test: InstructionValue | null; block: ReactiveBlock }
141 - >
142 - ): ReactiveStatement {
143 - let result: ReactiveTerminal;
144 - switch (terminal.kind) {
145 - case "break": {
146 - result = { kind: "break", label: terminal.label, id: terminal.id };
147 - break;
148 - }
149 - case "continue": {
150 - result = { kind: "continue", label: terminal.label, id: terminal.id };
151 - break;
433 +
434 + visitBreak(
435 + block: BlockId,
436 + id: InstructionId | null
437 + ): ReactiveStatement | null {
438 + const target = this.cx.getBreakTarget(block);
439 + if (target === null) {
440 + // TODO: we should always have a target
441 + return null;
442 + }
443 + switch (target.type) {
444 + case "implicit": {
445 + return null;
446 }
153 - case "for": {
154 - const { test, update } = terminal;
155 - result = {
156 - kind: "for",
157 - init: terminal.init,
158 - test: terminal.test as ReactiveValueBlock,
159 - update: terminal.update as ReactiveValueBlock,
160 - loop: terminal.loop,
161 - id: terminal.id,
447 + case "labeled": {
448 + return {
449 + kind: "terminal",
450 + terminal: { kind: "break", label: target.block, id },
451 + label: null,
452 };
163 - break;
453 }
165 - case "if": {
166 - result = {
167 - kind: "if",
168 - test: terminal.test as Place,
169 - consequent: terminal.consequent,
170 - alternate: terminal.alternate,
171 - id: terminal.id,
454 + case "unlabeled": {
455 + return {
456 + kind: "terminal",
457 + terminal: { kind: "break", label: null, id },
458 + label: null,
459 };
173 - break;
460 }
175 - case "return": {
176 - const value = terminal.value;
177 - if (value !== null && value.kind !== "Identifier") {
178 - invariant(false, "Expected return to be a Place");
179 - }
180 - result = { kind: "return", value, id: terminal.id };
181 - break;
461 + default: {
462 + assertExhaustive(
463 + target.type,
464 + `Unexpected break target kind '${(target as any).type}'`
465 + );
466 }
183 - case "switch": {
184 - result = {
185 - kind: "switch",
186 - test: terminal.test as Place,
187 - cases: terminal.cases as Array<{
188 - test: Place | null;
189 - block: ReactiveBlock | void;
190 - }>,
191 - id: terminal.id,
192 - };
193 - break;
467 + }
468 + }
469 +
470 + visitContinue(block: BlockId, id: InstructionId): ReactiveStatement | null {
471 + const target = this.cx.getContinueTarget(block);
472 + invariant(
473 + target !== null,
474 + `Expected continue target to be scheduled for bb${block}`
475 + );
476 + switch (target.type) {
477 + case "implicit": {
478 + return null;
479 }
195 - case "throw": {
196 - result = {
197 - kind: "throw",
198 - value: terminal.value as Place,
199 - id: terminal.id,
480 + case "labeled": {
481 + return {
482 + kind: "terminal",
483 + terminal: { kind: "continue", label: target.block, id },
484 + label: null,
485 };
201 - break;
486 }
203 - case "while": {
204 - result = {
205 - kind: "while",
206 - test: terminal.test as ReactiveValueBlock,
207 - loop: terminal.loop,
208 - id: terminal.id,
487 + case "unlabeled": {
488 + return {
489 + kind: "terminal",
490 + terminal: { kind: "continue", label: null, id },
491 + label: null,
492 };
210 - break;
493 }
494 default: {
495 assertExhaustive(
214 - terminal,
215 - `Unexpected terminal kind '${(terminal as any).kind}'`
496 + target.type,
497 + `Unexpected continue target kind '${(target as any).type}'`
498 );
499 }
500 }
219 - return {
220 - kind: "terminal",
221 - terminal: result,
222 - label: null,
223 - };
501 }
225 - visitCase(
226 - test: InstructionValue | ReactiveValueBlock | null,
227 - block: ReactiveBlock
228 - ): { test: InstructionValue | null; block: ReactiveBlock } {
229 - if (test !== null && test.kind !== "Identifier") {
230 - invariant(false, "Expected a Place");
502 +}
503 +
504 +class Context {
505 + ir: HIR;
506 + #nextScheduleId: number = 0;
507 +
508 + /**
509 + * Used to track which blocks *have been* generated already in order to
510 + * abort if a block is generated a second time. This is an error catching
511 + * mechanism for debugging purposes, and is not used by the codegen algorithm
512 + * to drive decisions about how to emit blocks.
513 + */
514 + emitted: Set<BlockId> = new Set();
515 +
516 + /**
517 + * A set of blocks that are already scheduled to be emitted by eg a parent.
518 + * This allows child nodes to avoid re-emitting the same block and emit eg
519 + * a break instead.
520 + */
521 + #scheduled: Set<BlockId> = new Set();
522 +
523 + /**
524 + * Represents which control flow operations are currently in scope, with the innermost
525 + * scope last. Roughly speaking, the last ControlFlowTarget on the stack indicates where
526 + * control will implicitly transfer, such that gotos to that block can be elided. Gotos
527 + * targeting items higher up the stack may need labeled break or continue; see
528 + * getBreakTarget() and getContinueTarget() for more details.
529 + */
530 + #controlFlowStack: Array<ControlFlowTarget> = [];
531 +
532 + constructor(ir: HIR) {
533 + this.ir = ir;
534 + }
535 +
536 + block(id: BlockId): BasicBlock {
537 + return this.ir.blocks.get(id)!;
538 + }
539 +
540 + /**
541 + * Record that the given block will be emitted (eg by the codegen of a parent node)
542 + * so that child nodes can avoid re-emitting it.
543 + */
544 + schedule(block: BlockId, type: "if" | "switch" | "case"): number {
545 + const id = this.#nextScheduleId++;
546 + invariant(
547 + !this.#scheduled.has(block),
548 + `Break block is already scheduled: bb${block}`
549 + );
550 + this.#scheduled.add(block);
551 + this.#controlFlowStack.push({ block, id, type });
552 + return id;
553 + }
554 +
555 + scheduleLoop(
556 + fallthroughBlock: BlockId,
557 + continueBlock: BlockId,
558 + loopBlock: BlockId | null
559 + ): number {
560 + const id = this.#nextScheduleId++;
561 + const ownsBlock = !this.#scheduled.has(fallthroughBlock);
562 + this.#scheduled.add(fallthroughBlock);
563 + invariant(
564 + !this.#scheduled.has(continueBlock),
565 + `Continue block is already scheduled: bb${continueBlock}`
566 + );
567 + this.#scheduled.add(continueBlock);
568 + let ownsLoop = false;
569 + if (loopBlock !== null) {
570 + ownsLoop = !this.#scheduled.has(loopBlock);
571 + this.#scheduled.add(loopBlock);
572 + }
573 +
574 + this.#controlFlowStack.push({
575 + block: fallthroughBlock,
576 + ownsBlock,
577 + id,
578 + type: "loop",
579 + continueBlock,
580 + loopBlock,
581 + ownsLoop,
582 + });
583 + return id;
584 + }
585 +
586 + /**
587 + * Removes a block that was scheduled; must be called after that block is emitted.
588 + */
589 + unschedule(scheduleId: number): void {
590 + const last = this.#controlFlowStack.pop();
591 + invariant(
592 + last !== undefined && last.id === scheduleId,
593 + "Can only unschedule the last target"
594 + );
595 + if (last.type !== "loop" || last.ownsBlock !== null) {
596 + this.#scheduled.delete(last.block);
597 + }
598 + if (last.type === "loop") {
599 + this.#scheduled.delete(last.continueBlock);
600 + if (last.ownsLoop && last.loopBlock !== null) {
601 + this.#scheduled.delete(last.loopBlock);
602 + }
603 + }
604 + }
605 +
606 + /**
607 + * Helper to unschedule multiple scheduled blocks. The ids should be in
608 + * the order in which they were scheduled, ie most recently scheduled last.
609 + */
610 + unscheduleAll(scheduleIds: Array<number>): void {
611 + for (let i = scheduleIds.length - 1; i >= 0; i--) {
612 + this.unschedule(scheduleIds[i]!);
613 + }
614 + }
615 +
616 + /**
617 + * Check if the given @param block is scheduled or not.
618 + */
619 + isScheduled(block: BlockId): boolean {
620 + return this.#scheduled.has(block);
621 + }
622 +
623 + /**
624 + * Given the current control flow stack, determines how a `break` to the given @param block
625 + * must be emitted. Returns as follows:
626 + * - 'implicit' if control would implicitly transfer to that block
627 + * - 'labeled' if a labeled break is required to transfer control to that block
628 + * - 'unlabeled' if an unlabeled break would transfer to that block
629 + * - null if there is no information for this block
630 + *
631 + * The returned 'block' value should be used as the label if necessary.
632 + */
633 + getBreakTarget(
634 + block: BlockId
635 + ): { block: BlockId; type: ControlFlowKind } | null {
636 + let hasPrecedingLoop = false;
637 + for (let i = this.#controlFlowStack.length - 1; i >= 0; i--) {
638 + const target = this.#controlFlowStack[i]!;
639 + if (target.block === block) {
640 + let type: ControlFlowKind;
641 + if (target.type === "loop") {
642 + // breaking out of a loop requires an explicit break,
643 + // but only requires a label if breaking past the innermost loop.
644 + type = hasPrecedingLoop ? "labeled" : "unlabeled";
645 + } else if (i === this.#controlFlowStack.length - 1) {
646 + // breaking to the last break point, which is where control will transfer
647 + // implicitly
648 + type = "implicit";
649 + } else {
650 + // breaking somewhere else requires an explicit break
651 + type = "labeled";
652 + }
653 + return {
654 + block: target.block,
655 + type,
656 + };
657 + }
658 + hasPrecedingLoop ||= target.type === "loop";
659 + }
660 + return null;
661 + }
662 +
663 + /**
664 + * Given the current control flow stack, determines how a `continue` to the given @param block
665 + * must be emitted. Returns as follows:
666 + * - 'implicit' if control would implicitly continue to that block
667 + * - 'labeled' if a labeled continue is required to continue to that block
668 + * - 'unlabeled' if an unlabeled continue would transfer to that block
669 + * - null if there is no information for this block
670 + *
671 + * The returned 'block' value should be used as the label if necessary.
672 + */
673 + getContinueTarget(
674 + block: BlockId
675 + ): { block: BlockId; type: ControlFlowKind } | null {
676 + let hasPrecedingLoop = false;
677 + for (let i = this.#controlFlowStack.length - 1; i >= 0; i--) {
678 + const target = this.#controlFlowStack[i]!;
679 + if (target.type == "loop" && target.continueBlock === block) {
680 + let type: ControlFlowKind;
681 + if (hasPrecedingLoop) {
682 + // continuing to a loop that is not the innermost loop always requires
683 + // a label
684 + type = "labeled";
685 + } else if (i === this.#controlFlowStack.length - 1) {
686 + // continuing to the last break point, which is where control will
687 + // transfer to naturally
688 + type = "implicit";
689 + } else {
690 + // the continue is inside some conditional logic, requires an explicit
691 + // continue
692 + type = "unlabeled";
693 + }
694 + return {
695 + block: target.block,
696 + type,
697 + };
698 + }
699 + hasPrecedingLoop ||= target.type === "loop";
700 }
232 - return { test, block };
701 + return null;
702 + }
703 +
704 + debugBreakTargets(): Array<ControlFlowTarget> {
705 + return this.#controlFlowStack.map((target) => ({ ...target }));
706 }
707 }
708 +
709 +type ControlFlowKind = "implicit" | "labeled" | "unlabeled";
710 +
711 +type ControlFlowTarget =
712 + | { type: "if"; block: BlockId; id: number }
713 + | { type: "switch"; block: BlockId; id: number }
714 + | { type: "case"; block: BlockId; id: number }
715 + | {
716 + type: "loop";
717 + block: BlockId;
718 + ownsBlock: boolean;
719 + continueBlock: BlockId;
720 + loopBlock: BlockId | null;
721 + ownsLoop: boolean;
722 + id: number;
723 + };