Skip to main content

prism_numerics/
bigint.rs

1//! `BigIntAxis` declaration + parametric modular-arithmetic impls + shape.
2//!
3//! Per [Wiki ADR-031][09-adr-031] the numerics sub-crate exposes
4//! `BigIntAxis` as the canonical Layer-3 vocabulary for fixed-width
5//! integer arithmetic. The reference impl [`BigIntModularNumeric`] is
6//! generic over operand byte-width per ADR-031's `BigInt<MaxBits>`
7//! shape commitment — every `BYTES ≥ 1` instantiation is a distinct
8//! sealed `AxisExtension` that the application's `AxisTuple` can select.
9//! The kernels carry no fixed-width scratch, so the operand width scales
10//! arbitrarily with no ceiling (§ 11.10).
11//!
12//! [`BigIntShape`] is the matching `ConstrainedTypeShape` so
13//! application authors can declare `BigInt<N>`-typed inputs and outputs
14//! to their `prism_model!` invocations without re-rolling the shape.
15//!
16//! [09-adr-031]: https://github.com/UOR-Foundation/UOR-Framework/wiki/09-Architecture-Decisions
17
18#![allow(missing_docs)]
19
20use uor_foundation::enforcement::{GroundedShape, ShapeViolation};
21use uor_foundation::pipeline::{ConstrainedTypeShape, ConstraintRef, IntoBindingValue, TermValue};
22use uor_foundation_sdk::axis;
23
24use crate::{check_output, split_pair};
25
26axis! {
27    /// Wiki ADR-031 fixed-width integer arithmetic axis.
28    ///
29    /// Kernels take input `a || b` (big-endian-encoded equal-width
30    /// operands) and emit modular arithmetic results. The reference
31    /// impl `BigIntModularNumeric<BYTES>` is generic in `BYTES` for
32    /// any `BYTES ≥ 1`.
33    pub trait BigIntAxis: AxisExtension {
34        /// ADR-017 content address.
35        const AXIS_ADDRESS: &'static str = "https://uor.foundation/axis/BigIntAxis";
36        /// Operand byte-width (overridden per impl).
37        const MAX_OUTPUT_BYTES: usize = 32;
38        /// `(a + b) mod 2^(8*N)` — input is `a || b` (`2N` bytes).
39        ///
40        /// # Errors
41        ///
42        /// Returns `ShapeViolation` on input/output arity mismatch.
43        fn add(input: &[u8], out: &mut [u8]) -> Result<usize, ShapeViolation>;
44        /// `(a - b) mod 2^(8*N)` — input is `a || b` (`2N` bytes).
45        ///
46        /// # Errors
47        ///
48        /// Returns `ShapeViolation` on input/output arity mismatch.
49        fn sub(input: &[u8], out: &mut [u8]) -> Result<usize, ShapeViolation>;
50        /// `(a * b) mod 2^(8*N)` — input is `a || b` (`2N` bytes).
51        ///
52        /// # Errors
53        ///
54        /// Returns `ShapeViolation` on input/output arity mismatch.
55        fn mul(input: &[u8], out: &mut [u8]) -> Result<usize, ShapeViolation>;
56    }
57}
58
59/// `BigIntModularNumeric<BYTES>` admits **any** operand byte-width
60/// `BYTES ≥ 1`: add/sub stream carries directly into `out`, and `mul`
61/// computes the modular product column-by-column with a single running
62/// `u64` carry (`O(1)` scratch), so there is no fixed-width accumulator
63/// and therefore no upper ceiling on the width (§ 11.10). The only floor
64/// is non-emptiness: a fixed-width integer needs at least one byte.
65fn width_violation() -> ShapeViolation {
66    ShapeViolation {
67        shape_iri: "https://uor.foundation/axis/BigIntAxis",
68        constraint_iri: "https://uor.foundation/axis/BigIntAxis/widthPositive",
69        property_iri: "https://uor.foundation/axis/operandByteWidth",
70        expected_range: "https://uor.foundation/axis/BigIntAxis/PositiveByteWidth",
71        min_count: 1,
72        max_count: u32::MAX,
73        kind: uor_foundation::ViolationKind::ValueCheck,
74    }
75}
76
77/// Parametric `N`-byte modular-arithmetic impl of [`BigIntAxis`].
78///
79/// `BYTES` is the operand width in bytes (`8 * BYTES` bits). Arithmetic
80/// is mod `2^(8 * BYTES)` (wrapping). Any `BYTES ≥ 1` is supported — the
81/// kernels carry no fixed-width scratch, so the width has no ceiling.
82#[derive(Debug, Clone, Copy)]
83pub struct BigIntModularNumeric<const BYTES: usize>;
84
85impl<const BYTES: usize> Default for BigIntModularNumeric<BYTES> {
86    fn default() -> Self {
87        Self
88    }
89}
90
91impl<const BYTES: usize> BigIntAxis for BigIntModularNumeric<BYTES> {
92    const AXIS_ADDRESS: &'static str = "https://uor.foundation/axis/BigIntAxis/Modular";
93    const MAX_OUTPUT_BYTES: usize = BYTES;
94
95    fn add(input: &[u8], out: &mut [u8]) -> Result<usize, ShapeViolation> {
96        if BYTES == 0 {
97            return Err(width_violation());
98        }
99        let (a, b) = split_pair(input, BYTES)?;
100        check_output(out, BYTES)?;
101        let mut carry: u16 = 0;
102        for i in (0..BYTES).rev() {
103            let sum = u16::from(a[i]) + u16::from(b[i]) + carry;
104            #[allow(clippy::cast_possible_truncation)]
105            {
106                out[i] = (sum & 0xff) as u8;
107            }
108            carry = sum >> 8;
109        }
110        Ok(BYTES)
111    }
112
113    fn sub(input: &[u8], out: &mut [u8]) -> Result<usize, ShapeViolation> {
114        if BYTES == 0 {
115            return Err(width_violation());
116        }
117        let (a, b) = split_pair(input, BYTES)?;
118        check_output(out, BYTES)?;
119        let mut borrow: i16 = 0;
120        for i in (0..BYTES).rev() {
121            let diff = i16::from(a[i]) - i16::from(b[i]) - borrow;
122            if diff < 0 {
123                #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
124                {
125                    out[i] = (diff + 256) as u8;
126                }
127                borrow = 1;
128            } else {
129                #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
130                {
131                    out[i] = diff as u8;
132                }
133                borrow = 0;
134            }
135        }
136        Ok(BYTES)
137    }
138
139    fn mul(input: &[u8], out: &mut [u8]) -> Result<usize, ShapeViolation> {
140        if BYTES == 0 {
141            return Err(width_violation());
142        }
143        let (a, b) = split_pair(input, BYTES)?;
144        check_output(out, BYTES)?;
145        // Modular product mod 2^(8*BYTES): only the low `BYTES` bytes of
146        // the schoolbook product survive, so we compute them column by
147        // column from least-significant upward, carrying forward in a
148        // single running `u64`. This needs `O(1)` scratch — no fixed-width
149        // accumulator — so the operand width has no ceiling. Operands are
150        // big-endian; little-endian position `p` is byte `BYTES - 1 - p`.
151        // For `p < BYTES` every (x, p - x) index pair is in range, so the
152        // inner sum runs `x` from `0..=p`. The column sum is bounded by
153        // `BYTES * 255^2 + carry`, which stays well within `u64` for any
154        // width whose operands fit in memory.
155        let mut carry: u64 = 0;
156        for p in 0..BYTES {
157            let mut col: u64 = carry;
158            for x in 0..=p {
159                let y = p - x;
160                col += u64::from(a[BYTES - 1 - x]) * u64::from(b[BYTES - 1 - y]);
161            }
162            #[allow(clippy::cast_possible_truncation)]
163            {
164                out[BYTES - 1 - p] = (col & 0xff) as u8;
165            }
166            carry = col >> 8;
167        }
168        Ok(BYTES)
169    }
170}
171
172// ADR-052 generic-form companion: replaces the hand-written
173// AxisExtension impl. The macro's @generic arm accepts a `:ty` plus a
174// generic parameter list so parametric Layer-3 axes inherit the
175// dispatch body from the `axis!` emission.
176axis_extension_impl_for_big_int_axis!(@generic BigIntModularNumeric<BYTES>, [const BYTES: usize]);
177
178/// 256-bit modular arithmetic (mod `2^256`).
179pub type BigInt256Numeric = BigIntModularNumeric<32>;
180/// 512-bit modular arithmetic (mod `2^512`).
181pub type BigInt512Numeric = BigIntModularNumeric<64>;
182/// 128-bit modular arithmetic (mod `2^128`).
183pub type BigInt128Numeric = BigIntModularNumeric<16>;
184/// 64-bit modular arithmetic (mod `2^64`) — matches `u64` wrapping.
185pub type BigInt64Numeric = BigIntModularNumeric<8>;
186
187// ---- BigIntShape: ConstrainedTypeShape carrier for BigInt<N> -----------
188
189/// Parametric ConstrainedTypeShape: an `N`-byte big-endian integer.
190///
191/// Per ADR-031 this is the canonical Layer-3 shape downstream
192/// `prism_model!` invocations use to type their `Input` / `Output` as
193/// big-integer values. The shape carries `BYTES` sites with no
194/// admission constraints; admission discipline (range bounds, modulus,
195/// etc.) is the consumer's responsibility through additional
196/// constraint refs.
197///
198/// Per ADR-017's closure rule the IRI is the foundation's shared
199/// `ConstrainedType` class; instance identity flows through
200/// `(SITE_COUNT, CONSTRAINTS)`.
201#[derive(Debug, Clone, Copy)]
202pub struct BigIntShape<const BYTES: usize>;
203
204impl<const BYTES: usize> Default for BigIntShape<BYTES> {
205    fn default() -> Self {
206        Self
207    }
208}
209
210impl<const BYTES: usize> ConstrainedTypeShape for BigIntShape<BYTES> {
211    const IRI: &'static str = "https://uor.foundation/type/ConstrainedType";
212    const SITE_COUNT: usize = BYTES;
213    const CONSTRAINTS: &'static [ConstraintRef] = &[];
214    #[allow(clippy::cast_possible_truncation)]
215    const CYCLE_SIZE: u64 = 256u64.saturating_pow(BYTES as u32);
216}
217
218impl<const BYTES: usize> uor_foundation::pipeline::__sdk_seal::Sealed for BigIntShape<BYTES> {}
219impl<const BYTES: usize> GroundedShape for BigIntShape<BYTES> {}
220impl<'a, const BYTES: usize> IntoBindingValue<'a> for BigIntShape<BYTES> {
221    fn as_binding_value<const INLINE_BYTES: usize>(&self) -> TermValue<'a, INLINE_BYTES> {
222        // The shape is a phantom carrier; downstream impls that want to
223        // bind an actual N-byte big-int value wrap this shape in a
224        // newtype carrying the data + a bespoke carrier.
225        TermValue::empty()
226    }
227}
228
229// ADR-033 G20 leaf-shape PartitionProductFields impl per
230// foundation-sdk 0.4.11's depth-2 verb!-macro projection chain.
231// Foundation-sdk 0.4.11 requires `PartitionProductFields` on every
232// type used as a partition-product factor (including leaves) for
233// the depth-2 chained-field-access trait-bound check to resolve.
234// Empty FIELDS signals "atomic byte-sequence carrier — no further
235// projection possible"; the macro respects the termination marker
236// without indexing into the empty array (the 0.4.10 const-eval
237// panic on empty FIELDS is fixed in 0.4.11).
238impl<const BYTES: usize> uor_foundation::pipeline::PartitionProductFields for BigIntShape<BYTES> {
239    const FIELDS: &'static [(u32, u32)] = &[];
240    const FIELD_NAMES: &'static [&'static str] = &[];
241}