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,
};

const INPUTNAME_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
    name: "name",
    ty: BuiltinParamType::StringScalar,
    arity: BuiltinParamArity::Required,
    default: None,
    description: "Caller argument variable name, or empty text when unavailable.",
}];

const INPUTNAME_INPUTS: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
    name: "argNumber",
    ty: BuiltinParamType::NumericScalar,
    arity: BuiltinParamArity::Required,
    default: None,
    description: "One-based caller argument index.",
}];

const INPUTNAME_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
    label: "name = inputname(argNumber)",
    inputs: &INPUTNAME_INPUTS,
    outputs: &INPUTNAME_OUTPUT,
}];

pub const INPUTNAME_ERROR_NOT_ENOUGH_INPUTS: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
    code: "RM.INPUTNAME.NOT_ENOUGH_INPUTS",
    identifier: Some("RunMat:NotEnoughInputs"),
    when: "No argument index is provided.",
    message: "inputname: not enough input arguments",
};

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

pub const INPUTNAME_ERROR_ARGUMENT_INVALID: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
    code: "RM.INPUTNAME.ARGUMENT_INVALID",
    identifier: Some("RunMat:InputnameArgumentInvalid"),
    when: "The argument index is not a positive integer scalar.",
    message: "inputname: argument index must be a positive integer scalar",
};

pub const INPUTNAME_ERRORS: [BuiltinErrorDescriptor; 3] = [
    INPUTNAME_ERROR_NOT_ENOUGH_INPUTS,
    INPUTNAME_ERROR_TOO_MANY_INPUTS,
    INPUTNAME_ERROR_ARGUMENT_INVALID,
];

pub const INPUTNAME_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
    signatures: &INPUTNAME_SIGNATURES,
    output_mode: BuiltinOutputMode::Fixed,
    completion_policy: BuiltinCompletionPolicy::Public,
    errors: &INPUTNAME_ERRORS,
};

fn descriptor_error(error: &'static BuiltinErrorDescriptor) -> crate::RuntimeError {
    crate::runtime_descriptor_error("inputname", error)
}

fn numeric_index(value: &Value) -> Option<usize> {
    let n = match value {
        Value::Int(value) => value.to_f64(),
        Value::Num(value) => *value,
        Value::Tensor(tensor) if tensor.data.len() == 1 => tensor.data[0],
        _ => return None,
    };
    if !n.is_finite() || n < 1.0 || n.fract() != 0.0 || n > usize::MAX as f64 {
        return None;
    }
    Some(n as usize)
}

fn is_simple_identifier(text: &str) -> bool {
    let mut chars = text.chars();
    let Some(first) = chars.next() else {
        return false;
    };
    if !(first == '_' || first.is_ascii_alphabetic()) {
        return false;
    }
    chars.all(|ch| ch == '_' || ch.is_ascii_alphanumeric())
}

pub(crate) fn dispatch_inputname(args: Vec<Value>) -> crate::BuiltinResult<Value> {
    match args.len() {
        0 => return Err(descriptor_error(&INPUTNAME_ERROR_NOT_ENOUGH_INPUTS)),
        1 => {}
        _ => return Err(descriptor_error(&INPUTNAME_ERROR_TOO_MANY_INPUTS)),
    }
    let index = numeric_index(&args[0])
        .ok_or_else(|| descriptor_error(&INPUTNAME_ERROR_ARGUMENT_INVALID))?;
    let text = crate::callsite::function_input_arg_text(index - 1)
        .map(|text| text.trim().to_string())
        .filter(|text| is_simple_identifier(text))
        .unwrap_or_default();
    Ok(Value::String(text))
}

#[runmat_macros::runtime_builtin(
    name = "inputname",
    category = "introspection",
    summary = "Return the caller argument variable name for a function input.",
    descriptor(self::INPUTNAME_DESCRIPTOR),
    builtin_path = "crate::builtins::introspection::inputname"
)]
pub fn inputname_builtin_registered(args: Vec<Value>) -> crate::BuiltinResult<Value> {
    dispatch_inputname(args)
}

#[cfg(test)]
mod tests {
    use super::*;
    use runmat_hir::{SourceId, Span};

    fn span_of(source: &str, needle: &str) -> Span {
        let start = source.find(needle).expect("needle present");
        Span {
            start,
            end: start + needle.len(),
        }
    }

    #[test]
    fn inputname_reads_simple_caller_argument_name() {
        let source = "out = probe(alpha, alpha + 1, 7);";
        let _catalog_guard = crate::source_context::replace_source_catalog(vec![(
            SourceId(7),
            "/tmp/caller.m".to_string(),
            source.to_string(),
        )]);
        let _callsite_guard = crate::callsite::push_function_input_callsite(
            Some(SourceId(7)),
            Some(vec![
                span_of(source, "alpha"),
                span_of(source, "alpha + 1"),
                span_of(source, "7"),
            ]),
        );

        let name = dispatch_inputname(vec![Value::Num(1.0)]).expect("inputname succeeds");
        assert_eq!(name, Value::String("alpha".to_string()));
    }

    #[test]
    fn inputname_returns_empty_for_expression_literal_and_missing_context() {
        let source = "out = probe(alpha, alpha + 1, 7);";
        let _catalog_guard = crate::source_context::replace_source_catalog(vec![(
            SourceId(8),
            "/tmp/caller.m".to_string(),
            source.to_string(),
        )]);
        let _callsite_guard = crate::callsite::push_function_input_callsite(
            Some(SourceId(8)),
            Some(vec![
                span_of(source, "alpha"),
                span_of(source, "alpha + 1"),
                span_of(source, "7"),
            ]),
        );

        let expr = dispatch_inputname(vec![Value::Num(2.0)]).expect("inputname succeeds");
        let literal = dispatch_inputname(vec![Value::Num(3.0)]).expect("inputname succeeds");
        let missing = dispatch_inputname(vec![Value::Num(4.0)]).expect("inputname succeeds");
        assert_eq!(expr, Value::String(String::new()));
        assert_eq!(literal, Value::String(String::new()));
        assert_eq!(missing, Value::String(String::new()));
    }
}