runmat-runtime 0.6.0

Core runtime for RunMat with builtins, BLAS/LAPACK integration, and execution APIs
Documentation
//! MATLAB-compatible `gobjects` builtin.

use crate::builtins::common::tensor;
use crate::builtins::plotting::type_resolvers::handle_array_type;
use runmat_builtins::{
    BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
    BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
    NumericDType, Tensor, Value,
};
use runmat_macros::runtime_builtin;

const GOBJECTS_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
    name: "h",
    ty: BuiltinParamType::NumericArray,
    arity: BuiltinParamArity::Required,
    default: None,
    description: "Array of invalid graphics-handle placeholders for preallocation.",
}];

const GOBJECTS_INPUTS_NONE: [BuiltinParamDescriptor; 0] = [];

const GOBJECTS_INPUTS_DIMS: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
    name: "sz",
    ty: BuiltinParamType::SizeArg,
    arity: BuiltinParamArity::Variadic,
    default: None,
    description: "Output dimensions, either as separate scalar sizes or a size vector.",
}];

const GOBJECTS_SIGNATURES: [BuiltinSignatureDescriptor; 2] = [
    BuiltinSignatureDescriptor {
        label: "h = gobjects()",
        inputs: &GOBJECTS_INPUTS_NONE,
        outputs: &GOBJECTS_OUTPUT,
    },
    BuiltinSignatureDescriptor {
        label: "h = gobjects(sz...)",
        inputs: &GOBJECTS_INPUTS_DIMS,
        outputs: &GOBJECTS_OUTPUT,
    },
];

const GOBJECTS_ERRORS: [BuiltinErrorDescriptor; 0] = [];

pub const GOBJECTS_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
    signatures: &GOBJECTS_SIGNATURES,
    output_mode: BuiltinOutputMode::Fixed,
    completion_policy: BuiltinCompletionPolicy::Public,
    errors: &GOBJECTS_ERRORS,
};

#[runtime_builtin(
    name = "gobjects",
    category = "plotting",
    summary = "Create an array of graphics-handle placeholders.",
    keywords = "gobjects,graphics,handle,preallocate,plotting",
    suppress_auto_output = true,
    type_resolver(handle_array_type),
    descriptor(crate::builtins::plotting::gobjects::GOBJECTS_DESCRIPTOR),
    builtin_path = "crate::builtins::plotting::gobjects"
)]
pub async fn gobjects_builtin(args: Vec<Value>) -> crate::BuiltinResult<Value> {
    let shape = parse_shape(args).await?;
    let len = tensor::element_count(&shape);
    let tensor = Tensor::new_with_dtype(vec![f64::NAN; len], shape, NumericDType::F64)
        .map_err(|err| format!("gobjects: {err}"))?;
    Ok(Value::Tensor(tensor))
}

async fn parse_shape(args: Vec<Value>) -> crate::BuiltinResult<Vec<usize>> {
    if args.is_empty() {
        return Ok(vec![1, 1]);
    }

    let mut dims = Vec::new();
    for arg in args {
        let Some(mut parsed) = tensor::dims_from_value_async(&arg)
            .await
            .map_err(|err| format!("gobjects: {err}"))?
        else {
            return Err(
                "gobjects: size arguments must be numeric scalar sizes or a size vector".into(),
            );
        };
        dims.append(&mut parsed);
    }

    Ok(match dims.len() {
        0 => vec![0, 0],
        1 => vec![dims[0], dims[0]],
        _ => dims,
    })
}

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

    fn tensor_from(value: Value) -> Tensor {
        match value {
            Value::Tensor(tensor) => tensor,
            other => panic!("expected tensor, got {other:?}"),
        }
    }

    #[test]
    fn gobjects_descriptor_signatures_cover_core_forms() {
        let labels: Vec<&str> = GOBJECTS_DESCRIPTOR
            .signatures
            .iter()
            .map(|sig| sig.label)
            .collect();
        assert!(labels.contains(&"h = gobjects()"));
        assert!(labels.contains(&"h = gobjects(sz...)"));
    }

    #[test]
    fn gobjects_defaults_to_scalar_placeholder() {
        let tensor = tensor_from(block_on(gobjects_builtin(Vec::new())).unwrap());
        assert_eq!(tensor.shape, vec![1, 1]);
        assert_eq!(tensor.data.len(), 1);
        assert!(tensor.data[0].is_nan());
    }

    #[test]
    fn gobjects_accepts_scalar_dims_and_size_vector() {
        let square = tensor_from(block_on(gobjects_builtin(vec![Value::Num(3.0)])).unwrap());
        assert_eq!(square.shape, vec![3, 3]);
        assert!(square.data.iter().all(|value| value.is_nan()));

        let rect = tensor_from(
            block_on(gobjects_builtin(vec![Value::Num(2.0), Value::Num(3.0)])).unwrap(),
        );
        assert_eq!(rect.shape, vec![2, 3]);

        let size_vec = Tensor::new(vec![4.0, 1.0], vec![1, 2]).unwrap();
        let vector =
            tensor_from(block_on(gobjects_builtin(vec![Value::Tensor(size_vec)])).unwrap());
        assert_eq!(vector.shape, vec![4, 1]);
    }

    #[test]
    fn gobjects_rejects_invalid_dimensions() {
        assert!(block_on(gobjects_builtin(vec![Value::Num(-1.0)])).is_err());
        assert!(block_on(gobjects_builtin(vec![Value::Num(1.5)])).is_err());
        let matrix = Tensor::new(vec![1.0, 2.0, 3.0, 4.0], vec![2, 2]).unwrap();
        assert!(block_on(gobjects_builtin(vec![Value::Tensor(matrix)])).is_err());
    }
}