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