use runmat_builtins::{StringArray, Value};
use runmat_macros::runtime_builtin;
use crate::builtins::common::map_control_flow_with_builtin;
use crate::builtins::common::random_args::{extract_dims, keyword_of};
use crate::builtins::common::spec::{
BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
ReductionNaN, ResidencyPolicy, ShapeRequirements,
};
use crate::builtins::strings::type_resolvers::string_array_type;
use crate::{build_runtime_error, gather_if_needed_async, BuiltinResult, RuntimeError};
const LABEL: &str = "string.empty";
fn string_empty_flow(message: impl Into<String>) -> RuntimeError {
build_runtime_error(message).with_builtin(LABEL).build()
}
fn remap_string_empty_flow(err: RuntimeError) -> RuntimeError {
map_control_flow_with_builtin(err, LABEL)
}
#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::strings::core::string_empty")]
pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
name: "string.empty",
op_kind: GpuOpKind::Custom("constructor"),
supported_precisions: &[],
broadcast: BroadcastSemantics::None,
provider_hooks: &[],
constant_strategy: ConstantStrategy::InlineLiteral,
residency: ResidencyPolicy::NewHandle,
nan_mode: ReductionNaN::Include,
two_pass_threshold: None,
workgroup_size: None,
accepts_nan_mode: false,
notes: "Host-only constructor that returns a new empty string array without contacting GPU providers.",
};
#[runmat_macros::register_fusion_spec(
builtin_path = "crate::builtins::strings::core::string_empty"
)]
pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
name: "string.empty",
shape: ShapeRequirements::Any,
constant_strategy: ConstantStrategy::InlineLiteral,
elementwise: None,
reduction: None,
emits_nan: false,
notes: "Pure constructor; fusion planner treats calls as non-fusable sinks.",
};
#[runtime_builtin(
name = "string.empty",
category = "strings/core",
summary = "Construct an empty string array with MATLAB-compatible dimensions.",
keywords = "string.empty,empty,string array,preallocate",
accel = "none",
type_resolver(string_array_type),
builtin_path = "crate::builtins::strings::core::string_empty"
)]
async fn string_empty_builtin(rest: Vec<Value>) -> crate::BuiltinResult<Value> {
let shape = parse_shape(&rest).await?;
let total: usize = shape.iter().product();
debug_assert_eq!(total, 0, "string.empty must produce an empty array");
let data = Vec::<String>::new();
let array =
StringArray::new(data, shape).map_err(|e| string_empty_flow(format!("{LABEL}: {e}")))?;
Ok(Value::StringArray(array))
}
async fn parse_shape(args: &[Value]) -> BuiltinResult<Vec<usize>> {
if args.is_empty() {
return Ok(vec![0, 0]);
}
let mut explicit_dims: Vec<usize> = Vec::new();
let mut like_shape: Option<Vec<usize>> = None;
let mut idx = 0;
while idx < args.len() {
let arg_host = gather_if_needed_async(&args[idx])
.await
.map_err(remap_string_empty_flow)?;
if let Some(keyword) = keyword_of(&arg_host) {
if keyword.as_str() == "like" {
if like_shape.is_some() {
return Err(string_empty_flow(format!(
"{LABEL}: multiple 'like' prototypes are not supported"
)));
}
let Some(proto_raw) = args.get(idx + 1) else {
return Err(string_empty_flow(format!(
"{LABEL}: expected prototype after 'like'"
)));
};
let proto = gather_if_needed_async(proto_raw)
.await
.map_err(remap_string_empty_flow)?;
like_shape = Some(prototype_dims(&proto));
idx += 2;
continue;
}
}
if let Some(parsed) = extract_dims(&arg_host, LABEL)
.await
.map_err(string_empty_flow)?
{
if explicit_dims.is_empty() {
explicit_dims = parsed;
} else {
explicit_dims.extend(parsed);
}
idx += 1;
continue;
}
return Err(string_empty_flow(format!(
"{LABEL}: size inputs must be numeric scalars or size vectors"
)));
}
let shape = if !explicit_dims.is_empty() {
shape_from_explicit_dims(&explicit_dims)
} else if let Some(proto_shape) = like_shape {
shape_from_like(&proto_shape)
} else {
vec![0, 0]
};
ensure_empty_shape(&shape)?;
Ok(shape)
}
fn shape_from_explicit_dims(dims: &[usize]) -> Vec<usize> {
match dims.len() {
0 => vec![0, 0],
1 => vec![0, dims[0]],
_ => {
let mut shape = Vec::with_capacity(dims.len());
shape.push(0);
shape.extend_from_slice(&dims[1..]);
shape
}
}
}
fn shape_from_like(proto: &[usize]) -> Vec<usize> {
if proto.is_empty() {
return vec![0, 0];
}
if proto.len() == 1 {
return vec![0, proto[0]];
}
let mut shape = Vec::with_capacity(proto.len());
shape.push(0);
shape.extend_from_slice(&proto[1..]);
shape
}
fn ensure_empty_shape(shape: &[usize]) -> BuiltinResult<()> {
if shape.iter().product::<usize>() != 0 {
return Err(string_empty_flow(format!(
"{LABEL}: at least one dimension must be zero to construct an empty string array"
)));
}
Ok(())
}
fn prototype_dims(proto: &Value) -> Vec<usize> {
match proto {
Value::StringArray(sa) => sa.shape.clone(),
Value::CharArray(ca) => vec![ca.rows, ca.cols],
Value::Tensor(t) => t.shape.clone(),
Value::ComplexTensor(t) => t.shape.clone(),
Value::LogicalArray(l) => l.shape.clone(),
Value::Cell(cell) => cell.shape.clone(),
Value::GpuTensor(handle) => handle.shape.clone(),
Value::Num(_) | Value::Int(_) | Value::Bool(_) | Value::Complex(_, _) => vec![1, 1],
Value::String(_) => vec![1, 1],
_ => vec![1, 1],
}
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::builtins::common::test_support;
use runmat_accelerate_api::HostTensorView;
use runmat_builtins::{ResolveContext, StringArray, Tensor, Type, Value};
fn string_empty_builtin(rest: Vec<Value>) -> BuiltinResult<Value> {
futures::executor::block_on(super::string_empty_builtin(rest))
}
fn error_message(err: crate::RuntimeError) -> String {
err.message().to_string()
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn default_is_zero_by_zero() {
let result = string_empty_builtin(Vec::new()).expect("string.empty");
match result {
Value::StringArray(sa) => {
assert_eq!(sa.shape, vec![0, 0]);
assert_eq!(sa.data.len(), 0);
}
other => panic!("expected string array, got {other:?}"),
}
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn single_dimension_creates_zero_by_n() {
let result = string_empty_builtin(vec![Value::from(5)]).expect("string.empty");
match result {
Value::StringArray(sa) => {
assert_eq!(sa.shape, vec![0, 5]);
assert_eq!(sa.data.len(), 0);
}
other => panic!("expected string array, got {other:?}"),
}
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn multiple_dimensions_respect_trailing_sizes() {
let args = vec![Value::from(3), Value::from(4), Value::from(2)];
let result = string_empty_builtin(args).expect("string.empty");
match result {
Value::StringArray(sa) => {
assert_eq!(sa.shape, vec![0, 4, 2]);
assert_eq!(sa.data.len(), 0);
}
other => panic!("expected string array, got {other:?}"),
}
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn size_vector_argument_supported() {
let tensor = Tensor::new(vec![0.0, 5.0, 3.0], vec![1, 3]).unwrap();
let result = string_empty_builtin(vec![Value::Tensor(tensor)]).expect("string.empty");
match result {
Value::StringArray(sa) => {
assert_eq!(sa.shape, vec![0, 5, 3]);
assert_eq!(sa.data.len(), 0);
}
other => panic!("expected string array, got {other:?}"),
}
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn size_vector_from_nonempty_array_drops_leading_extent() {
let tensor = Tensor::new(vec![3.0, 2.0], vec![1, 2]).unwrap();
let result = string_empty_builtin(vec![Value::Tensor(tensor)]).expect("string.empty");
match result {
Value::StringArray(sa) => {
assert_eq!(sa.shape, vec![0, 2]);
assert_eq!(sa.data.len(), 0);
}
other => panic!("expected string array, got {other:?}"),
}
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn accepts_zero_in_any_position() {
let args = vec![Value::from(3), Value::from(4), Value::from(0)];
let result = string_empty_builtin(args).expect("string.empty");
match result {
Value::StringArray(sa) => assert_eq!(sa.shape, vec![0, 4, 0]),
other => panic!("expected string array, got {other:?}"),
}
}
#[test]
fn string_empty_type_is_string_array() {
assert_eq!(
string_array_type(&[], &ResolveContext::new(Vec::new())),
Type::cell_of(Type::String)
);
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn like_prototype_without_explicit_dims() {
let proto = StringArray::new(vec!["alpha".to_string(); 6], vec![2, 3]).unwrap();
let result = string_empty_builtin(vec![Value::from("like"), Value::StringArray(proto)])
.expect("string.empty");
match result {
Value::StringArray(sa) => {
assert_eq!(sa.shape, vec![0, 3]);
assert_eq!(sa.data.len(), 0);
}
other => panic!("expected string array, got {other:?}"),
}
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn like_prototype_with_scalar_shape() {
let proto = StringArray::new(vec!["foo".to_string()], vec![1, 1]).unwrap();
let result = string_empty_builtin(vec![Value::from("like"), Value::StringArray(proto)])
.expect("string.empty");
match result {
Value::StringArray(sa) => {
assert_eq!(sa.shape, vec![0, 1]);
assert_eq!(sa.data.len(), 0);
}
other => panic!("expected string array, got {other:?}"),
}
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn like_with_numeric_prototype() {
let tensor = Tensor::new(vec![1.0, 2.0], vec![2, 1]).unwrap();
let result = string_empty_builtin(vec![Value::from("like"), Value::Tensor(tensor)])
.expect("string.empty");
match result {
Value::StringArray(sa) => {
assert_eq!(sa.shape, vec![0, 1]);
assert_eq!(sa.data.len(), 0);
}
other => panic!("expected string array, got {other:?}"),
}
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn like_with_explicit_dims_prefers_dimensions() {
let proto = StringArray::new(Vec::new(), vec![0, 2]).unwrap();
let args = vec![
Value::from(0),
Value::from(7),
Value::from("like"),
Value::StringArray(proto),
];
let result = string_empty_builtin(args).expect("string.empty");
match result {
Value::StringArray(sa) => {
assert_eq!(sa.shape, vec![0, 7]);
assert_eq!(sa.data.len(), 0);
}
other => panic!("expected string array, got {other:?}"),
}
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn missing_like_prototype_errors() {
let err = error_message(
string_empty_builtin(vec![Value::from("like")]).expect_err("expected error"),
);
assert!(
err.contains("expected prototype"),
"unexpected error: {err}"
);
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn duplicate_like_errors() {
let proto = StringArray::new(Vec::new(), vec![0, 2]).unwrap();
let err = error_message(
string_empty_builtin(vec![
Value::from("like"),
Value::StringArray(proto.clone()),
Value::from("like"),
Value::StringArray(proto),
])
.expect_err("expected error"),
);
assert!(err.contains("multiple 'like'"), "unexpected error: {err}");
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn rejects_non_dimension_inputs() {
let err = error_message(
string_empty_builtin(vec![Value::String("oops".into())]).expect_err("expected error"),
);
assert!(
err.contains("size inputs must be numeric"),
"unexpected error: {err}"
);
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn like_gathers_gpu_prototype() {
test_support::with_test_provider(|provider| {
let tensor =
Tensor::new((1..=6).map(|v| v as f64).collect::<Vec<_>>(), vec![2, 3]).unwrap();
let view = HostTensorView {
data: &tensor.data,
shape: &tensor.shape,
};
let handle = provider.upload(&view).expect("upload");
let result =
string_empty_builtin(vec![Value::from("like"), Value::GpuTensor(handle.clone())])
.expect("string.empty");
match result {
Value::StringArray(sa) => {
assert_eq!(sa.shape, vec![0, 3]);
assert_eq!(sa.data.len(), 0);
}
other => panic!("expected string array, got {other:?}"),
}
let _ = provider.free(&handle);
});
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn gpu_dimension_arguments_are_gathered() {
test_support::with_test_provider(|provider| {
let dims = Tensor::new(vec![0.0, 5.0, 3.0], vec![1, 3]).unwrap();
let view = HostTensorView {
data: &dims.data,
shape: &dims.shape,
};
let handle = provider.upload(&view).expect("upload");
let result =
string_empty_builtin(vec![Value::GpuTensor(handle.clone())]).expect("string.empty");
match result {
Value::StringArray(sa) => {
assert_eq!(sa.shape, vec![0, 5, 3]);
assert_eq!(sa.data.len(), 0);
}
other => panic!("expected string array, got {other:?}"),
}
let _ = provider.free(&handle);
});
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn rejects_negative_dimension() {
let err = error_message(
string_empty_builtin(vec![Value::from(-1.0)]).expect_err("expected error"),
);
assert!(
err.contains("matrix dimensions must be non-negative"),
"unexpected error: {err}"
);
}
}