eclisp 0.1.2

Bindings for Embeddable Common Lisp
use core::{fmt, slice};
use std::{
    ffi::CStr,
    marker::PhantomData,
    ptr::{self, NonNull},
};

use crate::{Lisp, ListError, ListIter, sys};

#[derive(thiserror::Error, Debug, PartialEq, Eq)]
pub enum ValueError {
    #[error("Expected {expected}, got {actual}")]
    TypeMismatch {
        expected: &'static str,
        actual: String,
    },

    #[error("Invalid UTF-8")]
    InvalidUtf8,

    #[error("Failed to convert Lisp value")]
    ConversionFailed,
}

#[derive(Clone, Copy)]
pub struct ValueRef<'a> {
    object: sys::cl_object,
    _lifetime: PhantomData<&'a ()>,
}

impl<'a> ValueRef<'a> {
    /// # Safety
    /// The object must remain reachable by ECL's GC for all of `'a`, and ECL operations must only
    /// run on an appropriately initialized thread.
    pub(crate) unsafe fn from_raw(object: sys::cl_object) -> Self {
        Self {
            object,
            _lifetime: PhantomData,
        }
    }

    pub(crate) fn as_raw(self) -> sys::cl_object {
        self.object
    }

    fn raw(&self) -> RawValue {
        RawValue::new(self.object)
    }

    pub fn is_nil(&self) -> bool {
        self.raw().is_nil()
    }

    pub fn is_t(&self) -> bool {
        self.raw().is_t()
    }

    pub fn is_int(&self) -> bool {
        self.raw().is_int()
    }

    pub fn is_float(&self) -> bool {
        self.raw().is_float()
    }

    pub fn is_string(&self) -> bool {
        self.raw().is_string()
    }

    pub fn is_cons(&self) -> bool {
        self.raw().is_cons()
    }

    pub fn is_list(&self) -> bool {
        self.raw().is_list()
    }

    pub fn try_as_i64(&self) -> Result<i64, ValueError> {
        self.raw().try_as_i64()
    }

    pub fn try_as_f64(&self) -> Result<f64, ValueError> {
        self.raw().try_as_f64()
    }

    pub fn try_as_string(&self) -> Result<String, ValueError> {
        self.raw().try_as_string()
    }

    pub fn repr(&self) -> Result<String, ValueError> {
        self.raw().repr()
    }
}

pub struct Value<'rt> {
    runtime: &'rt Lisp,
    root: NonNull<sys::eclisp_root>,
}

impl<'rt> Value<'rt> {
    /// # Safety
    /// `root` must be valid, uniquely owned, and allocated by `eclisp_root_new`.
    pub(crate) unsafe fn from_root(runtime: &'rt Lisp, root: NonNull<sys::eclisp_root>) -> Self {
        Self { runtime, root }
    }

    pub(crate) fn as_raw(&self) -> sys::cl_object {
        unsafe { sys::eclisp_root_get(self.root.as_ptr()) }
    }

    pub(crate) fn belongs_to(&self, runtime: &Lisp) -> bool {
        ptr::eq(self.runtime, runtime)
    }

    pub(crate) fn into_raw_root(self) -> *mut sys::eclisp_root {
        let root = self.root.as_ptr();
        std::mem::forget(self);
        root
    }

    fn raw(&self) -> RawValue {
        RawValue::new(self.as_raw())
    }

    pub fn as_ref(&self) -> ValueRef<'_> {
        unsafe { ValueRef::from_raw(self.as_raw()) }
    }

    pub fn iter(&self) -> ListIter<'_, 'rt> {
        ListIter::new(self)
    }

    pub fn is_nil(&self) -> bool {
        self.raw().is_nil()
    }

    pub fn is_t(&self) -> bool {
        self.raw().is_t()
    }

    pub fn is_int(&self) -> bool {
        self.raw().is_int()
    }

    pub fn is_float(&self) -> bool {
        self.raw().is_float()
    }

    pub fn is_string(&self) -> bool {
        self.raw().is_string()
    }

    pub fn is_cons(&self) -> bool {
        self.raw().is_cons()
    }

    pub fn is_list(&self) -> bool {
        self.raw().is_list()
    }

    pub fn try_as_i64(&self) -> Result<i64, ValueError> {
        self.raw().try_as_i64()
    }

    pub fn try_as_f64(&self) -> Result<f64, ValueError> {
        self.raw().try_as_f64()
    }

    pub fn try_as_string(&self) -> Result<String, ValueError> {
        self.raw().try_as_string()
    }

    pub fn repr(&self) -> Result<String, ValueError> {
        self.raw().repr()
    }

    pub fn car(&self) -> Result<Value<'rt>, ListError> {
        if !self.is_cons() {
            return Err(ListError::NotCons);
        }

        let mut root = ptr::null_mut();
        let success = unsafe { sys::eclisp_car(self.as_raw(), &mut root) };
        if success == 0 {
            return Err(ListError::AllocationFailed);
        }

        let root = NonNull::new(root).ok_or(ListError::AllocationFailed)?;
        Ok(unsafe { Value::from_root(self.runtime, root) })
    }

    pub fn cdr(&self) -> Result<Value<'rt>, ListError> {
        if !self.is_cons() {
            return Err(ListError::NotCons);
        }

        let mut root = ptr::null_mut();
        let success = unsafe { sys::eclisp_cdr(self.as_raw(), &mut root) };
        if success == 0 {
            return Err(ListError::AllocationFailed);
        }

        let root = NonNull::new(root).ok_or(ListError::AllocationFailed)?;
        Ok(unsafe { Value::from_root(self.runtime, root) })
    }
}

impl Drop for Value<'_> {
    fn drop(&mut self) {
        unsafe { sys::eclisp_root_free(self.root.as_ptr()) }
    }
}

impl fmt::Debug for Value<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self.repr() {
            Ok(repr) => write!(f, "Value({repr})"),
            Err(_) => write!(f, "Value(<error>)"),
        }
    }
}

impl fmt::Display for Value<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self.repr() {
            Ok(repr) => write!(f, "{repr}"),
            Err(_) => write!(f, "<error>"),
        }
    }
}

#[derive(Clone, Copy)]
pub(crate) struct RawValue {
    object: sys::cl_object,
}

impl RawValue {
    fn new(object: sys::cl_object) -> Self {
        Self { object }
    }

    fn is_nil(self) -> bool {
        unsafe { sys::eclisp_is_nil(self.object) != 0 }
    }

    fn is_t(self) -> bool {
        unsafe { sys::eclisp_is_t(self.object) != 0 }
    }

    fn is_int(self) -> bool {
        unsafe { sys::eclisp_is_int(self.object) != 0 }
    }

    fn is_float(self) -> bool {
        unsafe { sys::eclisp_is_float(self.object) != 0 }
    }

    fn is_string(self) -> bool {
        unsafe { sys::eclisp_is_string(self.object) != 0 }
    }

    fn is_cons(self) -> bool {
        unsafe { sys::eclisp_is_cons(self.object) != 0 }
    }

    fn is_list(self) -> bool {
        unsafe { sys::eclisp_is_list(self.object) != 0 }
    }

    fn try_as_i64(self) -> Result<i64, ValueError> {
        if !self.is_int() {
            return Err(ValueError::TypeMismatch {
                expected: "integer",
                actual: "other".into(),
            });
        }

        let mut value = 0i64;
        let success = unsafe { sys::eclisp_as_int(self.object, &mut value) };
        if success == 0 {
            return Err(ValueError::ConversionFailed);
        }

        Ok(value)
    }

    fn try_as_f64(self) -> Result<f64, ValueError> {
        if !self.is_float() {
            return Err(ValueError::TypeMismatch {
                expected: "float",
                actual: "other".into(),
            });
        }

        let mut value = 0f64;
        let success = unsafe { sys::eclisp_as_float(self.object, &mut value) };
        if success == 0 {
            return Err(ValueError::ConversionFailed);
        }

        Ok(value)
    }

    fn try_as_string(self) -> Result<String, ValueError> {
        let mut buffer = ptr::null_mut();
        let mut length = 0usize;

        let success = unsafe { sys::eclisp_as_string(self.object, &mut buffer, &mut length) };
        if success == 0 || buffer.is_null() {
            if !buffer.is_null() {
                unsafe { sys::eclisp_string_free(buffer) };
            }
            return Err(ValueError::ConversionFailed);
        }

        let result = unsafe {
            let bytes = slice::from_raw_parts(buffer.cast::<u8>(), length);
            str::from_utf8(bytes)
                .map(str::to_owned)
                .map_err(|_| ValueError::InvalidUtf8)
        };

        unsafe { sys::eclisp_string_free(buffer) };
        result
    }

    fn repr(self) -> Result<String, ValueError> {
        let mut buffer = ptr::null_mut();

        let success = unsafe { sys::eclisp_repr(self.object, &mut buffer) };
        if success == 0 || buffer.is_null() {
            if !buffer.is_null() {
                unsafe { sys::eclisp_string_free(buffer) };
            }
            return Err(ValueError::ConversionFailed);
        }

        let result = unsafe { CStr::from_ptr(buffer) }
            .to_str()
            .map(str::to_owned)
            .map_err(|_| ValueError::InvalidUtf8);

        unsafe { sys::eclisp_string_free(buffer) };
        result
    }
}