///|
pub suberror AArch64AllocationError {
  InvalidVCode(cause~ : TargetVCodeVerifyError)
  AllocatorFailure(cause~ : @vcode_regalloc.VCodeAllocationError)
  InvalidAllocation(cause~ : @vcode.AllocationVerifyError)
  IllegalRegister(reg~ : @vcode.PhysicalReg)
} derive(Debug)

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

///|
fn int_physical(index : Int) -> @vcode.PhysicalReg {
  @vcode.PhysicalReg::new(index, Int)
}

///|
fn fp_physical(index : Int) -> @vcode.PhysicalReg {
  @vcode.PhysicalReg::new(index, FpVector)
}

///|
/// IP0 is excluded from allocation so late emission can finalize arbitrary
/// stack offsets without changing the allocation.
fn stack_address_scratch() -> @vcode.PhysicalReg {
  int_physical(16)
}

///|
/// X15 is reserved exclusively for target-level parallel transfers.
fn int_transfer_scratch() -> @vcode.PhysicalReg {
  int_physical(15)
}

///|
fn fp_transfer_scratch() -> @vcode.PhysicalReg {
  fp_physical(18)
}

///|
/// IP1 carries allocator-inserted integer transfers and instruction-local
/// temporary values.
fn int_instruction_scratch() -> @vcode.PhysicalReg {
  int_physical(17)
}

///|
fn int_transfer_scratch_regs() -> Array[@vcode.PhysicalReg] {
  [int_transfer_scratch()]
}

///|
fn fp_transfer_scratch_regs() -> Array[@vcode.PhysicalReg] {
  [fp_transfer_scratch()]
}

///|
fn transfer_scratch_for_type(ty : @semantic.ValueType) -> @vcode.PhysicalReg {
  match @vcode.reg_class_for_value_type(ty) {
    Int => int_transfer_scratch()
    FpVector => fp_transfer_scratch()
  }
}

///|
priv enum AllocationRegisterRole {
  ValueHome
  SpillEdit
  Transfer
  StackAddress
  Reserved
}

///|
fn allocation_register_role(reg : @vcode.PhysicalReg) -> AllocationRegisterRole {
  match reg.class {
    Int =>
      if (reg.id >= 0 && reg.id < 15) || (reg.id >= 19 && reg.id < 29) {
        ValueHome
      } else if reg.id == 15 {
        Transfer
      } else if reg.id == 16 {
        StackAddress
      } else if reg.id == 17 {
        SpillEdit
      } else {
        Reserved
      }
    FpVector =>
      if (reg.id >= 0 && reg.id < 16) || (reg.id >= 19 && reg.id < 32) {
        ValueHome
      } else if reg.id == 16 || reg.id == 17 {
        SpillEdit
      } else if reg.id == 18 {
        Transfer
      } else {
        Reserved
      }
  }
}

///|
fn AllocationRegisterRole::allows_value_home(
  self : AllocationRegisterRole,
) -> Bool {
  match self {
    ValueHome => true
    SpillEdit | Transfer | StackAddress | Reserved => false
  }
}

///|
fn AllocationRegisterRole::allows_spill_edit(
  self : AllocationRegisterRole,
) -> Bool {
  self is SpillEdit
}

///|
fn is_allocatable(reg : @vcode.PhysicalReg) -> Bool {
  allocation_register_role(reg).allows_value_home()
}

///|
fn allocatable_physical_regs() -> Array[@vcode.PhysicalReg] {
  let regs : Array[@vcode.PhysicalReg] = []
  for index in 0..<32 {
    let reg = int_physical(index)
    if allocation_register_role(reg).allows_value_home() {
      regs.push(reg)
    }
  }
  for index in 0..<32 {
    let reg = fp_physical(index)
    if allocation_register_role(reg).allows_value_home() {
      regs.push(reg)
    }
  }
  regs
}

///|
fn spill_scratch_regs() -> Array[@vcode.PhysicalReg] {
  let regs : Array[@vcode.PhysicalReg] = []
  for index in 0..<32 {
    let reg = int_physical(index)
    if allocation_register_role(reg).allows_spill_edit() {
      regs.push(reg)
    }
  }
  for index in 0..<32 {
    let reg = fp_physical(index)
    if allocation_register_role(reg).allows_spill_edit() {
      regs.push(reg)
    }
  }
  regs
}

///|
fn require_legal_value_register(
  reg : @vcode.PhysicalReg,
) -> Unit raise AArch64AllocationError {
  if !allocation_register_role(reg).allows_value_home() {
    raise IllegalRegister(reg~)
  }
}

///|
fn require_legal_operand_register(
  reg : @vcode.PhysicalReg,
) -> Unit raise AArch64AllocationError {
  if !allocation_register_role(reg).allows_value_home() {
    raise IllegalRegister(reg~)
  }
}

///|
fn require_legal_edit_register(
  reg : @vcode.PhysicalReg,
) -> Unit raise AArch64AllocationError {
  let role = allocation_register_role(reg)
  if !role.allows_value_home() && !role.allows_spill_edit() {
    raise IllegalRegister(reg~)
  }
}

///|
pub fn verify_allocation(
  function : @vcode.Function[AArch64Inst],
  allocation : @vcode.Allocation,
) -> Unit raise AArch64AllocationError {
  verify_vcode(function) catch {
    error => raise InvalidVCode(cause=error)
  }
  @vcode.verify_allocated(function, allocation) catch {
    error => raise InvalidAllocation(cause=error)
  }
  verify_target_allocation(function, allocation)
}

///|
fn verify_target_allocation(
  function : @vcode.Function[AArch64Inst],
  allocation : @vcode.Allocation,
) -> Unit raise AArch64AllocationError {
  for index in 0.. require_legal_value_register(reg)
      Stack(_) => ()
    }
  }
  for instruction_index in 0.. require_legal_edit_register(reg)
      Move(from~, to~, ..) => {
        require_legal_edit_register(from)
        require_legal_edit_register(to)
      }
      EdgeMove(from~, to~, ..) => {
        if from is Register(reg) {
          require_legal_edit_register(reg)
        }
        if to is Register(reg) {
          require_legal_edit_register(reg)
        }
      }
    }
  }
}

///|
fn allocate_verified(
  function : @vcode.Function[AArch64Inst],
  on_phase? : ((String?) -> Unit)? = None,
  verify_allocation? : Bool = true,
) -> @vcode.Allocation raise AArch64AllocationError {
  let allocation = @vcode_regalloc.allocate_selected_vcode(
    function,
    @vcode_regalloc.VCodeAllocationEnvironment::new(
      allocatable_physical_regs(),
      spill_scratch_regs(),
    ),
    on_phase~,
    verify=verify_allocation,
  ) catch {
    error => raise AllocatorFailure(cause=error)
  }
  verify_target_allocation(function, allocation)
  allocation
}

///|
pub fn allocate(
  function : @vcode.Function[AArch64Inst],
) -> @vcode.Allocation raise AArch64AllocationError {
  verify_vcode(function) catch {
    error => raise InvalidVCode(cause=error)
  }
  allocate_verified(function)
}