compiler/packages/babel-plugin-react-compiler/src/HIR/HIRBuilder.ts TYPESCRIPT 956 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 {Binding, NodePath} from '@babel/traverse';9import * as t from '@babel/types';10import {11  CompilerError,12  CompilerDiagnostic,13  CompilerErrorDetail,14  ErrorCategory,15} from '../CompilerError';16import {Environment} from './Environment';17import {18  BasicBlock,19  BlockId,20  BlockKind,21  Effect,22  GeneratedSource,23  GotoVariant,24  HIR,25  Identifier,26  IdentifierId,27  Instruction,28  Place,29  SourceLocation,30  Terminal,31  VariableBinding,32  makeBlockId,33  makeDeclarationId,34  makeIdentifierName,35  makeInstructionId,36  makeTemporaryIdentifier,37  makeType,38} from './HIR';39import {printInstruction} from './PrintHIR';40import {41  eachTerminalSuccessor,42  mapTerminalSuccessors,43  terminalFallthrough,44} from './visitors';4546/*47 * *******************************************************************************************48 * *******************************************************************************************49 * ************************************* Lowering to HIR *************************************50 * *******************************************************************************************51 * *******************************************************************************************52 */5354// A work-in-progress block that does not yet have a terminator55export type WipBlock = {56  id: BlockId;57  instructions: Array<Instruction>;58  kind: BlockKind;59};6061type Scope = LoopScope | LabelScope | SwitchScope;6263type LoopScope = {64  kind: 'loop';65  label: string | null;66  continueBlock: BlockId;67  breakBlock: BlockId;68};6970type SwitchScope = {71  kind: 'switch';72  breakBlock: BlockId;73  label: string | null;74};7576type LabelScope = {77  kind: 'label';78  label: string;79  breakBlock: BlockId;80};8182function newBlock(id: BlockId, kind: BlockKind): WipBlock {83  return {id, kind, instructions: []};84}8586export type Bindings = Map<87  string,88  {node: t.Identifier; identifier: Identifier}89>;9091/*92 * Determines how instructions should be constructed in order to preserve93 * exception semantics94 */95export type ExceptionsMode =96  /*97   * Mode used for code not covered by explicit exception handling, any98   * errors are assumed to be thrown out of the function99   */100  | {kind: 'ThrowExceptions'}101  /*102   * Mode used for code that *is* covered by explicit exception handling103   * (ie try/catch), which requires modeling the possibility of control104   * flow to the exception handler.105   */106  | {kind: 'CatchExceptions'; handler: BlockId};107108// Helper class for constructing a CFG109export default class HIRBuilder {110  #completed: Map<BlockId, BasicBlock> = new Map();111  #current: WipBlock;112  #entry: BlockId;113  #scopes: Array<Scope> = [];114  #context: Map<t.Identifier, SourceLocation>;115  #bindings: Bindings;116  #env: Environment;117  #exceptionHandlerStack: Array<BlockId> = [];118  /**119   * Traversal context: counts the number of `fbt` tag parents120   * of the current babel node.121   */122  fbtDepth: number = 0;123124  get nextIdentifierId(): IdentifierId {125    return this.#env.nextIdentifierId;126  }127128  get context(): Map<t.Identifier, SourceLocation> {129    return this.#context;130  }131132  get bindings(): Bindings {133    return this.#bindings;134  }135136  get environment(): Environment {137    return this.#env;138  }139140  constructor(141    env: Environment,142    options?: {143      bindings?: Bindings | null;144      context?: Map<t.Identifier, SourceLocation>;145      entryBlockKind?: BlockKind;146    },147  ) {148    this.#env = env;149    this.#bindings = options?.bindings ?? new Map();150    this.#context = options?.context ?? new Map();151    this.#entry = makeBlockId(env.nextBlockId);152    this.#current = newBlock(this.#entry, options?.entryBlockKind ?? 'block');153  }154155  recordError(error: CompilerDiagnostic | CompilerErrorDetail): void {156    this.#env.recordError(error);157  }158159  currentBlockKind(): BlockKind {160    return this.#current.kind;161  }162163  // Push a statement or expression onto the current block164  push(instruction: Instruction): void {165    this.#current.instructions.push(instruction);166    const exceptionHandler = this.#exceptionHandlerStack.at(-1);167    if (exceptionHandler !== undefined) {168      const continuationBlock = this.reserve(this.currentBlockKind());169      this.terminateWithContinuation(170        {171          kind: 'maybe-throw',172          continuation: continuationBlock.id,173          handler: exceptionHandler,174          id: makeInstructionId(0),175          loc: instruction.loc,176          effects: null,177        },178        continuationBlock,179      );180    }181  }182183  enterTryCatch(handler: BlockId, fn: () => void): void {184    this.#exceptionHandlerStack.push(handler);185    fn();186    this.#exceptionHandlerStack.pop();187  }188189  resolveThrowHandler(): BlockId | null {190    const handler = this.#exceptionHandlerStack.at(-1);191    return handler ?? null;192  }193194  makeTemporary(loc: SourceLocation): Identifier {195    const id = this.nextIdentifierId;196    return makeTemporaryIdentifier(id, loc);197  }198199  #resolveBabelBinding(200    path: NodePath<t.Identifier | t.JSXIdentifier>,201  ): Binding | null {202    const originalName = path.node.name;203    const binding = path.scope.getBinding(originalName);204    if (binding == null) {205      return null;206    }207    return binding;208  }209210  /*211   * Maps an Identifier (or JSX identifier) Babel node to an internal `Identifier`212   * which represents the variable being referenced, according to the JS scoping rules.213   *214   * Because Forget does not preserve _all_ block scopes in the input (only those that215   * happen to occur from control flow), this resolution ensures that different variables216   * with the same name are mapped to a unique name. Concretely, this function maintains217   * the invariant that all references to a given variable will return an `Identifier`218   * with the same (unique for the function) `name` and `id`.219   *220   * Example:221   *222   * ```javascript223   * function foo() {224   *    const x = 0;225   *    {226   *      const x = 1;227   *    }228   *    return x;229   * }230   * ```231   *232   * The above converts as follows:233   *234   * ```235   * Const Identifier { name: 'x', id: 0 } = Primitive { value: 0 };236   * Const Identifier { name: 'x_0', id: 1 } = Primitive { value: 1 };237   * Return Identifier { name: 'x', id: 0};238   * ```239   */240  resolveIdentifier(241    path: NodePath<t.Identifier | t.JSXIdentifier>,242  ): VariableBinding {243    const originalName = path.node.name;244    const babelBinding = this.#resolveBabelBinding(path);245    if (babelBinding == null) {246      return {kind: 'Global', name: originalName};247    }248249    // Check if the binding is from module scope250    const outerBinding =251      this.#env.parentFunction.scope.parent.getBinding(originalName);252    if (babelBinding === outerBinding) {253      const path = babelBinding.path;254      if (path.isImportDefaultSpecifier()) {255        const importDeclaration =256          path.parentPath as NodePath<t.ImportDeclaration>;257        return {258          kind: 'ImportDefault',259          name: originalName,260          module: importDeclaration.node.source.value,261        };262      } else if (path.isImportSpecifier()) {263        const importDeclaration =264          path.parentPath as NodePath<t.ImportDeclaration>;265        return {266          kind: 'ImportSpecifier',267          name: originalName,268          module: importDeclaration.node.source.value,269          imported:270            path.node.imported.type === 'Identifier'271              ? path.node.imported.name272              : path.node.imported.value,273        };274      } else if (path.isImportNamespaceSpecifier()) {275        const importDeclaration =276          path.parentPath as NodePath<t.ImportDeclaration>;277        return {278          kind: 'ImportNamespace',279          name: originalName,280          module: importDeclaration.node.source.value,281        };282      } else {283        return {284          kind: 'ModuleLocal',285          name: originalName,286        };287      }288    }289290    const resolvedBinding = this.resolveBinding(babelBinding.identifier);291    if (resolvedBinding.name && resolvedBinding.name.value !== originalName) {292      babelBinding.scope.rename(originalName, resolvedBinding.name.value);293    }294    return {295      kind: 'Identifier',296      identifier: resolvedBinding,297      bindingKind: babelBinding.kind,298    };299  }300301  isContextIdentifier(path: NodePath<t.Identifier | t.JSXIdentifier>): boolean {302    const binding = this.#resolveBabelBinding(path);303    if (binding) {304      // Check if the binding is from module scope, if so return null305      const outerBinding = this.#env.parentFunction.scope.parent.getBinding(306        path.node.name,307      );308      if (binding === outerBinding) {309        return false;310      }311      return this.#env.isContextIdentifier(binding.identifier);312    } else {313      return false;314    }315  }316317  resolveBinding(node: t.Identifier): Identifier {318    if (node.name === 'fbt') {319      this.recordError(320        new CompilerErrorDetail({321          category: ErrorCategory.Todo,322          reason: 'Support local variables named `fbt`',323          description:324            'Local variables named `fbt` may conflict with the fbt plugin and are not yet supported',325          loc: node.loc ?? GeneratedSource,326          suggestions: null,327        }),328      );329    }330    if (node.name === 'this') {331      this.recordError(332        new CompilerErrorDetail({333          category: ErrorCategory.UnsupportedSyntax,334          reason: '`this` is not supported syntax',335          description:336            'React Compiler does not support compiling functions that use `this`',337          loc: node.loc ?? GeneratedSource,338          suggestions: null,339        }),340      );341    }342    const originalName = node.name;343    let name = originalName;344    let index = 0;345    while (true) {346      const mapping = this.#bindings.get(name);347      if (mapping === undefined) {348        const id = this.nextIdentifierId;349        const identifier: Identifier = {350          id,351          declarationId: makeDeclarationId(id),352          name: makeIdentifierName(name),353          mutableRange: {354            start: makeInstructionId(0),355            end: makeInstructionId(0),356          },357          scope: null,358          type: makeType(),359          loc: node.loc ?? GeneratedSource,360        };361        this.#env.programContext.addNewReference(name);362        this.#bindings.set(name, {node, identifier});363        return identifier;364      } else if (mapping.node === node) {365        return mapping.identifier;366      } else {367        name = `${originalName}_${index++}`;368      }369    }370  }371372  // Construct a final CFG from this context373  build(): HIR {374    let ir: HIR = {375      blocks: this.#completed,376      entry: this.#entry,377    };378    const rpoBlocks = getReversePostorderedBlocks(ir);379    for (const [id, block] of ir.blocks) {380      if (381        !rpoBlocks.has(id) &&382        block.instructions.some(383          instr => instr.value.kind === 'FunctionExpression',384        )385      ) {386        this.recordError(387          new CompilerErrorDetail({388            reason: `Support functions with unreachable code that may contain hoisted declarations`,389            loc: block.instructions[0]?.loc ?? block.terminal.loc,390            description: null,391            suggestions: null,392            category: ErrorCategory.Todo,393          }),394        );395      }396    }397    ir.blocks = rpoBlocks;398399    removeUnreachableForUpdates(ir);400    removeDeadDoWhileStatements(ir);401    removeUnnecessaryTryCatch(ir);402    markInstructionIds(ir);403    markPredecessors(ir);404405    return ir;406  }407408  // Terminate the current block w the given terminal, and start a new block409  terminate(terminal: Terminal, nextBlockKind: BlockKind | null): BlockId {410    const {id: blockId, kind, instructions} = this.#current;411    this.#completed.set(blockId, {412      kind,413      id: blockId,414      instructions,415      terminal,416      preds: new Set(),417      phis: new Set(),418    });419    if (nextBlockKind) {420      const nextId = this.#env.nextBlockId;421      this.#current = newBlock(nextId, nextBlockKind);422    }423    return blockId;424  }425426  /*427   * Terminate the current block w the given terminal, and set the previously428   * reserved block as the new current block429   */430  terminateWithContinuation(terminal: Terminal, continuation: WipBlock): void {431    const {id: blockId, kind, instructions} = this.#current;432    this.#completed.set(blockId, {433      kind: kind,434      id: blockId,435      instructions,436      terminal: terminal,437      preds: new Set(),438      phis: new Set(),439    });440    this.#current = continuation;441  }442443  /*444   * Reserve a block so that it can be referenced prior to construction.445   * Make this the current block with `terminateWithContinuation()` or446   * call `complete()` to save it without setting it as the current block.447   */448  reserve(kind: BlockKind): WipBlock {449    return newBlock(makeBlockId(this.#env.nextBlockId), kind);450  }451452  // Save a previously reserved block as completed453  complete(block: WipBlock, terminal: Terminal): void {454    const {id: blockId, kind, instructions} = block;455    this.#completed.set(blockId, {456      kind,457      id: blockId,458      instructions,459      terminal,460      preds: new Set(),461      phis: new Set(),462    });463  }464465  /*466   * Sets the given wip block as the current block, executes the provided callback to populate the block467   * up to its terminal, and then resets the previous actively block.468   */469  enterReserved(wip: WipBlock, fn: () => Terminal): void {470    const current = this.#current;471    this.#current = wip;472    const terminal = fn();473    const {id: blockId, kind, instructions} = this.#current;474    this.#completed.set(blockId, {475      kind,476      id: blockId,477      instructions,478      terminal,479      preds: new Set(),480      phis: new Set(),481    });482    this.#current = current;483  }484485  /*486   * Create a new block and execute the provided callback with the new block487   * set as the current, resetting to the previously active block upon exit.488   * The lambda must return a terminal node, which is used to terminate the489   * newly constructed block.490   */491  enter(nextBlockKind: BlockKind, fn: (blockId: BlockId) => Terminal): BlockId {492    const wip = this.reserve(nextBlockKind);493    this.enterReserved(wip, () => {494      return fn(wip.id);495    });496    return wip.id;497  }498499  label<T>(label: string, breakBlock: BlockId, fn: () => T): T {500    this.#scopes.push({501      kind: 'label',502      breakBlock,503      label,504    });505    const value = fn();506    const last = this.#scopes.pop();507    CompilerError.invariant(508      last != null &&509        last.kind === 'label' &&510        last.label === label &&511        last.breakBlock === breakBlock,512      {513        reason: 'Mismatched label',514        loc: GeneratedSource,515      },516    );517    return value;518  }519520  switch<T>(label: string | null, breakBlock: BlockId, fn: () => T): T {521    this.#scopes.push({522      kind: 'switch',523      breakBlock,524      label,525    });526    const value = fn();527    const last = this.#scopes.pop();528    CompilerError.invariant(529      last != null &&530        last.kind === 'switch' &&531        last.label === label &&532        last.breakBlock === breakBlock,533      {534        reason: 'Mismatched label',535        loc: GeneratedSource,536      },537    );538    return value;539  }540541  /*542   * Executes the provided lambda inside a scope in which the provided loop543   * information is cached for lookup with `lookupBreak()` and `lookupContinue()`544   */545  loop<T>(546    label: string | null,547    // block of the loop body. "continue" jumps here.548    continueBlock: BlockId,549    // block following the loop. "break" jumps here.550    breakBlock: BlockId,551    fn: () => T,552  ): T {553    this.#scopes.push({554      kind: 'loop',555      label,556      continueBlock,557      breakBlock,558    });559    const value = fn();560    const last = this.#scopes.pop();561    CompilerError.invariant(562      last != null &&563        last.kind === 'loop' &&564        last.label === label &&565        last.continueBlock === continueBlock &&566        last.breakBlock === breakBlock,567      {568        reason: 'Mismatched loops',569        loc: GeneratedSource,570      },571    );572    return value;573  }574575  /*576   * Lookup the block target for a break statement, based on loops and switch statements577   * in scope. Throws if there is no available location to break.578   */579  lookupBreak(label: string | null): BlockId {580    for (let ii = this.#scopes.length - 1; ii >= 0; ii--) {581      const scope = this.#scopes[ii];582      if (583        (label === null &&584          (scope.kind === 'loop' || scope.kind === 'switch')) ||585        label === scope.label586      ) {587        return scope.breakBlock;588      }589    }590    CompilerError.invariant(false, {591      reason: 'Expected a loop or switch to be in scope',592      loc: GeneratedSource,593    });594  }595596  /*597   * Lookup the block target for a continue statement, based on loops598   * in scope. Throws if there is no available location to continue, or if the given599   * label does not correspond to a loop (this should also be validated at parse time).600   */601  lookupContinue(label: string | null): BlockId {602    for (let ii = this.#scopes.length - 1; ii >= 0; ii--) {603      const scope = this.#scopes[ii];604      if (scope.kind === 'loop') {605        if (label === null || label === scope.label) {606          return scope.continueBlock;607        }608      } else if (label !== null && scope.label === label) {609        CompilerError.invariant(false, {610          reason: 'Continue may only refer to a labeled loop',611          loc: GeneratedSource,612        });613      }614    }615    CompilerError.invariant(false, {616      reason: 'Expected a loop to be in scope',617      loc: GeneratedSource,618    });619  }620}621622// Helper to shrink a CFG eliminate jump-only blocks.623function _shrink(func: HIR): void {624  const gotos = new Map();625  /*626   * Given a target block for some terminator, resolves the ideal block that should be627   * targeted instead. This transitively resolves any blocks that are simple indirections628   * (empty blocks that terminate in a goto).629   */630  function resolveBlockTarget(blockId: BlockId): BlockId {631    let target = gotos.get(blockId) ?? null;632    if (target !== null) {633      return target;634    }635    const block = func.blocks.get(blockId);636    CompilerError.invariant(block != null, {637      reason: `expected block ${blockId} to exist`,638      loc: GeneratedSource,639    });640    target = getTargetIfIndirection(block);641    if (target !== null) {642      //  the target might also be a simple goto, recurse643      target = resolveBlockTarget(target) ?? target;644      gotos.set(blockId, target);645      return target;646    } else {647      //  If the block wasn't an indirection, return the original input.648      return blockId;649    }650  }651652  const queue = [func.entry];653  const reachable = new Set<BlockId>();654  while (queue.length !== 0) {655    const blockId = queue.shift()!;656    if (reachable.has(blockId)) {657      continue;658    }659    reachable.add(blockId);660    const block = func.blocks.get(blockId)!;661    block.terminal = mapTerminalSuccessors(block.terminal, prevTarget => {662      const target = resolveBlockTarget(prevTarget);663      queue.push(target);664      return target;665    });666  }667  for (const [blockId] of func.blocks) {668    if (!reachable.has(blockId)) {669      func.blocks.delete(blockId);670    }671  }672}673674export function removeUnreachableForUpdates(fn: HIR): void {675  for (const [, block] of fn.blocks) {676    if (677      block.terminal.kind === 'for' &&678      block.terminal.update !== null &&679      !fn.blocks.has(block.terminal.update)680    ) {681      block.terminal.update = null;682    }683  }684}685686export function removeDeadDoWhileStatements(func: HIR): void {687  const visited: Set<BlockId> = new Set();688  for (const [_, block] of func.blocks) {689    visited.add(block.id);690  }691692  /*693   * If the test condition of a DoWhile is unreachable, the terminal is effectively deadcode and we694   * can just inline the loop body. We replace the terminal with a goto to the loop block and695   * MergeConsecutiveBlocks figures out how to merge as appropriate.696   */697  for (const [_, block] of func.blocks) {698    if (block.terminal.kind === 'do-while') {699      if (!visited.has(block.terminal.test)) {700        block.terminal = {701          kind: 'goto',702          block: block.terminal.loop,703          variant: GotoVariant.Break,704          id: block.terminal.id,705          loc: block.terminal.loc,706        };707      }708    }709  }710}711712/*713 * Converts the graph to reverse-postorder, with predecessor blocks appearing714 * before successors except in the case of back edges (ie loops).715 */716export function reversePostorderBlocks(func: HIR): void {717  const rpoBlocks = getReversePostorderedBlocks(func);718  func.blocks = rpoBlocks;719}720721/**722 * Returns a mapping of BlockId => BasicBlock where the insertion order of the map723 * has blocks in reverse-postorder, with predecessor blocks appearing before successors724 * except in the case of back edges (ie loops). Note that not all blocks in the input725 * may be in the output: blocks will be removed in the case of unreachable code in726 * the input.727 */728function getReversePostorderedBlocks(func: HIR): HIR['blocks'] {729  const visited: Set<BlockId> = new Set();730  const used: Set<BlockId> = new Set();731  const usedFallthroughs: Set<BlockId> = new Set();732  const postorder: Array<BlockId> = [];733  function visit(blockId: BlockId, isUsed: boolean): void {734    const wasUsed = used.has(blockId);735    const wasVisited = visited.has(blockId);736    visited.add(blockId);737    if (isUsed) {738      used.add(blockId);739    }740    if (wasVisited && (wasUsed || !isUsed)) {741      return;742    }743744    /*745     * Note that we visit successors in reverse order. This ensures that when we746     * reverse the list at the end, that "sibling" edges appear in-order. For example,747     * ```748     * // bb0749     * let x;750     * if (c) {751     *    // bb1752     *    x = 1;753     * } else {754     *    // bb2755     *    x = 2;756     * }757     * // bb3758     * x;759     * ```760     *761     * We want the output to be bb0, bb1, bb2, bb3 just to line up with the original762     * program order for visual debugging. By visiting the successors in reverse order763     * (eg bb2 then bb1), we ensure that they get reversed back to the correct order.764     */765    const block = func.blocks.get(blockId)!;766    CompilerError.invariant(block != null, {767      reason: '[HIRBuilder] Unexpected null block',768      description: `expected block ${blockId} to exist`,769      loc: GeneratedSource,770    });771    const successors = [...eachTerminalSuccessor(block.terminal)].reverse();772    const fallthrough = terminalFallthrough(block.terminal);773774    /**775     * Fallthrough blocks are only used to record original program block structure. If the776     * fallthrough is actually reachable, it will be reached through terminal successors.777     * To retain program structure, we visit fallthrough blocks first (marking them as not778     * actually used yet) to ensure their block IDs emitted in the correct order.779     */780    if (fallthrough != null) {781      if (isUsed) {782        usedFallthroughs.add(fallthrough);783      }784      visit(fallthrough, false);785    }786    for (const successor of successors) {787      visit(successor, isUsed);788    }789790    if (!wasVisited) {791      postorder.push(blockId);792    }793  }794  visit(func.entry, true);795  const blocks = new Map<BlockId, BasicBlock>();796  for (const blockId of postorder.reverse()) {797    const block = func.blocks.get(blockId)!;798    if (used.has(blockId)) {799      blocks.set(blockId, func.blocks.get(blockId)!);800    } else if (usedFallthroughs.has(blockId)) {801      blocks.set(blockId, {802        ...block,803        instructions: [],804        terminal: {805          kind: 'unreachable',806          id: block.terminal.id,807          loc: block.terminal.loc,808        },809      });810    }811    // otherwise this block is unreachable812  }813814  return blocks;815}816817export function markInstructionIds(func: HIR): void {818  let id = 0;819  const visited = new Set<Instruction>();820  for (const [_, block] of func.blocks) {821    for (const instr of block.instructions) {822      CompilerError.invariant(!visited.has(instr), {823        reason: `${printInstruction(instr)} already visited!`,824        loc: instr.loc,825      });826      visited.add(instr);827      instr.id = makeInstructionId(++id);828    }829    block.terminal.id = makeInstructionId(++id);830  }831}832833export function markPredecessors(func: HIR): void {834  for (const [, block] of func.blocks) {835    block.preds.clear();836  }837  const visited: Set<BlockId> = new Set();838  function visit(blockId: BlockId, prevBlock: BasicBlock | null): void {839    const block = func.blocks.get(blockId)!;840    if (block == null) {841      return;842    }843    CompilerError.invariant(block != null, {844      reason: 'unexpected missing block',845      description: `block ${blockId}`,846      loc: GeneratedSource,847    });848    if (prevBlock) {849      block.preds.add(prevBlock.id);850    }851852    if (visited.has(blockId)) {853      return;854    }855    visited.add(blockId);856857    const {terminal} = block;858859    for (const successor of eachTerminalSuccessor(terminal)) {860      visit(successor, block);861    }862  }863  visit(func.entry, null);864}865866/*867 * If the given block is a simple indirection  empty terminated with a goto(break) 868 * returns the block being pointed to. Otherwise returns null.869 */870function getTargetIfIndirection(block: BasicBlock): number | null {871  return block.instructions.length === 0 &&872    block.terminal.kind === 'goto' &&873    block.terminal.variant === GotoVariant.Break874    ? block.terminal.block875    : null;876}877878/*879 * Finds try terminals where the handler is unreachable, and converts the try880 * to a goto(terminal.block)881 */882export function removeUnnecessaryTryCatch(fn: HIR): void {883  for (const [, block] of fn.blocks) {884    if (885      block.terminal.kind === 'try' &&886      !fn.blocks.has(block.terminal.handler)887    ) {888      const handlerId = block.terminal.handler;889      const fallthroughId = block.terminal.fallthrough;890      const fallthrough = fn.blocks.get(fallthroughId);891      block.terminal = {892        kind: 'goto',893        block: block.terminal.block,894        id: makeInstructionId(0),895        loc: block.terminal.loc,896        variant: GotoVariant.Break,897      };898899      if (fallthrough != null) {900        if (fallthrough.preds.size === 1 && fallthrough.preds.has(handlerId)) {901          // delete fallthrough902          fn.blocks.delete(fallthroughId);903        } else {904          fallthrough.preds.delete(handlerId);905        }906      }907    }908  }909}910911export function createTemporaryPlace(912  env: Environment,913  loc: SourceLocation,914): Place {915  return {916    kind: 'Identifier',917    identifier: makeTemporaryIdentifier(env.nextIdentifierId, loc),918    reactive: false,919    effect: Effect.Unknown,920    loc: GeneratedSource,921  };922}923924/**925 * Clones an existing Place, returning a new temporary Place that shares the926 * same metadata properties as the original place (effect, reactive flag, type)927 * but has a new, temporary Identifier.928 */929export function clonePlaceToTemporary(env: Environment, place: Place): Place {930  const temp = createTemporaryPlace(env, place.loc);931  temp.effect = place.effect;932  temp.identifier.type = place.identifier.type;933  temp.reactive = place.reactive;934  return temp;935}936937/**938 * Fix scope and identifier ranges to account for renumbered instructions939 */940export function fixScopeAndIdentifierRanges(func: HIR): void {941  for (const [, block] of func.blocks) {942    const terminal = block.terminal;943    if (terminal.kind === 'scope' || terminal.kind === 'pruned-scope') {944      /*945       * Scope ranges should always align to start at the 'scope' terminal946       * and end at the first instruction of the fallthrough block947       */948      const fallthroughBlock = func.blocks.get(terminal.fallthrough)!;949      const firstId =950        fallthroughBlock.instructions[0]?.id ?? fallthroughBlock.terminal.id;951      terminal.scope.range.start = terminal.id;952      terminal.scope.range.end = firstId;953    }954  }955}

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.