axiolid_contracts/
capability.rs1use core::fmt;
4
5const IDENTIFIER_CAPACITY: usize = 47;
11
12#[derive(Clone, Copy)]
21pub struct BackendId {
22 bytes: [u8; IDENTIFIER_CAPACITY],
23 len: u8,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub struct BackendIdTooLong {
33 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 pub const CAPACITY: usize = IDENTIFIER_CAPACITY;
53
54 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 pub fn try_new(value: &str) -> Result<Self, BackendIdTooLong> {
76 Self::build(value.as_bytes()).ok_or(BackendIdTooLong { len: value.len() })
77 }
78
79 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 pub fn as_str(&self) -> &str {
100 core::str::from_utf8(&self.bytes[..self.len as usize])
103 .expect("identifier bytes originate from &str")
104 }
105}
106
107impl 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#[non_exhaustive]
149#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
150pub enum ExecutionTarget {
151 PortableCpu,
153 OptimizedCpu,
155 Gpu,
157 Accelerator,
159}
160
161#[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 MeshHiddenLines,
179 MeshVisibleFaces,
180}
181
182#[derive(Debug, Clone, Copy, PartialEq, Eq)]
184pub struct BackendDescriptor {
185 pub id: BackendId,
187 pub target: ExecutionTarget,
189}
190
191impl BackendDescriptor {
192 pub const fn new(id: BackendId, target: ExecutionTarget) -> Self {
194 Self { id, target }
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201
202 #[test]
205 fn identifiers_accept_runtime_owned_text() {
206 let ordinal = 1_u32;
207 let runtime = format!("cuda:{ordinal}");
208 let id = BackendId::try_new(&runtime).expect("short runtime identity");
209 assert_eq!(id.as_str(), "cuda:1");
210 assert_eq!(id.to_string(), "cuda:1");
211 }
212
213 #[test]
214 fn runtime_and_const_identifiers_compare_equal() {
215 const STATIC: BackendId = BackendId::new("cuda:0");
216 let runtime = BackendId::try_new(&String::from("cuda:0")).expect("identity");
217 assert_eq!(STATIC, runtime);
218 assert_eq!(STATIC.cmp(&runtime), core::cmp::Ordering::Equal);
219 }
220
221 #[test]
222 fn identifiers_stay_copy_and_orderable() {
223 fn assert_copy<T: Copy + Ord + core::hash::Hash>() {}
224 assert_copy::<BackendId>();
225 let a = BackendId::try_new("aa").expect("identity");
226 let b = BackendId::try_new("aab").expect("identity");
227 assert!(a < b, "zero padding must not invert lexicographic order");
228 }
229
230 #[test]
231 fn over_long_identifiers_are_rejected_not_truncated() {
232 let too_long = "x".repeat(BackendId::CAPACITY + 1);
233 assert!(BackendId::try_new(&too_long).is_err());
234 let at_limit = "x".repeat(BackendId::CAPACITY);
235 assert_eq!(
236 BackendId::try_new(&at_limit).expect("identity").as_str(),
237 at_limit
238 );
239 }
240
241 #[test]
244 fn long_shared_prefix_devices_do_not_alias() {
245 let base = "x".repeat(BackendId::CAPACITY - 1);
246 let first = BackendId::try_new(&format!("{base}0")).expect("identity");
247 let second = BackendId::try_new(&format!("{base}1")).expect("identity");
248 assert_ne!(first, second);
249 }
250
251 #[test]
252 fn hashing_matches_equality_across_construction_paths() {
253 use std::collections::HashSet;
254 let mut seen = HashSet::new();
255 seen.insert(BackendId::new("hip:0"));
256 assert!(seen.contains(&BackendId::try_new("hip:0").expect("identity")));
257 }
258
259 #[test]
260 fn multibyte_identifiers_round_trip() {
261 let id = BackendId::try_new("gpu-µ-0").expect("identity");
262 assert_eq!(id.as_str(), "gpu-µ-0");
263 }
264}