1use regex::Regex;
4use runmat_builtins::{
5 BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
6 BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
7};
8use runmat_builtins::{BuiltinIntegerAuditDescriptor, BuiltinIntegerAuditKind};
9use runmat_macros::runtime_builtin;
10use runmat_value::{CellArray, CharArray, StringArray, Value};
11
12use crate::builtins::common::map_control_flow_with_builtin;
13use crate::builtins::common::spec::{
14 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
15 ReductionNaN, ResidencyPolicy, ShapeRequirements,
16};
17use crate::builtins::strings::common::{
18 char_row_to_string_slice, contains_resident_text_input, is_missing_string,
19};
20use crate::builtins::strings::core::compat::pattern_regex;
21use crate::builtins::strings::type_resolvers::text_preserve_type;
22use crate::{build_runtime_error, gather_if_needed_async, make_cell, BuiltinResult, RuntimeError};
23
24#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::strings::transform::replace")]
25pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
26 name: "replace",
27 op_kind: GpuOpKind::Custom("string-transform"),
28 supported_precisions: &[],
29 broadcast: BroadcastSemantics::None,
30 provider_hooks: &[],
31 constant_strategy: ConstantStrategy::InlineLiteral,
32 residency: ResidencyPolicy::GatherImmediately,
33 nan_mode: ReductionNaN::Include,
34 two_pass_threshold: None,
35 workgroup_size: None,
36 accepts_nan_mode: false,
37 notes:
38 "Executes on the CPU; GPU-resident inputs are gathered to host memory prior to replacement.",
39};
40
41#[runmat_macros::register_fusion_spec(
42 builtin_path = "crate::builtins::strings::transform::replace"
43)]
44pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
45 name: "replace",
46 shape: ShapeRequirements::Any,
47 constant_strategy: ConstantStrategy::InlineLiteral,
48 elementwise: None,
49 reduction: None,
50 emits_nan: false,
51 notes:
52 "String manipulation builtin; not eligible for fusion plans and always gathers GPU inputs.",
53};
54
55const BUILTIN_NAME: &str = "replace";
56
57const REPLACE_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
58 name: "newText",
59 ty: BuiltinParamType::Any,
60 arity: BuiltinParamArity::Required,
61 default: None,
62 description: "Text with replacements applied, preserving input container kind.",
63}];
64
65const REPLACE_INPUTS: [BuiltinParamDescriptor; 3] = [
66 BuiltinParamDescriptor {
67 name: "str",
68 ty: BuiltinParamType::Any,
69 arity: BuiltinParamArity::Required,
70 default: None,
71 description: "Input text (string/char/cell).",
72 },
73 BuiltinParamDescriptor {
74 name: "oldText",
75 ty: BuiltinParamType::Any,
76 arity: BuiltinParamArity::Required,
77 default: None,
78 description: "Search text list (scalar or array/cell).",
79 },
80 BuiltinParamDescriptor {
81 name: "newText",
82 ty: BuiltinParamType::Any,
83 arity: BuiltinParamArity::Required,
84 default: None,
85 description: "Replacement text list (scalar or matching-size list).",
86 },
87];
88
89const REPLACE_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
90 label: "newText = replace(str, oldText, newText)",
91 inputs: &REPLACE_INPUTS,
92 outputs: &REPLACE_OUTPUT,
93}];
94
95const REPLACE_ERROR_INVALID_INPUT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
96 code: "RM.REPLACE.INVALID_INPUT",
97 identifier: Some("RunMat:replace:InvalidInput"),
98 when: "First argument is not a string array, char array, or cell array of text scalars.",
99 message:
100 "replace: first argument must be a string array, character array, or cell array of character vectors",
101};
102
103const REPLACE_ERROR_PATTERN_TYPE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
104 code: "RM.REPLACE.PATTERN_TYPE",
105 identifier: Some("RunMat:replace:PatternType"),
106 when: "Second argument is not a text scalar/array/cell of text scalars.",
107 message:
108 "replace: second argument must be a string array, character array, or cell array of character vectors",
109};
110
111const REPLACE_ERROR_REPLACEMENT_TYPE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
112 code: "RM.REPLACE.REPLACEMENT_TYPE",
113 identifier: Some("RunMat:replace:ReplacementType"),
114 when: "Third argument is not a text scalar/array/cell of text scalars.",
115 message:
116 "replace: third argument must be a string array, character array, or cell array of character vectors",
117};
118
119const REPLACE_ERROR_EMPTY_PATTERN: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
120 code: "RM.REPLACE.EMPTY_PATTERN",
121 identifier: Some("RunMat:replace:EmptyPattern"),
122 when: "Search text list is empty.",
123 message: "replace: second argument must contain at least one search string",
124};
125
126const REPLACE_ERROR_EMPTY_REPLACEMENT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
127 code: "RM.REPLACE.EMPTY_REPLACEMENT",
128 identifier: Some("RunMat:replace:EmptyReplacement"),
129 when: "Replacement text list is empty.",
130 message: "replace: third argument must contain at least one replacement string",
131};
132
133const REPLACE_ERROR_SIZE_MISMATCH: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
134 code: "RM.REPLACE.SIZE_MISMATCH",
135 identifier: Some("RunMat:replace:SizeMismatch"),
136 when: "Replacement list is neither scalar nor equal in length to search list.",
137 message: "replace: replacement array must be a scalar or match the number of search strings",
138};
139
140const REPLACE_ERROR_CELL_ELEMENT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
141 code: "RM.REPLACE.CELL_ELEMENT",
142 identifier: Some("RunMat:replace:CellElement"),
143 when: "Cell arrays contain non-text elements or non-row char arrays.",
144 message: "replace: cell array elements must be string scalars or character vectors",
145};
146
147const REPLACE_ERROR_INTERNAL: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
148 code: "RM.REPLACE.INTERNAL",
149 identifier: Some("RunMat:replace:InternalError"),
150 when: "Internal output container construction failed.",
151 message: "replace: internal error",
152};
153
154const REPLACE_ERRORS: [BuiltinErrorDescriptor; 8] = [
155 REPLACE_ERROR_INVALID_INPUT,
156 REPLACE_ERROR_PATTERN_TYPE,
157 REPLACE_ERROR_REPLACEMENT_TYPE,
158 REPLACE_ERROR_EMPTY_PATTERN,
159 REPLACE_ERROR_EMPTY_REPLACEMENT,
160 REPLACE_ERROR_SIZE_MISMATCH,
161 REPLACE_ERROR_CELL_ELEMENT,
162 REPLACE_ERROR_INTERNAL,
163];
164
165pub const REPLACE_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
166 signatures: &REPLACE_SIGNATURES,
167 output_mode: BuiltinOutputMode::Fixed,
168 completion_policy: BuiltinCompletionPolicy::Public,
169 errors: &REPLACE_ERRORS,
170};
171
172pub const REPLACE_INTEGER_AUDIT: BuiltinIntegerAuditDescriptor = BuiltinIntegerAuditDescriptor {
173 kind: BuiltinIntegerAuditKind::NotApplicable,
174 canonical_builtin: None,
175 notes: "replace accepts host string, character, cell text, and pattern inputs only and preserves the input text container. Numeric, logical, symbolic, and provider-resident values reject before gather or provider access.",
176};
177
178fn map_flow(err: RuntimeError) -> RuntimeError {
179 map_control_flow_with_builtin(err, BUILTIN_NAME)
180}
181
182fn replace_error_with_message(
183 message: impl Into<String>,
184 error: &'static BuiltinErrorDescriptor,
185) -> RuntimeError {
186 let mut builder = build_runtime_error(message).with_builtin(BUILTIN_NAME);
187 if let Some(identifier) = error.identifier {
188 builder = builder.with_identifier(identifier);
189 }
190 builder.build()
191}
192
193fn replace_error(error: &'static BuiltinErrorDescriptor) -> RuntimeError {
194 replace_error_with_message(error.message, error)
195}
196
197#[runtime_builtin(
198 name = "replace",
199 category = "strings/transform",
200 summary = "Replace substring occurrences in strings, character arrays, and cell arrays.",
201 keywords = "replace,strrep,strings,character array,text",
202 accel = "sink",
203 type_resolver(text_preserve_type),
204 descriptor(crate::builtins::strings::transform::replace::REPLACE_DESCRIPTOR),
205 integer_audit(crate::builtins::strings::transform::replace::REPLACE_INTEGER_AUDIT),
206 builtin_path = "crate::builtins::strings::transform::replace"
207)]
208async fn replace_builtin(text: Value, old: Value, new: Value) -> BuiltinResult<Value> {
209 if [&text, &old, &new]
210 .into_iter()
211 .any(contains_resident_text_input)
212 {
213 return Err(replace_error(&REPLACE_ERROR_INVALID_INPUT));
214 }
215 let text = gather_if_needed_async(&text).await.map_err(map_flow)?;
216 let old = gather_if_needed_async(&old).await.map_err(map_flow)?;
217 let new = gather_if_needed_async(&new).await.map_err(map_flow)?;
218
219 let spec = ReplacementSpec::from_values(&old, &new)?;
220
221 match text {
222 Value::String(s) => Ok(Value::String(replace_string_scalar(s, &spec))),
223 Value::StringArray(sa) => replace_string_array(sa, &spec),
224 Value::CharArray(ca) => replace_char_array(ca, &spec),
225 Value::Cell(cell) => replace_cell_array(cell, &spec),
226 _ => Err(replace_error(&REPLACE_ERROR_INVALID_INPUT)),
227 }
228}
229
230fn replace_string_scalar(text: String, spec: &ReplacementSpec) -> String {
231 if is_missing_string(&text) {
232 text
233 } else {
234 spec.apply(&text)
235 }
236}
237
238fn replace_string_array(array: StringArray, spec: &ReplacementSpec) -> BuiltinResult<Value> {
239 let StringArray { data, shape, .. } = array;
240 let mut replaced = Vec::with_capacity(data.len());
241 for entry in data {
242 if is_missing_string(&entry) {
243 replaced.push(entry);
244 } else {
245 replaced.push(spec.apply(&entry));
246 }
247 }
248 let result = StringArray::new(replaced, shape).map_err(|e| {
249 replace_error_with_message(format!("{BUILTIN_NAME}: {e}"), &REPLACE_ERROR_INTERNAL)
250 })?;
251 Ok(Value::StringArray(result))
252}
253
254fn replace_char_array(array: CharArray, spec: &ReplacementSpec) -> BuiltinResult<Value> {
255 let CharArray {
256 data,
257 shape,
258 rows,
259 cols,
260 } = array;
261 if rows == 0 {
262 return Ok(Value::CharArray(CharArray {
263 data,
264 shape,
265 rows,
266 cols,
267 }));
268 }
269
270 let mut replaced_rows = Vec::with_capacity(rows);
271 let mut target_cols = 0usize;
272 for row in 0..rows {
273 let slice = char_row_to_string_slice(&data, cols, row);
274 let replaced = spec.apply(&slice);
275 let len = replaced.chars().count();
276 target_cols = target_cols.max(len);
277 replaced_rows.push(replaced);
278 }
279
280 let mut flattened = Vec::with_capacity(rows * target_cols);
281 for row_text in replaced_rows {
282 let mut chars: Vec<char> = row_text.chars().collect();
283 if chars.len() < target_cols {
284 chars.resize(target_cols, ' ');
285 }
286 flattened.extend(chars);
287 }
288
289 CharArray::new(flattened, rows, target_cols)
290 .map(Value::CharArray)
291 .map_err(|e| {
292 replace_error_with_message(format!("{BUILTIN_NAME}: {e}"), &REPLACE_ERROR_INTERNAL)
293 })
294}
295
296fn replace_cell_array(cell: CellArray, spec: &ReplacementSpec) -> BuiltinResult<Value> {
297 let CellArray {
298 data, rows, cols, ..
299 } = cell;
300 let mut replaced = Vec::with_capacity(rows * cols);
301 for row in 0..rows {
302 for col in 0..cols {
303 let idx = row * cols + col;
304 let value = replace_cell_element(&data[idx], spec)?;
305 replaced.push(value);
306 }
307 }
308 make_cell(replaced, rows, cols).map_err(|e| {
309 replace_error_with_message(format!("{BUILTIN_NAME}: {e}"), &REPLACE_ERROR_INTERNAL)
310 })
311}
312
313fn replace_cell_element(value: &Value, spec: &ReplacementSpec) -> BuiltinResult<Value> {
314 match value {
315 Value::String(text) => Ok(Value::String(replace_string_scalar(text.clone(), spec))),
316 Value::StringArray(sa) if sa.data.len() == 1 => Ok(Value::String(replace_string_scalar(
317 sa.data[0].clone(),
318 spec,
319 ))),
320 Value::CharArray(ca) if ca.rows <= 1 => replace_char_array(ca.clone(), spec),
321 Value::CharArray(_) => Err(replace_error(&REPLACE_ERROR_CELL_ELEMENT)),
322 _ => Err(replace_error(&REPLACE_ERROR_CELL_ELEMENT)),
323 }
324}
325
326fn extract_pattern_list(value: &Value) -> BuiltinResult<Vec<SearchPattern>> {
327 if matches!(value, Value::Object(_)) {
328 let regex = pattern_regex(value, BUILTIN_NAME).map_err(|err| {
329 replace_error_with_message(err.message().to_string(), &REPLACE_ERROR_PATTERN_TYPE)
330 })?;
331 return Ok(vec![SearchPattern::Regex(Regex::new(®ex).map_err(
332 |err| replace_error_with_message(err.to_string(), &REPLACE_ERROR_PATTERN_TYPE),
333 )?)]);
334 }
335 extract_text_list(value, &REPLACE_ERROR_PATTERN_TYPE)
336 .map(|items| items.into_iter().map(SearchPattern::Literal).collect())
337}
338
339fn extract_replacement_list(value: &Value) -> BuiltinResult<Vec<String>> {
340 extract_text_list(value, &REPLACE_ERROR_REPLACEMENT_TYPE)
341}
342
343fn extract_text_list(
344 value: &Value,
345 type_error: &'static BuiltinErrorDescriptor,
346) -> BuiltinResult<Vec<String>> {
347 match value {
348 Value::String(text) => Ok(vec![text.clone()]),
349 Value::StringArray(array) => Ok(array.data.clone()),
350 Value::CharArray(array) => {
351 let CharArray {
352 data, rows, cols, ..
353 } = array.clone();
354 if rows == 0 {
355 Ok(Vec::new())
356 } else {
357 let mut entries = Vec::with_capacity(rows);
358 for row in 0..rows {
359 entries.push(char_row_to_string_slice(&data, cols, row));
360 }
361 Ok(entries)
362 }
363 }
364 Value::Cell(cell) => {
365 let CellArray { data, .. } = cell.clone();
366 let mut entries = Vec::with_capacity(data.len());
367 for element in data {
368 match &element {
369 Value::String(text) => entries.push(text.clone()),
370 Value::StringArray(sa) if sa.data.len() == 1 => {
371 entries.push(sa.data[0].clone());
372 }
373 Value::CharArray(ca) if ca.rows <= 1 => {
374 if ca.rows == 0 {
375 entries.push(String::new());
376 } else {
377 entries.push(char_row_to_string_slice(&ca.data, ca.cols, 0));
378 }
379 }
380 Value::CharArray(_) => {
381 return Err(replace_error(&REPLACE_ERROR_CELL_ELEMENT));
382 }
383 _ => {
384 return Err(replace_error(&REPLACE_ERROR_CELL_ELEMENT));
385 }
386 }
387 }
388 Ok(entries)
389 }
390 _ => Err(replace_error(type_error)),
391 }
392}
393
394enum SearchPattern {
395 Literal(String),
396 Regex(Regex),
397}
398
399struct ReplacementSpec {
400 pairs: Vec<(SearchPattern, String)>,
401}
402
403impl ReplacementSpec {
404 fn from_values(old: &Value, new: &Value) -> BuiltinResult<Self> {
405 let patterns = extract_pattern_list(old)?;
406 if patterns.is_empty() {
407 return Err(replace_error(&REPLACE_ERROR_EMPTY_PATTERN));
408 }
409
410 let replacements = extract_replacement_list(new)?;
411 if replacements.is_empty() {
412 return Err(replace_error(&REPLACE_ERROR_EMPTY_REPLACEMENT));
413 }
414
415 let pairs = if replacements.len() == patterns.len() {
416 patterns.into_iter().zip(replacements).collect::<Vec<_>>()
417 } else if replacements.len() == 1 {
418 let replacement = replacements[0].clone();
419 patterns
420 .into_iter()
421 .map(|pattern| (pattern, replacement.clone()))
422 .collect::<Vec<_>>()
423 } else {
424 return Err(replace_error(&REPLACE_ERROR_SIZE_MISMATCH));
425 };
426
427 Ok(Self { pairs })
428 }
429
430 fn apply(&self, input: &str) -> String {
431 let mut current = input.to_string();
432 for (pattern, replacement) in &self.pairs {
433 match pattern {
434 SearchPattern::Literal(pattern) => {
435 if pattern.is_empty() && replacement.is_empty() {
436 continue;
437 }
438 if pattern == replacement {
439 continue;
440 }
441 current = current.replace(pattern, replacement);
442 }
443 SearchPattern::Regex(pattern) => {
444 current = pattern
445 .replace_all(¤t, replacement.as_str())
446 .to_string();
447 }
448 }
449 }
450 current
451 }
452}
453
454#[cfg(test)]
455pub(crate) mod tests {
456 use super::*;
457 use runmat_builtins::{ResolveContext, Type};
458
459 fn replace_builtin(text: Value, old: Value, new: Value) -> BuiltinResult<Value> {
460 futures::executor::block_on(super::replace_builtin(text, old, new))
461 }
462
463 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
464 #[test]
465 fn replace_string_scalar_single_term() {
466 let result = replace_builtin(
467 Value::String("RunMat runtime".into()),
468 Value::String("runtime".into()),
469 Value::String("engine".into()),
470 )
471 .expect("replace");
472 assert_eq!(result, Value::String("RunMat engine".into()));
473 }
474
475 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
476 #[test]
477 fn replace_string_array_multiple_terms() {
478 let strings = StringArray::new(
479 vec!["gpu".into(), "cpu".into(), "<missing>".into()],
480 vec![3, 1],
481 )
482 .unwrap();
483 let result = replace_builtin(
484 Value::StringArray(strings),
485 Value::StringArray(
486 StringArray::new(vec!["gpu".into(), "cpu".into()], vec![2, 1]).unwrap(),
487 ),
488 Value::String("device".into()),
489 )
490 .expect("replace");
491 match result {
492 Value::StringArray(sa) => {
493 assert_eq!(sa.shape, vec![3, 1]);
494 assert_eq!(
495 sa.data,
496 vec![
497 String::from("device"),
498 String::from("device"),
499 String::from("<missing>")
500 ]
501 );
502 }
503 other => panic!("expected string array, got {other:?}"),
504 }
505 }
506
507 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
508 #[test]
509 fn replace_char_array_adjusts_width() {
510 let chars = CharArray::new("matrix".chars().collect(), 1, 6).unwrap();
511 let result = replace_builtin(
512 Value::CharArray(chars),
513 Value::String("matrix".into()),
514 Value::String("tensor".into()),
515 )
516 .expect("replace");
517 match result {
518 Value::CharArray(out) => {
519 assert_eq!(out.rows, 1);
520 assert_eq!(out.cols, 6);
521 let expected: Vec<char> = "tensor".chars().collect();
522 assert_eq!(out.data, expected);
523 }
524 other => panic!("expected char array, got {other:?}"),
525 }
526 }
527
528 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
529 #[test]
530 fn replace_char_array_handles_padding() {
531 let chars = CharArray::new(vec!['a', 'b', 'c', 'd'], 2, 2).unwrap();
532 let result = replace_builtin(
533 Value::CharArray(chars),
534 Value::String("b".into()),
535 Value::String("beta".into()),
536 )
537 .expect("replace");
538 match result {
539 Value::CharArray(out) => {
540 assert_eq!(out.rows, 2);
541 assert_eq!(out.cols, 5);
542 let expected: Vec<char> = vec!['a', 'b', 'e', 't', 'a', 'c', 'd', ' ', ' ', ' '];
543 assert_eq!(out.data, expected);
544 }
545 other => panic!("expected char array, got {other:?}"),
546 }
547 }
548
549 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
550 #[test]
551 fn replace_cell_array_mixed_content() {
552 let cell = CellArray::new(
553 vec![
554 Value::CharArray(CharArray::new_row("Kernel Planner")),
555 Value::String("GPU Fusion".into()),
556 ],
557 1,
558 2,
559 )
560 .unwrap();
561 let result = replace_builtin(
562 Value::Cell(cell),
563 Value::Cell(
564 CellArray::new(
565 vec![Value::String("Kernel".into()), Value::String("GPU".into())],
566 1,
567 2,
568 )
569 .unwrap(),
570 ),
571 Value::StringArray(
572 StringArray::new(vec!["Shader".into(), "Device".into()], vec![1, 2]).unwrap(),
573 ),
574 )
575 .expect("replace");
576 match result {
577 Value::Cell(out) => {
578 let first = out.get(0, 0).unwrap();
579 let second = out.get(0, 1).unwrap();
580 assert_eq!(
581 first,
582 Value::CharArray(CharArray::new_row("Shader Planner"))
583 );
584 assert_eq!(second, Value::String("Device Fusion".into()));
585 }
586 other => panic!("expected cell array, got {other:?}"),
587 }
588 }
589
590 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
591 #[test]
592 fn replace_errors_on_invalid_first_argument() {
593 let err = replace_builtin(
594 Value::Num(1.0),
595 Value::String("a".into()),
596 Value::String("b".into()),
597 )
598 .unwrap_err();
599 assert_eq!(err.to_string(), REPLACE_ERROR_INVALID_INPUT.message);
600 }
601
602 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
603 #[test]
604 fn replace_errors_on_invalid_pattern_type() {
605 let err = replace_builtin(
606 Value::String("abc".into()),
607 Value::Num(1.0),
608 Value::String("x".into()),
609 )
610 .unwrap_err();
611 assert_eq!(err.to_string(), REPLACE_ERROR_PATTERN_TYPE.message);
612 }
613
614 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
615 #[test]
616 fn replace_errors_on_size_mismatch() {
617 let err = replace_builtin(
618 Value::String("abc".into()),
619 Value::StringArray(StringArray::new(vec!["a".into(), "b".into()], vec![2, 1]).unwrap()),
620 Value::StringArray(
621 StringArray::new(vec!["x".into(), "y".into(), "z".into()], vec![3, 1]).unwrap(),
622 ),
623 )
624 .unwrap_err();
625 assert_eq!(err.to_string(), REPLACE_ERROR_SIZE_MISMATCH.message);
626 }
627
628 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
629 #[test]
630 fn replace_preserves_missing_string() {
631 let result = replace_builtin(
632 Value::String("<missing>".into()),
633 Value::String("missing".into()),
634 Value::String("value".into()),
635 )
636 .expect("replace");
637 assert_eq!(result, Value::String("<missing>".into()));
638 }
639
640 #[test]
641 fn replace_accepts_pattern_object() {
642 let pattern = crate::builtins::strings::core::compat::pattern_object(r"\d+");
643 let result = replace_builtin(
644 Value::String("run42mat".into()),
645 pattern,
646 Value::String("-".into()),
647 )
648 .expect("replace");
649 assert_eq!(result, Value::String("run-mat".into()));
650 }
651
652 #[test]
653 fn replace_type_preserves_text() {
654 assert_eq!(
655 text_preserve_type(&[Type::String], &ResolveContext::new(Vec::new())),
656 Type::String
657 );
658 }
659}