Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More Integer conversions #1861

Merged
merged 9 commits into from
Sep 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
chipshort marked this conversation as resolved.
Show resolved Hide resolved
/// 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.
chipshort marked this conversation as resolved.
Show resolved Hide resolved
/// 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;
}
}
}
chipshort marked this conversation as resolved.
Show resolved Hide resolved

// 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)
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be nice to try to upstream those. I.e. go from BInt::<X> to BInt::<Y>. Then you can also operate in 8 steps which should be faster.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, after looking into this, there seems to already be a way of casting them using bnum's CastFrom or As trait, but it needs nightly features to be const.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright cool. This solution is good anyways. I just want to be nice and upstream ideas and code where possible.

#[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)
}
}
chipshort marked this conversation as resolved.
Show resolved Hide resolved

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 {
webmaster128 marked this conversation as resolved.
Show resolved Hide resolved
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)
}
}
chipshort marked this conversation as resolved.
Show resolved Hide resolved

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
Loading