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::{CharArray, IntValue, IntegerStorage, Tensor, Value};
13
14use crate::builtins::common::gpu_helpers;
15use crate::builtins::common::map_control_flow_with_builtin;
16use crate::builtins::common::spec::{
17 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
18 ReductionNaN, ResidencyPolicy, ShapeRequirements,
19};
20use crate::builtins::common::tensor;
21use crate::builtins::strings::type_resolvers::string_scalar_type;
22use crate::{build_runtime_error, gather_if_needed_async, BuiltinResult, RuntimeError};
23
24const BUILTIN_NAME: &str = "int2str";
25
26const INT2STR_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
27 name: "chr",
28 ty: BuiltinParamType::Any,
29 arity: BuiltinParamArity::Required,
30 default: None,
31 description: "Character array containing the rounded integer text.",
32}];
33
34const INT2STR_INPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
35 name: "N",
36 ty: BuiltinParamType::Any,
37 arity: BuiltinParamArity::Required,
38 default: None,
39 description: "Numeric or logical scalar, vector, or matrix input.",
40}];
41
42const INT2STR_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
43 label: "chr = int2str(N)",
44 inputs: &INT2STR_INPUT,
45 outputs: &INT2STR_OUTPUT,
46}];
47
48const INT2STR_ERROR_INVALID_INPUT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
49 code: "RM.INT2STR.INVALID_INPUT",
50 identifier: Some("RunMat:int2str:InvalidInput"),
51 when: "Input value is not a supported numeric/logical scalar, vector, or matrix.",
52 message: "int2str: unsupported input type",
53};
54
55const INT2STR_ERROR_INVALID_OPTION: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
56 code: "RM.INT2STR.INVALID_OPTION",
57 identifier: Some("RunMat:int2str:InvalidOption"),
58 when: "Arguments are malformed or too many were supplied.",
59 message: "int2str: invalid option arguments",
60};
61
62const INT2STR_ERROR_INTERNAL: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
63 code: "RM.INT2STR.INTERNAL",
64 identifier: Some("RunMat:int2str:InternalError"),
65 when: "Internal char-array assembly failed.",
66 message: "int2str: internal error",
67};
68
69const INT2STR_ERRORS: [BuiltinErrorDescriptor; 3] = [
70 INT2STR_ERROR_INVALID_INPUT,
71 INT2STR_ERROR_INVALID_OPTION,
72 INT2STR_ERROR_INTERNAL,
73];
74
75pub const INT2STR_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
76 signatures: &INT2STR_SIGNATURES,
77 output_mode: BuiltinOutputMode::Fixed,
78 completion_policy: BuiltinCompletionPolicy::Public,
79 errors: &INT2STR_ERRORS,
80};
81
82const INT2STR_LOGICAL_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
83 id: "int2str-logical-input",
84 mode: BuiltinExtensionMode::RunMatOnly,
85 description: "int2str with logical input is a RunMat extension",
86 error_identifier: Some("RunMat:compatibility:Int2strLogicalInputExtension"),
87};
88pub const INT2STR_EXTENSIONS: [BuiltinExtensionDescriptor; 1] = [INT2STR_LOGICAL_EXTENSION];
89
90const INT2STR_INTEGER_INPUTS: [BuiltinIntegerInputCapability; 1] =
91 [BuiltinIntegerInputCapability {
92 name: "N",
93 classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
94 availability: BuiltinIntegerInputAvailability::Documented,
95 scalar_double: BuiltinIntegerScalarDoubleRule::NotApplicable,
96 notes: "Every integer class is formatted directly from authoritative storage, including wide values above flintmax.",
97 }];
98pub const INT2STR_INTEGER_CAPABILITIES: [BuiltinIntegerCapabilityDescriptor; 1] =
99 [BuiltinIntegerCapabilityDescriptor {
100 form: "chr = int2str(integer_N)",
101 inputs: &INT2STR_INTEGER_INPUTS,
102 computation_domain: BuiltinIntegerComputationDomain::ExactInteger,
103 output_class: BuiltinIntegerOutputClassRule::FunctionSpecific,
104 overflow: BuiltinIntegerOverflowRule::NotApplicable,
105 backend: BuiltinIntegerBackendRule::GatherFallback,
106 overload: BuiltinIntegerOverloadKind::FunctionSpecific,
107 notes: "Integer text is exact and the result is a host character array. Documented gpuArray input is downloaded through its owner because int2str does not execute on the GPU.",
108 }];
109
110fn int2str_error(error: &'static BuiltinErrorDescriptor) -> RuntimeError {
111 int2str_error_with_message(error.message, error)
112}
113
114fn int2str_error_with_message(
115 message: impl Into<String>,
116 error: &'static BuiltinErrorDescriptor,
117) -> RuntimeError {
118 let mut builder = build_runtime_error(message).with_builtin(BUILTIN_NAME);
119 if let Some(identifier) = error.identifier {
120 builder = builder.with_identifier(identifier);
121 }
122 builder.build()
123}
124
125fn remap_int2str_flow(err: RuntimeError) -> RuntimeError {
126 map_control_flow_with_builtin(err, BUILTIN_NAME)
127}
128
129#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::strings::core::int2str")]
130pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
131 name: "int2str",
132 op_kind: GpuOpKind::Custom("conversion"),
133 supported_precisions: &[],
134 broadcast: BroadcastSemantics::None,
135 provider_hooks: &[],
136 constant_strategy: ConstantStrategy::InlineLiteral,
137 residency: ResidencyPolicy::GatherImmediately,
138 nan_mode: ReductionNaN::Include,
139 two_pass_threshold: None,
140 workgroup_size: None,
141 accepts_nan_mode: false,
142 notes: "Accepts GPU inputs but gathers them to host memory before rounding and formatting.",
143};
144
145#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::strings::core::int2str")]
146pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
147 name: "int2str",
148 shape: ShapeRequirements::Any,
149 constant_strategy: ConstantStrategy::InlineLiteral,
150 elementwise: None,
151 reduction: None,
152 emits_nan: false,
153 notes:
154 "Conversion builtin; not eligible for fusion and always materialises host character arrays.",
155};
156
157#[runtime_builtin(
158 name = "int2str",
159 category = "strings/core",
160 summary = "Convert rounded integer values to a character array.",
161 keywords = "int2str,integer to string,number to string,round",
162 examples = "chr = int2str([5 10 20; 100 200 400]);",
163 type_resolver(string_scalar_type),
164 descriptor(crate::builtins::strings::core::int2str::INT2STR_DESCRIPTOR),
165 extensions(crate::builtins::strings::core::int2str::INT2STR_EXTENSIONS),
166 integer_capabilities(crate::builtins::strings::core::int2str::INT2STR_INTEGER_CAPABILITIES),
167 builtin_path = "crate::builtins::strings::core::int2str"
168)]
169async fn int2str_builtin(value: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
170 if !rest.is_empty() {
171 return Err(int2str_error_with_message(
172 "int2str: too many input arguments",
173 &INT2STR_ERROR_INVALID_OPTION,
174 ));
175 }
176
177 if value_is_logical(&value) {
178 crate::compatibility::ensure_builtin_extension_enabled(
179 &INT2STR_LOGICAL_EXTENSION,
180 BUILTIN_NAME,
181 )?;
182 }
183
184 let gathered = gather_if_needed_async(&value)
185 .await
186 .map_err(remap_int2str_flow)?;
187 let data = extract_numeric_data(gathered).await?;
188 Ok(Value::CharArray(format_numeric_data(data)?))
189}
190
191fn value_is_logical(value: &Value) -> bool {
192 matches!(value, Value::Bool(_) | Value::LogicalArray(_))
193 || matches!(value, Value::GpuTensor(handle) if runmat_accelerate_api::handle_is_logical(handle))
194}
195
196enum NumericData {
197 Real {
198 data: Vec<f64>,
199 rows: usize,
200 cols: usize,
201 },
202 Integer {
203 storage: IntegerStorage,
204 rows: usize,
205 cols: usize,
206 },
207}
208
209async fn extract_numeric_data(value: Value) -> BuiltinResult<NumericData> {
210 match value {
211 Value::Num(n) => Ok(NumericData::Real {
212 data: vec![n],
213 rows: 1,
214 cols: 1,
215 }),
216 Value::Int(i) => Ok(NumericData::Integer {
217 storage: integer_storage_from_scalar(i),
218 rows: 1,
219 cols: 1,
220 }),
221 Value::Bool(b) => Ok(NumericData::Real {
222 data: vec![if b { 1.0 } else { 0.0 }],
223 rows: 1,
224 cols: 1,
225 }),
226 Value::Tensor(tensor) => tensor_to_numeric_data(tensor),
227 Value::LogicalArray(logical) => {
228 let tensor = tensor::logical_to_tensor(&logical)
229 .map_err(|_| int2str_error(&INT2STR_ERROR_INVALID_INPUT))?;
230 tensor_to_numeric_data(tensor)
231 }
232 Value::Complex(_, _) | Value::ComplexTensor(_) => Err(int2str_error_with_message(
233 "int2str: complex input is not supported",
234 &INT2STR_ERROR_INVALID_INPUT,
235 )),
236 Value::GpuTensor(handle) => {
237 let gathered = gpu_helpers::gather_tensor_async(&handle)
238 .await
239 .map_err(remap_int2str_flow)?;
240 tensor_to_numeric_data(gathered)
241 }
242 other => Err(int2str_error_with_message(
243 format!(
244 "{} {:?}; expected numeric or logical values",
245 INT2STR_ERROR_INVALID_INPUT.message, other
246 ),
247 &INT2STR_ERROR_INVALID_INPUT,
248 )),
249 }
250}
251
252fn tensor_to_numeric_data(tensor: Tensor) -> BuiltinResult<NumericData> {
253 if tensor.shape.len() > 2 {
254 return Err(int2str_error_with_message(
255 "int2str: input must be scalar, vector, or 2-D matrix",
256 &INT2STR_ERROR_INVALID_INPUT,
257 ));
258 }
259 let rows = tensor.rows();
260 let cols = tensor.cols();
261 let storage = tensor
262 .into_numeric_storage()
263 .map_err(|_| int2str_error(&INT2STR_ERROR_INVALID_INPUT))?;
264 match storage.into_integer_storage() {
265 Ok(storage) => Ok(NumericData::Integer {
266 storage,
267 rows,
268 cols,
269 }),
270 Err(storage) => Ok(NumericData::Real {
271 data: storage.materialize_f64(),
272 rows,
273 cols,
274 }),
275 }
276}
277
278#[derive(Clone)]
279struct CellEntry {
280 text: String,
281 width: usize,
282}
283
284fn format_numeric_data(data: NumericData) -> BuiltinResult<CharArray> {
285 match data {
286 NumericData::Real { data, rows, cols } => format_real_matrix(&data, rows, cols),
287 NumericData::Integer {
288 storage,
289 rows,
290 cols,
291 } => format_integer_matrix(&storage, rows, cols),
292 }
293}
294
295fn format_real_matrix(data: &[f64], rows: usize, cols: usize) -> BuiltinResult<CharArray> {
296 if rows == 0 || cols == 0 {
297 return char_array(Vec::new(), 0, 0);
298 }
299
300 format_entries(rows, cols, |row, col| {
301 let idx = row + col * rows;
302 format_rounded_real(data.get(idx).copied().unwrap_or(0.0))
303 })
304}
305
306fn format_integer_matrix(
307 storage: &IntegerStorage,
308 rows: usize,
309 cols: usize,
310) -> BuiltinResult<CharArray> {
311 if rows == 0 || cols == 0 {
312 return char_array(Vec::new(), 0, 0);
313 }
314
315 format_entries(rows, cols, |row, col| {
316 integer_storage_string(storage, row + col * rows)
317 })
318}
319
320fn integer_storage_from_scalar(value: IntValue) -> IntegerStorage {
321 match value {
322 IntValue::I8(value) => IntegerStorage::I8(vec![value]),
323 IntValue::I16(value) => IntegerStorage::I16(vec![value]),
324 IntValue::I32(value) => IntegerStorage::I32(vec![value]),
325 IntValue::I64(value) => IntegerStorage::I64(vec![value]),
326 IntValue::U8(value) => IntegerStorage::U8(vec![value]),
327 IntValue::U16(value) => IntegerStorage::U16(vec![value]),
328 IntValue::U32(value) => IntegerStorage::U32(vec![value]),
329 IntValue::U64(value) => IntegerStorage::U64(vec![value]),
330 }
331}
332
333fn integer_storage_string(storage: &IntegerStorage, index: usize) -> String {
334 match storage {
335 IntegerStorage::I8(values) => values[index].to_string(),
336 IntegerStorage::I16(values) => values[index].to_string(),
337 IntegerStorage::I32(values) => values[index].to_string(),
338 IntegerStorage::I64(values) => values[index].to_string(),
339 IntegerStorage::U8(values) => values[index].to_string(),
340 IntegerStorage::U16(values) => values[index].to_string(),
341 IntegerStorage::U32(values) => values[index].to_string(),
342 IntegerStorage::U64(values) => values[index].to_string(),
343 }
344}
345
346fn format_entries<F>(rows: usize, cols: usize, mut value_at: F) -> BuiltinResult<CharArray>
347where
348 F: FnMut(usize, usize) -> String,
349{
350 let total_cells = rows.checked_mul(cols).ok_or_else(|| {
351 int2str_error_with_message(
352 "int2str: output dimensions are too large",
353 &INT2STR_ERROR_INTERNAL,
354 )
355 })?;
356 if total_cells > isize::MAX as usize {
357 return Err(int2str_error_with_message(
358 "int2str: output dimensions are too large",
359 &INT2STR_ERROR_INTERNAL,
360 ));
361 }
362
363 let mut entries = vec![
364 vec![
365 CellEntry {
366 text: String::new(),
367 width: 0,
368 };
369 cols
370 ];
371 rows
372 ];
373 let mut col_widths = vec![0usize; cols];
374
375 for (col, width) in col_widths.iter_mut().enumerate() {
376 for (row, row_entries) in entries.iter_mut().enumerate() {
377 let text = value_at(row, col);
378 let entry_width = text.chars().count();
379 row_entries[col] = CellEntry {
380 text,
381 width: entry_width,
382 };
383 *width = (*width).max(entry_width);
384 }
385 }
386
387 if cols > 1 {
388 for (idx, width) in col_widths.iter_mut().enumerate() {
389 if idx > 0 {
390 *width += 1;
391 }
392 }
393 }
394
395 rows_to_char_array(assemble_rows(entries, col_widths))
396}
397
398fn format_rounded_real(value: f64) -> String {
399 format_integer_like(matlab_round(value))
400}
401
402fn matlab_round(value: f64) -> f64 {
403 if value.is_finite() {
404 value.round()
405 } else {
406 value
407 }
408}
409
410fn format_integer_like(value: f64) -> String {
411 if value.is_nan() {
412 return "NaN".to_string();
413 }
414 if value.is_infinite() {
415 return if value.is_sign_negative() {
416 "-Inf".to_string()
417 } else {
418 "Inf".to_string()
419 };
420 }
421 if value == 0.0 {
422 return "0".to_string();
423 }
424 if value >= i64::MIN as f64 && value <= i64::MAX as f64 {
425 return format!("{}", value as i64);
426 }
427 format!("{value:.0}")
428}
429
430fn assemble_rows(entries: Vec<Vec<CellEntry>>, col_widths: Vec<usize>) -> Vec<String> {
431 entries
432 .into_iter()
433 .map(|row_entries| {
434 row_entries
435 .into_iter()
436 .enumerate()
437 .fold(String::new(), |mut acc, (col, entry)| {
438 if col > 0 {
439 acc.push(' ');
440 }
441 let target = col_widths[col];
442 let pad = target.saturating_sub(entry.width);
443 acc.extend(std::iter::repeat_n(' ', pad));
444 acc.push_str(&entry.text);
445 acc
446 })
447 })
448 .collect()
449}
450
451fn rows_to_char_array(rows: Vec<String>) -> BuiltinResult<CharArray> {
452 if rows.is_empty() {
453 return char_array(Vec::new(), 0, 0);
454 }
455
456 let row_count = rows.len();
457 let col_count = rows
458 .iter()
459 .map(|row| row.chars().count())
460 .max()
461 .unwrap_or(0);
462 let capacity = row_count.checked_mul(col_count).ok_or_else(|| {
463 int2str_error_with_message(
464 "int2str: output dimensions are too large",
465 &INT2STR_ERROR_INTERNAL,
466 )
467 })?;
468
469 let mut data = Vec::with_capacity(capacity);
470 for row in rows {
471 let mut chars: Vec<char> = row.chars().collect();
472 if chars.len() < col_count {
473 chars.extend(std::iter::repeat_n(' ', col_count - chars.len()));
474 }
475 data.extend(chars);
476 }
477 char_array(data, row_count, col_count)
478}
479
480fn char_array(data: Vec<char>, rows: usize, cols: usize) -> BuiltinResult<CharArray> {
481 let expected = rows.checked_mul(cols).ok_or_else(|| {
482 int2str_error_with_message(
483 "int2str: output dimensions are too large",
484 &INT2STR_ERROR_INTERNAL,
485 )
486 })?;
487 if expected != data.len() {
488 return Err(int2str_error(&INT2STR_ERROR_INTERNAL));
489 }
490 CharArray::new(data, rows, cols).map_err(|_| int2str_error(&INT2STR_ERROR_INTERNAL))
491}
492
493#[cfg(test)]
494pub(crate) mod tests {
495 use super::*;
496 use crate::builtins::common::test_support;
497 use runmat_builtins::{ResolveContext, Type};
498 use runmat_value::{IntValue, LogicalArray};
499
500 fn int2str_builtin(value: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
501 futures::executor::block_on(super::int2str_builtin(value, rest))
502 }
503
504 fn char_rows(value: Value) -> Vec<String> {
505 match value {
506 Value::CharArray(ca) => ca
507 .data
508 .chunks(ca.cols)
509 .map(|chunk| chunk.iter().collect())
510 .collect(),
511 other => panic!("expected char array, got {other:?}"),
512 }
513 }
514
515 fn error_message(err: RuntimeError) -> String {
516 err.message().to_string()
517 }
518
519 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
520 #[test]
521 fn int2str_rounds_scalar_float() {
522 let out = int2str_builtin(Value::Num(3.25), Vec::new()).expect("int2str");
523 assert_eq!(char_rows(out), vec!["3"]);
524
525 let out = int2str_builtin(Value::Num(4.5), Vec::new()).expect("int2str");
526 assert_eq!(char_rows(out), vec!["5"]);
527
528 let out = int2str_builtin(Value::Num(-4.5), Vec::new()).expect("int2str");
529 assert_eq!(char_rows(out), vec!["-5"]);
530 }
531
532 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
533 #[test]
534 fn int2str_preserves_exact_uint64_scalars_and_arrays() {
535 let scalar = int2str_builtin(Value::Int(IntValue::U64(u64::MAX)), Vec::new())
536 .expect("uint64 scalar");
537 assert_eq!(char_rows(scalar), vec![u64::MAX.to_string()]);
538
539 let tensor =
540 Tensor::new_integer(IntegerStorage::U64(vec![u64::MAX, 1_u64 << 63]), vec![1, 2])
541 .expect("uint64 tensor");
542 let matrix = int2str_builtin(Value::Tensor(tensor), Vec::new()).expect("uint64 array");
543 assert_eq!(
544 char_rows(matrix),
545 vec![format!("{} {}", u64::MAX, 1_u64 << 63)]
546 );
547 }
548
549 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
550 #[test]
551 fn int2str_formats_integer_matrix_with_column_alignment() {
552 let tensor =
553 Tensor::new(vec![5.0, 100.0, 10.0, 200.0, 20.0, 400.0], vec![2, 3]).expect("tensor");
554 let out = int2str_builtin(Value::Tensor(tensor), Vec::new()).expect("int2str");
555 assert_eq!(char_rows(out), vec![" 5 10 20", "100 200 400"]);
556 }
557
558 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
559 #[test]
560 fn int2str_accepts_int_and_logical_inputs() {
561 let out = int2str_builtin(Value::Int(IntValue::I32(-12)), Vec::new()).expect("int2str int");
562 assert_eq!(char_rows(out), vec!["-12"]);
563
564 let logical = LogicalArray::new(vec![1, 0, 1], vec![1, 3]).expect("logical");
565 let error = int2str_builtin(Value::LogicalArray(logical.clone()), Vec::new())
566 .expect_err("MATLAB-compatible mode rejects logical input");
567 assert_eq!(
568 error.identifier(),
569 INT2STR_LOGICAL_EXTENSION.error_identifier
570 );
571 let _guard = crate::compatibility::push_runmat_extensions_enabled(true);
572 let out = int2str_builtin(Value::LogicalArray(logical), Vec::new())
573 .expect("RunMat logical extension");
574 assert_eq!(char_rows(out), vec!["1 0 1"]);
575 }
576
577 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
578 #[test]
579 fn int2str_preserves_nonfinite_text() {
580 let tensor = Tensor::new(vec![f64::NAN, f64::INFINITY, f64::NEG_INFINITY], vec![1, 3])
581 .expect("tensor");
582 let out = int2str_builtin(Value::Tensor(tensor), Vec::new()).expect("int2str");
583 assert_eq!(char_rows(out), vec!["NaN Inf -Inf"]);
584 }
585
586 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
587 #[test]
588 fn int2str_empty_matrix_shapes() {
589 let empty = Tensor::new(Vec::new(), vec![0, 3]).expect("empty rows");
590 let out = int2str_builtin(Value::Tensor(empty), Vec::new()).expect("int2str");
591 match out {
592 Value::CharArray(ca) => {
593 assert_eq!(ca.rows, 0);
594 assert_eq!(ca.cols, 0);
595 assert!(ca.data.is_empty());
596 }
597 other => panic!("expected char array, got {other:?}"),
598 }
599
600 let empty_cols = Tensor::new(Vec::new(), vec![2, 0]).expect("empty cols");
601 let out = int2str_builtin(Value::Tensor(empty_cols), Vec::new()).expect("int2str");
602 match out {
603 Value::CharArray(ca) => {
604 assert_eq!(ca.rows, 0);
605 assert_eq!(ca.cols, 0);
606 assert!(ca.data.is_empty());
607 }
608 other => panic!("expected char array, got {other:?}"),
609 }
610 }
611
612 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
613 #[test]
614 fn int2str_gpu_tensor_roundtrip() {
615 test_support::with_test_provider(|provider| {
616 let tensor = Tensor::new(vec![10.2, 20.8], vec![1, 2]).expect("tensor");
617 let view = runmat_accelerate_api::HostTensorView {
618 data: &tensor.materialize_f64(),
619 shape: &tensor.shape,
620 };
621 let handle = provider.upload(&view).expect("upload");
622 let out = int2str_builtin(Value::GpuTensor(handle), Vec::new()).expect("int2str");
623 assert_eq!(char_rows(out), vec!["10 21"]);
624 });
625 }
626
627 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
628 #[test]
629 fn int2str_rejects_complex_input() {
630 let err = error_message(int2str_builtin(Value::Complex(3.0, 4.0), Vec::new()).unwrap_err());
631 assert!(err.contains("complex input is not supported"));
632 }
633
634 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
635 #[test]
636 fn int2str_rejects_extra_arguments() {
637 let err =
638 error_message(int2str_builtin(Value::Num(1.0), vec![Value::Num(2.0)]).unwrap_err());
639 assert!(err.contains("too many input arguments"));
640 }
641
642 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
643 #[test]
644 fn int2str_rejects_non_numeric_input() {
645 let err =
646 error_message(int2str_builtin(Value::String("hello".into()), Vec::new()).unwrap_err());
647 assert!(err.contains("unsupported input type"));
648 }
649
650 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
651 #[test]
652 fn int2str_rejects_nd_input() {
653 let tensor = Tensor::new(vec![1.0; 8], vec![2, 2, 2]).expect("tensor");
654 let err = error_message(int2str_builtin(Value::Tensor(tensor), Vec::new()).unwrap_err());
655 assert!(err.contains("scalar, vector, or 2-D matrix"));
656 }
657
658 #[test]
659 fn int2str_type_is_string_scalar() {
660 assert_eq!(
661 string_scalar_type(&[Type::Num], &ResolveContext::new(Vec::new())),
662 Type::String
663 );
664 }
665}