main
ts 1,486 lines 43.7 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and 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 {CompilerError} from '../CompilerError';
9 import {
10 BasicBlock,
11 BlockId,
12 GeneratedSource,
13 GotoVariant,
14 HIR,
15 InstructionId,
16 Place,
17 ReactiveBlock,
18 SourceLocation,
19 } from '../HIR';
20 import {
21 HIRFunction,
22 ReactiveBreakTerminal,
23 ReactiveContinueTerminal,
24 ReactiveFunction,
25 ReactiveInstruction,
26 ReactiveLogicalValue,
27 ReactiveSequenceValue,
28 ReactiveTerminalStatement,
29 ReactiveTerminalTargetKind,
30 ReactiveTernaryValue,
31 ReactiveValue,
32 Terminal,
33 } from '../HIR/HIR';
34 import {assertExhaustive} from '../Utils/utils';
35
36 /*
37 * Converts from HIR (lower-level CFG) to ReactiveFunction, a tree representation
38 * that is closer to an AST. This pass restores the original control flow constructs,
39 * including break/continue to labeled statements. Note that this pass naively emits
40 * labels for *all* terminals: see PruneUnusedLabels which removes unnecessary labels.
41 */
42 export function buildReactiveFunction(fn: HIRFunction): ReactiveFunction {
43 const cx = new Context(fn.body);
44 const driver = new Driver(cx);
45 const body = driver.traverseBlock(cx.block(fn.body.entry));
46 return {
47 loc: fn.loc,
48 id: fn.id,
49 nameHint: fn.nameHint,
50 params: fn.params,
51 generator: fn.generator,
52 async: fn.async,
53 body,
54 env: fn.env,
55 directives: fn.directives,
56 };
57 }
58
59 class Driver {
60 cx: Context;
61
62 constructor(cx: Context) {
63 this.cx = cx;
64 }
65
66 /*
67 * Wraps a continuation result with preceding instructions. If there are no
68 * instructions, returns the continuation as-is. Otherwise, wraps the continuation's
69 * value in a SequenceExpression with the instructions prepended.
70 */
71 wrapWithSequence(
72 instructions: Array<ReactiveInstruction>,
73 continuation: {
74 block: BlockId;
75 value: ReactiveValue;
76 place: Place;
77 id: InstructionId;
78 },
79 loc: SourceLocation,
80 ): {block: BlockId; value: ReactiveValue; place: Place; id: InstructionId} {
81 if (instructions.length === 0) {
82 return continuation;
83 }
84 const sequence: ReactiveSequenceValue = {
85 kind: 'SequenceExpression',
86 instructions,
87 id: continuation.id,
88 value: continuation.value,
89 loc,
90 };
91 return {
92 block: continuation.block,
93 value: sequence,
94 place: continuation.place,
95 id: continuation.id,
96 };
97 }
98
99 /*
100 * Extracts the result value from instructions at the end of a value block.
101 * Value blocks generally end in a StoreLocal to assign the value of the
102 * expression. These StoreLocal instructions can be pruned since we represent
103 * value blocks as compound values in ReactiveFunction (no phis). However,
104 * it's also possible to have a value block that ends in an AssignmentExpression,
105 * which we need to keep. So we only prune StoreLocal for temporaries.
106 */
107 extractValueBlockResult(
108 instructions: BasicBlock['instructions'],
109 blockId: BlockId,
110 loc: SourceLocation,
111 ): {block: BlockId; place: Place; value: ReactiveValue; id: InstructionId} {
112 CompilerError.invariant(instructions.length !== 0, {
113 reason: `Expected non-empty instructions in extractValueBlockResult`,
114 description: null,
115 loc,
116 });
117 const instr = instructions.at(-1)!;
118 let place: Place = instr.lvalue;
119 let value: ReactiveValue = instr.value;
120 if (
121 value.kind === 'StoreLocal' &&
122 value.lvalue.place.identifier.name === null
123 ) {
124 place = value.lvalue.place;
125 value = {
126 kind: 'LoadLocal',
127 place: value.value,
128 loc: value.value.loc,
129 };
130 }
131 if (instructions.length === 1) {
132 return {block: blockId, place, value, id: instr.id};
133 }
134 const sequence: ReactiveSequenceValue = {
135 kind: 'SequenceExpression',
136 instructions: instructions.slice(0, -1),
137 id: instr.id,
138 value,
139 loc,
140 };
141 return {block: blockId, place, value: sequence, id: instr.id};
142 }
143
144 /*
145 * Converts the result of visitValueBlock into a SequenceExpression that includes
146 * the instruction with its lvalue. This is needed for for/for-of/for-in init/test
147 * blocks where the instruction's lvalue assignment must be preserved.
148 *
149 * This also flattens nested SequenceExpressions that can occur from MaybeThrow
150 * handling in try-catch blocks.
151 */
152 valueBlockResultToSequence(
153 result: {
154 block: BlockId;
155 value: ReactiveValue;
156 place: Place;
157 id: InstructionId;
158 },
159 loc: SourceLocation,
160 ): ReactiveSequenceValue {
161 // Collect all instructions from potentially nested SequenceExpressions
162 const instructions: Array<ReactiveInstruction> = [];
163 let innerValue: ReactiveValue = result.value;
164
165 // Flatten nested SequenceExpressions
166 while (innerValue.kind === 'SequenceExpression') {
167 instructions.push(...innerValue.instructions);
168 innerValue = innerValue.value;
169 }
170
171 /*
172 * Only add the final instruction if the innermost value is not just a LoadLocal
173 * of the same place we're storing to (which would be a no-op).
174 * This happens when MaybeThrow blocks cause the sequence to already contain
175 * all the necessary instructions.
176 */
177 const isLoadOfSamePlace =
178 innerValue.kind === 'LoadLocal' &&
179 innerValue.place.identifier.id === result.place.identifier.id;
180
181 if (!isLoadOfSamePlace) {
182 instructions.push({
183 id: result.id,
184 lvalue: result.place,
185 value: innerValue,
186 loc,
187 });
188 }
189
190 return {
191 kind: 'SequenceExpression',
192 instructions,
193 id: result.id,
194 value: {kind: 'Primitive', value: undefined, loc},
195 loc,
196 };
197 }
198
199 traverseBlock(block: BasicBlock): ReactiveBlock {
200 const blockValue: ReactiveBlock = [];
201 this.visitBlock(block, blockValue);
202 return blockValue;
203 }
204
205 visitBlock(block: BasicBlock, blockValue: ReactiveBlock): void {
206 CompilerError.invariant(!this.cx.emitted.has(block.id), {
207 reason: `Cannot emit the same block twice: bb${block.id}`,
208 loc: GeneratedSource,
209 });
210 this.cx.emitted.add(block.id);
211 for (const instruction of block.instructions) {
212 blockValue.push({
213 kind: 'instruction',
214 instruction,
215 });
216 }
217
218 const terminal = block.terminal;
219 const scheduleIds = [];
220 switch (terminal.kind) {
221 case 'return': {
222 blockValue.push({
223 kind: 'terminal',
224 terminal: {
225 kind: 'return',
226 loc: terminal.loc,
227 value: terminal.value,
228 id: terminal.id,
229 },
230 label: null,
231 });
232 break;
233 }
234 case 'throw': {
235 blockValue.push({
236 kind: 'terminal',
237 terminal: {
238 kind: 'throw',
239 loc: terminal.loc,
240 value: terminal.value,
241 id: terminal.id,
242 },
243 label: null,
244 });
245 break;
246 }
247 case 'if': {
248 const fallthroughId =
249 this.cx.reachable(terminal.fallthrough) &&
250 !this.cx.isScheduled(terminal.fallthrough)
251 ? terminal.fallthrough
252 : null;
253 const alternateId =
254 terminal.alternate !== terminal.fallthrough
255 ? terminal.alternate
256 : null;
257
258 if (fallthroughId !== null) {
259 const scheduleId = this.cx.schedule(fallthroughId, 'if');
260 scheduleIds.push(scheduleId);
261 }
262
263 let consequent: ReactiveBlock | null = null;
264 if (this.cx.isScheduled(terminal.consequent)) {
265 CompilerError.invariant(false, {
266 reason: `Unexpected 'if' where the consequent is already scheduled`,
267 loc: terminal.loc,
268 });
269 } else {
270 consequent = this.traverseBlock(
271 this.cx.ir.blocks.get(terminal.consequent)!,
272 );
273 }
274
275 let alternate: ReactiveBlock | null = null;
276 if (alternateId !== null) {
277 if (this.cx.isScheduled(alternateId)) {
278 CompilerError.invariant(false, {
279 reason: `Unexpected 'if' where the alternate is already scheduled`,
280 loc: terminal.loc,
281 });
282 } else {
283 alternate = this.traverseBlock(this.cx.ir.blocks.get(alternateId)!);
284 }
285 }
286
287 this.cx.unscheduleAll(scheduleIds);
288 blockValue.push({
289 kind: 'terminal',
290 terminal: {
291 kind: 'if',
292 loc: terminal.loc,
293 test: terminal.test,
294 consequent: consequent ?? this.emptyBlock(),
295 alternate: alternate,
296 id: terminal.id,
297 },
298 label:
299 fallthroughId == null
300 ? null
301 : {
302 id: fallthroughId,
303 implicit: false,
304 },
305 });
306 if (fallthroughId !== null) {
307 this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue);
308 }
309 break;
310 }
311 case 'switch': {
312 const fallthroughId =
313 this.cx.reachable(terminal.fallthrough) &&
314 !this.cx.isScheduled(terminal.fallthrough)
315 ? terminal.fallthrough
316 : null;
317 if (fallthroughId !== null) {
318 const scheduleId = this.cx.schedule(fallthroughId, 'switch');
319 scheduleIds.push(scheduleId);
320 }
321
322 const cases: Array<{
323 test: Place | null;
324 block: ReactiveBlock;
325 }> = [];
326 [...terminal.cases].reverse().forEach((case_, _index) => {
327 const test = case_.test;
328
329 let consequent: ReactiveBlock;
330 if (this.cx.isScheduled(case_.block)) {
331 CompilerError.invariant(case_.block === terminal.fallthrough, {
332 reason: `Unexpected 'switch' where a case is already scheduled and block is not the fallthrough`,
333 loc: terminal.loc,
334 });
335 return;
336 } else {
337 consequent = this.traverseBlock(
338 this.cx.ir.blocks.get(case_.block)!,
339 );
340 const scheduleId = this.cx.schedule(case_.block, 'case');
341 scheduleIds.push(scheduleId);
342 }
343 cases.push({test, block: consequent});
344 });
345 cases.reverse();
346
347 this.cx.unscheduleAll(scheduleIds);
348 blockValue.push({
349 kind: 'terminal',
350 terminal: {
351 kind: 'switch',
352 loc: terminal.loc,
353 test: terminal.test,
354 cases,
355 id: terminal.id,
356 },
357 label:
358 fallthroughId == null
359 ? null
360 : {
361 id: fallthroughId,
362 implicit: false,
363 },
364 });
365 if (fallthroughId !== null) {
366 this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue);
367 }
368 break;
369 }
370 case 'do-while': {
371 const fallthroughId = !this.cx.isScheduled(terminal.fallthrough)
372 ? terminal.fallthrough
373 : null;
374 const loopId =
375 !this.cx.isScheduled(terminal.loop) &&
376 terminal.loop !== terminal.fallthrough
377 ? terminal.loop
378 : null;
379 const scheduleId = this.cx.scheduleLoop(
380 terminal.fallthrough,
381 terminal.test,
382 terminal.loop,
383 );
384 scheduleIds.push(scheduleId);
385
386 let loopBody: ReactiveBlock;
387 if (loopId) {
388 loopBody = this.traverseBlock(this.cx.ir.blocks.get(loopId)!);
389 } else {
390 CompilerError.invariant(false, {
391 reason: `Unexpected 'do-while' where the loop is already scheduled`,
392 loc: terminal.loc,
393 });
394 }
395
396 const testValue = this.visitValueBlock(
397 terminal.test,
398 terminal.loc,
399 ).value;
400
401 this.cx.unscheduleAll(scheduleIds);
402 blockValue.push({
403 kind: 'terminal',
404 terminal: {
405 kind: 'do-while',
406 loc: terminal.loc,
407 test: testValue,
408 loop: loopBody,
409 id: terminal.id,
410 },
411 label:
412 fallthroughId == null
413 ? null
414 : {
415 id: fallthroughId,
416 implicit: false,
417 },
418 });
419 if (fallthroughId !== null) {
420 this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue);
421 }
422 break;
423 }
424 case 'while': {
425 const fallthroughId =
426 this.cx.reachable(terminal.fallthrough) &&
427 !this.cx.isScheduled(terminal.fallthrough)
428 ? terminal.fallthrough
429 : null;
430 const loopId =
431 !this.cx.isScheduled(terminal.loop) &&
432 terminal.loop !== terminal.fallthrough
433 ? terminal.loop
434 : null;
435 const scheduleId = this.cx.scheduleLoop(
436 terminal.fallthrough,
437 terminal.test,
438 terminal.loop,
439 );
440 scheduleIds.push(scheduleId);
441
442 const testValue = this.visitValueBlock(
443 terminal.test,
444 terminal.loc,
445 ).value;
446
447 let loopBody: ReactiveBlock;
448 if (loopId) {
449 loopBody = this.traverseBlock(this.cx.ir.blocks.get(loopId)!);
450 } else {
451 CompilerError.invariant(false, {
452 reason: `Unexpected 'while' where the loop is already scheduled`,
453 loc: terminal.loc,
454 });
455 }
456
457 this.cx.unscheduleAll(scheduleIds);
458 blockValue.push({
459 kind: 'terminal',
460 terminal: {
461 kind: 'while',
462 loc: terminal.loc,
463 test: testValue,
464 loop: loopBody,
465 id: terminal.id,
466 },
467 label:
468 fallthroughId == null
469 ? null
470 : {
471 id: fallthroughId,
472 implicit: false,
473 },
474 });
475 if (fallthroughId !== null) {
476 this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue);
477 }
478 break;
479 }
480 case 'for': {
481 const loopId =
482 !this.cx.isScheduled(terminal.loop) &&
483 terminal.loop !== terminal.fallthrough
484 ? terminal.loop
485 : null;
486
487 const fallthroughId = !this.cx.isScheduled(terminal.fallthrough)
488 ? terminal.fallthrough
489 : null;
490
491 const scheduleId = this.cx.scheduleLoop(
492 terminal.fallthrough,
493 terminal.update ?? terminal.test,
494 terminal.loop,
495 );
496 scheduleIds.push(scheduleId);
497
498 const init = this.visitValueBlock(terminal.init, terminal.loc);
499 const initValue = this.valueBlockResultToSequence(init, terminal.loc);
500
501 const testValue = this.visitValueBlock(
502 terminal.test,
503 terminal.loc,
504 ).value;
505
506 const updateValue =
507 terminal.update !== null
508 ? this.visitValueBlock(terminal.update, terminal.loc).value
509 : null;
510
511 let loopBody: ReactiveBlock;
512 if (loopId) {
513 loopBody = this.traverseBlock(this.cx.ir.blocks.get(loopId)!);
514 } else {
515 CompilerError.invariant(false, {
516 reason: `Unexpected 'for' where the loop is already scheduled`,
517 loc: terminal.loc,
518 });
519 }
520
521 this.cx.unscheduleAll(scheduleIds);
522 blockValue.push({
523 kind: 'terminal',
524 terminal: {
525 kind: 'for',
526 loc: terminal.loc,
527 init: initValue,
528 test: testValue,
529 update: updateValue,
530 loop: loopBody,
531 id: terminal.id,
532 },
533 label:
534 fallthroughId == null ? null : {id: fallthroughId, implicit: false},
535 });
536 if (fallthroughId !== null) {
537 this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue);
538 }
539 break;
540 }
541 case 'for-of': {
542 const loopId =
543 !this.cx.isScheduled(terminal.loop) &&
544 terminal.loop !== terminal.fallthrough
545 ? terminal.loop
546 : null;
547
548 const fallthroughId = !this.cx.isScheduled(terminal.fallthrough)
549 ? terminal.fallthrough
550 : null;
551
552 const scheduleId = this.cx.scheduleLoop(
553 terminal.fallthrough,
554 terminal.init,
555 terminal.loop,
556 );
557 scheduleIds.push(scheduleId);
558
559 const init = this.visitValueBlock(terminal.init, terminal.loc);
560 const initValue = this.valueBlockResultToSequence(init, terminal.loc);
561
562 const test = this.visitValueBlock(terminal.test, terminal.loc);
563 const testValue = this.valueBlockResultToSequence(test, terminal.loc);
564
565 let loopBody: ReactiveBlock;
566 if (loopId) {
567 loopBody = this.traverseBlock(this.cx.ir.blocks.get(loopId)!);
568 } else {
569 CompilerError.invariant(false, {
570 reason: `Unexpected 'for-of' where the loop is already scheduled`,
571 loc: terminal.loc,
572 });
573 }
574
575 this.cx.unscheduleAll(scheduleIds);
576 blockValue.push({
577 kind: 'terminal',
578 terminal: {
579 kind: 'for-of',
580 loc: terminal.loc,
581 init: initValue,
582 test: testValue,
583 loop: loopBody,
584 id: terminal.id,
585 },
586 label:
587 fallthroughId == null ? null : {id: fallthroughId, implicit: false},
588 });
589 if (fallthroughId !== null) {
590 this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue);
591 }
592 break;
593 }
594 case 'for-in': {
595 const loopId =
596 !this.cx.isScheduled(terminal.loop) &&
597 terminal.loop !== terminal.fallthrough
598 ? terminal.loop
599 : null;
600
601 const fallthroughId = !this.cx.isScheduled(terminal.fallthrough)
602 ? terminal.fallthrough
603 : null;
604
605 const scheduleId = this.cx.scheduleLoop(
606 terminal.fallthrough,
607 terminal.init,
608 terminal.loop,
609 );
610 scheduleIds.push(scheduleId);
611
612 const init = this.visitValueBlock(terminal.init, terminal.loc);
613 const initValue = this.valueBlockResultToSequence(init, terminal.loc);
614
615 let loopBody: ReactiveBlock;
616 if (loopId) {
617 loopBody = this.traverseBlock(this.cx.ir.blocks.get(loopId)!);
618 } else {
619 CompilerError.invariant(false, {
620 reason: `Unexpected 'for-in' where the loop is already scheduled`,
621 loc: terminal.loc,
622 });
623 }
624
625 this.cx.unscheduleAll(scheduleIds);
626 blockValue.push({
627 kind: 'terminal',
628 terminal: {
629 kind: 'for-in',
630 loc: terminal.loc,
631 init: initValue,
632 loop: loopBody,
633 id: terminal.id,
634 },
635 label:
636 fallthroughId == null ? null : {id: fallthroughId, implicit: false},
637 });
638 if (fallthroughId !== null) {
639 this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue);
640 }
641 break;
642 }
643 case 'branch': {
644 let consequent: ReactiveBlock | null = null;
645 if (this.cx.isScheduled(terminal.consequent)) {
646 const break_ = this.visitBreak(
647 terminal.consequent,
648 terminal.id,
649 terminal.loc,
650 );
651 if (break_ !== null) {
652 consequent = [break_];
653 }
654 } else {
655 consequent = this.traverseBlock(
656 this.cx.ir.blocks.get(terminal.consequent)!,
657 );
658 }
659
660 let alternate: ReactiveBlock | null = null;
661 if (this.cx.isScheduled(terminal.alternate)) {
662 CompilerError.invariant(false, {
663 reason: `Unexpected 'branch' where the alternate is already scheduled`,
664 loc: terminal.loc,
665 });
666 } else {
667 alternate = this.traverseBlock(
668 this.cx.ir.blocks.get(terminal.alternate)!,
669 );
670 }
671
672 blockValue.push({
673 kind: 'terminal',
674 terminal: {
675 kind: 'if',
676 loc: terminal.loc,
677 test: terminal.test,
678 consequent: consequent ?? this.emptyBlock(),
679 alternate: alternate,
680 id: terminal.id,
681 },
682 label: null,
683 });
684
685 break;
686 }
687 case 'label': {
688 const fallthroughId =
689 this.cx.reachable(terminal.fallthrough) &&
690 !this.cx.isScheduled(terminal.fallthrough)
691 ? terminal.fallthrough
692 : null;
693 if (fallthroughId !== null) {
694 const scheduleId = this.cx.schedule(fallthroughId, 'if');
695 scheduleIds.push(scheduleId);
696 }
697
698 let block: ReactiveBlock;
699 if (this.cx.isScheduled(terminal.block)) {
700 CompilerError.invariant(false, {
701 reason: `Unexpected 'label' where the block is already scheduled`,
702 loc: terminal.loc,
703 });
704 } else {
705 block = this.traverseBlock(this.cx.ir.blocks.get(terminal.block)!);
706 }
707
708 this.cx.unscheduleAll(scheduleIds);
709 blockValue.push({
710 kind: 'terminal',
711 terminal: {
712 kind: 'label',
713 loc: terminal.loc,
714 block,
715 id: terminal.id,
716 },
717 label:
718 fallthroughId == null ? null : {id: fallthroughId, implicit: false},
719 });
720 if (fallthroughId !== null) {
721 this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue);
722 }
723
724 break;
725 }
726 case 'sequence':
727 case 'optional':
728 case 'ternary':
729 case 'logical': {
730 const fallthroughId =
731 terminal.fallthrough !== null &&
732 !this.cx.isScheduled(terminal.fallthrough)
733 ? terminal.fallthrough
734 : null;
735 if (fallthroughId !== null) {
736 const scheduleId = this.cx.schedule(fallthroughId, 'if');
737 scheduleIds.push(scheduleId);
738 }
739
740 const {place, value} = this.visitValueBlockTerminal(terminal);
741 this.cx.unscheduleAll(scheduleIds);
742 blockValue.push({
743 kind: 'instruction',
744 instruction: {
745 id: terminal.id,
746 lvalue: place,
747 value,
748 loc: terminal.loc,
749 },
750 });
751
752 if (fallthroughId !== null) {
753 this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue);
754 }
755 break;
756 }
757 case 'goto': {
758 switch (terminal.variant) {
759 case GotoVariant.Break: {
760 const break_ = this.visitBreak(
761 terminal.block,
762 terminal.id,
763 terminal.loc,
764 );
765 if (break_ !== null) {
766 blockValue.push(break_);
767 }
768 break;
769 }
770 case GotoVariant.Continue: {
771 const continue_ = this.visitContinue(
772 terminal.block,
773 terminal.id,
774 terminal.loc,
775 );
776 if (continue_ !== null) {
777 blockValue.push(continue_);
778 }
779 break;
780 }
781 case GotoVariant.Try: {
782 break;
783 }
784 default: {
785 assertExhaustive(
786 terminal.variant,
787 `Unexpected goto variant \`${terminal.variant}\``,
788 );
789 }
790 }
791 break;
792 }
793 case 'maybe-throw': {
794 /*
795 * ReactiveFunction does not explicit model maybe-throw semantics,
796 * so these terminals flatten away
797 */
798 if (!this.cx.isScheduled(terminal.continuation)) {
799 this.visitBlock(
800 this.cx.ir.blocks.get(terminal.continuation)!,
801 blockValue,
802 );
803 }
804 break;
805 }
806 case 'try': {
807 const fallthroughId =
808 this.cx.reachable(terminal.fallthrough) &&
809 !this.cx.isScheduled(terminal.fallthrough)
810 ? terminal.fallthrough
811 : null;
812 if (fallthroughId !== null) {
813 const scheduleId = this.cx.schedule(fallthroughId, 'if');
814 scheduleIds.push(scheduleId);
815 }
816 this.cx.scheduleCatchHandler(terminal.handler);
817
818 const block = this.traverseBlock(
819 this.cx.ir.blocks.get(terminal.block)!,
820 );
821 const handler = this.traverseBlock(
822 this.cx.ir.blocks.get(terminal.handler)!,
823 );
824
825 this.cx.unscheduleAll(scheduleIds);
826 blockValue.push({
827 kind: 'terminal',
828 label:
829 fallthroughId == null ? null : {id: fallthroughId, implicit: false},
830 terminal: {
831 kind: 'try',
832 loc: terminal.loc,
833 block,
834 handlerBinding: terminal.handlerBinding,
835 handler,
836 id: terminal.id,
837 },
838 });
839
840 if (fallthroughId !== null) {
841 this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue);
842 }
843 break;
844 }
845 case 'pruned-scope':
846 case 'scope': {
847 const fallthroughId = !this.cx.isScheduled(terminal.fallthrough)
848 ? terminal.fallthrough
849 : null;
850 if (fallthroughId !== null) {
851 const scheduleId = this.cx.schedule(fallthroughId, 'if');
852 scheduleIds.push(scheduleId);
853 this.cx.scopeFallthroughs.add(fallthroughId);
854 }
855
856 let block: ReactiveBlock;
857 if (this.cx.isScheduled(terminal.block)) {
858 CompilerError.invariant(false, {
859 reason: `Unexpected 'scope' where the block is already scheduled`,
860 loc: terminal.loc,
861 });
862 } else {
863 block = this.traverseBlock(this.cx.ir.blocks.get(terminal.block)!);
864 }
865
866 this.cx.unscheduleAll(scheduleIds);
867 blockValue.push({
868 kind: terminal.kind,
869 instructions: block,
870 scope: terminal.scope,
871 });
872 if (fallthroughId !== null) {
873 this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue);
874 }
875
876 break;
877 }
878 case 'unreachable': {
879 // noop
880 break;
881 }
882 case 'unsupported': {
883 CompilerError.invariant(false, {
884 reason: 'Unexpected unsupported terminal',
885 loc: terminal.loc,
886 });
887 }
888 default: {
889 assertExhaustive(terminal, 'Unexpected terminal');
890 }
891 }
892 }
893
894 visitValueBlock(
895 blockId: BlockId,
896 loc: SourceLocation,
897 fallthrough: BlockId | null = null,
898 ): {block: BlockId; value: ReactiveValue; place: Place; id: InstructionId} {
899 const block = this.cx.ir.blocks.get(blockId)!;
900 // If we've reached the fallthrough block, stop recursing
901 if (fallthrough !== null && blockId === fallthrough) {
902 CompilerError.invariant(false, {
903 reason: 'Did not expect to reach the fallthrough of a value block',
904 description: `Reached bb${blockId}, which is the fallthrough for this value block`,
905 loc,
906 });
907 }
908 if (block.terminal.kind === 'branch') {
909 if (block.instructions.length === 0) {
910 return {
911 block: block.id,
912 place: block.terminal.test,
913 value: {
914 kind: 'LoadLocal',
915 place: block.terminal.test,
916 loc: block.terminal.test.loc,
917 },
918 id: block.terminal.id,
919 };
920 }
921 return this.extractValueBlockResult(block.instructions, block.id, loc);
922 } else if (block.terminal.kind === 'goto') {
923 if (block.instructions.length === 0) {
924 CompilerError.invariant(false, {
925 reason: 'Unexpected empty block with `goto` terminal',
926 description: `Block bb${block.id} is empty`,
927 loc,
928 });
929 }
930 return this.extractValueBlockResult(block.instructions, block.id, loc);
931 } else if (block.terminal.kind === 'maybe-throw') {
932 /*
933 * ReactiveFunction does not explicitly model maybe-throw semantics,
934 * so maybe-throw terminals in value blocks flatten away. In general
935 * we recurse to the continuation block.
936 *
937 * However, if the last portion
938 * of the value block is a potentially throwing expression, then the
939 * value block could be of the form
940 * ```
941 * bb1:
942 * ...StoreLocal for the value block...
943 * maybe-throw continuation=bb2
944 * bb2:
945 * goto (exit the value block)
946 * ```
947 *
948 * Ie what would have been a StoreLocal+goto is split up because of
949 * the maybe-throw. We detect this case and return the value of the
950 * current block as the result of the value block
951 */
952 const continuationId = block.terminal.continuation;
953 const continuationBlock = this.cx.ir.blocks.get(continuationId)!;
954 if (
955 continuationBlock.instructions.length === 0 &&
956 continuationBlock.terminal.kind === 'goto'
957 ) {
958 return this.extractValueBlockResult(
959 block.instructions,
960 continuationBlock.id,
961 loc,
962 );
963 }
964
965 const continuation = this.visitValueBlock(
966 continuationId,
967 loc,
968 fallthrough,
969 );
970 return this.wrapWithSequence(block.instructions, continuation, loc);
971 } else {
972 /*
973 * The value block ended in a value terminal, recurse to get the value
974 * of that terminal and stitch them together in a sequence.
975 */
976 const init = this.visitValueBlockTerminal(block.terminal);
977 const final = this.visitValueBlock(init.fallthrough, loc);
978 return this.wrapWithSequence(
979 [
980 ...block.instructions,
981 {id: init.id, loc, lvalue: init.place, value: init.value},
982 ],
983 final,
984 loc,
985 );
986 }
987 }
988
989 /*
990 * Visits the test block of a value terminal (optional, logical, ternary) and
991 * returns the result along with the branch terminal. Throws a todo error if
992 * the test block does not end in a branch terminal.
993 */
994 visitTestBlock(
995 testBlockId: BlockId,
996 loc: SourceLocation,
997 terminalKind: string,
998 ): {
999 test: {
1000 block: BlockId;
1001 value: ReactiveValue;
1002 place: Place;
1003 id: InstructionId;
1004 };
1005 branch: {consequent: BlockId; alternate: BlockId; loc: SourceLocation};
1006 } {
1007 const test = this.visitValueBlock(testBlockId, loc);
1008 const testBlock = this.cx.ir.blocks.get(test.block)!;
1009 if (testBlock.terminal.kind !== 'branch') {
1010 CompilerError.invariant(false, {
1011 reason: `Expected a branch terminal for ${terminalKind} test block`,
1012 description: `Got \`${testBlock.terminal.kind}\``,
1013 loc: testBlock.terminal.loc,
1014 });
1015 }
1016 return {
1017 test,
1018 branch: {
1019 consequent: testBlock.terminal.consequent,
1020 alternate: testBlock.terminal.alternate,
1021 loc: testBlock.terminal.loc,
1022 },
1023 };
1024 }
1025
1026 visitValueBlockTerminal(terminal: Terminal): {
1027 value: ReactiveValue;
1028 place: Place;
1029 fallthrough: BlockId;
1030 id: InstructionId;
1031 } {
1032 switch (terminal.kind) {
1033 case 'sequence': {
1034 const block = this.visitValueBlock(
1035 terminal.block,
1036 terminal.loc,
1037 terminal.fallthrough,
1038 );
1039 return {
1040 value: block.value,
1041 place: block.place,
1042 fallthrough: terminal.fallthrough,
1043 id: terminal.id,
1044 };
1045 }
1046 case 'optional': {
1047 const {test, branch} = this.visitTestBlock(
1048 terminal.test,
1049 terminal.loc,
1050 'optional',
1051 );
1052 const consequent = this.visitValueBlock(
1053 branch.consequent,
1054 terminal.loc,
1055 terminal.fallthrough,
1056 );
1057 const call: ReactiveSequenceValue = {
1058 kind: 'SequenceExpression',
1059 instructions: [
1060 {
1061 id: test.id,
1062 loc: branch.loc,
1063 lvalue: test.place,
1064 value: test.value,
1065 },
1066 ],
1067 id: consequent.id,
1068 value: consequent.value,
1069 loc: terminal.loc,
1070 };
1071 return {
1072 place: {...consequent.place},
1073 value: {
1074 kind: 'OptionalExpression',
1075 optional: terminal.optional,
1076 value: call,
1077 id: terminal.id,
1078 loc: terminal.loc,
1079 },
1080 fallthrough: terminal.fallthrough,
1081 id: terminal.id,
1082 };
1083 }
1084 case 'logical': {
1085 const {test, branch} = this.visitTestBlock(
1086 terminal.test,
1087 terminal.loc,
1088 'logical',
1089 );
1090 const leftFinal = this.visitValueBlock(
1091 branch.consequent,
1092 terminal.loc,
1093 terminal.fallthrough,
1094 );
1095 const left: ReactiveSequenceValue = {
1096 kind: 'SequenceExpression',
1097 instructions: [
1098 {
1099 id: test.id,
1100 loc: terminal.loc,
1101 lvalue: test.place,
1102 value: test.value,
1103 },
1104 ],
1105 id: leftFinal.id,
1106 value: leftFinal.value,
1107 loc: terminal.loc,
1108 };
1109 const right = this.visitValueBlock(
1110 branch.alternate,
1111 terminal.loc,
1112 terminal.fallthrough,
1113 );
1114 const value: ReactiveLogicalValue = {
1115 kind: 'LogicalExpression',
1116 operator: terminal.operator,
1117 left: left,
1118 right: right.value,
1119 loc: terminal.loc,
1120 };
1121 return {
1122 place: {...leftFinal.place},
1123 value,
1124 fallthrough: terminal.fallthrough,
1125 id: terminal.id,
1126 };
1127 }
1128 case 'ternary': {
1129 const {test, branch} = this.visitTestBlock(
1130 terminal.test,
1131 terminal.loc,
1132 'ternary',
1133 );
1134 const consequent = this.visitValueBlock(
1135 branch.consequent,
1136 terminal.loc,
1137 terminal.fallthrough,
1138 );
1139 const alternate = this.visitValueBlock(
1140 branch.alternate,
1141 terminal.loc,
1142 terminal.fallthrough,
1143 );
1144 const value: ReactiveTernaryValue = {
1145 kind: 'ConditionalExpression',
1146 test: test.value,
1147 consequent: consequent.value,
1148 alternate: alternate.value,
1149 loc: terminal.loc,
1150 };
1151
1152 return {
1153 place: {...consequent.place},
1154 value,
1155 fallthrough: terminal.fallthrough,
1156 id: terminal.id,
1157 };
1158 }
1159 case 'maybe-throw': {
1160 CompilerError.invariant(false, {
1161 reason: `Unexpected maybe-throw in visitValueBlockTerminal - should be handled in visitValueBlock`,
1162 description: null,
1163 loc: terminal.loc,
1164 });
1165 }
1166 case 'label': {
1167 CompilerError.throwTodo({
1168 reason: `Support labeled statements combined with value blocks (conditional, logical, optional chaining, etc)`,
1169 description: null,
1170 loc: terminal.loc,
1171 suggestions: null,
1172 });
1173 }
1174 default: {
1175 CompilerError.throwTodo({
1176 reason: `Support \`${terminal.kind}\` as a value block terminal (conditional, logical, optional chaining, etc)`,
1177 description: null,
1178 loc: terminal.loc,
1179 suggestions: null,
1180 });
1181 }
1182 }
1183 }
1184
1185 emptyBlock(): ReactiveBlock {
1186 return [];
1187 }
1188
1189 visitBreak(
1190 block: BlockId,
1191 id: InstructionId,
1192 loc: SourceLocation,
1193 ): ReactiveTerminalStatement<ReactiveBreakTerminal> | null {
1194 const target = this.cx.getBreakTarget(block);
1195 if (target === null) {
1196 CompilerError.invariant(false, {
1197 reason: 'Expected a break target',
1198 loc: GeneratedSource,
1199 });
1200 }
1201 if (this.cx.scopeFallthroughs.has(target.block)) {
1202 CompilerError.invariant(target.type === 'implicit', {
1203 reason: 'Expected reactive scope to implicitly break to fallthrough',
1204 loc,
1205 });
1206 return null;
1207 }
1208 return {
1209 kind: 'terminal',
1210 terminal: {
1211 kind: 'break',
1212 loc,
1213 target: target.block,
1214 id,
1215 targetKind: target.type,
1216 },
1217 label: null,
1218 };
1219 }
1220
1221 visitContinue(
1222 block: BlockId,
1223 id: InstructionId,
1224 loc: SourceLocation,
1225 ): ReactiveTerminalStatement<ReactiveContinueTerminal> {
1226 const target = this.cx.getContinueTarget(block);
1227 CompilerError.invariant(target !== null, {
1228 reason: `Expected continue target to be scheduled for bb${block}`,
1229 loc: GeneratedSource,
1230 });
1231
1232 return {
1233 kind: 'terminal',
1234 terminal: {
1235 kind: 'continue',
1236 loc,
1237 target: target.block,
1238 id,
1239 targetKind: target.type,
1240 },
1241 label: null,
1242 };
1243 }
1244 }
1245
1246 class Context {
1247 ir: HIR;
1248 #nextScheduleId: number = 0;
1249
1250 /*
1251 * Used to track which blocks *have been* generated already in order to
1252 * abort if a block is generated a second time. This is an error catching
1253 * mechanism for debugging purposes, and is not used by the codegen algorithm
1254 * to drive decisions about how to emit blocks.
1255 */
1256 emitted: Set<BlockId> = new Set();
1257
1258 scopeFallthroughs: Set<BlockId> = new Set();
1259 /*
1260 * A set of blocks that are already scheduled to be emitted by eg a parent.
1261 * This allows child nodes to avoid re-emitting the same block and emit eg
1262 * a break instead.
1263 */
1264 #scheduled: Set<BlockId> = new Set();
1265
1266 #catchHandlers: Set<BlockId> = new Set();
1267
1268 /*
1269 * Represents which control flow operations are currently in scope, with the innermost
1270 * scope last. Roughly speaking, the last ControlFlowTarget on the stack indicates where
1271 * control will implicitly transfer, such that gotos to that block can be elided. Gotos
1272 * targeting items higher up the stack may need labeled break or continue; see
1273 * getBreakTarget() and getContinueTarget() for more details.
1274 */
1275 #controlFlowStack: Array<ControlFlowTarget> = [];
1276
1277 constructor(ir: HIR) {
1278 this.ir = ir;
1279 }
1280
1281 block(id: BlockId): BasicBlock {
1282 return this.ir.blocks.get(id)!;
1283 }
1284
1285 scheduleCatchHandler(block: BlockId): void {
1286 this.#catchHandlers.add(block);
1287 }
1288
1289 reachable(id: BlockId): boolean {
1290 const block = this.ir.blocks.get(id)!;
1291 return block.terminal.kind !== 'unreachable';
1292 }
1293
1294 /*
1295 * Record that the given block will be emitted (eg by the codegen of a parent node)
1296 * so that child nodes can avoid re-emitting it.
1297 */
1298 schedule(block: BlockId, type: 'if' | 'switch' | 'case'): number {
1299 const id = this.#nextScheduleId++;
1300 CompilerError.invariant(!this.#scheduled.has(block), {
1301 reason: `Break block is already scheduled: bb${block}`,
1302 loc: GeneratedSource,
1303 });
1304 this.#scheduled.add(block);
1305 this.#controlFlowStack.push({block, id, type});
1306 return id;
1307 }
1308
1309 scheduleLoop(
1310 fallthroughBlock: BlockId,
1311 continueBlock: BlockId,
1312 loopBlock: BlockId | null,
1313 ): number {
1314 const id = this.#nextScheduleId++;
1315 const ownsBlock = !this.#scheduled.has(fallthroughBlock);
1316 this.#scheduled.add(fallthroughBlock);
1317 CompilerError.invariant(!this.#scheduled.has(continueBlock), {
1318 reason: `Continue block is already scheduled: bb${continueBlock}`,
1319 loc: GeneratedSource,
1320 });
1321 this.#scheduled.add(continueBlock);
1322 let ownsLoop = false;
1323 if (loopBlock !== null) {
1324 ownsLoop = !this.#scheduled.has(loopBlock);
1325 this.#scheduled.add(loopBlock);
1326 }
1327
1328 this.#controlFlowStack.push({
1329 block: fallthroughBlock,
1330 ownsBlock,
1331 id,
1332 type: 'loop',
1333 continueBlock,
1334 loopBlock,
1335 ownsLoop,
1336 });
1337 return id;
1338 }
1339
1340 // Removes a block that was scheduled; must be called after that block is emitted.
1341 unschedule(scheduleId: number): void {
1342 const last = this.#controlFlowStack.pop();
1343 CompilerError.invariant(last !== undefined && last.id === scheduleId, {
1344 reason: 'Can only unschedule the last target',
1345 loc: GeneratedSource,
1346 });
1347 if (last.type !== 'loop' || last.ownsBlock !== null) {
1348 this.#scheduled.delete(last.block);
1349 }
1350 if (last.type === 'loop') {
1351 this.#scheduled.delete(last.continueBlock);
1352 if (last.ownsLoop && last.loopBlock !== null) {
1353 this.#scheduled.delete(last.loopBlock);
1354 }
1355 }
1356 }
1357
1358 /*
1359 * Helper to unschedule multiple scheduled blocks. The ids should be in
1360 * the order in which they were scheduled, ie most recently scheduled last.
1361 */
1362 unscheduleAll(scheduleIds: Array<number>): void {
1363 for (let i = scheduleIds.length - 1; i >= 0; i--) {
1364 this.unschedule(scheduleIds[i]!);
1365 }
1366 }
1367
1368 // Check if the given @param block is scheduled or not.
1369 isScheduled(block: BlockId): boolean {
1370 return this.#scheduled.has(block) || this.#catchHandlers.has(block);
1371 }
1372
1373 /*
1374 * Given the current control flow stack, determines how a `break` to the given @param block
1375 * must be emitted. Returns as follows:
1376 * - 'implicit' if control would implicitly transfer to that block
1377 * - 'labeled' if a labeled break is required to transfer control to that block
1378 * - 'unlabeled' if an unlabeled break would transfer to that block
1379 * - null if there is no information for this block
1380 *
1381 * The returned 'block' value should be used as the label if necessary.
1382 */
1383 getBreakTarget(block: BlockId): {
1384 block: BlockId;
1385 type: ReactiveTerminalTargetKind;
1386 } {
1387 let hasPrecedingLoop = false;
1388 for (let i = this.#controlFlowStack.length - 1; i >= 0; i--) {
1389 const target = this.#controlFlowStack[i]!;
1390 if (target.block === block) {
1391 let type: ReactiveTerminalTargetKind;
1392 if (target.type === 'loop') {
1393 /*
1394 * breaking out of a loop requires an explicit break,
1395 * but only requires a label if breaking past the innermost loop.
1396 */
1397 type = hasPrecedingLoop ? 'labeled' : 'unlabeled';
1398 } else if (i === this.#controlFlowStack.length - 1) {
1399 /*
1400 * breaking to the last break point, which is where control will transfer
1401 * implicitly
1402 */
1403 type = 'implicit';
1404 } else {
1405 // breaking somewhere else requires an explicit break
1406 type = 'labeled';
1407 }
1408 return {
1409 block: target.block,
1410 type,
1411 };
1412 }
1413 hasPrecedingLoop ||= target.type === 'loop';
1414 }
1415
1416 CompilerError.invariant(false, {
1417 reason: 'Expected a break target',
1418 loc: GeneratedSource,
1419 });
1420 }
1421
1422 /*
1423 * Given the current control flow stack, determines how a `continue` to the given @param block
1424 * must be emitted. Returns as follows:
1425 * - 'implicit' if control would implicitly continue to that block
1426 * - 'labeled' if a labeled continue is required to continue to that block
1427 * - 'unlabeled' if an unlabeled continue would transfer to that block
1428 * - null if there is no information for this block
1429 *
1430 * The returned 'block' value should be used as the label if necessary.
1431 */
1432 getContinueTarget(
1433 block: BlockId,
1434 ): {block: BlockId; type: ReactiveTerminalTargetKind} | null {
1435 let hasPrecedingLoop = false;
1436 for (let i = this.#controlFlowStack.length - 1; i >= 0; i--) {
1437 const target = this.#controlFlowStack[i]!;
1438 if (target.type == 'loop' && target.continueBlock === block) {
1439 let type: ReactiveTerminalTargetKind;
1440 if (hasPrecedingLoop) {
1441 /*
1442 * continuing to a loop that is not the innermost loop always requires
1443 * a label
1444 */
1445 type = 'labeled';
1446 } else if (i === this.#controlFlowStack.length - 1) {
1447 /*
1448 * continuing to the last break point, which is where control will
1449 * transfer to naturally
1450 */
1451 type = 'implicit';
1452 } else {
1453 /*
1454 * the continue is inside some conditional logic, requires an explicit
1455 * continue
1456 */
1457 type = 'unlabeled';
1458 }
1459 return {
1460 block: target.block,
1461 type,
1462 };
1463 }
1464 hasPrecedingLoop ||= target.type === 'loop';
1465 }
1466 return null;
1467 }
1468
1469 debugBreakTargets(): Array<ControlFlowTarget> {
1470 return this.#controlFlowStack.map(target => ({...target}));
1471 }
1472 }
1473
1474 type ControlFlowTarget =
1475 | {type: 'if'; block: BlockId; id: number}
1476 | {type: 'switch'; block: BlockId; id: number}
1477 | {type: 'case'; block: BlockId; id: number}
1478 | {
1479 type: 'loop';
1480 block: BlockId;
1481 ownsBlock: boolean;
1482 continueBlock: BlockId;
1483 loopBlock: BlockId | null;
1484 ownsLoop: boolean;
1485 id: number;
1486 };