///|
#valtype
pub struct CfgEdge {
  kind : CfgEdgeKind
  block : BlockId
} derive(Eq, Debug)

///|
pub impl Show for CfgEdge with fn output(self, logger) {
  let { kind, block, } = self
  logger.write_string("{kind: ")
  kind.output(logger)
  logger.write_string(", block: ")
  block.output(logger)
  logger.write_char('}')
}

///|
pub struct HotBlockInfo {
  id : BlockId
  nodes : Array[NodeId]
  preds : Array[CfgEdge]
  succs : Array[CfgEdge]
  is_synthetic : Bool
  region_holder : NodeId
  start_slot : Int32
  end_slot : Int32
} derive(Eq, Debug)

///|
pub impl Show for HotBlockInfo with fn output(self, logger) {
  logger.write_string(@debug.to_string(self))
}

///|
#valtype
pub struct CfgRegionRootBlock {
  region_holder : NodeId
  slot : Int32
  block : BlockId
} derive(Eq, Debug)

///|
pub impl Show for CfgRegionRootBlock with fn output(self, logger) {
  logger.write_string(@debug.to_string(self))
}

///|
pub struct HotCfg {
  operands_expanded : Bool
  blocks : Array[HotBlockInfo]
  node_to_block : Array[BlockId]
  region_roots : Array[CfgRegionRootBlock]
  mut entry_block : BlockId
  mut exit_block : BlockId
  mut exceptional_exit_block : BlockId?
} derive(Eq, Debug)

///|
priv struct CfgBuilder {
  cfg : HotCfg
  label_targets : Array[BlockId]
  conservative_exception_entries : Bool
  operand_seen : Array[Bool]?
  mut source_order : HotSourceOrderFacts?
  mut operand_minima : Array[Int32]?
  continuation_targets : Array[Array[LabelId]?]
}

///|
priv struct CfgRegionBuild {
  entry_block : BlockId?
  exit_blocks : Array[BlockId]
}

///|
#valtype
priv struct CfgRegionSegment {
  block_id : BlockId
  end_slot : Int32
  last_node : NodeId
}

///|
// Scalar rows avoid a separately allocated record for every segment. Keep the
// public CFG block and root mappings unchanged; this arena is builder-local.
#valtype
priv struct CfgRegionSegments {
  rows : Array[Int32]
}

///|
fn CfgRegionSegments::new() -> CfgRegionSegments {
  { rows: [], }
}

///|
#inline
fn CfgRegionSegments::push(
  self : CfgRegionSegments,
  block_id : BlockId,
  end_slot : Int32,
  last_node : NodeId,
) -> Unit {
  self.rows.push(block_id)
  self.rows.push(end_slot)
  self.rows.push(last_node)
}

///|
#inline
fn CfgRegionSegments::length(self : CfgRegionSegments) -> Int32 {
  self.rows.length() / 3
}

///|
#inline
fn CfgRegionSegments::block_id(
  self : CfgRegionSegments,
  index : Int32,
) -> BlockId {
  self.rows[index * 3]
}

///|
#inline
fn CfgRegionSegments::get(
  self : CfgRegionSegments,
  index : Int32,
) -> CfgRegionSegment {
  let offset = index * 3
  {
    block_id: self.rows[offset],
    end_slot: self.rows[offset + 1],
    last_node: self.rows[offset + 2],
  }
}

///|
fn cfg_builder_new(
  func : HotFunc,
  conservative_exception_entries : Bool,
  expand_operand_control : Bool,
) -> CfgBuilder {
  let node_count = hot_node_count(func)
  let node_to_block = Array::make(node_count, -1)
  let mut has_continuations = false
  // cfg_build verifies side-table references before reaching this constructor.
  // Every valid Continuation owns a branch-table entry, including empty handlers.
  // Nonempty tables remain conservative: br_table and retired payloads also use
  // this arena, so retain the full live-node scan in that case.
  if node_count > 0 && !func.branch_tables.is_empty() {
    for node in 0.. NodeId {
  region_ref.holder
}

///|
fn cfg_builder_add_block(
  builder : CfgBuilder,
  nodes : Array[NodeId],
  is_synthetic : Bool,
  region_holder~ : NodeId = HOT_NONE_NODE,
  start_slot~ : Int32 = -1,
  end_slot~ : Int32 = -1,
) -> BlockId {
  let id = builder.cfg.blocks.length()
  builder.cfg.blocks.push({
    id,
    nodes,
    preds: [],
    succs: [],
    is_synthetic,
    region_holder,
    start_slot,
    end_slot,
  })
  for node_id in nodes {
    builder.cfg.node_to_block[node_id] = id
  }
  id
}

///|
fn cfg_builder_add_edge(
  builder : CfgBuilder,
  from : BlockId,
  to : BlockId,
  kind : CfgEdgeKind,
) -> Unit {
  builder.cfg.blocks[from].succs.push({ kind, block: to, })
  builder.cfg.blocks[to].preds.push({ kind, block: from, })
}

///|
fn cfg_builder_label_target_set(
  builder : CfgBuilder,
  label_id : LabelId,
  block_id : BlockId,
) -> Unit {
  builder.label_targets[label_id] = block_id
}

///|
fn cfg_builder_label_target_get(
  builder : CfgBuilder,
  label_id : LabelId,
) -> BlockId {
  if label_id == HOT_IMPLICIT_FUNCTION_LABEL {
    return builder.cfg.exit_block
  }
  match builder.label_targets.get(label_id) {
    Some(block_id) if block_id >= 0 => block_id
    _ =>
      abort(
        "cfg build requires every referenced hot label to resolve to a built block",
      )
  }
}

///|
fn cfg_builder_exception_label_target_get(
  builder : CfgBuilder,
  label_id : LabelId,
) -> BlockId {
  if label_id == HOT_IMPLICIT_FUNCTION_LABEL {
    return cfg_builder_exceptional_exit_block(builder)
  }
  cfg_builder_label_target_get(builder, label_id)
}

///|
fn cfg_builder_exceptional_exit_block(builder : CfgBuilder) -> BlockId {
  match builder.cfg.exceptional_exit_block {
    Some(block_id) => block_id
    None => {
      let block_id = cfg_builder_add_block(builder, [], true)
      builder.cfg.exceptional_exit_block = Some(block_id)
      block_id
    }
  }
}

///|
fn cfg_builder_connect_exits(
  builder : CfgBuilder,
  exits : Array[BlockId],
  target : BlockId?,
) -> Unit {
  match target {
    Some(block_id) =>
      for exit_block in exits {
        cfg_builder_add_edge(
          builder,
          exit_block,
          block_id,
          CfgEdgeKind::FallthroughEdge,
        )
      }
    None => ()
  }
}

///|
fn cfg_region_segment_break_after_drop_operand_control(
  func : HotFunc,
  node_id : NodeId,
) -> Bool {
  if node_id < 0 || !hot_node_is_live(func, node_id) {
    return false
  }
  if hot_node_get(func, node_id).op != HotOp::Drop {
    return false
  }
  let child_id = hot_child_get(func, node_id, 0)
  cfg_dropped_branch_operand(func, node_id) is Some(_) ||
  ssa_simple_drop_result_if_allowed(func, node_id, 0, child_id) ||
  ssa_simple_drop_value_block_allowed(func, node_id, 0, child_id)
}

///|
fn cfg_dropped_branch_operand(func : HotFunc, node_id : NodeId) -> NodeId? {
  if hot_node_get(func, node_id).op != HotOp::Drop {
    return None
  }
  let child = hot_child_get(func, node_id, 0)
  if child < 0 || !hot_node_is_live(func, child) {
    return None
  }
  match hot_node_get(func, child).op {
    HotOp::BrIf
    | HotOp::BrOnNull
    | HotOp::BrOnNonNull
    | HotOp::BrOnCast
    | HotOp::BrOnCastFail => Some(child)
    _ => None
  }
}

///|
// Every selected carried value has value_order < current_order, and its own
// allocation order is <= value_order. If all future operand orders are at
// least every earlier root's current_order, no dependency can be selected.
// Memoize only operand minima, without effects/local rows, for one immutable
// CFG build. Shared operands are visited once; -2 is conservative on a cycle.
fn cfg_operand_minimum_order(
  func : HotFunc,
  node_id : NodeId,
  minima : Array[Int32],
  visits~ : Array[Int32]? = None,
) -> Int32 {
  if node_id == HOT_NONE_NODE {
    return hot_node_count(func)
  }
  if node_id < 0 || !hot_node_is_live(func, node_id) {
    return -2
  }
  if minima[node_id] != -1 {
    return minima[node_id]
  }
  minima[node_id] = -2
  if visits is Some(count) {
    count[0] += 1
  }
  let node = hot_node_get(func, node_id)
  // The collector carries only nonpure results. Pure leaves and void nodes
  // cannot contribute their own order; every operand is still visited below.
  let pure_leaf = match node.op {
    HotOp::Const | HotOp::RefNull | HotOp::RefFunc =>
      effects_mask_for_flags(node.flags) == 0U
    _ => false
  }
  let mut minimum = if pure_leaf || hot_node_result_arity(func, node_id) == 0 {
    hot_node_count(func)
  } else {
    node.order
  }
  for child in hot_lower_impl_input_info(func, node_id).nodes {
    minimum = minimum.min(
      cfg_operand_minimum_order(func, child, minima, visits~),
    )
  }
  minima[node_id] = minimum
  minimum
}

///|
fn cfg_region_may_need_source_order(
  func : HotFunc,
  roots : Array[NodeId],
  minima : Array[Int32],
) -> Bool {
  let mut previous_order = 0
  for root in roots {
    if cfg_operand_minimum_order(func, root, minima) < previous_order {
      return true
    }
    previous_order = previous_order.max(
      hot_node_get(func, root).order.min(root),
    )
  }
  false
}

///|
// The segment extent is already known. Copy once into its own exact-sized
// primitive row instead of growing an empty array one node at a time.
fn cfg_copy_segment_nodes(
  nodes : Array[NodeId],
  start : Int32,
  end : Int32,
) -> Array[NodeId] {
  if end == start + 1 {
    [nodes[start]]
  } else {
    nodes[start:end].to_owned()
  }
}

///|
fn cfg_builder_region_segments(
  builder : CfgBuilder,
  func : HotFunc,
  region_ref : HotRegionRef,
  handler_target~ : BlockId? = None,
) -> CfgRegionSegments {
  let segments = CfgRegionSegments::new()
  let region_holder = cfg_builder_region_holder(region_ref)
  let root_count = hot_region_root_count(func, region_ref)
  let expanded = builder.operand_seen is Some(_)
  let ordered_nodes : Array[NodeId] = []
  let root_slots : Array[Int32] = []
  let roots : Array[NodeId] = []
  if expanded {
    for slot = 0; slot < root_count; slot = slot + 1 {
      roots.push(hot_region_get(func, region_ref, slot))
    }
  }
  let facts = if expanded && root_count > 1 {
    let minima = match builder.operand_minima {
      Some(minima) => minima
      None => {
        let minima = Array::make(hot_node_count(func), -1)
        builder.operand_minima = Some(minima)
        minima
      }
    }
    // Facts from an earlier region do not establish a dependency here.
    // Reuse the immutable operand minima to prove each region separately.
    if cfg_region_may_need_source_order(func, roots, minima) {
      match builder.source_order {
        Some(facts) => Some(facts)
        None => {
          let facts = HotSourceOrderFacts::new(func)
          builder.source_order = Some(facts)
          Some(facts)
        }
      }
    } else {
      None
    }
  } else {
    None
  }
  let suffix_minima = match facts {
    Some(facts) => hot_source_order_future_suffix_minima(func, roots, facts)
    None => None
  }
  for slot = 0; slot < root_count; slot = slot + 1 {
    let root_id = if expanded {
      roots[slot]
    } else {
      hot_region_get(func, region_ref, slot)
    }
    match builder.operand_seen {
      Some(seen) => {
        // Even after another region initialized facts, the final root has
        // no future statement to constrain. Operand expansion still runs.
        if slot + 1 < root_count && facts is Some(_) {
          let facts = facts.unwrap()
          for
            dependency in hot_source_order_preceding_dependencies(
              func,
              root_id,
              roots,
              slot + 1,
              suffix_minima,
              facts,
            ) {
            cfg_builder_collect_operand_nodes(
              builder, func, dependency, slot, seen, ordered_nodes, root_slots,
            )
          }
        }
        cfg_builder_collect_operand_nodes(
          builder, func, root_id, slot, seen, ordered_nodes, root_slots,
        )
      }
      None => {
        ordered_nodes.push(root_id)
        root_slots.push(slot)
      }
    }
  }
  let mut start = 0
  while start < ordered_nodes.length() {
    let mut end = start + 1
    while end < ordered_nodes.length() {
      let previous = ordered_nodes[end - 1]
      let previous_op = hot_node_get(func, previous).op
      if cfg_op_is_terminator(previous_op) ||
        (
          handler_target is Some(_) &&
          (
            previous_op is HotOp::Call ||
            previous_op is HotOp::CallIndirect ||
            previous_op is HotOp::CallRef
          )
        ) {
        break
      }
      let next = ordered_nodes[end]
      if hot_is_control_node(func, next) {
        break
      }
      if cfg_expression_has_continuation_targets(
          func,
          previous,
          builder.continuation_targets,
        ) ||
        (
          previous_op is HotOp::Drop &&
          cfg_region_segment_break_after_drop_operand_control(func, previous)
        ) {
        break
      }
      end = end + 1
    }
    let nodes = cfg_copy_segment_nodes(ordered_nodes, start, end)
    let block_id = cfg_builder_add_block(
      builder,
      nodes,
      false,
      region_holder~,
      start_slot=root_slots[start],
      end_slot=root_slots[end - 1] + 1,
    )
    for index = start; index < end; index = index + 1 {
      let slot = root_slots[index]
      // Expanded operands reuse the immutable root snapshot already built
      // for source ordering. Without expansion, every listed node is a root.
      if !expanded || ordered_nodes[index] == roots[slot] {
        builder.cfg.region_roots.push({ region_holder, slot, block: block_id, })
      }
    }
    segments.push(block_id, root_slots[end - 1] + 1, ordered_nodes[end - 1])
    start = end
  }
  segments
}

///|
// Repeated entry evaluations can share one canonical CFG owner only when they
// are straight-line. Keep branching, caught-call and label scopes on their
// existing structured paths; do not invent a loop to approximate repetition.
fn cfg_repeat_entry_is_straight_line(
  func : HotFunc,
  root : NodeId,
  seen : @set.Set[NodeId],
) -> Bool {
  let work = [root]
  while !work.is_empty() {
    let id = work.pop().unwrap()
    if id < 0 || seen.contains(id) {
      continue
    }
    seen.add(id)
    let node = hot_node_get(func, id)
    if (node.flags & (HOT_FLAG_CALL | HOT_FLAG_BRANCH | HOT_FLAG_TERMINATOR)) !=
      0U ||
      (hot_is_control_node(func, id) && node.op != HotOp::Block) {
      return false
    }
    // Entry-bearing blocks can forward an earlier captured stack value. That
    // value needs its existing structured ownership, rather than replay here.
    if node.op == HotOp::Block && node.imm1 > 0 {
      return false
    }
    for child in hot_child_span(func, id) {
      work.push(child)
    }
  }
  true
}

///|
fn cfg_builder_collect_inline_entry(
  builder : CfgBuilder,
  func : HotFunc,
  node_id : NodeId,
  root_slot : Int32,
  occurrence_seen : @set.Set[NodeId],
  nodes : Array[NodeId],
  root_slots : Array[Int32],
) -> Unit {
  if node_id < 0 || occurrence_seen.contains(node_id) {
    return
  }
  occurrence_seen.add(node_id)
  if builder.operand_seen is Some(seen) {
    // Body consumers forward these already-produced lanes. They must not
    // replay the producer after the loop label is entered.
    seen[node_id] = true
  }
  let node = hot_node_get(func, node_id)
  if node.op == HotOp::Block {
    for slot in 0.. 1 {
      match builder.source_order {
        Some(facts) => Some(facts)
        None => {
          let facts = HotSourceOrderFacts::new(func)
          builder.source_order = Some(facts)
          Some(facts)
        }
      }
    } else {
      None
    }
    let suffix_minima = match facts {
      Some(facts) => hot_source_order_future_suffix_minima(func, roots, facts)
      None => None
    }
    for slot in 0.. Unit {
  let count = hot_label_branch_arity(func, node.imm0)
  let mut slot = 0
  while slot < count {
    let producer = func.children[node.child_base + slot]
    let mut end = slot + 1
    while end < count && func.children[node.child_base + end] == producer {
      end += 1
    }
    let span = hot_node_result_arity(func, producer).max(1)
    let evaluations = (end - slot + span - 1) / span
    if evaluations > 1 {
      // Reuse only within this producer group. A wide earlier producer must
      // not impose its retained set capacity on later small producers.
      let occurrence_seen : @set.Set[NodeId] = @set.Set([])
      if cfg_repeat_entry_is_straight_line(func, producer, occurrence_seen) {
        // All actions remain in one prefix block. Reuse this sparse mark set
        // for the proof and for every complete evaluation in this prefix.
        for _ in 0.. Unit {
  if node_id < 0 || !hot_node_is_live(func, node_id) || seen[node_id] {
    return
  }
  seen[node_id] = true
  let node = hot_node_get(func, node_id)
  // The control header owns its condition and value operands. Its branch
  // regions are built separately with their actual control-flow edges.
  if node.op == HotOp::Loop {
    cfg_builder_collect_loop_entries(
      builder, func, node, root_slot, seen, nodes, root_slots,
    )
  }
  for index = 0; index < node.child_count; index = index + 1 {
    let slot = hot_child_evaluation_slot(node, index)
    if !use_def_region_slot(node, slot) {
      cfg_builder_collect_operand_nodes(
        builder,
        func,
        hot_child_get(func, node_id, slot),
        root_slot,
        seen,
        nodes,
        root_slots,
      )
    }
  }
  nodes.push(node_id)
  root_slots.push(root_slot)
}

///|
fn cfg_builder_region_build_empty(
  builder : CfgBuilder,
  region_ref : HotRegionRef,
  force_entry : Bool,
) -> CfgRegionBuild {
  if !force_entry {
    return { entry_block: None, exit_blocks: [], }
  }
  let block_id = cfg_builder_add_block(
    builder,
    [],
    true,
    region_holder=cfg_builder_region_holder(region_ref),
    start_slot=0,
    end_slot=0,
  )
  { entry_block: Some(block_id), exit_blocks: [block_id], }
}

///|
fn cfg_builder_maybe_continuation(
  builder : CfgBuilder,
  region_ref : HotRegionRef,
  slot : Int32,
  next_block : BlockId?,
) -> (BlockId?, Bool) {
  match next_block {
    Some(block_id) => (Some(block_id), false)
    None => {
      let block_id = cfg_builder_add_block(
        builder,
        [],
        true,
        region_holder=cfg_builder_region_holder(region_ref),
        start_slot=slot,
        end_slot=slot,
      )
      (Some(block_id), true)
    }
  }
}

///|
fn cfg_builder_branch_targets(
  builder : CfgBuilder,
  func : HotFunc,
  node_id : NodeId,
) -> Array[BlockId] {
  let targets : Array[BlockId] = []
  match hot_node_get(func, node_id).op {
    HotOp::BrTable => {
      for label_id in hot_branch_table_targets_for_node(func, node_id) {
        targets.push(cfg_builder_label_target_get(builder, label_id))
      }
      match hot_branch_target(func, node_id) {
        Some(label_id) =>
          targets.push(cfg_builder_label_target_get(builder, label_id))
        None => ()
      }
    }
    _ =>
      match hot_branch_target(func, node_id) {
        Some(label_id) =>
          targets.push(cfg_builder_label_target_get(builder, label_id))
        None => ()
      }
  }
  targets
}

///|
fn cfg_builder_exception_branch_targets(
  builder : CfgBuilder,
  func : HotFunc,
  node_id : NodeId,
) -> Array[BlockId] {
  let targets : Array[BlockId] = []
  match hot_branch_target(func, node_id) {
    Some(label_id) =>
      targets.push(cfg_builder_exception_label_target_get(builder, label_id))
    None => ()
  }
  targets
}

///|
fn cfg_builder_exception_target(
  builder : CfgBuilder,
  handler_target : BlockId?,
) -> BlockId {
  match handler_target {
    Some(block_id) => block_id
    None => cfg_builder_exceptional_exit_block(builder)
  }
}

///|
fn cfg_builder_connect_result_if(
  builder : CfgBuilder,
  func : HotFunc,
  if_id : NodeId,
  current_block : BlockId,
  continuation : BlockId,
  handler_target : BlockId?,
) -> Unit {
  cfg_builder_label_target_set(
    builder,
    hot_control_node_label(func, if_id),
    continuation,
  )
  let then_build = cfg_builder_build_region(
    builder,
    func,
    hot_region_then_ref(func, if_id),
    handler_target,
    force_entry=true,
  )
  cfg_builder_add_edge(
    builder,
    current_block,
    then_build.entry_block.unwrap(),
    CfgEdgeKind::FallthroughEdge,
  )
  cfg_builder_connect_exits(builder, then_build.exit_blocks, Some(continuation))
  match hot_region_else_ref(func, if_id) {
    Some(else_ref) => {
      let else_build = cfg_builder_build_region(
        builder,
        func,
        else_ref,
        handler_target,
        force_entry=true,
      )
      cfg_builder_add_edge(
        builder,
        current_block,
        else_build.entry_block.unwrap(),
        CfgEdgeKind::FallthroughEdge,
      )
      cfg_builder_connect_exits(
        builder,
        else_build.exit_blocks,
        Some(continuation),
      )
    }
    None =>
      cfg_builder_add_edge(
        builder,
        current_block,
        continuation,
        CfgEdgeKind::FallthroughEdge,
      )
  }
}

///|
fn cfg_builder_process_block(
  builder : CfgBuilder,
  func : HotFunc,
  region_ref : HotRegionRef,
  segments : CfgRegionSegments,
  segment_idx : Int32,
  handler_target : BlockId?,
  exits : Array[BlockId],
) -> Unit {
  let segment = segments.get(segment_idx)
  let current_block = segment.block_id
  let next_block = if segment_idx + 1 < segments.length() {
    Some(segments.block_id(segment_idx + 1))
  } else {
    None
  }
  let last_node = segment.last_node
  let node = hot_node_get(func, last_node)
  if !builder.continuation_targets.is_empty() {
    for root in builder.cfg.blocks[current_block].nodes {
      for
        label in cfg_expression_continuation_targets(
          func,
          root,
          builder.continuation_targets,
        ) {
        cfg_builder_add_edge(
          builder,
          current_block,
          cfg_builder_label_target_get(builder, label),
          CfgEdgeKind::BranchEdge,
        )
      }
    }
  }
  match node.op {
    HotOp::Block => {
      let (continuation, synthetic_exit) = cfg_builder_maybe_continuation(
        builder,
        region_ref,
        segment.end_slot,
        next_block,
      )
      cfg_builder_label_target_set(
        builder,
        hot_control_node_label(func, last_node),
        continuation.unwrap(),
      )
      let body_build = cfg_builder_build_region(
        builder,
        func,
        hot_region_body_ref(func, last_node),
        handler_target,
        force_entry=true,
      )
      cfg_builder_add_edge(
        builder,
        current_block,
        body_build.entry_block.unwrap(),
        CfgEdgeKind::FallthroughEdge,
      )
      cfg_builder_connect_exits(builder, body_build.exit_blocks, continuation)
      if synthetic_exit {
        exits.push(continuation.unwrap())
      }
    }
    HotOp::Loop => {
      // The forced body entry is allocated next. Branches enter the body;
      // they must not evaluate the initial stack arguments again.
      cfg_builder_label_target_set(
        builder,
        hot_control_node_label(func, last_node),
        builder.cfg.blocks.length(),
      )
      let body_build = cfg_builder_build_region(
        builder,
        func,
        hot_region_body_ref(func, last_node),
        handler_target,
        force_entry=true,
      )
      cfg_builder_add_edge(
        builder,
        current_block,
        body_build.entry_block.unwrap(),
        CfgEdgeKind::FallthroughEdge,
      )
      cfg_builder_connect_exits(builder, body_build.exit_blocks, next_block)
      if next_block == None {
        for exit_block in body_build.exit_blocks {
          exits.push(exit_block)
        }
      }
    }
    HotOp::If => {
      let (continuation, synthetic_exit) = cfg_builder_maybe_continuation(
        builder,
        region_ref,
        segment.end_slot,
        next_block,
      )
      cfg_builder_connect_result_if(
        builder,
        func,
        last_node,
        current_block,
        continuation.unwrap(),
        handler_target,
      )
      if synthetic_exit {
        exits.push(continuation.unwrap())
      }
    }
    HotOp::Drop => {
      let operand_id = hot_child_get(func, last_node, 0)
      if builder.operand_seen == None &&
        operand_id >= 0 &&
        hot_node_is_live(func, operand_id) &&
        hot_node_get(func, operand_id).op == HotOp::If &&
        ssa_simple_drop_result_if_allowed(func, last_node, 0, operand_id) {
        let (continuation, synthetic_exit) = cfg_builder_maybe_continuation(
          builder,
          region_ref,
          segment.end_slot,
          next_block,
        )
        cfg_builder_connect_result_if(
          builder,
          func,
          operand_id,
          current_block,
          continuation.unwrap(),
          handler_target,
        )
        if synthetic_exit {
          exits.push(continuation.unwrap())
        }
      } else if cfg_dropped_branch_operand(func, last_node) is Some(branch) {
        for target in cfg_builder_branch_targets(builder, func, branch) {
          cfg_builder_add_edge(
            builder,
            current_block,
            target,
            CfgEdgeKind::BranchEdge,
          )
        }
        match next_block {
          Some(block_id) =>
            cfg_builder_add_edge(
              builder,
              current_block,
              block_id,
              CfgEdgeKind::FallthroughEdge,
            )
          None => exits.push(current_block)
        }
      } else {
        match next_block {
          Some(block_id) =>
            cfg_builder_add_edge(
              builder,
              current_block,
              block_id,
              CfgEdgeKind::FallthroughEdge,
            )
          None => exits.push(current_block)
        }
      }
    }
    HotOp::Try => {
      let (continuation, synthetic_exit) = cfg_builder_maybe_continuation(
        builder,
        region_ref,
        segment.end_slot,
        next_block,
      )
      cfg_builder_label_target_set(
        builder,
        hot_control_node_label(func, last_node),
        continuation.unwrap(),
      )
      let catch_build = match hot_region_catch_ref(func, last_node) {
        Some(catch_ref) =>
          Some(
            cfg_builder_build_region(
              builder,
              func,
              catch_ref,
              handler_target,
              force_entry=true,
            ),
          )
        None => None
      }
      let body_build = cfg_builder_build_region(
        builder,
        func,
        hot_region_body_ref(func, last_node),
        match catch_build {
          Some(build) => build.entry_block
          None => handler_target
        },
        force_entry=true,
      )
      cfg_builder_add_edge(
        builder,
        current_block,
        body_build.entry_block.unwrap(),
        CfgEdgeKind::FallthroughEdge,
      )
      cfg_builder_connect_exits(builder, body_build.exit_blocks, continuation)
      match catch_build {
        Some(build) => {
          if builder.conservative_exception_entries {
            cfg_builder_add_edge(
              builder,
              current_block,
              build.entry_block.unwrap(),
              CfgEdgeKind::ExceptionalEdge,
            )
          }
          cfg_builder_connect_exits(builder, build.exit_blocks, continuation)
        }
        None => ()
      }
      if synthetic_exit {
        exits.push(continuation.unwrap())
      }
    }
    HotOp::TryTable => {
      let (continuation, synthetic_exit) = cfg_builder_maybe_continuation(
        builder,
        region_ref,
        segment.end_slot,
        next_block,
      )
      cfg_builder_label_target_set(
        builder,
        hot_control_node_label(func, last_node),
        continuation.unwrap(),
      )
      let catch_info = hot_catch_info_get(func, node.imm1)
      let catch_build = cfg_builder_build_region(
        builder,
        func,
        hot_region_catch_list_ref(func, last_node),
        handler_target,
        force_entry=true,
      )
      let body_build = cfg_builder_build_region(
        builder,
        func,
        hot_region_body_ref(func, last_node),
        catch_build.entry_block,
        force_entry=true,
      )
      cfg_builder_add_edge(
        builder,
        current_block,
        body_build.entry_block.unwrap(),
        CfgEdgeKind::FallthroughEdge,
      )
      if builder.conservative_exception_entries {
        cfg_builder_add_edge(
          builder,
          current_block,
          catch_build.entry_block.unwrap(),
          CfgEdgeKind::ExceptionalEdge,
        )
      }
      cfg_builder_connect_exits(builder, body_build.exit_blocks, continuation)
      let catches_exhaustive = catch_info.catches.any(fn(catch_) {
        match catch_ {
          HotCatchArm::CatchAllArm(_) | HotCatchArm::CatchAllRefArm(_) => true
          _ => false
        }
      })
      for catch_exit in catch_build.exit_blocks {
        for catch_ in catch_info.catches {
          let label_id = match catch_ {
            HotCatchArm::CatchArm(_, target)
            | HotCatchArm::CatchRefArm(_, target)
            | HotCatchArm::CatchAllArm(target)
            | HotCatchArm::CatchAllRefArm(target) => target
          }
          cfg_builder_add_edge(
            builder,
            catch_exit,
            cfg_builder_exception_label_target_get(builder, label_id),
            CfgEdgeKind::ExceptionalEdge,
          )
        }
        if !catches_exhaustive {
          cfg_builder_add_edge(
            builder,
            catch_exit,
            cfg_builder_exception_target(builder, handler_target),
            CfgEdgeKind::ExceptionalEdge,
          )
        }
      }
      if synthetic_exit {
        exits.push(continuation.unwrap())
      }
    }
    HotOp::Br =>
      for target in cfg_builder_branch_targets(builder, func, last_node) {
        cfg_builder_add_edge(
          builder,
          current_block,
          target,
          CfgEdgeKind::BranchEdge,
        )
      }
    HotOp::BrIf
    | HotOp::BrOnNull
    | HotOp::BrOnNonNull
    | HotOp::BrOnCast
    | HotOp::BrOnCastFail => {
      for target in cfg_builder_branch_targets(builder, func, last_node) {
        cfg_builder_add_edge(
          builder,
          current_block,
          target,
          CfgEdgeKind::BranchEdge,
        )
      }
      match next_block {
        Some(block_id) =>
          cfg_builder_add_edge(
            builder,
            current_block,
            block_id,
            CfgEdgeKind::FallthroughEdge,
          )
        None => exits.push(current_block)
      }
    }
    HotOp::BrTable =>
      for target in cfg_builder_branch_targets(builder, func, last_node) {
        cfg_builder_add_edge(
          builder,
          current_block,
          target,
          CfgEdgeKind::BranchEdge,
        )
      }
    HotOp::Return
    | HotOp::ReturnCall
    | HotOp::ReturnCallIndirect
    | HotOp::ReturnCallRef =>
      cfg_builder_add_edge(
        builder,
        current_block,
        builder.cfg.exit_block,
        CfgEdgeKind::ReturnEdge,
      )
    HotOp::Throw | HotOp::ThrowRef | HotOp::Rethrow =>
      cfg_builder_add_edge(
        builder,
        current_block,
        cfg_builder_exception_target(builder, handler_target),
        CfgEdgeKind::ExceptionalEdge,
      )
    HotOp::Call | HotOp::CallIndirect | HotOp::CallRef => {
      // Argument writes precede this edge; result consumers belong to the
      // normal continuation. Traps and tail calls are not caught calls.
      if handler_target is Some(_) {
        cfg_builder_add_edge(
          builder,
          current_block,
          cfg_builder_exception_target(builder, handler_target),
          CfgEdgeKind::ExceptionalEdge,
        )
      }
      match next_block {
        Some(block_id) =>
          cfg_builder_add_edge(
            builder,
            current_block,
            block_id,
            CfgEdgeKind::FallthroughEdge,
          )
        None => exits.push(current_block)
      }
    }
    HotOp::Delegate =>
      for
        target in cfg_builder_exception_branch_targets(builder, func, last_node) {
        cfg_builder_add_edge(
          builder,
          current_block,
          target,
          CfgEdgeKind::ExceptionalEdge,
        )
      }
    HotOp::Unreachable =>
      cfg_builder_add_edge(
        builder,
        current_block,
        builder.cfg.exit_block,
        CfgEdgeKind::UnreachableExitEdge,
      )
    _ =>
      match next_block {
        Some(block_id) =>
          cfg_builder_add_edge(
            builder,
            current_block,
            block_id,
            CfgEdgeKind::FallthroughEdge,
          )
        None => exits.push(current_block)
      }
  }
}

///|
fn cfg_builder_build_region(
  builder : CfgBuilder,
  func : HotFunc,
  region_ref : HotRegionRef,
  handler_target : BlockId?,
  force_entry~ : Bool = false,
) -> CfgRegionBuild {
  if hot_region_root_count(func, region_ref) == 0 {
    return cfg_builder_region_build_empty(builder, region_ref, force_entry)
  }
  let segments = cfg_builder_region_segments(
    builder,
    func,
    region_ref,
    handler_target~,
  )
  if segments.length() == 0 {
    return cfg_builder_region_build_empty(builder, region_ref, force_entry)
  }
  let exits : Array[BlockId] = []
  for idx = 0; idx < segments.length(); idx = idx + 1 {
    cfg_builder_process_block(
      builder, func, region_ref, segments, idx, handler_target, exits,
    )
  }
  { entry_block: Some(segments.block_id(0)), exit_blocks: exits, }
}

///|
fn cfg_edge_kind_bit(kind : CfgEdgeKind) -> UInt32 {
  match kind {
    FallthroughEdge => 1U
    BranchEdge => 2U
    ReturnEdge => 4U
    ExceptionalEdge => 8U
    UnreachableExitEdge => 16U
  }
}

///|
fn cfg_indexed_edge_symmetry_error(cfg : HotCfg) -> String? {
  let count = cfg.blocks.length()
  let heads = Array::make(count, -1)
  let mut edge_count = 0
  for block in cfg.blocks {
    let wide_source = block.succs.length() > 8
    for edge in block.succs {
      if edge.block < 0 || edge.block >= count {
        return Some("cfg successor target must reference a valid block id")
      }
      if wide_source || cfg.blocks[edge.block].preds.length() > 8 {
        edge_count += 1
      }
    }
  }
  let rows = Array::make(edge_count * 3, 0)
  let mut next_row = 0
  for source in 0.. 8
    for edge in successors {
      if !wide_source && cfg.blocks[edge.block].preds.length() <= 8 {
        let mut found = false
        for pred in cfg.blocks[edge.block].preds {
          if pred.block == source && pred.kind == edge.kind {
            found = true
            break
          }
        }
        if !found {
          return Some("cfg successors and predecessors must stay symmetric")
        }
        continue
      }
      let row = next_row
      rows[row] = source
      rows[row + 1] = cfg_edge_kind_bit(edge.kind).reinterpret_as_int()
      rows[row + 2] = heads[edge.block]
      next_row += 3
      heads[edge.block] = row
    }
  }
  let masks = Array::make(count, 0U)
  for destination in 0.. 8
    for edge in predecessors {
      let source = edge.block
      if source < 0 || source >= count {
        return Some("cfg predecessor source must reference a valid block id")
      }
      if !wide_destination && cfg.blocks[source].succs.length() <= 8 {
        let mut found = false
        for succ in cfg.blocks[source].succs {
          if succ.block == destination && succ.kind == edge.kind {
            found = true
            break
          }
        }
        if !found {
          return Some("cfg predecessors and successors must stay symmetric")
        }
        continue
      }
      masks[source] = masks[source] | cfg_edge_kind_bit(edge.kind)
    }
    let mut row = heads[destination]
    while row >= 0 {
      let source = rows[row]
      let bit = rows[row + 1].reinterpret_as_uint()
      if (masks[source] & bit) == 0U {
        return Some("cfg successors and predecessors must stay symmetric")
      }
      masks[source] = masks[source] | (bit << 5)
      row = rows[row + 2]
    }
    for edge in predecessors {
      if (wide_destination || cfg.blocks[edge.block].succs.length() > 8) &&
        (masks[edge.block] & (cfg_edge_kind_bit(edge.kind) << 5)) == 0U {
        return Some("cfg predecessors and successors must stay symmetric")
      }
    }
    // Clear after all kinds for this destination have been checked. Duplicate
    // source/kind pairs share one mask; clearing during verification loses it.
    for edge in predecessors {
      masks[edge.block] = 0U
    }
  }
  None
}

///|
fn cfg_verify(
  cfg : HotCfg,
  node_membership_visits~ : Array[Int32]? = None,
) -> Unit {
  let listed = if cfg.blocks.any(fn(block) { block.nodes.length() > 8 }) {
    let seen = Array::make(cfg.node_to_block.length(), false)
    for block in cfg.blocks {
      for node in block.nodes {
        match node_membership_visits {
          Some(count) => count[0] += 1
          None => ()
        }
        if node >= 0 &&
          node < seen.length() &&
          cfg.node_to_block[node] == block.id {
          seen[node] = true
        }
      }
    }
    Some(seen)
  } else {
    None
  }
  // Keep the allocation-free bounded scan for ordinary small-degree CFGs.
  // Shared catch hubs need indexed symmetry checks rather than one scan of
  // all incoming calls for each call edge.
  let indexed_edges = cfg.blocks.any(fn(block) {
    block.preds.length() > 8 || block.succs.length() > 8
  })
  if indexed_edges {
    if cfg_indexed_edge_symmetry_error(cfg) is Some(error) {
      abort(error)
    }
  }
  for block_id = 0; block_id < cfg.blocks.length(); block_id = block_id + 1 {
    let block = cfg.blocks[block_id]
    if block.id != block_id {
      abort("cfg block ids must match their storage index")
    }
    if !indexed_edges {
      for succ in block.succs {
        if succ.block < 0 || succ.block >= cfg.blocks.length() {
          abort("cfg successor target must reference a valid block id")
        }
        let mut found = false
        for pred in cfg.blocks[succ.block].preds {
          if pred.block == block_id && pred.kind == succ.kind {
            found = true
            break
          }
        }
        if !found {
          abort("cfg successors and predecessors must stay symmetric")
        }
      }
      for pred in block.preds {
        if pred.block < 0 || pred.block >= cfg.blocks.length() {
          abort("cfg predecessor source must reference a valid block id")
        }
        let mut found = false
        for succ in cfg.blocks[pred.block].succs {
          if succ.block == block_id && succ.kind == pred.kind {
            found = true
            break
          }
        }
        if !found {
          abort("cfg predecessors and successors must stay symmetric")
        }
      }
    }
  }
  for node_id = 0; node_id < cfg.node_to_block.length(); node_id = node_id + 1 {
    let block_id = cfg.node_to_block[node_id]
    if block_id < 0 {
      continue
    }
    if block_id >= cfg.blocks.length() {
      abort("cfg node-to-block mapping must reference a valid block")
    }
    let mut found = match listed {
      Some(seen) => seen[node_id]
      None => false
    }
    if listed is None {
      for mapped in cfg.blocks[block_id].nodes {
        match node_membership_visits {
          Some(count) => count[0] += 1
          None => ()
        }
        if mapped == node_id {
          found = true
          break
        }
      }
    }
    if !found {
      abort("cfg node-to-block mapping must match the owning block node list")
    }
  }
  for entry in cfg.region_roots {
    if entry.block < 0 || entry.block >= cfg.blocks.length() {
      abort("cfg region-root mapping must reference a valid block")
    }
  }
}

///|
pub fn cfg_build(
  func : HotFunc,
  conservative_exception_entries~ : Bool = true,
  expand_operand_control~ : Bool = false,
) -> HotCfg {
  hot_verify_all_or_abort(func)
  let builder = cfg_builder_new(
    func, conservative_exception_entries, expand_operand_control,
  )
  let entry_block = cfg_builder_add_block(builder, [], true)
  let exit_block = cfg_builder_add_block(builder, [], true)
  builder.cfg.entry_block = entry_block
  builder.cfg.exit_block = exit_block

  let root_build = cfg_builder_build_region(
    builder,
    func,
    HotRegionRef::root(),
    None,
  )
  match root_build.entry_block {
    Some(block_id) =>
      cfg_builder_add_edge(
        builder,
        entry_block,
        block_id,
        CfgEdgeKind::FallthroughEdge,
      )
    None =>
      cfg_builder_add_edge(
        builder,
        entry_block,
        exit_block,
        CfgEdgeKind::FallthroughEdge,
      )
  }
  cfg_builder_connect_exits(builder, root_build.exit_blocks, Some(exit_block))

  cfg_verify(builder.cfg)
  builder.cfg
}

///|
pub fn cfg_block_count(cfg : HotCfg) -> Int32 {
  cfg.blocks.length()
}

///|
pub fn cfg_entry_block(cfg : HotCfg) -> BlockId {
  cfg.entry_block
}

///|
pub fn cfg_exit_block(cfg : HotCfg) -> BlockId {
  cfg.exit_block
}

///|
pub fn cfg_exceptional_exit_block(cfg : HotCfg) -> BlockId? {
  cfg.exceptional_exit_block
}

///|
pub fn cfg_block_get(cfg : HotCfg, block_id : BlockId) -> HotBlockInfo {
  cfg.blocks[block_id]
}

///|
pub fn cfg_block_for_node(cfg : HotCfg, node_id : NodeId) -> BlockId? {
  match cfg.node_to_block.get(node_id) {
    Some(block_id) if block_id >= 0 => Some(block_id)
    _ => None
  }
}

///|
pub fn cfg_block_for_region_root(
  cfg : HotCfg,
  region_ref : HotRegionRef,
  slot : Int32,
) -> BlockId? {
  let region_holder = cfg_builder_region_holder(region_ref)
  for entry in cfg.region_roots {
    if entry.region_holder == region_holder && entry.slot == slot {
      return Some(entry.block)
    }
  }
  None
}

///|
pub fn cfg_successors(cfg : HotCfg, block_id : BlockId) -> Array[CfgEdge] {
  cfg_successor_edges(cfg, block_id).copy()
}

///|
/// Reports whether execution beginning at one structured-region root can reach
/// the following root in the same region. This is stronger than inspecting the
/// root opcode: structured controls may complete through their own labels, and
/// exception or loop edges may still reach the continuation.
pub fn cfg_region_root_can_reach_next(
  func : HotFunc,
  cfg : HotCfg,
  region_ref : HotRegionRef,
  slot : Int32,
) -> Bool {
  let fallback = fn() {
    if slot < 0 || slot + 1 >= hot_region_root_count(func, region_ref) {
      return false
    }
    let root_id = hot_region_get(func, region_ref, slot)
    if root_id < 0 || !hot_node_is_live(func, root_id) {
      return true
    }
    let edge_kinds = cfg_terminator_edge_kinds(hot_query_op(func, root_id))
    if edge_kinds.length() == 0 {
      return true
    }
    for kind in edge_kinds {
      if kind == CfgEdgeKind::FallthroughEdge {
        return true
      }
    }
    false
  }
  let start = match cfg_block_for_region_root(cfg, region_ref, slot) {
    Some(block) => block
    None => return fallback()
  }
  let target = match cfg_block_for_region_root(cfg, region_ref, slot + 1) {
    Some(block) => block
    None => return fallback()
  }
  if start == target {
    return true
  }
  let seen = Array::make(cfg_block_count(cfg), false)
  let worklist = [start]
  while worklist.length() > 0 {
    let current = worklist.pop().unwrap()
    if current == target {
      return true
    }
    if current < 0 || current >= seen.length() || seen[current] {
      continue
    }
    seen[current] = true
    for edge in cfg_successor_edges(cfg, current) {
      if edge.block == target {
        return true
      }
      if edge.block >= 0 && edge.block < seen.length() && !seen[edge.block] {
        worklist.push(edge.block)
      }
    }
  }
  false
}

///|
pub fn cfg_predecessors(cfg : HotCfg, block_id : BlockId) -> Array[CfgEdge] {
  cfg_predecessor_edges(cfg, block_id).copy()
}

///|
pub fn cfg_successor_edges(cfg : HotCfg, block_id : BlockId) -> Array[CfgEdge] {
  cfg.blocks[block_id].succs
}

///|
pub fn cfg_predecessor_edges(
  cfg : HotCfg,
  block_id : BlockId,
) -> Array[CfgEdge] {
  cfg.blocks[block_id].preds
}