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