use crate::Dtype;
use std::convert::Infallible;
use std::ffi::{CStr, NulError};
use std::panic::Location;
use std::sync::Once;
use std::{cell::Cell, cell::RefCell, ffi::c_char};
use thiserror::Error;
pub type Result<T> = std::result::Result<T, Exception>;
#[derive(Error, PartialEq, Debug)]
pub enum IoError {
#[error("Path must point to a local file")]
NotFile,
#[error("Path contains invalid UTF-8")]
InvalidUtf8,
#[error("Path contains null bytes")]
NullBytes,
#[error("No file extension found")]
NoExtension,
#[error("Unsupported file format")]
UnsupportedFormat,
#[error("invalid serialized data: {0}")]
InvalidFormat(String),
#[error("Unable to open file")]
UnableToOpenFile,
#[error("Unable to allocate memory")]
AllocationError,
#[error(transparent)]
NulError(#[from] NulError),
#[error(transparent)]
Exception(#[from] Exception),
}
impl From<Infallible> for IoError {
fn from(_: Infallible) -> Self {
unreachable!()
}
}
impl From<RawException> for IoError {
#[track_caller]
fn from(e: RawException) -> Self {
let exception = Exception {
what: e.what,
location: Location::caller(),
};
Self::Exception(exception)
}
}
#[derive(Debug, PartialEq, Error)]
pub enum AsSliceError {
#[error("The data pointer is null.")]
Null,
#[error("dtype mismatch: expected {expecting:?}, found {found:?}")]
DtypeMismatch {
expecting: Dtype,
found: Dtype,
},
#[error(transparent)]
Exception(#[from] Exception),
}
cfg_safetensors! {
#[derive(Debug, Error)]
pub enum ConversionError {
#[error("The safetensors data type {0:?} is not supported.")]
SafeTensorDtype(safetensors::tensor::Dtype),
#[error("The mlx data type {0:?} is not supported.")]
MlxDtype(crate::Dtype),
#[error(transparent)]
PodCastError(#[from] bytemuck::PodCastError),
#[error(transparent)]
SafeTensorError(#[from] safetensors::tensor::SafeTensorError),
#[error(transparent)]
Exception(#[from] Exception),
}
}
pub(crate) struct RawException {
pub(crate) what: String,
}
#[derive(Debug, PartialEq, Error)]
#[error("{what:?} at {location}")]
pub struct Exception {
pub(crate) what: String,
pub(crate) location: &'static Location<'static>,
}
impl Exception {
pub fn what(&self) -> &str {
&self.what
}
pub fn location(&self) -> &'static Location<'static> {
self.location
}
#[track_caller]
pub fn custom(what: impl Into<String>) -> Self {
Self {
what: what.into(),
location: Location::caller(),
}
}
}
impl From<RawException> for Exception {
#[track_caller]
fn from(e: RawException) -> Self {
Self {
what: e.what,
location: Location::caller(),
}
}
}
impl From<&str> for Exception {
#[track_caller]
fn from(what: &str) -> Self {
Self {
what: what.to_string(),
location: Location::caller(),
}
}
}
impl From<Infallible> for Exception {
fn from(_: Infallible) -> Self {
unreachable!()
}
}
impl From<Exception> for String {
fn from(e: Exception) -> Self {
e.what
}
}
thread_local! {
static CLOSURE_ERROR: Cell<Option<Exception>> = const { Cell::new(None) };
static LAST_MLX_ERROR: RefCell<Option<String>> = const { RefCell::new(None) };
}
static INIT_ERR_HANDLER: Once = Once::new();
#[no_mangle]
extern "C" fn default_mlx_error_handler(msg: *const c_char, _data: *mut std::ffi::c_void) {
let message = unsafe { CStr::from_ptr(msg) }
.to_string_lossy()
.into_owned();
LAST_MLX_ERROR.with(|last_error| {
last_error.replace(Some(message));
});
}
fn take_last_mlx_error() -> Option<String> {
LAST_MLX_ERROR.with(|last_error| last_error.borrow_mut().take())
}
fn setup_mlx_error_handler() {
let handler = default_mlx_error_handler;
unsafe {
safemlx_sys::mlx_set_error_handler(Some(handler), std::ptr::null_mut(), None);
}
#[cfg(all(feature = "metal", target_vendor = "apple"))]
{
let status = unsafe {
safemlx_sys::mlx_metal_set_embedded_metallib(
safemlx_sys::MLX_METALLIB_LZFSE.as_ptr(),
safemlx_sys::MLX_METALLIB_LZFSE.len(),
safemlx_sys::MLX_METALLIB_UNCOMPRESSED_SIZE,
)
};
assert_eq!(status, 0, "failed to register the embedded MLX metallib");
}
}
pub(crate) fn ensure_mlx_error_handler() {
INIT_ERR_HANDLER.call_once(setup_mlx_error_handler);
}
pub(crate) fn set_closure_error(err: Exception) {
CLOSURE_ERROR.with(|closure_error| closure_error.set(Some(err)));
}
pub(crate) fn get_and_clear_closure_error() -> Option<Exception> {
CLOSURE_ERROR.with(|closure_error| closure_error.replace(None))
}
#[track_caller]
pub(crate) fn get_and_clear_last_mlx_error() -> Option<RawException> {
take_last_mlx_error().map(|what| RawException { what })
}
#[derive(Debug, Error)]
#[error("[finfo] dtype {:?} is not inexact", .0)]
pub struct InexactDtypeError(pub Dtype);
impl From<InexactDtypeError> for Exception {
#[track_caller]
fn from(value: InexactDtypeError) -> Self {
Exception::custom(value.to_string())
}
}
#[cfg(test)]
mod tests {
use crate::{array, Array};
#[test]
fn test_exception() {
let stream = crate::test_stream();
let a = array!([1.0, 2.0, 3.0]);
let b = array!([4.0, 5.0]);
let result = a.add(&b, stream);
let error = result.expect_err("Expected error");
assert!(error
.what()
.contains("Shapes (3) and (2) cannot be broadcast."))
}
#[test]
fn mlx_errors_are_thread_local() {
let threads = (0..8)
.map(|thread_index| {
std::thread::spawn(move || {
let stream = crate::Stream::new_with_device(&crate::Device::new(
crate::DeviceType::Gpu,
0,
));
for iteration in 0..64 {
let lhs_len = 3 + thread_index;
let rhs_len = lhs_len + 1 + iteration % 3;
let lhs = Array::from_slice(&vec![0.0f32; lhs_len], &[lhs_len as i32]);
let rhs = Array::from_slice(&vec![0.0f32; rhs_len], &[rhs_len as i32]);
let error = lhs.add(&rhs, &stream).expect_err("add should fail");
let expected = format!("Shapes ({lhs_len}) and ({rhs_len})");
assert!(
error.what().contains(&expected),
"expected thread-local error containing {expected:?}, got {:?}",
error.what()
);
}
})
})
.collect::<Vec<_>>();
for thread in threads {
thread.join().expect("worker thread panicked");
}
}
}