///|
pub(all) enum IBinaryOpCode {
  Add
  Sub
  Mul
  Div
  Rem
  And
  Or
  Xor
  Shl
  LShr
  AShr
} derive(Eq)

///|
pub fn IBinaryOpCode::is_commutative(self : Self) -> Bool {
  match self {
    Add | Mul | And | Or | Xor => true
    Sub | Div | Rem => false
    Shl | LShr | AShr => false
  }
}

///|
pub impl Show for IBinaryOpCode with output(self, logger) {
  let s = match self {
    Add => "add"
    Sub => "sub"
    Mul => "mul"
    Div => "div"
    Rem => "rem"
    And => "and"
    Or => "or"
    Xor => "xor"
    Shl => "shl"
    LShr => "lshr"
    AShr => "ashr"
  }
  logger.write_string(s)
}

///|
pub(all) enum IUnaryOpCode {
  Not
} derive(Eq)

///|
pub impl Show for IUnaryOpCode with output(self, logger) {
  let s = match self {
    Not => "not"
  }
  logger.write_string(s)
}

///|
pub(all) enum ICmpOpCode {
  Eq
  Ne
  Gt
  Ge
  Lt
  Le
  Gtu
  Geu
  Ltu
  Leu
} derive(Eq)

///|
pub impl Show for ICmpOpCode with output(self, logger) {
  let s = match self {
    Eq => "eq"
    Ne => "ne"
    Gt => "gt"
    Ge => "ge"
    Lt => "lt"
    Le => "le"
    Gtu => "gtu"
    Geu => "geu"
    Ltu => "ltu"
    Leu => "leu"
  }
  logger.write_string(s)
}

///|
pub(all) enum BranchOpCode {
  Beq
  Bne
  Bgt
  Bge
  Blt
  Ble
  Bgtu
  Bgeu
  Bltu
  Bleu
  Jmp
} derive(Eq)

///|
pub impl Show for BranchOpCode with output(self, logger) {
  let s = match self {
    Beq => "beq"
    Bne => "bne"
    Bgt => "bgt"
    Bge => "bge"
    Blt => "blt"
    Ble => "ble"
    Bgtu => "bgtu"
    Bgeu => "bgeu"
    Bltu => "bltu"
    Bleu => "bleu"
    Jmp => "jmp"
  }
  logger.write_string(s)
}

///|
pub(all) enum FBinaryOpCode {
  FAdd
  FSub
  FMul
  FDiv
  FRem
} derive(Eq)

///|
pub fn FBinaryOpCode::is_commutative(self : Self) -> Bool {
  match self {
    FAdd | FMul => true
    FSub | FDiv | FRem => false
  }
}

///|
pub impl Show for FBinaryOpCode with output(self, logger) {
  let s = match self {
    FAdd => "fadd"
    FSub => "fsub"
    FMul => "fmul"
    FDiv => "fdiv"
    FRem => "frem"
  }
  logger.write_string(s)
}

///|
pub(all) enum FUnaryOpCode {
  FNeg
} derive(Eq)

///|
pub impl Show for FUnaryOpCode with output(self, logger) {
  let s = match self {
    FNeg => "fneg"
  }
  logger.write_string(s)
}

///|
pub(all) enum FCmpOpCode {
  Feq
  Fne
  Fgt
  Fge
  Flt
  Fle
} derive(Eq)

///|
pub impl Show for FCmpOpCode with output(self, logger) {
  let s = match self {
    Feq => "feq"
    Fne => "fne"
    Fgt => "fgt"
    Fge => "fge"
    Flt => "flt"
    Fle => "fle"
  }
  logger.write_string(s)
}

///|
pub(all) enum CastOpCode {
  Trunc(Int, Int)
  ZExt(Int, Int)
  SExt(Int, Int)
  FPTrunc(Int, Int)
  FPExt(Int, Int)
  FPToSI(Int, Int)
  FPToUI(Int, Int)
  SIToFP(Int, Int)
  UIToFP(Int, Int)
} derive(Eq)

///|
pub impl Show for CastOpCode with output(self, logger) {
  let s = match self {
    Trunc(from, to) => "trunc.i\{from}_to_i\{to}"
    ZExt(from, to) => "zext.i\{from}_to_i\{to}"
    SExt(from, to) => "sext.i\{from}_to_i\{to}"
    FPTrunc(from, to) => "fptrunc.f\{from}_to_f\{to}"
    FPExt(from, to) => "fpext.f\{from}_to_f\{to}"
    FPToSI(from, to) => "fptosi.f\{from}_to_i\{to}"
    FPToUI(from, to) => "fptoui.f\{from}_to_i\{to}"
    SIToFP(from, to) => "sitofp.i\{from}_to_f\{to}"
    UIToFP(from, to) => "uitofp.i\{from}_to_f\{to}"
  }
  logger.write_string(s)
}

///|
pub(all) enum OpCode {
  IBinary(IBinaryOpCode, Int) // Int is the bit width (8, 16, 32, 64)
  IUnary(IUnaryOpCode, Int) // Int is the bit width (8, 16, 32, 64)
  ICmp(ICmpOpCode, Int) // Int is the bit width (8, 16, 32, 64)
  ILoad(Int) // Int is the bit width (8, 16, 32, 64)
  IStore(Int) // Int is the bit width (8, 16, 32, 64)
  IMove(Int) // Int is the bit width (8, 16, 32, 64)
  Branch(BranchOpCode, Int) // Int is the bit width (32, 64) for comparison
  FBinary(FBinaryOpCode, Int) // Int is the bit width (32, 64)
  FUnary(FUnaryOpCode, Int) // Int is the bit width (32, 64)
  FCmp(FCmpOpCode, Int) // Int is the bit width (32, 64)
  FLoad(Int) // Int is the bit width (32, 64)
  FStore(Int) // Int is the bit width (32, 64)
  FMove(Int) // Int is the bit width (32, 64)
  FMoveI(Int) // Int is the float bit width (32, 64), move from int to float register
  IMoveF(Int) // Int is the float bit width (32, 64), move from float to int register
  LoadAddr // Load address of a variable
  Cast(CastOpCode)
  Call
  Select
  Ret
  Nop
  Intrinsic(String)
} derive(Eq)

///|
pub fn OpCode::is_terminator(self : Self) -> Bool {
  match self {
    Branch(_) | Ret => true
    _ => false
  }
}

///|
pub impl Show for OpCode with output(self, logger) {
  let s = match self {
    IBinary(op, bits) =>
      match op {
        Add => "add.i\{bits}"
        Sub => "sub.i\{bits}"
        Mul => "mul.i\{bits}"
        Div => "div.i\{bits}"
        Rem => "rem.i\{bits}"
        And => "and.i\{bits}"
        Or => "or.i\{bits}"
        Xor => "xor.i\{bits}"
        Shl => "shl.i\{bits}"
        LShr => "lshr.i\{bits}"
        AShr => "ashr.i\{bits}"
      }
    IUnary(op, bits) =>
      match op {
        Not => "not.i\{bits}"
      }
    ICmp(op, bits) =>
      match op {
        Eq => "eq.i\{bits}"
        Ne => "ne.i\{bits}"
        Gt => "gt.i\{bits}"
        Ge => "ge.i\{bits}"
        Lt => "lt.i\{bits}"
        Le => "le.i\{bits}"
        Gtu => "gtu.i\{bits}"
        Geu => "geu.i\{bits}"
        Ltu => "ltu.i\{bits}"
        Leu => "leu.i\{bits}"
      }
    ILoad(bits) => "load.i\{bits}"
    IStore(bits) => "store.i\{bits}"
    IMove(bits) => "move.i\{bits}"
    Branch(op, _) =>
      match op {
        Beq => "beq"
        Bne => "bne"
        Bgt => "bgt"
        Bge => "bge"
        Blt => "blt"
        Ble => "ble"
        Bgtu => "bgtu"
        Bgeu => "bgeu"
        Bltu => "bltu"
        Bleu => "bleu"
        Jmp => "jmp"
      }
    FBinary(op, bits) =>
      match op {
        FAdd => "fadd.f\{bits}"
        FSub => "fsub.f\{bits}"
        FMul => "fmul.f\{bits}"
        FDiv => "fdiv.f\{bits}"
        FRem => "frem.f\{bits}"
      }
    FUnary(op, bits) =>
      match op {
        FNeg => "fneg.f\{bits}"
      }
    FCmp(op, bits) =>
      match op {
        Feq => "feq.f\{bits}"
        Fne => "fne.f\{bits}"
        Fgt => "fgt.f\{bits}"
        Fge => "fge.f\{bits}"
        Flt => "flt.f\{bits}"
        Fle => "fle.f\{bits}"
      }
    FLoad(bits) => "load.f\{bits}"
    FStore(bits) => "store.f\{bits}"
    FMove(bits) => "move.f\{bits}"
    FMoveI(bits) => "fmovei.f\{bits}"
    IMoveF(bits) => "imovef.f\{bits}"
    LoadAddr => "loadaddr"
    Cast(op) =>
      match op {
        Trunc(from, to) => "trunc.i\{from}_to_i\{to}"
        ZExt(from, to) => "zext.i\{from}_to_i\{to}"
        SExt(from, to) => "sext.i\{from}_to_i\{to}"
        FPTrunc(from, to) => "fptrunc.f\{from}_to_f\{to}"
        FPExt(from, to) => "fpext.f\{from}_to_f\{to}"
        FPToSI(from, to) => "fptosi.f\{from}_to_i\{to}"
        FPToUI(from, to) => "fptoui.f\{from}_to_i\{to}"
        SIToFP(from, to) => "sitofp.i\{from}_to_f\{to}"
        UIToFP(from, to) => "uitofp.i\{from}_to_f\{to}"
      }
    Call => "call"
    Select => "select"
    Ret => "ret"
    Nop => "nop"
    Intrinsic(name) => "intrinsic.\{name}"
  }
  logger.write_string(s)
}