Skip to content

Commit

Permalink
Merge pull request #1861 from CosmWasm/1855-int-conversions
Browse files Browse the repository at this point in the history
More Integer conversions
  • Loading branch information
chipshort authored Sep 6, 2023
2 parents b1845e8 + 167f48c commit f20bc85
Show file tree
Hide file tree
Showing 7 changed files with 264 additions and 4 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<Int{64,128,256}>` for `Int512`,
`TryFrom<Int{128,256,512}>` for `Int64`, `TryFrom<Int{256,512}>` for `Int128`,
`TryFrom<Int512>` 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

Expand Down
104 changes: 104 additions & 0 deletions packages/std/src/math/conversion.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/// Grows a big endian signed integer to a bigger size.
/// See <https://en.wikipedia.org/wiki/Sign_extension>
pub const fn grow_be_int<const INPUT_SIZE: usize, const OUTPUT_SIZE: usize>(
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<const INPUT_SIZE: usize, const OUTPUT_SIZE: usize>(
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);
}
}
}
26 changes: 25 additions & 1 deletion packages/std/src/math/int128.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -287,6 +291,26 @@ impl From<i8> for Int128 {
}
}

impl TryFrom<Int256> for Int128 {
type Error = ConversionOverflowError;

fn try_from(value: Int256) -> Result<Self, Self::Error> {
shrink_be_int(value.to_be_bytes())
.ok_or_else(|| ConversionOverflowError::new("Int256", "Int128", value))
.map(Self::from_be_bytes)
}
}

impl TryFrom<Int512> for Int128 {
type Error = ConversionOverflowError;

fn try_from(value: Int512) -> Result<Self, Self::Error> {
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;

Expand Down
42 changes: 41 additions & 1 deletion packages/std/src/math/int256.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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] = [
Expand Down Expand Up @@ -354,6 +365,16 @@ impl From<i8> for Int256 {
}
}

impl TryFrom<Int512> for Int256 {
type Error = ConversionOverflowError;

fn try_from(value: Int512) -> Result<Self, Self::Error> {
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;

Expand Down Expand Up @@ -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);
Expand Down
56 changes: 55 additions & 1 deletion packages/std/src/math/int512.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -387,6 +389,24 @@ impl From<i8> for Int512 {
}
}

impl From<Int64> for Int512 {
fn from(val: Int64) -> Self {
Int512(val.i64().into())
}
}

impl From<Int128> for Int512 {
fn from(val: Int128) -> Self {
Int512(val.i128().into())
}
}

impl From<Int256> 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;

Expand Down Expand Up @@ -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,
Expand Down
34 changes: 33 additions & 1 deletion packages/std/src/math/int64.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -263,6 +265,36 @@ impl From<i8> for Int64 {
}
}

impl TryFrom<Int128> for Int64 {
type Error = ConversionOverflowError;

fn try_from(value: Int128) -> Result<Self, Self::Error> {
shrink_be_int(value.to_be_bytes())
.ok_or_else(|| ConversionOverflowError::new("Int128", "Int64", value))
.map(Self::from_be_bytes)
}
}

impl TryFrom<Int256> for Int64 {
type Error = ConversionOverflowError;

fn try_from(value: Int256) -> Result<Self, Self::Error> {
shrink_be_int(value.to_be_bytes())
.ok_or_else(|| ConversionOverflowError::new("Int256", "Int64", value))
.map(Self::from_be_bytes)
}
}

impl TryFrom<Int512> for Int64 {
type Error = ConversionOverflowError;

fn try_from(value: Int512) -> Result<Self, Self::Error> {
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;

Expand Down
1 change: 1 addition & 0 deletions packages/std/src/math/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod conversion;
mod decimal;
mod decimal256;
mod fraction;
Expand Down

0 comments on commit f20bc85

Please sign in to comment.