Skip to main content

runmat_runtime/builtins/cells/core/
cell.rs

1//! MATLAB-compatible `cell` builtin implemented for the modern RunMat runtime.
2
3use 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}