runmat_runtime/builtins/structs/core/
fieldnames.rs1use 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}