runmat-runtime 0.6.0

Core runtime for RunMat with builtins, BLAS/LAPACK integration, and execution APIs
Documentation
//! MATLAB-compatible `superclasses` builtin backed by RunMat class metadata.

use crate::builtins::common::spec::{
    BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
    ReductionNaN, ResidencyPolicy, ShapeRequirements,
};
use crate::builtins::introspection::class::class_name_for_value;
use crate::builtins::introspection::type_resolvers::superclasses_type;
use crate::{build_runtime_error, BuiltinResult, RuntimeError};
use runmat_builtins::{
    superclass_chain, BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor,
    BuiltinOutputMode, BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType,
    BuiltinSignatureDescriptor, CellArray, CharArray, Value,
};
use runmat_macros::runtime_builtin;

#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::introspection::superclasses")]
pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
    name: "superclasses",
    op_kind: GpuOpKind::Custom("metadata"),
    supported_precisions: &[],
    broadcast: BroadcastSemantics::None,
    provider_hooks: &[],
    constant_strategy: ConstantStrategy::InlineLiteral,
    residency: ResidencyPolicy::InheritInputs,
    nan_mode: ReductionNaN::Include,
    two_pass_threshold: None,
    workgroup_size: None,
    accepts_nan_mode: false,
    notes: "Metadata-only class hierarchy query. gpuArray inputs stay resident while RunMat checks host class metadata.",
};

#[runmat_macros::register_fusion_spec(
    builtin_path = "crate::builtins::introspection::superclasses"
)]
pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
    name: "superclasses",
    shape: ShapeRequirements::Any,
    constant_strategy: ConstantStrategy::InlineLiteral,
    elementwise: None,
    reduction: None,
    emits_nan: false,
    notes: "Not eligible for fusion planning; superclasses executes on the host and returns cell metadata.",
};

const SUPERCLASSES_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
    name: "s",
    ty: BuiltinParamType::Any,
    arity: BuiltinParamArity::Required,
    default: None,
    description: "Cell row of visible superclass names as character vectors.",
}];

const SUPERCLASSES_INPUTS: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
    name: "obj_or_class",
    ty: BuiltinParamType::Any,
    arity: BuiltinParamArity::Required,
    default: None,
    description: "Object instance or class name string/character vector.",
}];

const SUPERCLASSES_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
    label: "s = superclasses(obj_or_class)",
    inputs: &SUPERCLASSES_INPUTS,
    outputs: &SUPERCLASSES_OUTPUT,
}];

const BUILTIN_NAME: &str = "superclasses";

const SUPERCLASSES_ERROR_CLASS_INVALID: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
    code: "RM.SUPERCLASSES.CLASS_INVALID",
    identifier: Some("RunMat:SuperclassesClassInvalid"),
    when: "The argument is neither an object nor a class-name string scalar/row character vector.",
    message: "superclasses: input must be an object or class name",
};

const SUPERCLASSES_ERROR_CLASS_UNKNOWN: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
    code: "RM.SUPERCLASSES.CLASS_UNKNOWN",
    identifier: Some("RunMat:SuperclassesClassUnknown"),
    when: "The requested class is not present in RunMat class metadata.",
    message: "superclasses: class metadata not found",
};

const SUPERCLASSES_ERRORS: [BuiltinErrorDescriptor; 2] = [
    SUPERCLASSES_ERROR_CLASS_INVALID,
    SUPERCLASSES_ERROR_CLASS_UNKNOWN,
];

pub const SUPERCLASSES_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
    signatures: &SUPERCLASSES_SIGNATURES,
    output_mode: BuiltinOutputMode::Fixed,
    completion_policy: BuiltinCompletionPolicy::Public,
    errors: &SUPERCLASSES_ERRORS,
};

#[runtime_builtin(
    name = "superclasses",
    category = "introspection",
    summary = "Return visible superclass names for a class or object.",
    keywords = "superclasses,classdef,inheritance,object,introspection",
    accel = "metadata",
    type_resolver(superclasses_type),
    descriptor(crate::builtins::introspection::superclasses::SUPERCLASSES_DESCRIPTOR),
    builtin_path = "crate::builtins::introspection::superclasses"
)]
fn superclasses_builtin(value: Value) -> crate::BuiltinResult<Value> {
    let class_name = requested_class_name(&value)?;
    let supers = if let Some(chain) = superclass_chain(&class_name) {
        chain
    } else if known_leaf_class_without_registered_metadata(&class_name) {
        Vec::new()
    } else {
        return Err(superclasses_error_with_detail(
            &SUPERCLASSES_ERROR_CLASS_UNKNOWN,
            class_name,
        ));
    };

    let cols = supers.len();
    let cells = supers
        .into_iter()
        .map(|name| Value::CharArray(CharArray::new_row(&name)))
        .collect::<Vec<_>>();
    Ok(Value::Cell(
        CellArray::new(cells, 1, cols).map_err(superclasses_message_error)?,
    ))
}

fn requested_class_name(value: &Value) -> BuiltinResult<String> {
    match value {
        Value::String(class_name) => Ok(class_name.clone()),
        Value::StringArray(sa) if sa.data.len() == 1 => Ok(sa.data[0].clone()),
        Value::CharArray(ca) if ca.rows <= 1 => Ok(ca.data.iter().collect::<String>()),
        Value::ClassRef(class_name) => Ok(class_name.clone()),
        Value::Object(_)
        | Value::HandleObject(_)
        | Value::GpuTensor(_)
        | Value::Listener(_)
        | Value::MException(_) => Ok(class_name_for_value(value)),
        _ => Err(superclasses_error(&SUPERCLASSES_ERROR_CLASS_INVALID)),
    }
}

fn known_leaf_class_without_registered_metadata(class_name: &str) -> bool {
    matches!(
        class_name,
        "double"
            | "single"
            | "logical"
            | "char"
            | "string"
            | "cell"
            | "struct"
            | "function_handle"
            | "handle"
            | "gpuArray"
            | "meta.class"
            | "MException"
            | "event.listener"
            | "sparse"
            | "sym"
            | "int8"
            | "uint8"
            | "int16"
            | "uint16"
            | "int32"
            | "uint32"
            | "int64"
            | "uint64"
    )
}

fn superclasses_error(error: &'static BuiltinErrorDescriptor) -> RuntimeError {
    let mut builder = build_runtime_error(error.message).with_builtin(BUILTIN_NAME);
    if let Some(identifier) = error.identifier {
        builder = builder.with_identifier(identifier);
    }
    builder.build()
}

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

fn superclasses_message_error(message: String) -> RuntimeError {
    build_runtime_error(format!("superclasses: {message}"))
        .with_builtin(BUILTIN_NAME)
        .build()
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::builtins::common::test_support;
    use runmat_accelerate_api::HostTensorView;
    use runmat_builtins::{
        register_class, Access, CharArray, ClassDef, HandleRef, MethodDef, ObjectInstance,
        StringArray, Tensor,
    };
    use std::collections::HashMap;

    fn method(name: &str) -> MethodDef {
        MethodDef {
            name: name.to_string(),
            is_static: false,
            is_abstract: false,
            is_sealed: false,
            access: Access::Public,
            function_name: format!("{name}_impl"),
            implicit_class_argument: None,
        }
    }

    fn unique_class_name(label: &str) -> String {
        format!(
            "Superclasses{}{}",
            label,
            std::thread::current().name().unwrap_or("thread")
        )
    }

    fn register_hierarchy(label: &str) -> (String, String, String) {
        let grand = unique_class_name(&format!("{label}Grand"));
        let parent = unique_class_name(&format!("{label}Parent"));
        let child = unique_class_name(&format!("{label}Child"));

        register_class(ClassDef {
            name: grand.clone(),
            parent: None,
            properties: HashMap::new(),
            methods: HashMap::from([("root".to_string(), method("root"))]),
        });
        register_class(ClassDef {
            name: parent.clone(),
            parent: Some(grand.clone()),
            properties: HashMap::new(),
            methods: HashMap::from([("mid".to_string(), method("mid"))]),
        });
        register_class(ClassDef {
            name: child.clone(),
            parent: Some(parent.clone()),
            properties: HashMap::new(),
            methods: HashMap::from([("leaf".to_string(), method("leaf"))]),
        });

        (grand, parent, child)
    }

    fn cell_strings(value: Value) -> Vec<String> {
        let Value::Cell(cell) = value else {
            panic!("superclasses should return cell");
        };
        cell.data
            .into_iter()
            .map(|value| match value {
                Value::CharArray(chars) => chars.data.iter().collect::<String>(),
                other => panic!("expected char superclass name, got {other:?}"),
            })
            .collect()
    }

    fn call(value: Value) -> Vec<String> {
        cell_strings(superclasses_builtin(value).expect("superclasses"))
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn superclasses_returns_parent_chain_for_class_name_text() {
        let (grand, parent, child) = register_hierarchy("ClassName");

        assert_eq!(
            call(Value::String(child.clone())),
            vec![parent.clone(), grand.clone()]
        );
        assert_eq!(
            call(Value::CharArray(CharArray::new_row(&child))),
            vec![parent.clone(), grand.clone()]
        );
        assert_eq!(
            call(Value::StringArray(
                StringArray::new(vec![child], vec![1, 1]).expect("string scalar")
            )),
            vec![parent, grand]
        );
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn superclasses_returns_parent_chain_for_objects_and_class_refs() {
        let (grand, parent, child) = register_hierarchy("Object");
        let target = runmat_gc::gc_allocate(Value::Object(ObjectInstance::new(child.clone())))
            .expect("gc allocation");

        assert_eq!(
            call(Value::Object(ObjectInstance::new(child.clone()))),
            vec![parent.clone(), grand.clone()]
        );
        assert_eq!(
            call(Value::HandleObject(HandleRef {
                class_name: child.clone(),
                target,
                valid: true,
            })),
            vec![parent.clone(), grand.clone()]
        );
        assert_eq!(call(Value::ClassRef(child)), vec![parent, grand]);
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn superclasses_returns_empty_cell_for_leaf_builtin_classes() {
        assert!(call(Value::from("char")).is_empty());
        assert!(call(Value::Object(ObjectInstance::new("double".to_string()))).is_empty());
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn superclasses_uses_gpuarray_metadata_without_gather() {
        register_class(ClassDef {
            name: "gpuArray".to_string(),
            parent: Some("handle".to_string()),
            properties: HashMap::new(),
            methods: HashMap::new(),
        });
        register_class(ClassDef {
            name: "handle".to_string(),
            parent: None,
            properties: HashMap::new(),
            methods: HashMap::new(),
        });

        test_support::with_test_provider(|provider| {
            let tensor = Tensor::new(vec![1.0, 2.0], vec![2, 1]).expect("tensor");
            let view = HostTensorView {
                data: &tensor.data,
                shape: &tensor.shape,
            };
            let handle = provider.upload(&view).expect("upload");
            assert_eq!(call(Value::GpuTensor(handle)), vec!["handle".to_string()]);
        });
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn superclasses_handles_known_runtime_leaf_classes_without_registry_metadata() {
        assert!(call(Value::GpuTensor(test_support::with_test_provider(
            |provider| {
                let tensor = Tensor::new(vec![1.0], vec![1, 1]).expect("tensor");
                let view = HostTensorView {
                    data: &tensor.data,
                    shape: &tensor.shape,
                };
                provider.upload(&view).expect("upload")
            }
        )))
        .is_empty());
        assert!(call(Value::HandleObject(HandleRef {
            class_name: String::new(),
            target: runmat_gc::gc_allocate(Value::Num(0.0)).expect("gc allocation"),
            valid: true,
        }))
        .is_empty());
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn superclasses_errors_for_invalid_or_unknown_class_inputs() {
        let invalid = superclasses_builtin(Value::Num(1.0)).expect_err("numeric is invalid");
        assert_eq!(
            invalid.identifier.as_deref(),
            Some("RunMat:SuperclassesClassInvalid")
        );

        let invalid_string_array = superclasses_builtin(Value::StringArray(
            StringArray::new(
                vec!["ClassA".to_string(), "ClassB".to_string()],
                vec![1, 1, 2],
            )
            .expect("string array"),
        ))
        .expect_err("nonscalar string array is invalid");
        assert_eq!(
            invalid_string_array.identifier.as_deref(),
            Some("RunMat:SuperclassesClassInvalid")
        );

        let unknown =
            superclasses_builtin(Value::from("MissingRunMatClass")).expect_err("missing class");
        assert_eq!(
            unknown.identifier.as_deref(),
            Some("RunMat:SuperclassesClassUnknown")
        );
    }
}