1use runmat_builtins::{StringArray, Value};
4use runmat_macros::runtime_builtin;
5
6use crate::builtins::common::map_control_flow_with_builtin;
7use crate::builtins::common::random_args::{extract_dims, keyword_of};
8use crate::builtins::common::spec::{
9 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
10 ReductionNaN, ResidencyPolicy, ShapeRequirements,
11};
12use crate::builtins::strings::type_resolvers::string_array_type;
13use crate::{build_runtime_error, gather_if_needed_async, BuiltinResult, RuntimeError};
14
15const LABEL: &str = "string.empty";
16
17fn string_empty_flow(message: impl Into<String>) -> RuntimeError {
18 build_runtime_error(message).with_builtin(LABEL).build()
19}
20
21fn remap_string_empty_flow(err: RuntimeError) -> RuntimeError {
22 map_control_flow_with_builtin(err, LABEL)
23}
24
25#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::strings::core::string_empty")]
26pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
27 name: "string.empty",
28 op_kind: GpuOpKind::Custom("constructor"),
29 supported_precisions: &[],
30 broadcast: BroadcastSemantics::None,
31 provider_hooks: &[],
32 constant_strategy: ConstantStrategy::InlineLiteral,
33 residency: ResidencyPolicy::NewHandle,
34 nan_mode: ReductionNaN::Include,
35 two_pass_threshold: None,
36 workgroup_size: None,
37 accepts_nan_mode: false,
38 notes: "Host-only constructor that returns a new empty string array without contacting GPU providers.",
39};
40
41#[runmat_macros::register_fusion_spec(
42 builtin_path = "crate::builtins::strings::core::string_empty"
43)]
44pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
45 name: "string.empty",
46 shape: ShapeRequirements::Any,
47 constant_strategy: ConstantStrategy::InlineLiteral,
48 elementwise: None,
49 reduction: None,
50 emits_nan: false,
51 notes: "Pure constructor; fusion planner treats calls as non-fusable sinks.",
52};
53
54#[runtime_builtin(
55 name = "string.empty",
56 category = "strings/core",
57 summary = "Construct an empty string array with MATLAB-compatible dimensions.",
58 keywords = "string.empty,empty,string array,preallocate",
59 accel = "none",
60 type_resolver(string_array_type),
61 builtin_path = "crate::builtins::strings::core::string_empty"
62)]
63async fn string_empty_builtin(rest: Vec<Value>) -> crate::BuiltinResult<Value> {
64 let shape = parse_shape(&rest).await?;
65 let total: usize = shape.iter().product();
66 debug_assert_eq!(total, 0, "string.empty must produce an empty array");
67 let data = Vec::<String>::new();
68 let array =
69 StringArray::new(data, shape).map_err(|e| string_empty_flow(format!("{LABEL}: {e}")))?;
70 Ok(Value::StringArray(array))
71}
72
73async fn parse_shape(args: &[Value]) -> BuiltinResult<Vec<usize>> {
74 if args.is_empty() {
75 return Ok(vec![0, 0]);
76 }
77
78 let mut explicit_dims: Vec<usize> = Vec::new();
79 let mut like_shape: Option<Vec<usize>> = None;
80 let mut idx = 0;
81
82 while idx < args.len() {
83 let arg_host = gather_if_needed_async(&args[idx])
84 .await
85 .map_err(remap_string_empty_flow)?;
86
87 if let Some(keyword) = keyword_of(&arg_host) {
88 if keyword.as_str() == "like" {
89 if like_shape.is_some() {
90 return Err(string_empty_flow(format!(
91 "{LABEL}: multiple 'like' prototypes are not supported"
92 )));
93 }
94 let Some(proto_raw) = args.get(idx + 1) else {
95 return Err(string_empty_flow(format!(
96 "{LABEL}: expected prototype after 'like'"
97 )));
98 };
99 let proto = gather_if_needed_async(proto_raw)
100 .await
101 .map_err(remap_string_empty_flow)?;
102 like_shape = Some(prototype_dims(&proto));
103 idx += 2;
104 continue;
105 }
106 }
109
110 if let Some(parsed) = extract_dims(&arg_host, LABEL)
111 .await
112 .map_err(string_empty_flow)?
113 {
114 if explicit_dims.is_empty() {
115 explicit_dims = parsed;
116 } else {
117 explicit_dims.extend(parsed);
118 }
119 idx += 1;
120 continue;
121 }
122
123 return Err(string_empty_flow(format!(
124 "{LABEL}: size inputs must be numeric scalars or size vectors"
125 )));
126 }
127
128 let shape = if !explicit_dims.is_empty() {
129 shape_from_explicit_dims(&explicit_dims)
130 } else if let Some(proto_shape) = like_shape {
131 shape_from_like(&proto_shape)
132 } else {
133 vec![0, 0]
134 };
135 ensure_empty_shape(&shape)?;
136 Ok(shape)
137}
138
139fn shape_from_explicit_dims(dims: &[usize]) -> Vec<usize> {
140 match dims.len() {
141 0 => vec![0, 0],
142 1 => vec![0, dims[0]],
143 _ => {
144 let mut shape = Vec::with_capacity(dims.len());
145 shape.push(0);
146 shape.extend_from_slice(&dims[1..]);
147 shape
148 }
149 }
150}
151
152fn shape_from_like(proto: &[usize]) -> Vec<usize> {
153 if proto.is_empty() {
154 return vec![0, 0];
155 }
156 if proto.len() == 1 {
157 return vec![0, proto[0]];
158 }
159 let mut shape = Vec::with_capacity(proto.len());
160 shape.push(0);
161 shape.extend_from_slice(&proto[1..]);
162 shape
163}
164
165fn ensure_empty_shape(shape: &[usize]) -> BuiltinResult<()> {
166 if shape.iter().product::<usize>() != 0 {
167 return Err(string_empty_flow(format!(
168 "{LABEL}: at least one dimension must be zero to construct an empty string array"
169 )));
170 }
171 Ok(())
172}
173
174fn prototype_dims(proto: &Value) -> Vec<usize> {
175 match proto {
176 Value::StringArray(sa) => sa.shape.clone(),
177 Value::CharArray(ca) => vec![ca.rows, ca.cols],
178 Value::Tensor(t) => t.shape.clone(),
179 Value::ComplexTensor(t) => t.shape.clone(),
180 Value::LogicalArray(l) => l.shape.clone(),
181 Value::Cell(cell) => cell.shape.clone(),
182 Value::GpuTensor(handle) => handle.shape.clone(),
183 Value::Num(_) | Value::Int(_) | Value::Bool(_) | Value::Complex(_, _) => vec![1, 1],
184 Value::String(_) => vec![1, 1],
185 _ => vec![1, 1],
186 }
187}
188
189#[cfg(test)]
190pub(crate) mod tests {
191 use super::*;
192 use crate::builtins::common::test_support;
193 use runmat_accelerate_api::HostTensorView;
194 use runmat_builtins::{ResolveContext, StringArray, Tensor, Type, Value};
195
196 fn string_empty_builtin(rest: Vec<Value>) -> BuiltinResult<Value> {
197 futures::executor::block_on(super::string_empty_builtin(rest))
198 }
199
200 fn error_message(err: crate::RuntimeError) -> String {
201 err.message().to_string()
202 }
203
204 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
205 #[test]
206 fn default_is_zero_by_zero() {
207 let result = string_empty_builtin(Vec::new()).expect("string.empty");
208 match result {
209 Value::StringArray(sa) => {
210 assert_eq!(sa.shape, vec![0, 0]);
211 assert_eq!(sa.data.len(), 0);
212 }
213 other => panic!("expected string array, got {other:?}"),
214 }
215 }
216
217 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
218 #[test]
219 fn single_dimension_creates_zero_by_n() {
220 let result = string_empty_builtin(vec![Value::from(5)]).expect("string.empty");
221 match result {
222 Value::StringArray(sa) => {
223 assert_eq!(sa.shape, vec![0, 5]);
224 assert_eq!(sa.data.len(), 0);
225 }
226 other => panic!("expected string array, got {other:?}"),
227 }
228 }
229
230 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
231 #[test]
232 fn multiple_dimensions_respect_trailing_sizes() {
233 let args = vec![Value::from(3), Value::from(4), Value::from(2)];
234 let result = string_empty_builtin(args).expect("string.empty");
235 match result {
236 Value::StringArray(sa) => {
237 assert_eq!(sa.shape, vec![0, 4, 2]);
238 assert_eq!(sa.data.len(), 0);
239 }
240 other => panic!("expected string array, got {other:?}"),
241 }
242 }
243
244 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
245 #[test]
246 fn size_vector_argument_supported() {
247 let tensor = Tensor::new(vec![0.0, 5.0, 3.0], vec![1, 3]).unwrap();
248 let result = string_empty_builtin(vec![Value::Tensor(tensor)]).expect("string.empty");
249 match result {
250 Value::StringArray(sa) => {
251 assert_eq!(sa.shape, vec![0, 5, 3]);
252 assert_eq!(sa.data.len(), 0);
253 }
254 other => panic!("expected string array, got {other:?}"),
255 }
256 }
257
258 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
259 #[test]
260 fn size_vector_from_nonempty_array_drops_leading_extent() {
261 let tensor = Tensor::new(vec![3.0, 2.0], vec![1, 2]).unwrap();
262 let result = string_empty_builtin(vec![Value::Tensor(tensor)]).expect("string.empty");
263 match result {
264 Value::StringArray(sa) => {
265 assert_eq!(sa.shape, vec![0, 2]);
266 assert_eq!(sa.data.len(), 0);
267 }
268 other => panic!("expected string array, got {other:?}"),
269 }
270 }
271
272 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
273 #[test]
274 fn accepts_zero_in_any_position() {
275 let args = vec![Value::from(3), Value::from(4), Value::from(0)];
276 let result = string_empty_builtin(args).expect("string.empty");
277 match result {
278 Value::StringArray(sa) => assert_eq!(sa.shape, vec![0, 4, 0]),
279 other => panic!("expected string array, got {other:?}"),
280 }
281 }
282
283 #[test]
284 fn string_empty_type_is_string_array() {
285 assert_eq!(
286 string_array_type(&[], &ResolveContext::new(Vec::new())),
287 Type::cell_of(Type::String)
288 );
289 }
290
291 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
292 #[test]
293 fn like_prototype_without_explicit_dims() {
294 let proto = StringArray::new(vec!["alpha".to_string(); 6], vec![2, 3]).unwrap();
295 let result = string_empty_builtin(vec![Value::from("like"), Value::StringArray(proto)])
296 .expect("string.empty");
297 match result {
298 Value::StringArray(sa) => {
299 assert_eq!(sa.shape, vec![0, 3]);
300 assert_eq!(sa.data.len(), 0);
301 }
302 other => panic!("expected string array, got {other:?}"),
303 }
304 }
305
306 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
307 #[test]
308 fn like_prototype_with_scalar_shape() {
309 let proto = StringArray::new(vec!["foo".to_string()], vec![1, 1]).unwrap();
310 let result = string_empty_builtin(vec![Value::from("like"), Value::StringArray(proto)])
311 .expect("string.empty");
312 match result {
313 Value::StringArray(sa) => {
314 assert_eq!(sa.shape, vec![0, 1]);
315 assert_eq!(sa.data.len(), 0);
316 }
317 other => panic!("expected string array, got {other:?}"),
318 }
319 }
320
321 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
322 #[test]
323 fn like_with_numeric_prototype() {
324 let tensor = Tensor::new(vec![1.0, 2.0], vec![2, 1]).unwrap();
325 let result = string_empty_builtin(vec![Value::from("like"), Value::Tensor(tensor)])
326 .expect("string.empty");
327 match result {
328 Value::StringArray(sa) => {
329 assert_eq!(sa.shape, vec![0, 1]);
330 assert_eq!(sa.data.len(), 0);
331 }
332 other => panic!("expected string array, got {other:?}"),
333 }
334 }
335
336 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
337 #[test]
338 fn like_with_explicit_dims_prefers_dimensions() {
339 let proto = StringArray::new(Vec::new(), vec![0, 2]).unwrap();
340 let args = vec![
341 Value::from(0),
342 Value::from(7),
343 Value::from("like"),
344 Value::StringArray(proto),
345 ];
346 let result = string_empty_builtin(args).expect("string.empty");
347 match result {
348 Value::StringArray(sa) => {
349 assert_eq!(sa.shape, vec![0, 7]);
350 assert_eq!(sa.data.len(), 0);
351 }
352 other => panic!("expected string array, got {other:?}"),
353 }
354 }
355
356 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
357 #[test]
358 fn missing_like_prototype_errors() {
359 let err = error_message(
360 string_empty_builtin(vec![Value::from("like")]).expect_err("expected error"),
361 );
362 assert!(
363 err.contains("expected prototype"),
364 "unexpected error: {err}"
365 );
366 }
367
368 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
369 #[test]
370 fn duplicate_like_errors() {
371 let proto = StringArray::new(Vec::new(), vec![0, 2]).unwrap();
372 let err = error_message(
373 string_empty_builtin(vec![
374 Value::from("like"),
375 Value::StringArray(proto.clone()),
376 Value::from("like"),
377 Value::StringArray(proto),
378 ])
379 .expect_err("expected error"),
380 );
381 assert!(err.contains("multiple 'like'"), "unexpected error: {err}");
382 }
383
384 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
385 #[test]
386 fn rejects_non_dimension_inputs() {
387 let err = error_message(
388 string_empty_builtin(vec![Value::String("oops".into())]).expect_err("expected error"),
389 );
390 assert!(
391 err.contains("size inputs must be numeric"),
392 "unexpected error: {err}"
393 );
394 }
395
396 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
397 #[test]
398 fn like_gathers_gpu_prototype() {
399 test_support::with_test_provider(|provider| {
400 let tensor =
401 Tensor::new((1..=6).map(|v| v as f64).collect::<Vec<_>>(), vec![2, 3]).unwrap();
402 let view = HostTensorView {
403 data: &tensor.data,
404 shape: &tensor.shape,
405 };
406 let handle = provider.upload(&view).expect("upload");
407 let result =
408 string_empty_builtin(vec![Value::from("like"), Value::GpuTensor(handle.clone())])
409 .expect("string.empty");
410 match result {
411 Value::StringArray(sa) => {
412 assert_eq!(sa.shape, vec![0, 3]);
413 assert_eq!(sa.data.len(), 0);
414 }
415 other => panic!("expected string array, got {other:?}"),
416 }
417 let _ = provider.free(&handle);
418 });
419 }
420
421 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
422 #[test]
423 fn gpu_dimension_arguments_are_gathered() {
424 test_support::with_test_provider(|provider| {
425 let dims = Tensor::new(vec![0.0, 5.0, 3.0], vec![1, 3]).unwrap();
426 let view = HostTensorView {
427 data: &dims.data,
428 shape: &dims.shape,
429 };
430 let handle = provider.upload(&view).expect("upload");
431 let result =
432 string_empty_builtin(vec![Value::GpuTensor(handle.clone())]).expect("string.empty");
433 match result {
434 Value::StringArray(sa) => {
435 assert_eq!(sa.shape, vec![0, 5, 3]);
436 assert_eq!(sa.data.len(), 0);
437 }
438 other => panic!("expected string array, got {other:?}"),
439 }
440 let _ = provider.free(&handle);
441 });
442 }
443
444 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
445 #[test]
446 fn rejects_negative_dimension() {
447 let err = error_message(
448 string_empty_builtin(vec![Value::from(-1.0)]).expect_err("expected error"),
449 );
450 assert!(
451 err.contains("matrix dimensions must be non-negative"),
452 "unexpected error: {err}"
453 );
454 }
455}