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