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 runmat_macros::runtime_builtin;

use super::properties::{get_properties, resolve_plot_handle};
use crate::builtins::plotting::type_resolvers::get_type;
use crate::{build_runtime_error, RuntimeError};

const BUILTIN_NAME: &str = "get";

const GET_OUTPUT_VALUE: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
    name: "value",
    ty: BuiltinParamType::Any,
    arity: BuiltinParamArity::Required,
    default: None,
    description: "Property value or a struct of all readable properties.",
}];

const GET_INPUTS_HANDLE: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
    name: "h",
    ty: BuiltinParamType::Any,
    arity: BuiltinParamArity::Required,
    default: None,
    description: "Graphics handle (figure, axes, plot object, text, legend, etc.).",
}];

const GET_INPUTS_HANDLE_PROPERTY: [BuiltinParamDescriptor; 2] = [
    BuiltinParamDescriptor {
        name: "h",
        ty: BuiltinParamType::Any,
        arity: BuiltinParamArity::Required,
        default: None,
        description: "Graphics handle (figure, axes, plot object, text, legend, etc.).",
    },
    BuiltinParamDescriptor {
        name: "property",
        ty: BuiltinParamType::PropertyName,
        arity: BuiltinParamArity::Optional,
        default: None,
        description: "Property name to query (case-insensitive).",
    },
];

const GET_SIGNATURES: [BuiltinSignatureDescriptor; 2] = [
    BuiltinSignatureDescriptor {
        label: "value = get(h)",
        inputs: &GET_INPUTS_HANDLE,
        outputs: &GET_OUTPUT_VALUE,
    },
    BuiltinSignatureDescriptor {
        label: "value = get(h, property)",
        inputs: &GET_INPUTS_HANDLE_PROPERTY,
        outputs: &GET_OUTPUT_VALUE,
    },
];

const GET_ERROR_INVALID_ARGUMENT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
    code: "RM.GET.INVALID_ARGUMENT",
    identifier: Some("RunMat:get:InvalidArgument"),
    when:
        "Handle value or property selector is missing/invalid, or property lookup fails validation.",
    message: "get: invalid argument",
};

const GET_ERROR_INTERNAL: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
    code: "RM.GET.INTERNAL",
    identifier: Some("RunMat:get:Internal"),
    when: "Internal handle/property retrieval fails unexpectedly.",
    message: "get: internal operation failed",
};

const GET_ERRORS: [BuiltinErrorDescriptor; 2] = [GET_ERROR_INVALID_ARGUMENT, GET_ERROR_INTERNAL];

pub const GET_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
    signatures: &GET_SIGNATURES,
    output_mode: BuiltinOutputMode::Fixed,
    completion_policy: BuiltinCompletionPolicy::Public,
    errors: &GET_ERRORS,
};

fn get_error_with_detail(
    error: &'static BuiltinErrorDescriptor,
    detail: impl AsRef<str>,
) -> RuntimeError {
    let mut builder = build_runtime_error(format!("{}: {}", error.message, detail.as_ref()))
        .with_builtin(BUILTIN_NAME);
    if let Some(identifier) = error.identifier {
        builder = builder.with_identifier(identifier);
    }
    builder.build()
}

fn map_get_error(err: RuntimeError) -> RuntimeError {
    if err.identifier().is_some() {
        return err;
    }
    get_error_with_detail(&GET_ERROR_INVALID_ARGUMENT, err.message)
}

#[runtime_builtin(
    name = "get",
    category = "plotting",
    summary = "Get graphics object properties.",
    keywords = "get,plotting,handle,property",
    type_resolver(get_type),
    descriptor(crate::builtins::plotting::get::GET_DESCRIPTOR),
    builtin_path = "crate::builtins::plotting::get"
)]
pub fn get_builtin(args: Vec<Value>) -> crate::BuiltinResult<Value> {
    if args.is_empty() {
        return Err(get_error_with_detail(
            &GET_ERROR_INVALID_ARGUMENT,
            "expected a plotting handle",
        ));
    }
    let handle = resolve_plot_handle(&args[0], BUILTIN_NAME).map_err(map_get_error)?;
    let property = args
        .get(1)
        .and_then(|v| crate::builtins::plotting::style::value_as_string(v));
    get_properties(handle, property.as_deref(), BUILTIN_NAME).map_err(map_get_error)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::builtins::plotting::isgraphics::isgraphics_builtin;
    use crate::builtins::plotting::ishandle::ishandle_builtin;
    use crate::builtins::plotting::legend::legend_builtin;
    use crate::builtins::plotting::tests::{ensure_plot_test_env, lock_plot_registry};
    use crate::builtins::plotting::title::title_builtin;
    use crate::builtins::plotting::{clear_figure, reset_hold_state_for_run};
    use runmat_builtins::Value;
    use runmat_plot::plots::{Figure, LinePlot};

    fn setup() -> crate::builtins::plotting::state::PlotTestLockGuard {
        let guard = lock_plot_registry();
        ensure_plot_test_env();
        reset_hold_state_for_run();
        let _ = clear_figure(None);
        guard
    }

    #[test]
    fn get_descriptor_signatures_cover_core_forms() {
        let labels: Vec<&str> = GET_DESCRIPTOR
            .signatures
            .iter()
            .map(|sig| sig.label)
            .collect();
        assert!(labels.contains(&"value = get(h)"));
        assert!(labels.contains(&"value = get(h, property)"));
    }

    #[test]
    fn get_missing_handle_uses_stable_identifier() {
        let _guard = setup();
        let err = get_builtin(vec![]).expect_err("expected missing handle to fail");
        assert_eq!(err.identifier(), GET_ERROR_INVALID_ARGUMENT.identifier);
    }

    #[test]
    fn get_reads_text_handle_properties() {
        let _guard = setup();
        let h = title_builtin(vec![
            Value::String("Signal".into()),
            Value::String("FontSize".into()),
            Value::Num(16.0),
        ])
        .unwrap();

        let value = get_builtin(vec![Value::Num(h)]).unwrap();
        let Value::Struct(st) = value else {
            panic!("expected struct");
        };
        assert_eq!(
            st.fields.get("String"),
            Some(&Value::String("Signal".into()))
        );
        assert_eq!(st.fields.get("FontSize"), Some(&Value::Num(16.0)));

        let string_value =
            get_builtin(vec![Value::Num(h), Value::String("String".into())]).unwrap();
        assert_eq!(string_value, Value::String("Signal".into()));

        let _ = crate::builtins::plotting::set::set_builtin(vec![
            Value::Num(h),
            Value::String("FontWeight".into()),
            Value::String("bold".into()),
        ])
        .unwrap();
        let weight = get_builtin(vec![Value::Num(h), Value::String("FontWeight".into())]).unwrap();
        assert_eq!(weight, Value::String("bold".into()));

        let _ = crate::builtins::plotting::set::set_builtin(vec![
            Value::Num(h),
            Value::String("String".into()),
            Value::StringArray(runmat_builtins::StringArray {
                data: vec!["Top".into(), "Bottom".into()],
                shape: vec![1, 2],
                rows: 1,
                cols: 2,
            }),
        ])
        .unwrap();
        let multiline = get_builtin(vec![Value::Num(h), Value::String("String".into())]).unwrap();
        assert!(matches!(multiline, Value::StringArray(_)));
    }

    #[test]
    fn get_reads_axes_and_legend_properties() {
        let _guard = setup();
        let mut figure = Figure::new();
        figure.add_line_plot(
            LinePlot::new(vec![0.0, 1.0], vec![1.0, 2.0])
                .unwrap()
                .with_label("A"),
        );
        let handle = crate::builtins::plotting::import_figure(figure);
        let ax = crate::builtins::plotting::state::encode_axes_handle(handle, 0);
        let legend = legend_builtin(vec![Value::Num(ax)]).unwrap();

        let axes_value = get_builtin(vec![Value::Num(ax)]).unwrap();
        assert!(matches!(axes_value, Value::Struct(_)));
        let title_handle =
            get_builtin(vec![Value::Num(ax), Value::String("Title".into())]).unwrap();
        assert!(matches!(title_handle, Value::Num(_)));

        let legend_value = get_builtin(vec![Value::Num(legend)]).unwrap();
        let Value::Struct(legend_struct) = legend_value else {
            panic!("expected struct");
        };
        assert!(legend_struct.fields.contains_key("String"));
        assert!(legend_struct.fields.contains_key("Visible"));

        let visible =
            get_builtin(vec![Value::Num(ax), Value::String("LegendVisible".into())]).unwrap();
        assert_eq!(visible, Value::Bool(true));

        let _ = crate::builtins::plotting::set::set_builtin(vec![
            Value::Num(legend),
            Value::String("Orientation".into()),
            Value::String("horizontal".into()),
        ])
        .unwrap();
        let orientation = get_builtin(vec![
            Value::Num(legend),
            Value::String("Orientation".into()),
        ])
        .unwrap();
        assert_eq!(orientation, Value::String("horizontal".into()));

        let _ = crate::builtins::plotting::set::set_builtin(vec![
            Value::Num(legend),
            Value::String("Box".into()),
            Value::Bool(false),
        ])
        .unwrap();
        let box_value = get_builtin(vec![Value::Num(legend), Value::String("Box".into())]).unwrap();
        assert_eq!(box_value, Value::Bool(false));
    }

    #[test]
    fn get_reads_axes_local_limit_and_scale_properties() {
        let _guard = setup();
        let ax = crate::builtins::plotting::subplot::subplot_builtin(
            Value::Num(1.0),
            Value::Num(2.0),
            Value::Num(2.0),
        )
        .unwrap();
        crate::builtins::plotting::set::set_builtin(vec![
            Value::Num(ax),
            Value::String("XLim".into()),
            Value::Tensor(runmat_builtins::Tensor {
                rows: 1,
                cols: 2,
                shape: vec![1, 2],
                data: vec![1.0, 5.0],
                dtype: runmat_builtins::NumericDType::F64,
            }),
            Value::String("XScale".into()),
            Value::String("log".into()),
            Value::String("Grid".into()),
            Value::Bool(false),
        ])
        .unwrap();
        let xlim = get_builtin(vec![Value::Num(ax), Value::String("XLim".into())]).unwrap();
        let scale = get_builtin(vec![Value::Num(ax), Value::String("XScale".into())]).unwrap();
        let grid = get_builtin(vec![Value::Num(ax), Value::String("Grid".into())]).unwrap();
        assert_eq!(
            runmat_builtins::Tensor::try_from(&xlim).unwrap().data,
            vec![1.0, 5.0]
        );
        assert_eq!(scale, Value::String("log".into()));
        assert_eq!(grid, Value::Bool(false));
    }

    #[test]
    fn get_reads_figure_properties() {
        let _guard = setup();
        let fig =
            crate::builtins::plotting::figure::figure_builtin(vec![Value::Num(1234.0)]).unwrap();
        let ax = crate::builtins::plotting::subplot::subplot_builtin(
            Value::Num(1.0),
            Value::Num(2.0),
            Value::Num(2.0),
        )
        .unwrap();
        let value = get_builtin(vec![Value::Num(fig)]).unwrap();
        let Value::Struct(st) = value else {
            panic!("expected struct");
        };
        assert_eq!(st.fields.get("Number"), Some(&Value::Num(1234.0)));
        assert_eq!(st.fields.get("Type"), Some(&Value::String("figure".into())));
        assert_eq!(st.fields.get("CurrentAxes"), Some(&Value::Num(ax)));
        assert!(matches!(st.fields.get("Children"), Some(Value::Tensor(_))));
        let parent = st.fields.get("Parent").expect("parent property");
        let Value::Num(v) = parent else {
            panic!("expected numeric parent");
        };
        assert!(v.is_nan());
        let sgtitle = get_builtin(vec![Value::Num(fig), Value::String("SGTitle".into())]).unwrap();
        assert!(matches!(sgtitle, Value::Num(_)));
    }

    #[test]
    fn hierarchy_and_query_semantics_exist() {
        let _guard = setup();
        let fig =
            crate::builtins::plotting::figure::figure_builtin(vec![Value::Num(5555.0)]).unwrap();
        let ax = crate::builtins::plotting::subplot::subplot_builtin(
            Value::Num(1.0),
            Value::Num(1.0),
            Value::Num(1.0),
        )
        .unwrap();
        let title =
            crate::builtins::plotting::title::title_builtin(vec![Value::String("Signal".into())])
                .unwrap();

        let axes_value = get_builtin(vec![Value::Num(ax)]).unwrap();
        let Value::Struct(axes_struct) = axes_value else {
            panic!("expected struct");
        };
        assert_eq!(
            axes_struct.fields.get("Type"),
            Some(&Value::String("axes".into()))
        );
        assert_eq!(axes_struct.fields.get("Parent"), Some(&Value::Num(fig)));
        assert!(matches!(
            axes_struct.fields.get("Children"),
            Some(Value::Tensor(_))
        ));

        let text_value = get_builtin(vec![Value::Num(title)]).unwrap();
        let Value::Struct(text_struct) = text_value else {
            panic!("expected struct");
        };
        assert_eq!(
            text_struct.fields.get("Type"),
            Some(&Value::String("text".into()))
        );
        assert_eq!(text_struct.fields.get("Parent"), Some(&Value::Num(ax)));

        assert!(ishandle_builtin(vec![Value::Num(fig)]).unwrap());
        assert!(isgraphics_builtin(vec![Value::Num(ax)]).unwrap());
        assert!(!ishandle_builtin(vec![Value::Num(-1.0)]).unwrap());
    }

    #[test]
    fn stem_handle_exposes_runtime_properties() {
        let _guard = setup();
        let handle = crate::builtins::plotting::stem::stem_builtin(vec![
            Value::Tensor(runmat_builtins::Tensor {
                rows: 2,
                cols: 1,
                shape: vec![2],
                data: vec![1.0, 2.0],
                dtype: runmat_builtins::NumericDType::F64,
            }),
            Value::String("DisplayName".into()),
            Value::String("Impulse".into()),
            Value::String("BaseValue".into()),
            Value::Num(-1.0),
            Value::String("filled".into()),
        ])
        .unwrap();
        let props = get_builtin(vec![Value::Num(handle)]).unwrap();
        let Value::Struct(st) = props else {
            panic!("expected struct");
        };
        assert_eq!(st.fields.get("Type"), Some(&Value::String("stem".into())));
        assert_eq!(st.fields.get("BaseValue"), Some(&Value::Num(-1.0)));
        assert_eq!(st.fields.get("Filled"), Some(&Value::Bool(true)));
    }
}