Skip to main content

runmat_runtime/builtins/strings/core/
char.rs

1//! MATLAB-compatible `char` builtin with GPU-aware conversion semantics for RunMat.
2
3use runmat_builtins::{
4    BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
5    BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
6    CellArray, CharArray, LogicalArray, StringArray, Tensor, Value,
7};
8use runmat_macros::runtime_builtin;
9
10use crate::builtins::common::map_control_flow_with_builtin;
11use crate::builtins::common::spec::{
12    BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
13    ReductionNaN, ResidencyPolicy, ShapeRequirements,
14};
15use crate::builtins::strings::type_resolvers::string_array_type;
16use crate::{build_runtime_error, gather_if_needed_async, BuiltinResult, RuntimeError};
17
18#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::strings::core::char")]
19pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
20    name: "char",
21    op_kind: GpuOpKind::Custom("conversion"),
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:
32        "Conversion always runs on the CPU; GPU tensors are gathered before building the result.",
33};
34
35#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::strings::core::char")]
36pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
37    name: "char",
38    shape: ShapeRequirements::Any,
39    constant_strategy: ConstantStrategy::InlineLiteral,
40    elementwise: None,
41    reduction: None,
42    emits_nan: false,
43    notes: "Character materialisation runs outside of fusion; results always live on the host.",
44};
45
46const BUILTIN_NAME: &str = "char";
47
48const CHAR_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
49    name: "C",
50    ty: BuiltinParamType::Any,
51    arity: BuiltinParamArity::Required,
52    default: None,
53    description: "Character array result.",
54}];
55
56const CHAR_INPUT_SINGLE: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
57    name: "X",
58    ty: BuiltinParamType::Any,
59    arity: BuiltinParamArity::Required,
60    default: None,
61    description: "Input value to convert into character data.",
62}];
63
64const CHAR_INPUT_VARIADIC: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
65    name: "X...",
66    ty: BuiltinParamType::Any,
67    arity: BuiltinParamArity::Variadic,
68    default: None,
69    description: "Multiple inputs converted row-wise and padded.",
70}];
71
72const CHAR_SIGNATURES: [BuiltinSignatureDescriptor; 3] = [
73    BuiltinSignatureDescriptor {
74        label: "C = char()",
75        inputs: &[],
76        outputs: &CHAR_OUTPUT,
77    },
78    BuiltinSignatureDescriptor {
79        label: "C = char(X)",
80        inputs: &CHAR_INPUT_SINGLE,
81        outputs: &CHAR_OUTPUT,
82    },
83    BuiltinSignatureDescriptor {
84        label: "C = char(X...)",
85        inputs: &CHAR_INPUT_VARIADIC,
86        outputs: &CHAR_OUTPUT,
87    },
88];
89
90const CHAR_ERROR_INVALID_INPUT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
91    code: "RM.CHAR.INVALID_INPUT",
92    identifier: Some("RunMat:char:InvalidInput"),
93    when: "Input type cannot be converted to character data.",
94    message: "char: invalid input",
95};
96
97const CHAR_ERROR_INVALID_CODEPOINT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
98    code: "RM.CHAR.INVALID_CODEPOINT",
99    identifier: Some("RunMat:char:InvalidCodePoint"),
100    when: "Numeric input is not a finite integer Unicode code point.",
101    message: "char: numeric inputs must be finite Unicode code points",
102};
103
104const CHAR_ERROR_DIMENSION: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
105    code: "RM.CHAR.INVALID_DIMENSION",
106    identifier: Some("RunMat:char:InvalidDimension"),
107    when: "Array inputs are not 2-D (or trailing singleton dimensions).",
108    message: "char: inputs must be 2-D",
109};
110
111const CHAR_ERROR_INTERNAL: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
112    code: "RM.CHAR.INTERNAL",
113    identifier: Some("RunMat:char:InternalError"),
114    when: "Internal character array construction failed.",
115    message: "char: internal error",
116};
117
118const CHAR_ERRORS: [BuiltinErrorDescriptor; 4] = [
119    CHAR_ERROR_INVALID_INPUT,
120    CHAR_ERROR_INVALID_CODEPOINT,
121    CHAR_ERROR_DIMENSION,
122    CHAR_ERROR_INTERNAL,
123];
124
125pub const CHAR_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
126    signatures: &CHAR_SIGNATURES,
127    output_mode: BuiltinOutputMode::Fixed,
128    completion_policy: BuiltinCompletionPolicy::Public,
129    errors: &CHAR_ERRORS,
130};
131
132fn char_error(error: &'static BuiltinErrorDescriptor) -> RuntimeError {
133    char_error_with_message(error.message, error)
134}
135
136fn char_error_with_message(
137    message: impl Into<String>,
138    error: &'static BuiltinErrorDescriptor,
139) -> RuntimeError {
140    let mut builder = build_runtime_error(message).with_builtin(BUILTIN_NAME);
141    if let Some(identifier) = error.identifier {
142        builder = builder.with_identifier(identifier);
143    }
144    builder.build()
145}
146
147fn char_flow(message: impl Into<String>) -> RuntimeError {
148    char_error_with_message(message, &CHAR_ERROR_INTERNAL)
149}
150
151fn remap_char_flow(err: RuntimeError) -> RuntimeError {
152    map_control_flow_with_builtin(err, BUILTIN_NAME)
153}
154
155#[runtime_builtin(
156    name = "char",
157    category = "strings/core",
158    summary = "Convert numeric codes and text values into character arrays.",
159    keywords = "char,character,string,gpu",
160    accel = "conversion",
161    type_resolver(string_array_type),
162    descriptor(crate::builtins::strings::core::char::CHAR_DESCRIPTOR),
163    builtin_path = "crate::builtins::strings::core::char"
164)]
165async fn char_builtin(rest: Vec<Value>) -> crate::BuiltinResult<Value> {
166    if rest.is_empty() {
167        let empty =
168            CharArray::new(Vec::new(), 0, 0).map_err(|_| char_error(&CHAR_ERROR_INTERNAL))?;
169        return Ok(Value::CharArray(empty));
170    }
171
172    let mut rows: Vec<Vec<char>> = Vec::new();
173    let mut max_width = 0usize;
174
175    for arg in rest {
176        let gathered = gather_if_needed_async(&arg)
177            .await
178            .map_err(remap_char_flow)?;
179        let mut produced = value_to_char_rows(&gathered)?;
180        for row in &produced {
181            if row.len() > max_width {
182                max_width = row.len();
183            }
184        }
185        rows.append(&mut produced);
186    }
187
188    if rows.is_empty() {
189        let empty =
190            CharArray::new(Vec::new(), 0, 0).map_err(|_| char_error(&CHAR_ERROR_INTERNAL))?;
191        return Ok(Value::CharArray(empty));
192    }
193
194    let cols = max_width;
195    let total_rows = rows.len();
196    let mut data = vec![' '; total_rows * cols];
197    for (row_idx, row) in rows.into_iter().enumerate() {
198        for (col_idx, ch) in row.into_iter().enumerate() {
199            if col_idx < cols {
200                data[row_idx * cols + col_idx] = ch;
201            }
202        }
203    }
204
205    let array =
206        CharArray::new(data, total_rows, cols).map_err(|_| char_error(&CHAR_ERROR_INTERNAL))?;
207    Ok(Value::CharArray(array))
208}
209
210fn value_to_char_rows(value: &Value) -> BuiltinResult<Vec<Vec<char>>> {
211    if let Some(array) = crate::builtins::datetime::datetime_char_array(value)
212        .map_err(|err| char_flow(err.message().to_string()))?
213    {
214        return Ok(char_array_rows(&array));
215    }
216    if let Some(array) = crate::builtins::duration::duration_char_array(value)
217        .map_err(|err| char_flow(err.message().to_string()))?
218    {
219        return Ok(char_array_rows(&array));
220    }
221    match value {
222        Value::CharArray(ca) => Ok(char_array_rows(ca)),
223        Value::String(s) => Ok(vec![s.chars().collect()]),
224        Value::StringArray(sa) => string_array_rows(sa),
225        Value::Num(n) => Ok(vec![vec![number_to_char(*n)?]]),
226        Value::Int(i) => {
227            let as_double = i.to_f64();
228            Ok(vec![vec![number_to_char(as_double)?]])
229        }
230        Value::Bool(b) => {
231            let code = if *b { 1.0 } else { 0.0 };
232            Ok(vec![vec![number_to_char(code)?]])
233        }
234        Value::Tensor(t) => tensor_rows(t),
235        Value::LogicalArray(la) => logical_rows(la),
236        Value::Cell(ca) => cell_rows(ca),
237        Value::GpuTensor(_) => Err(char_error(&CHAR_ERROR_INVALID_INPUT)),
238        Value::Complex(_, _) | Value::ComplexTensor(_) => Err(char_error_with_message(
239            "char: complex inputs are not supported",
240            &CHAR_ERROR_INVALID_INPUT,
241        )),
242        Value::Struct(_)
243        | Value::Object(_)
244        | Value::HandleObject(_)
245        | Value::Listener(_)
246        | Value::FunctionHandle(_)
247        | Value::ExternalFunctionHandle(_)
248        | Value::MethodFunctionHandle(_)
249        | Value::BoundFunctionHandle { .. }
250        | Value::Closure(_)
251        | Value::ClassRef(_)
252        | Value::MException(_)
253        | Value::OutputList(_) => Err(char_error_with_message(
254            format!("char: unsupported input type {:?}", value),
255            &CHAR_ERROR_INVALID_INPUT,
256        )),
257    }
258}
259
260fn char_array_rows(ca: &CharArray) -> Vec<Vec<char>> {
261    let mut rows = Vec::with_capacity(ca.rows);
262    for r in 0..ca.rows {
263        let mut row = Vec::with_capacity(ca.cols);
264        for c in 0..ca.cols {
265            row.push(ca.data[r * ca.cols + c]);
266        }
267        rows.push(row);
268    }
269    rows
270}
271
272fn string_array_rows(sa: &StringArray) -> BuiltinResult<Vec<Vec<char>>> {
273    ensure_two_dimensional(&sa.shape, "char")?;
274    if sa.data.is_empty() {
275        return Ok(Vec::new());
276    }
277    let mut rows = Vec::with_capacity(sa.data.len());
278    let rows_count = sa.rows();
279    let cols_count = sa.cols();
280    if rows_count == 0 || cols_count == 0 {
281        return Ok(Vec::new());
282    }
283    for c in 0..cols_count {
284        for r in 0..rows_count {
285            let idx = r + c * rows_count;
286            rows.push(sa.data[idx].chars().collect());
287        }
288    }
289    Ok(rows)
290}
291
292fn tensor_rows(t: &Tensor) -> BuiltinResult<Vec<Vec<char>>> {
293    ensure_two_dimensional(&t.shape, "char")?;
294    let (rows, cols) = infer_rows_cols(&t.shape, t.data.len());
295    if rows == 0 {
296        return Ok(Vec::new());
297    }
298    let mut out = Vec::with_capacity(rows);
299    for r in 0..rows {
300        let mut row = Vec::with_capacity(cols);
301        for c in 0..cols {
302            if cols == 0 {
303                continue;
304            }
305            let idx = r + c * rows;
306            let value = t.data[idx];
307            row.push(number_to_char(value)?);
308        }
309        out.push(row);
310    }
311    Ok(out)
312}
313
314fn logical_rows(la: &LogicalArray) -> BuiltinResult<Vec<Vec<char>>> {
315    ensure_two_dimensional(&la.shape, "char")?;
316    let (rows, cols) = infer_rows_cols(&la.shape, la.data.len());
317    if rows == 0 {
318        return Ok(Vec::new());
319    }
320    let mut out = Vec::with_capacity(rows);
321    for r in 0..rows {
322        let mut row = Vec::with_capacity(cols);
323        for c in 0..cols {
324            if cols == 0 {
325                continue;
326            }
327            let idx = r + c * rows;
328            let code = if la.data[idx] != 0 { 1.0 } else { 0.0 };
329            row.push(number_to_char(code)?);
330        }
331        out.push(row);
332    }
333    Ok(out)
334}
335
336fn cell_rows(ca: &CellArray) -> BuiltinResult<Vec<Vec<char>>> {
337    let mut rows = Vec::with_capacity(ca.data.len());
338    for ptr in &ca.data {
339        let element = (**ptr).clone();
340        let mut converted = value_to_char_rows(&element)?;
341        match converted.len() {
342            0 => rows.push(Vec::new()),
343            1 => rows.push(converted.remove(0)),
344            _ => {
345                return Err(char_error_with_message(
346                    "char: cell elements must be character vectors or string scalars",
347                    &CHAR_ERROR_INVALID_INPUT,
348                ))
349            }
350        }
351    }
352    Ok(rows)
353}
354
355fn number_to_char(value: f64) -> BuiltinResult<char> {
356    if !value.is_finite() {
357        return Err(char_error_with_message(
358            "char: numeric inputs must be finite",
359            &CHAR_ERROR_INVALID_CODEPOINT,
360        ));
361    }
362    let rounded = value.round();
363    if (value - rounded).abs() > 1e-9 {
364        return Err(char_error_with_message(
365            format!("char: numeric inputs must be integers in the Unicode range (got {value})"),
366            &CHAR_ERROR_INVALID_CODEPOINT,
367        ));
368    }
369    if rounded < 0.0 {
370        return Err(char_error_with_message(
371            format!("char: negative code points are invalid (got {rounded})"),
372            &CHAR_ERROR_INVALID_CODEPOINT,
373        ));
374    }
375    if rounded > 0x10FFFF as f64 {
376        return Err(char_error_with_message(
377            format!("char: code point {} exceeds Unicode range", rounded as u64),
378            &CHAR_ERROR_INVALID_CODEPOINT,
379        ));
380    }
381    let code = rounded as u32;
382    char::from_u32(code).ok_or_else(|| {
383        char_error_with_message(
384            format!("char: invalid code point {code}"),
385            &CHAR_ERROR_INVALID_CODEPOINT,
386        )
387    })
388}
389
390fn ensure_two_dimensional(shape: &[usize], context: &str) -> BuiltinResult<()> {
391    if shape.len() <= 2 {
392        return Ok(());
393    }
394    if shape.iter().skip(2).all(|&d| d == 1) {
395        return Ok(());
396    }
397    Err(char_error_with_message(
398        format!("{context}: inputs must be 2-D"),
399        &CHAR_ERROR_DIMENSION,
400    ))
401}
402
403fn infer_rows_cols(shape: &[usize], len: usize) -> (usize, usize) {
404    match shape.len() {
405        0 => {
406            if len == 0 {
407                (0, 0)
408            } else {
409                (1, 1)
410            }
411        }
412        1 => (1, shape[0]),
413        2 => (shape[0], shape[1]),
414        _ => {
415            let rows = shape[0];
416            let cols = if shape.len() > 1 { shape[1] } else { 1 };
417            (rows, cols)
418        }
419    }
420}
421
422#[cfg(test)]
423pub(crate) mod tests {
424    use super::*;
425    use crate::builtins::common::test_support;
426    use runmat_builtins::{ResolveContext, Type};
427
428    fn char_builtin(rest: Vec<Value>) -> BuiltinResult<Value> {
429        futures::executor::block_on(super::char_builtin(rest))
430    }
431    use runmat_builtins::StringArray;
432
433    fn error_message(err: crate::RuntimeError) -> String {
434        err.message().to_string()
435    }
436
437    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
438    #[test]
439    fn char_no_arguments_returns_empty() {
440        let result = char_builtin(Vec::new()).expect("char");
441        match result {
442            Value::CharArray(ca) => {
443                assert_eq!(ca.rows, 0);
444                assert_eq!(ca.cols, 0);
445                assert!(ca.data.is_empty());
446            }
447            other => panic!("expected char array, got {other:?}"),
448        }
449    }
450
451    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
452    #[test]
453    fn char_from_string_scalar() {
454        let value = Value::String("RunMat".to_string());
455        let result = char_builtin(vec![value]).expect("char");
456        match result {
457            Value::CharArray(ca) => {
458                assert_eq!(ca.rows, 1);
459                assert_eq!(ca.cols, 6);
460                assert_eq!(ca.data, "RunMat".chars().collect::<Vec<_>>());
461            }
462            other => panic!("expected char array, got {other:?}"),
463        }
464    }
465
466    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
467    #[test]
468    fn char_from_numeric_tensor() {
469        let tensor =
470            Tensor::new(vec![82.0, 85.0, 78.0, 77.0, 65.0, 84.0], vec![1, 6]).expect("tensor");
471        let result = char_builtin(vec![Value::Tensor(tensor)]).expect("char");
472        match result {
473            Value::CharArray(ca) => {
474                assert_eq!(ca.rows, 1);
475                assert_eq!(ca.cols, 6);
476                assert_eq!(ca.data, "RUNMAT".chars().collect::<Vec<_>>());
477            }
478            other => panic!("expected char array, got {other:?}"),
479        }
480    }
481
482    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
483    #[test]
484    fn char_from_string_array_with_padding() {
485        let data = vec!["cat".to_string(), "giraffe".to_string()];
486        let sa = StringArray::new(data, vec![2, 1]).expect("string array");
487        let result = char_builtin(vec![Value::StringArray(sa)]).expect("char from string array");
488        match result {
489            Value::CharArray(ca) => {
490                assert_eq!(ca.rows, 2);
491                assert_eq!(ca.cols, 7);
492                assert_eq!(
493                    ca.data,
494                    vec!['c', 'a', 't', ' ', ' ', ' ', ' ', 'g', 'i', 'r', 'a', 'f', 'f', 'e']
495                );
496            }
497            other => panic!("expected char array, got {other:?}"),
498        }
499    }
500
501    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
502    #[test]
503    fn char_from_cell_array_of_strings() {
504        let cell = CellArray::new(
505            vec![
506                Value::from("north"),
507                Value::from("east"),
508                Value::from("west"),
509            ],
510            3,
511            1,
512        )
513        .expect("cell array");
514        let result = char_builtin(vec![Value::Cell(cell)]).expect("char");
515        match result {
516            Value::CharArray(ca) => {
517                assert_eq!(ca.rows, 3);
518                assert_eq!(ca.cols, 5);
519                assert_eq!(
520                    ca.data,
521                    vec!['n', 'o', 'r', 't', 'h', 'e', 'a', 's', 't', ' ', 'w', 'e', 's', 't', ' ']
522                );
523            }
524            other => panic!("expected char array, got {other:?}"),
525        }
526    }
527
528    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
529    #[test]
530    fn char_numeric_and_text_arguments_concatenate() {
531        let text = Value::String("hi".to_string());
532        let codes = Tensor::new(vec![65.0, 66.0], vec![1, 2]).expect("tensor");
533        let result = char_builtin(vec![text, Value::Tensor(codes)]).expect("char");
534        match result {
535            Value::CharArray(ca) => {
536                assert_eq!(ca.rows, 2);
537                assert_eq!(ca.cols, 2);
538                assert_eq!(ca.data, vec!['h', 'i', 'A', 'B']);
539            }
540            other => panic!("expected char array, got {other:?}"),
541        }
542    }
543
544    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
545    #[test]
546    fn char_gpu_tensor_round_trip() {
547        test_support::with_test_provider(|provider| {
548            let tensor = Tensor::new(vec![82.0, 85.0, 78.0], vec![1, 3]).expect("tensor");
549            let view = runmat_accelerate_api::HostTensorView {
550                data: &tensor.data,
551                shape: &tensor.shape,
552            };
553            let handle = provider.upload(&view).expect("upload");
554            let result = char_builtin(vec![Value::GpuTensor(handle)]).expect("char");
555            match result {
556                Value::CharArray(ca) => {
557                    assert_eq!(ca.rows, 1);
558                    assert_eq!(ca.cols, 3);
559                    assert_eq!(ca.data, vec!['R', 'U', 'N']);
560                }
561                other => panic!("expected char array, got {other:?}"),
562            }
563        });
564    }
565
566    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
567    #[test]
568    fn char_rejects_non_integer_numeric() {
569        let err =
570            error_message(char_builtin(vec![Value::Num(65.5)]).expect_err("non-integer numeric"));
571        assert!(err.contains("integers"), "unexpected error message: {err}");
572    }
573
574    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
575    #[test]
576    fn char_rejects_high_dimension_tensor() {
577        let tensor =
578            Tensor::new(vec![65.0, 66.0], vec![1, 1, 2]).expect("tensor construction failed");
579        let err = error_message(
580            char_builtin(vec![Value::Tensor(tensor)]).expect_err("should reject >2D tensor"),
581        );
582        assert!(err.contains("2-D"), "expected dimension error, got {err}");
583    }
584
585    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
586    #[test]
587    fn char_string_array_column_major_order() {
588        let data = vec![
589            "c0r0".to_string(),
590            "c0r1".to_string(),
591            "c1r0".to_string(),
592            "c1r1".to_string(),
593        ];
594        let sa = StringArray::new(data, vec![2, 2]).expect("string array");
595        let result = char_builtin(vec![Value::StringArray(sa)]).expect("char");
596        match result {
597            Value::CharArray(ca) => {
598                assert_eq!(ca.rows, 4);
599                assert_eq!(ca.cols, 4);
600                assert_eq!(ca.data, "c0r0c0r1c1r0c1r1".chars().collect::<Vec<char>>());
601            }
602            other => panic!("expected char array, got {other:?}"),
603        }
604    }
605
606    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
607    #[test]
608    fn char_rejects_high_dimension_string_array() {
609        let sa = StringArray::new(vec!["a".to_string(), "b".to_string()], vec![1, 1, 2])
610            .expect("string array");
611        let err = error_message(
612            char_builtin(vec![Value::StringArray(sa)]).expect_err("should reject >2D string array"),
613        );
614        assert!(err.contains("2-D"), "expected dimension error, got {err}");
615    }
616
617    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
618    #[test]
619    fn char_rejects_complex_input() {
620        let err =
621            error_message(char_builtin(vec![Value::Complex(1.0, 2.0)]).expect_err("complex input"));
622        assert!(
623            err.contains("complex"),
624            "expected complex error message, got {err}"
625        );
626    }
627
628    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
629    #[test]
630    #[cfg(feature = "wgpu")]
631    fn char_wgpu_numeric_codes_matches_cpu() {
632        use runmat_accelerate::backend::wgpu::provider::{
633            register_wgpu_provider, WgpuProviderOptions,
634        };
635
636        let _ = register_wgpu_provider(WgpuProviderOptions::default());
637
638        let tensor = Tensor::new(vec![82.0, 85.0, 78.0], vec![1, 3]).unwrap();
639        let cpu = char_builtin(vec![Value::Tensor(tensor.clone())]).expect("char cpu");
640
641        let view = runmat_accelerate_api::HostTensorView {
642            data: &tensor.data,
643            shape: &tensor.shape,
644        };
645        let handle = runmat_accelerate_api::provider()
646            .expect("wgpu provider")
647            .upload(&view)
648            .expect("upload");
649        let gpu = char_builtin(vec![Value::GpuTensor(handle)]).expect("char gpu");
650
651        match (cpu, gpu) {
652            (Value::CharArray(expected), Value::CharArray(actual)) => {
653                assert_eq!(actual, expected);
654            }
655            other => panic!("unexpected results {other:?}"),
656        }
657    }
658
659    #[test]
660    fn char_type_is_string_array() {
661        assert_eq!(
662            string_array_type(&[Type::Num], &ResolveContext::new(Vec::new())),
663            Type::cell_of(Type::String)
664        );
665    }
666}