runmat-runtime 0.6.0

Core runtime for RunMat with builtins, BLAS/LAPACK integration, and execution APIs
Documentation
//! MATLAB runtime/version compatibility helpers.

use runmat_builtins::{
    BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
    BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor, Value,
};
use runmat_macros::runtime_builtin;

use crate::{build_runtime_error, BuiltinResult, RuntimeError};

const RUNMAT_VERSION: &str = env!("CARGO_PKG_VERSION");
const MATLAB_COMPAT_VERSION: &str = "9.15";
const MATLAB_COMPAT_RELEASE: &str = "R2023b";

const VALUE_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
    name: "value",
    ty: BuiltinParamType::Any,
    arity: BuiltinParamArity::Required,
    default: None,
    description: "Version or logical result.",
}];

const ANY_INPUTS: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
    name: "args",
    ty: BuiltinParamType::Any,
    arity: BuiltinParamArity::Variadic,
    default: None,
    description: "Version query arguments.",
}];

const SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
    label: "value = runtimeCompat(args...)",
    inputs: &ANY_INPUTS,
    outputs: &VALUE_OUTPUT,
}];

const ERROR_INVALID_INPUT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
    code: "RM.RUNTIME_COMPAT.INVALID_INPUT",
    identifier: Some("RunMat:runtimeCompat:InvalidInput"),
    when: "Arguments are malformed or unsupported.",
    message: "runtime compatibility helper: invalid input",
};

const ERRORS: [BuiltinErrorDescriptor; 1] = [ERROR_INVALID_INPUT];

pub const RUNTIME_COMPAT_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
    signatures: &SIGNATURES,
    output_mode: BuiltinOutputMode::Fixed,
    completion_policy: BuiltinCompletionPolicy::Public,
    errors: &ERRORS,
};

#[runtime_builtin(
    name = "version",
    category = "introspection",
    summary = "Return RunMat's MATLAB-compatible version string.",
    keywords = "version,release,environment,compatibility",
    descriptor(crate::builtins::introspection::runtime_compat::RUNTIME_COMPAT_DESCRIPTOR),
    builtin_path = "crate::builtins::introspection::runtime_compat"
)]
fn version_builtin(args: Vec<Value>) -> BuiltinResult<Value> {
    if args.is_empty() {
        return Ok(Value::from(RUNMAT_VERSION));
    }
    if args.len() != 1 {
        return Err(error("version: expected zero or one argument"));
    }
    match text_scalar(&args[0])?.to_ascii_lowercase().as_str() {
        "-release" => Ok(Value::from(MATLAB_COMPAT_RELEASE)),
        "-description" => Ok(Value::from(format!(
            "RunMat {RUNMAT_VERSION} (MATLAB compatibility {MATLAB_COMPAT_RELEASE})"
        ))),
        "-java" => Ok(Value::from("")),
        other => Err(error(format!("version: unsupported option '{other}'"))),
    }
}

#[runtime_builtin(
    name = "verLessThan",
    category = "introspection",
    summary = "Compare the MATLAB compatibility version against a required version.",
    keywords = "verLessThan,version,compatibility",
    descriptor(crate::builtins::introspection::runtime_compat::RUNTIME_COMPAT_DESCRIPTOR),
    builtin_path = "crate::builtins::introspection::runtime_compat"
)]
fn ver_less_than_builtin(product: Value, version: Value) -> BuiltinResult<Value> {
    let product = text_scalar(&product)?.to_ascii_lowercase();
    let requested = text_scalar(&version)?;
    if product != "matlab" {
        return Ok(Value::Bool(false));
    }
    Ok(Value::Bool(
        compare_versions(MATLAB_COMPAT_VERSION, &requested) < 0,
    ))
}

#[runtime_builtin(
    name = "isdeployed",
    category = "introspection",
    summary = "Return false because RunMat is not MATLAB Compiler deployed code.",
    keywords = "isdeployed,deployment,runtime",
    descriptor(crate::builtins::introspection::runtime_compat::RUNTIME_COMPAT_DESCRIPTOR),
    builtin_path = "crate::builtins::introspection::runtime_compat"
)]
fn isdeployed_builtin(args: Vec<Value>) -> BuiltinResult<Value> {
    if !args.is_empty() {
        return Err(error("isdeployed: expected no input arguments"));
    }
    Ok(Value::Bool(false))
}

fn text_scalar(value: &Value) -> BuiltinResult<String> {
    match value {
        Value::String(text) => Ok(text.clone()),
        Value::CharArray(chars) if chars.rows == 1 => Ok(chars.data.iter().collect()),
        Value::StringArray(array) if array.data.len() == 1 => Ok(array.data[0].clone()),
        _ => Err(error("runtime compatibility helper: expected text scalar")),
    }
}

fn compare_versions(left: &str, right: &str) -> i8 {
    let left_parts = version_parts(left);
    let right_parts = version_parts(right);
    let len = left_parts.len().max(right_parts.len());
    for idx in 0..len {
        let l = *left_parts.get(idx).unwrap_or(&0);
        let r = *right_parts.get(idx).unwrap_or(&0);
        if l < r {
            return -1;
        }
        if l > r {
            return 1;
        }
    }
    0
}

fn version_parts(value: &str) -> Vec<u64> {
    value
        .split(|ch: char| !ch.is_ascii_digit())
        .filter(|part| !part.is_empty())
        .map(|part| part.parse::<u64>().unwrap_or(0))
        .collect()
}

fn error(message: impl Into<String>) -> RuntimeError {
    let mut builder = build_runtime_error(message).with_builtin("runtime compatibility helper");
    if let Some(identifier) = ERROR_INVALID_INPUT.identifier {
        builder = builder.with_identifier(identifier);
    }
    builder.build()
}

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

    #[test]
    fn version_options_return_text() {
        assert!(matches!(
            version_builtin(Vec::new()).unwrap(),
            Value::String(_)
        ));
        assert_eq!(
            version_builtin(vec![Value::from("-release")]).unwrap(),
            Value::from(MATLAB_COMPAT_RELEASE)
        );
    }

    #[test]
    fn deployed_and_version_comparison_are_stable() {
        assert_eq!(isdeployed_builtin(Vec::new()).unwrap(), Value::Bool(false));
        assert_eq!(
            ver_less_than_builtin(Value::from("matlab"), Value::from("99.0")).unwrap(),
            Value::Bool(true)
        );
        assert_eq!(
            ver_less_than_builtin(Value::from("simulink"), Value::from("99.0")).unwrap(),
            Value::Bool(false)
        );
    }
}