1use std::collections::HashSet;
4use std::convert::TryFrom;
5use std::io::{BufRead, BufReader};
6use std::path::{Path, PathBuf};
7
8use runmat_accelerate_api::HostTensorView;
9use runmat_builtins::{LogicalArray, Tensor, Value};
10use runmat_filesystem::File;
11use runmat_macros::runtime_builtin;
12
13use crate::builtins::common::spec::{
14 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
15 ReductionNaN, ResidencyPolicy, ShapeRequirements,
16};
17use crate::{build_runtime_error, gather_if_needed_async, BuiltinResult, RuntimeError};
18
19const BUILTIN_NAME: &str = "readmatrix";
20
21#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::io::tabular::readmatrix")]
22pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
23 name: "readmatrix",
24 op_kind: GpuOpKind::Custom("io-readmatrix"),
25 supported_precisions: &[],
26 broadcast: BroadcastSemantics::None,
27 provider_hooks: &[],
28 constant_strategy: ConstantStrategy::InlineLiteral,
29 residency: ResidencyPolicy::GatherImmediately,
30 nan_mode: ReductionNaN::Include,
31 two_pass_threshold: None,
32 workgroup_size: None,
33 accepts_nan_mode: false,
34 notes: "Runs entirely on the host; acceleration providers are not involved.",
35};
36
37fn readmatrix_error(message: impl Into<String>) -> RuntimeError {
38 build_runtime_error(message)
39 .with_builtin(BUILTIN_NAME)
40 .build()
41}
42
43fn readmatrix_error_with_source<E>(message: impl Into<String>, source: E) -> RuntimeError
44where
45 E: std::error::Error + Send + Sync + 'static,
46{
47 build_runtime_error(message)
48 .with_builtin(BUILTIN_NAME)
49 .with_source(source)
50 .build()
51}
52
53fn map_control_flow(err: RuntimeError) -> RuntimeError {
54 let identifier = err.identifier().map(|value| value.to_string());
55 let message = err.message().to_string();
56 let mut builder = build_runtime_error(message)
57 .with_builtin(BUILTIN_NAME)
58 .with_source(err);
59 if let Some(identifier) = identifier {
60 builder = builder.with_identifier(identifier);
61 }
62 builder.build()
63}
64
65#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::io::tabular::readmatrix")]
66pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
67 name: "readmatrix",
68 shape: ShapeRequirements::Any,
69 constant_strategy: ConstantStrategy::InlineLiteral,
70 elementwise: None,
71 reduction: None,
72 emits_nan: false,
73 notes: "Not eligible for fusion; executes as a standalone host operation.",
74};
75
76#[runtime_builtin(
77 name = "readmatrix",
78 category = "io/tabular",
79 summary = "Import numeric data from delimited text files into a RunMat matrix.",
80 keywords = "readmatrix,csv,delimited text,numeric import,table",
81 accel = "cpu",
82 type_resolver(crate::builtins::io::type_resolvers::readmatrix_type),
83 builtin_path = "crate::builtins::io::tabular::readmatrix"
84)]
85async fn readmatrix_builtin(path: Value, rest: Vec<Value>) -> crate::BuiltinResult<Value> {
86 let path_value = gather_if_needed_async(&path)
87 .await
88 .map_err(map_control_flow)?;
89 let options = parse_options(&rest).await?;
90 options.validate()?;
91 let resolved = resolve_path(&path_value)?;
92 let tensor = read_numeric_matrix(&resolved, &options)?;
93 finalize_output(tensor, &options)
94}
95
96async fn parse_options(args: &[Value]) -> BuiltinResult<ReadMatrixOptions> {
97 let mut options = ReadMatrixOptions::default();
98 let mut index = 0usize;
99 if let Some(Value::Struct(struct_value)) = args.get(index) {
100 parse_struct_options(struct_value, &mut options).await?;
101 index += 1;
102 }
103 while index < args.len() {
104 if index + 1 >= args.len() {
105 return Err(readmatrix_error(
106 "readmatrix: name/value inputs must appear in pairs",
107 ));
108 }
109 let name_value = gather_if_needed_async(&args[index])
110 .await
111 .map_err(map_control_flow)?;
112 let name = option_name_from_value(&name_value)?;
113 let value = &args[index + 1];
114 apply_option(&mut options, &name, value).await?;
115 index += 2;
116 }
117 Ok(options)
118}
119
120async fn parse_struct_options(
121 struct_value: &runmat_builtins::StructValue,
122 options: &mut ReadMatrixOptions,
123) -> BuiltinResult<()> {
124 for (name, value) in &struct_value.fields {
125 apply_option(options, name, value).await?;
126 }
127 Ok(())
128}
129
130async fn apply_option(
131 options: &mut ReadMatrixOptions,
132 name: &str,
133 value: &Value,
134) -> BuiltinResult<()> {
135 let lowered = name.trim().to_ascii_lowercase();
136 let is_like = lowered == "like";
137 let effective_value = if is_like {
138 value.clone()
139 } else {
140 gather_if_needed_async(value)
141 .await
142 .map_err(map_control_flow)?
143 };
144 if name.eq_ignore_ascii_case("Delimiter") {
145 let delimiter = parse_delimiter(&effective_value)?;
146 options.delimiter = Some(delimiter);
147 return Ok(());
148 }
149 if name.eq_ignore_ascii_case("NumHeaderLines") {
150 let header_lines = value_to_usize(&effective_value, "NumHeaderLines")?;
151 options.num_header_lines = header_lines;
152 return Ok(());
153 }
154 if name.eq_ignore_ascii_case("TreatAsMissing") {
155 let tokens = parse_treat_as_missing(&effective_value).await?;
156 for token in tokens {
157 options.add_missing_token(&token);
158 }
159 return Ok(());
160 }
161 if name.eq_ignore_ascii_case("DecimalSeparator") {
162 let sep = parse_separator_char(&effective_value, "DecimalSeparator")?;
163 options.decimal_separator = sep;
164 return Ok(());
165 }
166 if name.eq_ignore_ascii_case("ThousandsSeparator") {
167 let sep = parse_separator_char(&effective_value, "ThousandsSeparator")?;
168 options.thousands_separator = Some(sep);
169 return Ok(());
170 }
171 if name.eq_ignore_ascii_case("EmptyValue") {
172 let numeric = value_to_f64(&effective_value, "EmptyValue")?;
173 options.empty_value = Some(numeric);
174 return Ok(());
175 }
176 if name.eq_ignore_ascii_case("OutputType") {
177 let text = value_to_string_scalar(&effective_value, "OutputType")?;
178 options.set_output_type(&text)?;
179 return Ok(());
180 }
181 if name.eq_ignore_ascii_case("Range") {
182 let range = parse_range(&effective_value)?;
183 options.range = Some(range);
184 return Ok(());
185 }
186 if is_like {
187 options.set_like(effective_value)?;
188 return Ok(());
189 }
190 Ok(())
192}
193
194fn option_name_from_value(value: &Value) -> BuiltinResult<String> {
195 value_to_string_scalar(value, "option name")
196}
197
198fn parse_delimiter(value: &Value) -> BuiltinResult<Delimiter> {
199 let text = value_to_string_scalar(value, "Delimiter")?;
200 if text.is_empty() {
201 return Err(readmatrix_error("readmatrix: Delimiter cannot be empty"));
202 }
203 let trimmed_lower = text.trim().to_ascii_lowercase();
204 match trimmed_lower.as_str() {
205 "tab" => Ok(Delimiter::Char('\t')),
206 "space" | "whitespace" => Ok(Delimiter::Whitespace),
207 "comma" => Ok(Delimiter::Char(',')),
208 "semicolon" => Ok(Delimiter::Char(';')),
209 "pipe" => Ok(Delimiter::Char('|')),
210 _ => {
211 if text.chars().count() == 1 {
212 Ok(Delimiter::Char(text.chars().next().unwrap()))
213 } else {
214 Ok(Delimiter::String(text))
215 }
216 }
217 }
218}
219
220fn parse_separator_char(value: &Value, option_name: &str) -> BuiltinResult<char> {
221 let text = value_to_string_scalar(value, option_name)?;
222 if text.is_empty() {
223 return Err(readmatrix_error(format!(
224 "readmatrix: {option_name} must be a single character"
225 )));
226 }
227 let mut chars = text.chars();
228 let ch = chars.next().unwrap();
229 if chars.next().is_some() {
230 return Err(readmatrix_error(format!(
231 "readmatrix: {option_name} must be a single character"
232 )));
233 }
234 if ch == '\n' || ch == '\r' {
235 return Err(readmatrix_error(format!(
236 "readmatrix: {option_name} cannot be a newline character"
237 )));
238 }
239 Ok(ch)
240}
241
242fn value_to_string_scalar(value: &Value, context: &str) -> BuiltinResult<String> {
243 match value {
244 Value::String(s) => Ok(s.clone()),
245 Value::CharArray(ca) if ca.rows == 1 => Ok(ca.data.iter().collect()),
246 Value::StringArray(sa) => {
247 if sa.data.len() == 1 {
248 Ok(sa.data[0].clone())
249 } else {
250 Err(readmatrix_error(format!(
251 "readmatrix: {context} must be a scalar string array"
252 )))
253 }
254 }
255 _ => Err(readmatrix_error(format!(
256 "readmatrix: expected {context} as a string scalar or character vector"
257 ))),
258 }
259}
260
261fn value_to_usize(value: &Value, context: &str) -> BuiltinResult<usize> {
262 match value {
263 Value::Int(i) => {
264 let num = i.to_i64();
265 if num < 0 {
266 Err(readmatrix_error(format!(
267 "readmatrix: {context} must be a non-negative integer"
268 )))
269 } else {
270 Ok(num as usize)
271 }
272 }
273 Value::Num(n) => {
274 if !n.is_finite() {
275 return Err(readmatrix_error(format!(
276 "readmatrix: {context} must be a finite non-negative integer"
277 )));
278 }
279 if *n < 0.0 {
280 return Err(readmatrix_error(format!(
281 "readmatrix: {context} must be a non-negative integer"
282 )));
283 }
284 if (n.round() - n).abs() > f64::EPSILON {
285 return Err(readmatrix_error(format!(
286 "readmatrix: {context} must be an integer value"
287 )));
288 }
289 Ok(n.round() as usize)
290 }
291 _ => Err(readmatrix_error(format!(
292 "readmatrix: {context} must be provided as a numeric scalar"
293 ))),
294 }
295}
296
297fn value_to_f64(value: &Value, context: &str) -> BuiltinResult<f64> {
298 match value {
299 Value::Num(n) => {
300 if n.is_finite() {
301 Ok(*n)
302 } else {
303 Err(readmatrix_error(format!(
304 "readmatrix: {context} must be a finite numeric scalar"
305 )))
306 }
307 }
308 Value::Int(i) => Ok(i.to_f64()),
309 Value::Tensor(t) => {
310 if t.data.len() == 1 {
311 let v = t.data[0];
312 if v.is_finite() {
313 Ok(v)
314 } else {
315 Err(readmatrix_error(format!(
316 "readmatrix: {context} must be a finite numeric scalar"
317 )))
318 }
319 } else {
320 Err(readmatrix_error(format!(
321 "readmatrix: {context} must be a numeric scalar"
322 )))
323 }
324 }
325 _ => Err(readmatrix_error(format!(
326 "readmatrix: {context} must be a numeric scalar"
327 ))),
328 }
329}
330
331async fn parse_treat_as_missing(value: &Value) -> BuiltinResult<Vec<String>> {
332 let mut out = Vec::new();
333 let mut stack = vec![value.clone()];
334 while let Some(value) = stack.pop() {
335 match value {
336 Value::String(s) => out.push(s),
337 Value::CharArray(ca) if ca.rows == 1 => out.push(ca.data.iter().collect()),
338 Value::StringArray(sa) => out.extend(sa.data),
339 Value::Num(n) => out.push(format_numeric_token(n)),
340 Value::Int(i) => out.push(format!("{}", i.to_i64())),
341 Value::Tensor(t) => {
342 if t.data.len() == 1 {
343 out.push(format_numeric_token(t.data[0]));
344 } else {
345 return Err(readmatrix_error(
346 "readmatrix: TreatAsMissing entries must be scalar values",
347 ));
348 }
349 }
350 Value::Cell(cell) => {
351 for handle in &cell.data {
352 let inner = unsafe { &*handle.as_raw() };
353 let gathered = gather_if_needed_async(inner)
354 .await
355 .map_err(map_control_flow)?;
356 stack.push(gathered);
357 }
358 }
359 _ => {
360 return Err(readmatrix_error(
361 "readmatrix: TreatAsMissing values must be strings or numeric scalars",
362 ))
363 }
364 }
365 }
366 Ok(out)
367}
368
369fn format_numeric_token(value: f64) -> String {
370 if value == 0.0 {
371 "0".to_string()
372 } else {
373 format!("{}", value)
374 }
375}
376
377#[derive(Clone)]
378struct ReadMatrixOptions {
379 delimiter: Option<Delimiter>,
380 num_header_lines: usize,
381 decimal_separator: char,
382 thousands_separator: Option<char>,
383 treat_as_missing: HashSet<String>,
384 empty_value: Option<f64>,
385 range: Option<RangeSpec>,
386 output_template: OutputTemplate,
387}
388
389impl Default for ReadMatrixOptions {
390 fn default() -> Self {
391 Self {
392 delimiter: None,
393 num_header_lines: 0,
394 decimal_separator: '.',
395 thousands_separator: None,
396 treat_as_missing: HashSet::new(),
397 empty_value: None,
398 range: None,
399 output_template: OutputTemplate::Double,
400 }
401 }
402}
403
404impl ReadMatrixOptions {
405 fn add_missing_token(&mut self, token: &str) {
406 let normalized = normalize_missing_token(token);
407 self.treat_as_missing.insert(normalized);
408 }
409
410 fn is_missing_token(&self, token: &str) -> bool {
411 if self.treat_as_missing.is_empty() {
412 return false;
413 }
414 let norm = normalize_missing_token(token);
415 self.treat_as_missing.contains(&norm)
416 }
417
418 fn empty_value(&self) -> f64 {
419 self.empty_value.unwrap_or(f64::NAN)
420 }
421
422 fn validate(&self) -> BuiltinResult<()> {
423 if let Some(range) = &self.range {
424 range.validate()?;
425 }
426 if let Some(sep) = self.thousands_separator {
427 if sep == self.decimal_separator {
428 return Err(readmatrix_error(
429 "readmatrix: DecimalSeparator and ThousandsSeparator must differ",
430 ));
431 }
432 }
433 Ok(())
434 }
435
436 fn set_output_type(&mut self, spec: &str) -> BuiltinResult<()> {
437 if matches!(self.output_template, OutputTemplate::Like(_)) {
438 return Err(readmatrix_error(
439 "readmatrix: cannot combine 'Like' with OutputType",
440 ));
441 }
442 if spec.eq_ignore_ascii_case("double") {
443 self.output_template = OutputTemplate::Double;
444 return Ok(());
445 }
446 if spec.eq_ignore_ascii_case("logical") {
447 self.output_template = OutputTemplate::Logical;
448 return Ok(());
449 }
450 Err(readmatrix_error(format!(
451 "readmatrix: unsupported OutputType '{}'",
452 spec
453 )))
454 }
455
456 fn set_like(&mut self, proto: Value) -> BuiltinResult<()> {
457 if matches!(self.output_template, OutputTemplate::Like(_)) {
458 return Err(readmatrix_error(
459 "readmatrix: multiple 'Like' specifications are not supported",
460 ));
461 }
462 if !matches!(self.output_template, OutputTemplate::Double) {
463 return Err(readmatrix_error(
464 "readmatrix: cannot combine 'Like' with OutputType overrides",
465 ));
466 }
467 self.output_template = OutputTemplate::Like(proto);
468 Ok(())
469 }
470}
471
472#[derive(Clone)]
473enum Delimiter {
474 Char(char),
475 String(String),
476 Whitespace,
477}
478
479#[derive(Clone)]
480enum OutputTemplate {
481 Double,
482 Logical,
483 Like(Value),
484}
485
486#[derive(Clone)]
487struct RangeSpec {
488 start_row: usize,
489 start_col: usize,
490 end_row: Option<usize>,
491 end_col: Option<usize>,
492}
493
494impl RangeSpec {
495 fn validate(&self) -> BuiltinResult<()> {
496 if let Some(end_row) = self.end_row {
497 if end_row < self.start_row {
498 return Err(readmatrix_error(
499 "readmatrix: Range end row must be >= start row",
500 ));
501 }
502 }
503 if let Some(end_col) = self.end_col {
504 if end_col < self.start_col {
505 return Err(readmatrix_error(
506 "readmatrix: Range end column must be >= start column",
507 ));
508 }
509 }
510 Ok(())
511 }
512}
513
514fn normalize_missing_token(token: &str) -> String {
515 token.trim().to_ascii_lowercase()
516}
517
518fn parse_range(value: &Value) -> BuiltinResult<RangeSpec> {
519 match value {
520 Value::String(s) => parse_range_string(s),
521 Value::CharArray(ca) if ca.rows == 1 => {
522 let text: String = ca.data.iter().collect();
523 parse_range_string(&text)
524 }
525 Value::StringArray(sa) => {
526 if sa.data.len() == 1 {
527 parse_range_string(&sa.data[0])
528 } else {
529 Err(readmatrix_error(
530 "readmatrix: Range string array inputs must be scalar",
531 ))
532 }
533 }
534 Value::Tensor(_) => parse_range_numeric(value),
535 _ => Err(readmatrix_error(
536 "readmatrix: Range must be provided as a string or numeric vector",
537 )),
538 }
539}
540
541fn parse_range_string(text: &str) -> BuiltinResult<RangeSpec> {
542 let trimmed = text.trim();
543 if trimmed.is_empty() {
544 return Err(readmatrix_error("readmatrix: Range string cannot be empty"));
545 }
546 let parts: Vec<&str> = trimmed.split(':').collect();
547 if parts.len() > 2 {
548 return Err(readmatrix_error(format!(
549 "readmatrix: invalid Range specification '{}'",
550 text
551 )));
552 }
553 let start = parse_cell_reference(parts[0])?;
554 if start.col.is_none() {
555 return Err(readmatrix_error(
556 "readmatrix: Range must specify a starting column",
557 ));
558 }
559 let end_ref = if parts.len() == 2 {
560 Some(parse_cell_reference(parts[1])?)
561 } else {
562 None
563 };
564 if let Some(ref end) = end_ref {
565 if end.col.is_none() {
566 return Err(readmatrix_error(
567 "readmatrix: Range end must include a column reference",
568 ));
569 }
570 }
571 let start_row = start.row.unwrap_or(0);
572 let start_col = start.col.unwrap();
573 let end_row = end_ref.as_ref().and_then(|r| r.row);
574 let end_col = end_ref.as_ref().and_then(|r| r.col);
575 Ok(RangeSpec {
576 start_row,
577 start_col,
578 end_row,
579 end_col,
580 })
581}
582
583fn parse_range_numeric(value: &Value) -> BuiltinResult<RangeSpec> {
584 let elements = match value {
585 Value::Tensor(t) => t.data.clone(),
586 _ => {
587 return Err(readmatrix_error(
588 "readmatrix: numeric Range must be provided as a vector with 2 or 4 elements",
589 ))
590 }
591 };
592 if elements.len() != 2 && elements.len() != 4 {
593 return Err(readmatrix_error(
594 "readmatrix: numeric Range must contain exactly 2 or 4 elements",
595 ));
596 }
597 let mut indices = Vec::with_capacity(elements.len());
598 for (idx, value) in elements.iter().enumerate() {
599 let converted = positive_index(*value, idx)?;
600 indices.push(converted);
601 }
602 let start_row = indices[0];
603 let start_col = indices[1];
604 let (end_row, end_col) = if indices.len() == 4 {
605 (Some(indices[2]), Some(indices[3]))
606 } else {
607 (None, None)
608 };
609 Ok(RangeSpec {
610 start_row,
611 start_col,
612 end_row,
613 end_col,
614 })
615}
616
617fn positive_index(value: f64, position: usize) -> BuiltinResult<usize> {
618 if !value.is_finite() {
619 return Err(readmatrix_error("readmatrix: Range indices must be finite"));
620 }
621 if value < 1.0 {
622 return Err(readmatrix_error("readmatrix: Range indices must be >= 1"));
623 }
624 let rounded = value.round();
625 if (rounded - value).abs() > f64::EPSILON {
626 return Err(readmatrix_error(
627 "readmatrix: Range indices must be integers",
628 ));
629 }
630 let zero_based = (rounded as i64) - 1;
631 if zero_based < 0 {
632 return Err(readmatrix_error("readmatrix: Range indices must be >= 1"));
633 }
634 usize::try_from(zero_based).map_err(|_| {
635 readmatrix_error(format!(
636 "readmatrix: Range index {} is too large to fit in usize",
637 position + 1
638 ))
639 })
640}
641
642#[derive(Clone, Copy)]
643struct CellReference {
644 row: Option<usize>,
645 col: Option<usize>,
646}
647
648fn parse_cell_reference(token: &str) -> BuiltinResult<CellReference> {
649 let mut letters = String::new();
650 let mut digits = String::new();
651 for ch in token.trim().chars() {
652 if ch == '$' {
653 continue;
654 }
655 if ch.is_ascii_alphabetic() {
656 letters.push(ch.to_ascii_uppercase());
657 } else if ch.is_ascii_digit() {
658 digits.push(ch);
659 } else {
660 return Err(readmatrix_error(format!(
661 "readmatrix: invalid Range component '{}'",
662 token
663 )));
664 }
665 }
666 if letters.is_empty() && digits.is_empty() {
667 return Err(readmatrix_error(
668 "readmatrix: Range references cannot be empty",
669 ));
670 }
671 let col = if letters.is_empty() {
672 None
673 } else {
674 Some(column_index_from_letters(&letters)?)
675 };
676 let row = if digits.is_empty() {
677 None
678 } else {
679 let parsed = digits.parse::<usize>().map_err(|_| {
680 readmatrix_error(format!(
681 "readmatrix: invalid row index '{}' in Range component '{}'",
682 digits, token
683 ))
684 })?;
685 if parsed == 0 {
686 return Err(readmatrix_error("readmatrix: Range rows must be >= 1"));
687 }
688 Some(parsed - 1)
689 };
690 Ok(CellReference { row, col })
691}
692
693fn column_index_from_letters(letters: &str) -> BuiltinResult<usize> {
694 let mut value: usize = 0;
695 for ch in letters.chars() {
696 if !ch.is_ascii_uppercase() {
697 return Err(readmatrix_error(format!(
698 "readmatrix: invalid column designator '{}' in Range",
699 letters
700 )));
701 }
702 let digit = (ch as u8 - b'A' + 1) as usize;
703 value = value
704 .checked_mul(26)
705 .and_then(|v| v.checked_add(digit))
706 .ok_or_else(|| readmatrix_error("readmatrix: Range column index overflowed"))?;
707 }
708 value
709 .checked_sub(1)
710 .ok_or_else(|| readmatrix_error("readmatrix: Range column index underflowed"))
711}
712
713fn apply_range(
714 rows: &[Vec<f64>],
715 max_cols: usize,
716 range: &RangeSpec,
717 default_fill: f64,
718) -> (Vec<Vec<f64>>, usize) {
719 if rows.is_empty() || max_cols == 0 {
720 return (Vec::new(), 0);
721 }
722 if range.start_row >= rows.len() || range.start_col >= max_cols {
723 return (Vec::new(), 0);
724 }
725 let last_row = rows.len().saturating_sub(1);
726 let mut end_row = range.end_row.unwrap_or(last_row);
727 if end_row > last_row {
728 end_row = last_row;
729 }
730 if end_row < range.start_row {
731 return (Vec::new(), 0);
732 }
733
734 let last_col = max_cols.saturating_sub(1);
735 let mut end_col = range.end_col.unwrap_or(last_col);
736 if end_col > last_col {
737 end_col = last_col;
738 }
739 if end_col < range.start_col {
740 return (Vec::new(), 0);
741 }
742
743 let mut subset = Vec::new();
744 let mut subset_max_cols = 0usize;
745 for row_idx in range.start_row..=end_row {
746 if row_idx >= rows.len() {
747 break;
748 }
749 let row = &rows[row_idx];
750 let mut extracted = Vec::with_capacity(end_col - range.start_col + 1);
751 for col_idx in range.start_col..=end_col {
752 if col_idx >= max_cols {
753 break;
754 }
755 let value = row.get(col_idx).copied().unwrap_or(default_fill);
756 extracted.push(value);
757 }
758 subset_max_cols = subset_max_cols.max(extracted.len());
759 subset.push(extracted);
760 }
761
762 if subset_max_cols == 0 {
763 (Vec::new(), 0)
764 } else {
765 (subset, subset_max_cols)
766 }
767}
768
769fn finalize_output(tensor: Tensor, options: &ReadMatrixOptions) -> BuiltinResult<Value> {
770 match &options.output_template {
771 OutputTemplate::Double => Ok(Value::Tensor(tensor)),
772 OutputTemplate::Logical => tensor_to_logical(tensor),
773 OutputTemplate::Like(proto) => finalize_like(tensor, proto),
774 }
775}
776
777fn tensor_to_logical(tensor: Tensor) -> BuiltinResult<Value> {
778 let mut data = Vec::with_capacity(tensor.data.len());
779 for value in &tensor.data {
780 let bit = if *value == 0.0 { 0 } else { 1 };
781 data.push(bit);
782 }
783 let logical = LogicalArray::new(data, tensor.shape.clone())
784 .map_err(|e| readmatrix_error(format!("readmatrix: {e}")))?;
785 Ok(Value::LogicalArray(logical))
786}
787
788fn finalize_like(tensor: Tensor, proto: &Value) -> BuiltinResult<Value> {
789 match proto {
790 Value::LogicalArray(_) | Value::Bool(_) => tensor_to_logical(tensor),
791 Value::GpuTensor(handle) => tensor_to_gpu(tensor, handle),
792 Value::Tensor(_) | Value::Num(_) | Value::Int(_) => Ok(Value::Tensor(tensor)),
793 Value::ComplexTensor(_) | Value::Complex(_, _) => Ok(Value::Tensor(tensor)),
794 Value::CharArray(_) | Value::String(_) | Value::StringArray(_) => Ok(Value::Tensor(tensor)),
795 Value::Cell(_) => Ok(Value::Tensor(tensor)),
796 _ => Ok(Value::Tensor(tensor)),
797 }
798}
799
800fn tensor_to_gpu(
801 tensor: Tensor,
802 _handle: &runmat_accelerate_api::GpuTensorHandle,
803) -> BuiltinResult<Value> {
804 if let Some(provider) = runmat_accelerate_api::provider() {
805 let view = HostTensorView {
806 data: &tensor.data,
807 shape: &tensor.shape,
808 };
809 if let Ok(uploaded) = provider.upload(&view) {
810 return Ok(Value::GpuTensor(uploaded));
811 }
812 }
813 Ok(Value::Tensor(tensor))
814}
815
816fn read_numeric_matrix(path: &Path, options: &ReadMatrixOptions) -> BuiltinResult<Tensor> {
817 let file = File::open(path).map_err(|err| {
818 readmatrix_error_with_source(
819 format!("readmatrix: unable to read '{}': {err}", path.display()),
820 err,
821 )
822 })?;
823 let reader = BufReader::new(file);
824 let mut data_lines: Vec<(usize, String)> = Vec::new();
825 for (idx, line_result) in reader.lines().enumerate() {
826 let line_number = idx + 1;
827 let line = line_result.map_err(|err| {
828 readmatrix_error_with_source(
829 format!("readmatrix: error reading '{}': {err}", path.display()),
830 err,
831 )
832 })?;
833 let cleaned = line.trim_end_matches('\r');
834 if line_number <= options.num_header_lines {
835 continue;
836 }
837 if cleaned.trim().is_empty() {
838 continue;
839 }
840 data_lines.push((line_number, cleaned.to_string()));
841 }
842
843 if data_lines.is_empty() {
844 return Tensor::new(Vec::new(), vec![0, 0])
845 .map_err(|e| readmatrix_error(format!("readmatrix: {e}")));
846 }
847
848 if let Some((_, first_line)) = data_lines.first_mut() {
849 if first_line.starts_with('\u{FEFF}') {
850 let stripped = first_line.trim_start_matches('\u{FEFF}').to_string();
851 *first_line = stripped;
852 }
853 }
854
855 let delimiter = options
856 .delimiter
857 .clone()
858 .or_else(|| detect_delimiter(&data_lines))
859 .unwrap_or(Delimiter::Whitespace);
860
861 let mut rows: Vec<Vec<f64>> = Vec::new();
862 let mut max_cols = 0usize;
863
864 for (line_number, text) in &data_lines {
865 let fields = split_fields(text, &delimiter);
866 if fields.is_empty() {
867 continue;
868 }
869 let mut row = Vec::with_capacity(fields.len());
870 for (index, field) in fields.iter().enumerate() {
871 let value = parse_numeric_token(field, options, *line_number, index + 1)?;
872 row.push(value);
873 }
874 if row.len() > max_cols {
875 max_cols = row.len();
876 }
877 rows.push(row);
878 }
879
880 if rows.is_empty() {
881 return Tensor::new(Vec::new(), vec![0, 0])
882 .map_err(|e| readmatrix_error(format!("readmatrix: {e}")));
883 }
884
885 let default_fill = options.empty_value();
886 if let Some(range) = &options.range {
887 let (subset_rows, subset_cols) = apply_range(&rows, max_cols, range, default_fill);
888 rows = subset_rows;
889 max_cols = subset_cols;
890 }
891
892 if rows.is_empty() || max_cols == 0 {
893 return Tensor::new(Vec::new(), vec![0, 0])
894 .map_err(|e| readmatrix_error(format!("readmatrix: {e}")));
895 }
896 let row_count = rows.len();
897 let mut data = vec![default_fill; row_count * max_cols];
898
899 for (row_index, row) in rows.iter().enumerate() {
900 for col_index in 0..max_cols {
901 let value = row.get(col_index).copied().unwrap_or(default_fill);
902 data[col_index * row_count + row_index] = value;
903 }
904 }
905
906 Tensor::new(data, vec![row_count, max_cols])
907 .map_err(|e| readmatrix_error(format!("readmatrix: {e}")))
908}
909
910fn detect_delimiter(lines: &[(usize, String)]) -> Option<Delimiter> {
911 if lines.is_empty() {
912 return None;
913 }
914 let sample: Vec<&str> = lines
915 .iter()
916 .take(32)
917 .map(|(_, line)| line.as_str())
918 .collect();
919 let candidates = [',', '\t', ';', '|'];
920 let mut best: Option<(f64, Delimiter)> = None;
921
922 for candidate in candidates {
923 let mut counts = Vec::new();
924 for line in &sample {
925 if !line.contains(candidate) {
926 continue;
927 }
928 let fields = line.split(candidate).count();
929 if fields >= 2 {
930 counts.push(fields);
931 }
932 }
933 if counts.is_empty() {
934 continue;
935 }
936 let average = counts.iter().copied().sum::<usize>() as f64 / counts.len() as f64;
937 if average < 2.0 {
938 continue;
939 }
940 if best
941 .as_ref()
942 .map(|(best_avg, _)| average > *best_avg)
943 .unwrap_or(true)
944 {
945 best = Some((average, Delimiter::Char(candidate)));
946 }
947 }
948
949 if let Some((_, delimiter)) = best {
950 return Some(delimiter);
951 }
952
953 let mut whitespace_hits = 0usize;
954 for line in &sample {
955 if line.split_whitespace().count() > 1 {
956 whitespace_hits += 1;
957 }
958 }
959 if whitespace_hits > 0 {
960 Some(Delimiter::Whitespace)
961 } else {
962 None
963 }
964}
965
966fn split_fields(line: &str, delimiter: &Delimiter) -> Vec<String> {
967 match delimiter {
968 Delimiter::Char(ch) => split_with_char_delim(line, *ch),
969 Delimiter::String(pattern) => line.split(pattern).map(|s| s.to_string()).collect(),
970 Delimiter::Whitespace => line.split_whitespace().map(|s| s.to_string()).collect(),
971 }
972}
973
974fn split_with_char_delim(line: &str, delimiter: char) -> Vec<String> {
975 let mut fields = Vec::new();
976 let mut current = String::new();
977 let mut in_quotes = false;
978 let mut chars = line.chars().peekable();
979 while let Some(ch) = chars.next() {
980 if ch == '"' {
981 if in_quotes && chars.peek() == Some(&'"') {
982 current.push('"');
983 chars.next();
984 } else {
985 in_quotes = !in_quotes;
986 }
987 continue;
988 }
989 if ch == delimiter && !in_quotes {
990 fields.push(current.clone());
991 current.clear();
992 } else {
993 current.push(ch);
994 }
995 }
996 fields.push(current);
997 fields
998}
999
1000fn parse_numeric_token(
1001 token: &str,
1002 options: &ReadMatrixOptions,
1003 line_number: usize,
1004 column_number: usize,
1005) -> BuiltinResult<f64> {
1006 let trimmed = token.trim();
1007 if trimmed.is_empty() {
1008 return Ok(options.empty_value());
1009 }
1010 let unquoted = unquote(trimmed);
1011 let inner = unquoted.trim();
1012 if inner.is_empty() {
1013 return Ok(options.empty_value());
1014 }
1015 if options.is_missing_token(inner) {
1016 return Ok(f64::NAN);
1017 }
1018 let normalized = normalize_numeric_token(inner, options);
1019 if normalized.is_empty() {
1020 return Ok(options.empty_value());
1021 }
1022 let lower = normalized.to_ascii_lowercase();
1023 if lower == "nan" {
1024 return Ok(f64::NAN);
1025 }
1026 if matches!(lower.as_str(), "inf" | "+inf" | "infinity" | "+infinity") {
1027 return Ok(f64::INFINITY);
1028 }
1029 if matches!(lower.as_str(), "-inf" | "-infinity") {
1030 return Ok(f64::NEG_INFINITY);
1031 }
1032 normalized.parse::<f64>().map_err(|_| {
1033 readmatrix_error(format!(
1034 "readmatrix: unable to parse numeric value '{}' on line {} column {}",
1035 inner, line_number, column_number
1036 ))
1037 })
1038}
1039
1040fn normalize_numeric_token(token: &str, options: &ReadMatrixOptions) -> String {
1041 let mut text = token.to_string();
1042 if let Some(thousands) = options.thousands_separator {
1043 if thousands != options.decimal_separator {
1044 text = text.chars().filter(|ch| *ch != thousands).collect();
1045 }
1046 }
1047 if options.decimal_separator != '.' {
1048 text = text.replace(options.decimal_separator, ".");
1049 }
1050 text
1051}
1052
1053fn unquote(token: &str) -> &str {
1054 if token.len() >= 2 {
1055 let bytes = token.as_bytes();
1056 if (bytes[0] == b'"' && bytes[token.len() - 1] == b'"')
1057 || (bytes[0] == b'\'' && bytes[token.len() - 1] == b'\'')
1058 {
1059 return &token[1..token.len() - 1];
1060 }
1061 }
1062 token
1063}
1064
1065fn resolve_path(value: &Value) -> BuiltinResult<PathBuf> {
1066 match value {
1067 Value::String(s) => normalize_path(s),
1068 Value::CharArray(ca) if ca.rows == 1 => {
1069 let text: String = ca.data.iter().collect();
1070 normalize_path(&text)
1071 }
1072 Value::CharArray(_) => Err(readmatrix_error(
1073 "readmatrix: expected a 1-by-N character vector for the file name",
1074 )),
1075 Value::StringArray(sa) => {
1076 if sa.data.len() == 1 {
1077 normalize_path(&sa.data[0])
1078 } else {
1079 Err(readmatrix_error(
1080 "readmatrix: string array inputs must be scalar",
1081 ))
1082 }
1083 }
1084 other => Err(readmatrix_error(format!(
1085 "readmatrix: expected filename as string scalar or character vector, got {other:?}"
1086 ))),
1087 }
1088}
1089
1090fn normalize_path(raw: &str) -> BuiltinResult<PathBuf> {
1091 if raw.is_empty() {
1092 return Err(readmatrix_error("readmatrix: filename must not be empty"));
1093 }
1094 Ok(Path::new(raw).to_path_buf())
1095}
1096
1097#[cfg(test)]
1098pub(crate) mod tests {
1099 use super::*;
1100 use futures::executor::block_on;
1101 use runmat_time::unix_timestamp_ms;
1102 use std::fs;
1103
1104 use crate::builtins::common::test_support;
1105 use runmat_accelerate_api::HostTensorView;
1106 use runmat_builtins::{CharArray, IntValue, LogicalArray, StringArray, Tensor};
1107
1108 fn unique_path(prefix: &str) -> PathBuf {
1109 let millis = unix_timestamp_ms();
1110 let mut path = std::env::temp_dir();
1111 path.push(format!("runmat_{prefix}_{}_{}", std::process::id(), millis));
1112 path
1113 }
1114
1115 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1116 #[test]
1117 fn readmatrix_reads_csv_data() {
1118 let path = unique_path("readmatrix_csv");
1119 fs::write(&path, "1,2,3\n4,5,6\n").expect("write sample file");
1120 let result = block_on(readmatrix_builtin(
1121 Value::from(path.to_string_lossy().to_string()),
1122 Vec::new(),
1123 ))
1124 .expect("readmatrix");
1125 match result {
1126 Value::Tensor(t) => {
1127 assert_eq!(t.shape, vec![2, 3]);
1128 assert_eq!(t.data, vec![1.0, 4.0, 2.0, 5.0, 3.0, 6.0]);
1129 }
1130 other => panic!("expected tensor result, got {other:?}"),
1131 }
1132 let _ = fs::remove_file(&path);
1133 }
1134
1135 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1136 #[test]
1137 fn readmatrix_skips_header_lines() {
1138 let path = unique_path("readmatrix_header");
1139 fs::write(&path, "time,value\n0,10\n1,12\n").expect("write sample file");
1140 let args = vec![Value::from("NumHeaderLines"), Value::Int(IntValue::I32(1))];
1141 let result = block_on(readmatrix_builtin(
1142 Value::from(path.to_string_lossy().to_string()),
1143 args,
1144 ))
1145 .expect("readmatrix");
1146 match result {
1147 Value::Tensor(t) => {
1148 assert_eq!(t.shape, vec![2, 2]);
1149 assert_eq!(t.data, vec![0.0, 1.0, 10.0, 12.0]);
1150 }
1151 other => panic!("expected tensor result, got {other:?}"),
1152 }
1153 let _ = fs::remove_file(&path);
1154 }
1155
1156 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1157 #[test]
1158 fn readmatrix_respects_delimiter_option() {
1159 let path = unique_path("readmatrix_tab");
1160 fs::write(&path, "1\t2\t3\n4\t5\t6\n").expect("write sample file");
1161 let args = vec![Value::from("Delimiter"), Value::from("tab")];
1162 let result = block_on(readmatrix_builtin(
1163 Value::from(path.to_string_lossy().to_string()),
1164 args,
1165 ))
1166 .expect("readmatrix");
1167 match result {
1168 Value::Tensor(t) => {
1169 assert_eq!(t.shape, vec![2, 3]);
1170 assert_eq!(t.data, vec![1.0, 4.0, 2.0, 5.0, 3.0, 6.0]);
1171 }
1172 other => panic!("expected tensor result, got {other:?}"),
1173 }
1174 let _ = fs::remove_file(&path);
1175 }
1176
1177 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1178 #[test]
1179 fn readmatrix_respects_range_string() {
1180 let path = unique_path("readmatrix_range_string");
1181 fs::write(&path, "11,12,13\n21,22,23\n31,32,33\n").expect("write sample file");
1182 let args = vec![Value::from("Range"), Value::from("B2:C3")];
1183 let result = block_on(readmatrix_builtin(
1184 Value::from(path.to_string_lossy().to_string()),
1185 args,
1186 ))
1187 .expect("readmatrix");
1188 match result {
1189 Value::Tensor(t) => {
1190 assert_eq!(t.shape, vec![2, 2]);
1191 assert_eq!(t.data, vec![22.0, 32.0, 23.0, 33.0]);
1192 }
1193 other => panic!("expected tensor result, got {other:?}"),
1194 }
1195 let _ = fs::remove_file(&path);
1196 }
1197
1198 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1199 #[test]
1200 fn readmatrix_respects_range_numeric_vector() {
1201 let path = unique_path("readmatrix_range_numeric");
1202 fs::write(&path, "11,12,13\n21,22,23\n31,32,33\n").expect("write sample file");
1203 let range = Tensor::new(vec![2.0, 2.0, 3.0, 3.0], vec![1, 4]).expect("range tensor");
1204 let args = vec![Value::from("Range"), Value::Tensor(range)];
1205 let result = block_on(readmatrix_builtin(
1206 Value::from(path.to_string_lossy().to_string()),
1207 args,
1208 ))
1209 .expect("readmatrix");
1210 match result {
1211 Value::Tensor(t) => {
1212 assert_eq!(t.shape, vec![2, 2]);
1213 assert_eq!(t.data, vec![22.0, 32.0, 23.0, 33.0]);
1214 }
1215 other => panic!("expected tensor result, got {other:?}"),
1216 }
1217 let _ = fs::remove_file(&path);
1218 }
1219
1220 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1221 #[test]
1222 fn readmatrix_treats_custom_missing_tokens() {
1223 let path = unique_path("readmatrix_missing");
1224 fs::write(&path, "1,NA,3\nNA,5,missing\n").expect("write file");
1225 let strings = StringArray::new(vec!["NA".to_string(), "missing".to_string()], vec![1, 2])
1226 .expect("string array");
1227 let args = vec![Value::from("TreatAsMissing"), Value::StringArray(strings)];
1228 let result = block_on(readmatrix_builtin(
1229 Value::from(path.to_string_lossy().to_string()),
1230 args,
1231 ))
1232 .expect("readmatrix");
1233 match result {
1234 Value::Tensor(t) => {
1235 assert_eq!(t.shape, vec![2, 3]);
1236 assert!(t.data[1].is_nan()); assert!(t.data[2].is_nan()); assert!(t.data[5].is_nan()); }
1240 other => panic!("expected tensor result, got {other:?}"),
1241 }
1242 let _ = fs::remove_file(&path);
1243 }
1244
1245 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1246 #[test]
1247 fn readmatrix_uses_decimal_and_thousands_separators() {
1248 let path = unique_path("readmatrix_decimal");
1249 fs::write(&path, "1.234,56;7.890,12\n").expect("write sample file");
1250 let args = vec![
1251 Value::from("Delimiter"),
1252 Value::from(";"),
1253 Value::from("DecimalSeparator"),
1254 Value::from(","),
1255 Value::from("ThousandsSeparator"),
1256 Value::from("."),
1257 ];
1258 let result = block_on(readmatrix_builtin(
1259 Value::from(path.to_string_lossy().to_string()),
1260 args,
1261 ))
1262 .expect("readmatrix");
1263 match result {
1264 Value::Tensor(t) => {
1265 assert_eq!(t.shape, vec![1, 2]);
1266 assert!((t.data[0] - 1234.56).abs() < 1e-9);
1267 assert!((t.data[1] - 7890.12).abs() < 1e-9);
1268 }
1269 other => panic!("expected tensor result, got {other:?}"),
1270 }
1271 let _ = fs::remove_file(&path);
1272 }
1273
1274 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1275 #[test]
1276 fn readmatrix_applies_empty_value() {
1277 let path = unique_path("readmatrix_empty_value");
1278 fs::write(&path, "1,,3\n4,,6\n").expect("write sample file");
1279 let args = vec![Value::from("EmptyValue"), Value::Num(0.0)];
1280 let result = block_on(readmatrix_builtin(
1281 Value::from(path.to_string_lossy().to_string()),
1282 args,
1283 ))
1284 .expect("readmatrix");
1285 match result {
1286 Value::Tensor(t) => {
1287 assert_eq!(t.shape, vec![2, 3]);
1288 assert_eq!(t.data, vec![1.0, 4.0, 0.0, 0.0, 3.0, 6.0]);
1289 }
1290 other => panic!("expected tensor result, got {other:?}"),
1291 }
1292 let _ = fs::remove_file(&path);
1293 }
1294
1295 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1296 #[test]
1297 fn readmatrix_accepts_struct_options() {
1298 let path = unique_path("readmatrix_struct_opts");
1299 fs::write(&path, "header1,header2\n9,10\n11,12\n").expect("write sample file");
1300 let mut options_struct = runmat_builtins::StructValue::new();
1301 options_struct
1302 .fields
1303 .insert("Delimiter".to_string(), Value::from(","));
1304 options_struct
1305 .fields
1306 .insert("NumHeaderLines".to_string(), Value::Int(IntValue::I32(1)));
1307 let args = vec![Value::Struct(options_struct)];
1308 let result = block_on(readmatrix_builtin(
1309 Value::from(path.to_string_lossy().to_string()),
1310 args,
1311 ))
1312 .expect("readmatrix");
1313 match result {
1314 Value::Tensor(t) => {
1315 assert_eq!(t.shape, vec![2, 2]);
1316 assert_eq!(t.data, vec![9.0, 11.0, 10.0, 12.0]);
1317 }
1318 other => panic!("expected tensor result, got {other:?}"),
1319 }
1320 let _ = fs::remove_file(&path);
1321 }
1322
1323 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1324 #[test]
1325 fn readmatrix_errors_on_non_numeric_field() {
1326 let path = unique_path("readmatrix_error");
1327 fs::write(&path, "1,abc,3\n").expect("write sample file");
1328 let err = block_on(readmatrix_builtin(
1329 Value::from(path.to_string_lossy().to_string()),
1330 Vec::new(),
1331 ))
1332 .expect_err("readmatrix should fail");
1333 let message = err.message().to_string();
1334 assert!(
1335 message.contains("unable to parse numeric value"),
1336 "unexpected error message: {message}"
1337 );
1338 let _ = fs::remove_file(&path);
1339 }
1340
1341 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1342 #[test]
1343 fn readmatrix_returns_empty_on_no_data() {
1344 let path = unique_path("readmatrix_empty");
1345 File::create(&path).expect("create file");
1346 let result = block_on(readmatrix_builtin(
1347 Value::from(path.to_string_lossy().to_string()),
1348 Vec::new(),
1349 ))
1350 .expect("readmatrix");
1351 match result {
1352 Value::Tensor(t) => {
1353 assert_eq!(t.shape, vec![0, 0]);
1354 assert!(t.data.is_empty());
1355 }
1356 other => panic!("expected tensor result, got {other:?}"),
1357 }
1358 let _ = fs::remove_file(&path);
1359 }
1360
1361 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1362 #[test]
1363 fn readmatrix_output_type_logical() {
1364 let path = unique_path("readmatrix_output_logical");
1365 fs::write(&path, "0,1,-3\nNaN,0,5\n").expect("write sample file");
1366 let args = vec![Value::from("OutputType"), Value::from("logical")];
1367 let result = block_on(readmatrix_builtin(
1368 Value::from(path.to_string_lossy().to_string()),
1369 args,
1370 ))
1371 .expect("readmatrix");
1372 match result {
1373 Value::LogicalArray(arr) => {
1374 assert_eq!(arr.shape, vec![2, 3]);
1375 assert_eq!(arr.data, vec![0, 1, 1, 0, 1, 1]);
1376 }
1377 other => panic!("expected logical array, got {other:?}"),
1378 }
1379 let _ = fs::remove_file(&path);
1380 }
1381
1382 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1383 #[test]
1384 fn readmatrix_like_logical_proto() {
1385 let path = unique_path("readmatrix_like_logical");
1386 fs::write(&path, "1,0\n0,5\n").expect("write sample file");
1387 let proto = LogicalArray::new(vec![1], vec![1]).expect("logical prototype");
1388 let args = vec![Value::from("Like"), Value::LogicalArray(proto)];
1389 let result = block_on(readmatrix_builtin(
1390 Value::from(path.to_string_lossy().to_string()),
1391 args,
1392 ))
1393 .expect("readmatrix");
1394 match result {
1395 Value::LogicalArray(arr) => {
1396 assert_eq!(arr.shape, vec![2, 2]);
1397 assert_eq!(arr.data, vec![1, 0, 0, 1]);
1398 }
1399 other => panic!("expected logical array, got {other:?}"),
1400 }
1401 let _ = fs::remove_file(&path);
1402 }
1403
1404 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1405 #[test]
1406 fn readmatrix_like_gpu_proto() {
1407 test_support::with_test_provider(|provider| {
1408 let path = unique_path("readmatrix_like_gpu");
1409 fs::write(&path, "1,2\n3,4\n").expect("write sample file");
1410 let proto_tensor = Tensor::new(vec![0.0, 0.0], vec![1, 2]).expect("tensor");
1411 let view = HostTensorView {
1412 data: &proto_tensor.data,
1413 shape: &proto_tensor.shape,
1414 };
1415 let handle = provider.upload(&view).expect("upload prototype");
1416 let args = vec![Value::from("Like"), Value::GpuTensor(handle.clone())];
1417 let result = block_on(readmatrix_builtin(
1418 Value::from(path.to_string_lossy().to_string()),
1419 args,
1420 ))
1421 .expect("readmatrix");
1422 assert!(
1423 matches!(result, Value::GpuTensor(_)),
1424 "expected GPU tensor result, got {result:?}"
1425 );
1426 let gathered = test_support::gather(result).expect("gather result");
1427 assert_eq!(gathered.shape, vec![2, 2]);
1428 assert_eq!(gathered.data, vec![1.0, 3.0, 2.0, 4.0]);
1429 let _ = fs::remove_file(&path);
1430 });
1431 }
1432
1433 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1434 #[test]
1435 fn readmatrix_accepts_character_vector_path() {
1436 let path = unique_path("readmatrix_char_path");
1437 fs::write(&path, "1 2 3\n").expect("write sample file");
1438 let text = path.to_string_lossy().to_string();
1439 let chars: Vec<char> = text.chars().collect();
1440 let len = chars.len();
1441 let char_array = CharArray::new(chars, 1, len).expect("char array");
1442 let result = block_on(readmatrix_builtin(Value::CharArray(char_array), Vec::new()))
1443 .expect("readmatrix");
1444 match result {
1445 Value::Tensor(t) => {
1446 assert_eq!(t.shape, vec![1, 3]);
1447 assert_eq!(t.data, vec![1.0, 2.0, 3.0]);
1448 }
1449 other => panic!("expected tensor result, got {other:?}"),
1450 }
1451 let _ = fs::remove_file(&path);
1452 }
1453
1454 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1455 #[test]
1456 fn readmatrix_handles_quoted_fields() {
1457 let path = unique_path("readmatrix_quotes");
1458 fs::write(&path, "\"1\",\"2\",\"3\"\n").expect("write sample file");
1459 let result = block_on(readmatrix_builtin(
1460 Value::from(path.to_string_lossy().to_string()),
1461 Vec::new(),
1462 ))
1463 .expect("readmatrix");
1464 match result {
1465 Value::Tensor(t) => {
1466 assert_eq!(t.shape, vec![1, 3]);
1467 assert_eq!(t.data, vec![1.0, 2.0, 3.0]);
1468 }
1469 other => panic!("expected tensor result, got {other:?}"),
1470 }
1471 let _ = fs::remove_file(&path);
1472 }
1473
1474 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1475 #[test]
1476 fn readmatrix_preserves_negative_infinity() {
1477 let path = unique_path("readmatrix_infinity");
1478 fs::write(&path, "-Inf,Inf,NaN\n").expect("write sample file");
1479 let result = block_on(readmatrix_builtin(
1480 Value::from(path.to_string_lossy().to_string()),
1481 Vec::new(),
1482 ))
1483 .expect("readmatrix");
1484 match result {
1485 Value::Tensor(t) => {
1486 assert_eq!(t.shape, vec![1, 3]);
1487 assert!(t.data[0].is_infinite() && t.data[0].is_sign_negative());
1488 assert!(t.data[1].is_infinite() && t.data[1].is_sign_positive());
1489 assert!(t.data[2].is_nan());
1490 }
1491 other => panic!("expected tensor result, got {other:?}"),
1492 }
1493 let _ = fs::remove_file(&path);
1494 }
1495
1496 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1497 #[test]
1498 fn readmatrix_supports_whitespace_delimiter() {
1499 let path = unique_path("readmatrix_whitespace");
1500 fs::write(&path, "1 2 3\n4 5 6\n").expect("write sample file");
1501 let result = block_on(readmatrix_builtin(
1502 Value::from(path.to_string_lossy().to_string()),
1503 Vec::new(),
1504 ))
1505 .expect("readmatrix");
1506 match result {
1507 Value::Tensor(t) => {
1508 assert_eq!(t.shape, vec![2, 3]);
1509 assert_eq!(t.data, vec![1.0, 4.0, 2.0, 5.0, 3.0, 6.0]);
1510 }
1511 other => panic!("expected tensor result, got {other:?}"),
1512 }
1513 let _ = fs::remove_file(&path);
1514 }
1515}