1use 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(®ex).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}