1use std::{
2 backtrace::{Backtrace, BacktraceStatus},
3 error::Error as StdError,
4 ffi::CStr,
5 fmt,
6};
7
8use cudf::{
9 ErrorCode,
10 interop::contract::{InteropError, InteropErrorKind},
11};
12use cuvs_sys::{cuvsError_t, cuvsErrorKind_t};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15#[non_exhaustive]
16pub enum ErrorKind {
17 Unexpected,
18 InvalidInput,
19 InvalidState,
20 OutOfMemory,
21 Unsupported,
22}
23
24impl ErrorKind {
25 fn capture_backtrace(&self) -> bool {
26 matches!(self, ErrorKind::Unexpected)
27 }
28}
29
30impl fmt::Display for ErrorKind {
31 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32 match self {
33 ErrorKind::Unexpected => write!(f, "Unexpected"),
34 ErrorKind::InvalidInput => write!(f, "InvalidInput"),
35 ErrorKind::InvalidState => write!(f, "InvalidState"),
36 ErrorKind::OutOfMemory => write!(f, "OutOfMemory"),
37 ErrorKind::Unsupported => write!(f, "Unsupported"),
38 }
39 }
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
43pub enum ErrorStatus {
44 Permanent,
45 Temporary,
46 Persistent,
47}
48
49impl fmt::Display for ErrorStatus {
50 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51 match self {
52 ErrorStatus::Permanent => write!(f, "permanent"),
53 ErrorStatus::Temporary => write!(f, "temporary"),
54 ErrorStatus::Persistent => write!(f, "persistent"),
55 }
56 }
57}
58
59pub struct Error {
60 kind: ErrorKind,
61 status: ErrorStatus,
62 operation: &'static str,
63 message: String,
64 context: Vec<(&'static str, String)>,
65 source: Option<Box<dyn StdError + Send + Sync + 'static>>,
66 backtrace: Option<Box<Backtrace>>,
67}
68
69impl Error {
70 pub fn new(kind: ErrorKind, operation: &'static str) -> Self {
71 Self {
72 kind,
73 status: ErrorStatus::Permanent,
74 operation,
75 message: String::new(),
76 context: Vec::new(),
77 source: None,
78 backtrace: kind
79 .capture_backtrace()
80 .then(Backtrace::capture)
81 .filter(|bt| bt.status() == BacktraceStatus::Captured)
82 .map(Box::new),
83 }
84 }
85
86 pub fn with_message(mut self, message: impl Into<String>) -> Self {
87 self.message = message.into();
88 self
89 }
90
91 pub fn with_operation(mut self, operation: &'static str) -> Self {
92 if !self.operation.is_empty() {
93 self.context.push(("called", self.operation.to_string()));
94 }
95 self.operation = operation;
96 self
97 }
98
99 pub fn with_context(mut self, key: &'static str, value: impl ToString) -> Self {
100 self.context.push((key, value.to_string()));
101 self
102 }
103
104 pub fn set_source<E>(mut self, source: E) -> Self
105 where E: StdError + Send + Sync + 'static {
106 debug_assert!(self.source.is_none(), "source already set");
107 self.source = Some(Box::new(source));
108 self
109 }
110
111 pub fn set_temporary(mut self) -> Self {
112 self.status = ErrorStatus::Temporary;
113 self
114 }
115
116 pub fn set_permanent(mut self) -> Self {
117 self.status = ErrorStatus::Permanent;
118 self
119 }
120
121 pub fn set_persistent(mut self) -> Self {
122 self.status = ErrorStatus::Persistent;
123 self
124 }
125
126 pub fn kind(&self) -> ErrorKind {
127 self.kind
128 }
129
130 pub fn status(&self) -> ErrorStatus {
131 self.status
132 }
133
134 pub fn operation(&self) -> &'static str {
135 self.operation
136 }
137
138 pub fn message(&self) -> &str {
139 &self.message
140 }
141
142 pub fn context(&self) -> impl ExactSizeIterator<Item = (&'static str, &str)> {
146 self.context.iter().map(|(key, value)| (*key, value.as_str()))
147 }
148
149 pub fn context_value(&self, key: &str) -> Option<&str> {
150 self.context.iter().rev().find(|(candidate, _)| *candidate == key).map(|(_, value)| value.as_str())
151 }
152
153 pub fn is_temporary(&self) -> bool {
154 self.status == ErrorStatus::Temporary
155 }
156}
157
158impl fmt::Debug for Error {
159 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
160 if f.alternate() {
161 let mut d = f.debug_struct("Error");
162 d.field("kind", &self.kind).field("status", &self.status).field("operation", &self.operation);
163 if !self.message.is_empty() {
164 d.field("message", &self.message);
165 }
166 if !self.context.is_empty() {
167 d.field("context", &self.context);
168 }
169 if let Some(source) = &self.source {
170 d.field("source", source);
171 }
172 d.finish()
173 } else {
174 write!(f, "{} ({}) at {}", self.kind, self.status, self.operation)?;
175 if !self.message.is_empty() {
176 write!(f, " => {}", self.message)?;
177 }
178 if !self.context.is_empty() {
179 write!(f, "\n\nContext:")?;
180 for (key, value) in &self.context {
181 write!(f, "\n {key}: {value}")?;
182 }
183 }
184 if let Some(source) = &self.source {
185 write!(f, "\n\nSource: {source:#}")?;
186 }
187 if let Some(backtrace) = &self.backtrace {
188 write!(f, "\n\nBacktrace:\n{backtrace}")?;
189 }
190 Ok(())
191 }
192 }
193}
194
195impl fmt::Display for Error {
196 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
197 write!(f, "{} ({}) at {}", self.kind, self.status, self.operation)?;
198 if !self.message.is_empty() {
199 write!(f, " => {}", self.message)?;
200 }
201 if !self.context.is_empty() {
202 write!(f, ", context: {{")?;
203 for (i, (key, value)) in self.context.iter().enumerate() {
204 if i > 0 {
205 write!(f, ",")?;
206 }
207 write!(f, " {key}: {value}")?;
208 }
209 write!(f, " }}")?;
210 }
211 if let Some(source) = &self.source {
212 write!(f, ", source: {source}")?;
213 }
214 Ok(())
215 }
216}
217
218impl StdError for Error {
219 fn source(&self) -> Option<&(dyn StdError + 'static)> {
220 self.source.as_ref().map(|source| source.as_ref() as &(dyn StdError + 'static))
221 }
222}
223
224pub type Result<T> = std::result::Result<T, Error>;
225
226pub(crate) fn invalid_input(operation: &'static str, message: impl Into<String>) -> Error {
227 Error::new(ErrorKind::InvalidInput, operation).with_message(message)
228}
229
230pub(crate) fn invalid_state(operation: &'static str, message: impl Into<String>) -> Error {
231 Error::new(ErrorKind::InvalidState, operation).with_message(message)
232}
233
234pub(crate) fn unsupported(operation: &'static str, message: impl Into<String>) -> Error {
235 Error::new(ErrorKind::Unsupported, operation).with_message(message)
236}
237
238pub(crate) fn from_cudf(operation: &'static str, error: cudf::Error) -> Error {
239 let kind = match error.code() {
240 ErrorCode::OutOfMemory | ErrorCode::ForeignResourceExhausted => ErrorKind::OutOfMemory,
241 ErrorCode::UnsupportedDtype | ErrorCode::UnsupportedLayout | ErrorCode::UnsupportedOperation => {
242 ErrorKind::Unsupported
243 },
244 ErrorCode::EmptyConstructionRequiresRuntime
245 | ErrorCode::RuntimeBindingMismatch
246 | ErrorCode::ReadinessUnavailable
247 | ErrorCode::ExecutionBindingUnavailable
248 | ErrorCode::InvariantViolation
249 | ErrorCode::ForeignInvariantViolation => ErrorKind::InvalidState,
250 ErrorCode::CudaRuntime
251 | ErrorCode::FatalCuda
252 | ErrorCode::IoFailure
253 | ErrorCode::IoBusy
254 | ErrorCode::ForeignTransientDependencyFailure
255 | ErrorCode::ForeignPersistentDependencyFailure => ErrorKind::Unexpected,
256 _ => ErrorKind::InvalidInput,
257 };
258 let status = error.status();
259 let mut mapped = Error::new(kind, operation)
260 .with_message(error.message())
261 .with_context("cudf_code", error.code())
262 .with_context("cudf_kind", error.kind())
263 .with_context("cudf_status", status)
264 .with_context("cudf_operation", error.operation());
265 mapped = match status {
266 cudf::ErrorStatus::Permanent => mapped.set_permanent(),
267 cudf::ErrorStatus::Temporary => mapped.set_temporary(),
268 cudf::ErrorStatus::Persistent => mapped.set_persistent(),
269 };
270 mapped.set_source(error)
271}
272
273pub(crate) fn from_interop(operation: &'static str, error: InteropError) -> Error {
274 let kind = match error.kind() {
275 InteropErrorKind::UnsupportedType
276 | InteropErrorKind::UnsupportedLayout
277 | InteropErrorKind::PlacementUnsupported
278 | InteropErrorKind::ProfileUnsupported => ErrorKind::Unsupported,
279 InteropErrorKind::ExecutionBindingUnavailable
280 | InteropErrorKind::ExecutionBindingMismatch
281 | InteropErrorKind::ReadinessUnavailable
282 | InteropErrorKind::ReadinessMismatch => ErrorKind::InvalidState,
283 InteropErrorKind::NotFound | InteropErrorKind::InvalidView | InteropErrorKind::MixedDeviceUnsupported => {
284 ErrorKind::InvalidInput
285 },
286 };
287 let mut mapped = Error::new(kind, operation)
288 .with_message(error.message())
289 .with_context("interop_kind", format!("{:?}", error.kind()));
290 for (key, value) in error.context() {
291 mapped = mapped.with_context(key, value);
292 }
293 mapped.set_source(error)
294}
295
296pub(crate) fn require_nonzero(value: usize, operation: &'static str, name: &'static str) -> Result<usize> {
297 if value == 0 { Err(invalid_input(operation, format!("{name} must be greater than zero"))) } else { Ok(value) }
298}
299
300pub(crate) fn check_cuvs(status: cuvsError_t, operation: &'static str) -> Result<()> {
301 if status == cuvsError_t::CUVS_SUCCESS {
302 return Ok(());
303 }
304
305 let kind = unsafe { cuvs_sys::cuvsGetLastErrorKind() };
306 let message = unsafe {
307 let ptr = cuvs_sys::cuvsGetLastErrorText();
308 if ptr.is_null() {
309 "cuVS C API returned CUVS_ERROR without an error message".to_string()
310 } else {
311 CStr::from_ptr(ptr).to_string_lossy().into_owned()
312 }
313 };
314
315 Err(native_error(status, kind, operation, message))
316}
317
318fn native_error(
319 status: cuvsError_t,
320 native_kind: cuvsErrorKind_t,
321 operation: &'static str,
322 message: impl Into<String>,
323) -> Error {
324 let kind = match native_kind {
325 cuvsErrorKind_t::CUVS_ERROR_KIND_DEVICE_OUT_OF_MEMORY => ErrorKind::OutOfMemory,
326 cuvsErrorKind_t::CUVS_ERROR_KIND_HOST_OUT_OF_MEMORY => ErrorKind::Unexpected,
328 cuvsErrorKind_t::CUVS_ERROR_KIND_NONE | cuvsErrorKind_t::CUVS_ERROR_KIND_UNKNOWN => ErrorKind::Unexpected,
329 };
330 let error = Error::new(kind, operation).with_message(message).with_context("native_status", format!("{status:?}"));
331 match native_kind {
332 cuvsErrorKind_t::CUVS_ERROR_KIND_DEVICE_OUT_OF_MEMORY | cuvsErrorKind_t::CUVS_ERROR_KIND_HOST_OUT_OF_MEMORY => {
333 error.set_temporary()
334 },
335 cuvsErrorKind_t::CUVS_ERROR_KIND_NONE | cuvsErrorKind_t::CUVS_ERROR_KIND_UNKNOWN => error,
336 }
337}
338
339#[cfg(test)]
340mod tests {
341 use super::*;
342
343 #[test]
344 fn native_message_text_never_changes_error_kind() {
345 for message in ["out of memory", "invalid argument", "unsupported dtype", "arbitrary localized native text"] {
346 let error =
347 native_error(cuvsError_t::CUVS_ERROR, cuvsErrorKind_t::CUVS_ERROR_KIND_UNKNOWN, "test", message);
348 assert_eq!(error.kind(), ErrorKind::Unexpected);
349 assert_eq!(error.message(), message);
350 }
351 }
352
353 #[test]
354 fn native_error_kind_classifies_allocations_without_parsing_text() {
355 for (native_kind, kind, status) in [
356 (cuvsErrorKind_t::CUVS_ERROR_KIND_DEVICE_OUT_OF_MEMORY, ErrorKind::OutOfMemory, ErrorStatus::Temporary),
357 (cuvsErrorKind_t::CUVS_ERROR_KIND_HOST_OUT_OF_MEMORY, ErrorKind::Unexpected, ErrorStatus::Temporary),
358 (cuvsErrorKind_t::CUVS_ERROR_KIND_NONE, ErrorKind::Unexpected, ErrorStatus::Permanent),
359 (cuvsErrorKind_t::CUVS_ERROR_KIND_UNKNOWN, ErrorKind::Unexpected, ErrorStatus::Permanent),
360 ] {
361 let error = native_error(cuvsError_t::CUVS_ERROR, native_kind, "test", "not an allocation message");
362 assert_eq!((error.kind(), error.status()), (kind, status));
363 }
364 }
365}