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::{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 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 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}