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}