1use 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}