1use std::io::{Seek, SeekFrom, Write};
3
4use runmat_builtins::{
5 BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinExtensionDescriptor,
6 BuiltinExtensionMode, BuiltinIntegerBackendRule, BuiltinIntegerCapabilityDescriptor,
7 BuiltinIntegerComputationDomain, BuiltinIntegerInputAvailability,
8 BuiltinIntegerInputCapability, BuiltinIntegerOutputClassRule, BuiltinIntegerOverflowRule,
9 BuiltinIntegerOverloadKind, BuiltinIntegerScalarDoubleRule, BuiltinOutputMode,
10 BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
11};
12use runmat_macros::runtime_builtin;
13use runmat_value::{CharArray, IntValue, NumericDType, Value};
14
15use crate::builtins::common::spec::{
16 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
17 ReductionNaN, ResidencyPolicy, ShapeRequirements,
18};
19use crate::builtins::common::tensor;
20use crate::builtins::io::filetext::registry;
21use crate::{build_runtime_error, gather_if_needed_async, BuiltinResult, RuntimeError};
22use runmat_filesystem::File;
23
24const FWRITE_GPU_INPUT_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
25 id: "fwrite-gpu-input",
26 mode: BuiltinExtensionMode::RunMatOnly,
27 description: "direct fwrite of gpuArray input is a RunMat extension",
28 error_identifier: Some("RunMat:compatibility:FwriteGpuInputExtension"),
29};
30const FWRITE_INTEGER_ID_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
31 id: "fwrite-integer-fileid",
32 mode: BuiltinExtensionMode::RunMatOnly,
33 description: "integer-class fwrite file identifiers are a RunMat extension",
34 error_identifier: Some("RunMat:compatibility:FwriteIntegerIdExtension"),
35};
36const FWRITE_INTEGER_SKIP_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
37 id: "fwrite-integer-skip",
38 mode: BuiltinExtensionMode::RunMatOnly,
39 description: "typed integer fwrite skip controls are a RunMat extension",
40 error_identifier: Some("RunMat:compatibility:FwriteIntegerSkipExtension"),
41};
42const FWRITE_LOGICAL_CONTROL_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
43 id: "fwrite-logical-control",
44 mode: BuiltinExtensionMode::RunMatOnly,
45 description: "logical fwrite file identifiers and skips are a RunMat extension",
46 error_identifier: Some("RunMat:compatibility:FwriteLogicalControlExtension"),
47};
48const FWRITE_SINGLE_CONTROL_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
49 id: "fwrite-single-control",
50 mode: BuiltinExtensionMode::RunMatOnly,
51 description: "single-precision fwrite file identifiers and skips are a RunMat extension",
52 error_identifier: Some("RunMat:compatibility:FwriteSingleControlExtension"),
53};
54const FWRITE_RESIDENT_CONTROL_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
55 id: "fwrite-resident-control",
56 mode: BuiltinExtensionMode::RunMatOnly,
57 description: "provider-resident fwrite control arguments are a RunMat extension",
58 error_identifier: Some("RunMat:compatibility:FwriteResidentControlExtension"),
59};
60const FWRITE_ARROW_PRECISION_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
61 id: "fwrite-arrow-precision",
62 mode: BuiltinExtensionMode::RunMatOnly,
63 description: "fread-style source=>output fwrite precision syntax is a RunMat extension",
64 error_identifier: Some("RunMat:compatibility:FwriteArrowPrecisionExtension"),
65};
66
67pub const FWRITE_EXTENSIONS: [BuiltinExtensionDescriptor; 7] = [
68 FWRITE_GPU_INPUT_EXTENSION,
69 FWRITE_INTEGER_ID_EXTENSION,
70 FWRITE_INTEGER_SKIP_EXTENSION,
71 FWRITE_LOGICAL_CONTROL_EXTENSION,
72 FWRITE_SINGLE_CONTROL_EXTENSION,
73 FWRITE_RESIDENT_CONTROL_EXTENSION,
74 FWRITE_ARROW_PRECISION_EXTENSION,
75];
76
77const FWRITE_INTEGER_DATA_INPUTS: [BuiltinIntegerInputCapability; 1] =
78 [BuiltinIntegerInputCapability {
79 name: "A",
80 classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
81 availability: BuiltinIntegerInputAvailability::Documented,
82 scalar_double: BuiltinIntegerScalarDoubleRule::Allowed,
83 notes: "The compatibility target documents all eight integer data classes; exact source values convert directly to the selected binary precision.",
84 }];
85const FWRITE_INTEGER_ID_INPUTS: [BuiltinIntegerInputCapability; 1] =
86 [BuiltinIntegerInputCapability {
87 name: "fileID",
88 classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
89 availability: BuiltinIntegerInputAvailability::RunMatOnly,
90 scalar_double: BuiltinIntegerScalarDoubleRule::Allowed,
91 notes: "The compatibility target documents double identifiers; typed integer identifiers are independently gated.",
92 }];
93const FWRITE_INTEGER_SKIP_INPUTS: [BuiltinIntegerInputCapability; 1] =
94 [BuiltinIntegerInputCapability {
95 name: "skip",
96 classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
97 availability: BuiltinIntegerInputAvailability::RunMatOnly,
98 scalar_double: BuiltinIntegerScalarDoubleRule::Allowed,
99 notes: "The compatibility target documents a double skip scalar; typed integer skips are independently gated and range-checked exactly.",
100 }];
101pub const INTEGER_CAPABILITIES: [BuiltinIntegerCapabilityDescriptor; 3] = [
102 BuiltinIntegerCapabilityDescriptor {
103 form: "count = fwrite(fileID, integer_A, precision, ...)",
104 inputs: &FWRITE_INTEGER_DATA_INPUTS,
105 computation_domain: BuiltinIntegerComputationDomain::ExactInteger,
106 output_class: BuiltinIntegerOutputClassRule::Double,
107 overflow: BuiltinIntegerOverflowRule::Saturate,
108 backend: BuiltinIntegerBackendRule::GatherFallback,
109 overload: BuiltinIntegerOverloadKind::FunctionSpecific,
110 notes: "Integer data remains authoritative through binary encoding; narrowing saturates and count is double.",
111 },
112 BuiltinIntegerCapabilityDescriptor {
113 form: "count = fwrite(integer_fileID, A, ...)",
114 inputs: &FWRITE_INTEGER_ID_INPUTS,
115 computation_domain: BuiltinIntegerComputationDomain::Structural,
116 output_class: BuiltinIntegerOutputClassRule::Double,
117 overflow: BuiltinIntegerOverflowRule::Error,
118 backend: BuiltinIntegerBackendRule::GatherFallback,
119 overload: BuiltinIntegerOverloadKind::ScalarOnly,
120 notes: "The identifier is validated exactly before registry access.",
121 },
122 BuiltinIntegerCapabilityDescriptor {
123 form: "count = fwrite(fileID, A, precision, integer_skip, ...)",
124 inputs: &FWRITE_INTEGER_SKIP_INPUTS,
125 computation_domain: BuiltinIntegerComputationDomain::Structural,
126 output_class: BuiltinIntegerOutputClassRule::Double,
127 overflow: BuiltinIntegerOverflowRule::Error,
128 backend: BuiltinIntegerBackendRule::GatherFallback,
129 overload: BuiltinIntegerOverloadKind::ScalarOnly,
130 notes: "The skip is validated exactly in the host seek domain.",
131 },
132];
133
134#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::io::filetext::fwrite")]
135pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
136 name: "fwrite",
137 op_kind: GpuOpKind::Custom("file-io-write"),
138 supported_precisions: &[],
139 broadcast: BroadcastSemantics::None,
140 provider_hooks: &[],
141 constant_strategy: ConstantStrategy::InlineLiteral,
142 residency: ResidencyPolicy::GatherImmediately,
143 nan_mode: ReductionNaN::Include,
144 two_pass_threshold: None,
145 workgroup_size: None,
146 accepts_nan_mode: false,
147 notes: "Host-only binary file I/O; GPU arguments are gathered to the CPU prior to writing.",
148};
149
150#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::io::filetext::fwrite")]
151pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
152 name: "fwrite",
153 shape: ShapeRequirements::Any,
154 constant_strategy: ConstantStrategy::InlineLiteral,
155 elementwise: None,
156 reduction: None,
157 emits_nan: false,
158 notes: "File I/O is never fused; metadata recorded for completeness.",
159};
160
161const BUILTIN_NAME: &str = "fwrite";
162
163const FWRITE_OUTPUT_COUNT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
164 name: "count",
165 ty: BuiltinParamType::NumericScalar,
166 arity: BuiltinParamArity::Required,
167 default: None,
168 description: "Number of elements successfully written.",
169}];
170const FWRITE_INPUTS_FID_DATA: [BuiltinParamDescriptor; 2] = [
171 BuiltinParamDescriptor {
172 name: "fid",
173 ty: BuiltinParamType::NumericScalar,
174 arity: BuiltinParamArity::Required,
175 default: None,
176 description: "File identifier opened by fopen.",
177 },
178 BuiltinParamDescriptor {
179 name: "data",
180 ty: BuiltinParamType::Any,
181 arity: BuiltinParamArity::Required,
182 default: None,
183 description: "Numeric/logical/text payload to write.",
184 },
185];
186const FWRITE_INPUTS_FID_DATA_PRECISION: [BuiltinParamDescriptor; 3] = [
187 BuiltinParamDescriptor {
188 name: "fid",
189 ty: BuiltinParamType::NumericScalar,
190 arity: BuiltinParamArity::Required,
191 default: None,
192 description: "File identifier opened by fopen.",
193 },
194 BuiltinParamDescriptor {
195 name: "data",
196 ty: BuiltinParamType::Any,
197 arity: BuiltinParamArity::Required,
198 default: None,
199 description: "Numeric/logical/text payload to write.",
200 },
201 BuiltinParamDescriptor {
202 name: "precision",
203 ty: BuiltinParamType::StringScalar,
204 arity: BuiltinParamArity::Optional,
205 default: Some("\"uint8\""),
206 description: "Write precision label (for example \"uint8\", \"double\").",
207 },
208];
209const FWRITE_INPUTS_FID_DATA_PRECISION_SKIP: [BuiltinParamDescriptor; 4] = [
210 BuiltinParamDescriptor {
211 name: "fid",
212 ty: BuiltinParamType::NumericScalar,
213 arity: BuiltinParamArity::Required,
214 default: None,
215 description: "File identifier opened by fopen.",
216 },
217 BuiltinParamDescriptor {
218 name: "data",
219 ty: BuiltinParamType::Any,
220 arity: BuiltinParamArity::Required,
221 default: None,
222 description: "Numeric/logical/text payload to write.",
223 },
224 BuiltinParamDescriptor {
225 name: "precision",
226 ty: BuiltinParamType::StringScalar,
227 arity: BuiltinParamArity::Optional,
228 default: Some("\"uint8\""),
229 description: "Write precision label (for example \"uint8\", \"double\").",
230 },
231 BuiltinParamDescriptor {
232 name: "skip",
233 ty: BuiltinParamType::NumericScalar,
234 arity: BuiltinParamArity::Optional,
235 default: Some("0"),
236 description: "Bytes skipped after each element written.",
237 },
238];
239const FWRITE_INPUTS_FID_DATA_PRECISION_MACHINEFMT: [BuiltinParamDescriptor; 4] = [
240 BuiltinParamDescriptor {
241 name: "fid",
242 ty: BuiltinParamType::NumericScalar,
243 arity: BuiltinParamArity::Required,
244 default: None,
245 description: "File identifier opened by fopen.",
246 },
247 BuiltinParamDescriptor {
248 name: "data",
249 ty: BuiltinParamType::Any,
250 arity: BuiltinParamArity::Required,
251 default: None,
252 description: "Numeric/logical/text payload to write.",
253 },
254 BuiltinParamDescriptor {
255 name: "precision",
256 ty: BuiltinParamType::StringScalar,
257 arity: BuiltinParamArity::Optional,
258 default: Some("\"uint8\""),
259 description: "Write precision label (for example \"uint8\", \"double\").",
260 },
261 BuiltinParamDescriptor {
262 name: "machinefmt",
263 ty: BuiltinParamType::StringScalar,
264 arity: BuiltinParamArity::Optional,
265 default: Some("\"native\""),
266 description: "Machine format label (native/little-endian/big-endian aliases).",
267 },
268];
269const FWRITE_INPUTS_FID_DATA_PRECISION_SKIP_MACHINEFMT: [BuiltinParamDescriptor; 5] = [
270 BuiltinParamDescriptor {
271 name: "fid",
272 ty: BuiltinParamType::NumericScalar,
273 arity: BuiltinParamArity::Required,
274 default: None,
275 description: "File identifier opened by fopen.",
276 },
277 BuiltinParamDescriptor {
278 name: "data",
279 ty: BuiltinParamType::Any,
280 arity: BuiltinParamArity::Required,
281 default: None,
282 description: "Numeric/logical/text payload to write.",
283 },
284 BuiltinParamDescriptor {
285 name: "precision",
286 ty: BuiltinParamType::StringScalar,
287 arity: BuiltinParamArity::Optional,
288 default: Some("\"uint8\""),
289 description: "Write precision label (for example \"uint8\", \"double\").",
290 },
291 BuiltinParamDescriptor {
292 name: "skip",
293 ty: BuiltinParamType::NumericScalar,
294 arity: BuiltinParamArity::Optional,
295 default: Some("0"),
296 description: "Bytes skipped after each element written.",
297 },
298 BuiltinParamDescriptor {
299 name: "machinefmt",
300 ty: BuiltinParamType::StringScalar,
301 arity: BuiltinParamArity::Optional,
302 default: Some("\"native\""),
303 description: "Machine format label (native/little-endian/big-endian aliases).",
304 },
305];
306const FWRITE_SIGNATURES: [BuiltinSignatureDescriptor; 5] = [
307 BuiltinSignatureDescriptor {
308 label: "count = fwrite(fid, data)",
309 inputs: &FWRITE_INPUTS_FID_DATA,
310 outputs: &FWRITE_OUTPUT_COUNT,
311 },
312 BuiltinSignatureDescriptor {
313 label: "count = fwrite(fid, data, precision)",
314 inputs: &FWRITE_INPUTS_FID_DATA_PRECISION,
315 outputs: &FWRITE_OUTPUT_COUNT,
316 },
317 BuiltinSignatureDescriptor {
318 label: "count = fwrite(fid, data, precision, skip)",
319 inputs: &FWRITE_INPUTS_FID_DATA_PRECISION_SKIP,
320 outputs: &FWRITE_OUTPUT_COUNT,
321 },
322 BuiltinSignatureDescriptor {
323 label: "count = fwrite(fid, data, precision, machinefmt)",
324 inputs: &FWRITE_INPUTS_FID_DATA_PRECISION_MACHINEFMT,
325 outputs: &FWRITE_OUTPUT_COUNT,
326 },
327 BuiltinSignatureDescriptor {
328 label: "count = fwrite(fid, data, precision, skip, machinefmt)",
329 inputs: &FWRITE_INPUTS_FID_DATA_PRECISION_SKIP_MACHINEFMT,
330 outputs: &FWRITE_OUTPUT_COUNT,
331 },
332];
333
334const FWRITE_ERROR_INVALID_INPUT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
335 code: "RM.FWRITE.INVALID_INPUT",
336 identifier: Some("RunMat:fwrite:InvalidInput"),
337 when: "Identifier, payload, or argument cardinality/type constraints are violated.",
338 message: "fwrite: invalid input arguments",
339};
340const FWRITE_ERROR_INVALID_IDENTIFIER: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
341 code: "RM.FWRITE.INVALID_IDENTIFIER",
342 identifier: Some("RunMat:fwrite:InvalidIdentifier"),
343 when: "Identifier does not refer to a writable open file.",
344 message: "fwrite: invalid file identifier. Use fopen to generate a valid file ID.",
345};
346const FWRITE_ERROR_INVALID_OPTION: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
347 code: "RM.FWRITE.INVALID_OPTION",
348 identifier: Some("RunMat:fwrite:InvalidOption"),
349 when: "Precision, skip, or machine format options are invalid.",
350 message: "fwrite: invalid option configuration",
351};
352const FWRITE_ERROR_IO: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
353 code: "RM.FWRITE.IO",
354 identifier: Some("RunMat:fwrite:IoFailure"),
355 when: "Write/seek operation fails.",
356 message: "fwrite: file write failed",
357};
358const FWRITE_ERROR_INTERNAL: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
359 code: "RM.FWRITE.INTERNAL",
360 identifier: None,
361 when: "Internal runtime control-flow conversion fails.",
362 message: "fwrite: internal error",
363};
364const FWRITE_ERRORS: [BuiltinErrorDescriptor; 5] = [
365 FWRITE_ERROR_INVALID_INPUT,
366 FWRITE_ERROR_INVALID_IDENTIFIER,
367 FWRITE_ERROR_INVALID_OPTION,
368 FWRITE_ERROR_IO,
369 FWRITE_ERROR_INTERNAL,
370];
371pub const FWRITE_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
372 signatures: &FWRITE_SIGNATURES,
373 output_mode: BuiltinOutputMode::Fixed,
374 completion_policy: BuiltinCompletionPolicy::Public,
375 errors: &FWRITE_ERRORS,
376};
377
378fn fwrite_error_with_detail(
379 error: &'static BuiltinErrorDescriptor,
380 detail: impl AsRef<str>,
381) -> RuntimeError {
382 let detail = detail.as_ref();
383 let detail = detail.strip_prefix("fwrite: ").unwrap_or(detail);
384 fwrite_error_with_message(format!("{}: {}", error.message, detail), error)
385}
386
387fn fwrite_error_with_message(
388 message: impl Into<String>,
389 error: &'static BuiltinErrorDescriptor,
390) -> RuntimeError {
391 let mut builder = build_runtime_error(message).with_builtin(BUILTIN_NAME);
392 if let Some(identifier) = error.identifier {
393 builder = builder.with_identifier(identifier);
394 }
395 builder.build()
396}
397
398fn map_control_flow(err: RuntimeError) -> RuntimeError {
399 let mut builder = build_runtime_error(format!("{BUILTIN_NAME}: {}", err.message()))
400 .with_builtin(BUILTIN_NAME)
401 .with_source(err);
402 if let Some(identifier) = FWRITE_ERROR_INTERNAL.identifier {
403 builder = builder.with_identifier(identifier);
404 }
405 builder.build()
406}
407
408fn map_string_result<T>(
409 result: Result<T, String>,
410 error: &'static BuiltinErrorDescriptor,
411) -> BuiltinResult<T> {
412 result.map_err(|detail| fwrite_error_with_detail(error, detail))
413}
414
415#[runtime_builtin(
416 name = "fwrite",
417 category = "io/filetext",
418 summary = "Write binary data to file identifiers.",
419 keywords = "fwrite,file,io,binary,precision",
420 accel = "cpu",
421 type_resolver(crate::builtins::io::type_resolvers::fwrite_type),
422 descriptor(crate::builtins::io::filetext::fwrite::FWRITE_DESCRIPTOR),
423 extensions(crate::builtins::io::filetext::fwrite::FWRITE_EXTENSIONS),
424 integer_capabilities(crate::builtins::io::filetext::fwrite::INTEGER_CAPABILITIES),
425 builtin_path = "crate::builtins::io::filetext::fwrite"
426)]
427async fn fwrite_builtin(fid: Value, data: Value, rest: Vec<Value>) -> crate::BuiltinResult<Value> {
428 let eval = evaluate(&fid, &data, &rest).await?;
429 Ok(Value::Num(eval.count as f64))
430}
431
432#[derive(Debug, Clone)]
434pub struct FwriteEval {
435 count: usize,
436}
437
438impl FwriteEval {
439 fn new(count: usize) -> Self {
440 Self { count }
441 }
442
443 pub fn count(&self) -> usize {
445 self.count
446 }
447}
448
449pub async fn evaluate(
451 fid_value: &Value,
452 data_value: &Value,
453 rest: &[Value],
454) -> BuiltinResult<FwriteEval> {
455 if matches!(data_value, Value::GpuTensor(_)) {
456 crate::compatibility::ensure_builtin_extension_enabled(
457 &FWRITE_GPU_INPUT_EXTENSION,
458 BUILTIN_NAME,
459 )?;
460 }
461 preflight_control(fid_value, ControlRole::FileId)?;
462 let raw_refs: Vec<Value> = rest.to_vec();
463 let (raw_precision, raw_skip, _) =
464 map_string_result(classify_arguments(&raw_refs), &FWRITE_ERROR_INVALID_INPUT)?;
465 if let Some(skip) = raw_skip {
466 preflight_control(skip, ControlRole::Skip)?;
467 }
468 if let Some(precision) = raw_precision {
469 if scalar_string(
470 precision,
471 "fwrite: precision argument must be a string scalar or character vector",
472 )
473 .is_ok_and(|value| value.contains("=>"))
474 {
475 crate::compatibility::ensure_builtin_extension_enabled(
476 &FWRITE_ARROW_PRECISION_EXTENSION,
477 BUILTIN_NAME,
478 )?;
479 }
480 }
481 let fid_host = gather_value(fid_value).await?;
482 let fid = map_string_result(parse_fid(&fid_host), &FWRITE_ERROR_INVALID_INPUT)?;
483 if fid < 0 {
484 return Err(fwrite_error_with_detail(
485 &FWRITE_ERROR_INVALID_INPUT,
486 "file identifier must be non-negative",
487 ));
488 }
489 if fid < 3 {
490 return Err(fwrite_error_with_detail(
491 &FWRITE_ERROR_INVALID_INPUT,
492 "standard input/output identifiers are not supported yet",
493 ));
494 }
495
496 let info = registry::info_for(fid).ok_or_else(|| {
497 fwrite_error_with_message(
498 FWRITE_ERROR_INVALID_IDENTIFIER.message,
499 &FWRITE_ERROR_INVALID_IDENTIFIER,
500 )
501 })?;
502 let handle = registry::shared_handle(fid).ok_or_else(|| {
503 fwrite_error_with_message(
504 FWRITE_ERROR_INVALID_IDENTIFIER.message,
505 &FWRITE_ERROR_INVALID_IDENTIFIER,
506 )
507 })?;
508
509 let data_host = gather_value(data_value).await?;
510 let rest_host = gather_args(rest).await?;
511 let (precision_arg, skip_arg, machine_arg) =
512 map_string_result(classify_arguments(&rest_host), &FWRITE_ERROR_INVALID_INPUT)?;
513
514 let precision_spec =
515 map_string_result(parse_precision(precision_arg), &FWRITE_ERROR_INVALID_OPTION)?;
516 let skip_bytes = map_string_result(parse_skip(skip_arg), &FWRITE_ERROR_INVALID_OPTION)?;
517 let machine_format = map_string_result(
518 parse_machine_format(machine_arg, &info.machinefmt),
519 &FWRITE_ERROR_INVALID_OPTION,
520 )?;
521
522 let mut guard = handle.lock().map_err(|_| {
523 fwrite_error_with_detail(
524 &FWRITE_ERROR_INTERNAL,
525 "failed to lock file handle (poisoned mutex)",
526 )
527 })?;
528 let file = guard.as_mut().ok_or_else(|| {
529 fwrite_error_with_message(
530 FWRITE_ERROR_INVALID_IDENTIFIER.message,
531 &FWRITE_ERROR_INVALID_IDENTIFIER,
532 )
533 })?;
534
535 let elements = map_string_result(flatten_elements(&data_host), &FWRITE_ERROR_INVALID_INPUT)?;
536 let count = map_string_result(
537 write_elements(file, &elements, precision_spec, skip_bytes, machine_format),
538 &FWRITE_ERROR_IO,
539 )?;
540 Ok(FwriteEval::new(count))
541}
542
543async fn gather_value(value: &Value) -> BuiltinResult<Value> {
544 gather_if_needed_async(value)
545 .await
546 .map_err(map_control_flow)
547}
548
549async fn gather_args(args: &[Value]) -> BuiltinResult<Vec<Value>> {
550 let mut gathered = Vec::with_capacity(args.len());
551 for value in args {
552 gathered.push(
553 gather_if_needed_async(value)
554 .await
555 .map_err(map_control_flow)?,
556 );
557 }
558 Ok(gathered)
559}
560
561#[derive(Clone, Copy)]
562enum ControlRole {
563 FileId,
564 Skip,
565}
566
567fn preflight_control(value: &Value, role: ControlRole) -> BuiltinResult<()> {
568 let integer_extension = match role {
569 ControlRole::FileId => &FWRITE_INTEGER_ID_EXTENSION,
570 ControlRole::Skip => &FWRITE_INTEGER_SKIP_EXTENSION,
571 };
572 match value {
573 Value::Int(_) => {
574 crate::compatibility::ensure_builtin_extension_enabled(integer_extension, BUILTIN_NAME)
575 }
576 Value::Tensor(tensor) if tensor.integer_storage().is_some() => {
577 crate::compatibility::ensure_builtin_extension_enabled(integer_extension, BUILTIN_NAME)
578 }
579 Value::Bool(_) | Value::LogicalArray(_) => {
580 crate::compatibility::ensure_builtin_extension_enabled(
581 &FWRITE_LOGICAL_CONTROL_EXTENSION,
582 BUILTIN_NAME,
583 )
584 }
585 Value::Tensor(tensor) if tensor.numeric_dtype() == NumericDType::F32 => {
586 crate::compatibility::ensure_builtin_extension_enabled(
587 &FWRITE_SINGLE_CONTROL_EXTENSION,
588 BUILTIN_NAME,
589 )
590 }
591 Value::GpuTensor(handle) => {
592 crate::compatibility::ensure_builtin_extension_enabled(
593 &FWRITE_RESIDENT_CONTROL_EXTENSION,
594 BUILTIN_NAME,
595 )?;
596 if runmat_accelerate_api::handle_is_logical(handle) {
597 crate::compatibility::ensure_builtin_extension_enabled(
598 &FWRITE_LOGICAL_CONTROL_EXTENSION,
599 BUILTIN_NAME,
600 )?;
601 } else if runmat_accelerate_api::handle_integer_type(handle).is_some() {
602 crate::compatibility::ensure_builtin_extension_enabled(
603 integer_extension,
604 BUILTIN_NAME,
605 )?;
606 } else if runmat_accelerate_api::handle_precision(handle)
607 == Some(runmat_accelerate_api::ProviderPrecision::F32)
608 {
609 crate::compatibility::ensure_builtin_extension_enabled(
610 &FWRITE_SINGLE_CONTROL_EXTENSION,
611 BUILTIN_NAME,
612 )?;
613 }
614 Ok(())
615 }
616 _ => Ok(()),
617 }
618}
619
620fn parse_fid(value: &Value) -> Result<i32, String> {
621 let scalar = match value {
622 Value::Num(n) => *n,
623 Value::Int(int) => {
624 return int
625 .try_to_i32()
626 .ok_or_else(|| "fwrite: file identifier is out of range".to_string());
627 }
628 Value::Tensor(t) if tensor::is_scalar_tensor(t) => {
629 if let Some(int) = t.integer_storage().and_then(|storage| storage.value_at(0)) {
630 return int
631 .try_to_i32()
632 .ok_or_else(|| "fwrite: file identifier is out of range".to_string());
633 }
634 tensor::tensor_value_f64(t, 0)
635 }
636 _ => return Err("fwrite: file identifier must be numeric".to_string()),
637 };
638 if !scalar.is_finite() {
639 return Err("fwrite: file identifier must be finite".to_string());
640 }
641 if scalar.fract().abs() > f64::EPSILON {
642 return Err("fwrite: file identifier must be an integer".to_string());
643 }
644 if scalar < i32::MIN as f64 || scalar > i32::MAX as f64 {
645 return Err("fwrite: file identifier is out of range".to_string());
646 }
647 Ok(scalar as i32)
648}
649
650type FwriteArgs<'a> = (Option<&'a Value>, Option<&'a Value>, Option<&'a Value>);
651
652fn classify_arguments(args: &[Value]) -> Result<FwriteArgs<'_>, String> {
653 match args.len() {
654 0 => Ok((None, None, None)),
655 1 => {
656 if is_string_like(&args[0]) {
657 Ok((Some(&args[0]), None, None))
658 } else {
659 Err(
660 "fwrite: precision argument must be a string scalar or character vector"
661 .to_string(),
662 )
663 }
664 }
665 2 => {
666 if !is_string_like(&args[0]) {
667 return Err(
668 "fwrite: precision argument must be a string scalar or character vector"
669 .to_string(),
670 );
671 }
672 if is_numeric_like(&args[1]) {
673 Ok((Some(&args[0]), Some(&args[1]), None))
674 } else if is_string_like(&args[1]) {
675 Ok((Some(&args[0]), None, Some(&args[1])))
676 } else {
677 Err("fwrite: invalid argument combination (expected numeric skip or machine format string)".to_string())
678 }
679 }
680 3 => {
681 if !is_string_like(&args[0]) || !is_numeric_like(&args[1]) || !is_string_like(&args[2])
682 {
683 return Err("fwrite: expected arguments (precision, skip, machinefmt)".to_string());
684 }
685 Ok((Some(&args[0]), Some(&args[1]), Some(&args[2])))
686 }
687 _ => Err("fwrite: too many input arguments".to_string()),
688 }
689}
690
691fn is_string_like(value: &Value) -> bool {
692 match value {
693 Value::String(_) => true,
694 Value::CharArray(ca) => ca.rows == 1,
695 Value::StringArray(sa) => sa.data.len() == 1,
696 _ => false,
697 }
698}
699
700fn is_numeric_like(value: &Value) -> bool {
701 match value {
702 Value::Num(_) | Value::Int(_) | Value::Bool(_) => true,
703 Value::Tensor(t) => tensor::is_scalar_tensor(t),
704 Value::LogicalArray(la) => la.data.len() == 1,
705 Value::GpuTensor(handle) => handle.shape.iter().product::<usize>() == 1,
706 _ => false,
707 }
708}
709
710#[derive(Clone, Copy, Debug)]
711struct WriteSpec {
712 input: InputType,
713}
714
715impl WriteSpec {
716 fn default() -> Self {
717 Self {
718 input: InputType::UInt8,
719 }
720 }
721}
722
723fn parse_precision(arg: Option<&Value>) -> Result<WriteSpec, String> {
724 match arg {
725 None => Ok(WriteSpec::default()),
726 Some(value) => {
727 let text = scalar_string(
728 value,
729 "fwrite: precision argument must be a string scalar or character vector",
730 )?;
731 parse_precision_string(&text)
732 }
733 }
734}
735
736fn parse_precision_string(raw: &str) -> Result<WriteSpec, String> {
737 let trimmed = raw.trim();
738 if trimmed.is_empty() {
739 return Err("fwrite: precision argument must not be empty".to_string());
740 }
741 let lower = trimmed.to_ascii_lowercase();
742 if let Some((lhs, rhs)) = lower.split_once("=>") {
743 let lhs = lhs.trim();
744 let rhs = rhs.trim();
745 let input = parse_input_label(lhs)?;
746 let output = parse_input_label(rhs)?;
747 if input != output {
748 return Err(
749 "fwrite: differing input/output precisions are not implemented yet".to_string(),
750 );
751 }
752 Ok(WriteSpec { input })
753 } else {
754 parse_input_label(lower.trim()).map(|input| WriteSpec { input })
755 }
756}
757
758fn parse_skip(arg: Option<&Value>) -> Result<usize, String> {
759 match arg {
760 None => Ok(0),
761 Some(Value::Int(int)) => int_to_skip(int),
762 Some(Value::Tensor(t)) if tensor::is_scalar_tensor(t) => {
763 if let Some(int) = t.integer_storage().and_then(|storage| storage.value_at(0)) {
764 return int_to_skip(&int);
765 }
766 parse_skip_scalar(tensor::tensor_value_f64(t, 0))
767 }
768 Some(value) => {
769 let scalar = numeric_scalar(value, "fwrite: skip must be numeric")?;
770 parse_skip_scalar(scalar)
771 }
772 }
773}
774
775fn parse_skip_scalar(scalar: f64) -> Result<usize, String> {
776 if !scalar.is_finite() {
777 return Err("fwrite: skip value must be finite".to_string());
778 }
779 if scalar < 0.0 {
780 return Err("fwrite: skip value must be non-negative".to_string());
781 }
782 let rounded = scalar.round();
783 if (rounded - scalar).abs() > f64::EPSILON {
784 return Err("fwrite: skip value must be an integer".to_string());
785 }
786 if rounded >= i64::MAX as f64 {
787 return Err("fwrite: skip value is too large".to_string());
788 }
789 Ok(rounded as usize)
790}
791
792fn int_to_skip(value: &IntValue) -> Result<usize, String> {
793 let Some(skip) = value.try_to_usize() else {
794 return Err("fwrite: skip value must be non-negative".to_string());
795 };
796 if skip > i64::MAX as usize {
797 return Err("fwrite: skip value is too large".to_string());
798 }
799 Ok(skip)
800}
801
802#[derive(Clone, Copy, Debug)]
803enum MachineFormat {
804 Native,
805 LittleEndian,
806 BigEndian,
807}
808
809impl MachineFormat {
810 fn to_endianness(self) -> Endianness {
811 match self {
812 MachineFormat::Native => {
813 if cfg!(target_endian = "little") {
814 Endianness::Little
815 } else {
816 Endianness::Big
817 }
818 }
819 MachineFormat::LittleEndian => Endianness::Little,
820 MachineFormat::BigEndian => Endianness::Big,
821 }
822 }
823}
824
825#[derive(Clone, Copy, Debug)]
826enum Endianness {
827 Little,
828 Big,
829}
830
831fn parse_machine_format(arg: Option<&Value>, default_label: &str) -> Result<MachineFormat, String> {
832 match arg {
833 Some(value) => {
834 let text = scalar_string(
835 value,
836 "fwrite: machine format must be a string scalar or character vector",
837 )?;
838 machine_format_from_label(&text)
839 }
840 None => machine_format_from_label(default_label),
841 }
842}
843
844fn machine_format_from_label(label: &str) -> Result<MachineFormat, String> {
845 let trimmed = label.trim();
846 if trimmed.is_empty() {
847 return Err("fwrite: machine format must not be empty".to_string());
848 }
849 let lower = trimmed.to_ascii_lowercase();
850 let collapsed: String = lower
851 .chars()
852 .filter(|c| !matches!(c, '-' | '_' | ' '))
853 .collect();
854 if matches!(collapsed.as_str(), "native" | "n" | "system" | "default") {
855 return Ok(MachineFormat::Native);
856 }
857 if matches!(
858 collapsed.as_str(),
859 "l" | "le" | "littleendian" | "pc" | "intel"
860 ) {
861 return Ok(MachineFormat::LittleEndian);
862 }
863 if matches!(
864 collapsed.as_str(),
865 "b" | "be" | "bigendian" | "mac" | "motorola"
866 ) {
867 return Ok(MachineFormat::BigEndian);
868 }
869 if lower.starts_with("ieee-le") {
870 return Ok(MachineFormat::LittleEndian);
871 }
872 if lower.starts_with("ieee-be") {
873 return Ok(MachineFormat::BigEndian);
874 }
875 Err(format!("fwrite: unsupported machine format '{trimmed}'"))
876}
877
878fn scalar_string(value: &Value, err: &str) -> Result<String, String> {
879 match value {
880 Value::String(s) => Ok(s.clone()),
881 Value::CharArray(ca) if ca.rows == 1 => Ok(ca.data.iter().collect()),
882 Value::StringArray(sa) if sa.data.len() == 1 => Ok(sa.data[0].clone()),
883 _ => Err(err.to_string()),
884 }
885}
886
887fn numeric_scalar(value: &Value, err: &str) -> Result<f64, String> {
888 match value {
889 Value::Num(n) => Ok(*n),
890 Value::Int(int) => Ok(int.to_f64()),
891 Value::Bool(b) => Ok(if *b { 1.0 } else { 0.0 }),
892 Value::Tensor(t) if tensor::is_scalar_tensor(t) => Ok(tensor::tensor_value_f64(t, 0)),
893 Value::LogicalArray(la) if la.data.len() == 1 => {
894 Ok(if la.data[0] != 0 { 1.0 } else { 0.0 })
895 }
896 _ => Err(err.to_string()),
897 }
898}
899
900#[derive(Clone, Debug)]
901enum WriteElement {
902 Floating(f64),
903 Integer(IntValue),
904}
905
906impl WriteElement {
907 fn as_f64(&self) -> f64 {
908 match self {
909 Self::Floating(value) => *value,
910 Self::Integer(value) => value.to_f64(),
911 }
912 }
913}
914
915fn flatten_elements(value: &Value) -> Result<Vec<WriteElement>, String> {
916 match value {
917 Value::Tensor(tensor) => Ok((0..tensor.len())
918 .map(|index| {
919 let value = tensor
920 .numeric_value_at(index)
921 .expect("index within authoritative numeric storage");
922 value.into_int_value().map_or_else(
923 || WriteElement::Floating(value.materialize_f64()),
924 WriteElement::Integer,
925 )
926 })
927 .collect()),
928 Value::Num(n) => Ok(vec![WriteElement::Floating(*n)]),
929 Value::Int(int) => Ok(vec![WriteElement::Integer(int.clone())]),
930 Value::Bool(b) => Ok(vec![WriteElement::Floating(if *b { 1.0 } else { 0.0 })]),
931 Value::LogicalArray(array) => Ok(array
932 .data
933 .iter()
934 .map(|bit| WriteElement::Floating(if *bit != 0 { 1.0 } else { 0.0 }))
935 .collect()),
936 Value::CharArray(ca) => Ok(flatten_char_array(ca)
937 .into_iter()
938 .map(WriteElement::Floating)
939 .collect()),
940 Value::String(text) => Ok(text
941 .chars()
942 .map(|ch| WriteElement::Floating(ch as u32 as f64))
943 .collect()),
944 Value::StringArray(sa) => Ok(flatten_string_array(sa)
945 .into_iter()
946 .map(WriteElement::Floating)
947 .collect()),
948 Value::GpuTensor(_) => Err("fwrite: expected host tensor data after gathering".to_string()),
949 Value::Complex(_, _) | Value::ComplexTensor(_) => {
950 Err("fwrite: complex values are not supported yet".to_string())
951 }
952 _ => Err(format!("fwrite: unsupported data type {:?}", value)),
953 }
954}
955
956fn flatten_char_array(ca: &CharArray) -> Vec<f64> {
957 let mut values = Vec::with_capacity(ca.rows.saturating_mul(ca.cols));
958 for c in 0..ca.cols {
959 for r in 0..ca.rows {
960 let idx = r * ca.cols + c;
961 values.push(ca.data[idx] as u32 as f64);
962 }
963 }
964 values
965}
966
967fn flatten_string_array(sa: &runmat_value::StringArray) -> Vec<f64> {
968 if sa.data.is_empty() {
969 return Vec::new();
970 }
971 let mut values = Vec::new();
972 for (idx, text) in sa.data.iter().enumerate() {
973 if idx > 0 {
974 values.push('\n' as u32 as f64);
975 }
976 values.extend(text.chars().map(|ch| ch as u32 as f64));
977 }
978 values
979}
980
981fn write_elements(
982 file: &mut File,
983 values: &[WriteElement],
984 spec: WriteSpec,
985 skip: usize,
986 machine: MachineFormat,
987) -> Result<usize, String> {
988 let endianness = machine.to_endianness();
989 let skip_offset = skip as i64;
990 for value in values {
991 match spec.input {
992 InputType::UInt8 => {
993 let byte = match integer_unsigned(value, u8::MAX as u64) {
994 Some(value) => value as u8,
995 None => to_u8(value.as_f64()),
996 };
997 write_bytes(file, &[byte])?;
998 }
999 InputType::Int8 => {
1000 let byte = match integer_signed(value, i8::MIN as i64, i8::MAX as i64) {
1001 Some(value) => value as i8,
1002 None => to_i8(value.as_f64()),
1003 } as u8;
1004 write_bytes(file, &[byte])?;
1005 }
1006 InputType::UInt16 => {
1007 let bytes = match integer_unsigned(value, u16::MAX as u64) {
1008 Some(value) => endian_u16(value as u16, endianness),
1009 None => encode_u16(value.as_f64(), endianness),
1010 };
1011 write_bytes(file, &bytes)?;
1012 }
1013 InputType::Int16 => {
1014 let bytes = match integer_signed(value, i16::MIN as i64, i16::MAX as i64) {
1015 Some(value) => endian_i16(value as i16, endianness),
1016 None => encode_i16(value.as_f64(), endianness),
1017 };
1018 write_bytes(file, &bytes)?;
1019 }
1020 InputType::UInt32 => {
1021 let bytes = match integer_unsigned(value, u32::MAX as u64) {
1022 Some(value) => endian_u32(value as u32, endianness),
1023 None => encode_u32(value.as_f64(), endianness),
1024 };
1025 write_bytes(file, &bytes)?;
1026 }
1027 InputType::Int32 => {
1028 let bytes = match integer_signed(value, i32::MIN as i64, i32::MAX as i64) {
1029 Some(value) => endian_i32(value as i32, endianness),
1030 None => encode_i32(value.as_f64(), endianness),
1031 };
1032 write_bytes(file, &bytes)?;
1033 }
1034 InputType::UInt64 => {
1035 let bytes = match value {
1036 WriteElement::Integer(_) => {
1037 endian_u64(integer_unsigned(value, u64::MAX).unwrap(), endianness)
1038 }
1039 _ => encode_u64(value.as_f64(), endianness),
1040 };
1041 write_bytes(file, &bytes)?;
1042 }
1043 InputType::Int64 => {
1044 let bytes = match value {
1045 WriteElement::Integer(_) => endian_i64(
1046 integer_signed(value, i64::MIN, i64::MAX).unwrap(),
1047 endianness,
1048 ),
1049 _ => encode_i64(value.as_f64(), endianness),
1050 };
1051 write_bytes(file, &bytes)?;
1052 }
1053 InputType::Float32 => {
1054 let bytes = encode_f32(value.as_f64(), endianness);
1055 write_bytes(file, &bytes)?;
1056 }
1057 InputType::Float64 => {
1058 let bytes = encode_f64(value.as_f64(), endianness);
1059 write_bytes(file, &bytes)?;
1060 }
1061 }
1062
1063 if skip > 0 {
1064 file.seek(SeekFrom::Current(skip_offset))
1065 .map_err(|err| format!("fwrite: failed to seek while applying skip ({err})"))?;
1066 }
1067 }
1068 Ok(values.len())
1069}
1070
1071fn integer_raw(value: &WriteElement) -> Option<i128> {
1072 match value {
1073 WriteElement::Integer(IntValue::I8(v)) => Some(*v as i128),
1074 WriteElement::Integer(IntValue::I16(v)) => Some(*v as i128),
1075 WriteElement::Integer(IntValue::I32(v)) => Some(*v as i128),
1076 WriteElement::Integer(IntValue::I64(v)) => Some(*v as i128),
1077 WriteElement::Integer(IntValue::U8(v)) => Some(*v as i128),
1078 WriteElement::Integer(IntValue::U16(v)) => Some(*v as i128),
1079 WriteElement::Integer(IntValue::U32(v)) => Some(*v as i128),
1080 WriteElement::Integer(IntValue::U64(v)) => Some(*v as i128),
1081 WriteElement::Floating(_) => None,
1082 }
1083}
1084fn integer_unsigned(value: &WriteElement, max: u64) -> Option<u64> {
1085 integer_raw(value).map(|v| v.clamp(0, max as i128) as u64)
1086}
1087fn integer_signed(value: &WriteElement, min: i64, max: i64) -> Option<i64> {
1088 integer_raw(value).map(|v| v.clamp(min as i128, max as i128) as i64)
1089}
1090fn endian_u16(value: u16, endian: Endianness) -> [u8; 2] {
1091 match endian {
1092 Endianness::Little => value.to_le_bytes(),
1093 Endianness::Big => value.to_be_bytes(),
1094 }
1095}
1096fn endian_i16(value: i16, endian: Endianness) -> [u8; 2] {
1097 match endian {
1098 Endianness::Little => value.to_le_bytes(),
1099 Endianness::Big => value.to_be_bytes(),
1100 }
1101}
1102fn endian_u32(value: u32, endian: Endianness) -> [u8; 4] {
1103 match endian {
1104 Endianness::Little => value.to_le_bytes(),
1105 Endianness::Big => value.to_be_bytes(),
1106 }
1107}
1108fn endian_i32(value: i32, endian: Endianness) -> [u8; 4] {
1109 match endian {
1110 Endianness::Little => value.to_le_bytes(),
1111 Endianness::Big => value.to_be_bytes(),
1112 }
1113}
1114fn endian_u64(value: u64, endian: Endianness) -> [u8; 8] {
1115 match endian {
1116 Endianness::Little => value.to_le_bytes(),
1117 Endianness::Big => value.to_be_bytes(),
1118 }
1119}
1120fn endian_i64(value: i64, endian: Endianness) -> [u8; 8] {
1121 match endian {
1122 Endianness::Little => value.to_le_bytes(),
1123 Endianness::Big => value.to_be_bytes(),
1124 }
1125}
1126
1127fn write_bytes(file: &mut File, bytes: &[u8]) -> Result<(), String> {
1128 file.write_all(bytes)
1129 .map_err(|err| format!("fwrite: failed to write to file ({err})"))
1130}
1131
1132fn to_u8(value: f64) -> u8 {
1133 if value.is_nan() {
1134 return 0;
1135 }
1136 if !value.is_finite() {
1137 return if value.is_sign_negative() { 0 } else { u8::MAX };
1138 }
1139 let mut rounded = value.round();
1140 if rounded.is_nan() {
1141 return 0;
1142 }
1143 if rounded < 0.0 {
1144 rounded = 0.0;
1145 }
1146 if rounded > u8::MAX as f64 {
1147 rounded = u8::MAX as f64;
1148 }
1149 rounded as u8
1150}
1151
1152fn to_i8(value: f64) -> i8 {
1153 saturating_round(value, i8::MIN as f64, i8::MAX as f64) as i8
1154}
1155
1156fn encode_u16(value: f64, endianness: Endianness) -> [u8; 2] {
1157 let rounded = saturating_round(value, 0.0, u16::MAX as f64) as u16;
1158 match endianness {
1159 Endianness::Little => rounded.to_le_bytes(),
1160 Endianness::Big => rounded.to_be_bytes(),
1161 }
1162}
1163
1164fn encode_i16(value: f64, endianness: Endianness) -> [u8; 2] {
1165 let rounded = saturating_round(value, i16::MIN as f64, i16::MAX as f64) as i16;
1166 match endianness {
1167 Endianness::Little => rounded.to_le_bytes(),
1168 Endianness::Big => rounded.to_be_bytes(),
1169 }
1170}
1171
1172fn encode_u32(value: f64, endianness: Endianness) -> [u8; 4] {
1173 let rounded = saturating_round(value, 0.0, u32::MAX as f64) as u32;
1174 match endianness {
1175 Endianness::Little => rounded.to_le_bytes(),
1176 Endianness::Big => rounded.to_be_bytes(),
1177 }
1178}
1179
1180fn encode_i32(value: f64, endianness: Endianness) -> [u8; 4] {
1181 let rounded = saturating_round(value, i32::MIN as f64, i32::MAX as f64) as i32;
1182 match endianness {
1183 Endianness::Little => rounded.to_le_bytes(),
1184 Endianness::Big => rounded.to_be_bytes(),
1185 }
1186}
1187
1188fn encode_u64(value: f64, endianness: Endianness) -> [u8; 8] {
1189 let rounded = saturating_round(value, 0.0, u64::MAX as f64);
1190 let as_u64 = if rounded.is_finite() {
1191 rounded as u64
1192 } else if rounded.is_sign_negative() {
1193 0
1194 } else {
1195 u64::MAX
1196 };
1197 match endianness {
1198 Endianness::Little => as_u64.to_le_bytes(),
1199 Endianness::Big => as_u64.to_be_bytes(),
1200 }
1201}
1202
1203fn encode_i64(value: f64, endianness: Endianness) -> [u8; 8] {
1204 let rounded = saturating_round(value, i64::MIN as f64, i64::MAX as f64);
1205 let as_i64 = if rounded.is_finite() {
1206 rounded as i64
1207 } else if rounded.is_sign_negative() {
1208 i64::MIN
1209 } else {
1210 i64::MAX
1211 };
1212 match endianness {
1213 Endianness::Little => as_i64.to_le_bytes(),
1214 Endianness::Big => as_i64.to_be_bytes(),
1215 }
1216}
1217
1218fn encode_f32(value: f64, endianness: Endianness) -> [u8; 4] {
1219 let as_f32 = value as f32;
1220 let bits = as_f32.to_bits();
1221 match endianness {
1222 Endianness::Little => bits.to_le_bytes(),
1223 Endianness::Big => bits.to_be_bytes(),
1224 }
1225}
1226
1227fn encode_f64(value: f64, endianness: Endianness) -> [u8; 8] {
1228 let bits = value.to_bits();
1229 match endianness {
1230 Endianness::Little => bits.to_le_bytes(),
1231 Endianness::Big => bits.to_be_bytes(),
1232 }
1233}
1234
1235fn saturating_round(value: f64, min: f64, max: f64) -> f64 {
1236 if value.is_nan() {
1237 return 0.0;
1238 }
1239 if !value.is_finite() {
1240 return if value.is_sign_negative() { min } else { max };
1241 }
1242 let mut rounded = value.round();
1243 if rounded.is_nan() {
1244 return 0.0;
1245 }
1246 if rounded < min {
1247 rounded = min;
1248 }
1249 if rounded > max {
1250 rounded = max;
1251 }
1252 rounded
1253}
1254
1255#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1256enum InputType {
1257 UInt8,
1258 Int8,
1259 UInt16,
1260 Int16,
1261 UInt32,
1262 Int32,
1263 UInt64,
1264 Int64,
1265 Float32,
1266 Float64,
1267}
1268
1269fn parse_input_label(label: &str) -> Result<InputType, String> {
1270 match label {
1271 "double" | "float64" | "real*8" => Ok(InputType::Float64),
1272 "single" | "float32" | "real*4" => Ok(InputType::Float32),
1273 "int8" | "schar" | "integer*1" => Ok(InputType::Int8),
1274 "uint8" | "uchar" | "unsignedchar" | "char" | "byte" => Ok(InputType::UInt8),
1275 "int16" | "short" | "integer*2" => Ok(InputType::Int16),
1276 "uint16" | "ushort" | "unsignedshort" => Ok(InputType::UInt16),
1277 "int32" | "integer*4" | "long" => Ok(InputType::Int32),
1278 "uint32" | "unsignedint" | "unsignedlong" => Ok(InputType::UInt32),
1279 "int64" | "integer*8" | "longlong" => Ok(InputType::Int64),
1280 "uint64" | "unsignedlonglong" => Ok(InputType::UInt64),
1281 other => Err(format!("fwrite: unsupported precision '{other}'")),
1282 }
1283}
1284
1285#[cfg(test)]
1286pub(crate) mod tests {
1287 use super::*;
1288 use crate::builtins::common::test_support;
1289 use crate::builtins::io::filetext::registry;
1290 use crate::builtins::io::filetext::{fclose, fopen};
1291 use crate::RuntimeError;
1292 #[cfg(feature = "wgpu")]
1293 use runmat_accelerate::backend::wgpu::provider;
1294 #[cfg(feature = "wgpu")]
1295 use runmat_accelerate_api::AccelProvider;
1296 use runmat_accelerate_api::HostTensorView;
1297 use runmat_filesystem::File;
1298 use runmat_time::system_time_now;
1299 use runmat_value::{IntegerStorage, Tensor};
1300 use std::io::Read;
1301 use std::path::PathBuf;
1302 use std::time::UNIX_EPOCH;
1303
1304 fn unwrap_error_message(err: RuntimeError) -> String {
1305 err.message().to_string()
1306 }
1307
1308 fn run_evaluate(
1309 fid_value: &Value,
1310 data_value: &Value,
1311 rest: &[Value],
1312 ) -> BuiltinResult<FwriteEval> {
1313 futures::executor::block_on(evaluate(fid_value, data_value, rest))
1314 }
1315
1316 fn run_fopen(args: &[Value]) -> BuiltinResult<fopen::FopenEval> {
1317 futures::executor::block_on(fopen::evaluate(args))
1318 }
1319
1320 fn run_fclose(args: &[Value]) -> BuiltinResult<fclose::FcloseEval> {
1321 futures::executor::block_on(fclose::evaluate(args))
1322 }
1323
1324 fn registry_guard() -> std::sync::MutexGuard<'static, ()> {
1325 registry::test_guard()
1326 }
1327
1328 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1329 #[test]
1330 fn fwrite_descriptor_signatures_cover_core_forms() {
1331 let labels: Vec<&str> = FWRITE_DESCRIPTOR
1332 .signatures
1333 .iter()
1334 .map(|sig| sig.label)
1335 .collect();
1336 assert!(labels.contains(&"count = fwrite(fid, data)"));
1337 assert!(labels.contains(&"count = fwrite(fid, data, precision, skip)"));
1338 assert!(labels.contains(&"count = fwrite(fid, data, precision, machinefmt)"));
1339 assert!(labels.contains(&"count = fwrite(fid, data, precision, skip, machinefmt)"));
1340 }
1341
1342 #[test]
1343 fn fwrite_integer_capabilities_and_control_roles_are_independently_gated() {
1344 assert_eq!(INTEGER_CAPABILITIES.len(), 3);
1345 let _matlab = crate::compatibility::push_runmat_extensions_enabled(false);
1346 let fid =
1347 preflight_control(&Value::Int(IntValue::I32(3)), ControlRole::FileId).unwrap_err();
1348 assert_eq!(
1349 fid.identifier(),
1350 Some("RunMat:compatibility:FwriteIntegerIdExtension")
1351 );
1352 let skip = preflight_control(&Value::Int(IntValue::U16(2)), ControlRole::Skip).unwrap_err();
1353 assert_eq!(
1354 skip.identifier(),
1355 Some("RunMat:compatibility:FwriteIntegerSkipExtension")
1356 );
1357 }
1358
1359 #[test]
1360 fn fwrite_classifies_resident_skip_before_gathering() {
1361 let resident = Value::GpuTensor(runmat_accelerate_api::GpuTensorHandle {
1362 shape: vec![1, 1],
1363 device_id: 903,
1364 buffer_id: 903,
1365 descriptor: Default::default(),
1366 });
1367 let args = vec![Value::from("uint8"), resident];
1368 let (_, skip, _) = classify_arguments(&args).expect("classified controls");
1369 let _matlab = crate::compatibility::push_runmat_extensions_enabled(false);
1370 let error = preflight_control(skip.expect("skip"), ControlRole::Skip).unwrap_err();
1371 assert_eq!(
1372 error.identifier(),
1373 Some("RunMat:compatibility:FwriteResidentControlExtension")
1374 );
1375 }
1376
1377 #[test]
1378 fn fwrite_nan_to_integer_precision_writes_zero() {
1379 assert_eq!(to_u8(f64::NAN), 0);
1380 assert_eq!(
1381 encode_i16(f64::NAN, Endianness::Little),
1382 0_i16.to_le_bytes()
1383 );
1384 assert_eq!(encode_u64(f64::NAN, Endianness::Big), 0_u64.to_be_bytes());
1385 }
1386
1387 #[test]
1388 fn typed_integer_precision_conversions_stay_in_integer_domain() {
1389 let signed_min = WriteElement::Integer(IntValue::I64(i64::MIN));
1390 let unsigned_max = WriteElement::Integer(IntValue::U64(u64::MAX));
1391 let wide_unsigned = WriteElement::Integer(IntValue::U64(9_007_199_254_740_993));
1392
1393 assert_eq!(integer_unsigned(&signed_min, u64::MAX), Some(0));
1394 assert_eq!(
1395 integer_signed(&unsigned_max, i64::MIN, i64::MAX),
1396 Some(i64::MAX)
1397 );
1398 assert_eq!(
1399 integer_unsigned(&wide_unsigned, u32::MAX as u64),
1400 Some(u32::MAX as u64)
1401 );
1402 assert_eq!(
1403 integer_signed(&wide_unsigned, i32::MIN as i64, i32::MAX as i64),
1404 Some(i32::MAX as i64)
1405 );
1406 }
1407
1408 #[test]
1409 fn fwrite_flattens_each_integer_storage_class_without_reading_f64_mirror() {
1410 let cases = [
1411 (IntegerStorage::I8(vec![-8]), -8_i128),
1412 (IntegerStorage::I16(vec![-16]), -16),
1413 (IntegerStorage::I32(vec![-32]), -32),
1414 (IntegerStorage::I64(vec![i64::MIN]), i64::MIN as i128),
1415 (IntegerStorage::U8(vec![8]), 8),
1416 (IntegerStorage::U16(vec![16]), 16),
1417 (IntegerStorage::U32(vec![32]), 32),
1418 (IntegerStorage::U64(vec![u64::MAX]), u64::MAX as i128),
1419 ];
1420
1421 for (storage, expected) in cases {
1422 let tensor = Tensor::new_integer(storage, vec![1, 1]).expect("typed tensor");
1423 let elements = flatten_elements(&Value::Tensor(tensor)).expect("typed elements");
1424 assert_eq!(elements.len(), 1);
1425 assert_eq!(integer_raw(&elements[0]), Some(expected));
1426 }
1427 }
1428
1429 #[test]
1430 fn fwrite_scalar_parser_reads_typed_integer_storage_exactly() {
1431 let scalar = Tensor::new_integer(IntegerStorage::U16(vec![7]), vec![1, 1]).expect("scalar");
1432 assert_eq!(
1433 numeric_scalar(&Value::Tensor(scalar), "scalar").expect("scalar"),
1434 7.0
1435 );
1436 }
1437
1438 #[test]
1439 #[cfg(target_pointer_width = "64")]
1440 fn fwrite_skip_parses_integer_values_exactly() {
1441 let exact = (1_u64 << 53) + 1;
1442
1443 assert_eq!(
1444 parse_skip(Some(&Value::Int(IntValue::U64(exact)))).unwrap(),
1445 exact as usize
1446 );
1447 assert!(parse_skip(Some(&Value::Int(IntValue::U64(u64::MAX)))).is_err());
1448 assert!(parse_skip(Some(&Value::Int(IntValue::I8(-1)))).is_err());
1449 assert!(parse_skip(Some(&Value::Num(i64::MAX as f64))).is_err());
1450 assert!(parse_skip(Some(&Value::Num((i64::MAX as f64) + 1.0))).is_err());
1451 }
1452
1453 #[test]
1454 fn fwrite_fid_and_skip_read_typed_integer_storage_exactly() {
1455 let fid =
1456 Tensor::new_integer(IntegerStorage::U16(vec![7]), vec![1, 1]).expect("fid tensor");
1457 assert_eq!(parse_fid(&Value::Tensor(fid)).unwrap(), 7);
1458 assert_eq!(parse_fid(&Value::Int(IntValue::U16(7))).unwrap(), 7);
1459 assert!(parse_fid(&Value::Int(IntValue::U64(u64::MAX))).is_err());
1460
1461 let skip =
1462 Tensor::new_integer(IntegerStorage::U16(vec![9]), vec![1, 1]).expect("skip tensor");
1463 assert_eq!(parse_skip(Some(&Value::Tensor(skip))).unwrap(), 9);
1464
1465 let too_large =
1466 Tensor::new_integer(IntegerStorage::U64(vec![u64::MAX]), vec![1, 1]).expect("skip");
1467 assert!(parse_skip(Some(&Value::Tensor(too_large))).is_err());
1468 }
1469
1470 #[test]
1471 fn fwrite_typed_scalar_parameters_ignore_poisoned_f64_mirrors() {
1472 let classes = [
1473 IntegerStorage::I8(vec![7]),
1474 IntegerStorage::I16(vec![7]),
1475 IntegerStorage::I32(vec![7]),
1476 IntegerStorage::I64(vec![7]),
1477 IntegerStorage::U8(vec![7]),
1478 IntegerStorage::U16(vec![7]),
1479 IntegerStorage::U32(vec![7]),
1480 IntegerStorage::U64(vec![7]),
1481 ];
1482 for storage in classes {
1483 let tensor = Tensor::new_integer(storage, vec![1, 1]).expect("typed scalar");
1484 let value = Value::Tensor(tensor);
1485 assert_eq!(parse_fid(&value).unwrap(), 7);
1486 assert_eq!(parse_skip(Some(&value)).unwrap(), 7);
1487 }
1488 }
1489
1490 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1491 #[test]
1492 fn fwrite_default_uint8_bytes() {
1493 let _guard = registry_guard();
1494 registry::reset_for_tests();
1495 let path = unique_path("fwrite_uint8");
1496 let open = run_fopen(&[
1497 Value::from(path.to_string_lossy().to_string()),
1498 Value::from("w+b"),
1499 ])
1500 .expect("fopen");
1501 let fid = open.as_open().unwrap().fid as i32;
1502
1503 let tensor = Tensor::new(vec![1.0, 2.0, 255.0], vec![3, 1]).unwrap();
1504 let eval = run_evaluate(&Value::Num(fid as f64), &Value::Tensor(tensor), &Vec::new())
1505 .expect("fwrite");
1506 assert_eq!(eval.count(), 3);
1507
1508 run_fclose(&[Value::Num(fid as f64)]).unwrap();
1509
1510 let bytes = test_support::fs::read(&path).expect("read");
1511 assert_eq!(bytes, vec![1u8, 2, 255]);
1512 test_support::fs::remove_file(path).unwrap();
1513 }
1514
1515 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1516 #[test]
1517 fn fwrite_uint64_preserves_exact_typed_tensor_bytes() {
1518 let _guard = registry_guard();
1519 registry::reset_for_tests();
1520 let path = unique_path("fwrite_uint64_exact");
1521 let open = run_fopen(&[
1522 Value::from(path.to_string_lossy().to_string()),
1523 Value::from("w+b"),
1524 ])
1525 .expect("fopen");
1526 let fid = open.as_open().unwrap().fid as i32;
1527 let values = [9_007_199_254_740_993, u64::MAX];
1528 let tensor = Tensor::new_integer(IntegerStorage::U64(values.to_vec()), vec![2, 1])
1529 .expect("typed uint64 tensor");
1530
1531 let eval = run_evaluate(
1532 &Value::Num(fid as f64),
1533 &Value::Tensor(tensor),
1534 &[Value::from("uint64")],
1535 )
1536 .expect("fwrite");
1537 assert_eq!(eval.count(), values.len());
1538 run_fclose(&[Value::Num(fid as f64)]).expect("fclose");
1539
1540 let bytes = test_support::fs::read(&path).expect("read");
1541 assert_eq!(
1542 bytes,
1543 values
1544 .into_iter()
1545 .flat_map(u64::to_ne_bytes)
1546 .collect::<Vec<_>>()
1547 );
1548 test_support::fs::remove_file(path).expect("remove file");
1549 }
1550
1551 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1552 #[test]
1553 fn fwrite_int64_preserves_exact_typed_tensor_bytes() {
1554 let _guard = registry_guard();
1555 registry::reset_for_tests();
1556 let path = unique_path("fwrite_int64_exact");
1557 let open = run_fopen(&[
1558 Value::from(path.to_string_lossy().to_string()),
1559 Value::from("w+b"),
1560 ])
1561 .expect("fopen");
1562 let fid = open.as_open().unwrap().fid as i32;
1563 let values = [i64::MIN, i64::MAX];
1564 let tensor = Tensor::new_integer(IntegerStorage::I64(values.to_vec()), vec![2, 1])
1565 .expect("typed int64 tensor");
1566
1567 let eval = run_evaluate(
1568 &Value::Num(fid as f64),
1569 &Value::Tensor(tensor),
1570 &[Value::from("int64")],
1571 )
1572 .expect("fwrite");
1573 assert_eq!(eval.count(), values.len());
1574 run_fclose(&[Value::Num(fid as f64)]).expect("fclose");
1575
1576 let bytes = test_support::fs::read(&path).expect("read");
1577 assert_eq!(
1578 bytes,
1579 values
1580 .into_iter()
1581 .flat_map(i64::to_ne_bytes)
1582 .collect::<Vec<_>>()
1583 );
1584 test_support::fs::remove_file(path).expect("remove file");
1585 }
1586
1587 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1588 #[test]
1589 fn fwrite_uint64_narrowing_stays_in_integer_domain() {
1590 let _guard = registry_guard();
1591 registry::reset_for_tests();
1592 let path = unique_path("fwrite_uint64_to_uint32");
1593 let open = run_fopen(&[
1594 Value::from(path.to_string_lossy().to_string()),
1595 Value::from("w+b"),
1596 Value::from("ieee-be"),
1597 ])
1598 .expect("fopen");
1599 let fid = open.as_open().unwrap().fid as i32;
1600 let tensor = Tensor::new_integer(
1601 IntegerStorage::U64(vec![9_007_199_254_740_993, u64::MAX]),
1602 vec![2, 1],
1603 )
1604 .expect("typed uint64 tensor");
1605
1606 let eval = run_evaluate(
1607 &Value::Num(fid as f64),
1608 &Value::Tensor(tensor),
1609 &[Value::from("uint32")],
1610 )
1611 .expect("fwrite");
1612 assert_eq!(eval.count(), 2);
1613 run_fclose(&[Value::Num(fid as f64)]).expect("fclose");
1614
1615 let bytes = test_support::fs::read(&path).expect("read");
1616 assert_eq!(
1617 bytes,
1618 [u32::MAX.to_be_bytes(), u32::MAX.to_be_bytes()].concat()
1619 );
1620 test_support::fs::remove_file(path).expect("remove file");
1621 }
1622
1623 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1624 #[test]
1625 fn fwrite_signed_integer_to_unsigned_target_saturates_without_f64_rounding() {
1626 let _guard = registry_guard();
1627 registry::reset_for_tests();
1628 let path = unique_path("fwrite_int64_to_uint16");
1629 let open = run_fopen(&[
1630 Value::from(path.to_string_lossy().to_string()),
1631 Value::from("w+b"),
1632 Value::from("ieee-be"),
1633 ])
1634 .expect("fopen");
1635 let fid = open.as_open().unwrap().fid as i32;
1636 let tensor = Tensor::new_integer(
1637 IntegerStorage::I64(vec![i64::MIN, -1, 65_535, i64::MAX]),
1638 vec![4, 1],
1639 )
1640 .expect("typed int64 tensor");
1641
1642 let eval = run_evaluate(
1643 &Value::Num(fid as f64),
1644 &Value::Tensor(tensor),
1645 &[Value::from("uint16")],
1646 )
1647 .expect("fwrite");
1648 assert_eq!(eval.count(), 4);
1649 run_fclose(&[Value::Num(fid as f64)]).expect("fclose");
1650
1651 let bytes = test_support::fs::read(&path).expect("read");
1652 assert_eq!(
1653 bytes,
1654 [
1655 0_u16.to_be_bytes(),
1656 0_u16.to_be_bytes(),
1657 65_535_u16.to_be_bytes(),
1658 u16::MAX.to_be_bytes(),
1659 ]
1660 .concat()
1661 );
1662 test_support::fs::remove_file(path).expect("remove file");
1663 }
1664
1665 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1666 #[test]
1667 fn fwrite_double_precision_writes_native_endian() {
1668 let _guard = registry_guard();
1669 registry::reset_for_tests();
1670 let path = unique_path("fwrite_double");
1671 let open = run_fopen(&[
1672 Value::from(path.to_string_lossy().to_string()),
1673 Value::from("w+b"),
1674 ])
1675 .expect("fopen");
1676 let fid = open.as_open().unwrap().fid as i32;
1677
1678 let tensor = Tensor::new(vec![1.5, -2.25], vec![2, 1]).unwrap();
1679 let args = vec![Value::from("double")];
1680 let eval =
1681 run_evaluate(&Value::Num(fid as f64), &Value::Tensor(tensor), &args).expect("fwrite");
1682 assert_eq!(eval.count(), 2);
1683
1684 run_fclose(&[Value::Num(fid as f64)]).unwrap();
1685
1686 let bytes = test_support::fs::read(&path).expect("read");
1687 let expected: Vec<u8> = if cfg!(target_endian = "little") {
1688 [1.5f64.to_le_bytes(), (-2.25f64).to_le_bytes()].concat()
1689 } else {
1690 [1.5f64.to_be_bytes(), (-2.25f64).to_be_bytes()].concat()
1691 };
1692 assert_eq!(bytes, expected);
1693 test_support::fs::remove_file(path).unwrap();
1694 }
1695
1696 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1697 #[test]
1698 fn fwrite_big_endian_uint16() {
1699 let _guard = registry_guard();
1700 registry::reset_for_tests();
1701 let path = unique_path("fwrite_be");
1702 let open = run_fopen(&[
1703 Value::from(path.to_string_lossy().to_string()),
1704 Value::from("w+b"),
1705 Value::from("ieee-be"),
1706 ])
1707 .expect("fopen");
1708 let fid = open.as_open().unwrap().fid as i32;
1709
1710 let tensor = Tensor::new(vec![258.0, 772.0], vec![2, 1]).unwrap();
1711 let args = vec![Value::from("uint16")];
1712 let eval =
1713 run_evaluate(&Value::Num(fid as f64), &Value::Tensor(tensor), &args).expect("fwrite");
1714 assert_eq!(eval.count(), 2);
1715
1716 run_fclose(&[Value::Num(fid as f64)]).unwrap();
1717
1718 let bytes = test_support::fs::read(&path).expect("read");
1719 assert_eq!(bytes, vec![0x01, 0x02, 0x03, 0x04]);
1720 test_support::fs::remove_file(path).unwrap();
1721 }
1722
1723 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1724 #[test]
1725 fn fwrite_skip_inserts_padding() {
1726 let _guard = registry_guard();
1727 registry::reset_for_tests();
1728 let path = unique_path("fwrite_skip");
1729 let open = run_fopen(&[
1730 Value::from(path.to_string_lossy().to_string()),
1731 Value::from("w+b"),
1732 ])
1733 .expect("fopen");
1734 let fid = open.as_open().unwrap().fid as i32;
1735
1736 let tensor = Tensor::new(vec![10.0, 20.0, 30.0], vec![3, 1]).unwrap();
1737 let args = vec![Value::from("uint8"), Value::Num(1.0)];
1738 let eval =
1739 run_evaluate(&Value::Num(fid as f64), &Value::Tensor(tensor), &args).expect("fwrite");
1740 assert_eq!(eval.count(), 3);
1741
1742 run_fclose(&[Value::Num(fid as f64)]).unwrap();
1743
1744 let bytes = test_support::fs::read(&path).expect("read");
1745 assert_eq!(bytes, vec![10u8, 0, 20, 0, 30]);
1746 test_support::fs::remove_file(path).unwrap();
1747 }
1748
1749 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1750 #[test]
1751 fn fwrite_gpu_tensor_gathers_before_write() {
1752 let _guard = registry_guard();
1753 registry::reset_for_tests();
1754 let path = unique_path("fwrite_gpu");
1755
1756 test_support::with_test_provider(|provider| {
1757 registry::reset_for_tests();
1758 let open = run_fopen(&[
1759 Value::from(path.to_string_lossy().to_string()),
1760 Value::from("w+b"),
1761 ])
1762 .expect("fopen");
1763 let fid = open.as_open().unwrap().fid as i32;
1764
1765 let tensor = Tensor::new(vec![1.0, 2.0, 3.0, 4.0], vec![4, 1]).unwrap();
1766 let view = HostTensorView {
1767 data: &tensor.materialize_f64(),
1768 shape: &tensor.shape,
1769 };
1770 let handle = provider.upload(&view).expect("upload");
1771 let args = vec![Value::from("uint16")];
1772 {
1773 let _compat = crate::compatibility::push_runmat_extensions_enabled(false);
1774 let error = run_evaluate(
1775 &Value::Num(fid as f64),
1776 &Value::GpuTensor(handle.clone()),
1777 &args,
1778 )
1779 .expect_err("MATLAB mode rejects direct gpuArray fwrite");
1780 assert_eq!(
1781 error.identifier(),
1782 Some("RunMat:compatibility:FwriteGpuInputExtension")
1783 );
1784 }
1785 let eval = {
1786 let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
1787 run_evaluate(&Value::Num(fid as f64), &Value::GpuTensor(handle), &args)
1788 .expect("RunMat mode accepts direct gpuArray fwrite")
1789 };
1790 assert_eq!(eval.count(), 4);
1791
1792 run_fclose(&[Value::Num(fid as f64)]).unwrap();
1793 });
1794
1795 let mut file = File::open(&path).expect("open");
1796 let mut bytes = Vec::new();
1797 file.read_to_end(&mut bytes).expect("read");
1798 assert_eq!(bytes.len(), 8);
1799 let mut decoded = Vec::new();
1800 for chunk in bytes.chunks_exact(2) {
1801 let value = if cfg!(target_endian = "little") {
1802 u16::from_le_bytes([chunk[0], chunk[1]])
1803 } else {
1804 u16::from_be_bytes([chunk[0], chunk[1]])
1805 };
1806 decoded.push(value);
1807 }
1808 assert_eq!(decoded, vec![1u16, 2, 3, 4]);
1809 test_support::fs::remove_file(path).unwrap();
1810 }
1811
1812 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1813 #[test]
1814 fn fwrite_invalid_precision_errors() {
1815 let _guard = registry_guard();
1816 registry::reset_for_tests();
1817 let path = unique_path("fwrite_invalid_precision");
1818 let open = run_fopen(&[
1819 Value::from(path.to_string_lossy().to_string()),
1820 Value::from("w+b"),
1821 ])
1822 .expect("fopen");
1823 let fid = open.as_open().unwrap().fid as i32;
1824
1825 let tensor = Tensor::new(vec![1.0], vec![1, 1]).unwrap();
1826 let args = vec![Value::from("bogus-class")];
1827 let err = unwrap_error_message(
1828 run_evaluate(&Value::Num(fid as f64), &Value::Tensor(tensor), &args).unwrap_err(),
1829 );
1830 assert!(err.contains("unsupported precision"));
1831 let _ = run_fclose(&[Value::Num(fid as f64)]);
1832 test_support::fs::remove_file(path).unwrap();
1833 }
1834
1835 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1836 #[test]
1837 fn fwrite_negative_skip_errors() {
1838 let _guard = registry_guard();
1839 registry::reset_for_tests();
1840 let path = unique_path("fwrite_negative_skip");
1841 let open = run_fopen(&[
1842 Value::from(path.to_string_lossy().to_string()),
1843 Value::from("w+b"),
1844 ])
1845 .expect("fopen");
1846 let fid = open.as_open().unwrap().fid as i32;
1847
1848 let tensor = Tensor::new(vec![10.0], vec![1, 1]).unwrap();
1849 let args = vec![Value::from("uint8"), Value::Num(-1.0)];
1850 let err = unwrap_error_message(
1851 run_evaluate(&Value::Num(fid as f64), &Value::Tensor(tensor), &args).unwrap_err(),
1852 );
1853 assert!(err.contains("skip value must be non-negative"));
1854 let _ = run_fclose(&[Value::Num(fid as f64)]);
1855 test_support::fs::remove_file(path).unwrap();
1856 }
1857
1858 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1859 #[test]
1860 #[cfg(feature = "wgpu")]
1861 fn fwrite_wgpu_tensor_roundtrip() {
1862 let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
1863 let _guard = registry_guard();
1864 registry::reset_for_tests();
1865 let path = unique_path("fwrite_wgpu_roundtrip");
1866 let open = run_fopen(&[
1867 Value::from(path.to_string_lossy().to_string()),
1868 Value::from("w+b"),
1869 ])
1870 .expect("fopen");
1871 let fid = open.as_open().unwrap().fid as i32;
1872
1873 let provider = provider::register_wgpu_provider(provider::WgpuProviderOptions::default())
1874 .expect("wgpu provider");
1875
1876 let tensor = Tensor::new(vec![0.5, -1.25, 3.75], vec![3, 1]).unwrap();
1877 let expected = tensor.materialize_f64().clone();
1878 let view = HostTensorView {
1879 data: &tensor.materialize_f64(),
1880 shape: &tensor.shape,
1881 };
1882 let handle = provider.upload(&view).expect("upload to gpu");
1883 let args = vec![Value::from("double")];
1884 let eval = run_evaluate(&Value::Num(fid as f64), &Value::GpuTensor(handle), &args)
1885 .expect("fwrite");
1886 assert_eq!(eval.count(), 3);
1887
1888 run_fclose(&[Value::Num(fid as f64)]).unwrap();
1889
1890 let mut file = File::open(&path).expect("open");
1891 let mut bytes = Vec::new();
1892 file.read_to_end(&mut bytes).expect("read");
1893 assert_eq!(bytes.len(), 24);
1894 for (chunk, expected_value) in bytes.chunks_exact(8).zip(expected.iter()) {
1895 let mut buf = [0u8; 8];
1896 buf.copy_from_slice(chunk);
1897 let value = if cfg!(target_endian = "little") {
1898 f64::from_le_bytes(buf)
1899 } else {
1900 f64::from_be_bytes(buf)
1901 };
1902 assert!(
1903 (value - expected_value).abs() < 1e-12,
1904 "mismatch: {} vs {}",
1905 value,
1906 expected_value
1907 );
1908 }
1909 test_support::fs::remove_file(path).unwrap();
1910 }
1911
1912 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1913 #[test]
1914 fn fwrite_invalid_identifier_errors() {
1915 let _guard = registry_guard();
1916 registry::reset_for_tests();
1917 let err = unwrap_error_message(
1918 run_evaluate(&Value::Num(-1.0), &Value::Num(1.0), &Vec::new()).unwrap_err(),
1919 );
1920 assert!(err.contains("file identifier must be non-negative"));
1921 }
1922
1923 fn unique_path(prefix: &str) -> PathBuf {
1924 let now = system_time_now()
1925 .duration_since(UNIX_EPOCH)
1926 .expect("time went backwards");
1927 let filename = format!(
1928 "runmat_{prefix}_{}_{}.tmp",
1929 now.as_secs(),
1930 now.subsec_nanos()
1931 );
1932 std::env::temp_dir().join(filename)
1933 }
1934}