From b086d9f97dcb1dc9db0d29212b8d5ddac8c19fb0 Mon Sep 17 00:00:00 2001 From: Christoph Otter Date: Tue, 5 Sep 2023 13:18:16 +0200 Subject: [PATCH 1/9] Implement missing From conversions for Int512 --- packages/std/src/math/conversion.rs | 22 ++++++++++++ packages/std/src/math/int512.rs | 56 ++++++++++++++++++++++++++++- packages/std/src/math/mod.rs | 1 + 3 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 packages/std/src/math/conversion.rs diff --git a/packages/std/src/math/conversion.rs b/packages/std/src/math/conversion.rs new file mode 100644 index 0000000000..3d3b8280f4 --- /dev/null +++ b/packages/std/src/math/conversion.rs @@ -0,0 +1,22 @@ +/// Grows a big endian signed integer to a bigger size. +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 +} 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/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; From 9e5123dc05b8376e3b25225c06ff46d26b5b8567 Mon Sep 17 00:00:00 2001 From: Christoph Otter Date: Tue, 5 Sep 2023 13:19:49 +0200 Subject: [PATCH 2/9] Implement const fn to convert i128 to Int256 --- packages/std/src/math/int256.rs | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/packages/std/src/math/int256.rs b/packages/std/src/math/int256.rs index 78f9a15af5..3b01cfe549 100644 --- a/packages/std/src/math/int256.rs +++ b/packages/std/src/math/int256.rs @@ -15,6 +15,8 @@ use crate::{forward_ref_partial_eq, Int128, Int64, Uint128, Uint256, Uint64}; /// the implementation in the future. use bnum::types::{I256, U256}; +use super::conversion::grow_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 +65,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] = [ @@ -681,6 +689,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); From b99bd923d28170dde55c187cde2f52a7d6fa310b Mon Sep 17 00:00:00 2001 From: Christoph Otter Date: Tue, 5 Sep 2023 13:51:20 +0200 Subject: [PATCH 3/9] Add test for grow_be_int --- packages/std/src/math/conversion.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/packages/std/src/math/conversion.rs b/packages/std/src/math/conversion.rs index 3d3b8280f4..135bed4241 100644 --- a/packages/std/src/math/conversion.rs +++ b/packages/std/src/math/conversion.rs @@ -20,3 +20,25 @@ pub const fn grow_be_int( } 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()); + } + } +} From 60efcea5560c7cbd5d2574e4a253a2a4846d768e Mon Sep 17 00:00:00 2001 From: Christoph Otter Date: Tue, 5 Sep 2023 14:55:01 +0200 Subject: [PATCH 4/9] Add TryFrom impl to convert bigger to smaller ints --- packages/std/src/math/conversion.rs | 56 +++++++++++++++++++++++++++++ packages/std/src/math/int128.rs | 16 ++++++++- packages/std/src/math/int256.rs | 17 +++++++-- packages/std/src/math/int64.rs | 14 +++++++- 4 files changed, 99 insertions(+), 4 deletions(-) diff --git a/packages/std/src/math/conversion.rs b/packages/std/src/math/conversion.rs index 135bed4241..c9305ab1f4 100644 --- a/packages/std/src/math/conversion.rs +++ b/packages/std/src/math/conversion.rs @@ -21,6 +21,33 @@ pub const fn grow_be_int( output } +/// Shrinks a big endian signed integer to a smaller size. +pub fn shrink_be_int( + input: [u8; INPUT_SIZE], +) -> Option<[u8; OUTPUT_SIZE]> { + debug_assert!(INPUT_SIZE >= OUTPUT_SIZE); + + // A positive number should start with only 0s and a negative one with only 1s until + // the size we want to look at. + // If this is not the case, then the value is too large / small for the target type + let ignored_byte = if input[0] & 0b10000000 != 0 { + 0b11111111u8 + } else { + 0u8 + }; + // Rust doesn't allow us to create an array of size `OUTPUT_SIZE - INPUT_SIZE`, + // so we work around this by taking a slice of a bigger array + let ignore_bytes = [ignored_byte; INPUT_SIZE]; + if input[0..(INPUT_SIZE - OUTPUT_SIZE)] != ignore_bytes[0..(INPUT_SIZE - OUTPUT_SIZE)] { + 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::*; @@ -41,4 +68,33 @@ mod tests { 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..5529fad94f 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,16 @@ 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<&str> for Int128 { type Error = StdError; diff --git a/packages/std/src/math/int256.rs b/packages/std/src/math/int256.rs index 3b01cfe549..b53680f723 100644 --- a/packages/std/src/math/int256.rs +++ b/packages/std/src/math/int256.rs @@ -9,13 +9,16 @@ 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; +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, @@ -362,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; diff --git a/packages/std/src/math/int64.rs b/packages/std/src/math/int64.rs index da4bc4f160..a44cca2b43 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, 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,16 @@ 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<&str> for Int64 { type Error = StdError; From 5f7df5b17bee662b42a2f88145dc5e64c4fe333f Mon Sep 17 00:00:00 2001 From: Christoph Otter Date: Tue, 5 Sep 2023 15:42:56 +0200 Subject: [PATCH 5/9] Fix clippy --- packages/std/src/math/int128.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/std/src/math/int128.rs b/packages/std/src/math/int128.rs index 5529fad94f..c21b66dd03 100644 --- a/packages/std/src/math/int128.rs +++ b/packages/std/src/math/int128.rs @@ -9,9 +9,7 @@ use schemars::JsonSchema; use serde::{de, ser, Deserialize, Deserializer, Serialize}; use crate::errors::{DivideByZeroError, DivisionError, OverflowError, OverflowOperation, StdError}; -use crate::{ - forward_ref_partial_eq, ConversionOverflowError, Int256, Int512, Int64, Uint128, Uint64, -}; +use crate::{forward_ref_partial_eq, ConversionOverflowError, Int256, Int64, Uint128, Uint64}; use super::conversion::shrink_be_int; From 345d561e78ac1e927173f7c86207cf4939e0dceb Mon Sep 17 00:00:00 2001 From: Christoph Otter Date: Tue, 5 Sep 2023 16:23:43 +0200 Subject: [PATCH 6/9] Improve shrink_be_int readability --- packages/std/src/math/conversion.rs | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/packages/std/src/math/conversion.rs b/packages/std/src/math/conversion.rs index c9305ab1f4..d83fbca5e6 100644 --- a/packages/std/src/math/conversion.rs +++ b/packages/std/src/math/conversion.rs @@ -27,19 +27,21 @@ pub fn shrink_be_int( ) -> Option<[u8; OUTPUT_SIZE]> { debug_assert!(INPUT_SIZE >= OUTPUT_SIZE); - // A positive number should start with only 0s and a negative one with only 1s until - // the size we want to look at. - // If this is not the case, then the value is too large / small for the target type - let ignored_byte = if input[0] & 0b10000000 != 0 { - 0b11111111u8 + // 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 { - 0u8 - }; - // Rust doesn't allow us to create an array of size `OUTPUT_SIZE - INPUT_SIZE`, - // so we work around this by taking a slice of a bigger array - let ignore_bytes = [ignored_byte; INPUT_SIZE]; - if input[0..(INPUT_SIZE - OUTPUT_SIZE)] != ignore_bytes[0..(INPUT_SIZE - OUTPUT_SIZE)] { - return None; + // 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 From a77b0fd95693939387f46a22a14390c9aaee0028 Mon Sep 17 00:00:00 2001 From: Christoph Otter Date: Tue, 5 Sep 2023 16:24:57 +0200 Subject: [PATCH 7/9] Improve docs Co-authored-by: Simon Warta <2603011+webmaster128@users.noreply.github.com> --- packages/std/src/math/conversion.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/std/src/math/conversion.rs b/packages/std/src/math/conversion.rs index d83fbca5e6..f7cb353c04 100644 --- a/packages/std/src/math/conversion.rs +++ b/packages/std/src/math/conversion.rs @@ -1,4 +1,5 @@ /// Grows a big endian signed integer to a bigger size. +/// See pub const fn grow_be_int( input: [u8; INPUT_SIZE], ) -> [u8; OUTPUT_SIZE] { @@ -22,6 +23,7 @@ pub const fn grow_be_int( } /// 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]> { From 9d66dcd64fce7e8b4772249e1f92e4f2eb9aa602 Mon Sep 17 00:00:00 2001 From: Christoph Otter Date: Tue, 5 Sep 2023 16:29:31 +0200 Subject: [PATCH 8/9] Add missing TryFrom integer conversions --- packages/std/src/math/int128.rs | 14 +++++++++++++- packages/std/src/math/int64.rs | 22 +++++++++++++++++++++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/packages/std/src/math/int128.rs b/packages/std/src/math/int128.rs index c21b66dd03..51943b08da 100644 --- a/packages/std/src/math/int128.rs +++ b/packages/std/src/math/int128.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, ConversionOverflowError, Int256, Int64, Uint128, Uint64}; +use crate::{ + forward_ref_partial_eq, ConversionOverflowError, Int256, Int512, Int64, Uint128, Uint64, +}; use super::conversion::shrink_be_int; @@ -299,6 +301,16 @@ impl TryFrom for Int128 { } } +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/int64.rs b/packages/std/src/math/int64.rs index a44cca2b43..057b0f3405 100644 --- a/packages/std/src/math/int64.rs +++ b/packages/std/src/math/int64.rs @@ -9,7 +9,7 @@ use schemars::JsonSchema; use serde::{de, ser, Deserialize, Deserializer, Serialize}; use crate::errors::{DivideByZeroError, DivisionError, OverflowError, OverflowOperation, StdError}; -use crate::{forward_ref_partial_eq, ConversionOverflowError, Int128, Uint64}; +use crate::{forward_ref_partial_eq, ConversionOverflowError, Int128, Int256, Int512, Uint64}; use super::conversion::shrink_be_int; @@ -275,6 +275,26 @@ impl TryFrom for Int64 { } } +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; From 167f48ca7b1bccd17cce84f700c2e982febe00da Mon Sep 17 00:00:00 2001 From: Christoph Otter Date: Tue, 5 Sep 2023 16:38:17 +0200 Subject: [PATCH 9/9] Add changelog entry --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) 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