Skip to main content

runmat_runtime/builtins/io/json/
jsonencode.rs

1//! MATLAB-compatible `jsonencode` builtin for serialising RunMat values to JSON text.
2
3use std::collections::BTreeMap;
4use std::fmt::Write as FmtWrite;
5
6use runmat_builtins::{
7    BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinExtensionDescriptor,
8    BuiltinExtensionMode, BuiltinIntegerBackendRule, BuiltinIntegerCapabilityDescriptor,
9    BuiltinIntegerComputationDomain, BuiltinIntegerInputAvailability,
10    BuiltinIntegerInputCapability, BuiltinIntegerOutputClassRule, BuiltinIntegerOverflowRule,
11    BuiltinIntegerOverloadKind, BuiltinIntegerScalarDoubleRule, BuiltinOutputMode,
12    BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
13};
14use runmat_macros::runtime_builtin;
15use runmat_value::{
16    CellArray, CharArray, ComplexTensor, IntValue, IntegerStorage, LogicalArray, ObjectInstance,
17    StringArray, StructValue, SymbolicArray, Tensor, Value,
18};
19
20use crate::builtins::common::spec::{
21    BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
22    ReductionNaN, ResidencyPolicy, ShapeRequirements,
23};
24use crate::builtins::common::tensor;
25use crate::{build_runtime_error, gather_if_needed_async, BuiltinResult, RuntimeError};
26
27const BUILTIN_NAME: &str = "jsonencode";
28
29const JSONENCODE_RESIDENT_INPUT_EXTENSION: BuiltinExtensionDescriptor =
30    BuiltinExtensionDescriptor {
31        id: "jsonencode-resident-input",
32        mode: BuiltinExtensionMode::RunMatOnly,
33        description: "jsonencode with explicit gpuArray input is a RunMat extension",
34        error_identifier: Some("RunMat:compatibility:JsonencodeResidentInputExtension"),
35    };
36const JSONENCODE_TYPED_INTEGER_OPTION_EXTENSION: BuiltinExtensionDescriptor =
37    BuiltinExtensionDescriptor {
38        id: "jsonencode-typed-integer-option",
39        mode: BuiltinExtensionMode::RunMatOnly,
40        description: "jsonencode with a typed-integer boolean option is a RunMat extension",
41        error_identifier: Some("RunMat:compatibility:JsonencodeTypedIntegerOptionExtension"),
42    };
43const JSONENCODE_NUMERIC_OPTION_EXTENSION: BuiltinExtensionDescriptor =
44    BuiltinExtensionDescriptor {
45        id: "jsonencode-numeric-option",
46        mode: BuiltinExtensionMode::RunMatOnly,
47        description: "jsonencode with a floating numeric boolean option is a RunMat extension",
48        error_identifier: Some("RunMat:compatibility:JsonencodeNumericOptionExtension"),
49    };
50const JSONENCODE_TEXT_OPTION_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
51    id: "jsonencode-text-option",
52    mode: BuiltinExtensionMode::RunMatOnly,
53    description: "jsonencode with a textual boolean option is a RunMat extension",
54    error_identifier: Some("RunMat:compatibility:JsonencodeTextOptionExtension"),
55};
56const JSONENCODE_COMPLEX_INPUT_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
57    id: "jsonencode-complex-input",
58    mode: BuiltinExtensionMode::RunMatOnly,
59    description: "jsonencode complex-number object encoding is a RunMat extension",
60    error_identifier: Some("RunMat:compatibility:JsonencodeComplexInputExtension"),
61};
62const JSONENCODE_SPARSE_INPUT_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
63    id: "jsonencode-sparse-input",
64    mode: BuiltinExtensionMode::RunMatOnly,
65    description: "jsonencode sparse-array densification is a RunMat extension",
66    error_identifier: Some("RunMat:compatibility:JsonencodeSparseInputExtension"),
67};
68pub const JSONENCODE_EXTENSIONS: [BuiltinExtensionDescriptor; 6] = [
69    JSONENCODE_RESIDENT_INPUT_EXTENSION,
70    JSONENCODE_TYPED_INTEGER_OPTION_EXTENSION,
71    JSONENCODE_NUMERIC_OPTION_EXTENSION,
72    JSONENCODE_TEXT_OPTION_EXTENSION,
73    JSONENCODE_COMPLEX_INPUT_EXTENSION,
74    JSONENCODE_SPARSE_INPUT_EXTENSION,
75];
76
77const JSONENCODE_REAL_INTEGER_INPUT: [BuiltinIntegerInputCapability; 1] =
78    [BuiltinIntegerInputCapability {
79        name: "value",
80        classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
81        availability: BuiltinIntegerInputAvailability::Documented,
82        scalar_double: BuiltinIntegerScalarDoubleRule::NotApplicable,
83        notes: "All eight real integer classes serialize from authoritative storage to exact signed or unsigned JSON decimal literals.",
84    }];
85const JSONENCODE_INTEGER_OPTION_INPUT: [BuiltinIntegerInputCapability; 1] =
86    [BuiltinIntegerInputCapability {
87        name: "PrettyPrint_or_ConvertInfAndNaN",
88        classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
89        availability: BuiltinIntegerInputAvailability::RunMatOnly,
90        scalar_double: BuiltinIntegerScalarDoubleRule::Allowed,
91        notes: "MATLAB-compatible mode requires logical option values; RunMat mode treats exact integer zero as false and every nonzero integer as true.",
92    }];
93const JSONENCODE_COMPLEX_INTEGER_INPUT: [BuiltinIntegerInputCapability; 1] =
94    [BuiltinIntegerInputCapability {
95        name: "value",
96        classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
97        availability: BuiltinIntegerInputAvailability::RunMatOnly,
98        scalar_double: BuiltinIntegerScalarDoubleRule::NotApplicable,
99        notes: "The public jsonencode contract rejects complex numeric data. RunMat mode retains its exact {real,imag} object encoding for paired integer storage.",
100    }];
101pub const JSONENCODE_INTEGER_CAPABILITIES: [BuiltinIntegerCapabilityDescriptor; 3] = [
102    BuiltinIntegerCapabilityDescriptor {
103        form: "jsonText = jsonencode(full_real_integer_value)",
104        inputs: &JSONENCODE_REAL_INTEGER_INPUT,
105        computation_domain: BuiltinIntegerComputationDomain::ExactInteger,
106        output_class: BuiltinIntegerOutputClassRule::FunctionSpecific,
107        overflow: BuiltinIntegerOverflowRule::Error,
108        backend: BuiltinIntegerBackendRule::GatherFallback,
109        overload: BuiltinIntegerOverloadKind::Multiple,
110        notes: "Host and automatically resident integer values serialize exactly. Explicit gpuArray input is independently compatibility-gated before owner access; accepted resident values gather through their owner at the serialization sink.",
111    },
112    BuiltinIntegerCapabilityDescriptor {
113        form: "jsonText = jsonencode(value, option, integer_flag)",
114        inputs: &JSONENCODE_INTEGER_OPTION_INPUT,
115        computation_domain: BuiltinIntegerComputationDomain::Predicate,
116        output_class: BuiltinIntegerOutputClassRule::FunctionSpecific,
117        overflow: BuiltinIntegerOverflowRule::NotApplicable,
118        backend: BuiltinIntegerBackendRule::GatherFallback,
119        overload: BuiltinIntegerOverloadKind::ScalarOnly,
120        notes: "Typed numeric option coercion is a RunMat convenience and is gated before resident access.",
121    },
122    BuiltinIntegerCapabilityDescriptor {
123        form: "jsonText = jsonencode(complex_integer_value)",
124        inputs: &JSONENCODE_COMPLEX_INTEGER_INPUT,
125        computation_domain: BuiltinIntegerComputationDomain::ExactInteger,
126        output_class: BuiltinIntegerOutputClassRule::FunctionSpecific,
127        overflow: BuiltinIntegerOverflowRule::Error,
128        backend: BuiltinIntegerBackendRule::GatherFallback,
129        overload: BuiltinIntegerOverloadKind::Multiple,
130        notes: "Complex JSON encoding is outside the public compatibility surface and requires the complex-input extension; integer components remain exact in RunMat mode.",
131    },
132];
133
134const JSONENCODE_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
135    name: "jsonText",
136    ty: BuiltinParamType::StringScalar,
137    arity: BuiltinParamArity::Required,
138    default: None,
139    description: "JSON text encoded as a character row vector.",
140}];
141const JSONENCODE_INPUTS_VALUE: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
142    name: "value",
143    ty: BuiltinParamType::Any,
144    arity: BuiltinParamArity::Required,
145    default: None,
146    description: "Value to encode as JSON.",
147}];
148const JSONENCODE_INPUTS_VALUE_OPTIONS: [BuiltinParamDescriptor; 2] = [
149    BuiltinParamDescriptor {
150        name: "value",
151        ty: BuiltinParamType::Any,
152        arity: BuiltinParamArity::Required,
153        default: None,
154        description: "Value to encode as JSON.",
155    },
156    BuiltinParamDescriptor {
157        name: "options",
158        ty: BuiltinParamType::Any,
159        arity: BuiltinParamArity::Required,
160        default: None,
161        description: "Options struct with fields such as PrettyPrint and ConvertInfAndNaN.",
162    },
163];
164const JSONENCODE_INPUTS_VALUE_NAME_VALUE: [BuiltinParamDescriptor; 3] = [
165    BuiltinParamDescriptor {
166        name: "value",
167        ty: BuiltinParamType::Any,
168        arity: BuiltinParamArity::Required,
169        default: None,
170        description: "Value to encode as JSON.",
171    },
172    BuiltinParamDescriptor {
173        name: "name",
174        ty: BuiltinParamType::StringScalar,
175        arity: BuiltinParamArity::Required,
176        default: None,
177        description: "Option name (for example \"PrettyPrint\" or \"ConvertInfAndNaN\").",
178    },
179    BuiltinParamDescriptor {
180        name: "optionValue",
181        ty: BuiltinParamType::Any,
182        arity: BuiltinParamArity::Required,
183        default: None,
184        description: "Option value for the preceding option name.",
185    },
186];
187const JSONENCODE_INPUTS_VALUE_NAME_VALUE_VARIADIC: [BuiltinParamDescriptor; 2] = [
188    BuiltinParamDescriptor {
189        name: "value",
190        ty: BuiltinParamType::Any,
191        arity: BuiltinParamArity::Required,
192        default: None,
193        description: "Value to encode as JSON.",
194    },
195    BuiltinParamDescriptor {
196        name: "nameValuePairs...",
197        ty: BuiltinParamType::Any,
198        arity: BuiltinParamArity::Variadic,
199        default: None,
200        description: "Name-value option pairs.",
201    },
202];
203const JSONENCODE_SIGNATURES: [BuiltinSignatureDescriptor; 4] = [
204    BuiltinSignatureDescriptor {
205        label: "jsonText = jsonencode(value)",
206        inputs: &JSONENCODE_INPUTS_VALUE,
207        outputs: &JSONENCODE_OUTPUT,
208    },
209    BuiltinSignatureDescriptor {
210        label: "jsonText = jsonencode(value, options)",
211        inputs: &JSONENCODE_INPUTS_VALUE_OPTIONS,
212        outputs: &JSONENCODE_OUTPUT,
213    },
214    BuiltinSignatureDescriptor {
215        label: "jsonText = jsonencode(value, name, optionValue)",
216        inputs: &JSONENCODE_INPUTS_VALUE_NAME_VALUE,
217        outputs: &JSONENCODE_OUTPUT,
218    },
219    BuiltinSignatureDescriptor {
220        label: "jsonText = jsonencode(value, nameValuePairs...)",
221        inputs: &JSONENCODE_INPUTS_VALUE_NAME_VALUE_VARIADIC,
222        outputs: &JSONENCODE_OUTPUT,
223    },
224];
225const JSONENCODE_ERROR_OPTIONS_CONFIG: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
226    code: "RM.JSONENCODE.OPTIONS_CONFIG",
227    identifier: None,
228    when: "Single options argument is provided but is not a struct.",
229    message: "jsonencode: expected name/value pairs or options struct",
230};
231const JSONENCODE_ERROR_NAME_VALUE_PAIRS: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
232    code: "RM.JSONENCODE.NAME_VALUE_PAIRS",
233    identifier: None,
234    when: "Name-value options do not come in pairs.",
235    message: "jsonencode: name/value pairs must come in pairs",
236};
237const JSONENCODE_ERROR_OPTION_NAME: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
238    code: "RM.JSONENCODE.OPTION_NAME",
239    identifier: None,
240    when: "Option name is not a character vector or string scalar.",
241    message: "jsonencode: option names must be character vectors or strings",
242};
243const JSONENCODE_ERROR_OPTION_VALUE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
244    code: "RM.JSONENCODE.OPTION_VALUE",
245    identifier: None,
246    when: "Option value is not a scalar logical/numeric or boolean-like text.",
247    message: "jsonencode: option value must be scalar logical or numeric",
248};
249const JSONENCODE_ERROR_UNKNOWN_OPTION: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
250    code: "RM.JSONENCODE.UNKNOWN_OPTION",
251    identifier: None,
252    when: "Option name is not recognized.",
253    message: "jsonencode: unknown option name",
254};
255const JSONENCODE_ERROR_INF_NAN: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
256    code: "RM.JSONENCODE.INF_NAN",
257    identifier: None,
258    when: "Input contains NaN/Inf while ConvertInfAndNaN is false.",
259    message: "jsonencode: ConvertInfAndNaN must be true to encode NaN or Inf values",
260};
261const JSONENCODE_ERROR_UNSUPPORTED_TYPE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
262    code: "RM.JSONENCODE.UNSUPPORTED_TYPE",
263    identifier: None,
264    when: "Input value type is not supported for JSON encoding.",
265    message:
266        "jsonencode: unsupported input type; expected numeric, logical, string, struct, cell, or object data",
267};
268const JSONENCODE_ERROR_UNEXPECTED_GPU: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
269    code: "RM.JSONENCODE.UNEXPECTED_GPU",
270    identifier: None,
271    when: "A GPU tensor handle reaches encoding after gather pass.",
272    message: "jsonencode: unexpected gpuArray handle after gather pass",
273};
274const JSONENCODE_ERROR_INTERNAL: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
275    code: "RM.JSONENCODE.INTERNAL",
276    identifier: None,
277    when: "Internal JSON conversion or container materialization fails.",
278    message: "jsonencode: internal conversion failed",
279};
280const JSONENCODE_ERRORS: [BuiltinErrorDescriptor; 9] = [
281    JSONENCODE_ERROR_OPTIONS_CONFIG,
282    JSONENCODE_ERROR_NAME_VALUE_PAIRS,
283    JSONENCODE_ERROR_OPTION_NAME,
284    JSONENCODE_ERROR_OPTION_VALUE,
285    JSONENCODE_ERROR_UNKNOWN_OPTION,
286    JSONENCODE_ERROR_INF_NAN,
287    JSONENCODE_ERROR_UNSUPPORTED_TYPE,
288    JSONENCODE_ERROR_UNEXPECTED_GPU,
289    JSONENCODE_ERROR_INTERNAL,
290];
291pub const JSONENCODE_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
292    signatures: &JSONENCODE_SIGNATURES,
293    output_mode: BuiltinOutputMode::Fixed,
294    completion_policy: BuiltinCompletionPolicy::Public,
295    errors: &JSONENCODE_ERRORS,
296};
297
298#[allow(clippy::too_many_lines)]
299#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::io::json::jsonencode")]
300pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
301    name: "jsonencode",
302    op_kind: GpuOpKind::Custom("serialization"),
303    supported_precisions: &[],
304    broadcast: BroadcastSemantics::None,
305    provider_hooks: &[],
306    constant_strategy: ConstantStrategy::InlineLiteral,
307    residency: ResidencyPolicy::GatherImmediately,
308    nan_mode: ReductionNaN::Include,
309    two_pass_threshold: None,
310    workgroup_size: None,
311    accepts_nan_mode: false,
312    notes:
313        "Serialization sink that gathers GPU data to host memory before emitting UTF-8 JSON text.",
314};
315
316fn jsonencode_error(error: &'static BuiltinErrorDescriptor) -> RuntimeError {
317    jsonencode_error_with(error, error.message)
318}
319
320fn jsonencode_error_with(
321    error: &'static BuiltinErrorDescriptor,
322    message: impl Into<String>,
323) -> RuntimeError {
324    let mut builder = build_runtime_error(message).with_builtin(BUILTIN_NAME);
325    if let Some(identifier) = error.identifier {
326        builder = builder.with_identifier(identifier);
327    }
328    builder.build()
329}
330
331fn jsonencode_flow_with_context(err: RuntimeError) -> RuntimeError {
332    let mut builder = build_runtime_error(err.message().to_string()).with_builtin(BUILTIN_NAME);
333    if let Some(identifier) = err.identifier() {
334        builder = builder.with_identifier(identifier.to_string());
335    }
336    builder.with_source(err).build()
337}
338
339#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::io::json::jsonencode")]
340pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
341    name: "jsonencode",
342    shape: ShapeRequirements::Any,
343    constant_strategy: ConstantStrategy::InlineLiteral,
344    elementwise: None,
345    reduction: None,
346    emits_nan: false,
347    notes: "jsonencode is a residency sink and never participates in fusion planning.",
348};
349
350#[derive(Debug, Clone)]
351struct JsonEncodeOptions {
352    pretty_print: bool,
353    convert_inf_and_nan: bool,
354}
355
356impl Default for JsonEncodeOptions {
357    fn default() -> Self {
358        Self {
359            pretty_print: false,
360            convert_inf_and_nan: true,
361        }
362    }
363}
364
365#[derive(Debug, Clone)]
366enum JsonValue {
367    Null,
368    Bool(bool),
369    Number(JsonNumber),
370    String(String),
371    Array(Vec<JsonValue>),
372    Object(Vec<(String, JsonValue)>),
373}
374
375#[derive(Debug, Clone)]
376enum JsonNumber {
377    Float(f64),
378    I64(i64),
379    U64(u64),
380}
381
382#[runtime_builtin(
383    name = "jsonencode",
384    category = "io/json",
385    summary = "Serialize MATLAB values to UTF-8 JSON text.",
386    keywords = "jsonencode,json,serialization,struct,gpu",
387    accel = "cpu",
388    type_resolver(crate::builtins::io::type_resolvers::jsonencode_type),
389    descriptor(crate::builtins::io::json::jsonencode::JSONENCODE_DESCRIPTOR),
390    extensions(crate::builtins::io::json::jsonencode::JSONENCODE_EXTENSIONS),
391    integer_capabilities(crate::builtins::io::json::jsonencode::JSONENCODE_INTEGER_CAPABILITIES),
392    builtin_path = "crate::builtins::io::json::jsonencode"
393)]
394async fn jsonencode_builtin(value: Value, rest: Vec<Value>) -> crate::BuiltinResult<Value> {
395    validate_option_layout(&rest)?;
396    preflight_jsonencode_extensions(&value, &rest)?;
397    let host_value = gather_if_needed_async(&value)
398        .await
399        .map_err(jsonencode_flow_with_context)?;
400    let mut gathered_args = Vec::with_capacity(rest.len());
401    for value in &rest {
402        gathered_args.push(
403            gather_if_needed_async(value)
404                .await
405                .map_err(jsonencode_flow_with_context)?,
406        );
407    }
408
409    preflight_option_extensions(&gathered_args)?;
410    let options = parse_options(&gathered_args)?;
411    let json_value = value_to_json(&host_value, &options)?;
412    let json_string = render_json(&json_value, &options);
413
414    Ok(Value::CharArray(CharArray::new_row(&json_string)))
415}
416
417fn preflight_jsonencode_extensions(value: &Value, rest: &[Value]) -> BuiltinResult<()> {
418    if value_contains_complex(value) {
419        crate::compatibility::ensure_builtin_extension_enabled(
420            &JSONENCODE_COMPLEX_INPUT_EXTENSION,
421            BUILTIN_NAME,
422        )?;
423    }
424    if value_contains_sparse(value) {
425        crate::compatibility::ensure_builtin_extension_enabled(
426            &JSONENCODE_SPARSE_INPUT_EXTENSION,
427            BUILTIN_NAME,
428        )?;
429    }
430    preflight_option_extensions(rest)?;
431    if value_contains_explicit_gpu(value) || rest.iter().any(value_contains_explicit_gpu) {
432        crate::compatibility::ensure_builtin_extension_enabled(
433            &JSONENCODE_RESIDENT_INPUT_EXTENSION,
434            BUILTIN_NAME,
435        )?;
436    }
437    Ok(())
438}
439
440fn validate_option_layout(rest: &[Value]) -> BuiltinResult<()> {
441    if rest.is_empty() {
442        return Ok(());
443    }
444    if rest.len() == 1 {
445        return match &rest[0] {
446            Value::Struct(options) => {
447                for name in options.fields.keys() {
448                    ensure_known_option(name)?;
449                }
450                Ok(())
451            }
452            _ => Err(jsonencode_error(&JSONENCODE_ERROR_OPTIONS_CONFIG)),
453        };
454    }
455    if !rest.len().is_multiple_of(2) {
456        return Err(jsonencode_error(&JSONENCODE_ERROR_NAME_VALUE_PAIRS));
457    }
458    for pair in rest.chunks_exact(2) {
459        let name = option_name(&pair[0])?;
460        ensure_known_option(&name)?;
461    }
462    Ok(())
463}
464
465fn ensure_known_option(name: &str) -> BuiltinResult<()> {
466    if name.eq_ignore_ascii_case("PrettyPrint") || name.eq_ignore_ascii_case("ConvertInfAndNaN") {
467        Ok(())
468    } else {
469        Err(jsonencode_error_with(
470            &JSONENCODE_ERROR_UNKNOWN_OPTION,
471            format!("{} ('{}')", JSONENCODE_ERROR_UNKNOWN_OPTION.message, name),
472        ))
473    }
474}
475
476fn preflight_option_extensions(rest: &[Value]) -> BuiltinResult<()> {
477    for (name, option_value) in raw_options(rest)? {
478        if !option_value_is_scalar(option_value) {
479            return Err(jsonencode_error(&JSONENCODE_ERROR_OPTION_VALUE));
480        }
481        if is_typed_integer_option(option_value) {
482            crate::compatibility::ensure_builtin_extension_enabled(
483                &JSONENCODE_TYPED_INTEGER_OPTION_EXTENSION,
484                BUILTIN_NAME,
485            )?;
486        } else if is_floating_numeric_option(option_value)
487            && !(name.eq_ignore_ascii_case("ConvertInfAndNaN")
488                && is_documented_numeric_bool(option_value))
489        {
490            crate::compatibility::ensure_builtin_extension_enabled(
491                &JSONENCODE_NUMERIC_OPTION_EXTENSION,
492                BUILTIN_NAME,
493            )?;
494        } else if is_text_option(option_value) {
495            crate::compatibility::ensure_builtin_extension_enabled(
496                &JSONENCODE_TEXT_OPTION_EXTENSION,
497                BUILTIN_NAME,
498            )?;
499        }
500    }
501    Ok(())
502}
503
504fn raw_options(rest: &[Value]) -> BuiltinResult<Vec<(String, &Value)>> {
505    if let [Value::Struct(options)] = rest {
506        return Ok(options
507            .fields
508            .iter()
509            .map(|(name, value)| (name.clone(), value))
510            .collect());
511    }
512    rest.chunks_exact(2)
513        .map(|pair| option_name(&pair[0]).map(|name| (name, &pair[1])))
514        .collect()
515}
516
517fn option_value_is_scalar(value: &Value) -> bool {
518    match value {
519        Value::Bool(_) | Value::Int(_) | Value::Num(_) | Value::String(_) => true,
520        Value::Tensor(tensor) => tensor::is_scalar_tensor(tensor),
521        Value::LogicalArray(logical) => logical.data.len() == 1,
522        Value::CharArray(chars) => chars.rows == 1,
523        Value::StringArray(strings) => strings.data.len() == 1,
524        Value::GpuTensor(handle) => handle.shape.iter().copied().product::<usize>() == 1,
525        _ => false,
526    }
527}
528
529fn is_documented_numeric_bool(value: &Value) -> bool {
530    let number = match value {
531        Value::Num(number) => Some(*number),
532        Value::Tensor(tensor) if tensor::is_scalar_tensor(tensor) => {
533            Some(tensor::tensor_value_f64(tensor, 0))
534        }
535        _ => None,
536    };
537    matches!(number, Some(0.0 | 1.0))
538}
539
540fn is_typed_integer_option(value: &Value) -> bool {
541    match value {
542        Value::Int(_) => true,
543        Value::Tensor(tensor) => tensor.integer_storage().is_some(),
544        Value::GpuTensor(handle) => runmat_accelerate_api::handle_integer_type(handle).is_some(),
545        _ => false,
546    }
547}
548
549fn is_floating_numeric_option(value: &Value) -> bool {
550    match value {
551        Value::Num(_) => true,
552        Value::Tensor(tensor) => tensor.integer_storage().is_none(),
553        Value::GpuTensor(_) => false,
554        _ => false,
555    }
556}
557
558fn is_text_option(value: &Value) -> bool {
559    matches!(
560        value,
561        Value::String(_) | Value::StringArray(_) | Value::CharArray(_)
562    )
563}
564
565fn value_contains_complex(value: &Value) -> bool {
566    match value {
567        Value::Complex(_, _) | Value::ComplexTensor(_) => true,
568        Value::GpuTensor(handle) => {
569            runmat_accelerate_api::handle_storage(handle)
570                != runmat_accelerate_api::GpuTensorStorage::Real
571        }
572        Value::Cell(cell) => cell.data.iter().any(value_contains_complex),
573        Value::Struct(struct_value) => struct_value.fields.values().any(value_contains_complex),
574        Value::Object(object) => object.properties.values().any(value_contains_complex),
575        _ => false,
576    }
577}
578
579fn value_contains_sparse(value: &Value) -> bool {
580    match value {
581        Value::SparseTensor(_) => true,
582        Value::Cell(cell) => cell.data.iter().any(value_contains_sparse),
583        Value::Struct(value) => value.fields.values().any(value_contains_sparse),
584        Value::Object(value) => value.properties.values().any(value_contains_sparse),
585        _ => false,
586    }
587}
588
589fn value_contains_explicit_gpu(value: &Value) -> bool {
590    match value {
591        Value::GpuTensor(handle) => runmat_accelerate_api::handle_is_explicit(handle),
592        Value::Cell(cell) => cell.data.iter().any(value_contains_explicit_gpu),
593        Value::Struct(value) => value.fields.values().any(value_contains_explicit_gpu),
594        Value::Object(value) => value.properties.values().any(value_contains_explicit_gpu),
595        _ => false,
596    }
597}
598
599fn parse_options(args: &[Value]) -> BuiltinResult<JsonEncodeOptions> {
600    let mut options = JsonEncodeOptions::default();
601    if args.is_empty() {
602        return Ok(options);
603    }
604
605    if args.len() == 1 {
606        if let Value::Struct(struct_value) = &args[0] {
607            apply_struct_options(struct_value, &mut options)?;
608            return Ok(options);
609        }
610        return Err(jsonencode_error(&JSONENCODE_ERROR_OPTIONS_CONFIG));
611    }
612
613    if !args.len().is_multiple_of(2) {
614        return Err(jsonencode_error(&JSONENCODE_ERROR_NAME_VALUE_PAIRS));
615    }
616
617    let mut idx = 0usize;
618    while idx < args.len() {
619        let name = option_name(&args[idx])?;
620        let value = &args[idx + 1];
621        apply_option(&name, value, &mut options)?;
622        idx += 2;
623    }
624
625    Ok(options)
626}
627
628fn apply_struct_options(
629    struct_value: &StructValue,
630    options: &mut JsonEncodeOptions,
631) -> BuiltinResult<()> {
632    for (key, value) in &struct_value.fields {
633        apply_option(key, value, options)?;
634    }
635    Ok(())
636}
637
638fn option_name(value: &Value) -> BuiltinResult<String> {
639    match value {
640        Value::String(s) => Ok(s.clone()),
641        Value::CharArray(ca) if ca.rows == 1 => Ok(ca.data.iter().collect()),
642        Value::StringArray(sa) if sa.data.len() == 1 => Ok(sa.data[0].clone()),
643        _ => Err(jsonencode_error(&JSONENCODE_ERROR_OPTION_NAME)),
644    }
645}
646
647fn apply_option(
648    raw_name: &str,
649    value: &Value,
650    options: &mut JsonEncodeOptions,
651) -> BuiltinResult<()> {
652    let lowered = raw_name.to_ascii_lowercase();
653    match lowered.as_str() {
654        "prettyprint" => {
655            options.pretty_print = coerce_bool(value)?;
656            Ok(())
657        }
658        "convertinfandnan" => {
659            options.convert_inf_and_nan = coerce_bool(value)?;
660            Ok(())
661        }
662        other => Err(jsonencode_error_with(
663            &JSONENCODE_ERROR_UNKNOWN_OPTION,
664            format!("{} ('{}')", JSONENCODE_ERROR_UNKNOWN_OPTION.message, other),
665        )),
666    }
667}
668
669fn coerce_bool(value: &Value) -> BuiltinResult<bool> {
670    match value {
671        Value::Bool(b) => Ok(*b),
672        Value::Int(i) => Ok(!i.is_zero()),
673        Value::Num(n) => bool_from_f64(*n),
674        Value::Tensor(t) => {
675            if tensor::is_scalar_tensor(t) {
676                if let Some(int) = t.integer_storage().and_then(|storage| storage.value_at(0)) {
677                    return Ok(!int.is_zero());
678                }
679                bool_from_f64(tensor::tensor_value_f64(t, 0))
680            } else {
681                Err(jsonencode_error(&JSONENCODE_ERROR_OPTION_VALUE))
682            }
683        }
684        Value::LogicalArray(la) => match la.data.len() {
685            1 => Ok(la.data[0] != 0),
686            _ => Err(jsonencode_error(&JSONENCODE_ERROR_OPTION_VALUE)),
687        },
688        Value::CharArray(ca) if ca.rows == 1 => {
689            parse_bool_string(&ca.data.iter().collect::<String>())
690        }
691        Value::String(s) => parse_bool_string(s),
692        Value::StringArray(sa) if sa.data.len() == 1 => parse_bool_string(&sa.data[0]),
693        _ => Err(jsonencode_error(&JSONENCODE_ERROR_OPTION_VALUE)),
694    }
695}
696
697fn bool_from_f64(value: f64) -> BuiltinResult<bool> {
698    if value.is_finite() {
699        Ok(value != 0.0)
700    } else {
701        Err(jsonencode_error(&JSONENCODE_ERROR_OPTION_VALUE))
702    }
703}
704
705fn parse_bool_string(text: &str) -> BuiltinResult<bool> {
706    match text.trim().to_ascii_lowercase().as_str() {
707        "true" | "on" | "yes" | "1" => Ok(true),
708        "false" | "off" | "no" | "0" => Ok(false),
709        _ => Err(jsonencode_error(&JSONENCODE_ERROR_OPTION_VALUE)),
710    }
711}
712
713fn value_to_json(value: &Value, options: &JsonEncodeOptions) -> BuiltinResult<JsonValue> {
714    match value {
715        Value::Num(n) => number_to_json(*n, options),
716        Value::Int(i) => Ok(JsonValue::Number(int_to_number(i))),
717        Value::Bool(b) => Ok(JsonValue::Bool(*b)),
718        Value::LogicalArray(logical) => logical_array_to_json(logical, options),
719        Value::Tensor(tensor) => tensor_to_json(tensor, options),
720        Value::SparseTensor(sparse) => {
721            let total_elements = sparse.rows.checked_mul(sparse.cols).ok_or_else(|| {
722                jsonencode_error_with(
723                    &JSONENCODE_ERROR_INTERNAL,
724                    "jsonencode: sparse matrix dimensions overflow",
725                )
726            })?;
727            if total_elements > 10_000_000 {
728                return Err(jsonencode_error_with(
729                    &JSONENCODE_ERROR_INTERNAL,
730                    format!("jsonencode: cannot densify sparse tensor {}x{} ({} elements exceeds safe threshold)", sparse.rows, sparse.cols, total_elements),
731                ));
732            }
733            if sparse.is_logical() {
734                let dense = sparse.to_dense_logical().map_err(|err| {
735                    jsonencode_error_with(&JSONENCODE_ERROR_INTERNAL, format!("jsonencode: {err}"))
736                })?;
737                return logical_array_to_json(&dense, options);
738            }
739            let dense = sparse.to_dense().map_err(|err| {
740                jsonencode_error_with(&JSONENCODE_ERROR_INTERNAL, format!("jsonencode: {err}"))
741            })?;
742            tensor_to_json(&dense, options)
743        }
744        Value::Complex(re, im) => complex_scalar_to_json(*re, *im, options),
745        Value::ComplexTensor(ct) => complex_tensor_to_json(ct, options),
746        Value::String(s) => Ok(JsonValue::String(s.clone())),
747        Value::Symbolic(expr) => Ok(JsonValue::String(expr.to_string())),
748        Value::SymbolicArray(array) => symbolic_array_to_json(array, options),
749        Value::StringArray(sa) => string_array_to_json(sa, options),
750        Value::CharArray(ca) => char_array_to_json(ca, options),
751        Value::Struct(sv) => struct_to_json(sv, options),
752        Value::Cell(ca) => cell_array_to_json(ca, options),
753        Value::ObjectArray(array) => array
754            .data()
755            .iter()
756            .map(|value| value_to_json(value, options))
757            .collect::<BuiltinResult<Vec<_>>>()
758            .map(JsonValue::Array),
759        Value::Object(obj) => object_to_json(obj, options),
760        Value::GpuTensor(_) => Err(jsonencode_error(&JSONENCODE_ERROR_UNEXPECTED_GPU)),
761        Value::HandleObject(_)
762        | Value::Listener(_)
763        | Value::FunctionHandle(_)
764        | Value::ExternalFunctionHandle(_)
765        | Value::MethodFunctionHandle(_)
766        | Value::BoundFunctionHandle { .. }
767        | Value::Closure(_)
768        | Value::ClassRef(_)
769        | Value::MException(_)
770        | Value::Future(_)
771        | Value::Task(_)
772        | Value::Pool(_)
773        | Value::Job(_)
774        | Value::Foreign(_)
775        | Value::OutputList(_) => Err(jsonencode_error(&JSONENCODE_ERROR_UNSUPPORTED_TYPE)),
776    }
777}
778
779fn int_to_number(value: &IntValue) -> JsonNumber {
780    match value {
781        IntValue::I8(v) => JsonNumber::I64(*v as i64),
782        IntValue::I16(v) => JsonNumber::I64(*v as i64),
783        IntValue::I32(v) => JsonNumber::I64(*v as i64),
784        IntValue::I64(v) => JsonNumber::I64(*v),
785        IntValue::U8(v) => JsonNumber::U64(*v as u64),
786        IntValue::U16(v) => JsonNumber::U64(*v as u64),
787        IntValue::U32(v) => JsonNumber::U64(*v as u64),
788        IntValue::U64(v) => JsonNumber::U64(*v),
789    }
790}
791
792fn number_to_json(value: f64, options: &JsonEncodeOptions) -> BuiltinResult<JsonValue> {
793    if !value.is_finite() {
794        if options.convert_inf_and_nan {
795            return Ok(JsonValue::Null);
796        }
797        return Err(jsonencode_error(&JSONENCODE_ERROR_INF_NAN));
798    }
799    Ok(JsonValue::Number(JsonNumber::Float(value)))
800}
801
802fn logical_array_to_json(
803    logical: &LogicalArray,
804    _options: &JsonEncodeOptions,
805) -> BuiltinResult<JsonValue> {
806    let keep_dims = compute_keep_dims(&logical.shape, true);
807    if logical.shape.is_empty() || logical.data.is_empty() {
808        return Ok(JsonValue::Array(Vec::new()));
809    }
810    if keep_dims.is_empty() {
811        let first = logical.data.first().copied().unwrap_or(0) != 0;
812        return Ok(JsonValue::Bool(first));
813    }
814    build_strided_array(&logical.shape, &keep_dims, |offset| {
815        Ok(JsonValue::Bool(logical.data[offset] != 0))
816    })
817}
818
819fn tensor_to_json(tensor: &Tensor, options: &JsonEncodeOptions) -> BuiltinResult<JsonValue> {
820    if tensor::tensor_element_len(tensor) == 0 {
821        return Ok(JsonValue::Array(Vec::new()));
822    }
823    let keep_dims = compute_keep_dims(&tensor.shape, true);
824    if keep_dims.is_empty() {
825        return tensor_value_to_json(tensor, 0, options);
826    }
827    build_strided_array(&tensor.shape, &keep_dims, |offset| {
828        tensor_value_to_json(tensor, offset, options)
829    })
830}
831
832fn tensor_value_to_json(
833    tensor: &Tensor,
834    offset: usize,
835    options: &JsonEncodeOptions,
836) -> BuiltinResult<JsonValue> {
837    let value = tensor
838        .numeric_value_at(offset)
839        .expect("index within authoritative numeric storage");
840    match value.into_int_value() {
841        Some(value) => Ok(JsonValue::Number(integer_value_number(&value))),
842        None => number_to_json(value.materialize_f64(), options),
843    }
844}
845
846fn integer_value_number(value: &IntValue) -> JsonNumber {
847    match value {
848        IntValue::I8(value) => JsonNumber::I64(*value as i64),
849        IntValue::I16(value) => JsonNumber::I64(*value as i64),
850        IntValue::I32(value) => JsonNumber::I64(*value as i64),
851        IntValue::I64(value) => JsonNumber::I64(*value),
852        IntValue::U8(value) => JsonNumber::U64(*value as u64),
853        IntValue::U16(value) => JsonNumber::U64(*value as u64),
854        IntValue::U32(value) => JsonNumber::U64(*value as u64),
855        IntValue::U64(value) => JsonNumber::U64(*value),
856    }
857}
858
859fn integer_storage_number(storage: &IntegerStorage, offset: usize) -> JsonNumber {
860    integer_value_number(
861        &storage
862            .value_at(offset)
863            .expect("index within authoritative integer storage"),
864    )
865}
866
867fn complex_scalar_to_json(
868    real: f64,
869    imag: f64,
870    options: &JsonEncodeOptions,
871) -> BuiltinResult<JsonValue> {
872    let real_json = number_to_json(real, options)?;
873    let imag_json = number_to_json(imag, options)?;
874    Ok(JsonValue::Object(vec![
875        ("real".to_string(), real_json),
876        ("imag".to_string(), imag_json),
877    ]))
878}
879
880fn complex_tensor_to_json(
881    ct: &ComplexTensor,
882    options: &JsonEncodeOptions,
883) -> BuiltinResult<JsonValue> {
884    if tensor::complex_tensor_element_len(ct) == 0 {
885        return Ok(JsonValue::Array(Vec::new()));
886    }
887    let keep_dims = compute_keep_dims(&ct.shape, true);
888    if keep_dims.is_empty() {
889        return complex_tensor_value_to_json(ct, 0, options);
890    }
891    build_strided_array(&ct.shape, &keep_dims, |offset| {
892        complex_tensor_value_to_json(ct, offset, options)
893    })
894}
895
896fn complex_tensor_value_to_json(
897    ct: &ComplexTensor,
898    offset: usize,
899    options: &JsonEncodeOptions,
900) -> BuiltinResult<JsonValue> {
901    if let Some(storage) = &ct.integer_storage() {
902        return Ok(JsonValue::Object(vec![
903            (
904                "real".to_string(),
905                JsonValue::Number(integer_storage_number(&storage.real, offset)),
906            ),
907            (
908                "imag".to_string(),
909                JsonValue::Number(integer_storage_number(&storage.imag, offset)),
910            ),
911        ]));
912    }
913
914    let (real, imag) = ct.materialize_f64()[offset];
915    complex_scalar_to_json(real, imag, options)
916}
917
918fn string_array_to_json(
919    sa: &StringArray,
920    _options: &JsonEncodeOptions,
921) -> BuiltinResult<JsonValue> {
922    if sa.data.is_empty() {
923        return Ok(JsonValue::Array(Vec::new()));
924    }
925    let keep_dims = compute_keep_dims(&sa.shape, true);
926    if keep_dims.is_empty() {
927        return Ok(JsonValue::String(sa.data[0].clone()));
928    }
929    build_strided_array(&sa.shape, &keep_dims, |offset| {
930        Ok(JsonValue::String(sa.data[offset].clone()))
931    })
932}
933
934fn symbolic_array_to_json(
935    array: &SymbolicArray,
936    _options: &JsonEncodeOptions,
937) -> BuiltinResult<JsonValue> {
938    if array.data.is_empty() {
939        return Ok(JsonValue::Array(Vec::new()));
940    }
941    let keep_dims = compute_keep_dims(&array.shape, true);
942    if keep_dims.is_empty() {
943        return Ok(JsonValue::String(array.data[0].to_string()));
944    }
945    build_strided_array(&array.shape, &keep_dims, |offset| {
946        Ok(JsonValue::String(array.data[offset].to_string()))
947    })
948}
949
950fn char_array_to_json(ca: &CharArray, _options: &JsonEncodeOptions) -> BuiltinResult<JsonValue> {
951    if ca.rows == 0 {
952        return Ok(JsonValue::Array(Vec::new()));
953    }
954
955    if ca.cols == 0 {
956        if ca.rows == 1 {
957            return Ok(JsonValue::String(String::new()));
958        }
959        let mut rows = Vec::with_capacity(ca.rows);
960        for _ in 0..ca.rows {
961            rows.push(JsonValue::String(String::new()));
962        }
963        return Ok(JsonValue::Array(rows));
964    }
965
966    if ca.rows == 1 {
967        return Ok(JsonValue::String(ca.data.iter().collect()));
968    }
969
970    let mut rows = Vec::with_capacity(ca.rows);
971    for r in 0..ca.rows {
972        let mut row_string = String::with_capacity(ca.cols);
973        for c in 0..ca.cols {
974            row_string.push(ca.data[r * ca.cols + c]);
975        }
976        rows.push(JsonValue::String(row_string));
977    }
978    Ok(JsonValue::Array(rows))
979}
980
981fn struct_to_json(sv: &StructValue, options: &JsonEncodeOptions) -> BuiltinResult<JsonValue> {
982    if sv.fields.is_empty() {
983        return Ok(JsonValue::Object(Vec::new()));
984    }
985    let mut map = BTreeMap::new();
986    for (key, value) in &sv.fields {
987        map.insert(key.clone(), value_to_json(value, options)?);
988    }
989    Ok(JsonValue::Object(map.into_iter().collect()))
990}
991
992fn object_to_json(obj: &ObjectInstance, options: &JsonEncodeOptions) -> BuiltinResult<JsonValue> {
993    let mut map = BTreeMap::new();
994    for (key, value) in &obj.properties {
995        map.insert(key.clone(), value_to_json(value, options)?);
996    }
997    Ok(JsonValue::Object(map.into_iter().collect()))
998}
999
1000fn cell_array_to_json(ca: &CellArray, options: &JsonEncodeOptions) -> BuiltinResult<JsonValue> {
1001    if ca.rows == 0 || ca.cols == 0 {
1002        return Ok(JsonValue::Array(Vec::new()));
1003    }
1004
1005    if ca.rows == 1 && ca.cols == 1 {
1006        let value = ca.get(0, 0).map_err(|e| {
1007            jsonencode_error_with(
1008                &JSONENCODE_ERROR_INTERNAL,
1009                format!("{} ({e})", JSONENCODE_ERROR_INTERNAL.message),
1010            )
1011        })?;
1012        return Ok(JsonValue::Array(vec![value_to_json(&value, options)?]));
1013    }
1014
1015    if ca.rows == 1 {
1016        let mut row = Vec::with_capacity(ca.cols);
1017        for c in 0..ca.cols {
1018            let element = ca.get(0, c).map_err(|e| {
1019                jsonencode_error_with(
1020                    &JSONENCODE_ERROR_INTERNAL,
1021                    format!("{} ({e})", JSONENCODE_ERROR_INTERNAL.message),
1022                )
1023            })?;
1024            row.push(value_to_json(&element, options)?);
1025        }
1026        return Ok(JsonValue::Array(row));
1027    }
1028
1029    if ca.cols == 1 {
1030        let mut column = Vec::with_capacity(ca.rows);
1031        for r in 0..ca.rows {
1032            let element = ca.get(r, 0).map_err(|e| {
1033                jsonencode_error_with(
1034                    &JSONENCODE_ERROR_INTERNAL,
1035                    format!("{} ({e})", JSONENCODE_ERROR_INTERNAL.message),
1036                )
1037            })?;
1038            column.push(value_to_json(&element, options)?);
1039        }
1040        return Ok(JsonValue::Array(column));
1041    }
1042
1043    let mut rows = Vec::with_capacity(ca.rows);
1044    for r in 0..ca.rows {
1045        let mut row = Vec::with_capacity(ca.cols);
1046        for c in 0..ca.cols {
1047            let element = ca.get(r, c).map_err(|e| {
1048                jsonencode_error_with(
1049                    &JSONENCODE_ERROR_INTERNAL,
1050                    format!("{} ({e})", JSONENCODE_ERROR_INTERNAL.message),
1051                )
1052            })?;
1053            row.push(value_to_json(&element, options)?);
1054        }
1055        rows.push(JsonValue::Array(row));
1056    }
1057    Ok(JsonValue::Array(rows))
1058}
1059
1060fn compute_keep_dims(shape: &[usize], drop_singletons: bool) -> Vec<usize> {
1061    let mut keep = Vec::new();
1062    for (idx, &size) in shape.iter().enumerate() {
1063        if size != 1 || !drop_singletons {
1064            keep.push(idx);
1065        }
1066    }
1067    keep
1068}
1069
1070fn compute_strides(shape: &[usize]) -> Vec<usize> {
1071    let mut strides = Vec::with_capacity(shape.len());
1072    let mut acc = 1usize;
1073    for &size in shape {
1074        strides.push(acc);
1075        acc = acc.saturating_mul(size.max(1));
1076    }
1077    strides
1078}
1079
1080fn build_strided_array<F>(
1081    shape: &[usize],
1082    keep_dims: &[usize],
1083    mut fetch: F,
1084) -> BuiltinResult<JsonValue>
1085where
1086    F: FnMut(usize) -> BuiltinResult<JsonValue>,
1087{
1088    if keep_dims.is_empty() {
1089        return fetch(0);
1090    }
1091    if keep_dims.iter().any(|&idx| shape[idx] == 0) {
1092        return Ok(JsonValue::Array(Vec::new()));
1093    }
1094    let strides = compute_strides(shape);
1095    let dims: Vec<usize> = keep_dims.iter().map(|&idx| shape[idx]).collect();
1096    build_nd_array(&dims, |indices| {
1097        let mut offset = 0usize;
1098        for (value, dim_idx) in indices.iter().zip(keep_dims.iter()) {
1099            offset += value * strides[*dim_idx];
1100        }
1101        fetch(offset)
1102    })
1103}
1104
1105fn build_nd_array<F>(dims: &[usize], mut fetch: F) -> BuiltinResult<JsonValue>
1106where
1107    F: FnMut(&[usize]) -> BuiltinResult<JsonValue>,
1108{
1109    if dims.is_empty() {
1110        return fetch(&[]);
1111    }
1112    if dims[0] == 0 {
1113        return Ok(JsonValue::Array(Vec::new()));
1114    }
1115    let mut indices = vec![0usize; dims.len()];
1116    build_nd_array_recursive(dims, 0, &mut indices, &mut fetch)
1117}
1118
1119fn build_nd_array_recursive<F>(
1120    dims: &[usize],
1121    level: usize,
1122    indices: &mut [usize],
1123    fetch: &mut F,
1124) -> BuiltinResult<JsonValue>
1125where
1126    F: FnMut(&[usize]) -> BuiltinResult<JsonValue>,
1127{
1128    let size = dims[level];
1129    if size == 0 {
1130        return Ok(JsonValue::Array(Vec::new()));
1131    }
1132    if level + 1 == dims.len() {
1133        let mut items = Vec::with_capacity(size);
1134        for i in 0..size {
1135            indices[level] = i;
1136            items.push(fetch(indices)?);
1137        }
1138        return Ok(JsonValue::Array(items));
1139    }
1140    let mut items = Vec::with_capacity(size);
1141    for i in 0..size {
1142        indices[level] = i;
1143        items.push(build_nd_array_recursive(dims, level + 1, indices, fetch)?);
1144    }
1145    Ok(JsonValue::Array(items))
1146}
1147
1148fn render_json(value: &JsonValue, options: &JsonEncodeOptions) -> String {
1149    let mut writer = JsonWriter::new(options.pretty_print);
1150    writer.write_value(value);
1151    writer.finish()
1152}
1153
1154struct JsonWriter {
1155    output: String,
1156    pretty: bool,
1157    indent: usize,
1158}
1159
1160impl JsonWriter {
1161    fn new(pretty: bool) -> Self {
1162        Self {
1163            output: String::new(),
1164            pretty,
1165            indent: 0,
1166        }
1167    }
1168
1169    fn finish(self) -> String {
1170        self.output
1171    }
1172
1173    fn write_value(&mut self, value: &JsonValue) {
1174        match value {
1175            JsonValue::Null => self.output.push_str("null"),
1176            JsonValue::Bool(true) => self.output.push_str("true"),
1177            JsonValue::Bool(false) => self.output.push_str("false"),
1178            JsonValue::Number(number) => self.write_number(number),
1179            JsonValue::String(text) => {
1180                self.output.push('"');
1181                self.output.push_str(&escape_json_string(text));
1182                self.output.push('"');
1183            }
1184            JsonValue::Array(items) => self.write_array(items),
1185            JsonValue::Object(fields) => self.write_object(fields),
1186        }
1187    }
1188
1189    fn write_number(&mut self, number: &JsonNumber) {
1190        match number {
1191            JsonNumber::Float(f) => {
1192                if f.is_nan() || !f.is_finite() {
1193                    self.output.push_str("null");
1194                } else {
1195                    self.output.push_str(&format_number(*f));
1196                }
1197            }
1198            JsonNumber::I64(i) => {
1199                let _ = write!(self.output, "{i}");
1200            }
1201            JsonNumber::U64(u) => {
1202                let _ = write!(self.output, "{u}");
1203            }
1204        }
1205    }
1206
1207    fn write_array(&mut self, items: &[JsonValue]) {
1208        if items.is_empty() {
1209            self.output.push_str("[]");
1210            return;
1211        }
1212        let inline = if self.pretty {
1213            items.iter().all(|item| {
1214                matches!(
1215                    item,
1216                    JsonValue::Null
1217                        | JsonValue::Bool(_)
1218                        | JsonValue::Number(_)
1219                        | JsonValue::String(_)
1220                )
1221            })
1222        } else {
1223            false
1224        };
1225        if inline {
1226            self.output.push('[');
1227            for (index, item) in items.iter().enumerate() {
1228                self.write_value(item);
1229                if index + 1 < items.len() {
1230                    self.output.push(',');
1231                }
1232            }
1233            self.output.push(']');
1234            return;
1235        }
1236        self.output.push('[');
1237        if self.pretty {
1238            self.output.push('\n');
1239            self.indent += 1;
1240        }
1241        for (index, item) in items.iter().enumerate() {
1242            if self.pretty {
1243                self.write_indent();
1244            }
1245            self.write_value(item);
1246            if index + 1 < items.len() {
1247                if self.pretty {
1248                    self.output.push_str(",\n");
1249                } else {
1250                    self.output.push(',');
1251                }
1252            }
1253        }
1254        if self.pretty {
1255            self.output.push('\n');
1256            if self.indent > 0 {
1257                self.indent -= 1;
1258            }
1259            self.write_indent();
1260        }
1261        self.output.push(']');
1262    }
1263
1264    fn write_object(&mut self, fields: &[(String, JsonValue)]) {
1265        if fields.is_empty() {
1266            self.output.push_str("{}");
1267            return;
1268        }
1269        self.output.push('{');
1270        if self.pretty {
1271            self.output.push('\n');
1272            self.indent += 1;
1273        }
1274        for (index, (key, value)) in fields.iter().enumerate() {
1275            if self.pretty {
1276                self.write_indent();
1277            }
1278            self.output.push('"');
1279            self.output.push_str(&escape_json_string(key));
1280            self.output.push('"');
1281            if self.pretty {
1282                self.output.push_str(": ");
1283            } else {
1284                self.output.push(':');
1285            }
1286            self.write_value(value);
1287            if index + 1 < fields.len() {
1288                if self.pretty {
1289                    self.output.push_str(",\n");
1290                } else {
1291                    self.output.push(',');
1292                }
1293            }
1294        }
1295        if self.pretty {
1296            self.output.push('\n');
1297            if self.indent > 0 {
1298                self.indent -= 1;
1299            }
1300            self.write_indent();
1301        }
1302        self.output.push('}');
1303    }
1304
1305    fn write_indent(&mut self) {
1306        if self.pretty {
1307            for _ in 0..self.indent {
1308                self.output.push_str("    ");
1309            }
1310        }
1311    }
1312}
1313
1314fn escape_json_string(value: &str) -> String {
1315    let mut escaped = String::with_capacity(value.len());
1316    for ch in value.chars() {
1317        match ch {
1318            '"' => escaped.push_str("\\\""),
1319            '\\' => escaped.push_str("\\\\"),
1320            '\u{08}' => escaped.push_str("\\b"),
1321            '\u{0C}' => escaped.push_str("\\f"),
1322            '\n' => escaped.push_str("\\n"),
1323            '\r' => escaped.push_str("\\r"),
1324            '\t' => escaped.push_str("\\t"),
1325            c if (c as u32) < 0x20 => {
1326                let _ = write!(escaped, "\\u{:04X}", c as u32);
1327            }
1328            _ => escaped.push(ch),
1329        }
1330    }
1331    escaped
1332}
1333
1334fn format_number(value: f64) -> String {
1335    if value.fract() == 0.0 {
1336        // Display integer-like doubles without decimal point
1337        format!("{:.0}", value)
1338    } else {
1339        format!("{}", value)
1340    }
1341}
1342
1343#[cfg(test)]
1344pub(crate) mod tests {
1345    use super::*;
1346    use crate::builtins::common::test_support;
1347    use futures::executor::block_on;
1348    use runmat_value::{
1349        CellArray, CharArray, ComplexTensor, LogicalArray, StringArray, StructValue, SymbolicArray,
1350        SymbolicExpr, Tensor,
1351    };
1352
1353    fn as_string(value: Value) -> String {
1354        match value {
1355            Value::CharArray(ca) => ca.data.iter().collect(),
1356            Value::String(s) => s,
1357            other => panic!("expected char array, got {:?}", other),
1358        }
1359    }
1360
1361    fn error_message(err: crate::RuntimeError) -> String {
1362        err.message().to_string()
1363    }
1364
1365    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1366    #[test]
1367    fn jsonencode_descriptor_signatures_cover_core_forms() {
1368        let labels: Vec<&str> = JSONENCODE_DESCRIPTOR
1369            .signatures
1370            .iter()
1371            .map(|sig| sig.label)
1372            .collect();
1373        assert!(labels.contains(&"jsonText = jsonencode(value)"));
1374        assert!(labels.contains(&"jsonText = jsonencode(value, options)"));
1375        assert!(labels.contains(&"jsonText = jsonencode(value, name, optionValue)"));
1376        assert!(labels.contains(&"jsonText = jsonencode(value, nameValuePairs...)"));
1377    }
1378
1379    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1380    #[test]
1381    fn jsonencode_scalar_double() {
1382        let encoded =
1383            block_on(jsonencode_builtin(Value::Num(5.0), Vec::new())).expect("jsonencode");
1384        assert_eq!(as_string(encoded), "5");
1385    }
1386
1387    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1388    #[test]
1389    fn jsonencode_symbolic_value_as_text() {
1390        let expr = SymbolicExpr::div_expr(
1391            SymbolicExpr::function(
1392                runmat_value::SymbolicFunction::Sin,
1393                SymbolicExpr::variable("x"),
1394            ),
1395            SymbolicExpr::variable("x"),
1396        );
1397        let encoded =
1398            block_on(jsonencode_builtin(Value::Symbolic(expr), Vec::new())).expect("jsonencode");
1399
1400        assert_eq!(as_string(encoded), "\"sin(x)/x\"");
1401    }
1402
1403    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1404    #[test]
1405    fn jsonencode_symbolic_array_preserves_matrix_shape() {
1406        let array = SymbolicArray::new(
1407            vec![
1408                SymbolicExpr::variable("a"),
1409                SymbolicExpr::variable("c"),
1410                SymbolicExpr::variable("b"),
1411                SymbolicExpr::variable("d"),
1412            ],
1413            vec![2, 2],
1414        )
1415        .expect("symbolic array");
1416
1417        let encoded = block_on(jsonencode_builtin(Value::SymbolicArray(array), Vec::new()))
1418            .expect("jsonencode");
1419
1420        assert_eq!(as_string(encoded), "[[\"a\",\"b\"],[\"c\",\"d\"]]");
1421    }
1422
1423    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1424    #[test]
1425    fn jsonencode_matrix_pretty_print() {
1426        let tensor = Tensor::new(vec![1.0, 4.0, 2.0, 5.0, 3.0, 6.0], vec![2, 3]).expect("tensor");
1427        let args = vec![Value::from("PrettyPrint"), Value::Bool(true)];
1428        let encoded =
1429            block_on(jsonencode_builtin(Value::Tensor(tensor), args)).expect("jsonencode");
1430        let expected = "[\n    [1,2,3],\n    [4,5,6]\n]";
1431        assert_eq!(as_string(encoded), expected);
1432    }
1433
1434    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1435    #[test]
1436    fn jsonencode_integer_tensor_preserves_exact_uint64_values() {
1437        let tensor =
1438            Tensor::new_integer(IntegerStorage::U64(vec![u64::MAX, 1_u64 << 63]), vec![1, 2])
1439                .expect("integer tensor");
1440
1441        let encoded =
1442            block_on(jsonencode_builtin(Value::Tensor(tensor), Vec::new())).expect("jsonencode");
1443
1444        assert_eq!(
1445            as_string(encoded),
1446            format!("[{},{}]", u64::MAX, 1_u64 << 63)
1447        );
1448    }
1449
1450    #[test]
1451    fn jsonencode_integer_tensor_serializes_all_eight_classes_exactly() {
1452        let cases = [
1453            (IntegerStorage::I8(vec![i8::MIN]), "-128"),
1454            (IntegerStorage::I16(vec![i16::MIN]), "-32768"),
1455            (IntegerStorage::I32(vec![i32::MIN]), "-2147483648"),
1456            (IntegerStorage::I64(vec![i64::MIN]), "-9223372036854775808"),
1457            (IntegerStorage::U8(vec![u8::MAX]), "255"),
1458            (IntegerStorage::U16(vec![u16::MAX]), "65535"),
1459            (IntegerStorage::U32(vec![u32::MAX]), "4294967295"),
1460            (IntegerStorage::U64(vec![u64::MAX]), "18446744073709551615"),
1461        ];
1462        for (storage, expected) in cases {
1463            let tensor = Tensor::new_integer(storage, vec![1, 1]).expect("integer tensor");
1464            let encoded = block_on(jsonencode_builtin(Value::Tensor(tensor), Vec::new()))
1465                .expect("jsonencode integer tensor");
1466            assert_eq!(as_string(encoded), expected);
1467        }
1468    }
1469
1470    #[test]
1471    fn jsonencode_real_integer_payload_remains_public_and_exact_in_strict_mode() {
1472        let _strict = crate::compatibility::push_runmat_extensions_enabled(false);
1473        let cases = [
1474            (Value::Int(IntValue::I8(i8::MIN)), "-128"),
1475            (Value::Int(IntValue::I16(i16::MIN)), "-32768"),
1476            (Value::Int(IntValue::I32(i32::MIN)), "-2147483648"),
1477            (Value::Int(IntValue::I64(i64::MIN)), "-9223372036854775808"),
1478            (Value::Int(IntValue::U8(u8::MAX)), "255"),
1479            (Value::Int(IntValue::U16(u16::MAX)), "65535"),
1480            (Value::Int(IntValue::U32(u32::MAX)), "4294967295"),
1481            (Value::Int(IntValue::U64(u64::MAX)), "18446744073709551615"),
1482        ];
1483        for (value, expected) in cases {
1484            let encoded = block_on(jsonencode_builtin(value, Vec::new()))
1485                .expect("documented real integer payload");
1486            assert_eq!(as_string(encoded), expected);
1487        }
1488    }
1489
1490    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1491    #[test]
1492    fn jsonencode_struct_round_trip() {
1493        let mut fields = StructValue::new();
1494        fields
1495            .fields
1496            .insert("name".to_string(), Value::from("RunMat"));
1497        fields
1498            .fields
1499            .insert("year".to_string(), Value::Int(IntValue::I32(2025)));
1500        let encoded =
1501            block_on(jsonencode_builtin(Value::Struct(fields), Vec::new())).expect("jsonencode");
1502        assert_eq!(as_string(encoded), "{\"name\":\"RunMat\",\"year\":2025}");
1503    }
1504
1505    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1506    #[test]
1507    fn jsonencode_struct_options_enable_pretty_print() {
1508        let tensor = Tensor::new(vec![1.0, 4.0, 2.0, 5.0], vec![2, 2]).expect("tensor");
1509        let mut opts = StructValue::new();
1510        opts.fields
1511            .insert("PrettyPrint".to_string(), Value::Bool(true));
1512        let encoded = block_on(jsonencode_builtin(
1513            Value::Tensor(tensor),
1514            vec![Value::Struct(opts)],
1515        ))
1516        .expect("jsonencode");
1517        let expected = "[\n    [1,2],\n    [4,5]\n]";
1518        assert_eq!(as_string(encoded), expected);
1519    }
1520
1521    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1522    #[test]
1523    fn jsonencode_options_accept_scalar_tensor_bool() {
1524        let _runmat = crate::compatibility::push_runmat_extensions_enabled(true);
1525        let tensor_value = Tensor::new(vec![1.0], vec![1, 1]).expect("tensor");
1526        let args = vec![Value::from("PrettyPrint"), Value::Tensor(tensor_value)];
1527        let encoded = block_on(jsonencode_builtin(Value::Num(42.0), args)).expect("jsonencode");
1528        assert_eq!(as_string(encoded), "42");
1529    }
1530
1531    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1532    #[test]
1533    fn jsonencode_options_read_wide_uint64_storage_exactly() {
1534        let _runmat = crate::compatibility::push_runmat_extensions_enabled(true);
1535        let false_option =
1536            Tensor::new_integer(IntegerStorage::U8(vec![0]), vec![1, 1]).expect("integer tensor");
1537        let compact = block_on(jsonencode_builtin(
1538            Value::Tensor(Tensor::new(vec![1.0, 2.0], vec![1, 2]).expect("tensor")),
1539            vec![Value::from("PrettyPrint"), Value::Tensor(false_option)],
1540        ))
1541        .expect("jsonencode");
1542        assert_eq!(as_string(compact), "[1,2]");
1543
1544        let true_option = Tensor::new_integer(IntegerStorage::U64(vec![u64::MAX]), vec![1, 1])
1545            .expect("integer tensor");
1546        let pretty = block_on(jsonencode_builtin(
1547            Value::Tensor(Tensor::new(vec![1.0, 3.0, 2.0, 4.0], vec![2, 2]).expect("tensor")),
1548            vec![Value::from("PrettyPrint"), Value::Tensor(true_option)],
1549        ))
1550        .expect("jsonencode");
1551        assert_eq!(as_string(pretty), "[\n    [1,2],\n    [3,4]\n]");
1552    }
1553
1554    #[test]
1555    fn jsonencode_option_coercions_are_independently_gated() {
1556        let cases = [
1557            (
1558                Value::Int(IntValue::U64(u64::MAX)),
1559                JSONENCODE_TYPED_INTEGER_OPTION_EXTENSION.error_identifier,
1560            ),
1561            (
1562                Value::Num(1.0),
1563                JSONENCODE_NUMERIC_OPTION_EXTENSION.error_identifier,
1564            ),
1565            (
1566                Value::from("true"),
1567                JSONENCODE_TEXT_OPTION_EXTENSION.error_identifier,
1568            ),
1569        ];
1570        for (option, expected_identifier) in cases {
1571            let _strict = crate::compatibility::push_runmat_extensions_enabled(false);
1572            let error = block_on(jsonencode_builtin(
1573                Value::Num(1.0),
1574                vec![Value::from("PrettyPrint"), option],
1575            ))
1576            .expect_err("option coercion must be gated");
1577            assert_eq!(error.identifier(), expected_identifier);
1578        }
1579    }
1580
1581    #[test]
1582    fn jsonencode_documented_convert_inf_numeric_bool_remains_public() {
1583        let _strict = crate::compatibility::push_runmat_extensions_enabled(false);
1584        let encoded = block_on(jsonencode_builtin(
1585            Value::Num(1.0),
1586            vec![Value::from("ConvertInfAndNaN"), Value::Num(1.0)],
1587        ))
1588        .expect("documented numeric one");
1589        assert_eq!(as_string(encoded), "1");
1590
1591        let error = block_on(jsonencode_builtin(
1592            Value::Num(f64::NAN),
1593            vec![Value::from("ConvertInfAndNaN"), Value::Num(0.0)],
1594        ))
1595        .expect_err("documented numeric zero disables conversion");
1596        assert_eq!(error.message(), JSONENCODE_ERROR_INF_NAN.message);
1597    }
1598
1599    #[test]
1600    fn jsonencode_option_errors_precede_irrelevant_extension_gates() {
1601        let _strict = crate::compatibility::push_runmat_extensions_enabled(false);
1602        let unknown = block_on(jsonencode_builtin(
1603            Value::Num(1.0),
1604            vec![Value::from("Unknown"), Value::Num(1.0)],
1605        ))
1606        .expect_err("unknown name must reject before numeric coercion gate");
1607        assert!(unknown
1608            .message()
1609            .starts_with(JSONENCODE_ERROR_UNKNOWN_OPTION.message));
1610
1611        let nonscalar = Tensor::new(vec![1.0, 0.0], vec![1, 2]).expect("tensor");
1612        let invalid = block_on(jsonencode_builtin(
1613            Value::Num(1.0),
1614            vec![Value::from("PrettyPrint"), Value::Tensor(nonscalar)],
1615        ))
1616        .expect_err("nonscalar value must reject before numeric coercion gate");
1617        assert_eq!(invalid.message(), JSONENCODE_ERROR_OPTION_VALUE.message);
1618    }
1619
1620    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1621    #[test]
1622    fn jsonencode_options_reject_non_scalar_tensor_bool() {
1623        let tensor = Tensor::new(vec![1.0, 0.0], vec![1, 2]).expect("tensor");
1624        let err = block_on(jsonencode_builtin(
1625            Value::Num(1.0),
1626            vec![Value::from("PrettyPrint"), Value::Tensor(tensor)],
1627        ))
1628        .expect_err("expected failure");
1629        assert_eq!(error_message(err), JSONENCODE_ERROR_OPTION_VALUE.message);
1630    }
1631
1632    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1633    #[test]
1634    fn jsonencode_options_accept_scalar_logical_array() {
1635        let logical = LogicalArray::new(vec![1], vec![1]).expect("logical");
1636        let args = vec![Value::from("PrettyPrint"), Value::LogicalArray(logical)];
1637        let encoded = block_on(jsonencode_builtin(Value::Num(7.0), args)).expect("jsonencode");
1638        assert_eq!(as_string(encoded), "7");
1639    }
1640
1641    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1642    #[test]
1643    fn jsonencode_convert_inf_and_nan_controls_null_output() {
1644        let tensor = Tensor::new(vec![1.0, f64::NAN], vec![1, 2]).expect("tensor");
1645        let encoded = block_on(jsonencode_builtin(
1646            Value::Tensor(tensor.clone()),
1647            Vec::new(),
1648        ))
1649        .expect("jsonencode");
1650        assert_eq!(as_string(encoded), "[1,null]");
1651
1652        let err = block_on(jsonencode_builtin(
1653            Value::Tensor(tensor),
1654            vec![Value::from("ConvertInfAndNaN"), Value::Bool(false)],
1655        ))
1656        .expect_err("expected failure");
1657        assert_eq!(error_message(err), JSONENCODE_ERROR_INF_NAN.message);
1658    }
1659
1660    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1661    #[test]
1662    fn jsonencode_cell_array() {
1663        let elements = vec![Value::from(1.0), Value::from("two")];
1664        let cell = CellArray::new(elements, 1, 2).expect("cell");
1665        let encoded =
1666            block_on(jsonencode_builtin(Value::Cell(cell), Vec::new())).expect("jsonencode");
1667        assert_eq!(as_string(encoded), "[1,\"two\"]");
1668    }
1669
1670    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1671    #[test]
1672    fn jsonencode_char_array_zero_rows_is_empty_array() {
1673        let chars = CharArray::new(Vec::new(), 0, 3).expect("char array");
1674        let encoded =
1675            block_on(jsonencode_builtin(Value::CharArray(chars), Vec::new())).expect("jsonencode");
1676        assert_eq!(as_string(encoded), "[]");
1677    }
1678
1679    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1680    #[test]
1681    fn jsonencode_char_array_empty_strings_per_row() {
1682        let chars = CharArray::new(Vec::new(), 2, 0).expect("char array");
1683        let encoded =
1684            block_on(jsonencode_builtin(Value::CharArray(chars), Vec::new())).expect("jsonencode");
1685        let encoded_str = as_string(encoded);
1686        assert_eq!(encoded_str, "[\"\",\"\"]");
1687    }
1688
1689    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1690    #[test]
1691    fn jsonencode_string_array_matrix() {
1692        let sa = StringArray::new(vec!["alpha".to_string(), "beta".to_string()], vec![2, 1])
1693            .expect("string array");
1694        let encoded =
1695            block_on(jsonencode_builtin(Value::StringArray(sa), Vec::new())).expect("jsonencode");
1696        assert_eq!(as_string(encoded), "[\"alpha\",\"beta\"]");
1697    }
1698
1699    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1700    #[test]
1701    fn jsonencode_complex_tensor_outputs_objects() {
1702        let _runmat = crate::compatibility::push_runmat_extensions_enabled(true);
1703        let ct = ComplexTensor::new(vec![(1.0, 2.0), (3.5, -4.0)], vec![2, 1]).expect("complex");
1704        let encoded =
1705            block_on(jsonencode_builtin(Value::ComplexTensor(ct), Vec::new())).expect("jsonencode");
1706        assert_eq!(
1707            as_string(encoded),
1708            "[{\"real\":1,\"imag\":2},{\"real\":3.5,\"imag\":-4}]"
1709        );
1710    }
1711
1712    #[test]
1713    fn jsonencode_typed_complex_integers_preserves_every_class_exactly() {
1714        let _runmat = crate::compatibility::push_runmat_extensions_enabled(true);
1715        let cases = [
1716            (
1717                "int8",
1718                IntegerStorage::I8(vec![-1]),
1719                IntegerStorage::I8(vec![2]),
1720                "{\"real\":-1,\"imag\":2}",
1721            ),
1722            (
1723                "int16",
1724                IntegerStorage::I16(vec![-3]),
1725                IntegerStorage::I16(vec![4]),
1726                "{\"real\":-3,\"imag\":4}",
1727            ),
1728            (
1729                "int32",
1730                IntegerStorage::I32(vec![-5]),
1731                IntegerStorage::I32(vec![6]),
1732                "{\"real\":-5,\"imag\":6}",
1733            ),
1734            (
1735                "int64",
1736                IntegerStorage::I64(vec![-9223372036854775808]),
1737                IntegerStorage::I64(vec![9223372036854775807]),
1738                "{\"real\":-9223372036854775808,\"imag\":9223372036854775807}",
1739            ),
1740            (
1741                "uint8",
1742                IntegerStorage::U8(vec![1]),
1743                IntegerStorage::U8(vec![2]),
1744                "{\"real\":1,\"imag\":2}",
1745            ),
1746            (
1747                "uint16",
1748                IntegerStorage::U16(vec![3]),
1749                IntegerStorage::U16(vec![4]),
1750                "{\"real\":3,\"imag\":4}",
1751            ),
1752            (
1753                "uint32",
1754                IntegerStorage::U32(vec![5]),
1755                IntegerStorage::U32(vec![6]),
1756                "{\"real\":5,\"imag\":6}",
1757            ),
1758            (
1759                "uint64",
1760                IntegerStorage::U64(vec![u64::MAX]),
1761                IntegerStorage::U64(vec![1_u64 << 63]),
1762                "{\"real\":18446744073709551615,\"imag\":9223372036854775808}",
1763            ),
1764        ];
1765
1766        for (class, real, imag, expected) in cases {
1767            let storage = runmat_value::IntegerComplexStorage::new(real, imag)
1768                .expect("matching integer components");
1769            let tensor = ComplexTensor::new_integer(storage, vec![1, 1]).expect("typed complex");
1770            let encoded = block_on(jsonencode_builtin(Value::ComplexTensor(tensor), Vec::new()))
1771                .expect("jsonencode typed complex integer");
1772            assert_eq!(as_string(encoded), expected, "{class}");
1773        }
1774    }
1775
1776    #[test]
1777    fn jsonencode_complex_input_is_rejected_by_compatibility_gate() {
1778        let _strict = crate::compatibility::push_runmat_extensions_enabled(false);
1779        let error = block_on(jsonencode_builtin(Value::Complex(1.0, 2.0), Vec::new()))
1780            .expect_err("complex encoding is a RunMat extension");
1781        assert_eq!(
1782            error.identifier(),
1783            JSONENCODE_COMPLEX_INPUT_EXTENSION.error_identifier
1784        );
1785    }
1786
1787    #[test]
1788    fn jsonencode_nested_complex_and_sparse_inputs_are_compatibility_gated() {
1789        let _strict = crate::compatibility::push_runmat_extensions_enabled(false);
1790        let complex_cell = CellArray::new(vec![Value::Complex(1.0, 2.0)], 1, 1).expect("cell");
1791        let complex_error = block_on(jsonencode_builtin(Value::Cell(complex_cell), Vec::new()))
1792            .expect_err("nested complex input is an extension");
1793        assert_eq!(
1794            complex_error.identifier(),
1795            JSONENCODE_COMPLEX_INPUT_EXTENSION.error_identifier
1796        );
1797
1798        let sparse =
1799            runmat_value::SparseTensor::new(1, 1, vec![0, 1], vec![0], vec![1.0]).expect("sparse");
1800        let mut fields = StructValue::new();
1801        fields
1802            .fields
1803            .insert("value".to_string(), Value::SparseTensor(sparse));
1804        let sparse_error = block_on(jsonencode_builtin(Value::Struct(fields), Vec::new()))
1805            .expect_err("nested sparse input is an extension");
1806        assert_eq!(
1807            sparse_error.identifier(),
1808            JSONENCODE_SPARSE_INPUT_EXTENSION.error_identifier
1809        );
1810    }
1811
1812    #[test]
1813    fn jsonencode_resident_gate_precedes_provider_access() {
1814        let _strict = crate::compatibility::push_runmat_extensions_enabled(false);
1815        let handle = runmat_accelerate_api::GpuTensorHandle {
1816            shape: vec![1, 1],
1817            device_id: u32::MAX,
1818            buffer_id: u64::MAX,
1819            descriptor: Default::default(),
1820        };
1821        let handle = handle.with_provenance(runmat_accelerate_api::GpuHandleProvenance::Explicit);
1822        let error = block_on(jsonencode_builtin(
1823            Value::GpuTensor(handle.clone()),
1824            Vec::new(),
1825        ))
1826        .expect_err("resident extension must gate before owner lookup");
1827        runmat_accelerate_api::clear_handle_metadata(&handle);
1828        assert_eq!(
1829            error.identifier(),
1830            JSONENCODE_RESIDENT_INPUT_EXTENSION.error_identifier
1831        );
1832    }
1833
1834    #[test]
1835    fn jsonencode_integer_metadata_distinguishes_public_real_and_extended_complex() {
1836        assert_eq!(JSONENCODE_INTEGER_CAPABILITIES.len(), 3);
1837        assert_eq!(
1838            JSONENCODE_INTEGER_CAPABILITIES[0].inputs[0].availability,
1839            BuiltinIntegerInputAvailability::Documented
1840        );
1841        assert_eq!(
1842            JSONENCODE_INTEGER_CAPABILITIES[1].inputs[0].availability,
1843            BuiltinIntegerInputAvailability::RunMatOnly
1844        );
1845        assert_eq!(
1846            JSONENCODE_INTEGER_CAPABILITIES[2].inputs[0].availability,
1847            BuiltinIntegerInputAvailability::RunMatOnly
1848        );
1849        assert_eq!(JSONENCODE_EXTENSIONS.len(), 6);
1850    }
1851
1852    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1853    #[test]
1854    fn jsonencode_gpu_tensor_gathers_host_data() {
1855        test_support::with_test_provider(|provider| {
1856            let _strict = crate::compatibility::push_runmat_extensions_enabled(false);
1857            let tensor = Tensor::new(vec![1.0, 0.0, 0.0, 1.0], vec![2, 2]).expect("tensor");
1858            let view = runmat_accelerate_api::HostTensorView {
1859                data: &tensor.materialize_f64(),
1860                shape: &tensor.shape,
1861            };
1862            let handle = provider.upload(&view).expect("upload");
1863            let encoded = block_on(jsonencode_builtin(Value::GpuTensor(handle), Vec::new()))
1864                .expect("jsonencode");
1865            assert_eq!(as_string(encoded), "[[1,0],[0,1]]");
1866        });
1867    }
1868
1869    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1870    #[test]
1871    #[cfg(feature = "wgpu")]
1872    fn jsonencode_gpu_tensor_wgpu_gathers_host_data() {
1873        let ensure = runmat_accelerate::backend::wgpu::provider::ensure_wgpu_provider();
1874        let Some(_) = ensure.ok().flatten() else {
1875            // No WGPU device available on this host; skip.
1876            return;
1877        };
1878        let provider = runmat_accelerate_api::provider().expect("wgpu provider");
1879        let tensor = Tensor::new(vec![1.0, 2.0, 3.0], vec![3, 1]).expect("tensor");
1880        let view = runmat_accelerate_api::HostTensorView {
1881            data: &tensor.materialize_f64(),
1882            shape: &tensor.shape,
1883        };
1884        let handle = provider.upload(&view).expect("upload");
1885        let encoded =
1886            block_on(jsonencode_builtin(Value::GpuTensor(handle), Vec::new())).expect("jsonencode");
1887        assert_eq!(as_string(encoded), "[1,2,3]");
1888    }
1889}