1use runmat_builtins::{CellArray, CharArray, StringArray, Value};
4use runmat_macros::runtime_builtin;
5
6use crate::builtins::common::spec::{
7 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
8 ReductionNaN, ResidencyPolicy, ShapeRequirements,
9};
10use crate::builtins::strings::common::{char_row_to_string_slice, is_missing_string};
11#[cfg(feature = "doc_export")]
12use crate::register_builtin_doc_text;
13use crate::{
14 gather_if_needed, make_cell_with_shape, register_builtin_fusion_spec, register_builtin_gpu_spec,
15};
16
17#[cfg(feature = "doc_export")]
18pub const DOC_MD: &str = r#"---
19title: "strrep"
20category: "strings/transform"
21keywords: ["strrep", "string replace", "character array replace", "text replacement", "substring replace"]
22summary: "Replace substring occurrences in strings, character arrays, and cell arrays while mirroring MATLAB semantics."
23references:
24 - https://www.mathworks.com/help/matlab/ref/strrep.html
25gpu_support:
26 elementwise: false
27 reduction: false
28 precisions: []
29 broadcasting: "none"
30 notes: "Runs on the CPU. RunMat gathers GPU-resident text inputs before performing replacements."
31fusion:
32 elementwise: false
33 reduction: false
34 max_inputs: 3
35 constants: "inline"
36requires_feature: null
37tested:
38 unit: "builtins::strings::transform::strrep::tests"
39 integration: "builtins::strings::transform::strrep::tests::strrep_cell_array_char_vectors, builtins::strings::transform::strrep::tests::strrep_wgpu_provider_fallback"
40---
41
42# What does the `strrep` function do in MATLAB / RunMat?
43`strrep(str, old, new)` replaces every non-overlapping instance of the substring `old` that appears in
44`str` with the text provided in `new`. The builtin accepts string scalars, string arrays, character arrays,
45and cell arrays of character vectors, matching MATLAB behaviour exactly.
46
47## How does the `strrep` function behave in MATLAB / RunMat?
48- String scalars remain strings. Missing string values (`<missing>`) propagate unchanged.
49- String arrays are processed element-wise while preserving their full shape and orientation.
50- Character arrays are handled row by row. Rows expand or shrink as needed and are padded with spaces so
51 the result stays a rectangular char array, just like MATLAB.
52- Cell arrays must contain character vectors or string scalars. The result is a cell array of identical size
53 where each element has had the replacement applied.
54- The `old` and `new` arguments must be string scalars or character vectors of the same data type.
55- `old` can be empty. In that case, `strrep` inserts `new` before the first character, between existing
56 characters, and after the final character.
57
58## `strrep` Function GPU Execution Behaviour
59RunMat treats text replacement as a CPU-first workflow:
60
611. The builtin is registered as an Accelerate *sink*, so the planner gathers any GPU-resident inputs
62 (string arrays, char arrays, or cell contents) back to host memory before work begins.
632. Replacements are computed entirely on the CPU, mirroring MATLAB’s behaviour and avoiding GPU/device
64 divergence in string handling.
653. Results are returned as host values (string array, char array, or cell array). Residency is never pushed
66 back to the GPU, keeping semantics deterministic regardless of the active provider.
67
68## GPU residency in RunMat (Do I need `gpuArray`?)
69No. `strrep` registers as a sink with RunMat Accelerate, so the fusion planner never keeps its inputs or
70outputs on the GPU. Even if you start with GPU data, the runtime gathers it automatically—manual `gpuArray`
71or `gather` calls are unnecessary.
72
73## Examples of using the `strrep` function in MATLAB / RunMat
74
75### Replacing a word inside a string scalar
76```matlab
77txt = "RunMat turbo mode";
78result = strrep(txt, "turbo", "accelerate");
79```
80Expected output:
81```matlab
82result = "RunMat accelerate mode"
83```
84
85### Updating every element of a string array
86```matlab
87labels = ["GPU planner", "CPU planner"];
88updated = strrep(labels, "planner", "pipeline");
89```
90Expected output:
91```matlab
92updated = 2×1 string
93 "GPU pipeline"
94 "CPU pipeline"
95```
96
97### Preserving rectangular shape in character arrays
98```matlab
99chars = char("alpha", "beta ");
100out = strrep(chars, "a", "A");
101```
102Expected output:
103```matlab
104out =
105
106 2×5 char array
107
108 'AlphA'
109 'betA '
110```
111
112### Applying replacements inside a cell array of character vectors
113```matlab
114C = {'Kernel Fusion', 'GPU Planner'};
115renamed = strrep(C, ' ', '_');
116```
117Expected output:
118```matlab
119renamed = 1×2 cell array
120 {'Kernel_Fusion'} {'GPU_Planner'}
121```
122
123### Inserting text with an empty search pattern
124```matlab
125stub = "abc";
126expanded = strrep(stub, "", "-");
127```
128Expected output:
129```matlab
130expanded = "-a-b-c-"
131```
132
133### Leaving missing string values untouched
134```matlab
135vals = ["RunMat", "<missing>", "Accelerate"];
136out = strrep(vals, "RunMat", "RUNMAT");
137```
138Expected output:
139```matlab
140out = 1×3 string
141 "RUNMAT" <missing> "Accelerate"
142```
143
144### Replacing substrings gathered from GPU inputs
145```matlab
146g = gpuArray("Turbine");
147host = strrep(g, "bine", "bo");
148```
149Expected output:
150```matlab
151host = "Turbo"
152```
153
154## FAQ
155
156### Which input types does `strrep` accept?
157String scalars, string arrays, character vectors, character arrays, and cell arrays of character vectors.
158The `old` and `new` arguments must be string scalars or character vectors of the same data type.
159
160### Does `strrep` support multiple search terms at once?
161No. Use the newer `replace` builtin if you need to substitute several search terms in a single call.
162
163### How does `strrep` handle missing strings?
164Missing string scalars remain `<missing>` and are returned unchanged, even when the search pattern matches
165ordinary text.
166
167### Will rows of a character array stay aligned?
168Yes. Each row is replaced individually, then padded with spaces so that the overall array stays rectangular,
169matching MATLAB exactly.
170
171### What happens when `old` is empty?
172RunMat mirrors MATLAB: `new` is inserted before the first character, between every existing character, and
173after the last character.
174
175### Does `strrep` run on the GPU?
176Not today. The builtin gathers GPU-resident data to host memory automatically before performing the
177replacement logic.
178
179### Can I mix strings and character vectors for `old` and `new`?
180No. MATLAB requires `old` and `new` to share the same data type. RunMat enforces the same rule and raises a
181descriptive error when they differ.
182
183### How do I replace text stored inside cell arrays?
184`strrep` traverses the cell array, applying the replacement to each character vector or string scalar element
185and returning a cell array of the same shape.
186
187## See Also
188[replace](./replace), [regexprep](../../regex/regexprep), [string](../core/string), [char](../core/char), [join](./join)
189"#;
190
191pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
192 name: "strrep",
193 op_kind: GpuOpKind::Custom("string-transform"),
194 supported_precisions: &[],
195 broadcast: BroadcastSemantics::None,
196 provider_hooks: &[],
197 constant_strategy: ConstantStrategy::InlineLiteral,
198 residency: ResidencyPolicy::GatherImmediately,
199 nan_mode: ReductionNaN::Include,
200 two_pass_threshold: None,
201 workgroup_size: None,
202 accepts_nan_mode: false,
203 notes: "Executes on the CPU; GPU-resident inputs are gathered before replacements are applied.",
204};
205
206register_builtin_gpu_spec!(GPU_SPEC);
207
208pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
209 name: "strrep",
210 shape: ShapeRequirements::Any,
211 constant_strategy: ConstantStrategy::InlineLiteral,
212 elementwise: None,
213 reduction: None,
214 emits_nan: false,
215 notes: "String transformation builtin; marked as a sink so fusion skips GPU residency.",
216};
217
218register_builtin_fusion_spec!(FUSION_SPEC);
219
220#[cfg(feature = "doc_export")]
221register_builtin_doc_text!("strrep", DOC_MD);
222
223const ARGUMENT_TYPE_ERROR: &str =
224 "strrep: first argument must be a string array, character array, or cell array of character vectors";
225const PATTERN_TYPE_ERROR: &str = "strrep: old and new must be string scalars or character vectors";
226const PATTERN_MISMATCH_ERROR: &str = "strrep: old and new must be the same data type";
227const CELL_ELEMENT_ERROR: &str =
228 "strrep: cell array elements must be string scalars or character vectors";
229
230#[derive(Clone, Copy, PartialEq, Eq)]
231enum PatternKind {
232 String,
233 Char,
234}
235
236#[runtime_builtin(
237 name = "strrep",
238 category = "strings/transform",
239 summary = "Replace substring occurrences with MATLAB-compatible semantics.",
240 keywords = "strrep,replace,strings,character array,text",
241 accel = "sink"
242)]
243fn strrep_builtin(str_value: Value, old_value: Value, new_value: Value) -> Result<Value, String> {
244 let gathered_str = gather_if_needed(&str_value).map_err(|e| format!("strrep: {e}"))?;
245 let gathered_old = gather_if_needed(&old_value).map_err(|e| format!("strrep: {e}"))?;
246 let gathered_new = gather_if_needed(&new_value).map_err(|e| format!("strrep: {e}"))?;
247
248 let (old_text, old_kind) = parse_pattern(gathered_old)?;
249 let (new_text, new_kind) = parse_pattern(gathered_new)?;
250 if old_kind != new_kind {
251 return Err(PATTERN_MISMATCH_ERROR.to_string());
252 }
253
254 match gathered_str {
255 Value::String(text) => Ok(Value::String(strrep_string_value(
256 text, &old_text, &new_text,
257 ))),
258 Value::StringArray(array) => strrep_string_array(array, &old_text, &new_text),
259 Value::CharArray(array) => strrep_char_array(array, &old_text, &new_text),
260 Value::Cell(cell) => strrep_cell_array(cell, &old_text, &new_text),
261 _ => Err(ARGUMENT_TYPE_ERROR.to_string()),
262 }
263}
264
265fn parse_pattern(value: Value) -> Result<(String, PatternKind), String> {
266 match value {
267 Value::String(text) => Ok((text, PatternKind::String)),
268 Value::StringArray(array) => {
269 if array.data.len() == 1 {
270 Ok((array.data[0].clone(), PatternKind::String))
271 } else {
272 Err(PATTERN_TYPE_ERROR.to_string())
273 }
274 }
275 Value::CharArray(array) => {
276 if array.rows <= 1 {
277 let text = if array.rows == 0 {
278 String::new()
279 } else {
280 char_row_to_string_slice(&array.data, array.cols, 0)
281 };
282 Ok((text, PatternKind::Char))
283 } else {
284 Err(PATTERN_TYPE_ERROR.to_string())
285 }
286 }
287 _ => Err(PATTERN_TYPE_ERROR.to_string()),
288 }
289}
290
291fn strrep_string_value(text: String, old: &str, new: &str) -> String {
292 if is_missing_string(&text) {
293 text
294 } else {
295 text.replace(old, new)
296 }
297}
298
299fn strrep_string_array(array: StringArray, old: &str, new: &str) -> Result<Value, String> {
300 let StringArray { data, shape, .. } = array;
301 let replaced = data
302 .into_iter()
303 .map(|text| strrep_string_value(text, old, new))
304 .collect::<Vec<_>>();
305 let rebuilt = StringArray::new(replaced, shape).map_err(|e| format!("strrep: {e}"))?;
306 Ok(Value::StringArray(rebuilt))
307}
308
309fn strrep_char_array(array: CharArray, old: &str, new: &str) -> Result<Value, String> {
310 let CharArray { data, rows, cols } = array;
311 if rows == 0 || cols == 0 {
312 return Ok(Value::CharArray(CharArray { data, rows, cols }));
313 }
314
315 let mut replaced_rows = Vec::with_capacity(rows);
316 let mut target_cols = 0usize;
317 for row in 0..rows {
318 let text = char_row_to_string_slice(&data, cols, row);
319 let replaced = text.replace(old, new);
320 target_cols = target_cols.max(replaced.chars().count());
321 replaced_rows.push(replaced);
322 }
323
324 let mut new_data = Vec::with_capacity(rows * target_cols);
325 for row_text in replaced_rows {
326 let mut chars: Vec<char> = row_text.chars().collect();
327 if chars.len() < target_cols {
328 chars.resize(target_cols, ' ');
329 }
330 new_data.extend(chars);
331 }
332
333 CharArray::new(new_data, rows, target_cols)
334 .map(Value::CharArray)
335 .map_err(|e| format!("strrep: {e}"))
336}
337
338fn strrep_cell_array(cell: CellArray, old: &str, new: &str) -> Result<Value, String> {
339 let CellArray { data, shape, .. } = cell;
340 let mut replaced = Vec::with_capacity(data.len());
341 for ptr in &data {
342 replaced.push(strrep_cell_element(ptr, old, new)?);
343 }
344 make_cell_with_shape(replaced, shape).map_err(|e| format!("strrep: {e}"))
345}
346
347fn strrep_cell_element(value: &Value, old: &str, new: &str) -> Result<Value, String> {
348 match value {
349 Value::String(text) => Ok(Value::String(strrep_string_value(text.clone(), old, new))),
350 Value::StringArray(array) => strrep_string_array(array.clone(), old, new),
351 Value::CharArray(array) => strrep_char_array(array.clone(), old, new),
352 _ => Err(CELL_ELEMENT_ERROR.to_string()),
353 }
354}
355
356#[cfg(test)]
357mod tests {
358 use super::*;
359 #[cfg(feature = "doc_export")]
360 use crate::builtins::common::test_support;
361
362 #[test]
363 fn strrep_string_scalar_basic() {
364 let result = strrep_builtin(
365 Value::String("RunMat Ignite".into()),
366 Value::String("Ignite".into()),
367 Value::String("Interpreter".into()),
368 )
369 .expect("strrep");
370 assert_eq!(result, Value::String("RunMat Interpreter".into()));
371 }
372
373 #[test]
374 fn strrep_string_array_preserves_missing() {
375 let array = StringArray::new(
376 vec![
377 String::from("gpu"),
378 String::from("<missing>"),
379 String::from("planner"),
380 ],
381 vec![3, 1],
382 )
383 .unwrap();
384 let result = strrep_builtin(
385 Value::StringArray(array),
386 Value::String("gpu".into()),
387 Value::String("GPU".into()),
388 )
389 .expect("strrep");
390 match result {
391 Value::StringArray(sa) => {
392 assert_eq!(sa.shape, vec![3, 1]);
393 assert_eq!(
394 sa.data,
395 vec![
396 String::from("GPU"),
397 String::from("<missing>"),
398 String::from("planner")
399 ]
400 );
401 }
402 other => panic!("expected string array, got {other:?}"),
403 }
404 }
405
406 #[test]
407 fn strrep_string_array_with_char_pattern() {
408 let array = StringArray::new(
409 vec![String::from("alpha"), String::from("beta")],
410 vec![2, 1],
411 )
412 .unwrap();
413 let result = strrep_builtin(
414 Value::StringArray(array),
415 Value::CharArray(CharArray::new_row("a")),
416 Value::CharArray(CharArray::new_row("A")),
417 )
418 .expect("strrep");
419 match result {
420 Value::StringArray(sa) => {
421 assert_eq!(sa.shape, vec![2, 1]);
422 assert_eq!(sa.data, vec![String::from("AlphA"), String::from("betA")]);
423 }
424 other => panic!("expected string array, got {other:?}"),
425 }
426 }
427
428 #[test]
429 fn strrep_char_array_padding() {
430 let chars = CharArray::new(vec!['R', 'u', 'n', ' ', 'M', 'a', 't'], 1, 7).unwrap();
431 let result = strrep_builtin(
432 Value::CharArray(chars),
433 Value::String(" ".into()),
434 Value::String("_".into()),
435 )
436 .expect("strrep");
437 match result {
438 Value::CharArray(out) => {
439 assert_eq!(out.rows, 1);
440 assert_eq!(out.cols, 7);
441 let expected: Vec<char> = "Run_Mat".chars().collect();
442 assert_eq!(out.data, expected);
443 }
444 other => panic!("expected char array, got {other:?}"),
445 }
446 }
447
448 #[test]
449 fn strrep_char_array_shrinks_rows_pad_with_spaces() {
450 let mut data: Vec<char> = "alpha".chars().collect();
451 data.extend("beta ".chars());
452 let array = CharArray::new(data, 2, 5).unwrap();
453 let result = strrep_builtin(
454 Value::CharArray(array),
455 Value::String("a".into()),
456 Value::String("".into()),
457 )
458 .expect("strrep");
459 match result {
460 Value::CharArray(out) => {
461 assert_eq!(out.rows, 2);
462 assert_eq!(out.cols, 4);
463 let expected: Vec<char> = vec!['l', 'p', 'h', ' ', 'b', 'e', 't', ' '];
464 assert_eq!(out.data, expected);
465 }
466 other => panic!("expected char array, got {other:?}"),
467 }
468 }
469
470 #[test]
471 fn strrep_cell_array_char_vectors() {
472 let cell = CellArray::new(
473 vec![
474 Value::CharArray(CharArray::new_row("Kernel Fusion")),
475 Value::CharArray(CharArray::new_row("GPU Planner")),
476 ],
477 1,
478 2,
479 )
480 .unwrap();
481 let result = strrep_builtin(
482 Value::Cell(cell),
483 Value::String(" ".into()),
484 Value::String("_".into()),
485 )
486 .expect("strrep");
487 match result {
488 Value::Cell(out) => {
489 assert_eq!(out.rows, 1);
490 assert_eq!(out.cols, 2);
491 assert_eq!(
492 out.get(0, 0).unwrap(),
493 Value::CharArray(CharArray::new_row("Kernel_Fusion"))
494 );
495 assert_eq!(
496 out.get(0, 1).unwrap(),
497 Value::CharArray(CharArray::new_row("GPU_Planner"))
498 );
499 }
500 other => panic!("expected cell array, got {other:?}"),
501 }
502 }
503
504 #[test]
505 fn strrep_cell_array_string_scalars() {
506 let cell = CellArray::new(
507 vec![
508 Value::String("Planner".into()),
509 Value::String("Profiler".into()),
510 ],
511 1,
512 2,
513 )
514 .unwrap();
515 let result = strrep_builtin(
516 Value::Cell(cell),
517 Value::String("er".into()),
518 Value::String("ER".into()),
519 )
520 .expect("strrep");
521 match result {
522 Value::Cell(out) => {
523 assert_eq!(out.rows, 1);
524 assert_eq!(out.cols, 2);
525 assert_eq!(out.get(0, 0).unwrap(), Value::String("PlannER".into()));
526 assert_eq!(out.get(0, 1).unwrap(), Value::String("ProfilER".into()));
527 }
528 other => panic!("expected cell array, got {other:?}"),
529 }
530 }
531
532 #[test]
533 fn strrep_cell_array_invalid_element_error() {
534 let cell = CellArray::new(vec![Value::Num(1.0)], 1, 1).unwrap();
535 let err = strrep_builtin(
536 Value::Cell(cell),
537 Value::String("1".into()),
538 Value::String("one".into()),
539 )
540 .expect_err("expected cell element error");
541 assert!(err.contains("cell array elements"));
542 }
543
544 #[test]
545 fn strrep_cell_array_char_matrix_element() {
546 let mut chars: Vec<char> = "alpha".chars().collect();
547 chars.extend("beta ".chars());
548 let element = CharArray::new(chars, 2, 5).unwrap();
549 let cell = CellArray::new(vec![Value::CharArray(element)], 1, 1).unwrap();
550 let result = strrep_builtin(
551 Value::Cell(cell),
552 Value::String("a".into()),
553 Value::String("A".into()),
554 )
555 .expect("strrep");
556 match result {
557 Value::Cell(out) => {
558 let nested = out.get(0, 0).unwrap();
559 match nested {
560 Value::CharArray(ca) => {
561 assert_eq!(ca.rows, 2);
562 assert_eq!(ca.cols, 5);
563 let expected: Vec<char> =
564 vec!['A', 'l', 'p', 'h', 'A', 'b', 'e', 't', 'A', ' '];
565 assert_eq!(ca.data, expected);
566 }
567 other => panic!("expected char array element, got {other:?}"),
568 }
569 }
570 other => panic!("expected cell array, got {other:?}"),
571 }
572 }
573
574 #[test]
575 fn strrep_cell_array_string_arrays() {
576 let element = StringArray::new(vec!["alpha".into(), "beta".into()], vec![1, 2]).unwrap();
577 let cell = CellArray::new(vec![Value::StringArray(element)], 1, 1).unwrap();
578 let result = strrep_builtin(
579 Value::Cell(cell),
580 Value::String("a".into()),
581 Value::String("A".into()),
582 )
583 .expect("strrep");
584 match result {
585 Value::Cell(out) => {
586 let nested = out.get(0, 0).unwrap();
587 match nested {
588 Value::StringArray(sa) => {
589 assert_eq!(sa.shape, vec![1, 2]);
590 assert_eq!(sa.data, vec![String::from("AlphA"), String::from("betA")]);
591 }
592 other => panic!("expected string array element, got {other:?}"),
593 }
594 }
595 other => panic!("expected cell array, got {other:?}"),
596 }
597 }
598
599 #[test]
600 fn strrep_empty_pattern_inserts_replacement() {
601 let result = strrep_builtin(
602 Value::String("abc".into()),
603 Value::String("".into()),
604 Value::String("-".into()),
605 )
606 .expect("strrep");
607 assert_eq!(result, Value::String("-a-b-c-".into()));
608 }
609
610 #[test]
611 fn strrep_type_mismatch_errors() {
612 let err = strrep_builtin(
613 Value::String("abc".into()),
614 Value::String("a".into()),
615 Value::CharArray(CharArray::new_row("x")),
616 )
617 .expect_err("expected type mismatch");
618 assert!(err.contains("same data type"));
619 }
620
621 #[test]
622 fn strrep_invalid_pattern_type_errors() {
623 let err = strrep_builtin(
624 Value::String("abc".into()),
625 Value::Num(1.0),
626 Value::String("x".into()),
627 )
628 .expect_err("expected pattern error");
629 assert!(err.contains("string scalars or character vectors"));
630 }
631
632 #[test]
633 fn strrep_first_argument_type_error() {
634 let err = strrep_builtin(
635 Value::Num(42.0),
636 Value::String("a".into()),
637 Value::String("b".into()),
638 )
639 .expect_err("expected argument type error");
640 assert!(err.contains("first argument"));
641 }
642
643 #[test]
644 #[cfg(feature = "wgpu")]
645 fn strrep_wgpu_provider_fallback() {
646 if runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
647 runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
648 )
649 .is_err()
650 {
651 return;
653 }
654 let result = strrep_builtin(
655 Value::String("Turbine Engine".into()),
656 Value::String("Engine".into()),
657 Value::String("JIT".into()),
658 )
659 .expect("strrep");
660 assert_eq!(result, Value::String("Turbine JIT".into()));
661 }
662
663 #[test]
664 #[cfg(feature = "doc_export")]
665 fn doc_examples_smoke() {
666 let blocks = test_support::doc_examples(DOC_MD);
667 assert!(!blocks.is_empty());
668 }
669}