1/**2 * Copyright (c) Meta Platforms, Inc. and affiliates.3 *4 * This source code is licensed under the MIT license found in the5 * LICENSE file in the root directory of this source tree.6 */78import {CompilerError} from '../CompilerError';9import {10 BasicBlock,11 BlockId,12 GeneratedSource,13 GotoVariant,14 HIR,15 InstructionId,16 Place,17 ReactiveBlock,18 SourceLocation,19} from '../HIR';20import {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';34import {assertExhaustive} from '../Utils/utils';3536/*37 * Converts from HIR (lower-level CFG) to ReactiveFunction, a tree representation38 * 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 emits40 * labels for *all* terminals: see PruneUnusedLabels which removes unnecessary labels.41 */42export 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}5859class Driver {60 cx: Context;6162 constructor(cx: Context) {63 this.cx = cx;64 }6566 /*67 * Wraps a continuation result with preceding instructions. If there are no68 * instructions, returns the continuation as-is. Otherwise, wraps the continuation's69 * 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 }9899 /*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 the102 * expression. These StoreLocal instructions can be pruned since we represent103 * 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 === null123 ) {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 }143144 /*145 * Converts the result of visitValueBlock into a SequenceExpression that includes146 * the instruction with its lvalue. This is needed for for/for-of/for-in init/test147 * blocks where the instruction's lvalue assignment must be preserved.148 *149 * This also flattens nested SequenceExpressions that can occur from MaybeThrow150 * 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 SequenceExpressions162 const instructions: Array<ReactiveInstruction> = [];163 let innerValue: ReactiveValue = result.value;164165 // Flatten nested SequenceExpressions166 while (innerValue.kind === 'SequenceExpression') {167 instructions.push(...innerValue.instructions);168 innerValue = innerValue.value;169 }170171 /*172 * Only add the final instruction if the innermost value is not just a LoadLocal173 * 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 contain175 * all the necessary instructions.176 */177 const isLoadOfSamePlace =178 innerValue.kind === 'LoadLocal' &&179 innerValue.place.identifier.id === result.place.identifier.id;180181 if (!isLoadOfSamePlace) {182 instructions.push({183 id: result.id,184 lvalue: result.place,185 value: innerValue,186 loc,187 });188 }189190 return {191 kind: 'SequenceExpression',192 instructions,193 id: result.id,194 value: {kind: 'Primitive', value: undefined, loc},195 loc,196 };197 }198199 traverseBlock(block: BasicBlock): ReactiveBlock {200 const blockValue: ReactiveBlock = [];201 this.visitBlock(block, blockValue);202 return blockValue;203 }204205 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 }217218 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.fallthrough252 : null;253 const alternateId =254 terminal.alternate !== terminal.fallthrough255 ? terminal.alternate256 : null;257258 if (fallthroughId !== null) {259 const scheduleId = this.cx.schedule(fallthroughId, 'if');260 scheduleIds.push(scheduleId);261 }262263 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 }274275 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 }286287 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 == null300 ? null301 : {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.fallthrough316 : null;317 if (fallthroughId !== null) {318 const scheduleId = this.cx.schedule(fallthroughId, 'switch');319 scheduleIds.push(scheduleId);320 }321322 const cases: Array<{323 test: Place | null;324 block: ReactiveBlock;325 }> = [];326 [...terminal.cases].reverse().forEach((case_, _index) => {327 const test = case_.test;328329 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();346347 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 == null359 ? null360 : {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.fallthrough373 : null;374 const loopId =375 !this.cx.isScheduled(terminal.loop) &&376 terminal.loop !== terminal.fallthrough377 ? terminal.loop378 : null;379 const scheduleId = this.cx.scheduleLoop(380 terminal.fallthrough,381 terminal.test,382 terminal.loop,383 );384 scheduleIds.push(scheduleId);385386 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 }395396 const testValue = this.visitValueBlock(397 terminal.test,398 terminal.loc,399 ).value;400401 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 == null413 ? null414 : {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.fallthrough429 : null;430 const loopId =431 !this.cx.isScheduled(terminal.loop) &&432 terminal.loop !== terminal.fallthrough433 ? terminal.loop434 : null;435 const scheduleId = this.cx.scheduleLoop(436 terminal.fallthrough,437 terminal.test,438 terminal.loop,439 );440 scheduleIds.push(scheduleId);441442 const testValue = this.visitValueBlock(443 terminal.test,444 terminal.loc,445 ).value;446447 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 }456457 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 == null469 ? null470 : {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.fallthrough484 ? terminal.loop485 : null;486487 const fallthroughId = !this.cx.isScheduled(terminal.fallthrough)488 ? terminal.fallthrough489 : null;490491 const scheduleId = this.cx.scheduleLoop(492 terminal.fallthrough,493 terminal.update ?? terminal.test,494 terminal.loop,495 );496 scheduleIds.push(scheduleId);497498 const init = this.visitValueBlock(terminal.init, terminal.loc);499 const initValue = this.valueBlockResultToSequence(init, terminal.loc);500501 const testValue = this.visitValueBlock(502 terminal.test,503 terminal.loc,504 ).value;505506 const updateValue =507 terminal.update !== null508 ? this.visitValueBlock(terminal.update, terminal.loc).value509 : null;510511 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 }520521 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.fallthrough545 ? terminal.loop546 : null;547548 const fallthroughId = !this.cx.isScheduled(terminal.fallthrough)549 ? terminal.fallthrough550 : null;551552 const scheduleId = this.cx.scheduleLoop(553 terminal.fallthrough,554 terminal.init,555 terminal.loop,556 );557 scheduleIds.push(scheduleId);558559 const init = this.visitValueBlock(terminal.init, terminal.loc);560 const initValue = this.valueBlockResultToSequence(init, terminal.loc);561562 const test = this.visitValueBlock(terminal.test, terminal.loc);563 const testValue = this.valueBlockResultToSequence(test, terminal.loc);564565 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 }574575 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.fallthrough598 ? terminal.loop599 : null;600601 const fallthroughId = !this.cx.isScheduled(terminal.fallthrough)602 ? terminal.fallthrough603 : null;604605 const scheduleId = this.cx.scheduleLoop(606 terminal.fallthrough,607 terminal.init,608 terminal.loop,609 );610 scheduleIds.push(scheduleId);611612 const init = this.visitValueBlock(terminal.init, terminal.loc);613 const initValue = this.valueBlockResultToSequence(init, terminal.loc);614615 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 }624625 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 }659660 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 }671672 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 });684685 break;686 }687 case 'label': {688 const fallthroughId =689 this.cx.reachable(terminal.fallthrough) &&690 !this.cx.isScheduled(terminal.fallthrough)691 ? terminal.fallthrough692 : null;693 if (fallthroughId !== null) {694 const scheduleId = this.cx.schedule(fallthroughId, 'if');695 scheduleIds.push(scheduleId);696 }697698 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 }707708 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 }723724 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.fallthrough734 : null;735 if (fallthroughId !== null) {736 const scheduleId = this.cx.schedule(fallthroughId, 'if');737 scheduleIds.push(scheduleId);738 }739740 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 });751752 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 away797 */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.fallthrough811 : null;812 if (fallthroughId !== null) {813 const scheduleId = this.cx.schedule(fallthroughId, 'if');814 scheduleIds.push(scheduleId);815 }816 this.cx.scheduleCatchHandler(terminal.handler);817818 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 );824825 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 });839840 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.fallthrough849 : null;850 if (fallthroughId !== null) {851 const scheduleId = this.cx.schedule(fallthroughId, 'if');852 scheduleIds.push(scheduleId);853 this.cx.scopeFallthroughs.add(fallthroughId);854 }855856 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 }865866 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 }875876 break;877 }878 case 'unreachable': {879 // noop880 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 }893894 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 recursing901 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 general935 * we recurse to the continuation block.936 *937 * However, if the last portion938 * of the value block is a potentially throwing expression, then the939 * value block could be of the form940 * ```941 * bb1:942 * ...StoreLocal for the value block...943 * maybe-throw continuation=bb2944 * bb2:945 * goto (exit the value block)946 * ```947 *948 * Ie what would have been a StoreLocal+goto is split up because of949 * the maybe-throw. We detect this case and return the value of the950 * current block as the result of the value block951 */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 }964965 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 value974 * 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 }988989 /*990 * Visits the test block of a value terminal (optional, logical, ternary) and991 * returns the result along with the branch terminal. Throws a todo error if992 * 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 }10251026 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 };11511152 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 }11841185 emptyBlock(): ReactiveBlock {1186 return [];1187 }11881189 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 }12201221 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 });12311232 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}12451246class Context {1247 ir: HIR;1248 #nextScheduleId: number = 0;12491250 /*1251 * Used to track which blocks *have been* generated already in order to1252 * abort if a block is generated a second time. This is an error catching1253 * mechanism for debugging purposes, and is not used by the codegen algorithm1254 * to drive decisions about how to emit blocks.1255 */1256 emitted: Set<BlockId> = new Set();12571258 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 eg1262 * a break instead.1263 */1264 #scheduled: Set<BlockId> = new Set();12651266 #catchHandlers: Set<BlockId> = new Set();12671268 /*1269 * Represents which control flow operations are currently in scope, with the innermost1270 * scope last. Roughly speaking, the last ControlFlowTarget on the stack indicates where1271 * control will implicitly transfer, such that gotos to that block can be elided. Gotos1272 * targeting items higher up the stack may need labeled break or continue; see1273 * getBreakTarget() and getContinueTarget() for more details.1274 */1275 #controlFlowStack: Array<ControlFlowTarget> = [];12761277 constructor(ir: HIR) {1278 this.ir = ir;1279 }12801281 block(id: BlockId): BasicBlock {1282 return this.ir.blocks.get(id)!;1283 }12841285 scheduleCatchHandler(block: BlockId): void {1286 this.#catchHandlers.add(block);1287 }12881289 reachable(id: BlockId): boolean {1290 const block = this.ir.blocks.get(id)!;1291 return block.terminal.kind !== 'unreachable';1292 }12931294 /*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 }13081309 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 }13271328 this.#controlFlowStack.push({1329 block: fallthroughBlock,1330 ownsBlock,1331 id,1332 type: 'loop',1333 continueBlock,1334 loopBlock,1335 ownsLoop,1336 });1337 return id;1338 }13391340 // 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 }13571358 /*1359 * Helper to unschedule multiple scheduled blocks. The ids should be in1360 * 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 }13671368 // 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 }13721373 /*1374 * Given the current control flow stack, determines how a `break` to the given @param block1375 * must be emitted. Returns as follows:1376 * - 'implicit' if control would implicitly transfer to that block1377 * - 'labeled' if a labeled break is required to transfer control to that block1378 * - 'unlabeled' if an unlabeled break would transfer to that block1379 * - null if there is no information for this block1380 *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 transfer1401 * implicitly1402 */1403 type = 'implicit';1404 } else {1405 // breaking somewhere else requires an explicit break1406 type = 'labeled';1407 }1408 return {1409 block: target.block,1410 type,1411 };1412 }1413 hasPrecedingLoop ||= target.type === 'loop';1414 }14151416 CompilerError.invariant(false, {1417 reason: 'Expected a break target',1418 loc: GeneratedSource,1419 });1420 }14211422 /*1423 * Given the current control flow stack, determines how a `continue` to the given @param block1424 * must be emitted. Returns as follows:1425 * - 'implicit' if control would implicitly continue to that block1426 * - 'labeled' if a labeled continue is required to continue to that block1427 * - 'unlabeled' if an unlabeled continue would transfer to that block1428 * - null if there is no information for this block1429 *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 requires1443 * a label1444 */1445 type = 'labeled';1446 } else if (i === this.#controlFlowStack.length - 1) {1447 /*1448 * continuing to the last break point, which is where control will1449 * transfer to naturally1450 */1451 type = 'implicit';1452 } else {1453 /*1454 * the continue is inside some conditional logic, requires an explicit1455 * continue1456 */1457 type = 'unlabeled';1458 }1459 return {1460 block: target.block,1461 type,1462 };1463 }1464 hasPrecedingLoop ||= target.type === 'loop';1465 }1466 return null;1467 }14681469 debugBreakTargets(): Array<ControlFlowTarget> {1470 return this.#controlFlowStack.map(target => ({...target}));1471 }1472}14731474type 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 };
Findings
✓ No findings reported for this file.