1use 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::{gather_if_needed, register_builtin_fusion_spec, register_builtin_gpu_spec};
17
18#[cfg(feature = "doc_export")]
19use crate::register_builtin_doc_text;
20
21const OPTION_NAME_ERROR: &str = "jsonencode: option names must be character vectors or strings";
22const OPTION_VALUE_ERROR: &str = "jsonencode: option value must be scalar logical or numeric";
23const INF_NAN_ERROR: &str = "jsonencode: ConvertInfAndNaN must be true to encode NaN or Inf values";
24const UNSUPPORTED_TYPE_ERROR: &str =
25 "jsonencode: unsupported input type; expected numeric, logical, string, struct, cell, or object data";
26
27#[cfg(feature = "doc_export")]
28#[allow(clippy::too_many_lines)]
29pub const DOC_MD: &str = r#"---
30title: "jsonencode"
31category: "io/json"
32keywords: ["jsonencode", "json", "serialization", "struct to json", "pretty print json", "gpu gather"]
33summary: "Serialize MATLAB values to UTF-8 JSON text with MATLAB-compatible defaults."
34references:
35 - https://www.mathworks.com/help/matlab/ref/jsonencode.html
36gpu_support:
37 elementwise: false
38 reduction: false
39 precisions: []
40 broadcasting: "none"
41 notes: "jsonencode gathers GPU-resident data to host memory before serialisation and executes entirely on the CPU."
42fusion:
43 elementwise: false
44 reduction: false
45 max_inputs: 2
46 constants: "inline"
47requires_feature: null
48tested:
49 unit: "builtins::io::json::jsonencode::tests"
50 integration:
51 - "builtins::io::json::jsonencode::tests::jsonencode_struct_round_trip"
52 - "builtins::io::json::jsonencode::tests::jsonencode_gpu_tensor_gathers_host_data"
53 - "builtins::io::json::jsonencode::tests::jsonencode_struct_options_enable_pretty_print"
54 - "builtins::io::json::jsonencode::tests::jsonencode_convert_inf_and_nan_controls_null_output"
55 - "builtins::io::json::jsonencode::tests::jsonencode_char_array_zero_rows_is_empty_array"
56 - "builtins::io::json::jsonencode::tests::jsonencode_gpu_tensor_wgpu_gathers_host_data"
57---
58
59# What does the `jsonencode` function do in MATLAB / RunMat?
60`jsonencode` converts MATLAB values into UTF-8 JSON text. The builtin mirrors MATLAB defaults:
61scalars become numbers or strings, matrices turn into JSON arrays, structs map to JSON objects,
62and `NaN`/`Inf` values encode as `null` unless you disable the conversion.
63
64## How does the `jsonencode` function behave in MATLAB / RunMat?
65- Returns a 1×N character array containing UTF-8 encoded JSON text.
66- Numeric and logical arrays become JSON arrays, preserving MATLAB column-major ordering.
67- Scalars encode as bare numbers/strings rather than single-element arrays.
68- Struct scalars become JSON objects; struct arrays become JSON arrays of objects.
69- Cell arrays map to JSON arrays, with nested arrays when the cell is 2-D.
70- String arrays and char arrays become JSON strings (1 element) or arrays of strings (multiple rows).
71- By default, `NaN`, `Inf`, and `-Inf` values encode as `null`. Set `'ConvertInfAndNaN'` to `false`
72 to raise an error instead.
73- Pretty printing is disabled by default; enable it with the `'PrettyPrint'` option.
74- Inputs that reside on the GPU are gathered back to host memory automatically.
75
76## jsonencode Options
77| Name | Type | Default | Description |
78| ---- | ---- | ------- | ----------- |
79| `PrettyPrint` | logical | `false` | Emit indented, multi-line JSON output. |
80| `ConvertInfAndNaN` | logical | `true` | Convert `NaN`, `Inf`, `-Inf` to `null`. Set `false` to raise an error when these values are encountered. |
81
82## Examples of using the `jsonencode` function in MATLAB / RunMat
83
84### Converting a MATLAB struct to JSON
85```matlab
86person = struct('name', 'Ada', 'age', 37);
87encoded = jsonencode(person);
88```
89Expected output:
90```matlab
91encoded = '{"age":37,"name":"Ada"}'
92```
93
94### Serialising a matrix with pretty printing
95```matlab
96A = magic(3);
97encoded = jsonencode(A, 'PrettyPrint', true);
98```
99Expected output:
100```matlab
101encoded =
102'[
103 [8,1,6],
104 [3,5,7],
105 [4,9,2]
106]'
107```
108
109### Encoding nested cell arrays
110```matlab
111C = {struct('task','encode','ok',true), {'nested', 42}};
112encoded = jsonencode(C);
113```
114Expected output:
115```matlab
116encoded = '[{"ok":true,"task":"encode"},["nested",42]]'
117```
118
119### Handling NaN and Inf values
120```matlab
121data = [1 NaN Inf];
122encoded = jsonencode(data);
123```
124Expected output:
125```matlab
126encoded = '[1,null,null]'
127```
128
129### Rejecting NaN when ConvertInfAndNaN is false
130```matlab
131try
132 jsonencode(data, 'ConvertInfAndNaN', false);
133catch err
134 disp(err.message);
135end
136```
137Expected output:
138```matlab
139jsonencode: ConvertInfAndNaN must be true to encode NaN or Inf values
140```
141
142### Serialising GPU-resident tensors
143```matlab
144G = gpuArray(eye(2));
145encoded = jsonencode(G);
146```
147Expected output:
148```matlab
149encoded = '[[1,0],[0,1]]'
150```
151
152## `jsonencode` Function GPU Execution Behaviour
153`jsonencode` never launches GPU kernels. When the input contains `gpuArray` data, RunMat gathers
154those values back to host memory via the active acceleration provider and then serialises on the CPU.
155If no provider is registered, the builtin propagates the same gather error used by other residency
156sinks (`gather: no acceleration provider registered`).
157
158## GPU residency in RunMat (Do I need `gpuArray`?)
159For most workflows you do not need to call `gpuArray` explicitly before using `jsonencode`. The
160auto-offload planner and fusion system keep track of residency, so any GPU-backed tensors that flow
161into `jsonencode` are gathered automatically as part of this sink operation. If you prefer to control
162residency manually—or need MATLAB parity—you can still wrap data with `gpuArray` and call `gather`
163explicitly before serialising.
164
165## FAQ
166
167### What MATLAB types does `jsonencode` support?
168Numeric, logical, string, char, struct, cell, and table-like structs are supported. Unsupported types
169such as function handles or opaque objects raise an error.
170
171### Why are my field names sorted alphabetically?
172RunMat sorts struct field names to produce deterministic JSON (matching MATLAB when fields are stored
173as scalar structs).
174
175### How are complex numbers encoded?
176Complex scalars become objects with `real` and `imag` fields. Complex arrays become arrays of those
177objects, mirroring MATLAB.
178
179### Does `jsonencode` return a character array or string?
180It returns a row character array (`char`) for MATLAB compatibility. Use `string(jsonencode(x))` if
181you prefer string scalars.
182
183### Can I pretty-print nested structures?
184Yes. Pass `'PrettyPrint', true` to `jsonencode`. Indentation uses four spaces per nesting level, just
185like MATLAB's pretty-print mode.
186
187### How are empty arrays encoded?
188Empty numeric, logical, char, and string arrays become `[]`. Empty structs become `{}` if scalar, or
189`[]` if they are empty arrays of structs.
190
191### Does `jsonencode` preserve MATLAB column-major ordering?
192Yes. Arrays are emitted in MATLAB's logical row/column order, so reshaping on decode reproduces the
193original layout.
194
195### What happens when ConvertInfAndNaN is false?
196Encountering `NaN`, `Inf`, or `-Inf` raises `jsonencode: ConvertInfAndNaN must be true to encode NaN or Inf values`.
197
198### How do I control the newline style?
199`jsonencode` always emits `\n` (LF) line endings when `PrettyPrint` is enabled, regardless of platform,
200matching MATLAB's behaviour.
201
202### Are Unicode characters escaped?
203Printable Unicode characters are emitted verbatim. Control characters and quotes are escaped using
204standard JSON escape sequences.
205
206## See Also
207[jsondecode](./jsondecode), [gpuArray](../../acceleration/gpu/gpuArray), [gather](../../acceleration/gpu/gather)
208
209## Source & Feedback
210- The full source code for the implementation of the `jsonencode` function is available at: [`crates/runmat-runtime/src/builtins/io/json/jsonencode.rs`](https://github.com/runmat-org/runmat/blob/main/crates/runmat-runtime/src/builtins/io/json/jsonencode.rs)
211- Found a bug or behavioural difference? Please [open an issue](https://github.com/runmat-org/runmat/issues/new/choose) with details and a minimal repro.
212"#;
213
214pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
215 name: "jsonencode",
216 op_kind: GpuOpKind::Custom("serialization"),
217 supported_precisions: &[],
218 broadcast: BroadcastSemantics::None,
219 provider_hooks: &[],
220 constant_strategy: ConstantStrategy::InlineLiteral,
221 residency: ResidencyPolicy::GatherImmediately,
222 nan_mode: ReductionNaN::Include,
223 two_pass_threshold: None,
224 workgroup_size: None,
225 accepts_nan_mode: false,
226 notes:
227 "Serialization sink that gathers GPU data to host memory before emitting UTF-8 JSON text.",
228};
229
230pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
231 name: "jsonencode",
232 shape: ShapeRequirements::Any,
233 constant_strategy: ConstantStrategy::InlineLiteral,
234 elementwise: None,
235 reduction: None,
236 emits_nan: false,
237 notes: "jsonencode is a residency sink and never participates in fusion planning.",
238};
239
240register_builtin_gpu_spec!(GPU_SPEC);
241register_builtin_fusion_spec!(FUSION_SPEC);
242
243#[cfg(feature = "doc_export")]
244register_builtin_doc_text!("jsonencode", DOC_MD);
245
246#[derive(Debug, Clone)]
247struct JsonEncodeOptions {
248 pretty_print: bool,
249 convert_inf_and_nan: bool,
250}
251
252impl Default for JsonEncodeOptions {
253 fn default() -> Self {
254 Self {
255 pretty_print: false,
256 convert_inf_and_nan: true,
257 }
258 }
259}
260
261#[derive(Debug, Clone)]
262enum JsonValue {
263 Null,
264 Bool(bool),
265 Number(JsonNumber),
266 String(String),
267 Array(Vec<JsonValue>),
268 Object(Vec<(String, JsonValue)>),
269}
270
271#[derive(Debug, Clone)]
272enum JsonNumber {
273 Float(f64),
274 I64(i64),
275 U64(u64),
276}
277
278#[runtime_builtin(
279 name = "jsonencode",
280 category = "io/json",
281 summary = "Serialize MATLAB values to UTF-8 JSON text.",
282 keywords = "jsonencode,json,serialization,struct,gpu",
283 accel = "cpu"
284)]
285fn jsonencode_builtin(value: Value, rest: Vec<Value>) -> Result<Value, String> {
286 let host_value = gather_if_needed(&value)?;
287 let gathered_args: Vec<Value> = rest
288 .iter()
289 .map(gather_if_needed)
290 .collect::<Result<_, _>>()?;
291
292 let options = parse_options(&gathered_args)?;
293 let json_value = value_to_json(&host_value, &options)?;
294 let json_string = render_json(&json_value, &options);
295
296 Ok(Value::CharArray(CharArray::new_row(&json_string)))
297}
298
299fn parse_options(args: &[Value]) -> Result<JsonEncodeOptions, String> {
300 let mut options = JsonEncodeOptions::default();
301 if args.is_empty() {
302 return Ok(options);
303 }
304
305 if args.len() == 1 {
306 if let Value::Struct(struct_value) = &args[0] {
307 apply_struct_options(struct_value, &mut options)?;
308 return Ok(options);
309 }
310 return Err("jsonencode: expected name/value pairs or options struct".to_string());
311 }
312
313 if !args.len().is_multiple_of(2) {
314 return Err("jsonencode: name/value pairs must come in pairs".to_string());
315 }
316
317 let mut idx = 0usize;
318 while idx < args.len() {
319 let name = option_name(&args[idx])?;
320 let value = &args[idx + 1];
321 apply_option(&name, value, &mut options)?;
322 idx += 2;
323 }
324
325 Ok(options)
326}
327
328fn apply_struct_options(
329 struct_value: &StructValue,
330 options: &mut JsonEncodeOptions,
331) -> Result<(), String> {
332 for (key, value) in &struct_value.fields {
333 apply_option(key, value, options)?;
334 }
335 Ok(())
336}
337
338fn option_name(value: &Value) -> Result<String, String> {
339 match value {
340 Value::String(s) => Ok(s.clone()),
341 Value::CharArray(ca) if ca.rows == 1 => Ok(ca.data.iter().collect()),
342 Value::StringArray(sa) if sa.data.len() == 1 => Ok(sa.data[0].clone()),
343 _ => Err(OPTION_NAME_ERROR.to_string()),
344 }
345}
346
347fn apply_option(
348 raw_name: &str,
349 value: &Value,
350 options: &mut JsonEncodeOptions,
351) -> Result<(), String> {
352 let lowered = raw_name.to_ascii_lowercase();
353 match lowered.as_str() {
354 "prettyprint" => {
355 options.pretty_print = coerce_bool(value)?;
356 Ok(())
357 }
358 "convertinfandnan" => {
359 options.convert_inf_and_nan = coerce_bool(value)?;
360 Ok(())
361 }
362 other => Err(format!("jsonencode: unknown option '{}'", other)),
363 }
364}
365
366fn coerce_bool(value: &Value) -> Result<bool, String> {
367 match value {
368 Value::Bool(b) => Ok(*b),
369 Value::Int(i) => Ok(i.to_i64() != 0),
370 Value::Num(n) => bool_from_f64(*n),
371 Value::Tensor(t) => {
372 if t.data.len() == 1 {
373 bool_from_f64(t.data[0])
374 } else {
375 Err(OPTION_VALUE_ERROR.to_string())
376 }
377 }
378 Value::LogicalArray(la) => match la.data.len() {
379 1 => Ok(la.data[0] != 0),
380 _ => Err(OPTION_VALUE_ERROR.to_string()),
381 },
382 Value::CharArray(ca) if ca.rows == 1 => {
383 parse_bool_string(&ca.data.iter().collect::<String>())
384 }
385 Value::String(s) => parse_bool_string(s),
386 Value::StringArray(sa) if sa.data.len() == 1 => parse_bool_string(&sa.data[0]),
387 _ => Err(OPTION_VALUE_ERROR.to_string()),
388 }
389}
390
391fn bool_from_f64(value: f64) -> Result<bool, String> {
392 if value.is_finite() {
393 Ok(value != 0.0)
394 } else {
395 Err(OPTION_VALUE_ERROR.to_string())
396 }
397}
398
399fn parse_bool_string(text: &str) -> Result<bool, String> {
400 match text.trim().to_ascii_lowercase().as_str() {
401 "true" | "on" | "yes" | "1" => Ok(true),
402 "false" | "off" | "no" | "0" => Ok(false),
403 _ => Err(OPTION_VALUE_ERROR.to_string()),
404 }
405}
406
407fn value_to_json(value: &Value, options: &JsonEncodeOptions) -> Result<JsonValue, String> {
408 match value {
409 Value::Num(n) => number_to_json(*n, options),
410 Value::Int(i) => Ok(JsonValue::Number(int_to_number(i))),
411 Value::Bool(b) => Ok(JsonValue::Bool(*b)),
412 Value::LogicalArray(logical) => logical_array_to_json(logical, options),
413 Value::Tensor(tensor) => tensor_to_json(tensor, options),
414 Value::Complex(re, im) => complex_scalar_to_json(*re, *im, options),
415 Value::ComplexTensor(ct) => complex_tensor_to_json(ct, options),
416 Value::String(s) => Ok(JsonValue::String(s.clone())),
417 Value::StringArray(sa) => string_array_to_json(sa, options),
418 Value::CharArray(ca) => char_array_to_json(ca, options),
419 Value::Struct(sv) => struct_to_json(sv, options),
420 Value::Cell(ca) => cell_array_to_json(ca, options),
421 Value::Object(obj) => object_to_json(obj, options),
422 Value::GpuTensor(_) => {
423 Err("jsonencode: unexpected gpuArray handle after gather pass".to_string())
424 }
425 Value::HandleObject(_)
426 | Value::Listener(_)
427 | Value::FunctionHandle(_)
428 | Value::Closure(_)
429 | Value::ClassRef(_)
430 | Value::MException(_) => Err(UNSUPPORTED_TYPE_ERROR.to_string()),
431 }
432}
433
434fn int_to_number(value: &IntValue) -> JsonNumber {
435 match value {
436 IntValue::I8(v) => JsonNumber::I64(*v as i64),
437 IntValue::I16(v) => JsonNumber::I64(*v as i64),
438 IntValue::I32(v) => JsonNumber::I64(*v as i64),
439 IntValue::I64(v) => JsonNumber::I64(*v),
440 IntValue::U8(v) => JsonNumber::U64(*v as u64),
441 IntValue::U16(v) => JsonNumber::U64(*v as u64),
442 IntValue::U32(v) => JsonNumber::U64(*v as u64),
443 IntValue::U64(v) => JsonNumber::U64(*v),
444 }
445}
446
447fn number_to_json(value: f64, options: &JsonEncodeOptions) -> Result<JsonValue, String> {
448 if !value.is_finite() {
449 if options.convert_inf_and_nan {
450 return Ok(JsonValue::Null);
451 }
452 return Err(INF_NAN_ERROR.to_string());
453 }
454 Ok(JsonValue::Number(JsonNumber::Float(value)))
455}
456
457fn logical_array_to_json(
458 logical: &LogicalArray,
459 _options: &JsonEncodeOptions,
460) -> Result<JsonValue, String> {
461 let keep_dims = compute_keep_dims(&logical.shape, true);
462 if logical.shape.is_empty() || logical.data.is_empty() {
463 return Ok(JsonValue::Array(Vec::new()));
464 }
465 if keep_dims.is_empty() {
466 let first = logical.data.first().copied().unwrap_or(0) != 0;
467 return Ok(JsonValue::Bool(first));
468 }
469 build_strided_array(&logical.shape, &keep_dims, |offset| {
470 Ok(JsonValue::Bool(logical.data[offset] != 0))
471 })
472}
473
474fn tensor_to_json(tensor: &Tensor, options: &JsonEncodeOptions) -> Result<JsonValue, String> {
475 if tensor.data.is_empty() {
476 return Ok(JsonValue::Array(Vec::new()));
477 }
478 let keep_dims = compute_keep_dims(&tensor.shape, true);
479 if keep_dims.is_empty() {
480 return number_to_json(tensor.data[0], options);
481 }
482 build_strided_array(&tensor.shape, &keep_dims, |offset| {
483 number_to_json(tensor.data[offset], options)
484 })
485}
486
487fn complex_scalar_to_json(
488 real: f64,
489 imag: f64,
490 options: &JsonEncodeOptions,
491) -> Result<JsonValue, String> {
492 let real_json = number_to_json(real, options)?;
493 let imag_json = number_to_json(imag, options)?;
494 Ok(JsonValue::Object(vec![
495 ("real".to_string(), real_json),
496 ("imag".to_string(), imag_json),
497 ]))
498}
499
500fn complex_tensor_to_json(
501 ct: &ComplexTensor,
502 options: &JsonEncodeOptions,
503) -> Result<JsonValue, String> {
504 if ct.data.is_empty() {
505 return Ok(JsonValue::Array(Vec::new()));
506 }
507 let keep_dims = compute_keep_dims(&ct.shape, true);
508 if keep_dims.is_empty() {
509 let (re, im) = ct.data[0];
510 return complex_scalar_to_json(re, im, options);
511 }
512 build_strided_array(&ct.shape, &keep_dims, |offset| {
513 let (re, im) = ct.data[offset];
514 complex_scalar_to_json(re, im, options)
515 })
516}
517
518fn string_array_to_json(
519 sa: &StringArray,
520 _options: &JsonEncodeOptions,
521) -> Result<JsonValue, String> {
522 if sa.data.is_empty() {
523 return Ok(JsonValue::Array(Vec::new()));
524 }
525 let keep_dims = compute_keep_dims(&sa.shape, true);
526 if keep_dims.is_empty() {
527 return Ok(JsonValue::String(sa.data[0].clone()));
528 }
529 build_strided_array(&sa.shape, &keep_dims, |offset| {
530 Ok(JsonValue::String(sa.data[offset].clone()))
531 })
532}
533
534fn char_array_to_json(ca: &CharArray, _options: &JsonEncodeOptions) -> Result<JsonValue, String> {
535 if ca.rows == 0 {
536 return Ok(JsonValue::Array(Vec::new()));
537 }
538
539 if ca.cols == 0 {
540 if ca.rows == 1 {
541 return Ok(JsonValue::String(String::new()));
542 }
543 let mut rows = Vec::with_capacity(ca.rows);
544 for _ in 0..ca.rows {
545 rows.push(JsonValue::String(String::new()));
546 }
547 return Ok(JsonValue::Array(rows));
548 }
549
550 if ca.rows == 1 {
551 return Ok(JsonValue::String(ca.data.iter().collect()));
552 }
553
554 let mut rows = Vec::with_capacity(ca.rows);
555 for r in 0..ca.rows {
556 let mut row_string = String::with_capacity(ca.cols);
557 for c in 0..ca.cols {
558 row_string.push(ca.data[r * ca.cols + c]);
559 }
560 rows.push(JsonValue::String(row_string));
561 }
562 Ok(JsonValue::Array(rows))
563}
564
565fn struct_to_json(sv: &StructValue, options: &JsonEncodeOptions) -> Result<JsonValue, String> {
566 if sv.fields.is_empty() {
567 return Ok(JsonValue::Object(Vec::new()));
568 }
569 let mut map = BTreeMap::new();
570 for (key, value) in &sv.fields {
571 map.insert(key.clone(), value_to_json(value, options)?);
572 }
573 Ok(JsonValue::Object(map.into_iter().collect()))
574}
575
576fn object_to_json(obj: &ObjectInstance, options: &JsonEncodeOptions) -> Result<JsonValue, String> {
577 let mut map = BTreeMap::new();
578 for (key, value) in &obj.properties {
579 map.insert(key.clone(), value_to_json(value, options)?);
580 }
581 Ok(JsonValue::Object(map.into_iter().collect()))
582}
583
584fn cell_array_to_json(ca: &CellArray, options: &JsonEncodeOptions) -> Result<JsonValue, String> {
585 if ca.rows == 0 || ca.cols == 0 {
586 return Ok(JsonValue::Array(Vec::new()));
587 }
588
589 if ca.rows == 1 && ca.cols == 1 {
590 let value = ca.get(0, 0).map_err(|e| format!("jsonencode: {e}"))?;
591 return Ok(JsonValue::Array(vec![value_to_json(&value, options)?]));
592 }
593
594 if ca.rows == 1 {
595 let mut row = Vec::with_capacity(ca.cols);
596 for c in 0..ca.cols {
597 let element = ca.get(0, c).map_err(|e| format!("jsonencode: {e}"))?;
598 row.push(value_to_json(&element, options)?);
599 }
600 return Ok(JsonValue::Array(row));
601 }
602
603 if ca.cols == 1 {
604 let mut column = Vec::with_capacity(ca.rows);
605 for r in 0..ca.rows {
606 let element = ca.get(r, 0).map_err(|e| format!("jsonencode: {e}"))?;
607 column.push(value_to_json(&element, options)?);
608 }
609 return Ok(JsonValue::Array(column));
610 }
611
612 let mut rows = Vec::with_capacity(ca.rows);
613 for r in 0..ca.rows {
614 let mut row = Vec::with_capacity(ca.cols);
615 for c in 0..ca.cols {
616 let element = ca.get(r, c).map_err(|e| format!("jsonencode: {e}"))?;
617 row.push(value_to_json(&element, options)?);
618 }
619 rows.push(JsonValue::Array(row));
620 }
621 Ok(JsonValue::Array(rows))
622}
623
624fn compute_keep_dims(shape: &[usize], drop_singletons: bool) -> Vec<usize> {
625 let mut keep = Vec::new();
626 for (idx, &size) in shape.iter().enumerate() {
627 if size != 1 || !drop_singletons {
628 keep.push(idx);
629 }
630 }
631 keep
632}
633
634fn compute_strides(shape: &[usize]) -> Vec<usize> {
635 let mut strides = Vec::with_capacity(shape.len());
636 let mut acc = 1usize;
637 for &size in shape {
638 strides.push(acc);
639 acc = acc.saturating_mul(size.max(1));
640 }
641 strides
642}
643
644fn build_strided_array<F>(
645 shape: &[usize],
646 keep_dims: &[usize],
647 mut fetch: F,
648) -> Result<JsonValue, String>
649where
650 F: FnMut(usize) -> Result<JsonValue, String>,
651{
652 if keep_dims.is_empty() {
653 return fetch(0);
654 }
655 if keep_dims.iter().any(|&idx| shape[idx] == 0) {
656 return Ok(JsonValue::Array(Vec::new()));
657 }
658 let strides = compute_strides(shape);
659 let dims: Vec<usize> = keep_dims.iter().map(|&idx| shape[idx]).collect();
660 build_nd_array(&dims, |indices| {
661 let mut offset = 0usize;
662 for (value, dim_idx) in indices.iter().zip(keep_dims.iter()) {
663 offset += value * strides[*dim_idx];
664 }
665 fetch(offset)
666 })
667}
668
669fn build_nd_array<F>(dims: &[usize], mut fetch: F) -> Result<JsonValue, String>
670where
671 F: FnMut(&[usize]) -> Result<JsonValue, String>,
672{
673 if dims.is_empty() {
674 return fetch(&[]);
675 }
676 if dims[0] == 0 {
677 return Ok(JsonValue::Array(Vec::new()));
678 }
679 let mut indices = vec![0usize; dims.len()];
680 build_nd_array_recursive(dims, 0, &mut indices, &mut fetch)
681}
682
683fn build_nd_array_recursive<F>(
684 dims: &[usize],
685 level: usize,
686 indices: &mut [usize],
687 fetch: &mut F,
688) -> Result<JsonValue, String>
689where
690 F: FnMut(&[usize]) -> Result<JsonValue, String>,
691{
692 let size = dims[level];
693 if size == 0 {
694 return Ok(JsonValue::Array(Vec::new()));
695 }
696 if level + 1 == dims.len() {
697 let mut items = Vec::with_capacity(size);
698 for i in 0..size {
699 indices[level] = i;
700 items.push(fetch(indices)?);
701 }
702 return Ok(JsonValue::Array(items));
703 }
704 let mut items = Vec::with_capacity(size);
705 for i in 0..size {
706 indices[level] = i;
707 items.push(build_nd_array_recursive(dims, level + 1, indices, fetch)?);
708 }
709 Ok(JsonValue::Array(items))
710}
711
712fn render_json(value: &JsonValue, options: &JsonEncodeOptions) -> String {
713 let mut writer = JsonWriter::new(options.pretty_print);
714 writer.write_value(value);
715 writer.finish()
716}
717
718struct JsonWriter {
719 output: String,
720 pretty: bool,
721 indent: usize,
722}
723
724impl JsonWriter {
725 fn new(pretty: bool) -> Self {
726 Self {
727 output: String::new(),
728 pretty,
729 indent: 0,
730 }
731 }
732
733 fn finish(self) -> String {
734 self.output
735 }
736
737 fn write_value(&mut self, value: &JsonValue) {
738 match value {
739 JsonValue::Null => self.output.push_str("null"),
740 JsonValue::Bool(true) => self.output.push_str("true"),
741 JsonValue::Bool(false) => self.output.push_str("false"),
742 JsonValue::Number(number) => self.write_number(number),
743 JsonValue::String(text) => {
744 self.output.push('"');
745 self.output.push_str(&escape_json_string(text));
746 self.output.push('"');
747 }
748 JsonValue::Array(items) => self.write_array(items),
749 JsonValue::Object(fields) => self.write_object(fields),
750 }
751 }
752
753 fn write_number(&mut self, number: &JsonNumber) {
754 match number {
755 JsonNumber::Float(f) => {
756 if f.is_nan() || !f.is_finite() {
757 self.output.push_str("null");
758 } else {
759 self.output.push_str(&format_number(*f));
760 }
761 }
762 JsonNumber::I64(i) => {
763 let _ = write!(self.output, "{i}");
764 }
765 JsonNumber::U64(u) => {
766 let _ = write!(self.output, "{u}");
767 }
768 }
769 }
770
771 fn write_array(&mut self, items: &[JsonValue]) {
772 if items.is_empty() {
773 self.output.push_str("[]");
774 return;
775 }
776 let inline = if self.pretty {
777 items.iter().all(|item| {
778 matches!(
779 item,
780 JsonValue::Null
781 | JsonValue::Bool(_)
782 | JsonValue::Number(_)
783 | JsonValue::String(_)
784 )
785 })
786 } else {
787 false
788 };
789 if inline {
790 self.output.push('[');
791 for (index, item) in items.iter().enumerate() {
792 self.write_value(item);
793 if index + 1 < items.len() {
794 self.output.push(',');
795 }
796 }
797 self.output.push(']');
798 return;
799 }
800 self.output.push('[');
801 if self.pretty {
802 self.output.push('\n');
803 self.indent += 1;
804 }
805 for (index, item) in items.iter().enumerate() {
806 if self.pretty {
807 self.write_indent();
808 }
809 self.write_value(item);
810 if index + 1 < items.len() {
811 if self.pretty {
812 self.output.push_str(",\n");
813 } else {
814 self.output.push(',');
815 }
816 }
817 }
818 if self.pretty {
819 self.output.push('\n');
820 if self.indent > 0 {
821 self.indent -= 1;
822 }
823 self.write_indent();
824 }
825 self.output.push(']');
826 }
827
828 fn write_object(&mut self, fields: &[(String, JsonValue)]) {
829 if fields.is_empty() {
830 self.output.push_str("{}");
831 return;
832 }
833 self.output.push('{');
834 if self.pretty {
835 self.output.push('\n');
836 self.indent += 1;
837 }
838 for (index, (key, value)) in fields.iter().enumerate() {
839 if self.pretty {
840 self.write_indent();
841 }
842 self.output.push('"');
843 self.output.push_str(&escape_json_string(key));
844 self.output.push('"');
845 if self.pretty {
846 self.output.push_str(": ");
847 } else {
848 self.output.push(':');
849 }
850 self.write_value(value);
851 if index + 1 < fields.len() {
852 if self.pretty {
853 self.output.push_str(",\n");
854 } else {
855 self.output.push(',');
856 }
857 }
858 }
859 if self.pretty {
860 self.output.push('\n');
861 if self.indent > 0 {
862 self.indent -= 1;
863 }
864 self.write_indent();
865 }
866 self.output.push('}');
867 }
868
869 fn write_indent(&mut self) {
870 if self.pretty {
871 for _ in 0..self.indent {
872 self.output.push_str(" ");
873 }
874 }
875 }
876}
877
878fn escape_json_string(value: &str) -> String {
879 let mut escaped = String::with_capacity(value.len());
880 for ch in value.chars() {
881 match ch {
882 '"' => escaped.push_str("\\\""),
883 '\\' => escaped.push_str("\\\\"),
884 '\u{08}' => escaped.push_str("\\b"),
885 '\u{0C}' => escaped.push_str("\\f"),
886 '\n' => escaped.push_str("\\n"),
887 '\r' => escaped.push_str("\\r"),
888 '\t' => escaped.push_str("\\t"),
889 c if (c as u32) < 0x20 => {
890 let _ = write!(escaped, "\\u{:04X}", c as u32);
891 }
892 _ => escaped.push(ch),
893 }
894 }
895 escaped
896}
897
898fn format_number(value: f64) -> String {
899 if value.fract() == 0.0 {
900 format!("{:.0}", value)
902 } else {
903 format!("{}", value)
904 }
905}
906
907#[cfg(test)]
908mod tests {
909 use super::*;
910 use crate::builtins::common::test_support;
911 use runmat_builtins::{
912 CellArray, CharArray, ComplexTensor, LogicalArray, StringArray, StructValue, Tensor,
913 };
914
915 fn as_string(value: Value) -> String {
916 match value {
917 Value::CharArray(ca) => ca.data.iter().collect(),
918 Value::String(s) => s,
919 other => panic!("expected char array, got {:?}", other),
920 }
921 }
922
923 #[test]
924 fn jsonencode_scalar_double() {
925 let encoded = jsonencode_builtin(Value::Num(5.0), Vec::new()).expect("jsonencode");
926 assert_eq!(as_string(encoded), "5");
927 }
928
929 #[test]
930 fn jsonencode_matrix_pretty_print() {
931 let tensor = Tensor::new(vec![1.0, 4.0, 2.0, 5.0, 3.0, 6.0], vec![2, 3]).expect("tensor");
932 let args = vec![Value::from("PrettyPrint"), Value::Bool(true)];
933 let encoded = jsonencode_builtin(Value::Tensor(tensor), args).expect("jsonencode");
934 let expected = "[\n [1,2,3],\n [4,5,6]\n]";
935 assert_eq!(as_string(encoded), expected);
936 }
937
938 #[test]
939 fn jsonencode_struct_round_trip() {
940 let mut fields = StructValue::new();
941 fields
942 .fields
943 .insert("name".to_string(), Value::from("RunMat"));
944 fields
945 .fields
946 .insert("year".to_string(), Value::Int(IntValue::I32(2025)));
947 let encoded = jsonencode_builtin(Value::Struct(fields), Vec::new()).expect("jsonencode");
948 assert_eq!(as_string(encoded), "{\"name\":\"RunMat\",\"year\":2025}");
949 }
950
951 #[test]
952 fn jsonencode_struct_options_enable_pretty_print() {
953 let tensor = Tensor::new(vec![1.0, 4.0, 2.0, 5.0], vec![2, 2]).expect("tensor");
954 let mut opts = StructValue::new();
955 opts.fields
956 .insert("PrettyPrint".to_string(), Value::Bool(true));
957 let encoded = jsonencode_builtin(Value::Tensor(tensor), vec![Value::Struct(opts)])
958 .expect("jsonencode");
959 let expected = "[\n [1,2],\n [4,5]\n]";
960 assert_eq!(as_string(encoded), expected);
961 }
962
963 #[test]
964 fn jsonencode_options_accept_scalar_tensor_bool() {
965 let tensor_value = Tensor::new(vec![1.0], vec![1, 1]).expect("tensor");
966 let args = vec![Value::from("PrettyPrint"), Value::Tensor(tensor_value)];
967 let encoded = jsonencode_builtin(Value::Num(42.0), args).expect("jsonencode");
968 assert_eq!(as_string(encoded), "42");
969 }
970
971 #[test]
972 fn jsonencode_options_reject_non_scalar_tensor_bool() {
973 let tensor = Tensor::new(vec![1.0, 0.0], vec![1, 2]).expect("tensor");
974 let err = jsonencode_builtin(
975 Value::Num(1.0),
976 vec![Value::from("PrettyPrint"), Value::Tensor(tensor)],
977 )
978 .expect_err("expected failure");
979 assert_eq!(err, OPTION_VALUE_ERROR);
980 }
981
982 #[test]
983 fn jsonencode_options_accept_scalar_logical_array() {
984 let logical = LogicalArray::new(vec![1], vec![1]).expect("logical");
985 let args = vec![Value::from("PrettyPrint"), Value::LogicalArray(logical)];
986 let encoded = jsonencode_builtin(Value::Num(7.0), args).expect("jsonencode");
987 assert_eq!(as_string(encoded), "7");
988 }
989
990 #[test]
991 fn jsonencode_convert_inf_and_nan_controls_null_output() {
992 let tensor = Tensor::new(vec![1.0, f64::NAN], vec![1, 2]).expect("tensor");
993 let encoded =
994 jsonencode_builtin(Value::Tensor(tensor.clone()), Vec::new()).expect("jsonencode");
995 assert_eq!(as_string(encoded), "[1,null]");
996
997 let err = jsonencode_builtin(
998 Value::Tensor(tensor),
999 vec![Value::from("ConvertInfAndNaN"), Value::Bool(false)],
1000 )
1001 .expect_err("expected failure");
1002 assert_eq!(err, INF_NAN_ERROR);
1003 }
1004
1005 #[test]
1006 fn jsonencode_cell_array() {
1007 let elements = vec![Value::from(1.0), Value::from("two")];
1008 let cell = CellArray::new(elements, 1, 2).expect("cell");
1009 let encoded = jsonencode_builtin(Value::Cell(cell), Vec::new()).expect("jsonencode");
1010 assert_eq!(as_string(encoded), "[1,\"two\"]");
1011 }
1012
1013 #[test]
1014 fn jsonencode_char_array_zero_rows_is_empty_array() {
1015 let chars = CharArray::new(Vec::new(), 0, 3).expect("char array");
1016 let encoded = jsonencode_builtin(Value::CharArray(chars), Vec::new()).expect("jsonencode");
1017 assert_eq!(as_string(encoded), "[]");
1018 }
1019
1020 #[test]
1021 fn jsonencode_char_array_empty_strings_per_row() {
1022 let chars = CharArray::new(Vec::new(), 2, 0).expect("char array");
1023 let encoded = jsonencode_builtin(Value::CharArray(chars), Vec::new()).expect("jsonencode");
1024 let encoded_str = as_string(encoded);
1025 assert_eq!(encoded_str, "[\"\",\"\"]");
1026 }
1027
1028 #[test]
1029 fn jsonencode_string_array_matrix() {
1030 let sa = StringArray::new(vec!["alpha".to_string(), "beta".to_string()], vec![2, 1])
1031 .expect("string array");
1032 let encoded = jsonencode_builtin(Value::StringArray(sa), Vec::new()).expect("jsonencode");
1033 assert_eq!(as_string(encoded), "[\"alpha\",\"beta\"]");
1034 }
1035
1036 #[test]
1037 fn jsonencode_complex_tensor_outputs_objects() {
1038 let ct = ComplexTensor::new(vec![(1.0, 2.0), (3.5, -4.0)], vec![2, 1]).expect("complex");
1039 let encoded = jsonencode_builtin(Value::ComplexTensor(ct), Vec::new()).expect("jsonencode");
1040 assert_eq!(
1041 as_string(encoded),
1042 "[{\"real\":1,\"imag\":2},{\"real\":3.5,\"imag\":-4}]"
1043 );
1044 }
1045
1046 #[test]
1047 fn jsonencode_gpu_tensor_gathers_host_data() {
1048 test_support::with_test_provider(|provider| {
1049 let tensor = Tensor::new(vec![1.0, 0.0, 0.0, 1.0], vec![2, 2]).expect("tensor");
1050 let view = runmat_accelerate_api::HostTensorView {
1051 data: &tensor.data,
1052 shape: &tensor.shape,
1053 };
1054 let handle = provider.upload(&view).expect("upload");
1055 let encoded =
1056 jsonencode_builtin(Value::GpuTensor(handle), Vec::new()).expect("jsonencode");
1057 assert_eq!(as_string(encoded), "[[1,0],[0,1]]");
1058 });
1059 }
1060
1061 #[test]
1062 #[cfg(feature = "wgpu")]
1063 fn jsonencode_gpu_tensor_wgpu_gathers_host_data() {
1064 let ensure = runmat_accelerate::backend::wgpu::provider::ensure_wgpu_provider();
1065 let Some(_) = ensure.ok().flatten() else {
1066 return;
1068 };
1069 let provider = runmat_accelerate_api::provider().expect("wgpu provider");
1070 let tensor = Tensor::new(vec![1.0, 2.0, 3.0], vec![3, 1]).expect("tensor");
1071 let view = runmat_accelerate_api::HostTensorView {
1072 data: &tensor.data,
1073 shape: &tensor.shape,
1074 };
1075 let handle = provider.upload(&view).expect("upload");
1076 let encoded = jsonencode_builtin(Value::GpuTensor(handle), Vec::new()).expect("jsonencode");
1077 assert_eq!(as_string(encoded), "[1,2,3]");
1078 }
1079
1080 #[test]
1081 #[cfg(feature = "doc_export")]
1082 fn doc_examples_present() {
1083 let examples = test_support::doc_examples(DOC_MD);
1084 assert!(!examples.is_empty());
1085 }
1086}