Skip to main content

runmat_runtime/builtins/strings/transform/
erasebetween.rs

1//! MATLAB-compatible `eraseBetween` builtin with GPU-aware semantics for RunMat.
2
3use std::cmp::min;
4
5use regex::Regex;
6
7use crate::builtins::common::broadcast::{broadcast_index, broadcast_shapes, compute_strides};
8use crate::builtins::common::map_control_flow_with_builtin;
9use crate::builtins::strings::common::{char_row_to_string_slice, is_missing_string};
10use crate::builtins::strings::core::compat::pattern_regex;
11use crate::builtins::strings::type_resolvers::text_preserve_type;
12use crate::{
13    build_runtime_error, gather_if_needed_async, make_cell_with_shape, BuiltinResult, RuntimeError,
14};
15use runmat_builtins::{
16    BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinExtensionDescriptor,
17    BuiltinExtensionMode, BuiltinIntegerBackendRule, BuiltinIntegerCapabilityDescriptor,
18    BuiltinIntegerComputationDomain, BuiltinIntegerInputAvailability,
19    BuiltinIntegerInputCapability, BuiltinIntegerOutputClassRule, BuiltinIntegerOverflowRule,
20    BuiltinIntegerOverloadKind, BuiltinIntegerScalarDoubleRule, BuiltinOutputMode,
21    BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
22};
23use runmat_macros::runtime_builtin;
24use runmat_value::{CharArray, IntValue, NumericScalar, StringArray, Value};
25
26use crate::builtins::common::spec::{
27    BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
28    ReductionNaN, ResidencyPolicy, ShapeRequirements,
29};
30
31#[runmat_macros::register_gpu_spec(
32    builtin_path = "crate::builtins::strings::transform::erasebetween"
33)]
34pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
35    name: "eraseBetween",
36    op_kind: GpuOpKind::Custom("string-transform"),
37    supported_precisions: &[],
38    broadcast: BroadcastSemantics::Matlab,
39    provider_hooks: &[],
40    constant_strategy: ConstantStrategy::InlineLiteral,
41    residency: ResidencyPolicy::NewHandle,
42    nan_mode: ReductionNaN::Include,
43    two_pass_threshold: None,
44    workgroup_size: None,
45    accepts_nan_mode: false,
46    notes: "The builtin owns resident arguments so compatibility gates run before provider access; RunMat mode may then gather resident numeric position controls, while text and outputs remain on host.",
47};
48
49#[runmat_macros::register_fusion_spec(
50    builtin_path = "crate::builtins::strings::transform::erasebetween"
51)]
52pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
53    name: "eraseBetween",
54    shape: ShapeRequirements::Any,
55    constant_strategy: ConstantStrategy::InlineLiteral,
56    elementwise: None,
57    reduction: None,
58    emits_nan: false,
59    notes: "Pure string manipulation builtin; excluded from fusion plans, with resident numeric position controls handled only by an explicit extension.",
60};
61
62const BUILTIN_NAME: &str = "eraseBetween";
63
64const FULL_BROADCAST_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
65    id: "erasebetween-full-broadcast",
66    mode: BuiltinExtensionMode::RunMatOnly,
67    description: "eraseBetween with non-scalar boundary expansion is a RunMat extension",
68    error_identifier: Some("RunMat:compatibility:EraseBetweenFullBroadcastExtension"),
69};
70const RESIDENT_POSITION_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
71    id: "erasebetween-resident-position",
72    mode: BuiltinExtensionMode::RunMatOnly,
73    description: "eraseBetween with GPU-resident position arrays is a RunMat extension",
74    error_identifier: Some("RunMat:compatibility:EraseBetweenResidentPositionExtension"),
75};
76const CHAR_MATRIX_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
77    id: "erasebetween-char-matrix",
78    mode: BuiltinExtensionMode::RunMatOnly,
79    description: "eraseBetween row-wise character-matrix input is a RunMat extension",
80    error_identifier: Some("RunMat:compatibility:EraseBetweenCharMatrixExtension"),
81};
82const STRING_CELL_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
83    id: "erasebetween-string-cell",
84    mode: BuiltinExtensionMode::RunMatOnly,
85    description: "eraseBetween cells containing string scalars are a RunMat extension",
86    error_identifier: Some("RunMat:compatibility:EraseBetweenStringCellExtension"),
87};
88const EXTENSIONS: [BuiltinExtensionDescriptor; 4] = [
89    FULL_BROADCAST_EXTENSION,
90    RESIDENT_POSITION_EXTENSION,
91    CHAR_MATRIX_EXTENSION,
92    STRING_CELL_EXTENSION,
93];
94const INTEGER_POSITION_INPUTS: [BuiltinIntegerInputCapability; 2] = [
95    BuiltinIntegerInputCapability {
96        name: "startPos",
97        classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
98        availability: BuiltinIntegerInputAvailability::Documented,
99        scalar_double: BuiltinIntegerScalarDoubleRule::Allowed,
100        notes: "The public numeric position array accepts positive integer values; typed storage is parsed exactly with one-based indexing.",
101    },
102    BuiltinIntegerInputCapability {
103        name: "endPos",
104        classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
105        availability: BuiltinIntegerInputAvailability::Documented,
106        scalar_double: BuiltinIntegerScalarDoubleRule::Allowed,
107        notes: "The public numeric position array accepts positive integer values; typed storage is parsed exactly with one-based indexing.",
108    },
109];
110const INTEGER_TEXT_INPUT: [BuiltinIntegerInputCapability; 1] = [BuiltinIntegerInputCapability {
111    name: "str",
112    classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
113    availability: BuiltinIntegerInputAvailability::Rejected,
114    scalar_double: BuiltinIntegerScalarDoubleRule::Rejected,
115    notes:
116        "The public first argument is text; integer data is rejected before any provider access.",
117}];
118pub const INTEGER_CAPABILITIES: [BuiltinIntegerCapabilityDescriptor; 2] = [
119    BuiltinIntegerCapabilityDescriptor {
120        form: "newStr = eraseBetween(str, integer_startPos, integer_endPos)",
121        inputs: &INTEGER_POSITION_INPUTS,
122        computation_domain: BuiltinIntegerComputationDomain::Structural,
123        output_class: BuiltinIntegerOutputClassRule::FunctionSpecific,
124        overflow: BuiltinIntegerOverflowRule::Error,
125        backend: BuiltinIntegerBackendRule::HostOnly,
126        overload: BuiltinIntegerOverloadKind::SameSizeOrScalar,
127        notes: "Positions are exact structural controls; strict compatibility accepts scalar positions or arrays the same size as str and preserves the str container class.",
128    },
129    BuiltinIntegerCapabilityDescriptor {
130        form: "newStr = eraseBetween(integer_str, start, end)",
131        inputs: &INTEGER_TEXT_INPUT,
132        computation_domain: BuiltinIntegerComputationDomain::FunctionSpecific,
133        output_class: BuiltinIntegerOutputClassRule::NotApplicable,
134        overflow: BuiltinIntegerOverflowRule::NotApplicable,
135        backend: BuiltinIntegerBackendRule::HostOnly,
136        overload: BuiltinIntegerOverloadKind::FunctionSpecific,
137        notes: "Integer text input is outside the public text domain and is rejected without gathering.",
138    },
139];
140
141const ERASE_BETWEEN_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
142    name: "newText",
143    ty: BuiltinParamType::Any,
144    arity: BuiltinParamArity::Required,
145    default: None,
146    description: "Text with between-boundary content erased, preserving text container semantics.",
147}];
148
149const ERASE_BETWEEN_INPUTS_BASE: [BuiltinParamDescriptor; 3] = [
150    BuiltinParamDescriptor {
151        name: "str",
152        ty: BuiltinParamType::Any,
153        arity: BuiltinParamArity::Required,
154        default: None,
155        description: "Input text scalar/array/cell.",
156    },
157    BuiltinParamDescriptor {
158        name: "start",
159        ty: BuiltinParamType::Any,
160        arity: BuiltinParamArity::Required,
161        default: None,
162        description: "Start boundary marker text or positive integer position(s).",
163    },
164    BuiltinParamDescriptor {
165        name: "end",
166        ty: BuiltinParamType::Any,
167        arity: BuiltinParamArity::Required,
168        default: None,
169        description: "End boundary marker text or positive integer position(s).",
170    },
171];
172
173const ERASE_BETWEEN_INPUTS_NAME_VALUE: [BuiltinParamDescriptor; 5] = [
174    BuiltinParamDescriptor {
175        name: "str",
176        ty: BuiltinParamType::Any,
177        arity: BuiltinParamArity::Required,
178        default: None,
179        description: "Input text scalar/array/cell.",
180    },
181    BuiltinParamDescriptor {
182        name: "start",
183        ty: BuiltinParamType::Any,
184        arity: BuiltinParamArity::Required,
185        default: None,
186        description: "Start boundary marker text or positive integer position(s).",
187    },
188    BuiltinParamDescriptor {
189        name: "end",
190        ty: BuiltinParamType::Any,
191        arity: BuiltinParamArity::Required,
192        default: None,
193        description: "End boundary marker text or positive integer position(s).",
194    },
195    BuiltinParamDescriptor {
196        name: "Name",
197        ty: BuiltinParamType::StringScalar,
198        arity: BuiltinParamArity::Required,
199        default: None,
200        description: "Option name (`Boundaries`).",
201    },
202    BuiltinParamDescriptor {
203        name: "Value",
204        ty: BuiltinParamType::Any,
205        arity: BuiltinParamArity::Variadic,
206        default: None,
207        description: "Option value and additional Name/Value pairs.",
208    },
209];
210
211const ERASE_BETWEEN_SIGNATURES: [BuiltinSignatureDescriptor; 2] = [
212    BuiltinSignatureDescriptor {
213        label: "newText = eraseBetween(str, start, end)",
214        inputs: &ERASE_BETWEEN_INPUTS_BASE,
215        outputs: &ERASE_BETWEEN_OUTPUT,
216    },
217    BuiltinSignatureDescriptor {
218        label: "newText = eraseBetween(str, start, end, Name, Value, ...)",
219        inputs: &ERASE_BETWEEN_INPUTS_NAME_VALUE,
220        outputs: &ERASE_BETWEEN_OUTPUT,
221    },
222];
223
224const ERASE_BETWEEN_ERROR_INVALID_INPUT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
225    code: "RM.ERASE_BETWEEN.INVALID_INPUT",
226    identifier: Some("RunMat:eraseBetween:InvalidInput"),
227    when: "First argument is not a string array, character array, or cell array of text scalars.",
228    message:
229        "eraseBetween: first argument must be a string array, character array, or cell array of character vectors",
230};
231
232const ERASE_BETWEEN_ERROR_BOUNDARY_TYPE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
233    code: "RM.ERASE_BETWEEN.BOUNDARY_TYPE",
234    identifier: Some("RunMat:eraseBetween:BoundaryType"),
235    when: "Start/end boundaries are mixed text/numeric domains or use unsupported boundary types.",
236    message: "eraseBetween: start and end arguments must both be text or both be numeric positions",
237};
238
239const ERASE_BETWEEN_ERROR_POSITION_TYPE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
240    code: "RM.ERASE_BETWEEN.POSITION_TYPE",
241    identifier: Some("RunMat:eraseBetween:PositionType"),
242    when: "Numeric boundary positions are not positive finite integers.",
243    message: "eraseBetween: position arguments must be positive integers",
244};
245
246const ERASE_BETWEEN_ERROR_NAME_VALUE_PAIR: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
247    code: "RM.ERASE_BETWEEN.NAME_VALUE_PAIR",
248    identifier: Some("RunMat:eraseBetween:NameValuePair"),
249    when: "Name/value options are not supplied in complete pairs.",
250    message: "eraseBetween: name-value arguments must appear in pairs",
251};
252
253const ERASE_BETWEEN_ERROR_OPTION_NAME: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
254    code: "RM.ERASE_BETWEEN.OPTION_NAME",
255    identifier: Some("RunMat:eraseBetween:OptionName"),
256    when: "An option name other than `Boundaries` was supplied.",
257    message: "eraseBetween: unrecognized parameter name",
258};
259
260const ERASE_BETWEEN_ERROR_OPTION_VALUE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
261    code: "RM.ERASE_BETWEEN.OPTION_VALUE",
262    identifier: Some("RunMat:eraseBetween:OptionValue"),
263    when: "`Boundaries` option value is not `inclusive` or `exclusive`.",
264    message: "eraseBetween: 'Boundaries' must be either 'inclusive' or 'exclusive'",
265};
266
267const ERASE_BETWEEN_ERROR_CELL_ELEMENT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
268    code: "RM.ERASE_BETWEEN.CELL_ELEMENT",
269    identifier: Some("RunMat:eraseBetween:CellElement"),
270    when: "Cell text input/boundary contains non-text values or non-row char arrays.",
271    message: "eraseBetween: cell array elements must be string scalars or character vectors",
272};
273
274const ERASE_BETWEEN_ERROR_SIZE_MISMATCH: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
275    code: "RM.ERASE_BETWEEN.SIZE_MISMATCH",
276    identifier: Some("RunMat:eraseBetween:SizeMismatch"),
277    when: "Text/boundary inputs are not broadcast-compatible for erase semantics.",
278    message: "eraseBetween: boundary sizes must be compatible with the text input",
279};
280
281const ERASE_BETWEEN_ERROR_INTERNAL: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
282    code: "RM.ERASE_BETWEEN.INTERNAL",
283    identifier: Some("RunMat:eraseBetween:InternalError"),
284    when: "Internal output construction failed.",
285    message: "eraseBetween: internal error",
286};
287
288const ERASE_BETWEEN_ERRORS: [BuiltinErrorDescriptor; 9] = [
289    ERASE_BETWEEN_ERROR_INVALID_INPUT,
290    ERASE_BETWEEN_ERROR_BOUNDARY_TYPE,
291    ERASE_BETWEEN_ERROR_POSITION_TYPE,
292    ERASE_BETWEEN_ERROR_NAME_VALUE_PAIR,
293    ERASE_BETWEEN_ERROR_OPTION_NAME,
294    ERASE_BETWEEN_ERROR_OPTION_VALUE,
295    ERASE_BETWEEN_ERROR_CELL_ELEMENT,
296    ERASE_BETWEEN_ERROR_SIZE_MISMATCH,
297    ERASE_BETWEEN_ERROR_INTERNAL,
298];
299
300pub const ERASE_BETWEEN_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
301    signatures: &ERASE_BETWEEN_SIGNATURES,
302    output_mode: BuiltinOutputMode::Fixed,
303    completion_policy: BuiltinCompletionPolicy::Public,
304    errors: &ERASE_BETWEEN_ERRORS,
305};
306
307fn erase_between_error(error: &'static BuiltinErrorDescriptor) -> RuntimeError {
308    erase_between_error_with_message(error.message, error)
309}
310
311fn erase_between_error_with_message(
312    message: impl Into<String>,
313    error: &'static BuiltinErrorDescriptor,
314) -> RuntimeError {
315    let mut builder = build_runtime_error(message).with_builtin(BUILTIN_NAME);
316    if let Some(identifier) = error.identifier {
317        builder = builder.with_identifier(identifier);
318    }
319    builder.build()
320}
321
322fn map_flow(err: RuntimeError) -> RuntimeError {
323    map_control_flow_with_builtin(err, BUILTIN_NAME)
324}
325
326#[derive(Clone, Copy, Debug, PartialEq, Eq)]
327enum BoundariesMode {
328    Exclusive,
329    Inclusive,
330}
331
332#[runtime_builtin(
333    name = "eraseBetween",
334    category = "strings/transform",
335    summary = "Delete text between boundary markers.",
336    keywords = "eraseBetween,delete,boundaries,strings",
337    accel = "sink",
338    extensions(EXTENSIONS),
339    integer_capabilities(INTEGER_CAPABILITIES),
340    type_resolver(text_preserve_type),
341    descriptor(crate::builtins::strings::transform::erasebetween::ERASE_BETWEEN_DESCRIPTOR),
342    builtin_path = "crate::builtins::strings::transform::erasebetween"
343)]
344async fn erase_between_builtin(
345    text: Value,
346    start: Value,
347    stop: Value,
348    rest: Vec<Value>,
349) -> BuiltinResult<Value> {
350    if is_numeric_or_resident(&text) || contains_nested_numeric_or_resident(&text) {
351        return Err(erase_between_error(&ERASE_BETWEEN_ERROR_INVALID_INPUT));
352    }
353    if is_resident_or_contains_resident(&start) || is_resident_or_contains_resident(&stop) {
354        crate::compatibility::ensure_builtin_extension_enabled(
355            &RESIDENT_POSITION_EXTENSION,
356            BUILTIN_NAME,
357        )?;
358    }
359    if is_char_matrix(&text) || is_char_matrix(&start) || is_char_matrix(&stop) {
360        crate::compatibility::ensure_builtin_extension_enabled(
361            &CHAR_MATRIX_EXTENSION,
362            BUILTIN_NAME,
363        )?;
364    }
365    if cell_contains_string(&text) || cell_contains_string(&start) || cell_contains_string(&stop) {
366        crate::compatibility::ensure_builtin_extension_enabled(
367            &STRING_CELL_EXTENSION,
368            BUILTIN_NAME,
369        )?;
370    }
371    if raw_boundary_uses_full_broadcast(&text, &start)
372        || raw_boundary_uses_full_broadcast(&text, &stop)
373    {
374        crate::compatibility::ensure_builtin_extension_enabled(
375            &FULL_BROADCAST_EXTENSION,
376            BUILTIN_NAME,
377        )?;
378    }
379    let text = gather_if_needed_async(&text).await.map_err(map_flow)?;
380    let start = gather_if_needed_async(&start).await.map_err(map_flow)?;
381    let stop = gather_if_needed_async(&stop).await.map_err(map_flow)?;
382
383    let mode_override = parse_boundaries_option(&rest).await?;
384
385    let normalized_text = NormalizedText::from_value(text)?;
386    let start_boundary = BoundaryArg::from_value(start)?;
387    let stop_boundary = BoundaryArg::from_value(stop)?;
388
389    if start_boundary.kind() != stop_boundary.kind() {
390        return Err(erase_between_error(&ERASE_BETWEEN_ERROR_BOUNDARY_TYPE));
391    }
392    let boundary_kind = start_boundary.kind();
393    let effective_mode = mode_override.unwrap_or(match boundary_kind {
394        BoundaryKind::Text => BoundariesMode::Exclusive,
395        BoundaryKind::Position => BoundariesMode::Inclusive,
396    });
397
398    let start_shape = start_boundary.shape();
399    let stop_shape = stop_boundary.shape();
400    let text_shape = normalized_text.shape();
401
402    if !shape_is_scalar_or_same(start_shape, text_shape)
403        || !shape_is_scalar_or_same(stop_shape, text_shape)
404    {
405        crate::compatibility::ensure_builtin_extension_enabled(
406            &FULL_BROADCAST_EXTENSION,
407            BUILTIN_NAME,
408        )?;
409    }
410
411    let shape_ts = broadcast_shapes(BUILTIN_NAME, text_shape, start_shape).map_err(|err| {
412        erase_between_error_with_message(
413            format!("{}: {err}", ERASE_BETWEEN_ERROR_SIZE_MISMATCH.message),
414            &ERASE_BETWEEN_ERROR_SIZE_MISMATCH,
415        )
416    })?;
417    let output_shape = broadcast_shapes(BUILTIN_NAME, &shape_ts, stop_shape).map_err(|err| {
418        erase_between_error_with_message(
419            format!("{}: {err}", ERASE_BETWEEN_ERROR_SIZE_MISMATCH.message),
420            &ERASE_BETWEEN_ERROR_SIZE_MISMATCH,
421        )
422    })?;
423    if !normalized_text.supports_shape(&output_shape) {
424        return Err(erase_between_error(&ERASE_BETWEEN_ERROR_SIZE_MISMATCH));
425    }
426
427    let total: usize = output_shape.iter().copied().product();
428    if total == 0 {
429        return normalized_text.into_value(Vec::new(), output_shape);
430    }
431
432    let text_strides = compute_strides(text_shape);
433    let start_strides = compute_strides(start_shape);
434    let stop_strides = compute_strides(stop_shape);
435
436    let mut results = Vec::with_capacity(total);
437
438    for idx in 0..total {
439        let text_idx = broadcast_index(idx, &output_shape, text_shape, &text_strides);
440        let start_idx = broadcast_index(idx, &output_shape, start_shape, &start_strides);
441        let stop_idx = broadcast_index(idx, &output_shape, stop_shape, &stop_strides);
442
443        let result = match boundary_kind {
444            BoundaryKind::Text => {
445                let text_value = normalized_text.data(text_idx);
446                let start_value = start_boundary.marker(start_idx);
447                let stop_value = stop_boundary.marker(stop_idx);
448                erase_with_text_boundaries(text_value, start_value, stop_value, effective_mode)
449            }
450            BoundaryKind::Position => {
451                let text_value = normalized_text.data(text_idx);
452                let start_value = start_boundary.position(start_idx);
453                let stop_value = stop_boundary.position(stop_idx);
454                erase_with_positions(text_value, start_value, stop_value, effective_mode)
455            }
456        };
457        results.push(result);
458    }
459
460    normalized_text.into_value(results, output_shape)
461}
462
463fn is_numeric_or_resident(value: &Value) -> bool {
464    matches!(
465        value,
466        Value::Num(_)
467            | Value::Int(_)
468            | Value::Bool(_)
469            | Value::Tensor(_)
470            | Value::LogicalArray(_)
471            | Value::Complex(_, _)
472            | Value::ComplexTensor(_)
473            | Value::GpuTensor(_)
474    )
475}
476
477fn contains_nested_numeric_or_resident(value: &Value) -> bool {
478    match value {
479        Value::Cell(cell) => cell.data.iter().any(|value| {
480            is_numeric_or_resident(value) || contains_nested_numeric_or_resident(value)
481        }),
482        _ => false,
483    }
484}
485
486fn is_resident_or_contains_resident(value: &Value) -> bool {
487    match value {
488        Value::GpuTensor(_) => true,
489        Value::Cell(cell) => cell.data.iter().any(is_resident_or_contains_resident),
490        _ => false,
491    }
492}
493
494fn is_char_matrix(value: &Value) -> bool {
495    matches!(value, Value::CharArray(array) if array.rows > 1)
496}
497
498fn cell_contains_string(value: &Value) -> bool {
499    match value {
500        Value::Cell(cell) => cell.data.iter().any(|value| {
501            matches!(value, Value::String(_) | Value::StringArray(_)) || cell_contains_string(value)
502        }),
503        _ => false,
504    }
505}
506
507fn raw_text_shape(value: &Value) -> Option<Vec<usize>> {
508    match value {
509        Value::String(_) => Some(vec![1, 1]),
510        Value::StringArray(array) => Some(array.shape.clone()),
511        Value::CharArray(array) => Some(vec![array.rows, 1]),
512        Value::Cell(cell) => Some(cell.shape.clone()),
513        _ => None,
514    }
515}
516
517fn raw_boundary_shape(value: &Value) -> Option<Vec<usize>> {
518    match value {
519        Value::String(_) | Value::Num(_) | Value::Int(_) | Value::Object(_) => Some(vec![1, 1]),
520        Value::StringArray(array) => Some(array.shape.clone()),
521        Value::CharArray(array) => Some(vec![array.rows, 1]),
522        Value::Cell(cell) => Some(cell.shape.clone()),
523        Value::Tensor(tensor) => Some(tensor.shape.clone()),
524        Value::GpuTensor(handle) => Some(handle.shape.clone()),
525        _ => None,
526    }
527}
528
529fn raw_boundary_uses_full_broadcast(text: &Value, boundary: &Value) -> bool {
530    let (Some(text_shape), Some(boundary_shape)) =
531        (raw_text_shape(text), raw_boundary_shape(boundary))
532    else {
533        return false;
534    };
535    !shape_is_scalar_or_same(&boundary_shape, &text_shape)
536}
537
538fn shape_is_scalar_or_same(shape: &[usize], text_shape: &[usize]) -> bool {
539    shape
540        .iter()
541        .try_fold(1usize, |acc, dim| acc.checked_mul(*dim))
542        == Some(1)
543        || shape == text_shape
544}
545
546async fn parse_boundaries_option(args: &[Value]) -> BuiltinResult<Option<BoundariesMode>> {
547    if args.is_empty() {
548        return Ok(None);
549    }
550    if !args.len().is_multiple_of(2) {
551        return Err(erase_between_error(&ERASE_BETWEEN_ERROR_NAME_VALUE_PAIR));
552    }
553
554    let mut mode: Option<BoundariesMode> = None;
555    let mut idx = 0;
556    while idx < args.len() {
557        let name_value = gather_if_needed_async(&args[idx]).await.map_err(map_flow)?;
558        let name = value_to_string(&name_value)
559            .ok_or_else(|| erase_between_error(&ERASE_BETWEEN_ERROR_OPTION_NAME))?;
560        if !name.eq_ignore_ascii_case("boundaries") {
561            return Err(erase_between_error(&ERASE_BETWEEN_ERROR_OPTION_NAME));
562        }
563        let value = gather_if_needed_async(&args[idx + 1])
564            .await
565            .map_err(map_flow)?;
566        let value_str = value_to_string(&value)
567            .ok_or_else(|| erase_between_error(&ERASE_BETWEEN_ERROR_OPTION_VALUE))?;
568        let parsed_mode = if value_str.eq_ignore_ascii_case("inclusive") {
569            BoundariesMode::Inclusive
570        } else if value_str.eq_ignore_ascii_case("exclusive") {
571            BoundariesMode::Exclusive
572        } else {
573            return Err(erase_between_error(&ERASE_BETWEEN_ERROR_OPTION_VALUE));
574        };
575        mode = Some(parsed_mode);
576        idx += 2;
577    }
578    Ok(mode)
579}
580
581fn value_to_string(value: &Value) -> Option<String> {
582    match value {
583        Value::String(s) => Some(s.clone()),
584        Value::StringArray(sa) if sa.data.len() == 1 => Some(sa.data[0].clone()),
585        Value::CharArray(ca) if ca.rows <= 1 => {
586            if ca.rows == 0 {
587                Some(String::new())
588            } else {
589                Some(char_row_to_string_slice(&ca.data, ca.cols, 0))
590            }
591        }
592        Value::CharArray(_) => None,
593        Value::Cell(cell) if cell.data.len() == 1 => {
594            let element = &cell.data[0];
595            value_to_string(element)
596        }
597        _ => None,
598    }
599}
600
601#[derive(Clone)]
602struct EraseResult {
603    text: String,
604}
605
606impl EraseResult {
607    fn missing() -> Self {
608        Self {
609            text: "<missing>".to_string(),
610        }
611    }
612
613    fn text(text: String) -> Self {
614        Self { text }
615    }
616}
617
618fn erase_with_text_boundaries(
619    text: &str,
620    start: &BoundaryMarker,
621    stop: &BoundaryMarker,
622    mode: BoundariesMode,
623) -> EraseResult {
624    if is_missing_string(text) || start.is_missing() || stop.is_missing() {
625        return EraseResult::missing();
626    }
627
628    if let Some((start_idx, start_end)) = start.find(text) {
629        let search_start = start_end;
630        if search_start > text.len() {
631            return EraseResult::text(text.to_string());
632        }
633        if let Some((relative_end, relative_capture_end)) = stop.find(&text[search_start..]) {
634            let end_idx = search_start + relative_end;
635            match mode {
636                BoundariesMode::Inclusive => {
637                    let end_capture = min(text.len(), search_start + relative_capture_end);
638                    let mut result = String::with_capacity(text.len());
639                    result.push_str(&text[..start_idx]);
640                    result.push_str(&text[end_capture..]);
641                    EraseResult::text(result)
642                }
643                BoundariesMode::Exclusive => {
644                    let mut result = String::with_capacity(text.len());
645                    result.push_str(&text[..start_end]);
646                    result.push_str(&text[end_idx..]);
647                    EraseResult::text(result)
648                }
649            }
650        } else {
651            EraseResult::text(text.to_string())
652        }
653    } else {
654        EraseResult::text(text.to_string())
655    }
656}
657
658fn erase_with_positions(
659    text: &str,
660    start: usize,
661    stop: usize,
662    mode: BoundariesMode,
663) -> EraseResult {
664    if is_missing_string(text) {
665        return EraseResult::missing();
666    }
667    if text.is_empty() {
668        return EraseResult::text(String::new());
669    }
670    let chars: Vec<char> = text.chars().collect();
671    let len = chars.len();
672    if len == 0 {
673        return EraseResult::text(String::new());
674    }
675
676    if start == 0 || stop == 0 {
677        return EraseResult::text(text.to_string());
678    }
679
680    if start > len {
681        return EraseResult::text(text.to_string());
682    }
683    let stop_clamped = stop.min(len);
684
685    match mode {
686        BoundariesMode::Inclusive => {
687            if stop_clamped < start {
688                return EraseResult::text(text.to_string());
689            }
690            let start_idx = start - 1;
691            let end_idx = stop_clamped - 1;
692            if start_idx >= len || end_idx >= len || start_idx > end_idx {
693                EraseResult::text(text.to_string())
694            } else {
695                let mut result = String::with_capacity(len);
696                for (idx, ch) in chars.iter().enumerate() {
697                    if idx < start_idx || idx > end_idx {
698                        result.push(*ch);
699                    }
700                }
701                EraseResult::text(result)
702            }
703        }
704        BoundariesMode::Exclusive => {
705            if start + 1 >= stop_clamped {
706                return EraseResult::text(text.to_string());
707            }
708            let start_idx = start;
709            let end_idx = stop_clamped - 2;
710            if start_idx >= len || end_idx >= len || start_idx > end_idx {
711                EraseResult::text(text.to_string())
712            } else {
713                let mut result = String::with_capacity(len);
714                for (idx, ch) in chars.iter().enumerate() {
715                    if idx >= start_idx && idx <= end_idx {
716                        continue;
717                    }
718                    result.push(*ch);
719                }
720                EraseResult::text(result)
721            }
722        }
723    }
724}
725
726#[derive(Clone, Debug)]
727struct CellInfo {
728    shape: Vec<usize>,
729    element_kinds: Vec<CellElementKind>,
730}
731
732#[derive(Clone, Debug)]
733enum CellElementKind {
734    String,
735    Char,
736}
737
738#[derive(Clone, Debug)]
739enum TextKind {
740    StringScalar,
741    StringArray,
742    CharArray { rows: usize },
743    CellArray(CellInfo),
744}
745
746#[derive(Clone, Debug)]
747struct NormalizedText {
748    data: Vec<String>,
749    shape: Vec<usize>,
750    kind: TextKind,
751}
752
753impl NormalizedText {
754    fn from_value(value: Value) -> BuiltinResult<Self> {
755        match value {
756            Value::String(s) => Ok(Self {
757                data: vec![s],
758                shape: vec![1, 1],
759                kind: TextKind::StringScalar,
760            }),
761            Value::StringArray(sa) => Ok(Self {
762                data: sa.data.clone(),
763                shape: sa.shape.clone(),
764                kind: TextKind::StringArray,
765            }),
766            Value::CharArray(ca) => {
767                let rows = ca.rows;
768                let mut data = Vec::with_capacity(rows);
769                for row in 0..rows {
770                    data.push(char_row_to_string_slice(&ca.data, ca.cols, row));
771                }
772                Ok(Self {
773                    data,
774                    shape: vec![rows, 1],
775                    kind: TextKind::CharArray { rows },
776                })
777            }
778            Value::Cell(cell) => {
779                let shape = cell.shape.clone();
780                let mut data = Vec::with_capacity(cell.data.len());
781                let mut kinds = Vec::with_capacity(cell.data.len());
782                for element in &cell.data {
783                    match &element {
784                        Value::String(s) => {
785                            data.push(s.clone());
786                            kinds.push(CellElementKind::String);
787                        }
788                        Value::StringArray(sa) if sa.data.len() == 1 => {
789                            data.push(sa.data[0].clone());
790                            kinds.push(CellElementKind::String);
791                        }
792                        Value::CharArray(ca) if ca.rows <= 1 => {
793                            if ca.rows == 0 {
794                                data.push(String::new());
795                            } else {
796                                data.push(char_row_to_string_slice(&ca.data, ca.cols, 0));
797                            }
798                            kinds.push(CellElementKind::Char);
799                        }
800                        Value::CharArray(_) => {
801                            return Err(erase_between_error(&ERASE_BETWEEN_ERROR_CELL_ELEMENT))
802                        }
803                        _ => return Err(erase_between_error(&ERASE_BETWEEN_ERROR_CELL_ELEMENT)),
804                    }
805                }
806                Ok(Self {
807                    data,
808                    shape: shape.clone(),
809                    kind: TextKind::CellArray(CellInfo {
810                        shape,
811                        element_kinds: kinds,
812                    }),
813                })
814            }
815            _ => Err(erase_between_error(&ERASE_BETWEEN_ERROR_INVALID_INPUT)),
816        }
817    }
818
819    fn shape(&self) -> &[usize] {
820        &self.shape
821    }
822
823    fn data(&self, idx: usize) -> &str {
824        &self.data[idx]
825    }
826
827    fn supports_shape(&self, output_shape: &[usize]) -> bool {
828        match &self.kind {
829            TextKind::StringScalar => true,
830            TextKind::StringArray => true,
831            TextKind::CharArray { .. } => output_shape == self.shape,
832            TextKind::CellArray(info) => output_shape == info.shape,
833        }
834    }
835
836    fn into_value(
837        self,
838        results: Vec<EraseResult>,
839        output_shape: Vec<usize>,
840    ) -> BuiltinResult<Value> {
841        match self.kind {
842            TextKind::StringScalar => {
843                let total: usize = output_shape.iter().product();
844                if total == 0 {
845                    let data = results.into_iter().map(|r| r.text).collect::<Vec<_>>();
846                    let array = StringArray::new(data, output_shape).map_err(|e| {
847                        erase_between_error_with_message(
848                            format!("{BUILTIN_NAME}: {e}"),
849                            &ERASE_BETWEEN_ERROR_INTERNAL,
850                        )
851                    })?;
852                    return Ok(Value::StringArray(array));
853                }
854
855                if results.len() <= 1 {
856                    let value = results
857                        .into_iter()
858                        .next()
859                        .unwrap_or_else(|| EraseResult::text(String::new()));
860                    Ok(Value::String(value.text))
861                } else {
862                    let data = results.into_iter().map(|r| r.text).collect::<Vec<_>>();
863                    let array = StringArray::new(data, output_shape).map_err(|e| {
864                        erase_between_error_with_message(
865                            format!("{BUILTIN_NAME}: {e}"),
866                            &ERASE_BETWEEN_ERROR_INTERNAL,
867                        )
868                    })?;
869                    Ok(Value::StringArray(array))
870                }
871            }
872            TextKind::StringArray => {
873                let data = results.into_iter().map(|r| r.text).collect::<Vec<_>>();
874                let array = StringArray::new(data, output_shape).map_err(|e| {
875                    erase_between_error_with_message(
876                        format!("{BUILTIN_NAME}: {e}"),
877                        &ERASE_BETWEEN_ERROR_INTERNAL,
878                    )
879                })?;
880                Ok(Value::StringArray(array))
881            }
882            TextKind::CharArray { rows } => {
883                if rows == 0 {
884                    return CharArray::new(Vec::new(), 0, 0)
885                        .map(Value::CharArray)
886                        .map_err(|e| {
887                            erase_between_error_with_message(
888                                format!("{BUILTIN_NAME}: {e}"),
889                                &ERASE_BETWEEN_ERROR_INTERNAL,
890                            )
891                        });
892                }
893                if results.len() != rows {
894                    return Err(erase_between_error(&ERASE_BETWEEN_ERROR_SIZE_MISMATCH));
895                }
896                let mut max_width = 0usize;
897                let mut row_strings = Vec::with_capacity(rows);
898                for result in &results {
899                    let width = result.text.chars().count();
900                    max_width = max_width.max(width);
901                    row_strings.push(result.text.clone());
902                }
903                let mut flattened = Vec::with_capacity(rows * max_width);
904                for row in row_strings {
905                    let mut chars: Vec<char> = row.chars().collect();
906                    if chars.len() < max_width {
907                        chars.resize(max_width, ' ');
908                    }
909                    flattened.extend(chars);
910                }
911                CharArray::new(flattened, rows, max_width)
912                    .map(Value::CharArray)
913                    .map_err(|e| {
914                        erase_between_error_with_message(
915                            format!("{BUILTIN_NAME}: {e}"),
916                            &ERASE_BETWEEN_ERROR_INTERNAL,
917                        )
918                    })
919            }
920            TextKind::CellArray(info) => {
921                if results.len() != info.element_kinds.len() {
922                    return Err(erase_between_error(&ERASE_BETWEEN_ERROR_SIZE_MISMATCH));
923                }
924                let mut values = Vec::with_capacity(results.len());
925                for (idx, result) in results.into_iter().enumerate() {
926                    match info.element_kinds[idx] {
927                        CellElementKind::String => values.push(Value::String(result.text)),
928                        CellElementKind::Char => {
929                            let ca = CharArray::new_row(&result.text);
930                            values.push(Value::CharArray(ca));
931                        }
932                    }
933                }
934                make_cell_with_shape(values, info.shape).map_err(|e| {
935                    erase_between_error_with_message(
936                        format!("{BUILTIN_NAME}: {e}"),
937                        &ERASE_BETWEEN_ERROR_INTERNAL,
938                    )
939                })
940            }
941        }
942    }
943}
944
945#[derive(Clone, Debug, PartialEq, Eq)]
946enum BoundaryKind {
947    Text,
948    Position,
949}
950
951#[derive(Clone, Debug)]
952enum BoundaryArg {
953    Text(BoundaryText),
954    Position(BoundaryPositions),
955}
956
957impl BoundaryArg {
958    fn from_value(value: Value) -> BuiltinResult<Self> {
959        match value {
960            Value::String(_)
961            | Value::StringArray(_)
962            | Value::CharArray(_)
963            | Value::Cell(_)
964            | Value::Object(_) => BoundaryText::from_value(value).map(BoundaryArg::Text),
965            Value::Num(_) | Value::Int(_) | Value::Tensor(_) => {
966                BoundaryPositions::from_value(value).map(BoundaryArg::Position)
967            }
968            other => Err(erase_between_error_with_message(
969                format!(
970                    "{}: unsupported argument {other:?}",
971                    ERASE_BETWEEN_ERROR_BOUNDARY_TYPE.message
972                ),
973                &ERASE_BETWEEN_ERROR_BOUNDARY_TYPE,
974            )),
975        }
976    }
977
978    fn kind(&self) -> BoundaryKind {
979        match self {
980            BoundaryArg::Text(_) => BoundaryKind::Text,
981            BoundaryArg::Position(_) => BoundaryKind::Position,
982        }
983    }
984
985    fn shape(&self) -> &[usize] {
986        match self {
987            BoundaryArg::Text(text) => &text.shape,
988            BoundaryArg::Position(pos) => &pos.shape,
989        }
990    }
991
992    fn marker(&self, idx: usize) -> &BoundaryMarker {
993        match self {
994            BoundaryArg::Text(text) => &text.data[idx],
995            BoundaryArg::Position(_) => unreachable!(),
996        }
997    }
998
999    fn position(&self, idx: usize) -> usize {
1000        match self {
1001            BoundaryArg::Position(pos) => pos.data[idx],
1002            BoundaryArg::Text(_) => unreachable!(),
1003        }
1004    }
1005}
1006
1007#[derive(Clone, Debug)]
1008struct BoundaryText {
1009    data: Vec<BoundaryMarker>,
1010    shape: Vec<usize>,
1011}
1012
1013#[derive(Clone, Debug)]
1014enum BoundaryMarker {
1015    Literal(String),
1016    Pattern(Regex),
1017}
1018
1019impl BoundaryMarker {
1020    fn is_missing(&self) -> bool {
1021        matches!(self, Self::Literal(text) if is_missing_string(text))
1022    }
1023
1024    fn find(&self, text: &str) -> Option<(usize, usize)> {
1025        match self {
1026            Self::Literal(marker) => text
1027                .find(marker)
1028                .map(|start| (start, start.saturating_add(marker.len()))),
1029            Self::Pattern(pattern) => pattern
1030                .find(text)
1031                .map(|matched| (matched.start(), matched.end())),
1032        }
1033    }
1034}
1035
1036impl BoundaryText {
1037    fn from_value(value: Value) -> BuiltinResult<Self> {
1038        match value {
1039            Value::String(s) => Ok(Self {
1040                data: vec![BoundaryMarker::Literal(s)],
1041                shape: vec![1, 1],
1042            }),
1043            Value::StringArray(sa) => Ok(Self {
1044                data: sa
1045                    .data
1046                    .iter()
1047                    .cloned()
1048                    .map(BoundaryMarker::Literal)
1049                    .collect(),
1050                shape: sa.shape.clone(),
1051            }),
1052            Value::CharArray(ca) => {
1053                let mut data = Vec::with_capacity(ca.rows);
1054                for row in 0..ca.rows {
1055                    data.push(BoundaryMarker::Literal(char_row_to_string_slice(
1056                        &ca.data, ca.cols, row,
1057                    )));
1058                }
1059                Ok(Self {
1060                    data,
1061                    shape: vec![ca.rows, 1],
1062                })
1063            }
1064            Value::Cell(cell) => {
1065                let shape = cell.shape.clone();
1066                let mut data = Vec::with_capacity(cell.data.len());
1067                for element in &cell.data {
1068                    match &element {
1069                        Value::String(s) => data.push(BoundaryMarker::Literal(s.clone())),
1070                        Value::StringArray(sa) if sa.data.len() == 1 => {
1071                            data.push(BoundaryMarker::Literal(sa.data[0].clone()));
1072                        }
1073                        Value::CharArray(ca) if ca.rows <= 1 => {
1074                            if ca.rows == 0 {
1075                                data.push(BoundaryMarker::Literal(String::new()));
1076                            } else {
1077                                data.push(BoundaryMarker::Literal(char_row_to_string_slice(
1078                                    &ca.data, ca.cols, 0,
1079                                )));
1080                            }
1081                        }
1082                        Value::CharArray(_) => {
1083                            return Err(erase_between_error(&ERASE_BETWEEN_ERROR_CELL_ELEMENT))
1084                        }
1085                        _ => return Err(erase_between_error(&ERASE_BETWEEN_ERROR_CELL_ELEMENT)),
1086                    }
1087                }
1088                Ok(Self { data, shape })
1089            }
1090            object @ Value::Object(_) => {
1091                let regex = pattern_regex(&object, BUILTIN_NAME).map_err(|error| {
1092                    erase_between_error_with_message(
1093                        error.message().to_string(),
1094                        &ERASE_BETWEEN_ERROR_BOUNDARY_TYPE,
1095                    )
1096                })?;
1097                let pattern = Regex::new(&regex).map_err(|error| {
1098                    erase_between_error_with_message(
1099                        format!("{}: {error}", ERASE_BETWEEN_ERROR_BOUNDARY_TYPE.message),
1100                        &ERASE_BETWEEN_ERROR_BOUNDARY_TYPE,
1101                    )
1102                })?;
1103                Ok(Self {
1104                    data: vec![BoundaryMarker::Pattern(pattern)],
1105                    shape: vec![1, 1],
1106                })
1107            }
1108            _ => Err(erase_between_error(&ERASE_BETWEEN_ERROR_BOUNDARY_TYPE)),
1109        }
1110    }
1111}
1112
1113#[derive(Clone, Debug)]
1114struct BoundaryPositions {
1115    data: Vec<usize>,
1116    shape: Vec<usize>,
1117}
1118
1119impl BoundaryPositions {
1120    fn from_value(value: Value) -> BuiltinResult<Self> {
1121        match value {
1122            Value::Num(n) => Ok(Self {
1123                data: vec![parse_position(n)?],
1124                shape: vec![1, 1],
1125            }),
1126            Value::Int(i) => Ok(Self {
1127                data: vec![parse_position_int(i)?],
1128                shape: vec![1, 1],
1129            }),
1130            Value::Tensor(t) => {
1131                let mut data = Vec::with_capacity(t.len());
1132                for idx in 0..t.len() {
1133                    let entry = t
1134                        .numeric_value_at(idx)
1135                        .ok_or_else(|| erase_between_error(&ERASE_BETWEEN_ERROR_POSITION_TYPE))?;
1136                    data.push(match entry {
1137                        NumericScalar::F64(value) => parse_position(value)?,
1138                        NumericScalar::F32(value) => parse_position(f64::from(value))?,
1139                        value => parse_position_int(
1140                            value
1141                                .into_int_value()
1142                                .expect("non-floating numeric scalar is integer"),
1143                        )?,
1144                    });
1145                }
1146                Ok(Self {
1147                    data,
1148                    shape: if t.shape.is_empty() {
1149                        vec![t.rows, t.cols.max(1)]
1150                    } else {
1151                        t.shape
1152                    },
1153                })
1154            }
1155            _ => Err(erase_between_error(&ERASE_BETWEEN_ERROR_BOUNDARY_TYPE)),
1156        }
1157    }
1158}
1159
1160fn parse_position(value: f64) -> BuiltinResult<usize> {
1161    if !value.is_finite() || value < 1.0 {
1162        return Err(erase_between_error(&ERASE_BETWEEN_ERROR_POSITION_TYPE));
1163    }
1164    if (value.fract()).abs() > f64::EPSILON {
1165        return Err(erase_between_error(&ERASE_BETWEEN_ERROR_POSITION_TYPE));
1166    }
1167    if value > usize::MAX.saturating_sub(1) as f64 {
1168        return Err(erase_between_error(&ERASE_BETWEEN_ERROR_POSITION_TYPE));
1169    }
1170    let parsed = value as usize;
1171    if parsed as f64 != value || parsed == usize::MAX {
1172        return Err(erase_between_error(&ERASE_BETWEEN_ERROR_POSITION_TYPE));
1173    }
1174    Ok(parsed)
1175}
1176
1177fn parse_position_int(value: IntValue) -> BuiltinResult<usize> {
1178    value
1179        .try_to_usize()
1180        .filter(|position| *position > 0)
1181        .ok_or_else(|| erase_between_error(&ERASE_BETWEEN_ERROR_POSITION_TYPE))
1182}
1183
1184#[cfg(test)]
1185pub(crate) mod tests {
1186    #![allow(non_snake_case)]
1187
1188    use super::*;
1189    use runmat_builtins::{ResolveContext, Type};
1190    use runmat_value::{CellArray, CharArray, IntegerStorage, NumericStorage, StringArray, Tensor};
1191
1192    fn erase_between_builtin(
1193        text: Value,
1194        start: Value,
1195        stop: Value,
1196        rest: Vec<Value>,
1197    ) -> BuiltinResult<Value> {
1198        futures::executor::block_on(super::erase_between_builtin(text, start, stop, rest))
1199    }
1200
1201    #[test]
1202    fn eraseBetween_position_vectors_read_typed_integer_storage_exactly() {
1203        let positions =
1204            Tensor::new_integer(IntegerStorage::U16(vec![2, 4]), vec![1, 2]).expect("positions");
1205
1206        let parsed = BoundaryPositions::from_value(Value::Tensor(positions)).unwrap();
1207        assert_eq!(parsed.data, vec![2, 4]);
1208        assert_eq!(parsed.shape, vec![1, 2]);
1209    }
1210
1211    #[test]
1212    fn eraseBetween_position_vectors_read_native_single_storage() {
1213        let positions =
1214            Tensor::from_numeric_storage(NumericStorage::F32(vec![2.0, 4.0]), vec![1, 2])
1215                .expect("positions");
1216        let parsed = BoundaryPositions::from_value(Value::Tensor(positions)).unwrap();
1217        assert_eq!(parsed.data, vec![2, 4]);
1218    }
1219
1220    #[test]
1221    fn eraseBetween_position_vectors_reject_nonpositive_integer_storage() {
1222        let positions =
1223            Tensor::new_integer(IntegerStorage::I16(vec![1, 0]), vec![1, 2]).expect("positions");
1224
1225        assert!(BoundaryPositions::from_value(Value::Tensor(positions)).is_err());
1226    }
1227
1228    #[test]
1229    fn eraseBetween_position_vectors_reject_oversized_double_positions() {
1230        assert!(BoundaryPositions::from_value(Value::Num(1.0e300)).is_err());
1231    }
1232
1233    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1234    #[test]
1235    fn eraseBetween_text_default_exclusive() {
1236        let result = erase_between_builtin(
1237            Value::String("The quick brown fox".into()),
1238            Value::String("quick".into()),
1239            Value::String(" fox".into()),
1240            Vec::new(),
1241        )
1242        .expect("eraseBetween");
1243        assert_eq!(result, Value::String("The quick fox".into()));
1244    }
1245
1246    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1247    #[test]
1248    fn eraseBetween_text_inclusive_option() {
1249        let result = erase_between_builtin(
1250            Value::String("The quick brown fox jumps over the lazy dog".into()),
1251            Value::String(" brown".into()),
1252            Value::String("lazy".into()),
1253            vec![
1254                Value::String("Boundaries".into()),
1255                Value::String("inclusive".into()),
1256            ],
1257        )
1258        .expect("eraseBetween");
1259        assert_eq!(result, Value::String("The quick dog".into()));
1260    }
1261
1262    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1263    #[test]
1264    fn eraseBetween_numeric_positions_default_inclusive() {
1265        let result = erase_between_builtin(
1266            Value::String("Edgar Allen Poe".into()),
1267            Value::Num(6.0),
1268            Value::Num(11.0),
1269            Vec::new(),
1270        )
1271        .expect("eraseBetween");
1272        assert_eq!(result, Value::String("Edgar Poe".into()));
1273    }
1274
1275    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1276    #[test]
1277    fn eraseBetween_numeric_positions_int_inputs() {
1278        let result = erase_between_builtin(
1279            Value::String("abcdef".into()),
1280            Value::Int(IntValue::I32(2)),
1281            Value::Int(IntValue::I32(5)),
1282            Vec::new(),
1283        )
1284        .expect("eraseBetween");
1285        assert_eq!(result, Value::String("af".into()));
1286    }
1287
1288    #[test]
1289    fn eraseBetween_full_broadcast_is_mode_gated() {
1290        let text = StringArray::new(vec!["abcd".into(), "efgh".into()], vec![2, 1]).unwrap();
1291        let start = Tensor::new_integer(IntegerStorage::U8(vec![1, 2]), vec![1, 2]).unwrap();
1292        let stop = Tensor::new_integer(IntegerStorage::U8(vec![2, 3]), vec![1, 2]).unwrap();
1293        let _compat = crate::compatibility::push_runmat_extensions_enabled(false);
1294        let error = erase_between_builtin(
1295            Value::StringArray(text),
1296            Value::Tensor(start),
1297            Value::Tensor(stop),
1298            Vec::new(),
1299        )
1300        .expect_err("general broadcasting is an extension");
1301        assert_eq!(
1302            error.identifier(),
1303            FULL_BROADCAST_EXTENSION.error_identifier
1304        );
1305    }
1306
1307    #[test]
1308    fn eraseBetween_resident_positions_are_gated_before_gather() {
1309        use crate::builtins::common::{gpu_helpers, test_support};
1310
1311        test_support::with_test_provider(|provider| {
1312            let positions = Tensor::new_integer(IntegerStorage::U64(vec![2]), vec![1, 1]).unwrap();
1313            let handle = gpu_helpers::upload_tensor(provider, &positions).expect("upload");
1314            provider.reset_telemetry();
1315            let _compat = crate::compatibility::push_runmat_extensions_enabled(false);
1316            let error = erase_between_builtin(
1317                Value::String("abcd".into()),
1318                Value::GpuTensor(handle.clone()),
1319                Value::Num(3.0),
1320                Vec::new(),
1321            )
1322            .expect_err("resident positions are an extension");
1323            assert_eq!(
1324                error.identifier(),
1325                RESIDENT_POSITION_EXTENSION.error_identifier
1326            );
1327            assert_eq!(provider.telemetry_snapshot().download_bytes, 0);
1328            provider.free(&handle).expect("free input");
1329        });
1330    }
1331
1332    #[test]
1333    fn eraseBetween_dispatch_preserves_residency_until_builtin_validation() {
1334        assert_eq!(GPU_SPEC.residency, ResidencyPolicy::NewHandle);
1335    }
1336
1337    #[test]
1338    fn eraseBetween_accepts_scalar_pattern_boundaries() {
1339        let start = crate::builtins::strings::core::compat::pattern_object(r"<[^>]*>");
1340        let stop = crate::builtins::strings::core::compat::pattern_object(r"</[^>]*>");
1341        let result = erase_between_builtin(
1342            Value::String("<course>algebra</course>".into()),
1343            start,
1344            stop,
1345            Vec::new(),
1346        )
1347        .expect("pattern boundaries");
1348        assert_eq!(result, Value::String("<course></course>".into()));
1349    }
1350
1351    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1352    #[test]
1353    fn eraseBetween_numeric_positions_exclusive_option() {
1354        let result = erase_between_builtin(
1355            Value::String("small|medium|large".into()),
1356            Value::Num(6.0),
1357            Value::Num(13.0),
1358            vec![
1359                Value::String("Boundaries".into()),
1360                Value::String("exclusive".into()),
1361            ],
1362        )
1363        .expect("eraseBetween");
1364        assert_eq!(result, Value::String("small||large".into()));
1365    }
1366
1367    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1368    #[test]
1369    fn eraseBetween_start_not_found_returns_original() {
1370        let result = erase_between_builtin(
1371            Value::String("RunMat Accelerate".into()),
1372            Value::String("<".into()),
1373            Value::String(">".into()),
1374            Vec::new(),
1375        )
1376        .expect("eraseBetween");
1377        assert_eq!(result, Value::String("RunMat Accelerate".into()));
1378    }
1379
1380    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1381    #[test]
1382    fn eraseBetween_stop_not_found_returns_original() {
1383        let result = erase_between_builtin(
1384            Value::String("Device<GPU>".into()),
1385            Value::String("<".into()),
1386            Value::String(")".into()),
1387            Vec::new(),
1388        )
1389        .expect("eraseBetween");
1390        assert_eq!(result, Value::String("Device<GPU>".into()));
1391    }
1392
1393    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1394    #[test]
1395    fn eraseBetween_missing_string_propagates() {
1396        let strings = StringArray::new(vec!["<missing>".into()], vec![1, 1]).unwrap();
1397        let result = erase_between_builtin(
1398            Value::StringArray(strings),
1399            Value::String("<".into()),
1400            Value::String(">".into()),
1401            Vec::new(),
1402        )
1403        .expect("eraseBetween");
1404        assert_eq!(
1405            result,
1406            Value::StringArray(StringArray::new(vec!["<missing>".into()], vec![1, 1]).unwrap())
1407        );
1408    }
1409
1410    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1411    #[test]
1412    fn eraseBetween_zero_sized_broadcast_produces_empty_array() {
1413        let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
1414        let start = StringArray::new(Vec::new(), vec![0, 1]).unwrap();
1415        let stop = StringArray::new(Vec::new(), vec![0, 1]).unwrap();
1416        let result = erase_between_builtin(
1417            Value::String("abc".into()),
1418            Value::StringArray(start),
1419            Value::StringArray(stop),
1420            Vec::new(),
1421        )
1422        .expect("eraseBetween");
1423        match result {
1424            Value::StringArray(sa) => {
1425                assert_eq!(sa.data.len(), 0);
1426                assert_eq!(sa.shape, vec![0, 1]);
1427            }
1428            other => panic!("expected string array, got {other:?}"),
1429        }
1430    }
1431
1432    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1433    #[test]
1434    fn eraseBetween_numeric_positions_array() {
1435        let text = StringArray::new(vec!["abcd".into(), "wxyz".into()], vec![2, 1]).unwrap();
1436        let start = Tensor::new(vec![1.0, 2.0], vec![2, 1]).unwrap();
1437        let stop = Tensor::new(vec![3.0, 4.0], vec![2, 1]).unwrap();
1438        let result = erase_between_builtin(
1439            Value::StringArray(text),
1440            Value::Tensor(start),
1441            Value::Tensor(stop),
1442            Vec::new(),
1443        )
1444        .expect("eraseBetween");
1445        match result {
1446            Value::StringArray(sa) => {
1447                assert_eq!(sa.data, vec!["d".to_string(), "w".to_string()]);
1448                assert_eq!(sa.shape, vec![2, 1]);
1449            }
1450            other => panic!("expected string array, got {other:?}"),
1451        }
1452    }
1453
1454    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1455    #[test]
1456    fn eraseBetween_cell_array_preserves_types() {
1457        let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
1458        let cell = CellArray::new(
1459            vec![
1460                Value::CharArray(CharArray::new_row("A[B]C")),
1461                Value::String("Planner<GPU>".into()),
1462            ],
1463            1,
1464            2,
1465        )
1466        .unwrap();
1467        let start = CellArray::new(
1468            vec![Value::String("[".into()), Value::String("<".into())],
1469            1,
1470            2,
1471        )
1472        .unwrap();
1473        let stop = CellArray::new(
1474            vec![Value::String("]".into()), Value::String(">".into())],
1475            1,
1476            2,
1477        )
1478        .unwrap();
1479        let result = erase_between_builtin(
1480            Value::Cell(cell),
1481            Value::Cell(start),
1482            Value::Cell(stop),
1483            vec![
1484                Value::String("Boundaries".into()),
1485                Value::String("inclusive".into()),
1486            ],
1487        )
1488        .expect("eraseBetween");
1489        match result {
1490            Value::Cell(out) => {
1491                let first = out.get(0, 0).unwrap();
1492                let second = out.get(0, 1).unwrap();
1493                assert_eq!(first, Value::CharArray(CharArray::new_row("AC")));
1494                assert_eq!(second, Value::String("Planner".into()));
1495            }
1496            other => panic!("expected cell array, got {other:?}"),
1497        }
1498    }
1499
1500    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1501    #[test]
1502    fn eraseBetween_char_array_default_and_inclusive() {
1503        let chars =
1504            CharArray::new("Device<GPU>".chars().collect(), 1, "Device<GPU>".len()).unwrap();
1505        let default = erase_between_builtin(
1506            Value::CharArray(chars.clone()),
1507            Value::String("<".into()),
1508            Value::String(">".into()),
1509            Vec::new(),
1510        )
1511        .expect("eraseBetween");
1512        match default {
1513            Value::CharArray(out) => {
1514                let text: String = out.data.iter().collect();
1515                assert_eq!(text.trim_end(), "Device<>");
1516            }
1517            other => panic!("expected char array, got {other:?}"),
1518        }
1519
1520        let inclusive = erase_between_builtin(
1521            Value::CharArray(chars),
1522            Value::String("<".into()),
1523            Value::String(">".into()),
1524            vec![
1525                Value::String("Boundaries".into()),
1526                Value::String("inclusive".into()),
1527            ],
1528        )
1529        .expect("eraseBetween");
1530        match inclusive {
1531            Value::CharArray(out) => {
1532                let text: String = out.data.iter().collect();
1533                assert_eq!(text.trim_end(), "Device");
1534            }
1535            other => panic!("expected char array, got {other:?}"),
1536        }
1537    }
1538
1539    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1540    #[test]
1541    fn eraseBetween_option_with_char_arrays_case_insensitive() {
1542        let result = erase_between_builtin(
1543            Value::String("A<mid>B".into()),
1544            Value::String("<".into()),
1545            Value::String(">".into()),
1546            vec![
1547                Value::CharArray(CharArray::new_row("Boundaries")),
1548                Value::CharArray(CharArray::new_row("INCLUSIVE")),
1549            ],
1550        )
1551        .expect("eraseBetween");
1552        assert_eq!(result, Value::String("AB".into()));
1553    }
1554
1555    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1556    #[test]
1557    fn eraseBetween_text_scalar_broadcast() {
1558        let text =
1559            StringArray::new(vec!["alpha[GPU]".into(), "beta[GPU]".into()], vec![2, 1]).unwrap();
1560        let result = erase_between_builtin(
1561            Value::StringArray(text),
1562            Value::String("[".into()),
1563            Value::String("]".into()),
1564            Vec::new(),
1565        )
1566        .expect("eraseBetween");
1567        match result {
1568            Value::StringArray(sa) => {
1569                assert_eq!(sa.data, vec!["alpha[]".to_string(), "beta[]".to_string()]);
1570            }
1571            other => panic!("expected string array, got {other:?}"),
1572        }
1573    }
1574
1575    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1576    #[test]
1577    fn eraseBetween_option_invalid_value() {
1578        let err = erase_between_builtin(
1579            Value::String("abc".into()),
1580            Value::String("a".into()),
1581            Value::String("c".into()),
1582            vec![
1583                Value::String("Boundaries".into()),
1584                Value::String("middle".into()),
1585            ],
1586        )
1587        .unwrap_err();
1588        assert_eq!(err.to_string(), ERASE_BETWEEN_ERROR_OPTION_VALUE.message);
1589        assert_eq!(
1590            err.identifier(),
1591            ERASE_BETWEEN_ERROR_OPTION_VALUE.identifier
1592        );
1593    }
1594
1595    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1596    #[test]
1597    fn eraseBetween_option_name_error() {
1598        let err = erase_between_builtin(
1599            Value::String("abc".into()),
1600            Value::String("a".into()),
1601            Value::String("c".into()),
1602            vec![
1603                Value::String("Padding".into()),
1604                Value::String("inclusive".into()),
1605            ],
1606        )
1607        .unwrap_err();
1608        assert_eq!(err.to_string(), ERASE_BETWEEN_ERROR_OPTION_NAME.message);
1609        assert_eq!(err.identifier(), ERASE_BETWEEN_ERROR_OPTION_NAME.identifier);
1610    }
1611
1612    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1613    #[test]
1614    fn eraseBetween_option_pair_error() {
1615        let err = erase_between_builtin(
1616            Value::String("abc".into()),
1617            Value::String("a".into()),
1618            Value::String("b".into()),
1619            vec![Value::String("Boundaries".into())],
1620        )
1621        .unwrap_err();
1622        assert_eq!(err.to_string(), ERASE_BETWEEN_ERROR_NAME_VALUE_PAIR.message);
1623        assert_eq!(
1624            err.identifier(),
1625            ERASE_BETWEEN_ERROR_NAME_VALUE_PAIR.identifier
1626        );
1627    }
1628
1629    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1630    #[test]
1631    fn eraseBetween_position_type_error() {
1632        let err = erase_between_builtin(
1633            Value::String("abc".into()),
1634            Value::Num(0.5),
1635            Value::Num(2.0),
1636            Vec::new(),
1637        )
1638        .unwrap_err();
1639        assert_eq!(err.to_string(), ERASE_BETWEEN_ERROR_POSITION_TYPE.message);
1640        assert_eq!(
1641            err.identifier(),
1642            ERASE_BETWEEN_ERROR_POSITION_TYPE.identifier
1643        );
1644    }
1645
1646    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1647    #[test]
1648    fn eraseBetween_mixed_boundary_error() {
1649        let err = erase_between_builtin(
1650            Value::String("abc".into()),
1651            Value::String("a".into()),
1652            Value::Num(3.0),
1653            Vec::new(),
1654        )
1655        .unwrap_err();
1656        assert_eq!(err.to_string(), ERASE_BETWEEN_ERROR_BOUNDARY_TYPE.message);
1657        assert_eq!(
1658            err.identifier(),
1659            ERASE_BETWEEN_ERROR_BOUNDARY_TYPE.identifier
1660        );
1661    }
1662
1663    #[test]
1664    fn erase_between_type_preserves_text() {
1665        assert_eq!(
1666            text_preserve_type(&[Type::String], &ResolveContext::new(Vec::new())),
1667            Type::String
1668        );
1669    }
1670}