Skip to main content

runmat_runtime/builtins/strings/transform/
erase.rs

1//! MATLAB-compatible `erase` builtin with GPU-aware semantics for RunMat.
2use 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(&current, "").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}