@samitouri / QOS-React-2 / commits / 9e549e9417

Fork HIRTreeVisitor to specialize it for ReactiveFunction conversion

Joe Savona committed Jan 12, 2023 at 09:11 UTC 9e549e9417698a8a12fb04917461e7a20b0b3000
2 files changed +963 -1
compiler/forget/src/HIR/ReactiveFunctionVisitor.ts new
+958
@@ -0,0 +1,958 @@
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 visitTreeForReactiveFunction<
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 + default: {
489 + assertExhaustive(terminal, "Unexpected terminal");
490 + }
491 + }
492 + }
493 +
494 + visitInitBlock(parent: TBlockBuilder, block: BasicBlock): TInit {
495 + const initBlock = this.visitor.enterInitBlock(parent);
496 + for (const instr of block.instructions) {
497 + const value = this.visitor.visitValue(instr.value, instr.id);
498 + const item = this.visitor.visitInstruction(instr, value);
499 + this.visitor.appendInitBlock(initBlock, item);
500 + }
501 + return this.visitor.leaveInitBlock(initBlock);
502 + }
503 +
504 + visitValueBlock(
505 + parent: TBlockBuilder,
506 + block: BasicBlock,
507 + terminalValue?: { value: InstructionValue; id: InstructionId }
508 + ): TValue {
509 + const valueBlock = this.visitor.enterValueBlock(parent);
510 + const instructions = [...block.instructions];
511 + let lastValue: { value: InstructionValue; id: InstructionId } | null = null;
512 + if (terminalValue != null) {
513 + lastValue = terminalValue;
514 + }
515 + for (const instr of instructions) {
516 + const value = this.visitor.visitValue(instr.value, instr.id);
517 + const item = this.visitor.visitInstruction(instr, value);
518 + this.visitor.appendValueBlock(valueBlock, item);
519 + }
520 + const value =
521 + lastValue !== null
522 + ? this.visitor.visitValue(lastValue.value, lastValue.id)
523 + : null;
524 + return this.visitor.leaveValueBlock(valueBlock, value);
525 + }
526 +
527 + emptyBlock(): TBlock {
528 + const block = this.visitor.enterBlock();
529 + return this.visitor.leaveBlock(block);
530 + }
531 +
532 + visitBreak(block: BlockId, id: InstructionId | null): TStatement | null {
533 + const target = this.cx.getBreakTarget(block);
534 + if (target === null) {
535 + // TODO: we should always have a target
536 + return null;
537 + }
538 + switch (target.type) {
539 + case "implicit": {
540 + return this.visitor.visitImplicitTerminal();
541 + }
542 + case "unlabeled": {
543 + return this.visitor.visitTerminal({ kind: "break", label: null, id });
544 + }
545 + case "labeled": {
546 + return this.visitor.visitTerminal({
547 + kind: "break",
548 + label: target.block,
549 + id,
550 + });
551 + }
552 + }
553 + }
554 +
555 + visitContinue(block: BlockId, id: InstructionId): TStatement | null {
556 + const target = this.cx.getContinueTarget(block);
557 + invariant(
558 + target !== null,
559 + `Expected continue target to be scheduled for bb${block}`
560 + );
561 + switch (target.type) {
562 + case "labeled": {
563 + return this.visitor.visitTerminal({
564 + kind: "continue",
565 + label: target.block,
566 + id,
567 + });
568 + }
569 + case "unlabeled": {
570 + return this.visitor.visitTerminal({
571 + kind: "continue",
572 + label: null,
573 + id,
574 + });
575 + }
576 + case "implicit": {
577 + return this.visitor.visitImplicitTerminal();
578 + }
579 + default: {
580 + assertExhaustive(
581 + target.type,
582 + `Unexpected continue target kind '${(target as any).type}'`
583 + );
584 + }
585 + }
586 + }
587 +
588 + visitInstr(instr: Instruction, blockValue: TBlockBuilder): void {
589 + const value = this.visitor.visitValue(instr.value, instr.id);
590 + const item = this.visitor.visitInstruction(instr, value);
591 + this.visitor.appendBlock(blockValue, item);
592 + }
593 +
594 + visitPlace(place: Place, id: InstructionId): TValue {
595 + return this.visitor.visitValue(place, id);
596 + }
597 +}
598 +
599 +class Context {
600 + ir: HIR;
601 + #nextScheduleId: number = 0;
602 +
603 + /**
604 + * Used to track which blocks *have been* generated already in order to
605 + * abort if a block is generated a second time. This is an error catching
606 + * mechanism for debugging purposes, and is not used by the codegen algorithm
607 + * to drive decisions about how to emit blocks.
608 + */
609 + emitted: Set<BlockId> = new Set();
610 +
611 + /**
612 + * A set of blocks that are already scheduled to be emitted by eg a parent.
613 + * This allows child nodes to avoid re-emitting the same block and emit eg
614 + * a break instead.
615 + */
616 + #scheduled: Set<BlockId> = new Set();
617 +
618 + /**
619 + * Represents which control flow operations are currently in scope, with the innermost
620 + * scope last. Roughly speaking, the last ControlFlowTarget on the stack indicates where
621 + * control will implicitly transfer, such that gotos to that block can be elided. Gotos
622 + * targeting items higher up the stack may need labeled break or continue; see
623 + * getBreakTarget() and getContinueTarget() for more details.
624 + */
625 + #controlFlowStack: Array<ControlFlowTarget> = [];
626 +
627 + constructor(ir: HIR) {
628 + this.ir = ir;
629 + }
630 +
631 + block(id: BlockId): BasicBlock {
632 + return this.ir.blocks.get(id)!;
633 + }
634 +
635 + /**
636 + * Record that the given block will be emitted (eg by the codegen of a parent node)
637 + * so that child nodes can avoid re-emitting it.
638 + */
639 + schedule(block: BlockId, type: "if" | "switch" | "case"): number {
640 + const id = this.#nextScheduleId++;
641 + invariant(
642 + !this.#scheduled.has(block),
643 + `Break block is already scheduled: bb${block}`
644 + );
645 + this.#scheduled.add(block);
646 + this.#controlFlowStack.push({ block, id, type });
647 + return id;
648 + }
649 +
650 + scheduleLoop(
651 + fallthroughBlock: BlockId,
652 + continueBlock: BlockId,
653 + loopBlock: BlockId | null
654 + ): number {
655 + const id = this.#nextScheduleId++;
656 + const ownsBlock = !this.#scheduled.has(fallthroughBlock);
657 + this.#scheduled.add(fallthroughBlock);
658 + invariant(
659 + !this.#scheduled.has(continueBlock),
660 + `Continue block is already scheduled: bb${continueBlock}`
661 + );
662 + this.#scheduled.add(continueBlock);
663 + let ownsLoop = false;
664 + if (loopBlock !== null) {
665 + ownsLoop = !this.#scheduled.has(loopBlock);
666 + this.#scheduled.add(loopBlock);
667 + }
668 +
669 + this.#controlFlowStack.push({
670 + block: fallthroughBlock,
671 + ownsBlock,
672 + id,
673 + type: "loop",
674 + continueBlock,
675 + loopBlock,
676 + ownsLoop,
677 + });
678 + return id;
679 + }
680 +
681 + /**
682 + * Removes a block that was scheduled; must be called after that block is emitted.
683 + */
684 + unschedule(scheduleId: number): void {
685 + const last = this.#controlFlowStack.pop();
686 + invariant(
687 + last !== undefined && last.id === scheduleId,
688 + "Can only unschedule the last target"
689 + );
690 + if (last.type !== "loop" || last.ownsBlock !== null) {
691 + this.#scheduled.delete(last.block);
692 + }
693 + if (last.type === "loop") {
694 + this.#scheduled.delete(last.continueBlock);
695 + if (last.ownsLoop && last.loopBlock !== null) {
696 + this.#scheduled.delete(last.loopBlock);
697 + }
698 + }
699 + }
700 +
701 + /**
702 + * Helper to unschedule multiple scheduled blocks. The ids should be in
703 + * the order in which they were scheduled, ie most recently scheduled last.
704 + */
705 + unscheduleAll(scheduleIds: Array<number>): void {
706 + for (let i = scheduleIds.length - 1; i >= 0; i--) {
707 + this.unschedule(scheduleIds[i]!);
708 + }
709 + }
710 +
711 + /**
712 + * Check if the given @param block is scheduled or not.
713 + */
714 + isScheduled(block: BlockId): boolean {
715 + return this.#scheduled.has(block);
716 + }
717 +
718 + /**
719 + * Given the current control flow stack, determines how a `break` to the given @param block
720 + * must be emitted. Returns as follows:
721 + * - 'implicit' if control would implicitly transfer to that block
722 + * - 'labeled' if a labeled break is required to transfer control to that block
723 + * - 'unlabeled' if an unlabeled break would transfer to that block
724 + * - null if there is no information for this block
725 + *
726 + * The returned 'block' value should be used as the label if necessary.
727 + */
728 + getBreakTarget(
729 + block: BlockId
730 + ): { block: BlockId; type: ControlFlowKind } | null {
731 + let hasPrecedingLoop = false;
732 + for (let i = this.#controlFlowStack.length - 1; i >= 0; i--) {
733 + const target = this.#controlFlowStack[i]!;
734 + if (target.block === block) {
735 + let type: ControlFlowKind;
736 + if (target.type === "loop") {
737 + // breaking out of a loop requires an explicit break,
738 + // but only requires a label if breaking past the innermost loop.
739 + type = hasPrecedingLoop ? "labeled" : "unlabeled";
740 + } else if (i === this.#controlFlowStack.length - 1) {
741 + // breaking to the last break point, which is where control will transfer
742 + // implicitly
743 + type = "implicit";
744 + } else {
745 + // breaking somewhere else requires an explicit break
746 + type = "labeled";
747 + }
748 + return {
749 + block: target.block,
750 + type,
751 + };
752 + }
753 + hasPrecedingLoop ||= target.type === "loop";
754 + }
755 + return null;
756 + }
757 +
758 + /**
759 + * Given the current control flow stack, determines how a `continue` to the given @param block
760 + * must be emitted. Returns as follows:
761 + * - 'implicit' if control would implicitly continue to that block
762 + * - 'labeled' if a labeled continue is required to continue to that block
763 + * - 'unlabeled' if an unlabeled continue would transfer to that block
764 + * - null if there is no information for this block
765 + *
766 + * The returned 'block' value should be used as the label if necessary.
767 + */
768 + getContinueTarget(
769 + block: BlockId
770 + ): { block: BlockId; type: ControlFlowKind } | null {
771 + let hasPrecedingLoop = false;
772 + for (let i = this.#controlFlowStack.length - 1; i >= 0; i--) {
773 + const target = this.#controlFlowStack[i]!;
774 + if (target.type == "loop" && target.continueBlock === block) {
775 + let type: ControlFlowKind;
776 + if (hasPrecedingLoop) {
777 + // continuing to a loop that is not the innermost loop always requires
778 + // a label
779 + type = "labeled";
780 + } else if (i === this.#controlFlowStack.length - 1) {
781 + // continuing to the last break point, which is where control will
782 + // transfer to naturally
783 + type = "implicit";
784 + } else {
785 + // the continue is inside some conditional logic, requires an explicit
786 + // continue
787 + type = "unlabeled";
788 + }
789 + return {
790 + block: target.block,
791 + type,
792 + };
793 + }
794 + hasPrecedingLoop ||= target.type === "loop";
795 + }
796 + return null;
797 + }
798 +
799 + debugBreakTargets(): Array<ControlFlowTarget> {
800 + return this.#controlFlowStack.map((target) => ({ ...target }));
801 + }
802 +}
803 +
804 +type ControlFlowKind = "implicit" | "labeled" | "unlabeled";
805 +
806 +type ControlFlowTarget =
807 + | { type: "if"; block: BlockId; id: number }
808 + | { type: "switch"; block: BlockId; id: number }
809 + | { type: "case"; block: BlockId; id: number }
810 + | {
811 + type: "loop";
812 + block: BlockId;
813 + ownsBlock: boolean;
814 + continueBlock: BlockId;
815 + loopBlock: BlockId | null;
816 + ownsLoop: boolean;
817 + id: number;
818 + };
819 +
820 +/**
821 + * An object that can receive structured callbacks to visit HIR as a tree,
822 + * and convert it to an alternate format.
823 + *
824 + * TBlock = representation of a list of statements
825 + * TValue = represenation of an InstructionValue
826 + * TStatement = representation of an Instruction
827 + * TCase = representation of a switch case
828 + */
829 +export interface Visitor<
830 + TBlockBuilder,
831 + TBlock,
832 + TInit,
833 + TValueBlock,
834 + TValue,
835 + TStatement,
836 + TCase
837 +> {
838 + /**
839 + * Must create an "empty" instance of the visitor's represenation for
840 + * the contents of a block.
841 + */
842 + enterBlock(): TBlockBuilder;
843 +
844 + /**
845 + * Appends an item onto the given block, with an optional label. The label
846 + * indicates that a break/continue will proceed to code *after* the given item.
847 + */
848 + appendBlock(block: TBlockBuilder, item: TStatement, label?: BlockId): void;
849 +
850 + /**
851 + * Converts the visitor's block representation into the representation of a
852 + * block item, simultaneously "closing" the given block. The block will no
853 + * longer be modified by the visitor driver.
854 + */
855 + leaveBlock(block: TBlockBuilder): TBlock;
856 +
857 + /**
858 + * Must create an "empty" instance of the visitor's representation for a value
859 + * block, which can generally contain only expressions but not statements. The
860 + * currently active parent block is passed as an argument.
861 + *
862 + * The exception is the for initializer, which may contain declarations but not
863 + * other statement types.
864 + */
865 + enterValueBlock(block: TBlockBuilder): TValueBlock;
866 +
867 + /**
868 + * Appends an item onto the given value block.
869 + */
870 + appendValueBlock(block: TValueBlock, item: TStatement): void;
871 +
872 + /**
873 + * Converts the visitor's value block (and final value) to the visitor's
874 + * value representation.
875 + */
876 + leaveValueBlock(block: TValueBlock, value: TValue | null): TValue;
877 +
878 + enterInitBlock(block: TBlockBuilder): TValueBlock;
879 +
880 + appendInitBlock(block: TValueBlock, item: TStatement): void;
881 +
882 + leaveInitBlock(block: TValueBlock): TInit;
883 +
884 + /**
885 + * Convert an InstructionValue into the visitor's own representation
886 + * of a value.
887 + */
888 + visitValue(value: InstructionValue, id: InstructionId): TValue;
889 +
890 + /**
891 + * Convert an Instruction into the visitor's own representation of
892 + * a block item.
893 + */
894 + visitInstruction(instruction: Instruction, value: TValue): TStatement;
895 +
896 + /**
897 + * Called when a terminal is reached, before processing any of its
898 + * possible branches.
899 + */
900 + visitTerminalId(id: InstructionId): void;
901 +
902 + /**
903 + * Converts a break/continue that is implicit — that does not strictly
904 + * have to be emitted — to the visitor's representation. The visitor
905 + * can choose to return null if this does not need to be represented.
906 + */
907 + visitImplicitTerminal(): TStatement | null;
908 +
909 + /**
910 + * Converts a terminal into the visitor's own representation of a block
911 + * item. Note that the terminal differs from HIR Terminals, because
912 + * values and block ids will have already been converted into the visitor's
913 + * own representations.
914 + */
915 + visitTerminal(
916 + terminal: BlockTerminal<TInit, TValue, TBlock, TCase>
917 + ): TStatement;
918 +
919 + /**
920 + * Visits a switch case statement, which is collected into a switch terminal
921 + * variant.
922 + */
923 + visitCase(test: TValue | null, block: TBlock): TCase;
924 +}
925 +
926 +export type BlockTerminal<TInit, TValue, TBlock, TCase> =
927 + | {
928 + kind: "return";
929 + loc: SourceLocation;
930 + value: TValue | null;
931 + id: InstructionId;
932 + }
933 + | { kind: "throw"; value: TValue; id: InstructionId }
934 + | {
935 + kind: "if";
936 + test: TValue;
937 + consequent: TBlock;
938 + alternate: TBlock | null;
939 + id: InstructionId;
940 + }
941 + | { kind: "switch"; test: TValue; cases: Array<TCase>; id: InstructionId }
942 + | {
943 + kind: "while";
944 + loc: SourceLocation;
945 + test: TValue;
946 + loop: TBlock;
947 + id: InstructionId;
948 + }
949 + | {
950 + kind: "for";
951 + init: TInit;
952 + test: TValue;
953 + update: TValue;
954 + loop: TBlock;
955 + id: InstructionId;
956 + }
957 + | { kind: "break"; label: BlockId | null; id: InstructionId | null }
958 + | { kind: "continue"; label: BlockId | null; id: InstructionId };
compiler/forget/src/ReactiveScopes/BuildReactiveFunctionWithoutScopes.ts
+5 -1
@@ -19,7 +19,11 @@ import {
19 ReactiveTerminal,
20 ReactiveValueBlock,
21 } from "../HIR/HIR";
22 -import { BlockTerminal, Visitor, visitTree } from "../HIR/HIRTreeVisitor";
22 +import {
23 + BlockTerminal,
24 + Visitor,
25 + visitTreeForReactiveFunction as visitTree,
26 +} from "../HIR/ReactiveFunctionVisitor";
27 import { assertExhaustive } from "../Utils/utils";
28
29 export function buildReactiveFunction(fn: HIRFunction): ReactiveFunction {