From 59792f2a5379db9ebed678006358843cea99812c Mon Sep 17 00:00:00 2001 From: 0xgleb Date: Mon, 16 Jun 2025 18:02:12 +0400 Subject: [PATCH 01/10] mul/div --- crates/float/src/lib.rs | 84 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 83 insertions(+), 1 deletion(-) diff --git a/crates/float/src/lib.rs b/crates/float/src/lib.rs index 208f0fe2..35d36082 100644 --- a/crates/float/src/lib.rs +++ b/crates/float/src/lib.rs @@ -12,7 +12,7 @@ use revm::interpreter::interpreter::EthInterpreter; use revm::primitives::{address, fixed_bytes}; use revm::{Context, MainBuilder, MainContext, SystemCallEvm}; use std::cell::RefCell; -use std::ops::{Add, Sub}; +use std::ops::{Add, Div, Mul, Sub}; use std::thread::AccessError; use thiserror::Error; @@ -262,6 +262,36 @@ impl Sub for Float { } } +impl Mul for Float { + type Output = Result; + + fn mul(self, b: Self) -> Self::Output { + let Float(a) = self; + let Float(b) = b; + let calldata = DecimalFloat::mulCall { a, b }.abi_encode(); + + execute_call(Bytes::from(calldata), |output| { + let decoded = DecimalFloat::mulCall::abi_decode_returns(output.as_ref())?; + Ok(Float(decoded)) + }) + } +} + +impl Div for Float { + type Output = Result; + + fn div(self, b: Self) -> Self::Output { + let Float(a) = self; + let Float(b) = b; + let calldata = DecimalFloat::divCall { a, b }.abi_encode(); + + execute_call(Bytes::from(calldata), |output| { + let decoded = DecimalFloat::divCall::abi_decode_returns(output.as_ref())?; + Ok(Float(decoded)) + }) + } +} + #[cfg(test)] mod tests { use super::*; @@ -415,4 +445,56 @@ mod tests { prop_assert!(!(lt && gt), "both less than and greater than: a: {a_str}, b: {b_str}"); } } + + proptest! { + #[test] + fn test_mul(a in reasonable_float(), b in reasonable_float()) { + (a * b).unwrap(); + } + } + + proptest! { + #[test] + fn test_div(a in reasonable_float(), b in reasonable_float()) { + let zero = Float::parse("0".to_string()).unwrap(); + prop_assume!(!b.eq(zero).unwrap()); + + (a / b).unwrap(); + } + } + + prop_compose! { + fn small_int_float()(int_part in -1_000_000_000_000i128..1_000_000_000_000i128) -> Float { + Float::parse(int_part.to_string()).unwrap() + } + } + + proptest! { + #[test] + fn test_mul_div_int(a in small_int_float(), b in small_int_float()) { + let zero = Float::parse("0".to_string()).unwrap(); + prop_assume!(!b.eq(zero).unwrap()); + + let product = (a * b).unwrap(); + let quotient = (product / b).unwrap(); + + prop_assert!( + a.eq(quotient).unwrap(), + "a: {}, quotient: {}, b: {}", + a.show_unpacked().unwrap(), + quotient.show_unpacked().unwrap(), + b.show_unpacked().unwrap() + ); + } + } + + #[test] + fn test_mul_div_manual() { + let two = Float::parse("2".to_string()).unwrap(); + let three = Float::parse("3".to_string()).unwrap(); + let six = Float::parse("6".to_string()).unwrap(); + + assert!(two.eq((six / three).unwrap()).unwrap()); + assert!(six.eq((two * three).unwrap()).unwrap()); + } } From 57c8d41062f1c0fb9126ca12740da17cd145121c Mon Sep 17 00:00:00 2001 From: 0xgleb Date: Mon, 16 Jun 2025 18:21:19 +0400 Subject: [PATCH 02/10] cover more error cases --- crates/float/src/lib.rs | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/crates/float/src/lib.rs b/crates/float/src/lib.rs index 35d36082..331e5273 100644 --- a/crates/float/src/lib.rs +++ b/crates/float/src/lib.rs @@ -188,6 +188,7 @@ impl Float { }) } + // NOTE: LibFormatDecimalFloat.toDecimalString currently uses 18 decimal places pub fn format(self) -> Result { let Float(a) = self; let calldata = DecimalFloat::formatCall { a }.abi_encode(); @@ -324,8 +325,6 @@ mod tests { #[test] fn test_parse_and_format() { let float = Float::parse("1.1341234234625468391".to_string()).unwrap(); - // NOTE: LibFormatDecimalFloat.toDecimalString currently uses 18 decimal places - // TODO: make this fail on a separate PR let err = float.format().unwrap_err(); assert!(matches!( @@ -334,6 +333,23 @@ mod tests { )); } + #[test] + fn test_parse_empty_string_error() { + let err = Float::parse("".to_string()).unwrap_err(); + // We don't know the exact selector here, just ensure the error path is hit. + assert!(matches!(err, FloatError::DecimalFloatSelector(_))); + } + + #[test] + fn test_parse_exponent_overflow_error() { + // Extremely large exponent expected to overflow (exponent >> i32::MAX). + let err = Float::parse("1e3000000000".to_string()).unwrap_err(); + assert!(matches!( + err, + FloatError::DecimalFloat(DecimalFloatErrors::ExponentOverflow(_)) + )); + } + #[test] fn test_parse_edge_cases() { let err = Float::parse("1.2.3".to_string()).unwrap_err(); @@ -497,4 +513,18 @@ mod tests { assert!(two.eq((six / three).unwrap()).unwrap()); assert!(six.eq((two * three).unwrap()).unwrap()); } + + #[test] + fn test_divide_by_zero_error() { + let one = Float::parse("1".to_string()).unwrap(); + let zero = Float::parse("0".to_string()).unwrap(); + let err = (one / zero).unwrap_err(); + // Division by zero should revert or return a DecimalFloat error/selector. + match err { + FloatError::DecimalFloat(_) + | FloatError::DecimalFloatSelector(_) + | FloatError::Revert(_) => {} + _ => panic!("Unexpected error type: {err:?}"), + } + } } From db51d129585dbeca50ec65e8aac601794e73f351 Mon Sep 17 00:00:00 2001 From: 0xgleb Date: Mon, 16 Jun 2025 18:49:46 +0400 Subject: [PATCH 03/10] cover more error cases --- crates/float/src/lib.rs | 45 ++++++++++++++++++++++++++++++++++------- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/crates/float/src/lib.rs b/crates/float/src/lib.rs index 331e5273..81f08e92 100644 --- a/crates/float/src/lib.rs +++ b/crates/float/src/lib.rs @@ -519,12 +519,43 @@ mod tests { let one = Float::parse("1".to_string()).unwrap(); let zero = Float::parse("0".to_string()).unwrap(); let err = (one / zero).unwrap_err(); - // Division by zero should revert or return a DecimalFloat error/selector. - match err { - FloatError::DecimalFloat(_) - | FloatError::DecimalFloatSelector(_) - | FloatError::Revert(_) => {} - _ => panic!("Unexpected error type: {err:?}"), - } + + assert!(matches!(err, FloatError::Revert(_))); + } + + #[test] + fn test_mul_exponent_overflow_error() { + let near_max_exp = Float::parse("1e2147483646".to_string()).unwrap(); + let one_e_two = Float::parse("1e2".to_string()).unwrap(); + + let err = (near_max_exp * one_e_two).unwrap_err(); + assert!(matches!( + err, + FloatError::DecimalFloat(DecimalFloatErrors::ExponentOverflow(_)) + )); + } + + #[test] + fn test_div_exponent_overflow_error() { + let near_max_exp = Float::parse("1e2147483646".to_string()).unwrap(); + let one_e_neg_hundred = Float::parse("1e-100".to_string()).unwrap(); + + let err = (near_max_exp / one_e_neg_hundred).unwrap_err(); + assert!(matches!( + err, + FloatError::DecimalFloat(DecimalFloatErrors::ExponentOverflow(_)) + )); + } + + #[test] + fn test_mul_exponent_underflow_error() { + let near_min_exp = Float::parse("1e-2147483646".to_string()).unwrap(); + let one_e_neg_three = Float::parse("1e-3".to_string()).unwrap(); + + let err = (near_min_exp * one_e_neg_three).unwrap_err(); + assert!(matches!( + err, + FloatError::DecimalFloat(DecimalFloatErrors::ExponentOverflow(_)) + )); } } From 1dd2427da685fa4f031309d83b240309d6e5d9bb Mon Sep 17 00:00:00 2001 From: 0xgleb Date: Mon, 16 Jun 2025 19:26:57 +0400 Subject: [PATCH 04/10] add error test cases for add/sub --- crates/float/src/lib.rs | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/crates/float/src/lib.rs b/crates/float/src/lib.rs index 81f08e92..1d1dc972 100644 --- a/crates/float/src/lib.rs +++ b/crates/float/src/lib.rs @@ -296,6 +296,7 @@ impl Div for Float { #[cfg(test)] mod tests { use super::*; + use core::str::FromStr; use proptest::prelude::*; prop_compose! { @@ -376,6 +377,39 @@ mod tests { } } + #[test] + fn test_add_exponent_overflow_error() { + let max_coeff_str = "13479973333575319897333507543509815336818572211270286240551805124607"; + let large_coeff_i224 = I224::from_str(max_coeff_str).unwrap(); + let exponent_max = i32::MAX; + + let a = Float::pack_lossless(large_coeff_i224, exponent_max).unwrap(); + + let err = (a + a).unwrap_err(); + + assert!(matches!( + err, + FloatError::DecimalFloat(DecimalFloatErrors::ExponentOverflow(_)) + )); + } + + #[test] + fn test_sub_exponent_overflow_error() { + let max_coeff_str = "13479973333575319897333507543509815336818572211270286240551805124607"; + let large_coeff_i224 = I224::from_str(max_coeff_str).unwrap(); + let exponent_max = i32::MAX; + + let a = Float::pack_lossless(large_coeff_i224, exponent_max).unwrap(); + let b = Float::pack_lossless(-large_coeff_i224, exponent_max).unwrap(); + + let err = (b - a).unwrap_err(); + + assert!(matches!( + err, + FloatError::DecimalFloat(DecimalFloatErrors::ExponentOverflow(_)) + )); + } + proptest! { #[test] fn test_add(a in reasonable_float(), b in reasonable_float()) { From b7246aa8e02859ad5cbf4609079d6452f7604853 Mon Sep 17 00:00:00 2001 From: 0xgleb Date: Wed, 11 Jun 2025 15:05:37 +0400 Subject: [PATCH 05/10] fn minus --- crates/float/src/lib.rs | 46 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/crates/float/src/lib.rs b/crates/float/src/lib.rs index 1d1dc972..c36b5519 100644 --- a/crates/float/src/lib.rs +++ b/crates/float/src/lib.rs @@ -231,6 +231,16 @@ impl Float { Ok(decoded) }) } + + pub fn minus(&mut self, float: Float) -> Result { + let Float(a) = float; + let calldata = DecimalFloat::minusCall { a }.abi_encode(); + + self.execute_call(Bytes::from(calldata), |output| { + let decoded = DecimalFloat::minusCall::abi_decode_returns(output.as_ref())?; + Ok(Float(decoded)) + }) + } } impl Add for Float { @@ -503,6 +513,28 @@ mod tests { } } + #[test] + fn test_minus_format() { + let mut calculator = Calculator::new().unwrap(); + + let float = calculator + .parse("-123.1234234625468391".to_string()) + .unwrap(); + let negated = calculator.minus(float).unwrap(); + let formatted = calculator.format(negated).unwrap(); + assert_eq!(formatted, "123.1234234625468391"); + + let float = calculator.parse(formatted).unwrap(); + let negated = calculator.minus(float).unwrap(); + let formatted = calculator.format(negated).unwrap(); + assert_eq!(formatted, "-123.1234234625468391"); + + let float = calculator.parse("0".to_string()).unwrap(); + let negated = calculator.minus(float).unwrap(); + let formatted = calculator.format(negated).unwrap(); + assert_eq!(formatted, "0"); + } + proptest! { #[test] fn test_div(a in reasonable_float(), b in reasonable_float()) { @@ -592,4 +624,18 @@ mod tests { FloatError::DecimalFloat(DecimalFloatErrors::ExponentOverflow(_)) )); } + + proptest! { + #[test] + fn test_minus_minus(float in valid_float()) { + let mut calculator = Calculator::new().unwrap(); + + let negated = calculator.minus(float).unwrap(); + let renegated = calculator.minus(negated).unwrap(); + prop_assert_eq!( + calculator.format(float).unwrap(), + calculator.format(renegated).unwrap(), + ); + } + } } From 3321e457768e79c6cfa1f1a66c7b7210dfacd794 Mon Sep 17 00:00:00 2001 From: 0xgleb Date: Wed, 11 Jun 2025 15:10:17 +0400 Subject: [PATCH 06/10] inv wip --- crates/float/src/lib.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/crates/float/src/lib.rs b/crates/float/src/lib.rs index c36b5519..5f760fbe 100644 --- a/crates/float/src/lib.rs +++ b/crates/float/src/lib.rs @@ -241,6 +241,16 @@ impl Float { Ok(Float(decoded)) }) } + + pub fn inv(&mut self, float: Float) -> Result { + let Float(a) = float; + let calldata = DecimalFloat::invCall { a }.abi_encode(); + + self.execute_call(Bytes::from(calldata), |output| { + let decoded = DecimalFloat::invCall::abi_decode_returns(output.as_ref())?; + Ok(Float(decoded)) + }) + } } impl Add for Float { @@ -638,4 +648,18 @@ mod tests { ); } } + + proptest! { + #[test] + fn test_inv_inv(float in valid_float()) { + let mut calculator = Calculator::new().unwrap(); + + let inv = calculator.inv(float).unwrap(); + let inv_inv = calculator.inv(inv).unwrap(); + prop_assert_eq!( + calculator.format(float).unwrap(), + calculator.format(inv_inv).unwrap(), + ); + } + } } From 5c08b5dc6f1c5a7fb76e0d6dad4a3e34002361db Mon Sep 17 00:00:00 2001 From: 0xgleb Date: Mon, 16 Jun 2025 17:28:22 +0400 Subject: [PATCH 07/10] rm Calculator --- crates/float/src/lib.rs | 69 +++++++++++++++++++++-------------------- 1 file changed, 36 insertions(+), 33 deletions(-) diff --git a/crates/float/src/lib.rs b/crates/float/src/lib.rs index 5f760fbe..58f0d90c 100644 --- a/crates/float/src/lib.rs +++ b/crates/float/src/lib.rs @@ -232,21 +232,21 @@ impl Float { }) } - pub fn minus(&mut self, float: Float) -> Result { - let Float(a) = float; + pub fn minus(self) -> Result { + let Float(a) = self; let calldata = DecimalFloat::minusCall { a }.abi_encode(); - self.execute_call(Bytes::from(calldata), |output| { + execute_call(Bytes::from(calldata), |output| { let decoded = DecimalFloat::minusCall::abi_decode_returns(output.as_ref())?; Ok(Float(decoded)) }) } - pub fn inv(&mut self, float: Float) -> Result { - let Float(a) = float; + pub fn inv(self) -> Result { + let Float(a) = self; let calldata = DecimalFloat::invCall { a }.abi_encode(); - self.execute_call(Bytes::from(calldata), |output| { + execute_call(Bytes::from(calldata), |output| { let decoded = DecimalFloat::invCall::abi_decode_returns(output.as_ref())?; Ok(Float(decoded)) }) @@ -525,26 +525,43 @@ mod tests { #[test] fn test_minus_format() { - let mut calculator = Calculator::new().unwrap(); - - let float = calculator - .parse("-123.1234234625468391".to_string()) - .unwrap(); - let negated = calculator.minus(float).unwrap(); - let formatted = calculator.format(negated).unwrap(); + let float = Float::parse("-123.1234234625468391".to_string()).unwrap(); + let negated = float.minus().unwrap(); + let formatted = negated.format().unwrap(); assert_eq!(formatted, "123.1234234625468391"); - let float = calculator.parse(formatted).unwrap(); - let negated = calculator.minus(float).unwrap(); - let formatted = calculator.format(negated).unwrap(); + let float = Float::parse(formatted).unwrap(); + let negated = float.minus().unwrap(); + let formatted = negated.format().unwrap(); assert_eq!(formatted, "-123.1234234625468391"); - let float = calculator.parse("0".to_string()).unwrap(); - let negated = calculator.minus(float).unwrap(); - let formatted = calculator.format(negated).unwrap(); + let float = Float::parse("0".to_string()).unwrap(); + let negated = float.minus().unwrap(); + let formatted = negated.format().unwrap(); assert_eq!(formatted, "0"); } + proptest! { + #[test] + fn test_minus_minus(float in arb_float()) { + let negated = float.minus().unwrap(); + let renegated = negated.minus().unwrap(); + prop_assert!(float.eq(renegated).unwrap()); + } + } + + proptest! { + #[test] + fn test_inv_inv(float in reasonable_float()) { + let inv = float.inv().unwrap(); + let inv_inv = inv.inv().unwrap(); + prop_assert_eq!( + float.format().unwrap(), + inv_inv.format().unwrap(), + ); + } + } + proptest! { #[test] fn test_div(a in reasonable_float(), b in reasonable_float()) { @@ -648,18 +665,4 @@ mod tests { ); } } - - proptest! { - #[test] - fn test_inv_inv(float in valid_float()) { - let mut calculator = Calculator::new().unwrap(); - - let inv = calculator.inv(float).unwrap(); - let inv_inv = calculator.inv(inv).unwrap(); - prop_assert_eq!( - calculator.format(float).unwrap(), - calculator.format(inv_inv).unwrap(), - ); - } - } } From 33606f7a61125d472cdebb76d8f95ece5bc83839 Mon Sep 17 00:00:00 2001 From: 0xgleb Date: Mon, 16 Jun 2025 19:44:47 +0400 Subject: [PATCH 08/10] fix inv property test --- crates/float/src/lib.rs | 40 +++++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/crates/float/src/lib.rs b/crates/float/src/lib.rs index 58f0d90c..6ba7688f 100644 --- a/crates/float/src/lib.rs +++ b/crates/float/src/lib.rs @@ -552,12 +552,28 @@ mod tests { proptest! { #[test] - fn test_inv_inv(float in reasonable_float()) { + fn test_inv_prod(float in reasonable_float()) { let inv = float.inv().unwrap(); - let inv_inv = inv.inv().unwrap(); - prop_assert_eq!( - float.format().unwrap(), - inv_inv.format().unwrap(), + let product = (float * inv).unwrap(); + let one = Float::parse("1".to_string()).unwrap(); + + // Allow for minor rounding errors introduced by the lossy + // `inv` implementation. We consider the property to + // hold if the product is within `±1e-37` of 1. + + let eps = Float::parse("1e-37".to_string()).unwrap(); + let one_plus_eps = (one + eps).unwrap(); + let one_minus_eps = (one - eps).unwrap(); + + let within_upper = !product.gt(one_plus_eps).unwrap(); + let within_lower = !product.lt(one_minus_eps).unwrap(); + + prop_assert!( + within_upper && within_lower, + "float: {}, inv: {}, product: {} (not within ±ε)", + float.show_unpacked().unwrap(), + inv.show_unpacked().unwrap(), + product.show_unpacked().unwrap(), ); } } @@ -651,18 +667,4 @@ mod tests { FloatError::DecimalFloat(DecimalFloatErrors::ExponentOverflow(_)) )); } - - proptest! { - #[test] - fn test_minus_minus(float in valid_float()) { - let mut calculator = Calculator::new().unwrap(); - - let negated = calculator.minus(float).unwrap(); - let renegated = calculator.minus(negated).unwrap(); - prop_assert_eq!( - calculator.format(float).unwrap(), - calculator.format(renegated).unwrap(), - ); - } - } } From 335b200ad2de476ddd1436a44cb2dc08c85cd7fb Mon Sep 17 00:00:00 2001 From: 0xgleb Date: Wed, 18 Jun 2025 13:04:42 +0400 Subject: [PATCH 09/10] impl Neg --- crates/float/src/lib.rs | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/crates/float/src/lib.rs b/crates/float/src/lib.rs index b400b8ae..e53a5707 100644 --- a/crates/float/src/lib.rs +++ b/crates/float/src/lib.rs @@ -12,7 +12,7 @@ use revm::interpreter::interpreter::EthInterpreter; use revm::primitives::{address, fixed_bytes}; use revm::{Context, MainBuilder, MainContext, SystemCallEvm}; use std::cell::RefCell; -use std::ops::{Add, Div, Mul, Sub}; +use std::ops::{Add, Div, Mul, Neg, Sub}; use std::thread::AccessError; use thiserror::Error; @@ -232,16 +232,6 @@ impl Float { }) } - pub fn minus(self) -> Result { - let Float(a) = self; - let calldata = DecimalFloat::minusCall { a }.abi_encode(); - - execute_call(Bytes::from(calldata), |output| { - let decoded = DecimalFloat::minusCall::abi_decode_returns(output.as_ref())?; - Ok(Float(decoded)) - }) - } - pub fn inv(self) -> Result { let Float(a) = self; let calldata = DecimalFloat::invCall { a }.abi_encode(); @@ -323,6 +313,20 @@ impl Div for Float { } } +impl Neg for Float { + type Output = Result; + + fn neg(self) -> Self::Output { + let Float(a) = self; + let calldata = DecimalFloat::minusCall { a }.abi_encode(); + + execute_call(Bytes::from(calldata), |output| { + let decoded = DecimalFloat::minusCall::abi_decode_returns(output.as_ref())?; + Ok(Float(decoded)) + }) + } +} + #[cfg(test)] mod tests { use super::*; @@ -554,17 +558,17 @@ mod tests { #[test] fn test_minus_format() { let float = Float::parse("-123.1234234625468391".to_string()).unwrap(); - let negated = float.minus().unwrap(); + let negated = float.neg().unwrap(); let formatted = negated.format().unwrap(); assert_eq!(formatted, "123.1234234625468391"); let float = Float::parse(formatted).unwrap(); - let negated = float.minus().unwrap(); + let negated = float.neg().unwrap(); let formatted = negated.format().unwrap(); assert_eq!(formatted, "-123.1234234625468391"); let float = Float::parse("0".to_string()).unwrap(); - let negated = float.minus().unwrap(); + let negated = float.neg().unwrap(); let formatted = negated.format().unwrap(); assert_eq!(formatted, "0"); } @@ -572,8 +576,8 @@ mod tests { proptest! { #[test] fn test_minus_minus(float in arb_float()) { - let negated = float.minus().unwrap(); - let renegated = negated.minus().unwrap(); + let negated = float.neg().unwrap(); + let renegated = negated.neg().unwrap(); prop_assert!(float.eq(renegated).unwrap()); } } From 1c2e810f451a48a0fcd3e182a2933583355e1ae6 Mon Sep 17 00:00:00 2001 From: 0xgleb Date: Wed, 18 Jun 2025 13:12:14 +0400 Subject: [PATCH 10/10] update property test --- crates/float/src/lib.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/float/src/lib.rs b/crates/float/src/lib.rs index e53a5707..e261a8d5 100644 --- a/crates/float/src/lib.rs +++ b/crates/float/src/lib.rs @@ -585,6 +585,9 @@ mod tests { proptest! { #[test] fn test_inv_prod(float in reasonable_float()) { + let zero = Float::parse("0".to_string()).unwrap(); + prop_assume!(!float.eq(zero).unwrap()); + let inv = float.inv().unwrap(); let product = (float * inv).unwrap(); let one = Float::parse("1".to_string()).unwrap();