Skip to main content

runmat_runtime/builtins/io/filetext/
fwrite.rs

1//! MATLAB-compatible `fwrite` builtin for RunMat.
2use 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/// Result of an `fwrite` evaluation.
433#[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    /// Number of elements successfully written.
444    pub fn count(&self) -> usize {
445        self.count
446    }
447}
448
449/// Evaluate the `fwrite` builtin without invoking the runtime dispatcher.
450pub 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}