diff --git a/CHANGELOG.md b/CHANGELOG.md index e15bac5a8c..68ce49e7b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,13 @@ and this project adheres to - cosmwasm-std: Add `abs` and `unsigned_abs` for `Int{64,128,256,512}` ([#1854]). +- cosmwasm-std: Add `From` for `Int512`, + `TryFrom` for `Int64`, `TryFrom` for `Int128`, + `TryFrom` for `Int256` and `Int256::from_i128` for const contexts + ([#1861]). [#1854]: https://github.com/CosmWasm/cosmwasm/pull/1854 +[#1861]: https://github.com/CosmWasm/cosmwasm/pull/1861 ## [1.4.0] - 2023-09-04 diff --git a/packages/std/src/math/conversion.rs b/packages/std/src/math/conversion.rs new file mode 100644 index 0000000000..f7cb353c04 --- /dev/null +++ b/packages/std/src/math/conversion.rs @@ -0,0 +1,104 @@ +/// Grows a big endian signed integer to a bigger size. +/// See +pub const fn grow_be_int( + input: [u8; INPUT_SIZE], +) -> [u8; OUTPUT_SIZE] { + debug_assert!(INPUT_SIZE <= OUTPUT_SIZE); + // check if sign bit is set + let mut output = if input[0] & 0b10000000 != 0 { + // negative number is filled up with 1s + [0b11111111u8; OUTPUT_SIZE] + } else { + [0u8; OUTPUT_SIZE] + }; + let mut i = 0; + + // copy input to the end of output + // copy_from_slice is not const, so we have to do this manually + while i < INPUT_SIZE { + output[OUTPUT_SIZE - INPUT_SIZE + i] = input[i]; + i += 1; + } + output +} + +/// Shrinks a big endian signed integer to a smaller size. +/// This is the opposite operation of sign extension. +pub fn shrink_be_int( + input: [u8; INPUT_SIZE], +) -> Option<[u8; OUTPUT_SIZE]> { + debug_assert!(INPUT_SIZE >= OUTPUT_SIZE); + + // check bounds + if input[0] & 0b10000000 != 0 { + // a negative number should start with only 1s, otherwise it's too small + for i in &input[0..(INPUT_SIZE - OUTPUT_SIZE)] { + if *i != 0b11111111u8 { + return None; + } + } + } else { + // a positive number should start with only 0s, otherwise it's too large + for i in &input[0..(INPUT_SIZE - OUTPUT_SIZE)] { + if *i != 0u8 { + return None; + } + } + } + + // Now, we can just copy the last bytes + let mut output = [0u8; OUTPUT_SIZE]; + output.copy_from_slice(&input[(INPUT_SIZE - OUTPUT_SIZE)..]); + Some(output) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn grow_be_int_works() { + // test against rust std's integers + let i32s = [i32::MIN, -1, 0, 1, 42, i32::MAX]; + for i in i32s { + assert_eq!(grow_be_int(i.to_be_bytes()), (i as i64).to_be_bytes()); + assert_eq!(grow_be_int(i.to_be_bytes()), (i as i128).to_be_bytes()); + } + let i8s = [i8::MIN, -1, 0, 1, 42, i8::MAX]; + for i in i8s { + assert_eq!(grow_be_int(i.to_be_bytes()), (i as i16).to_be_bytes()); + assert_eq!(grow_be_int(i.to_be_bytes()), (i as i32).to_be_bytes()); + assert_eq!(grow_be_int(i.to_be_bytes()), (i as i64).to_be_bytes()); + assert_eq!(grow_be_int(i.to_be_bytes()), (i as i128).to_be_bytes()); + } + } + + #[test] + fn shrink_be_int_works() { + // test against rust std's integers + let i32s = [-42, -1, 0i32, 1, 42]; + for i in i32s { + assert_eq!( + shrink_be_int(i.to_be_bytes()), + Some((i as i16).to_be_bytes()) + ); + assert_eq!( + shrink_be_int(i.to_be_bytes()), + Some((i as i8).to_be_bytes()) + ); + } + // these should be too big to fit into an i16 or i8 + let oob = [ + i32::MIN, + i32::MIN + 10, + i32::MIN + 1234, + i32::MAX - 1234, + i32::MAX - 10, + i32::MAX, + ]; + for i in oob { + assert_eq!(shrink_be_int::<4, 2>(i.to_be_bytes()), None); + assert_eq!(shrink_be_int::<4, 1>(i.to_be_bytes()), None); + } + } +} diff --git a/packages/std/src/math/int128.rs b/packages/std/src/math/int128.rs index 1eb62c3f10..51943b08da 100644 --- a/packages/std/src/math/int128.rs +++ b/packages/std/src/math/int128.rs @@ -9,7 +9,11 @@ use schemars::JsonSchema; use serde::{de, ser, Deserialize, Deserializer, Serialize}; use crate::errors::{DivideByZeroError, DivisionError, OverflowError, OverflowOperation, StdError}; -use crate::{forward_ref_partial_eq, Int64, Uint128, Uint64}; +use crate::{ + forward_ref_partial_eq, ConversionOverflowError, Int256, Int512, Int64, Uint128, Uint64, +}; + +use super::conversion::shrink_be_int; /// An implementation of i128 that is using strings for JSON encoding/decoding, /// such that the full i128 range can be used for clients that convert JSON numbers to floats, @@ -287,6 +291,26 @@ impl From for Int128 { } } +impl TryFrom for Int128 { + type Error = ConversionOverflowError; + + fn try_from(value: Int256) -> Result { + shrink_be_int(value.to_be_bytes()) + .ok_or_else(|| ConversionOverflowError::new("Int256", "Int128", value)) + .map(Self::from_be_bytes) + } +} + +impl TryFrom for Int128 { + type Error = ConversionOverflowError; + + fn try_from(value: Int512) -> Result { + shrink_be_int(value.to_be_bytes()) + .ok_or_else(|| ConversionOverflowError::new("Int512", "Int128", value)) + .map(Self::from_be_bytes) + } +} + impl TryFrom<&str> for Int128 { type Error = StdError; diff --git a/packages/std/src/math/int256.rs b/packages/std/src/math/int256.rs index 78f9a15af5..b53680f723 100644 --- a/packages/std/src/math/int256.rs +++ b/packages/std/src/math/int256.rs @@ -9,12 +9,17 @@ use schemars::JsonSchema; use serde::{de, ser, Deserialize, Deserializer, Serialize}; use crate::errors::{DivideByZeroError, DivisionError, OverflowError, OverflowOperation, StdError}; -use crate::{forward_ref_partial_eq, Int128, Int64, Uint128, Uint256, Uint64}; +use crate::{ + forward_ref_partial_eq, ConversionOverflowError, Int128, Int512, Int64, Uint128, Uint256, + Uint64, +}; /// Used internally - we don't want to leak this type since we might change /// the implementation in the future. use bnum::types::{I256, U256}; +use super::conversion::{grow_be_int, shrink_be_int}; + /// An implementation of i256 that is using strings for JSON encoding/decoding, /// such that the full i256 range can be used for clients that convert JSON numbers to floats, /// like JavaScript and jq. @@ -63,6 +68,12 @@ impl Int256 { Self(I256::ONE) } + /// A conversion from `i128` that, unlike the one provided by the `From` trait, + /// can be used in a `const` context. + pub const fn from_i128(v: i128) -> Self { + Self::from_be_bytes(grow_be_int(v.to_be_bytes())) + } + #[must_use] pub const fn from_be_bytes(data: [u8; 32]) -> Self { let words: [u64; 4] = [ @@ -354,6 +365,16 @@ impl From for Int256 { } } +impl TryFrom for Int256 { + type Error = ConversionOverflowError; + + fn try_from(value: Int512) -> Result { + shrink_be_int(value.to_be_bytes()) + .ok_or_else(|| ConversionOverflowError::new("Int512", "Int256", value)) + .map(Self::from_be_bytes) + } +} + impl TryFrom<&str> for Int256 { type Error = StdError; @@ -681,6 +702,25 @@ mod tests { assert!(result.is_err()); } + #[test] + fn int256_from_i128() { + assert_eq!(Int256::from_i128(123i128), Int256::from_str("123").unwrap()); + + assert_eq!( + Int256::from_i128(9785746283745i128), + Int256::from_str("9785746283745").unwrap() + ); + + assert_eq!( + Int256::from_i128(i128::MAX).to_string(), + i128::MAX.to_string() + ); + assert_eq!( + Int256::from_i128(i128::MIN).to_string(), + i128::MIN.to_string() + ); + } + #[test] fn int256_implements_display() { let a = Int256::from(12345u32); diff --git a/packages/std/src/math/int512.rs b/packages/std/src/math/int512.rs index 6004a6f134..b09fb0c725 100644 --- a/packages/std/src/math/int512.rs +++ b/packages/std/src/math/int512.rs @@ -9,12 +9,14 @@ use schemars::JsonSchema; use serde::{de, ser, Deserialize, Deserializer, Serialize}; use crate::errors::{DivideByZeroError, DivisionError, OverflowError, OverflowOperation, StdError}; -use crate::{forward_ref_partial_eq, Uint128, Uint256, Uint512, Uint64}; +use crate::{forward_ref_partial_eq, Int128, Int256, Int64, Uint128, Uint256, Uint512, Uint64}; /// Used internally - we don't want to leak this type since we might change /// the implementation in the future. use bnum::types::{I512, U512}; +use super::conversion::grow_be_int; + /// An implementation of i512 that is using strings for JSON encoding/decoding, /// such that the full i512 range can be used for clients that convert JSON numbers to floats, /// like JavaScript and jq. @@ -387,6 +389,24 @@ impl From for Int512 { } } +impl From for Int512 { + fn from(val: Int64) -> Self { + Int512(val.i64().into()) + } +} + +impl From for Int512 { + fn from(val: Int128) -> Self { + Int512(val.i128().into()) + } +} + +impl From for Int512 { + fn from(val: Int256) -> Self { + Self::from_be_bytes(grow_be_int(val.to_be_bytes())) + } +} + impl TryFrom<&str> for Int512 { type Error = StdError; @@ -710,6 +730,40 @@ mod tests { let a = Int512::from(-5i8); assert_eq!(a.0, I512::from(-5i32)); + // other big signed integers + let values = [ + Int64::MAX, + Int64::MIN, + Int64::one(), + -Int64::one(), + Int64::zero(), + ]; + for v in values { + assert_eq!(Int512::from(v).to_string(), v.to_string()); + } + + let values = [ + Int128::MAX, + Int128::MIN, + Int128::one(), + -Int128::one(), + Int128::zero(), + ]; + for v in values { + assert_eq!(Int512::from(v).to_string(), v.to_string()); + } + + let values = [ + Int256::MAX, + Int256::MIN, + Int256::one(), + -Int256::one(), + Int256::zero(), + ]; + for v in values { + assert_eq!(Int512::from(v).to_string(), v.to_string()); + } + let result = Int512::try_from("34567"); assert_eq!( result.unwrap().0, diff --git a/packages/std/src/math/int64.rs b/packages/std/src/math/int64.rs index da4bc4f160..057b0f3405 100644 --- a/packages/std/src/math/int64.rs +++ b/packages/std/src/math/int64.rs @@ -9,7 +9,9 @@ use schemars::JsonSchema; use serde::{de, ser, Deserialize, Deserializer, Serialize}; use crate::errors::{DivideByZeroError, DivisionError, OverflowError, OverflowOperation, StdError}; -use crate::{forward_ref_partial_eq, Uint64}; +use crate::{forward_ref_partial_eq, ConversionOverflowError, Int128, Int256, Int512, Uint64}; + +use super::conversion::shrink_be_int; /// An implementation of i64 that is using strings for JSON encoding/decoding, /// such that the full i64 range can be used for clients that convert JSON numbers to floats, @@ -263,6 +265,36 @@ impl From for Int64 { } } +impl TryFrom for Int64 { + type Error = ConversionOverflowError; + + fn try_from(value: Int128) -> Result { + shrink_be_int(value.to_be_bytes()) + .ok_or_else(|| ConversionOverflowError::new("Int128", "Int64", value)) + .map(Self::from_be_bytes) + } +} + +impl TryFrom for Int64 { + type Error = ConversionOverflowError; + + fn try_from(value: Int256) -> Result { + shrink_be_int(value.to_be_bytes()) + .ok_or_else(|| ConversionOverflowError::new("Int256", "Int64", value)) + .map(Self::from_be_bytes) + } +} + +impl TryFrom for Int64 { + type Error = ConversionOverflowError; + + fn try_from(value: Int512) -> Result { + shrink_be_int(value.to_be_bytes()) + .ok_or_else(|| ConversionOverflowError::new("Int512", "Int64", value)) + .map(Self::from_be_bytes) + } +} + impl TryFrom<&str> for Int64 { type Error = StdError; diff --git a/packages/std/src/math/mod.rs b/packages/std/src/math/mod.rs index a6cf8af3cb..9b27fd859a 100644 --- a/packages/std/src/math/mod.rs +++ b/packages/std/src/math/mod.rs @@ -1,3 +1,4 @@ +mod conversion; mod decimal; mod decimal256; mod fraction;