///|
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(to_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)
}

///|
/// IP1 carries allocator-inserted integer transfers and the occasional second
/// emitter temporary.
fn int_transfer_scratch() -> @vcode.PhysicalReg {
  int_physical(17)
}

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

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

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

///|
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()
  }
}

///|
fn allocatable_physical_regs() -> Array[@vcode.PhysicalReg] {
  let regs : Array[@vcode.PhysicalReg] = []
  for index in 0..<16 {
    regs.push(int_physical(index))
  }
  for index in 19..<29 {
    regs.push(int_physical(index))
  }
  for index in 0..<16 {
    regs.push(fp_physical(index))
  }
  for index in 18..<32 {
    regs.push(fp_physical(index))
  }
  regs
}

///|
fn spill_scratch_regs() -> Array[@vcode.PhysicalReg] {
  [int_transfer_scratch(), fp_physical(16), fp_physical(17)]
}

///|
fn is_allocatable(reg : @vcode.PhysicalReg) -> Bool {
  match reg.class {
    Int => (reg.id >= 0 && reg.id < 16) || (reg.id >= 19 && reg.id < 29)
    FpVector => (reg.id >= 0 && reg.id < 16) || (reg.id >= 18 && reg.id < 32)
  }
}

///|
fn is_scratch(reg : @vcode.PhysicalReg) -> Bool {
  match reg.class {
    Int => reg.id == 17
    FpVector => reg.id == 16 || reg.id == 17
  }
}

///|
fn require_legal_register(
  reg : @vcode.PhysicalReg,
  allow_scratch : Bool,
) -> Unit raise AArch64AllocationError {
  if !is_allocatable(reg) && !(allow_scratch && is_scratch(reg)) {
    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_register(reg, false)
      Stack(_) => ()
    }
  }
  for instruction_index in 0.. require_legal_register(reg, true)
      Move(from~, to~, ..) => {
        require_legal_register(from, true)
        require_legal_register(to, true)
      }
      EdgeMove(from~, to~, ..) => {
        if from is Register(reg) {
          require_legal_register(reg, true)
        }
        if to is Register(reg) {
          require_legal_register(reg, true)
        }
      }
    }
  }
}

///|
fn allocate_verified(
  function : @vcode.Function[AArch64Inst],
) -> @vcode.Allocation raise AArch64AllocationError {
  let allocation = @vcode_regalloc.allocate_selected_vcode(
    function,
    @vcode_regalloc.VCodeAllocationEnvironment::new(
      allocatable_physical_regs(),
      spill_scratch_regs(),
    ),
  ) 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)
}