diff --git a/Cargo.lock b/Cargo.lock index 00d92c3..5679893 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -121,6 +121,7 @@ dependencies = [ "num-runtime-fmt", "num-traits", "regex", + "rstest", "rustyline", "strum", "thiserror", @@ -301,6 +302,49 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-macro", + "futures-task", + "pin-project-lite", + "pin-utils", + "slab", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -312,6 +356,12 @@ dependencies = [ "wasi", ] +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + [[package]] name = "hashbrown" version = "0.15.2" @@ -558,12 +608,33 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "precomputed-hash" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro2" version = "1.0.92" @@ -641,6 +712,50 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + +[[package]] +name = "rstest" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5a3193c063baaa2a95a33f03035c8a72b83d97a54916055ba22d35ed3839d49" +dependencies = [ + "futures-timer", + "futures-util", + "rstest_macros", +] + +[[package]] +name = "rstest_macros" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c845311f0ff7951c5506121a9ad75aec44d083c31583b2ea5a30bcb0b0abba0" +dependencies = [ + "cfg-if", + "glob", + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn", + "unicode-ident", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "0.38.42" @@ -698,12 +813,44 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "siphasher" version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + [[package]] name = "smallvec" version = "1.13.2" @@ -807,6 +954,36 @@ dependencies = [ "crunchy", ] +[[package]] +name = "toml_datetime" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.23.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3effe7c0e86fdff4f69cdd2ccc1b96f933e24811c5441d44904e8683e27184b" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627" +dependencies = [ + "winnow", +] + [[package]] name = "unicode-ident" version = "1.0.14" @@ -1031,3 +1208,12 @@ name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +dependencies = [ + "memchr", +] diff --git a/Cargo.toml b/Cargo.toml index c485045..7ddcfc9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,3 +40,6 @@ cli = [ [[bin]] name = "calc" required-features = ["cli"] + +[dev-dependencies] +rstest = "0.26.1" diff --git a/src/value/arithmetic.rs b/src/value/arithmetic.rs index c259162..1b9fa70 100644 --- a/src/value/arithmetic.rs +++ b/src/value/arithmetic.rs @@ -1,15 +1,30 @@ -use std::ops; +use std::ops::{self, AddAssign, DivAssign, MulAssign, RemAssign, SubAssign}; use super::dispatch_operation; use crate::Value; +// shim for the missing method on f64 +trait CheckedAdd: Sized + ops::Add { + fn checked_add(self, rhs: Self) -> Option; +} + +impl CheckedAdd for f64 { + fn checked_add(self, rhs: Self) -> Option { + Some(self + rhs) + } +} + impl ops::AddAssign for Value where Rhs: Into, { fn add_assign(&mut self, rhs: Rhs) { let mut rhs = rhs.into(); - dispatch_operation!(self, rhs, n, |rhs| *n += rhs); + *self = dispatch_operation!(self, rhs, n, |rhs| (*n).checked_add(rhs).map(Value::from)) + .unwrap_or_else(|| { + self.promote(); + dispatch_operation!(self, rhs, n, |rhs| Value::from(*n + rhs)) + }); } } @@ -19,11 +34,8 @@ where { type Output = Value; fn add(mut self, rhs: Rhs) -> Value { - let mut rhs = rhs.into(); - dispatch_operation!(&mut self, rhs, n, |rhs| { - *n += rhs; - (*n).into() - }) + self.add_assign(rhs); + self } } @@ -47,14 +59,8 @@ where type Output = Value; fn sub(mut self, rhs: Rhs) -> Self::Output { - let mut rhs = rhs.into(); - if rhs > self { - self.promote_to_signed(); - } - dispatch_operation!(&mut self, rhs, n, |rhs| { - *n -= rhs; - (*n).into() - }) + self.sub_assign(rhs); + self } } @@ -75,11 +81,8 @@ where type Output = Value; fn mul(mut self, rhs: Rhs) -> Self::Output { - let mut rhs = rhs.into(); - dispatch_operation!(&mut self, rhs, n, |rhs| { - *n *= rhs; - (*n).into() - }) + self.mul_assign(rhs); + self } } @@ -102,13 +105,8 @@ where type Output = Value; fn div(mut self, rhs: Rhs) -> Self::Output { - self.promote_to_float(); - let mut rhs = rhs.into(); - rhs.promote_to_float(); - dispatch_operation!(&mut self, rhs, n, |rhs| { - *n /= rhs; - (*n).into() - }) + self.div_assign(rhs); + self } } @@ -129,11 +127,8 @@ where type Output = Value; fn rem(mut self, rhs: Rhs) -> Self::Output { - let mut rhs = rhs.into(); - dispatch_operation!(&mut self, rhs, n, |rhs| { - *n %= rhs; - (*n).into() - }) + self.rem_assign(rhs); + self } } @@ -146,9 +141,326 @@ impl ops::Neg for Value { Value::UnsignedInt(_) | Value::UnsignedBigInt(_) => { unreachable!("we have already promoted out of unsigned territory") } + // these integers cannot represent the negative of their minima + Value::SignedInt(n) if n == i64::MIN => { + self.promote(); + self.neg() + } + Value::SignedBigInt(n) if n == i128::MIN => { + self.promote(); + self.neg() + } + // everything else is simple negation Value::SignedInt(n) => (-n).into(), Value::SignedBigInt(n) => (-n).into(), Value::Float(n) => (-n).into(), } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::value::Order; + use rstest::rstest; + + // ---------- ADD ---------- + #[rstest] + fn add( + #[values(10_u64, 20_u128, -30_i64, -40_i128, 1.5_f64)] left: impl Into, + #[values(1_u64, 2_u128, -3_i64, -4_i128, 2.5_f64)] right: impl Into, + ) { + let left = left.into(); + let right = right.into(); + let expect_order = left.order().max(right.order()); + + let result = left + right; + assert_eq!(result.order(), expect_order); + } + + #[rstest] + fn add_assign( + #[values(10_u64, 20_u128, 30_i64, 40_i128, 50_f64)] left: impl Into, + #[values(1_u64, 2_u128, 3_i64, 4_i128, 5_f64)] right: impl Into, + ) { + let mut left = left.into(); + let right = right.into(); + let expect_order = left.order().max(right.order()); + + left += right; + assert_eq!(left.order(), expect_order); + } + + // ---------- SUB ---------- + #[rstest] + fn sub( + #[values(10_u64, 20_u128, 30_i64, 40_i128, 5.5_f64)] left: impl Into, + #[values(1_u64, 2_u128, 3_i64, 4_i128, 2.5_f64)] right: impl Into, + ) { + let left = left.into(); + let right = right.into(); + // subtraction always produces a signed order + let expect_order = left.order().max(right.order()); + + let result = left - right; + assert_eq!(result.order(), expect_order); + } + + #[rstest] + fn sub_assign( + #[values(10_u64, 20_u128, 30_i64, 40_i128, 5.5_f64)] left: impl Into, + #[values(1_u64, 2_u128, 3_i64, 4_i128, 2.5_f64)] right: impl Into, + ) { + let mut left = left.into(); + let right = right.into(); + // subtraction always produces a signed order + let expect_order = left.order().max(right.order()); + + left -= right; + assert_eq!(left.order(), expect_order); + } + + // subtraction will auto-promote to a signed type to avoid underflow + #[rstest] + fn sub_underflow( + #[values(1_u64, 2_u128, 3_i64, 4_i128, 2.5_f64)] left: impl Into, + #[values(10_u64, 20_u128, 30_i64, 40_i128, 5.5_f64)] right: impl Into, + ) { + let left = left.into(); + let right = right.into(); + // subtraction always produces a signed order + let expect_order = left.order().max(right.order()).max(Order::SignedInt); + + let result = left - right; + assert_eq!(result.order(), expect_order); + } + + #[rstest] + fn sub_assign_underflow( + #[values(1_u64, 2_u128, 3_i64, 4_i128, 2.5_f64)] left: impl Into, + #[values(10_u64, 20_u128, 30_i64, 40_i128, 5.5_f64)] right: impl Into, + ) { + let mut left = left.into(); + let right = right.into(); + // subtraction always produces a signed order + let expect_order = left.order().max(right.order()).max(Order::SignedInt); + + left -= right; + assert_eq!(left.order(), expect_order); + } + + // ---------- MUL ---------- + #[rstest] + fn mul( + #[values(2_u64, 3_u128, -4_i64, -5_i128, 1.5_f64)] left: impl Into, + #[values(2_u64, 3_u128, -4_i64, -5_i128, 2.5_f64)] right: impl Into, + ) { + let left = left.into(); + let right = right.into(); + let expect_order = left.order().max(right.order()); + + let result = left * right; + assert_eq!(result.order(), expect_order); + } + + #[rstest] + fn mul_assign( + #[values(2_u64, 3_u128, -4_i64, -5_i128, 1.5_f64)] left: impl Into, + #[values(2_u64, 3_u128, -4_i64, -5_i128, 2.5_f64)] right: impl Into, + ) { + let mut left = left.into(); + let right = right.into(); + let expect_order = left.order().max(right.order()); + + left *= right; + assert_eq!(left.order(), expect_order); + } + + // ---------- DIV ---------- + #[rstest] + fn div( + #[values(10_u64, 20_u128, -30_i64, -40_i128, 5.5_f64)] left: impl Into, + #[values(1_u64, 2_u128, -3_i64, -4_i128, 2.5_f64)] right: impl Into, + ) { + let left = left.into(); + let right = right.into(); + // division always produces a float + let expect_order = Order::Float; + + let result = left / right; + assert_eq!(result.order(), expect_order); + } + + #[test] + fn div_by_zero_produces_infinity() { + let left: Value = 10_u64.into(); + let right: Value = 0_u64.into(); + let result = left / right; + assert_eq!(result, f64::INFINITY.into()); + } + + #[rstest] + fn div_assign( + #[values(10_u64, 20_u128, -30_i64, -40_i128, 5.5_f64)] left: impl Into, + #[values(1_u64, 2_u128, -3_i64, -4_i128, 2.5_f64)] right: impl Into, + ) { + let mut left = left.into(); + let right = right.into(); + // division always produces a float + let expect_order = Order::Float; + + left /= right; + assert_eq!(left.order(), expect_order); + } + + // ---------- REM ---------- + #[rstest] + fn rem( + #[values(10_u64, 20_u128, -30_i64, -40_i128, 5.5_f64)] left: impl Into, + #[values(3_u64, 4_u128, -5_i64, -6_i128, 2.5_f64)] right: impl Into, + ) { + let left = left.into(); + let right = right.into(); + let expect_order = left.order().max(right.order()); + + let result = left % right; + assert_eq!(result.order(), expect_order); + } + + #[test] + #[should_panic] + fn rem_by_zero_panics() { + let left: Value = 10_i64.into(); + let right: Value = 0_i64.into(); + let _ = left % right; + } + + #[rstest] + fn rem_assign( + #[values(10_u64, 20_u128, -30_i64, -40_i128, 5.5_f64)] left: impl Into, + #[values(3_u64, 4_u128, -5_i64, -6_i128, 2.5_f64)] right: impl Into, + ) { + let mut left = left.into(); + let right = right.into(); + let expect_order = left.order().max(right.order()); + + left %= right; + assert_eq!(left.order(), expect_order); + } + + // ---------- NEG ---------- + #[rstest] + fn neg(#[values(10_u64, 20_u128, -30_i64, -40_i128, 5.5_f64)] val: impl Into) { + let val = val.into(); + let result = -val; + + assert!( + result.order() >= Order::SignedInt, + "negation should always yield a signed order" + ); + } + + // ---------- EDGE CASES ---------- + #[test] + fn add_large_unsigned_promotes_to_u128() { + let left: Value = u64::MAX.into(); + let right: Value = 1_u64.into(); + let result = left + right; + eprintln!("{result:?}"); + assert!(matches!(result, Value::UnsignedBigInt(_))); + } + + #[test] + fn sub_underflow_promotes_to_signed() { + let left: Value = 0_u64.into(); + let right: Value = 1_u64.into(); + let result = left - right; + assert!(matches!( + result, + Value::SignedInt(_) | Value::SignedBigInt(_) + )); + assert_eq!(result, (-1_i64).into()); + } + + // ---------- INFINITY PROPAGATION ---------- + #[test] + fn inf_plus_finite_is_inf() { + let inf: Value = (f64::INFINITY).into(); + let finite: Value = 42_u64.into(); + let result = inf + finite; + assert!(matches!(result, Value::Float(f) if f.is_infinite())); + } + + #[test] + fn inf_times_zero_is_nan_like() { + let inf: Value = (f64::INFINITY).into(); + let zero: Value = 0_u64.into(); + let result = inf * zero; + assert!(matches!(result, Value::Float(f) if f.is_nan())); + } + + #[test] + fn inf_div_inf_is_nan_like() { + let inf: Value = (f64::INFINITY).into(); + let result = inf / inf; + assert!(matches!(result, Value::Float(f) if f.is_nan())); + } + + // ---------- SIGNED ZERO ---------- + #[test] + fn zero_div_negative_one_is_negative_zero() { + let zero: Value = 0.0_f64.into(); + let neg_one: Value = (-1_i64).into(); + let result = zero / neg_one; + assert!(matches!(result, Value::Float(f) if f == 0.0 && f.is_sign_negative())); + } + + // ---------- OVERFLOW BOUNDARIES ---------- + #[test] + fn u64_max_plus_one_promotes_to_u128() { + let left: Value = u64::MAX.into(); + let right: Value = 1_u64.into(); + let result = left + right; + assert!(matches!(result, Value::UnsignedBigInt(_))); + } + + #[test] + fn i64_max_plus_one_promotes_to_i128() { + let left: Value = i64::MAX.into(); + let right: Value = 1_i64.into(); + let result = left + right; + assert!(matches!(result, Value::SignedBigInt(_))); + } + + #[test] + fn i128_min_neg_promotes() { + let val: Value = i128::MIN.into(); + let result = -val; + assert!(matches!(result, Value::Float(_))); + } + + // ---------- CROSS-TYPE INTERACTIONS ---------- + #[test] + fn unsigned_plus_negative_signed_promotes_to_signed() { + let left: Value = u64::MAX.into(); + let right: Value = (-1_i64).into(); + let result = left + right; + assert_eq!(result, Value::SignedBigInt((u64::MAX - 1) as _)); + } + + #[test] + fn float_and_int_promotes_to_float() { + let left: Value = 10_u64.into(); + let right: Value = 2.5_f64.into(); + let result = left + right; + assert!(matches!(result, Value::Float(_))); + } + + #[test] + fn float_and_bigint_promotes_to_float() { + let left: Value = u128::MAX.into(); + let right: Value = 1.5_f64.into(); + let result = left * right; + assert!(matches!(result, Value::Float(_))); + } +} diff --git a/src/value/bitwise.rs b/src/value/bitwise.rs index fb6b27d..28f36ec 100644 --- a/src/value/bitwise.rs +++ b/src/value/bitwise.rs @@ -5,21 +5,27 @@ use crate::Value; impl Value { /// Compute this value left-shifted by `other` bits, wrapping the bits around. - pub fn rotate_left(mut self, right: impl Into) -> Result { - let mut right = right.into(); - dispatch_operation!(INTS: &mut self, right, n, |rhs| { - *n <<= rhs; - (*n).into() - }) + pub fn rotate_left(self, shift: impl Into) -> Result { + let shift = shift.into().as_u32()?; + match self { + Value::UnsignedInt(n) => Ok(n.rotate_left(shift).into()), + Value::UnsignedBigInt(n) => Ok(n.rotate_left(shift).into()), + Value::SignedInt(n) => Ok(n.rotate_left(shift).into()), + Value::SignedBigInt(n) => Ok(n.rotate_left(shift).into()), + Value::Float(_) => Err(Error::ImproperlyFloat), + } } /// Compute this value right-shifted by `other` bits, wrapping the bits around. - pub fn rotate_right(mut self, right: impl Into) -> Result { - let mut right = right.into(); - dispatch_operation!(INTS: &mut self, right, n, |rhs| { - *n <<= rhs; - (*n).into() - }) + pub fn rotate_right(self, shift: impl Into) -> Result { + let shift = shift.into().as_u32()?; + match self { + Value::UnsignedInt(n) => Ok(n.rotate_right(shift).into()), + Value::UnsignedBigInt(n) => Ok(n.rotate_right(shift).into()), + Value::SignedInt(n) => Ok(n.rotate_right(shift).into()), + Value::SignedBigInt(n) => Ok(n.rotate_right(shift).into()), + Value::Float(_) => Err(Error::ImproperlyFloat), + } } } @@ -111,3 +117,182 @@ impl ops::Not for Value { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::value::Order; + use rstest::rstest; + + // ---------- SHIFT LEFT ---------- + #[rstest] + fn shl_integers( + #[values(1_u64, 1_u128, 1_i64, 1_i128)] left: impl Into, + #[values(1, 2, 3)] shift: u32, + ) { + let left = left.into(); + let result = (left << shift).unwrap(); + assert_ne!(result.order(), Order::Float); + } + + #[test] + fn shl_float_is_error() { + let left: Value = 1.5_f64.into(); + let result = left << 1_u32; + assert!(matches!(result, Err(Error::ImproperlyFloat))); + } + + // ---------- SHIFT RIGHT ---------- + #[rstest] + fn shr_integers( + #[values(8_u64, 8_u128, 8_i64, 8_i128)] left: impl Into, + #[values(1, 2)] shift: u32, + ) { + let left = left.into(); + let result = (left >> shift).unwrap(); + assert_ne!(result.order(), Order::Float); + } + + #[test] + fn shr_float_is_error() { + let left: Value = 8.0_f64.into(); + let result = left >> 1_u32; + assert!(matches!(result, Err(Error::ImproperlyFloat))); + } + + // ---------- BITWISE AND ---------- + #[rstest] + fn bitand_integers( + #[values(0b1010_u64, 0b1010_u128, 0b1010_i64, 0b1010_i128)] left: impl Into, + #[values(0b1100_u64, 0b1100_u128, 0b1100_i64, 0b1100_i128)] right: impl Into, + ) { + let left = left.into(); + let right = right.into(); + let result = (left & right).unwrap(); + assert_ne!(result.order(), Order::Float); + } + + #[test] + fn bitand_float_is_error() { + let left: Value = 1.0_f64.into(); + let right: Value = 2_u64.into(); + let result = left & right; + assert!(matches!(result, Err(Error::ImproperlyFloat))); + } + + // ---------- BITWISE OR ---------- + #[rstest] + fn bitor_integers( + #[values(0b1010_u64, 0b1010_u128, 0b1010_i64, 0b1010_i128)] left: impl Into, + #[values(0b0101_u64, 0b0101_u128, 0b0101_i64, 0b0101_i128)] right: impl Into, + ) { + let left = left.into(); + let right = right.into(); + let result = (left | right).unwrap(); + assert_ne!(result.order(), Order::Float); + } + + #[test] + fn bitor_float_is_error() { + let left: Value = 1.0_f64.into(); + let right: Value = 2_u64.into(); + let result = left | right; + assert!(matches!(result, Err(Error::ImproperlyFloat))); + } + + // ---------- BITWISE XOR ---------- + #[rstest] + fn bitxor_integers( + #[values(0b1010_u64, 0b1010_u128, 0b1010_i64, 0b1010_i128)] left: impl Into, + #[values(0b1100_u64, 0b1100_u128, 0b1100_i64, 0b1100_i128)] right: impl Into, + ) { + let left = left.into(); + let right = right.into(); + let result = (left ^ right).unwrap(); + assert_ne!(result.order(), Order::Float); + } + + #[test] + fn bitxor_float_is_error() { + let left: Value = 1.0_f64.into(); + let right: Value = 2_u64.into(); + let result = left ^ right; + assert!(matches!(result, Err(Error::ImproperlyFloat))); + } + + // ---------- BITWISE NOT ---------- + #[rstest] + fn not_integers(#[values(0_u64, 0_u128, 0_i64, 0_i128)] val: impl Into) { + let val = val.into(); + let result = (!val).unwrap(); + assert_ne!(result.order(), Order::Float); + } + + #[test] + fn not_float_is_error() { + let val: Value = 1.0_f64.into(); + let result = !val; + assert!(matches!(result, Err(Error::ImproperlyFloat))); + } + + // ---------- ROTATE LEFT ---------- + #[rstest] + fn rotate_left_integers( + #[values(0b0001_u64, 0b0001_u128, 0b0001_i64, 0b0001_i128)] val: impl Into, + #[values(1, 2, 63)] shift: u32, + ) { + let val = val.into(); + let result = val.rotate_left(shift).unwrap(); + assert_ne!(result.order(), Order::Float); + } + + #[test] + fn rotate_left_float_is_error() { + let val: Value = 1.0_f64.into(); + let result = val.rotate_left(1); + assert!(matches!(result, Err(Error::ImproperlyFloat))); + } + + // ---------- ROTATE RIGHT ---------- + #[rstest] + fn rotate_right_integers( + #[values(0b1000_u64, 0b1000_u128, 0b1000_i64, 0b1000_i128)] val: impl Into, + #[values(1, 2, 63)] shift: u32, + ) { + let val = val.into(); + let result = val.rotate_right(shift).unwrap(); + assert_ne!(result.order(), Order::Float); + } + + #[test] + fn rotate_right_float_is_error() { + let val: Value = 1.0_f64.into(); + let result = val.rotate_right(1); + assert!(matches!(result, Err(Error::ImproperlyFloat))); + } + + // ---------- CROSS-TYPE BITWISE ---------- + #[test] + fn u64_and_u128_promotes_to_u128() { + let left: Value = 0b1010_u64.into(); + let right: Value = 0b1100_u128.into(); + let result = (left & right).unwrap(); + assert!(matches!(result, Value::UnsignedBigInt(_))); + } + + #[test] + fn i64_or_i128_promotes_to_i128() { + let left: Value = 0b1010_i64.into(); + let right: Value = 0b0101_i128.into(); + let result = (left | right).unwrap(); + assert!(matches!(result, Value::SignedBigInt(_))); + } + + #[test] + fn u64_xor_i64_promotes_to_signed() { + let left: Value = 0b1111_u64.into(); + let right: Value = (-1_i64).into(); + let result = (left ^ right).unwrap(); + assert!(matches!(result, Value::SignedInt(_))); + } +} diff --git a/src/value/comparison.rs b/src/value/comparison.rs index b51299e..94e531c 100644 --- a/src/value/comparison.rs +++ b/src/value/comparison.rs @@ -55,3 +55,104 @@ impl Value { .then_with(|| self.cmp(&other)) } } + +#[cfg(test)] +mod ordering_tests { + use super::*; + use rstest::rstest; + + // ---------- EQUALITY ---------- + #[rstest] + fn equality_across_variants( + #[values(42_u64, 42_u128, 42_i64, 42_i128, 42.0_f64)] left: impl Into, + ) { + let left = left.into(); + let right: Value = 42_u64.into(); + // All representations of 42 should be equal + assert_eq!(left, right); + } + + #[test] + fn float_total_cmp_nan_behavior() { + let nan1: Value = f64::NAN.into(); + let nan2: Value = f64::NAN.into(); + // NaN is not equal to NaN under PartialEq + assert_ne!(nan1, nan2); + // But ordering is defined via total_cmp + assert_eq!(nan1.partial_cmp(&nan2), Some(Ordering::Equal)); + } + + // ---------- ORDERING ---------- + #[rstest] + fn ordering_across_variants( + #[values(1_u64, 1_u128, 1_i64, 1_i128, 1.0_f64)] one: impl Into, + #[values(2_u64, 2_u128, 2_i64, 2_i128, 2.0_f64)] two: impl Into, + ) { + let one = one.into(); + let two = two.into(); + // All representations of 1 < 2 + assert!(one < two); + } + + #[test] + fn float_ordering_total_cmp() { + let neg_zero: Value = (-0.0_f64).into(); + let pos_zero: Value = 0.0_f64.into(); + // total_cmp distinguishes -0.0 and +0.0 ordering + assert_eq!(neg_zero.cmp(&pos_zero), f64::total_cmp(&-0.0, &0.0)); + } + + // ---------- STRICT EQUALITY ---------- + #[test] + fn strict_eq_same_variant_same_value() { + let a: Value = 42_u64.into(); + let b: Value = 42_u64.into(); + assert!(a.strict_eq(b)); + } + + #[test] + fn strict_eq_different_variants_same_value() { + let a: Value = 42_u64.into(); + let b: Value = 42_u128.into(); + // Same numeric value, but different variants + assert!(!a.strict_eq(b)); + } + + // ---------- STRICT ORDERING ---------- + #[test] + fn strict_cmp_same_variant() { + let a: Value = 10_i64.into(); + let b: Value = 20_i64.into(); + assert_eq!(a.strict_cmp(b), Ordering::Less); + } + + #[test] + fn strict_cmp_different_variants() { + let a: Value = 10_u64.into(); + let b: Value = 10_u128.into(); + // Different variants: strict_cmp should order by variant discriminant first + assert_eq!(a.strict_cmp(b), Ordering::Less); + } + + // ---------- EDGE CASES ---------- + #[test] + fn equality_large_values_promote() { + let a: Value = u64::MAX.into(); + let b: Value = (u64::MAX as u128).into(); + assert_eq!(a, b); + } + + #[test] + fn ordering_signed_vs_unsigned() { + let a: Value = (-1_i64).into(); + let b: Value = 1_u64.into(); + assert!(a < b); + } + + #[test] + fn ordering_float_vs_int() { + let a: Value = 3.5_f64.into(); + let b: Value = 4_i64.into(); + assert!(a < b); + } +} diff --git a/src/value/conversion.rs b/src/value/conversion.rs new file mode 100644 index 0000000..4f2e98f --- /dev/null +++ b/src/value/conversion.rs @@ -0,0 +1,19 @@ +use crate::Value; + +macro_rules! impl_from { + ($t:ty => $variant:ident) => { + impl From<$t> for Value { + fn from(value: $t) -> Self { + Value::$variant(value as _) + } + } + }; +} + +impl_from!(u8 => UnsignedInt); +impl_from!(u16 => UnsignedInt); +impl_from!(u32 => UnsignedInt); + +impl_from!(i8 => SignedInt); +impl_from!(i16 => SignedInt); +impl_from!(i32 => SignedInt); diff --git a/src/value/mod.rs b/src/value/mod.rs index 07f8f2d..36f5ba0 100644 --- a/src/value/mod.rs +++ b/src/value/mod.rs @@ -1,6 +1,7 @@ mod arithmetic; mod bitwise; mod comparison; +mod conversion; mod error; mod format; mod numeric; @@ -272,7 +273,7 @@ impl Value { let narrowest_order = [ (ZERO..=UI_MAX, Order::UnsignedInt), - (ZERO..=UBI_MAX, Order::SignedBigInt), + (ZERO..=UBI_MAX, Order::UnsignedBigInt), (SI_MIN..=SI_MAX, Order::SignedInt), (SBI_MIN..=SBI_MAX, Order::SignedBigInt), ] diff --git a/src/value/numeric.rs b/src/value/numeric.rs index cfde111..f6a6dd8 100644 --- a/src/value/numeric.rs +++ b/src/value/numeric.rs @@ -3,7 +3,7 @@ use crate::Value; use super::{ArithmeticError, Error, Result}; impl Value { - fn as_u32(self) -> Result { + pub(crate) fn as_u32(self) -> Result { match self { Value::UnsignedInt(n) => u32::try_from(n).map_err(|_| ArithmeticError::Overflow.into()), Value::UnsignedBigInt(n) => { @@ -296,3 +296,163 @@ impl Value { self } } + +#[cfg(test)] +mod demotion_tests { + use super::*; + use crate::value::Order; + use rstest::rstest; + + // ---------- TRUNC_DIV ---------- + #[rstest] + fn trunc_div_integers( + #[values(10_u64, 20_u128, -30_i64, -40_i128)] left: impl Into, + #[values(2_u64, 5_u128, -3_i64, -4_i128)] right: impl Into, + ) { + let left = left.into(); + let right = right.into(); + let result = left.trunc_div(right); + // Integral inputs remain integral + assert_ne!(result.order(), Order::Float); + } + + #[rstest] + fn trunc_div_float_demotes( + #[values(10.5_f64, -20.9_f64, 1.0_f64)] left: f64, + #[values(2.0_f64, -3.0_f64)] right: f64, + ) { + let left: Value = left.into(); + let right: Value = right.into(); + let result = left.trunc_div(right); + // Floats demote to an integer type + assert_ne!(result.order(), Order::Float); + } + + // ---------- CEIL ---------- + #[rstest] + fn ceil_integers_remain_integral( + #[values(10_u64, 20_u128, -30_i64, -40_i128)] val: impl Into, + ) { + let val = val.into(); + let result = val.ceil(); + assert_eq!(result.order(), val.order()); + } + + #[rstest] + fn ceil_float_demotes(#[values(1.2_f64, -1.8_f64, 1000.0_f64)] val: f64) { + let val: Value = val.into(); + let result = val.ceil(); + assert_ne!(result.order(), Order::Float); + } + + // ---------- FLOOR ---------- + #[rstest] + fn floor_integers_remain_integral( + #[values(10_u64, 20_u128, -30_i64, -40_i128)] val: impl Into, + ) { + let val = val.into(); + let result = val.floor(); + assert_eq!(result.order(), val.order()); + } + + #[rstest] + fn floor_float_demotes(#[values(1.2_f64, -1.8_f64, 1000.0_f64)] val: f64) { + let val: Value = val.into(); + let result = val.floor(); + assert_ne!(result.order(), Order::Float); + } + + // ---------- ROUND ---------- + #[rstest] + fn round_integers_remain_integral( + #[values(10_u64, 20_u128, -30_i64, -40_i128)] val: impl Into, + ) { + let val = val.into(); + let result = val.round(); + assert_eq!(result.order(), val.order()); + } + + #[rstest] + fn round_float_demotes(#[values(1.2_f64, 1.5_f64, -1.8_f64, -2.5_f64)] val: f64) { + let val: Value = val.into(); + let result = val.round(); + assert_ne!(result.order(), Order::Float); + } + + // ---------- EDGE CASES ---------- + #[test] + fn ceil_of_large_float_demotes_to_bigint() { + let val: Value = (u64::MAX as f64 * 1.5).into(); + let result = val.ceil(); + // Should require promotion to a big integer type + assert!(matches!(result, Value::UnsignedBigInt(_))); + } + + #[test] + fn floor_of_negative_float_demotes_to_signed() { + let val: Value = (-123.45_f64).into(); + let result = val.floor(); + assert!(matches!(result, Value::SignedInt(_))); + } + + // ---------- FLOAT ÷ INT ---------- + #[rstest] + fn trunc_div_float_div_int_demotes( + #[values(10.9_f64, -20.1_f64, 1.5_f64)] left: f64, + #[values(2_u64, 3_i64, 4_u128, -5_i128)] right: impl Into, + ) { + let left: Value = left.into(); + let right = right.into(); + let result = left.trunc_div(right); + // Floats should demote to an integer type + assert_ne!(result.order(), Order::Float); + } + + // ---------- INT ÷ FLOAT ---------- + #[rstest] + fn trunc_div_int_div_float_demotes( + #[values(10_u64, -20_i64, 30_u128, -40_i128)] left: impl Into, + #[values(2.5_f64, -3.5_f64, 1000.0_f64)] right: f64, + ) { + let left = left.into(); + let right: Value = right.into(); + let result = left.trunc_div(right); + // Floats should demote to an integer type + assert_ne!(result.order(), Order::Float); + } + + // ---------- EDGE CASES ---------- + #[test] + fn trunc_div_large_float_by_int_promotes_to_bigint() { + let left: Value = (u64::MAX as f64 * 3.0).into(); + let right: Value = 2_u64.into(); + let result = left.trunc_div(right); + // Result should require a big integer type + assert!(matches!(result, Value::UnsignedBigInt(_))); + } + + #[test] + fn trunc_div_middling_large_float_by_int_promotes_to_bigint() { + let left: Value = (u64::MAX as f64 * 1.5).into(); + let right: Value = 2_u64.into(); + let result = left.trunc_div(right); + // Result should require a big integer type + assert!(matches!(result, Value::UnsignedInt(_))); + } + + #[test] + fn trunc_div_negative_float_by_positive_int_is_signed() { + let left: Value = (-123.45_f64).into(); + let right: Value = 2_u64.into(); + let result = left.trunc_div(right); + assert!(matches!(result, Value::SignedInt(_))); + } + + #[test] + fn trunc_div_positive_int_by_negative_float_is_signed() { + let left: Value = 123_u64.into(); + let right: Value = (-2.5_f64).into(); + let result = left.trunc_div(right); + assert!(matches!(result, Value::SignedInt(_))); + } +}