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    CellArray, CharArray, ComplexTensor, IntValue, LogicalArray, ObjectInstance, StringArray,
8    StructValue, Tensor, Value,
9};
10use runmat_macros::runtime_builtin;
11
12use crate::builtins::common::spec::{
13    BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
14    ReductionNaN, ResidencyPolicy, ShapeRequirements,
15};
16use crate::{build_runtime_error, gather_if_needed_async, BuiltinResult, RuntimeError};
17
18const OPTION_NAME_ERROR: &str = "jsonencode: option names must be character vectors or strings";
19const OPTION_VALUE_ERROR: &str = "jsonencode: option value must be scalar logical or numeric";
20const INF_NAN_ERROR: &str = "jsonencode: ConvertInfAndNaN must be true to encode NaN or Inf values";
21const UNSUPPORTED_TYPE_ERROR: &str =
22    "jsonencode: unsupported input type; expected numeric, logical, string, struct, cell, or object data";
23
24#[allow(clippy::too_many_lines)]
25#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::io::json::jsonencode")]
26pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
27    name: "jsonencode",
28    op_kind: GpuOpKind::Custom("serialization"),
29    supported_precisions: &[],
30    broadcast: BroadcastSemantics::None,
31    provider_hooks: &[],
32    constant_strategy: ConstantStrategy::InlineLiteral,
33    residency: ResidencyPolicy::GatherImmediately,
34    nan_mode: ReductionNaN::Include,
35    two_pass_threshold: None,
36    workgroup_size: None,
37    accepts_nan_mode: false,
38    notes:
39        "Serialization sink that gathers GPU data to host memory before emitting UTF-8 JSON text.",
40};
41
42fn jsonencode_error(message: impl Into<String>) -> RuntimeError {
43    build_runtime_error(message)
44        .with_builtin("jsonencode")
45        .build()
46}
47
48fn jsonencode_flow_with_context(err: RuntimeError) -> RuntimeError {
49    let mut builder = build_runtime_error(err.message().to_string()).with_builtin("jsonencode");
50    if let Some(identifier) = err.identifier() {
51        builder = builder.with_identifier(identifier.to_string());
52    }
53    builder.with_source(err).build()
54}
55
56#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::io::json::jsonencode")]
57pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
58    name: "jsonencode",
59    shape: ShapeRequirements::Any,
60    constant_strategy: ConstantStrategy::InlineLiteral,
61    elementwise: None,
62    reduction: None,
63    emits_nan: false,
64    notes: "jsonencode is a residency sink and never participates in fusion planning.",
65};
66
67#[derive(Debug, Clone)]
68struct JsonEncodeOptions {
69    pretty_print: bool,
70    convert_inf_and_nan: bool,
71}
72
73impl Default for JsonEncodeOptions {
74    fn default() -> Self {
75        Self {
76            pretty_print: false,
77            convert_inf_and_nan: true,
78        }
79    }
80}
81
82#[derive(Debug, Clone)]
83enum JsonValue {
84    Null,
85    Bool(bool),
86    Number(JsonNumber),
87    String(String),
88    Array(Vec<JsonValue>),
89    Object(Vec<(String, JsonValue)>),
90}
91
92#[derive(Debug, Clone)]
93enum JsonNumber {
94    Float(f64),
95    I64(i64),
96    U64(u64),
97}
98
99#[runtime_builtin(
100    name = "jsonencode",
101    category = "io/json",
102    summary = "Serialize MATLAB values to UTF-8 JSON text.",
103    keywords = "jsonencode,json,serialization,struct,gpu",
104    accel = "cpu",
105    type_resolver(crate::builtins::io::type_resolvers::jsonencode_type),
106    builtin_path = "crate::builtins::io::json::jsonencode"
107)]
108async fn jsonencode_builtin(value: Value, rest: Vec<Value>) -> crate::BuiltinResult<Value> {
109    let host_value = gather_if_needed_async(&value)
110        .await
111        .map_err(jsonencode_flow_with_context)?;
112    let mut gathered_args = Vec::with_capacity(rest.len());
113    for value in &rest {
114        gathered_args.push(
115            gather_if_needed_async(value)
116                .await
117                .map_err(jsonencode_flow_with_context)?,
118        );
119    }
120
121    let options = parse_options(&gathered_args)?;
122    let json_value = value_to_json(&host_value, &options)?;
123    let json_string = render_json(&json_value, &options);
124
125    Ok(Value::CharArray(CharArray::new_row(&json_string)))
126}
127
128fn parse_options(args: &[Value]) -> BuiltinResult<JsonEncodeOptions> {
129    let mut options = JsonEncodeOptions::default();
130    if args.is_empty() {
131        return Ok(options);
132    }
133
134    if args.len() == 1 {
135        if let Value::Struct(struct_value) = &args[0] {
136            apply_struct_options(struct_value, &mut options)?;
137            return Ok(options);
138        }
139        return Err(jsonencode_error(
140            "jsonencode: expected name/value pairs or options struct",
141        ));
142    }
143
144    if !args.len().is_multiple_of(2) {
145        return Err(jsonencode_error(
146            "jsonencode: name/value pairs must come in pairs",
147        ));
148    }
149
150    let mut idx = 0usize;
151    while idx < args.len() {
152        let name = option_name(&args[idx])?;
153        let value = &args[idx + 1];
154        apply_option(&name, value, &mut options)?;
155        idx += 2;
156    }
157
158    Ok(options)
159}
160
161fn apply_struct_options(
162    struct_value: &StructValue,
163    options: &mut JsonEncodeOptions,
164) -> BuiltinResult<()> {
165    for (key, value) in &struct_value.fields {
166        apply_option(key, value, options)?;
167    }
168    Ok(())
169}
170
171fn option_name(value: &Value) -> BuiltinResult<String> {
172    match value {
173        Value::String(s) => Ok(s.clone()),
174        Value::CharArray(ca) if ca.rows == 1 => Ok(ca.data.iter().collect()),
175        Value::StringArray(sa) if sa.data.len() == 1 => Ok(sa.data[0].clone()),
176        _ => Err(jsonencode_error(OPTION_NAME_ERROR)),
177    }
178}
179
180fn apply_option(
181    raw_name: &str,
182    value: &Value,
183    options: &mut JsonEncodeOptions,
184) -> BuiltinResult<()> {
185    let lowered = raw_name.to_ascii_lowercase();
186    match lowered.as_str() {
187        "prettyprint" => {
188            options.pretty_print = coerce_bool(value)?;
189            Ok(())
190        }
191        "convertinfandnan" => {
192            options.convert_inf_and_nan = coerce_bool(value)?;
193            Ok(())
194        }
195        other => Err(jsonencode_error(format!(
196            "jsonencode: unknown option '{}'",
197            other
198        ))),
199    }
200}
201
202fn coerce_bool(value: &Value) -> BuiltinResult<bool> {
203    match value {
204        Value::Bool(b) => Ok(*b),
205        Value::Int(i) => Ok(i.to_i64() != 0),
206        Value::Num(n) => bool_from_f64(*n),
207        Value::Tensor(t) => {
208            if t.data.len() == 1 {
209                bool_from_f64(t.data[0])
210            } else {
211                Err(jsonencode_error(OPTION_VALUE_ERROR))
212            }
213        }
214        Value::LogicalArray(la) => match la.data.len() {
215            1 => Ok(la.data[0] != 0),
216            _ => Err(jsonencode_error(OPTION_VALUE_ERROR)),
217        },
218        Value::CharArray(ca) if ca.rows == 1 => {
219            parse_bool_string(&ca.data.iter().collect::<String>())
220        }
221        Value::String(s) => parse_bool_string(s),
222        Value::StringArray(sa) if sa.data.len() == 1 => parse_bool_string(&sa.data[0]),
223        _ => Err(jsonencode_error(OPTION_VALUE_ERROR)),
224    }
225}
226
227fn bool_from_f64(value: f64) -> BuiltinResult<bool> {
228    if value.is_finite() {
229        Ok(value != 0.0)
230    } else {
231        Err(jsonencode_error(OPTION_VALUE_ERROR))
232    }
233}
234
235fn parse_bool_string(text: &str) -> BuiltinResult<bool> {
236    match text.trim().to_ascii_lowercase().as_str() {
237        "true" | "on" | "yes" | "1" => Ok(true),
238        "false" | "off" | "no" | "0" => Ok(false),
239        _ => Err(jsonencode_error(OPTION_VALUE_ERROR)),
240    }
241}
242
243fn value_to_json(value: &Value, options: &JsonEncodeOptions) -> BuiltinResult<JsonValue> {
244    match value {
245        Value::Num(n) => number_to_json(*n, options),
246        Value::Int(i) => Ok(JsonValue::Number(int_to_number(i))),
247        Value::Bool(b) => Ok(JsonValue::Bool(*b)),
248        Value::LogicalArray(logical) => logical_array_to_json(logical, options),
249        Value::Tensor(tensor) => tensor_to_json(tensor, options),
250        Value::Complex(re, im) => complex_scalar_to_json(*re, *im, options),
251        Value::ComplexTensor(ct) => complex_tensor_to_json(ct, options),
252        Value::String(s) => Ok(JsonValue::String(s.clone())),
253        Value::StringArray(sa) => string_array_to_json(sa, options),
254        Value::CharArray(ca) => char_array_to_json(ca, options),
255        Value::Struct(sv) => struct_to_json(sv, options),
256        Value::Cell(ca) => cell_array_to_json(ca, options),
257        Value::Object(obj) => object_to_json(obj, options),
258        Value::GpuTensor(_) => Err(jsonencode_error(
259            "jsonencode: unexpected gpuArray handle after gather pass",
260        )),
261        Value::HandleObject(_)
262        | Value::Listener(_)
263        | Value::FunctionHandle(_)
264        | Value::Closure(_)
265        | Value::ClassRef(_)
266        | Value::MException(_)
267        | Value::OutputList(_) => Err(jsonencode_error(UNSUPPORTED_TYPE_ERROR)),
268    }
269}
270
271fn int_to_number(value: &IntValue) -> JsonNumber {
272    match value {
273        IntValue::I8(v) => JsonNumber::I64(*v as i64),
274        IntValue::I16(v) => JsonNumber::I64(*v as i64),
275        IntValue::I32(v) => JsonNumber::I64(*v as i64),
276        IntValue::I64(v) => JsonNumber::I64(*v),
277        IntValue::U8(v) => JsonNumber::U64(*v as u64),
278        IntValue::U16(v) => JsonNumber::U64(*v as u64),
279        IntValue::U32(v) => JsonNumber::U64(*v as u64),
280        IntValue::U64(v) => JsonNumber::U64(*v),
281    }
282}
283
284fn number_to_json(value: f64, options: &JsonEncodeOptions) -> BuiltinResult<JsonValue> {
285    if !value.is_finite() {
286        if options.convert_inf_and_nan {
287            return Ok(JsonValue::Null);
288        }
289        return Err(jsonencode_error(INF_NAN_ERROR));
290    }
291    Ok(JsonValue::Number(JsonNumber::Float(value)))
292}
293
294fn logical_array_to_json(
295    logical: &LogicalArray,
296    _options: &JsonEncodeOptions,
297) -> BuiltinResult<JsonValue> {
298    let keep_dims = compute_keep_dims(&logical.shape, true);
299    if logical.shape.is_empty() || logical.data.is_empty() {
300        return Ok(JsonValue::Array(Vec::new()));
301    }
302    if keep_dims.is_empty() {
303        let first = logical.data.first().copied().unwrap_or(0) != 0;
304        return Ok(JsonValue::Bool(first));
305    }
306    build_strided_array(&logical.shape, &keep_dims, |offset| {
307        Ok(JsonValue::Bool(logical.data[offset] != 0))
308    })
309}
310
311fn tensor_to_json(tensor: &Tensor, options: &JsonEncodeOptions) -> BuiltinResult<JsonValue> {
312    if tensor.data.is_empty() {
313        return Ok(JsonValue::Array(Vec::new()));
314    }
315    let keep_dims = compute_keep_dims(&tensor.shape, true);
316    if keep_dims.is_empty() {
317        return number_to_json(tensor.data[0], options);
318    }
319    build_strided_array(&tensor.shape, &keep_dims, |offset| {
320        number_to_json(tensor.data[offset], options)
321    })
322}
323
324fn complex_scalar_to_json(
325    real: f64,
326    imag: f64,
327    options: &JsonEncodeOptions,
328) -> BuiltinResult<JsonValue> {
329    let real_json = number_to_json(real, options)?;
330    let imag_json = number_to_json(imag, options)?;
331    Ok(JsonValue::Object(vec![
332        ("real".to_string(), real_json),
333        ("imag".to_string(), imag_json),
334    ]))
335}
336
337fn complex_tensor_to_json(
338    ct: &ComplexTensor,
339    options: &JsonEncodeOptions,
340) -> BuiltinResult<JsonValue> {
341    if ct.data.is_empty() {
342        return Ok(JsonValue::Array(Vec::new()));
343    }
344    let keep_dims = compute_keep_dims(&ct.shape, true);
345    if keep_dims.is_empty() {
346        let (re, im) = ct.data[0];
347        return complex_scalar_to_json(re, im, options);
348    }
349    build_strided_array(&ct.shape, &keep_dims, |offset| {
350        let (re, im) = ct.data[offset];
351        complex_scalar_to_json(re, im, options)
352    })
353}
354
355fn string_array_to_json(
356    sa: &StringArray,
357    _options: &JsonEncodeOptions,
358) -> BuiltinResult<JsonValue> {
359    if sa.data.is_empty() {
360        return Ok(JsonValue::Array(Vec::new()));
361    }
362    let keep_dims = compute_keep_dims(&sa.shape, true);
363    if keep_dims.is_empty() {
364        return Ok(JsonValue::String(sa.data[0].clone()));
365    }
366    build_strided_array(&sa.shape, &keep_dims, |offset| {
367        Ok(JsonValue::String(sa.data[offset].clone()))
368    })
369}
370
371fn char_array_to_json(ca: &CharArray, _options: &JsonEncodeOptions) -> BuiltinResult<JsonValue> {
372    if ca.rows == 0 {
373        return Ok(JsonValue::Array(Vec::new()));
374    }
375
376    if ca.cols == 0 {
377        if ca.rows == 1 {
378            return Ok(JsonValue::String(String::new()));
379        }
380        let mut rows = Vec::with_capacity(ca.rows);
381        for _ in 0..ca.rows {
382            rows.push(JsonValue::String(String::new()));
383        }
384        return Ok(JsonValue::Array(rows));
385    }
386
387    if ca.rows == 1 {
388        return Ok(JsonValue::String(ca.data.iter().collect()));
389    }
390
391    let mut rows = Vec::with_capacity(ca.rows);
392    for r in 0..ca.rows {
393        let mut row_string = String::with_capacity(ca.cols);
394        for c in 0..ca.cols {
395            row_string.push(ca.data[r * ca.cols + c]);
396        }
397        rows.push(JsonValue::String(row_string));
398    }
399    Ok(JsonValue::Array(rows))
400}
401
402fn struct_to_json(sv: &StructValue, options: &JsonEncodeOptions) -> BuiltinResult<JsonValue> {
403    if sv.fields.is_empty() {
404        return Ok(JsonValue::Object(Vec::new()));
405    }
406    let mut map = BTreeMap::new();
407    for (key, value) in &sv.fields {
408        map.insert(key.clone(), value_to_json(value, options)?);
409    }
410    Ok(JsonValue::Object(map.into_iter().collect()))
411}
412
413fn object_to_json(obj: &ObjectInstance, options: &JsonEncodeOptions) -> BuiltinResult<JsonValue> {
414    let mut map = BTreeMap::new();
415    for (key, value) in &obj.properties {
416        map.insert(key.clone(), value_to_json(value, options)?);
417    }
418    Ok(JsonValue::Object(map.into_iter().collect()))
419}
420
421fn cell_array_to_json(ca: &CellArray, options: &JsonEncodeOptions) -> BuiltinResult<JsonValue> {
422    if ca.rows == 0 || ca.cols == 0 {
423        return Ok(JsonValue::Array(Vec::new()));
424    }
425
426    if ca.rows == 1 && ca.cols == 1 {
427        let value = ca
428            .get(0, 0)
429            .map_err(|e| jsonencode_error(format!("jsonencode: {e}")))?;
430        return Ok(JsonValue::Array(vec![value_to_json(&value, options)?]));
431    }
432
433    if ca.rows == 1 {
434        let mut row = Vec::with_capacity(ca.cols);
435        for c in 0..ca.cols {
436            let element = ca
437                .get(0, c)
438                .map_err(|e| jsonencode_error(format!("jsonencode: {e}")))?;
439            row.push(value_to_json(&element, options)?);
440        }
441        return Ok(JsonValue::Array(row));
442    }
443
444    if ca.cols == 1 {
445        let mut column = Vec::with_capacity(ca.rows);
446        for r in 0..ca.rows {
447            let element = ca
448                .get(r, 0)
449                .map_err(|e| jsonencode_error(format!("jsonencode: {e}")))?;
450            column.push(value_to_json(&element, options)?);
451        }
452        return Ok(JsonValue::Array(column));
453    }
454
455    let mut rows = Vec::with_capacity(ca.rows);
456    for r in 0..ca.rows {
457        let mut row = Vec::with_capacity(ca.cols);
458        for c in 0..ca.cols {
459            let element = ca
460                .get(r, c)
461                .map_err(|e| jsonencode_error(format!("jsonencode: {e}")))?;
462            row.push(value_to_json(&element, options)?);
463        }
464        rows.push(JsonValue::Array(row));
465    }
466    Ok(JsonValue::Array(rows))
467}
468
469fn compute_keep_dims(shape: &[usize], drop_singletons: bool) -> Vec<usize> {
470    let mut keep = Vec::new();
471    for (idx, &size) in shape.iter().enumerate() {
472        if size != 1 || !drop_singletons {
473            keep.push(idx);
474        }
475    }
476    keep
477}
478
479fn compute_strides(shape: &[usize]) -> Vec<usize> {
480    let mut strides = Vec::with_capacity(shape.len());
481    let mut acc = 1usize;
482    for &size in shape {
483        strides.push(acc);
484        acc = acc.saturating_mul(size.max(1));
485    }
486    strides
487}
488
489fn build_strided_array<F>(
490    shape: &[usize],
491    keep_dims: &[usize],
492    mut fetch: F,
493) -> BuiltinResult<JsonValue>
494where
495    F: FnMut(usize) -> BuiltinResult<JsonValue>,
496{
497    if keep_dims.is_empty() {
498        return fetch(0);
499    }
500    if keep_dims.iter().any(|&idx| shape[idx] == 0) {
501        return Ok(JsonValue::Array(Vec::new()));
502    }
503    let strides = compute_strides(shape);
504    let dims: Vec<usize> = keep_dims.iter().map(|&idx| shape[idx]).collect();
505    build_nd_array(&dims, |indices| {
506        let mut offset = 0usize;
507        for (value, dim_idx) in indices.iter().zip(keep_dims.iter()) {
508            offset += value * strides[*dim_idx];
509        }
510        fetch(offset)
511    })
512}
513
514fn build_nd_array<F>(dims: &[usize], mut fetch: F) -> BuiltinResult<JsonValue>
515where
516    F: FnMut(&[usize]) -> BuiltinResult<JsonValue>,
517{
518    if dims.is_empty() {
519        return fetch(&[]);
520    }
521    if dims[0] == 0 {
522        return Ok(JsonValue::Array(Vec::new()));
523    }
524    let mut indices = vec![0usize; dims.len()];
525    build_nd_array_recursive(dims, 0, &mut indices, &mut fetch)
526}
527
528fn build_nd_array_recursive<F>(
529    dims: &[usize],
530    level: usize,
531    indices: &mut [usize],
532    fetch: &mut F,
533) -> BuiltinResult<JsonValue>
534where
535    F: FnMut(&[usize]) -> BuiltinResult<JsonValue>,
536{
537    let size = dims[level];
538    if size == 0 {
539        return Ok(JsonValue::Array(Vec::new()));
540    }
541    if level + 1 == dims.len() {
542        let mut items = Vec::with_capacity(size);
543        for i in 0..size {
544            indices[level] = i;
545            items.push(fetch(indices)?);
546        }
547        return Ok(JsonValue::Array(items));
548    }
549    let mut items = Vec::with_capacity(size);
550    for i in 0..size {
551        indices[level] = i;
552        items.push(build_nd_array_recursive(dims, level + 1, indices, fetch)?);
553    }
554    Ok(JsonValue::Array(items))
555}
556
557fn render_json(value: &JsonValue, options: &JsonEncodeOptions) -> String {
558    let mut writer = JsonWriter::new(options.pretty_print);
559    writer.write_value(value);
560    writer.finish()
561}
562
563struct JsonWriter {
564    output: String,
565    pretty: bool,
566    indent: usize,
567}
568
569impl JsonWriter {
570    fn new(pretty: bool) -> Self {
571        Self {
572            output: String::new(),
573            pretty,
574            indent: 0,
575        }
576    }
577
578    fn finish(self) -> String {
579        self.output
580    }
581
582    fn write_value(&mut self, value: &JsonValue) {
583        match value {
584            JsonValue::Null => self.output.push_str("null"),
585            JsonValue::Bool(true) => self.output.push_str("true"),
586            JsonValue::Bool(false) => self.output.push_str("false"),
587            JsonValue::Number(number) => self.write_number(number),
588            JsonValue::String(text) => {
589                self.output.push('"');
590                self.output.push_str(&escape_json_string(text));
591                self.output.push('"');
592            }
593            JsonValue::Array(items) => self.write_array(items),
594            JsonValue::Object(fields) => self.write_object(fields),
595        }
596    }
597
598    fn write_number(&mut self, number: &JsonNumber) {
599        match number {
600            JsonNumber::Float(f) => {
601                if f.is_nan() || !f.is_finite() {
602                    self.output.push_str("null");
603                } else {
604                    self.output.push_str(&format_number(*f));
605                }
606            }
607            JsonNumber::I64(i) => {
608                let _ = write!(self.output, "{i}");
609            }
610            JsonNumber::U64(u) => {
611                let _ = write!(self.output, "{u}");
612            }
613        }
614    }
615
616    fn write_array(&mut self, items: &[JsonValue]) {
617        if items.is_empty() {
618            self.output.push_str("[]");
619            return;
620        }
621        let inline = if self.pretty {
622            items.iter().all(|item| {
623                matches!(
624                    item,
625                    JsonValue::Null
626                        | JsonValue::Bool(_)
627                        | JsonValue::Number(_)
628                        | JsonValue::String(_)
629                )
630            })
631        } else {
632            false
633        };
634        if inline {
635            self.output.push('[');
636            for (index, item) in items.iter().enumerate() {
637                self.write_value(item);
638                if index + 1 < items.len() {
639                    self.output.push(',');
640                }
641            }
642            self.output.push(']');
643            return;
644        }
645        self.output.push('[');
646        if self.pretty {
647            self.output.push('\n');
648            self.indent += 1;
649        }
650        for (index, item) in items.iter().enumerate() {
651            if self.pretty {
652                self.write_indent();
653            }
654            self.write_value(item);
655            if index + 1 < items.len() {
656                if self.pretty {
657                    self.output.push_str(",\n");
658                } else {
659                    self.output.push(',');
660                }
661            }
662        }
663        if self.pretty {
664            self.output.push('\n');
665            if self.indent > 0 {
666                self.indent -= 1;
667            }
668            self.write_indent();
669        }
670        self.output.push(']');
671    }
672
673    fn write_object(&mut self, fields: &[(String, JsonValue)]) {
674        if fields.is_empty() {
675            self.output.push_str("{}");
676            return;
677        }
678        self.output.push('{');
679        if self.pretty {
680            self.output.push('\n');
681            self.indent += 1;
682        }
683        for (index, (key, value)) in fields.iter().enumerate() {
684            if self.pretty {
685                self.write_indent();
686            }
687            self.output.push('"');
688            self.output.push_str(&escape_json_string(key));
689            self.output.push('"');
690            if self.pretty {
691                self.output.push_str(": ");
692            } else {
693                self.output.push(':');
694            }
695            self.write_value(value);
696            if index + 1 < fields.len() {
697                if self.pretty {
698                    self.output.push_str(",\n");
699                } else {
700                    self.output.push(',');
701                }
702            }
703        }
704        if self.pretty {
705            self.output.push('\n');
706            if self.indent > 0 {
707                self.indent -= 1;
708            }
709            self.write_indent();
710        }
711        self.output.push('}');
712    }
713
714    fn write_indent(&mut self) {
715        if self.pretty {
716            for _ in 0..self.indent {
717                self.output.push_str("    ");
718            }
719        }
720    }
721}
722
723fn escape_json_string(value: &str) -> String {
724    let mut escaped = String::with_capacity(value.len());
725    for ch in value.chars() {
726        match ch {
727            '"' => escaped.push_str("\\\""),
728            '\\' => escaped.push_str("\\\\"),
729            '\u{08}' => escaped.push_str("\\b"),
730            '\u{0C}' => escaped.push_str("\\f"),
731            '\n' => escaped.push_str("\\n"),
732            '\r' => escaped.push_str("\\r"),
733            '\t' => escaped.push_str("\\t"),
734            c if (c as u32) < 0x20 => {
735                let _ = write!(escaped, "\\u{:04X}", c as u32);
736            }
737            _ => escaped.push(ch),
738        }
739    }
740    escaped
741}
742
743fn format_number(value: f64) -> String {
744    if value.fract() == 0.0 {
745        // Display integer-like doubles without decimal point
746        format!("{:.0}", value)
747    } else {
748        format!("{}", value)
749    }
750}
751
752#[cfg(test)]
753pub(crate) mod tests {
754    use super::*;
755    use crate::builtins::common::test_support;
756    use futures::executor::block_on;
757    use runmat_builtins::{
758        CellArray, CharArray, ComplexTensor, LogicalArray, StringArray, StructValue, Tensor,
759    };
760
761    fn as_string(value: Value) -> String {
762        match value {
763            Value::CharArray(ca) => ca.data.iter().collect(),
764            Value::String(s) => s,
765            other => panic!("expected char array, got {:?}", other),
766        }
767    }
768
769    fn error_message(err: crate::RuntimeError) -> String {
770        err.message().to_string()
771    }
772
773    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
774    #[test]
775    fn jsonencode_scalar_double() {
776        let encoded =
777            block_on(jsonencode_builtin(Value::Num(5.0), Vec::new())).expect("jsonencode");
778        assert_eq!(as_string(encoded), "5");
779    }
780
781    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
782    #[test]
783    fn jsonencode_matrix_pretty_print() {
784        let tensor = Tensor::new(vec![1.0, 4.0, 2.0, 5.0, 3.0, 6.0], vec![2, 3]).expect("tensor");
785        let args = vec![Value::from("PrettyPrint"), Value::Bool(true)];
786        let encoded =
787            block_on(jsonencode_builtin(Value::Tensor(tensor), args)).expect("jsonencode");
788        let expected = "[\n    [1,2,3],\n    [4,5,6]\n]";
789        assert_eq!(as_string(encoded), expected);
790    }
791
792    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
793    #[test]
794    fn jsonencode_struct_round_trip() {
795        let mut fields = StructValue::new();
796        fields
797            .fields
798            .insert("name".to_string(), Value::from("RunMat"));
799        fields
800            .fields
801            .insert("year".to_string(), Value::Int(IntValue::I32(2025)));
802        let encoded =
803            block_on(jsonencode_builtin(Value::Struct(fields), Vec::new())).expect("jsonencode");
804        assert_eq!(as_string(encoded), "{\"name\":\"RunMat\",\"year\":2025}");
805    }
806
807    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
808    #[test]
809    fn jsonencode_struct_options_enable_pretty_print() {
810        let tensor = Tensor::new(vec![1.0, 4.0, 2.0, 5.0], vec![2, 2]).expect("tensor");
811        let mut opts = StructValue::new();
812        opts.fields
813            .insert("PrettyPrint".to_string(), Value::Bool(true));
814        let encoded = block_on(jsonencode_builtin(
815            Value::Tensor(tensor),
816            vec![Value::Struct(opts)],
817        ))
818        .expect("jsonencode");
819        let expected = "[\n    [1,2],\n    [4,5]\n]";
820        assert_eq!(as_string(encoded), expected);
821    }
822
823    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
824    #[test]
825    fn jsonencode_options_accept_scalar_tensor_bool() {
826        let tensor_value = Tensor::new(vec![1.0], vec![1, 1]).expect("tensor");
827        let args = vec![Value::from("PrettyPrint"), Value::Tensor(tensor_value)];
828        let encoded = block_on(jsonencode_builtin(Value::Num(42.0), args)).expect("jsonencode");
829        assert_eq!(as_string(encoded), "42");
830    }
831
832    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
833    #[test]
834    fn jsonencode_options_reject_non_scalar_tensor_bool() {
835        let tensor = Tensor::new(vec![1.0, 0.0], vec![1, 2]).expect("tensor");
836        let err = block_on(jsonencode_builtin(
837            Value::Num(1.0),
838            vec![Value::from("PrettyPrint"), Value::Tensor(tensor)],
839        ))
840        .expect_err("expected failure");
841        assert_eq!(error_message(err), OPTION_VALUE_ERROR);
842    }
843
844    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
845    #[test]
846    fn jsonencode_options_accept_scalar_logical_array() {
847        let logical = LogicalArray::new(vec![1], vec![1]).expect("logical");
848        let args = vec![Value::from("PrettyPrint"), Value::LogicalArray(logical)];
849        let encoded = block_on(jsonencode_builtin(Value::Num(7.0), args)).expect("jsonencode");
850        assert_eq!(as_string(encoded), "7");
851    }
852
853    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
854    #[test]
855    fn jsonencode_convert_inf_and_nan_controls_null_output() {
856        let tensor = Tensor::new(vec![1.0, f64::NAN], vec![1, 2]).expect("tensor");
857        let encoded = block_on(jsonencode_builtin(
858            Value::Tensor(tensor.clone()),
859            Vec::new(),
860        ))
861        .expect("jsonencode");
862        assert_eq!(as_string(encoded), "[1,null]");
863
864        let err = block_on(jsonencode_builtin(
865            Value::Tensor(tensor),
866            vec![Value::from("ConvertInfAndNaN"), Value::Bool(false)],
867        ))
868        .expect_err("expected failure");
869        assert_eq!(error_message(err), INF_NAN_ERROR);
870    }
871
872    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
873    #[test]
874    fn jsonencode_cell_array() {
875        let elements = vec![Value::from(1.0), Value::from("two")];
876        let cell = CellArray::new(elements, 1, 2).expect("cell");
877        let encoded =
878            block_on(jsonencode_builtin(Value::Cell(cell), Vec::new())).expect("jsonencode");
879        assert_eq!(as_string(encoded), "[1,\"two\"]");
880    }
881
882    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
883    #[test]
884    fn jsonencode_char_array_zero_rows_is_empty_array() {
885        let chars = CharArray::new(Vec::new(), 0, 3).expect("char array");
886        let encoded =
887            block_on(jsonencode_builtin(Value::CharArray(chars), Vec::new())).expect("jsonencode");
888        assert_eq!(as_string(encoded), "[]");
889    }
890
891    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
892    #[test]
893    fn jsonencode_char_array_empty_strings_per_row() {
894        let chars = CharArray::new(Vec::new(), 2, 0).expect("char array");
895        let encoded =
896            block_on(jsonencode_builtin(Value::CharArray(chars), Vec::new())).expect("jsonencode");
897        let encoded_str = as_string(encoded);
898        assert_eq!(encoded_str, "[\"\",\"\"]");
899    }
900
901    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
902    #[test]
903    fn jsonencode_string_array_matrix() {
904        let sa = StringArray::new(vec!["alpha".to_string(), "beta".to_string()], vec![2, 1])
905            .expect("string array");
906        let encoded =
907            block_on(jsonencode_builtin(Value::StringArray(sa), Vec::new())).expect("jsonencode");
908        assert_eq!(as_string(encoded), "[\"alpha\",\"beta\"]");
909    }
910
911    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
912    #[test]
913    fn jsonencode_complex_tensor_outputs_objects() {
914        let ct = ComplexTensor::new(vec![(1.0, 2.0), (3.5, -4.0)], vec![2, 1]).expect("complex");
915        let encoded =
916            block_on(jsonencode_builtin(Value::ComplexTensor(ct), Vec::new())).expect("jsonencode");
917        assert_eq!(
918            as_string(encoded),
919            "[{\"real\":1,\"imag\":2},{\"real\":3.5,\"imag\":-4}]"
920        );
921    }
922
923    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
924    #[test]
925    fn jsonencode_gpu_tensor_gathers_host_data() {
926        test_support::with_test_provider(|provider| {
927            let tensor = Tensor::new(vec![1.0, 0.0, 0.0, 1.0], vec![2, 2]).expect("tensor");
928            let view = runmat_accelerate_api::HostTensorView {
929                data: &tensor.data,
930                shape: &tensor.shape,
931            };
932            let handle = provider.upload(&view).expect("upload");
933            let encoded = block_on(jsonencode_builtin(Value::GpuTensor(handle), Vec::new()))
934                .expect("jsonencode");
935            assert_eq!(as_string(encoded), "[[1,0],[0,1]]");
936        });
937    }
938
939    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
940    #[test]
941    #[cfg(feature = "wgpu")]
942    fn jsonencode_gpu_tensor_wgpu_gathers_host_data() {
943        let ensure = runmat_accelerate::backend::wgpu::provider::ensure_wgpu_provider();
944        let Some(_) = ensure.ok().flatten() else {
945            // No WGPU device available on this host; skip.
946            return;
947        };
948        let provider = runmat_accelerate_api::provider().expect("wgpu provider");
949        let tensor = Tensor::new(vec![1.0, 2.0, 3.0], vec![3, 1]).expect("tensor");
950        let view = runmat_accelerate_api::HostTensorView {
951            data: &tensor.data,
952            shape: &tensor.shape,
953        };
954        let handle = provider.upload(&view).expect("upload");
955        let encoded =
956            block_on(jsonencode_builtin(Value::GpuTensor(handle), Vec::new())).expect("jsonencode");
957        assert_eq!(as_string(encoded), "[1,2,3]");
958    }
959}