axiolid_contracts/
capability.rs

1//! Backend identity metadata. Operation traits remain the sole capability truth.
2
3use core::fmt;
4
5/// Maximum identifier length in bytes.
6///
7/// Sized so driver-enumerated accelerator names (`cuda:0`, `hip:1`,
8/// `vulkan:discrete:0`) fit without allocating, while keeping [`BackendId`]
9/// small enough to stay `Copy` inside every error variant.
10const IDENTIFIER_CAPACITY: usize = 47;
11
12/// Stable provider identifier for logs and explicit selection.
13///
14/// Identity is stored inline as fixed-capacity UTF-8 rather than
15/// `&'static str`, because accelerator backends (CUDA, HIP, Vulkan) enumerate
16/// their devices at runtime and cannot produce `'static` text without leaking.
17/// Keeping the value inline preserves `Copy`, so `BackendId` can continue to
18/// live inside `GeomError` variants and `DevicePreference` without forcing an
19/// allocation or a lifetime onto the error type.
20#[derive(Clone, Copy)]
21pub struct BackendId {
22    bytes: [u8; IDENTIFIER_CAPACITY],
23    len: u8,
24}
25
26/// An identifier was longer than [`BackendId::CAPACITY`] bytes.
27///
28/// Rejecting is deliberate: a silently truncated identity would make two
29/// distinct devices compare equal, which would corrupt provider selection and
30/// error attribution.
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub struct BackendIdTooLong {
33    /// Length of the rejected identifier, in bytes.
34    pub len: usize,
35}
36
37impl fmt::Display for BackendIdTooLong {
38    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39        write!(
40            f,
41            "backend identifier is {} bytes, limit is {}",
42            self.len,
43            BackendId::CAPACITY
44        )
45    }
46}
47
48impl core::error::Error for BackendIdTooLong {}
49
50impl BackendId {
51    /// Maximum identifier length in bytes.
52    pub const CAPACITY: usize = IDENTIFIER_CAPACITY;
53
54    /// Construct an identifier known at compile time.
55    ///
56    /// # Panics
57    ///
58    /// Panics if `value` exceeds [`BackendId::CAPACITY`] bytes. This is a
59    /// `const fn`, so an over-long literal fails the build rather than a test
60    /// run. Use [`BackendId::try_new`] for runtime-derived text.
61    pub const fn new(value: &str) -> Self {
62        match Self::build(value.as_bytes()) {
63            Some(id) => id,
64            None => panic!("backend identifier exceeds BackendId::CAPACITY"),
65        }
66    }
67
68    /// Construct an identifier from runtime-derived text, such as a
69    /// driver-enumerated device name.
70    ///
71    /// # Errors
72    ///
73    /// Returns [`BackendIdTooLong`] when the text exceeds
74    /// [`BackendId::CAPACITY`] bytes. The identity is never truncated.
75    pub fn try_new(value: &str) -> Result<Self, BackendIdTooLong> {
76        Self::build(value.as_bytes()).ok_or(BackendIdTooLong { len: value.len() })
77    }
78
79    /// Shared inline copy used by both constructors.
80    ///
81    /// Written as an index loop because `copy_from_slice` is not `const`.
82    const fn build(source: &[u8]) -> Option<Self> {
83        if source.len() > IDENTIFIER_CAPACITY {
84            return None;
85        }
86        let mut bytes = [0_u8; IDENTIFIER_CAPACITY];
87        let mut index = 0;
88        while index < source.len() {
89            bytes[index] = source[index];
90            index += 1;
91        }
92        Some(Self {
93            bytes,
94            len: source.len() as u8,
95        })
96    }
97
98    /// Identifier text.
99    pub fn as_str(&self) -> &str {
100        // The only constructors copy from a `&str`, so the populated prefix is
101        // always valid UTF-8. Zero padding beyond `len` is never included.
102        core::str::from_utf8(&self.bytes[..self.len as usize])
103            .expect("identifier bytes originate from &str")
104    }
105}
106
107// Comparison and hashing use the semantic prefix, never the zero padding, so a
108// runtime-built identity equals a compile-time one with the same text.
109impl PartialEq for BackendId {
110    fn eq(&self, other: &Self) -> bool {
111        self.as_str() == other.as_str()
112    }
113}
114
115impl Eq for BackendId {}
116
117impl PartialOrd for BackendId {
118    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
119        Some(self.cmp(other))
120    }
121}
122
123impl Ord for BackendId {
124    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
125        self.as_str().cmp(other.as_str())
126    }
127}
128
129impl core::hash::Hash for BackendId {
130    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
131        self.as_str().hash(state);
132    }
133}
134
135impl fmt::Debug for BackendId {
136    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137        write!(f, "BackendId({:?})", self.as_str())
138    }
139}
140
141impl fmt::Display for BackendId {
142    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143        f.write_str(self.as_str())
144    }
145}
146
147/// Broad execution target. Specific ISA/device features stay in provider crates.
148#[non_exhaustive]
149#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
150pub enum ExecutionTarget {
151    /// Portable scalar CPU implementation.
152    PortableCpu,
153    /// Runtime-selected CPU implementation.
154    OptimizedCpu,
155    /// General-purpose GPU compute.
156    Gpu,
157    /// Other accelerator supplied downstream.
158    Accelerator,
159}
160
161/// Operation name used for diagnostics only. Implementing an operation trait is
162/// the capability proof; this enum never drives capability discovery.
163#[non_exhaustive]
164#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
165pub enum Operation {
166    CurveEvaluation,
167    SurfaceEvaluation,
168    ProfileTriangulation,
169    Sweep,
170    Tessellation,
171    MeshBoolean,
172    MeshPlaneSection,
173    PointcloudReconstruction,
174    SpatialQuery,
175    Measurement,
176    Healing,
177    GraphCompilation,
178}
179
180/// Provider identity only. It deliberately contains no operation booleans.
181#[derive(Debug, Clone, Copy, PartialEq, Eq)]
182pub struct BackendDescriptor {
183    /// Stable implementation identity.
184    pub id: BackendId,
185    /// Hardware class used by execution policy.
186    pub target: ExecutionTarget,
187}
188
189impl BackendDescriptor {
190    /// Construct provider identity metadata.
191    pub const fn new(id: BackendId, target: ExecutionTarget) -> Self {
192        Self { id, target }
193    }
194}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199
200    /// A driver-enumerated accelerator identity (`cuda:0`, `hip:1`) is only
201    /// known at runtime, so identifiers must not require `&'static str`.
202    #[test]
203    fn identifiers_accept_runtime_owned_text() {
204        let ordinal = 1_u32;
205        let runtime = format!("cuda:{ordinal}");
206        let id = BackendId::try_new(&runtime).expect("short runtime identity");
207        assert_eq!(id.as_str(), "cuda:1");
208        assert_eq!(id.to_string(), "cuda:1");
209    }
210
211    #[test]
212    fn runtime_and_const_identifiers_compare_equal() {
213        const STATIC: BackendId = BackendId::new("cuda:0");
214        let runtime = BackendId::try_new(&String::from("cuda:0")).expect("identity");
215        assert_eq!(STATIC, runtime);
216        assert_eq!(STATIC.cmp(&runtime), core::cmp::Ordering::Equal);
217    }
218
219    #[test]
220    fn identifiers_stay_copy_and_orderable() {
221        fn assert_copy<T: Copy + Ord + core::hash::Hash>() {}
222        assert_copy::<BackendId>();
223        let a = BackendId::try_new("aa").expect("identity");
224        let b = BackendId::try_new("aab").expect("identity");
225        assert!(a < b, "zero padding must not invert lexicographic order");
226    }
227
228    #[test]
229    fn over_long_identifiers_are_rejected_not_truncated() {
230        let too_long = "x".repeat(BackendId::CAPACITY + 1);
231        assert!(BackendId::try_new(&too_long).is_err());
232        let at_limit = "x".repeat(BackendId::CAPACITY);
233        assert_eq!(
234            BackendId::try_new(&at_limit).expect("identity").as_str(),
235            at_limit
236        );
237    }
238
239    /// Two devices whose names share a prefix must stay distinct. A truncating
240    /// implementation would alias them and misroute both selection and blame.
241    #[test]
242    fn long_shared_prefix_devices_do_not_alias() {
243        let base = "x".repeat(BackendId::CAPACITY - 1);
244        let first = BackendId::try_new(&format!("{base}0")).expect("identity");
245        let second = BackendId::try_new(&format!("{base}1")).expect("identity");
246        assert_ne!(first, second);
247    }
248
249    #[test]
250    fn hashing_matches_equality_across_construction_paths() {
251        use std::collections::HashSet;
252        let mut seen = HashSet::new();
253        seen.insert(BackendId::new("hip:0"));
254        assert!(seen.contains(&BackendId::try_new("hip:0").expect("identity")));
255    }
256
257    #[test]
258    fn multibyte_identifiers_round_trip() {
259        let id = BackendId::try_new("gpu-µ-0").expect("identity");
260        assert_eq!(id.as_str(), "gpu-µ-0");
261    }
262}