1use 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}