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