///|
pub type GprWidth = @vcode.RegisterWidth

///|
pub type AArch64IntUnary = @vcode.ScalarIntUnary

///|
#derive(Eq, Debug)
pub(all) enum AArch64IntBinary {
  Add
  Sub
  Mul
  And
  Orr
  Eor
  Sdiv
  Udiv
}

///|
#derive(Eq, Debug)
pub(all) enum AArch64Shift {
  Lsl
  Asr
  Lsr
  Ror
}

///|
pub type AArch64Condition = @vcode.IntegerCondition

///|
pub type AArch64FloatUnary = @lowering.FloatUnaryOp

///|
pub type AArch64FloatBinary = @lowering.FloatBinaryOp

///|
pub type AArch64FloatTernary = @lowering.FloatTernaryOp

///|
pub type AArch64VectorIntUnary = @lowering.VectorIntUnaryOp

///|
pub type AArch64VectorIntBinary = @lowering.VectorIntBinaryOp

///|
pub type AArch64VectorPredicate = @lowering.VectorPredicateOp

///|
pub type AArch64VectorConversion = @vcode.VectorConversion

///|
pub type AArch64FloatCondition = @lowering.FloatComparison

///|
pub type AArch64FloatTrapCondition = @vcode.FloatTrapCondition

///|
pub type AArch64Address = @vcode.SymbolicAddress

///|
#derive(Eq, Debug)
pub(all) enum AArch64Conversion {
  WrapI64ToI32
  ExtendI32ToI64(@native.Signedness)
  SignExtend(@native.IntegerType, @native.AccessWidth)
  DemoteF64ToF32
  PromoteF32ToF64
  Bitcast(@native.ValueType, @native.ValueType)
  IntToFloat(@native.IntegerType, @native.FloatType, @native.Signedness)
  FloatToInt(@native.FloatType, @native.IntegerType, @native.Signedness)
}

///|
pub type AArch64StackObject = @vcode.SizedStackObject

///|
#derive(Eq, Debug)
pub(all) enum AArch64Inst {
  IncomingReg(@native.ValueType, @vcode.PhysicalReg)
  IncomingStack(@native.ValueType, Int32)
  IncomingResultArea(@vcode.PhysicalReg)
  IncomingCallResult(@native.ValueType, @vcode.PhysicalReg)
  IncomingCallAreaResult(@native.ValueType, Int32)
  OutgoingReg(@native.ValueType, @vcode.PhysicalReg)
  OutgoingAreaResult(@native.ValueType, Int32)
  KeepAlive(@native.ValueType)
  LoadConstant(GprWidth, UInt64)
  LoadVectorConstant(UInt64, UInt64)
  LoadNull(@native.ValueType)
  LoadAddress(AArch64Address)
  StackAddress(AArch64StackObject)
  LoadFloatConstant(@native.ValueType, UInt64)
  Move(@native.ValueType)
  CarrierMove(@native.ValueType, @native.ValueType)
  Select(@native.ValueType)
  SpectreGuard
  VectorSelect
  VectorSplat(@lowering.VectorLane)
  VectorExtractLane(@lowering.VectorLane, Int32, @native.Signedness?)
  VectorReplaceLane(@lowering.VectorLane, Int32)
  VectorShuffle(FixedArray[Int32])
  VectorSwizzle
  VectorBitwise(@lowering.VectorBitwiseOp)
  VectorIntUnary(@lowering.VectorLane, AArch64VectorIntUnary)
  VectorIntBinary(@lowering.VectorLane, AArch64VectorIntBinary)
  VectorIntShift(@lowering.VectorLane, @lowering.VectorIntShiftOp)
  VectorIntCompare(@lowering.VectorLane, @lowering.VectorIntComparison)
  VectorConvert(AArch64VectorConversion)
  VectorPredicate(AArch64VectorPredicate)
  VectorFloatUnary(@lowering.VectorLane, @lowering.VectorFloatUnaryOp)
  VectorFloatBinary(@lowering.VectorLane, @lowering.VectorFloatBinaryOp)
  VectorFloatTernary(@lowering.VectorLane, @lowering.FloatTernaryOp)
  VectorFloatCompare(@lowering.VectorLane, @lowering.VectorFloatComparison)
  VectorPairwiseAddI16x8
  VectorRelaxedDot8To16
  IntUnary(GprWidth, AArch64IntUnary)
  IntBinary(GprWidth, AArch64IntBinary)
  IntBinaryImmediate(GprWidth, AArch64IntBinary, UInt64)
  IntMultiplyAdd(GprWidth)
  IntMultiplySubtract(GprWidth)
  IntShiftRegister(GprWidth, AArch64Shift)
  IntShiftImmediate(GprWidth, AArch64Shift, Int32)
  IntAddShiftedLeft(GprWidth, Int32)
  IntAddShiftedRight(GprWidth, Int32)
  IntLowBitsShiftLeft(GprWidth, Int32, Int32)
  IntExtractBits(GprWidth, Int32, Int32)
  PopulationCount(GprWidth)
  IntHighMultiply(GprWidth, @native.Signedness)
  IntWithOverflow(GprWidth, @lowering.IntOverflowOp)
  IntRemainder(GprWidth, @native.Signedness)
  TrapIfZero(GprWidth)
  TrapIfSignedDivOverflow(GprWidth)
  CompareSet(GprWidth, AArch64Condition)
  CompareSetImmediate(GprWidth, AArch64Condition, UInt64)
  ReferenceCompareSet(@native.ValueType, AArch64Condition)
  FloatUnary(@native.ValueType, AArch64FloatUnary)
  FloatBinary(@native.ValueType, AArch64FloatBinary)
  FloatTernary(@native.ValueType, AArch64FloatTernary)
  FloatCompareSet(@native.ValueType, AArch64FloatCondition)
  TrapIfFloat(@native.ValueType, AArch64FloatTrapCondition)
  Convert(AArch64Conversion)
  AddAddress
  AddAddressImmediate(UInt64)
  AddAddressUxtw(Int32)
  ScalarLoad(
    @native.AccessWidth,
    @native.LoadExtension,
    @native.ValueType,
    UInt64
  )
  ScalarLoadUxtw(
    @native.AccessWidth,
    @native.LoadExtension,
    @native.ValueType,
    Int32
  )
  ScalarStore(@native.AccessWidth, @native.ValueType, UInt64)
  StoreRepeated64(UInt64, UInt32, UInt64)
  ScalarStoreUxtw(@native.AccessWidth, @native.ValueType, Int32)
  VectorLoad128(UInt64)
  VectorStore128(UInt64)
  VectorLoadSplat(@lowering.VectorLane, UInt64)
  VectorLoadExtend(@lowering.VectorLane, @native.Signedness, UInt64)
  VectorLoadZero(@native.AccessWidth, UInt64)
  VectorLoadLane(@lowering.VectorLane, Int32, UInt64)
  VectorStoreLane(@lowering.VectorLane, Int32, UInt64)
  AtomicLoad(@native.AccessWidth, @native.ValueType)
  AtomicStore(@native.AccessWidth, @native.ValueType)
  AtomicRmw(@native.AccessWidth, @native.ValueType, @lowering.AtomicRmwOp)
  AtomicCompareExchange(@native.AccessWidth, @native.ValueType)
  AtomicFence
  SafepointMarker
  PlatformCall(@native.ExternalSymbol, @native.Signature)
  ReturnsTwicePlatformCall(@native.ExternalSymbol, @native.Signature)
  InternalCall(@native.CodeSymbol, @native.Signature, InternalCallPlan)
  InternalCallIndirect(@native.Signature, InternalCallPlan)
  TailCallDirect(@native.CodeSymbol, @native.Signature, InternalCallPlan)
  TailCallIndirect(@native.Signature, InternalCallPlan)
  Jump
  BranchNonZero(GprWidth)
  BranchCompare(GprWidth, AArch64Condition)
  BranchCompareImmediate(GprWidth, AArch64Condition, UInt64)
  Switch(GprWidth, Array[UInt64])
  Trap(@native.TrapReason)
  Return
}

///|
let platform_integer_arguments : Array[Int32] = [0, 1, 2, 3, 4, 5, 6, 7]

///|
fn platform_call_layout(types : Array[@native.ValueType]) -> CallLayout {
  @vcode.platform_call_layout(types, platform_integer_arguments[:])
}

///|
fn platform_result_registers(
  types : Array[@native.ValueType],
) -> Array[@vcode.PhysicalReg] {
  @vcode.platform_result_registers(types)
}

///|
fn platform_call_clobbers() -> Array[@vcode.PhysicalReg] {
  let clobbers : Array[@vcode.PhysicalReg] = []
  for index in 0..<18 {
    clobbers.push(@vcode.PhysicalReg::new(index, Int))
  }
  // The unified FP/vector bank must conservatively treat v8-v15 as clobbered:
  // AAPCS64 preserves only their low 64 bits, not a complete V128 value.
  for index in 0..<32 {
    clobbers.push(@vcode.PhysicalReg::new(index, FpVector))
  }
  clobbers
}

///|
fn integer_type(width : GprWidth) -> @native.ValueType {
  match width {
    W32 => I32
    W64 => I64
  }
}

///|
fn valid_scalar_register_shift(
  width : @native.AccessWidth,
  shift : Int32,
) -> Bool {
  let natural_shift = match width {
    W8 => 0
    W16 => 1
    W32 => 2
    W64 => 3
    W128 => 4
  }
  shift == 0 || shift == natural_shift
}

///|
fn conversion_shape(
  conversion : AArch64Conversion,
) -> (@native.ValueType, @native.ValueType)? {
  match conversion {
    WrapI64ToI32 => Some((I64, I32))
    ExtendI32ToI64(_) => Some((I32, I64))
    SignExtend(I32, W8 | W16) => Some((I32, I32))
    SignExtend(I64, W8 | W16 | W32) => Some((I64, I64))
    DemoteF64ToF32 => Some((F64, F32))
    PromoteF32ToF64 => Some((F32, F64))
    Bitcast(from, to) =>
      if @native.valid_scalar_bitcast(from, to) {
        Some((from, to))
      } else {
        None
      }
    IntToFloat(I32, F32, _) => Some((I32, F32))
    IntToFloat(I32, F64, _) => Some((I32, F64))
    IntToFloat(I64, F32, _) => Some((I64, F32))
    IntToFloat(I64, F64, _) => Some((I64, F64))
    FloatToInt(F32, I32, _) => Some((F32, I32))
    FloatToInt(F32, I64, _) => Some((F32, I64))
    FloatToInt(F64, I32, _) => Some((F64, I32))
    FloatToInt(F64, I64, _) => Some((F64, I64))
    _ => None
  }
}

///|
pub fn verify_vcode(
  function : @vcode.Function[AArch64Inst],
) -> Unit raise TargetVCodeVerifyError {
  @vcode.verify_selected(function) catch {
    error => raise CommonFailure(cause=error)
  }
  verify_target_vcode(function)
}

///|
fn verify_target_vcode(
  function : @vcode.Function[AArch64Inst],
) -> Unit raise TargetVCodeVerifyError {
  for index in 0.. check.incoming_reg(ty, reg)
      IncomingStack(ty, offset) => check.incoming_stack(ty, offset)
      IncomingResultArea(reg) => check.incoming_result_area(reg)
      IncomingCallResult(ty, reg) => check.incoming_call_result(ty, reg)
      IncomingCallAreaResult(ty, offset) =>
        check.incoming_call_area_result(ty, offset)
      LoadConstant(width, bits) =>
        check.load_constant(integer_type(width), bits)
      LoadVectorConstant(_, _) => check.shape([], [V128])
      LoadNull(ty) => check.load_null(ty)
      LoadAddress(_) => check.shape([], [Ptr64])
      StackAddress(object) => check.stack_address(object.is_valid())
      LoadFloatConstant(ty, bits) => check.load_float_constant(ty, bits)
      OutgoingReg(ty, reg) => check.outgoing_reg(ty, reg)
      OutgoingAreaResult(ty, offset) => check.outgoing_area_result(ty, offset)
      KeepAlive(ty) => check.shape([ty], [])
      Move(ty) => check.shape([ty], [ty])
      CarrierMove(from, to) => check.carrier_move(from, to)
      Select(ty) => check.scalar_select(ty)
      SpectreGuard => check.shape([I32, Ptr64, Ptr64], [Ptr64])
      VectorSelect => check.shape([I32, V128, V128], [V128])
      VectorSplat(lane) => check.shape([lane.scalar_type()], [V128])
      VectorExtractLane(lane, index, extension) =>
        check.vector_extract_lane(lane, index, extension is Some(_))
      VectorReplaceLane(lane, index) => {
        check.vector_replace_lane(lane, index)
        let source = function.instruction_operand_at(instruction, 0).unwrap()
        let result = function.instruction_operand_at(instruction, 2).unwrap()
        if result.tie_id < 0 || result.tie_id != source.tie_id {
          raise InvalidInstruction(
            instruction~,
            message="vector lane replacement result must be tied to its source vector",
          )
        }
      }
      VectorShuffle(mask) => check.vector_shuffle(mask)
      VectorSwizzle | VectorRelaxedDot8To16 | VectorPairwiseAddI16x8 =>
        check.shape([V128, V128], [V128])
      VectorIntUnary(lane, operation) => check.vector_int_unary(lane, operation)
      VectorIntBinary(lane, operation) =>
        check.vector_int_binary(lane, operation)
      VectorIntShift(lane, _) => check.vector_int_shift(lane)
      VectorIntCompare(lane, _) => check.vector_int_compare(lane)
      VectorConvert(conversion) => check.vector_conversion(conversion)
      VectorPredicate(predicate) => check.vector_predicate(predicate)
      VectorFloatUnary(lane, _) => check.vector_float_unary(lane)
      VectorFloatBinary(lane, _) | VectorFloatCompare(lane, _) =>
        check.vector_float_binary(lane)
      VectorFloatTernary(lane, _) => check.vector_float_ternary(lane)
      VectorBitwise(operation) => check.vector_bitwise(operation)
      IntUnary(width, _) | PopulationCount(width) =>
        check.shape([integer_type(width)], [integer_type(width)])
      IntBinary(width, _)
      | IntShiftRegister(width, _)
      | IntHighMultiply(width, _)
      | IntRemainder(width, _) =>
        check.shape([integer_type(width), integer_type(width)], [
          integer_type(width),
        ])
      IntBinaryImmediate(width, operation, bits) => {
        let ty = integer_type(width)
        if !valid_int_binary_immediate(width, operation, bits) {
          raise InvalidInstruction(
            instruction~,
            message="integer immediate cannot be encoded",
          )
        }
        check.shape([ty], [ty])
      }
      IntMultiplyAdd(width) | IntMultiplySubtract(width) =>
        check.shape(
          [integer_type(width), integer_type(width), integer_type(width)],
          [integer_type(width)],
        )
      IntShiftImmediate(width, _, amount) => {
        let ty = integer_type(width)
        let bit_width = if width == W32 { 32 } else { 64 }
        if amount < 0 || amount >= bit_width {
          raise InvalidInstruction(
            instruction~,
            message="integer shift amount is out of range",
          )
        }
        check.shape([ty], [ty])
      }
      IntExtractBits(width, start, field_bits) => {
        let bits = if width == W32 { 32 } else { 64 }
        if start < 0 ||
          start >= bits ||
          field_bits <= 0 ||
          field_bits > bits - start {
          raise InvalidInstruction(
            instruction~,
            message="integer bit extract is out of range",
          )
        }
        let ty = integer_type(width)
        check.shape([ty], [ty])
      }
      IntLowBitsShiftLeft(width, field_bits, amount) => {
        let ty = integer_type(width)
        let bits = if width == W32 { 32 } else { 64 }
        if field_bits <= 0 ||
          field_bits > bits ||
          amount < 0 ||
          amount > bits - field_bits {
          raise InvalidInstruction(
            instruction~,
            message="low-bit shift field or amount is out of range",
          )
        }
        check.shape([ty], [ty])
      }
      IntAddShiftedLeft(width, amount) | IntAddShiftedRight(width, amount) => {
        let ty = integer_type(width)
        let bit_width = if width == W32 { 32 } else { 64 }
        if amount < 0 || amount >= bit_width {
          raise InvalidInstruction(
            instruction~,
            message="integer shift amount is out of range",
          )
        }
        check.shape([ty, ty], [ty])
      }
      IntWithOverflow(width, _) =>
        check.shape([integer_type(width), integer_type(width)], [
          integer_type(width),
          I32,
        ])
      TrapIfZero(width) => check.trap_if_zero(integer_type(width))
      TrapIfSignedDivOverflow(width) =>
        check.trap_if_signed_div_overflow(integer_type(width))
      CompareSetImmediate(width, _, bits) => {
        if add_sub_immediate_shift(bits) is None {
          raise InvalidInstruction(
            instruction~,
            message="comparison immediate cannot be encoded",
          )
        }
        check.shape([integer_type(width)], [I32])
      }
      CompareSet(width, _) =>
        check.shape([integer_type(width), integer_type(width)], [I32])
      ReferenceCompareSet(ty, _) => check.reference_compare(ty)
      FloatUnary(ty, _) => check.float_unary(ty)
      FloatBinary(ty, _) => check.float_binary(ty)
      FloatTernary(ty, _) => check.float_ternary(ty)
      FloatCompareSet(ty, _) => check.float_compare(ty)
      TrapIfFloat(ty, condition) =>
        check.trap_if_float(ty, condition == Unordered)
      Convert(conversion) =>
        check.conversion(
          conversion_shape(conversion),
          conversion is (Bitcast(I64, Ptr64) | Bitcast(Ptr64, I64)),
          target="AArch64",
        )
      AddAddress => check.shape([Ptr64, I64], [Ptr64])
      AddAddressImmediate(bits) => {
        if add_sub_immediate_shift(bits) is None {
          raise InvalidInstruction(
            instruction~,
            message="address immediate cannot be encoded",
          )
        }
        check.shape([Ptr64], [Ptr64])
      }
      AddAddressUxtw(shift) => {
        if shift < 0 || shift > 4 {
          raise InvalidInstruction(
            instruction~,
            message="invalid AArch64 extended address shift",
          )
        }
        check.shape([Ptr64, I32], [Ptr64])
      }
      ScalarLoad(width, extension, result_type, _) =>
        check.scalar_load(width, extension, result_type, target="AArch64")
      ScalarLoadUxtw(width, extension, result_type, shift) => {
        if !@native.valid_scalar_load(width, extension, result_type) ||
          !valid_scalar_register_shift(width, shift) {
          raise InvalidInstruction(
            instruction~,
            message="invalid AArch64 scalar register-offset load",
          )
        }
        check.shape([Ptr64, I32], [result_type])
      }
      StoreRepeated64(offset, count, _) => check.store_repeated64(offset, count)
      ScalarStore(width, value_type, _) =>
        check.scalar_store(width, value_type, target="AArch64")
      ScalarStoreUxtw(width, value_type, shift) => {
        if !@native.valid_scalar_store(width, value_type) ||
          !valid_scalar_register_shift(width, shift) {
          raise InvalidInstruction(
            instruction~,
            message="invalid AArch64 scalar register-offset store",
          )
        }
        check.shape([Ptr64, I32, value_type], [])
      }
      VectorLoad128(_) | VectorLoadSplat(_, _) => check.shape([Ptr64], [V128])
      VectorStore128(_) => check.shape([Ptr64, V128], [])
      VectorLoadZero(width, _) => check.vector_load_zero(width)
      VectorLoadExtend(lane, _, _) => check.vector_load_extend(lane)
      VectorLoadLane(lane, index, _) => check.vector_load_lane(lane, index)
      VectorStoreLane(lane, index, _) => check.vector_store_lane(lane, index)
      AtomicLoad(width, ty) => check.atomic_load(width, ty, target="AArch64")
      AtomicStore(width, ty) => check.atomic_store(width, ty, target="AArch64")
      AtomicRmw(width, ty, _) => {
        check.atomic_rmw(width, ty, target="AArch64")
        check.atomic_early_def("atomic RMW result must be an early definition")
      }
      AtomicCompareExchange(width, ty) =>
        check.atomic_compare_exchange(width, ty, target="AArch64")
      AtomicFence => check.shape([], [])
      SafepointMarker => check.safepoint_marker()
      PlatformCall(_, signature) | ReturnsTwicePlatformCall(_, signature) =>
        check.platform_call(
          signature, platform_call_layout, is_allocatable, platform_call_clobbers,
        )
      InternalCall(_, signature, plan) =>
        check.internal_call(signature, plan, indirect=false, is_allocatable, _ => {
          platform_call_clobbers()
        })
      InternalCallIndirect(signature, plan) =>
        check.internal_call(signature, plan, indirect=true, is_allocatable, _ => {
          platform_call_clobbers()
        })
      TailCallDirect(_, signature, plan) =>
        require_tail_call_plan(
          function,
          instruction,
          signature,
          plan,
          false,
          is_allocatable,
          platform_call_clobbers(),
        )
      TailCallIndirect(signature, plan) =>
        require_tail_call_plan(
          function,
          instruction,
          signature,
          plan,
          true,
          is_allocatable,
          platform_call_clobbers(),
        )
      Jump => check.shape([], [], successors=1)
      BranchNonZero(width) =>
        check.shape([integer_type(width)], [], successors=2)
      BranchCompare(width, _) =>
        check.shape(
          [integer_type(width), integer_type(width)],
          [],
          successors=2,
        )
      BranchCompareImmediate(width, _, bits) => {
        let ty = integer_type(width)
        if add_sub_immediate_shift(bits) is None {
          raise InvalidInstruction(
            instruction~,
            message="branch comparison immediate cannot be encoded",
          )
        }
        check.shape([ty], [], successors=2)
      }
      Switch(width, cases) => check.switch_shape(integer_type(width), cases)
      Trap(reason) => check.trap(reason)
      Return => check.return_shape()
    }
  }
  @vcode.verify_abi_materialization(
    function, abi_materialization, platform_result_registers,
  )
}

///|
fn abi_materialization(inst : AArch64Inst) -> @vcode.AbiMaterialization {
  match inst {
    PlatformCall(_, signature) | ReturnsTwicePlatformCall(_, signature) =>
      PlatformResults(signature)
    InternalCall(_, signature, plan) | InternalCallIndirect(signature, plan) =>
      InternalResults(signature, plan)
    TailCallDirect(_, signature, plan) if plan.result_area_size > 0 =>
      ForwardResultArea(signature.params.length(), indirect=false)
    TailCallIndirect(signature, plan) if plan.result_area_size > 0 =>
      ForwardResultArea(signature.params.length() + 1, indirect=true)
    IncomingResultArea(_) => ReceiveResultArea
    IncomingCallResult(ty, reg) => ReceiveRegisterResult(ty, reg)
    IncomingCallAreaResult(ty, offset) => ReceiveAreaResult(ty, offset)
    OutgoingReg(ty, _) => ReturnRegisterResult(ty)
    OutgoingAreaResult(ty, _) => ReturnAreaResult(ty)
    Return => Return
    _ => Ordinary
  }
}