1use runmat_builtins::{
4 BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinExtensionDescriptor,
5 BuiltinExtensionMode, BuiltinIntegerBackendRule, BuiltinIntegerCapabilityDescriptor,
6 BuiltinIntegerComputationDomain, BuiltinIntegerInputAvailability,
7 BuiltinIntegerInputCapability, BuiltinIntegerOutputClassRule, BuiltinIntegerOverflowRule,
8 BuiltinIntegerOverloadKind, BuiltinIntegerScalarDoubleRule, BuiltinOutputMode,
9 BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
10};
11use runmat_macros::runtime_builtin;
12use runmat_value::{
13 CellArray, CharArray, IntValue, LogicalArray, NumericScalar, SparseTensor, StringArray,
14 SymbolicArray, Tensor, Value,
15};
16
17use crate::builtins::common::map_control_flow_with_builtin;
18use crate::builtins::common::spec::{
19 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
20 ReductionNaN, ResidencyPolicy, ShapeRequirements,
21};
22use crate::builtins::strings::type_resolvers::string_array_type;
23use crate::{build_runtime_error, gather_if_needed_async, BuiltinResult, RuntimeError};
24
25#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::strings::core::char")]
26pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
27 name: "char",
28 op_kind: GpuOpKind::Custom("conversion"),
29 supported_precisions: &[],
30 broadcast: BroadcastSemantics::None,
31 provider_hooks: &[],
32 constant_strategy: ConstantStrategy::InlineLiteral,
33 residency: ResidencyPolicy::GatherImmediately,
34 nan_mode: ReductionNaN::Include,
35 two_pass_threshold: None,
36 workgroup_size: None,
37 accepts_nan_mode: false,
38 notes: "Conversion always runs on the CPU. Interactive resident numeric input is undocumented and therefore mode-gated before the gather fallback; output is host character data.",
39};
40
41#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::strings::core::char")]
42pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
43 name: "char",
44 shape: ShapeRequirements::Any,
45 constant_strategy: ConstantStrategy::InlineLiteral,
46 elementwise: None,
47 reduction: None,
48 emits_nan: false,
49 notes: "Character materialisation runs outside of fusion; results always live on the host.",
50};
51
52const BUILTIN_NAME: &str = "char";
53const CHAR_SPARSE_DENSE_ELEMENT_LIMIT: usize = 10_000_000;
54
55pub(crate) const CHAR_RESIDENT_NUMERIC_EXTENSION: BuiltinExtensionDescriptor =
56 BuiltinExtensionDescriptor {
57 id: "char-resident-numeric-input",
58 mode: BuiltinExtensionMode::RunMatOnly,
59 description: "char with interactive resident numeric input is a RunMat extension",
60 error_identifier: Some("RunMat:compatibility:CharResidentNumericInputExtension"),
61 };
62pub(crate) const CHAR_LOGICAL_INPUT_EXTENSION: BuiltinExtensionDescriptor =
63 BuiltinExtensionDescriptor {
64 id: "char-logical-input",
65 mode: BuiltinExtensionMode::RunMatOnly,
66 description: "char with logical input is a RunMat extension",
67 error_identifier: Some("RunMat:compatibility:CharLogicalInputExtension"),
68 };
69
70pub const CHAR_EXTENSIONS: [BuiltinExtensionDescriptor; 2] = [
71 CHAR_LOGICAL_INPUT_EXTENSION,
72 CHAR_RESIDENT_NUMERIC_EXTENSION,
73];
74
75const CHAR_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
76 name: "C",
77 ty: BuiltinParamType::Any,
78 arity: BuiltinParamArity::Required,
79 default: None,
80 description: "Character array result.",
81}];
82
83const CHAR_INPUT_SINGLE: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
84 name: "X",
85 ty: BuiltinParamType::Any,
86 arity: BuiltinParamArity::Required,
87 default: None,
88 description: "Input value to convert into character data.",
89}];
90
91const CHAR_INPUT_VARIADIC: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
92 name: "X...",
93 ty: BuiltinParamType::Any,
94 arity: BuiltinParamArity::Variadic,
95 default: None,
96 description: "Multiple inputs converted row-wise and padded.",
97}];
98
99const CHAR_SIGNATURES: [BuiltinSignatureDescriptor; 3] = [
100 BuiltinSignatureDescriptor {
101 label: "C = char()",
102 inputs: &[],
103 outputs: &CHAR_OUTPUT,
104 },
105 BuiltinSignatureDescriptor {
106 label: "C = char(X)",
107 inputs: &CHAR_INPUT_SINGLE,
108 outputs: &CHAR_OUTPUT,
109 },
110 BuiltinSignatureDescriptor {
111 label: "C = char(X...)",
112 inputs: &CHAR_INPUT_VARIADIC,
113 outputs: &CHAR_OUTPUT,
114 },
115];
116
117const CHAR_ERROR_INVALID_INPUT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
118 code: "RM.CHAR.INVALID_INPUT",
119 identifier: Some("RunMat:char:InvalidInput"),
120 when: "Input type cannot be converted to character data.",
121 message: "char: invalid input",
122};
123
124const CHAR_ERROR_INVALID_CODEPOINT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
125 code: "RM.CHAR.INVALID_CODEPOINT",
126 identifier: Some("RunMat:char:InvalidCodePoint"),
127 when: "Numeric input cannot be represented by RunMat's scalar-value character storage.",
128 message: "char: numeric input cannot be represented as a RunMat character",
129};
130
131const CHAR_ERROR_DIMENSION: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
132 code: "RM.CHAR.INVALID_DIMENSION",
133 identifier: Some("RunMat:char:InvalidDimension"),
134 when: "Array inputs are not 2-D (or trailing singleton dimensions).",
135 message: "char: inputs must be 2-D",
136};
137
138const CHAR_ERROR_INTERNAL: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
139 code: "RM.CHAR.INTERNAL",
140 identifier: Some("RunMat:char:InternalError"),
141 when: "Internal character array construction failed.",
142 message: "char: internal error",
143};
144
145const CHAR_ERRORS: [BuiltinErrorDescriptor; 4] = [
146 CHAR_ERROR_INVALID_INPUT,
147 CHAR_ERROR_INVALID_CODEPOINT,
148 CHAR_ERROR_DIMENSION,
149 CHAR_ERROR_INTERNAL,
150];
151
152pub const CHAR_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
153 signatures: &CHAR_SIGNATURES,
154 output_mode: BuiltinOutputMode::Fixed,
155 completion_policy: BuiltinCompletionPolicy::Public,
156 errors: &CHAR_ERRORS,
157};
158
159const CHAR_INTEGER_INPUTS: [BuiltinIntegerInputCapability; 1] = [BuiltinIntegerInputCapability {
160 name: "X",
161 classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
162 availability: BuiltinIntegerInputAvailability::Documented,
163 scalar_double: BuiltinIntegerScalarDoubleRule::Allowed,
164 notes: "All eight integer classes are documented numeric-code inputs. Floating values truncate toward zero and all numeric codes clamp to the UTF-16 code-unit interval 0..65535.",
165}];
166
167pub const CHAR_INTEGER_CAPABILITIES: [BuiltinIntegerCapabilityDescriptor; 1] =
168 [BuiltinIntegerCapabilityDescriptor {
169 form: "C = char(integer_X) or char(X1, ..., XN)",
170 inputs: &CHAR_INTEGER_INPUTS,
171 computation_domain: BuiltinIntegerComputationDomain::ExactInteger,
172 output_class: BuiltinIntegerOutputClassRule::NotApplicable,
173 overflow: BuiltinIntegerOverflowRule::Saturate,
174 backend: BuiltinIntegerBackendRule::GatherFallback,
175 overload: BuiltinIntegerOverloadKind::Multiple,
176 notes: "Host integer values are decoded exactly, then clamped to 0..65535. RunMat CharArray stores Rust Unicode scalar values, so isolated UTF-16 surrogate code units U+D800..U+DFFF cannot yet be represented and produce an explicit error. Interactive resident numeric input is a mode-gated RunMat extension before gather.",
177 }];
178
179fn char_error(error: &'static BuiltinErrorDescriptor) -> RuntimeError {
180 char_error_with_message(error.message, error)
181}
182
183fn char_error_with_message(
184 message: impl Into<String>,
185 error: &'static BuiltinErrorDescriptor,
186) -> RuntimeError {
187 let mut builder = build_runtime_error(message).with_builtin(BUILTIN_NAME);
188 if let Some(identifier) = error.identifier {
189 builder = builder.with_identifier(identifier);
190 }
191 builder.build()
192}
193
194fn char_flow(message: impl Into<String>) -> RuntimeError {
195 char_error_with_message(message, &CHAR_ERROR_INTERNAL)
196}
197
198fn remap_char_flow(err: RuntimeError) -> RuntimeError {
199 map_control_flow_with_builtin(err, BUILTIN_NAME)
200}
201
202#[runtime_builtin(
203 name = "char",
204 category = "strings/core",
205 summary = "Convert numeric codes and text values into character arrays.",
206 keywords = "char,character,string,gpu",
207 accel = "conversion",
208 type_resolver(string_array_type),
209 descriptor(crate::builtins::strings::core::char::CHAR_DESCRIPTOR),
210 extensions(crate::builtins::strings::core::char::CHAR_EXTENSIONS),
211 integer_capabilities(crate::builtins::strings::core::char::CHAR_INTEGER_CAPABILITIES),
212 builtin_path = "crate::builtins::strings::core::char"
213)]
214async fn char_builtin(rest: Vec<Value>) -> crate::BuiltinResult<Value> {
215 if rest.is_empty() {
216 let empty =
217 CharArray::new(Vec::new(), 0, 0).map_err(|_| char_error(&CHAR_ERROR_INTERNAL))?;
218 return Ok(Value::CharArray(empty));
219 }
220
221 let mut rows: Vec<Vec<char>> = Vec::new();
222 let mut max_width = 0usize;
223
224 for arg in rest {
225 if matches!(&arg, Value::Bool(_) | Value::LogicalArray(_))
226 || matches!(&arg, Value::GpuTensor(handle) if runmat_accelerate_api::handle_is_logical(handle))
227 {
228 crate::compatibility::ensure_builtin_extension_enabled(
229 &CHAR_LOGICAL_INPUT_EXTENSION,
230 BUILTIN_NAME,
231 )?;
232 }
233 if matches!(&arg, Value::GpuTensor(_)) {
234 crate::compatibility::ensure_builtin_extension_enabled(
235 &CHAR_RESIDENT_NUMERIC_EXTENSION,
236 BUILTIN_NAME,
237 )?;
238 }
239 let gathered = gather_if_needed_async(&arg)
240 .await
241 .map_err(remap_char_flow)?;
242 let mut produced = value_to_char_rows(&gathered)?;
243 for row in &produced {
244 if row.len() > max_width {
245 max_width = row.len();
246 }
247 }
248 rows.append(&mut produced);
249 }
250
251 if rows.is_empty() {
252 let empty =
253 CharArray::new(Vec::new(), 0, 0).map_err(|_| char_error(&CHAR_ERROR_INTERNAL))?;
254 return Ok(Value::CharArray(empty));
255 }
256
257 let cols = max_width;
258 let total_rows = rows.len();
259 let mut data = vec![' '; total_rows * cols];
260 for (row_idx, row) in rows.into_iter().enumerate() {
261 for (col_idx, ch) in row.into_iter().enumerate() {
262 if col_idx < cols {
263 data[row_idx * cols + col_idx] = ch;
264 }
265 }
266 }
267
268 let array =
269 CharArray::new(data, total_rows, cols).map_err(|_| char_error(&CHAR_ERROR_INTERNAL))?;
270 Ok(Value::CharArray(array))
271}
272
273fn value_to_char_rows(value: &Value) -> BuiltinResult<Vec<Vec<char>>> {
274 if let Some(array) = crate::builtins::datetime::datetime_char_array(value)
275 .map_err(|err| char_flow(err.message().to_string()))?
276 {
277 return Ok(char_array_rows(&array));
278 }
279 if let Some(array) = crate::builtins::duration::duration_char_array(value)
280 .map_err(|err| char_flow(err.message().to_string()))?
281 {
282 return Ok(char_array_rows(&array));
283 }
284 match value {
285 Value::CharArray(ca) => Ok(char_array_rows(ca)),
286 Value::String(s) => Ok(vec![s.chars().collect()]),
287 Value::Symbolic(expr) => Ok(vec![expr.to_string().chars().collect()]),
288 Value::SymbolicArray(array) => symbolic_array_rows(array),
289 Value::StringArray(sa) => string_array_rows(sa),
290 Value::Num(n) => Ok(vec![vec![number_to_char(*n)?]]),
291 Value::Int(i) => Ok(vec![vec![integer_value_to_char(i)?]]),
292 Value::Bool(b) => {
293 let code = if *b { 1.0 } else { 0.0 };
294 Ok(vec![vec![number_to_char(code)?]])
295 }
296 Value::Tensor(t) => tensor_rows(t),
297 Value::SparseTensor(s) => {
298 ensure_sparse_dense_conversion(s)?;
299 let dense = s.to_dense().map_err(char_flow)?;
300 tensor_rows(&dense)
301 }
302 Value::LogicalArray(la) => logical_rows(la),
303 Value::Cell(ca) => cell_rows(ca),
304 Value::GpuTensor(_) => Err(char_error(&CHAR_ERROR_INVALID_INPUT)),
305 Value::Complex(_, _) | Value::ComplexTensor(_) => Err(char_error_with_message(
306 "char: complex inputs are not supported",
307 &CHAR_ERROR_INVALID_INPUT,
308 )),
309 Value::Struct(_)
310 | Value::ObjectArray(_)
311 | Value::Object(_)
312 | Value::HandleObject(_)
313 | Value::Listener(_)
314 | Value::FunctionHandle(_)
315 | Value::ExternalFunctionHandle(_)
316 | Value::MethodFunctionHandle(_)
317 | Value::BoundFunctionHandle { .. }
318 | Value::Closure(_)
319 | Value::ClassRef(_)
320 | Value::MException(_)
321 | Value::Future(_)
322 | Value::Task(_)
323 | Value::Pool(_)
324 | Value::Job(_)
325 | Value::Foreign(_)
326 | Value::OutputList(_) => Err(char_error_with_message(
327 format!("char: unsupported input type {:?}", value),
328 &CHAR_ERROR_INVALID_INPUT,
329 )),
330 }
331}
332
333fn char_array_rows(ca: &CharArray) -> Vec<Vec<char>> {
334 let mut rows = Vec::with_capacity(ca.rows);
335 for r in 0..ca.rows {
336 let mut row = Vec::with_capacity(ca.cols);
337 for c in 0..ca.cols {
338 row.push(ca.data[r * ca.cols + c]);
339 }
340 rows.push(row);
341 }
342 rows
343}
344
345fn string_array_rows(sa: &StringArray) -> BuiltinResult<Vec<Vec<char>>> {
346 ensure_two_dimensional(&sa.shape, "char")?;
347 if sa.data.is_empty() {
348 return Ok(Vec::new());
349 }
350 let mut rows = Vec::with_capacity(sa.data.len());
351 let rows_count = sa.rows();
352 let cols_count = sa.cols();
353 if rows_count == 0 || cols_count == 0 {
354 return Ok(Vec::new());
355 }
356 for c in 0..cols_count {
357 for r in 0..rows_count {
358 let idx = r + c * rows_count;
359 rows.push(sa.data[idx].chars().collect());
360 }
361 }
362 Ok(rows)
363}
364
365fn symbolic_array_rows(array: &SymbolicArray) -> BuiltinResult<Vec<Vec<char>>> {
366 ensure_two_dimensional(&array.shape, "char")?;
367 let (rows, cols) = infer_rows_cols(&array.shape, array.data.len());
368 if rows == 0 {
369 return Ok(Vec::new());
370 }
371 let mut out = Vec::with_capacity(rows);
372 for r in 0..rows {
373 let mut row = Vec::new();
374 for c in 0..cols {
375 if cols == 0 {
376 continue;
377 }
378 let idx = r + c * rows;
379 row.extend(array.data[idx].to_string().chars());
380 }
381 out.push(row);
382 }
383 Ok(out)
384}
385
386fn tensor_rows(t: &Tensor) -> BuiltinResult<Vec<Vec<char>>> {
387 ensure_two_dimensional(&t.shape, "char")?;
388 let element_len = t.len();
389 let (rows, cols) = infer_rows_cols(&t.shape, element_len);
390 if rows == 0 {
391 return Ok(Vec::new());
392 }
393 let mut out = Vec::with_capacity(rows);
394 for r in 0..rows {
395 let mut row = Vec::with_capacity(cols);
396 for c in 0..cols {
397 if cols == 0 {
398 continue;
399 }
400 let idx = r + c * rows;
401 let value = t.numeric_value_at(idx).ok_or_else(|| {
402 char_error_with_message(
403 "char: numeric storage length does not match tensor shape",
404 &CHAR_ERROR_INTERNAL,
405 )
406 })?;
407 let ch = match value {
408 NumericScalar::F64(value) => number_to_char(value)?,
409 NumericScalar::F32(value) => number_to_char(f64::from(value))?,
410 value => integer_value_to_char(
411 &value
412 .into_int_value()
413 .expect("non-floating numeric scalar is integer"),
414 )?,
415 };
416 row.push(ch);
417 }
418 out.push(row);
419 }
420 Ok(out)
421}
422
423fn logical_rows(la: &LogicalArray) -> BuiltinResult<Vec<Vec<char>>> {
424 ensure_two_dimensional(&la.shape, "char")?;
425 let (rows, cols) = infer_rows_cols(&la.shape, la.data.len());
426 if rows == 0 {
427 return Ok(Vec::new());
428 }
429 let mut out = Vec::with_capacity(rows);
430 for r in 0..rows {
431 let mut row = Vec::with_capacity(cols);
432 for c in 0..cols {
433 if cols == 0 {
434 continue;
435 }
436 let idx = r + c * rows;
437 let code = if la.data[idx] != 0 { 1.0 } else { 0.0 };
438 row.push(number_to_char(code)?);
439 }
440 out.push(row);
441 }
442 Ok(out)
443}
444
445fn cell_rows(ca: &CellArray) -> BuiltinResult<Vec<Vec<char>>> {
446 let mut rows = Vec::with_capacity(ca.data.len());
447 for ptr in &ca.data {
448 let element = (ptr).clone();
449 let mut converted = value_to_char_rows(&element)?;
450 match converted.len() {
451 0 => rows.push(Vec::new()),
452 1 => rows.push(converted.remove(0)),
453 _ => {
454 return Err(char_error_with_message(
455 "char: cell elements must be character vectors or string scalars",
456 &CHAR_ERROR_INVALID_INPUT,
457 ))
458 }
459 }
460 }
461 Ok(rows)
462}
463
464fn ensure_sparse_dense_conversion(sparse: &SparseTensor) -> BuiltinResult<()> {
465 let total_elements = sparse.rows.checked_mul(sparse.cols).ok_or_else(|| {
466 char_error_with_message(
467 "char: sparse matrix dimensions overflow",
468 &CHAR_ERROR_INVALID_INPUT,
469 )
470 })?;
471 if total_elements > CHAR_SPARSE_DENSE_ELEMENT_LIMIT {
472 return Err(char_error_with_message(
473 format!(
474 "char: cannot convert sparse tensor {}x{} with {} stored entries to dense character array ({} elements exceeds safe threshold)",
475 sparse.rows,
476 sparse.cols,
477 sparse.nnz(),
478 total_elements
479 ),
480 &CHAR_ERROR_INVALID_INPUT,
481 ));
482 }
483 Ok(())
484}
485
486fn number_to_char(value: f64) -> BuiltinResult<char> {
487 if !value.is_finite() {
488 return Err(char_error_with_message(
489 "char: numeric inputs must be finite",
490 &CHAR_ERROR_INVALID_CODEPOINT,
491 ));
492 }
493 let code_unit = value.trunc().clamp(0.0, u16::MAX as f64) as u16;
494 utf16_code_unit_to_char(code_unit)
495}
496
497fn integer_value_to_char(value: &IntValue) -> BuiltinResult<char> {
498 let code_unit = match value {
499 IntValue::I8(value) => signed_integer_to_code_unit(*value as i128),
500 IntValue::I16(value) => signed_integer_to_code_unit(*value as i128),
501 IntValue::I32(value) => signed_integer_to_code_unit(*value as i128),
502 IntValue::I64(value) => signed_integer_to_code_unit(*value as i128),
503 IntValue::U8(value) => unsigned_integer_to_code_unit(*value as u128),
504 IntValue::U16(value) => unsigned_integer_to_code_unit(*value as u128),
505 IntValue::U32(value) => unsigned_integer_to_code_unit(*value as u128),
506 IntValue::U64(value) => unsigned_integer_to_code_unit(*value as u128),
507 };
508 utf16_code_unit_to_char(code_unit)
509}
510
511fn utf16_code_unit_to_char(code_unit: u16) -> BuiltinResult<char> {
512 char::from_u32(u32::from(code_unit)).ok_or_else(|| {
513 char_error_with_message(
514 format!(
515 "char: UTF-16 surrogate code unit U+{code_unit:04X} cannot be represented by the current RunMat CharArray scalar-value storage"
516 ),
517 &CHAR_ERROR_INVALID_CODEPOINT,
518 )
519 })
520}
521
522fn signed_integer_to_code_unit(value: i128) -> u16 {
523 value.clamp(0, u16::MAX as i128) as u16
524}
525
526fn unsigned_integer_to_code_unit(value: u128) -> u16 {
527 value.min(u16::MAX as u128) as u16
528}
529
530fn ensure_two_dimensional(shape: &[usize], context: &str) -> BuiltinResult<()> {
531 if shape.len() <= 2 {
532 return Ok(());
533 }
534 if shape.iter().skip(2).all(|&d| d == 1) {
535 return Ok(());
536 }
537 Err(char_error_with_message(
538 format!("{context}: inputs must be 2-D"),
539 &CHAR_ERROR_DIMENSION,
540 ))
541}
542
543fn infer_rows_cols(shape: &[usize], len: usize) -> (usize, usize) {
544 match shape.len() {
545 0 => {
546 if len == 0 {
547 (0, 0)
548 } else {
549 (1, 1)
550 }
551 }
552 1 => (1, shape[0]),
553 2 => (shape[0], shape[1]),
554 _ => {
555 let rows = shape[0];
556 let cols = if shape.len() > 1 { shape[1] } else { 1 };
557 (rows, cols)
558 }
559 }
560}
561
562#[cfg(test)]
563pub(crate) mod tests {
564 use super::*;
565 use crate::builtins::common::test_support;
566 use runmat_builtins::{ResolveContext, Type};
567 use runmat_value::{IntegerStorage, NumericStorage, SymbolicArray, SymbolicExpr};
568
569 fn char_builtin(rest: Vec<Value>) -> BuiltinResult<Value> {
570 futures::executor::block_on(super::char_builtin(rest))
571 }
572 use runmat_value::StringArray;
573
574 fn error_message(err: crate::RuntimeError) -> String {
575 err.message().to_string()
576 }
577
578 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
579 #[test]
580 fn char_no_arguments_returns_empty() {
581 let result = char_builtin(Vec::new()).expect("char");
582 match result {
583 Value::CharArray(ca) => {
584 assert_eq!(ca.rows, 0);
585 assert_eq!(ca.cols, 0);
586 assert!(ca.data.is_empty());
587 }
588 other => panic!("expected char array, got {other:?}"),
589 }
590 }
591
592 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
593 #[test]
594 fn char_from_string_scalar() {
595 let value = Value::String("RunMat".to_string());
596 let result = char_builtin(vec![value]).expect("char");
597 match result {
598 Value::CharArray(ca) => {
599 assert_eq!(ca.rows, 1);
600 assert_eq!(ca.cols, 6);
601 assert_eq!(ca.data, "RunMat".chars().collect::<Vec<_>>());
602 }
603 other => panic!("expected char array, got {other:?}"),
604 }
605 }
606
607 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
608 #[test]
609 fn char_from_numeric_tensor() {
610 let tensor =
611 Tensor::new(vec![82.0, 85.0, 78.0, 77.0, 65.0, 84.0], vec![1, 6]).expect("tensor");
612 let result = char_builtin(vec![Value::Tensor(tensor)]).expect("char");
613 match result {
614 Value::CharArray(ca) => {
615 assert_eq!(ca.rows, 1);
616 assert_eq!(ca.cols, 6);
617 assert_eq!(ca.data, "RUNMAT".chars().collect::<Vec<_>>());
618 }
619 other => panic!("expected char array, got {other:?}"),
620 }
621 }
622
623 #[test]
624 fn char_from_native_single_tensor_reads_authoritative_storage() {
625 let tensor =
626 Tensor::from_numeric_storage(NumericStorage::F32(vec![82.0, 77.0]), vec![1, 2])
627 .expect("single tensor");
628 let result = char_builtin(vec![Value::Tensor(tensor)]).expect("char");
629 match result {
630 Value::CharArray(array) => assert_eq!(array.data, vec!['R', 'M']),
631 other => panic!("expected char array, got {other:?}"),
632 }
633 }
634
635 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
636 #[test]
637 fn char_from_typed_integer_tensor_reads_exact_storage_without_mirror() {
638 let tensor = Tensor::new_integer(IntegerStorage::U64(vec![82, u64::MAX]), vec![1, 2])
639 .expect("typed tensor");
640
641 let result = char_builtin(vec![Value::Tensor(tensor)]).expect("char");
642 match result {
643 Value::CharArray(ca) => {
644 assert_eq!(ca.rows, 1);
645 assert_eq!(ca.cols, 2);
646 assert_eq!(ca.data, vec!['R', '\u{FFFF}']);
647 }
648 other => panic!("expected char array, got {other:?}"),
649 }
650 }
651
652 #[test]
653 fn char_reads_every_integer_tensor_storage_class() {
654 for storage in [
655 IntegerStorage::I8(vec![65]),
656 IntegerStorage::I16(vec![65]),
657 IntegerStorage::I32(vec![65]),
658 IntegerStorage::I64(vec![65]),
659 IntegerStorage::U8(vec![65]),
660 IntegerStorage::U16(vec![65]),
661 IntegerStorage::U32(vec![65]),
662 IntegerStorage::U64(vec![65]),
663 ] {
664 let tensor = Tensor::new_integer(storage, vec![1, 1]).expect("typed tensor");
665 let Value::CharArray(array) = char_builtin(vec![Value::Tensor(tensor)]).expect("char")
666 else {
667 panic!("expected char array");
668 };
669 assert_eq!(array.data, vec!['A']);
670 }
671 }
672
673 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
674 #[test]
675 fn char_clamps_negative_typed_integer_storage_without_mirror() {
676 let tensor =
677 Tensor::new_integer(IntegerStorage::I16(vec![-1]), vec![1, 1]).expect("typed tensor");
678
679 let Value::CharArray(array) = char_builtin(vec![Value::Tensor(tensor)]).expect("char")
680 else {
681 panic!("expected char array");
682 };
683 assert_eq!(array.data, vec!['\0']);
684 }
685
686 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
687 #[test]
688 fn char_clamps_out_of_range_uint64_storage_without_mirror() {
689 let tensor = Tensor::new_integer(IntegerStorage::U64(vec![0x110000]), vec![1, 1])
690 .expect("typed tensor");
691
692 let Value::CharArray(array) = char_builtin(vec![Value::Tensor(tensor)]).expect("char")
693 else {
694 panic!("expected char array");
695 };
696 assert_eq!(array.data, vec!['\u{FFFF}']);
697 }
698
699 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
700 #[test]
701 fn char_from_string_array_with_padding() {
702 let data = vec!["cat".to_string(), "giraffe".to_string()];
703 let sa = StringArray::new(data, vec![2, 1]).expect("string array");
704 let result = char_builtin(vec![Value::StringArray(sa)]).expect("char from string array");
705 match result {
706 Value::CharArray(ca) => {
707 assert_eq!(ca.rows, 2);
708 assert_eq!(ca.cols, 7);
709 assert_eq!(
710 ca.data,
711 vec!['c', 'a', 't', ' ', ' ', ' ', ' ', 'g', 'i', 'r', 'a', 'f', 'f', 'e']
712 );
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_from_cell_array_of_strings() {
721 let cell = CellArray::new(
722 vec![
723 Value::from("north"),
724 Value::from("east"),
725 Value::from("west"),
726 ],
727 3,
728 1,
729 )
730 .expect("cell array");
731 let result = char_builtin(vec![Value::Cell(cell)]).expect("char");
732 match result {
733 Value::CharArray(ca) => {
734 assert_eq!(ca.rows, 3);
735 assert_eq!(ca.cols, 5);
736 assert_eq!(
737 ca.data,
738 vec!['n', 'o', 'r', 't', 'h', 'e', 'a', 's', 't', ' ', 'w', 'e', 's', 't', ' ']
739 );
740 }
741 other => panic!("expected char array, got {other:?}"),
742 }
743 }
744
745 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
746 #[test]
747 fn char_numeric_and_text_arguments_concatenate() {
748 let text = Value::String("hi".to_string());
749 let codes = Tensor::new(vec![65.0, 66.0], vec![1, 2]).expect("tensor");
750 let result = char_builtin(vec![text, Value::Tensor(codes)]).expect("char");
751 match result {
752 Value::CharArray(ca) => {
753 assert_eq!(ca.rows, 2);
754 assert_eq!(ca.cols, 2);
755 assert_eq!(ca.data, vec!['h', 'i', 'A', 'B']);
756 }
757 other => panic!("expected char array, got {other:?}"),
758 }
759 }
760
761 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
762 #[test]
763 fn char_gpu_tensor_round_trip() {
764 let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
765 test_support::with_test_provider(|provider| {
766 let tensor = Tensor::new(vec![82.0, 85.0, 78.0], vec![1, 3]).expect("tensor");
767 let view = runmat_accelerate_api::HostTensorView {
768 data: &tensor.materialize_f64(),
769 shape: &tensor.shape,
770 };
771 let handle = provider.upload(&view).expect("upload");
772 let result = char_builtin(vec![Value::GpuTensor(handle)]).expect("char");
773 match result {
774 Value::CharArray(ca) => {
775 assert_eq!(ca.rows, 1);
776 assert_eq!(ca.cols, 3);
777 assert_eq!(ca.data, vec!['R', 'U', 'N']);
778 }
779 other => panic!("expected char array, got {other:?}"),
780 }
781 });
782 }
783
784 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
785 #[test]
786 fn char_truncates_floating_numeric_toward_zero() {
787 let Value::CharArray(array) = char_builtin(vec![Value::Num(65.9)]).expect("char") else {
788 panic!("expected char array");
789 };
790 assert_eq!(array.data, vec!['A']);
791 }
792
793 #[test]
794 fn char_supports_all_integer_classes_and_reports_surrogate_gap() {
795 let values = [
796 IntValue::I8(65),
797 IntValue::I16(65),
798 IntValue::I32(65),
799 IntValue::I64(65),
800 IntValue::U8(65),
801 IntValue::U16(65),
802 IntValue::U32(65),
803 IntValue::U64(65),
804 ];
805 for value in values {
806 let Value::CharArray(array) = char_builtin(vec![Value::Int(value)]).expect("char")
807 else {
808 panic!("expected char array");
809 };
810 assert_eq!(array.data, vec!['A']);
811 }
812
813 let err = char_builtin(vec![Value::Int(IntValue::U16(0xD800))])
814 .expect_err("surrogate is not a Rust char");
815 assert_eq!(err.identifier(), Some("RunMat:char:InvalidCodePoint"));
816 assert!(err.message().contains("surrogate code unit"));
817 }
818
819 #[test]
820 fn char_logical_extension_is_ordered_and_mode_gated_before_conversion() {
821 assert_eq!(CHAR_EXTENSIONS[0].id, "char-logical-input");
822 assert_eq!(CHAR_EXTENSIONS[1].id, "char-resident-numeric-input");
823
824 let _guard = crate::compatibility::push_runmat_extensions_enabled(false);
825 let err = char_builtin(vec![Value::Bool(true)]).expect_err("logical extension gate");
826 assert_eq!(
827 err.identifier(),
828 CHAR_LOGICAL_INPUT_EXTENSION.error_identifier
829 );
830 }
831
832 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
833 #[test]
834 fn char_rejects_high_dimension_tensor() {
835 let tensor =
836 Tensor::new(vec![65.0, 66.0], vec![1, 1, 2]).expect("tensor construction failed");
837 let err = error_message(
838 char_builtin(vec![Value::Tensor(tensor)]).expect_err("should reject >2D tensor"),
839 );
840 assert!(err.contains("2-D"), "expected dimension error, got {err}");
841 }
842
843 #[test]
844 fn char_rejects_oversized_sparse_tensor_before_densifying() {
845 let sparse = SparseTensor::zeros(CHAR_SPARSE_DENSE_ELEMENT_LIMIT + 1, 1);
846 let err = char_builtin(vec![Value::SparseTensor(sparse)]).unwrap_err();
847
848 assert_eq!(err.identifier(), Some("RunMat:char:InvalidInput"));
849 assert!(err.message().contains("exceeds safe threshold"));
850 }
851
852 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
853 #[test]
854 fn char_string_array_column_major_order() {
855 let data = vec![
856 "c0r0".to_string(),
857 "c0r1".to_string(),
858 "c1r0".to_string(),
859 "c1r1".to_string(),
860 ];
861 let sa = StringArray::new(data, vec![2, 2]).expect("string array");
862 let result = char_builtin(vec![Value::StringArray(sa)]).expect("char");
863 match result {
864 Value::CharArray(ca) => {
865 assert_eq!(ca.rows, 4);
866 assert_eq!(ca.cols, 4);
867 assert_eq!(ca.data, "c0r0c0r1c1r0c1r1".chars().collect::<Vec<char>>());
868 }
869 other => panic!("expected char array, got {other:?}"),
870 }
871 }
872
873 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
874 #[test]
875 fn char_symbolic_array_preserves_matrix_rows() {
876 let array = SymbolicArray::new(
877 vec![
878 SymbolicExpr::variable("x"),
879 SymbolicExpr::variable("z"),
880 SymbolicExpr::variable("y"),
881 SymbolicExpr::variable("w"),
882 ],
883 vec![2, 2],
884 )
885 .expect("symbolic array");
886
887 let result = char_builtin(vec![Value::SymbolicArray(array)]).expect("char");
888
889 match result {
890 Value::CharArray(ca) => {
891 assert_eq!(ca.rows, 2);
892 assert_eq!(ca.cols, 2);
893 assert_eq!(ca.data, vec!['x', 'y', 'z', 'w']);
894 }
895 other => panic!("expected char array, got {other:?}"),
896 }
897 }
898
899 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
900 #[test]
901 fn char_symbolic_array_pads_multi_character_rows() {
902 let array = SymbolicArray::new(
903 vec![
904 SymbolicExpr::variable("x1"),
905 SymbolicExpr::variable("y"),
906 SymbolicExpr::variable("theta"),
907 SymbolicExpr::variable("z"),
908 ],
909 vec![2, 2],
910 )
911 .expect("symbolic array");
912
913 let result = char_builtin(vec![Value::SymbolicArray(array)]).expect("char");
914
915 match result {
916 Value::CharArray(ca) => {
917 assert_eq!(ca.rows, 2);
918 assert_eq!(ca.cols, 7);
919 assert_eq!(ca.data.iter().collect::<String>(), "x1thetayz ");
920 }
921 other => panic!("expected char array, got {other:?}"),
922 }
923 }
924
925 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
926 #[test]
927 fn char_symbolic_one_dimensional_array_is_row_vector() {
928 let array = SymbolicArray::new(
929 vec![SymbolicExpr::variable("x"), SymbolicExpr::variable("y")],
930 vec![2],
931 )
932 .expect("symbolic array");
933
934 let result = char_builtin(vec![Value::SymbolicArray(array)]).expect("char");
935
936 match result {
937 Value::CharArray(ca) => {
938 assert_eq!(ca.rows, 1);
939 assert_eq!(ca.cols, 2);
940 assert_eq!(ca.data, vec!['x', 'y']);
941 }
942 other => panic!("expected char array, got {other:?}"),
943 }
944 }
945
946 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
947 #[test]
948 fn char_rejects_high_dimension_string_array() {
949 let sa = StringArray::new(vec!["a".to_string(), "b".to_string()], vec![1, 1, 2])
950 .expect("string array");
951 let err = error_message(
952 char_builtin(vec![Value::StringArray(sa)]).expect_err("should reject >2D string array"),
953 );
954 assert!(err.contains("2-D"), "expected dimension error, got {err}");
955 }
956
957 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
958 #[test]
959 fn char_rejects_complex_input() {
960 let err =
961 error_message(char_builtin(vec![Value::Complex(1.0, 2.0)]).expect_err("complex input"));
962 assert!(
963 err.contains("complex"),
964 "expected complex error message, got {err}"
965 );
966 }
967
968 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
969 #[test]
970 #[cfg(feature = "wgpu")]
971 fn char_wgpu_numeric_codes_matches_cpu() {
972 use runmat_accelerate::backend::wgpu::provider::{
973 register_wgpu_provider, WgpuProviderOptions,
974 };
975
976 let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
977 let _ = register_wgpu_provider(WgpuProviderOptions::default());
978
979 let tensor = Tensor::new(vec![82.0, 85.0, 78.0], vec![1, 3]).unwrap();
980 let cpu = char_builtin(vec![Value::Tensor(tensor.clone())]).expect("char cpu");
981
982 let view = runmat_accelerate_api::HostTensorView {
983 data: &tensor.materialize_f64(),
984 shape: &tensor.shape,
985 };
986 let handle = runmat_accelerate_api::provider()
987 .expect("wgpu provider")
988 .upload(&view)
989 .expect("upload");
990 let gpu = char_builtin(vec![Value::GpuTensor(handle)]).expect("char gpu");
991
992 match (cpu, gpu) {
993 (Value::CharArray(expected), Value::CharArray(actual)) => {
994 assert_eq!(actual, expected);
995 }
996 other => panic!("unexpected results {other:?}"),
997 }
998 }
999
1000 #[test]
1001 fn char_type_is_string_array() {
1002 assert_eq!(
1003 string_array_type(&[Type::Num], &ResolveContext::new(Vec::new())),
1004 Type::cell_of(Type::String)
1005 );
1006 }
1007}