Skip to main content

runmat_runtime/builtins/structs/core/
fieldnames.rs

1//! MATLAB-compatible `fieldnames` builtin.
2
3use crate::builtins::common::spec::{
4    BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
5    ReductionNaN, ResidencyPolicy, ShapeRequirements,
6};
7use crate::builtins::structs::type_resolvers::fieldnames_type;
8use runmat_builtins::{
9    CellArray, CharArray, HandleRef, Listener, ObjectInstance, StructValue, Value,
10};
11use runmat_macros::runtime_builtin;
12use std::collections::BTreeSet;
13
14use crate::{build_runtime_error, BuiltinResult, RuntimeError};
15
16#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::structs::core::fieldnames")]
17pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
18    name: "fieldnames",
19    op_kind: GpuOpKind::Custom("fieldnames"),
20    supported_precisions: &[],
21    broadcast: BroadcastSemantics::None,
22    provider_hooks: &[],
23    constant_strategy: ConstantStrategy::InlineLiteral,
24    residency: ResidencyPolicy::InheritInputs,
25    nan_mode: ReductionNaN::Include,
26    two_pass_threshold: None,
27    workgroup_size: None,
28    accepts_nan_mode: false,
29    notes: "Host-only introspection; providers do not participate.",
30};
31
32#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::structs::core::fieldnames")]
33pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
34    name: "fieldnames",
35    shape: ShapeRequirements::Any,
36    constant_strategy: ConstantStrategy::InlineLiteral,
37    elementwise: None,
38    reduction: None,
39    emits_nan: false,
40    notes: "Fusion planner treats fieldnames as a host inspector; it terminates any pending fusion group.",
41};
42
43fn fieldnames_flow(message: impl Into<String>) -> RuntimeError {
44    build_runtime_error(message)
45        .with_builtin("fieldnames")
46        .build()
47}
48
49#[runtime_builtin(
50    name = "fieldnames",
51    category = "structs/core",
52    summary = "List the field names of scalar structs or struct arrays.",
53    keywords = "fieldnames,struct,introspection,fields",
54    type_resolver(fieldnames_type),
55    builtin_path = "crate::builtins::structs::core::fieldnames"
56)]
57async fn fieldnames_builtin(value: Value) -> BuiltinResult<Value> {
58    let names = match &value {
59        Value::Struct(st) => collect_struct_fieldnames(st),
60        Value::Cell(cell) => collect_struct_array_fieldnames(cell)?,
61        Value::Object(obj) => collect_object_fieldnames(obj),
62        Value::HandleObject(handle) => collect_handle_fieldnames(handle)?,
63        Value::Listener(listener) => collect_listener_fieldnames(listener),
64        other => {
65            return Err(fieldnames_flow(format!(
66                "fieldnames: expected struct, struct array, or object (got {other:?})"
67            )))
68        }
69    };
70
71    let rows = names.len();
72    let cells: Vec<Value> = names
73        .into_iter()
74        .map(|name| Value::CharArray(CharArray::new_row(&name)))
75        .collect();
76    crate::make_cell(cells, rows, 1).map_err(|e| fieldnames_flow(format!("fieldnames: {e}")))
77}
78
79fn collect_struct_fieldnames(st: &StructValue) -> Vec<String> {
80    let mut names: Vec<String> = st.fields.keys().cloned().collect();
81    names.sort();
82    names
83}
84
85fn collect_struct_array_fieldnames(array: &CellArray) -> BuiltinResult<Vec<String>> {
86    let mut names = BTreeSet::new();
87    for handle in array.data.iter() {
88        let value = unsafe { &*handle.as_raw() };
89        let Value::Struct(st) = value else {
90            return Err(fieldnames_flow(
91                "fieldnames: expected struct array contents to be structs",
92            ));
93        };
94        names.extend(st.fields.keys().cloned());
95    }
96    Ok(names.into_iter().collect())
97}
98
99fn collect_object_fieldnames(obj: &ObjectInstance) -> Vec<String> {
100    let mut names = class_instance_property_names(&obj.class_name);
101    names.extend(obj.properties.keys().cloned());
102    names.into_iter().collect()
103}
104
105fn collect_handle_fieldnames(handle: &HandleRef) -> BuiltinResult<Vec<String>> {
106    let mut names = class_instance_property_names(&handle.class_name);
107
108    if handle.valid {
109        let target = unsafe { &*handle.target.as_raw() };
110        match target {
111            Value::Struct(st) => {
112                names.extend(collect_struct_fieldnames(st));
113            }
114            Value::Cell(array) => {
115                names.extend(collect_struct_array_fieldnames(array)?);
116            }
117            Value::Object(obj) => {
118                names.extend(collect_object_fieldnames(obj));
119            }
120            Value::Listener(listener) => {
121                names.extend(collect_listener_fieldnames(listener));
122            }
123            Value::HandleObject(other) => {
124                names.extend(class_instance_property_names(&other.class_name));
125            }
126            _ => {}
127        }
128    }
129
130    Ok(names.into_iter().collect())
131}
132
133fn collect_listener_fieldnames(_listener: &Listener) -> Vec<String> {
134    let mut names = vec![
135        "callback".to_string(),
136        "enabled".to_string(),
137        "event_name".to_string(),
138        "id".to_string(),
139        "target".to_string(),
140        "valid".to_string(),
141    ];
142    names.sort();
143    names
144}
145
146fn class_instance_property_names(class_name: &str) -> BTreeSet<String> {
147    let mut names = BTreeSet::new();
148    if let Some(class_def) = runmat_builtins::get_class(class_name) {
149        for (name, prop) in &class_def.properties {
150            if !prop.is_static {
151                names.insert(name.clone());
152            }
153        }
154    }
155    names
156}
157
158#[cfg(test)]
159pub(crate) mod tests {
160    use super::*;
161    use runmat_builtins::{
162        Access, CellArray, ClassDef, HandleRef, ObjectInstance, PropertyDef, StructValue, Value,
163    };
164    use std::collections::HashMap;
165
166    fn error_message(err: crate::RuntimeError) -> String {
167        err.message().to_string()
168    }
169
170    fn run_fieldnames(value: Value) -> BuiltinResult<Value> {
171        futures::executor::block_on(fieldnames_builtin(value))
172    }
173
174    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
175    #[test]
176    fn fieldnames_returns_sorted_names_for_scalar_struct() {
177        let mut fields = StructValue::new();
178        fields.fields.insert("beta".to_string(), Value::Num(1.0));
179        fields.fields.insert("alpha".to_string(), Value::Num(2.0));
180        let result = run_fieldnames(Value::Struct(fields)).expect("fieldnames");
181        let Value::Cell(cell) = result else {
182            panic!("expected cell array result");
183        };
184        assert_eq!(cell.cols, 1);
185        assert_eq!(cell.rows, 2);
186        let collected = cell_strings(&cell);
187        assert_eq!(collected, vec!["alpha".to_string(), "beta".to_string()]);
188    }
189
190    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
191    #[test]
192    fn fieldnames_struct_array_collects_union() {
193        let mut first = StructValue::new();
194        first
195            .fields
196            .insert("name".to_string(), Value::from("Ada".to_string()));
197        first.fields.insert("id".to_string(), Value::Num(101.0));
198
199        let mut second = StructValue::new();
200        second
201            .fields
202            .insert("name".to_string(), Value::from("Grace".to_string()));
203        second
204            .fields
205            .insert("department".to_string(), Value::from("Research"));
206
207        let cell = CellArray::new_with_shape(
208            vec![Value::Struct(first), Value::Struct(second)],
209            vec![1, 2],
210        )
211        .expect("struct array");
212
213        let result = run_fieldnames(Value::Cell(cell)).expect("fieldnames");
214        let Value::Cell(names) = result else {
215            panic!("expected cell array result");
216        };
217        assert_eq!(names.cols, 1);
218        assert_eq!(names.rows, 3);
219        let collected = cell_strings(&names);
220        assert_eq!(
221            collected,
222            vec![
223                "department".to_string(),
224                "id".to_string(),
225                "name".to_string()
226            ]
227        );
228    }
229
230    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
231    #[test]
232    fn fieldnames_errors_for_non_struct_inputs() {
233        let err = error_message(run_fieldnames(Value::Num(1.0)).unwrap_err());
234        assert!(
235            err.contains("expected struct, struct array, or object"),
236            "unexpected error message: {err}"
237        );
238    }
239
240    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
241    #[test]
242    fn fieldnames_handles_empty_struct_array() {
243        let empty_array = CellArray::new(Vec::new(), 0, 0).expect("empty struct array backing");
244        let result = run_fieldnames(Value::Cell(empty_array)).expect("fieldnames");
245        let Value::Cell(cell) = result else {
246            panic!("expected cell array");
247        };
248        assert_eq!(cell.rows, 0);
249        assert_eq!(cell.cols, 1);
250    }
251
252    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
253    #[test]
254    fn fieldnames_cell_without_struct_errors() {
255        let cell = CellArray::new(vec![Value::Num(1.0)], 1, 1).expect("cell");
256        let err = error_message(run_fieldnames(Value::Cell(cell)).unwrap_err());
257        assert!(
258            err.contains("expected struct array contents to be structs"),
259            "unexpected error message: {err}"
260        );
261    }
262
263    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
264    #[test]
265    fn fieldnames_preserves_case_distinctions() {
266        let mut fields = StructValue::new();
267        fields.fields.insert("name".to_string(), Value::Num(1.0));
268        fields.fields.insert("Name".to_string(), Value::Num(2.0));
269        let Value::Cell(cell) = run_fieldnames(Value::Struct(fields)).expect("fieldnames") else {
270            panic!("expected cell array result");
271        };
272        let collected = cell_strings(&cell);
273        assert_eq!(collected, vec!["Name".to_string(), "name".to_string()]);
274    }
275
276    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
277    #[test]
278    fn fieldnames_object_includes_class_and_dynamic_properties() {
279        let class_name = "runmat.unittest.FieldnamesObject";
280        let mut def = ClassDef {
281            name: class_name.to_string(),
282            parent: None,
283            properties: HashMap::new(),
284            methods: HashMap::new(),
285        };
286        def.properties.insert(
287            "Value".to_string(),
288            PropertyDef {
289                name: "Value".to_string(),
290                is_static: false,
291                is_dependent: false,
292                get_access: Access::Public,
293                set_access: Access::Public,
294                default_value: None,
295            },
296        );
297        def.properties.insert(
298            "Version".to_string(),
299            PropertyDef {
300                name: "Version".to_string(),
301                is_static: true,
302                is_dependent: false,
303                get_access: Access::Public,
304                set_access: Access::Public,
305                default_value: None,
306            },
307        );
308        runmat_builtins::register_class(def);
309
310        let mut obj = ObjectInstance::new(class_name.to_string());
311        obj.properties.insert("Step".to_string(), Value::Num(2.0));
312
313        let Value::Cell(cell) = run_fieldnames(Value::Object(obj)).expect("fieldnames object")
314        else {
315            panic!("expected cell array");
316        };
317        let collected = cell_strings(&cell);
318        assert_eq!(collected, vec!["Step".to_string(), "Value".to_string()]);
319    }
320
321    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
322    #[test]
323    fn fieldnames_handle_object_merges_class_and_target() {
324        let class_name = "runmat.unittest.FieldnamesHandle";
325        let mut def = ClassDef {
326            name: class_name.to_string(),
327            parent: None,
328            properties: HashMap::new(),
329            methods: HashMap::new(),
330        };
331        def.properties.insert(
332            "Enabled".to_string(),
333            PropertyDef {
334                name: "Enabled".to_string(),
335                is_static: false,
336                is_dependent: false,
337                get_access: Access::Public,
338                set_access: Access::Public,
339                default_value: None,
340            },
341        );
342        runmat_builtins::register_class(def);
343
344        let mut payload = StructValue::new();
345        payload
346            .fields
347            .insert("Status".to_string(), Value::from("ready"));
348        let target = unsafe {
349            runmat_gc_api::GcPtr::from_raw(Box::into_raw(Box::new(Value::Struct(payload))))
350        };
351
352        let handle = HandleRef {
353            class_name: class_name.to_string(),
354            target,
355            valid: true,
356        };
357
358        let Value::Cell(cell) =
359            run_fieldnames(Value::HandleObject(handle)).expect("fieldnames handle")
360        else {
361            panic!("expected cell array");
362        };
363        let collected = cell_strings(&cell);
364        assert_eq!(collected, vec!["Enabled".to_string(), "Status".to_string()]);
365    }
366
367    fn cell_strings(cell: &CellArray) -> Vec<String> {
368        cell.data
369            .iter()
370            .map(|ptr| match unsafe { &*ptr.as_raw() } {
371                Value::CharArray(ca) => ca.data.iter().collect(),
372                other => panic!("expected character array cell element, got {other:?}"),
373            })
374            .collect()
375    }
376}