Skip to main content

runmat_runtime/builtins/structs/core/
rmfield.rs

1//! MATLAB-compatible `rmfield` builtin that removes fields from structs and struct arrays.
2
3use crate::builtins::common::spec::{
4    BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
5    ReductionNaN, ResidencyPolicy, ShapeRequirements,
6};
7use crate::builtins::structs::type_resolvers::rmfield_type;
8use crate::{build_runtime_error, BuiltinResult, RuntimeError};
9use runmat_builtins::{
10    BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
11    BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
12    CellArray, StringArray, StructValue, Value,
13};
14use runmat_macros::runtime_builtin;
15use std::collections::HashSet;
16
17#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::structs::core::rmfield")]
18pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
19    name: "rmfield",
20    op_kind: GpuOpKind::Custom("rmfield"),
21    supported_precisions: &[],
22    broadcast: BroadcastSemantics::None,
23    provider_hooks: &[],
24    constant_strategy: ConstantStrategy::InlineLiteral,
25    residency: ResidencyPolicy::InheritInputs,
26    nan_mode: ReductionNaN::Include,
27    two_pass_threshold: None,
28    workgroup_size: None,
29    accepts_nan_mode: false,
30    notes: "Host-only struct metadata update; acceleration providers are not consulted.",
31};
32
33#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::structs::core::rmfield")]
34pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
35    name: "rmfield",
36    shape: ShapeRequirements::Any,
37    constant_strategy: ConstantStrategy::InlineLiteral,
38    elementwise: None,
39    reduction: None,
40    emits_nan: false,
41    notes: "Metadata mutation forces fusion planners to flush pending groups on the host.",
42};
43
44const BUILTIN_NAME: &str = "rmfield";
45
46const RMFIELD_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
47    name: "S",
48    ty: BuiltinParamType::Any,
49    arity: BuiltinParamArity::Required,
50    default: None,
51    description: "Updated struct or struct array.",
52}];
53
54const RMFIELD_INPUTS_SCALAR: [BuiltinParamDescriptor; 2] = [
55    BuiltinParamDescriptor {
56        name: "S",
57        ty: BuiltinParamType::Any,
58        arity: BuiltinParamArity::Required,
59        default: None,
60        description: "Input struct or struct array.",
61    },
62    BuiltinParamDescriptor {
63        name: "field",
64        ty: BuiltinParamType::StringScalar,
65        arity: BuiltinParamArity::Required,
66        default: None,
67        description: "Field name to remove.",
68    },
69];
70
71const RMFIELD_INPUTS_COLLECTION: [BuiltinParamDescriptor; 2] = [
72    BuiltinParamDescriptor {
73        name: "S",
74        ty: BuiltinParamType::Any,
75        arity: BuiltinParamArity::Required,
76        default: None,
77        description: "Input struct or struct array.",
78    },
79    BuiltinParamDescriptor {
80        name: "fields",
81        ty: BuiltinParamType::Any,
82        arity: BuiltinParamArity::Required,
83        default: None,
84        description: "String array or cell array of field names.",
85    },
86];
87
88const RMFIELD_INPUTS_VARIADIC: [BuiltinParamDescriptor; 3] = [
89    BuiltinParamDescriptor {
90        name: "S",
91        ty: BuiltinParamType::Any,
92        arity: BuiltinParamArity::Required,
93        default: None,
94        description: "Input struct or struct array.",
95    },
96    BuiltinParamDescriptor {
97        name: "field",
98        ty: BuiltinParamType::StringScalar,
99        arity: BuiltinParamArity::Required,
100        default: None,
101        description: "First field name to remove.",
102    },
103    BuiltinParamDescriptor {
104        name: "more_fields",
105        ty: BuiltinParamType::Any,
106        arity: BuiltinParamArity::Variadic,
107        default: None,
108        description: "Additional field names.",
109    },
110];
111
112const RMFIELD_SIGNATURES: [BuiltinSignatureDescriptor; 3] = [
113    BuiltinSignatureDescriptor {
114        label: "S = rmfield(S, field)",
115        inputs: &RMFIELD_INPUTS_SCALAR,
116        outputs: &RMFIELD_OUTPUT,
117    },
118    BuiltinSignatureDescriptor {
119        label: "S = rmfield(S, fields)",
120        inputs: &RMFIELD_INPUTS_COLLECTION,
121        outputs: &RMFIELD_OUTPUT,
122    },
123    BuiltinSignatureDescriptor {
124        label: "S = rmfield(S, field, ...)",
125        inputs: &RMFIELD_INPUTS_VARIADIC,
126        outputs: &RMFIELD_OUTPUT,
127    },
128];
129
130const RMFIELD_ERROR_NOT_ENOUGH_INPUTS: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
131    code: "RM.RMFIELD.NOT_ENOUGH_INPUTS",
132    identifier: Some("RunMat:rmfield:NotEnoughInputs"),
133    when: "No field-name arguments are supplied.",
134    message: "rmfield: not enough input arguments",
135};
136
137const RMFIELD_ERROR_INVALID_TARGET: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
138    code: "RM.RMFIELD.INVALID_TARGET",
139    identifier: Some("RunMat:rmfield:InvalidTarget"),
140    when: "First input is not a struct or struct array.",
141    message: "rmfield: expected struct or struct array",
142};
143
144const RMFIELD_ERROR_FIELD_NAME_TYPE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
145    code: "RM.RMFIELD.FIELD_NAME_TYPE",
146    identifier: Some("RunMat:rmfield:FieldNameType"),
147    when: "Field-name argument has unsupported type or non-scalar shape.",
148    message: "rmfield: field names must be string scalars, character vectors, or single-element string arrays",
149};
150
151const RMFIELD_ERROR_FIELD_NAME_EMPTY: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
152    code: "RM.RMFIELD.FIELD_NAME_EMPTY",
153    identifier: Some("RunMat:rmfield:FieldNameEmpty"),
154    when: "A field name is empty.",
155    message: "rmfield: field names must be nonempty character vectors or strings",
156};
157
158const RMFIELD_ERROR_MISSING_FIELD: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
159    code: "RM.RMFIELD.MISSING_FIELD",
160    identifier: Some("RunMat:rmfield:MissingField"),
161    when: "At least one requested field does not exist on the input struct.",
162    message: "Reference to non-existent field",
163};
164
165const RMFIELD_ERROR_STRUCT_ARRAY_CONTENTS: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
166    code: "RM.RMFIELD.STRUCT_ARRAY_CONTENTS",
167    identifier: Some("RunMat:rmfield:StructArrayContents"),
168    when: "Struct-array input contains non-struct elements.",
169    message: "rmfield: expected struct array contents to be structs",
170};
171
172const RMFIELD_ERROR_REBUILD_FAILED: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
173    code: "RM.RMFIELD.REBUILD_FAILED",
174    identifier: Some("RunMat:rmfield:RebuildFailed"),
175    when: "Rebuilding the updated struct array failed.",
176    message: "rmfield: failed to rebuild struct array",
177};
178
179const RMFIELD_ERRORS: [BuiltinErrorDescriptor; 7] = [
180    RMFIELD_ERROR_NOT_ENOUGH_INPUTS,
181    RMFIELD_ERROR_INVALID_TARGET,
182    RMFIELD_ERROR_FIELD_NAME_TYPE,
183    RMFIELD_ERROR_FIELD_NAME_EMPTY,
184    RMFIELD_ERROR_MISSING_FIELD,
185    RMFIELD_ERROR_STRUCT_ARRAY_CONTENTS,
186    RMFIELD_ERROR_REBUILD_FAILED,
187];
188
189pub const RMFIELD_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
190    signatures: &RMFIELD_SIGNATURES,
191    output_mode: BuiltinOutputMode::Fixed,
192    completion_policy: BuiltinCompletionPolicy::Public,
193    errors: &RMFIELD_ERRORS,
194};
195
196fn rmfield_error(error: &'static BuiltinErrorDescriptor) -> RuntimeError {
197    rmfield_error_with_message(error.message, error)
198}
199
200fn rmfield_error_with_message(
201    message: impl Into<String>,
202    error: &'static BuiltinErrorDescriptor,
203) -> RuntimeError {
204    let mut builder = build_runtime_error(message).with_builtin(BUILTIN_NAME);
205    if let Some(identifier) = error.identifier {
206        builder = builder.with_identifier(identifier);
207    }
208    builder.build()
209}
210
211#[runtime_builtin(
212    name = "rmfield",
213    category = "structs/core",
214    summary = "Remove one or more named fields from structs or struct arrays.",
215    keywords = "rmfield,struct,remove field,struct array",
216    type_resolver(rmfield_type),
217    descriptor(crate::builtins::structs::core::rmfield::RMFIELD_DESCRIPTOR),
218    builtin_path = "crate::builtins::structs::core::rmfield"
219)]
220async fn rmfield_builtin(value: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
221    let names = parse_field_names(&rest)?;
222    if names.is_empty() {
223        return Ok(value);
224    }
225
226    match value {
227        Value::Struct(st) => {
228            let updated = remove_fields_from_struct_owned(st, &names)?;
229            Ok(Value::Struct(updated))
230        }
231        Value::Cell(cell) if is_struct_array(&cell) => {
232            let updated = remove_fields_from_struct_array(&cell, &names)?;
233            Ok(Value::Cell(updated))
234        }
235        other => Err(rmfield_error_with_message(
236            format!("{} (got {other:?})", RMFIELD_ERROR_INVALID_TARGET.message),
237            &RMFIELD_ERROR_INVALID_TARGET,
238        )),
239    }
240}
241
242fn parse_field_names(args: &[Value]) -> BuiltinResult<Vec<String>> {
243    if args.is_empty() {
244        return Err(rmfield_error(&RMFIELD_ERROR_NOT_ENOUGH_INPUTS));
245    }
246    let mut names: Vec<String> = Vec::new();
247    for value in args {
248        names.extend(collect_field_names(value)?);
249    }
250    Ok(names)
251}
252
253fn collect_field_names(value: &Value) -> BuiltinResult<Vec<String>> {
254    match value {
255        Value::String(_) | Value::CharArray(_) => expect_scalar_name(value)
256            .map(|name| vec![name])
257            .map_err(|err| field_name_error(err, None)),
258        Value::StringArray(sa) => {
259            if sa.data.len() == 1 {
260                expect_scalar_name(value)
261                    .map(|name| vec![name])
262                    .map_err(|err| field_name_error(err, None))
263            } else {
264                string_array_to_names(sa)
265            }
266        }
267        Value::Cell(cell) => cell_to_names(cell),
268        other => Err(rmfield_error_with_message(
269            format!("{} (got {other:?})", RMFIELD_ERROR_FIELD_NAME_TYPE.message),
270            &RMFIELD_ERROR_FIELD_NAME_TYPE,
271        )),
272    }
273}
274
275fn string_array_to_names(array: &StringArray) -> BuiltinResult<Vec<String>> {
276    let mut names = Vec::with_capacity(array.data.len());
277    for (index, name) in array.data.iter().enumerate() {
278        if name.is_empty() {
279            return Err(rmfield_error_with_message(
280                format!(
281                    "{} (string array element {})",
282                    RMFIELD_ERROR_FIELD_NAME_EMPTY.message,
283                    index + 1
284                ),
285                &RMFIELD_ERROR_FIELD_NAME_EMPTY,
286            ));
287        }
288        names.push(name.clone());
289    }
290    Ok(names)
291}
292
293fn cell_to_names(cell: &CellArray) -> BuiltinResult<Vec<String>> {
294    let mut output = Vec::with_capacity(cell.data.len());
295    for (index, handle) in cell.data.iter().enumerate() {
296        let value = handle;
297        let name =
298            expect_scalar_name(value).map_err(|err| field_name_error(err, Some(index + 1)))?;
299        output.push(name);
300    }
301    Ok(output)
302}
303
304#[derive(Clone, Copy)]
305enum FieldNameError {
306    Type,
307    Empty,
308}
309
310fn describe_field_name_error(kind: FieldNameError) -> &'static str {
311    match kind {
312        FieldNameError::Type => RMFIELD_ERROR_FIELD_NAME_TYPE.message,
313        FieldNameError::Empty => RMFIELD_ERROR_FIELD_NAME_EMPTY.message,
314    }
315}
316
317fn field_name_error(kind: FieldNameError, cell_index: Option<usize>) -> RuntimeError {
318    let descriptor = match kind {
319        FieldNameError::Type => &RMFIELD_ERROR_FIELD_NAME_TYPE,
320        FieldNameError::Empty => &RMFIELD_ERROR_FIELD_NAME_EMPTY,
321    };
322    let mut message = String::from(describe_field_name_error(kind));
323    if let Some(index) = cell_index {
324        message.push_str(&format!(" (cell element {index})"));
325    }
326    rmfield_error_with_message(message, descriptor)
327}
328
329fn expect_scalar_name(value: &Value) -> Result<String, FieldNameError> {
330    match value {
331        Value::String(s) => {
332            if s.is_empty() {
333                Err(FieldNameError::Empty)
334            } else {
335                Ok(s.clone())
336            }
337        }
338        Value::CharArray(ca) => {
339            if ca.rows != 1 {
340                return Err(FieldNameError::Type);
341            }
342            let text: String = ca.data.iter().collect();
343            if text.is_empty() {
344                Err(FieldNameError::Empty)
345            } else {
346                Ok(text)
347            }
348        }
349        Value::StringArray(sa) => {
350            if sa.data.len() != 1 {
351                return Err(FieldNameError::Type);
352            }
353            let text = sa.data[0].clone();
354            if text.is_empty() {
355                Err(FieldNameError::Empty)
356            } else {
357                Ok(text)
358            }
359        }
360        _ => Err(FieldNameError::Type),
361    }
362}
363
364fn remove_fields_from_struct_owned(
365    mut st: StructValue,
366    names: &[String],
367) -> BuiltinResult<StructValue> {
368    let mut seen: HashSet<&str> = HashSet::new();
369    for name in names {
370        if !seen.insert(name.as_str()) {
371            continue;
372        }
373        if st.remove(name).is_none() {
374            return Err(missing_field_error(name));
375        }
376    }
377    Ok(st)
378}
379
380fn remove_fields_from_struct_array(
381    array: &CellArray,
382    names: &[String],
383) -> BuiltinResult<CellArray> {
384    if array.data.is_empty() {
385        return Ok(array.clone());
386    }
387
388    let mut updated: Vec<Value> = Vec::with_capacity(array.data.len());
389    for handle in &array.data {
390        let value = handle;
391        let Value::Struct(st) = value else {
392            return Err(rmfield_error(&RMFIELD_ERROR_STRUCT_ARRAY_CONTENTS));
393        };
394        let revised = remove_fields_from_struct_owned(st.clone(), names)?;
395        updated.push(Value::Struct(revised));
396    }
397    CellArray::new_with_shape(updated, array.shape.clone()).map_err(|e| {
398        rmfield_error_with_message(
399            format!("{}: {e}", RMFIELD_ERROR_REBUILD_FAILED.message),
400            &RMFIELD_ERROR_REBUILD_FAILED,
401        )
402    })
403}
404
405fn missing_field_error(name: &str) -> RuntimeError {
406    rmfield_error_with_message(
407        format!("{} '{name}'.", RMFIELD_ERROR_MISSING_FIELD.message),
408        &RMFIELD_ERROR_MISSING_FIELD,
409    )
410}
411
412fn is_struct_array(cell: &CellArray) -> bool {
413    cell.data
414        .iter()
415        .all(|handle| matches!(handle, Value::Struct(_)))
416}
417
418#[cfg(test)]
419pub(crate) mod tests {
420    use super::*;
421    use runmat_builtins::{CellArray, CharArray, StringArray, StructValue, Value};
422
423    fn error_message(err: crate::RuntimeError) -> String {
424        err.message().to_string()
425    }
426
427    fn run_rmfield(value: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
428        futures::executor::block_on(rmfield_builtin(value, rest))
429    }
430    #[cfg(feature = "wgpu")]
431    use runmat_accelerate_api::HostTensorView;
432
433    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
434    #[test]
435    fn rmfield_removes_single_field_from_scalar_struct() {
436        let mut st = StructValue::new();
437        st.fields.insert("name".to_string(), Value::from("Ada"));
438        st.fields.insert("score".to_string(), Value::Num(42.0));
439        let result = run_rmfield(Value::Struct(st), vec![Value::from("score")]).expect("rmfield");
440        let Value::Struct(updated) = result else {
441            panic!("expected struct result");
442        };
443        assert!(!updated.fields.contains_key("score"));
444        assert!(updated.fields.contains_key("name"));
445    }
446
447    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
448    #[test]
449    fn rmfield_accepts_cell_array_of_field_names() {
450        let mut st = StructValue::new();
451        st.fields.insert("left".to_string(), Value::Num(1.0));
452        st.fields.insert("right".to_string(), Value::Num(2.0));
453        st.fields.insert("top".to_string(), Value::Num(3.0));
454        let cell =
455            CellArray::new(vec![Value::from("left"), Value::from("top")], 1, 2).expect("cell");
456        let result = run_rmfield(Value::Struct(st), vec![Value::Cell(cell)]).expect("rmfield");
457        let Value::Struct(updated) = result else {
458            panic!("expected struct result");
459        };
460        assert!(!updated.fields.contains_key("left"));
461        assert!(!updated.fields.contains_key("top"));
462        assert!(updated.fields.contains_key("right"));
463    }
464
465    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
466    #[test]
467    fn rmfield_supports_string_array_names() {
468        let mut st = StructValue::new();
469        st.fields.insert("alpha".to_string(), Value::Num(1.0));
470        st.fields.insert("beta".to_string(), Value::Num(2.0));
471        st.fields.insert("gamma".to_string(), Value::Num(3.0));
472        let strings = StringArray::new(vec!["alpha".into(), "gamma".into()], vec![1, 2]).unwrap();
473        let result =
474            run_rmfield(Value::Struct(st), vec![Value::StringArray(strings)]).expect("rmfield");
475        let Value::Struct(updated) = result else {
476            panic!("expected struct result");
477        };
478        assert!(!updated.fields.contains_key("alpha"));
479        assert!(!updated.fields.contains_key("gamma"));
480        assert!(updated.fields.contains_key("beta"));
481    }
482
483    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
484    #[test]
485    fn rmfield_errors_when_field_missing() {
486        let mut st = StructValue::new();
487        st.fields.insert("name".to_string(), Value::from("Ada"));
488        let err =
489            error_message(run_rmfield(Value::Struct(st), vec![Value::from("id")]).unwrap_err());
490        assert!(
491            err.contains("Reference to non-existent field 'id'."),
492            "unexpected error: {err}"
493        );
494    }
495
496    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
497    #[test]
498    fn rmfield_struct_array_roundtrip() {
499        let mut first = StructValue::new();
500        first.fields.insert("name".to_string(), Value::from("Ada"));
501        first.fields.insert("score".to_string(), Value::Num(90.0));
502
503        let mut second = StructValue::new();
504        second
505            .fields
506            .insert("name".to_string(), Value::from("Grace"));
507        second.fields.insert("score".to_string(), Value::Num(95.0));
508
509        let array = CellArray::new_with_shape(
510            vec![Value::Struct(first), Value::Struct(second)],
511            vec![1, 2],
512        )
513        .expect("struct array");
514
515        let result = run_rmfield(Value::Cell(array), vec![Value::from("score")]).expect("rmfield");
516        let Value::Cell(updated) = result else {
517            panic!("expected struct array");
518        };
519        for handle in &updated.data {
520            let value = handle;
521            let Value::Struct(st) = value else {
522                panic!("expected struct element");
523            };
524            assert!(!st.fields.contains_key("score"));
525            assert!(st.fields.contains_key("name"));
526        }
527    }
528
529    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
530    #[test]
531    fn rmfield_struct_array_missing_field_errors() {
532        let mut first = StructValue::new();
533        first.fields.insert("id".to_string(), Value::Num(1.0));
534        let mut second = StructValue::new();
535        second.fields.insert("id".to_string(), Value::Num(2.0));
536        second.fields.insert("extra".to_string(), Value::Num(3.0));
537
538        let array = CellArray::new_with_shape(
539            vec![Value::Struct(first), Value::Struct(second)],
540            vec![1, 2],
541        )
542        .expect("struct array");
543
544        let err = error_message(
545            run_rmfield(Value::Cell(array), vec![Value::from("missing")]).unwrap_err(),
546        );
547        assert!(
548            err.contains("Reference to non-existent field 'missing'."),
549            "unexpected error: {err}"
550        );
551    }
552
553    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
554    #[test]
555    fn rmfield_rejects_non_struct_inputs() {
556        let err =
557            error_message(run_rmfield(Value::Num(1.0), vec![Value::from("field")]).unwrap_err());
558        assert!(
559            err.contains("expected struct or struct array"),
560            "unexpected error: {err}"
561        );
562    }
563
564    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
565    #[test]
566    fn rmfield_produces_error_for_empty_field_name() {
567        let mut st = StructValue::new();
568        st.fields.insert("data".to_string(), Value::Num(1.0));
569        let err = error_message(run_rmfield(Value::Struct(st), vec![Value::from("")]).unwrap_err());
570        assert!(
571            err.contains("field names must be nonempty"),
572            "unexpected error: {err}"
573        );
574    }
575
576    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
577    #[test]
578    fn rmfield_accepts_multiple_argument_forms() {
579        let mut st = StructValue::new();
580        st.fields.insert("alpha".to_string(), Value::Num(1.0));
581        st.fields.insert("beta".to_string(), Value::Num(2.0));
582        st.fields.insert("gamma".to_string(), Value::Num(3.0));
583        st.fields.insert("delta".to_string(), Value::Num(4.0));
584
585        let char_name = CharArray::new_row("beta");
586        let string_array =
587            StringArray::new(vec!["gamma".into()], vec![1, 1]).expect("string scalar array");
588        let cell = CellArray::new(vec![Value::from("delta")], 1, 1).expect("cell array of strings");
589
590        let result = run_rmfield(
591            Value::Struct(st),
592            vec![
593                Value::from("alpha"),
594                Value::CharArray(char_name),
595                Value::StringArray(string_array),
596                Value::Cell(cell),
597            ],
598        )
599        .expect("rmfield");
600
601        let Value::Struct(updated) = result else {
602            panic!("expected struct result");
603        };
604
605        assert!(updated.fields.is_empty());
606    }
607
608    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
609    #[test]
610    fn rmfield_ignores_duplicate_field_names() {
611        let mut st = StructValue::new();
612        st.fields.insert("keep".to_string(), Value::Num(1.0));
613        st.fields.insert("drop".to_string(), Value::Num(2.0));
614        let result = run_rmfield(
615            Value::Struct(st),
616            vec![Value::from("drop"), Value::from("drop")],
617        )
618        .expect("rmfield");
619        let Value::Struct(updated) = result else {
620            panic!("expected struct result");
621        };
622        assert!(!updated.fields.contains_key("drop"));
623        assert!(updated.fields.contains_key("keep"));
624    }
625
626    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
627    #[test]
628    fn rmfield_returns_original_when_no_names_supplied() {
629        let mut st = StructValue::new();
630        st.fields.insert("value".to_string(), Value::Num(10.0));
631        let empty = CellArray::new(Vec::new(), 0, 0).expect("empty cell array");
632        let original = st.clone();
633        let result =
634            run_rmfield(Value::Struct(st), vec![Value::Cell(empty)]).expect("rmfield empty");
635        assert_eq!(result, Value::Struct(original));
636    }
637
638    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
639    #[test]
640    fn rmfield_requires_field_names() {
641        let mut st = StructValue::new();
642        st.fields.insert("value".to_string(), Value::Num(10.0));
643        let err = error_message(run_rmfield(Value::Struct(st), Vec::new()).unwrap_err());
644        assert!(
645            err.contains("rmfield: not enough input arguments"),
646            "unexpected error: {err}"
647        );
648    }
649
650    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
651    #[test]
652    #[cfg(feature = "wgpu")]
653    fn rmfield_preserves_gpu_handles() {
654        let _ = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
655            runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
656        );
657        let provider = runmat_accelerate_api::provider().expect("wgpu provider");
658        let view = HostTensorView {
659            data: &[1.0, 2.0],
660            shape: &[2, 1],
661        };
662        let handle = provider.upload(&view).expect("upload");
663
664        let mut st = StructValue::new();
665        st.fields
666            .insert("gpu".to_string(), Value::GpuTensor(handle.clone()));
667        st.fields.insert("remove".to_string(), Value::Num(5.0));
668
669        let result = run_rmfield(Value::Struct(st), vec![Value::from("remove")]).expect("rmfield");
670
671        let Value::Struct(updated) = result else {
672            panic!("expected struct result");
673        };
674
675        assert!(matches!(
676            updated.fields.get("gpu"),
677            Some(Value::GpuTensor(h)) if h == &handle
678        ));
679        assert!(!updated.fields.contains_key("remove"));
680    }
681}