1use regex::Regex;
3use runmat_builtins::{
4 BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinIntegerBackendRule,
5 BuiltinIntegerCapabilityDescriptor, BuiltinIntegerComputationDomain,
6 BuiltinIntegerInputAvailability, BuiltinIntegerInputCapability, BuiltinIntegerOutputClassRule,
7 BuiltinIntegerOverflowRule, BuiltinIntegerOverloadKind, BuiltinIntegerScalarDoubleRule,
8 BuiltinOutputMode, BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType,
9 BuiltinSignatureDescriptor,
10};
11use runmat_macros::runtime_builtin;
12use runmat_value::{CellArray, CharArray, StringArray, Value};
13
14use crate::builtins::common::map_control_flow_with_builtin;
15use crate::builtins::common::spec::{
16 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
17 ReductionNaN, ResidencyPolicy, ShapeRequirements,
18};
19use crate::builtins::strings::common::{char_row_to_string_slice, is_missing_string};
20use crate::builtins::strings::core::compat::pattern_regex;
21use crate::builtins::strings::type_resolvers::text_preserve_type;
22use crate::{
23 build_runtime_error, gather_if_needed_async, make_cell_with_shape, BuiltinResult, RuntimeError,
24};
25
26#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::strings::transform::erase")]
27pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
28 name: "erase",
29 op_kind: GpuOpKind::Custom("string-transform"),
30 supported_precisions: &[],
31 broadcast: BroadcastSemantics::None,
32 provider_hooks: &[],
33 constant_strategy: ConstantStrategy::InlineLiteral,
34 residency: ResidencyPolicy::NewHandle,
35 nan_mode: ReductionNaN::Include,
36 two_pass_threshold: None,
37 workgroup_size: None,
38 accepts_nan_mode: false,
39 notes: "The builtin owns resident arguments so it can reject numeric values before provider access; accepted text executes on the host and produces host text.",
40};
41
42#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::strings::transform::erase")]
43pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
44 name: "erase",
45 shape: ShapeRequirements::Any,
46 constant_strategy: ConstantStrategy::InlineLiteral,
47 elementwise: None,
48 reduction: None,
49 emits_nan: false,
50 notes: "String manipulation builtin; not eligible for fusion plans, and resident numeric values are rejected as non-text.",
51};
52
53const BUILTIN_NAME: &str = "erase";
54
55const INTEGER_INPUTS: [BuiltinIntegerInputCapability; 2] = [
56 BuiltinIntegerInputCapability {
57 name: "str",
58 classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
59 availability: BuiltinIntegerInputAvailability::Rejected,
60 scalar_double: BuiltinIntegerScalarDoubleRule::Rejected,
61 notes: "The public input text accepts string arrays, a character vector, or a cell array of character vectors; numeric values are rejected before provider access.",
62 },
63 BuiltinIntegerInputCapability {
64 name: "match",
65 classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
66 availability: BuiltinIntegerInputAvailability::Rejected,
67 scalar_double: BuiltinIntegerScalarDoubleRule::Rejected,
68 notes: "The public match argument accepts text or pattern values; numeric values are rejected before provider access.",
69 },
70];
71pub const INTEGER_CAPABILITIES: [BuiltinIntegerCapabilityDescriptor; 1] =
72 [BuiltinIntegerCapabilityDescriptor {
73 form: "newStr = erase(str, match)",
74 inputs: &INTEGER_INPUTS,
75 computation_domain: BuiltinIntegerComputationDomain::FunctionSpecific,
76 output_class: BuiltinIntegerOutputClassRule::NotApplicable,
77 overflow: BuiltinIntegerOverflowRule::NotApplicable,
78 backend: BuiltinIntegerBackendRule::HostOnly,
79 overload: BuiltinIntegerOverloadKind::FunctionSpecific,
80 notes: "erase is text-only; integer values in either role, including nested resident values, are rejected without gathering.",
81 }];
82
83const ERASE_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
84 name: "newStr",
85 ty: BuiltinParamType::Any,
86 arity: BuiltinParamArity::Required,
87 default: None,
88 description: "Text with substring occurrences removed, preserving input container kind.",
89}];
90
91const ERASE_INPUTS: [BuiltinParamDescriptor; 2] = [
92 BuiltinParamDescriptor {
93 name: "str",
94 ty: BuiltinParamType::Any,
95 arity: BuiltinParamArity::Required,
96 default: None,
97 description: "Input text (string/char/cell).",
98 },
99 BuiltinParamDescriptor {
100 name: "pattern",
101 ty: BuiltinParamType::Any,
102 arity: BuiltinParamArity::Required,
103 default: None,
104 description: "Pattern text list (scalar or array/cell).",
105 },
106];
107
108const ERASE_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
109 label: "newStr = erase(str, pattern)",
110 inputs: &ERASE_INPUTS,
111 outputs: &ERASE_OUTPUT,
112}];
113
114const ERASE_ERROR_INVALID_INPUT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
115 code: "RM.ERASE.INVALID_INPUT",
116 identifier: Some("RunMat:erase:InvalidInput"),
117 when: "First argument is not a string array, char array, or cell array of text scalars.",
118 message:
119 "erase: first argument must be a string array, character array, or cell array of character vectors",
120};
121
122const ERASE_ERROR_PATTERN_TYPE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
123 code: "RM.ERASE.PATTERN_TYPE",
124 identifier: Some("RunMat:erase:PatternType"),
125 when: "Second argument is not a text scalar/array/cell of text scalars.",
126 message:
127 "erase: second argument must be a string array, character array, or cell array of character vectors",
128};
129
130const ERASE_ERROR_CELL_ELEMENT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
131 code: "RM.ERASE.CELL_ELEMENT",
132 identifier: Some("RunMat:erase:CellElement"),
133 when: "Cell arrays contain non-text elements or non-row char arrays.",
134 message: "erase: cell array elements must be string scalars or character vectors",
135};
136
137const ERASE_ERROR_INTERNAL: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
138 code: "RM.ERASE.INTERNAL",
139 identifier: Some("RunMat:erase:InternalError"),
140 when: "Internal output container construction failed.",
141 message: "erase: internal error",
142};
143
144const ERASE_ERRORS: [BuiltinErrorDescriptor; 4] = [
145 ERASE_ERROR_INVALID_INPUT,
146 ERASE_ERROR_PATTERN_TYPE,
147 ERASE_ERROR_CELL_ELEMENT,
148 ERASE_ERROR_INTERNAL,
149];
150
151pub const ERASE_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
152 signatures: &ERASE_SIGNATURES,
153 output_mode: BuiltinOutputMode::Fixed,
154 completion_policy: BuiltinCompletionPolicy::Public,
155 errors: &ERASE_ERRORS,
156};
157
158fn map_flow(err: RuntimeError) -> RuntimeError {
159 map_control_flow_with_builtin(err, BUILTIN_NAME)
160}
161
162fn erase_error_with_message(
163 message: impl Into<String>,
164 error: &'static BuiltinErrorDescriptor,
165) -> RuntimeError {
166 let mut builder = build_runtime_error(message).with_builtin(BUILTIN_NAME);
167 if let Some(identifier) = error.identifier {
168 builder = builder.with_identifier(identifier);
169 }
170 builder.build()
171}
172
173fn erase_error(error: &'static BuiltinErrorDescriptor) -> RuntimeError {
174 erase_error_with_message(error.message, error)
175}
176
177#[runtime_builtin(
178 name = "erase",
179 category = "strings/transform",
180 summary = "Remove substring occurrences from text inputs.",
181 keywords = "erase,remove substring,strings,character array,text",
182 accel = "sink",
183 integer_capabilities(INTEGER_CAPABILITIES),
184 type_resolver(text_preserve_type),
185 descriptor(crate::builtins::strings::transform::erase::ERASE_DESCRIPTOR),
186 builtin_path = "crate::builtins::strings::transform::erase"
187)]
188async fn erase_builtin(text: Value, pattern: Value) -> BuiltinResult<Value> {
189 if contains_numeric_or_resident(&text) {
190 return Err(erase_error(&ERASE_ERROR_INVALID_INPUT));
191 }
192 if contains_numeric_or_resident(&pattern) {
193 return Err(erase_error(&ERASE_ERROR_PATTERN_TYPE));
194 }
195 let text = gather_if_needed_async(&text).await.map_err(map_flow)?;
196 let pattern = gather_if_needed_async(&pattern).await.map_err(map_flow)?;
197
198 let patterns = PatternList::from_value(&pattern)?;
199
200 match text {
201 Value::String(s) => Ok(Value::String(erase_string_scalar(s, &patterns))),
202 Value::StringArray(sa) => erase_string_array(sa, &patterns),
203 Value::CharArray(ca) => erase_char_array(ca, &patterns),
204 Value::Cell(cell) => erase_cell_array(cell, &patterns),
205 _ => Err(erase_error(&ERASE_ERROR_INVALID_INPUT)),
206 }
207}
208
209fn contains_numeric_or_resident(value: &Value) -> bool {
210 match value {
211 Value::Num(_)
212 | Value::Int(_)
213 | Value::Bool(_)
214 | Value::Tensor(_)
215 | Value::LogicalArray(_)
216 | Value::Complex(_, _)
217 | Value::ComplexTensor(_)
218 | Value::GpuTensor(_) => true,
219 Value::Cell(cell) => cell.data.iter().any(contains_numeric_or_resident),
220 _ => false,
221 }
222}
223
224struct PatternList {
225 entries: Vec<PatternEntry>,
226}
227
228enum PatternEntry {
229 Literal(String),
230 Regex(Regex),
231}
232
233impl PatternList {
234 fn from_value(value: &Value) -> BuiltinResult<Self> {
235 let entries = match value {
236 Value::Object(_) => vec![PatternEntry::Regex(
237 Regex::new(&pattern_regex(value, BUILTIN_NAME).map_err(|err| {
238 erase_error_with_message(err.message().to_string(), &ERASE_ERROR_PATTERN_TYPE)
239 })?)
240 .map_err(|err| {
241 erase_error_with_message(err.to_string(), &ERASE_ERROR_PATTERN_TYPE)
242 })?,
243 )],
244 Value::String(text) => vec![PatternEntry::Literal(text.clone())],
245 Value::StringArray(array) => array
246 .data
247 .iter()
248 .cloned()
249 .map(PatternEntry::Literal)
250 .collect(),
251 Value::CharArray(array) => {
252 if array.rows == 0 {
253 Vec::new()
254 } else {
255 let mut list = Vec::with_capacity(array.rows);
256 for row in 0..array.rows {
257 list.push(PatternEntry::Literal(char_row_to_string_slice(
258 &array.data,
259 array.cols,
260 row,
261 )));
262 }
263 list
264 }
265 }
266 Value::Cell(cell) => {
267 let mut list = Vec::with_capacity(cell.data.len());
268 for handle in &cell.data {
269 match &handle {
270 Value::String(text) => list.push(PatternEntry::Literal(text.clone())),
271 Value::StringArray(sa) if sa.data.len() == 1 => {
272 list.push(PatternEntry::Literal(sa.data[0].clone()));
273 }
274 Value::CharArray(ca) if ca.rows == 0 => {
275 list.push(PatternEntry::Literal(String::new()));
276 }
277 Value::CharArray(ca) if ca.rows == 1 => {
278 list.push(PatternEntry::Literal(char_row_to_string_slice(
279 &ca.data, ca.cols, 0,
280 )));
281 }
282 Value::CharArray(_) => return Err(erase_error(&ERASE_ERROR_CELL_ELEMENT)),
283 _ => return Err(erase_error(&ERASE_ERROR_CELL_ELEMENT)),
284 }
285 }
286 list
287 }
288 _ => return Err(erase_error(&ERASE_ERROR_PATTERN_TYPE)),
289 };
290 Ok(Self { entries })
291 }
292
293 fn apply(&self, input: &str) -> String {
294 if self.entries.is_empty() {
295 return input.to_string();
296 }
297 let mut current = input.to_string();
298 for pattern in &self.entries {
299 match pattern {
300 PatternEntry::Literal(pattern) => {
301 if pattern.is_empty() {
302 continue;
303 }
304 current = current.replace(pattern, "");
305 }
306 PatternEntry::Regex(pattern) => {
307 current = pattern.replace_all(¤t, "").to_string();
308 }
309 }
310 if current.is_empty() {
311 break;
312 }
313 }
314 current
315 }
316}
317
318fn erase_string_scalar(text: String, patterns: &PatternList) -> String {
319 if is_missing_string(&text) {
320 text
321 } else {
322 patterns.apply(&text)
323 }
324}
325
326fn erase_string_array(array: StringArray, patterns: &PatternList) -> BuiltinResult<Value> {
327 let StringArray { data, shape, .. } = array;
328 let mut erased = Vec::with_capacity(data.len());
329 for entry in data {
330 if is_missing_string(&entry) {
331 erased.push(entry);
332 } else {
333 erased.push(patterns.apply(&entry));
334 }
335 }
336 StringArray::new(erased, shape)
337 .map(Value::StringArray)
338 .map_err(|e| {
339 erase_error_with_message(format!("{BUILTIN_NAME}: {e}"), &ERASE_ERROR_INTERNAL)
340 })
341}
342
343fn erase_char_array(array: CharArray, patterns: &PatternList) -> BuiltinResult<Value> {
344 let CharArray {
345 data,
346 shape,
347 rows,
348 cols,
349 } = array;
350 if rows == 0 {
351 return Ok(Value::CharArray(CharArray {
352 data,
353 shape,
354 rows,
355 cols,
356 }));
357 }
358
359 let mut processed: Vec<String> = Vec::with_capacity(rows);
360 let mut target_cols = 0usize;
361 for row in 0..rows {
362 let slice = char_row_to_string_slice(&data, cols, row);
363 let erased = patterns.apply(&slice);
364 let len = erased.chars().count();
365 if len > target_cols {
366 target_cols = len;
367 }
368 processed.push(erased);
369 }
370
371 let mut flattened: Vec<char> = Vec::with_capacity(rows * target_cols);
372 for row_text in processed {
373 let mut chars: Vec<char> = row_text.chars().collect();
374 if chars.len() < target_cols {
375 chars.resize(target_cols, ' ');
376 }
377 flattened.extend(chars);
378 }
379
380 CharArray::new(flattened, rows, target_cols)
381 .map(Value::CharArray)
382 .map_err(|e| {
383 erase_error_with_message(format!("{BUILTIN_NAME}: {e}"), &ERASE_ERROR_INTERNAL)
384 })
385}
386
387fn erase_cell_array(cell: CellArray, patterns: &PatternList) -> BuiltinResult<Value> {
388 let shape = cell.shape.clone();
389 let mut values = Vec::with_capacity(cell.data.len());
390 for handle in &cell.data {
391 values.push(erase_cell_element(handle, patterns)?);
392 }
393 make_cell_with_shape(values, shape).map_err(|e| {
394 erase_error_with_message(format!("{BUILTIN_NAME}: {e}"), &ERASE_ERROR_INTERNAL)
395 })
396}
397
398fn erase_cell_element(value: &Value, patterns: &PatternList) -> BuiltinResult<Value> {
399 match value {
400 Value::String(text) => Ok(Value::String(erase_string_scalar(text.clone(), patterns))),
401 Value::StringArray(sa) if sa.data.len() == 1 => Ok(Value::String(erase_string_scalar(
402 sa.data[0].clone(),
403 patterns,
404 ))),
405 Value::CharArray(ca) if ca.rows == 0 => Ok(Value::CharArray(ca.clone())),
406 Value::CharArray(ca) if ca.rows == 1 => {
407 let slice = char_row_to_string_slice(&ca.data, ca.cols, 0);
408 let erased = patterns.apply(&slice);
409 Ok(Value::CharArray(CharArray::new_row(&erased)))
410 }
411 Value::CharArray(_) => Err(erase_error(&ERASE_ERROR_CELL_ELEMENT)),
412 _ => Err(erase_error(&ERASE_ERROR_CELL_ELEMENT)),
413 }
414}
415
416#[cfg(test)]
417pub(crate) mod tests {
418 use super::*;
419 use runmat_builtins::{ResolveContext, Type};
420
421 fn erase_builtin(text: Value, pattern: Value) -> BuiltinResult<Value> {
422 futures::executor::block_on(super::erase_builtin(text, pattern))
423 }
424
425 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
426 #[test]
427 fn erase_string_scalar_single_pattern() {
428 let result = erase_builtin(
429 Value::String("RunMat runtime".into()),
430 Value::String(" runtime".into()),
431 )
432 .expect("erase");
433 assert_eq!(result, Value::String("RunMat".into()));
434 }
435
436 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
437 #[test]
438 fn erase_string_array_multiple_patterns() {
439 let strings = StringArray::new(
440 vec!["gpu".into(), "cpu".into(), "<missing>".into()],
441 vec![3, 1],
442 )
443 .unwrap();
444 let result = erase_builtin(
445 Value::StringArray(strings),
446 Value::StringArray(StringArray::new(vec!["g".into(), "c".into()], vec![2, 1]).unwrap()),
447 )
448 .expect("erase");
449 match result {
450 Value::StringArray(sa) => {
451 assert_eq!(sa.shape, vec![3, 1]);
452 assert_eq!(
453 sa.data,
454 vec![
455 String::from("pu"),
456 String::from("pu"),
457 String::from("<missing>")
458 ]
459 );
460 }
461 other => panic!("expected string array, got {other:?}"),
462 }
463 }
464
465 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
466 #[test]
467 fn erase_string_array_shape_mismatch_applies_all_patterns() {
468 let strings =
469 StringArray::new(vec!["GPU kernel".into(), "CPU kernel".into()], vec![2, 1]).unwrap();
470 let patterns = StringArray::new(vec!["GPU ".into(), "CPU ".into()], vec![1, 2]).unwrap();
471 let result = erase_builtin(Value::StringArray(strings), Value::StringArray(patterns))
472 .expect("erase");
473 match result {
474 Value::StringArray(sa) => {
475 assert_eq!(sa.shape, vec![2, 1]);
476 assert_eq!(
477 sa.data,
478 vec![String::from("kernel"), String::from("kernel")]
479 );
480 }
481 other => panic!("expected string array, got {other:?}"),
482 }
483 }
484
485 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
486 #[test]
487 fn erase_char_array_adjusts_width() {
488 let chars = CharArray::new("matrix".chars().collect(), 1, 6).unwrap();
489 let result =
490 erase_builtin(Value::CharArray(chars), Value::String("tr".into())).expect("erase");
491 match result {
492 Value::CharArray(out) => {
493 assert_eq!(out.rows, 1);
494 assert_eq!(out.cols, 4);
495 let expected: Vec<char> = "maix".chars().collect();
496 assert_eq!(out.data, expected);
497 }
498 other => panic!("expected char array, got {other:?}"),
499 }
500 }
501
502 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
503 #[test]
504 fn erase_char_array_handles_full_removal() {
505 let chars = CharArray::new_row("abc");
506 let result = erase_builtin(Value::CharArray(chars.clone()), Value::String("abc".into()))
507 .expect("erase");
508 match result {
509 Value::CharArray(out) => {
510 assert_eq!(out.rows, 1);
511 assert_eq!(out.cols, 0);
512 assert!(out.data.is_empty());
513 }
514 other => panic!("expected empty char array, got {other:?}"),
515 }
516 }
517
518 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
519 #[test]
520 fn erase_char_array_multiple_rows_sequential_patterns() {
521 let chars = CharArray::new(
522 vec![
523 'G', 'P', 'U', ' ', 'p', 'i', 'p', 'e', 'l', 'i', 'n', 'e', 'C', 'P', 'U', ' ',
524 'p', 'i', 'p', 'e', 'l', 'i', 'n', 'e',
525 ],
526 2,
527 12,
528 )
529 .unwrap();
530 let patterns = CharArray::new_row("GPU ");
531 let result =
532 erase_builtin(Value::CharArray(chars), Value::CharArray(patterns)).expect("erase");
533 match result {
534 Value::CharArray(out) => {
535 assert_eq!(out.rows, 2);
536 assert_eq!(out.cols, 12);
537 let first = char_row_to_string_slice(&out.data, out.cols, 0);
538 let second = char_row_to_string_slice(&out.data, out.cols, 1);
539 assert_eq!(first.trim_end(), "pipeline");
540 assert_eq!(second.trim_end(), "CPU pipeline");
541 }
542 other => panic!("expected char array, got {other:?}"),
543 }
544 }
545
546 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
547 #[test]
548 fn erase_cell_array_mixed_content() {
549 let cell = CellArray::new(
550 vec![
551 Value::CharArray(CharArray::new_row("Kernel Planner")),
552 Value::String("GPU Fusion".into()),
553 ],
554 1,
555 2,
556 )
557 .unwrap();
558 let result = erase_builtin(
559 Value::Cell(cell),
560 Value::Cell(
561 CellArray::new(
562 vec![
563 Value::String("Kernel ".into()),
564 Value::String("GPU ".into()),
565 ],
566 1,
567 2,
568 )
569 .unwrap(),
570 ),
571 )
572 .expect("erase");
573 match result {
574 Value::Cell(out) => {
575 let first = out.get(0, 0).unwrap();
576 let second = out.get(0, 1).unwrap();
577 assert_eq!(first, Value::CharArray(CharArray::new_row("Planner")));
578 assert_eq!(second, Value::String("Fusion".into()));
579 }
580 other => panic!("expected cell array, got {other:?}"),
581 }
582 }
583
584 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
585 #[test]
586 fn erase_cell_array_preserves_shape() {
587 let cell = CellArray::new(
588 vec![
589 Value::String("alpha".into()),
590 Value::String("beta".into()),
591 Value::String("gamma".into()),
592 Value::String("delta".into()),
593 ],
594 2,
595 2,
596 )
597 .unwrap();
598 let patterns = StringArray::new(vec!["a".into()], vec![1, 1]).unwrap();
599 let result = erase_builtin(Value::Cell(cell), Value::StringArray(patterns)).expect("erase");
600 match result {
601 Value::Cell(out) => {
602 assert_eq!(out.rows, 2);
603 assert_eq!(out.cols, 2);
604 assert_eq!(out.get(0, 0).unwrap(), Value::String("lph".into()));
605 assert_eq!(out.get(1, 1).unwrap(), Value::String("delt".into()));
606 }
607 other => panic!("expected cell array, got {other:?}"),
608 }
609 }
610
611 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
612 #[test]
613 fn erase_preserves_missing_string() {
614 let result = erase_builtin(
615 Value::String("<missing>".into()),
616 Value::String("missing".into()),
617 )
618 .expect("erase");
619 assert_eq!(result, Value::String("<missing>".into()));
620 }
621
622 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
623 #[test]
624 fn erase_allows_empty_pattern_list() {
625 let strings = StringArray::new(vec!["alpha".into(), "beta".into()], vec![2, 1]).unwrap();
626 let pattern = StringArray::new(Vec::<String>::new(), vec![0, 0]).unwrap();
627 let result = erase_builtin(
628 Value::StringArray(strings.clone()),
629 Value::StringArray(pattern),
630 )
631 .expect("erase");
632 assert_eq!(result, Value::StringArray(strings));
633 }
634
635 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
636 #[test]
637 fn erase_errors_on_invalid_first_argument() {
638 let err = erase_builtin(Value::Num(1.0), Value::String("a".into())).unwrap_err();
639 assert_eq!(err.to_string(), ERASE_ERROR_INVALID_INPUT.message);
640 }
641
642 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
643 #[test]
644 fn erase_errors_on_invalid_pattern_type() {
645 let err = erase_builtin(Value::String("abc".into()), Value::Num(1.0)).unwrap_err();
646 assert_eq!(err.to_string(), ERASE_ERROR_PATTERN_TYPE.message);
647 }
648
649 #[test]
650 fn erase_rejects_nested_integer_text_before_conversion() {
651 let cell = CellArray::new(
652 vec![Value::Tensor(
653 runmat_value::Tensor::new_integer(
654 runmat_value::IntegerStorage::U64(vec![u64::MAX]),
655 vec![1, 1],
656 )
657 .expect("integer tensor"),
658 )],
659 1,
660 1,
661 )
662 .expect("cell");
663 let error = erase_builtin(Value::Cell(cell), Value::String("x".into()))
664 .expect_err("numeric cell is invalid text");
665 assert_eq!(error.identifier(), ERASE_ERROR_INVALID_INPUT.identifier);
666 }
667
668 #[test]
669 fn erase_rejects_resident_numeric_pattern_before_gather() {
670 use crate::builtins::common::test_support;
671 use runmat_accelerate_api::HostTensorView;
672
673 test_support::with_test_provider(|provider| {
674 let handle = provider
675 .upload(&HostTensorView {
676 data: &[1.0],
677 shape: &[1, 1],
678 })
679 .expect("upload");
680 provider.reset_telemetry();
681 let error = erase_builtin(
682 Value::String("abc".into()),
683 Value::GpuTensor(handle.clone()),
684 )
685 .expect_err("resident numeric pattern is invalid");
686 assert_eq!(error.identifier(), ERASE_ERROR_PATTERN_TYPE.identifier);
687 assert_eq!(provider.telemetry_snapshot().download_bytes, 0);
688 provider.free(&handle).expect("free input");
689 });
690 }
691
692 #[test]
693 fn erase_dispatch_preserves_residency_until_builtin_validation() {
694 assert_eq!(GPU_SPEC.residency, ResidencyPolicy::NewHandle);
695 }
696
697 #[test]
698 fn erase_accepts_pattern_object() {
699 let pattern = crate::builtins::strings::core::compat::pattern_object(r"\d+");
700 let result = erase_builtin(Value::String("run42mat".into()), pattern).expect("erase");
701 assert_eq!(result, Value::String("runmat".into()));
702 }
703
704 #[test]
705 fn erase_type_preserves_text() {
706 assert_eq!(
707 text_preserve_type(&[Type::String], &ResolveContext::new(Vec::new())),
708 Type::String
709 );
710 }
711}