runmat-runtime 0.5.0

Core runtime for RunMat with builtins, BLAS/LAPACK integration, and execution APIs
Documentation
use runmat_builtins::{
    BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
    BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor, Value,
};
use std::path::Path;

const MFILENAME_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
    name: "name",
    ty: BuiltinParamType::StringScalar,
    arity: BuiltinParamArity::Required,
    default: None,
    description: "Current file name.",
}];

const MFILENAME_INPUTS: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
    name: "option",
    ty: BuiltinParamType::StringScalar,
    arity: BuiltinParamArity::Optional,
    default: None,
    description: "`fullpath` for full source path or `class` for current method class.",
}];

const MFILENAME_SIGNATURES: [BuiltinSignatureDescriptor; 2] = [
    BuiltinSignatureDescriptor {
        label: "name = mfilename()",
        inputs: &[],
        outputs: &MFILENAME_OUTPUT,
    },
    BuiltinSignatureDescriptor {
        label: "name = mfilename(option)",
        inputs: &MFILENAME_INPUTS,
        outputs: &MFILENAME_OUTPUT,
    },
];

pub const MFILENAME_ERROR_TOO_MANY_INPUTS: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
    code: "RM.MFILENAME.TOO_MANY_INPUTS",
    identifier: Some("RunMat:TooManyInputs"),
    when: "More than one option argument is provided.",
    message: "mfilename: too many input arguments",
};

pub const MFILENAME_ERRORS: [BuiltinErrorDescriptor; 1] = [MFILENAME_ERROR_TOO_MANY_INPUTS];

pub const MFILENAME_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
    signatures: &MFILENAME_SIGNATURES,
    output_mode: BuiltinOutputMode::Fixed,
    completion_policy: BuiltinCompletionPolicy::Public,
    errors: &MFILENAME_ERRORS,
};

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum MfilenameMode {
    Name,
    Fullpath,
    Class,
}

fn text_value(value: &Value) -> Option<String> {
    match value {
        Value::String(text) => Some(text.clone()),
        Value::StringArray(array) if array.data.len() == 1 => Some(array.data[0].clone()),
        Value::CharArray(array) if array.rows == 1 => Some(array.data.iter().collect()),
        _ => None,
    }
}

fn mode_from_args(args: &[Value]) -> crate::BuiltinResult<MfilenameMode> {
    if args.len() > 1 {
        return Err(crate::runtime_descriptor_error(
            "mfilename",
            &MFILENAME_ERROR_TOO_MANY_INPUTS,
        ));
    }
    let Some(option) = args.first().and_then(text_value) else {
        return Ok(MfilenameMode::Name);
    };
    match option.trim().to_ascii_lowercase().as_str() {
        "fullpath" => Ok(MfilenameMode::Fullpath),
        "class" => Ok(MfilenameMode::Class),
        _ => Ok(MfilenameMode::Name),
    }
}

fn path_without_extension(name: &str) -> String {
    if name.is_empty() {
        return String::new();
    }
    let mut path = Path::new(name).to_path_buf();
    path.set_extension("");
    path_to_display_string(&path)
}

fn path_to_display_string(path: &Path) -> String {
    let text = path.to_string_lossy().to_string();
    #[cfg(windows)]
    {
        strip_windows_verbatim_prefix(&text)
    }
    #[cfg(not(windows))]
    {
        text
    }
}

#[cfg(windows)]
fn strip_windows_verbatim_prefix(path: &str) -> String {
    if let Some(stripped) = path.strip_prefix(r"\\?\UNC\") {
        format!(r"\\{stripped}")
    } else if let Some(stripped) = path.strip_prefix(r"\\?\") {
        stripped.to_string()
    } else {
        path.to_string()
    }
}

fn file_stem_without_extension(name: &str) -> String {
    Path::new(name)
        .file_stem()
        .map(|stem| stem.to_string_lossy().to_string())
        .unwrap_or_else(|| path_without_extension(name))
}

pub(crate) fn dispatch_mfilename(args: Vec<Value>) -> crate::BuiltinResult<Value> {
    let mode = mode_from_args(&args)?;
    let result = match mode {
        MfilenameMode::Name => crate::source_context::current_source_info()
            .map(|source| file_stem_without_extension(&source.name))
            .unwrap_or_default(),
        MfilenameMode::Fullpath => crate::source_context::current_source_info()
            .map(|source| path_without_extension(&source.name))
            .unwrap_or_default(),
        MfilenameMode::Class => crate::class_access_context().unwrap_or_default(),
    };
    Ok(Value::String(result))
}

#[runmat_macros::runtime_builtin(
    name = "mfilename",
    category = "introspection",
    summary = "Return the filename of the currently running source.",
    descriptor(self::MFILENAME_DESCRIPTOR),
    builtin_path = "crate::builtins::introspection::mfilename"
)]
pub fn mfilename_builtin_registered(args: Vec<Value>) -> crate::BuiltinResult<Value> {
    dispatch_mfilename(args)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn mfilename_uses_source_context_name() {
        let _guard = crate::source_context::replace_current_source_context(
            Some("/tmp/demo_file.m"),
            Some("name = mfilename();"),
        );
        let value = dispatch_mfilename(Vec::new()).expect("mfilename succeeds");
        assert_eq!(value, Value::String("demo_file".to_string()));
    }

    #[test]
    fn mfilename_fullpath_strips_extension() {
        let _guard = crate::source_context::replace_current_source_context(
            Some("/tmp/demo_file.m"),
            Some("name = mfilename('fullpath');"),
        );
        let value = dispatch_mfilename(vec![Value::String("fullpath".to_string())])
            .expect("mfilename fullpath succeeds");
        assert_eq!(value, Value::String("/tmp/demo_file".to_string()));
    }
}