1use std::collections::HashMap;
4use std::io::{BufReader, Cursor, Read};
5use std::path::{Path, PathBuf};
6
7use flate2::read::ZlibDecoder;
8use regex::Regex;
9use runmat_builtins::{
10 BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
11 BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
12 CharArray, ComplexTensor, IntValue, IntegerStorage, LogicalArray, NumericDType, SparseTensor,
13 StringArray, StructValue, Tensor, Value,
14};
15use runmat_filesystem::File;
16use runmat_macros::runtime_builtin;
17
18use super::format::{
19 MatArray, MatClass, MatData, FLAG_COMPLEX, FLAG_LOGICAL, MAT_HEADER_LEN, MI_COMPRESSED,
20 MI_DOUBLE, MI_INT16, MI_INT32, MI_INT64, MI_INT8, MI_MATRIX, MI_SINGLE, MI_UINT16, MI_UINT32,
21 MI_UINT64, MI_UINT8, MI_UTF16, MI_UTF32, MI_UTF8,
22};
23use crate::builtins::common::spec::{
24 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
25 ReductionNaN, ResidencyPolicy, ShapeRequirements,
26};
27use crate::builtins::introspection::object_serialization::restore_value_from_mat_load;
28use crate::{build_runtime_error, gather_if_needed_async, make_cell, BuiltinResult, RuntimeError};
29
30const LOAD_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
31 name: "S",
32 ty: BuiltinParamType::Any,
33 arity: BuiltinParamArity::Required,
34 default: None,
35 description: "Struct containing the loaded variables.",
36}];
37const LOAD_INPUTS_NONE: [BuiltinParamDescriptor; 0] = [];
38const LOAD_INPUTS_FILENAME: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
39 name: "filename",
40 ty: BuiltinParamType::StringScalar,
41 arity: BuiltinParamArity::Required,
42 default: Some("\"matlab.mat\""),
43 description: "MAT-file path.",
44}];
45const LOAD_INPUTS_FILENAME_VARS: [BuiltinParamDescriptor; 2] = [
46 BuiltinParamDescriptor {
47 name: "filename",
48 ty: BuiltinParamType::StringScalar,
49 arity: BuiltinParamArity::Required,
50 default: Some("\"matlab.mat\""),
51 description: "MAT-file path.",
52 },
53 BuiltinParamDescriptor {
54 name: "varName",
55 ty: BuiltinParamType::StringScalar,
56 arity: BuiltinParamArity::Variadic,
57 default: None,
58 description: "Variable names to load.",
59 },
60];
61const LOAD_INPUTS_FILENAME_REGEXP: [BuiltinParamDescriptor; 3] = [
62 BuiltinParamDescriptor {
63 name: "filename",
64 ty: BuiltinParamType::StringScalar,
65 arity: BuiltinParamArity::Required,
66 default: Some("\"matlab.mat\""),
67 description: "MAT-file path.",
68 },
69 BuiltinParamDescriptor {
70 name: "option",
71 ty: BuiltinParamType::StringScalar,
72 arity: BuiltinParamArity::Required,
73 default: Some("\"-regexp\""),
74 description: "Regular-expression selection option.",
75 },
76 BuiltinParamDescriptor {
77 name: "pattern",
78 ty: BuiltinParamType::StringScalar,
79 arity: BuiltinParamArity::Variadic,
80 default: None,
81 description: "Regex patterns matched against variable names.",
82 },
83];
84const LOAD_INPUTS_OPTIONS: [BuiltinParamDescriptor; 2] = [
85 BuiltinParamDescriptor {
86 name: "option",
87 ty: BuiltinParamType::StringScalar,
88 arity: BuiltinParamArity::Variadic,
89 default: None,
90 description: "Compatibility options such as '-mat' and '-regexp'.",
91 },
92 BuiltinParamDescriptor {
93 name: "value",
94 ty: BuiltinParamType::Any,
95 arity: BuiltinParamArity::Variadic,
96 default: None,
97 description: "Option arguments and variable selectors.",
98 },
99];
100const LOAD_SIGNATURES: [BuiltinSignatureDescriptor; 5] = [
101 BuiltinSignatureDescriptor {
102 label: "S = load()",
103 inputs: &LOAD_INPUTS_NONE,
104 outputs: &LOAD_OUTPUT,
105 },
106 BuiltinSignatureDescriptor {
107 label: "S = load(filename)",
108 inputs: &LOAD_INPUTS_FILENAME,
109 outputs: &LOAD_OUTPUT,
110 },
111 BuiltinSignatureDescriptor {
112 label: "S = load(filename, varName1, varName2, ...)",
113 inputs: &LOAD_INPUTS_FILENAME_VARS,
114 outputs: &LOAD_OUTPUT,
115 },
116 BuiltinSignatureDescriptor {
117 label: "S = load(filename, \"-regexp\", pattern1, ...)",
118 inputs: &LOAD_INPUTS_FILENAME_REGEXP,
119 outputs: &LOAD_OUTPUT,
120 },
121 BuiltinSignatureDescriptor {
122 label: "S = load(option, value, ...)",
123 inputs: &LOAD_INPUTS_OPTIONS,
124 outputs: &LOAD_OUTPUT,
125 },
126];
127const LOAD_ERROR_INVALID_ARGUMENT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
128 code: "RM.LOAD.INVALID_ARGUMENT",
129 identifier: Some("RunMat:load:InvalidArgument"),
130 when: "Arguments do not match a supported load invocation form.",
131 message: "load: invalid argument",
132};
133const LOAD_ERROR_INVALID_OPTION: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
134 code: "RM.LOAD.INVALID_OPTION",
135 identifier: Some("RunMat:load:InvalidOption"),
136 when: "An option token or option argument is invalid.",
137 message: "load: invalid option",
138};
139const LOAD_ERROR_FILENAME: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
140 code: "RM.LOAD.FILENAME",
141 identifier: Some("RunMat:load:Filename"),
142 when: "Filename is invalid or cannot be normalized.",
143 message: "load: invalid filename",
144};
145const LOAD_ERROR_SELECTION: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
146 code: "RM.LOAD.SELECTION",
147 identifier: Some("RunMat:load:Selection"),
148 when: "Requested variables are missing or no variables are selected.",
149 message: "load: variable selection failed",
150};
151const LOAD_ERROR_IO: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
152 code: "RM.LOAD.IO",
153 identifier: Some("RunMat:load:Io"),
154 when: "MAT-file data cannot be read or decoded.",
155 message: "load: MAT-file I/O failure",
156};
157const LOAD_ERROR_OUTPUT_COUNT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
158 code: "RM.LOAD.OUTPUT_COUNT",
159 identifier: Some("RunMat:load:OutputCount"),
160 when: "Caller requests more outputs than supported by load.",
161 message: "load: unsupported output count",
162};
163const LOAD_ERROR_WORKSPACE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
164 code: "RM.LOAD.WORKSPACE",
165 identifier: Some("RunMat:load:Workspace"),
166 when: "Statement-form load cannot assign values into workspace.",
167 message: "load: workspace assignment failed",
168};
169const LOAD_ERRORS: [BuiltinErrorDescriptor; 7] = [
170 LOAD_ERROR_INVALID_ARGUMENT,
171 LOAD_ERROR_INVALID_OPTION,
172 LOAD_ERROR_FILENAME,
173 LOAD_ERROR_SELECTION,
174 LOAD_ERROR_IO,
175 LOAD_ERROR_OUTPUT_COUNT,
176 LOAD_ERROR_WORKSPACE,
177];
178pub const LOAD_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
179 signatures: &LOAD_SIGNATURES,
180 output_mode: BuiltinOutputMode::ByRequestedOutputCount,
181 completion_policy: BuiltinCompletionPolicy::Public,
182 errors: &LOAD_ERRORS,
183};
184
185#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::io::mat::load")]
186pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
187 name: "load",
188 op_kind: GpuOpKind::Custom("io-load"),
189 supported_precisions: &[],
190 broadcast: BroadcastSemantics::None,
191 provider_hooks: &[],
192 constant_strategy: ConstantStrategy::InlineLiteral,
193 residency: ResidencyPolicy::NewHandle,
194 nan_mode: ReductionNaN::Include,
195 two_pass_threshold: None,
196 workgroup_size: None,
197 accepts_nan_mode: false,
198 notes: "Reads MAT-files on the host and produces CPU-resident values. Providers are not involved until accelerated code later promotes the results.",
199};
200
201#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::io::mat::load")]
202pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
203 name: "load",
204 shape: ShapeRequirements::Any,
205 constant_strategy: ConstantStrategy::InlineLiteral,
206 elementwise: None,
207 reduction: None,
208 emits_nan: false,
209 notes: "File I/O is not eligible for fusion. Registration exists for documentation completeness only.",
210};
211
212#[runtime_builtin(
213 name = "load",
214 category = "io/mat",
215 summary = "Load variables from a MAT-file.",
216 keywords = "load,mat,workspace",
217 accel = "cpu",
218 sink = true,
219 type_resolver(crate::builtins::io::type_resolvers::load_type),
220 descriptor(crate::builtins::io::mat::load::LOAD_DESCRIPTOR),
221 builtin_path = "crate::builtins::io::mat::load"
222)]
223async fn load_builtin(args: Vec<Value>) -> crate::BuiltinResult<Value> {
224 let eval = evaluate(&args).await?;
225
226 if let Some(n) = crate::output_count::current_output_count() {
229 if n > 1 {
230 return Err(load_error_with(
231 &LOAD_ERROR_OUTPUT_COUNT,
232 "load supports at most one output argument",
233 ));
234 }
235 }
236
237 if crate::output_context::requested_output_count() == Some(0) {
244 for (name, value) in eval.variables() {
245 crate::workspace::assign(name, value.clone())
246 .map_err(|err| load_error_with(&LOAD_ERROR_WORKSPACE, err))?;
247 }
248 return Ok(Value::OutputList(Vec::new()));
249 }
250
251 Ok(eval.first_output())
252}
253
254#[derive(Clone, Debug)]
255pub struct LoadEval {
256 variables: Vec<(String, Value)>,
257}
258
259impl LoadEval {
260 pub fn first_output(&self) -> Value {
261 let mut st = StructValue::new();
262 for (name, value) in &self.variables {
263 st.fields.insert(name.clone(), value.clone());
264 }
265 Value::Struct(st)
266 }
267
268 pub fn variables(&self) -> &[(String, Value)] {
269 &self.variables
270 }
271
272 pub fn into_variables(self) -> Vec<(String, Value)> {
273 self.variables
274 }
275}
276
277struct LoadRequest {
278 variables: Vec<String>,
279 regex_patterns: Vec<Regex>,
280}
281
282const BUILTIN_NAME: &str = "load";
283
284fn load_error(message: impl Into<String>) -> RuntimeError {
285 load_error_with(&LOAD_ERROR_INVALID_ARGUMENT, message)
286}
287
288fn load_error_with(
289 error: &'static BuiltinErrorDescriptor,
290 message: impl Into<String>,
291) -> RuntimeError {
292 let mut builder = build_runtime_error(message).with_builtin(BUILTIN_NAME);
293 if let Some(identifier) = error.identifier {
294 builder = builder.with_identifier(identifier);
295 }
296 builder.build()
297}
298
299fn load_error_with_source(
300 error: &'static BuiltinErrorDescriptor,
301 message: impl Into<String>,
302 source: impl std::error::Error + Send + Sync + 'static,
303) -> RuntimeError {
304 let mut builder = build_runtime_error(message)
305 .with_builtin(BUILTIN_NAME)
306 .with_source(source);
307 if let Some(identifier) = error.identifier {
308 builder = builder.with_identifier(identifier);
309 }
310 builder.build()
311}
312
313pub async fn evaluate(args: &[Value]) -> BuiltinResult<LoadEval> {
314 let mut host_args = Vec::with_capacity(args.len());
315 for arg in args {
316 host_args.push(gather_if_needed_async(arg).await?);
317 }
318
319 let invocation = parse_invocation(&host_args).await?;
320
321 let mut path_value = if let Some(path) = invocation.path_value {
322 path
323 } else {
324 Value::from("matlab.mat")
325 };
326
327 if invocation.path_was_default {
328 if let Ok(override_path) = std::env::var("RUNMAT_LOAD_DEFAULT_PATH") {
329 path_value = Value::from(override_path);
330 }
331 }
332
333 let mut regex_patterns = Vec::with_capacity(invocation.regex_tokens.len());
334 for pattern in invocation.regex_tokens {
335 let regex = Regex::new(&pattern).map_err(|err| {
336 load_error_with_source(
337 &LOAD_ERROR_INVALID_OPTION,
338 format!("load: invalid regular expression '{pattern}': {err}"),
339 err,
340 )
341 })?;
342 regex_patterns.push(regex);
343 }
344
345 let request = LoadRequest {
346 variables: invocation.variables,
347 regex_patterns,
348 };
349 let path = normalise_path(&path_value)?;
350 let entries = read_mat_file(&path).await?;
351
352 let selected = select_variables(&entries, &request)?;
353 Ok(LoadEval {
354 variables: selected,
355 })
356}
357
358struct ParsedInvocation {
359 path_value: Option<Value>,
360 path_was_default: bool,
361 variables: Vec<String>,
362 regex_tokens: Vec<String>,
363}
364
365async fn parse_invocation(values: &[Value]) -> BuiltinResult<ParsedInvocation> {
366 let mut path_value = None;
367 let mut path_was_default = false;
368 let mut variables = Vec::new();
369 let mut regex_tokens = Vec::new();
370 let mut idx = 0usize;
371 while idx < values.len() {
372 if let Some(flag) = option_token(&values[idx])? {
373 match flag.as_str() {
374 "-mat" => {
375 idx += 1;
376 continue;
377 }
378 "-regexp" => {
379 idx += 1;
380 if idx >= values.len() {
381 return Err(load_error_with(
382 &LOAD_ERROR_INVALID_OPTION,
383 "load: '-regexp' requires at least one pattern",
384 ));
385 }
386 while idx < values.len() {
387 if option_token(&values[idx])?.is_some() {
388 break;
389 }
390 let names = extract_names(&values[idx]).await?;
391 if names.is_empty() {
392 return Err(load_error_with(
393 &LOAD_ERROR_INVALID_OPTION,
394 "load: '-regexp' requires non-empty pattern strings",
395 ));
396 }
397 regex_tokens.extend(names);
398 idx += 1;
399 }
400 continue;
401 }
402 other => {
403 return Err(load_error_with(
404 &LOAD_ERROR_INVALID_OPTION,
405 format!("load: unsupported option '{other}'"),
406 ));
407 }
408 }
409 } else {
410 if path_value.is_none() {
411 path_value = Some(values[idx].clone());
412 idx += 1;
413 continue;
414 }
415 let names = extract_names(&values[idx]).await?;
416 variables.extend(names);
417 idx += 1;
418 }
419 }
420
421 if path_value.is_none() {
422 path_was_default = true;
423 }
424
425 Ok(ParsedInvocation {
426 path_value,
427 path_was_default,
428 variables,
429 regex_tokens,
430 })
431}
432
433fn normalise_path(value: &Value) -> BuiltinResult<PathBuf> {
434 let raw = value_to_string_scalar(value).ok_or_else(|| {
435 load_error_with(
436 &LOAD_ERROR_FILENAME,
437 "load: filename must be a character vector or string scalar",
438 )
439 })?;
440 let mut path = PathBuf::from(raw);
441 if path.extension().is_none() {
442 path.set_extension("mat");
443 }
444 Ok(path)
445}
446
447fn select_variables(
448 entries: &[(String, Value)],
449 request: &LoadRequest,
450) -> BuiltinResult<Vec<(String, Value)>> {
451 if request.variables.is_empty() && request.regex_patterns.is_empty() {
452 return Ok(entries.to_vec());
453 }
454
455 let mut by_name: HashMap<&str, &Value> = HashMap::with_capacity(entries.len());
456 for (name, value) in entries {
457 by_name.insert(name, value);
458 }
459
460 let mut selected = Vec::new();
461
462 for name in &request.variables {
463 let value = by_name.get(name.as_str()).ok_or_else(|| {
464 load_error_with(
465 &LOAD_ERROR_SELECTION,
466 format!("load: variable '{name}' was not found in the file"),
467 )
468 })?;
469 insert_or_replace(&mut selected, name, (*value).clone());
470 }
471
472 if !request.regex_patterns.is_empty() {
473 let mut matched = 0usize;
474 for (name, value) in entries {
475 if request
476 .regex_patterns
477 .iter()
478 .any(|regex| regex.is_match(name))
479 {
480 matched += 1;
481 insert_or_replace(&mut selected, name, value.clone());
482 }
483 }
484 if matched == 0 && request.variables.is_empty() {
485 return Err(load_error_with(
486 &LOAD_ERROR_SELECTION,
487 "load: no variables matched '-regexp' patterns",
488 ));
489 }
490 }
491
492 if selected.is_empty() {
493 return Err(load_error_with(
494 &LOAD_ERROR_SELECTION,
495 "load: no variables selected",
496 ));
497 }
498
499 Ok(selected)
500}
501
502fn insert_or_replace(selected: &mut Vec<(String, Value)>, name: &str, value: Value) {
503 if let Some(entry) = selected.iter_mut().find(|(existing, _)| existing == name) {
504 entry.1 = value;
505 } else {
506 selected.push((name.to_string(), value));
507 }
508}
509
510pub(crate) async fn read_mat_file_for_builtin(
511 path: &Path,
512 builtin: &str,
513) -> crate::BuiltinResult<Vec<(String, Value)>> {
514 match read_mat_file(path).await {
515 Ok(entries) => Ok(entries),
516 Err(err) => {
517 let message = err.message().replacen("load:", &format!("{builtin}:"), 1);
518 let mut builder = build_runtime_error(message).with_builtin(builtin);
519 if let Some(identifier) = err.identifier() {
520 builder = builder.with_identifier(identifier);
521 }
522 Err(builder.with_source(err).build())
523 }
524 }
525}
526
527pub(crate) async fn read_mat_file(path: &Path) -> BuiltinResult<Vec<(String, Value)>> {
528 let file = File::open_async(path).await.map_err(|err| {
529 load_error_with_source(
530 &LOAD_ERROR_IO,
531 format!("load: failed to open '{}': {err}", path.display()),
532 err,
533 )
534 })?;
535 let mut reader = BufReader::new(file);
536 let entries = read_mat_reader(&mut reader)?;
537 restore_loaded_entries(entries).await
538}
539
540pub fn decode_workspace_from_mat_bytes(bytes: &[u8]) -> BuiltinResult<Vec<(String, Value)>> {
541 let mut cursor = Cursor::new(bytes);
542 let entries = read_mat_reader(&mut cursor)?;
543 futures::executor::block_on(restore_loaded_entries(entries))
544}
545
546async fn restore_loaded_entries(
547 entries: Vec<(String, Value)>,
548) -> BuiltinResult<Vec<(String, Value)>> {
549 let mut restored = Vec::with_capacity(entries.len());
550 for (name, value) in entries {
551 restored.push((name, restore_value_from_mat_load(value).await?));
552 }
553 Ok(restored)
554}
555
556#[derive(Clone, Copy, Debug)]
557enum Endian {
558 Little,
559 Big,
560}
561
562impl Endian {
563 fn read_u16(self, bytes: &[u8]) -> u16 {
564 match self {
565 Endian::Little => u16::from_le_bytes(bytes.try_into().unwrap()),
566 Endian::Big => u16::from_be_bytes(bytes.try_into().unwrap()),
567 }
568 }
569
570 fn read_i16(self, bytes: &[u8]) -> i16 {
571 match self {
572 Endian::Little => i16::from_le_bytes(bytes.try_into().unwrap()),
573 Endian::Big => i16::from_be_bytes(bytes.try_into().unwrap()),
574 }
575 }
576
577 fn read_u32(self, bytes: &[u8]) -> u32 {
578 match self {
579 Endian::Little => u32::from_le_bytes(bytes.try_into().unwrap()),
580 Endian::Big => u32::from_be_bytes(bytes.try_into().unwrap()),
581 }
582 }
583
584 fn read_i32(self, bytes: &[u8]) -> i32 {
585 match self {
586 Endian::Little => i32::from_le_bytes(bytes.try_into().unwrap()),
587 Endian::Big => i32::from_be_bytes(bytes.try_into().unwrap()),
588 }
589 }
590
591 fn read_u64(self, bytes: &[u8]) -> u64 {
592 match self {
593 Endian::Little => u64::from_le_bytes(bytes.try_into().unwrap()),
594 Endian::Big => u64::from_be_bytes(bytes.try_into().unwrap()),
595 }
596 }
597
598 fn read_i64(self, bytes: &[u8]) -> i64 {
599 match self {
600 Endian::Little => i64::from_le_bytes(bytes.try_into().unwrap()),
601 Endian::Big => i64::from_be_bytes(bytes.try_into().unwrap()),
602 }
603 }
604
605 fn read_f32(self, bytes: &[u8]) -> f32 {
606 match self {
607 Endian::Little => f32::from_le_bytes(bytes.try_into().unwrap()),
608 Endian::Big => f32::from_be_bytes(bytes.try_into().unwrap()),
609 }
610 }
611
612 fn read_f64(self, bytes: &[u8]) -> f64 {
613 match self {
614 Endian::Little => f64::from_le_bytes(bytes.try_into().unwrap()),
615 Endian::Big => f64::from_be_bytes(bytes.try_into().unwrap()),
616 }
617 }
618}
619
620fn read_mat_reader<R: Read>(reader: &mut R) -> BuiltinResult<Vec<(String, Value)>> {
621 let mut header = [0u8; MAT_HEADER_LEN];
622 reader.read_exact(&mut header).map_err(|err| {
623 load_error_with_source(
624 &LOAD_ERROR_IO,
625 format!("load: failed to read MAT-file header: {err}"),
626 err,
627 )
628 })?;
629
630 let description = String::from_utf8_lossy(&header[..116]);
631 if description.contains("MATLAB 7.3") || header.starts_with(b"\x89HDF\r\n\x1a\n") {
632 return Err(load_error(
633 "load: MATLAB v7.3 MAT-files are HDF5-backed and are not supported yet",
634 ));
635 }
636
637 let endian = match (header[126], header[127]) {
638 (b'I', b'M') => Endian::Little,
639 (b'M', b'I') => Endian::Big,
640 _ => return Err(load_error("load: file is not a MATLAB Level-5 MAT-file")),
641 };
642
643 read_variables_from_elements(reader, endian)
644}
645
646fn read_variables_from_elements<R: Read>(
647 reader: &mut R,
648 endian: Endian,
649) -> BuiltinResult<Vec<(String, Value)>> {
650 let mut variables = Vec::new();
651 while let Some(tagged) = read_tagged(reader, true, endian)? {
652 match tagged.data_type {
653 MI_MATRIX => {
654 let parsed = parse_matrix(&tagged.data, endian)?;
655 let value = mat_array_to_value(parsed.array)?;
656 variables.push((parsed.name, value));
657 }
658 MI_COMPRESSED => {
659 let mut decoder = ZlibDecoder::new(Cursor::new(tagged.data));
660 let mut inflated = Vec::new();
661 decoder.read_to_end(&mut inflated).map_err(|err| {
662 load_error_with_source(
663 &LOAD_ERROR_IO,
664 format!("load: failed to decompress MAT element: {err}"),
665 err,
666 )
667 })?;
668 let mut cursor = Cursor::new(inflated);
669 variables.extend(read_variables_from_elements(&mut cursor, endian)?);
670 }
671 _ => continue,
672 }
673 }
674 Ok(variables)
675}
676
677struct ParsedMatrix {
678 name: String,
679 array: MatArray,
680}
681
682fn parse_matrix(buffer: &[u8], endian: Endian) -> BuiltinResult<ParsedMatrix> {
683 let mut cursor = Cursor::new(buffer);
684
685 let flags = read_tagged(&mut cursor, false, endian)?
686 .ok_or_else(|| load_error("load: matrix element missing array flags"))?;
687 if flags.data_type != MI_UINT32 || flags.data.len() < 8 {
688 return Err(load_error("load: invalid array flags block"));
689 }
690 let flags0 = endian.read_u32(&flags.data[0..4]);
691 let class_code = flags0 & 0xFF;
692 let mut class = MatClass::from_class_code(class_code)
693 .ok_or_else(|| load_error("load: unsupported MATLAB class"))?;
694 let is_logical = (flags0 & FLAG_LOGICAL) != 0;
695 let has_imag = (flags0 & FLAG_COMPLEX) != 0;
696 if is_logical {
697 class = MatClass::Logical;
698 }
699
700 let dims_elem = read_tagged(&mut cursor, false, endian)?
701 .ok_or_else(|| load_error("load: matrix element missing dimensions"))?;
702 if dims_elem.data_type != MI_INT32 {
703 return Err(load_error("load: dimension block must use MI_INT32"));
704 }
705 if dims_elem.data.is_empty() || dims_elem.data.len() % 4 != 0 {
706 return Err(load_error("load: malformed dimension block"));
707 }
708 let mut dims = Vec::with_capacity(dims_elem.data.len() / 4);
709 for chunk in dims_elem.data.chunks_exact(4) {
710 let value = endian.read_i32(chunk);
711 if value < 0 {
712 return Err(load_error("load: negative dimensions are not supported"));
713 }
714 dims.push(value as usize);
715 }
716 if dims.is_empty() {
717 dims.push(1);
718 dims.push(1);
719 }
720
721 let name_elem = read_tagged(&mut cursor, false, endian)?
722 .ok_or_else(|| load_error("load: matrix element missing name"))?;
723 let name = match name_elem.data_type {
724 MI_INT8 | MI_UINT8 => bytes_to_string(&name_elem.data),
725 MI_UINT16 | MI_UTF16 => {
726 let mut bytes = Vec::with_capacity(name_elem.data.len());
727 for chunk in name_elem.data.chunks_exact(2) {
728 let code = endian.read_u16(chunk);
729 if code == 0 {
730 break;
731 }
732 if let Some(ch) = char::from_u32(code as u32) {
733 bytes.push(ch);
734 }
735 }
736 bytes.into_iter().collect()
737 }
738 _ => {
739 return Err(load_error("load: unsupported array name encoding"));
740 }
741 };
742
743 let array = match class {
744 MatClass::Double => parse_numeric_array(&mut cursor, class, dims, has_imag, endian)?,
745 MatClass::Single
746 | MatClass::Int8
747 | MatClass::UInt8
748 | MatClass::Int16
749 | MatClass::UInt16
750 | MatClass::Int32
751 | MatClass::UInt32
752 | MatClass::Int64
753 | MatClass::UInt64 => parse_numeric_array(&mut cursor, class, dims, has_imag, endian)?,
754 MatClass::Logical => parse_logical_array(&mut cursor, dims, endian)?,
755 MatClass::Char => parse_char_array(&mut cursor, dims, endian)?,
756 MatClass::Cell => parse_cell_array(&mut cursor, dims, endian)?,
757 MatClass::Struct => parse_struct(&mut cursor, dims, endian)?,
758 MatClass::Sparse => parse_sparse_array(&mut cursor, dims, has_imag, endian)?,
759 };
760
761 Ok(ParsedMatrix { name, array })
762}
763
764fn parse_numeric_array(
765 cursor: &mut Cursor<&[u8]>,
766 class: MatClass,
767 dims: Vec<usize>,
768 has_imag: bool,
769 endian: Endian,
770) -> BuiltinResult<MatArray> {
771 let real_elem = read_tagged(cursor, false, endian)?
772 .ok_or_else(|| load_error("load: numeric array missing real component"))?;
773 if class_is_integer(class) {
774 if has_imag {
775 return Err(load_error(
776 "load: complex integer MAT arrays require typed complex integer storage",
777 ));
778 }
779 return Ok(MatArray {
780 class,
781 dims,
782 data: MatData::Integer {
783 storage: decode_integer_storage(&real_elem, class, endian)?,
784 },
785 });
786 }
787
788 let real = decode_numeric_values(&real_elem, endian)?;
789
790 let imag = if has_imag {
791 let imag_elem = read_tagged(cursor, false, endian)?
792 .ok_or_else(|| load_error("load: numeric array missing imaginary component"))?;
793 Some(decode_numeric_values(&imag_elem, endian)?)
794 } else {
795 None
796 };
797
798 let data = if class == MatClass::Double {
799 MatData::Double { real, imag }
800 } else {
801 MatData::Numeric { real, imag }
802 };
803
804 Ok(MatArray { class, dims, data })
805}
806
807fn class_is_integer(class: MatClass) -> bool {
808 matches!(
809 class,
810 MatClass::Int8
811 | MatClass::UInt8
812 | MatClass::Int16
813 | MatClass::UInt16
814 | MatClass::Int32
815 | MatClass::UInt32
816 | MatClass::Int64
817 | MatClass::UInt64
818 )
819}
820
821fn decode_integer_storage(
822 elem: &TaggedData,
823 class: MatClass,
824 endian: Endian,
825) -> BuiltinResult<IntegerStorage> {
826 let expected_type = match class {
827 MatClass::Int8 => MI_INT8,
828 MatClass::UInt8 => MI_UINT8,
829 MatClass::Int16 => MI_INT16,
830 MatClass::UInt16 => MI_UINT16,
831 MatClass::Int32 => MI_INT32,
832 MatClass::UInt32 => MI_UINT32,
833 MatClass::Int64 => MI_INT64,
834 MatClass::UInt64 => MI_UINT64,
835 _ => return Err(load_error("load: expected an integer MAT class")),
836 };
837 if elem.data_type != expected_type {
838 return Err(load_error(format!(
839 "load: integer MAT class payload type {} does not match class {:?}",
840 elem.data_type, class
841 )));
842 }
843
844 let data = &elem.data;
845 match class {
846 MatClass::Int8 => Ok(IntegerStorage::I8(
847 data.iter().map(|value| *value as i8).collect(),
848 )),
849 MatClass::UInt8 => Ok(IntegerStorage::U8(data.clone())),
850 MatClass::Int16 => {
851 ensure_data_width(data, 2, "integer int16 data")?;
852 Ok(IntegerStorage::I16(
853 data.chunks_exact(2)
854 .map(|chunk| endian.read_i16(chunk))
855 .collect(),
856 ))
857 }
858 MatClass::UInt16 => {
859 ensure_data_width(data, 2, "integer uint16 data")?;
860 Ok(IntegerStorage::U16(
861 data.chunks_exact(2)
862 .map(|chunk| endian.read_u16(chunk))
863 .collect(),
864 ))
865 }
866 MatClass::Int32 => {
867 ensure_data_width(data, 4, "integer int32 data")?;
868 Ok(IntegerStorage::I32(
869 data.chunks_exact(4)
870 .map(|chunk| endian.read_i32(chunk))
871 .collect(),
872 ))
873 }
874 MatClass::UInt32 => {
875 ensure_data_width(data, 4, "integer uint32 data")?;
876 Ok(IntegerStorage::U32(
877 data.chunks_exact(4)
878 .map(|chunk| endian.read_u32(chunk))
879 .collect(),
880 ))
881 }
882 MatClass::Int64 => {
883 ensure_data_width(data, 8, "integer int64 data")?;
884 Ok(IntegerStorage::I64(
885 data.chunks_exact(8)
886 .map(|chunk| endian.read_i64(chunk))
887 .collect(),
888 ))
889 }
890 MatClass::UInt64 => {
891 ensure_data_width(data, 8, "integer uint64 data")?;
892 Ok(IntegerStorage::U64(
893 data.chunks_exact(8)
894 .map(|chunk| endian.read_u64(chunk))
895 .collect(),
896 ))
897 }
898 _ => Err(load_error("load: expected an integer MAT class")),
899 }
900}
901
902fn parse_logical_array(
903 cursor: &mut Cursor<&[u8]>,
904 dims: Vec<usize>,
905 endian: Endian,
906) -> BuiltinResult<MatArray> {
907 let elem = read_tagged(cursor, false, endian)?
908 .ok_or_else(|| load_error("load: logical array missing data block"))?;
909 let data = decode_numeric_values(&elem, endian)?
910 .into_iter()
911 .map(|v| if v != 0.0 { 1 } else { 0 })
912 .collect();
913 Ok(MatArray {
914 class: MatClass::Logical,
915 dims,
916 data: MatData::Logical { data },
917 })
918}
919
920fn parse_char_array(
921 cursor: &mut Cursor<&[u8]>,
922 dims: Vec<usize>,
923 endian: Endian,
924) -> BuiltinResult<MatArray> {
925 let elem = read_tagged(cursor, false, endian)?
926 .ok_or_else(|| load_error("load: character array missing data block"))?;
927 let data = decode_char_codes(&elem, endian)?;
928 Ok(MatArray {
929 class: MatClass::Char,
930 dims,
931 data: MatData::Char { data },
932 })
933}
934
935fn parse_cell_array(
936 cursor: &mut Cursor<&[u8]>,
937 dims: Vec<usize>,
938 endian: Endian,
939) -> BuiltinResult<MatArray> {
940 let total: usize = dims
941 .iter()
942 .copied()
943 .fold(1usize, |acc, d| acc.saturating_mul(d));
944 let mut elements = Vec::with_capacity(total);
945 for _ in 0..total {
946 let elem = read_tagged(cursor, false, endian)?
947 .ok_or_else(|| load_error("load: cell element missing matrix payload"))?;
948 if elem.data_type != MI_MATRIX {
949 return Err(load_error("load: cell elements must be matrices"));
950 }
951 let parsed = parse_matrix(&elem.data, endian)?;
952 elements.push(parsed.array);
953 }
954 Ok(MatArray {
955 class: MatClass::Cell,
956 dims,
957 data: MatData::Cell { elements },
958 })
959}
960
961fn parse_struct(
962 cursor: &mut Cursor<&[u8]>,
963 dims: Vec<usize>,
964 endian: Endian,
965) -> BuiltinResult<MatArray> {
966 if dims.len() != 2 || dims[0] != 1 || dims[1] != 1 {
967 return Err(load_error("load: struct arrays are not supported yet"));
968 }
969 let len_elem = read_tagged(cursor, false, endian)?
970 .ok_or_else(|| load_error("load: struct missing maximum field length specifier"))?;
971 if len_elem.data_type != MI_INT32 || len_elem.data.len() != 4 {
972 return Err(load_error("load: struct field length must be MI_INT32"));
973 }
974 let max_len = endian.read_i32(&len_elem.data[..4]);
975 if max_len <= 0 {
976 return Err(load_error("load: struct field length must be positive"));
977 }
978
979 let names_elem = read_tagged(cursor, false, endian)?
980 .ok_or_else(|| load_error("load: struct missing field name table"))?;
981 if names_elem.data_type != MI_INT8 && names_elem.data_type != MI_UINT8 {
982 return Err(load_error(
983 "load: struct field names must be stored as MI_INT8/MI_UINT8",
984 ));
985 }
986 if names_elem.data.len() % (max_len as usize) != 0 {
987 return Err(load_error("load: malformed struct field name table"));
988 }
989 let field_count = names_elem.data.len() / (max_len as usize);
990 let mut field_names = Vec::with_capacity(field_count);
991 for i in 0..field_count {
992 let start = i * (max_len as usize);
993 let end = start + (max_len as usize);
994 let slice = &names_elem.data[start..end];
995 field_names.push(bytes_to_string(slice));
996 }
997
998 let mut field_values = Vec::with_capacity(field_count);
999 for _ in 0..field_count {
1000 let elem = read_tagged(cursor, false, endian)?
1001 .ok_or_else(|| load_error("load: struct field missing matrix payload"))?;
1002 if elem.data_type != MI_MATRIX {
1003 return Err(load_error("load: struct fields must be matrices"));
1004 }
1005 let parsed = parse_matrix(&elem.data, endian)?;
1006 field_values.push(parsed.array);
1007 }
1008
1009 Ok(MatArray {
1010 class: MatClass::Struct,
1011 dims,
1012 data: MatData::Struct {
1013 field_names,
1014 field_values,
1015 },
1016 })
1017}
1018
1019fn parse_sparse_array(
1020 cursor: &mut Cursor<&[u8]>,
1021 dims: Vec<usize>,
1022 has_imag: bool,
1023 endian: Endian,
1024) -> BuiltinResult<MatArray> {
1025 if has_imag {
1026 return Err(load_error(
1027 "load: complex sparse MAT arrays are not supported yet",
1028 ));
1029 }
1030 let rows = dims.first().copied().unwrap_or(0);
1031 let cols = dims.get(1).copied().unwrap_or(0);
1032
1033 let ir_elem = read_tagged(cursor, false, endian)?
1034 .ok_or_else(|| load_error("load: sparse array missing row indices"))?;
1035 let row_indices = decode_index_values(&ir_elem, endian, "sparse row indices")?;
1036
1037 let jc_elem = read_tagged(cursor, false, endian)?
1038 .ok_or_else(|| load_error("load: sparse array missing column pointers"))?;
1039 let col_ptrs = decode_index_values(&jc_elem, endian, "sparse column pointers")?;
1040
1041 let real_elem = read_tagged(cursor, false, endian)?
1042 .ok_or_else(|| load_error("load: sparse array missing values"))?;
1043 let values = decode_numeric_values(&real_elem, endian)?;
1044
1045 Ok(MatArray {
1046 class: MatClass::Sparse,
1047 dims,
1048 data: MatData::Sparse {
1049 rows,
1050 cols,
1051 col_ptrs,
1052 row_indices,
1053 values,
1054 },
1055 })
1056}
1057
1058fn decode_numeric_values(elem: &TaggedData, endian: Endian) -> BuiltinResult<Vec<f64>> {
1059 let data = &elem.data;
1060 match elem.data_type {
1061 MI_INT8 => Ok(data.iter().map(|v| (*v as i8) as f64).collect()),
1062 MI_UINT8 => Ok(data.iter().map(|v| *v as f64).collect()),
1063 MI_INT16 => {
1064 ensure_data_width(data, 2, "numeric int16 data")?;
1065 Ok(data
1066 .chunks_exact(2)
1067 .map(|chunk| endian.read_i16(chunk) as f64)
1068 .collect())
1069 }
1070 MI_UINT16 => {
1071 ensure_data_width(data, 2, "numeric uint16 data")?;
1072 Ok(data
1073 .chunks_exact(2)
1074 .map(|chunk| endian.read_u16(chunk) as f64)
1075 .collect())
1076 }
1077 MI_INT32 => {
1078 ensure_data_width(data, 4, "numeric int32 data")?;
1079 Ok(data
1080 .chunks_exact(4)
1081 .map(|chunk| endian.read_i32(chunk) as f64)
1082 .collect())
1083 }
1084 MI_UINT32 => {
1085 ensure_data_width(data, 4, "numeric uint32 data")?;
1086 Ok(data
1087 .chunks_exact(4)
1088 .map(|chunk| endian.read_u32(chunk) as f64)
1089 .collect())
1090 }
1091 MI_SINGLE => {
1092 ensure_data_width(data, 4, "numeric single data")?;
1093 Ok(data
1094 .chunks_exact(4)
1095 .map(|chunk| endian.read_f32(chunk) as f64)
1096 .collect())
1097 }
1098 MI_DOUBLE => {
1099 ensure_data_width(data, 8, "numeric double data")?;
1100 Ok(data
1101 .chunks_exact(8)
1102 .map(|chunk| endian.read_f64(chunk))
1103 .collect())
1104 }
1105 MI_INT64 => {
1106 ensure_data_width(data, 8, "numeric int64 data")?;
1107 Ok(data
1108 .chunks_exact(8)
1109 .map(|chunk| endian.read_i64(chunk) as f64)
1110 .collect())
1111 }
1112 MI_UINT64 => {
1113 ensure_data_width(data, 8, "numeric uint64 data")?;
1114 Ok(data
1115 .chunks_exact(8)
1116 .map(|chunk| endian.read_u64(chunk) as f64)
1117 .collect())
1118 }
1119 _ => Err(load_error(format!(
1120 "load: unsupported numeric data type {}",
1121 elem.data_type
1122 ))),
1123 }
1124}
1125
1126fn decode_char_codes(elem: &TaggedData, endian: Endian) -> BuiltinResult<Vec<u16>> {
1127 match elem.data_type {
1128 MI_INT8 | MI_UINT8 | MI_UTF8 => {
1129 let text = String::from_utf8_lossy(&elem.data);
1130 Ok(text
1131 .chars()
1132 .map(|ch| {
1133 if ch as u32 <= u16::MAX as u32 {
1134 ch as u16
1135 } else {
1136 0xFFFD
1137 }
1138 })
1139 .collect())
1140 }
1141 MI_UINT16 | MI_UTF16 => {
1142 ensure_data_width(&elem.data, 2, "character UTF-16 data")?;
1143 Ok(elem
1144 .data
1145 .chunks_exact(2)
1146 .map(|chunk| endian.read_u16(chunk))
1147 .collect())
1148 }
1149 MI_UTF32 => {
1150 ensure_data_width(&elem.data, 4, "character UTF-32 data")?;
1151 Ok(elem
1152 .data
1153 .chunks_exact(4)
1154 .map(|chunk| {
1155 let code = endian.read_u32(chunk);
1156 if code <= u16::MAX as u32 {
1157 code as u16
1158 } else {
1159 0xFFFD
1160 }
1161 })
1162 .collect())
1163 }
1164 _ => Err(load_error("load: unsupported character data encoding")),
1165 }
1166}
1167
1168fn decode_index_values(
1169 elem: &TaggedData,
1170 endian: Endian,
1171 label: &str,
1172) -> BuiltinResult<Vec<usize>> {
1173 match elem.data_type {
1174 MI_INT32 => {
1175 ensure_data_width(&elem.data, 4, label)?;
1176 let mut out = Vec::with_capacity(elem.data.len() / 4);
1177 for chunk in elem.data.chunks_exact(4) {
1178 let value = endian.read_i32(chunk);
1179 if value < 0 {
1180 return Err(load_error(format!(
1181 "load: {label} contain a negative index"
1182 )));
1183 }
1184 out.push(value as usize);
1185 }
1186 Ok(out)
1187 }
1188 MI_UINT32 => {
1189 ensure_data_width(&elem.data, 4, label)?;
1190 Ok(elem
1191 .data
1192 .chunks_exact(4)
1193 .map(|chunk| endian.read_u32(chunk) as usize)
1194 .collect())
1195 }
1196 _ => Err(load_error(format!(
1197 "load: {label} must be MI_INT32/MI_UINT32"
1198 ))),
1199 }
1200}
1201
1202fn ensure_data_width(data: &[u8], width: usize, label: &str) -> BuiltinResult<()> {
1203 if !data.len().is_multiple_of(width) {
1204 return Err(load_error(format!("load: malformed {label}")));
1205 }
1206 Ok(())
1207}
1208
1209fn mat_array_to_value(array: MatArray) -> BuiltinResult<Value> {
1210 match array.data {
1211 MatData::Double { real, imag } => {
1212 let len = real.len();
1213 if let Some(imag) = imag {
1214 if imag.len() != len {
1215 return Err(load_error(
1216 "load: complex data has mismatched real/imag parts",
1217 ));
1218 }
1219 if len == 1 {
1220 Ok(Value::Complex(real[0], imag[0]))
1221 } else {
1222 let mut pairs = Vec::with_capacity(len);
1223 for i in 0..len {
1224 pairs.push((real[i], imag[i]));
1225 }
1226 let tensor = ComplexTensor::new(pairs, array.dims.clone())
1227 .map_err(|e| load_error(format!("load: {e}")))?;
1228 Ok(Value::ComplexTensor(tensor))
1229 }
1230 } else if len == 1 {
1231 Ok(Value::Num(real[0]))
1232 } else {
1233 let tensor = Tensor::new(real, array.dims.clone())
1234 .map_err(|e| load_error(format!("load: {e}")))?;
1235 Ok(Value::Tensor(tensor))
1236 }
1237 }
1238 MatData::Numeric { real, imag } => {
1239 let len = real.len();
1240 if let Some(imag) = imag {
1241 if imag.len() != len {
1242 return Err(load_error(
1243 "load: complex data has mismatched real/imag parts",
1244 ));
1245 }
1246 if len == 1 {
1247 return Ok(Value::Complex(real[0], imag[0]));
1248 }
1249 let pairs: Vec<(f64, f64)> = real.into_iter().zip(imag).collect();
1250 let tensor = ComplexTensor::new(pairs, array.dims.clone())
1251 .map_err(|e| load_error(format!("load: {e}")))?;
1252 return Ok(Value::ComplexTensor(tensor));
1253 }
1254
1255 if len == 1 {
1256 if let Some(value) = numeric_scalar_to_int_value(array.class, real[0]) {
1257 return Ok(value);
1258 }
1259 return Ok(Value::Num(real[0]));
1260 }
1261
1262 let dtype = numeric_tensor_dtype(array.class);
1263 let tensor = Tensor::new_with_dtype(real, array.dims.clone(), dtype)
1264 .map_err(|e| load_error(format!("load: {e}")))?;
1265 Ok(Value::Tensor(tensor))
1266 }
1267 MatData::Integer { storage } => {
1268 if storage.len() == 1 {
1269 return Ok(Value::Int(integer_storage_scalar(&storage, 0)));
1270 }
1271 let tensor = Tensor::new_integer(storage, array.dims.clone())
1272 .map_err(|e| load_error(format!("load: {e}")))?;
1273 Ok(Value::Tensor(tensor))
1274 }
1275 MatData::Logical { data } => {
1276 let total: usize = array
1277 .dims
1278 .iter()
1279 .copied()
1280 .fold(1usize, |acc, d| acc.saturating_mul(d));
1281 if data.len() != total {
1282 return Err(load_error("load: logical data length mismatch"));
1283 }
1284 if total == 1 {
1285 Ok(Value::Bool(data.first().copied().unwrap_or(0) != 0))
1286 } else {
1287 let logical = LogicalArray::new(data, array.dims.clone())
1288 .map_err(|e| load_error(format!("load: {e}")))?;
1289 Ok(Value::LogicalArray(logical))
1290 }
1291 }
1292 MatData::Char { data } => {
1293 let rows = array.dims.first().copied().unwrap_or(1);
1294 let cols = array.dims.get(1).copied().unwrap_or(1);
1295 let mut chars = Vec::with_capacity(rows.saturating_mul(cols));
1296 for code in data {
1297 let ch = char::from_u32(code as u32).unwrap_or('\u{FFFD}');
1298 chars.push(ch);
1299 }
1300 let char_array =
1301 CharArray::new(chars, rows, cols).map_err(|e| load_error(format!("load: {e}")))?;
1302 Ok(Value::CharArray(char_array))
1303 }
1304 MatData::Cell { elements } => {
1305 if let Some(strings) = cell_elements_to_strings(&elements) {
1306 let string_array = StringArray::new(strings, array.dims.clone())
1307 .map_err(|e| load_error(format!("load: {e}")))?;
1308 return Ok(Value::StringArray(string_array));
1309 }
1310 if array.dims.len() != 2 {
1311 return Err(load_error(
1312 "load: cell arrays with more than two dimensions are not supported yet",
1313 ));
1314 }
1315 let rows = array.dims[0];
1316 let cols = array.dims[1];
1317 let expected = rows.saturating_mul(cols);
1318 if elements.len() != expected {
1319 return Err(load_error("load: cell array element count mismatch"));
1320 }
1321 let mut converted = Vec::with_capacity(elements.len());
1322 for elem in elements {
1323 converted.push(mat_array_to_value(elem)?);
1324 }
1325 let mut row_major = vec![Value::Num(0.0); expected];
1326 for col in 0..cols {
1327 for row in 0..rows {
1328 let cm_idx = col * rows + row;
1329 let rm_idx = row * cols + col;
1330 row_major[rm_idx] = converted[cm_idx].clone();
1331 }
1332 }
1333 make_cell(row_major, rows, cols).map_err(|err| load_error(format!("load: {err}")))
1334 }
1335 MatData::Struct {
1336 field_names,
1337 field_values,
1338 } => {
1339 if field_names.len() != field_values.len() {
1340 return Err(load_error("load: struct field metadata is inconsistent"));
1341 }
1342 let mut st = StructValue::new();
1343 for (name, value) in field_names.into_iter().zip(field_values.into_iter()) {
1344 let converted = mat_array_to_value(value)?;
1345 st.fields.insert(name, converted);
1346 }
1347 Ok(Value::Struct(st))
1348 }
1349 MatData::Sparse {
1350 rows,
1351 cols,
1352 col_ptrs,
1353 row_indices,
1354 values,
1355 } => {
1356 let sparse = SparseTensor::new(rows, cols, col_ptrs, row_indices, values)
1357 .map_err(|e| load_error(format!("load: {e}")))?;
1358 Ok(Value::SparseTensor(sparse))
1359 }
1360 }
1361}
1362
1363fn integer_storage_scalar(storage: &IntegerStorage, index: usize) -> IntValue {
1364 match storage {
1365 IntegerStorage::I8(values) => IntValue::I8(values[index]),
1366 IntegerStorage::I16(values) => IntValue::I16(values[index]),
1367 IntegerStorage::I32(values) => IntValue::I32(values[index]),
1368 IntegerStorage::I64(values) => IntValue::I64(values[index]),
1369 IntegerStorage::U8(values) => IntValue::U8(values[index]),
1370 IntegerStorage::U16(values) => IntValue::U16(values[index]),
1371 IntegerStorage::U32(values) => IntValue::U32(values[index]),
1372 IntegerStorage::U64(values) => IntValue::U64(values[index]),
1373 }
1374}
1375
1376fn numeric_scalar_to_int_value(class: MatClass, value: f64) -> Option<Value> {
1377 match class {
1378 MatClass::Int8 => Some(Value::Int(IntValue::I8(value as i8))),
1379 MatClass::UInt8 => Some(Value::Int(IntValue::U8(value as u8))),
1380 MatClass::Int16 => Some(Value::Int(IntValue::I16(value as i16))),
1381 MatClass::UInt16 => Some(Value::Int(IntValue::U16(value as u16))),
1382 MatClass::Int32 => Some(Value::Int(IntValue::I32(value as i32))),
1383 MatClass::UInt32 => Some(Value::Int(IntValue::U32(value as u32))),
1384 MatClass::Int64 => Some(Value::Int(IntValue::I64(value as i64))),
1385 MatClass::UInt64 => Some(Value::Int(IntValue::U64(value as u64))),
1386 _ => None,
1387 }
1388}
1389
1390fn numeric_tensor_dtype(class: MatClass) -> NumericDType {
1391 match class {
1392 MatClass::Single => NumericDType::F32,
1393 MatClass::UInt8 => NumericDType::U8,
1394 MatClass::UInt16 => NumericDType::U16,
1395 _ => NumericDType::F64,
1396 }
1397}
1398
1399fn cell_elements_to_strings(elements: &[MatArray]) -> Option<Vec<String>> {
1400 let mut strings = Vec::with_capacity(elements.len());
1401 for element in elements {
1402 if element.class != MatClass::Char {
1403 return None;
1404 }
1405 let rows = element.dims.first().copied().unwrap_or(1);
1406 if rows > 1 {
1407 return None;
1408 }
1409 match &element.data {
1410 MatData::Char { data } => strings.push(utf16_codes_to_string(data)),
1411 _ => return None,
1412 }
1413 }
1414 Some(strings)
1415}
1416
1417fn utf16_codes_to_string(data: &[u16]) -> String {
1418 let mut chars: Vec<char> = data
1419 .iter()
1420 .map(|code| char::from_u32(*code as u32).unwrap_or('\u{FFFD}'))
1421 .collect();
1422 while matches!(chars.last(), Some(&'\0')) {
1423 chars.pop();
1424 }
1425 chars.into_iter().collect()
1426}
1427
1428fn option_token(value: &Value) -> BuiltinResult<Option<String>> {
1429 if let Some(token) = value_to_string_scalar(value) {
1430 if token.starts_with('-') {
1431 return Ok(Some(token.to_ascii_lowercase()));
1432 }
1433 }
1434 Ok(None)
1435}
1436
1437#[async_recursion::async_recursion(?Send)]
1438async fn extract_names(value: &Value) -> BuiltinResult<Vec<String>> {
1439 match value {
1440 Value::String(s) => Ok(vec![s.clone()]),
1441 Value::CharArray(ca) => Ok(char_array_rows_as_strings(ca)),
1442 Value::StringArray(sa) => Ok(sa.data.clone()),
1443 Value::Cell(ca) => {
1444 let mut names = Vec::with_capacity(ca.data.len());
1445 for handle in &ca.data {
1446 let inner = handle;
1447 let text = value_to_string_scalar(inner).ok_or_else(|| {
1448 load_error(
1449 "load: cell arrays used for variable selection must contain string scalars",
1450 )
1451 })?;
1452 names.push(text);
1453 }
1454 Ok(names)
1455 }
1456 other => {
1457 let gathered = gather_if_needed_async(other).await?;
1458 extract_names(&gathered).await
1459 }
1460 }
1461}
1462
1463fn value_to_string_scalar(value: &Value) -> Option<String> {
1464 match value {
1465 Value::String(s) => Some(s.clone()),
1466 Value::CharArray(ca) if ca.rows == 1 => Some(ca.data.iter().collect()),
1467 Value::StringArray(sa) if sa.data.len() == 1 => Some(sa.data[0].clone()),
1468 _ => None,
1469 }
1470}
1471
1472fn char_array_rows_as_strings(ca: &CharArray) -> Vec<String> {
1473 let mut rows = Vec::with_capacity(ca.rows);
1474 for r in 0..ca.rows {
1475 let mut row = String::with_capacity(ca.cols);
1476 for c in 0..ca.cols {
1477 let idx = r * ca.cols + c;
1478 row.push(ca.data[idx]);
1479 }
1480 let trimmed = row.trim_end_matches([' ', '\0']).to_string();
1481 rows.push(trimmed);
1482 }
1483 rows
1484}
1485
1486fn bytes_to_string(bytes: &[u8]) -> String {
1487 let trimmed = bytes
1488 .iter()
1489 .copied()
1490 .take_while(|b| *b != 0)
1491 .collect::<Vec<u8>>();
1492 String::from_utf8(trimmed).unwrap_or_default()
1493}
1494
1495struct TaggedData {
1496 data_type: u32,
1497 data: Vec<u8>,
1498}
1499
1500fn read_tagged<R: Read>(
1501 reader: &mut R,
1502 allow_eof: bool,
1503 endian: Endian,
1504) -> BuiltinResult<Option<TaggedData>> {
1505 let mut type_bytes = [0u8; 4];
1506 match reader.read_exact(&mut type_bytes) {
1507 Ok(()) => {}
1508 Err(err) => {
1509 if allow_eof && err.kind() == std::io::ErrorKind::UnexpectedEof {
1510 return Ok(None);
1511 }
1512 return Err(load_error_with_source(
1513 &LOAD_ERROR_IO,
1514 format!("load: failed to read MAT element header: {err}"),
1515 err,
1516 ));
1517 }
1518 }
1519
1520 if allow_eof && type_bytes == [0; 4] {
1521 return Ok(None);
1522 }
1523
1524 let type_field = endian.read_u32(&type_bytes);
1525 let high = (type_field >> 16) & 0xFFFF;
1526 let low = type_field & 0xFFFF;
1527 let small = match endian {
1528 Endian::Little if high != 0 => Some((low, high as usize)),
1529 Endian::Big if high != 0 && low <= 4 => Some((high, low as usize)),
1530 _ => None,
1531 };
1532
1533 if let Some((data_type, num_bytes)) = small {
1534 let mut inline = [0u8; 4];
1535 reader.read_exact(&mut inline).map_err(|err| {
1536 load_error_with_source(
1537 &LOAD_ERROR_IO,
1538 format!("load: failed to read compact MAT element: {err}"),
1539 err,
1540 )
1541 })?;
1542 let mut data = inline[..num_bytes.min(4)].to_vec();
1543 data.truncate(num_bytes.min(4));
1544 Ok(Some(TaggedData { data_type, data }))
1545 } else {
1546 let mut len_bytes = [0u8; 4];
1547 reader.read_exact(&mut len_bytes).map_err(|err| {
1548 load_error_with_source(
1549 &LOAD_ERROR_IO,
1550 format!("load: failed to read MAT element length: {err}"),
1551 err,
1552 )
1553 })?;
1554 let length = endian.read_u32(&len_bytes) as usize;
1555 let mut data = vec![0u8; length];
1556 reader.read_exact(&mut data).map_err(|err| {
1557 load_error_with_source(
1558 &LOAD_ERROR_IO,
1559 format!("load: failed to read MAT element body: {err}"),
1560 err,
1561 )
1562 })?;
1563 let padding = if type_field == MI_COMPRESSED {
1564 0
1565 } else {
1566 (8 - (length % 8)) % 8
1567 };
1568 if padding != 0 {
1569 let mut pad = vec![0u8; padding];
1570 reader.read_exact(&mut pad).map_err(|err| {
1571 load_error_with_source(
1572 &LOAD_ERROR_IO,
1573 format!("load: failed to read MAT padding: {err}"),
1574 err,
1575 )
1576 })?;
1577 }
1578 Ok(Some(TaggedData {
1579 data_type: type_field,
1580 data,
1581 }))
1582 }
1583}
1584
1585#[cfg(test)]
1586pub(crate) mod tests {
1587 use super::*;
1588 use crate::builtins::io::mat::save::encode_workspace_to_mat_bytes;
1589 use crate::workspace::WorkspaceResolver;
1590 use flate2::write::ZlibEncoder;
1591 use flate2::Compression;
1592 use futures::executor::block_on;
1593 use runmat_builtins::{IntegerStorage, StringArray};
1594 use runmat_thread_local::runmat_thread_local;
1595 use std::cell::RefCell;
1596 use std::collections::HashMap;
1597 use std::io::Write;
1598 use tempfile::tempdir;
1599
1600 runmat_thread_local! {
1601 static TEST_WORKSPACE: RefCell<HashMap<String, Value>> = RefCell::new(HashMap::new());
1602 }
1603
1604 fn ensure_test_resolver() {
1605 crate::workspace::register_workspace_resolver(WorkspaceResolver {
1606 lookup: |name| TEST_WORKSPACE.with(|slot| slot.borrow().get(name).cloned()),
1607 snapshot: || {
1608 let mut entries: Vec<(String, Value)> =
1609 TEST_WORKSPACE.with(|slot| slot.borrow().clone().into_iter().collect());
1610 entries.sort_by(|a, b| a.0.cmp(&b.0));
1611 entries
1612 },
1613 globals: || Vec::new(),
1614 assign: None,
1615 clear: None,
1616 remove: None,
1617 });
1618 }
1619
1620 fn set_workspace(entries: &[(&str, Value)]) {
1621 TEST_WORKSPACE.with(|slot| {
1622 let mut map = slot.borrow_mut();
1623 map.clear();
1624 for (name, value) in entries {
1625 map.insert((*name).to_string(), value.clone());
1626 }
1627 });
1628 }
1629
1630 fn workspace_guard() -> std::sync::MutexGuard<'static, ()> {
1631 crate::workspace::test_guard()
1632 }
1633
1634 fn assert_error_contains<T>(result: crate::BuiltinResult<T>, snippet: &str) {
1635 match result {
1636 Err(err) => {
1637 assert!(
1638 err.message().contains(snippet),
1639 "expected error to contain '{snippet}', got '{}'",
1640 err.message()
1641 );
1642 }
1643 Ok(_) => panic!("expected error containing '{snippet}'"),
1644 }
1645 }
1646
1647 fn wrap_payload_as_compressed(mat_bytes: &[u8]) -> Vec<u8> {
1648 let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
1649 encoder.write_all(&mat_bytes[MAT_HEADER_LEN..]).unwrap();
1650 let compressed = encoder.finish().unwrap();
1651
1652 let mut out = mat_bytes[..MAT_HEADER_LEN].to_vec();
1653 out.extend_from_slice(&MI_COMPRESSED.to_le_bytes());
1654 out.extend_from_slice(&(compressed.len() as u32).to_le_bytes());
1655 out.extend_from_slice(&compressed);
1656 out
1657 }
1658
1659 fn load_entries_from_bytes(bytes: Vec<u8>) -> Vec<(String, Value)> {
1660 let mut cursor = Cursor::new(bytes);
1661 read_mat_reader(&mut cursor).expect("read MAT bytes")
1662 }
1663
1664 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1665 #[test]
1666 fn load_descriptor_signatures_cover_core_forms() {
1667 let labels: Vec<&str> = LOAD_DESCRIPTOR
1668 .signatures
1669 .iter()
1670 .map(|sig| sig.label)
1671 .collect();
1672 assert!(labels.contains(&"S = load()"));
1673 assert!(labels.contains(&"S = load(filename)"));
1674 assert!(labels.contains(&"S = load(filename, varName1, varName2, ...)"));
1675 assert!(labels.contains(&"S = load(filename, \"-regexp\", pattern1, ...)"));
1676 }
1677
1678 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1679 #[test]
1680 fn load_roundtrip_numeric() {
1681 let _guard = workspace_guard();
1682 ensure_test_resolver();
1683 let tensor = Tensor::new(vec![1.0, 4.0, 2.0, 5.0], vec![2, 2]).unwrap();
1684 set_workspace(&[("A", Value::Tensor(tensor))]);
1685
1686 let dir = tempdir().unwrap();
1687 let path = dir.path().join("numeric.mat");
1688 let save_arg = Value::from(path.to_string_lossy().to_string());
1689 block_on(crate::call_builtin_async(
1690 "save",
1691 std::slice::from_ref(&save_arg),
1692 ))
1693 .unwrap();
1694
1695 let eval = block_on(evaluate(&[Value::from(path.to_string_lossy().to_string())]))
1696 .expect("load numeric");
1697 let struct_value = eval.first_output();
1698 match struct_value {
1699 Value::Struct(sv) => {
1700 assert!(sv.fields.contains_key("A"));
1701 match sv.fields.get("A").unwrap() {
1702 Value::Tensor(t) => {
1703 assert_eq!(t.shape, vec![2, 2]);
1704 assert_eq!(t.data, vec![1.0, 4.0, 2.0, 5.0]);
1705 }
1706 other => panic!("expected tensor, got {other:?}"),
1707 }
1708 }
1709 other => panic!("expected struct, got {other:?}"),
1710 }
1711 }
1712
1713 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1714 #[test]
1715 fn load_selected_variables() {
1716 let _guard = workspace_guard();
1717 ensure_test_resolver();
1718 set_workspace(&[("signal", Value::Num(42.0)), ("noise", Value::Num(5.0))]);
1719 let dir = tempdir().unwrap();
1720 let path = dir.path().join("selection.mat");
1721 let save_arg = Value::from(path.to_string_lossy().to_string());
1722 block_on(crate::call_builtin_async(
1723 "save",
1724 std::slice::from_ref(&save_arg),
1725 ))
1726 .unwrap();
1727
1728 let eval = block_on(evaluate(&[
1729 Value::from(path.to_string_lossy().to_string()),
1730 Value::from("signal"),
1731 ]))
1732 .expect("load selection");
1733 let vars = eval.variables();
1734 assert_eq!(vars.len(), 1);
1735 assert_eq!(vars[0].0, "signal");
1736 assert!(matches!(vars[0].1, Value::Num(42.0)));
1737 }
1738
1739 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1740 #[test]
1741 fn load_regex_selection() {
1742 let _guard = workspace_guard();
1743 ensure_test_resolver();
1744 set_workspace(&[
1745 ("w1", Value::Num(1.0)),
1746 ("w2", Value::Num(2.0)),
1747 ("bias", Value::Num(3.0)),
1748 ]);
1749 let dir = tempdir().unwrap();
1750 let path = dir.path().join("regex.mat");
1751 let save_arg = Value::from(path.to_string_lossy().to_string());
1752 block_on(crate::call_builtin_async(
1753 "save",
1754 std::slice::from_ref(&save_arg),
1755 ))
1756 .unwrap();
1757
1758 let eval = block_on(evaluate(&[
1759 Value::from(path.to_string_lossy().to_string()),
1760 Value::from("-regexp"),
1761 Value::from("^w\\d$"),
1762 ]))
1763 .expect("load regex");
1764 let mut names: Vec<_> = eval.variables().iter().map(|(n, _)| n.clone()).collect();
1765 names.sort();
1766 assert_eq!(names, vec!["w1".to_string(), "w2".to_string()]);
1767 }
1768
1769 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1770 #[test]
1771 fn load_missing_variable_errors() {
1772 let _guard = workspace_guard();
1773 ensure_test_resolver();
1774 set_workspace(&[("existing", Value::Num(7.0))]);
1775 let dir = tempdir().unwrap();
1776 let path = dir.path().join("missing.mat");
1777 let save_arg = Value::from(path.to_string_lossy().to_string());
1778 block_on(crate::call_builtin_async(
1779 "save",
1780 std::slice::from_ref(&save_arg),
1781 ))
1782 .unwrap();
1783
1784 assert_error_contains(
1785 block_on(evaluate(&[
1786 Value::from(path.to_string_lossy().to_string()),
1787 Value::from("missing"),
1788 ])),
1789 "variable 'missing' was not found",
1790 );
1791 }
1792
1793 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1794 #[test]
1795 fn load_string_array_roundtrip() {
1796 let _guard = workspace_guard();
1797 ensure_test_resolver();
1798 let strings = StringArray::new(vec!["foo".into(), "bar".into()], vec![1, 2]).unwrap();
1799 set_workspace(&[("labels", Value::StringArray(strings))]);
1800 let dir = tempdir().unwrap();
1801 let path = dir.path().join("strings.mat");
1802 let save_arg = Value::from(path.to_string_lossy().to_string());
1803 block_on(crate::call_builtin_async(
1804 "save",
1805 std::slice::from_ref(&save_arg),
1806 ))
1807 .unwrap();
1808
1809 let eval = block_on(evaluate(&[Value::from(path.to_string_lossy().to_string())]))
1810 .expect("load strings");
1811 let struct_value = eval.first_output();
1812 match struct_value {
1813 Value::Struct(sv) => {
1814 let value = sv
1815 .fields
1816 .get("labels")
1817 .expect("labels field missing in struct");
1818 match value {
1819 Value::StringArray(sa) => {
1820 assert_eq!(sa.shape, vec![1, 2]);
1821 assert_eq!(sa.data, vec![String::from("foo"), String::from("bar")]);
1822 }
1823 other => panic!("expected string array, got {other:?}"),
1824 }
1825 }
1826 other => panic!("expected struct, got {other:?}"),
1827 }
1828 }
1829
1830 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1831 #[test]
1832 fn load_compressed_level5_payload() {
1833 let mat_bytes = block_on(encode_workspace_to_mat_bytes(&[
1834 ("A".to_string(), Value::Num(123.0)),
1835 ("B".to_string(), Value::from("text")),
1836 ]))
1837 .unwrap();
1838 let compressed = wrap_payload_as_compressed(&mat_bytes);
1839
1840 let entries = load_entries_from_bytes(compressed);
1841 assert_eq!(entries.len(), 2);
1842 assert_eq!(entries[0].0, "A");
1843 assert!(matches!(entries[0].1, Value::Num(123.0)));
1844 assert_eq!(entries[1].0, "B");
1845 match &entries[1].1 {
1846 Value::CharArray(ca) => assert_eq!(ca.data.iter().collect::<String>(), "text"),
1847 other => panic!("expected char array, got {other:?}"),
1848 }
1849 }
1850
1851 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1852 #[test]
1853 fn load_preserves_supported_numeric_mat_classes() {
1854 let single = Tensor::new_with_dtype(vec![1.5, 2.5], vec![1, 2], NumericDType::F32)
1855 .expect("single tensor");
1856 let uint16 = Tensor::new_with_dtype(vec![10.0, 20.0], vec![1, 2], NumericDType::U16)
1857 .expect("uint16 tensor");
1858 let bytes = block_on(encode_workspace_to_mat_bytes(&[
1859 ("s".to_string(), Value::Tensor(single)),
1860 ("u16".to_string(), Value::Tensor(uint16)),
1861 ("i".to_string(), Value::Int(IntValue::I16(-7))),
1862 ]))
1863 .unwrap();
1864
1865 let entries = load_entries_from_bytes(bytes);
1866 let values: HashMap<_, _> = entries.into_iter().collect();
1867 match values.get("s").unwrap() {
1868 Value::Tensor(t) => {
1869 assert_eq!(t.dtype, NumericDType::F32);
1870 assert_eq!(t.data, vec![1.5, 2.5]);
1871 }
1872 other => panic!("expected tensor, got {other:?}"),
1873 }
1874 match values.get("u16").unwrap() {
1875 Value::Tensor(t) => {
1876 assert_eq!(
1877 t.integer_storage(),
1878 Some(&IntegerStorage::U16(vec![10, 20]))
1879 );
1880 assert_eq!(t.data, vec![10.0, 20.0]);
1881 }
1882 other => panic!("expected tensor, got {other:?}"),
1883 }
1884 assert_eq!(values.get("i"), Some(&Value::Int(IntValue::I16(-7))));
1885 }
1886
1887 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1888 #[test]
1889 fn load_save_roundtrip_preserves_every_integer_tensor_class_exactly() {
1890 let cases = vec![
1891 ("i8", IntegerStorage::I8(vec![i8::MIN, i8::MAX])),
1892 ("u8", IntegerStorage::U8(vec![0, u8::MAX])),
1893 ("i16", IntegerStorage::I16(vec![i16::MIN, i16::MAX])),
1894 ("u16", IntegerStorage::U16(vec![0, u16::MAX])),
1895 ("i32", IntegerStorage::I32(vec![i32::MIN, i32::MAX])),
1896 ("u32", IntegerStorage::U32(vec![0, u32::MAX])),
1897 ("i64", IntegerStorage::I64(vec![i64::MIN, i64::MAX])),
1898 ("u64", IntegerStorage::U64(vec![1_u64 << 63, u64::MAX])),
1899 ];
1900 let mut entries = Vec::new();
1901 for (name, storage) in &cases {
1902 let tensor = Tensor::new_integer(storage.clone(), vec![2, 1]).expect("integer tensor");
1903 entries.push(((*name).to_string(), Value::Tensor(tensor)));
1904 }
1905 let empty = Tensor::new_integer(IntegerStorage::U64(Vec::new()), vec![0, 2])
1906 .expect("empty integer tensor");
1907 entries.push(("empty_u64".to_string(), Value::Tensor(empty)));
1908 entries.push((
1909 "scalar_u64".to_string(),
1910 Value::Int(IntValue::U64(u64::MAX)),
1911 ));
1912
1913 let bytes = block_on(encode_workspace_to_mat_bytes(&entries)).expect("encode MAT bytes");
1914 let values: HashMap<_, _> = load_entries_from_bytes(bytes).into_iter().collect();
1915
1916 for (name, storage) in cases {
1917 match values.get(name).expect("loaded integer tensor") {
1918 Value::Tensor(tensor) => assert_eq!(tensor.integer_storage(), Some(&storage)),
1919 other => panic!("expected tensor for {name}, got {other:?}"),
1920 }
1921 }
1922 match values.get("empty_u64").expect("loaded empty tensor") {
1923 Value::Tensor(tensor) => {
1924 assert_eq!(tensor.shape, vec![0, 2]);
1925 assert_eq!(
1926 tensor.integer_storage(),
1927 Some(&IntegerStorage::U64(Vec::new()))
1928 );
1929 }
1930 other => panic!("expected empty tensor, got {other:?}"),
1931 }
1932 assert_eq!(
1933 values.get("scalar_u64"),
1934 Some(&Value::Int(IntValue::U64(u64::MAX)))
1935 );
1936 }
1937
1938 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1939 #[test]
1940 fn load_save_roundtrip_preserves_nested_integer_tensors() {
1941 let unsigned =
1942 Tensor::new_integer(IntegerStorage::U64(vec![1_u64 << 63, u64::MAX]), vec![1, 2])
1943 .expect("unsigned tensor");
1944 let signed = Tensor::new_integer(IntegerStorage::I64(vec![i64::MIN, i64::MAX]), vec![2, 1])
1945 .expect("signed tensor");
1946 let cell = make_cell(
1947 vec![
1948 Value::Tensor(unsigned.clone()),
1949 Value::Tensor(signed.clone()),
1950 ],
1951 1,
1952 2,
1953 )
1954 .expect("cell");
1955 let mut nested = StructValue::new();
1956 nested.insert("unsigned", Value::Tensor(unsigned));
1957 nested.insert("values", cell);
1958
1959 let bytes = block_on(encode_workspace_to_mat_bytes(&[(
1960 "nested".to_string(),
1961 Value::Struct(nested),
1962 )]))
1963 .expect("encode MAT bytes");
1964 let values: HashMap<_, _> = load_entries_from_bytes(bytes).into_iter().collect();
1965
1966 let Value::Struct(nested) = values.get("nested").expect("nested struct") else {
1967 panic!("expected struct");
1968 };
1969 match nested.fields.get("unsigned") {
1970 Some(Value::Tensor(tensor)) => assert_eq!(
1971 tensor.integer_storage(),
1972 Some(&IntegerStorage::U64(vec![1_u64 << 63, u64::MAX]))
1973 ),
1974 other => panic!("expected unsigned tensor, got {other:?}"),
1975 }
1976 match nested.fields.get("values") {
1977 Some(Value::Cell(cell)) => {
1978 match &cell.data[0] {
1979 Value::Tensor(tensor) => assert_eq!(
1980 tensor.integer_storage(),
1981 Some(&IntegerStorage::U64(vec![1_u64 << 63, u64::MAX]))
1982 ),
1983 other => panic!("expected unsigned cell tensor, got {other:?}"),
1984 }
1985 match &cell.data[1] {
1986 Value::Tensor(tensor) => assert_eq!(
1987 tensor.integer_storage(),
1988 Some(&IntegerStorage::I64(vec![i64::MIN, i64::MAX]))
1989 ),
1990 other => panic!("expected signed cell tensor, got {other:?}"),
1991 }
1992 }
1993 other => panic!("expected cell, got {other:?}"),
1994 }
1995 }
1996
1997 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1998 #[test]
1999 fn load_save_real_sparse_roundtrip() {
2000 let sparse = SparseTensor::new(3, 3, vec![0, 1, 1, 3], vec![1, 0, 2], vec![4.0, 5.0, 6.0])
2001 .expect("sparse");
2002 let bytes = block_on(encode_workspace_to_mat_bytes(&[(
2003 "S".to_string(),
2004 Value::SparseTensor(sparse.clone()),
2005 )]))
2006 .unwrap();
2007
2008 let entries = load_entries_from_bytes(bytes);
2009 assert_eq!(entries.len(), 1);
2010 assert_eq!(entries[0].0, "S");
2011 assert_eq!(entries[0].1, Value::SparseTensor(sparse));
2012 }
2013
2014 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2015 #[test]
2016 fn load_option_before_filename() {
2017 let _guard = workspace_guard();
2018 ensure_test_resolver();
2019 set_workspace(&[("alpha", Value::Num(1.0)), ("beta", Value::Num(2.0))]);
2020 let dir = tempdir().unwrap();
2021 let path = dir.path().join("option_first.mat");
2022 let save_arg = Value::from(path.to_string_lossy().to_string());
2023 block_on(crate::call_builtin_async(
2024 "save",
2025 std::slice::from_ref(&save_arg),
2026 ))
2027 .unwrap();
2028
2029 let eval = block_on(evaluate(&[
2030 Value::from("-mat"),
2031 Value::from(path.to_string_lossy().to_string()),
2032 Value::from("beta"),
2033 ]))
2034 .expect("load with option first");
2035 let vars = eval.variables();
2036 assert_eq!(vars.len(), 1);
2037 assert_eq!(vars[0].0, "beta");
2038 assert!(matches!(vars[0].1, Value::Num(2.0)));
2039 }
2040
2041 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2042 #[test]
2043 fn load_char_array_names_trimmed() {
2044 let _guard = workspace_guard();
2045 ensure_test_resolver();
2046 set_workspace(&[("short", Value::Num(5.0)), ("longer", Value::Num(9.0))]);
2047 let dir = tempdir().unwrap();
2048 let path = dir.path().join("char_names.mat");
2049 let save_arg = Value::from(path.to_string_lossy().to_string());
2050 block_on(crate::call_builtin_async(
2051 "save",
2052 std::slice::from_ref(&save_arg),
2053 ))
2054 .unwrap();
2055
2056 let cols = 6;
2057 let mut data = Vec::new();
2058 for name in ["short", "longer"] {
2059 let mut chars: Vec<char> = name.chars().collect();
2060 while chars.len() < cols {
2061 chars.push(' ');
2062 }
2063 data.extend(chars);
2064 }
2065 let name_array = CharArray::new(data, 2, cols).unwrap();
2066
2067 let eval = block_on(evaluate(&[
2068 Value::from(path.to_string_lossy().to_string()),
2069 Value::CharArray(name_array),
2070 ]))
2071 .expect("load with char array names");
2072 let vars = eval.variables();
2073 assert_eq!(vars.len(), 2);
2074 assert_eq!(vars[0].0, "short");
2075 assert!(matches!(vars[0].1, Value::Num(5.0)));
2076 assert_eq!(vars[1].0, "longer");
2077 assert!(matches!(vars[1].1, Value::Num(9.0)));
2078 }
2079
2080 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2081 #[test]
2082 fn load_duplicate_names_last_wins() {
2083 let _guard = workspace_guard();
2084 ensure_test_resolver();
2085 set_workspace(&[("dup", Value::Num(11.0))]);
2086 let dir = tempdir().unwrap();
2087 let path = dir.path().join("duplicates.mat");
2088 let save_arg = Value::from(path.to_string_lossy().to_string());
2089 block_on(crate::call_builtin_async(
2090 "save",
2091 std::slice::from_ref(&save_arg),
2092 ))
2093 .unwrap();
2094
2095 let eval = block_on(evaluate(&[
2096 Value::from(path.to_string_lossy().to_string()),
2097 Value::from("dup"),
2098 Value::from("dup"),
2099 ]))
2100 .expect("load with duplicate names");
2101 let vars = eval.variables();
2102 assert_eq!(vars.len(), 1);
2103 assert_eq!(vars[0].0, "dup");
2104 assert!(matches!(vars[0].1, Value::Num(11.0)));
2105 }
2106
2107 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2108 #[test]
2109 #[cfg(feature = "wgpu")]
2110 fn load_wgpu_tensor_roundtrip() {
2111 let _guard = workspace_guard();
2112 ensure_test_resolver();
2113 if runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
2114 runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
2115 )
2116 .is_err()
2117 {
2118 return;
2119 }
2120 let Some(provider) = runmat_accelerate_api::provider() else {
2121 return;
2122 };
2123
2124 use runmat_accelerate_api::HostTensorView;
2125
2126 let tensor = Tensor::new(vec![0.0, 1.0, 2.0, 3.0], vec![2, 2]).unwrap();
2127 let view = HostTensorView {
2128 data: &tensor.data,
2129 shape: &tensor.shape,
2130 };
2131 let handle = provider.upload(&view).expect("upload tensor");
2132 set_workspace(&[("gpu_var", Value::GpuTensor(handle))]);
2133
2134 let dir = tempdir().unwrap();
2135 let path = dir.path().join("wgpu_load.mat");
2136 let save_args = vec![
2137 Value::from(path.to_string_lossy().to_string()),
2138 Value::from("gpu_var"),
2139 ];
2140 block_on(crate::call_builtin_async("save", &save_args)).unwrap();
2141
2142 let eval = block_on(evaluate(&[Value::from(path.to_string_lossy().to_string())]))
2143 .expect("load wgpu file");
2144 let struct_value = eval.first_output();
2145 match struct_value {
2146 Value::Struct(sv) => match sv.fields.get("gpu_var") {
2147 Some(Value::Tensor(t)) => {
2148 assert_eq!(t.shape, vec![2, 2]);
2149 assert_eq!(t.data, tensor.data);
2150 }
2151 other => panic!("expected tensor, got {other:?}"),
2152 },
2153 other => panic!("expected struct, got {other:?}"),
2154 }
2155 }
2156}