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