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