Skip to main content

runmat_runtime/builtins/strings/transform/
strrep.rs

1//! MATLAB-compatible `strrep` builtin with GPU-aware semantics for RunMat.
2
3use runmat_builtins::{
4    BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
5    BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
6};
7use runmat_builtins::{BuiltinIntegerAuditDescriptor, BuiltinIntegerAuditKind};
8use runmat_macros::runtime_builtin;
9use runmat_value::{CellArray, CharArray, StringArray, Value};
10
11use crate::builtins::common::map_control_flow_with_builtin;
12use crate::builtins::common::spec::{
13    BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
14    ReductionNaN, ResidencyPolicy, ShapeRequirements,
15};
16use crate::builtins::strings::common::{
17    char_row_to_string_slice, contains_resident_text_input, is_missing_string,
18};
19use crate::builtins::strings::type_resolvers::text_preserve_type;
20use crate::{
21    build_runtime_error, gather_if_needed_async, make_cell_with_shape, BuiltinResult, RuntimeError,
22};
23
24#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::strings::transform::strrep")]
25pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
26    name: "strrep",
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: "Executes on the CPU; GPU-resident inputs are gathered before replacements are applied.",
38};
39
40#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::strings::transform::strrep")]
41pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
42    name: "strrep",
43    shape: ShapeRequirements::Any,
44    constant_strategy: ConstantStrategy::InlineLiteral,
45    elementwise: None,
46    reduction: None,
47    emits_nan: false,
48    notes: "String transformation builtin; marked as a sink so fusion skips GPU residency.",
49};
50
51const BUILTIN_NAME: &str = "strrep";
52
53const STRREP_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
54    name: "newStr",
55    ty: BuiltinParamType::Any,
56    arity: BuiltinParamArity::Required,
57    default: None,
58    description: "Text with pattern occurrences replaced, preserving input container kind.",
59}];
60
61const STRREP_INPUTS: [BuiltinParamDescriptor; 3] = [
62    BuiltinParamDescriptor {
63        name: "str",
64        ty: BuiltinParamType::Any,
65        arity: BuiltinParamArity::Required,
66        default: None,
67        description: "Input text (string/char/cell).",
68    },
69    BuiltinParamDescriptor {
70        name: "old",
71        ty: BuiltinParamType::Any,
72        arity: BuiltinParamArity::Required,
73        default: None,
74        description: "Pattern text scalar (string or char row).",
75    },
76    BuiltinParamDescriptor {
77        name: "new",
78        ty: BuiltinParamType::Any,
79        arity: BuiltinParamArity::Required,
80        default: None,
81        description: "Replacement text scalar matching old's data type family.",
82    },
83];
84
85const STRREP_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
86    label: "newStr = strrep(str, old, new)",
87    inputs: &STRREP_INPUTS,
88    outputs: &STRREP_OUTPUT,
89}];
90
91const STRREP_ERROR_INVALID_INPUT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
92    code: "RM.STRREP.INVALID_INPUT",
93    identifier: Some("RunMat:strrep:InvalidInput"),
94    when: "First argument is not a string array, char array, or cell array of text scalars.",
95    message:
96        "strrep: first argument must be a string array, character array, or cell array of character vectors",
97};
98
99const STRREP_ERROR_PATTERN_TYPE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
100    code: "RM.STRREP.PATTERN_TYPE",
101    identifier: Some("RunMat:strrep:PatternType"),
102    when: "old/new arguments are not string scalars or character vectors.",
103    message: "strrep: old and new must be string scalars or character vectors",
104};
105
106const STRREP_ERROR_PATTERN_MISMATCH: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
107    code: "RM.STRREP.PATTERN_MISMATCH",
108    identifier: Some("RunMat:strrep:PatternMismatch"),
109    when: "old and new are different text data families (string vs char).",
110    message: "strrep: old and new must be the same data type",
111};
112
113const STRREP_ERROR_CELL_ELEMENT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
114    code: "RM.STRREP.CELL_ELEMENT",
115    identifier: Some("RunMat:strrep:CellElement"),
116    when: "Cell input contains non-text elements or non-row char arrays.",
117    message: "strrep: cell array elements must be string scalars or character vectors",
118};
119
120const STRREP_ERROR_INTERNAL: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
121    code: "RM.STRREP.INTERNAL",
122    identifier: Some("RunMat:strrep:InternalError"),
123    when: "Internal output container construction failed.",
124    message: "strrep: internal error",
125};
126
127const STRREP_ERRORS: [BuiltinErrorDescriptor; 5] = [
128    STRREP_ERROR_INVALID_INPUT,
129    STRREP_ERROR_PATTERN_TYPE,
130    STRREP_ERROR_PATTERN_MISMATCH,
131    STRREP_ERROR_CELL_ELEMENT,
132    STRREP_ERROR_INTERNAL,
133];
134
135pub const STRREP_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
136    signatures: &STRREP_SIGNATURES,
137    output_mode: BuiltinOutputMode::Fixed,
138    completion_policy: BuiltinCompletionPolicy::Public,
139    errors: &STRREP_ERRORS,
140};
141
142pub const STRREP_INTEGER_AUDIT: BuiltinIntegerAuditDescriptor = BuiltinIntegerAuditDescriptor {
143    kind: BuiltinIntegerAuditKind::NotApplicable,
144    canonical_builtin: None,
145    notes: "strrep accepts text in the subject, old-pattern, and replacement roles. Numeric, integer, and provider-resident values reject before provider access and are never interpreted as character codes.",
146};
147
148#[derive(Clone, Copy, PartialEq, Eq)]
149enum PatternKind {
150    String,
151    Char,
152}
153
154fn map_flow(err: RuntimeError) -> RuntimeError {
155    map_control_flow_with_builtin(err, BUILTIN_NAME)
156}
157
158fn strrep_error_with_message(
159    message: impl Into<String>,
160    error: &'static BuiltinErrorDescriptor,
161) -> RuntimeError {
162    let mut builder = build_runtime_error(message).with_builtin(BUILTIN_NAME);
163    if let Some(identifier) = error.identifier {
164        builder = builder.with_identifier(identifier);
165    }
166    builder.build()
167}
168
169fn strrep_error(error: &'static BuiltinErrorDescriptor) -> RuntimeError {
170    strrep_error_with_message(error.message, error)
171}
172
173#[runtime_builtin(
174    name = "strrep",
175    category = "strings/transform",
176    summary = "Replace non-overlapping substring occurrences in text inputs.",
177    keywords = "strrep,replace,strings,character array,text",
178    accel = "sink",
179    type_resolver(text_preserve_type),
180    descriptor(crate::builtins::strings::transform::strrep::STRREP_DESCRIPTOR),
181    integer_audit(crate::builtins::strings::transform::strrep::STRREP_INTEGER_AUDIT),
182    builtin_path = "crate::builtins::strings::transform::strrep"
183)]
184async fn strrep_builtin(
185    str_value: Value,
186    old_value: Value,
187    new_value: Value,
188) -> BuiltinResult<Value> {
189    if contains_resident_text_input(&str_value)
190        || contains_resident_text_input(&old_value)
191        || contains_resident_text_input(&new_value)
192    {
193        return Err(strrep_error(&STRREP_ERROR_INVALID_INPUT));
194    }
195    let gathered_str = gather_if_needed_async(&str_value).await.map_err(map_flow)?;
196    let gathered_old = gather_if_needed_async(&old_value).await.map_err(map_flow)?;
197    let gathered_new = gather_if_needed_async(&new_value).await.map_err(map_flow)?;
198
199    let (old_text, old_kind) = parse_pattern(gathered_old)?;
200    let (new_text, new_kind) = parse_pattern(gathered_new)?;
201    if old_kind != new_kind {
202        return Err(strrep_error(&STRREP_ERROR_PATTERN_MISMATCH));
203    }
204
205    match gathered_str {
206        Value::String(text) => Ok(Value::String(strrep_string_value(
207            text, &old_text, &new_text,
208        ))),
209        Value::StringArray(array) => strrep_string_array(array, &old_text, &new_text),
210        Value::CharArray(array) => strrep_char_array(array, &old_text, &new_text),
211        Value::Cell(cell) => strrep_cell_array(cell, &old_text, &new_text),
212        _ => Err(strrep_error(&STRREP_ERROR_INVALID_INPUT)),
213    }
214}
215
216fn parse_pattern(value: Value) -> BuiltinResult<(String, PatternKind)> {
217    match value {
218        Value::String(text) => Ok((text, PatternKind::String)),
219        Value::StringArray(array) => {
220            if array.data.len() == 1 {
221                Ok((array.data[0].clone(), PatternKind::String))
222            } else {
223                Err(strrep_error(&STRREP_ERROR_PATTERN_TYPE))
224            }
225        }
226        Value::CharArray(array) => {
227            if array.rows <= 1 {
228                let text = if array.rows == 0 {
229                    String::new()
230                } else {
231                    char_row_to_string_slice(&array.data, array.cols, 0)
232                };
233                Ok((text, PatternKind::Char))
234            } else {
235                Err(strrep_error(&STRREP_ERROR_PATTERN_TYPE))
236            }
237        }
238        _ => Err(strrep_error(&STRREP_ERROR_PATTERN_TYPE)),
239    }
240}
241
242fn strrep_string_value(text: String, old: &str, new: &str) -> String {
243    if is_missing_string(&text) {
244        text
245    } else {
246        text.replace(old, new)
247    }
248}
249
250fn strrep_string_array(array: StringArray, old: &str, new: &str) -> BuiltinResult<Value> {
251    let StringArray { data, shape, .. } = array;
252    let replaced = data
253        .into_iter()
254        .map(|text| strrep_string_value(text, old, new))
255        .collect::<Vec<_>>();
256    let rebuilt = StringArray::new(replaced, shape).map_err(|e| {
257        strrep_error_with_message(format!("{BUILTIN_NAME}: {e}"), &STRREP_ERROR_INTERNAL)
258    })?;
259    Ok(Value::StringArray(rebuilt))
260}
261
262fn strrep_char_array(array: CharArray, old: &str, new: &str) -> BuiltinResult<Value> {
263    let CharArray {
264        data,
265        shape,
266        rows,
267        cols,
268    } = array;
269    if rows == 0 || cols == 0 {
270        return Ok(Value::CharArray(CharArray {
271            data,
272            shape,
273            rows,
274            cols,
275        }));
276    }
277
278    let mut replaced_rows = Vec::with_capacity(rows);
279    let mut target_cols = 0usize;
280    for row in 0..rows {
281        let text = char_row_to_string_slice(&data, cols, row);
282        let replaced = text.replace(old, new);
283        target_cols = target_cols.max(replaced.chars().count());
284        replaced_rows.push(replaced);
285    }
286
287    let mut new_data = Vec::with_capacity(rows * target_cols);
288    for row_text in replaced_rows {
289        let mut chars: Vec<char> = row_text.chars().collect();
290        if chars.len() < target_cols {
291            chars.resize(target_cols, ' ');
292        }
293        new_data.extend(chars);
294    }
295
296    CharArray::new(new_data, rows, target_cols)
297        .map(Value::CharArray)
298        .map_err(|e| {
299            strrep_error_with_message(format!("{BUILTIN_NAME}: {e}"), &STRREP_ERROR_INTERNAL)
300        })
301}
302
303fn strrep_cell_array(cell: CellArray, old: &str, new: &str) -> BuiltinResult<Value> {
304    let CellArray { data, shape, .. } = cell;
305    let mut replaced = Vec::with_capacity(data.len());
306    for ptr in &data {
307        replaced.push(strrep_cell_element(ptr, old, new)?);
308    }
309    make_cell_with_shape(replaced, shape).map_err(|e| {
310        strrep_error_with_message(format!("{BUILTIN_NAME}: {e}"), &STRREP_ERROR_INTERNAL)
311    })
312}
313
314fn strrep_cell_element(value: &Value, old: &str, new: &str) -> BuiltinResult<Value> {
315    match value {
316        Value::String(text) => Ok(Value::String(strrep_string_value(text.clone(), old, new))),
317        Value::StringArray(array) => strrep_string_array(array.clone(), old, new),
318        Value::CharArray(array) => strrep_char_array(array.clone(), old, new),
319        _ => Err(strrep_error(&STRREP_ERROR_CELL_ELEMENT)),
320    }
321}
322
323#[cfg(test)]
324pub(crate) mod tests {
325    use super::*;
326    use runmat_builtins::{ResolveContext, Type};
327
328    fn run_strrep(str_value: Value, old_value: Value, new_value: Value) -> BuiltinResult<Value> {
329        futures::executor::block_on(strrep_builtin(str_value, old_value, new_value))
330    }
331
332    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
333    #[test]
334    fn strrep_string_scalar_basic() {
335        let result = run_strrep(
336            Value::String("RunMat Ignite".into()),
337            Value::String("Ignite".into()),
338            Value::String("Interpreter".into()),
339        )
340        .expect("strrep");
341        assert_eq!(result, Value::String("RunMat Interpreter".into()));
342    }
343
344    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
345    #[test]
346    fn strrep_string_array_preserves_missing() {
347        let array = StringArray::new(
348            vec![
349                String::from("gpu"),
350                String::from("<missing>"),
351                String::from("planner"),
352            ],
353            vec![3, 1],
354        )
355        .unwrap();
356        let result = run_strrep(
357            Value::StringArray(array),
358            Value::String("gpu".into()),
359            Value::String("GPU".into()),
360        )
361        .expect("strrep");
362        match result {
363            Value::StringArray(sa) => {
364                assert_eq!(sa.shape, vec![3, 1]);
365                assert_eq!(
366                    sa.data,
367                    vec![
368                        String::from("GPU"),
369                        String::from("<missing>"),
370                        String::from("planner")
371                    ]
372                );
373            }
374            other => panic!("expected string array, got {other:?}"),
375        }
376    }
377
378    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
379    #[test]
380    fn strrep_string_array_with_char_pattern() {
381        let array = StringArray::new(
382            vec![String::from("alpha"), String::from("beta")],
383            vec![2, 1],
384        )
385        .unwrap();
386        let result = run_strrep(
387            Value::StringArray(array),
388            Value::CharArray(CharArray::new_row("a")),
389            Value::CharArray(CharArray::new_row("A")),
390        )
391        .expect("strrep");
392        match result {
393            Value::StringArray(sa) => {
394                assert_eq!(sa.shape, vec![2, 1]);
395                assert_eq!(sa.data, vec![String::from("AlphA"), String::from("betA")]);
396            }
397            other => panic!("expected string array, got {other:?}"),
398        }
399    }
400
401    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
402    #[test]
403    fn strrep_char_array_padding() {
404        let chars = CharArray::new(vec!['R', 'u', 'n', ' ', 'M', 'a', 't'], 1, 7).unwrap();
405        let result = run_strrep(
406            Value::CharArray(chars),
407            Value::String(" ".into()),
408            Value::String("_".into()),
409        )
410        .expect("strrep");
411        match result {
412            Value::CharArray(out) => {
413                assert_eq!(out.rows, 1);
414                assert_eq!(out.cols, 7);
415                let expected: Vec<char> = "Run_Mat".chars().collect();
416                assert_eq!(out.data, expected);
417            }
418            other => panic!("expected char array, got {other:?}"),
419        }
420    }
421
422    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
423    #[test]
424    fn strrep_char_array_shrinks_rows_pad_with_spaces() {
425        let mut data: Vec<char> = "alpha".chars().collect();
426        data.extend("beta ".chars());
427        let array = CharArray::new(data, 2, 5).unwrap();
428        let result = run_strrep(
429            Value::CharArray(array),
430            Value::String("a".into()),
431            Value::String("".into()),
432        )
433        .expect("strrep");
434        match result {
435            Value::CharArray(out) => {
436                assert_eq!(out.rows, 2);
437                assert_eq!(out.cols, 4);
438                let expected: Vec<char> = vec!['l', 'p', 'h', ' ', 'b', 'e', 't', ' '];
439                assert_eq!(out.data, expected);
440            }
441            other => panic!("expected char array, got {other:?}"),
442        }
443    }
444
445    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
446    #[test]
447    fn strrep_cell_array_char_vectors() {
448        let cell = CellArray::new(
449            vec![
450                Value::CharArray(CharArray::new_row("Kernel Fusion")),
451                Value::CharArray(CharArray::new_row("GPU Planner")),
452            ],
453            1,
454            2,
455        )
456        .unwrap();
457        let result = run_strrep(
458            Value::Cell(cell),
459            Value::String(" ".into()),
460            Value::String("_".into()),
461        )
462        .expect("strrep");
463        match result {
464            Value::Cell(out) => {
465                assert_eq!(out.rows, 1);
466                assert_eq!(out.cols, 2);
467                assert_eq!(
468                    out.get(0, 0).unwrap(),
469                    Value::CharArray(CharArray::new_row("Kernel_Fusion"))
470                );
471                assert_eq!(
472                    out.get(0, 1).unwrap(),
473                    Value::CharArray(CharArray::new_row("GPU_Planner"))
474                );
475            }
476            other => panic!("expected cell array, got {other:?}"),
477        }
478    }
479
480    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
481    #[test]
482    fn strrep_cell_array_string_scalars() {
483        let cell = CellArray::new(
484            vec![
485                Value::String("Planner".into()),
486                Value::String("Profiler".into()),
487            ],
488            1,
489            2,
490        )
491        .unwrap();
492        let result = run_strrep(
493            Value::Cell(cell),
494            Value::String("er".into()),
495            Value::String("ER".into()),
496        )
497        .expect("strrep");
498        match result {
499            Value::Cell(out) => {
500                assert_eq!(out.rows, 1);
501                assert_eq!(out.cols, 2);
502                assert_eq!(out.get(0, 0).unwrap(), Value::String("PlannER".into()));
503                assert_eq!(out.get(0, 1).unwrap(), Value::String("ProfilER".into()));
504            }
505            other => panic!("expected cell array, got {other:?}"),
506        }
507    }
508
509    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
510    #[test]
511    fn strrep_cell_array_invalid_element_error() {
512        let cell = CellArray::new(vec![Value::Num(1.0)], 1, 1).unwrap();
513        let err = run_strrep(
514            Value::Cell(cell),
515            Value::String("1".into()),
516            Value::String("one".into()),
517        )
518        .expect_err("expected cell element error");
519        assert!(err.to_string().contains("cell array elements"));
520    }
521
522    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
523    #[test]
524    fn strrep_cell_array_char_matrix_element() {
525        let mut chars: Vec<char> = "alpha".chars().collect();
526        chars.extend("beta ".chars());
527        let element = CharArray::new(chars, 2, 5).unwrap();
528        let cell = CellArray::new(vec![Value::CharArray(element)], 1, 1).unwrap();
529        let result = run_strrep(
530            Value::Cell(cell),
531            Value::String("a".into()),
532            Value::String("A".into()),
533        )
534        .expect("strrep");
535        match result {
536            Value::Cell(out) => {
537                let nested = out.get(0, 0).unwrap();
538                match nested {
539                    Value::CharArray(ca) => {
540                        assert_eq!(ca.rows, 2);
541                        assert_eq!(ca.cols, 5);
542                        let expected: Vec<char> =
543                            vec!['A', 'l', 'p', 'h', 'A', 'b', 'e', 't', 'A', ' '];
544                        assert_eq!(ca.data, expected);
545                    }
546                    other => panic!("expected char array element, got {other:?}"),
547                }
548            }
549            other => panic!("expected cell array, got {other:?}"),
550        }
551    }
552
553    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
554    #[test]
555    fn strrep_cell_array_string_arrays() {
556        let element = StringArray::new(vec!["alpha".into(), "beta".into()], vec![1, 2]).unwrap();
557        let cell = CellArray::new(vec![Value::StringArray(element)], 1, 1).unwrap();
558        let result = run_strrep(
559            Value::Cell(cell),
560            Value::String("a".into()),
561            Value::String("A".into()),
562        )
563        .expect("strrep");
564        match result {
565            Value::Cell(out) => {
566                let nested = out.get(0, 0).unwrap();
567                match nested {
568                    Value::StringArray(sa) => {
569                        assert_eq!(sa.shape, vec![1, 2]);
570                        assert_eq!(sa.data, vec![String::from("AlphA"), String::from("betA")]);
571                    }
572                    other => panic!("expected string array element, got {other:?}"),
573                }
574            }
575            other => panic!("expected cell array, got {other:?}"),
576        }
577    }
578
579    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
580    #[test]
581    fn strrep_empty_pattern_inserts_replacement() {
582        let result = run_strrep(
583            Value::String("abc".into()),
584            Value::String("".into()),
585            Value::String("-".into()),
586        )
587        .expect("strrep");
588        assert_eq!(result, Value::String("-a-b-c-".into()));
589    }
590
591    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
592    #[test]
593    fn strrep_type_mismatch_errors() {
594        let err = run_strrep(
595            Value::String("abc".into()),
596            Value::String("a".into()),
597            Value::CharArray(CharArray::new_row("x")),
598        )
599        .expect_err("expected type mismatch");
600        assert!(err.to_string().contains("same data type"));
601    }
602
603    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
604    #[test]
605    fn strrep_invalid_pattern_type_errors() {
606        let err = run_strrep(
607            Value::String("abc".into()),
608            Value::Num(1.0),
609            Value::String("x".into()),
610        )
611        .expect_err("expected pattern error");
612        assert!(err
613            .to_string()
614            .contains("string scalars or character vectors"));
615    }
616
617    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
618    #[test]
619    fn strrep_first_argument_type_error() {
620        let err = run_strrep(
621            Value::Num(42.0),
622            Value::String("a".into()),
623            Value::String("b".into()),
624        )
625        .expect_err("expected argument type error");
626        assert!(err.to_string().contains("first argument"));
627    }
628
629    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
630    #[test]
631    #[cfg(feature = "wgpu")]
632    fn strrep_wgpu_provider_fallback() {
633        if runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
634            runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
635        )
636        .is_err()
637        {
638            // Unable to initialize the provider in this environment; skip.
639            return;
640        }
641        let result = run_strrep(
642            Value::String("Native Engine".into()),
643            Value::String("Engine".into()),
644            Value::String("JIT".into()),
645        )
646        .expect("strrep");
647        assert_eq!(result, Value::String("Native JIT".into()));
648    }
649
650    #[test]
651    fn strrep_type_preserves_text() {
652        assert_eq!(
653            text_preserve_type(&[Type::String], &ResolveContext::new(Vec::new())),
654            Type::String
655        );
656    }
657}