use std::fmt;
use num_enum::{IntoPrimitive, TryFromPrimitive};
use serde::{Deserialize, Serialize};
use strum::VariantArray;
#[derive(
Clone,
Copy,
Debug,
PartialEq,
Eq,
Hash,
IntoPrimitive,
TryFromPrimitive,
VariantArray,
Serialize,
Deserialize,
)]
#[repr(u16)]
#[doc(alias("ctype_t"))]
pub enum BinaryOp {
Comma = 1,
LogOr = 17,
LogAnd = 18,
BitOr = 19,
BitXor = 20,
BitAnd = 21,
Eq = 22,
Ne = 23,
Sge = 24,
Uge = 25,
Sle = 26,
Ule = 27,
Sgt = 28,
Ugt = 29,
Slt = 30,
Ult = 31,
Sshr = 32,
Ushr = 33,
Shl = 34,
Add = 35,
Sub = 36,
Mul = 37,
Sdiv = 38,
Udiv = 39,
Smod = 40,
Umod = 41,
Fadd = 42,
Fsub = 43,
Fmul = 44,
Fdiv = 45,
}
#[derive(
Clone,
Copy,
Debug,
PartialEq,
Eq,
Hash,
IntoPrimitive,
TryFromPrimitive,
VariantArray,
Serialize,
Deserialize,
)]
#[repr(u16)]
#[doc(alias("ctype_t"))]
pub enum AssignmentOp {
Assign = 2,
BitOrAssign = 3,
BitXorAssign = 4,
BitAndAssign = 5,
AddAssign = 6,
SubAssign = 7,
MulAssign = 8,
SshrAssign = 9,
UshrAssign = 10,
ShlAssign = 11,
SdivAssign = 12,
UdivAssign = 13,
SmodAssign = 14,
UmodAssign = 15,
}
#[derive(
Clone,
Copy,
Debug,
PartialEq,
Eq,
Hash,
IntoPrimitive,
TryFromPrimitive,
VariantArray,
Serialize,
Deserialize,
)]
#[repr(u16)]
#[doc(alias("ctype_t"))]
pub enum UnaryOp {
FNeg = 46,
Neg = 47,
LogNot = 49,
BitNot = 50,
Ref = 52,
PostInc = 53,
PostDec = 54,
PreInc = 55,
PreDec = 56,
}
impl BinaryOp {
#[must_use]
pub fn symbol(self) -> &'static str {
match self {
Self::Comma => ",",
Self::LogOr => "||",
Self::LogAnd => "&&",
Self::BitOr => "|",
Self::BitXor => "^",
Self::BitAnd => "&",
Self::Eq => "==",
Self::Ne => "!=",
Self::Sge | Self::Uge => ">=",
Self::Sle | Self::Ule => "<=",
Self::Sgt | Self::Ugt => ">",
Self::Slt | Self::Ult => "<",
Self::Sshr | Self::Ushr => ">>",
Self::Shl => "<<",
Self::Add | Self::Fadd => "+",
Self::Sub | Self::Fsub => "-",
Self::Mul | Self::Fmul => "*",
Self::Sdiv | Self::Udiv | Self::Fdiv => "/",
Self::Smod | Self::Umod => "%",
}
}
}
impl fmt::Display for BinaryOp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.symbol())
}
}
impl AssignmentOp {
#[must_use]
pub fn symbol(self) -> &'static str {
match self {
Self::Assign => "=",
Self::BitOrAssign => "|=",
Self::BitXorAssign => "^=",
Self::BitAndAssign => "&=",
Self::AddAssign => "+=",
Self::SubAssign => "-=",
Self::MulAssign => "*=",
Self::SshrAssign | Self::UshrAssign => ">>=",
Self::ShlAssign => "<<=",
Self::SdivAssign | Self::UdivAssign => "/=",
Self::SmodAssign | Self::UmodAssign => "%=",
}
}
}
impl fmt::Display for AssignmentOp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.symbol())
}
}
impl UnaryOp {
#[must_use]
pub fn symbol(self) -> &'static str {
match self {
Self::FNeg | Self::Neg => "-",
Self::LogNot => "!",
Self::BitNot => "~",
Self::Ref => "&",
Self::PreInc | Self::PostInc => "++",
Self::PreDec | Self::PostDec => "--",
}
}
}
impl fmt::Display for UnaryOp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.symbol())
}
}
#[cfg(test)]
mod tests {
use assert2::assert;
use idakit_sys as sys;
use rstest::rstest;
use super::*;
#[rstest]
#[case(BinaryOp::Comma, 1)]
#[case(BinaryOp::Add, 35)]
#[case(BinaryOp::Sdiv, 38)]
#[case(BinaryOp::Fdiv, 45)]
fn binop_raw_matches_ctype(#[case] op: BinaryOp, #[case] raw: u16) {
assert!(u16::from(op) == raw);
assert!(BinaryOp::try_from(raw).ok() == Some(op));
}
#[rstest]
#[case(AssignmentOp::Assign, 2)]
#[case(AssignmentOp::UmodAssign, 15)]
fn assignop_raw_matches_ctype(#[case] op: AssignmentOp, #[case] raw: u16) {
assert!(u16::from(op) == raw);
assert!(AssignmentOp::try_from(raw).ok() == Some(op));
}
#[rstest]
#[case(UnaryOp::Neg, 47)]
#[case(UnaryOp::PreDec, 56)]
fn unop_raw_matches_ctype(#[case] op: UnaryOp, #[case] raw: u16) {
assert!(u16::from(op) == raw);
assert!(UnaryOp::try_from(raw).ok() == Some(op));
}
#[rstest]
#[case::asg_is_not_binary(2)]
#[case::empty_is_not_binary(0)]
fn binop_rejects_non_binary(#[case] v: u16) {
assert!(BinaryOp::try_from(v).is_err());
}
#[test]
fn try_from_rejects_cross_group_discriminants() {
assert!(AssignmentOp::try_from(35).is_err());
assert!(UnaryOp::try_from(48).is_err());
assert!(UnaryOp::try_from(51).is_err());
}
#[rstest]
#[case(BinaryOp::Add, "+")]
#[case(BinaryOp::Fadd, "+")]
#[case(BinaryOp::LogAnd, "&&")]
#[case(BinaryOp::Shl, "<<")]
#[case(BinaryOp::Sdiv, "/")]
#[case(BinaryOp::Udiv, "/")]
#[case(BinaryOp::Fdiv, "/")]
fn binop_symbol(#[case] op: BinaryOp, #[case] sym: &str) {
assert!(op.symbol() == sym);
}
#[rstest]
#[case(AssignmentOp::Assign, "=")]
#[case(AssignmentOp::SshrAssign, ">>=")]
#[case(AssignmentOp::UshrAssign, ">>=")]
fn assignop_symbol(#[case] op: AssignmentOp, #[case] sym: &str) {
assert!(op.symbol() == sym);
}
#[rstest]
#[case(UnaryOp::Neg, "-")]
#[case(UnaryOp::FNeg, "-")]
#[case(UnaryOp::PreInc, "++")]
#[case(UnaryOp::PostInc, "++")]
fn unop_symbol(#[case] op: UnaryOp, #[case] sym: &str) {
assert!(op.symbol() == sym);
}
#[test]
fn every_variant_has_a_symbol() {
for op in BinaryOp::VARIANTS {
assert!(!op.symbol().is_empty());
}
for op in AssignmentOp::VARIANTS {
assert!(!op.symbol().is_empty());
}
for op in UnaryOp::VARIANTS {
assert!(!op.symbol().is_empty());
}
}
#[test]
fn display_matches_symbol() {
assert!(BinaryOp::Add.to_string() == BinaryOp::Add.symbol());
assert!(AssignmentOp::Assign.to_string() == AssignmentOp::Assign.symbol());
assert!(UnaryOp::PreInc.to_string() == UnaryOp::PreInc.symbol());
}
#[test]
fn serde_round_trips() {
let json = serde_json::to_string(&BinaryOp::Sdiv).unwrap();
assert!(serde_json::from_str::<BinaryOp>(&json).unwrap() == BinaryOp::Sdiv);
let json = serde_json::to_string(&AssignmentOp::AddAssign).unwrap();
assert!(serde_json::from_str::<AssignmentOp>(&json).unwrap() == AssignmentOp::AddAssign);
let json = serde_json::to_string(&UnaryOp::LogNot).unwrap();
assert!(serde_json::from_str::<UnaryOp>(&json).unwrap() == UnaryOp::LogNot);
}
#[test]
fn ctype_ids_align_with_the_facade() {
fn check<T: Copy + fmt::Debug + Into<u16>>(name: &str, variants: &[T], ids: &[u32]) {
assert!(
ids.len() == variants.len(),
"{name}: facade lists {} ids for {} variants",
ids.len(),
variants.len()
);
for (i, &op) in variants.iter().enumerate() {
let raw = u32::from(op.into());
assert!(
ids[i] == raw,
"{name} {op:?}: facade ctype_t {} != discriminant {raw}",
ids[i]
);
}
}
check("BinaryOp", BinaryOp::VARIANTS, &sys::binop_ctype_ids());
check(
"AssignmentOp",
AssignmentOp::VARIANTS,
&sys::assignop_ctype_ids(),
);
check("UnaryOp", UnaryOp::VARIANTS, &sys::unop_ctype_ids());
}
#[test]
fn every_variant_round_trips_through_its_discriminant() {
for &op in BinaryOp::VARIANTS {
assert!(BinaryOp::try_from(u16::from(op)).ok() == Some(op));
}
for &op in AssignmentOp::VARIANTS {
assert!(AssignmentOp::try_from(u16::from(op)).ok() == Some(op));
}
for &op in UnaryOp::VARIANTS {
assert!(UnaryOp::try_from(u16::from(op)).ok() == Some(op));
}
}
mod proptests {
use proptest::prelude::*;
use super::*;
proptest! {
#[test]
fn discriminant_groups_are_mutually_exclusive(raw in any::<u16>()) {
let matches = [
BinaryOp::try_from(raw).is_ok(),
AssignmentOp::try_from(raw).is_ok(),
UnaryOp::try_from(raw).is_ok(),
]
.into_iter()
.filter(|&m| m)
.count();
prop_assert!(matches <= 1, "raw {raw} matched {matches} operator groups");
}
#[test]
fn successful_round_trip_preserves_the_raw_value(raw in any::<u16>()) {
if let Ok(op) = BinaryOp::try_from(raw) {
prop_assert_eq!(u16::from(op), raw);
}
if let Ok(op) = AssignmentOp::try_from(raw) {
prop_assert_eq!(u16::from(op), raw);
}
if let Ok(op) = UnaryOp::try_from(raw) {
prop_assert_eq!(u16::from(op), raw);
}
}
}
}
}