///|
fn hot_mutate_type_id_is_valid(func : HotFunc, ty : TypeId) -> Bool {
  ty >= 0 && ty < func.types.length()
}

///|
fn hot_mutate_require_type_id(
  func : HotFunc,
  ty : TypeId,
  context : String,
) -> Unit {
  if !hot_mutate_type_id_is_valid(func, ty) {
    abort("\{context} must reference a valid interned TypeId")
  }
}

///|
fn hot_mutate_require_label_id(
  func : HotFunc,
  label_id : LabelId,
  context : String,
) -> Unit {
  if !hot_label_is_live(func, label_id) {
    abort("\{context} must reference a valid LabelId")
  }
}

///|
fn hot_mutate_require_live_node(
  func : HotFunc,
  node_id : NodeId,
  context : String,
) -> Unit {
  if !hot_node_is_live(func, node_id) {
    abort("\{context} must reference a live NodeId")
  }
}

///|
fn hot_mutate_child_ref_is_valid(func : HotFunc, child_id : NodeId) -> Bool {
  child_id == HOT_NONE_NODE || hot_node_is_live(func, child_id)
}

///|
fn hot_mutate_require_child_ref(func : HotFunc, child_id : NodeId) -> Unit {
  if !hot_mutate_child_ref_is_valid(func, child_id) {
    abort("hot child slots must reference live NodeIds or HOT_NONE_NODE")
  }
}

///|
fn hot_mutate_require_root_index(
  func : HotFunc,
  idx : Int32,
  allow_end~ : Bool = false,
) -> Unit {
  let upper = if allow_end {
    func.roots.length()
  } else {
    func.roots.length() - 1
  }
  if idx < 0 || idx > upper {
    abort("hot root index out of bounds")
  }
}

///|
fn hot_mutate_require_remove_count(
  length : Int32,
  idx : Int32,
  remove_count : Int32,
) -> Unit {
  if remove_count < 0 || idx + remove_count > length {
    abort("hot splice remove_count out of bounds")
  }
}

///|
fn hot_mutate_require_child_index(node : HotNode, idx : Int32) -> Unit {
  if idx < 0 || idx >= node.child_count {
    abort("hot child index out of bounds")
  }
}

///|
fn hot_mutate_require_node_shape(
  func : HotFunc,
  node : HotNode,
  context : String,
) -> Unit {
  hot_mutate_require_type_id(func, node.ty, context)
  if node.child_base < 0 ||
    node.child_count < 0 ||
    node.child_base + node.child_count > func.children.length() {
    abort("\{context} must reference an in-bounds child span")
  }
}

///|
fn hot_mutate_write_child_span(
  func : HotFunc,
  base : Int32,
  children : Array[NodeId],
) -> Unit {
  for i = 0; i < children.length(); i = i + 1 {
    hot_mutate_require_child_ref(func, children[i])
    func.children[base + i] = children[i]
  }
}

///|
fn hot_mutate_attach_child_span(
  func : HotFunc,
  node : HotNode,
  children : Array[NodeId],
  context : String,
) -> HotNode {
  hot_mutate_require_type_id(func, node.ty, context)
  if node.child_base != 0 || node.child_count != 0 {
    abort("\{context} requires a detached child span")
  }
  if children.length() == 0 {
    return node
  }
  let base = hot_alloc_child_span(func, children.length())
  hot_mutate_write_child_span(func, base, children)
  { ..node, child_base: base, child_count: children.length(), }
}

///|
fn hot_mutate_attach_child1(
  func : HotFunc,
  node : HotNode,
  child0 : NodeId,
  context : String,
) -> HotNode {
  hot_mutate_require_type_id(func, node.ty, context)
  if node.child_base != 0 || node.child_count != 0 {
    abort("\{context} requires a detached child span")
  }
  hot_mutate_require_child_ref(func, child0)
  let base = hot_alloc_child_span(func, 1)
  func.children[base] = child0
  { ..node, child_base: base, child_count: 1, }
}

///|
fn hot_mutate_attach_child2(
  func : HotFunc,
  node : HotNode,
  child0 : NodeId,
  child1 : NodeId,
  context : String,
) -> HotNode {
  hot_mutate_require_type_id(func, node.ty, context)
  if node.child_base != 0 || node.child_count != 0 {
    abort("\{context} requires a detached child span")
  }
  hot_mutate_require_child_ref(func, child0)
  hot_mutate_require_child_ref(func, child1)
  let base = hot_alloc_child_span(func, 2)
  func.children[base] = child0
  func.children[base + 1] = child1
  { ..node, child_base: base, child_count: 2, }
}

///|
fn hot_mutate_attach_child3(
  func : HotFunc,
  node : HotNode,
  child0 : NodeId,
  child1 : NodeId,
  child2 : NodeId,
  context : String,
) -> HotNode {
  hot_mutate_require_type_id(func, node.ty, context)
  if node.child_base != 0 || node.child_count != 0 {
    abort("\{context} requires a detached child span")
  }
  hot_mutate_require_child_ref(func, child0)
  hot_mutate_require_child_ref(func, child1)
  hot_mutate_require_child_ref(func, child2)
  let base = hot_alloc_child_span(func, 3)
  func.children[base] = child0
  func.children[base + 1] = child1
  func.children[base + 2] = child2
  { ..node, child_base: base, child_count: 3, }
}

///|
pub fn hot_revision_bump(func : HotFunc) -> Unit {
  func.revision += 1
}

///|
pub fn hot_append_body_local(func : HotFunc, ty : ValType) -> LocalId {
  let local_id = hot_local_count(func)
  func.locals.locals.push(ty)
  hot_revision_bump(func)
  local_id
}

///|
priv struct HotCatchPayloadLaneRepair {
  payload_id : NodeId
  parent_id : NodeId
  parent_slot : Int32
  val_type : ValType
}

///|
priv struct HotCatchPayloadRepair {
  catch_ref : HotRegionRef
  lanes : Array[HotCatchPayloadLaneRepair]
}

///|
fn hot_catch_payload_site_matches(
  site : HotUseSite,
  user : NodeId,
  slot : Int32,
  kind : HotUseSlotKind,
) -> Bool {
  site.user == user && site.slot == slot && site.kind == kind
}

///|
fn hot_catch_payload_lane_repair(
  func : HotFunc,
  use_def : HotUseDef,
  catch_ref : HotRegionRef,
  entry_slot : Int32,
  payload_id : NodeId,
  root_id : NodeId,
  root_user : NodeId,
  root_slot : Int32,
) -> HotCatchPayloadLaneRepair? {
  let payload = hot_node_get(func, payload_id)
  let payload_types = hot_type_results(func, payload.ty)
  if payload.op != HotOp::Catch ||
    payload.child_count != 0 ||
    payload_types.length() != 1 {
    return None
  }

  let mut current = root_id
  let mut expected_user = root_user
  let mut expected_slot = root_slot
  let mut expected_kind = HotUseSlotKind::RootUseSlot
  let mut parent_id = HOT_NONE_NODE
  let mut parent_slot = -1
  while current != payload_id {
    if current < 0 || !hot_node_is_live(func, current) {
      return None
    }
    let sites = node_use_sites_borrowed(use_def, current)
    if sites.length() != 1 ||
      !hot_catch_payload_site_matches(
        sites[0],
        expected_user,
        expected_slot,
        expected_kind,
      ) {
      return None
    }
    let node = hot_node_get(func, current)
    match node.op {
      HotOp::Block => {
        let body_ref = hot_region_body_ref(func, current)
        if hot_region_root_count(func, body_ref) == 0 {
          return None
        }
        expected_user = current
        expected_slot = 0
        expected_kind = HotUseSlotKind::RootUseSlot
        current = hot_region_get(func, body_ref, 0)
      }
      HotOp::If => {
        parent_id = current
        parent_slot = 0
        expected_user = current
        expected_slot = 0
        expected_kind = HotUseSlotKind::ChildUseSlot
        current = hot_child_get(func, current, 0)
      }
      HotOp::Loop | HotOp::Try | HotOp::TryTable => return None
      _ => {
        if node.child_count == 0 {
          return None
        }
        parent_id = current
        parent_slot = 0
        expected_user = current
        expected_slot = 0
        expected_kind = HotUseSlotKind::ChildUseSlot
        current = hot_child_get(func, current, 0)
      }
    }
  }
  if parent_id == HOT_NONE_NODE {
    return None
  }

  let payload_sites = node_use_sites_borrowed(use_def, payload_id)
  if payload_sites.length() != 2 {
    return None
  }
  let mut saw_entry = false
  let mut saw_nested = false
  for site in payload_sites {
    if hot_catch_payload_site_matches(
        site,
        catch_ref.holder,
        entry_slot,
        HotUseSlotKind::RootUseSlot,
      ) {
      saw_entry = true
    } else if hot_catch_payload_site_matches(
        site, expected_user, expected_slot, expected_kind,
      ) {
      saw_nested = true
    } else {
      return None
    }
  }
  if !saw_entry || !saw_nested {
    return None
  }
  Some({ payload_id, parent_id, parent_slot, val_type: payload_types[0], })
}

///|
fn hot_scalar_catch_payload_first_descendant_repair(
  func : HotFunc,
  use_def : HotUseDef,
  catch_ref : HotRegionRef,
  payload_id : NodeId,
) -> HotCatchPayloadLaneRepair? {
  if hot_region_root_count(func, catch_ref) < 2 ||
    hot_region_get(func, catch_ref, 0) != payload_id {
    return None
  }
  hot_catch_payload_lane_repair(
    func,
    use_def,
    catch_ref,
    0,
    payload_id,
    hot_region_get(func, catch_ref, 1),
    catch_ref.holder,
    1,
  )
}

///|
fn hot_grouped_catch_payload_repairs(
  func : HotFunc,
  use_def : HotUseDef,
  catch_ref : HotRegionRef,
  payloads : Array[NodeId],
  payload_start : Int32,
  root_id : NodeId,
  root_user : NodeId,
  root_slot : Int32,
) -> Array[HotCatchPayloadLaneRepair]? {
  let mut current = root_id
  let mut expected_user = root_user
  let mut expected_slot = root_slot
  while true {
    if current < 0 || !hot_node_is_live(func, current) {
      return None
    }
    let sites = node_use_sites_borrowed(use_def, current)
    if sites.length() != 1 ||
      !hot_catch_payload_site_matches(
        sites[0],
        expected_user,
        expected_slot,
        HotUseSlotKind::RootUseSlot,
      ) {
      return None
    }
    if hot_node_get(func, current).op != HotOp::Block {
      return None
    }
    let body_ref = hot_region_body_ref(func, current)
    let root_count = hot_region_root_count(func, body_ref)
    if root_count == 0 {
      return None
    }
    let first_root = hot_region_get(func, body_ref, 0)
    if root_count == 1 && hot_node_get(func, first_root).op == HotOp::Block {
      expected_user = current
      expected_slot = 0
      current = first_root
      continue
    }
    let repairs : Array[HotCatchPayloadLaneRepair] = []
    let mut lane_slot = payload_start
    let mut root_slot = 0
    while lane_slot < payloads.length() && root_slot < root_count {
      let candidate_slot = root_slot
      root_slot += 1
      match
        hot_catch_payload_lane_repair(
          func,
          use_def,
          catch_ref,
          lane_slot,
          payloads[lane_slot],
          hot_region_get(func, body_ref, candidate_slot),
          body_ref.holder,
          candidate_slot,
        ) {
        Some(repair) => {
          repairs.push(repair)
          lane_slot += 1
        }
        None => ()
      }
    }
    return if repairs.is_empty() { None } else { Some(repairs) }
  }
  None
}

///|
fn hot_ordered_catch_payload_repair(
  func : HotFunc,
  use_def : HotUseDef,
  catch_ref : HotRegionRef,
  payloads : Array[NodeId],
) -> HotCatchPayloadRepair? {
  let catch_root_count = hot_region_root_count(func, catch_ref)
  if payloads.length() < 2 || catch_root_count < payloads.length() + 1 {
    return None
  }
  let first_tag = hot_node_get(func, payloads[0]).imm0
  for slot = 0; slot < payloads.length(); slot = slot + 1 {
    let payload_id = payloads[slot]
    let payload = hot_node_get(func, payload_id)
    if hot_region_get(func, catch_ref, slot) != payload_id ||
      payload.op != HotOp::Catch ||
      payload.imm0 != first_tag {
      return None
    }
  }

  let lanes : Array[HotCatchPayloadLaneRepair] = []
  if catch_root_count == payloads.length() + 1 {
    let mut final_ref = catch_ref
    let mut current = hot_region_get(func, catch_ref, payloads.length())
    let mut expected_user = catch_ref.holder
    let mut expected_slot = payloads.length()
    while true {
      if current < 0 || !hot_node_is_live(func, current) {
        return None
      }
      let sites = node_use_sites_borrowed(use_def, current)
      if sites.length() != 1 ||
        !hot_catch_payload_site_matches(
          sites[0],
          expected_user,
          expected_slot,
          HotUseSlotKind::RootUseSlot,
        ) {
        return None
      }
      if hot_node_get(func, current).op != HotOp::Block {
        return None
      }
      let body_ref = hot_region_body_ref(func, current)
      let root_count = hot_region_root_count(func, body_ref)
      if root_count == 0 {
        return None
      }
      let first_root = hot_region_get(func, body_ref, 0)
      if root_count == 1 && hot_node_get(func, first_root).op == HotOp::Block {
        expected_user = current
        expected_slot = 0
        current = first_root
        continue
      }
      if root_count < payloads.length() {
        return None
      }
      final_ref = body_ref
      break
    }
    for slot = 0; slot < payloads.length(); slot = slot + 1 {
      let lane = match
        hot_catch_payload_lane_repair(
          func,
          use_def,
          catch_ref,
          slot,
          payloads[slot],
          hot_region_get(func, final_ref, slot),
          final_ref.holder,
          slot,
        ) {
        Some(plan) => plan
        None => return None
      }
      lanes.push(lane)
    }
  } else {
    let mut root_slot = payloads.length()
    let mut lane_slot = 0
    while lane_slot < payloads.length() {
      let mut found = false
      while root_slot < catch_root_count {
        let candidate_slot = root_slot
        root_slot += 1
        let candidate = hot_region_get(func, catch_ref, candidate_slot)
        match
          hot_grouped_catch_payload_repairs(
            func,
            use_def,
            catch_ref,
            payloads,
            lane_slot,
            candidate,
            catch_ref.holder,
            candidate_slot,
          ) {
          Some(grouped) => {
            lane_slot += grouped.length()
            for repair in grouped {
              lanes.push(repair)
            }
            found = true
            break
          }
          None => ()
        }
        match
          hot_catch_payload_lane_repair(
            func,
            use_def,
            catch_ref,
            lane_slot,
            payloads[lane_slot],
            candidate,
            catch_ref.holder,
            candidate_slot,
          ) {
          Some(repair) => {
            lanes.push(repair)
            lane_slot += 1
            found = true
            break
          }
          None => ()
        }
      }
      if !found {
        return None
      }
    }
  }
  Some({ catch_ref, lanes, })
}

///|
fn hot_catch_payload_repair_plan(
  func : HotFunc,
  max_lanes : Int32,
  use_def_builds~ : Array[Int32]? = None,
) -> Array[HotCatchPayloadRepair]? {
  let plans : Array[HotCatchPayloadRepair] = []
  let mut payload_count = 0
  for node_id = 0; node_id < hot_node_count(func); node_id = node_id + 1 {
    if !hot_node_is_live(func, node_id) {
      continue
    }
    match hot_node_get(func, node_id).op {
      HotOp::Catch => payload_count += 1
      HotOp::CatchAll => return None
      _ => ()
    }
  }
  if payload_count == 0 {
    return Some(plans)
  }
  // A use graph is needed only when typed payloads require capture planning.
  match use_def_builds {
    Some(builds) => builds[0] += 1
    None => ()
  }
  let use_def = use_def_build_node_uses(func)
  let mut planned_payloads = 0
  for try_id = 0; try_id < hot_node_count(func); try_id = try_id + 1 {
    if !hot_node_is_live(func, try_id) ||
      hot_node_get(func, try_id).op != HotOp::Try {
      continue
    }
    let catch_ref = match hot_region_catch_ref(func, try_id) {
      Some(catch_ref) => catch_ref
      None => continue
    }
    let root_count = hot_region_root_count(func, catch_ref)
    let mut lane_count = 0
    while lane_count < root_count &&
          hot_node_get(func, hot_region_get(func, catch_ref, lane_count)).op ==
          HotOp::Catch {
      lane_count += 1
    }
    if lane_count == 0 {
      continue
    }
    if max_lanes >= 0 && lane_count > max_lanes {
      return None
    }
    if lane_count == 1 {
      let payload_id = hot_region_get(func, catch_ref, 0)
      let lane = match
        hot_scalar_catch_payload_first_descendant_repair(
          func, use_def, catch_ref, payload_id,
        ) {
        Some(lane) => lane
        None => return None
      }
      plans.push({ catch_ref, lanes: [lane], })
      planned_payloads += 1
    } else {
      let payloads : Array[NodeId] = []
      for slot = 0; slot < lane_count; slot = slot + 1 {
        payloads.push(hot_region_get(func, catch_ref, slot))
      }
      let plan = match
        hot_ordered_catch_payload_repair(func, use_def, catch_ref, payloads) {
        Some(plan) => plan
        None => return None
      }
      plans.push(plan)
      planned_payloads += lane_count
    }
  }
  if planned_payloads != payload_count {
    return None
  }
  Some(plans)
}

///|
fn hot_repair_catch_payloads(func : HotFunc, max_lanes : Int32) -> Bool {
  let plans = match hot_catch_payload_repair_plan(func, max_lanes) {
    Some(plans) => plans
    None => return false
  }
  if plans.is_empty() {
    return false
  }
  for plan in plans {
    let captures : Array[NodeId] = []
    for lane in plan.lanes {
      let local_id = hot_append_body_local(func, lane.val_type)
      captures.push(hot_build_local_set(func, local_id, lane.payload_id))
      let replacement = hot_build_local_get(func, local_id)
      hot_child_set(func, lane.parent_id, lane.parent_slot, replacement)
    }
    let stack_ordered_captures : Array[NodeId] = []
    for idx = captures.length() - 1; idx >= 0; idx = idx - 1 {
      stack_ordered_captures.push(captures[idx])
    }
    hot_region_insert(
      func,
      plan.catch_ref,
      plan.lanes.length(),
      stack_ordered_captures,
    )
  }
  true
}

///|
/// Report whether every typed legacy catch payload in the function can be
/// captured exactly once on its catch region's first-descendant line.
pub fn hot_scalar_catch_payloads_repairable(func : HotFunc) -> Bool {
  hot_catch_payload_repair_plan(func, 1) is Some(_)
}

///|
/// Capture every repairable scalar legacy catch payload into a typed local at
/// catch entry and replace its exact nested first-descendant use. The complete
/// function is preflighted before any local, node, root, child, or revision
/// mutation; unsupported multiple payloads, nested catches, loops, sharing, or
/// outside uses leave the function unchanged.
pub fn hot_repair_scalar_catch_payloads(func : HotFunc) -> Bool {
  hot_repair_catch_payloads(func, 1)
}

///|
/// Report whether every typed legacy catch payload is either the exact scalar
/// first-descendant family or one ordered same-tag lane vector under an
/// exclusive direct block chain. Catch-all markers, loops, nested catches,
/// sharing, outside uses, or any partial lane reject the complete function.
pub fn hot_ordered_catch_payloads_repairable(func : HotFunc) -> Bool {
  hot_catch_payload_repair_plan(func, -1) is Some(_)
}

///|
/// Capture exact scalar or ordered same-tag legacy catch payloads atomically.
/// Lane locals retain source order; entry captures consume the handler stack in
/// reverse lane order before exact old-position reads replace every payload use.
pub fn hot_repair_ordered_catch_payloads(func : HotFunc) -> Bool {
  hot_repair_catch_payloads(func, -1)
}

///|
pub fn hot_set_body_local_type(
  func : HotFunc,
  local_id : LocalId,
  ty : ValType,
) -> Bool {
  let body_local_id = local_id - hot_param_count(func)
  if body_local_id < 0 || body_local_id >= func.locals.locals.length() {
    return false
  }
  if func.locals.locals[body_local_id] == ty {
    return false
  }
  func.locals.locals[body_local_id] = ty
  hot_revision_bump(func)
  true
}

///|
pub fn hot_weaken_body_local_to_nullable_ref(
  func : HotFunc,
  local_id : LocalId,
) -> Bool {
  let current = match hot_local_type(func, local_id) {
    Some(ty) => ty
    None => return false
  }
  let ref_type = match current {
    RefTypeValType(rt) if rt.is_non_nullable() => rt
    _ => return false
  }
  hot_set_body_local_type(
    func,
    local_id,
    ValType::ref_type(
      RefType::new(true, ref_type.get_heap_type(), exact=ref_type.is_exact()),
    ),
  )
}

///|
pub fn hot_root_count(func : HotFunc) -> Int32 {
  func.roots.length()
}

///|
pub fn hot_root_get(func : HotFunc, idx : Int32) -> NodeId {
  hot_mutate_require_root_index(func, idx)
  func.roots[idx]
}

///|
pub fn hot_root_set(func : HotFunc, idx : Int32, id : NodeId) -> Unit {
  hot_mutate_require_root_index(func, idx)
  hot_mutate_require_live_node(func, id, "hot root slots")
  func.roots[idx] = id
  hot_revision_bump(func)
}

///|
pub fn hot_root_append(func : HotFunc, id : NodeId) -> Unit {
  hot_mutate_require_live_node(func, id, "hot root slots")
  func.roots.push(id)
  hot_revision_bump(func)
}

///|
pub fn hot_append_root(func : HotFunc, id : NodeId) -> Unit {
  hot_root_append(func, id)
}

///|
fn hot_mutate_root_insert_no_bump(
  func : HotFunc,
  idx : Int32,
  id : NodeId,
) -> Unit {
  func.roots.push(id)
  let mut i = func.roots.length() - 1
  while i > idx {
    func.roots[i] = func.roots[i - 1]
    i = i - 1
  }
  func.roots[idx] = id
}

///|
pub fn hot_root_insert(func : HotFunc, idx : Int32, id : NodeId) -> Unit {
  hot_mutate_require_root_index(func, idx, allow_end=true)
  hot_mutate_require_live_node(func, id, "hot root slots")
  hot_mutate_root_insert_no_bump(func, idx, id)
  hot_revision_bump(func)
}

///|
fn hot_mutate_root_remove_no_bump(func : HotFunc, idx : Int32) -> NodeId {
  let removed = func.roots[idx]
  let last = func.roots.length() - 1
  for i = idx; i < last; i = i + 1 {
    func.roots[i] = func.roots[i + 1]
  }
  ignore(func.roots.pop())
  removed
}

///|
pub fn hot_root_remove(func : HotFunc, idx : Int32) -> NodeId {
  hot_mutate_require_root_index(func, idx)
  let removed = hot_mutate_root_remove_no_bump(func, idx)
  hot_revision_bump(func)
  removed
}

///|
pub fn hot_root_splice(
  func : HotFunc,
  idx : Int32,
  remove_count : Int32,
  new_roots : Array[NodeId],
) -> Array[NodeId] {
  hot_mutate_require_root_index(func, idx, allow_end=true)
  hot_mutate_require_remove_count(func.roots.length(), idx, remove_count)
  for root in new_roots {
    hot_mutate_require_live_node(func, root, "hot root slots")
  }
  // Callers may supply the root array itself. Freeze that input before moving
  // slots, then shift the retained suffix once rather than once per edit.
  let new_roots = if physical_equal(new_roots, func.roots) {
    new_roots.copy()
  } else {
    new_roots
  }
  let old_length = func.roots.length()
  let delta = new_roots.length() - remove_count
  let removed : Array[NodeId] = []
  for i = idx; i < idx + remove_count; i = i + 1 {
    removed.push(func.roots[i])
  }
  if delta > 0 {
    for _ in 0..= idx + remove_count; slot = slot - 1 {
      func.roots[slot + delta] = func.roots[slot]
    }
  } else if delta < 0 {
    for slot = idx + remove_count; slot < old_length; slot = slot + 1 {
      func.roots[slot + delta] = func.roots[slot]
    }
    for _ in 0..<-delta {
      ignore(func.roots.pop())
    }
  }
  for slot = 0; slot < new_roots.length(); slot = slot + 1 {
    func.roots[idx + slot] = new_roots[slot]
  }
  hot_revision_bump(func)
  removed
}

///|
pub fn hot_child_count(func : HotFunc, id : NodeId) -> Int32 {
  if !hot_node_read_is_proven_live(func, id) {
    hot_mutate_require_live_node(func, id, "hot child_count")
  }
  func.nodes[id].child_count
}

///|
pub fn hot_child_get(func : HotFunc, id : NodeId, idx : Int32) -> NodeId {
  hot_node_require_live_read(func, id)
  if idx < 0 || idx >= func.nodes[id].child_count {
    abort("hot child index out of bounds")
  }
  func.children[func.nodes[id].child_base + idx]
}

///|
pub fn hot_child_set(
  func : HotFunc,
  id : NodeId,
  idx : Int32,
  child : NodeId,
) -> Unit {
  let node = hot_node_get(func, id)
  hot_mutate_require_child_index(node, idx)
  hot_mutate_require_child_ref(func, child)
  func.children[node.child_base + idx] = child
  hot_revision_bump(func)
}

///|
pub fn hot_alloc_child_span(func : HotFunc, count : Int32) -> Int32 {
  if count < 0 {
    abort("hot child span count must be non-negative")
  }
  let base = func.children.length()
  for _ in 0.. Int32 {
  hot_alloc_child_span(func, count)
}

///|
pub struct HotChildSpanRewrite {
  node_id : NodeId
  new_children : Array[NodeId]
} derive(Eq, Debug)

///|
pub fn HotChildSpanRewrite::new(
  node_id : NodeId,
  new_children : Array[NodeId],
) -> HotChildSpanRewrite {
  { node_id, new_children, }
}

///|
pub fn hot_replace_child_span(
  func : HotFunc,
  id : NodeId,
  new_children : Array[NodeId],
) -> Array[NodeId] {
  let node = hot_node_get(func, id)
  let removed : Array[NodeId] = []
  for i = 0; i < node.child_count; i = i + 1 {
    removed.push(func.children[node.child_base + i])
  }
  let base = hot_alloc_child_span(func, new_children.length())
  hot_mutate_write_child_span(func, base, new_children)
  func.nodes[id] = {
    ..node,
    child_base: base,
    child_count: new_children.length(),
  }
  hot_revision_bump(func)
  removed
}

///|
pub fn hot_control_result_type_set(
  func : HotFunc,
  control_id : NodeId,
  ty : TypeId,
) -> Unit {
  hot_mutate_require_live_node(func, control_id, "hot_control_result_type_set")
  hot_mutate_require_type_id(
    func, ty, "hot_control_result_type_set result type",
  )
  let node = hot_node_get(func, control_id)
  let label_id = hot_control_node_label(func, control_id)
  hot_mutate_require_label_id(
    func, label_id, "hot_control_result_type_set control label",
  )
  let result_arity = hot_type_result_arity(func, ty)
  func.nodes[control_id] = { ..node, ty, }
  let label = hot_label_get(func, label_id)
  let branch_arity = match node.op {
    HotOp::Loop => label.branch_arity
    _ => result_arity
  }
  func.labels[label_id] = { ..label, result_type: ty, branch_arity, }
  for slot_info in hot_control_region_slot_info(node.op) {
    let holder = match slot_info.role {
      HotControlRegionRole::BodyRegion =>
        match node.op {
          HotOp::Block | HotOp::Loop => control_id
          _ =>
            if slot_info.slot >= node.child_count {
              HOT_NONE_NODE
            } else {
              hot_child_get(func, control_id, slot_info.slot)
            }
        }
      _ =>
        if slot_info.slot >= node.child_count {
          HOT_NONE_NODE
        } else {
          hot_child_get(func, control_id, slot_info.slot)
        }
    }
    if holder == HOT_NONE_NODE {
      continue
    }
    let region_label_id = hot_control_node_label(func, holder)
    hot_mutate_require_label_id(
      func, region_label_id, "hot_control_result_type_set region label",
    )
    let region_label = hot_label_get(func, region_label_id)
    let region_branch_arity = if node.op == HotOp::Loop &&
      region_label_id == label_id {
      // A loop's own label branch arity describes loop-entry parameters, not
      // fallthrough results. Changing a result-only loop from void to a value
      // type must not reinterpret its first body child as an entry operand.
      label.branch_arity
    } else {
      result_arity
    }
    func.labels[region_label_id] = {
      ..region_label,
      branch_arity: region_branch_arity,
    }
  }
  hot_revision_bump(func)
}

///|
pub fn hot_loop_entry_values_clear(
  func : HotFunc,
  loop_id : NodeId,
) -> Array[NodeId] {
  hot_mutate_require_live_node(func, loop_id, "hot_loop_entry_values_clear")
  let loop_node = hot_node_get(func, loop_id)
  if loop_node.op != HotOp::Loop {
    abort("hot_loop_entry_values_clear requires a loop node")
  }
  let label_id = hot_control_node_label(func, loop_id)
  hot_mutate_require_label_id(
    func, label_id, "hot_loop_entry_values_clear loop label",
  )
  let label = hot_label_get(func, label_id)
  let input_count = label.branch_arity
  if input_count < 0 || input_count > loop_node.child_count {
    abort("hot_loop_entry_values_clear requires a valid loop input prefix")
  }
  let removed : Array[NodeId] = []
  let body : Array[NodeId] = []
  for slot = 0; slot < loop_node.child_count; slot = slot + 1 {
    let child_id = hot_child_get(func, loop_id, slot)
    if slot < input_count {
      removed.push(child_id)
    } else {
      body.push(child_id)
    }
  }
  ignore(hot_replace_child_span(func, loop_id, body))
  func.labels[label_id] = { ..label, branch_arity: 0, }
  hot_revision_bump(func)
  removed
}

///|
fn hot_mutate_child_spans_control_preflight(
  func : HotFunc,
  control_id : NodeId,
  rewrites : Array[HotChildSpanRewrite],
) -> Bool {
  let node = hot_node_get(func, control_id)
  if !hot_label_is_live(func, hot_control_node_label(func, control_id)) {
    return false
  }
  let mut planned_children : Array[NodeId]? = None
  for rewrite in rewrites {
    if rewrite.node_id == control_id {
      planned_children = Some(rewrite.new_children)
    }
  }
  let child_count = match planned_children {
    Some(children) => children.length()
    None => node.child_count
  }
  for slot_info in hot_control_region_slot_info(node.op) {
    let holder = match slot_info.role {
      HotControlRegionRole::BodyRegion if node.op is HotOp::Block ||
        node.op is HotOp::Loop => control_id
      _ =>
        if slot_info.slot >= child_count {
          HOT_NONE_NODE
        } else {
          match planned_children {
            Some(children) => children[slot_info.slot]
            None => hot_child_get(func, control_id, slot_info.slot)
          }
        }
    }
    if holder != HOT_NONE_NODE &&
      !hot_label_is_live(func, hot_control_node_label(func, holder)) {
      return false
    }
  }
  true
}

///|
pub fn hot_replace_child_spans_and_control_result_type(
  func : HotFunc,
  control_id : NodeId,
  ty : TypeId,
  rewrites : Array[HotChildSpanRewrite],
) -> Unit {
  hot_mutate_require_live_node(
    func, control_id, "hot_replace_child_spans_and_control_result_type",
  )
  hot_mutate_require_type_id(
    func, ty, "hot_replace_child_spans_and_control_result_type result type",
  )
  for rewrite in rewrites {
    hot_mutate_require_live_node(
      func,
      rewrite.node_id,
      "hot_replace_child_spans_and_control_result_type child rewrite",
    )
    for child_id in rewrite.new_children {
      hot_mutate_require_child_ref(func, child_id)
    }
  }
  if !hot_mutate_child_spans_control_preflight(func, control_id, rewrites) {
    abort(
      "hot_replace_child_spans_and_control_result_type requires live control labels",
    )
  }
  for rewrite in rewrites {
    let node = hot_node_get(func, rewrite.node_id)
    let base = hot_alloc_child_span(func, rewrite.new_children.length())
    hot_mutate_write_child_span(func, base, rewrite.new_children)
    func.nodes[rewrite.node_id] = {
      ..node,
      child_base: base,
      child_count: rewrite.new_children.length(),
    }
  }

  let node = hot_node_get(func, control_id)
  let label_id = hot_control_node_label(func, control_id)
  hot_mutate_require_label_id(
    func, label_id, "hot_replace_child_spans_and_control_result_type control label",
  )
  let result_arity = hot_type_result_arity(func, ty)
  func.nodes[control_id] = { ..node, ty, }
  let label = hot_label_get(func, label_id)
  let branch_arity = match node.op {
    HotOp::Loop => label.branch_arity
    _ => result_arity
  }
  func.labels[label_id] = { ..label, result_type: ty, branch_arity, }
  for slot_info in hot_control_region_slot_info(node.op) {
    let holder = match slot_info.role {
      HotControlRegionRole::BodyRegion =>
        match node.op {
          HotOp::Block | HotOp::Loop => control_id
          _ =>
            if slot_info.slot >= node.child_count {
              HOT_NONE_NODE
            } else {
              hot_child_get(func, control_id, slot_info.slot)
            }
        }
      _ =>
        if slot_info.slot >= node.child_count {
          HOT_NONE_NODE
        } else {
          hot_child_get(func, control_id, slot_info.slot)
        }
    }
    if holder == HOT_NONE_NODE {
      continue
    }
    let region_label_id = hot_control_node_label(func, holder)
    hot_mutate_require_label_id(
      func, region_label_id, "hot_replace_child_spans_and_control_result_type region label",
    )
    let region_label = hot_label_get(func, region_label_id)
    let region_branch_arity = if node.op == HotOp::Loop &&
      region_label_id == label_id {
      // A loop branches with entry parameters, not fallthrough results.
      label.branch_arity
    } else {
      result_arity
    }
    func.labels[region_label_id] = {
      ..region_label,
      branch_arity: region_branch_arity,
    }
  }
  hot_revision_bump(func)
}

///|
pub fn hot_control_demote_loop_to_block(
  func : HotFunc,
  loop_id : NodeId,
) -> Unit {
  hot_mutate_require_live_node(
    func, loop_id, "hot_control_demote_loop_to_block",
  )
  let node = hot_node_get(func, loop_id)
  if node.op != HotOp::Loop {
    abort("hot_control_demote_loop_to_block requires a loop node")
  }
  let label_id = hot_control_node_label(func, loop_id)
  hot_mutate_require_label_id(
    func, label_id, "hot_control_demote_loop_to_block loop label",
  )
  let label = hot_label_get(func, label_id)
  func.nodes[loop_id] = {
    ..node,
    op: HotOp::Block,
    flags: hot_default_flags_for_op(HotOp::Block),
  }
  func.labels[label_id] = { ..label, kind: HotLabelKind::BlockLabel, }
  hot_revision_bump(func)
}

///|
pub fn hot_control_demote_if_to_block(
  func : HotFunc,
  if_id : NodeId,
  new_children : Array[NodeId],
) -> Unit {
  hot_mutate_require_live_node(func, if_id, "hot_control_demote_if_to_block")
  let node = hot_node_get(func, if_id)
  if node.op != HotOp::If {
    abort("hot_control_demote_if_to_block requires an if node")
  }
  let label_id = hot_control_node_label(func, if_id)
  hot_mutate_require_label_id(
    func, label_id, "hot_control_demote_if_to_block if label",
  )
  let entry_count = node.child_count - 3
  let children = if entry_count == 0 {
    new_children
  } else {
    let children : Array[NodeId] = []
    for slot in 3.. Unit {
  hot_mutate_require_live_node(func, node_id, "hot_node_promote_to_block")
  let ty = hot_type_intern_block_result(func, block_type)
  ignore(hot_replace_child_span(func, node_id, new_children))
  let label_id = hot_label_alloc(
    func,
    HotLabelKind::BlockLabel,
    ty,
    node_id,
    hot_type_result_arity(func, ty),
  )
  let node = hot_node_get(func, node_id)
  func.nodes[node_id] = {
    ..node,
    op: HotOp::Block,
    ty,
    flags: hot_default_flags_for_op(HotOp::Block),
    imm0: label_id,
    imm1: 0,
  }
  hot_revision_bump(func)
}

///|
pub fn hot_tuple_make_promote_to_block(
  func : HotFunc,
  tuple_make_id : NodeId,
  block_type : BlockType,
) -> Unit {
  hot_mutate_require_live_node(
    func, tuple_make_id, "hot_tuple_make_promote_to_block",
  )
  let node = hot_node_get(func, tuple_make_id)
  if node.op != HotOp::TupleMake {
    abort("hot_tuple_make_promote_to_block requires a tuple.make node")
  }
  let results = hot_type_results(func, node.ty)
  if results.length() <= 1 {
    abort("hot_tuple_make_promote_to_block requires a multi-result tuple.make")
  }
  let ty = match block_type {
    TypeIdxBlockType(_) =>
      hot_type_intern_resolved_block_result(func, block_type, [], results)
    _ => hot_type_intern_block_result(func, block_type)
  }
  let label_id = hot_label_alloc(
    func,
    HotLabelKind::BlockLabel,
    ty,
    tuple_make_id,
    results.length(),
  )
  func.nodes[tuple_make_id] = {
    ..node,
    op: HotOp::Block,
    ty,
    flags: hot_default_flags_for_op(HotOp::Block),
    imm0: label_id,
    imm1: 0,
  }
  hot_revision_bump(func)
}

///|
pub fn hot_node_get(func : HotFunc, id : NodeId) -> HotNode {
  hot_node_get_impl(func, id, None)
}

///|
#inline
fn hot_node_get_impl(
  func : HotFunc,
  id : NodeId,
  fallback_checks : Array[Int32]?,
) -> HotNode {
  if !hot_node_read_is_proven_live(func, id) {
    match fallback_checks {
      Some(checks) => checks[0] += 1
      None => ()
    }
    hot_mutate_require_live_node(func, id, "hot node_get")
  }
  func.nodes[id]
}

///|
// Field reads share the same checked admission as complete node reads without
// materializing a HotNode return value at the package boundary.
#inline
fn hot_node_read_is_proven_live(func : HotFunc, id : NodeId) -> Bool {
  if id < 0 || id >= func.nodes.length() {
    false
  } else if func.free_nodes.is_empty() {
    true
  } else if id < func.deleted_nodes.length() {
    !func.deleted_nodes[id]
  } else {
    false
  }
}

///|
#inline
fn hot_node_require_live_read(func : HotFunc, id : NodeId) -> Unit {
  if !hot_node_read_is_proven_live(func, id) {
    hot_mutate_require_live_node(func, id, "hot node_get")
  }
}

///|
pub fn hot_node_op(func : HotFunc, id : NodeId) -> HotOp {
  hot_node_require_live_read(func, id)
  func.nodes[id].op
}

///|
pub fn hot_node_set(func : HotFunc, id : NodeId, node : HotNode) -> Unit {
  hot_mutate_require_live_node(func, id, "hot node_set")
  hot_mutate_require_node_shape(func, node, "hot node_set")
  func.nodes[id] = {
    ..node,
    flags: hot_flags_for_exact_payload(func, node.op, node.imm0),
    order: if node.order < func.nodes[id].order {
      func.nodes[id].order
    } else {
      node.order
    },
  }
  hot_revision_bump(func)
}

///|
pub fn hot_label_owner_set(
  func : HotFunc,
  label_id : LabelId,
  owner : NodeId,
) -> Unit {
  hot_mutate_require_label_id(func, label_id, "hot label_owner_set label")
  hot_mutate_require_live_node(func, owner, "hot label_owner_set owner")
  let label = hot_label_get(func, label_id)
  func.labels[label_id] = { ..label, owner, }
  hot_revision_bump(func)
}

///|
pub fn hot_local_node_id_set(
  func : HotFunc,
  node_id : NodeId,
  local_id : LocalId,
) -> Unit {
  hot_mutate_require_live_node(func, node_id, "hot local_node_id_set node")
  if hot_local_type(func, local_id) is None {
    abort("hot local_node_id_set requires a valid local id")
  }
  let node = hot_node_get(func, node_id)
  match node.op {
    HotOp::LocalGet | HotOp::LocalSet | HotOp::LocalTee =>
      hot_node_set(func, node_id, { ..node, imm0: local_id, })
    _ =>
      abort("hot local_node_id_set requires local.get, local.set, or local.tee")
  }
}

///|
pub fn hot_branch_target_set(
  func : HotFunc,
  node_id : NodeId,
  target : LabelId,
) -> Unit {
  hot_mutate_require_live_node(func, node_id, "hot branch_target_set node")
  hot_mutate_require_label_id(func, target, "hot branch_target_set target")
  let node = hot_node_get(func, node_id)
  match node.op {
    HotOp::Br
    | HotOp::BrIf
    | HotOp::BrOnNull
    | HotOp::BrOnNonNull
    | HotOp::BrOnCast
    | HotOp::BrOnCastFail
    | HotOp::Delegate => {
      func.nodes[node_id] = { ..node, imm0: target, }
      hot_revision_bump(func)
    }
    _ => abort("hot branch_target_set requires a single-target branch node")
  }
}

///|
pub fn hot_branch_table_set(
  func : HotFunc,
  node_id : NodeId,
  targets : Array[LabelId],
  default_target : LabelId,
) -> Unit {
  hot_mutate_require_live_node(func, node_id, "hot branch_table_set node")
  for target in targets {
    hot_mutate_require_label_id(func, target, "hot branch_table_set target")
  }
  hot_mutate_require_label_id(
    func, default_target, "hot branch_table_set default target",
  )
  let node = hot_node_get(func, node_id)
  match node.op {
    HotOp::BrTable => {
      let table_id = hot_alloc_branch_table(
        func,
        HotBranchTable::new(targets, default_target),
      )
      func.nodes[node_id] = { ..node, imm0: table_id, }
      hot_revision_bump(func)
    }
    _ => abort("hot branch_table_set requires a br_table node")
  }
}

///|
pub fn hot_try_table_catches_set(
  func : HotFunc,
  node_id : NodeId,
  catches : Array[HotCatchArm],
) -> Unit {
  hot_mutate_require_live_node(func, node_id, "hot try_table_catches_set node")
  for catch_ in catches {
    let label_id = match catch_ {
      HotCatchArm::CatchArm(_, label_id)
      | HotCatchArm::CatchRefArm(_, label_id)
      | HotCatchArm::CatchAllArm(label_id)
      | HotCatchArm::CatchAllRefArm(label_id) => label_id
    }
    hot_mutate_require_label_id(
      func, label_id, "hot try_table_catches_set catch target",
    )
  }
  let node = hot_node_get(func, node_id)
  match node.op {
    HotOp::TryTable => {
      let catch_id = hot_alloc_catch_info(func, HotCatchInfo::new(catches))
      func.nodes[node_id] = { ..node, imm1: catch_id, }
      hot_revision_bump(func)
    }
    _ => abort("hot try_table_catches_set requires a try_table node")
  }
}

///|
pub fn hot_node_exact_instr_set(
  func : HotFunc,
  id : NodeId,
  instr : Instruction,
) -> Unit {
  hot_mutate_require_live_node(func, id, "hot node_exact_instr_set")
  let node = hot_node_get(func, id)
  match hot_node_exact_instr_payload_id(func, id) {
    Some(_) => {
      if !hot_mutate_exact_instr_matches_node(func, id, instr) {
        abort(
          "hot node_exact_instr_set requires matching operand and result types",
        )
      }
      let payload_id = hot_alloc_instr_payload(
        func,
        HotInstrPayload::new(instr),
      )
      let next_node = match node.op {
        HotOp::Load
        | HotOp::Store
        | HotOp::RefTest
        | HotOp::RefCast
        | HotOp::RefTestDesc
        | HotOp::RefCastDescEq
        | HotOp::BrOnCast
        | HotOp::BrOnCastFail
        | HotOp::Atomic => { ..node, imm1: payload_id, }
        _ =>
          {
            ..node,
            imm0: payload_id,
            flags: hot_flags_for_exact_payload(func, node.op, payload_id),
          }
      }
      func.nodes[id] = next_node
      hot_revision_bump(func)
    }
    None => abort("hot node_exact_instr_set requires an exact-instruction node")
  }
}

///|
fn hot_mutate_exact_instr_matches_node(
  func : HotFunc,
  id : NodeId,
  instr : Instruction,
) -> Bool {
  let node = hot_node_get(func, id)
  // Other exact families can be assembled in stages by lift and passes.
  // Concrete binary and comparison operands can be checked here.
  if !(node.op is HotOp::Binary || node.op is HotOp::Compare) {
    return true
  }
  let env = match func.validation_env {
    Some(value) => value
    None => @validate.empty_env()
  }
  let operands : Array[@lib.ValType] = []
  let mut slot = 0
  while slot < hot_child_count(func, id) {
    let child = hot_child_get(func, id, slot)
    if child == HOT_NONE_NODE {
      // The pass may attach an opcode before filling a staged child slot.
      return true
    }
    let types = hot_type_results(func, hot_node_type(func, child))
    if types.length() == 0 {
      // Unreachable operands are polymorphic until the enclosing rewrite
      // reaches its final form.
      return true
    }
    let mut end = slot + 1
    while end < hot_child_count(func, id) &&
          hot_child_get(func, id, end) == child {
      end += 1
    }
    let lanes = end - slot
    if types.length() == 1 {
      for _ in 0..= lanes {
      for lane in 0.. after.stack == hot_type_results(func, node.ty)
    Err(_) => false
  }
}

///|
pub fn hot_replace_node(
  func : HotFunc,
  id : NodeId,
  new_node : HotNode,
  preserve_value_order~ : Bool = false,
) -> Unit {
  if preserve_value_order && new_node.order < 0 {
    abort("hot_replace_node requires an existing value order when forwarding")
  }
  hot_node_set(func, id, {
    ..new_node,
    flags: hot_flags_for_exact_payload(func, new_node.op, new_node.imm0),
  })
  // Removing a proven identity wrapper forwards its already evaluated value.
  // Its source can precede intervening roots, unlike a newly computed value
  // installed at the wrapper's position. Keep the default replacement rule
  // for other mutations, which may deliberately move computation later.
  if preserve_value_order {
    func.nodes[id] = { ..func.nodes[id], order: new_node.order, }
  }
}

///|
// Equivalent computation replacements can retain effects inside a new region.
// Such callers must keep the old evaluation position, not the fresh node order.
pub fn hot_replace_node_preserving_position(
  func : HotFunc,
  id : NodeId,
  new_node : HotNode,
) -> Unit {
  hot_replace_node(
    func,
    id,
    { ..new_node, order: hot_node_get(func, id).order, },
    preserve_value_order=true,
  )
}

///|
// A retained prefix can replace an expression with a separate region node.
// Keep that region at the expression's position while retaining both nodes.
pub fn hot_preserve_replacement_position(
  func : HotFunc,
  replacement : NodeId,
  original : NodeId,
) -> Unit {
  let order = hot_node_get(func, original).order
  hot_replace_node(
    func,
    replacement,
    { ..hot_node_get(func, replacement), order, },
    preserve_value_order=true,
  )
}

///|
pub fn hot_replace_local_set_with_drop(
  func : HotFunc,
  node_id : NodeId,
) -> Bool {
  if node_id < 0 || !hot_node_is_live(func, node_id) {
    return false
  }
  let node = hot_node_get(func, node_id)
  match node.op {
    HotOp::LocalSet => {
      hot_replace_node(func, node_id, {
        ..node,
        op: HotOp::Drop,
        ty: hot_type_void(func),
        imm0: 0,
        imm1: 0,
      })
      true
    }
    _ => false
  }
}

///|
pub fn hot_replace_local_tee_with_value(
  func : HotFunc,
  node_id : NodeId,
) -> Bool {
  if node_id < 0 || !hot_node_is_live(func, node_id) {
    return false
  }
  let node = hot_node_get(func, node_id)
  if node.op != HotOp::LocalTee || hot_child_count(func, node_id) != 1 {
    return false
  }
  let value_id = hot_child_get(func, node_id, 0)
  if value_id < 0 || !hot_node_is_live(func, value_id) {
    return false
  }
  let value = hot_node_get(func, value_id)
  match value.op {
    HotOp::Block | HotOp::Loop | HotOp::If | HotOp::Try | HotOp::TryTable =>
      false
    _ => {
      hot_replace_node(func, node_id, value, preserve_value_order=true)
      true
    }
  }
}

///|
pub fn hot_alloc_node(func : HotFunc, node : HotNode) -> NodeId {
  hot_mutate_require_node_shape(func, node, "hot alloc_node")
  let id = func.nodes.length()
  func.nodes.push({ ..node, order: id, })
  func.deleted_nodes.push(false)
  hot_revision_bump(func)
  id
}

///|
fn hot_alloc_node_with_children(
  func : HotFunc,
  node : HotNode,
  children : Array[NodeId],
) -> NodeId {
  hot_alloc_node(
    func,
    hot_mutate_attach_child_span(
      func, node, children, "hot alloc_node_with_children",
    ),
  )
}

///|
fn hot_alloc_node_with_child1(
  func : HotFunc,
  node : HotNode,
  child0 : NodeId,
) -> NodeId {
  hot_alloc_node(
    func,
    hot_mutate_attach_child1(func, node, child0, "hot alloc_node_with_child1"),
  )
}

///|
fn hot_alloc_node_with_child2(
  func : HotFunc,
  node : HotNode,
  child0 : NodeId,
  child1 : NodeId,
) -> NodeId {
  hot_alloc_node(
    func,
    hot_mutate_attach_child2(
      func, node, child0, child1, "hot alloc_node_with_child2",
    ),
  )
}

///|
fn hot_alloc_node_with_child3(
  func : HotFunc,
  node : HotNode,
  child0 : NodeId,
  child1 : NodeId,
  child2 : NodeId,
) -> NodeId {
  hot_alloc_node(
    func,
    hot_mutate_attach_child3(
      func, node, child0, child1, child2, "hot alloc_node_with_child3",
    ),
  )
}

///|
fn hot_mutate_assert_unreferenced(func : HotFunc, id : NodeId) -> Unit {
  for root in func.roots {
    if root == id {
      abort(
        "hot_delete_node requires the target node to be detached from roots",
      )
    }
  }
  for node_id = 0; node_id < func.nodes.length(); node_id = node_id + 1 {
    if !hot_node_is_live(func, node_id) || node_id == id {
      continue
    }
    let node = func.nodes[node_id]
    for i = 0; i < node.child_count; i = i + 1 {
      if func.children[node.child_base + i] == id {
        abort(
          "hot_delete_node requires the target node to be detached from child spans",
        )
      }
    }
  }
}

///|
fn hot_delete_node_tombstone(func : HotFunc, id : NodeId) -> Unit {
  func.nodes[id] = {
    op: HotOp::Nop,
    ty: hot_type_void(func),
    flags: hot_default_flags_for_op(HotOp::Nop),
    order: -1,
    imm0: 0,
    imm1: 0,
    child_base: 0,
    child_count: 0,
  }
  func.free_nodes.push(id)
  if id < func.deleted_nodes.length() {
    func.deleted_nodes[id] = true
  }
}

///|
fn hot_delete_node_finalize(func : HotFunc, id : NodeId) -> Unit {
  hot_delete_node_tombstone(func, id)
  hot_revision_bump(func)
}

///|
fn hot_mutate_node_targets_marked_label(
  func : HotFunc,
  node_id : NodeId,
  label_marks : @bitset.BitSet,
) -> Bool {
  let node = hot_node_get(func, node_id)
  match node.op {
    HotOp::Br
    | HotOp::BrIf
    | HotOp::BrOnNull
    | HotOp::BrOnNonNull
    | HotOp::BrOnCast
    | HotOp::BrOnCastFail
    | HotOp::Delegate =>
      node.imm0 >= 0 &&
      node.imm0 < func.labels.length() &&
      label_marks[node.imm0]
    HotOp::BrTable => {
      let table_id = node.imm0
      let default_target = hot_branch_table_default_target(func, table_id)
      if default_target >= 0 &&
        default_target < func.labels.length() &&
        label_marks[default_target] {
        return true
      }
      for target in hot_branch_table_targets(func, table_id) {
        if target >= 0 && target < func.labels.length() && label_marks[target] {
          return true
        }
      }
      false
    }
    HotOp::Continuation =>
      hot_continuation_targets(func, node_id).any(fn(target) {
        target >= 0 && target < func.labels.length() && label_marks[target]
      })
    HotOp::TryTable => {
      for catch_arm in hot_catch_info_get(func, node.imm1).catches {
        let target = match catch_arm {
          HotCatchArm::CatchArm(_, label_id)
          | HotCatchArm::CatchRefArm(_, label_id)
          | HotCatchArm::CatchAllArm(label_id)
          | HotCatchArm::CatchAllRefArm(label_id) => label_id
        }
        if target >= 0 && target < func.labels.length() && label_marks[target] {
          return true
        }
      }
      false
    }
    _ => false
  }
}

///|
fn hot_mutate_node_is_control(node : HotNode) -> Bool {
  match node.op {
    HotOp::Block | HotOp::Loop | HotOp::If | HotOp::Try | HotOp::TryTable =>
      true
    _ => false
  }
}

///|
/// Delete an exact detached forest and all labels its controls own.
///
/// The operation fails without mutation when the roots are empty, duplicate,
/// or overlapping; when any descendant graph is shared or cyclic; when any node
/// is still rooted or owned by another live node; when control-label ownership
/// is incomplete; or when any outside branch/catch targets an owned label.
pub fn hot_delete_detached_forest(
  func : HotFunc,
  root_ids : Array[NodeId],
) -> Bool {
  if root_ids.is_empty() {
    return false
  }
  let node_count = hot_node_count(func)
  let root_marks = @bitset.BitSet::new(node_count)
  let node_marks = @bitset.BitSet::new(node_count)
  let nodes : Array[NodeId] = []
  let pending : Array[NodeId] = []
  for root_id in root_ids {
    if root_id < 0 ||
      root_id >= node_count ||
      root_marks[root_id] ||
      !hot_node_is_live(func, root_id) {
      return false
    }
    root_marks[root_id] = true
    pending.push(root_id)
  }
  while !pending.is_empty() {
    let node_id = pending.pop().unwrap()
    if !hot_node_is_live(func, node_id) {
      return false
    }
    if node_marks[node_id] {
      continue
    }
    node_marks[node_id] = true
    nodes.push(node_id)
    let node = hot_node_get(func, node_id)
    for slot = 0; slot < node.child_count; slot = slot + 1 {
      let child_id = hot_child_get(func, node_id, slot)
      if child_id != HOT_NONE_NODE {
        pending.push(child_id)
      }
    }
  }

  let incoming = Array::make(node_count, 0)
  for node_id in nodes {
    let node = hot_node_get(func, node_id)
    for slot = 0; slot < node.child_count; slot = slot + 1 {
      let child_id = hot_child_get(func, node_id, slot)
      if child_id == HOT_NONE_NODE || !node_marks[child_id] {
        continue
      }
      incoming[child_id] = incoming[child_id] + 1
      if incoming[child_id] > 1 {
        return false
      }
    }
  }
  for root_id in root_ids {
    if incoming[root_id] != 0 {
      return false
    }
  }
  for node_id in nodes {
    if !root_marks[node_id] && incoming[node_id] != 1 {
      return false
    }
  }
  for root in func.roots {
    if node_marks[root] {
      return false
    }
  }
  for node_id = 0; node_id < node_count; node_id = node_id + 1 {
    if !hot_node_is_live(func, node_id) || node_marks[node_id] {
      continue
    }
    let node = hot_node_get(func, node_id)
    for slot = 0; slot < node.child_count; slot = slot + 1 {
      let child_id = hot_child_get(func, node_id, slot)
      if child_id != HOT_NONE_NODE && node_marks[child_id] {
        return false
      }
    }
  }

  let label_marks = @bitset.BitSet::new(func.labels.length())
  let labels : Array[LabelId] = []
  for label_id = 0; label_id < func.labels.length(); label_id = label_id + 1 {
    if !hot_label_is_live(func, label_id) {
      continue
    }
    let owner = hot_label_owner(func, label_id)
    if owner >= 0 && owner < node_count && node_marks[owner] {
      label_marks[label_id] = true
      labels.push(label_id)
    }
  }
  for node_id in nodes {
    let node = hot_node_get(func, node_id)
    if !hot_mutate_node_is_control(node) {
      continue
    }
    let label_id = hot_control_node_label(func, node_id)
    if !hot_label_is_live(func, label_id) ||
      !label_marks[label_id] ||
      hot_label_owner(func, label_id) != node_id {
      return false
    }
  }
  for label_id in labels {
    let owner = hot_label_owner(func, label_id)
    if !hot_mutate_node_is_control(hot_node_get(func, owner)) ||
      hot_control_node_label(func, owner) != label_id {
      return false
    }
  }
  for node_id = 0; node_id < node_count; node_id = node_id + 1 {
    if hot_node_is_live(func, node_id) &&
      !node_marks[node_id] &&
      hot_mutate_node_targets_marked_label(func, node_id, label_marks) {
      return false
    }
  }

  for label_id in labels {
    let label = hot_label_get(func, label_id)
    func.labels[label_id] = { ..label, owner: HOT_NONE_NODE, branch_arity: 0, }
  }
  for node_id in nodes {
    hot_delete_node_tombstone(func, node_id)
  }
  hot_revision_bump(func)
  true
}

///|
/// Delete exact detached structured trees and all labels they own.
pub fn hot_delete_detached_control_forest(
  func : HotFunc,
  root_ids : Array[NodeId],
) -> Bool {
  if root_ids.is_empty() {
    return false
  }
  let node_count = hot_node_count(func)
  for root_id in root_ids {
    if root_id < 0 ||
      root_id >= node_count ||
      !hot_node_is_live(func, root_id) ||
      !hot_mutate_node_is_control(hot_node_get(func, root_id)) {
      return false
    }
  }
  hot_delete_detached_forest(func, root_ids)
}

///|
/// Delete one exact detached structured subtree and all labels it owns.
pub fn hot_delete_detached_control_subtree(
  func : HotFunc,
  root_id : NodeId,
) -> Bool {
  hot_delete_detached_control_forest(func, [root_id])
}

///|
pub fn hot_delete_detached_node(func : HotFunc, id : NodeId) -> Unit {
  hot_mutate_require_live_node(func, id, "hot_delete_detached_node")
  hot_delete_node_finalize(func, id)
}

///|
fn hot_mutate_detached_nodes_are_unreferenced(
  func : HotFunc,
  node_ids : Array[NodeId],
) -> Bool {
  let node_count = hot_node_count(func)
  let selected = @bitset.BitSet::new(node_count)
  for node_id in node_ids {
    if node_id < 0 ||
      node_id >= node_count ||
      !hot_node_is_live(func, node_id) ||
      selected[node_id] {
      return false
    }
    selected[node_id] = true
  }
  for root in func.roots {
    if selected[root] {
      return false
    }
  }
  for owner = 0; owner < node_count; owner = owner + 1 {
    if !hot_node_is_live(func, owner) || selected[owner] {
      continue
    }
    let node = hot_node_get(func, owner)
    for slot = 0; slot < node.child_count; slot = slot + 1 {
      let child = hot_child_get(func, owner, slot)
      if child >= 0 && child < node_count && selected[child] {
        return false
      }
    }
  }
  true
}

///|
fn hot_mutate_delete_detached_nodes_if_unreferenced(
  func : HotFunc,
  node_ids : Array[NodeId],
) -> Bool {
  if !hot_mutate_detached_nodes_are_unreferenced(func, node_ids) {
    return false
  }
  for node_id in node_ids {
    hot_delete_node_tombstone(func, node_id)
  }
  hot_revision_bump(func)
  true
}

///|
/// Delete an exact distinct detached node set with one revision invalidation.
pub fn hot_delete_detached_nodes(
  func : HotFunc,
  node_ids : Array[NodeId],
) -> Unit {
  if node_ids.is_empty() {
    return
  }
  if !hot_mutate_delete_detached_nodes_if_unreferenced(func, node_ids) {
    abort("hot_delete_detached_nodes requires an unreferenced detached set")
  }
}

///|
pub fn hot_delete_node(func : HotFunc, id : NodeId) -> Unit {
  hot_mutate_require_live_node(func, id, "hot_delete_node")
  hot_mutate_assert_unreferenced(func, id)
  hot_delete_node_finalize(func, id)
}