eclisp 0.2.0

Bindings for Embeddable Common Lisp
use std::{
    any::Any,
    cell::RefCell,
    panic::{AssertUnwindSafe, catch_unwind},
};

use crate::{FromLisp, IntoLisp, LispContext, LispError, ValueRef, callback, sys};

thread_local! {
    static ERROR: RefCell<Option<String>> = const {
        RefCell::new(None)
    };
}

fn clear_error() {
    ERROR.with(|error| error.borrow_mut().take());
}

fn set_error(message: String) {
    ERROR.with(|error| *error.borrow_mut() = Some(message));
}

fn panic_message(payload: Box<dyn Any + Send>) -> String {
    if let Some(message) = payload.downcast_ref::<&str>() {
        (*message).to_string()
    } else if let Some(message) = payload.downcast_ref::<String>() {
        message.clone()
    } else {
        "Native callback panicked".into()
    }
}

#[doc(hidden)]
pub fn invoke(
    f: impl FnOnce(&LispContext<'_>) -> Result<sys::cl_object, LispError>,
) -> sys::cl_object {
    clear_error();

    let result = catch_unwind(AssertUnwindSafe(|| callback::with_active_context(f)));

    match result {
        Ok(Some(Ok(raw))) => raw,
        Ok(Some(Err(error))) => {
            set_error(error.to_string());
            unsafe { sys::eclisp_native_failure_raw() }
        }
        Ok(None) => {
            set_error("No active Lisp context".into());
            unsafe { sys::eclisp_native_failure_raw() }
        }
        Err(payload) => {
            set_error(panic_message(payload));
            unsafe { sys::eclisp_native_failure_raw() }
        }
    }
}

pub(crate) unsafe extern "C" fn take_error() -> sys::cl_object {
    let message = ERROR.with(|error| error.borrow_mut().take());

    let Some(message) = message else {
        return unsafe { sys::eclisp_nil_raw() };
    };

    match callback::with_active_context(|ctx| {
        ctx.string(message).map(callback::retain_native_result)
    }) {
        Some(Ok(raw)) => raw,
        Some(Err(_)) | None => unsafe { sys::eclisp_native_failure_raw() },
    }
}

#[doc(hidden)]
pub unsafe fn from_native<T: FromLisp>(raw: sys::cl_object) -> Result<T, LispError> {
    let value = unsafe { ValueRef::from_raw(raw) };
    T::from_lisp(value)
}

#[doc(hidden)]
pub fn to_native<T: IntoLisp>(
    ctx: &LispContext<'_>,
    value: T,
) -> Result<sys::cl_object, LispError> {
    let value = value.into_lisp(ctx)?;
    Ok(callback::retain_native_result(value))
}