From 3fa9db906e09a5500a97cc0b330eca4fbb2838a7 Mon Sep 17 00:00:00 2001 From: metaproph3t Date: Sat, 26 Jul 2025 00:00:00 -0700 Subject: [PATCH] Add `256-bit` feature --- Cargo.toml | 1 + README.md | 35 +++++++- src/consts.rs | 216 ++++++++++++++++++++++++++++++++++++++++++------ src/exp.rs | 48 +++++++---- src/lib.rs | 6 ++ src/signed.rs | 11 ++- src/unsigned.rs | 135 +++++++++++++++++++++++++----- 7 files changed, 385 insertions(+), 67 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 79c65b8..4e707f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ crate-type = ["lib"] [features] default = ["std"] std = ["uint/std"] +256-bit = [] [dependencies] uint = { version = "0.9", default-features = false } diff --git a/README.md b/README.md index 060293d..86bc5de 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ![license][license-image] [![crates.io](https://img.shields.io/crates/v/brine-fp.svg?style=flat)](https://crates.io/crates/brine-fp) -`brine-fp` is a 192-bit fixed-point math library built for high-precision, deterministic computation in constrained environments like the Solana SVM. It provides arithmetic, exponentiation, logarithms, and powers using `u192`-based representations, and is optimized for low compute unit (CU) usage on-chain. +`brine-fp` is a high-precision fixed-point math library built for deterministic computation in constrained environments like the Solana SVM. It provides arithmetic, exponentiation, logarithms, and powers using configurable bit-width representations, and is optimized for low compute unit (CU) usage on-chain. --- @@ -23,7 +23,9 @@ These values are measured inside the Solana SVM (via test programs). ## Features -- 192-bit unsigned & signed fixed-point types with 18 decimal places. +- Configurable bit-width fixed-point types with 18 decimal places: + - **Default**: 192-bit unsigned & signed types + - **With `256-bit` feature**: 256-bit unsigned & signed types - Supports `log`, `exp`, `pow`, `floor`, `ceil`, `almost_eq`, etc. - Remez polynomial approximations based on FreeBSD `msun`. - Designed for deterministic and overflow-aware computation. @@ -47,10 +49,26 @@ println!("e^5 ≈ {}", result.to_string()); --- +## Feature Flags + +### 256-bit Support + +Enable 256-bit precision by adding the feature flag to your `Cargo.toml`: + +```toml +[dependencies] +brine-fp = { version = "0.2.0", features = ["256-bit"] } +``` + +This changes the internal representation from 192-bit to 256-bit, providing even larger dynamic range. + +--- + ## Internal Representation -Each `UnsignedNumeric` wraps a `InnerUint([lo, mid, hi])`, representing a 192-bit unsigned integer: +Each `UnsignedNumeric` wraps a `InnerUint`, representing a configurable-width unsigned integer: +**Default (192-bit)**: ```rust InnerUint([lo, mid, hi]) @@ -58,11 +76,20 @@ InnerUint([lo, mid, hi]) // value = lo + (mid << 64) + (hi << 128) ``` +**With `256-bit` feature**: +```rust +InnerUint([lo, mid, hi, hi2]) + +// Equivalent to: +// value = lo + (mid << 64) + (hi << 128) + (hi2 << 192) +``` + All values are scaled by `10^18`, enabling 18-digit decimal precision. This format ensures: -- Extreme range: from `1e-18` up to `~6.3 × 10³⁹` real-world units +- **192-bit**: Range from `1e-18` up to `~6.3 × 10³⁹` real-world units +- **256-bit**: Range from `1e-18` up to `~1.2 × 10⁵⁸` real-world units - High precision: 18 decimals - Fully deterministic integer math (no `f64`, no `float`) diff --git a/src/consts.rs b/src/consts.rs index 447bf75..f62eb6e 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -14,39 +14,81 @@ pub static TWO_PREC: UnsignedNumeric = UnsignedNumeric { value: two() }; /// Zero value in fixed-point format. #[inline] pub const fn zero() -> InnerUint { - InnerUint([0, 0, 0]) + #[cfg(feature = "256-bit")] + { + InnerUint([0, 0, 0, 0]) + } + #[cfg(not(feature = "256-bit"))] + { + InnerUint([0, 0, 0]) + } } /// Returns the internal representation of 1.0 in fixed-point format. #[inline] pub const fn one() -> InnerUint { - InnerUint([ONE as u64, 0, 0]) + #[cfg(feature = "256-bit")] + { + InnerUint([ONE as u64, 0, 0, 0]) + } + #[cfg(not(feature = "256-bit"))] + { + InnerUint([ONE as u64, 0, 0]) + } } /// Returns the internal representation of 2.0 in fixed-point format. #[inline] pub const fn two() -> InnerUint { - InnerUint([2 * ONE as u64, 0, 0]) + #[cfg(feature = "256-bit")] + { + InnerUint([2 * ONE as u64, 0, 0, 0]) + } + #[cfg(not(feature = "256-bit"))] + { + InnerUint([2 * ONE as u64, 0, 0]) + } } /// Fixed-point representation of 0.5 (HALF). #[inline] pub const fn half() -> InnerUint { - InnerUint([500000000000000000_u64, 0, 0]) + #[cfg(feature = "256-bit")] + { + InnerUint([500000000000000000_u64, 0, 0, 0]) + } + #[cfg(not(feature = "256-bit"))] + { + InnerUint([500000000000000000_u64, 0, 0]) + } } pub const HALF: UnsignedNumeric = UnsignedNumeric { value: half() }; /// High part of ln(2), used for logarithmic calculations. Stored as a fixed-point number. #[inline] pub const fn ln2hi() -> InnerUint { - InnerUint([13974485815783726801_u64, 3_u64, 0]) + #[cfg(feature = "256-bit")] + { + InnerUint([13974485815783726801_u64, 3_u64, 0, 0]) + } + #[cfg(not(feature = "256-bit"))] + { + InnerUint([13974485815783726801_u64, 3_u64, 0]) + } } pub const LN2HI: UnsignedNumeric = UnsignedNumeric { value: ln2hi() }; /// Scaled variant of ln2hi for internal use in higher-precision approximations. #[inline] pub const fn ln2hi_scale() -> InnerUint { - InnerUint([7766279631452241920_u64, 5_u64, 0]) + #[cfg(feature = "256-bit")] + { + InnerUint([7766279631452241920_u64, 5_u64, 0, 0]) + } + #[cfg(not(feature = "256-bit"))] + { + InnerUint([7766279631452241920_u64, 5_u64, 0]) + } } pub const LN2HI_SCALE: UnsignedNumeric = UnsignedNumeric { value: ln2hi_scale() }; @@ -54,28 +96,56 @@ pub const LN2HI_SCALE: UnsignedNumeric = UnsignedNumeric { value: ln2hi_scale() #[inline] pub const fn ln2lo() -> InnerUint { // Note that ln2lo is lower than our max precision, so we store both it and the thirty zeroes to scale by - InnerUint([3405790746697269248_u64, 1034445385942222_u64, 0]) + #[cfg(feature = "256-bit")] + { + InnerUint([3405790746697269248_u64, 1034445385942222_u64, 0, 0]) + } + #[cfg(not(feature = "256-bit"))] + { + InnerUint([3405790746697269248_u64, 1034445385942222_u64, 0]) + } } pub const LN2LO: UnsignedNumeric = UnsignedNumeric { value: ln2lo() }; /// Scaled low part of ln(2), for use in precision-sensitive computations. #[inline] pub const fn ln2lo_scale() -> InnerUint { - InnerUint([80237960548581376_u64, 10841254275107988496_u64, 293873_u64]) + #[cfg(feature = "256-bit")] + { + InnerUint([80237960548581376_u64, 10841254275107988496_u64, 293873_u64, 0]) + } + #[cfg(not(feature = "256-bit"))] + { + InnerUint([80237960548581376_u64, 10841254275107988496_u64, 293873_u64]) + } } pub const LN2LO_SCALE: UnsignedNumeric = UnsignedNumeric { value: ln2lo_scale() }; /// Constant for 1/2 * ln(2), useful in logarithmic calculations. #[inline] pub const fn halfln2() -> InnerUint { - InnerUint([346573590279972640_u64, 0_u64, 0_u64]) + #[cfg(feature = "256-bit")] + { + InnerUint([346573590279972640_u64, 0_u64, 0_u64, 0]) + } + #[cfg(not(feature = "256-bit"))] + { + InnerUint([346573590279972640_u64, 0_u64, 0_u64]) + } } pub const HALFLN2: UnsignedNumeric = UnsignedNumeric { value: halfln2() }; /// Constant for 3/2 * ln(2), useful in logarithmic calculations. #[inline] pub const fn threehalfln2() -> InnerUint { - InnerUint([1039720770839917900_u64, 0_u64, 0_u64]) + #[cfg(feature = "256-bit")] + { + InnerUint([1039720770839917900_u64, 0_u64, 0_u64, 0]) + } + #[cfg(not(feature = "256-bit"))] + { + InnerUint([1039720770839917900_u64, 0_u64, 0_u64]) + } } pub const THREEHALFLN2: UnsignedNumeric = UnsignedNumeric { value: threehalfln2(), @@ -84,14 +154,28 @@ pub const THREEHALFLN2: UnsignedNumeric = UnsignedNumeric { /// Constant for 1/ln(2), useful in logarithmic calculations. #[inline] pub const fn invln2() -> InnerUint { - InnerUint([1442695040888963387_u64, 0_u64, 0_u64]) + #[cfg(feature = "256-bit")] + { + InnerUint([1442695040888963387_u64, 0_u64, 0_u64, 0]) + } + #[cfg(not(feature = "256-bit"))] + { + InnerUint([1442695040888963387_u64, 0_u64, 0_u64]) + } } pub const INVLN2: UnsignedNumeric = UnsignedNumeric { value: invln2() }; /// Constant for sqrt(2)/2, useful in trig/log calculations. #[inline] pub const fn sqrt2overtwo() -> InnerUint { - InnerUint([707106781186547600_u64, 0, 0]) + #[cfg(feature = "256-bit")] + { + InnerUint([707106781186547600_u64, 0, 0, 0]) + } + #[cfg(not(feature = "256-bit"))] + { + InnerUint([707106781186547600_u64, 0, 0]) + } } pub const SQRT2OVERTWO: UnsignedNumeric = UnsignedNumeric { value: sqrt2overtwo() }; @@ -99,42 +183,91 @@ pub const SQRT2OVERTWO: UnsignedNumeric = UnsignedNumeric { value: sqrt2overtwo( #[inline] pub const fn l1() -> InnerUint { - InnerUint([666666666666673513_u64, 0_u64, 0_u64]) + #[cfg(feature = "256-bit")] + { + InnerUint([666666666666673513_u64, 0_u64, 0_u64, 0]) + } + #[cfg(not(feature = "256-bit"))] + { + InnerUint([666666666666673513_u64, 0_u64, 0_u64]) + } } pub const L1: UnsignedNumeric = UnsignedNumeric { value: l1() }; #[inline] pub const fn l2() -> InnerUint { - InnerUint([399999999994094190_u64, 0_u64, 0_u64]) + #[cfg(feature = "256-bit")] + { + InnerUint([399999999994094190_u64, 0_u64, 0_u64, 0]) + } + #[cfg(not(feature = "256-bit"))] + { + InnerUint([399999999994094190_u64, 0_u64, 0_u64]) + } } pub const L2: UnsignedNumeric = UnsignedNumeric { value: l2() }; #[inline] pub const fn l3() -> InnerUint { - InnerUint([285714287436623914_u64, 0_u64, 0_u64]) + #[cfg(feature = "256-bit")] + { + InnerUint([285714287436623914_u64, 0_u64, 0_u64, 0]) + } + #[cfg(not(feature = "256-bit"))] + { + InnerUint([285714287436623914_u64, 0_u64, 0_u64]) + } } pub const L3: UnsignedNumeric = UnsignedNumeric { value: l3() }; #[inline] pub const fn l4() -> InnerUint { - InnerUint([222221984321497839_u64, 0_u64, 0_u64]) + #[cfg(feature = "256-bit")] + { + InnerUint([222221984321497839_u64, 0_u64, 0_u64, 0]) + } + #[cfg(not(feature = "256-bit"))] + { + InnerUint([222221984321497839_u64, 0_u64, 0_u64]) + } } pub const L4: UnsignedNumeric = UnsignedNumeric { value: l4() }; #[inline] pub const fn l5() -> InnerUint { - InnerUint([181835721616180501_u64, 0_u64, 0_u64]) + #[cfg(feature = "256-bit")] + { + InnerUint([181835721616180501_u64, 0_u64, 0_u64, 0]) + } + #[cfg(not(feature = "256-bit"))] + { + InnerUint([181835721616180501_u64, 0_u64, 0_u64]) + } } pub const L5: UnsignedNumeric = UnsignedNumeric { value: l5() }; pub const fn l6() -> InnerUint { - InnerUint([153138376992093733_u64, 0_u64, 0_u64]) + #[cfg(feature = "256-bit")] + { + InnerUint([153138376992093733_u64, 0_u64, 0_u64, 0]) + } + #[cfg(not(feature = "256-bit"))] + { + InnerUint([153138376992093733_u64, 0_u64, 0_u64]) + } } pub const L6: UnsignedNumeric = UnsignedNumeric { value: l6() }; #[inline] pub const fn l7() -> InnerUint { - InnerUint([147981986051165859_u64, 0_u64, 0_u64]) + #[cfg(feature = "256-bit")] + { + InnerUint([147981986051165859_u64, 0_u64, 0_u64, 0]) + } + #[cfg(not(feature = "256-bit"))] + { + InnerUint([147981986051165859_u64, 0_u64, 0_u64]) + } } pub const L7: UnsignedNumeric = UnsignedNumeric { value: l7() }; @@ -142,7 +275,14 @@ pub const L7: UnsignedNumeric = UnsignedNumeric { value: l7() }; #[inline] pub const fn p1() -> InnerUint { - InnerUint([166666666666666019_u64, 0_u64, 0_u64]) + #[cfg(feature = "256-bit")] + { + InnerUint([166666666666666019_u64, 0_u64, 0_u64, 0]) + } + #[cfg(not(feature = "256-bit"))] + { + InnerUint([166666666666666019_u64, 0_u64, 0_u64]) + } } pub const P1: SignedNumeric = SignedNumeric { value: UnsignedNumeric { value: p1() }, @@ -151,7 +291,14 @@ pub const P1: SignedNumeric = SignedNumeric { #[inline] pub const fn p2() -> InnerUint { - InnerUint([2777777777701559_u64, 0_u64, 0_u64]) + #[cfg(feature = "256-bit")] + { + InnerUint([2777777777701559_u64, 0_u64, 0_u64, 0]) + } + #[cfg(not(feature = "256-bit"))] + { + InnerUint([2777777777701559_u64, 0_u64, 0_u64]) + } } pub const P2: SignedNumeric = SignedNumeric { value: UnsignedNumeric { value: p2() }, @@ -160,7 +307,14 @@ pub const P2: SignedNumeric = SignedNumeric { #[inline] pub const fn p3() -> InnerUint { - InnerUint([66137563214379_u64, 0_u64, 0_u64]) + #[cfg(feature = "256-bit")] + { + InnerUint([66137563214379_u64, 0_u64, 0_u64, 0]) + } + #[cfg(not(feature = "256-bit"))] + { + InnerUint([66137563214379_u64, 0_u64, 0_u64]) + } } pub const P3: SignedNumeric = SignedNumeric { value: UnsignedNumeric { value: p3() }, @@ -169,7 +323,14 @@ pub const P3: SignedNumeric = SignedNumeric { #[inline] pub const fn p4() -> InnerUint { - InnerUint([1653390220546_u64, 0_u64, 0_u64]) + #[cfg(feature = "256-bit")] + { + InnerUint([1653390220546_u64, 0_u64, 0_u64, 0]) + } + #[cfg(not(feature = "256-bit"))] + { + InnerUint([1653390220546_u64, 0_u64, 0_u64]) + } } pub const P4: SignedNumeric = SignedNumeric { value: UnsignedNumeric { value: p4() }, @@ -178,7 +339,14 @@ pub const P4: SignedNumeric = SignedNumeric { #[inline] pub const fn p5() -> InnerUint { - InnerUint([41381367970_u64, 0_u64, 0_u64]) + #[cfg(feature = "256-bit")] + { + InnerUint([41381367970_u64, 0_u64, 0_u64, 0]) + } + #[cfg(not(feature = "256-bit"))] + { + InnerUint([41381367970_u64, 0_u64, 0_u64]) + } } pub const P5: SignedNumeric = SignedNumeric { value: UnsignedNumeric { value: p5() }, diff --git a/src/exp.rs b/src/exp.rs index d728315..4c9f23d 100644 --- a/src/exp.rs +++ b/src/exp.rs @@ -467,19 +467,39 @@ mod tests { #[test] fn test_frexp_large_value() { - let val = UnsignedNumeric { - value: InnerUint([0, 0, 1]), // 2^128 scaled fixed-point - }; - let (frac, exp) = val.frexp().unwrap(); - let recombined = frexp_recombine(frac.clone(), exp); - - assert!( - recombined.almost_eq(&val, InnerUint::from(10_000_000_000_u64)), - "Expected: {}, Got: {} × 2^{} = {}", - val.to_string(), - frac.to_string(), - exp, - recombined.to_string() - ); + #[cfg(not(feature = "256-bit"))] + { + let val = UnsignedNumeric { + value: InnerUint([0, 0, 1]), // 2^128 scaled fixed-point + }; + let (frac, exp) = val.frexp().unwrap(); + let recombined = frexp_recombine(frac.clone(), exp); + + assert!( + recombined.almost_eq(&val, InnerUint::from(10_000_000_000_u64)), + "Expected: {}, Got: {} × 2^{} = {}", + val.to_string(), + frac.to_string(), + exp, + recombined.to_string() + ); + } + #[cfg(feature = "256-bit")] + { + let val = UnsignedNumeric { + value: InnerUint([0, 0, 1, 0]), // 2^128 scaled fixed-point + }; + let (frac, exp) = val.frexp().unwrap(); + let recombined = frexp_recombine(frac.clone(), exp); + + assert!( + recombined.almost_eq(&val, InnerUint::from(10_000_000_000_u64)), + "Expected: {}, Got: {} × 2^{} = {}", + val.to_string(), + frac.to_string(), + exp, + recombined.to_string() + ); + } } } diff --git a/src/lib.rs b/src/lib.rs index d0f7acb..e5d3f11 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,6 +8,12 @@ use uint::construct_uint; +#[cfg(feature = "256-bit")] +construct_uint! { + pub struct InnerUint(4); +} + +#[cfg(not(feature = "256-bit"))] construct_uint! { pub struct InnerUint(3); } diff --git a/src/signed.rs b/src/signed.rs index 6f576a5..5d75bee 100644 --- a/src/signed.rs +++ b/src/signed.rs @@ -5,13 +5,16 @@ use core::ops::{Add, Sub, Mul, Div}; // https://github.com/solana-labs/solana-program-library/blob/v2.0/libraries/math/src/precise_number.rs // https://github.com/StrataFoundation/strata/blob/master/programs/spl-token-bonding/src/signed_precise_number.rs -/// A `SignedNumeric` represents a signed 192-bit fixed-point number with 18 decimal places of precision. +/// A `SignedNumeric` represents a signed fixed-point number with 18 decimal places of precision. /// /// This struct extends [`UnsignedNumeric`] by adding a `bool` flag to indicate whether the value is negative, /// enabling full support for signed decimal arithmetic. /// /// ### Internal Representation -/// - The magnitude is stored as a [`UnsignedNumeric`], which wraps a [`InnerUint`] representing a 192-bit unsigned integer scaled by 10¹⁸. +/// - The magnitude is stored as a [`UnsignedNumeric`], which wraps a [`InnerUint`] representing an unsigned integer scaled by 10¹⁸. +/// - The bit width depends on the feature flag: +/// - **Default (192-bit)**: 192-bit unsigned integer +/// - **With `256-bit` feature**: 256-bit unsigned integer /// - The `is_negative` flag determines the sign of the number. /// /// ### Interpretation @@ -21,8 +24,8 @@ use core::ops::{Add, Sub, Mul, Div}; /// ``` /// /// ### Examples: -/// - `value = UnsignedNumeric::from_u192([1_000_000_000_000_000_000, 0, 0]), is_negative = false` → 1.0 -/// - `value = UnsignedNumeric::from_u192([5_000_000_000_000_000_000, 0, 0]), is_negative = true` → -5.0 +/// - `value = UnsignedNumeric::from_values(1_000_000_000_000_000_000, 0, 0), is_negative = false` → 1.0 +/// - `value = UnsignedNumeric::from_values(5_000_000_000_000_000_000, 0, 0), is_negative = true` → -5.0 /// /// This format is useful for financial and scientific applications where both precision and sign are critical, /// and where floating-point inaccuracies are unacceptable. diff --git a/src/unsigned.rs b/src/unsigned.rs index 3fa319c..7412016 100644 --- a/src/unsigned.rs +++ b/src/unsigned.rs @@ -8,25 +8,34 @@ use core::ops::{Add, Sub, Mul, Div}; // https://github.com/solana-labs/solana-program-library/blob/v2.0/libraries/math/src/precise_number.rs // https://github.com/StrataFoundation/strata/blob/master/programs/spl-token-bonding/src/precise_number.rs -/// A `UnsignedNumeric` is an unsigned 192-bit fixed-point number with 18 decimal places of -/// precision. +/// A `UnsignedNumeric` is an unsigned fixed-point number with 18 decimal places of precision. /// /// ### Internal Representation -/// Internally, the value is stored as a [`InnerUint`], which wraps a little-endian array `[u64; 3]`. -/// This means the layout is: +/// Internally, the value is stored as a [`InnerUint`], which wraps a little-endian array. +/// The size depends on the feature flag: /// +/// - **Default (192-bit)**: `[u64; 3]` layout +/// - **With `256-bit` feature**: `[u64; 4]` layout +/// +/// **Default layout (192-bit)**: /// ```text /// InnerUint([lo, mid, hi]) /// // equivalent to: /// // value = lo + (mid << 64) + (hi << 128) /// ``` /// -/// Each component contributes to the full 192-bit value: -/// +/// **256-bit layout**: /// ```text -/// value = (hi × 2^128) + (mid × 2^64) + lo +/// InnerUint([lo, mid, hi, hi2]) +/// // equivalent to: +/// // value = lo + (mid << 64) + (hi << 128) + (hi2 << 192) /// ``` /// +/// Each component contributes to the full value: +/// +/// **Default**: `value = (hi × 2^128) + (mid × 2^64) + lo` +/// **256-bit**: `value = (hi2 × 2^192) + (hi × 2^128) + (mid × 2^64) + lo` +/// /// ### Fixed-Point Scaling /// All values are scaled by [`ONE`] (10^18). That is, the internal number is interpreted /// as `raw / ONE` to recover its real-world value. @@ -38,9 +47,10 @@ use core::ops::{Add, Sub, Mul, Div}; /// /// ### Example: High-Bit Usage /// +/// **Default (192-bit)**: /// When you write: /// ```text -/// let a = UnsignedNumeric::from([0, 0, 1]); +/// let a = UnsignedNumeric::from_values(0, 0, 1); /// ``` /// This initializes the internal 192-bit value with the array `[0, 0, 1]`. /// In this representation: @@ -56,12 +66,31 @@ use core::ops::{Add, Sub, Mul, Div}; /// = 340282366920938463463374607431768211456 /// ``` /// -/// Since this is a fixed-point number, the real-world value is: +/// **256-bit feature**: +/// When you write: +/// ```text +/// let a = UnsignedNumeric::from_values(0, 0, 0, 1); +/// ``` +/// This initializes the internal 256-bit value with the array `[0, 0, 0, 1]`. +/// In this representation: +/// +/// - `0` is the least significant 64 bits (`lo`) +/// - `0` is the middle 64 bits (`mid`) +/// - `0` is the high 64 bits (`hi`) +/// - `1` is the most significant 64 bits (`hi2`) +/// +/// The actual 256-bit value is computed as: /// /// ```text -/// real_value = value / 10^18 = 340282366920938463463.374607431768211456 +/// value = (1 × 2^192) + (0 × 2^128) + (0 × 2^64) + 0 = 2^192 +/// = 6277101735386680763835789423207666416102355444464034512896 /// ``` /// +/// Since this is a fixed-point number, the real-world value is: +/// +/// **Default**: `real_value = value / 10^18 = 340282366920938463463.374607431768211456` +/// **256-bit**: `real_value = value / 10^18 = 6277101735386680763835789423207666416102355444464034512896 / 10^18` +/// /// This system allows for both extremely high precision and a vast dynamic range, /// making [`UnsignedNumeric`] ideal for financial, scientific, or blockchain applications /// where `f64` or even `u128` would lose accuracy or overflow. @@ -101,13 +130,25 @@ impl UnsignedNumeric { /// Constructs a `UnsignedNumeric` directly from a raw `[u64; 3]` value. /// The input is interpreted as already scaled (fixed-point). /// Layout is little-endian: `[lo, mid, hi]` = `lo + (mid << 64) + (hi << 128)`. + #[cfg(not(feature = "256-bit"))] pub fn from_values(lo: u64, mid: u64, hi: u64) -> Self { Self { value: InnerUint([lo, mid, hi]), } } + /// Constructs a `UnsignedNumeric` directly from a raw `[u64; 4]` value. + /// The input is interpreted as already scaled (fixed-point). + /// Layout is little-endian: `[lo, mid, hi, hi2]` = `lo + (mid << 64) + (hi << 128) + (hi2 << 192)`. + #[cfg(feature = "256-bit")] + pub fn from_values(lo: u64, mid: u64, hi: u64, hi2: u64) -> Self { + Self { + value: InnerUint([lo, mid, hi, hi2]), + } + } + /// Converts this `UnsignedNumeric` into a raw `[u8; 24]` representation. + #[cfg(not(feature = "256-bit"))] pub fn to_bytes(&self) -> [u8; 24] { let InnerUint([lo, mid, hi]) = self.value; @@ -119,7 +160,22 @@ impl UnsignedNumeric { bytes } + /// Converts this `UnsignedNumeric` into a raw `[u8; 32]` representation. + #[cfg(feature = "256-bit")] + pub fn to_bytes(&self) -> [u8; 32] { + let InnerUint([lo, mid, hi, hi2]) = self.value; + + let mut bytes = [0u8; 32]; + bytes[0..8].copy_from_slice(&lo.to_le_bytes()); + bytes[8..16].copy_from_slice(&mid.to_le_bytes()); + bytes[16..24].copy_from_slice(&hi.to_le_bytes()); + bytes[24..32].copy_from_slice(&hi2.to_le_bytes()); + + bytes + } + /// Converts a raw `[u8; 24]` representation into a `UnsignedNumeric`. + #[cfg(not(feature = "256-bit"))] pub fn from_bytes(bytes: &[u8; 24]) -> Self { let lo = u64::from_le_bytes(bytes[0..8].try_into().unwrap()); let mid = u64::from_le_bytes(bytes[8..16].try_into().unwrap()); @@ -130,6 +186,19 @@ impl UnsignedNumeric { } } + /// Converts a raw `[u8; 32]` representation into a `UnsignedNumeric`. + #[cfg(feature = "256-bit")] + pub fn from_bytes(bytes: &[u8; 32]) -> Self { + let lo = u64::from_le_bytes(bytes[0..8].try_into().unwrap()); + let mid = u64::from_le_bytes(bytes[8..16].try_into().unwrap()); + let hi = u64::from_le_bytes(bytes[16..24].try_into().unwrap()); + let hi2 = u64::from_le_bytes(bytes[24..32].try_into().unwrap()); + + Self { + value: InnerUint([lo, mid, hi, hi2]), + } + } + /// Converts this `UnsignedNumeric` into a regular `u128` by dividing by ONE. /// Applies rounding correction to avoid always flooring the result. /// Returns `None` if the division would overflow or the result exceeds `u128::MAX`. @@ -552,11 +621,22 @@ mod tests { #[test] fn test_serialization() { - let original = UnsignedNumeric::from_values(123, 456, 789); - let bytes = original.to_bytes(); - let recovered = UnsignedNumeric::from_bytes(&bytes); + #[cfg(not(feature = "256-bit"))] + { + let original = UnsignedNumeric::from_values(123, 456, 789); + let bytes = original.to_bytes(); + let recovered = UnsignedNumeric::from_bytes(&bytes); + + assert_eq!(original, recovered); + } + #[cfg(feature = "256-bit")] + { + let original = UnsignedNumeric::from_values(123, 456, 789, 101112); + let bytes = original.to_bytes(); + let recovered = UnsignedNumeric::from_bytes(&bytes); - assert_eq!(original, recovered); + assert_eq!(original, recovered); + } } #[test] @@ -608,13 +688,26 @@ mod tests { #[test] fn test_mul_large_by_large_overflow() { - let a = UnsignedNumeric { - value: InnerUint([0, 0, 1]), // 2^128 - }; - let b = a.clone(); // 2^128 * 2^128 = 2^256, definitely too large - - let result = a.checked_mul(&b); - assert!(result.is_none(), "Expected overflow on large * large"); + #[cfg(not(feature = "256-bit"))] + { + let a = UnsignedNumeric { + value: InnerUint([0, 0, 1]), // 2^128 + }; + let b = a.clone(); // 2^128 * 2^128 = 2^256, definitely too large + + let result = a.checked_mul(&b); + assert!(result.is_none(), "Expected overflow on large * large"); + } + #[cfg(feature = "256-bit")] + { + let a = UnsignedNumeric { + value: InnerUint([0, 0, 0, 1]), // 2^192 + }; + let b = a.clone(); // 2^192 * 2^192 = 2^384, definitely too large + + let result = a.checked_mul(&b); + assert!(result.is_none(), "Expected overflow on large * large"); + } } #[test]