runmat-runtime 0.4.1

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

use crate::builtins::common::spec::{
    BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
    ReductionNaN, ResidencyPolicy, ShapeRequirements,
};
use crate::builtins::structs::type_resolvers::fieldnames_type;
use runmat_builtins::{
    CellArray, CharArray, HandleRef, Listener, ObjectInstance, StructValue, Value,
};
use runmat_macros::runtime_builtin;
use std::collections::BTreeSet;

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

#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::structs::core::fieldnames")]
pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
    name: "fieldnames",
    op_kind: GpuOpKind::Custom("fieldnames"),
    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: "Host-only introspection; providers do not participate.",
};

#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::structs::core::fieldnames")]
pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
    name: "fieldnames",
    shape: ShapeRequirements::Any,
    constant_strategy: ConstantStrategy::InlineLiteral,
    elementwise: None,
    reduction: None,
    emits_nan: false,
    notes: "Fusion planner treats fieldnames as a host inspector; it terminates any pending fusion group.",
};

fn fieldnames_flow(message: impl Into<String>) -> RuntimeError {
    build_runtime_error(message)
        .with_builtin("fieldnames")
        .build()
}

#[runtime_builtin(
    name = "fieldnames",
    category = "structs/core",
    summary = "List the field names of scalar structs or struct arrays.",
    keywords = "fieldnames,struct,introspection,fields",
    type_resolver(fieldnames_type),
    builtin_path = "crate::builtins::structs::core::fieldnames"
)]
async fn fieldnames_builtin(value: Value) -> BuiltinResult<Value> {
    let names = match &value {
        Value::Struct(st) => collect_struct_fieldnames(st),
        Value::Cell(cell) => collect_struct_array_fieldnames(cell)?,
        Value::Object(obj) => collect_object_fieldnames(obj),
        Value::HandleObject(handle) => collect_handle_fieldnames(handle)?,
        Value::Listener(listener) => collect_listener_fieldnames(listener),
        other => {
            return Err(fieldnames_flow(format!(
                "fieldnames: expected struct, struct array, or object (got {other:?})"
            )))
        }
    };

    let rows = names.len();
    let cells: Vec<Value> = names
        .into_iter()
        .map(|name| Value::CharArray(CharArray::new_row(&name)))
        .collect();
    crate::make_cell(cells, rows, 1).map_err(|e| fieldnames_flow(format!("fieldnames: {e}")))
}

fn collect_struct_fieldnames(st: &StructValue) -> Vec<String> {
    let mut names: Vec<String> = st.fields.keys().cloned().collect();
    names.sort();
    names
}

fn collect_struct_array_fieldnames(array: &CellArray) -> BuiltinResult<Vec<String>> {
    let mut names = BTreeSet::new();
    for handle in array.data.iter() {
        let value = unsafe { &*handle.as_raw() };
        let Value::Struct(st) = value else {
            return Err(fieldnames_flow(
                "fieldnames: expected struct array contents to be structs",
            ));
        };
        names.extend(st.fields.keys().cloned());
    }
    Ok(names.into_iter().collect())
}

fn collect_object_fieldnames(obj: &ObjectInstance) -> Vec<String> {
    let mut names = class_instance_property_names(&obj.class_name);
    names.extend(obj.properties.keys().cloned());
    names.into_iter().collect()
}

fn collect_handle_fieldnames(handle: &HandleRef) -> BuiltinResult<Vec<String>> {
    let mut names = class_instance_property_names(&handle.class_name);

    if handle.valid {
        let target = unsafe { &*handle.target.as_raw() };
        match target {
            Value::Struct(st) => {
                names.extend(collect_struct_fieldnames(st));
            }
            Value::Cell(array) => {
                names.extend(collect_struct_array_fieldnames(array)?);
            }
            Value::Object(obj) => {
                names.extend(collect_object_fieldnames(obj));
            }
            Value::Listener(listener) => {
                names.extend(collect_listener_fieldnames(listener));
            }
            Value::HandleObject(other) => {
                names.extend(class_instance_property_names(&other.class_name));
            }
            _ => {}
        }
    }

    Ok(names.into_iter().collect())
}

fn collect_listener_fieldnames(_listener: &Listener) -> Vec<String> {
    let mut names = vec![
        "callback".to_string(),
        "enabled".to_string(),
        "event_name".to_string(),
        "id".to_string(),
        "target".to_string(),
        "valid".to_string(),
    ];
    names.sort();
    names
}

fn class_instance_property_names(class_name: &str) -> BTreeSet<String> {
    let mut names = BTreeSet::new();
    if let Some(class_def) = runmat_builtins::get_class(class_name) {
        for (name, prop) in &class_def.properties {
            if !prop.is_static {
                names.insert(name.clone());
            }
        }
    }
    names
}

#[cfg(test)]
pub(crate) mod tests {
    use super::*;
    use runmat_builtins::{
        Access, CellArray, ClassDef, HandleRef, ObjectInstance, PropertyDef, StructValue, Value,
    };
    use std::collections::HashMap;

    fn error_message(err: crate::RuntimeError) -> String {
        err.message().to_string()
    }

    fn run_fieldnames(value: Value) -> BuiltinResult<Value> {
        futures::executor::block_on(fieldnames_builtin(value))
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn fieldnames_returns_sorted_names_for_scalar_struct() {
        let mut fields = StructValue::new();
        fields.fields.insert("beta".to_string(), Value::Num(1.0));
        fields.fields.insert("alpha".to_string(), Value::Num(2.0));
        let result = run_fieldnames(Value::Struct(fields)).expect("fieldnames");
        let Value::Cell(cell) = result else {
            panic!("expected cell array result");
        };
        assert_eq!(cell.cols, 1);
        assert_eq!(cell.rows, 2);
        let collected = cell_strings(&cell);
        assert_eq!(collected, vec!["alpha".to_string(), "beta".to_string()]);
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn fieldnames_struct_array_collects_union() {
        let mut first = StructValue::new();
        first
            .fields
            .insert("name".to_string(), Value::from("Ada".to_string()));
        first.fields.insert("id".to_string(), Value::Num(101.0));

        let mut second = StructValue::new();
        second
            .fields
            .insert("name".to_string(), Value::from("Grace".to_string()));
        second
            .fields
            .insert("department".to_string(), Value::from("Research"));

        let cell = CellArray::new_with_shape(
            vec![Value::Struct(first), Value::Struct(second)],
            vec![1, 2],
        )
        .expect("struct array");

        let result = run_fieldnames(Value::Cell(cell)).expect("fieldnames");
        let Value::Cell(names) = result else {
            panic!("expected cell array result");
        };
        assert_eq!(names.cols, 1);
        assert_eq!(names.rows, 3);
        let collected = cell_strings(&names);
        assert_eq!(
            collected,
            vec![
                "department".to_string(),
                "id".to_string(),
                "name".to_string()
            ]
        );
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn fieldnames_errors_for_non_struct_inputs() {
        let err = error_message(run_fieldnames(Value::Num(1.0)).unwrap_err());
        assert!(
            err.contains("expected struct, struct array, or object"),
            "unexpected error message: {err}"
        );
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn fieldnames_handles_empty_struct_array() {
        let empty_array = CellArray::new(Vec::new(), 0, 0).expect("empty struct array backing");
        let result = run_fieldnames(Value::Cell(empty_array)).expect("fieldnames");
        let Value::Cell(cell) = result else {
            panic!("expected cell array");
        };
        assert_eq!(cell.rows, 0);
        assert_eq!(cell.cols, 1);
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn fieldnames_cell_without_struct_errors() {
        let cell = CellArray::new(vec![Value::Num(1.0)], 1, 1).expect("cell");
        let err = error_message(run_fieldnames(Value::Cell(cell)).unwrap_err());
        assert!(
            err.contains("expected struct array contents to be structs"),
            "unexpected error message: {err}"
        );
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn fieldnames_preserves_case_distinctions() {
        let mut fields = StructValue::new();
        fields.fields.insert("name".to_string(), Value::Num(1.0));
        fields.fields.insert("Name".to_string(), Value::Num(2.0));
        let Value::Cell(cell) = run_fieldnames(Value::Struct(fields)).expect("fieldnames") else {
            panic!("expected cell array result");
        };
        let collected = cell_strings(&cell);
        assert_eq!(collected, vec!["Name".to_string(), "name".to_string()]);
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn fieldnames_object_includes_class_and_dynamic_properties() {
        let class_name = "runmat.unittest.FieldnamesObject";
        let mut def = ClassDef {
            name: class_name.to_string(),
            parent: None,
            properties: HashMap::new(),
            methods: HashMap::new(),
        };
        def.properties.insert(
            "Value".to_string(),
            PropertyDef {
                name: "Value".to_string(),
                is_static: false,
                is_dependent: false,
                get_access: Access::Public,
                set_access: Access::Public,
                default_value: None,
            },
        );
        def.properties.insert(
            "Version".to_string(),
            PropertyDef {
                name: "Version".to_string(),
                is_static: true,
                is_dependent: false,
                get_access: Access::Public,
                set_access: Access::Public,
                default_value: None,
            },
        );
        runmat_builtins::register_class(def);

        let mut obj = ObjectInstance::new(class_name.to_string());
        obj.properties.insert("Step".to_string(), Value::Num(2.0));

        let Value::Cell(cell) = run_fieldnames(Value::Object(obj)).expect("fieldnames object")
        else {
            panic!("expected cell array");
        };
        let collected = cell_strings(&cell);
        assert_eq!(collected, vec!["Step".to_string(), "Value".to_string()]);
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn fieldnames_handle_object_merges_class_and_target() {
        let class_name = "runmat.unittest.FieldnamesHandle";
        let mut def = ClassDef {
            name: class_name.to_string(),
            parent: None,
            properties: HashMap::new(),
            methods: HashMap::new(),
        };
        def.properties.insert(
            "Enabled".to_string(),
            PropertyDef {
                name: "Enabled".to_string(),
                is_static: false,
                is_dependent: false,
                get_access: Access::Public,
                set_access: Access::Public,
                default_value: None,
            },
        );
        runmat_builtins::register_class(def);

        let mut payload = StructValue::new();
        payload
            .fields
            .insert("Status".to_string(), Value::from("ready"));
        let target = unsafe {
            runmat_gc_api::GcPtr::from_raw(Box::into_raw(Box::new(Value::Struct(payload))))
        };

        let handle = HandleRef {
            class_name: class_name.to_string(),
            target,
            valid: true,
        };

        let Value::Cell(cell) =
            run_fieldnames(Value::HandleObject(handle)).expect("fieldnames handle")
        else {
            panic!("expected cell array");
        };
        let collected = cell_strings(&cell);
        assert_eq!(collected, vec!["Enabled".to_string(), "Status".to_string()]);
    }

    fn cell_strings(cell: &CellArray) -> Vec<String> {
        cell.data
            .iter()
            .map(|ptr| match unsafe { &*ptr.as_raw() } {
                Value::CharArray(ca) => ca.data.iter().collect(),
                other => panic!("expected character array cell element, got {other:?}"),
            })
            .collect()
    }
}