// Ion-Style LiveRange Data Structures
// Enhanced live interval representation with multiple precise spans
// and use position constraints for the backtracking allocator.

///|
/// A contiguous program point range (start inclusive, end exclusive)
struct ProgPointRange {
  start : ProgPoint
  end : ProgPoint
}

///|
fn ProgPointRange::ProgPointRange(
  start : ProgPoint,
  end : ProgPoint,
) -> ProgPointRange {
  { start, end }
}

///|
fn ProgPointRange::to_string(self : ProgPointRange) -> String {
  "\{self.start}-\{self.end}"
}

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

///|
/// Check if two ranges overlap (using block order for comparison)
fn ProgPointRange::overlaps(
  self : ProgPointRange,
  other : ProgPointRange,
  block_order : FixedArray[Int],
) -> Bool {
  // Ranges overlap if: self.start < other.end AND other.start < self.end
  let self_start_cmp = self.start.compare_with_order(other.end, block_order)
  let other_start_cmp = other.start.compare_with_order(self.end, block_order)
  self_start_cmp < 0 && other_start_cmp < 0
}

///|
/// Check if a point is within this range
pub fn ProgPointRange::contains(
  self : ProgPointRange,
  point : ProgPoint,
  block_order : FixedArray[Int],
) -> Bool {
  let start_cmp = self.start.compare_with_order(point, block_order)
  let end_cmp = point.compare_with_order(self.end, block_order)
  start_cmp <= 0 && end_cmp < 0
}

///|
/// Kind of use at a program point
/// Note: For tied operands (same reg as both def and use), add a DefUse variant
/// when Inst supports tied operand representation.
enum UseKind {
  Def // Definition (value produced)
  Use // Use (value consumed)
}

///|
fn UseKind::to_string(self : UseKind) -> String {
  match self {
    Def => "def"
    Use => "use"
  }
}

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

///|
/// Operand constraint for a use position
pub enum OperandConstraint {
  AnyReg // Any register in the class
  FixedReg(@abi.PReg) // Must be this specific register
}

///|
fn OperandConstraint::to_string(self : OperandConstraint) -> String {
  match self {
    AnyReg => "any"
    FixedReg(preg) => "fixed(\{preg})"
  }
}

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

///|
/// A use position within a LiveRange
struct UsePosition {
  point : ProgPoint
  kind : UseKind
  constraint : OperandConstraint
}

///|
fn UsePosition::UsePosition(
  point : ProgPoint,
  kind : UseKind,
  constraint : OperandConstraint,
) -> UsePosition {
  { point, kind, constraint }
}

///|
fn UsePosition::to_string(self : UsePosition) -> String {
  "\{self.point}:\{self.kind}:\{self.constraint}"
}

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

///|
/// Allocation result for a LiveRange or Bundle
pub enum Allocation {
  Reg(@abi.PReg) // Allocated to a physical register
  Spill(Int) // Spilled to stack slot
  Unallocated // Not yet allocated
}

///|
fn Allocation::to_string(self : Allocation) -> String {
  match self {
    Reg(preg) => "\{preg}"
    Spill(slot) => "[sp+\{slot}]"
    Unallocated => "?"
  }
}

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

///|
/// A LiveRange represents the liveness of a single virtual register
/// with precise span information and use constraints.
struct LiveRange {
  id : Int
  vreg : @abi.VReg
  ranges : Array[ProgPointRange] // Multiple non-overlapping spans
  uses : Array[UsePosition] // Use positions with constraints
  mut bundle_id : Int // Owning Bundle ID (-1 if none)
  mut allocation : Allocation // Allocation result
  // Cached properties
  mut crosses_call : Bool // Spans across any function call
  mut crosses_internal_call : Bool // Spans across an internal call
  mut crosses_foreign_call : Bool // Spans across a foreign/helper call
}

///|
pub fn LiveRange::LiveRange(id : Int, vreg : @abi.VReg) -> LiveRange {
  {
    id,
    vreg,
    ranges: [],
    uses: [],
    bundle_id: -1,
    allocation: Unallocated,
    crosses_call: false,
    crosses_internal_call: false,
    crosses_foreign_call: false,
  }
}

///|
/// Add a span to this LiveRange
fn LiveRange::add_range(self : LiveRange, range : ProgPointRange) -> Unit {
  self.ranges.push(range)
}

///|
/// Add a use position
fn LiveRange::add_use(self : LiveRange, use_pos : UsePosition) -> Unit {
  self.uses.push(use_pos)
}

///|
/// Check if this LiveRange has any fixed register constraint
pub fn LiveRange::has_fixed_constraint(self : LiveRange) -> Bool {
  for use_pos in self.uses {
    if use_pos.constraint is FixedReg(_) {
      return true
    }
  }
  false
}

///|
/// Get the fixed register constraint if all uses require the same fixed reg
pub fn LiveRange::get_fixed_reg(self : LiveRange) -> @abi.PReg? {
  fn is_allocatable_fixed(preg : @abi.PReg) -> Bool {
    if preg.class is Int {
      // Keep architectural reserved/scratch regs out of fixed allocation.
      // Argument regs (x0-x7) are allowed so fixed-arg constraints can be
      // satisfied by allocation when profitable, matching regalloc2 style.
      preg.index != 16 &&
      preg.index != 17 &&
      preg.index != 18 &&
      preg.index != 19 &&
      preg.index != 29 &&
      preg.index != 30 &&
      preg.index != 31
    } else {
      // For float/vector bank, keep v16/v17 reserved as scratch.
      preg.index != 16 && preg.index != 17
    }
  }

  let mut fixed : @abi.PReg? = None
  for use_pos in self.uses {
    if use_pos.constraint is FixedReg(preg) {
      if !is_allocatable_fixed(preg) {
        // Treat non-allocatable fixed regs as "use-time constraints" only.
        // They are satisfied via inserted moves, not by pinning allocation.
        continue
      }
      match fixed {
        None => fixed = Some(preg)
        Some(existing) =>
          if existing.index != preg.index {
            return None // Conflicting constraints
          }
      }
    }
  }
  fixed
}

///|
/// Check if this LiveRange overlaps with another
pub fn LiveRange::overlaps(
  self : LiveRange,
  other : LiveRange,
  block_order : FixedArray[Int],
) -> Bool {
  for self_range in self.ranges {
    for other_range in other.ranges {
      if self_range.overlaps(other_range, block_order) {
        return true
      }
    }
  }
  false
}

///|
/// Get the start point (earliest point in all ranges)
pub fn LiveRange::start(
  self : LiveRange,
  block_order : FixedArray[Int],
) -> ProgPoint? {
  ignore(block_order)
  if self.ranges.is_empty() {
    return None
  }
  Some(self.ranges[0].start)
}

///|
/// Get the end point (latest point in all ranges)
pub fn LiveRange::end(
  self : LiveRange,
  block_order : FixedArray[Int],
) -> ProgPoint? {
  ignore(block_order)
  if self.ranges.is_empty() {
    return None
  }
  Some(self.ranges[self.ranges.length() - 1].end)
}

///|
/// Compute total length of all ranges (in instruction count)
pub fn LiveRange::total_length(self : LiveRange) -> Int {
  let mut total = 0
  for range in self.ranges {
    // Simple approximation: count instructions
    // Same block: inst difference
    // Different blocks: add a fixed cost
    if range.start.block == range.end.block {
      total += range.end.inst - range.start.inst + 1
    } else {
      // Cross-block: use a larger estimate
      total += (range.end.block - range.start.block) * 10 +
        (range.end.inst - range.start.inst).abs() +
        1
    }
  }
  if total <= 0 {
    1
  } else {
    total
  }
}

///|
fn LiveRange::to_string(self : LiveRange) -> String {
  let mut result = "LR\{self.id} \{self.vreg}: ["
  for i, range in self.ranges {
    if i > 0 {
      result = result + ", "
    }
    result = result + range.to_string()
  }
  result = result + "] -> \{self.allocation}"
  if self.bundle_id >= 0 {
    result = result + " (bundle \{self.bundle_id})"
  }
  result
}

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

///|
/// Collection of LiveRanges built from liveness analysis
struct LiveRangeSet {
  ranges : Array[LiveRange]
  vreg_to_range_dense : Array[Int] // vreg.id -> range index (-1 if absent)
  block_order : FixedArray[Int] // Block execution order (O(1) lookup)
  blockparam_out_pairs : Array[(Int, Int)] // (to_range_idx, from_range_idx)
}

///|
fn LiveRangeSet::LiveRangeSet(
  block_order : FixedArray[Int],
  max_vreg_id : Int,
) -> LiveRangeSet {
  {
    ranges: [],
    vreg_to_range_dense: Array::make(max_vreg_id, -1),
    block_order,
    blockparam_out_pairs: [],
  }
}

///|
/// Add a new LiveRange
fn LiveRangeSet::add_range(self : LiveRangeSet, range : LiveRange) -> Unit {
  let idx = self.ranges.length()
  guard range.vreg.id >= 0 && range.vreg.id < self.vreg_to_range_dense.length() else {
    abort("vreg id out of range when building live ranges")
  }
  self.vreg_to_range_dense[range.vreg.id] = idx
  self.ranges.push(range)
}

///|
/// Get LiveRange by vreg id
pub fn LiveRangeSet::get_by_vreg(
  self : LiveRangeSet,
  vreg_id : Int,
) -> LiveRange? {
  if vreg_id < 0 || vreg_id >= self.vreg_to_range_dense.length() {
    return None
  }
  let idx = self.vreg_to_range_dense[vreg_id]
  if idx >= 0 {
    Some(self.ranges[idx])
  } else {
    None
  }
}

///|
/// Get LiveRange index by vreg id
fn LiveRangeSet::get_range_index_by_vreg(
  self : LiveRangeSet,
  vreg_id : Int,
) -> Int? {
  if vreg_id < 0 || vreg_id >= self.vreg_to_range_dense.length() {
    return None
  }
  let idx = self.vreg_to_range_dense[vreg_id]
  if idx >= 0 {
    Some(idx)
  } else {
    None
  }
}

///|
/// Get LiveRange by index
pub fn LiveRangeSet::get(self : LiveRangeSet, idx : Int) -> LiveRange {
  self.ranges[idx]
}

///|
/// Get number of ranges
pub fn LiveRangeSet::length(self : LiveRangeSet) -> Int {
  self.ranges.length()
}

///|
/// Build LiveRanges from liveness analysis result
/// This is Phase 2 of the Ion allocator
pub fn build_live_ranges(
  func : @machv.Function,
  liveness : LivenessResult,
) -> LiveRangeSet {
  let result = LiveRangeSet::LiveRangeSet(
    liveness.block_order,
    func.next_vreg_id,
  )
  let mut next_id = 0
  let block_order = liveness.block_order
  let num_blocks = func.blocks.length()
  // Match regalloc2's dense global ProgPoint ordering:
  // order points by block order, and within each block by inst/pos.
  let block_idx_by_order : Array[Int] = Array::make(num_blocks, 0)
  for block_idx in 0.. Int {
    let block_ord = block_order[point.block]
    let pos_ord = match point.pos {
      Before => 0
      After => 1
    }
    block_point_base[block_ord] + (point.inst + 1) * 2 + pos_ord
  }
  let sorted_call_points = liveness.call_points.copy()
  sorted_call_points.sort_by(fn(a, b) {
    let a_ord = point_order(a.0, block_order, block_point_base)
    let b_ord = point_order(b.0, block_order, block_point_base)
    if a_ord < b_ord {
      -1
    } else if a_ord > b_ord {
      1
    } else {
      0
    }
  })
  let block_call_point_orders : Array[Array[Int]] = []
  let block_foreign_call_point_orders : Array[Array[Int]] = []
  for _ in 0..= 0 && call_point.block < num_blocks {
      block_call_point_orders[call_point.block].push(call_ord)
      if call_class is Foreign {
        block_foreign_call_point_orders[call_point.block].push(call_ord)
      }
    }
    let prev = foreign_call_prefix[foreign_call_prefix.length() - 1]
    let add = if call_class is Foreign { 1 } else { 0 }
    foreign_call_prefix.push(prev + add)
  }
  let calls_per_block_order : Array[Int] = Array::make(num_blocks, 0)
  let foreign_calls_per_block_order : Array[Int] = Array::make(num_blocks, 0)
  for block_idx in 0..= 0 && ord < num_blocks {
      calls_per_block_order[ord] = block_call_point_orders[block_idx].length()
      foreign_calls_per_block_order[ord] = block_foreign_call_point_orders[block_idx].length()
    }
  }
  let call_prefix_by_block_order : Array[Int] = [0]
  let foreign_call_prefix_by_block_order : Array[Int] = [0]
  for ord in 0.. Int {
    let mut lo = 0
    let mut hi = point_orders.length()
    while lo < hi {
      let mid = lo + (hi - lo) / 2
      if point_orders[mid] <= point_ord {
        lo = mid + 1
      } else {
        hi = mid
      }
    }
    lo
  }

  fn lower_bound_call_points(
    point_orders : Array[Int],
    point_ord : Int,
  ) -> Int {
    let mut lo = 0
    let mut hi = point_orders.length()
    while lo < hi {
      let mid = lo + (hi - lo) / 2
      if point_orders[mid] < point_ord {
        lo = mid + 1
      } else {
        hi = mid
      }
    }
    lo
  }
  fn count_call_points_in_range(
    point_orders : Array[Int],
    start_ord : Int,
    end_ord : Int,
  ) -> Int {
    let first_after_start = upper_bound_call_points(point_orders, start_ord)
    let first_at_or_after_end = lower_bound_call_points(point_orders, end_ord)
    if first_after_start < first_at_or_after_end {
      first_at_or_after_end - first_after_start
    } else {
      0
    }
  }
  fn point_after(point : ProgPoint, block_len : Int) -> ProgPoint {
    match point.pos {
      Before => { block: point.block, inst: point.inst, pos: After }
      After =>
        if point.inst < block_len {
          { block: point.block, inst: point.inst + 1, pos: Before }
        } else {
          { block: point.block, inst: block_len, pos: After }
        }
    }
  }

  fn pp_lt(
    a : ProgPoint,
    b : ProgPoint,
    block_order : FixedArray[Int],
    block_point_base : Array[Int],
  ) -> Bool {
    point_order(a, block_order, block_point_base) <
    point_order(b, block_order, block_point_base)
  }

  fn pp_le(
    a : ProgPoint,
    b : ProgPoint,
    block_order : FixedArray[Int],
    block_point_base : Array[Int],
  ) -> Bool {
    point_order(a, block_order, block_point_base) <=
    point_order(b, block_order, block_point_base)
  }

  fn pp_min(
    a : ProgPoint,
    b : ProgPoint,
    block_order : FixedArray[Int],
    block_point_base : Array[Int],
  ) -> ProgPoint {
    if pp_le(a, b, block_order, block_point_base) {
      a
    } else {
      b
    }
  }

  fn pp_max(
    a : ProgPoint,
    b : ProgPoint,
    block_order : FixedArray[Int],
    block_point_base : Array[Int],
  ) -> ProgPoint {
    if pp_lt(a, b, block_order, block_point_base) {
      b
    } else {
      a
    }
  }

  fn add_live_block_dense(
    block_map : Array[Array[Int]?],
    vreg_id : Int,
    block_idx : Int,
  ) -> Unit {
    if vreg_id < 0 || vreg_id >= block_map.length() {
      return
    }
    match block_map[vreg_id] {
      Some(blocks) => blocks.push(block_idx)
      None => block_map[vreg_id] = Some([block_idx])
    }
  }

  // Precompute vreg -> live block mapping once (dense by vreg id).
  // This avoids O(vregs × blocks × contains) scans when forming spans.
  let max_vreg_id = func.next_vreg_id
  let vreg_live_in_blocks : Array[Array[Int]?] = Array::make(max_vreg_id, None)
  let vreg_live_out_blocks : Array[Array[Int]?] = Array::make(max_vreg_id, None)
  let live_in_rows : Array[Array[Int]] = match liveness.live_in_dense {
    Some(rows) => rows
    None => {
      let rows : Array[Array[Int]] = []
      for block_idx in 0.. rows
    None => {
      let rows : Array[Array[Int]] = []
      for block_idx in 0.. FixedReg(preg)
          None => AnyReg
        }
        range.add_use(UsePosition(def, Def, constraint))
      }
      for use_entry in info.use_points {
        let (use_point, fixed) = use_entry
        let constraint = match fixed {
          Some(preg) => FixedReg(preg)
          None => AnyReg
        }
        range.add_use(UsePosition(use_point, Use, constraint))
      }

      // Build multi-span ranges (with holes) from per-block liveness plus use/def.
      // This is a conservative refinement over the previous single [start, end] span:
      // it never drops blocks where the value is live-in/out, but it avoids spanning
      // across blocks where the value is not live, reducing register pressure.
      use_blocks_scratch.clear()
      for use_entry in info.use_points {
        let p = use_entry.0
        let block_idx = p.block
        if use_block_epoch[block_idx] != epoch {
          use_block_epoch[block_idx] = epoch
          use_first_points[block_idx] = Some(p)
          use_last_points[block_idx] = Some(p)
          use_blocks_scratch.push(block_idx)
        } else {
          let first = use_first_points[block_idx].unwrap()
          let last = use_last_points[block_idx].unwrap()
          use_first_points[block_idx] = Some(
            pp_min(first, p, block_order, block_point_base),
          )
          use_last_points[block_idx] = Some(
            pp_max(last, p, block_order, block_point_base),
          )
        }
      }
      fn block_less(a : Int, b : Int, block_order : FixedArray[Int]) -> Bool {
        let ord_a = block_order[a]
        let ord_b = block_order[b]
        ord_a < ord_b || (ord_a == ord_b && a < b)
      }

      let live_in_blocks = if vreg_id >= 0 &&
        vreg_id < max_vreg_id &&
        vreg_live_in_blocks[vreg_id] is Some(blocks) {
        blocks
      } else {
        []
      }
      let live_out_blocks = if vreg_id >= 0 &&
        vreg_id < max_vreg_id &&
        vreg_live_out_blocks[vreg_id] is Some(blocks) {
        blocks
      } else {
        []
      }
      def_blocks_scratch.clear()
      if info.def_point is Some((def, _)) {
        def_blocks_scratch.push(def.block)
      }

      // Merge live-in/live-out/use/def block lists in block-order without
      // sorting the union every iteration (regalloc2-style list walk), and
      // emit per-block spans directly in one pass.
      let mut i_in = 0
      let mut i_out = 0
      let mut i_def = 0
      while i_in < live_in_blocks.length() ||
            i_out < live_out_blocks.length() ||
            i_def < def_blocks_scratch.length() {
        let mut next_block = -1
        if i_in < live_in_blocks.length() {
          next_block = live_in_blocks[i_in]
        }
        if i_out < live_out_blocks.length() {
          let candidate = live_out_blocks[i_out]
          if next_block < 0 || block_less(candidate, next_block, block_order) {
            next_block = candidate
          }
        }
        if i_def < def_blocks_scratch.length() {
          let candidate = def_blocks_scratch[i_def]
          if next_block < 0 || block_less(candidate, next_block, block_order) {
            next_block = candidate
          }
        }
        if next_block < 0 {
          break
        }
        let mut live_in = false
        while i_in < live_in_blocks.length() &&
              live_in_blocks[i_in] == next_block {
          live_in = true
          i_in = i_in + 1
        }
        let mut live_out = false
        while i_out < live_out_blocks.length() &&
              live_out_blocks[i_out] == next_block {
          live_out = true
          i_out = i_out + 1
        }
        let mut has_def_in_block = false
        while i_def < def_blocks_scratch.length() &&
              def_blocks_scratch[i_def] == next_block {
          has_def_in_block = true
          i_def = i_def + 1
        }
        let block_idx = next_block
        covered_block_epoch[block_idx] = epoch
        let block = func.blocks[block_idx]
        let use_bounds = if use_block_epoch[block_idx] == epoch {
          Some(
            (
              use_first_points[block_idx].unwrap(),
              use_last_points[block_idx].unwrap(),
            ),
          )
        } else {
          None
        }
        let block_len = block.insts.length()
        let entry_point : ProgPoint = {
          block: block_idx,
          inst: -1,
          pos: Before,
        }
        let exit_point : ProgPoint = {
          block: block_idx,
          inst: block_len,
          pos: After,
        }
        let start = if has_def_in_block {
          info.def_point.unwrap().0
        } else if live_in {
          entry_point
        } else if use_bounds is Some((first_use, _)) {
          // Should not normally happen (use implies live_in or local def),
          // but be conservative.
          pp_min(entry_point, first_use, block_order, block_point_base)
        } else {
          entry_point
        }
        let end = if live_out {
          exit_point
        } else if use_bounds is Some((_, last_use)) {
          point_after(last_use, block_len)
        } else if has_def_in_block {
          // Defined in this block and dead here: keep a minimal span to avoid
          // surprising empty ranges in allocator internals.
          point_after(info.def_point.unwrap().0, block_len)
        } else {
          point_after(start, block_len)
        }
        let end_ord = point_order(end, block_order, block_point_base)
        let start_ord = point_order(start, block_order, block_point_base)
        let (start, end) = if end_ord <= start_ord {
          (start, point_after(start, block_len))
        } else {
          (start, end)
        }
        range.add_range(ProgPointRange(start, end))
      }
      // Fallback for malformed/edge cases where a use block was not covered by
      // live-in/live-out/def (should not happen in SSA; kept for safety).
      for block_idx in use_blocks_scratch {
        if covered_block_epoch[block_idx] == epoch {
          continue
        }
        let block = func.blocks[block_idx]
        let block_len = block.insts.length()
        let use_bounds = (
          use_first_points[block_idx].unwrap(),
          use_last_points[block_idx].unwrap(),
        )
        let start = pp_min(
          { block: block_idx, inst: -1, pos: Before },
          use_bounds.0,
          block_order,
          block_point_base,
        )
        let end = point_after(use_bounds.1, block_len)
        range.add_range(ProgPointRange(start, end))
      }
      use_blocks_scratch.clear()

      // Compute call-crossing from normalized spans.
      // This is more precise than coarse def..end intervals (which can over-mark
      // values as call-crossing across dead holes between live spans).
      if !call_prog_point_orders.is_empty() {
        for span in range.ranges {
          let span_start_ord = point_order(
            span.start,
            block_order,
            block_point_base,
          )
          let span_end_ord = point_order(
            span.end,
            block_order,
            block_point_base,
          )
          let mut call_count = 0
          let mut foreign_call_count = 0
          let mut used_block_fast_path = false
          let start_block = span.start.block
          let end_block = span.end.block
          if start_block >= 0 &&
            start_block < num_blocks &&
            end_block >= 0 &&
            end_block < num_blocks {
            let start_block_ord = block_order[start_block]
            let end_block_ord = block_order[end_block]
            if start_block_ord <= end_block_ord {
              used_block_fast_path = true
              if start_block == end_block {
                call_count = count_call_points_in_range(
                  block_call_point_orders[start_block],
                  span_start_ord,
                  span_end_ord,
                )
                foreign_call_count = count_call_points_in_range(
                  block_foreign_call_point_orders[start_block],
                  span_start_ord,
                  span_end_ord,
                )
              } else {
                call_count = call_count +
                  count_call_points_in_range(
                    block_call_point_orders[start_block],
                    span_start_ord,
                    span_end_ord,
                  )
                foreign_call_count = foreign_call_count +
                  count_call_points_in_range(
                    block_foreign_call_point_orders[start_block],
                    span_start_ord,
                    span_end_ord,
                  )
                call_count = call_count +
                  count_call_points_in_range(
                    block_call_point_orders[end_block],
                    span_start_ord,
                    span_end_ord,
                  )
                foreign_call_count = foreign_call_count +
                  count_call_points_in_range(
                    block_foreign_call_point_orders[end_block],
                    span_start_ord,
                    span_end_ord,
                  )
                if end_block_ord - start_block_ord > 1 {
                  let mid_lo = start_block_ord + 1
                  let mid_hi = end_block_ord
                  call_count = call_count +
                    (
                      call_prefix_by_block_order[mid_hi] -
                      call_prefix_by_block_order[mid_lo]
                    )
                  foreign_call_count = foreign_call_count +
                    (
                      foreign_call_prefix_by_block_order[mid_hi] -
                      foreign_call_prefix_by_block_order[mid_lo]
                    )
                }
              }
            }
          }
          if !used_block_fast_path {
            let first_after_start = upper_bound_call_points(
              call_prog_point_orders, span_start_ord,
            )
            let first_at_or_after_end = lower_bound_call_points(
              call_prog_point_orders, span_end_ord,
            )
            if first_after_start < first_at_or_after_end {
              call_count = first_at_or_after_end - first_after_start
              foreign_call_count = foreign_call_prefix[first_at_or_after_end] -
                foreign_call_prefix[first_after_start]
            }
          }
          if call_count > 0 {
            range.crosses_call = true
            if foreign_call_count > 0 {
              range.crosses_foreign_call = true
            }
            if range.crosses_foreign_call {
              break
            }
          }
        }
        range.crosses_internal_call = range.crosses_call &&
          !range.crosses_foreign_call
      }
      result.add_range(range)
    }
  }
  // Build blockparam-out merge pairs once (regalloc2-style `blockparam_outs`):
  // each pair links `to` (block param) <- `from` (incoming edge arg).
  let mut max_block_id = -1
  for block in func.blocks {
    if block.id > max_block_id {
      max_block_id = block.id
    }
  }
  let block_idx_dense : Array[Int] = Array::make(max_block_id + 1, -1)
  for i, block in func.blocks {
    if block.id >= 0 && block.id < block_idx_dense.length() {
      block_idx_dense[block.id] = i
    }
  }
  // Keep blockparam-out pairs unique. Repeated (param,arg) edges are a no-op
  // for union-find coalescing but can amplify merge-phase cost on large CFGs.
  let seen_blockparam_pairs : Map[(Int, Int), Unit] = Map([])
  for pred_block in func.blocks {
    if pred_block.terminator is Some(Jump(target, args)) {
      if target < 0 || target >= block_idx_dense.length() {
        continue
      }
      let target_idx = block_idx_dense[target]
      if target_idx < 0 {
        continue
      }
      let target_block = func.blocks[target_idx]
      for i, param in target_block.params {
        if i >= args.length() {
          break
        }
        guard args[i] is Virtual(arg_vreg) else { continue }
        guard result.get_range_index_by_vreg(param.id) is Some(param_idx) else {
          continue
        }
        guard result.get_range_index_by_vreg(arg_vreg.id) is Some(arg_idx) else {
          continue
        }
        if arg_idx != param_idx {
          let pair = (param_idx, arg_idx)
          if seen_blockparam_pairs.get(pair) is None {
            seen_blockparam_pairs.set(pair, ())
            result.blockparam_out_pairs.push(pair)
          }
        }
      }
    }
  }
  result
}