1use crate::builtins::common::spec::{
4 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
5 ReductionNaN, ResidencyPolicy, ShapeRequirements,
6};
7#[cfg(feature = "doc_export")]
8use crate::register_builtin_doc_text;
9use crate::{register_builtin_fusion_spec, register_builtin_gpu_spec};
10use runmat_builtins::{CellArray, StringArray, StructValue, Value};
11use runmat_macros::runtime_builtin;
12use std::collections::HashSet;
13
14#[cfg(feature = "doc_export")]
15pub const DOC_MD: &str = r#"---
16title: "rmfield"
17category: "structs/core"
18keywords: ["rmfield", "remove field", "struct", "struct array", "metadata"]
19summary: "Remove one or more fields from scalar structs or struct arrays."
20references: []
21gpu_support:
22 elementwise: false
23 reduction: false
24 precisions: []
25 broadcasting: "none"
26 notes: "Runs entirely on the host; values that already live on the GPU remain device-resident."
27fusion:
28 elementwise: false
29 reduction: false
30 max_inputs: 1
31 constants: "inline"
32requires_feature: null
33tested:
34 unit: "builtins::structs::core::rmfield::tests"
35 integration: "builtins::structs::core::rmfield::tests::rmfield_struct_array_roundtrip"
36---
37
38# What does the `rmfield` function do in MATLAB / RunMat?
39`S2 = rmfield(S, name)` returns a copy of `S` with the field `name` removed. The builtin accepts
40additional field names, string arrays, or cell arrays of names to delete several fields in one call.
41
42## How does the `rmfield` function behave in MATLAB / RunMat?
43- Works with scalar structs and struct arrays created by `struct`, `load`, or other builtins.
44- Accepts character vectors, string scalars, string arrays, and cell arrays containing those types
45 to identify the fields that should be removed.
46- Every listed field must already exist. Attempting to remove a missing field raises the standard
47 MATLAB-style error `Reference to non-existent field '<name>'`.
48- Removing multiple fields applies to every element in a struct array; the operation fails if any
49 element is missing one of the requested fields.
50- The input `S` is not mutated in place. `rmfield` returns a new struct (or struct array) while the
51 original remains unchanged.
52
53## `rmfield` Function GPU Execution Behaviour
54`rmfield` performs metadata updates on the host. Values that already reside on the GPU—such as
55`gpuArray` tensors stored in other fields—stay on the device. Because this builtin only rewrites
56struct metadata it does not require or invoke acceleration provider hooks.
57
58## Examples of using the `rmfield` function in MATLAB / RunMat
59
60### Removing a single field from a scalar struct
61```matlab
62s = struct("name", "Ada", "score", 42);
63t = rmfield(s, "score");
64isfield(t, "score")
65```
66
67Expected output:
68```matlab
69ans =
70 logical
71 0
72```
73
74### Removing several fields with a cell array of names
75```matlab
76cfg = struct("mode", "fast", "rate", 60, "debug", true);
77cfg = rmfield(cfg, {"rate", "debug"});
78fieldnames(cfg)
79```
80
81Expected output:
82```matlab
83ans =
84 1×1 cell array
85 {'mode'}
86```
87
88### Removing a field from every element of a struct array
89```matlab
90people = struct("name", {"Ada", "Grace"}, "id", {101, 102}, "email", {"ada@example.com", "grace@example.com"});
91trimmed = rmfield(people, "email");
92fieldnames(trimmed)
93```
94
95Expected output:
96```matlab
97ans =
98 2×1 cell array
99 {'id'}
100 {'name'}
101```
102
103### Supplying a string array of field names to delete
104```matlab
105stats = struct("mean", 10, "median", 9, "stdev", 2);
106names = ["mean", "median"];
107reduced = rmfield(stats, names);
108fieldnames(reduced)
109```
110
111Expected output:
112```matlab
113ans =
114 1×1 cell array
115 {'stdev'}
116```
117
118### Conditionally removing optional fields
119```matlab
120record = struct("id", 7, "notes", "draft");
121if isfield(record, "notes")
122 record = rmfield(record, "notes");
123end
124fieldnames(record)
125```
126
127Expected output:
128```matlab
129ans =
130 1×1 cell array
131 {'id'}
132```
133
134## GPU residency in RunMat (Do I need `gpuArray`?)
135No additional residency management is required. `rmfield` leaves existing GPU tensors untouched and
136never gathers or uploads buffers. Subsequent GPU-aware builtins decide whether to keep values on the
137device.
138
139## FAQ
140
141### Does `rmfield` modify the input in place?
142No. The function returns a new struct (or struct array) with the specified fields removed. The input
143value remains unchanged, mirroring MATLAB's copy-on-write semantics.
144
145### What argument types can I use for the field names?
146You can pass character vectors, string scalars, string arrays, or cell arrays whose elements are
147strings or character vectors. Mixing these forms in a single call is supported—`rmfield`
148concatenates all supplied names into one list.
149
150### What happens if a field is missing?
151RunMat raises the MATLAB-compatible error `Reference to non-existent field '<name>'.` and leaves the
152struct unchanged.
153
154### Can I remove nested fields with `rmfield`?
155No. `rmfield` only removes top-level fields. Use `setfield` with nested assignments or restructure
156your data if you need to manipulate nested content.
157
158### Does `rmfield` work with MATLAB-style objects or handle classes?
159No. The builtin is restricted to structs and struct arrays. Use class-specific helpers (such as
160`rmprop`) for objects.
161
162### Does removing a field move GPU tensors back to the CPU?
163No. The builtin merely rewrites metadata. Any GPU-resident values stored in remaining fields stay on
164the device until another operation decides otherwise.
165
166## See Also
167[fieldnames](./fieldnames), [isfield](./isfield), [setfield](./setfield), [struct](./struct), [orderfields](./orderfields)
168"#;
169
170pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
171 name: "rmfield",
172 op_kind: GpuOpKind::Custom("rmfield"),
173 supported_precisions: &[],
174 broadcast: BroadcastSemantics::None,
175 provider_hooks: &[],
176 constant_strategy: ConstantStrategy::InlineLiteral,
177 residency: ResidencyPolicy::InheritInputs,
178 nan_mode: ReductionNaN::Include,
179 two_pass_threshold: None,
180 workgroup_size: None,
181 accepts_nan_mode: false,
182 notes: "Host-only struct metadata update; acceleration providers are not consulted.",
183};
184
185register_builtin_gpu_spec!(GPU_SPEC);
186
187pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
188 name: "rmfield",
189 shape: ShapeRequirements::Any,
190 constant_strategy: ConstantStrategy::InlineLiteral,
191 elementwise: None,
192 reduction: None,
193 emits_nan: false,
194 notes: "Metadata mutation forces fusion planners to flush pending groups on the host.",
195};
196
197register_builtin_fusion_spec!(FUSION_SPEC);
198
199#[cfg(feature = "doc_export")]
200register_builtin_doc_text!("rmfield", DOC_MD);
201
202#[runtime_builtin(
203 name = "rmfield",
204 category = "structs/core",
205 summary = "Remove one or more fields from scalar structs or struct arrays.",
206 keywords = "rmfield,struct,remove field,struct array"
207)]
208fn rmfield_builtin(value: Value, rest: Vec<Value>) -> Result<Value, String> {
209 let names = parse_field_names(&rest)?;
210 if names.is_empty() {
211 return Ok(value);
212 }
213
214 match value {
215 Value::Struct(st) => {
216 let updated = remove_fields_from_struct_owned(st, &names)?;
217 Ok(Value::Struct(updated))
218 }
219 Value::Cell(cell) if is_struct_array(&cell) => {
220 let updated = remove_fields_from_struct_array(&cell, &names)?;
221 Ok(Value::Cell(updated))
222 }
223 other => Err(format!(
224 "rmfield: expected struct or struct array, got {other:?}"
225 )),
226 }
227}
228
229fn parse_field_names(args: &[Value]) -> Result<Vec<String>, String> {
230 if args.is_empty() {
231 return Err("rmfield: not enough input arguments".to_string());
232 }
233 let mut names: Vec<String> = Vec::new();
234 for value in args {
235 names.extend(collect_field_names(value)?);
236 }
237 Ok(names)
238}
239
240fn collect_field_names(value: &Value) -> Result<Vec<String>, String> {
241 match value {
242 Value::String(_) | Value::CharArray(_) => expect_scalar_name(value)
243 .map(|name| vec![name])
244 .map_err(|err| format!("rmfield: {}", describe_field_name_error(err))),
245 Value::StringArray(sa) => {
246 if sa.data.len() == 1 {
247 expect_scalar_name(value)
248 .map(|name| vec![name])
249 .map_err(|err| format!("rmfield: {}", describe_field_name_error(err)))
250 } else {
251 string_array_to_names(sa)
252 }
253 }
254 Value::Cell(cell) => cell_to_names(cell),
255 other => Err(format!(
256 "rmfield: field names must be strings or character vectors (got {other:?})"
257 )),
258 }
259}
260
261fn string_array_to_names(array: &StringArray) -> Result<Vec<String>, String> {
262 let mut names = Vec::with_capacity(array.data.len());
263 for (index, name) in array.data.iter().enumerate() {
264 if name.is_empty() {
265 return Err(format!(
266 "rmfield: field names must be nonempty character vectors or strings (string array element {})",
267 index + 1
268 ));
269 }
270 names.push(name.clone());
271 }
272 Ok(names)
273}
274
275fn cell_to_names(cell: &CellArray) -> Result<Vec<String>, String> {
276 let mut output = Vec::with_capacity(cell.data.len());
277 for (index, handle) in cell.data.iter().enumerate() {
278 let value = unsafe { &*handle.as_raw() };
279 let name = expect_scalar_name(value).map_err(|err| {
280 format!(
281 "rmfield: {} (cell element {})",
282 describe_field_name_error(err),
283 index + 1
284 )
285 })?;
286 output.push(name);
287 }
288 Ok(output)
289}
290
291#[derive(Clone, Copy)]
292enum FieldNameError {
293 Type,
294 Empty,
295}
296
297fn describe_field_name_error(kind: FieldNameError) -> &'static str {
298 match kind {
299 FieldNameError::Type => {
300 "field names must be string scalars, character vectors, or single-element string arrays"
301 }
302 FieldNameError::Empty => "field names must be nonempty character vectors or strings",
303 }
304}
305
306fn expect_scalar_name(value: &Value) -> Result<String, FieldNameError> {
307 match value {
308 Value::String(s) => {
309 if s.is_empty() {
310 Err(FieldNameError::Empty)
311 } else {
312 Ok(s.clone())
313 }
314 }
315 Value::CharArray(ca) => {
316 if ca.rows != 1 {
317 return Err(FieldNameError::Type);
318 }
319 let text: String = ca.data.iter().collect();
320 if text.is_empty() {
321 Err(FieldNameError::Empty)
322 } else {
323 Ok(text)
324 }
325 }
326 Value::StringArray(sa) => {
327 if sa.data.len() != 1 {
328 return Err(FieldNameError::Type);
329 }
330 let text = sa.data[0].clone();
331 if text.is_empty() {
332 Err(FieldNameError::Empty)
333 } else {
334 Ok(text)
335 }
336 }
337 _ => Err(FieldNameError::Type),
338 }
339}
340
341fn remove_fields_from_struct_owned(
342 mut st: StructValue,
343 names: &[String],
344) -> Result<StructValue, String> {
345 let mut seen: HashSet<&str> = HashSet::new();
346 for name in names {
347 if !seen.insert(name.as_str()) {
348 continue;
349 }
350 if st.remove(name).is_none() {
351 return Err(missing_field_error(name));
352 }
353 }
354 Ok(st)
355}
356
357fn remove_fields_from_struct_array(
358 array: &CellArray,
359 names: &[String],
360) -> Result<CellArray, String> {
361 if array.data.is_empty() {
362 return Ok(array.clone());
363 }
364
365 let mut updated: Vec<Value> = Vec::with_capacity(array.data.len());
366 for handle in &array.data {
367 let value = unsafe { &*handle.as_raw() };
368 let Value::Struct(st) = value else {
369 return Err("rmfield: expected struct array contents to be structs".to_string());
370 };
371 let revised = remove_fields_from_struct_owned(st.clone(), names)?;
372 updated.push(Value::Struct(revised));
373 }
374 CellArray::new_with_shape(updated, array.shape.clone())
375 .map_err(|e| format!("rmfield: failed to rebuild struct array: {e}"))
376}
377
378fn missing_field_error(name: &str) -> String {
379 format!("Reference to non-existent field '{name}'.")
380}
381
382fn is_struct_array(cell: &CellArray) -> bool {
383 cell.data
384 .iter()
385 .all(|handle| matches!(unsafe { &*handle.as_raw() }, Value::Struct(_)))
386}
387
388#[cfg(test)]
389mod tests {
390 use super::*;
391 use runmat_builtins::{CellArray, CharArray, StringArray, StructValue, Value};
392
393 #[cfg(feature = "doc_export")]
394 use crate::builtins::common::test_support;
395 #[cfg(feature = "wgpu")]
396 use runmat_accelerate_api::HostTensorView;
397
398 #[test]
399 fn rmfield_removes_single_field_from_scalar_struct() {
400 let mut st = StructValue::new();
401 st.fields.insert("name".to_string(), Value::from("Ada"));
402 st.fields.insert("score".to_string(), Value::Num(42.0));
403 let result =
404 rmfield_builtin(Value::Struct(st), vec![Value::from("score")]).expect("rmfield");
405 let Value::Struct(updated) = result else {
406 panic!("expected struct result");
407 };
408 assert!(!updated.fields.contains_key("score"));
409 assert!(updated.fields.contains_key("name"));
410 }
411
412 #[test]
413 fn rmfield_accepts_cell_array_of_field_names() {
414 let mut st = StructValue::new();
415 st.fields.insert("left".to_string(), Value::Num(1.0));
416 st.fields.insert("right".to_string(), Value::Num(2.0));
417 st.fields.insert("top".to_string(), Value::Num(3.0));
418 let cell =
419 CellArray::new(vec![Value::from("left"), Value::from("top")], 1, 2).expect("cell");
420 let result = rmfield_builtin(Value::Struct(st), vec![Value::Cell(cell)]).expect("rmfield");
421 let Value::Struct(updated) = result else {
422 panic!("expected struct result");
423 };
424 assert!(!updated.fields.contains_key("left"));
425 assert!(!updated.fields.contains_key("top"));
426 assert!(updated.fields.contains_key("right"));
427 }
428
429 #[test]
430 fn rmfield_supports_string_array_names() {
431 let mut st = StructValue::new();
432 st.fields.insert("alpha".to_string(), Value::Num(1.0));
433 st.fields.insert("beta".to_string(), Value::Num(2.0));
434 st.fields.insert("gamma".to_string(), Value::Num(3.0));
435 let strings = StringArray::new(vec!["alpha".into(), "gamma".into()], vec![1, 2]).unwrap();
436 let result =
437 rmfield_builtin(Value::Struct(st), vec![Value::StringArray(strings)]).expect("rmfield");
438 let Value::Struct(updated) = result else {
439 panic!("expected struct result");
440 };
441 assert!(!updated.fields.contains_key("alpha"));
442 assert!(!updated.fields.contains_key("gamma"));
443 assert!(updated.fields.contains_key("beta"));
444 }
445
446 #[test]
447 fn rmfield_errors_when_field_missing() {
448 let mut st = StructValue::new();
449 st.fields.insert("name".to_string(), Value::from("Ada"));
450 let err = rmfield_builtin(Value::Struct(st), vec![Value::from("id")]).unwrap_err();
451 assert!(
452 err.contains("Reference to non-existent field 'id'."),
453 "unexpected error: {err}"
454 );
455 }
456
457 #[test]
458 fn rmfield_struct_array_roundtrip() {
459 let mut first = StructValue::new();
460 first.fields.insert("name".to_string(), Value::from("Ada"));
461 first.fields.insert("score".to_string(), Value::Num(90.0));
462
463 let mut second = StructValue::new();
464 second
465 .fields
466 .insert("name".to_string(), Value::from("Grace"));
467 second.fields.insert("score".to_string(), Value::Num(95.0));
468
469 let array = CellArray::new_with_shape(
470 vec![Value::Struct(first), Value::Struct(second)],
471 vec![1, 2],
472 )
473 .expect("struct array");
474
475 let result =
476 rmfield_builtin(Value::Cell(array), vec![Value::from("score")]).expect("rmfield");
477 let Value::Cell(updated) = result else {
478 panic!("expected struct array");
479 };
480 for handle in &updated.data {
481 let value = unsafe { &*handle.as_raw() };
482 let Value::Struct(st) = value else {
483 panic!("expected struct element");
484 };
485 assert!(!st.fields.contains_key("score"));
486 assert!(st.fields.contains_key("name"));
487 }
488 }
489
490 #[test]
491 fn rmfield_struct_array_missing_field_errors() {
492 let mut first = StructValue::new();
493 first.fields.insert("id".to_string(), Value::Num(1.0));
494 let mut second = StructValue::new();
495 second.fields.insert("id".to_string(), Value::Num(2.0));
496 second.fields.insert("extra".to_string(), Value::Num(3.0));
497
498 let array = CellArray::new_with_shape(
499 vec![Value::Struct(first), Value::Struct(second)],
500 vec![1, 2],
501 )
502 .expect("struct array");
503
504 let err = rmfield_builtin(Value::Cell(array), vec![Value::from("missing")]).unwrap_err();
505 assert!(
506 err.contains("Reference to non-existent field 'missing'."),
507 "unexpected error: {err}"
508 );
509 }
510
511 #[test]
512 fn rmfield_rejects_non_struct_inputs() {
513 let err = rmfield_builtin(Value::Num(1.0), vec![Value::from("field")]).unwrap_err();
514 assert!(
515 err.contains("expected struct or struct array"),
516 "unexpected error: {err}"
517 );
518 }
519
520 #[test]
521 fn rmfield_produces_error_for_empty_field_name() {
522 let mut st = StructValue::new();
523 st.fields.insert("data".to_string(), Value::Num(1.0));
524 let err = rmfield_builtin(Value::Struct(st), vec![Value::from("")]).unwrap_err();
525 assert!(
526 err.contains("field names must be nonempty"),
527 "unexpected error: {err}"
528 );
529 }
530
531 #[test]
532 fn rmfield_accepts_multiple_argument_forms() {
533 let mut st = StructValue::new();
534 st.fields.insert("alpha".to_string(), Value::Num(1.0));
535 st.fields.insert("beta".to_string(), Value::Num(2.0));
536 st.fields.insert("gamma".to_string(), Value::Num(3.0));
537 st.fields.insert("delta".to_string(), Value::Num(4.0));
538
539 let char_name = CharArray::new_row("beta");
540 let string_array =
541 StringArray::new(vec!["gamma".into()], vec![1, 1]).expect("string scalar array");
542 let cell = CellArray::new(vec![Value::from("delta")], 1, 1).expect("cell array of strings");
543
544 let result = rmfield_builtin(
545 Value::Struct(st),
546 vec![
547 Value::from("alpha"),
548 Value::CharArray(char_name),
549 Value::StringArray(string_array),
550 Value::Cell(cell),
551 ],
552 )
553 .expect("rmfield");
554
555 let Value::Struct(updated) = result else {
556 panic!("expected struct result");
557 };
558
559 assert!(updated.fields.is_empty());
560 }
561
562 #[test]
563 fn rmfield_ignores_duplicate_field_names() {
564 let mut st = StructValue::new();
565 st.fields.insert("keep".to_string(), Value::Num(1.0));
566 st.fields.insert("drop".to_string(), Value::Num(2.0));
567 let result = rmfield_builtin(
568 Value::Struct(st),
569 vec![Value::from("drop"), Value::from("drop")],
570 )
571 .expect("rmfield");
572 let Value::Struct(updated) = result else {
573 panic!("expected struct result");
574 };
575 assert!(!updated.fields.contains_key("drop"));
576 assert!(updated.fields.contains_key("keep"));
577 }
578
579 #[test]
580 fn rmfield_returns_original_when_no_names_supplied() {
581 let mut st = StructValue::new();
582 st.fields.insert("value".to_string(), Value::Num(10.0));
583 let empty = CellArray::new(Vec::new(), 0, 0).expect("empty cell array");
584 let original = st.clone();
585 let result =
586 rmfield_builtin(Value::Struct(st), vec![Value::Cell(empty)]).expect("rmfield empty");
587 assert_eq!(result, Value::Struct(original));
588 }
589
590 #[test]
591 fn rmfield_requires_field_names() {
592 let mut st = StructValue::new();
593 st.fields.insert("value".to_string(), Value::Num(10.0));
594 let err = rmfield_builtin(Value::Struct(st), Vec::new()).unwrap_err();
595 assert!(
596 err.contains("rmfield: not enough input arguments"),
597 "unexpected error: {err}"
598 );
599 }
600
601 #[test]
602 #[cfg(feature = "wgpu")]
603 fn rmfield_preserves_gpu_handles() {
604 let _ = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
605 runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
606 );
607 let provider = runmat_accelerate_api::provider().expect("wgpu provider");
608 let view = HostTensorView {
609 data: &[1.0, 2.0],
610 shape: &[2, 1],
611 };
612 let handle = provider.upload(&view).expect("upload");
613
614 let mut st = StructValue::new();
615 st.fields
616 .insert("gpu".to_string(), Value::GpuTensor(handle.clone()));
617 st.fields.insert("remove".to_string(), Value::Num(5.0));
618
619 let result =
620 rmfield_builtin(Value::Struct(st), vec![Value::from("remove")]).expect("rmfield");
621
622 let Value::Struct(updated) = result else {
623 panic!("expected struct result");
624 };
625
626 assert!(matches!(
627 updated.fields.get("gpu"),
628 Some(Value::GpuTensor(h)) if h == &handle
629 ));
630 assert!(!updated.fields.contains_key("remove"));
631 }
632
633 #[test]
634 #[cfg(feature = "doc_export")]
635 fn doc_examples_present() {
636 let blocks = test_support::doc_examples(DOC_MD);
637 assert!(!blocks.is_empty());
638 }
639}