runmat_runtime/builtins/strings/core/
strings.rs

1//! MATLAB-compatible `strings` builtin that preallocates string arrays filled with empty scalars.
2
3use runmat_builtins::{LogicalArray, StringArray, Tensor, Value};
4use runmat_macros::runtime_builtin;
5
6use crate::builtins::common::random_args::{keyword_of, shape_from_value};
7use crate::builtins::common::spec::{
8    BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
9    ReductionNaN, ResidencyPolicy, ShapeRequirements,
10};
11#[cfg(feature = "doc_export")]
12use crate::register_builtin_doc_text;
13use crate::{gather_if_needed, register_builtin_fusion_spec, register_builtin_gpu_spec};
14
15const FN_NAME: &str = "strings";
16const SIZE_INTEGER_ERR: &str = "size inputs must be integers";
17const SIZE_NONNEGATIVE_ERR: &str = "size inputs must be nonnegative integers";
18const SIZE_FINITE_ERR: &str = "size inputs must be finite";
19const SIZE_NUMERIC_ERR: &str = "size arguments must be numeric scalars or vectors";
20const SIZE_SCALAR_ERR: &str = "size inputs must be scalar";
21
22#[cfg(feature = "doc_export")]
23pub const DOC_MD: &str = r#"---
24title: "strings"
25category: "strings/core"
26keywords: ["strings", "preallocate", "string array", "empty strings", "missing", "like", "gpu"]
27summary: "Preallocate string arrays filled with empty text scalars using MATLAB-compatible size syntax."
28references:
29  - https://www.mathworks.com/help/matlab/ref/strings.html
30gpu_support:
31  elementwise: false
32  reduction: false
33  precisions: []
34  broadcasting: "none"
35  notes: "Runs entirely on the host; GPU-resident size inputs are gathered before allocation and outputs always live in host memory."
36fusion:
37  elementwise: false
38  reduction: false
39  max_inputs: 0
40  constants: "inline"
41requires_feature: null
42tested:
43  unit: "builtins::strings::core::strings::tests"
44  integration: "builtins::strings::core::strings::tests::doc_examples_present"
45---
46
47# What does the `strings` function do in MATLAB / RunMat?
48`strings` creates string arrays whose elements are empty string scalars (`""`). It mirrors MATLAB's
49preallocation helper, accepting scalar, vector, or multiple dimension arguments to control the
50array shape.
51
52## How does the `strings` function behave in MATLAB / RunMat?
53- `strings` with no inputs returns a 1×1 string array containing `""`.
54- `strings(n)` produces an `n`-by-`n` array of empty strings. The single input must be a nonnegative
55  integer scalar.
56- `strings(sz1,...,szN)` and `strings(sz)` accept nonnegative integer sizes. All specified
57  dimensions—including trailing singletons—are preserved in the resulting array.
58- Setting any dimension to `0` yields an empty array whose remaining dimensions still shape the
59  result (for example, `strings(0, 5, 3)` is a `0×5×3` string array).
60- `strings(___, "missing")` fills the allocation with the missing sentinel (`<missing>`) instead of
61  empty strings, which is useful when you plan to replace placeholders later.
62- `strings(___, "like", prototype)` or `strings("like", prototype)` reuses the size of `prototype`
63  when you omit explicit dimensions. Any provided dimensions still take precedence, and GPU
64  prototypes are gathered before their shape is inspected.
65- Size inputs must be finite integers. Negative, fractional, or NaN values trigger
66  MATLAB-compatible "Size inputs must be nonnegative integers" errors.
67- Only numeric or logical size arguments are supported. Other types (strings, structs, objects)
68  raise descriptive errors.
69
70## `strings` Function GPU Execution Behaviour
71`strings` never allocates data on the GPU. Size arguments that reside on a GPU are automatically
72gathered to the host before validation, and the resulting string array always lives in host memory.
73`"like"` prototypes follow the same rule—they are gathered before their shape is inspected. No
74provider hooks are required, so the GPU metadata marks the builtin as a gather-only operation.
75
76## Examples of using the `strings` function in MATLAB / RunMat
77
78### Creating a square array of empty strings
79```matlab
80S = strings(4);
81```
82Expected output:
83```matlab
84S = 4x4 string
85    ""    ""    ""    ""
86    ""    ""    ""    ""
87    ""    ""    ""    ""
88    ""    ""    ""    ""
89```
90
91### Preallocating with separate dimension arguments
92```matlab
93grid = strings(2, 3, 4);
94```
95Expected output:
96```matlab
97grid = 2x3x4 string
98grid(:,:,1) =
99    ""    ""    ""
100    ""    ""    ""
101```
102
103### Cloning the size of another array
104```matlab
105A = magic(3);
106placeholders = strings(size(A));
107```
108Expected output:
109```matlab
110placeholders = 3x3 string
111    ""    ""    ""
112    ""    ""    ""
113    ""    ""    ""
114```
115
116### Handling zero dimensions
117```matlab
118emptyRow = strings(0, 5);
119```
120Expected output:
121```matlab
122emptyRow = 0x5 string
123```
124
125### Preserving trailing singleton dimensions
126```matlab
127column = strings(3, 1, 1, 1);
128sz = size(column);
129```
130Expected output:
131```matlab
132sz =
133     3     1     1     1
134```
135
136### Filling arrays with missing string scalars
137```matlab
138placeholders = strings(2, 3, "missing");
139```
140Expected output:
141```matlab
142placeholders = 2x3 string
143    <missing>    <missing>    <missing>
144    <missing>    <missing>    <missing>
145```
146
147### Matching an existing array with `'like'`
148```matlab
149proto = zeros(3, 2);
150labels = strings("like", proto);
151```
152Expected output:
153```matlab
154labels = 3x2 string
155    ""    ""
156    ""    ""
157    ""    ""
158```
159
160### Validating size inputs
161```matlab
162try
163    strings(-3);
164catch ME
165    disp(ME.message)
166end
167```
168Expected output:
169```matlab
170Error using strings
171Size inputs must be nonnegative integers.
172```
173
174## FAQ
175
176### How is `strings` different from `string`?
177`strings` preallocates empty string scalars, while `string` converts existing data to string
178scalars. Use `strings` to reserve space, then assign values later.
179
180### Can I use non-integer sizes such as 2.5?
181No. All size arguments must be finite integers. Fractional or NaN values raise descriptive errors.
182
183### How do I create missing string values (`<missing>`)?
184Pass `"missing"` as an option— for example `strings(2, 3, "missing")` produces a `2×3` array filled
185with `<missing>` placeholders. You can still assign values later to replace the sentinel.
186
187### How can I reuse the size of an existing array?
188Provide the `"like"` option: `strings("like", prototype)` copies the size of `prototype` when you do
189not supply explicit dimensions. Any dimensions you specify override the inferred size.
190
191### Does the output ever live on the GPU?
192No. `strings` always returns a host-resident string array. GPU inputs supplying sizes are gathered
193before validation.
194
195### How can I create a row versus column vector?
196Use `strings(1, n)` for a row and `strings(n, 1)` for a column. Additional dimensions—including trailing singletons—remain part of the array shape.
197
198### Can I pass non-numeric types such as structs or string arrays as size inputs?
199No. Only numeric or logical values are accepted. Other types produce MATLAB-compatible usage
200errors.
201
202### Is there an equivalent to `string.empty`?
203Yes. `strings(0)` returns the same 0-by-0 empty string array as `string.empty`.
204
205## See Also
206`string`, `char`, `zeros`, `string.empty`
207"#;
208
209pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
210    name: FN_NAME,
211    op_kind: GpuOpKind::Custom("array_creation"),
212    supported_precisions: &[],
213    broadcast: BroadcastSemantics::None,
214    provider_hooks: &[],
215    constant_strategy: ConstantStrategy::InlineLiteral,
216    residency: ResidencyPolicy::GatherImmediately,
217    nan_mode: ReductionNaN::Include,
218    two_pass_threshold: None,
219    workgroup_size: None,
220    accepts_nan_mode: false,
221    notes: "Runs entirely on the host; size arguments pulled from the GPU are gathered before allocation.",
222};
223
224register_builtin_gpu_spec!(GPU_SPEC);
225
226pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
227    name: FN_NAME,
228    shape: ShapeRequirements::Any,
229    constant_strategy: ConstantStrategy::InlineLiteral,
230    elementwise: None,
231    reduction: None,
232    emits_nan: false,
233    notes: "Preallocates host string arrays; no fusion-supported kernels are generated.",
234};
235
236register_builtin_fusion_spec!(FUSION_SPEC);
237
238#[cfg(feature = "doc_export")]
239register_builtin_doc_text!(FN_NAME, DOC_MD);
240
241struct ParsedStrings {
242    shape: Vec<usize>,
243    fill: FillKind,
244}
245
246#[derive(Clone, Copy, PartialEq, Eq)]
247enum FillKind {
248    Empty,
249    Missing,
250}
251
252#[runtime_builtin(
253    name = "strings",
254    category = "strings/core",
255    summary = "Preallocate string arrays filled with empty string scalars.",
256    keywords = "strings,string array,empty,preallocate",
257    accel = "array_construct"
258)]
259fn strings_builtin(rest: Vec<Value>) -> Result<Value, String> {
260    let ParsedStrings { shape, fill } = parse_arguments(rest)?;
261    let total = shape.iter().try_fold(1usize, |acc, &dim| {
262        acc.checked_mul(dim)
263            .ok_or_else(|| format!("{FN_NAME}: requested size exceeds platform limits"))
264    })?;
265
266    let fill_text = match fill {
267        FillKind::Empty => String::new(),
268        FillKind::Missing => "<missing>".to_string(),
269    };
270
271    let mut data = Vec::with_capacity(total);
272    for _ in 0..total {
273        data.push(fill_text.clone());
274    }
275
276    let array = StringArray::new(data, shape).map_err(|e| format!("{FN_NAME}: {e}"))?;
277    Ok(Value::StringArray(array))
278}
279
280fn parse_arguments(args: Vec<Value>) -> Result<ParsedStrings, String> {
281    let mut size_values: Vec<Value> = Vec::new();
282    let mut like_proto: Option<Value> = None;
283    let mut fill = FillKind::Empty;
284
285    let mut idx = 0;
286    while idx < args.len() {
287        let host = gather_if_needed(&args[idx]).map_err(|e| format!("{FN_NAME}: {e}"))?;
288        if let Some(keyword) = keyword_of(&host) {
289            match keyword.as_str() {
290                "like" => {
291                    if like_proto.is_some() {
292                        return Err(format!(
293                            "{FN_NAME}: multiple 'like' specifications are not supported"
294                        ));
295                    }
296                    let Some(proto_raw) = args.get(idx + 1) else {
297                        return Err(format!("{FN_NAME}: expected prototype after 'like'"));
298                    };
299                    let proto =
300                        gather_if_needed(proto_raw).map_err(|e| format!("{FN_NAME}: {e}"))?;
301                    like_proto = Some(proto);
302                    idx += 2;
303                    continue;
304                }
305                "missing" => {
306                    fill = FillKind::Missing;
307                    idx += 1;
308                    continue;
309                }
310                "empty" => {
311                    fill = FillKind::Empty;
312                    idx += 1;
313                    continue;
314                }
315                _ => {}
316            }
317        }
318        size_values.push(host);
319        idx += 1;
320    }
321
322    let dims = parse_size_values(size_values)?;
323    let mut shape = if let Some(dims) = dims {
324        normalize_dims(dims)
325    } else if let Some(proto) = like_proto.as_ref() {
326        prototype_shape(proto)?
327    } else {
328        vec![1, 1]
329    };
330
331    if shape.is_empty() {
332        shape = vec![0, 0];
333    }
334
335    Ok(ParsedStrings { shape, fill })
336}
337
338fn prototype_shape(value: &Value) -> Result<Vec<usize>, String> {
339    match value {
340        Value::StringArray(sa) => Ok(sa.shape.clone()),
341        _ => shape_from_value(value, FN_NAME),
342    }
343}
344
345fn err_integer() -> String {
346    format!("{FN_NAME}: {SIZE_INTEGER_ERR}")
347}
348
349fn err_nonnegative() -> String {
350    format!("{FN_NAME}: {SIZE_NONNEGATIVE_ERR}")
351}
352
353fn err_finite() -> String {
354    format!("{FN_NAME}: {SIZE_FINITE_ERR}")
355}
356
357fn parse_size_values(values: Vec<Value>) -> Result<Option<Vec<usize>>, String> {
358    match values.len() {
359        0 => Ok(None),
360        1 => parse_single_argument(values.into_iter().next().unwrap()).map(Some),
361        _ => {
362            let mut dims = Vec::with_capacity(values.len());
363            for value in &values {
364                dims.push(parse_size_scalar(value)?);
365            }
366            Ok(Some(dims))
367        }
368    }
369}
370
371fn parse_single_argument(value: Value) -> Result<Vec<usize>, String> {
372    match value {
373        Value::Int(iv) => Ok(vec![validate_i64_dimension(iv.to_i64())?]),
374        Value::Num(n) => Ok(vec![parse_numeric_dimension(n)?]),
375        Value::Bool(b) => Ok(vec![if b { 1 } else { 0 }]),
376        Value::Tensor(t) => parse_size_tensor(&t),
377        Value::LogicalArray(arr) => parse_size_logical_array(&arr),
378        other => Err(format!("{FN_NAME}: {SIZE_NUMERIC_ERR}, got {other:?}")),
379    }
380}
381
382fn parse_size_scalar(value: &Value) -> Result<usize, String> {
383    match value {
384        Value::Int(iv) => {
385            let raw = iv.to_i64();
386            validate_i64_dimension(raw)
387        }
388        Value::Num(n) => parse_numeric_dimension(*n),
389        Value::Bool(b) => Ok(if *b { 1 } else { 0 }),
390        Value::Tensor(t) => {
391            if t.data.len() != 1 {
392                return Err(format!("{FN_NAME}: {SIZE_SCALAR_ERR}"));
393            }
394            parse_numeric_dimension(t.data[0])
395        }
396        Value::LogicalArray(arr) => {
397            if arr.data.len() != 1 {
398                return Err(format!("{FN_NAME}: {SIZE_SCALAR_ERR}"));
399            }
400            Ok(if arr.data[0] != 0 { 1 } else { 0 })
401        }
402        other => Err(format!("{FN_NAME}: {SIZE_NUMERIC_ERR}, got {other:?}")),
403    }
404}
405
406fn parse_size_tensor(tensor: &Tensor) -> Result<Vec<usize>, String> {
407    if tensor.data.is_empty() {
408        return Ok(vec![0, 0]);
409    }
410    if !is_vector_shape(&tensor.shape) {
411        return Err(format!(
412            "{FN_NAME}: size vector must be a row or column vector"
413        ));
414    }
415    tensor
416        .data
417        .iter()
418        .map(|&value| parse_numeric_dimension(value))
419        .collect()
420}
421
422fn parse_size_logical_array(array: &LogicalArray) -> Result<Vec<usize>, String> {
423    if array.data.is_empty() {
424        return Ok(vec![0, 0]);
425    }
426    if !is_vector_shape(&array.shape) {
427        return Err(format!(
428            "{FN_NAME}: size vector must be a row or column vector"
429        ));
430    }
431    array
432        .data
433        .iter()
434        .map(|&value| Ok(if value != 0 { 1 } else { 0 }))
435        .collect()
436}
437
438fn parse_numeric_dimension(value: f64) -> Result<usize, String> {
439    if !value.is_finite() {
440        return Err(err_finite());
441    }
442    let rounded = value.round();
443    if (rounded - value).abs() > f64::EPSILON {
444        return Err(err_integer());
445    }
446    if rounded < 0.0 {
447        return Err(err_nonnegative());
448    }
449    if rounded > usize::MAX as f64 {
450        return Err(format!(
451            "{FN_NAME}: requested dimension exceeds platform limits"
452        ));
453    }
454    Ok(rounded as usize)
455}
456
457fn normalize_dims(dims: Vec<usize>) -> Vec<usize> {
458    match dims.len() {
459        0 => vec![0, 0],
460        1 => {
461            let side = dims[0];
462            vec![side, side]
463        }
464        _ => dims,
465    }
466}
467
468fn is_vector_shape(shape: &[usize]) -> bool {
469    match shape.len() {
470        0 | 1 => true,
471        2 => shape[0] == 1 || shape[1] == 1,
472        _ => shape.iter().filter(|&&d| d > 1).count() <= 1,
473    }
474}
475
476fn validate_i64_dimension(raw: i64) -> Result<usize, String> {
477    if raw < 0 {
478        return Err(err_nonnegative());
479    }
480    if (raw as u128) > (usize::MAX as u128) {
481        return Err(format!(
482            "{FN_NAME}: requested dimension exceeds platform limits"
483        ));
484    }
485    Ok(raw as usize)
486}
487
488#[cfg(test)]
489mod tests {
490    use super::*;
491
492    use crate::builtins::common::test_support;
493    use runmat_accelerate_api::HostTensorView;
494
495    #[test]
496    fn strings_default_scalar() {
497        let result = strings_builtin(Vec::new()).expect("strings");
498        match result {
499            Value::StringArray(array) => {
500                assert_eq!(array.shape, vec![1, 1]);
501                assert_eq!(array.data, vec![String::new()]);
502            }
503            other => panic!("expected string array, got {other:?}"),
504        }
505    }
506
507    #[test]
508    fn strings_square_from_single_dimension() {
509        let args = vec![Value::Num(4.0)];
510        let result = strings_builtin(args).expect("strings");
511        match result {
512            Value::StringArray(array) => {
513                assert_eq!(array.shape, vec![4, 4]);
514                assert!(array.data.iter().all(|s| s.is_empty()));
515            }
516            other => panic!("expected string array, got {other:?}"),
517        }
518    }
519
520    #[test]
521    fn strings_rectangular_multiple_args() {
522        let args = vec![
523            Value::Int(runmat_builtins::IntValue::I32(2)),
524            Value::Num(3.0),
525        ];
526        let result = strings_builtin(args).expect("strings");
527        match result {
528            Value::StringArray(array) => {
529                assert_eq!(array.shape, vec![2, 3]);
530                assert_eq!(array.data.len(), 6);
531            }
532            other => panic!("expected string array, got {other:?}"),
533        }
534    }
535
536    #[test]
537    fn strings_from_size_vector_tensor() {
538        let dims = Tensor::new(vec![2.0, 3.0, 1.0], vec![1, 3]).unwrap();
539        let result = strings_builtin(vec![Value::Tensor(dims)]).expect("strings");
540        match result {
541            Value::StringArray(array) => {
542                assert_eq!(array.shape, vec![2, 3, 1]);
543                assert_eq!(array.data.len(), 6);
544            }
545            other => panic!("expected string array, got {other:?}"),
546        }
547    }
548
549    #[test]
550    fn strings_preserves_trailing_singletons() {
551        let args = vec![
552            Value::Num(3.0),
553            Value::Int(runmat_builtins::IntValue::I32(1)),
554            Value::Num(1.0),
555            Value::Bool(true),
556        ];
557        let result = strings_builtin(args).expect("strings");
558        match result {
559            Value::StringArray(array) => {
560                assert_eq!(array.shape, vec![3, 1, 1, 1]);
561                assert_eq!(array.data.len(), 3);
562            }
563            other => panic!("expected string array, got {other:?}"),
564        }
565    }
566
567    #[test]
568    fn strings_bool_dimensions() {
569        let result = strings_builtin(vec![Value::Bool(true), Value::Bool(false)]).expect("strings");
570        match result {
571            Value::StringArray(array) => {
572                assert_eq!(array.shape, vec![1, 0]);
573                assert!(array.data.is_empty());
574            }
575            other => panic!("expected string array, got {other:?}"),
576        }
577    }
578
579    #[test]
580    fn strings_logical_vector_argument() {
581        let logical =
582            LogicalArray::new(vec![1u8, 0, 1], vec![1, 3]).expect("logical size construction");
583        let result = strings_builtin(vec![Value::LogicalArray(logical)]).expect("strings");
584        match result {
585            Value::StringArray(array) => {
586                assert_eq!(array.shape, vec![1, 0, 1]);
587                assert!(array.data.is_empty());
588            }
589            other => panic!("expected string array, got {other:?}"),
590        }
591    }
592
593    #[test]
594    fn strings_negative_dimension_errors() {
595        let err = strings_builtin(vec![Value::Num(-5.0)]).expect_err("expected error");
596        assert!(err.contains(super::SIZE_NONNEGATIVE_ERR));
597    }
598
599    #[test]
600    fn strings_rejects_non_integer_dimension() {
601        let err = strings_builtin(vec![Value::Num(2.5)]).expect_err("expected error");
602        assert!(err.contains(super::SIZE_INTEGER_ERR));
603    }
604
605    #[test]
606    fn strings_rejects_non_numeric_dimension() {
607        let err = strings_builtin(vec![Value::String("size".into())]).expect_err("expected error");
608        assert!(err.contains("size arguments must be numeric"));
609    }
610
611    #[test]
612    fn strings_empty_vector_returns_empty_array() {
613        let dims = Tensor::new(Vec::<f64>::new(), vec![0, 0]).unwrap();
614        let result = strings_builtin(vec![Value::Tensor(dims)]).expect("strings");
615        match result {
616            Value::StringArray(array) => {
617                assert_eq!(array.shape, vec![0, 0]);
618                assert!(array.data.is_empty());
619            }
620            other => panic!("expected string array, got {other:?}"),
621        }
622    }
623
624    #[test]
625    fn strings_missing_option_fills_with_missing() {
626        let result = strings_builtin(vec![
627            Value::Num(2.0),
628            Value::Num(3.0),
629            Value::String("missing".into()),
630        ])
631        .expect("strings");
632        match result {
633            Value::StringArray(array) => {
634                assert_eq!(array.shape, vec![2, 3]);
635                assert_eq!(array.data.len(), 6);
636                assert!(array.data.iter().all(|s| s == "<missing>"));
637            }
638            other => panic!("expected string array, got {other:?}"),
639        }
640    }
641
642    #[test]
643    fn strings_missing_without_dims_defaults_to_scalar() {
644        let result = strings_builtin(vec![Value::String("missing".into())]).expect("strings");
645        match result {
646            Value::StringArray(array) => {
647                assert_eq!(array.shape, vec![1, 1]);
648                assert_eq!(array.data, vec!["<missing>".to_string()]);
649            }
650            other => panic!("expected string array, got {other:?}"),
651        }
652    }
653
654    #[test]
655    fn strings_like_prototype_shape() {
656        let proto = StringArray::new(
657            vec!["alpha".into(), "beta".into(), "gamma".into()],
658            vec![3, 1],
659        )
660        .unwrap();
661        let result = strings_builtin(vec![
662            Value::String("like".into()),
663            Value::StringArray(proto.clone()),
664        ])
665        .expect("strings");
666        match result {
667            Value::StringArray(array) => {
668                assert_eq!(array.shape, proto.shape);
669                assert!(array.data.iter().all(|s| s.is_empty()));
670            }
671            other => panic!("expected string array, got {other:?}"),
672        }
673    }
674
675    #[test]
676    fn strings_like_numeric_prototype() {
677        let tensor = Tensor::new(vec![1.0, 2.0, 3.0, 4.0], vec![2, 2]).unwrap();
678        let result = strings_builtin(vec![
679            Value::String("like".into()),
680            Value::Tensor(tensor.clone()),
681        ])
682        .expect("strings");
683        match result {
684            Value::StringArray(array) => {
685                assert_eq!(array.shape, tensor.shape);
686                assert_eq!(array.data.len(), tensor.data.len());
687            }
688            other => panic!("expected string array, got {other:?}"),
689        }
690    }
691
692    #[test]
693    fn strings_like_overrides_shape_when_dims_provided() {
694        let tensor = Tensor::new(vec![1.0, 2.0], vec![1, 2]).unwrap();
695        let result = strings_builtin(vec![
696            Value::String("like".into()),
697            Value::Tensor(tensor),
698            Value::Int(runmat_builtins::IntValue::I32(3)),
699        ])
700        .expect("strings");
701        match result {
702            Value::StringArray(array) => {
703                assert_eq!(array.shape, vec![3, 3]);
704            }
705            other => panic!("expected string array, got {other:?}"),
706        }
707    }
708
709    #[test]
710    fn strings_like_requires_prototype() {
711        let err = strings_builtin(vec![Value::String("like".into())]).expect_err("expected error");
712        assert!(err.contains("expected prototype"));
713    }
714
715    #[test]
716    fn strings_like_rejects_multiple_specs() {
717        let err = strings_builtin(vec![
718            Value::String("like".into()),
719            Value::Num(1.0),
720            Value::String("like".into()),
721            Value::Num(2.0),
722        ])
723        .expect_err("expected error");
724        assert!(err.contains("multiple 'like'"));
725    }
726
727    #[test]
728    fn strings_gpu_size_vector_argument() {
729        test_support::with_test_provider(|provider| {
730            let dims = Tensor::new(vec![2.0, 3.0], vec![1, 2]).unwrap();
731            let view = HostTensorView {
732                data: &dims.data,
733                shape: &dims.shape,
734            };
735            let handle = provider.upload(&view).expect("upload");
736            let result = strings_builtin(vec![Value::GpuTensor(handle)]).expect("strings");
737            match result {
738                Value::StringArray(array) => {
739                    assert_eq!(array.shape, vec![2, 3]);
740                    assert_eq!(array.data.len(), 6);
741                }
742                other => panic!("expected string array, got {other:?}"),
743            }
744        });
745    }
746
747    #[test]
748    fn strings_like_accepts_gpu_prototype() {
749        test_support::with_test_provider(|provider| {
750            let tensor = Tensor::new(vec![1.0, 2.0, 3.0, 4.0], vec![2, 2]).unwrap();
751            let view = HostTensorView {
752                data: &tensor.data,
753                shape: &tensor.shape,
754            };
755            let handle = provider.upload(&view).expect("upload");
756            let result =
757                strings_builtin(vec![Value::String("like".into()), Value::GpuTensor(handle)])
758                    .expect("strings");
759            match result {
760                Value::StringArray(array) => {
761                    assert_eq!(array.shape, vec![2, 2]);
762                }
763                other => panic!("expected string array, got {other:?}"),
764            }
765        });
766    }
767
768    #[cfg(feature = "wgpu")]
769    #[test]
770    fn strings_handles_wgpu_size_vectors() {
771        let _ = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
772            runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
773        );
774        let dims = Tensor::new(vec![1.0, 4.0], vec![1, 2]).unwrap();
775        let view = HostTensorView {
776            data: &dims.data,
777            shape: &dims.shape,
778        };
779        let provider = runmat_accelerate_api::provider().expect("wgpu provider");
780        let handle = provider.upload(&view).expect("upload");
781        let result = strings_builtin(vec![Value::GpuTensor(handle)]).expect("strings");
782        match result {
783            Value::StringArray(array) => {
784                assert_eq!(array.shape, vec![1, 4]);
785            }
786            other => panic!("expected string array, got {other:?}"),
787        }
788    }
789
790    #[test]
791    #[cfg(feature = "doc_export")]
792    fn doc_examples_present() {
793        let examples = test_support::doc_examples(DOC_MD);
794        assert!(!examples.is_empty());
795    }
796}