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, LogicalArray, ObjectInstance, StringArray,
10    StructValue, 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::StringArray(sa) => string_array_to_json(sa, options),
441        Value::CharArray(ca) => char_array_to_json(ca, options),
442        Value::Struct(sv) => struct_to_json(sv, options),
443        Value::Cell(ca) => cell_array_to_json(ca, options),
444        Value::Object(obj) => object_to_json(obj, options),
445        Value::GpuTensor(_) => Err(jsonencode_error(&JSONENCODE_ERROR_UNEXPECTED_GPU)),
446        Value::HandleObject(_)
447        | Value::Listener(_)
448        | Value::FunctionHandle(_)
449        | Value::ExternalFunctionHandle(_)
450        | Value::MethodFunctionHandle(_)
451        | Value::BoundFunctionHandle { .. }
452        | Value::Closure(_)
453        | Value::ClassRef(_)
454        | Value::MException(_)
455        | Value::OutputList(_) => Err(jsonencode_error(&JSONENCODE_ERROR_UNSUPPORTED_TYPE)),
456    }
457}
458
459fn int_to_number(value: &IntValue) -> JsonNumber {
460    match value {
461        IntValue::I8(v) => JsonNumber::I64(*v as i64),
462        IntValue::I16(v) => JsonNumber::I64(*v as i64),
463        IntValue::I32(v) => JsonNumber::I64(*v as i64),
464        IntValue::I64(v) => JsonNumber::I64(*v),
465        IntValue::U8(v) => JsonNumber::U64(*v as u64),
466        IntValue::U16(v) => JsonNumber::U64(*v as u64),
467        IntValue::U32(v) => JsonNumber::U64(*v as u64),
468        IntValue::U64(v) => JsonNumber::U64(*v),
469    }
470}
471
472fn number_to_json(value: f64, options: &JsonEncodeOptions) -> BuiltinResult<JsonValue> {
473    if !value.is_finite() {
474        if options.convert_inf_and_nan {
475            return Ok(JsonValue::Null);
476        }
477        return Err(jsonencode_error(&JSONENCODE_ERROR_INF_NAN));
478    }
479    Ok(JsonValue::Number(JsonNumber::Float(value)))
480}
481
482fn logical_array_to_json(
483    logical: &LogicalArray,
484    _options: &JsonEncodeOptions,
485) -> BuiltinResult<JsonValue> {
486    let keep_dims = compute_keep_dims(&logical.shape, true);
487    if logical.shape.is_empty() || logical.data.is_empty() {
488        return Ok(JsonValue::Array(Vec::new()));
489    }
490    if keep_dims.is_empty() {
491        let first = logical.data.first().copied().unwrap_or(0) != 0;
492        return Ok(JsonValue::Bool(first));
493    }
494    build_strided_array(&logical.shape, &keep_dims, |offset| {
495        Ok(JsonValue::Bool(logical.data[offset] != 0))
496    })
497}
498
499fn tensor_to_json(tensor: &Tensor, options: &JsonEncodeOptions) -> BuiltinResult<JsonValue> {
500    if tensor.data.is_empty() {
501        return Ok(JsonValue::Array(Vec::new()));
502    }
503    let keep_dims = compute_keep_dims(&tensor.shape, true);
504    if keep_dims.is_empty() {
505        return number_to_json(tensor.data[0], options);
506    }
507    build_strided_array(&tensor.shape, &keep_dims, |offset| {
508        number_to_json(tensor.data[offset], options)
509    })
510}
511
512fn complex_scalar_to_json(
513    real: f64,
514    imag: f64,
515    options: &JsonEncodeOptions,
516) -> BuiltinResult<JsonValue> {
517    let real_json = number_to_json(real, options)?;
518    let imag_json = number_to_json(imag, options)?;
519    Ok(JsonValue::Object(vec![
520        ("real".to_string(), real_json),
521        ("imag".to_string(), imag_json),
522    ]))
523}
524
525fn complex_tensor_to_json(
526    ct: &ComplexTensor,
527    options: &JsonEncodeOptions,
528) -> BuiltinResult<JsonValue> {
529    if ct.data.is_empty() {
530        return Ok(JsonValue::Array(Vec::new()));
531    }
532    let keep_dims = compute_keep_dims(&ct.shape, true);
533    if keep_dims.is_empty() {
534        let (re, im) = ct.data[0];
535        return complex_scalar_to_json(re, im, options);
536    }
537    build_strided_array(&ct.shape, &keep_dims, |offset| {
538        let (re, im) = ct.data[offset];
539        complex_scalar_to_json(re, im, options)
540    })
541}
542
543fn string_array_to_json(
544    sa: &StringArray,
545    _options: &JsonEncodeOptions,
546) -> BuiltinResult<JsonValue> {
547    if sa.data.is_empty() {
548        return Ok(JsonValue::Array(Vec::new()));
549    }
550    let keep_dims = compute_keep_dims(&sa.shape, true);
551    if keep_dims.is_empty() {
552        return Ok(JsonValue::String(sa.data[0].clone()));
553    }
554    build_strided_array(&sa.shape, &keep_dims, |offset| {
555        Ok(JsonValue::String(sa.data[offset].clone()))
556    })
557}
558
559fn char_array_to_json(ca: &CharArray, _options: &JsonEncodeOptions) -> BuiltinResult<JsonValue> {
560    if ca.rows == 0 {
561        return Ok(JsonValue::Array(Vec::new()));
562    }
563
564    if ca.cols == 0 {
565        if ca.rows == 1 {
566            return Ok(JsonValue::String(String::new()));
567        }
568        let mut rows = Vec::with_capacity(ca.rows);
569        for _ in 0..ca.rows {
570            rows.push(JsonValue::String(String::new()));
571        }
572        return Ok(JsonValue::Array(rows));
573    }
574
575    if ca.rows == 1 {
576        return Ok(JsonValue::String(ca.data.iter().collect()));
577    }
578
579    let mut rows = Vec::with_capacity(ca.rows);
580    for r in 0..ca.rows {
581        let mut row_string = String::with_capacity(ca.cols);
582        for c in 0..ca.cols {
583            row_string.push(ca.data[r * ca.cols + c]);
584        }
585        rows.push(JsonValue::String(row_string));
586    }
587    Ok(JsonValue::Array(rows))
588}
589
590fn struct_to_json(sv: &StructValue, options: &JsonEncodeOptions) -> BuiltinResult<JsonValue> {
591    if sv.fields.is_empty() {
592        return Ok(JsonValue::Object(Vec::new()));
593    }
594    let mut map = BTreeMap::new();
595    for (key, value) in &sv.fields {
596        map.insert(key.clone(), value_to_json(value, options)?);
597    }
598    Ok(JsonValue::Object(map.into_iter().collect()))
599}
600
601fn object_to_json(obj: &ObjectInstance, options: &JsonEncodeOptions) -> BuiltinResult<JsonValue> {
602    let mut map = BTreeMap::new();
603    for (key, value) in &obj.properties {
604        map.insert(key.clone(), value_to_json(value, options)?);
605    }
606    Ok(JsonValue::Object(map.into_iter().collect()))
607}
608
609fn cell_array_to_json(ca: &CellArray, options: &JsonEncodeOptions) -> BuiltinResult<JsonValue> {
610    if ca.rows == 0 || ca.cols == 0 {
611        return Ok(JsonValue::Array(Vec::new()));
612    }
613
614    if ca.rows == 1 && ca.cols == 1 {
615        let value = ca.get(0, 0).map_err(|e| {
616            jsonencode_error_with(
617                &JSONENCODE_ERROR_INTERNAL,
618                format!("{} ({e})", JSONENCODE_ERROR_INTERNAL.message),
619            )
620        })?;
621        return Ok(JsonValue::Array(vec![value_to_json(&value, options)?]));
622    }
623
624    if ca.rows == 1 {
625        let mut row = Vec::with_capacity(ca.cols);
626        for c in 0..ca.cols {
627            let element = ca.get(0, c).map_err(|e| {
628                jsonencode_error_with(
629                    &JSONENCODE_ERROR_INTERNAL,
630                    format!("{} ({e})", JSONENCODE_ERROR_INTERNAL.message),
631                )
632            })?;
633            row.push(value_to_json(&element, options)?);
634        }
635        return Ok(JsonValue::Array(row));
636    }
637
638    if ca.cols == 1 {
639        let mut column = Vec::with_capacity(ca.rows);
640        for r in 0..ca.rows {
641            let element = ca.get(r, 0).map_err(|e| {
642                jsonencode_error_with(
643                    &JSONENCODE_ERROR_INTERNAL,
644                    format!("{} ({e})", JSONENCODE_ERROR_INTERNAL.message),
645                )
646            })?;
647            column.push(value_to_json(&element, options)?);
648        }
649        return Ok(JsonValue::Array(column));
650    }
651
652    let mut rows = Vec::with_capacity(ca.rows);
653    for r in 0..ca.rows {
654        let mut row = Vec::with_capacity(ca.cols);
655        for c in 0..ca.cols {
656            let element = ca.get(r, c).map_err(|e| {
657                jsonencode_error_with(
658                    &JSONENCODE_ERROR_INTERNAL,
659                    format!("{} ({e})", JSONENCODE_ERROR_INTERNAL.message),
660                )
661            })?;
662            row.push(value_to_json(&element, options)?);
663        }
664        rows.push(JsonValue::Array(row));
665    }
666    Ok(JsonValue::Array(rows))
667}
668
669fn compute_keep_dims(shape: &[usize], drop_singletons: bool) -> Vec<usize> {
670    let mut keep = Vec::new();
671    for (idx, &size) in shape.iter().enumerate() {
672        if size != 1 || !drop_singletons {
673            keep.push(idx);
674        }
675    }
676    keep
677}
678
679fn compute_strides(shape: &[usize]) -> Vec<usize> {
680    let mut strides = Vec::with_capacity(shape.len());
681    let mut acc = 1usize;
682    for &size in shape {
683        strides.push(acc);
684        acc = acc.saturating_mul(size.max(1));
685    }
686    strides
687}
688
689fn build_strided_array<F>(
690    shape: &[usize],
691    keep_dims: &[usize],
692    mut fetch: F,
693) -> BuiltinResult<JsonValue>
694where
695    F: FnMut(usize) -> BuiltinResult<JsonValue>,
696{
697    if keep_dims.is_empty() {
698        return fetch(0);
699    }
700    if keep_dims.iter().any(|&idx| shape[idx] == 0) {
701        return Ok(JsonValue::Array(Vec::new()));
702    }
703    let strides = compute_strides(shape);
704    let dims: Vec<usize> = keep_dims.iter().map(|&idx| shape[idx]).collect();
705    build_nd_array(&dims, |indices| {
706        let mut offset = 0usize;
707        for (value, dim_idx) in indices.iter().zip(keep_dims.iter()) {
708            offset += value * strides[*dim_idx];
709        }
710        fetch(offset)
711    })
712}
713
714fn build_nd_array<F>(dims: &[usize], mut fetch: F) -> BuiltinResult<JsonValue>
715where
716    F: FnMut(&[usize]) -> BuiltinResult<JsonValue>,
717{
718    if dims.is_empty() {
719        return fetch(&[]);
720    }
721    if dims[0] == 0 {
722        return Ok(JsonValue::Array(Vec::new()));
723    }
724    let mut indices = vec![0usize; dims.len()];
725    build_nd_array_recursive(dims, 0, &mut indices, &mut fetch)
726}
727
728fn build_nd_array_recursive<F>(
729    dims: &[usize],
730    level: usize,
731    indices: &mut [usize],
732    fetch: &mut F,
733) -> BuiltinResult<JsonValue>
734where
735    F: FnMut(&[usize]) -> BuiltinResult<JsonValue>,
736{
737    let size = dims[level];
738    if size == 0 {
739        return Ok(JsonValue::Array(Vec::new()));
740    }
741    if level + 1 == dims.len() {
742        let mut items = Vec::with_capacity(size);
743        for i in 0..size {
744            indices[level] = i;
745            items.push(fetch(indices)?);
746        }
747        return Ok(JsonValue::Array(items));
748    }
749    let mut items = Vec::with_capacity(size);
750    for i in 0..size {
751        indices[level] = i;
752        items.push(build_nd_array_recursive(dims, level + 1, indices, fetch)?);
753    }
754    Ok(JsonValue::Array(items))
755}
756
757fn render_json(value: &JsonValue, options: &JsonEncodeOptions) -> String {
758    let mut writer = JsonWriter::new(options.pretty_print);
759    writer.write_value(value);
760    writer.finish()
761}
762
763struct JsonWriter {
764    output: String,
765    pretty: bool,
766    indent: usize,
767}
768
769impl JsonWriter {
770    fn new(pretty: bool) -> Self {
771        Self {
772            output: String::new(),
773            pretty,
774            indent: 0,
775        }
776    }
777
778    fn finish(self) -> String {
779        self.output
780    }
781
782    fn write_value(&mut self, value: &JsonValue) {
783        match value {
784            JsonValue::Null => self.output.push_str("null"),
785            JsonValue::Bool(true) => self.output.push_str("true"),
786            JsonValue::Bool(false) => self.output.push_str("false"),
787            JsonValue::Number(number) => self.write_number(number),
788            JsonValue::String(text) => {
789                self.output.push('"');
790                self.output.push_str(&escape_json_string(text));
791                self.output.push('"');
792            }
793            JsonValue::Array(items) => self.write_array(items),
794            JsonValue::Object(fields) => self.write_object(fields),
795        }
796    }
797
798    fn write_number(&mut self, number: &JsonNumber) {
799        match number {
800            JsonNumber::Float(f) => {
801                if f.is_nan() || !f.is_finite() {
802                    self.output.push_str("null");
803                } else {
804                    self.output.push_str(&format_number(*f));
805                }
806            }
807            JsonNumber::I64(i) => {
808                let _ = write!(self.output, "{i}");
809            }
810            JsonNumber::U64(u) => {
811                let _ = write!(self.output, "{u}");
812            }
813        }
814    }
815
816    fn write_array(&mut self, items: &[JsonValue]) {
817        if items.is_empty() {
818            self.output.push_str("[]");
819            return;
820        }
821        let inline = if self.pretty {
822            items.iter().all(|item| {
823                matches!(
824                    item,
825                    JsonValue::Null
826                        | JsonValue::Bool(_)
827                        | JsonValue::Number(_)
828                        | JsonValue::String(_)
829                )
830            })
831        } else {
832            false
833        };
834        if inline {
835            self.output.push('[');
836            for (index, item) in items.iter().enumerate() {
837                self.write_value(item);
838                if index + 1 < items.len() {
839                    self.output.push(',');
840                }
841            }
842            self.output.push(']');
843            return;
844        }
845        self.output.push('[');
846        if self.pretty {
847            self.output.push('\n');
848            self.indent += 1;
849        }
850        for (index, item) in items.iter().enumerate() {
851            if self.pretty {
852                self.write_indent();
853            }
854            self.write_value(item);
855            if index + 1 < items.len() {
856                if self.pretty {
857                    self.output.push_str(",\n");
858                } else {
859                    self.output.push(',');
860                }
861            }
862        }
863        if self.pretty {
864            self.output.push('\n');
865            if self.indent > 0 {
866                self.indent -= 1;
867            }
868            self.write_indent();
869        }
870        self.output.push(']');
871    }
872
873    fn write_object(&mut self, fields: &[(String, JsonValue)]) {
874        if fields.is_empty() {
875            self.output.push_str("{}");
876            return;
877        }
878        self.output.push('{');
879        if self.pretty {
880            self.output.push('\n');
881            self.indent += 1;
882        }
883        for (index, (key, value)) in fields.iter().enumerate() {
884            if self.pretty {
885                self.write_indent();
886            }
887            self.output.push('"');
888            self.output.push_str(&escape_json_string(key));
889            self.output.push('"');
890            if self.pretty {
891                self.output.push_str(": ");
892            } else {
893                self.output.push(':');
894            }
895            self.write_value(value);
896            if index + 1 < fields.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_indent(&mut self) {
915        if self.pretty {
916            for _ in 0..self.indent {
917                self.output.push_str("    ");
918            }
919        }
920    }
921}
922
923fn escape_json_string(value: &str) -> String {
924    let mut escaped = String::with_capacity(value.len());
925    for ch in value.chars() {
926        match ch {
927            '"' => escaped.push_str("\\\""),
928            '\\' => escaped.push_str("\\\\"),
929            '\u{08}' => escaped.push_str("\\b"),
930            '\u{0C}' => escaped.push_str("\\f"),
931            '\n' => escaped.push_str("\\n"),
932            '\r' => escaped.push_str("\\r"),
933            '\t' => escaped.push_str("\\t"),
934            c if (c as u32) < 0x20 => {
935                let _ = write!(escaped, "\\u{:04X}", c as u32);
936            }
937            _ => escaped.push(ch),
938        }
939    }
940    escaped
941}
942
943fn format_number(value: f64) -> String {
944    if value.fract() == 0.0 {
945        // Display integer-like doubles without decimal point
946        format!("{:.0}", value)
947    } else {
948        format!("{}", value)
949    }
950}
951
952#[cfg(test)]
953pub(crate) mod tests {
954    use super::*;
955    use crate::builtins::common::test_support;
956    use futures::executor::block_on;
957    use runmat_builtins::{
958        CellArray, CharArray, ComplexTensor, LogicalArray, StringArray, StructValue, SymbolicExpr,
959        Tensor,
960    };
961
962    fn as_string(value: Value) -> String {
963        match value {
964            Value::CharArray(ca) => ca.data.iter().collect(),
965            Value::String(s) => s,
966            other => panic!("expected char array, got {:?}", other),
967        }
968    }
969
970    fn error_message(err: crate::RuntimeError) -> String {
971        err.message().to_string()
972    }
973
974    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
975    #[test]
976    fn jsonencode_descriptor_signatures_cover_core_forms() {
977        let labels: Vec<&str> = JSONENCODE_DESCRIPTOR
978            .signatures
979            .iter()
980            .map(|sig| sig.label)
981            .collect();
982        assert!(labels.contains(&"jsonText = jsonencode(value)"));
983        assert!(labels.contains(&"jsonText = jsonencode(value, options)"));
984        assert!(labels.contains(&"jsonText = jsonencode(value, name, optionValue)"));
985        assert!(labels.contains(&"jsonText = jsonencode(value, nameValuePairs...)"));
986    }
987
988    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
989    #[test]
990    fn jsonencode_scalar_double() {
991        let encoded =
992            block_on(jsonencode_builtin(Value::Num(5.0), Vec::new())).expect("jsonencode");
993        assert_eq!(as_string(encoded), "5");
994    }
995
996    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
997    #[test]
998    fn jsonencode_symbolic_value_as_text() {
999        let expr = SymbolicExpr::div_expr(
1000            SymbolicExpr::function(
1001                runmat_builtins::symbolic::SymbolicFunction::Sin,
1002                SymbolicExpr::variable("x"),
1003            ),
1004            SymbolicExpr::variable("x"),
1005        );
1006        let encoded =
1007            block_on(jsonencode_builtin(Value::Symbolic(expr), Vec::new())).expect("jsonencode");
1008
1009        assert_eq!(as_string(encoded), "\"sin(x)/x\"");
1010    }
1011
1012    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1013    #[test]
1014    fn jsonencode_matrix_pretty_print() {
1015        let tensor = Tensor::new(vec![1.0, 4.0, 2.0, 5.0, 3.0, 6.0], vec![2, 3]).expect("tensor");
1016        let args = vec![Value::from("PrettyPrint"), Value::Bool(true)];
1017        let encoded =
1018            block_on(jsonencode_builtin(Value::Tensor(tensor), args)).expect("jsonencode");
1019        let expected = "[\n    [1,2,3],\n    [4,5,6]\n]";
1020        assert_eq!(as_string(encoded), expected);
1021    }
1022
1023    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1024    #[test]
1025    fn jsonencode_struct_round_trip() {
1026        let mut fields = StructValue::new();
1027        fields
1028            .fields
1029            .insert("name".to_string(), Value::from("RunMat"));
1030        fields
1031            .fields
1032            .insert("year".to_string(), Value::Int(IntValue::I32(2025)));
1033        let encoded =
1034            block_on(jsonencode_builtin(Value::Struct(fields), Vec::new())).expect("jsonencode");
1035        assert_eq!(as_string(encoded), "{\"name\":\"RunMat\",\"year\":2025}");
1036    }
1037
1038    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1039    #[test]
1040    fn jsonencode_struct_options_enable_pretty_print() {
1041        let tensor = Tensor::new(vec![1.0, 4.0, 2.0, 5.0], vec![2, 2]).expect("tensor");
1042        let mut opts = StructValue::new();
1043        opts.fields
1044            .insert("PrettyPrint".to_string(), Value::Bool(true));
1045        let encoded = block_on(jsonencode_builtin(
1046            Value::Tensor(tensor),
1047            vec![Value::Struct(opts)],
1048        ))
1049        .expect("jsonencode");
1050        let expected = "[\n    [1,2],\n    [4,5]\n]";
1051        assert_eq!(as_string(encoded), expected);
1052    }
1053
1054    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1055    #[test]
1056    fn jsonencode_options_accept_scalar_tensor_bool() {
1057        let tensor_value = Tensor::new(vec![1.0], vec![1, 1]).expect("tensor");
1058        let args = vec![Value::from("PrettyPrint"), Value::Tensor(tensor_value)];
1059        let encoded = block_on(jsonencode_builtin(Value::Num(42.0), args)).expect("jsonencode");
1060        assert_eq!(as_string(encoded), "42");
1061    }
1062
1063    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1064    #[test]
1065    fn jsonencode_options_reject_non_scalar_tensor_bool() {
1066        let tensor = Tensor::new(vec![1.0, 0.0], vec![1, 2]).expect("tensor");
1067        let err = block_on(jsonencode_builtin(
1068            Value::Num(1.0),
1069            vec![Value::from("PrettyPrint"), Value::Tensor(tensor)],
1070        ))
1071        .expect_err("expected failure");
1072        assert_eq!(error_message(err), JSONENCODE_ERROR_OPTION_VALUE.message);
1073    }
1074
1075    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1076    #[test]
1077    fn jsonencode_options_accept_scalar_logical_array() {
1078        let logical = LogicalArray::new(vec![1], vec![1]).expect("logical");
1079        let args = vec![Value::from("PrettyPrint"), Value::LogicalArray(logical)];
1080        let encoded = block_on(jsonencode_builtin(Value::Num(7.0), args)).expect("jsonencode");
1081        assert_eq!(as_string(encoded), "7");
1082    }
1083
1084    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1085    #[test]
1086    fn jsonencode_convert_inf_and_nan_controls_null_output() {
1087        let tensor = Tensor::new(vec![1.0, f64::NAN], vec![1, 2]).expect("tensor");
1088        let encoded = block_on(jsonencode_builtin(
1089            Value::Tensor(tensor.clone()),
1090            Vec::new(),
1091        ))
1092        .expect("jsonencode");
1093        assert_eq!(as_string(encoded), "[1,null]");
1094
1095        let err = block_on(jsonencode_builtin(
1096            Value::Tensor(tensor),
1097            vec![Value::from("ConvertInfAndNaN"), Value::Bool(false)],
1098        ))
1099        .expect_err("expected failure");
1100        assert_eq!(error_message(err), JSONENCODE_ERROR_INF_NAN.message);
1101    }
1102
1103    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1104    #[test]
1105    fn jsonencode_cell_array() {
1106        let elements = vec![Value::from(1.0), Value::from("two")];
1107        let cell = CellArray::new(elements, 1, 2).expect("cell");
1108        let encoded =
1109            block_on(jsonencode_builtin(Value::Cell(cell), Vec::new())).expect("jsonencode");
1110        assert_eq!(as_string(encoded), "[1,\"two\"]");
1111    }
1112
1113    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1114    #[test]
1115    fn jsonencode_char_array_zero_rows_is_empty_array() {
1116        let chars = CharArray::new(Vec::new(), 0, 3).expect("char array");
1117        let encoded =
1118            block_on(jsonencode_builtin(Value::CharArray(chars), Vec::new())).expect("jsonencode");
1119        assert_eq!(as_string(encoded), "[]");
1120    }
1121
1122    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1123    #[test]
1124    fn jsonencode_char_array_empty_strings_per_row() {
1125        let chars = CharArray::new(Vec::new(), 2, 0).expect("char array");
1126        let encoded =
1127            block_on(jsonencode_builtin(Value::CharArray(chars), Vec::new())).expect("jsonencode");
1128        let encoded_str = as_string(encoded);
1129        assert_eq!(encoded_str, "[\"\",\"\"]");
1130    }
1131
1132    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1133    #[test]
1134    fn jsonencode_string_array_matrix() {
1135        let sa = StringArray::new(vec!["alpha".to_string(), "beta".to_string()], vec![2, 1])
1136            .expect("string array");
1137        let encoded =
1138            block_on(jsonencode_builtin(Value::StringArray(sa), Vec::new())).expect("jsonencode");
1139        assert_eq!(as_string(encoded), "[\"alpha\",\"beta\"]");
1140    }
1141
1142    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1143    #[test]
1144    fn jsonencode_complex_tensor_outputs_objects() {
1145        let ct = ComplexTensor::new(vec![(1.0, 2.0), (3.5, -4.0)], vec![2, 1]).expect("complex");
1146        let encoded =
1147            block_on(jsonencode_builtin(Value::ComplexTensor(ct), Vec::new())).expect("jsonencode");
1148        assert_eq!(
1149            as_string(encoded),
1150            "[{\"real\":1,\"imag\":2},{\"real\":3.5,\"imag\":-4}]"
1151        );
1152    }
1153
1154    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1155    #[test]
1156    fn jsonencode_gpu_tensor_gathers_host_data() {
1157        test_support::with_test_provider(|provider| {
1158            let tensor = Tensor::new(vec![1.0, 0.0, 0.0, 1.0], vec![2, 2]).expect("tensor");
1159            let view = runmat_accelerate_api::HostTensorView {
1160                data: &tensor.data,
1161                shape: &tensor.shape,
1162            };
1163            let handle = provider.upload(&view).expect("upload");
1164            let encoded = block_on(jsonencode_builtin(Value::GpuTensor(handle), Vec::new()))
1165                .expect("jsonencode");
1166            assert_eq!(as_string(encoded), "[[1,0],[0,1]]");
1167        });
1168    }
1169
1170    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1171    #[test]
1172    #[cfg(feature = "wgpu")]
1173    fn jsonencode_gpu_tensor_wgpu_gathers_host_data() {
1174        let ensure = runmat_accelerate::backend::wgpu::provider::ensure_wgpu_provider();
1175        let Some(_) = ensure.ok().flatten() else {
1176            // No WGPU device available on this host; skip.
1177            return;
1178        };
1179        let provider = runmat_accelerate_api::provider().expect("wgpu provider");
1180        let tensor = Tensor::new(vec![1.0, 2.0, 3.0], vec![3, 1]).expect("tensor");
1181        let view = runmat_accelerate_api::HostTensorView {
1182            data: &tensor.data,
1183            shape: &tensor.shape,
1184        };
1185        let handle = provider.upload(&view).expect("upload");
1186        let encoded =
1187            block_on(jsonencode_builtin(Value::GpuTensor(handle), Vec::new())).expect("jsonencode");
1188        assert_eq!(as_string(encoded), "[1,2,3]");
1189    }
1190}