1use runmat_builtins::{
4 BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
5 BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
6};
7use runmat_macros::runtime_builtin;
8use runmat_value::{StringArray, Value};
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) => ca.shape.clone(),
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, Type};
332 use runmat_value::{StringArray, Tensor, Value};
333
334 fn string_empty_builtin(rest: Vec<Value>) -> BuiltinResult<Value> {
335 futures::executor::block_on(super::string_empty_builtin(rest))
336 }
337
338 fn error_message(err: crate::RuntimeError) -> String {
339 err.message().to_string()
340 }
341
342 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
343 #[test]
344 fn default_is_zero_by_zero() {
345 let result = string_empty_builtin(Vec::new()).expect("string.empty");
346 match result {
347 Value::StringArray(sa) => {
348 assert_eq!(sa.shape, vec![0, 0]);
349 assert_eq!(sa.data.len(), 0);
350 }
351 other => panic!("expected string array, got {other:?}"),
352 }
353 }
354
355 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
356 #[test]
357 fn single_dimension_creates_zero_by_n() {
358 let result = string_empty_builtin(vec![Value::from(5)]).expect("string.empty");
359 match result {
360 Value::StringArray(sa) => {
361 assert_eq!(sa.shape, vec![0, 5]);
362 assert_eq!(sa.data.len(), 0);
363 }
364 other => panic!("expected string array, got {other:?}"),
365 }
366 }
367
368 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
369 #[test]
370 fn multiple_dimensions_respect_trailing_sizes() {
371 let args = vec![Value::from(3), Value::from(4), Value::from(2)];
372 let result = string_empty_builtin(args).expect("string.empty");
373 match result {
374 Value::StringArray(sa) => {
375 assert_eq!(sa.shape, vec![0, 4, 2]);
376 assert_eq!(sa.data.len(), 0);
377 }
378 other => panic!("expected string array, got {other:?}"),
379 }
380 }
381
382 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
383 #[test]
384 fn size_vector_argument_supported() {
385 let tensor = Tensor::new(vec![0.0, 5.0, 3.0], vec![1, 3]).unwrap();
386 let result = string_empty_builtin(vec![Value::Tensor(tensor)]).expect("string.empty");
387 match result {
388 Value::StringArray(sa) => {
389 assert_eq!(sa.shape, vec![0, 5, 3]);
390 assert_eq!(sa.data.len(), 0);
391 }
392 other => panic!("expected string array, got {other:?}"),
393 }
394 }
395
396 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
397 #[test]
398 fn size_vector_from_nonempty_array_drops_leading_extent() {
399 let tensor = Tensor::new(vec![3.0, 2.0], vec![1, 2]).unwrap();
400 let result = string_empty_builtin(vec![Value::Tensor(tensor)]).expect("string.empty");
401 match result {
402 Value::StringArray(sa) => {
403 assert_eq!(sa.shape, vec![0, 2]);
404 assert_eq!(sa.data.len(), 0);
405 }
406 other => panic!("expected string array, got {other:?}"),
407 }
408 }
409
410 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
411 #[test]
412 fn accepts_zero_in_any_position() {
413 let args = vec![Value::from(3), Value::from(4), Value::from(0)];
414 let result = string_empty_builtin(args).expect("string.empty");
415 match result {
416 Value::StringArray(sa) => assert_eq!(sa.shape, vec![0, 4, 0]),
417 other => panic!("expected string array, got {other:?}"),
418 }
419 }
420
421 #[test]
422 fn string_empty_type_is_string_array() {
423 assert_eq!(
424 string_array_type(&[], &ResolveContext::new(Vec::new())),
425 Type::cell_of(Type::String)
426 );
427 }
428
429 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
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 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
445 #[test]
446 fn like_prototype_with_scalar_shape() {
447 let proto = StringArray::new(vec!["foo".to_string()], vec![1, 1]).unwrap();
448 let result = string_empty_builtin(vec![Value::from("like"), Value::StringArray(proto)])
449 .expect("string.empty");
450 match result {
451 Value::StringArray(sa) => {
452 assert_eq!(sa.shape, vec![0, 1]);
453 assert_eq!(sa.data.len(), 0);
454 }
455 other => panic!("expected string array, got {other:?}"),
456 }
457 }
458
459 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
460 #[test]
461 fn like_with_numeric_prototype() {
462 let tensor = Tensor::new(vec![1.0, 2.0], vec![2, 1]).unwrap();
463 let result = string_empty_builtin(vec![Value::from("like"), Value::Tensor(tensor)])
464 .expect("string.empty");
465 match result {
466 Value::StringArray(sa) => {
467 assert_eq!(sa.shape, vec![0, 1]);
468 assert_eq!(sa.data.len(), 0);
469 }
470 other => panic!("expected string array, got {other:?}"),
471 }
472 }
473
474 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
475 #[test]
476 fn like_with_explicit_dims_prefers_dimensions() {
477 let proto = StringArray::new(Vec::new(), vec![0, 2]).unwrap();
478 let args = vec![
479 Value::from(0),
480 Value::from(7),
481 Value::from("like"),
482 Value::StringArray(proto),
483 ];
484 let result = string_empty_builtin(args).expect("string.empty");
485 match result {
486 Value::StringArray(sa) => {
487 assert_eq!(sa.shape, vec![0, 7]);
488 assert_eq!(sa.data.len(), 0);
489 }
490 other => panic!("expected string array, got {other:?}"),
491 }
492 }
493
494 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
495 #[test]
496 fn missing_like_prototype_errors() {
497 let err = error_message(
498 string_empty_builtin(vec![Value::from("like")]).expect_err("expected error"),
499 );
500 assert!(
501 err.contains("expected prototype"),
502 "unexpected error: {err}"
503 );
504 }
505
506 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
507 #[test]
508 fn duplicate_like_errors() {
509 let proto = StringArray::new(Vec::new(), vec![0, 2]).unwrap();
510 let err = error_message(
511 string_empty_builtin(vec![
512 Value::from("like"),
513 Value::StringArray(proto.clone()),
514 Value::from("like"),
515 Value::StringArray(proto),
516 ])
517 .expect_err("expected error"),
518 );
519 assert!(err.contains("multiple 'like'"), "unexpected error: {err}");
520 }
521
522 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
523 #[test]
524 fn rejects_non_dimension_inputs() {
525 let err = error_message(
526 string_empty_builtin(vec![Value::String("oops".into())]).expect_err("expected error"),
527 );
528 assert!(
529 err.contains("size inputs must be numeric"),
530 "unexpected error: {err}"
531 );
532 }
533
534 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
535 #[test]
536 fn like_gathers_gpu_prototype() {
537 test_support::with_test_provider(|provider| {
538 let tensor =
539 Tensor::new((1..=6).map(|v| v as f64).collect::<Vec<_>>(), vec![2, 3]).unwrap();
540 let view = HostTensorView {
541 data: &tensor.materialize_f64(),
542 shape: &tensor.shape,
543 };
544 let handle = provider.upload(&view).expect("upload");
545 let result =
546 string_empty_builtin(vec![Value::from("like"), Value::GpuTensor(handle.clone())])
547 .expect("string.empty");
548 match result {
549 Value::StringArray(sa) => {
550 assert_eq!(sa.shape, vec![0, 3]);
551 assert_eq!(sa.data.len(), 0);
552 }
553 other => panic!("expected string array, got {other:?}"),
554 }
555 let _ = provider.free(&handle);
556 });
557 }
558
559 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
560 #[test]
561 fn gpu_dimension_arguments_are_gathered() {
562 test_support::with_test_provider(|provider| {
563 let dims = Tensor::new(vec![0.0, 5.0, 3.0], vec![1, 3]).unwrap();
564 let view = HostTensorView {
565 data: &dims.materialize_f64(),
566 shape: &dims.shape,
567 };
568 let handle = provider.upload(&view).expect("upload");
569 let result =
570 string_empty_builtin(vec![Value::GpuTensor(handle.clone())]).expect("string.empty");
571 match result {
572 Value::StringArray(sa) => {
573 assert_eq!(sa.shape, vec![0, 5, 3]);
574 assert_eq!(sa.data.len(), 0);
575 }
576 other => panic!("expected string array, got {other:?}"),
577 }
578 let _ = provider.free(&handle);
579 });
580 }
581
582 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
583 #[test]
584 fn rejects_negative_dimension() {
585 let err = error_message(
586 string_empty_builtin(vec![Value::from(-1.0)]).expect_err("expected error"),
587 );
588 assert!(
589 err.contains("matrix dimensions must be non-negative"),
590 "unexpected error: {err}"
591 );
592 }
593}