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, BuiltinOutputMode,
8    BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
9    CellArray, CharArray, ComplexTensor, IntValue, IntegerStorage, LogicalArray, ObjectInstance,
10    StringArray, StructValue, SymbolicArray, Tensor, Value,
11};
12use runmat_macros::runtime_builtin;
13
14use crate::builtins::common::spec::{
15    BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
16    ReductionNaN, ResidencyPolicy, ShapeRequirements,
17};
18use crate::{build_runtime_error, gather_if_needed_async, BuiltinResult, RuntimeError};
19
20const BUILTIN_NAME: &str = "jsonencode";
21
22const JSONENCODE_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
23    name: "jsonText",
24    ty: BuiltinParamType::StringScalar,
25    arity: BuiltinParamArity::Required,
26    default: None,
27    description: "JSON text encoded as a character row vector.",
28}];
29const JSONENCODE_INPUTS_VALUE: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
30    name: "value",
31    ty: BuiltinParamType::Any,
32    arity: BuiltinParamArity::Required,
33    default: None,
34    description: "Value to encode as JSON.",
35}];
36const JSONENCODE_INPUTS_VALUE_OPTIONS: [BuiltinParamDescriptor; 2] = [
37    BuiltinParamDescriptor {
38        name: "value",
39        ty: BuiltinParamType::Any,
40        arity: BuiltinParamArity::Required,
41        default: None,
42        description: "Value to encode as JSON.",
43    },
44    BuiltinParamDescriptor {
45        name: "options",
46        ty: BuiltinParamType::Any,
47        arity: BuiltinParamArity::Required,
48        default: None,
49        description: "Options struct with fields such as PrettyPrint and ConvertInfAndNaN.",
50    },
51];
52const JSONENCODE_INPUTS_VALUE_NAME_VALUE: [BuiltinParamDescriptor; 3] = [
53    BuiltinParamDescriptor {
54        name: "value",
55        ty: BuiltinParamType::Any,
56        arity: BuiltinParamArity::Required,
57        default: None,
58        description: "Value to encode as JSON.",
59    },
60    BuiltinParamDescriptor {
61        name: "name",
62        ty: BuiltinParamType::StringScalar,
63        arity: BuiltinParamArity::Required,
64        default: None,
65        description: "Option name (for example \"PrettyPrint\" or \"ConvertInfAndNaN\").",
66    },
67    BuiltinParamDescriptor {
68        name: "optionValue",
69        ty: BuiltinParamType::Any,
70        arity: BuiltinParamArity::Required,
71        default: None,
72        description: "Option value for the preceding option name.",
73    },
74];
75const JSONENCODE_INPUTS_VALUE_NAME_VALUE_VARIADIC: [BuiltinParamDescriptor; 2] = [
76    BuiltinParamDescriptor {
77        name: "value",
78        ty: BuiltinParamType::Any,
79        arity: BuiltinParamArity::Required,
80        default: None,
81        description: "Value to encode as JSON.",
82    },
83    BuiltinParamDescriptor {
84        name: "nameValuePairs...",
85        ty: BuiltinParamType::Any,
86        arity: BuiltinParamArity::Variadic,
87        default: None,
88        description: "Name-value option pairs.",
89    },
90];
91const JSONENCODE_SIGNATURES: [BuiltinSignatureDescriptor; 4] = [
92    BuiltinSignatureDescriptor {
93        label: "jsonText = jsonencode(value)",
94        inputs: &JSONENCODE_INPUTS_VALUE,
95        outputs: &JSONENCODE_OUTPUT,
96    },
97    BuiltinSignatureDescriptor {
98        label: "jsonText = jsonencode(value, options)",
99        inputs: &JSONENCODE_INPUTS_VALUE_OPTIONS,
100        outputs: &JSONENCODE_OUTPUT,
101    },
102    BuiltinSignatureDescriptor {
103        label: "jsonText = jsonencode(value, name, optionValue)",
104        inputs: &JSONENCODE_INPUTS_VALUE_NAME_VALUE,
105        outputs: &JSONENCODE_OUTPUT,
106    },
107    BuiltinSignatureDescriptor {
108        label: "jsonText = jsonencode(value, nameValuePairs...)",
109        inputs: &JSONENCODE_INPUTS_VALUE_NAME_VALUE_VARIADIC,
110        outputs: &JSONENCODE_OUTPUT,
111    },
112];
113const JSONENCODE_ERROR_OPTIONS_CONFIG: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
114    code: "RM.JSONENCODE.OPTIONS_CONFIG",
115    identifier: None,
116    when: "Single options argument is provided but is not a struct.",
117    message: "jsonencode: expected name/value pairs or options struct",
118};
119const JSONENCODE_ERROR_NAME_VALUE_PAIRS: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
120    code: "RM.JSONENCODE.NAME_VALUE_PAIRS",
121    identifier: None,
122    when: "Name-value options do not come in pairs.",
123    message: "jsonencode: name/value pairs must come in pairs",
124};
125const JSONENCODE_ERROR_OPTION_NAME: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
126    code: "RM.JSONENCODE.OPTION_NAME",
127    identifier: None,
128    when: "Option name is not a character vector or string scalar.",
129    message: "jsonencode: option names must be character vectors or strings",
130};
131const JSONENCODE_ERROR_OPTION_VALUE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
132    code: "RM.JSONENCODE.OPTION_VALUE",
133    identifier: None,
134    when: "Option value is not a scalar logical/numeric or boolean-like text.",
135    message: "jsonencode: option value must be scalar logical or numeric",
136};
137const JSONENCODE_ERROR_UNKNOWN_OPTION: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
138    code: "RM.JSONENCODE.UNKNOWN_OPTION",
139    identifier: None,
140    when: "Option name is not recognized.",
141    message: "jsonencode: unknown option name",
142};
143const JSONENCODE_ERROR_INF_NAN: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
144    code: "RM.JSONENCODE.INF_NAN",
145    identifier: None,
146    when: "Input contains NaN/Inf while ConvertInfAndNaN is false.",
147    message: "jsonencode: ConvertInfAndNaN must be true to encode NaN or Inf values",
148};
149const JSONENCODE_ERROR_UNSUPPORTED_TYPE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
150    code: "RM.JSONENCODE.UNSUPPORTED_TYPE",
151    identifier: None,
152    when: "Input value type is not supported for JSON encoding.",
153    message:
154        "jsonencode: unsupported input type; expected numeric, logical, string, struct, cell, or object data",
155};
156const JSONENCODE_ERROR_UNEXPECTED_GPU: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
157    code: "RM.JSONENCODE.UNEXPECTED_GPU",
158    identifier: None,
159    when: "A GPU tensor handle reaches encoding after gather pass.",
160    message: "jsonencode: unexpected gpuArray handle after gather pass",
161};
162const JSONENCODE_ERROR_INTERNAL: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
163    code: "RM.JSONENCODE.INTERNAL",
164    identifier: None,
165    when: "Internal JSON conversion or container materialization fails.",
166    message: "jsonencode: internal conversion failed",
167};
168const JSONENCODE_ERRORS: [BuiltinErrorDescriptor; 9] = [
169    JSONENCODE_ERROR_OPTIONS_CONFIG,
170    JSONENCODE_ERROR_NAME_VALUE_PAIRS,
171    JSONENCODE_ERROR_OPTION_NAME,
172    JSONENCODE_ERROR_OPTION_VALUE,
173    JSONENCODE_ERROR_UNKNOWN_OPTION,
174    JSONENCODE_ERROR_INF_NAN,
175    JSONENCODE_ERROR_UNSUPPORTED_TYPE,
176    JSONENCODE_ERROR_UNEXPECTED_GPU,
177    JSONENCODE_ERROR_INTERNAL,
178];
179pub const JSONENCODE_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
180    signatures: &JSONENCODE_SIGNATURES,
181    output_mode: BuiltinOutputMode::Fixed,
182    completion_policy: BuiltinCompletionPolicy::Public,
183    errors: &JSONENCODE_ERRORS,
184};
185
186#[allow(clippy::too_many_lines)]
187#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::io::json::jsonencode")]
188pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
189    name: "jsonencode",
190    op_kind: GpuOpKind::Custom("serialization"),
191    supported_precisions: &[],
192    broadcast: BroadcastSemantics::None,
193    provider_hooks: &[],
194    constant_strategy: ConstantStrategy::InlineLiteral,
195    residency: ResidencyPolicy::GatherImmediately,
196    nan_mode: ReductionNaN::Include,
197    two_pass_threshold: None,
198    workgroup_size: None,
199    accepts_nan_mode: false,
200    notes:
201        "Serialization sink that gathers GPU data to host memory before emitting UTF-8 JSON text.",
202};
203
204fn jsonencode_error(error: &'static BuiltinErrorDescriptor) -> RuntimeError {
205    jsonencode_error_with(error, error.message)
206}
207
208fn jsonencode_error_with(
209    error: &'static BuiltinErrorDescriptor,
210    message: impl Into<String>,
211) -> RuntimeError {
212    let mut builder = build_runtime_error(message).with_builtin(BUILTIN_NAME);
213    if let Some(identifier) = error.identifier {
214        builder = builder.with_identifier(identifier);
215    }
216    builder.build()
217}
218
219fn jsonencode_flow_with_context(err: RuntimeError) -> RuntimeError {
220    let mut builder = build_runtime_error(err.message().to_string()).with_builtin(BUILTIN_NAME);
221    if let Some(identifier) = err.identifier() {
222        builder = builder.with_identifier(identifier.to_string());
223    }
224    builder.with_source(err).build()
225}
226
227#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::io::json::jsonencode")]
228pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
229    name: "jsonencode",
230    shape: ShapeRequirements::Any,
231    constant_strategy: ConstantStrategy::InlineLiteral,
232    elementwise: None,
233    reduction: None,
234    emits_nan: false,
235    notes: "jsonencode is a residency sink and never participates in fusion planning.",
236};
237
238#[derive(Debug, Clone)]
239struct JsonEncodeOptions {
240    pretty_print: bool,
241    convert_inf_and_nan: bool,
242}
243
244impl Default for JsonEncodeOptions {
245    fn default() -> Self {
246        Self {
247            pretty_print: false,
248            convert_inf_and_nan: true,
249        }
250    }
251}
252
253#[derive(Debug, Clone)]
254enum JsonValue {
255    Null,
256    Bool(bool),
257    Number(JsonNumber),
258    String(String),
259    Array(Vec<JsonValue>),
260    Object(Vec<(String, JsonValue)>),
261}
262
263#[derive(Debug, Clone)]
264enum JsonNumber {
265    Float(f64),
266    I64(i64),
267    U64(u64),
268}
269
270#[runtime_builtin(
271    name = "jsonencode",
272    category = "io/json",
273    summary = "Serialize MATLAB values to UTF-8 JSON text.",
274    keywords = "jsonencode,json,serialization,struct,gpu",
275    accel = "cpu",
276    type_resolver(crate::builtins::io::type_resolvers::jsonencode_type),
277    descriptor(crate::builtins::io::json::jsonencode::JSONENCODE_DESCRIPTOR),
278    builtin_path = "crate::builtins::io::json::jsonencode"
279)]
280async fn jsonencode_builtin(value: Value, rest: Vec<Value>) -> crate::BuiltinResult<Value> {
281    let host_value = gather_if_needed_async(&value)
282        .await
283        .map_err(jsonencode_flow_with_context)?;
284    let mut gathered_args = Vec::with_capacity(rest.len());
285    for value in &rest {
286        gathered_args.push(
287            gather_if_needed_async(value)
288                .await
289                .map_err(jsonencode_flow_with_context)?,
290        );
291    }
292
293    let options = parse_options(&gathered_args)?;
294    let json_value = value_to_json(&host_value, &options)?;
295    let json_string = render_json(&json_value, &options);
296
297    Ok(Value::CharArray(CharArray::new_row(&json_string)))
298}
299
300fn parse_options(args: &[Value]) -> BuiltinResult<JsonEncodeOptions> {
301    let mut options = JsonEncodeOptions::default();
302    if args.is_empty() {
303        return Ok(options);
304    }
305
306    if args.len() == 1 {
307        if let Value::Struct(struct_value) = &args[0] {
308            apply_struct_options(struct_value, &mut options)?;
309            return Ok(options);
310        }
311        return Err(jsonencode_error(&JSONENCODE_ERROR_OPTIONS_CONFIG));
312    }
313
314    if !args.len().is_multiple_of(2) {
315        return Err(jsonencode_error(&JSONENCODE_ERROR_NAME_VALUE_PAIRS));
316    }
317
318    let mut idx = 0usize;
319    while idx < args.len() {
320        let name = option_name(&args[idx])?;
321        let value = &args[idx + 1];
322        apply_option(&name, value, &mut options)?;
323        idx += 2;
324    }
325
326    Ok(options)
327}
328
329fn apply_struct_options(
330    struct_value: &StructValue,
331    options: &mut JsonEncodeOptions,
332) -> BuiltinResult<()> {
333    for (key, value) in &struct_value.fields {
334        apply_option(key, value, options)?;
335    }
336    Ok(())
337}
338
339fn option_name(value: &Value) -> BuiltinResult<String> {
340    match value {
341        Value::String(s) => Ok(s.clone()),
342        Value::CharArray(ca) if ca.rows == 1 => Ok(ca.data.iter().collect()),
343        Value::StringArray(sa) if sa.data.len() == 1 => Ok(sa.data[0].clone()),
344        _ => Err(jsonencode_error(&JSONENCODE_ERROR_OPTION_NAME)),
345    }
346}
347
348fn apply_option(
349    raw_name: &str,
350    value: &Value,
351    options: &mut JsonEncodeOptions,
352) -> BuiltinResult<()> {
353    let lowered = raw_name.to_ascii_lowercase();
354    match lowered.as_str() {
355        "prettyprint" => {
356            options.pretty_print = coerce_bool(value)?;
357            Ok(())
358        }
359        "convertinfandnan" => {
360            options.convert_inf_and_nan = coerce_bool(value)?;
361            Ok(())
362        }
363        other => Err(jsonencode_error_with(
364            &JSONENCODE_ERROR_UNKNOWN_OPTION,
365            format!("{} ('{}')", JSONENCODE_ERROR_UNKNOWN_OPTION.message, other),
366        )),
367    }
368}
369
370fn coerce_bool(value: &Value) -> BuiltinResult<bool> {
371    match value {
372        Value::Bool(b) => Ok(*b),
373        Value::Int(i) => Ok(i.to_i64() != 0),
374        Value::Num(n) => bool_from_f64(*n),
375        Value::Tensor(t) => {
376            if t.data.len() == 1 {
377                bool_from_f64(t.data[0])
378            } else {
379                Err(jsonencode_error(&JSONENCODE_ERROR_OPTION_VALUE))
380            }
381        }
382        Value::LogicalArray(la) => match la.data.len() {
383            1 => Ok(la.data[0] != 0),
384            _ => Err(jsonencode_error(&JSONENCODE_ERROR_OPTION_VALUE)),
385        },
386        Value::CharArray(ca) if ca.rows == 1 => {
387            parse_bool_string(&ca.data.iter().collect::<String>())
388        }
389        Value::String(s) => parse_bool_string(s),
390        Value::StringArray(sa) if sa.data.len() == 1 => parse_bool_string(&sa.data[0]),
391        _ => Err(jsonencode_error(&JSONENCODE_ERROR_OPTION_VALUE)),
392    }
393}
394
395fn bool_from_f64(value: f64) -> BuiltinResult<bool> {
396    if value.is_finite() {
397        Ok(value != 0.0)
398    } else {
399        Err(jsonencode_error(&JSONENCODE_ERROR_OPTION_VALUE))
400    }
401}
402
403fn parse_bool_string(text: &str) -> BuiltinResult<bool> {
404    match text.trim().to_ascii_lowercase().as_str() {
405        "true" | "on" | "yes" | "1" => Ok(true),
406        "false" | "off" | "no" | "0" => Ok(false),
407        _ => Err(jsonencode_error(&JSONENCODE_ERROR_OPTION_VALUE)),
408    }
409}
410
411fn value_to_json(value: &Value, options: &JsonEncodeOptions) -> BuiltinResult<JsonValue> {
412    match value {
413        Value::Num(n) => number_to_json(*n, options),
414        Value::Int(i) => Ok(JsonValue::Number(int_to_number(i))),
415        Value::Bool(b) => Ok(JsonValue::Bool(*b)),
416        Value::LogicalArray(logical) => logical_array_to_json(logical, options),
417        Value::Tensor(tensor) => tensor_to_json(tensor, options),
418        Value::SparseTensor(sparse) => {
419            let total_elements = sparse.rows.checked_mul(sparse.cols).ok_or_else(|| {
420                jsonencode_error_with(
421                    &JSONENCODE_ERROR_INTERNAL,
422                    "jsonencode: sparse matrix dimensions overflow",
423                )
424            })?;
425            if total_elements > 10_000_000 {
426                return Err(jsonencode_error_with(
427                    &JSONENCODE_ERROR_INTERNAL,
428                    format!("jsonencode: cannot densify sparse tensor {}x{} ({} elements exceeds safe threshold)", sparse.rows, sparse.cols, total_elements),
429                ));
430            }
431            let dense = sparse.to_dense().map_err(|err| {
432                jsonencode_error_with(&JSONENCODE_ERROR_INTERNAL, format!("jsonencode: {err}"))
433            })?;
434            tensor_to_json(&dense, options)
435        }
436        Value::Complex(re, im) => complex_scalar_to_json(*re, *im, options),
437        Value::ComplexTensor(ct) => complex_tensor_to_json(ct, options),
438        Value::String(s) => Ok(JsonValue::String(s.clone())),
439        Value::Symbolic(expr) => Ok(JsonValue::String(expr.to_string())),
440        Value::SymbolicArray(array) => symbolic_array_to_json(array, options),
441        Value::StringArray(sa) => string_array_to_json(sa, options),
442        Value::CharArray(ca) => char_array_to_json(ca, options),
443        Value::Struct(sv) => struct_to_json(sv, options),
444        Value::Cell(ca) => cell_array_to_json(ca, options),
445        Value::Object(obj) => object_to_json(obj, options),
446        Value::GpuTensor(_) => Err(jsonencode_error(&JSONENCODE_ERROR_UNEXPECTED_GPU)),
447        Value::HandleObject(_)
448        | Value::Listener(_)
449        | Value::FunctionHandle(_)
450        | Value::ExternalFunctionHandle(_)
451        | Value::MethodFunctionHandle(_)
452        | Value::BoundFunctionHandle { .. }
453        | Value::Closure(_)
454        | Value::ClassRef(_)
455        | Value::MException(_)
456        | Value::OutputList(_) => Err(jsonencode_error(&JSONENCODE_ERROR_UNSUPPORTED_TYPE)),
457    }
458}
459
460fn int_to_number(value: &IntValue) -> JsonNumber {
461    match value {
462        IntValue::I8(v) => JsonNumber::I64(*v as i64),
463        IntValue::I16(v) => JsonNumber::I64(*v as i64),
464        IntValue::I32(v) => JsonNumber::I64(*v as i64),
465        IntValue::I64(v) => JsonNumber::I64(*v),
466        IntValue::U8(v) => JsonNumber::U64(*v as u64),
467        IntValue::U16(v) => JsonNumber::U64(*v as u64),
468        IntValue::U32(v) => JsonNumber::U64(*v as u64),
469        IntValue::U64(v) => JsonNumber::U64(*v),
470    }
471}
472
473fn number_to_json(value: f64, options: &JsonEncodeOptions) -> BuiltinResult<JsonValue> {
474    if !value.is_finite() {
475        if options.convert_inf_and_nan {
476            return Ok(JsonValue::Null);
477        }
478        return Err(jsonencode_error(&JSONENCODE_ERROR_INF_NAN));
479    }
480    Ok(JsonValue::Number(JsonNumber::Float(value)))
481}
482
483fn logical_array_to_json(
484    logical: &LogicalArray,
485    _options: &JsonEncodeOptions,
486) -> BuiltinResult<JsonValue> {
487    let keep_dims = compute_keep_dims(&logical.shape, true);
488    if logical.shape.is_empty() || logical.data.is_empty() {
489        return Ok(JsonValue::Array(Vec::new()));
490    }
491    if keep_dims.is_empty() {
492        let first = logical.data.first().copied().unwrap_or(0) != 0;
493        return Ok(JsonValue::Bool(first));
494    }
495    build_strided_array(&logical.shape, &keep_dims, |offset| {
496        Ok(JsonValue::Bool(logical.data[offset] != 0))
497    })
498}
499
500fn tensor_to_json(tensor: &Tensor, options: &JsonEncodeOptions) -> BuiltinResult<JsonValue> {
501    if tensor.data.is_empty() {
502        return Ok(JsonValue::Array(Vec::new()));
503    }
504    let keep_dims = compute_keep_dims(&tensor.shape, true);
505    if keep_dims.is_empty() {
506        return tensor_value_to_json(tensor, 0, options);
507    }
508    build_strided_array(&tensor.shape, &keep_dims, |offset| {
509        tensor_value_to_json(tensor, offset, options)
510    })
511}
512
513fn tensor_value_to_json(
514    tensor: &Tensor,
515    offset: usize,
516    options: &JsonEncodeOptions,
517) -> BuiltinResult<JsonValue> {
518    match tensor.integer_storage() {
519        Some(storage) => Ok(JsonValue::Number(integer_storage_number(storage, offset))),
520        None => number_to_json(tensor.data[offset], options),
521    }
522}
523
524fn integer_storage_number(storage: &IntegerStorage, offset: usize) -> JsonNumber {
525    match storage {
526        IntegerStorage::I8(values) => JsonNumber::I64(values[offset] as i64),
527        IntegerStorage::I16(values) => JsonNumber::I64(values[offset] as i64),
528        IntegerStorage::I32(values) => JsonNumber::I64(values[offset] as i64),
529        IntegerStorage::I64(values) => JsonNumber::I64(values[offset]),
530        IntegerStorage::U8(values) => JsonNumber::U64(values[offset] as u64),
531        IntegerStorage::U16(values) => JsonNumber::U64(values[offset] as u64),
532        IntegerStorage::U32(values) => JsonNumber::U64(values[offset] as u64),
533        IntegerStorage::U64(values) => JsonNumber::U64(values[offset]),
534    }
535}
536
537fn complex_scalar_to_json(
538    real: f64,
539    imag: f64,
540    options: &JsonEncodeOptions,
541) -> BuiltinResult<JsonValue> {
542    let real_json = number_to_json(real, options)?;
543    let imag_json = number_to_json(imag, options)?;
544    Ok(JsonValue::Object(vec![
545        ("real".to_string(), real_json),
546        ("imag".to_string(), imag_json),
547    ]))
548}
549
550fn complex_tensor_to_json(
551    ct: &ComplexTensor,
552    options: &JsonEncodeOptions,
553) -> BuiltinResult<JsonValue> {
554    if ct.data.is_empty() {
555        return Ok(JsonValue::Array(Vec::new()));
556    }
557    let keep_dims = compute_keep_dims(&ct.shape, true);
558    if keep_dims.is_empty() {
559        let (re, im) = ct.data[0];
560        return complex_scalar_to_json(re, im, options);
561    }
562    build_strided_array(&ct.shape, &keep_dims, |offset| {
563        let (re, im) = ct.data[offset];
564        complex_scalar_to_json(re, im, options)
565    })
566}
567
568fn string_array_to_json(
569    sa: &StringArray,
570    _options: &JsonEncodeOptions,
571) -> BuiltinResult<JsonValue> {
572    if sa.data.is_empty() {
573        return Ok(JsonValue::Array(Vec::new()));
574    }
575    let keep_dims = compute_keep_dims(&sa.shape, true);
576    if keep_dims.is_empty() {
577        return Ok(JsonValue::String(sa.data[0].clone()));
578    }
579    build_strided_array(&sa.shape, &keep_dims, |offset| {
580        Ok(JsonValue::String(sa.data[offset].clone()))
581    })
582}
583
584fn symbolic_array_to_json(
585    array: &SymbolicArray,
586    _options: &JsonEncodeOptions,
587) -> BuiltinResult<JsonValue> {
588    if array.data.is_empty() {
589        return Ok(JsonValue::Array(Vec::new()));
590    }
591    let keep_dims = compute_keep_dims(&array.shape, true);
592    if keep_dims.is_empty() {
593        return Ok(JsonValue::String(array.data[0].to_string()));
594    }
595    build_strided_array(&array.shape, &keep_dims, |offset| {
596        Ok(JsonValue::String(array.data[offset].to_string()))
597    })
598}
599
600fn char_array_to_json(ca: &CharArray, _options: &JsonEncodeOptions) -> BuiltinResult<JsonValue> {
601    if ca.rows == 0 {
602        return Ok(JsonValue::Array(Vec::new()));
603    }
604
605    if ca.cols == 0 {
606        if ca.rows == 1 {
607            return Ok(JsonValue::String(String::new()));
608        }
609        let mut rows = Vec::with_capacity(ca.rows);
610        for _ in 0..ca.rows {
611            rows.push(JsonValue::String(String::new()));
612        }
613        return Ok(JsonValue::Array(rows));
614    }
615
616    if ca.rows == 1 {
617        return Ok(JsonValue::String(ca.data.iter().collect()));
618    }
619
620    let mut rows = Vec::with_capacity(ca.rows);
621    for r in 0..ca.rows {
622        let mut row_string = String::with_capacity(ca.cols);
623        for c in 0..ca.cols {
624            row_string.push(ca.data[r * ca.cols + c]);
625        }
626        rows.push(JsonValue::String(row_string));
627    }
628    Ok(JsonValue::Array(rows))
629}
630
631fn struct_to_json(sv: &StructValue, options: &JsonEncodeOptions) -> BuiltinResult<JsonValue> {
632    if sv.fields.is_empty() {
633        return Ok(JsonValue::Object(Vec::new()));
634    }
635    let mut map = BTreeMap::new();
636    for (key, value) in &sv.fields {
637        map.insert(key.clone(), value_to_json(value, options)?);
638    }
639    Ok(JsonValue::Object(map.into_iter().collect()))
640}
641
642fn object_to_json(obj: &ObjectInstance, options: &JsonEncodeOptions) -> BuiltinResult<JsonValue> {
643    let mut map = BTreeMap::new();
644    for (key, value) in &obj.properties {
645        map.insert(key.clone(), value_to_json(value, options)?);
646    }
647    Ok(JsonValue::Object(map.into_iter().collect()))
648}
649
650fn cell_array_to_json(ca: &CellArray, options: &JsonEncodeOptions) -> BuiltinResult<JsonValue> {
651    if ca.rows == 0 || ca.cols == 0 {
652        return Ok(JsonValue::Array(Vec::new()));
653    }
654
655    if ca.rows == 1 && ca.cols == 1 {
656        let value = ca.get(0, 0).map_err(|e| {
657            jsonencode_error_with(
658                &JSONENCODE_ERROR_INTERNAL,
659                format!("{} ({e})", JSONENCODE_ERROR_INTERNAL.message),
660            )
661        })?;
662        return Ok(JsonValue::Array(vec![value_to_json(&value, options)?]));
663    }
664
665    if ca.rows == 1 {
666        let mut row = Vec::with_capacity(ca.cols);
667        for c in 0..ca.cols {
668            let element = ca.get(0, c).map_err(|e| {
669                jsonencode_error_with(
670                    &JSONENCODE_ERROR_INTERNAL,
671                    format!("{} ({e})", JSONENCODE_ERROR_INTERNAL.message),
672                )
673            })?;
674            row.push(value_to_json(&element, options)?);
675        }
676        return Ok(JsonValue::Array(row));
677    }
678
679    if ca.cols == 1 {
680        let mut column = Vec::with_capacity(ca.rows);
681        for r in 0..ca.rows {
682            let element = ca.get(r, 0).map_err(|e| {
683                jsonencode_error_with(
684                    &JSONENCODE_ERROR_INTERNAL,
685                    format!("{} ({e})", JSONENCODE_ERROR_INTERNAL.message),
686                )
687            })?;
688            column.push(value_to_json(&element, options)?);
689        }
690        return Ok(JsonValue::Array(column));
691    }
692
693    let mut rows = Vec::with_capacity(ca.rows);
694    for r in 0..ca.rows {
695        let mut row = Vec::with_capacity(ca.cols);
696        for c in 0..ca.cols {
697            let element = ca.get(r, c).map_err(|e| {
698                jsonencode_error_with(
699                    &JSONENCODE_ERROR_INTERNAL,
700                    format!("{} ({e})", JSONENCODE_ERROR_INTERNAL.message),
701                )
702            })?;
703            row.push(value_to_json(&element, options)?);
704        }
705        rows.push(JsonValue::Array(row));
706    }
707    Ok(JsonValue::Array(rows))
708}
709
710fn compute_keep_dims(shape: &[usize], drop_singletons: bool) -> Vec<usize> {
711    let mut keep = Vec::new();
712    for (idx, &size) in shape.iter().enumerate() {
713        if size != 1 || !drop_singletons {
714            keep.push(idx);
715        }
716    }
717    keep
718}
719
720fn compute_strides(shape: &[usize]) -> Vec<usize> {
721    let mut strides = Vec::with_capacity(shape.len());
722    let mut acc = 1usize;
723    for &size in shape {
724        strides.push(acc);
725        acc = acc.saturating_mul(size.max(1));
726    }
727    strides
728}
729
730fn build_strided_array<F>(
731    shape: &[usize],
732    keep_dims: &[usize],
733    mut fetch: F,
734) -> BuiltinResult<JsonValue>
735where
736    F: FnMut(usize) -> BuiltinResult<JsonValue>,
737{
738    if keep_dims.is_empty() {
739        return fetch(0);
740    }
741    if keep_dims.iter().any(|&idx| shape[idx] == 0) {
742        return Ok(JsonValue::Array(Vec::new()));
743    }
744    let strides = compute_strides(shape);
745    let dims: Vec<usize> = keep_dims.iter().map(|&idx| shape[idx]).collect();
746    build_nd_array(&dims, |indices| {
747        let mut offset = 0usize;
748        for (value, dim_idx) in indices.iter().zip(keep_dims.iter()) {
749            offset += value * strides[*dim_idx];
750        }
751        fetch(offset)
752    })
753}
754
755fn build_nd_array<F>(dims: &[usize], mut fetch: F) -> BuiltinResult<JsonValue>
756where
757    F: FnMut(&[usize]) -> BuiltinResult<JsonValue>,
758{
759    if dims.is_empty() {
760        return fetch(&[]);
761    }
762    if dims[0] == 0 {
763        return Ok(JsonValue::Array(Vec::new()));
764    }
765    let mut indices = vec![0usize; dims.len()];
766    build_nd_array_recursive(dims, 0, &mut indices, &mut fetch)
767}
768
769fn build_nd_array_recursive<F>(
770    dims: &[usize],
771    level: usize,
772    indices: &mut [usize],
773    fetch: &mut F,
774) -> BuiltinResult<JsonValue>
775where
776    F: FnMut(&[usize]) -> BuiltinResult<JsonValue>,
777{
778    let size = dims[level];
779    if size == 0 {
780        return Ok(JsonValue::Array(Vec::new()));
781    }
782    if level + 1 == dims.len() {
783        let mut items = Vec::with_capacity(size);
784        for i in 0..size {
785            indices[level] = i;
786            items.push(fetch(indices)?);
787        }
788        return Ok(JsonValue::Array(items));
789    }
790    let mut items = Vec::with_capacity(size);
791    for i in 0..size {
792        indices[level] = i;
793        items.push(build_nd_array_recursive(dims, level + 1, indices, fetch)?);
794    }
795    Ok(JsonValue::Array(items))
796}
797
798fn render_json(value: &JsonValue, options: &JsonEncodeOptions) -> String {
799    let mut writer = JsonWriter::new(options.pretty_print);
800    writer.write_value(value);
801    writer.finish()
802}
803
804struct JsonWriter {
805    output: String,
806    pretty: bool,
807    indent: usize,
808}
809
810impl JsonWriter {
811    fn new(pretty: bool) -> Self {
812        Self {
813            output: String::new(),
814            pretty,
815            indent: 0,
816        }
817    }
818
819    fn finish(self) -> String {
820        self.output
821    }
822
823    fn write_value(&mut self, value: &JsonValue) {
824        match value {
825            JsonValue::Null => self.output.push_str("null"),
826            JsonValue::Bool(true) => self.output.push_str("true"),
827            JsonValue::Bool(false) => self.output.push_str("false"),
828            JsonValue::Number(number) => self.write_number(number),
829            JsonValue::String(text) => {
830                self.output.push('"');
831                self.output.push_str(&escape_json_string(text));
832                self.output.push('"');
833            }
834            JsonValue::Array(items) => self.write_array(items),
835            JsonValue::Object(fields) => self.write_object(fields),
836        }
837    }
838
839    fn write_number(&mut self, number: &JsonNumber) {
840        match number {
841            JsonNumber::Float(f) => {
842                if f.is_nan() || !f.is_finite() {
843                    self.output.push_str("null");
844                } else {
845                    self.output.push_str(&format_number(*f));
846                }
847            }
848            JsonNumber::I64(i) => {
849                let _ = write!(self.output, "{i}");
850            }
851            JsonNumber::U64(u) => {
852                let _ = write!(self.output, "{u}");
853            }
854        }
855    }
856
857    fn write_array(&mut self, items: &[JsonValue]) {
858        if items.is_empty() {
859            self.output.push_str("[]");
860            return;
861        }
862        let inline = if self.pretty {
863            items.iter().all(|item| {
864                matches!(
865                    item,
866                    JsonValue::Null
867                        | JsonValue::Bool(_)
868                        | JsonValue::Number(_)
869                        | JsonValue::String(_)
870                )
871            })
872        } else {
873            false
874        };
875        if inline {
876            self.output.push('[');
877            for (index, item) in items.iter().enumerate() {
878                self.write_value(item);
879                if index + 1 < items.len() {
880                    self.output.push(',');
881                }
882            }
883            self.output.push(']');
884            return;
885        }
886        self.output.push('[');
887        if self.pretty {
888            self.output.push('\n');
889            self.indent += 1;
890        }
891        for (index, item) in items.iter().enumerate() {
892            if self.pretty {
893                self.write_indent();
894            }
895            self.write_value(item);
896            if index + 1 < items.len() {
897                if self.pretty {
898                    self.output.push_str(",\n");
899                } else {
900                    self.output.push(',');
901                }
902            }
903        }
904        if self.pretty {
905            self.output.push('\n');
906            if self.indent > 0 {
907                self.indent -= 1;
908            }
909            self.write_indent();
910        }
911        self.output.push(']');
912    }
913
914    fn write_object(&mut self, fields: &[(String, JsonValue)]) {
915        if fields.is_empty() {
916            self.output.push_str("{}");
917            return;
918        }
919        self.output.push('{');
920        if self.pretty {
921            self.output.push('\n');
922            self.indent += 1;
923        }
924        for (index, (key, value)) in fields.iter().enumerate() {
925            if self.pretty {
926                self.write_indent();
927            }
928            self.output.push('"');
929            self.output.push_str(&escape_json_string(key));
930            self.output.push('"');
931            if self.pretty {
932                self.output.push_str(": ");
933            } else {
934                self.output.push(':');
935            }
936            self.write_value(value);
937            if index + 1 < fields.len() {
938                if self.pretty {
939                    self.output.push_str(",\n");
940                } else {
941                    self.output.push(',');
942                }
943            }
944        }
945        if self.pretty {
946            self.output.push('\n');
947            if self.indent > 0 {
948                self.indent -= 1;
949            }
950            self.write_indent();
951        }
952        self.output.push('}');
953    }
954
955    fn write_indent(&mut self) {
956        if self.pretty {
957            for _ in 0..self.indent {
958                self.output.push_str("    ");
959            }
960        }
961    }
962}
963
964fn escape_json_string(value: &str) -> String {
965    let mut escaped = String::with_capacity(value.len());
966    for ch in value.chars() {
967        match ch {
968            '"' => escaped.push_str("\\\""),
969            '\\' => escaped.push_str("\\\\"),
970            '\u{08}' => escaped.push_str("\\b"),
971            '\u{0C}' => escaped.push_str("\\f"),
972            '\n' => escaped.push_str("\\n"),
973            '\r' => escaped.push_str("\\r"),
974            '\t' => escaped.push_str("\\t"),
975            c if (c as u32) < 0x20 => {
976                let _ = write!(escaped, "\\u{:04X}", c as u32);
977            }
978            _ => escaped.push(ch),
979        }
980    }
981    escaped
982}
983
984fn format_number(value: f64) -> String {
985    if value.fract() == 0.0 {
986        // Display integer-like doubles without decimal point
987        format!("{:.0}", value)
988    } else {
989        format!("{}", value)
990    }
991}
992
993#[cfg(test)]
994pub(crate) mod tests {
995    use super::*;
996    use crate::builtins::common::test_support;
997    use futures::executor::block_on;
998    use runmat_builtins::{
999        CellArray, CharArray, ComplexTensor, LogicalArray, StringArray, StructValue, SymbolicArray,
1000        SymbolicExpr, Tensor,
1001    };
1002
1003    fn as_string(value: Value) -> String {
1004        match value {
1005            Value::CharArray(ca) => ca.data.iter().collect(),
1006            Value::String(s) => s,
1007            other => panic!("expected char array, got {:?}", other),
1008        }
1009    }
1010
1011    fn error_message(err: crate::RuntimeError) -> String {
1012        err.message().to_string()
1013    }
1014
1015    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1016    #[test]
1017    fn jsonencode_descriptor_signatures_cover_core_forms() {
1018        let labels: Vec<&str> = JSONENCODE_DESCRIPTOR
1019            .signatures
1020            .iter()
1021            .map(|sig| sig.label)
1022            .collect();
1023        assert!(labels.contains(&"jsonText = jsonencode(value)"));
1024        assert!(labels.contains(&"jsonText = jsonencode(value, options)"));
1025        assert!(labels.contains(&"jsonText = jsonencode(value, name, optionValue)"));
1026        assert!(labels.contains(&"jsonText = jsonencode(value, nameValuePairs...)"));
1027    }
1028
1029    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1030    #[test]
1031    fn jsonencode_scalar_double() {
1032        let encoded =
1033            block_on(jsonencode_builtin(Value::Num(5.0), Vec::new())).expect("jsonencode");
1034        assert_eq!(as_string(encoded), "5");
1035    }
1036
1037    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1038    #[test]
1039    fn jsonencode_symbolic_value_as_text() {
1040        let expr = SymbolicExpr::div_expr(
1041            SymbolicExpr::function(
1042                runmat_builtins::symbolic::SymbolicFunction::Sin,
1043                SymbolicExpr::variable("x"),
1044            ),
1045            SymbolicExpr::variable("x"),
1046        );
1047        let encoded =
1048            block_on(jsonencode_builtin(Value::Symbolic(expr), Vec::new())).expect("jsonencode");
1049
1050        assert_eq!(as_string(encoded), "\"sin(x)/x\"");
1051    }
1052
1053    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1054    #[test]
1055    fn jsonencode_symbolic_array_preserves_matrix_shape() {
1056        let array = SymbolicArray::new(
1057            vec![
1058                SymbolicExpr::variable("a"),
1059                SymbolicExpr::variable("c"),
1060                SymbolicExpr::variable("b"),
1061                SymbolicExpr::variable("d"),
1062            ],
1063            vec![2, 2],
1064        )
1065        .expect("symbolic array");
1066
1067        let encoded = block_on(jsonencode_builtin(Value::SymbolicArray(array), Vec::new()))
1068            .expect("jsonencode");
1069
1070        assert_eq!(as_string(encoded), "[[\"a\",\"b\"],[\"c\",\"d\"]]");
1071    }
1072
1073    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1074    #[test]
1075    fn jsonencode_matrix_pretty_print() {
1076        let tensor = Tensor::new(vec![1.0, 4.0, 2.0, 5.0, 3.0, 6.0], vec![2, 3]).expect("tensor");
1077        let args = vec![Value::from("PrettyPrint"), Value::Bool(true)];
1078        let encoded =
1079            block_on(jsonencode_builtin(Value::Tensor(tensor), args)).expect("jsonencode");
1080        let expected = "[\n    [1,2,3],\n    [4,5,6]\n]";
1081        assert_eq!(as_string(encoded), expected);
1082    }
1083
1084    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1085    #[test]
1086    fn jsonencode_integer_tensor_preserves_exact_uint64_values() {
1087        let tensor =
1088            Tensor::new_integer(IntegerStorage::U64(vec![u64::MAX, 1_u64 << 63]), vec![1, 2])
1089                .expect("integer tensor");
1090
1091        let encoded =
1092            block_on(jsonencode_builtin(Value::Tensor(tensor), Vec::new())).expect("jsonencode");
1093
1094        assert_eq!(
1095            as_string(encoded),
1096            format!("[{},{}]", u64::MAX, 1_u64 << 63)
1097        );
1098    }
1099
1100    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1101    #[test]
1102    fn jsonencode_struct_round_trip() {
1103        let mut fields = StructValue::new();
1104        fields
1105            .fields
1106            .insert("name".to_string(), Value::from("RunMat"));
1107        fields
1108            .fields
1109            .insert("year".to_string(), Value::Int(IntValue::I32(2025)));
1110        let encoded =
1111            block_on(jsonencode_builtin(Value::Struct(fields), Vec::new())).expect("jsonencode");
1112        assert_eq!(as_string(encoded), "{\"name\":\"RunMat\",\"year\":2025}");
1113    }
1114
1115    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1116    #[test]
1117    fn jsonencode_struct_options_enable_pretty_print() {
1118        let tensor = Tensor::new(vec![1.0, 4.0, 2.0, 5.0], vec![2, 2]).expect("tensor");
1119        let mut opts = StructValue::new();
1120        opts.fields
1121            .insert("PrettyPrint".to_string(), Value::Bool(true));
1122        let encoded = block_on(jsonencode_builtin(
1123            Value::Tensor(tensor),
1124            vec![Value::Struct(opts)],
1125        ))
1126        .expect("jsonencode");
1127        let expected = "[\n    [1,2],\n    [4,5]\n]";
1128        assert_eq!(as_string(encoded), expected);
1129    }
1130
1131    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1132    #[test]
1133    fn jsonencode_options_accept_scalar_tensor_bool() {
1134        let tensor_value = Tensor::new(vec![1.0], vec![1, 1]).expect("tensor");
1135        let args = vec![Value::from("PrettyPrint"), Value::Tensor(tensor_value)];
1136        let encoded = block_on(jsonencode_builtin(Value::Num(42.0), args)).expect("jsonencode");
1137        assert_eq!(as_string(encoded), "42");
1138    }
1139
1140    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1141    #[test]
1142    fn jsonencode_options_reject_non_scalar_tensor_bool() {
1143        let tensor = Tensor::new(vec![1.0, 0.0], vec![1, 2]).expect("tensor");
1144        let err = block_on(jsonencode_builtin(
1145            Value::Num(1.0),
1146            vec![Value::from("PrettyPrint"), Value::Tensor(tensor)],
1147        ))
1148        .expect_err("expected failure");
1149        assert_eq!(error_message(err), JSONENCODE_ERROR_OPTION_VALUE.message);
1150    }
1151
1152    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1153    #[test]
1154    fn jsonencode_options_accept_scalar_logical_array() {
1155        let logical = LogicalArray::new(vec![1], vec![1]).expect("logical");
1156        let args = vec![Value::from("PrettyPrint"), Value::LogicalArray(logical)];
1157        let encoded = block_on(jsonencode_builtin(Value::Num(7.0), args)).expect("jsonencode");
1158        assert_eq!(as_string(encoded), "7");
1159    }
1160
1161    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1162    #[test]
1163    fn jsonencode_convert_inf_and_nan_controls_null_output() {
1164        let tensor = Tensor::new(vec![1.0, f64::NAN], vec![1, 2]).expect("tensor");
1165        let encoded = block_on(jsonencode_builtin(
1166            Value::Tensor(tensor.clone()),
1167            Vec::new(),
1168        ))
1169        .expect("jsonencode");
1170        assert_eq!(as_string(encoded), "[1,null]");
1171
1172        let err = block_on(jsonencode_builtin(
1173            Value::Tensor(tensor),
1174            vec![Value::from("ConvertInfAndNaN"), Value::Bool(false)],
1175        ))
1176        .expect_err("expected failure");
1177        assert_eq!(error_message(err), JSONENCODE_ERROR_INF_NAN.message);
1178    }
1179
1180    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1181    #[test]
1182    fn jsonencode_cell_array() {
1183        let elements = vec![Value::from(1.0), Value::from("two")];
1184        let cell = CellArray::new(elements, 1, 2).expect("cell");
1185        let encoded =
1186            block_on(jsonencode_builtin(Value::Cell(cell), Vec::new())).expect("jsonencode");
1187        assert_eq!(as_string(encoded), "[1,\"two\"]");
1188    }
1189
1190    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1191    #[test]
1192    fn jsonencode_char_array_zero_rows_is_empty_array() {
1193        let chars = CharArray::new(Vec::new(), 0, 3).expect("char array");
1194        let encoded =
1195            block_on(jsonencode_builtin(Value::CharArray(chars), Vec::new())).expect("jsonencode");
1196        assert_eq!(as_string(encoded), "[]");
1197    }
1198
1199    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1200    #[test]
1201    fn jsonencode_char_array_empty_strings_per_row() {
1202        let chars = CharArray::new(Vec::new(), 2, 0).expect("char array");
1203        let encoded =
1204            block_on(jsonencode_builtin(Value::CharArray(chars), Vec::new())).expect("jsonencode");
1205        let encoded_str = as_string(encoded);
1206        assert_eq!(encoded_str, "[\"\",\"\"]");
1207    }
1208
1209    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1210    #[test]
1211    fn jsonencode_string_array_matrix() {
1212        let sa = StringArray::new(vec!["alpha".to_string(), "beta".to_string()], vec![2, 1])
1213            .expect("string array");
1214        let encoded =
1215            block_on(jsonencode_builtin(Value::StringArray(sa), Vec::new())).expect("jsonencode");
1216        assert_eq!(as_string(encoded), "[\"alpha\",\"beta\"]");
1217    }
1218
1219    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1220    #[test]
1221    fn jsonencode_complex_tensor_outputs_objects() {
1222        let ct = ComplexTensor::new(vec![(1.0, 2.0), (3.5, -4.0)], vec![2, 1]).expect("complex");
1223        let encoded =
1224            block_on(jsonencode_builtin(Value::ComplexTensor(ct), Vec::new())).expect("jsonencode");
1225        assert_eq!(
1226            as_string(encoded),
1227            "[{\"real\":1,\"imag\":2},{\"real\":3.5,\"imag\":-4}]"
1228        );
1229    }
1230
1231    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1232    #[test]
1233    fn jsonencode_gpu_tensor_gathers_host_data() {
1234        test_support::with_test_provider(|provider| {
1235            let tensor = Tensor::new(vec![1.0, 0.0, 0.0, 1.0], vec![2, 2]).expect("tensor");
1236            let view = runmat_accelerate_api::HostTensorView {
1237                data: &tensor.data,
1238                shape: &tensor.shape,
1239            };
1240            let handle = provider.upload(&view).expect("upload");
1241            let encoded = block_on(jsonencode_builtin(Value::GpuTensor(handle), Vec::new()))
1242                .expect("jsonencode");
1243            assert_eq!(as_string(encoded), "[[1,0],[0,1]]");
1244        });
1245    }
1246
1247    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1248    #[test]
1249    #[cfg(feature = "wgpu")]
1250    fn jsonencode_gpu_tensor_wgpu_gathers_host_data() {
1251        let ensure = runmat_accelerate::backend::wgpu::provider::ensure_wgpu_provider();
1252        let Some(_) = ensure.ok().flatten() else {
1253            // No WGPU device available on this host; skip.
1254            return;
1255        };
1256        let provider = runmat_accelerate_api::provider().expect("wgpu provider");
1257        let tensor = Tensor::new(vec![1.0, 2.0, 3.0], vec![3, 1]).expect("tensor");
1258        let view = runmat_accelerate_api::HostTensorView {
1259            data: &tensor.data,
1260            shape: &tensor.shape,
1261        };
1262        let handle = provider.upload(&view).expect("upload");
1263        let encoded =
1264            block_on(jsonencode_builtin(Value::GpuTensor(handle), Vec::new())).expect("jsonencode");
1265        assert_eq!(as_string(encoded), "[1,2,3]");
1266    }
1267}