1use runmat_builtins::{StringArray, Value};
4use runmat_macros::runtime_builtin;
5
6use crate::builtins::common::random_args::{extract_dims, keyword_of};
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 LABEL: &str = "string.empty";
16
17#[cfg(feature = "doc_export")]
18pub const DOC_MD: &str = r#"---
19title: "string.empty"
20category: "strings/core"
21keywords: ["string.empty", "empty string array", "preallocate text", "size vector", "0-by-N", "'like'"]
22summary: "Construct empty string arrays with MATLAB-compatible dimension semantics and 'like' prototypes."
23references:
24 - https://www.mathworks.com/help/matlab/ref/string.empty.html
25gpu_support:
26 elementwise: false
27 reduction: false
28 precisions: []
29 broadcasting: "none"
30 notes: "Creates host string arrays; GPU tensors are neither read nor written."
31fusion:
32 elementwise: false
33 reduction: false
34 max_inputs: 0
35 constants: "inline"
36requires_feature: null
37tested:
38 unit: "builtins::strings::core::string_empty::tests"
39 integration: "builtins::strings::core::string_empty::tests::doc_examples_present"
40---
41
42# What does the `string.empty` function do in MATLAB / RunMat?
43`string.empty` constructs an empty string array. By default it returns a `0×0` array, and when you
44specify additional dimensions they define the trailing extents while the leading dimension remains
45zero, ensuring the total element count is zero. This mirrors MATLAB's static `string.empty` method.
46
47## How does the `string.empty` function behave in MATLAB / RunMat?
48- `string.empty` with no arguments yields a `0×0` string array.
49- `string.empty(n)` produces a `0×n` array. The leading dimension is fixed at `0`, so the result is
50 still empty even if `n > 0`.
51- `string.empty(m, n, p, ...)` returns a `0×n×p×…` array. All trailing dimensions are honoured while the leading dimension remains zero.
52- You can provide a single size vector such as `string.empty([0 5 3])`; the first entry is ignored
53 beyond confirming it is non-negative, and the remaining entries set the trailing dimensions.
54- `string.empty(___, 'like', prototype)` copies the trailing dimensions from `prototype` when you do
55 not supply explicit sizes. Any dimensions you pass explicitly take precedence. GPU-resident
56 prototypes are automatically gathered so their shape can be inspected.
57- Size inputs must be finite, real, non-negative integers. Fractional or negative values produce a
58 MATLAB-compatible error.
59- The result always resides on the host; there is no GPU counterpart for string arrays.
60
61## `string.empty` GPU Execution Behaviour
62`string.empty` does not allocate or interact with GPU memory. It is a pure host constructor that
63instantly returns the requested shape metadata and an empty data buffer. When the runtime is
64executing under RunMat Accelerate, no provider hooks are invoked. `'like'` prototypes that happen to
65live on the GPU are gathered to the host before their shape is examined.
66
67## Examples of using the `string.empty` function in MATLAB / RunMat
68
69### Creating a 0x0 string array
70```matlab
71S = string.empty;
72```
73Expected output:
74```matlab
75S =
76 0x0 string array
77```
78
79### Building a 0xN string row vector
80```matlab
81row = string.empty(5);
82```
83Expected output:
84```matlab
85row =
86 0x5 string array
87```
88
89### Creating a 0xN string array with extra dimensions
90```matlab
91cube = string.empty(0, 4, 3);
92```
93Expected output:
94```matlab
95cube =
96 0x4x3 string array
97```
98
99### Using a size vector with string.empty
100```matlab
101sz = [0 2 5];
102grid = string.empty(sz);
103```
104Expected output:
105```matlab
106grid =
107 0x2x5 string array
108```
109
110### Resetting a preallocated string array to empty
111```matlab
112A = strings(3, 2); % Some application-specific strings
113A = string.empty(size(A));
114```
115Expected output:
116```matlab
117A =
118 0x2 string array
119```
120
121### Preserving higher-dimensional layout while empty
122```matlab
123layout = string.empty([2 0 4 6]);
124```
125Expected output:
126```matlab
127layout =
128 0x0x4x6 string array
129```
130
131### Reusing the shape of an existing array with `'like'`
132```matlab
133proto = strings(3, 2);
134sameCols = string.empty('like', proto);
135```
136Expected output:
137```matlab
138sameCols =
139 0x2 string array
140```
141
142## GPU residency in RunMat (Do I need `gpuArray`?)
143No. `string.empty` allocates metadata for an empty string array entirely on the host. Because the
144result contains no elements and string scalars are host-only, there is nothing to transfer to or from
145the GPU. Using `gpuArray` with `string.empty` has no effect and is unnecessary.
146
147## FAQ
148
149### Why is the first dimension always zero?
150MATLAB defines `classname.empty` so that the leading dimension is zero, guaranteeing the result
151contains no elements. RunMat mirrors this rule for perfect compatibility.
152
153### Can I request negative or fractional dimensions?
154No. Dimensions must be finite, non-negative integers. Any other input raises a descriptive error.
155
156### Does `string.empty(n)` create space for `n` elements?
157No. It returns a `0×n` array, which still has zero elements. Use `strings(n)` if you want an array of
158string scalars that you can fill later.
159
160### Can I combine scalars and size vectors?
161Yes. Calls like `string.empty([0 3], 5)` flatten to `string.empty(0, 3, 5)` internally.
162
163### What does the `'like'` option do?
164`'like', prototype` copies the trailing dimensions from `prototype` when you omit explicit sizes.
165The first dimension is still forced to `0`, so the result remains empty. The prototype is gathered
166automatically if it resides on the GPU.
167
168### Does the result share storage with existing arrays?
169No. Every call returns a new handle. Because the array is empty, the data buffer is an empty vector
170and consumes negligible memory.
171
172### Is there a GPU-accelerated variant?
173No. String arrays live on the host in RunMat, and this builtin never touches GPU memory.
174
175### How do I obtain a 0x0 string array quickly?
176Call `string.empty` with no arguments. It is equivalent to `strings(0)` but makes the intention
177explicit.
178
179### Can I use `size` output directly?
180Yes. Expressions like `string.empty(size(existingArray))` are supported. The first element of the
181size vector is ignored when constructing the new array so that the first dimension is zero.
182
183### What happens if I pass an empty array as the size vector?
184`string.empty([])` returns the canonical `0×0` string array, just like calling `string.empty` with no
185arguments.
186
187### Does `string.empty` ever throw away extra arguments?
188Only when they cannot be interpreted as dimensions. In that case RunMat throws an error rather than
189guessing.
190
191## See Also
192`string`, `strings`, `char`, `zeros`, `ones`
193"#;
194
195pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
196 name: "string.empty",
197 op_kind: GpuOpKind::Custom("constructor"),
198 supported_precisions: &[],
199 broadcast: BroadcastSemantics::None,
200 provider_hooks: &[],
201 constant_strategy: ConstantStrategy::InlineLiteral,
202 residency: ResidencyPolicy::NewHandle,
203 nan_mode: ReductionNaN::Include,
204 two_pass_threshold: None,
205 workgroup_size: None,
206 accepts_nan_mode: false,
207 notes: "Host-only constructor that returns a new empty string array without contacting GPU providers.",
208};
209
210register_builtin_gpu_spec!(GPU_SPEC);
211
212pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
213 name: "string.empty",
214 shape: ShapeRequirements::Any,
215 constant_strategy: ConstantStrategy::InlineLiteral,
216 elementwise: None,
217 reduction: None,
218 emits_nan: false,
219 notes: "Pure constructor; fusion planner treats calls as non-fusable sinks.",
220};
221
222register_builtin_fusion_spec!(FUSION_SPEC);
223
224#[cfg(feature = "doc_export")]
225register_builtin_doc_text!("string.empty", DOC_MD);
226
227#[runtime_builtin(
228 name = "string.empty",
229 category = "strings/core",
230 summary = "Construct an empty string array with MATLAB-compatible dimensions.",
231 keywords = "string.empty,empty,string array,preallocate",
232 accel = "none"
233)]
234fn string_empty_builtin(rest: Vec<Value>) -> Result<Value, String> {
235 let shape = parse_shape(&rest)?;
236 let total: usize = shape.iter().product();
237 debug_assert_eq!(total, 0, "string.empty must produce an empty array");
238 let data = Vec::<String>::new();
239 let array = StringArray::new(data, shape).map_err(|e| format!("{LABEL}: {e}"))?;
240 Ok(Value::StringArray(array))
241}
242
243fn parse_shape(args: &[Value]) -> Result<Vec<usize>, String> {
244 if args.is_empty() {
245 return Ok(vec![0, 0]);
246 }
247
248 let mut explicit_dims: Vec<usize> = Vec::new();
249 let mut like_shape: Option<Vec<usize>> = None;
250 let mut idx = 0;
251
252 while idx < args.len() {
253 let arg_host = gather_if_needed(&args[idx]).map_err(|e| format!("{LABEL}: {e}"))?;
254
255 if let Some(keyword) = keyword_of(&arg_host) {
256 if keyword.as_str() == "like" {
257 if like_shape.is_some() {
258 return Err(format!(
259 "{LABEL}: multiple 'like' prototypes are not supported"
260 ));
261 }
262 let Some(proto_raw) = args.get(idx + 1) else {
263 return Err(format!("{LABEL}: expected prototype after 'like'"));
264 };
265 let proto = gather_if_needed(proto_raw).map_err(|e| format!("{LABEL}: {e}"))?;
266 like_shape = Some(prototype_dims(&proto));
267 idx += 2;
268 continue;
269 }
270 }
273
274 if let Some(parsed) = extract_dims(&arg_host, LABEL)? {
275 if explicit_dims.is_empty() {
276 explicit_dims = parsed;
277 } else {
278 explicit_dims.extend(parsed);
279 }
280 idx += 1;
281 continue;
282 }
283
284 return Err(format!(
285 "{LABEL}: size inputs must be numeric scalars or size vectors"
286 ));
287 }
288
289 let shape = if !explicit_dims.is_empty() {
290 shape_from_explicit_dims(&explicit_dims)
291 } else if let Some(proto_shape) = like_shape {
292 shape_from_like(&proto_shape)
293 } else {
294 vec![0, 0]
295 };
296 ensure_empty_shape(&shape)?;
297 Ok(shape)
298}
299
300fn shape_from_explicit_dims(dims: &[usize]) -> Vec<usize> {
301 match dims.len() {
302 0 => vec![0, 0],
303 1 => vec![0, dims[0]],
304 _ => {
305 let mut shape = Vec::with_capacity(dims.len());
306 shape.push(0);
307 shape.extend_from_slice(&dims[1..]);
308 shape
309 }
310 }
311}
312
313fn shape_from_like(proto: &[usize]) -> Vec<usize> {
314 if proto.is_empty() {
315 return vec![0, 0];
316 }
317 if proto.len() == 1 {
318 return vec![0, proto[0]];
319 }
320 let mut shape = Vec::with_capacity(proto.len());
321 shape.push(0);
322 shape.extend_from_slice(&proto[1..]);
323 shape
324}
325
326fn ensure_empty_shape(shape: &[usize]) -> Result<(), String> {
327 if shape.iter().product::<usize>() != 0 {
328 return Err(format!(
329 "{LABEL}: at least one dimension must be zero to construct an empty string array"
330 ));
331 }
332 Ok(())
333}
334
335fn prototype_dims(proto: &Value) -> Vec<usize> {
336 match proto {
337 Value::StringArray(sa) => sa.shape.clone(),
338 Value::CharArray(ca) => vec![ca.rows, ca.cols],
339 Value::Tensor(t) => t.shape.clone(),
340 Value::ComplexTensor(t) => t.shape.clone(),
341 Value::LogicalArray(l) => l.shape.clone(),
342 Value::Cell(cell) => cell.shape.clone(),
343 Value::GpuTensor(handle) => handle.shape.clone(),
344 Value::Num(_) | Value::Int(_) | Value::Bool(_) | Value::Complex(_, _) => vec![1, 1],
345 Value::String(_) => vec![1, 1],
346 _ => vec![1, 1],
347 }
348}
349
350#[cfg(test)]
351mod tests {
352 use super::*;
353 use crate::builtins::common::test_support;
354 use runmat_accelerate_api::HostTensorView;
355 use runmat_builtins::{StringArray, Tensor, Value};
356
357 #[test]
358 fn default_is_zero_by_zero() {
359 let result = string_empty_builtin(Vec::new()).expect("string.empty");
360 match result {
361 Value::StringArray(sa) => {
362 assert_eq!(sa.shape, vec![0, 0]);
363 assert_eq!(sa.data.len(), 0);
364 }
365 other => panic!("expected string array, got {other:?}"),
366 }
367 }
368
369 #[test]
370 fn single_dimension_creates_zero_by_n() {
371 let result = string_empty_builtin(vec![Value::from(5)]).expect("string.empty");
372 match result {
373 Value::StringArray(sa) => {
374 assert_eq!(sa.shape, vec![0, 5]);
375 assert_eq!(sa.data.len(), 0);
376 }
377 other => panic!("expected string array, got {other:?}"),
378 }
379 }
380
381 #[test]
382 fn multiple_dimensions_respect_trailing_sizes() {
383 let args = vec![Value::from(3), Value::from(4), Value::from(2)];
384 let result = string_empty_builtin(args).expect("string.empty");
385 match result {
386 Value::StringArray(sa) => {
387 assert_eq!(sa.shape, vec![0, 4, 2]);
388 assert_eq!(sa.data.len(), 0);
389 }
390 other => panic!("expected string array, got {other:?}"),
391 }
392 }
393
394 #[test]
395 fn size_vector_argument_supported() {
396 let tensor = Tensor::new(vec![0.0, 5.0, 3.0], vec![1, 3]).unwrap();
397 let result = string_empty_builtin(vec![Value::Tensor(tensor)]).expect("string.empty");
398 match result {
399 Value::StringArray(sa) => {
400 assert_eq!(sa.shape, vec![0, 5, 3]);
401 assert_eq!(sa.data.len(), 0);
402 }
403 other => panic!("expected string array, got {other:?}"),
404 }
405 }
406
407 #[test]
408 fn size_vector_from_nonempty_array_drops_leading_extent() {
409 let tensor = Tensor::new(vec![3.0, 2.0], vec![1, 2]).unwrap();
410 let result = string_empty_builtin(vec![Value::Tensor(tensor)]).expect("string.empty");
411 match result {
412 Value::StringArray(sa) => {
413 assert_eq!(sa.shape, vec![0, 2]);
414 assert_eq!(sa.data.len(), 0);
415 }
416 other => panic!("expected string array, got {other:?}"),
417 }
418 }
419
420 #[test]
421 fn accepts_zero_in_any_position() {
422 let args = vec![Value::from(3), Value::from(4), Value::from(0)];
423 let result = string_empty_builtin(args).expect("string.empty");
424 match result {
425 Value::StringArray(sa) => assert_eq!(sa.shape, vec![0, 4, 0]),
426 other => panic!("expected string array, got {other:?}"),
427 }
428 }
429
430 #[test]
431 fn like_prototype_without_explicit_dims() {
432 let proto = StringArray::new(vec!["alpha".to_string(); 6], vec![2, 3]).unwrap();
433 let result = string_empty_builtin(vec![Value::from("like"), Value::StringArray(proto)])
434 .expect("string.empty");
435 match result {
436 Value::StringArray(sa) => {
437 assert_eq!(sa.shape, vec![0, 3]);
438 assert_eq!(sa.data.len(), 0);
439 }
440 other => panic!("expected string array, got {other:?}"),
441 }
442 }
443
444 #[test]
445 fn like_prototype_with_scalar_shape() {
446 let proto = StringArray::new(vec!["foo".to_string()], vec![1, 1]).unwrap();
447 let result = string_empty_builtin(vec![Value::from("like"), Value::StringArray(proto)])
448 .expect("string.empty");
449 match result {
450 Value::StringArray(sa) => {
451 assert_eq!(sa.shape, vec![0, 1]);
452 assert_eq!(sa.data.len(), 0);
453 }
454 other => panic!("expected string array, got {other:?}"),
455 }
456 }
457
458 #[test]
459 fn like_with_numeric_prototype() {
460 let tensor = Tensor::new(vec![1.0, 2.0], vec![2, 1]).unwrap();
461 let result = string_empty_builtin(vec![Value::from("like"), Value::Tensor(tensor)])
462 .expect("string.empty");
463 match result {
464 Value::StringArray(sa) => {
465 assert_eq!(sa.shape, vec![0, 1]);
466 assert_eq!(sa.data.len(), 0);
467 }
468 other => panic!("expected string array, got {other:?}"),
469 }
470 }
471
472 #[test]
473 fn like_with_explicit_dims_prefers_dimensions() {
474 let proto = StringArray::new(Vec::new(), vec![0, 2]).unwrap();
475 let args = vec![
476 Value::from(0),
477 Value::from(7),
478 Value::from("like"),
479 Value::StringArray(proto),
480 ];
481 let result = string_empty_builtin(args).expect("string.empty");
482 match result {
483 Value::StringArray(sa) => {
484 assert_eq!(sa.shape, vec![0, 7]);
485 assert_eq!(sa.data.len(), 0);
486 }
487 other => panic!("expected string array, got {other:?}"),
488 }
489 }
490
491 #[test]
492 fn missing_like_prototype_errors() {
493 let err = string_empty_builtin(vec![Value::from("like")]).expect_err("expected error");
494 assert!(
495 err.contains("expected prototype"),
496 "unexpected error: {err}"
497 );
498 }
499
500 #[test]
501 fn duplicate_like_errors() {
502 let proto = StringArray::new(Vec::new(), vec![0, 2]).unwrap();
503 let err = string_empty_builtin(vec![
504 Value::from("like"),
505 Value::StringArray(proto.clone()),
506 Value::from("like"),
507 Value::StringArray(proto),
508 ])
509 .expect_err("expected error");
510 assert!(err.contains("multiple 'like'"), "unexpected error: {err}");
511 }
512
513 #[test]
514 fn rejects_non_dimension_inputs() {
515 let err =
516 string_empty_builtin(vec![Value::String("oops".into())]).expect_err("expected error");
517 assert!(
518 err.contains("size inputs must be numeric"),
519 "unexpected error: {err}"
520 );
521 }
522
523 #[test]
524 fn like_gathers_gpu_prototype() {
525 test_support::with_test_provider(|provider| {
526 let tensor =
527 Tensor::new((1..=6).map(|v| v as f64).collect::<Vec<_>>(), vec![2, 3]).unwrap();
528 let view = HostTensorView {
529 data: &tensor.data,
530 shape: &tensor.shape,
531 };
532 let handle = provider.upload(&view).expect("upload");
533 let result =
534 string_empty_builtin(vec![Value::from("like"), Value::GpuTensor(handle.clone())])
535 .expect("string.empty");
536 match result {
537 Value::StringArray(sa) => {
538 assert_eq!(sa.shape, vec![0, 3]);
539 assert_eq!(sa.data.len(), 0);
540 }
541 other => panic!("expected string array, got {other:?}"),
542 }
543 let _ = provider.free(&handle);
544 });
545 }
546
547 #[test]
548 fn gpu_dimension_arguments_are_gathered() {
549 test_support::with_test_provider(|provider| {
550 let dims = Tensor::new(vec![0.0, 5.0, 3.0], vec![1, 3]).unwrap();
551 let view = HostTensorView {
552 data: &dims.data,
553 shape: &dims.shape,
554 };
555 let handle = provider.upload(&view).expect("upload");
556 let result =
557 string_empty_builtin(vec![Value::GpuTensor(handle.clone())]).expect("string.empty");
558 match result {
559 Value::StringArray(sa) => {
560 assert_eq!(sa.shape, vec![0, 5, 3]);
561 assert_eq!(sa.data.len(), 0);
562 }
563 other => panic!("expected string array, got {other:?}"),
564 }
565 let _ = provider.free(&handle);
566 });
567 }
568
569 #[test]
570 fn rejects_negative_dimension() {
571 let err = string_empty_builtin(vec![Value::from(-1.0)]).expect_err("expected error");
572 assert!(
573 err.contains("matrix dimensions must be non-negative"),
574 "unexpected error: {err}"
575 );
576 }
577
578 #[test]
579 #[cfg(feature = "doc_export")]
580 fn doc_examples_present() {
581 let blocks = test_support::doc_examples(DOC_MD);
582 assert!(!blocks.is_empty());
583 }
584}