1use once_cell::sync::Lazy;
4use runmat_builtins::{
5 CellArray, CharArray, LogicalArray, StringArray, StructValue, Tensor, Value,
6};
7use runmat_macros::runtime_builtin;
8use serde_json::Value as JsonValue;
9
10use crate::builtins::common::spec::{
11 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
12 ReductionNaN, ResidencyPolicy, ShapeRequirements,
13};
14use crate::{build_runtime_error, gather_if_needed_async, BuiltinResult, RuntimeError};
15
16const INPUT_TYPE_ERROR: &str = "jsondecode: JSON text must be a character vector or string scalar";
17const PARSE_ERROR_PREFIX: &str = "jsondecode: invalid JSON text";
18
19#[allow(clippy::too_many_lines)]
20#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::io::json::jsondecode")]
21pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
22 name: "jsondecode",
23 op_kind: GpuOpKind::Custom("parse"),
24 supported_precisions: &[],
25 broadcast: BroadcastSemantics::None,
26 provider_hooks: &[],
27 constant_strategy: ConstantStrategy::InlineLiteral,
28 residency: ResidencyPolicy::GatherImmediately,
29 nan_mode: ReductionNaN::Include,
30 two_pass_threshold: None,
31 workgroup_size: None,
32 accepts_nan_mode: false,
33 notes: "No GPU kernels: jsondecode gathers gpuArray input to host memory before parsing the JSON text.",
34};
35
36fn jsondecode_error(message: impl Into<String>) -> RuntimeError {
37 build_runtime_error(message)
38 .with_builtin("jsondecode")
39 .build()
40}
41
42fn jsondecode_flow_with_context(err: RuntimeError) -> RuntimeError {
43 let mut builder = build_runtime_error(err.message().to_string()).with_builtin("jsondecode");
44 if let Some(identifier) = err.identifier() {
45 builder = builder.with_identifier(identifier.to_string());
46 }
47 builder.with_source(err).build()
48}
49
50#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::io::json::jsondecode")]
51pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
52 name: "jsondecode",
53 shape: ShapeRequirements::Any,
54 constant_strategy: ConstantStrategy::InlineLiteral,
55 elementwise: None,
56 reduction: None,
57 emits_nan: false,
58 notes: "jsondecode is a residency sink; it always runs on the CPU and breaks fusion graphs.",
59};
60
61#[runtime_builtin(
62 name = "jsondecode",
63 category = "io/json",
64 summary = "Parse UTF-8 JSON text into MATLAB-compatible RunMat values.",
65 keywords = "jsondecode,json,parse json,struct,gpu",
66 accel = "sink",
67 type_resolver(crate::builtins::io::type_resolvers::jsondecode_type),
68 builtin_path = "crate::builtins::io::json::jsondecode"
69)]
70async fn jsondecode_builtin(text: Value) -> crate::BuiltinResult<Value> {
71 let gathered = gather_if_needed_async(&text)
72 .await
73 .map_err(jsondecode_flow_with_context)?;
74 let source = extract_text(gathered)?;
75 let parsed: JsonValue = serde_json::from_str(&source).map_err(|err| {
76 build_runtime_error(format!("{PARSE_ERROR_PREFIX} ({err})"))
77 .with_builtin("jsondecode")
78 .with_source(err)
79 .build()
80 })?;
81 value_from_json(&parsed)
82}
83
84pub(crate) fn decode_json_text(text: &str) -> BuiltinResult<Value> {
85 let parsed: JsonValue = serde_json::from_str(text).map_err(|err| {
86 build_runtime_error(format!("{PARSE_ERROR_PREFIX} ({err})"))
87 .with_builtin("jsondecode")
88 .with_source(err)
89 .build()
90 })?;
91 value_from_json(&parsed)
92}
93
94fn extract_text(value: Value) -> BuiltinResult<String> {
95 match value {
96 Value::CharArray(array) => {
97 if array.rows > 1 {
98 return Err(jsondecode_error(INPUT_TYPE_ERROR));
99 }
100 Ok(array.data.into_iter().collect::<String>())
101 }
102 Value::String(s) => Ok(s),
103 Value::StringArray(sa) => {
104 if sa.data.len() == 1 {
105 Ok(sa.data[0].clone())
106 } else {
107 Err(jsondecode_error(INPUT_TYPE_ERROR))
108 }
109 }
110 _other => Err(jsondecode_error(INPUT_TYPE_ERROR)),
111 }
112}
113
114fn value_from_json(value: &JsonValue) -> BuiltinResult<Value> {
115 match value {
116 JsonValue::Null => empty_double(),
117 JsonValue::Bool(b) => Ok(Value::Bool(*b)),
118 JsonValue::Number(num) => parse_json_number(num).map(Value::Num),
119 JsonValue::String(s) => {
120 let char_array = CharArray::new_row(s);
121 Ok(Value::CharArray(char_array))
122 }
123 JsonValue::Array(arr) => decode_json_array(arr),
124 JsonValue::Object(map) => decode_json_object(map),
125 }
126}
127
128fn decode_json_object(map: &serde_json::Map<String, JsonValue>) -> BuiltinResult<Value> {
129 let mut struct_value = StructValue::new();
130 for (key, val) in map {
131 struct_value
132 .fields
133 .insert(key.clone(), value_from_json(val)?);
134 }
135 Ok(Value::Struct(struct_value))
136}
137
138fn parse_json_number(number: &serde_json::Number) -> BuiltinResult<f64> {
139 if let Some(value) = number.as_f64() {
140 return Ok(value);
141 }
142 let text = number.to_string();
143 match text.parse::<f64>() {
144 Ok(value) => {
145 if value.is_nan() {
146 Err(jsondecode_error(format!(
147 "{PARSE_ERROR_PREFIX}: unsupported numeric literal ({text})"
148 )))
149 } else {
150 Ok(value)
151 }
152 }
153 Err(_) => {
154 let display = if text.len() > 64 {
155 format!("{}...", &text[..64])
156 } else {
157 text
158 };
159 Err(jsondecode_error(format!(
160 "{PARSE_ERROR_PREFIX}: numeric literal out of range ({display})"
161 )))
162 }
163 }
164}
165
166fn decode_json_array(values: &[JsonValue]) -> BuiltinResult<Value> {
167 if values.is_empty() {
168 let tensor = Tensor::new(Vec::new(), vec![0, 0])
169 .map_err(|e| jsondecode_error(format!("jsondecode: {e}")))?;
170 return Ok(Value::Tensor(tensor));
171 }
172
173 if let Some(numeric) = parse_numeric_array(values)? {
174 let tensor = Tensor::new(numeric.data, numeric.shape)
175 .map_err(|e| jsondecode_error(format!("jsondecode: {e}")))?;
176 return Ok(Value::Tensor(tensor));
177 }
178
179 if let Some(logical) = parse_logical_array(values) {
180 let array = LogicalArray::new(logical.data, logical.shape)
181 .map_err(|e| jsondecode_error(format!("jsondecode: {e}")))?;
182 return Ok(Value::LogicalArray(array));
183 }
184
185 if let Some(strings) = parse_string_array(values) {
186 let array = StringArray::new(strings.data, strings.shape)
187 .map_err(|e| jsondecode_error(format!("jsondecode: {e}")))?;
188 return Ok(Value::StringArray(array));
189 }
190
191 if let Some(cell) = parse_rectangular_cell_array(values)? {
192 return Ok(cell);
193 }
194
195 let mut elements = Vec::with_capacity(values.len());
196 for element in values {
197 elements.push(value_from_json(element)?);
198 }
199 cell_row(elements)
200}
201
202fn empty_double() -> BuiltinResult<Value> {
203 static EMPTY_DOUBLE: Lazy<Option<Value>> =
204 Lazy::new(|| Tensor::new(Vec::new(), vec![0, 0]).map(Value::Tensor).ok());
205 if let Some(value) = EMPTY_DOUBLE.as_ref() {
206 return Ok(value.clone());
207 }
208 Tensor::new(Vec::new(), vec![0, 0])
209 .map(Value::Tensor)
210 .map_err(|e| jsondecode_error(format!("jsondecode: {e}")))
211}
212
213fn cell_matrix(elements: Vec<Value>, rows: usize, cols: usize) -> BuiltinResult<Value> {
214 let cell = CellArray::new(elements, rows, cols)
215 .map_err(|e| jsondecode_error(format!("jsondecode: {e}")))?;
216 Ok(Value::Cell(cell))
217}
218
219fn cell_row(elements: Vec<Value>) -> BuiltinResult<Value> {
220 let cols = elements.len();
221 cell_matrix(elements, 1, cols)
222}
223
224struct NumericTensor {
225 data: Vec<f64>,
226 shape: Vec<usize>,
227}
228
229fn parse_numeric_array(values: &[JsonValue]) -> BuiltinResult<Option<NumericTensor>> {
230 if values.is_empty() {
231 return Ok(None);
232 }
233
234 if values.iter().all(|v| v.is_number()) {
235 let mut data = Vec::with_capacity(values.len());
236 for value in values {
237 if let JsonValue::Number(number) = value {
238 data.push(parse_json_number(number)?);
239 }
240 }
241 return Ok(Some(NumericTensor {
242 data,
243 shape: vec![values.len()],
244 }));
245 }
246
247 if values.iter().all(|v| v.is_array()) {
248 let mut children = Vec::with_capacity(values.len());
249 for value in values {
250 let Some(child_values) = value.as_array() else {
251 return Ok(None);
252 };
253 let Some(child) = parse_numeric_array(child_values)? else {
254 return Ok(None);
255 };
256 children.push(child);
257 }
258 if children.is_empty() {
259 return Ok(None);
260 }
261 let first_shape = children[0].shape.clone();
262 if !children.iter().all(|child| child.shape == first_shape) {
263 return Ok(None);
264 }
265 let mut shape = Vec::with_capacity(first_shape.len() + 1);
266 shape.push(children.len());
267 shape.extend(first_shape.clone());
268
269 let total: usize = shape.iter().product();
270 let rows = shape[0];
271 if rows == 0 {
272 return Ok(None);
273 }
274 let inner = total / rows;
275 let mut data = vec![0.0; total];
276 for (row, child) in children.into_iter().enumerate() {
277 if child.data.len() != inner {
278 return Ok(None);
279 }
280 for (idx, value) in child.data.into_iter().enumerate() {
281 let offset = row + rows * idx;
282 data[offset] = value;
283 }
284 }
285 return Ok(Some(NumericTensor { data, shape }));
286 }
287
288 Ok(None)
289}
290
291struct LogicalTensor {
292 data: Vec<u8>,
293 shape: Vec<usize>,
294}
295
296fn parse_logical_array(values: &[JsonValue]) -> Option<LogicalTensor> {
297 if values.is_empty() {
298 return None;
299 }
300
301 if values.iter().all(|v| v.is_boolean()) {
302 let mut data = Vec::with_capacity(values.len());
303 for value in values {
304 data.push(if value.as_bool()? { 1 } else { 0 });
305 }
306 return Some(LogicalTensor {
307 data,
308 shape: vec![values.len()],
309 });
310 }
311
312 if values.iter().all(|v| v.is_array()) {
313 let mut children = Vec::with_capacity(values.len());
314 for value in values {
315 let child = parse_logical_array(value.as_array()?)?;
316 children.push(child);
317 }
318 if children.is_empty() {
319 return None;
320 }
321 let first_shape = children[0].shape.clone();
322 if !children.iter().all(|child| child.shape == first_shape) {
323 return None;
324 }
325 let mut shape = Vec::with_capacity(first_shape.len() + 1);
326 shape.push(children.len());
327 shape.extend(first_shape.clone());
328
329 let total: usize = shape.iter().product();
330 let rows = shape[0];
331 if rows == 0 {
332 return None;
333 }
334 let inner = total / rows;
335 let mut data = vec![0u8; total];
336 for (row, child) in children.into_iter().enumerate() {
337 if child.data.len() != inner {
338 return None;
339 }
340 for (idx, value) in child.data.into_iter().enumerate() {
341 let offset = row + rows * idx;
342 data[offset] = value;
343 }
344 }
345 return Some(LogicalTensor { data, shape });
346 }
347
348 None
349}
350
351struct StringTensor {
352 data: Vec<String>,
353 shape: Vec<usize>,
354}
355
356fn parse_string_array(values: &[JsonValue]) -> Option<StringTensor> {
357 if values.is_empty() {
358 return None;
359 }
360
361 if values.iter().all(|v| v.is_string()) {
362 let mut data = Vec::with_capacity(values.len());
363 for value in values {
364 data.push(value.as_str()?.to_string());
365 }
366 return Some(StringTensor {
367 data,
368 shape: vec![values.len()],
369 });
370 }
371
372 if values.iter().all(|v| v.is_array()) {
373 let mut children = Vec::with_capacity(values.len());
374 for value in values {
375 let child = parse_string_array(value.as_array()?)?;
376 children.push(child);
377 }
378 if children.is_empty() {
379 return None;
380 }
381 let first_shape = children[0].shape.clone();
382 if !children.iter().all(|child| child.shape == first_shape) {
383 return None;
384 }
385 let mut shape = Vec::with_capacity(first_shape.len() + 1);
386 shape.push(children.len());
387 shape.extend(first_shape.clone());
388
389 let total: usize = shape.iter().product();
390 let rows = shape[0];
391 if rows == 0 {
392 return None;
393 }
394 let inner = total / rows;
395 let mut data = vec![String::new(); total];
396 for (row, mut child) in children.into_iter().enumerate() {
397 if child.data.len() != inner {
398 return None;
399 }
400 for (idx, value) in child.data.drain(..).enumerate() {
401 let offset = row + rows * idx;
402 data[offset] = value;
403 }
404 }
405 return Some(StringTensor { data, shape });
406 }
407
408 None
409}
410
411fn parse_rectangular_cell_array(values: &[JsonValue]) -> BuiltinResult<Option<Value>> {
412 if values.is_empty() || !values.iter().all(|v| v.is_array()) {
413 return Ok(None);
414 }
415
416 let mut expected_len: Option<usize> = None;
417 for value in values {
418 let arr = value.as_array().ok_or_else(|| {
419 jsondecode_error("jsondecode: inconsistent array state")
421 })?;
422 match expected_len {
423 Some(len) if arr.len() != len => return Ok(None),
424 None => expected_len = Some(arr.len()),
425 _ => {}
426 }
427 }
428
429 let cols = expected_len.unwrap_or(0);
430 let rows = values.len();
431
432 let mut elements = Vec::with_capacity(rows.saturating_mul(cols));
433 if cols > 0 {
434 for value in values {
435 let arr = value.as_array().expect("validated array value");
436 for element in arr {
437 elements.push(value_from_json(element)?);
438 }
439 }
440 }
441
442 let cell = cell_matrix(elements, rows, cols)?;
443 Ok(Some(cell))
444}
445
446#[cfg(test)]
447pub(crate) mod tests {
448 use super::*;
449 use futures::executor::block_on;
450 use runmat_builtins::{IntValue, Tensor};
451
452 use crate::RuntimeError;
453
454 fn char_row(text: &str) -> Value {
455 Value::CharArray(CharArray::new_row(text))
456 }
457
458 fn error_message(err: RuntimeError) -> String {
459 err.message().to_string()
460 }
461
462 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
463 #[test]
464 fn jsondecode_scalar_number() {
465 let result = block_on(jsondecode_builtin(char_row("42"))).expect("jsondecode");
466 assert_eq!(result, Value::Num(42.0));
467 }
468
469 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
470 #[test]
471 fn jsondecode_boolean_array() {
472 let result =
473 block_on(jsondecode_builtin(char_row("[true,false,true]"))).expect("jsondecode");
474 match result {
475 Value::LogicalArray(array) => {
476 assert_eq!(array.shape, vec![3]);
477 assert_eq!(array.data, vec![1, 0, 1]);
478 }
479 other => panic!("expected logical array, got {:?}", other),
480 }
481 }
482
483 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
484 #[test]
485 fn jsondecode_matrix_to_tensor() {
486 let result =
487 block_on(jsondecode_builtin(char_row("[[1,2,3],[4,5,6]]"))).expect("jsondecode matrix");
488 match result {
489 Value::Tensor(tensor) => {
490 assert_eq!(tensor.shape, vec![2, 3]);
491 assert_eq!(tensor.data, vec![1.0, 4.0, 2.0, 5.0, 3.0, 6.0]);
492 }
493 other => panic!("expected tensor, got {:?}", other),
494 }
495 }
496
497 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
498 #[test]
499 fn jsondecode_numeric_tensor_3d() {
500 let json = "[[[1,2],[3,4]],[[5,6],[7,8]]]";
501 let result = block_on(jsondecode_builtin(char_row(json))).expect("jsondecode 3d tensor");
502 match result {
503 Value::Tensor(tensor) => {
504 assert_eq!(tensor.shape, vec![2, 2, 2]);
505 assert_eq!(tensor.data, vec![1.0, 5.0, 3.0, 7.0, 2.0, 6.0, 4.0, 8.0]);
506 }
507 other => panic!("expected tensor, got {:?}", other),
508 }
509 }
510
511 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
512 #[test]
513 fn jsondecode_numeric_singleton_array_retains_tensor() {
514 let result = block_on(jsondecode_builtin(char_row("[42]")))
515 .expect("jsondecode singleton numeric array");
516 match result {
517 Value::Tensor(tensor) => {
518 assert_eq!(tensor.shape, vec![1]);
519 assert_eq!(tensor.rows(), 1);
520 assert_eq!(tensor.cols(), 1);
521 assert_eq!(tensor.data, vec![42.0]);
522 }
523 other => panic!("expected tensor, got {:?}", other),
524 }
525 }
526
527 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
528 #[test]
529 fn jsondecode_object_to_struct() {
530 let result = block_on(jsondecode_builtin(char_row(
531 "{\"name\":\"RunMat\",\"year\":2025}",
532 )))
533 .expect("jsondecode struct");
534 match result {
535 Value::Struct(struct_value) => {
536 assert_eq!(
537 struct_value.fields.get("name"),
538 Some(&Value::CharArray(CharArray::new_row("RunMat")))
539 );
540 match struct_value.fields.get("year") {
541 Some(Value::Num(year)) => assert_eq!(*year, 2025.0),
542 Some(Value::Int(IntValue::I32(year))) => assert_eq!(*year, 2025),
543 other => panic!("unexpected year field {other:?}"),
544 }
545 }
546 other => panic!("expected struct, got {:?}", other),
547 }
548 }
549
550 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
551 #[test]
552 fn jsondecode_string_array() {
553 let result = block_on(jsondecode_builtin(char_row(
554 "[\"alpha\",\"beta\",\"gamma\"]",
555 )))
556 .expect("jsondecode string array");
557 match result {
558 Value::StringArray(array) => {
559 assert_eq!(array.shape, vec![3]);
560 assert_eq!(array.rows, 1);
561 assert_eq!(array.cols, 3);
562 assert_eq!(
563 array.data,
564 vec![
565 String::from("alpha"),
566 String::from("beta"),
567 String::from("gamma"),
568 ]
569 );
570 }
571 other => panic!("expected string array, got {:?}", other),
572 }
573 }
574
575 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
576 #[test]
577 fn jsondecode_mixed_array_returns_cell() {
578 let result = block_on(jsondecode_builtin(char_row("[\"RunMat\",42,true]")))
579 .expect("jsondecode mixed");
580 match result {
581 Value::Cell(cell) => {
582 assert_eq!(cell.rows, 1);
583 assert_eq!(cell.cols, 3);
584 let first = cell.get(0, 0).expect("cell");
585 assert_eq!(first, Value::CharArray(CharArray::new_row("RunMat")));
586 let second = cell.get(0, 1).expect("cell");
587 assert_eq!(second, Value::Num(42.0));
588 let third = cell.get(0, 2).expect("cell");
589 assert_eq!(third, Value::Bool(true));
590 }
591 other => panic!("expected cell array, got {:?}", other),
592 }
593 }
594
595 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
596 #[test]
597 fn jsondecode_rectangular_cell_array_preserves_layout() {
598 let text = "[[1,true],[false,null]]";
599 let result = block_on(jsondecode_builtin(char_row(text)))
600 .expect("jsondecode rectangular heterogeneous array");
601 match result {
602 Value::Cell(cell) => {
603 assert_eq!(cell.rows, 2);
604 assert_eq!(cell.cols, 2);
605 assert_eq!(cell.get(0, 0).unwrap(), Value::Num(1.0));
606 assert_eq!(cell.get(0, 1).unwrap(), Value::Bool(true));
607 assert_eq!(cell.get(1, 0).unwrap(), Value::Bool(false));
608 match cell.get(1, 1).unwrap() {
609 Value::Tensor(t) => {
610 assert_eq!(t.shape, vec![0, 0]);
611 assert!(t.data.is_empty());
612 }
613 other => panic!("expected empty tensor, got {:?}", other),
614 }
615 }
616 other => panic!("expected cell array, got {:?}", other),
617 }
618 }
619
620 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
621 #[test]
622 fn jsondecode_array_of_objects_returns_cell() {
623 let text = "[{\"id\":1,\"name\":\"Ada\"},{\"id\":2,\"name\":\"Charles\"}]";
624 let result = block_on(jsondecode_builtin(char_row(text))).expect("jsondecode object array");
625 match result {
626 Value::Cell(cell) => {
627 assert_eq!(cell.rows, 1);
628 assert_eq!(cell.cols, 2);
629
630 let first = cell.get(0, 0).expect("first struct");
631 match first {
632 Value::Struct(struct_value) => {
633 assert_eq!(struct_value.fields.get("id"), Some(&Value::Num(1.0)));
634 assert_eq!(
635 struct_value.fields.get("name"),
636 Some(&Value::CharArray(CharArray::new_row("Ada")))
637 );
638 }
639 other => panic!("expected struct, got {:?}", other),
640 }
641
642 let second = cell.get(0, 1).expect("second struct");
643 match second {
644 Value::Struct(struct_value) => {
645 assert_eq!(struct_value.fields.get("id"), Some(&Value::Num(2.0)));
646 assert_eq!(
647 struct_value.fields.get("name"),
648 Some(&Value::CharArray(CharArray::new_row("Charles")))
649 );
650 }
651 other => panic!("expected struct, got {:?}", other),
652 }
653 }
654 other => panic!("expected cell array, got {:?}", other),
655 }
656 }
657
658 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
659 #[test]
660 fn jsondecode_null_returns_empty_double() {
661 let result = block_on(jsondecode_builtin(char_row("null"))).expect("jsondecode null");
662 match result {
663 Value::Tensor(tensor) => {
664 assert_eq!(tensor.shape, vec![0, 0]);
665 assert!(tensor.data.is_empty());
666 }
667 other => panic!("expected empty tensor, got {:?}", other),
668 }
669 }
670
671 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
672 #[test]
673 fn jsondecode_invalid_text_reports_error() {
674 let err =
675 block_on(jsondecode_builtin(char_row("{not json}"))).expect_err("expected failure");
676 let err = error_message(err);
677 assert!(
678 err.starts_with(PARSE_ERROR_PREFIX),
679 "unexpected error message: {err}"
680 );
681 }
682
683 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
684 #[test]
685 fn jsondecode_rejects_multirow_char_input() {
686 let chars = CharArray::new(vec!['a', 'b', 'c', 'd'], 2, 2).expect("char array");
687 let err =
688 block_on(jsondecode_builtin(Value::CharArray(chars))).expect_err("expected type error");
689 assert_eq!(error_message(err), INPUT_TYPE_ERROR);
690 }
691
692 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
693 #[test]
694 fn jsondecode_accepts_string_input() {
695 let result =
696 block_on(jsondecode_builtin(Value::String("[1,2]".to_string()))).expect("jsondecode");
697 match result {
698 Value::Tensor(tensor) => {
699 assert_eq!(tensor.shape, vec![2]);
700 assert_eq!(tensor.data, vec![1.0, 2.0]);
701 }
702 other => panic!("expected tensor, got {:?}", other),
703 }
704 }
705
706 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
707 #[test]
708 fn jsondecode_accepts_string_array_scalar_input() {
709 let array = StringArray::new(vec!["[1,2]".to_string()], vec![1, 1]).expect("string scalar");
710 let result = block_on(jsondecode_builtin(Value::StringArray(array))).expect("jsondecode");
711 match result {
712 Value::Tensor(tensor) => {
713 assert_eq!(tensor.shape, vec![2]);
714 assert_eq!(tensor.data, vec![1.0, 2.0]);
715 }
716 other => panic!("expected tensor, got {:?}", other),
717 }
718 }
719
720 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
721 #[test]
722 fn jsondecode_round_trip_with_jsonencode() {
723 let tensor = Tensor::new(vec![1.0, 2.0, 3.0], vec![3, 1]).expect("tensor");
724 let encoded = block_on(crate::call_builtin_async(
725 "jsonencode",
726 &[Value::Tensor(tensor.clone())],
727 ))
728 .expect("encode");
729 let decoded = block_on(jsondecode_builtin(encoded)).expect("decode");
730 match decoded {
731 Value::Tensor(result) => {
732 assert_eq!(result.data.len(), tensor.data.len());
733 assert_eq!(result.data, tensor.data);
734 }
735 other => panic!("expected tensor, got {:?}", other),
736 }
737 }
738}