diff --git a/math/core/sources/decimal_scaling.move b/math/core/sources/decimal_scaling.move index 886bb524..2fc44bd6 100644 --- a/math/core/sources/decimal_scaling.move +++ b/math/core/sources/decimal_scaling.move @@ -59,7 +59,7 @@ const MAX_DECIMALS: u8 = 24; /// /// #### Examples /// -/// ``` +/// ```move /// // Scaling down: Ethereum to Sui (precision preserved for clean values) /// // 1.0 token with 18 decimals = 1000000000000000000 /// // 1.0 token with 9 decimals = 1000000000 @@ -132,7 +132,7 @@ public fun safe_downcast_balance(raw_amount: u256, source_decimals: u8, target_d /// /// #### Examples /// -/// ``` +/// ```move /// // Scaling up: Sui to Ethereum (no precision loss) /// // 1.0 token with 9 decimals = 1000000000 /// // 1.0 token with 18 decimals = 1000000000000000000 @@ -155,7 +155,7 @@ public fun safe_upcast_balance(amount: u64, source_decimals: u8, target_decimals /// Internal helper to scale an amount between different decimal precisions. /// -/// # Truncation Behavior +/// #### Truncation Behavior /// /// When scaling down (source_decimals > target_decimals), this function uses /// integer division which TRUNCATES the result. Fractional parts are discarded, @@ -177,10 +177,16 @@ public fun safe_upcast_balance(amount: u64, source_decimals: u8, target_decimals /// /// #### Examples /// -/// - Scaling up: amount=1000000, source=6, target=9 → 1000000000 (no precision loss). -/// - Scaling down: amount=1000000000, source=9, target=6 → 1000000. -/// - Same decimals: amount=1000000000, source=9, target=9 → 1000000000 (no conversion). -/// - Truncation example: amount=1999000000, source=9, target=0 → 1 (not 2). +/// ```move +/// // Scaling up (no precision loss): 1000000 at 6 decimals -> 1000000000 at 9 decimals. +/// let up = scale_amount(1000000, 6, 9); +/// // Scaling down: 1000000000 at 9 decimals -> 1000000 at 6 decimals. +/// let down = scale_amount(1000000000, 9, 6); +/// // Same decimals: no conversion. +/// let same = scale_amount(1000000000, 9, 9); +/// // Truncation: 1999000000 at 9 decimals -> 1 at 0 decimals (not 2). +/// let truncated = scale_amount(1999000000, 9, 0); +/// ``` fun scale_amount(amount: u256, source_decimals: u8, target_decimals: u8): u256 { // Fast path: same decimals, no scaling needed. if (source_decimals == target_decimals) { diff --git a/math/core/sources/internal/macros.move b/math/core/sources/internal/macros.move index dfd51669..e6418707 100644 --- a/math/core/sources/internal/macros.move +++ b/math/core/sources/internal/macros.move @@ -10,8 +10,10 @@ use openzeppelin_math::u512; // === Errors === +/// Raised when a division or modular operation is given a zero divisor. #[error(code = 0)] const EDivideByZero: vector = "Divisor must be non-zero"; +/// Raised when a modular operation is given a zero modulus. #[error(code = 1)] const EZeroModulus: vector = "Modulus must be non-zero"; @@ -19,11 +21,17 @@ const EZeroModulus: vector = "Modulus must be non-zero"; /// Maximum floor(log10(n)) for any non-zero `u256`. const MAX_LOG10: u8 = 77; +/// 10^2, the base step in the binary-exponentiation log10 ladder. const TEN_POW_2: u256 = 100; +/// 10^4 = (10^2)^2. const TEN_POW_4: u256 = TEN_POW_2 * TEN_POW_2; +/// 10^8 = (10^4)^2. const TEN_POW_8: u256 = TEN_POW_4 * TEN_POW_4; +/// 10^16 = (10^8)^2. const TEN_POW_16: u256 = TEN_POW_8 * TEN_POW_8; +/// 10^32 = (10^16)^2. const TEN_POW_32: u256 = TEN_POW_16 * TEN_POW_16; +/// 10^64 = (10^32)^2. const TEN_POW_64: u256 = TEN_POW_32 * TEN_POW_32; // === Package Functions === @@ -88,8 +96,8 @@ public(package) macro fun average<$Int>($a: $Int, $b: $Int, $rounding_mode: Roun /// - `option::none()`. /// /// #### Aborts -/// - Does not emit custom errors, but will inherit the Move abort that occurs when `$shift` is greater -/// - than or equal to the bit-width of `$Int`. +/// - Does not emit custom errors, but inherits the Move abort that occurs when `$shift` is +/// greater than or equal to the bit-width of `$Int`. public(package) macro fun checked_shl<$Int>($value: $Int, $shift: u8): Option<$Int> { let (value, shift) = ($value, $shift); if (shift == 0) { @@ -126,8 +134,8 @@ public(package) macro fun checked_shl<$Int>($value: $Int, $shift: u8): Option<$I /// - `option::none()`. /// /// #### Aborts -/// - Does not emit custom errors, but will inherit the Move abort that occurs when `$shift` is greater -/// - than or equal to the bit-width of `$Int`. +/// - Does not emit custom errors, but inherits the Move abort that occurs when `$shift` is +/// greater than or equal to the bit-width of `$Int`. public(package) macro fun checked_shr<$Int>($value: $Int, $shift: u8): Option<$Int> { let (value, shift) = ($value, $shift); let mask = (1_u256 << shift) - 1; @@ -155,8 +163,8 @@ public(package) macro fun checked_shr<$Int>($value: $Int, $shift: u8): Option<$I /// - `$rounding_mode`: Rounding strategy. /// /// #### Returns -/// - `(overflow, result)` where `overflow` is `true` when the rounded quotient exceeds `u256::MAX` and -/// - `result` carries the rounded value when no overflow occurred. +/// - `(overflow, result)` where `overflow` is `true` when the rounded quotient exceeds `u256::MAX`, +/// and `result` carries the rounded value when no overflow occurred. /// /// #### Aborts /// - Propagates the same error codes as the underlying helpers (`EDivideByZero`). @@ -191,8 +199,8 @@ public(package) macro fun mul_div<$Int>( /// - `$rounding_mode`: Rounding strategy drawn from `rounding::RoundingMode`. /// /// #### Returns -/// - `(overflow, result)` where `overflow` reports that the rounded value cannot fit in 256 bits and -/// - `result` contains the rounded quotient when no overflow occurs. +/// - `(overflow, result)` where `overflow` reports that the rounded value cannot fit in 256 bits, +/// and `result` contains the rounded quotient when no overflow occurs. /// /// #### Aborts /// - Does not emit custom errors. @@ -238,6 +246,9 @@ public(package) macro fun clz<$Int>($value: $Int, $bit_width: u16): u16 { /// `$value` and `$modulus` are co-prime. If the inverse does not exist, the function returns /// `option::none()`. /// +/// #### Generics +/// - `$Int`: Any unsigned integer type (`u8`, `u16`, `u32`, `u64`, `u128`, or `u256`). +/// /// #### Parameters /// - `$value`: Unsigned integer whose inverse is being computed. /// - `$modulus`: Modulus for the arithmetic; must be non-zero. @@ -247,7 +258,7 @@ public(package) macro fun clz<$Int>($value: $Int, $bit_width: u16): u16 { /// - `option::none()` when `value` and `modulus` are not co-prime, or when `modulus` is 1. /// /// #### Aborts -/// - Aborts if `modulus` is zero. +/// - `EZeroModulus` if `$modulus` is zero. public(package) macro fun inv_mod<$Int>($value: $Int, $modulus: $Int): Option<$Int> { let value_u256 = ($value as u256); let modulus_u256 = ($modulus as u256); @@ -260,12 +271,18 @@ public(package) macro fun inv_mod<$Int>($value: $Int, $modulus: $Int): Option<$I /// Uses the shared internal helper that automatically chooses between the fast `u256` path and /// the wide `u512` implementation. The modulus must be non-zero. /// +/// #### Generics +/// - `$Int`: Any unsigned integer type (`u8`, `u16`, `u32`, `u64`, `u128`, or `u256`). +/// /// #### Parameters /// - `$a`, `$b`: Unsigned operands. /// - `$modulus`: Modulus for the arithmetic; must be non-zero. /// /// #### Returns /// - The product reduced modulo `$modulus`. +/// +/// #### Aborts +/// - `EZeroModulus` if `$modulus` is zero. public(package) macro fun mul_mod<$Int>($a: $Int, $b: $Int, $modulus: $Int): $Int { let a_u256 = ($a as u256); let b_u256 = ($b as u256); @@ -481,6 +498,9 @@ public(package) fun log10_floor(value: u256): u8 { /// /// #### Returns /// - `(overflow, quotient)` mirroring the macro implementation. +/// +/// #### Aborts +/// - `EDivideByZero` if `denominator` is zero. public(package) fun mul_div_inner( a: u256, b: u256, @@ -507,8 +527,8 @@ public(package) fun mul_div_inner( /// - `rounding_mode`: Rounding strategy drawn from `rounding::RoundingMode`. /// /// #### Returns -/// `(overflow, quotient)` where `overflow` is always `false` for this fast path and `quotient` -/// carries the rounded result. +/// - `(overflow, quotient)` where `overflow` is always `false` for this fast path and `quotient` +/// carries the rounded result. /// /// #### Aborts /// - `EDivideByZero` if `denominator` is zero. @@ -546,8 +566,8 @@ public(package) fun mul_div_u256_fast( /// - `rounding_mode`: Rounding strategy drawn from `rounding::RoundingMode`. /// /// #### Returns -/// - `(overflow, result)` where `overflow` indicates whether the exact (or rounded) quotient exceeds -/// - the `u256` range. `result` is only meaningful when `overflow` is `false`. +/// - `(overflow, result)` where `overflow` indicates whether the exact (or rounded) quotient +/// exceeds the `u256` range. `result` is only meaningful when `overflow` is `false`. /// /// #### Aborts /// - `EDivideByZero` if `denominator` is zero. @@ -642,8 +662,8 @@ public(package) fun mul_shr_u256_fast( /// - `rounding_mode`: Rounding strategy drawn from `rounding::RoundingMode`. /// /// #### Returns -/// - `(overflow, result)` where `overflow` indicates whether the shifted value cannot fit in 256 bits -/// - and `result` contains the shifted and rounded value when no overflow occurred. +/// - `(overflow, result)` where `overflow` indicates whether the shifted value cannot fit in +/// 256 bits and `result` contains the shifted and rounded value when no overflow occurred. public(package) fun mul_shr_u256_wide( a: u256, b: u256, @@ -721,8 +741,8 @@ public(package) macro fun sqrt<$Int>($value: $Int, $rounding_mode: RoundingMode) /// - `rounding_mode`: Rounding strategy. /// /// #### Returns -/// - `(overflow, result)` where `overflow` is `true` if the rounded value cannot be represented as -/// - `u256`. +/// - `(overflow, result)` where `overflow` is `true` if the rounded value cannot be represented +/// as `u256`. public(package) fun round_division_result( result: u256, denominator: u256, @@ -769,8 +789,8 @@ public(package) fun round_division_result( /// Tie-break: equality goes up (`≥`), i.e., “round half up”. /// /// #### Returns -/// - The rounded base-2 logarithm: `floor_log + 1` when the midpoint threshold is met, otherwise -/// - `floor_log`. +/// - The rounded base-2 logarithm: `floor_log + 1` when the midpoint threshold is met, +/// otherwise `floor_log`. public(package) fun round_log2_to_nearest(value: u256, floor_log: u16): u16 { let threshold_exp = 2 * floor_log + 1; let max_small = std::u128::max_value!() as u256; @@ -813,8 +833,8 @@ public(package) fun round_log2_to_nearest(value: u256, floor_log: u16): u16 { /// Tie-break: equality goes up (`≥`), i.e., “round half up”. /// /// #### Returns -/// - The rounded base-256 logarithm: `floor_log + 1` when the midpoint threshold is met, otherwise -/// - `floor_log`. +/// - The rounded base-256 logarithm: `floor_log + 1` when the midpoint threshold is met, +/// otherwise `floor_log`. public(package) fun round_log256_to_nearest(value: u256, floor_log: u8): u8 { // For u256 values, floor_log ∈ [0, 31], so `threshold_exp = 8 * floor_log + 4 ≤ 252` // and the power-of-two threshold fits safely in u256. @@ -951,7 +971,11 @@ public(package) fun round_sqrt_result( /// - `modulus`: Modulus, must be non-zero. /// /// #### Returns -/// - `option::some(inverse)` when `value` and `modulus` are co-prime, otherwise `option::none()`. +/// - `option::some(inverse)` when `value` and `modulus` are co-prime. +/// - `option::none()` when they are not co-prime, or when `modulus` is 1. +/// +/// #### Aborts +/// - `EZeroModulus` if `modulus` is zero. public(package) fun inv_mod_extended_impl(value: u256, modulus: u256): Option { // Guard against invalid modulus values up front. assert!(modulus != 0, EZeroModulus); diff --git a/math/core/sources/u512.move b/math/core/sources/u512.move index 4107063c..708de5ae 100644 --- a/math/core/sources/u512.move +++ b/math/core/sources/u512.move @@ -8,25 +8,33 @@ use openzeppelin_math::common; // === Errors === +/// Raised when a cross-limb addition carry exceeds the representable range. #[error(code = 0)] const ECarryOverflow: vector = "Cross-limb addition overflowed"; +/// Raised when a subtraction borrow underflows the high limb. #[error(code = 1)] const EUnderflow: vector = "Borrow underflowed high limb"; +/// Raised when a division or modular operation is given a zero divisor. #[error(code = 2)] const EDivideByZero: vector = "Divisor must be non-zero"; +/// Raised when a division remainder leaves non-zero high bits, violating the result invariant. #[error(code = 3)] const EInvalidRemainder: vector = "High remainder bits must be zero"; // === Constants === +/// Bit-width of each half-limb used when splitting a `u256` into two 128-bit halves. const HALF_BITS: u8 = 128; +/// Bitmask selecting the low 128 bits of a `u256` value. const HALF_MASK: u256 = (1u256 << HALF_BITS) - 1; // === Structs === /// Represents a 512-bit unsigned integer as two 256-bit words. public struct U512 has copy, drop, store { + /// Upper 256 bits. hi: u256, + /// Lower 256 bits. lo: u256, } diff --git a/math/core/sources/vector.move b/math/core/sources/vector.move index 293dede8..d68be540 100644 --- a/math/core/sources/vector.move +++ b/math/core/sources/vector.move @@ -419,6 +419,13 @@ public macro fun median<$Int>($vec: &vector<$Int>, $rounding_mode: RoundingMode) /// The input vector is not mutated. See `median!` for the full contract and the bytecode /// vs. gas trade-off. /// +/// #### Parameters +/// - `vec`: Reference to the vector whose median is desired. +/// - `rounding_mode`: Rounding strategy, applied only when the length is even. +/// +/// #### Returns +/// - The median of `vec`. +/// /// #### Aborts /// - `EMedianOfEmptyVector` if `vec` is empty. public fun median_u8(vec: &vector, rounding_mode: RoundingMode): u8 { @@ -433,6 +440,13 @@ public fun median_u8(vec: &vector, rounding_mode: RoundingMode): u8 { /// The input vector is not mutated. See `median!` for the full contract and the bytecode /// vs. gas trade-off. /// +/// #### Parameters +/// - `vec`: Reference to the vector whose median is desired. +/// - `rounding_mode`: Rounding strategy, applied only when the length is even. +/// +/// #### Returns +/// - The median of `vec`. +/// /// #### Aborts /// - `EMedianOfEmptyVector` if `vec` is empty. public fun median_u16(vec: &vector, rounding_mode: RoundingMode): u16 { @@ -447,6 +461,13 @@ public fun median_u16(vec: &vector, rounding_mode: RoundingMode): u16 { /// The input vector is not mutated. See `median!` for the full contract and the bytecode /// vs. gas trade-off. /// +/// #### Parameters +/// - `vec`: Reference to the vector whose median is desired. +/// - `rounding_mode`: Rounding strategy, applied only when the length is even. +/// +/// #### Returns +/// - The median of `vec`. +/// /// #### Aborts /// - `EMedianOfEmptyVector` if `vec` is empty. public fun median_u32(vec: &vector, rounding_mode: RoundingMode): u32 { @@ -461,6 +482,13 @@ public fun median_u32(vec: &vector, rounding_mode: RoundingMode): u32 { /// The input vector is not mutated. See `median!` for the full contract and the bytecode /// vs. gas trade-off. /// +/// #### Parameters +/// - `vec`: Reference to the vector whose median is desired. +/// - `rounding_mode`: Rounding strategy, applied only when the length is even. +/// +/// #### Returns +/// - The median of `vec`. +/// /// #### Aborts /// - `EMedianOfEmptyVector` if `vec` is empty. public fun median_u64(vec: &vector, rounding_mode: RoundingMode): u64 { @@ -475,6 +503,13 @@ public fun median_u64(vec: &vector, rounding_mode: RoundingMode): u64 { /// The input vector is not mutated. See `median!` for the full contract and the bytecode /// vs. gas trade-off. /// +/// #### Parameters +/// - `vec`: Reference to the vector whose median is desired. +/// - `rounding_mode`: Rounding strategy, applied only when the length is even. +/// +/// #### Returns +/// - The median of `vec`. +/// /// #### Aborts /// - `EMedianOfEmptyVector` if `vec` is empty. public fun median_u128(vec: &vector, rounding_mode: RoundingMode): u128 { @@ -489,6 +524,13 @@ public fun median_u128(vec: &vector, rounding_mode: RoundingMode): u128 { /// The input vector is not mutated. See `median!` for the full contract and the bytecode /// vs. gas trade-off. /// +/// #### Parameters +/// - `vec`: Reference to the vector whose median is desired. +/// - `rounding_mode`: Rounding strategy, applied only when the length is even. +/// +/// #### Returns +/// - The median of `vec`. +/// /// #### Aborts /// - `EMedianOfEmptyVector` if `vec` is empty. public fun median_u256(vec: &vector, rounding_mode: RoundingMode): u256 { diff --git a/math/core/tests/decimal_scaling_tests.move b/math/core/tests/decimal_scaling_tests.move index 1dcf0b6f..c84be139 100644 --- a/math/core/tests/decimal_scaling_tests.move +++ b/math/core/tests/decimal_scaling_tests.move @@ -4,7 +4,7 @@ module openzeppelin_math::decimal_scaling_tests; use openzeppelin_math::decimal_scaling; use std::unit_test::assert_eq; -// === Test Helpers === +// === Test-Only Helpers === /// Build a test value with specified base and decimal places. /// diff --git a/math/core/tests/macros/median.move b/math/core/tests/macros/median.move index 2b93c4b8..6893cd5a 100644 --- a/math/core/tests/macros/median.move +++ b/math/core/tests/macros/median.move @@ -453,6 +453,10 @@ fun median_u128_basics() { std::u128::max_value!() / 2, ); assert_eq!(vector::median_u128(&vector[4u128, 1, 4, 4, 2, 4], rounding::down()), 4); + // width extreme: the 0.5 tie rounds up under `up` and `nearest`, and must not overflow + let max = std::u128::max_value!(); + assert_eq!(vector::median_u128(&vector[0u128, max], rounding::up()), max / 2 + 1); + assert_eq!(vector::median_u128(&vector[0u128, max], rounding::nearest()), max / 2 + 1); } #[test] @@ -470,6 +474,14 @@ fun median_u32_even_length_rounding_modes() { assert_eq!(vector::median_u32(&vector[1u32, 2, 5, 9], rounding::up()), 4); } +#[test] +fun median_u128_even_length_rounding_modes() { + // sorted: [1, 2, 5, 9]; central pair (2, 5) -> (2 + 5) / 2 = 3.5 + assert_eq!(vector::median_u128(&vector[1u128, 2, 5, 9], rounding::down()), 3); + assert_eq!(vector::median_u128(&vector[1u128, 2, 5, 9], rounding::nearest()), 4); + assert_eq!(vector::median_u128(&vector[1u128, 2, 5, 9], rounding::up()), 4); +} + #[test] fun median_odd_length_rounding_modes_agree_u16() { let v = vector[5u16, 1, 9, 3, 7];