1use runmat_builtins::{
4 BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
5 BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
6 LogicalArray, StringArray, Tensor, Value,
7};
8use runmat_macros::runtime_builtin;
9
10use crate::builtins::common::map_control_flow_with_builtin;
11use crate::builtins::common::random_args::{keyword_of, shape_from_value};
12use crate::builtins::common::spec::{
13 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
14 ReductionNaN, ResidencyPolicy, ShapeRequirements,
15};
16use crate::builtins::strings::type_resolvers::string_array_type;
17use crate::{build_runtime_error, gather_if_needed_async, BuiltinResult, RuntimeError};
18
19const FN_NAME: &str = "strings";
20const SIZE_INTEGER_ERR: &str = "size inputs must be integers";
21const SIZE_NONNEGATIVE_ERR: &str = "size inputs must be nonnegative integers";
22const SIZE_FINITE_ERR: &str = "size inputs must be finite";
23const SIZE_SCALAR_ERR: &str = "size inputs must be scalar";
24
25const STRINGS_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
26 name: "S",
27 ty: BuiltinParamType::Any,
28 arity: BuiltinParamArity::Required,
29 default: None,
30 description: "Preallocated string array.",
31}];
32
33const STRINGS_INPUT_SZ: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
34 name: "sz",
35 ty: BuiltinParamType::SizeArg,
36 arity: BuiltinParamArity::Required,
37 default: None,
38 description: "Size vector or scalar side length.",
39}];
40
41const STRINGS_INPUT_DIMS: [BuiltinParamDescriptor; 2] = [
42 BuiltinParamDescriptor {
43 name: "m",
44 ty: BuiltinParamType::SizeArg,
45 arity: BuiltinParamArity::Required,
46 default: None,
47 description: "First array dimension.",
48 },
49 BuiltinParamDescriptor {
50 name: "n...",
51 ty: BuiltinParamType::SizeArg,
52 arity: BuiltinParamArity::Variadic,
53 default: None,
54 description: "Additional array dimensions.",
55 },
56];
57
58const STRINGS_INPUT_LIKE: [BuiltinParamDescriptor; 3] = [
59 BuiltinParamDescriptor {
60 name: "dims...",
61 ty: BuiltinParamType::SizeArg,
62 arity: BuiltinParamArity::Variadic,
63 default: None,
64 description: "Optional explicit size dimensions.",
65 },
66 BuiltinParamDescriptor {
67 name: "like",
68 ty: BuiltinParamType::StringScalar,
69 arity: BuiltinParamArity::Required,
70 default: Some("\"like\""),
71 description: "Literal option keyword \"like\".",
72 },
73 BuiltinParamDescriptor {
74 name: "p",
75 ty: BuiltinParamType::LikePrototype,
76 arity: BuiltinParamArity::Required,
77 default: None,
78 description: "Prototype value supplying output shape when dims are omitted.",
79 },
80];
81
82const STRINGS_INPUT_FILL: [BuiltinParamDescriptor; 2] = [
83 BuiltinParamDescriptor {
84 name: "dims...",
85 ty: BuiltinParamType::SizeArg,
86 arity: BuiltinParamArity::Variadic,
87 default: None,
88 description: "Optional explicit size dimensions.",
89 },
90 BuiltinParamDescriptor {
91 name: "fill",
92 ty: BuiltinParamType::StringScalar,
93 arity: BuiltinParamArity::Required,
94 default: Some("\"empty\""),
95 description: "Fill mode keyword: \"empty\" or \"missing\".",
96 },
97];
98
99const STRINGS_SIGNATURES: [BuiltinSignatureDescriptor; 5] = [
100 BuiltinSignatureDescriptor {
101 label: "S = strings()",
102 inputs: &[],
103 outputs: &STRINGS_OUTPUT,
104 },
105 BuiltinSignatureDescriptor {
106 label: "S = strings(sz)",
107 inputs: &STRINGS_INPUT_SZ,
108 outputs: &STRINGS_OUTPUT,
109 },
110 BuiltinSignatureDescriptor {
111 label: "S = strings(m, n...)",
112 inputs: &STRINGS_INPUT_DIMS,
113 outputs: &STRINGS_OUTPUT,
114 },
115 BuiltinSignatureDescriptor {
116 label: "S = strings(___, \"like\", p)",
117 inputs: &STRINGS_INPUT_LIKE,
118 outputs: &STRINGS_OUTPUT,
119 },
120 BuiltinSignatureDescriptor {
121 label: "S = strings(___, fill)",
122 inputs: &STRINGS_INPUT_FILL,
123 outputs: &STRINGS_OUTPUT,
124 },
125];
126
127const STRINGS_ERROR_INVALID_SIZE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
128 code: "RM.STRINGS.INVALID_SIZE",
129 identifier: Some("RunMat:strings:InvalidSize"),
130 when: "Size arguments are not valid numeric scalar/vector dimensions.",
131 message: "strings: size arguments must be numeric scalars or vectors",
132};
133
134const STRINGS_ERROR_LIKE_MISSING_PROTOTYPE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
135 code: "RM.STRINGS.LIKE_MISSING_PROTOTYPE",
136 identifier: Some("RunMat:strings:LikeMissingPrototype"),
137 when: "\"like\" is provided without a following prototype.",
138 message: "strings: expected prototype after 'like'",
139};
140
141const STRINGS_ERROR_LIKE_DUPLICATE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
142 code: "RM.STRINGS.LIKE_DUPLICATE",
143 identifier: Some("RunMat:strings:LikeDuplicate"),
144 when: "Multiple \"like\" options are supplied in one call.",
145 message: "strings: multiple 'like' specifications are not supported",
146};
147
148const STRINGS_ERROR_SIZE_OVERFLOW: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
149 code: "RM.STRINGS.SIZE_OVERFLOW",
150 identifier: Some("RunMat:strings:SizeOverflow"),
151 when: "Requested dimensions overflow platform limits.",
152 message: "strings: requested size exceeds platform limits",
153};
154
155const STRINGS_ERROR_INTERNAL: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
156 code: "RM.STRINGS.INTERNAL",
157 identifier: Some("RunMat:strings:InternalError"),
158 when: "Internal string array construction failed.",
159 message: "strings: internal error",
160};
161
162const STRINGS_ERRORS: [BuiltinErrorDescriptor; 5] = [
163 STRINGS_ERROR_INVALID_SIZE,
164 STRINGS_ERROR_LIKE_MISSING_PROTOTYPE,
165 STRINGS_ERROR_LIKE_DUPLICATE,
166 STRINGS_ERROR_SIZE_OVERFLOW,
167 STRINGS_ERROR_INTERNAL,
168];
169
170pub const STRINGS_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
171 signatures: &STRINGS_SIGNATURES,
172 output_mode: BuiltinOutputMode::Fixed,
173 completion_policy: BuiltinCompletionPolicy::Public,
174 errors: &STRINGS_ERRORS,
175};
176
177fn strings_error(error: &'static BuiltinErrorDescriptor) -> RuntimeError {
178 strings_error_with_message(error.message, error)
179}
180
181fn strings_error_with_message(
182 message: impl Into<String>,
183 error: &'static BuiltinErrorDescriptor,
184) -> RuntimeError {
185 let mut builder = build_runtime_error(message).with_builtin(FN_NAME);
186 if let Some(identifier) = error.identifier {
187 builder = builder.with_identifier(identifier);
188 }
189 builder.build()
190}
191
192fn remap_strings_flow(err: RuntimeError) -> RuntimeError {
193 map_control_flow_with_builtin(err, FN_NAME)
194}
195
196#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::strings::core::strings")]
197pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
198 name: FN_NAME,
199 op_kind: GpuOpKind::Custom("array_creation"),
200 supported_precisions: &[],
201 broadcast: BroadcastSemantics::None,
202 provider_hooks: &[],
203 constant_strategy: ConstantStrategy::InlineLiteral,
204 residency: ResidencyPolicy::GatherImmediately,
205 nan_mode: ReductionNaN::Include,
206 two_pass_threshold: None,
207 workgroup_size: None,
208 accepts_nan_mode: false,
209 notes: "Runs entirely on the host; size arguments pulled from the GPU are gathered before allocation.",
210};
211
212#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::strings::core::strings")]
213pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
214 name: FN_NAME,
215 shape: ShapeRequirements::Any,
216 constant_strategy: ConstantStrategy::InlineLiteral,
217 elementwise: None,
218 reduction: None,
219 emits_nan: false,
220 notes: "Preallocates host string arrays; no fusion-supported kernels are generated.",
221};
222
223struct ParsedStrings {
224 shape: Vec<usize>,
225 fill: FillKind,
226}
227
228#[derive(Clone, Copy, PartialEq, Eq)]
229enum FillKind {
230 Empty,
231 Missing,
232}
233
234#[runtime_builtin(
235 name = "strings",
236 category = "strings/core",
237 summary = "Preallocate string arrays filled with empty string scalars (`\"\"`).",
238 keywords = "strings,string array,empty,preallocate",
239 accel = "array_construct",
240 type_resolver(string_array_type),
241 descriptor(crate::builtins::strings::core::strings::STRINGS_DESCRIPTOR),
242 builtin_path = "crate::builtins::strings::core::strings"
243)]
244async fn strings_builtin(rest: Vec<Value>) -> crate::BuiltinResult<Value> {
245 let ParsedStrings { shape, fill } = parse_arguments(rest).await?;
246 let total = shape.iter().try_fold(1usize, |acc, &dim| {
247 acc.checked_mul(dim)
248 .ok_or_else(|| strings_error(&STRINGS_ERROR_SIZE_OVERFLOW))
249 })?;
250
251 let fill_text = match fill {
252 FillKind::Empty => String::new(),
253 FillKind::Missing => "<missing>".to_string(),
254 };
255
256 let mut data = Vec::with_capacity(total);
257 for _ in 0..total {
258 data.push(fill_text.clone());
259 }
260
261 let array =
262 StringArray::new(data, shape).map_err(|_| strings_error(&STRINGS_ERROR_INTERNAL))?;
263 Ok(Value::StringArray(array))
264}
265
266async fn parse_arguments(args: Vec<Value>) -> BuiltinResult<ParsedStrings> {
267 let mut size_values: Vec<Value> = Vec::new();
268 let mut like_proto: Option<Value> = None;
269 let mut fill = FillKind::Empty;
270
271 let mut idx = 0;
272 while idx < args.len() {
273 let host = gather_if_needed_async(&args[idx])
274 .await
275 .map_err(remap_strings_flow)?;
276 if let Some(keyword) = keyword_of(&host) {
277 match keyword.as_str() {
278 "like" => {
279 if like_proto.is_some() {
280 return Err(strings_error(&STRINGS_ERROR_LIKE_DUPLICATE));
281 }
282 let Some(proto_raw) = args.get(idx + 1) else {
283 return Err(strings_error(&STRINGS_ERROR_LIKE_MISSING_PROTOTYPE));
284 };
285 let proto = gather_if_needed_async(proto_raw)
286 .await
287 .map_err(remap_strings_flow)?;
288 like_proto = Some(proto);
289 idx += 2;
290 continue;
291 }
292 "missing" => {
293 fill = FillKind::Missing;
294 idx += 1;
295 continue;
296 }
297 "empty" => {
298 fill = FillKind::Empty;
299 idx += 1;
300 continue;
301 }
302 _ => {}
303 }
304 }
305 size_values.push(host);
306 idx += 1;
307 }
308
309 let dims = parse_size_values(size_values)?;
310 let mut shape = if let Some(dims) = dims {
311 normalize_dims(dims)
312 } else if let Some(proto) = like_proto.as_ref() {
313 prototype_shape(proto)?
314 } else {
315 vec![1, 1]
316 };
317
318 if shape.is_empty() {
319 shape = vec![0, 0];
320 }
321
322 Ok(ParsedStrings { shape, fill })
323}
324
325fn prototype_shape(value: &Value) -> BuiltinResult<Vec<usize>> {
326 match value {
327 Value::StringArray(sa) => Ok(sa.shape.clone()),
328 _ => {
329 shape_from_value(value, FN_NAME).map_err(|_| strings_error(&STRINGS_ERROR_INVALID_SIZE))
330 }
331 }
332}
333
334fn err_integer() -> RuntimeError {
335 strings_error_with_message(
336 format!("{FN_NAME}: {SIZE_INTEGER_ERR}"),
337 &STRINGS_ERROR_INVALID_SIZE,
338 )
339}
340
341fn err_nonnegative() -> RuntimeError {
342 strings_error_with_message(
343 format!("{FN_NAME}: {SIZE_NONNEGATIVE_ERR}"),
344 &STRINGS_ERROR_INVALID_SIZE,
345 )
346}
347
348fn err_finite() -> RuntimeError {
349 strings_error_with_message(
350 format!("{FN_NAME}: {SIZE_FINITE_ERR}"),
351 &STRINGS_ERROR_INVALID_SIZE,
352 )
353}
354
355fn parse_size_values(values: Vec<Value>) -> BuiltinResult<Option<Vec<usize>>> {
356 match values.len() {
357 0 => Ok(None),
358 1 => parse_single_argument(values.into_iter().next().unwrap()).map(Some),
359 _ => {
360 let mut dims = Vec::with_capacity(values.len());
361 for value in &values {
362 dims.push(parse_size_scalar(value)?);
363 }
364 Ok(Some(dims))
365 }
366 }
367}
368
369fn parse_single_argument(value: Value) -> BuiltinResult<Vec<usize>> {
370 match value {
371 Value::Int(iv) => Ok(vec![validate_i64_dimension(iv.to_i64())?]),
372 Value::Num(n) => Ok(vec![parse_numeric_dimension(n)?]),
373 Value::Bool(b) => Ok(vec![if b { 1 } else { 0 }]),
374 Value::Tensor(t) => parse_size_tensor(&t),
375 Value::LogicalArray(arr) => parse_size_logical_array(&arr),
376 _ => Err(strings_error(&STRINGS_ERROR_INVALID_SIZE)),
377 }
378}
379
380fn parse_size_scalar(value: &Value) -> BuiltinResult<usize> {
381 match value {
382 Value::Int(iv) => {
383 let raw = iv.to_i64();
384 validate_i64_dimension(raw)
385 }
386 Value::Num(n) => parse_numeric_dimension(*n),
387 Value::Bool(b) => Ok(if *b { 1 } else { 0 }),
388 Value::Tensor(t) => {
389 if t.data.len() != 1 {
390 return Err(strings_error_with_message(
391 format!("{FN_NAME}: {SIZE_SCALAR_ERR}"),
392 &STRINGS_ERROR_INVALID_SIZE,
393 ));
394 }
395 parse_numeric_dimension(t.data[0])
396 }
397 Value::LogicalArray(arr) => {
398 if arr.data.len() != 1 {
399 return Err(strings_error_with_message(
400 format!("{FN_NAME}: {SIZE_SCALAR_ERR}"),
401 &STRINGS_ERROR_INVALID_SIZE,
402 ));
403 }
404 Ok(if arr.data[0] != 0 { 1 } else { 0 })
405 }
406 _ => Err(strings_error(&STRINGS_ERROR_INVALID_SIZE)),
407 }
408}
409
410fn parse_size_tensor(tensor: &Tensor) -> BuiltinResult<Vec<usize>> {
411 if tensor.data.is_empty() {
412 return Ok(vec![0, 0]);
413 }
414 if !is_vector_shape(&tensor.shape) {
415 return Err(strings_error_with_message(
416 format!("{FN_NAME}: size vector must be a row or column vector"),
417 &STRINGS_ERROR_INVALID_SIZE,
418 ));
419 }
420 tensor
421 .data
422 .iter()
423 .map(|&value| parse_numeric_dimension(value))
424 .collect()
425}
426
427fn parse_size_logical_array(array: &LogicalArray) -> BuiltinResult<Vec<usize>> {
428 if array.data.is_empty() {
429 return Ok(vec![0, 0]);
430 }
431 if !is_vector_shape(&array.shape) {
432 return Err(strings_error_with_message(
433 format!("{FN_NAME}: size vector must be a row or column vector"),
434 &STRINGS_ERROR_INVALID_SIZE,
435 ));
436 }
437 array
438 .data
439 .iter()
440 .map(|&value| Ok(if value != 0 { 1 } else { 0 }))
441 .collect()
442}
443
444fn parse_numeric_dimension(value: f64) -> BuiltinResult<usize> {
445 if !value.is_finite() {
446 return Err(err_finite());
447 }
448 let rounded = value.round();
449 if (rounded - value).abs() > f64::EPSILON {
450 return Err(err_integer());
451 }
452 if rounded < 0.0 {
453 return Err(err_nonnegative());
454 }
455 if rounded > usize::MAX as f64 {
456 return Err(strings_error_with_message(
457 format!("{FN_NAME}: requested dimension exceeds platform limits"),
458 &STRINGS_ERROR_SIZE_OVERFLOW,
459 ));
460 }
461 Ok(rounded as usize)
462}
463
464fn normalize_dims(dims: Vec<usize>) -> Vec<usize> {
465 match dims.len() {
466 0 => vec![0, 0],
467 1 => {
468 let side = dims[0];
469 vec![side, side]
470 }
471 _ => dims,
472 }
473}
474
475fn is_vector_shape(shape: &[usize]) -> bool {
476 match shape.len() {
477 0 | 1 => true,
478 2 => shape[0] == 1 || shape[1] == 1,
479 _ => shape.iter().filter(|&&d| d > 1).count() <= 1,
480 }
481}
482
483fn validate_i64_dimension(raw: i64) -> BuiltinResult<usize> {
484 if raw < 0 {
485 return Err(err_nonnegative());
486 }
487 if (raw as u128) > (usize::MAX as u128) {
488 return Err(strings_error_with_message(
489 format!("{FN_NAME}: requested dimension exceeds platform limits"),
490 &STRINGS_ERROR_SIZE_OVERFLOW,
491 ));
492 }
493 Ok(raw as usize)
494}
495
496#[cfg(test)]
497pub(crate) mod tests {
498 use super::*;
499
500 use crate::builtins::common::test_support;
501 use runmat_accelerate_api::HostTensorView;
502 use runmat_builtins::{ResolveContext, Type};
503
504 fn strings_builtin(rest: Vec<Value>) -> BuiltinResult<Value> {
505 futures::executor::block_on(super::strings_builtin(rest))
506 }
507
508 fn error_message(err: crate::RuntimeError) -> String {
509 err.message().to_string()
510 }
511
512 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
513 #[test]
514 fn strings_default_scalar() {
515 let result = strings_builtin(Vec::new()).expect("strings");
516 match result {
517 Value::StringArray(array) => {
518 assert_eq!(array.shape, vec![1, 1]);
519 assert_eq!(array.data, vec![String::new()]);
520 }
521 other => panic!("expected string array, got {other:?}"),
522 }
523 }
524
525 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
526 #[test]
527 fn strings_square_from_single_dimension() {
528 let args = vec![Value::Num(4.0)];
529 let result = strings_builtin(args).expect("strings");
530 match result {
531 Value::StringArray(array) => {
532 assert_eq!(array.shape, vec![4, 4]);
533 assert!(array.data.iter().all(|s| s.is_empty()));
534 }
535 other => panic!("expected string array, got {other:?}"),
536 }
537 }
538
539 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
540 #[test]
541 fn strings_rectangular_multiple_args() {
542 let args = vec![
543 Value::Int(runmat_builtins::IntValue::I32(2)),
544 Value::Num(3.0),
545 ];
546 let result = strings_builtin(args).expect("strings");
547 match result {
548 Value::StringArray(array) => {
549 assert_eq!(array.shape, vec![2, 3]);
550 assert_eq!(array.data.len(), 6);
551 }
552 other => panic!("expected string array, got {other:?}"),
553 }
554 }
555
556 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
557 #[test]
558 fn strings_from_size_vector_tensor() {
559 let dims = Tensor::new(vec![2.0, 3.0, 1.0], vec![1, 3]).unwrap();
560 let result = strings_builtin(vec![Value::Tensor(dims)]).expect("strings");
561 match result {
562 Value::StringArray(array) => {
563 assert_eq!(array.shape, vec![2, 3, 1]);
564 assert_eq!(array.data.len(), 6);
565 }
566 other => panic!("expected string array, got {other:?}"),
567 }
568 }
569
570 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
571 #[test]
572 fn strings_preserves_trailing_singletons() {
573 let args = vec![
574 Value::Num(3.0),
575 Value::Int(runmat_builtins::IntValue::I32(1)),
576 Value::Num(1.0),
577 Value::Bool(true),
578 ];
579 let result = strings_builtin(args).expect("strings");
580 match result {
581 Value::StringArray(array) => {
582 assert_eq!(array.shape, vec![3, 1, 1, 1]);
583 assert_eq!(array.data.len(), 3);
584 }
585 other => panic!("expected string array, got {other:?}"),
586 }
587 }
588
589 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
590 #[test]
591 fn strings_bool_dimensions() {
592 let result = strings_builtin(vec![Value::Bool(true), Value::Bool(false)]).expect("strings");
593 match result {
594 Value::StringArray(array) => {
595 assert_eq!(array.shape, vec![1, 0]);
596 assert!(array.data.is_empty());
597 }
598 other => panic!("expected string array, got {other:?}"),
599 }
600 }
601
602 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
603 #[test]
604 fn strings_logical_vector_argument() {
605 let logical =
606 LogicalArray::new(vec![1u8, 0, 1], vec![1, 3]).expect("logical size construction");
607 let result = strings_builtin(vec![Value::LogicalArray(logical)]).expect("strings");
608 match result {
609 Value::StringArray(array) => {
610 assert_eq!(array.shape, vec![1, 0, 1]);
611 assert!(array.data.is_empty());
612 }
613 other => panic!("expected string array, got {other:?}"),
614 }
615 }
616
617 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
618 #[test]
619 fn strings_negative_dimension_errors() {
620 let err =
621 error_message(strings_builtin(vec![Value::Num(-5.0)]).expect_err("expected error"));
622 assert!(err.contains(super::SIZE_NONNEGATIVE_ERR));
623 }
624
625 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
626 #[test]
627 fn strings_rejects_non_integer_dimension() {
628 let err =
629 error_message(strings_builtin(vec![Value::Num(2.5)]).expect_err("expected error"));
630 assert!(err.contains(super::SIZE_INTEGER_ERR));
631 }
632
633 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
634 #[test]
635 fn strings_rejects_non_numeric_dimension() {
636 let err = error_message(
637 strings_builtin(vec![Value::String("size".into())]).expect_err("expected error"),
638 );
639 assert!(err.contains("size arguments must be numeric"));
640 }
641
642 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
643 #[test]
644 fn strings_empty_vector_returns_empty_array() {
645 let dims = Tensor::new(Vec::<f64>::new(), vec![0, 0]).unwrap();
646 let result = strings_builtin(vec![Value::Tensor(dims)]).expect("strings");
647 match result {
648 Value::StringArray(array) => {
649 assert_eq!(array.shape, vec![0, 0]);
650 assert!(array.data.is_empty());
651 }
652 other => panic!("expected string array, got {other:?}"),
653 }
654 }
655
656 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
657 #[test]
658 fn strings_missing_option_fills_with_missing() {
659 let result = strings_builtin(vec![
660 Value::Num(2.0),
661 Value::Num(3.0),
662 Value::String("missing".into()),
663 ])
664 .expect("strings");
665 match result {
666 Value::StringArray(array) => {
667 assert_eq!(array.shape, vec![2, 3]);
668 assert_eq!(array.data.len(), 6);
669 assert!(array.data.iter().all(|s| s == "<missing>"));
670 }
671 other => panic!("expected string array, got {other:?}"),
672 }
673 }
674
675 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
676 #[test]
677 fn strings_missing_without_dims_defaults_to_scalar() {
678 let result = strings_builtin(vec![Value::String("missing".into())]).expect("strings");
679 match result {
680 Value::StringArray(array) => {
681 assert_eq!(array.shape, vec![1, 1]);
682 assert_eq!(array.data, vec!["<missing>".to_string()]);
683 }
684 other => panic!("expected string array, got {other:?}"),
685 }
686 }
687
688 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
689 #[test]
690 fn strings_like_prototype_shape() {
691 let proto = StringArray::new(
692 vec!["alpha".into(), "beta".into(), "gamma".into()],
693 vec![3, 1],
694 )
695 .unwrap();
696 let result = strings_builtin(vec![
697 Value::String("like".into()),
698 Value::StringArray(proto.clone()),
699 ])
700 .expect("strings");
701 match result {
702 Value::StringArray(array) => {
703 assert_eq!(array.shape, proto.shape);
704 assert!(array.data.iter().all(|s| s.is_empty()));
705 }
706 other => panic!("expected string array, got {other:?}"),
707 }
708 }
709
710 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
711 #[test]
712 fn strings_like_numeric_prototype() {
713 let tensor = Tensor::new(vec![1.0, 2.0, 3.0, 4.0], vec![2, 2]).unwrap();
714 let result = strings_builtin(vec![
715 Value::String("like".into()),
716 Value::Tensor(tensor.clone()),
717 ])
718 .expect("strings");
719 match result {
720 Value::StringArray(array) => {
721 assert_eq!(array.shape, tensor.shape);
722 assert_eq!(array.data.len(), tensor.data.len());
723 }
724 other => panic!("expected string array, got {other:?}"),
725 }
726 }
727
728 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
729 #[test]
730 fn strings_like_overrides_shape_when_dims_provided() {
731 let tensor = Tensor::new(vec![1.0, 2.0], vec![1, 2]).unwrap();
732 let result = strings_builtin(vec![
733 Value::String("like".into()),
734 Value::Tensor(tensor),
735 Value::Int(runmat_builtins::IntValue::I32(3)),
736 ])
737 .expect("strings");
738 match result {
739 Value::StringArray(array) => {
740 assert_eq!(array.shape, vec![3, 3]);
741 }
742 other => panic!("expected string array, got {other:?}"),
743 }
744 }
745
746 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
747 #[test]
748 fn strings_like_requires_prototype() {
749 let err = error_message(
750 strings_builtin(vec![Value::String("like".into())]).expect_err("expected error"),
751 );
752 assert!(err.contains("expected prototype"));
753 }
754
755 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
756 #[test]
757 fn strings_like_rejects_multiple_specs() {
758 let err = error_message(
759 strings_builtin(vec![
760 Value::String("like".into()),
761 Value::Num(1.0),
762 Value::String("like".into()),
763 Value::Num(2.0),
764 ])
765 .expect_err("expected error"),
766 );
767 assert!(err.contains("multiple 'like'"));
768 }
769
770 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
771 #[test]
772 fn strings_gpu_size_vector_argument() {
773 test_support::with_test_provider(|provider| {
774 let dims = Tensor::new(vec![2.0, 3.0], vec![1, 2]).unwrap();
775 let view = HostTensorView {
776 data: &dims.data,
777 shape: &dims.shape,
778 };
779 let handle = provider.upload(&view).expect("upload");
780 let result = strings_builtin(vec![Value::GpuTensor(handle)]).expect("strings");
781 match result {
782 Value::StringArray(array) => {
783 assert_eq!(array.shape, vec![2, 3]);
784 assert_eq!(array.data.len(), 6);
785 }
786 other => panic!("expected string array, got {other:?}"),
787 }
788 });
789 }
790
791 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
792 #[test]
793 fn strings_like_accepts_gpu_prototype() {
794 test_support::with_test_provider(|provider| {
795 let tensor = Tensor::new(vec![1.0, 2.0, 3.0, 4.0], vec![2, 2]).unwrap();
796 let view = HostTensorView {
797 data: &tensor.data,
798 shape: &tensor.shape,
799 };
800 let handle = provider.upload(&view).expect("upload");
801 let result =
802 strings_builtin(vec![Value::String("like".into()), Value::GpuTensor(handle)])
803 .expect("strings");
804 match result {
805 Value::StringArray(array) => {
806 assert_eq!(array.shape, vec![2, 2]);
807 }
808 other => panic!("expected string array, got {other:?}"),
809 }
810 });
811 }
812
813 #[test]
814 fn strings_type_is_string_array() {
815 assert_eq!(
816 string_array_type(&[Type::Num], &ResolveContext::new(Vec::new())),
817 Type::cell_of(Type::String)
818 );
819 }
820
821 #[cfg(feature = "wgpu")]
822 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
823 #[test]
824 fn strings_handles_wgpu_size_vectors() {
825 let _ = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
826 runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
827 );
828 let dims = Tensor::new(vec![1.0, 4.0], vec![1, 2]).unwrap();
829 let view = HostTensorView {
830 data: &dims.data,
831 shape: &dims.shape,
832 };
833 let provider = runmat_accelerate_api::provider().expect("wgpu provider");
834 let handle = provider.upload(&view).expect("upload");
835 let result = strings_builtin(vec![Value::GpuTensor(handle)]).expect("strings");
836 match result {
837 Value::StringArray(array) => {
838 assert_eq!(array.shape, vec![1, 4]);
839 }
840 other => panic!("expected string array, got {other:?}"),
841 }
842 }
843}