compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/BuildReactiveFunction.ts TYPESCRIPT 1,487 lines View on github.com → Search inside
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.

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.