1use runmat_builtins::{
4 CharArray, ComplexTensor, IntValue, LogicalArray, StringArray, StructValue, Tensor, Value,
5};
6use runmat_macros::runtime_builtin;
7
8use crate::builtins::cells::type_resolvers::cell_type;
9use crate::builtins::common::random_args::{keyword_of, shape_from_value};
10use crate::builtins::common::spec::{
11 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
12 ReductionNaN, ResidencyPolicy, ShapeRequirements,
13};
14use crate::{
15 build_runtime_error, gather_if_needed_async, make_cell_with_shape, BuiltinResult, RuntimeError,
16};
17
18#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::cells::core::cell")]
19pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
20 name: "cell",
21 op_kind: GpuOpKind::Custom("container"),
22 supported_precisions: &[],
23 broadcast: BroadcastSemantics::None,
24 provider_hooks: &[],
25 constant_strategy: ConstantStrategy::InlineLiteral,
26 residency: ResidencyPolicy::GatherImmediately,
27 nan_mode: ReductionNaN::Include,
28 two_pass_threshold: None,
29 workgroup_size: None,
30 accepts_nan_mode: false,
31 notes: "Cell arrays are allocated on the host heap; providers currently gather any GPU inputs and rely on host execution.",
32};
33
34#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::cells::core::cell")]
35pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
36 name: "cell",
37 shape: ShapeRequirements::Any,
38 constant_strategy: ConstantStrategy::InlineLiteral,
39 elementwise: None,
40 reduction: None,
41 emits_nan: false,
42 notes: "Cell creation acts as a fusion sink and terminates GPU fusion plans.",
43};
44
45const IDENT_INVALID_INPUT: &str = "RunMat:cell:InvalidInput";
46const IDENT_INVALID_SIZE: &str = "RunMat:cell:InvalidSize";
47
48fn cell_error(message: impl Into<String>) -> RuntimeError {
49 build_runtime_error(message).with_builtin("cell").build()
50}
51
52fn cell_error_with_identifier(message: impl Into<String>, identifier: &str) -> RuntimeError {
53 build_runtime_error(message)
54 .with_builtin("cell")
55 .with_identifier(identifier)
56 .build()
57}
58
59#[runtime_builtin(
60 name = "cell",
61 category = "cells/core",
62 summary = "Create empty MATLAB cell arrays.",
63 keywords = "cell,cell array,container,empty",
64 accel = "array_construct",
65 sink = true,
66 type_resolver(cell_type),
67 builtin_path = "crate::builtins::cells::core::cell"
68)]
69async fn cell_builtin(args: Vec<Value>) -> crate::BuiltinResult<Value> {
70 let parsed = ParsedCell::parse(args).await?;
71 build_cell(parsed)
72}
73
74struct ParsedCell {
75 shape: Vec<usize>,
76 prototype: Option<Value>,
77}
78
79impl ParsedCell {
80 async fn parse(args: Vec<Value>) -> BuiltinResult<Self> {
81 let mut dims: Vec<Value> = Vec::new();
82 let mut prototype: Option<Value> = None;
83 let mut idx = 0;
84
85 while idx < args.len() {
86 let value = &args[idx];
87 if let Some(keyword) = keyword_of(value) {
88 match keyword.as_str() {
89 "like" => {
90 if prototype.is_some() {
91 return Err(cell_error_with_identifier(
92 "cell: multiple 'like' specifications are not supported",
93 IDENT_INVALID_INPUT,
94 ));
95 }
96 let Some(proto) = args.get(idx + 1) else {
97 return Err(cell_error_with_identifier(
98 "cell: expected prototype after 'like'",
99 IDENT_INVALID_INPUT,
100 ));
101 };
102 prototype = Some(proto.clone());
103 idx += 2;
104 continue;
105 }
106 other => {
107 return Err(cell_error_with_identifier(
108 format!("cell: unrecognised option '{other}'"),
109 IDENT_INVALID_INPUT,
110 ));
111 }
112 }
113 }
114
115 dims.push(args[idx].clone());
116 idx += 1;
117 }
118
119 let shape = parse_shape_arguments(&dims, prototype.as_ref()).await?;
120 Ok(Self { shape, prototype })
121 }
122}
123
124fn build_cell(parsed: ParsedCell) -> BuiltinResult<Value> {
125 let shape = ensure_min_rank(parsed.shape);
126 let total = if shape.is_empty() {
127 0
128 } else {
129 shape
130 .iter()
131 .try_fold(1usize, |acc, &dim| acc.checked_mul(dim))
132 .ok_or_else(|| {
133 cell_error_with_identifier(
134 "cell: requested size exceeds platform limits",
135 IDENT_INVALID_SIZE,
136 )
137 })?
138 };
139
140 if total == 0 {
141 return make_cell_with_shape(Vec::new(), shape)
142 .map_err(|e| cell_error(format!("cell: {e}")));
143 }
144
145 let default_value = empty_value_like(parsed.prototype.as_ref())?;
146 let mut values = Vec::with_capacity(total);
147 values.resize(total, default_value);
148 make_cell_with_shape(values, shape).map_err(|e| cell_error(format!("cell: {e}")))
149}
150
151fn ensure_min_rank(dims: Vec<usize>) -> Vec<usize> {
152 match dims.len() {
153 0 => vec![0, 0],
154 1 => vec![dims[0], 1],
155 _ => dims,
156 }
157}
158
159async fn parse_shape_arguments(
160 args: &[Value],
161 prototype: Option<&Value>,
162) -> BuiltinResult<Vec<usize>> {
163 if args.is_empty() {
164 if let Some(proto) = prototype {
165 return shape_from_value(proto, "cell")
166 .map_err(|err| cell_error_with_identifier(err, IDENT_INVALID_INPUT));
167 }
168 return Ok(vec![0, 0]);
169 }
170
171 if args.len() == 1 {
172 let host = gather_if_needed_async(&args[0]).await?;
173 return parse_single_argument(&host);
174 }
175
176 let mut dims = Vec::with_capacity(args.len());
177 for value in args {
178 let host = gather_if_needed_async(value).await?;
179 dims.push(parse_size_scalar(&host, "cell")?);
180 }
181 Ok(dims)
182}
183
184fn parse_single_argument(value: &Value) -> BuiltinResult<Vec<usize>> {
185 match value {
186 Value::Int(_) | Value::Num(_) | Value::Bool(_) => {
187 let n = parse_size_scalar(value, "cell")?;
188 Ok(vec![n, n])
189 }
190 Value::Tensor(t) => parse_size_tensor(t),
191 Value::LogicalArray(arr) => parse_size_logical_array(arr),
192 other => Err(cell_error_with_identifier(
193 format!("cell: size arguments must be numeric scalars or vectors, got {other:?}"),
194 IDENT_INVALID_INPUT,
195 )),
196 }
197}
198
199fn parse_size_scalar(value: &Value, context: &str) -> BuiltinResult<usize> {
200 match value {
201 Value::Int(iv) => parse_intvalue(iv, context),
202 Value::Num(n) => parse_numeric(*n, context),
203 Value::Bool(b) => Ok(if *b { 1 } else { 0 }),
204 Value::Tensor(t) => {
205 if t.data.len() != 1 {
206 return Err(cell_error_with_identifier(
207 format!("{context}: size inputs must be scalar"),
208 IDENT_INVALID_SIZE,
209 ));
210 }
211 parse_numeric(t.data[0], context)
212 }
213 Value::LogicalArray(arr) => {
214 if arr.data.len() != 1 {
215 return Err(cell_error_with_identifier(
216 format!("{context}: size inputs must be scalar"),
217 IDENT_INVALID_SIZE,
218 ));
219 }
220 let numeric = if arr.data[0] != 0 { 1.0 } else { 0.0 };
221 parse_numeric(numeric, context)
222 }
223 other => Err(cell_error_with_identifier(
224 format!("{context}: size inputs must be numeric scalars, got {other:?}"),
225 IDENT_INVALID_INPUT,
226 )),
227 }
228}
229
230fn parse_size_tensor(t: &Tensor) -> BuiltinResult<Vec<usize>> {
231 if t.data.is_empty() {
232 return Ok(vec![0, 0]);
233 }
234 if !is_vector_shape(&t.shape) {
235 return Err(cell_error_with_identifier(
236 "cell: size vector must be 1-D",
237 IDENT_INVALID_SIZE,
238 ));
239 }
240 let dims = t
241 .data
242 .iter()
243 .map(|&value| parse_numeric(value, "cell"))
244 .collect::<Result<Vec<_>, _>>()?;
245 if dims.len() == 1 {
246 Ok(vec![dims[0], 1])
247 } else {
248 Ok(dims)
249 }
250}
251
252fn parse_size_logical_array(arr: &LogicalArray) -> BuiltinResult<Vec<usize>> {
253 if arr.data.is_empty() {
254 return Ok(vec![0, 0]);
255 }
256 if !is_vector_shape(&arr.shape) {
257 return Err(cell_error_with_identifier(
258 "cell: size vector must be 1-D",
259 IDENT_INVALID_SIZE,
260 ));
261 }
262 let dims = arr
263 .data
264 .iter()
265 .map(|&value| {
266 let numeric = if value != 0 { 1.0 } else { 0.0 };
267 parse_numeric(numeric, "cell")
268 })
269 .collect::<Result<Vec<_>, _>>()?;
270 if dims.len() == 1 {
271 Ok(vec![dims[0], 1])
272 } else {
273 Ok(dims)
274 }
275}
276
277fn is_vector_shape(shape: &[usize]) -> bool {
278 match shape.len() {
279 0 => true,
280 1 => true,
281 2 => shape[0] == 1 || shape[1] == 1,
282 _ => false,
283 }
284}
285
286fn empty_value_like(proto: Option<&Value>) -> BuiltinResult<Value> {
287 match proto {
288 Some(value) => match value {
289 Value::LogicalArray(_) | Value::Bool(_) => LogicalArray::new(Vec::new(), vec![0, 0])
290 .map(Value::LogicalArray)
291 .map_err(|e| cell_error(format!("cell: {e}"))),
292 Value::ComplexTensor(_) | Value::Complex(_, _) => {
293 ComplexTensor::new(Vec::new(), vec![0, 0])
294 .map(Value::ComplexTensor)
295 .map_err(|e| cell_error(format!("cell: {e}")))
296 }
297 Value::String(_) => Ok(Value::String(String::new())),
298 Value::StringArray(_) => StringArray::new(Vec::new(), vec![0, 0])
299 .map(Value::StringArray)
300 .map_err(|e| cell_error(format!("cell: {e}"))),
301 Value::CharArray(_) => CharArray::new(Vec::new(), 0, 0)
302 .map(Value::CharArray)
303 .map_err(|e| cell_error(format!("cell: {e}"))),
304 Value::Cell(_) => make_cell_with_shape(Vec::new(), vec![0, 0])
305 .map_err(|e| cell_error(format!("cell: {e}"))),
306 Value::Struct(_) => Ok(Value::Struct(StructValue::new())),
307 Value::Tensor(_) | Value::Num(_) | Value::Int(_) | Value::GpuTensor(_) => {
308 default_empty_double()
309 }
310 Value::Object(_)
311 | Value::HandleObject(_)
312 | Value::Listener(_)
313 | Value::FunctionHandle(_)
314 | Value::Closure(_)
315 | Value::ClassRef(_)
316 | Value::MException(_)
317 | Value::OutputList(_) => default_empty_double(),
318 },
319 None => default_empty_double(),
320 }
321}
322
323fn default_empty_double() -> BuiltinResult<Value> {
324 Tensor::new(Vec::new(), vec![0, 0])
325 .map(Value::Tensor)
326 .map_err(|e| cell_error(format!("cell: {e}")))
327}
328
329fn parse_intvalue(value: &IntValue, context: &str) -> BuiltinResult<usize> {
330 let raw = match value {
331 IntValue::I8(v) => *v as i128,
332 IntValue::I16(v) => *v as i128,
333 IntValue::I32(v) => *v as i128,
334 IntValue::I64(v) => *v as i128,
335 IntValue::U8(v) => *v as i128,
336 IntValue::U16(v) => *v as i128,
337 IntValue::U32(v) => *v as i128,
338 IntValue::U64(v) => *v as i128,
339 };
340 if raw < 0 {
341 return Err(cell_error_with_identifier(
342 format!("{context}: size inputs must be non-negative integers"),
343 IDENT_INVALID_SIZE,
344 ));
345 }
346 if raw as u128 > usize::MAX as u128 {
347 return Err(cell_error_with_identifier(
348 "cell: requested size exceeds platform limits",
349 IDENT_INVALID_SIZE,
350 ));
351 }
352 Ok(raw as usize)
353}
354
355fn parse_numeric(value: f64, context: &str) -> BuiltinResult<usize> {
356 if !value.is_finite() {
357 return Err(cell_error_with_identifier(
358 format!("{context}: size inputs must be finite"),
359 IDENT_INVALID_SIZE,
360 ));
361 }
362 let rounded = value.round();
363 if (rounded - value).abs() > f64::EPSILON {
364 return Err(cell_error_with_identifier(
365 format!("{context}: size inputs must be integers"),
366 IDENT_INVALID_SIZE,
367 ));
368 }
369 if rounded < 0.0 {
370 return Err(cell_error_with_identifier(
371 format!("{context}: size inputs must be non-negative integers"),
372 IDENT_INVALID_SIZE,
373 ));
374 }
375 if rounded > (1u64 << 53) as f64 {
376 return Err(cell_error_with_identifier(
377 "cell: size inputs larger than 2^53 are not supported",
378 IDENT_INVALID_SIZE,
379 ));
380 }
381 if rounded > usize::MAX as f64 {
382 return Err(cell_error_with_identifier(
383 "cell: requested size exceeds platform limits",
384 IDENT_INVALID_SIZE,
385 ));
386 }
387 Ok(rounded as usize)
388}
389
390#[cfg(test)]
391pub(crate) mod tests {
392 use super::*;
393 use crate::builtins::common::test_support;
394 use futures::executor::block_on;
395
396 fn cell_builtin(args: Vec<Value>) -> BuiltinResult<Value> {
397 block_on(super::cell_builtin(args))
398 }
399
400 fn expect_cell_with<F>(value: Value, expected_shape: &[usize], mut check: F)
401 where
402 F: FnMut(&Value),
403 {
404 match value {
405 Value::Cell(cell) => {
406 assert_eq!(cell.shape, expected_shape, "shape mismatch");
407 let expected_rows = expected_shape.first().copied().unwrap_or(0);
408 let expected_cols = match expected_shape.len() {
409 0 => 0,
410 1 => 1,
411 _ => expected_shape[1],
412 };
413 assert_eq!(cell.rows, expected_rows, "rows mismatch");
414 assert_eq!(cell.cols, expected_cols, "cols mismatch");
415 let expected_total = expected_shape
416 .iter()
417 .fold(1usize, |acc, &dim| acc.saturating_mul(dim));
418 let expected_total = if expected_shape.is_empty() {
419 0
420 } else {
421 expected_total
422 };
423 assert_eq!(cell.data.len(), expected_total, "element count mismatch");
424 for handle in cell.data {
425 let element = unsafe { &*handle.as_raw() };
426 check(element);
427 }
428 }
429 other => panic!("expected cell array, got {other:?}"),
430 }
431 }
432
433 fn expect_cell(value: Value, expected_shape: &[usize]) {
434 expect_cell_with(value, expected_shape, |element| match element {
435 Value::Tensor(t) => {
436 assert_eq!(t.shape, vec![0, 0]);
437 assert!(t.data.is_empty());
438 }
439 other => panic!("expected empty double array, found {other:?}"),
440 });
441 }
442
443 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
444 #[test]
445 fn cell_no_arguments_returns_empty() {
446 let result = cell_builtin(Vec::new()).expect("cell()");
447 expect_cell(result, &[0, 0]);
448 }
449
450 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
451 #[test]
452 fn cell_like_requires_prototype() {
453 let err = cell_builtin(vec![Value::from("like")])
454 .unwrap_err()
455 .to_string();
456 assert!(
457 err.contains("expected prototype"),
458 "unexpected error: {err}"
459 );
460 }
461
462 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
463 #[test]
464 fn cell_with_two_sizes() {
465 let args = vec![Value::Num(2.0), Value::Num(4.0)];
466 let result = cell_builtin(args).expect("cell(2,4)");
467 expect_cell(result, &[2, 4]);
468 }
469
470 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
471 #[test]
472 fn cell_with_size_vector() {
473 let tensor = Tensor::new(vec![2.0, 5.0], vec![1, 2]).unwrap();
474 let result = cell_builtin(vec![Value::Tensor(tensor)]).expect("cell([2 5])");
475 expect_cell(result, &[2, 5]);
476 }
477
478 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
479 #[test]
480 fn cell_with_column_size_vector() {
481 let tensor = Tensor::new(vec![4.0, 1.0], vec![2, 1]).unwrap();
482 let result = cell_builtin(vec![Value::Tensor(tensor)]).expect("cell([4; 1])");
483 expect_cell(result, &[4, 1]);
484 }
485
486 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
487 #[test]
488 fn cell_accepts_gpu_size_vector() {
489 test_support::with_test_provider(|provider| {
490 let tensor = Tensor::new(vec![3.0, 2.0], vec![1, 2]).unwrap();
491 let view = runmat_accelerate_api::HostTensorView {
492 data: &tensor.data,
493 shape: &tensor.shape,
494 };
495 let handle = provider.upload(&view).expect("upload size vector");
496 let result = cell_builtin(vec![Value::GpuTensor(handle)]).expect("cell(gpu size)");
497 expect_cell(result, &[3, 2]);
498 });
499 }
500
501 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
502 #[test]
503 #[cfg(feature = "wgpu")]
504 fn cell_wgpu_size_vector_and_like() {
505 let _ = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
506 runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
507 );
508 let tensor = Tensor::new(vec![2.0, 3.0, 1.0], vec![1, 3]).unwrap();
509 let view = runmat_accelerate_api::HostTensorView {
510 data: &tensor.data,
511 shape: &tensor.shape,
512 };
513 let provider = runmat_accelerate_api::provider().expect("wgpu provider");
514 let handle = provider.upload(&view).expect("upload size vector");
515 let result = cell_builtin(vec![Value::GpuTensor(handle)]).expect("cell(wgpu size)");
516 expect_cell(result, &[2, 3, 1]);
517
518 let proto = Tensor::new(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], vec![2, 3]).unwrap();
519 let proto_view = runmat_accelerate_api::HostTensorView {
520 data: &proto.data,
521 shape: &proto.shape,
522 };
523 let proto_handle = provider.upload(&proto_view).expect("upload prototype");
524 let like_result = cell_builtin(vec![Value::from("like"), Value::GpuTensor(proto_handle)])
525 .expect("cell('like', gpu prototype)");
526 expect_cell(like_result, &[2, 3]);
527 }
528
529 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
530 #[test]
531 fn cell_with_multi_dimensional_vector() {
532 let tensor = Tensor::new(vec![2.0, 3.0, 4.0], vec![1, 3]).unwrap();
533 let result = cell_builtin(vec![Value::Tensor(tensor)]).expect("cell([2 3 4])");
534 expect_cell(result, &[2, 3, 4]);
535 }
536
537 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
538 #[test]
539 fn cell_with_variadic_dimensions() {
540 let args = vec![Value::Num(2.0), Value::Num(3.0), Value::Num(5.0)];
541 let result = cell_builtin(args).expect("cell(2,3,5)");
542 expect_cell(result, &[2, 3, 5]);
543 }
544
545 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
546 #[test]
547 fn cell_with_single_element_vector_is_column() {
548 let tensor = Tensor::new(vec![4.0], vec![1, 1]).unwrap();
549 let result = cell_builtin(vec![Value::Tensor(tensor)]).expect("cell([4])");
550 expect_cell(result, &[4, 1]);
551 }
552
553 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
554 #[test]
555 fn cell_rejects_negative() {
556 let err = cell_builtin(vec![Value::Num(-1.0)])
557 .unwrap_err()
558 .to_string();
559 assert!(err.contains("non-negative"), "unexpected error: {err}");
560 }
561
562 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
563 #[test]
564 fn cell_rejects_fractional() {
565 let err = cell_builtin(vec![Value::Num(2.5)]).unwrap_err().to_string();
566 assert!(err.contains("integers"), "unexpected error: {err}");
567 }
568
569 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
570 #[test]
571 fn cell_like_infers_shape_from_prototype() {
572 let proto = Tensor::new(vec![1.0, 2.0, 3.0, 4.0], vec![2, 2]).unwrap();
573 let args = vec![Value::from("like"), Value::Tensor(proto)];
574 let result = cell_builtin(args).expect("cell('like', tensor)");
575 expect_cell(result, &[2, 2]);
576 }
577
578 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
579 #[test]
580 fn cell_like_logical_uses_logical_empty() {
581 let logical = LogicalArray::new(vec![1], vec![1, 1]).unwrap();
582 let args = vec![
583 Value::Num(2.0),
584 Value::from("like"),
585 Value::LogicalArray(logical),
586 ];
587 let result = cell_builtin(args).expect("cell(___, 'like', logical)");
588 expect_cell_with(result, &[2, 2], |element| match element {
589 Value::LogicalArray(arr) => {
590 assert!(arr.data.is_empty());
591 assert_eq!(arr.shape, vec![0, 0]);
592 }
593 other => panic!("expected logical empty, got {other:?}"),
594 });
595 }
596
597 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
598 #[test]
599 fn cell_like_cell_prototype_produces_empty_cell_elements() {
600 let proto = crate::make_cell_with_shape(Vec::new(), vec![0, 0]).unwrap();
601 let args = vec![Value::Num(1.0), Value::from("like"), proto.clone()];
602 let result = cell_builtin(args).expect("cell(1,'like',cell)");
603 expect_cell_with(result, &[1, 1], |element| match element {
604 Value::Cell(inner) => {
605 assert_eq!(inner.shape, vec![0, 0]);
606 assert_eq!(inner.data.len(), 0);
607 }
608 other => panic!("expected nested empty cell, got {other:?}"),
609 });
610 }
611
612 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
613 #[test]
614 fn cell_like_is_case_insensitive() {
615 let proto = Tensor::new(vec![1.0], vec![1, 1]).unwrap();
616 let result = cell_builtin(vec![Value::from("LIKE"), Value::Tensor(proto)])
617 .expect("cell('LIKE', ...)");
618 expect_cell(result, &[1, 1]);
619 }
620
621 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
622 #[test]
623 fn cell_like_rejects_multiple_keywords() {
624 let proto = Tensor::new(vec![1.0], vec![1, 1]).unwrap();
625 let err = cell_builtin(vec![
626 Value::Num(1.0),
627 Value::from("like"),
628 Value::Tensor(proto.clone()),
629 Value::from("like"),
630 Value::Tensor(proto),
631 ])
632 .unwrap_err()
633 .to_string();
634 assert!(err.contains("multiple 'like'"), "unexpected error: {err}");
635 }
636
637 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
638 #[test]
639 fn cell_like_gpu_prototype_falls_back_to_host() {
640 test_support::with_test_provider(|provider| {
641 let tensor = Tensor::new(vec![1.0, 2.0], vec![2, 1]).unwrap();
642 let view = runmat_accelerate_api::HostTensorView {
643 data: &tensor.data,
644 shape: &tensor.shape,
645 };
646 let handle = provider.upload(&view).expect("upload prototype");
647 let result = cell_builtin(vec![Value::from("like"), Value::GpuTensor(handle)])
648 .expect("cell('like', gpu)");
649 expect_cell(result, &[2, 1]);
650 });
651 }
652}