Skip to content
Merged
46 changes: 26 additions & 20 deletions crates/sui-framework/packages/move-stdlib/sources/uq32_32.move
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

/// Defines an unisnged, fixed-point numeric type with a 32-bit integer part and a 32-bit fractional
/// Defines an unsigned, fixed-point numeric type with a 32-bit integer part and a 32-bit fractional
/// part. The notation `uq32_32` and `UQ32_32` is based on
/// [Q notation](https://en.wikipedia.org/wiki/Q_(number_format)).`q` indicates it a
/// fixed-point number number. The `u` prefix indicates it is unsigned. The `32_32` suffix indicates
/// number of bits, where the first number indicates the number of bits in the integer part,
/// and the second the number of bits in the fractional part--in this case 32 bits for each.
/// [Q notation](https://en.wikipedia.org/wiki/Q_(number_format)). `q` indicates it a fixed-point
/// number. The `u` prefix indicates it is unsigned. The `32_32` suffix indicates the number of
/// bits, where the first number indicates the number of bits in the integer part, and the second
/// the number of bits in the fractional part--in this case 32 bits for each.
module std::uq32_32;

#[error]
const EDenominator: vector<u8> = b"`from_rational` called with a zero denominator";
const EDenominator: vector<u8> = b"Quotient specified with a zero denominator";

#[error]
const ERatioTooSmall: vector<u8> =
b"`from_rational` called with a ratio that is too small, and is outside of the supported range";
const EQuotientTooSmall: vector<u8> =
b"Quotient specified is too small, and is outside of the supported range";

#[error]
const ERatioTooLarge: vector<u8> =
b"`from_rational` called with a ratio that is too large, and is outside of the supported range";
const EQuotientTooLarge: vector<u8> =
b"Quotient specified is too large, and is outside of the supported range";

#[error]
const EOverflow: vector<u8> = b"Overflow from an arithmetic operation";
Expand All @@ -32,14 +32,15 @@ const EDivisionByZero: vector<u8> = b"Division by zero";
/// decimal point (18 digits total).
public struct UQ32_32(u64) has copy, drop, store;

/// Create a fixed-point value from a rational number specified by its numerator and denominator.
/// `from_rational` and `from_integer` should be preferred over using `from_raw`.
/// Create a fixed-point value from a quotient specified by its numerator and denominator.
/// `from_quotient` and `from_int` should be preferred over using `from_raw`.
/// Unless the denominator is a power of two, fractions can not be represented accurately,
/// so be careful about rounding errors.
/// Aborts if the denominator is zero.
/// Aborts if the input is non-zero but so small that it will be represented as zero, e.g. smaller than 2^{-32}.
/// Aborts if the input is non-zero but so small that it will be represented as zero, e.g. smaller
/// than 2^{-32}.
/// Aborts if the input is too large, e.g. larger than or equal to 2^32.
public fun from_rational(numerator: u64, denominator: u64): UQ32_32 {
public fun from_quotient(numerator: u64, denominator: u64): UQ32_32 {
Copy link
Contributor

Choose a reason for hiding this comment

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

Thank you!

assert!(denominator != 0, EDenominator);

// Scale the numerator to have 64 fractional bits and the denominator to have 32 fractional
Expand All @@ -49,17 +50,17 @@ public fun from_rational(numerator: u64, denominator: u64): UQ32_32 {
let quotient = scaled_numerator / scaled_denominator;

// The quotient can only be zero if the numerator is also zero.
assert!(quotient != 0 || numerator == 0, ERatioTooSmall);
assert!(quotient != 0 || numerator == 0, EQuotientTooSmall);

// Return the quotient as a fixed-point number. We first need to check whether the cast
// can succeed.
assert!(quotient <= std::u64::max_value!() as u128, ERatioTooLarge);
assert!(quotient <= std::u64::max_value!() as u128, EQuotientTooLarge);
UQ32_32(quotient as u64)
}

/// Create a fixed-point value from an integer.
/// `from_integer` and `from_rational` should be preferred over using `from_raw`.
public fun from_integer(integer: u32): UQ32_32 {
/// `from_int` and `from_quotient` should be preferred over using `from_raw`.
public fun from_int(integer: u32): UQ32_32 {
UQ32_32((integer as u64) << 32)
}

Expand All @@ -78,7 +79,7 @@ public fun sub(a: UQ32_32, b: UQ32_32): UQ32_32 {
UQ32_32(a.0 - b.0)
}

// Multiply two fixed-point numbers, truncating any fractional part of the product.
/// Multiply two fixed-point numbers, truncating any fractional part of the product.
/// Aborts if the product overflows.
public fun mul(a: UQ32_32, b: UQ32_32): UQ32_32 {
UQ32_32(int_mul(a.0, b))
Expand All @@ -91,6 +92,11 @@ public fun div(a: UQ32_32, b: UQ32_32): UQ32_32 {
UQ32_32(int_div(a.0, b))
}

/// Convert a fixed-point number to an integer, truncating any fractional part.
public fun to_int(a: UQ32_32): u32 {
(a.0 >> 32) as u32
}

/// Multiply a `u64` integer by a fixed-point number, truncating any fractional part of the product.
/// Aborts if the product overflows.
public fun int_mul(val: u64, multiplier: UQ32_32): u64 {
Copy link
Contributor

Choose a reason for hiding this comment

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

The idea behind int_mul and int_div is to have a common prefix for these integer leading operations that are agnostic to the actual bitsize here.

My main question is whether or not we should have:

  • from_integer and int_mul
  • from_int and int_mul
  • from_integer and integer_mul

Copy link
Contributor

Choose a reason for hiding this comment

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

Of the options above, I prefer the second option (from_int and int_mul) for consistency while still being terse and easy to write.

Now, lets enter the land of pedantry (but I think it's useful pedantry):

I'm wondering if int/integer is what we should be using here for names since technically integers mathematically mean signed numbers. Thoughts on possibly from_nat, and nat_mul?

Similarly, from_rational may be better named from_quotient since rational has a specific mathematical meaning (including negative numbers) and more importantly the "rational" you are supplying to the function need not be an actual expressible rational number (e.g., 1/0).

Copy link
Contributor

Choose a reason for hiding this comment

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

I think having everything be "int" even while imprecise, will make everything line up nicely with the signed version

Expand Down
133 changes: 76 additions & 57 deletions crates/sui-framework/packages/move-stdlib/tests/uq32_32_tests.move
Original file line number Diff line number Diff line change
Expand Up @@ -13,145 +13,145 @@ use std::uq32_32::{
div,
int_div,
int_mul,
from_integer,
from_rational,
from_int,
from_quotient,
from_raw,
to_raw
to_raw,
};

#[test]
fun from_rational_zero() {
let x = from_rational(0, 1);
fun from_quotient_zero() {
let x = from_quotient(0, 1);
assert_eq!(x.to_raw(), 0);
}

#[test]
fun from_rational_max_numerator_denominator() {
fun from_quotient_max_numerator_denominator() {
// Test creating a 1.0 fraction from the maximum u64 value.
let f = from_rational(std::u64::max_value!(), std::u64::max_value!());
let f = from_quotient(std::u64::max_value!(), std::u64::max_value!());
let one = f.to_raw();
assert_eq!(one, 1 << 32); // 0x1.00000000
}

#[test]
#[expected_failure(abort_code = uq32_32::EDenominator)]
fun from_rational_div_zero() {
fun from_quotient_div_zero() {
// A denominator of zero should cause an arithmetic error.
from_rational(2, 0);
from_quotient(2, 0);
}

#[test]
#[expected_failure(abort_code = uq32_32::ERatioTooLarge)]
fun from_rational_ratio_too_large() {
#[expected_failure(abort_code = uq32_32::EQuotientTooLarge)]
fun from_quotient_ratio_too_large() {
// The maximum value is 2^32 - 1. Check that anything larger aborts
// with an overflow.
from_rational(1 << 32, 1); // 2^32
from_quotient(1 << 32, 1); // 2^32
}

#[test]
#[expected_failure(abort_code = uq32_32::ERatioTooSmall)]
fun from_rational_ratio_too_small() {
#[expected_failure(abort_code = uq32_32::EQuotientTooSmall)]
fun from_quotient_ratio_too_small() {
// The minimum non-zero value is 2^-32. Check that anything smaller
// aborts.
from_rational(1, (1 << 32) + 1); // 1/(2^32 + 1)
from_quotient(1, (1 << 32) + 1); // 1/(2^32 + 1)
}

#[test]
fun test_from_integer() {
assert_eq!(from_integer(0).to_raw(), 0);
assert_eq!(from_integer(1).to_raw(), 0x1_0000_0000);
assert_eq!(from_integer(std::u32::max_value!()).to_raw(), std::u32::max_value!() as u64 << 32);
fun test_from_int() {
assert_eq!(from_int(0).to_raw(), 0);
assert_eq!(from_int(1).to_raw(), 0x1_0000_0000);
assert_eq!(from_int(std::u32::max_value!()).to_raw(), std::u32::max_value!() as u64 << 32);
}

#[test]
fun test_add() {
let a = from_rational(3, 4);
assert!(a.add(from_integer(0)) == a);
let a = from_quotient(3, 4);
assert!(a.add(from_int(0)) == a);

let c = a.add(from_integer(1));
assert!(from_rational(7, 4) == c);
let c = a.add(from_int(1));
assert!(from_quotient(7, 4) == c);

let b = from_rational(1, 4);
let b = from_quotient(1, 4);
let c = a.add(b);
assert!(from_integer(1) == c);
assert!(from_int(1) == c);
}

#[test]
#[expected_failure(abort_code = uq32_32::EOverflow)]
fun test_add_overflow() {
let a = from_integer(1 << 31);
let b = from_integer(1 << 31);
let a = from_int(1 << 31);
let b = from_int(1 << 31);
let _ = a.add(b);
}

#[test]
fun test_sub() {
let a = from_integer(5);
assert_eq!(a.sub(from_integer(0)), a);
let a = from_int(5);
assert_eq!(a.sub(from_int(0)), a);

let b = from_integer(4);
let b = from_int(4);
let c = a.sub(b);
assert_eq!(from_integer(1), c);
assert_eq!(from_int(1), c);
}

#[test]
#[expected_failure(abort_code = uq32_32::EOverflow)]
fun test_sub_underflow() {
let a = from_integer(3);
let b = from_integer(5);
let a = from_int(3);
let b = from_int(5);
a.sub(b);
}

#[test]
fun test_mul() {
let a = from_rational(3, 4);
assert!(a.mul(from_integer(0)) == from_integer(0));
assert!(a.mul(from_integer(1)) == a);
let a = from_quotient(3, 4);
assert!(a.mul(from_int(0)) == from_int(0));
assert!(a.mul(from_int(1)) == a);

let b = from_rational(3, 2);
let b = from_quotient(3, 2);
let c = a.mul(b);
let expected = from_rational(9, 8);
let expected = from_quotient(9, 8);
assert_eq!(c, expected);
}

#[test]
#[expected_failure(abort_code = uq32_32::EOverflow)]
fun test_mul_overflow() {
let a = from_integer(1 << 16);
let b = from_integer(1 << 16);
let a = from_int(1 << 16);
let b = from_int(1 << 16);
let _ = a.mul(b);
}

#[test]
fun test_div() {
let a = from_rational(3, 4);
assert!(a.div(from_integer(1)) == a);
let a = from_quotient(3, 4);
assert!(a.div(from_int(1)) == a);

let b = from_integer(8);
let b = from_int(8);
let c = a.div(b);
let expected = from_rational(3, 32);
let expected = from_quotient(3, 32);
assert_eq!(c, expected);
}

#[test]
#[expected_failure(abort_code = uq32_32::EDivisionByZero)]
fun test_div_by_zero() {
let a = from_integer(7);
let b = from_integer(0);
let a = from_int(7);
let b = from_int(0);
let _ = a.div(b);
}

#[test]
#[expected_failure(abort_code = uq32_32::EOverflow)]
fun test_div_overflow() {
let a = from_integer(1 << 31);
let b = from_rational(1, 2);
let a = from_int(1 << 31);
let b = from_quotient(1, 2);
let _ = a.div(b);
}

#[test]
fun exact_int_div() {
let f = from_rational(3, 4); // 0.75
let f = from_quotient(3, 4); // 0.75
let twelve = int_div(9, f); // 9 / 0.75
assert_eq!(twelve, 12);
}
Expand All @@ -175,21 +175,21 @@ fun int_div_overflow_small_divisor() {
#[test]
#[expected_failure(abort_code = uq32_32::EOverflow)]
fun int_div_overflow_large_numerator() {
let f = from_rational(1, 2); // 0.5
let f = from_quotient(1, 2); // 0.5
// Divide the maximum u64 value by 0.5. This should overflow.
int_div(std::u64::max_value!(), f);
}

#[test]
fun exact_int_mul() {
let f = from_rational(3, 4); // 0.75
let f = from_quotient(3, 4); // 0.75
let nine = int_mul(12, f); // 12 * 0.75
assert_eq!(nine, 9);
}

#[test]
fun int_mul_truncates() {
let f = from_rational(1, 3); // 0.333...
let f = from_quotient(1, 3); // 0.333...
let not_three = int_mul(9, copy f); // 9 * 0.333...
// multiply_u64 does NOT round -- it truncates -- so values that
// are not perfectly representable in binary may be off by one.
Expand All @@ -204,7 +204,7 @@ fun int_mul_truncates() {
#[test]
#[expected_failure(abort_code = uq32_32::EOverflow)]
fun int_mul_overflow_small_multiplier() {
let f = from_rational(3, 2); // 1.5
let f = from_quotient(3, 2); // 1.5
// Multiply the maximum u64 value by 1.5. This should overflow.
int_mul(std::u64::max_value!(), f);
}
Expand All @@ -219,20 +219,39 @@ fun int_mul_overflow_large_multiplier() {

#[test]
fun test_comparison() {
let a = from_rational(5, 2);
let b = from_rational(5, 3);
let c = from_rational(5, 2);
let a = from_quotient(5, 2);
let b = from_quotient(5, 3);
let c = from_quotient(5, 2);

assert!(b.le(a));
assert!(b.lt(a));
assert!(c.le(a));
assert_eq!(c, a);
assert!(a.ge(b));
assert!(a.gt(b));
assert!(from_integer(0).le(a));
assert!(from_int(0).le(a));
}

#[random_test]
fun test_raw(raw: u64) {
assert_eq!(from_raw(raw).to_raw(), raw);
}

#[random_test]
fun test_int_roundtrip(c: u32) {
assert_eq!(from_int(c).to_int(), c);
}

#[random_test]
fun test_mul_rand(n: u16, d: u16, c: u16) {
if (d == 0) return;
let q = from_quotient(n as u64, d as u64);
assert_eq!(int_mul(c as u64, q), q.mul(from_int(c as u32)).to_int() as u64);
}

#[random_test]
fun test_div_rand(n: u16, d: u16, c: u16) {
if (d == 0) return;
let q = from_quotient(n as u64, d as u64);
assert_eq!(int_div(c as u64, q), from_int(c as u32).div(q).to_int() as u64);
}
Comment on lines +240 to +257
Copy link
Contributor

Choose a reason for hiding this comment

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

❤️

Loading