1use 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 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 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}