umka 0.1.0

high level bindings for umka
Documentation
mod function_calls;

use core::fmt;
use std::{
    ffi,
    io::{self, Read},
    ptr::null_mut,
    string::ToString,
};

use crate::function_calls::FunctionCallBuilder;

/// Get the Umka interpreter version
#[must_use]
pub fn version() -> String {
    unsafe {
        let version = umka_sys::umkaGetVersion();
        ffi::CStr::from_ptr(version).to_string_lossy().to_string()
    }
}

pub struct Umka(*mut umka_sys::tagUmka);

#[derive(thiserror::Error, Debug)]
pub enum Error {
    #[error("failed to read source: {0}")]
    IO(#[from] io::Error),
    #[error("source string contains a null byte")]
    NullByte(#[from] ffi::NulError),
    #[error("failed to allocate memory for umka interpreter")]
    AllocError,
    #[error("failed to initialise umka")]
    InitError,
    #[error("{}", .0.as_ref().map_or_else(
        || "generic compilation error".to_string(),
        ToString::to_string
    ))]
    ModuleError(Option<ErrorDetails>),
    #[error("{}", .0.as_ref().map_or_else(
        || "generic compilation error".to_string(),
        ToString::to_string
    ))]
    CompileError(Option<ErrorDetails>),
    #[error("{}", .0.as_ref().map_or_else(
        || "generic runtime error".to_string(),
        ToString::to_string
    ))]
    RuntimeError(Option<ErrorDetails>),
}

#[derive(Debug)]
pub struct ErrorDetails {
    pub pos: i32,
    pub line: i32,
    pub code: i32,
    pub fn_name: String,
    pub message: String,
    pub file_name: String,
}

/// # Initialisation and running
impl Umka {
    /// Initialise the Umka interpreter.
    ///
    /// # Errors
    ///
    /// - `Error::IO` if consuming the reader fails
    /// - `Error::NullByte` if the program source contains a null byte
    /// - `Error::Alloc` if allocating memory for the Umka interpreter fails
    /// - `Error::Init` if initialising the Umka interpreter fails
    pub fn new<R: Read>(mut reader: R, file_name: &str) -> Result<Self, Error> {
        let mut source = String::new();
        reader.read_to_string(&mut source).map_err(Error::IO)?;
        let umka = init(file_name, &source)?;
        Ok(Self(umka))
    }

    /// Add an Umka module.
    ///
    /// # Errors
    ///
    /// - `Error::IO` if consuming the reader fails
    /// - `Error::NullByte` if the module source contains a null byte
    /// - `Error::ModuleError` if adding the module fails
    pub fn add_module<R: Read>(&self, mut reader: R, file_name: &str) -> Result<(), Error> {
        let mut source = String::new();
        reader.read_to_string(&mut source).map_err(Error::IO)?;
        unsafe {
            let file_name = ffi::CString::new(file_name).map_err(Error::NullByte)?;
            let source = ffi::CString::new(source).map_err(Error::NullByte)?;
            let ok = umka_sys::umkaAddModule(self.0, file_name.as_ptr(), source.as_ptr());
            if ok {
                Ok(())
            } else {
                Err(Error::ModuleError(self.error_details()))
            }
        }
    }

    /// Compile the Umka program into bytecode.
    ///
    /// # Errors
    ///
    /// Returns an `Error::CompileError` if compilation fails.
    pub fn compile(&self) -> Result<(), Error> {
        if unsafe { umka_sys::umkaCompile(self.0) } {
            Ok(())
        } else {
            Err(Error::CompileError(self.error_details()))
        }
    }

    /// Lookup and run the main function in the compiled program. Will do nothing if
    /// `self::compile` was not called first.
    ///
    /// Returns the exit code of the main function.
    #[must_use]
    pub fn run(&self) -> i32 {
        unsafe { umka_sys::umkaRun(self.0) }
    }
}

/// # Calling functions
impl Umka {
    /// Prepare to call an Umka function by name.
    ///
    /// # Errors
    ///
    /// Returns `Error::NullByte` if `name` contains a null byte.
    pub fn function(&self, name: &str) -> Result<FunctionCallBuilder<'_>, Error> {
        let name = ffi::CString::new(name).map_err(Error::NullByte)?;
        Ok(FunctionCallBuilder::new(self, name))
    }
}

impl Umka {
    fn error_details(&self) -> Option<ErrorDetails> {
        unsafe {
            let error = *umka_sys::umkaGetError(self.0);
            if error.msg.is_null() {
                None
            } else {
                let fn_name = ffi::CStr::from_ptr(error.fileName)
                    .to_string_lossy()
                    .to_string();
                let message = ffi::CStr::from_ptr(error.msg).to_string_lossy().to_string();
                let file_name = ffi::CStr::from_ptr(error.fileName)
                    .to_string_lossy()
                    .to_string();
                Some(ErrorDetails {
                    pos: error.pos,
                    line: error.line,
                    code: error.code,
                    fn_name,
                    message,
                    file_name,
                })
            }
        }
    }
}

impl Drop for Umka {
    fn drop(&mut self) {
        unsafe {
            umka_sys::umkaFree(self.0);
        }
    }
}

fn init(file_name: &str, source_string: &str) -> Result<*mut umka_sys::tagUmka, Error> {
    unsafe {
        let umka = umka_sys::umkaAlloc();
        if umka.is_null() {
            return Err(Error::AllocError);
        }

        let file_name = ffi::CString::new(file_name).map_err(Error::NullByte)?;
        let source_string = ffi::CString::new(source_string).map_err(Error::NullByte)?;

        let ok = umka_sys::umkaInit(
            umka,
            file_name.as_ptr(),
            source_string.as_ptr(),
            4096,
            null_mut(),
            0,
            null_mut(),
            true,
            true,
            None,
        );
        if !ok {
            return Err(Error::InitError);
        }

        Ok(umka)
    }
}

impl fmt::Display for ErrorDetails {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "Error {} ({}, {}): {}",
            self.file_name, self.line, self.pos, self.message
        )?;
        Ok(())
    }
}