1use 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}