1use runmat_builtins::{
4 BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
5 BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
6 StringArray, Value,
7};
8use runmat_macros::runtime_builtin;
9
10use crate::builtins::common::map_control_flow_with_builtin;
11use crate::builtins::common::random_args::{extract_dims, keyword_of};
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 LABEL: &str = "string.empty";
20
21const STRING_EMPTY_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
22 name: "S",
23 ty: BuiltinParamType::Any,
24 arity: BuiltinParamArity::Required,
25 default: None,
26 description: "Empty string array with at least one zero dimension.",
27}];
28
29const STRING_EMPTY_INPUT_SZ: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
30 name: "sz",
31 ty: BuiltinParamType::SizeArg,
32 arity: BuiltinParamArity::Required,
33 default: None,
34 description: "Size vector or scalar.",
35}];
36
37const STRING_EMPTY_INPUT_DIMS: [BuiltinParamDescriptor; 2] = [
38 BuiltinParamDescriptor {
39 name: "m",
40 ty: BuiltinParamType::SizeArg,
41 arity: BuiltinParamArity::Required,
42 default: None,
43 description: "First dimension.",
44 },
45 BuiltinParamDescriptor {
46 name: "n...",
47 ty: BuiltinParamType::SizeArg,
48 arity: BuiltinParamArity::Variadic,
49 default: None,
50 description: "Additional dimensions.",
51 },
52];
53
54const STRING_EMPTY_INPUT_LIKE: [BuiltinParamDescriptor; 3] = [
55 BuiltinParamDescriptor {
56 name: "dims...",
57 ty: BuiltinParamType::SizeArg,
58 arity: BuiltinParamArity::Variadic,
59 default: None,
60 description: "Optional explicit dimensions.",
61 },
62 BuiltinParamDescriptor {
63 name: "like",
64 ty: BuiltinParamType::StringScalar,
65 arity: BuiltinParamArity::Required,
66 default: Some("\"like\""),
67 description: "Literal option keyword \"like\".",
68 },
69 BuiltinParamDescriptor {
70 name: "p",
71 ty: BuiltinParamType::LikePrototype,
72 arity: BuiltinParamArity::Required,
73 default: None,
74 description: "Prototype supplying trailing dimensions.",
75 },
76];
77
78const STRING_EMPTY_SIGNATURES: [BuiltinSignatureDescriptor; 4] = [
79 BuiltinSignatureDescriptor {
80 label: "S = string.empty()",
81 inputs: &[],
82 outputs: &STRING_EMPTY_OUTPUT,
83 },
84 BuiltinSignatureDescriptor {
85 label: "S = string.empty(sz)",
86 inputs: &STRING_EMPTY_INPUT_SZ,
87 outputs: &STRING_EMPTY_OUTPUT,
88 },
89 BuiltinSignatureDescriptor {
90 label: "S = string.empty(m, n...)",
91 inputs: &STRING_EMPTY_INPUT_DIMS,
92 outputs: &STRING_EMPTY_OUTPUT,
93 },
94 BuiltinSignatureDescriptor {
95 label: "S = string.empty(___, \"like\", p)",
96 inputs: &STRING_EMPTY_INPUT_LIKE,
97 outputs: &STRING_EMPTY_OUTPUT,
98 },
99];
100
101const STRING_EMPTY_ERROR_INVALID_SIZE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
102 code: "RM.STRING_EMPTY.INVALID_SIZE",
103 identifier: Some("RunMat:string.empty:InvalidSize"),
104 when: "Size inputs are not valid numeric dimensions or vectors.",
105 message: "string.empty: size inputs must be numeric scalars or size vectors",
106};
107
108const STRING_EMPTY_ERROR_LIKE_MISSING: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
109 code: "RM.STRING_EMPTY.LIKE_MISSING_PROTOTYPE",
110 identifier: Some("RunMat:string.empty:LikeMissingPrototype"),
111 when: "\"like\" keyword is present without a prototype.",
112 message: "string.empty: expected prototype after 'like'",
113};
114
115const STRING_EMPTY_ERROR_LIKE_DUPLICATE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
116 code: "RM.STRING_EMPTY.LIKE_DUPLICATE",
117 identifier: Some("RunMat:string.empty:LikeDuplicate"),
118 when: "Multiple \"like\" specifications are supplied.",
119 message: "string.empty: multiple 'like' prototypes are not supported",
120};
121
122const STRING_EMPTY_ERROR_NOT_EMPTY_SHAPE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
123 code: "RM.STRING_EMPTY.NONEMPTY_SHAPE",
124 identifier: Some("RunMat:string.empty:NonEmptyShape"),
125 when: "Parsed dimensions do not produce an empty array shape.",
126 message: "string.empty: at least one dimension must be zero",
127};
128
129const STRING_EMPTY_ERROR_INTERNAL: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
130 code: "RM.STRING_EMPTY.INTERNAL",
131 identifier: Some("RunMat:string.empty:InternalError"),
132 when: "Internal empty string-array construction failed.",
133 message: "string.empty: internal error",
134};
135
136const STRING_EMPTY_ERRORS: [BuiltinErrorDescriptor; 5] = [
137 STRING_EMPTY_ERROR_INVALID_SIZE,
138 STRING_EMPTY_ERROR_LIKE_MISSING,
139 STRING_EMPTY_ERROR_LIKE_DUPLICATE,
140 STRING_EMPTY_ERROR_NOT_EMPTY_SHAPE,
141 STRING_EMPTY_ERROR_INTERNAL,
142];
143
144pub const STRING_EMPTY_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
145 signatures: &STRING_EMPTY_SIGNATURES,
146 output_mode: BuiltinOutputMode::Fixed,
147 completion_policy: BuiltinCompletionPolicy::MethodOnly,
148 errors: &STRING_EMPTY_ERRORS,
149};
150
151fn string_empty_error(error: &'static BuiltinErrorDescriptor) -> RuntimeError {
152 string_empty_error_with_message(error.message, error)
153}
154
155fn string_empty_error_with_message(
156 message: impl Into<String>,
157 error: &'static BuiltinErrorDescriptor,
158) -> RuntimeError {
159 let mut builder = build_runtime_error(message).with_builtin(LABEL);
160 if let Some(identifier) = error.identifier {
161 builder = builder.with_identifier(identifier);
162 }
163 builder.build()
164}
165
166fn remap_string_empty_flow(err: RuntimeError) -> RuntimeError {
167 map_control_flow_with_builtin(err, LABEL)
168}
169
170#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::strings::core::string_empty")]
171pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
172 name: "string.empty",
173 op_kind: GpuOpKind::Custom("constructor"),
174 supported_precisions: &[],
175 broadcast: BroadcastSemantics::None,
176 provider_hooks: &[],
177 constant_strategy: ConstantStrategy::InlineLiteral,
178 residency: ResidencyPolicy::NewHandle,
179 nan_mode: ReductionNaN::Include,
180 two_pass_threshold: None,
181 workgroup_size: None,
182 accepts_nan_mode: false,
183 notes: "Host-only constructor that returns a new empty string array without contacting GPU providers.",
184};
185
186#[runmat_macros::register_fusion_spec(
187 builtin_path = "crate::builtins::strings::core::string_empty"
188)]
189pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
190 name: "string.empty",
191 shape: ShapeRequirements::Any,
192 constant_strategy: ConstantStrategy::InlineLiteral,
193 elementwise: None,
194 reduction: None,
195 emits_nan: false,
196 notes: "Pure constructor; fusion planner treats calls as non-fusable sinks.",
197};
198
199#[runtime_builtin(
200 name = "string.empty",
201 category = "strings/core",
202 summary = "Construct empty string arrays with MATLAB-compatible dimension semantics.",
203 keywords = "string.empty,empty,string array,preallocate",
204 accel = "none",
205 type_resolver(string_array_type),
206 descriptor(crate::builtins::strings::core::string_empty::STRING_EMPTY_DESCRIPTOR),
207 builtin_path = "crate::builtins::strings::core::string_empty"
208)]
209async fn string_empty_builtin(rest: Vec<Value>) -> crate::BuiltinResult<Value> {
210 let shape = parse_shape(&rest).await?;
211 let total: usize = shape.iter().product();
212 debug_assert_eq!(total, 0, "string.empty must produce an empty array");
213 let data = Vec::<String>::new();
214 let array = StringArray::new(data, shape)
215 .map_err(|_| string_empty_error(&STRING_EMPTY_ERROR_INTERNAL))?;
216 Ok(Value::StringArray(array))
217}
218
219async fn parse_shape(args: &[Value]) -> BuiltinResult<Vec<usize>> {
220 if args.is_empty() {
221 return Ok(vec![0, 0]);
222 }
223
224 let mut explicit_dims: Vec<usize> = Vec::new();
225 let mut like_shape: Option<Vec<usize>> = None;
226 let mut idx = 0;
227
228 while idx < args.len() {
229 let arg_host = gather_if_needed_async(&args[idx])
230 .await
231 .map_err(remap_string_empty_flow)?;
232
233 if let Some(keyword) = keyword_of(&arg_host) {
234 if keyword.as_str() == "like" {
235 if like_shape.is_some() {
236 return Err(string_empty_error(&STRING_EMPTY_ERROR_LIKE_DUPLICATE));
237 }
238 let Some(proto_raw) = args.get(idx + 1) else {
239 return Err(string_empty_error(&STRING_EMPTY_ERROR_LIKE_MISSING));
240 };
241 let proto = gather_if_needed_async(proto_raw)
242 .await
243 .map_err(remap_string_empty_flow)?;
244 like_shape = Some(prototype_dims(&proto));
245 idx += 2;
246 continue;
247 }
248 }
251
252 if let Some(parsed) = extract_dims(&arg_host, LABEL).await.map_err(|message| {
253 string_empty_error_with_message(message, &STRING_EMPTY_ERROR_INVALID_SIZE)
254 })? {
255 if explicit_dims.is_empty() {
256 explicit_dims = parsed;
257 } else {
258 explicit_dims.extend(parsed);
259 }
260 idx += 1;
261 continue;
262 }
263
264 return Err(string_empty_error(&STRING_EMPTY_ERROR_INVALID_SIZE));
265 }
266
267 let shape = if !explicit_dims.is_empty() {
268 shape_from_explicit_dims(&explicit_dims)
269 } else if let Some(proto_shape) = like_shape {
270 shape_from_like(&proto_shape)
271 } else {
272 vec![0, 0]
273 };
274 ensure_empty_shape(&shape)?;
275 Ok(shape)
276}
277
278fn shape_from_explicit_dims(dims: &[usize]) -> Vec<usize> {
279 match dims.len() {
280 0 => vec![0, 0],
281 1 => vec![0, dims[0]],
282 _ => {
283 let mut shape = Vec::with_capacity(dims.len());
284 shape.push(0);
285 shape.extend_from_slice(&dims[1..]);
286 shape
287 }
288 }
289}
290
291fn shape_from_like(proto: &[usize]) -> Vec<usize> {
292 if proto.is_empty() {
293 return vec![0, 0];
294 }
295 if proto.len() == 1 {
296 return vec![0, proto[0]];
297 }
298 let mut shape = Vec::with_capacity(proto.len());
299 shape.push(0);
300 shape.extend_from_slice(&proto[1..]);
301 shape
302}
303
304fn ensure_empty_shape(shape: &[usize]) -> BuiltinResult<()> {
305 if shape.iter().product::<usize>() != 0 {
306 return Err(string_empty_error(&STRING_EMPTY_ERROR_NOT_EMPTY_SHAPE));
307 }
308 Ok(())
309}
310
311fn prototype_dims(proto: &Value) -> Vec<usize> {
312 match proto {
313 Value::StringArray(sa) => sa.shape.clone(),
314 Value::CharArray(ca) => vec![ca.rows, ca.cols],
315 Value::Tensor(t) => t.shape.clone(),
316 Value::ComplexTensor(t) => t.shape.clone(),
317 Value::LogicalArray(l) => l.shape.clone(),
318 Value::Cell(cell) => cell.shape.clone(),
319 Value::GpuTensor(handle) => handle.shape.clone(),
320 Value::Num(_) | Value::Int(_) | Value::Bool(_) | Value::Complex(_, _) => vec![1, 1],
321 Value::String(_) => vec![1, 1],
322 _ => vec![1, 1],
323 }
324}
325
326#[cfg(test)]
327pub(crate) mod tests {
328 use super::*;
329 use crate::builtins::common::test_support;
330 use runmat_accelerate_api::HostTensorView;
331 use runmat_builtins::{ResolveContext, StringArray, Tensor, Type, Value};
332
333 fn string_empty_builtin(rest: Vec<Value>) -> BuiltinResult<Value> {
334 futures::executor::block_on(super::string_empty_builtin(rest))
335 }
336
337 fn error_message(err: crate::RuntimeError) -> String {
338 err.message().to_string()
339 }
340
341 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
342 #[test]
343 fn default_is_zero_by_zero() {
344 let result = string_empty_builtin(Vec::new()).expect("string.empty");
345 match result {
346 Value::StringArray(sa) => {
347 assert_eq!(sa.shape, vec![0, 0]);
348 assert_eq!(sa.data.len(), 0);
349 }
350 other => panic!("expected string array, got {other:?}"),
351 }
352 }
353
354 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
355 #[test]
356 fn single_dimension_creates_zero_by_n() {
357 let result = string_empty_builtin(vec![Value::from(5)]).expect("string.empty");
358 match result {
359 Value::StringArray(sa) => {
360 assert_eq!(sa.shape, vec![0, 5]);
361 assert_eq!(sa.data.len(), 0);
362 }
363 other => panic!("expected string array, got {other:?}"),
364 }
365 }
366
367 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
368 #[test]
369 fn multiple_dimensions_respect_trailing_sizes() {
370 let args = vec![Value::from(3), Value::from(4), Value::from(2)];
371 let result = string_empty_builtin(args).expect("string.empty");
372 match result {
373 Value::StringArray(sa) => {
374 assert_eq!(sa.shape, vec![0, 4, 2]);
375 assert_eq!(sa.data.len(), 0);
376 }
377 other => panic!("expected string array, got {other:?}"),
378 }
379 }
380
381 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
382 #[test]
383 fn size_vector_argument_supported() {
384 let tensor = Tensor::new(vec![0.0, 5.0, 3.0], vec![1, 3]).unwrap();
385 let result = string_empty_builtin(vec![Value::Tensor(tensor)]).expect("string.empty");
386 match result {
387 Value::StringArray(sa) => {
388 assert_eq!(sa.shape, vec![0, 5, 3]);
389 assert_eq!(sa.data.len(), 0);
390 }
391 other => panic!("expected string array, got {other:?}"),
392 }
393 }
394
395 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
396 #[test]
397 fn size_vector_from_nonempty_array_drops_leading_extent() {
398 let tensor = Tensor::new(vec![3.0, 2.0], vec![1, 2]).unwrap();
399 let result = string_empty_builtin(vec![Value::Tensor(tensor)]).expect("string.empty");
400 match result {
401 Value::StringArray(sa) => {
402 assert_eq!(sa.shape, vec![0, 2]);
403 assert_eq!(sa.data.len(), 0);
404 }
405 other => panic!("expected string array, got {other:?}"),
406 }
407 }
408
409 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
410 #[test]
411 fn accepts_zero_in_any_position() {
412 let args = vec![Value::from(3), Value::from(4), Value::from(0)];
413 let result = string_empty_builtin(args).expect("string.empty");
414 match result {
415 Value::StringArray(sa) => assert_eq!(sa.shape, vec![0, 4, 0]),
416 other => panic!("expected string array, got {other:?}"),
417 }
418 }
419
420 #[test]
421 fn string_empty_type_is_string_array() {
422 assert_eq!(
423 string_array_type(&[], &ResolveContext::new(Vec::new())),
424 Type::cell_of(Type::String)
425 );
426 }
427
428 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
429 #[test]
430 fn like_prototype_without_explicit_dims() {
431 let proto = StringArray::new(vec!["alpha".to_string(); 6], vec![2, 3]).unwrap();
432 let result = string_empty_builtin(vec![Value::from("like"), Value::StringArray(proto)])
433 .expect("string.empty");
434 match result {
435 Value::StringArray(sa) => {
436 assert_eq!(sa.shape, vec![0, 3]);
437 assert_eq!(sa.data.len(), 0);
438 }
439 other => panic!("expected string array, got {other:?}"),
440 }
441 }
442
443 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
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 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
459 #[test]
460 fn like_with_numeric_prototype() {
461 let tensor = Tensor::new(vec![1.0, 2.0], vec![2, 1]).unwrap();
462 let result = string_empty_builtin(vec![Value::from("like"), Value::Tensor(tensor)])
463 .expect("string.empty");
464 match result {
465 Value::StringArray(sa) => {
466 assert_eq!(sa.shape, vec![0, 1]);
467 assert_eq!(sa.data.len(), 0);
468 }
469 other => panic!("expected string array, got {other:?}"),
470 }
471 }
472
473 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
474 #[test]
475 fn like_with_explicit_dims_prefers_dimensions() {
476 let proto = StringArray::new(Vec::new(), vec![0, 2]).unwrap();
477 let args = vec![
478 Value::from(0),
479 Value::from(7),
480 Value::from("like"),
481 Value::StringArray(proto),
482 ];
483 let result = string_empty_builtin(args).expect("string.empty");
484 match result {
485 Value::StringArray(sa) => {
486 assert_eq!(sa.shape, vec![0, 7]);
487 assert_eq!(sa.data.len(), 0);
488 }
489 other => panic!("expected string array, got {other:?}"),
490 }
491 }
492
493 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
494 #[test]
495 fn missing_like_prototype_errors() {
496 let err = error_message(
497 string_empty_builtin(vec![Value::from("like")]).expect_err("expected error"),
498 );
499 assert!(
500 err.contains("expected prototype"),
501 "unexpected error: {err}"
502 );
503 }
504
505 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
506 #[test]
507 fn duplicate_like_errors() {
508 let proto = StringArray::new(Vec::new(), vec![0, 2]).unwrap();
509 let err = error_message(
510 string_empty_builtin(vec![
511 Value::from("like"),
512 Value::StringArray(proto.clone()),
513 Value::from("like"),
514 Value::StringArray(proto),
515 ])
516 .expect_err("expected error"),
517 );
518 assert!(err.contains("multiple 'like'"), "unexpected error: {err}");
519 }
520
521 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
522 #[test]
523 fn rejects_non_dimension_inputs() {
524 let err = error_message(
525 string_empty_builtin(vec![Value::String("oops".into())]).expect_err("expected error"),
526 );
527 assert!(
528 err.contains("size inputs must be numeric"),
529 "unexpected error: {err}"
530 );
531 }
532
533 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
534 #[test]
535 fn like_gathers_gpu_prototype() {
536 test_support::with_test_provider(|provider| {
537 let tensor =
538 Tensor::new((1..=6).map(|v| v as f64).collect::<Vec<_>>(), vec![2, 3]).unwrap();
539 let view = HostTensorView {
540 data: &tensor.data,
541 shape: &tensor.shape,
542 };
543 let handle = provider.upload(&view).expect("upload");
544 let result =
545 string_empty_builtin(vec![Value::from("like"), Value::GpuTensor(handle.clone())])
546 .expect("string.empty");
547 match result {
548 Value::StringArray(sa) => {
549 assert_eq!(sa.shape, vec![0, 3]);
550 assert_eq!(sa.data.len(), 0);
551 }
552 other => panic!("expected string array, got {other:?}"),
553 }
554 let _ = provider.free(&handle);
555 });
556 }
557
558 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
559 #[test]
560 fn gpu_dimension_arguments_are_gathered() {
561 test_support::with_test_provider(|provider| {
562 let dims = Tensor::new(vec![0.0, 5.0, 3.0], vec![1, 3]).unwrap();
563 let view = HostTensorView {
564 data: &dims.data,
565 shape: &dims.shape,
566 };
567 let handle = provider.upload(&view).expect("upload");
568 let result =
569 string_empty_builtin(vec![Value::GpuTensor(handle.clone())]).expect("string.empty");
570 match result {
571 Value::StringArray(sa) => {
572 assert_eq!(sa.shape, vec![0, 5, 3]);
573 assert_eq!(sa.data.len(), 0);
574 }
575 other => panic!("expected string array, got {other:?}"),
576 }
577 let _ = provider.free(&handle);
578 });
579 }
580
581 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
582 #[test]
583 fn rejects_negative_dimension() {
584 let err = error_message(
585 string_empty_builtin(vec![Value::from(-1.0)]).expect_err("expected error"),
586 );
587 assert!(
588 err.contains("matrix dimensions must be non-negative"),
589 "unexpected error: {err}"
590 );
591 }
592}