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