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}