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