Skip to main content

prism_numerics/
fixed_point.rs

1//! `FixedPointAxis` declaration + parametric Q-format impl + shape.
2//!
3//! Per [Wiki ADR-031][09-adr-031] the numerics sub-crate exposes
4//! `FixedPointAxis` and `FixedPoint<I, F>` as the canonical Layer-3
5//! surface for Q-format fixed-point arithmetic. The reference impl
6//! [`FixedPointQNumeric`] is generic over integer-bit width `I` and
7//! fraction-bit width `F`, with `I + F ≤ 64` (so each value fits a
8//! single signed 64-bit container).
9//!
10//! [09-adr-031]: https://github.com/UOR-Foundation/UOR-Framework/wiki/09-Architecture-Decisions
11
12#![allow(missing_docs)]
13
14use uor_foundation::enforcement::{GroundedShape, ShapeViolation};
15use uor_foundation::pipeline::{
16    AxisExtension, ConstrainedTypeShape, ConstraintRef, IntoBindingValue,
17};
18use uor_foundation_sdk::axis;
19
20use crate::{check_output, split_pair};
21
22axis! {
23    /// Wiki ADR-031 fixed-point arithmetic axis.
24    ///
25    /// Operates on Q-format two's-complement integers within a signed
26    /// 64-bit container. The reference impl
27    /// `FixedPointQNumeric<I, F>` is generic over the integer-bit /
28    /// fraction-bit split, with `I + F ≤ 64`.
29    pub trait FixedPointAxis: AxisExtension {
30        /// ADR-017 content address.
31        const AXIS_ADDRESS: &'static str = "https://uor.foundation/axis/FixedPointAxis";
32        /// Operand byte-width (fixed 8 bytes = i64 container).
33        const MAX_OUTPUT_BYTES: usize = 8;
34        /// Q-format addition: `a + b`. Input `a || b` (16 bytes).
35        ///
36        /// # Errors
37        ///
38        /// Returns `ShapeViolation` on input/output arity mismatch.
39        fn add(input: &[u8], out: &mut [u8]) -> Result<usize, ShapeViolation>;
40        /// Q-format subtraction: `a - b`. Input `a || b` (16 bytes).
41        ///
42        /// # Errors
43        ///
44        /// Returns `ShapeViolation` on input/output arity mismatch.
45        fn sub(input: &[u8], out: &mut [u8]) -> Result<usize, ShapeViolation>;
46        /// Q-format multiplication with bias-aware re-scaling by `F`
47        /// fraction bits.
48        ///
49        /// # Errors
50        ///
51        /// Returns `ShapeViolation` on input/output arity mismatch.
52        fn mul(input: &[u8], out: &mut [u8]) -> Result<usize, ShapeViolation>;
53    }
54}
55
56const WIDTH: usize = 8;
57
58fn format_violation() -> ShapeViolation {
59    ShapeViolation {
60        shape_iri: "https://uor.foundation/axis/FixedPointAxis",
61        constraint_iri: "https://uor.foundation/axis/FixedPointAxis/iPlusFInRange",
62        property_iri: "https://uor.foundation/axis/qFormatTotalBits",
63        expected_range: "https://uor.foundation/axis/FixedPointAxis/I64Fit",
64        min_count: 1,
65        max_count: 64,
66        kind: uor_foundation::ViolationKind::ValueCheck,
67    }
68}
69
70fn decode(slice: &[u8]) -> i64 {
71    let mut buf = [0u8; 8];
72    buf.copy_from_slice(&slice[..8]);
73    i64::from_be_bytes(buf)
74}
75
76fn encode(value: i64) -> [u8; 8] {
77    value.to_be_bytes()
78}
79
80/// Parametric Q-format fixed-point arithmetic.
81///
82/// `INT_BITS + FRAC_BITS ≤ 64` and `INT_BITS + FRAC_BITS ≥ 1`. Values
83/// are two's-complement signed integers in the canonical 64-bit
84/// container; the `INT_BITS`/`FRAC_BITS` split governs the implicit
85/// decimal point and the multiplication re-scaling.
86#[derive(Debug, Clone, Copy)]
87pub struct FixedPointQNumeric<const INT_BITS: u32, const FRAC_BITS: u32>;
88
89impl<const I: u32, const F: u32> Default for FixedPointQNumeric<I, F> {
90    fn default() -> Self {
91        Self
92    }
93}
94
95impl<const INT_BITS: u32, const FRAC_BITS: u32> FixedPointAxis
96    for FixedPointQNumeric<INT_BITS, FRAC_BITS>
97{
98    const AXIS_ADDRESS: &'static str = "https://uor.foundation/axis/FixedPointAxis/Q";
99    const MAX_OUTPUT_BYTES: usize = WIDTH;
100
101    fn add(input: &[u8], out: &mut [u8]) -> Result<usize, ShapeViolation> {
102        if INT_BITS + FRAC_BITS == 0 || INT_BITS + FRAC_BITS > 64 {
103            return Err(format_violation());
104        }
105        let (a, b) = split_pair(input, WIDTH)?;
106        check_output(out, WIDTH)?;
107        let result = decode(a).saturating_add(decode(b));
108        out[..WIDTH].copy_from_slice(&encode(result));
109        Ok(WIDTH)
110    }
111
112    fn sub(input: &[u8], out: &mut [u8]) -> Result<usize, ShapeViolation> {
113        if INT_BITS + FRAC_BITS == 0 || INT_BITS + FRAC_BITS > 64 {
114            return Err(format_violation());
115        }
116        let (a, b) = split_pair(input, WIDTH)?;
117        check_output(out, WIDTH)?;
118        let result = decode(a).saturating_sub(decode(b));
119        out[..WIDTH].copy_from_slice(&encode(result));
120        Ok(WIDTH)
121    }
122
123    fn mul(input: &[u8], out: &mut [u8]) -> Result<usize, ShapeViolation> {
124        if INT_BITS + FRAC_BITS == 0 || INT_BITS + FRAC_BITS > 64 {
125            return Err(format_violation());
126        }
127        let (a, b) = split_pair(input, WIDTH)?;
128        check_output(out, WIDTH)?;
129        let product = i128::from(decode(a)) * i128::from(decode(b));
130        let rescaled = product >> FRAC_BITS;
131        let saturated: i64 = if rescaled > i128::from(i64::MAX) {
132            i64::MAX
133        } else if rescaled < i128::from(i64::MIN) {
134            i64::MIN
135        } else {
136            #[allow(clippy::cast_possible_truncation)]
137            {
138                rescaled as i64
139            }
140        };
141        out[..WIDTH].copy_from_slice(&encode(saturated));
142        Ok(WIDTH)
143    }
144}
145
146// ADR-052 generic-form companion: parametric impl inherits the
147// dispatch body from the `axis!` emission.
148axis_extension_impl_for_fixed_point_axis!(
149    @generic FixedPointQNumeric<INT_BITS, FRAC_BITS>,
150    [const INT_BITS: u32, const FRAC_BITS: u32]
151);
152
153/// Q32.32 — 32 integer bits, 32 fraction bits.
154pub type FixedPointQ32_32Numeric = FixedPointQNumeric<32, 32>;
155/// Q16.16 — DSP / graphics canonical split.
156pub type FixedPointQ16_16Numeric = FixedPointQNumeric<16, 16>;
157/// Q1.31 — high-precision fraction-heavy split (financial / signal).
158pub type FixedPointQ1_31Numeric = FixedPointQNumeric<1, 31>;
159/// Q48.16 — large-magnitude integer with sub-integer precision.
160pub type FixedPointQ48_16Numeric = FixedPointQNumeric<48, 16>;
161
162// ---- FixedPointShape: ConstrainedTypeShape carrier ----
163
164/// Parametric ConstrainedTypeShape carrying an 8-byte Q-format value.
165///
166/// `INT_BITS + FRAC_BITS = 64` is the canonical full-container case;
167/// other splits within `≤ 64` are admissible. The shape's identity
168/// flows through `(SITE_COUNT, CONSTRAINTS)` per ADR-017's closure
169/// rule — distinct `(I, F)` instantiations content-address identically
170/// when their site counts coincide.
171#[derive(Debug, Clone, Copy)]
172pub struct FixedPointShape<const INT_BITS: u32, const FRAC_BITS: u32>;
173
174impl<const I: u32, const F: u32> Default for FixedPointShape<I, F> {
175    fn default() -> Self {
176        Self
177    }
178}
179
180impl<const INT_BITS: u32, const FRAC_BITS: u32> ConstrainedTypeShape
181    for FixedPointShape<INT_BITS, FRAC_BITS>
182{
183    const IRI: &'static str = "https://uor.foundation/type/ConstrainedType";
184    const SITE_COUNT: usize = WIDTH;
185    const CONSTRAINTS: &'static [ConstraintRef] = &[];
186    #[allow(clippy::cast_possible_truncation)]
187    const CYCLE_SIZE: u64 = 256u64.saturating_pow(WIDTH as u32);
188}
189
190impl<const INT_BITS: u32, const FRAC_BITS: u32> uor_foundation::pipeline::__sdk_seal::Sealed
191    for FixedPointShape<INT_BITS, FRAC_BITS>
192{
193}
194impl<const INT_BITS: u32, const FRAC_BITS: u32> GroundedShape
195    for FixedPointShape<INT_BITS, FRAC_BITS>
196{
197}
198impl<const INT_BITS: u32, const FRAC_BITS: u32> IntoBindingValue
199    for FixedPointShape<INT_BITS, FRAC_BITS>
200{
201    const MAX_BYTES: usize = WIDTH;
202
203    fn into_binding_bytes(&self, _out: &mut [u8]) -> Result<usize, ShapeViolation> {
204        Ok(0)
205    }
206}