1use std::collections::BTreeMap;
4use std::fmt::Write as FmtWrite;
5
6use runmat_builtins::{
7 BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinExtensionDescriptor,
8 BuiltinExtensionMode, BuiltinIntegerBackendRule, BuiltinIntegerCapabilityDescriptor,
9 BuiltinIntegerComputationDomain, BuiltinIntegerInputAvailability,
10 BuiltinIntegerInputCapability, BuiltinIntegerOutputClassRule, BuiltinIntegerOverflowRule,
11 BuiltinIntegerOverloadKind, BuiltinIntegerScalarDoubleRule, BuiltinOutputMode,
12 BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
13};
14use runmat_macros::runtime_builtin;
15use runmat_value::{
16 CellArray, CharArray, ComplexTensor, IntValue, IntegerStorage, LogicalArray, ObjectInstance,
17 StringArray, StructValue, SymbolicArray, Tensor, Value,
18};
19
20use crate::builtins::common::spec::{
21 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
22 ReductionNaN, ResidencyPolicy, ShapeRequirements,
23};
24use crate::builtins::common::tensor;
25use crate::{build_runtime_error, gather_if_needed_async, BuiltinResult, RuntimeError};
26
27const BUILTIN_NAME: &str = "jsonencode";
28
29const JSONENCODE_RESIDENT_INPUT_EXTENSION: BuiltinExtensionDescriptor =
30 BuiltinExtensionDescriptor {
31 id: "jsonencode-resident-input",
32 mode: BuiltinExtensionMode::RunMatOnly,
33 description: "jsonencode with explicit gpuArray input is a RunMat extension",
34 error_identifier: Some("RunMat:compatibility:JsonencodeResidentInputExtension"),
35 };
36const JSONENCODE_TYPED_INTEGER_OPTION_EXTENSION: BuiltinExtensionDescriptor =
37 BuiltinExtensionDescriptor {
38 id: "jsonencode-typed-integer-option",
39 mode: BuiltinExtensionMode::RunMatOnly,
40 description: "jsonencode with a typed-integer boolean option is a RunMat extension",
41 error_identifier: Some("RunMat:compatibility:JsonencodeTypedIntegerOptionExtension"),
42 };
43const JSONENCODE_NUMERIC_OPTION_EXTENSION: BuiltinExtensionDescriptor =
44 BuiltinExtensionDescriptor {
45 id: "jsonencode-numeric-option",
46 mode: BuiltinExtensionMode::RunMatOnly,
47 description: "jsonencode with a floating numeric boolean option is a RunMat extension",
48 error_identifier: Some("RunMat:compatibility:JsonencodeNumericOptionExtension"),
49 };
50const JSONENCODE_TEXT_OPTION_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
51 id: "jsonencode-text-option",
52 mode: BuiltinExtensionMode::RunMatOnly,
53 description: "jsonencode with a textual boolean option is a RunMat extension",
54 error_identifier: Some("RunMat:compatibility:JsonencodeTextOptionExtension"),
55};
56const JSONENCODE_COMPLEX_INPUT_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
57 id: "jsonencode-complex-input",
58 mode: BuiltinExtensionMode::RunMatOnly,
59 description: "jsonencode complex-number object encoding is a RunMat extension",
60 error_identifier: Some("RunMat:compatibility:JsonencodeComplexInputExtension"),
61};
62const JSONENCODE_SPARSE_INPUT_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
63 id: "jsonencode-sparse-input",
64 mode: BuiltinExtensionMode::RunMatOnly,
65 description: "jsonencode sparse-array densification is a RunMat extension",
66 error_identifier: Some("RunMat:compatibility:JsonencodeSparseInputExtension"),
67};
68pub const JSONENCODE_EXTENSIONS: [BuiltinExtensionDescriptor; 6] = [
69 JSONENCODE_RESIDENT_INPUT_EXTENSION,
70 JSONENCODE_TYPED_INTEGER_OPTION_EXTENSION,
71 JSONENCODE_NUMERIC_OPTION_EXTENSION,
72 JSONENCODE_TEXT_OPTION_EXTENSION,
73 JSONENCODE_COMPLEX_INPUT_EXTENSION,
74 JSONENCODE_SPARSE_INPUT_EXTENSION,
75];
76
77const JSONENCODE_REAL_INTEGER_INPUT: [BuiltinIntegerInputCapability; 1] =
78 [BuiltinIntegerInputCapability {
79 name: "value",
80 classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
81 availability: BuiltinIntegerInputAvailability::Documented,
82 scalar_double: BuiltinIntegerScalarDoubleRule::NotApplicable,
83 notes: "All eight real integer classes serialize from authoritative storage to exact signed or unsigned JSON decimal literals.",
84 }];
85const JSONENCODE_INTEGER_OPTION_INPUT: [BuiltinIntegerInputCapability; 1] =
86 [BuiltinIntegerInputCapability {
87 name: "PrettyPrint_or_ConvertInfAndNaN",
88 classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
89 availability: BuiltinIntegerInputAvailability::RunMatOnly,
90 scalar_double: BuiltinIntegerScalarDoubleRule::Allowed,
91 notes: "MATLAB-compatible mode requires logical option values; RunMat mode treats exact integer zero as false and every nonzero integer as true.",
92 }];
93const JSONENCODE_COMPLEX_INTEGER_INPUT: [BuiltinIntegerInputCapability; 1] =
94 [BuiltinIntegerInputCapability {
95 name: "value",
96 classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
97 availability: BuiltinIntegerInputAvailability::RunMatOnly,
98 scalar_double: BuiltinIntegerScalarDoubleRule::NotApplicable,
99 notes: "The public jsonencode contract rejects complex numeric data. RunMat mode retains its exact {real,imag} object encoding for paired integer storage.",
100 }];
101pub const JSONENCODE_INTEGER_CAPABILITIES: [BuiltinIntegerCapabilityDescriptor; 3] = [
102 BuiltinIntegerCapabilityDescriptor {
103 form: "jsonText = jsonencode(full_real_integer_value)",
104 inputs: &JSONENCODE_REAL_INTEGER_INPUT,
105 computation_domain: BuiltinIntegerComputationDomain::ExactInteger,
106 output_class: BuiltinIntegerOutputClassRule::FunctionSpecific,
107 overflow: BuiltinIntegerOverflowRule::Error,
108 backend: BuiltinIntegerBackendRule::GatherFallback,
109 overload: BuiltinIntegerOverloadKind::Multiple,
110 notes: "Host and automatically resident integer values serialize exactly. Explicit gpuArray input is independently compatibility-gated before owner access; accepted resident values gather through their owner at the serialization sink.",
111 },
112 BuiltinIntegerCapabilityDescriptor {
113 form: "jsonText = jsonencode(value, option, integer_flag)",
114 inputs: &JSONENCODE_INTEGER_OPTION_INPUT,
115 computation_domain: BuiltinIntegerComputationDomain::Predicate,
116 output_class: BuiltinIntegerOutputClassRule::FunctionSpecific,
117 overflow: BuiltinIntegerOverflowRule::NotApplicable,
118 backend: BuiltinIntegerBackendRule::GatherFallback,
119 overload: BuiltinIntegerOverloadKind::ScalarOnly,
120 notes: "Typed numeric option coercion is a RunMat convenience and is gated before resident access.",
121 },
122 BuiltinIntegerCapabilityDescriptor {
123 form: "jsonText = jsonencode(complex_integer_value)",
124 inputs: &JSONENCODE_COMPLEX_INTEGER_INPUT,
125 computation_domain: BuiltinIntegerComputationDomain::ExactInteger,
126 output_class: BuiltinIntegerOutputClassRule::FunctionSpecific,
127 overflow: BuiltinIntegerOverflowRule::Error,
128 backend: BuiltinIntegerBackendRule::GatherFallback,
129 overload: BuiltinIntegerOverloadKind::Multiple,
130 notes: "Complex JSON encoding is outside the public compatibility surface and requires the complex-input extension; integer components remain exact in RunMat mode.",
131 },
132];
133
134const JSONENCODE_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
135 name: "jsonText",
136 ty: BuiltinParamType::StringScalar,
137 arity: BuiltinParamArity::Required,
138 default: None,
139 description: "JSON text encoded as a character row vector.",
140}];
141const JSONENCODE_INPUTS_VALUE: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
142 name: "value",
143 ty: BuiltinParamType::Any,
144 arity: BuiltinParamArity::Required,
145 default: None,
146 description: "Value to encode as JSON.",
147}];
148const JSONENCODE_INPUTS_VALUE_OPTIONS: [BuiltinParamDescriptor; 2] = [
149 BuiltinParamDescriptor {
150 name: "value",
151 ty: BuiltinParamType::Any,
152 arity: BuiltinParamArity::Required,
153 default: None,
154 description: "Value to encode as JSON.",
155 },
156 BuiltinParamDescriptor {
157 name: "options",
158 ty: BuiltinParamType::Any,
159 arity: BuiltinParamArity::Required,
160 default: None,
161 description: "Options struct with fields such as PrettyPrint and ConvertInfAndNaN.",
162 },
163];
164const JSONENCODE_INPUTS_VALUE_NAME_VALUE: [BuiltinParamDescriptor; 3] = [
165 BuiltinParamDescriptor {
166 name: "value",
167 ty: BuiltinParamType::Any,
168 arity: BuiltinParamArity::Required,
169 default: None,
170 description: "Value to encode as JSON.",
171 },
172 BuiltinParamDescriptor {
173 name: "name",
174 ty: BuiltinParamType::StringScalar,
175 arity: BuiltinParamArity::Required,
176 default: None,
177 description: "Option name (for example \"PrettyPrint\" or \"ConvertInfAndNaN\").",
178 },
179 BuiltinParamDescriptor {
180 name: "optionValue",
181 ty: BuiltinParamType::Any,
182 arity: BuiltinParamArity::Required,
183 default: None,
184 description: "Option value for the preceding option name.",
185 },
186];
187const JSONENCODE_INPUTS_VALUE_NAME_VALUE_VARIADIC: [BuiltinParamDescriptor; 2] = [
188 BuiltinParamDescriptor {
189 name: "value",
190 ty: BuiltinParamType::Any,
191 arity: BuiltinParamArity::Required,
192 default: None,
193 description: "Value to encode as JSON.",
194 },
195 BuiltinParamDescriptor {
196 name: "nameValuePairs...",
197 ty: BuiltinParamType::Any,
198 arity: BuiltinParamArity::Variadic,
199 default: None,
200 description: "Name-value option pairs.",
201 },
202];
203const JSONENCODE_SIGNATURES: [BuiltinSignatureDescriptor; 4] = [
204 BuiltinSignatureDescriptor {
205 label: "jsonText = jsonencode(value)",
206 inputs: &JSONENCODE_INPUTS_VALUE,
207 outputs: &JSONENCODE_OUTPUT,
208 },
209 BuiltinSignatureDescriptor {
210 label: "jsonText = jsonencode(value, options)",
211 inputs: &JSONENCODE_INPUTS_VALUE_OPTIONS,
212 outputs: &JSONENCODE_OUTPUT,
213 },
214 BuiltinSignatureDescriptor {
215 label: "jsonText = jsonencode(value, name, optionValue)",
216 inputs: &JSONENCODE_INPUTS_VALUE_NAME_VALUE,
217 outputs: &JSONENCODE_OUTPUT,
218 },
219 BuiltinSignatureDescriptor {
220 label: "jsonText = jsonencode(value, nameValuePairs...)",
221 inputs: &JSONENCODE_INPUTS_VALUE_NAME_VALUE_VARIADIC,
222 outputs: &JSONENCODE_OUTPUT,
223 },
224];
225const JSONENCODE_ERROR_OPTIONS_CONFIG: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
226 code: "RM.JSONENCODE.OPTIONS_CONFIG",
227 identifier: None,
228 when: "Single options argument is provided but is not a struct.",
229 message: "jsonencode: expected name/value pairs or options struct",
230};
231const JSONENCODE_ERROR_NAME_VALUE_PAIRS: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
232 code: "RM.JSONENCODE.NAME_VALUE_PAIRS",
233 identifier: None,
234 when: "Name-value options do not come in pairs.",
235 message: "jsonencode: name/value pairs must come in pairs",
236};
237const JSONENCODE_ERROR_OPTION_NAME: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
238 code: "RM.JSONENCODE.OPTION_NAME",
239 identifier: None,
240 when: "Option name is not a character vector or string scalar.",
241 message: "jsonencode: option names must be character vectors or strings",
242};
243const JSONENCODE_ERROR_OPTION_VALUE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
244 code: "RM.JSONENCODE.OPTION_VALUE",
245 identifier: None,
246 when: "Option value is not a scalar logical/numeric or boolean-like text.",
247 message: "jsonencode: option value must be scalar logical or numeric",
248};
249const JSONENCODE_ERROR_UNKNOWN_OPTION: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
250 code: "RM.JSONENCODE.UNKNOWN_OPTION",
251 identifier: None,
252 when: "Option name is not recognized.",
253 message: "jsonencode: unknown option name",
254};
255const JSONENCODE_ERROR_INF_NAN: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
256 code: "RM.JSONENCODE.INF_NAN",
257 identifier: None,
258 when: "Input contains NaN/Inf while ConvertInfAndNaN is false.",
259 message: "jsonencode: ConvertInfAndNaN must be true to encode NaN or Inf values",
260};
261const JSONENCODE_ERROR_UNSUPPORTED_TYPE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
262 code: "RM.JSONENCODE.UNSUPPORTED_TYPE",
263 identifier: None,
264 when: "Input value type is not supported for JSON encoding.",
265 message:
266 "jsonencode: unsupported input type; expected numeric, logical, string, struct, cell, or object data",
267};
268const JSONENCODE_ERROR_UNEXPECTED_GPU: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
269 code: "RM.JSONENCODE.UNEXPECTED_GPU",
270 identifier: None,
271 when: "A GPU tensor handle reaches encoding after gather pass.",
272 message: "jsonencode: unexpected gpuArray handle after gather pass",
273};
274const JSONENCODE_ERROR_INTERNAL: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
275 code: "RM.JSONENCODE.INTERNAL",
276 identifier: None,
277 when: "Internal JSON conversion or container materialization fails.",
278 message: "jsonencode: internal conversion failed",
279};
280const JSONENCODE_ERRORS: [BuiltinErrorDescriptor; 9] = [
281 JSONENCODE_ERROR_OPTIONS_CONFIG,
282 JSONENCODE_ERROR_NAME_VALUE_PAIRS,
283 JSONENCODE_ERROR_OPTION_NAME,
284 JSONENCODE_ERROR_OPTION_VALUE,
285 JSONENCODE_ERROR_UNKNOWN_OPTION,
286 JSONENCODE_ERROR_INF_NAN,
287 JSONENCODE_ERROR_UNSUPPORTED_TYPE,
288 JSONENCODE_ERROR_UNEXPECTED_GPU,
289 JSONENCODE_ERROR_INTERNAL,
290];
291pub const JSONENCODE_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
292 signatures: &JSONENCODE_SIGNATURES,
293 output_mode: BuiltinOutputMode::Fixed,
294 completion_policy: BuiltinCompletionPolicy::Public,
295 errors: &JSONENCODE_ERRORS,
296};
297
298#[allow(clippy::too_many_lines)]
299#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::io::json::jsonencode")]
300pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
301 name: "jsonencode",
302 op_kind: GpuOpKind::Custom("serialization"),
303 supported_precisions: &[],
304 broadcast: BroadcastSemantics::None,
305 provider_hooks: &[],
306 constant_strategy: ConstantStrategy::InlineLiteral,
307 residency: ResidencyPolicy::GatherImmediately,
308 nan_mode: ReductionNaN::Include,
309 two_pass_threshold: None,
310 workgroup_size: None,
311 accepts_nan_mode: false,
312 notes:
313 "Serialization sink that gathers GPU data to host memory before emitting UTF-8 JSON text.",
314};
315
316fn jsonencode_error(error: &'static BuiltinErrorDescriptor) -> RuntimeError {
317 jsonencode_error_with(error, error.message)
318}
319
320fn jsonencode_error_with(
321 error: &'static BuiltinErrorDescriptor,
322 message: impl Into<String>,
323) -> RuntimeError {
324 let mut builder = build_runtime_error(message).with_builtin(BUILTIN_NAME);
325 if let Some(identifier) = error.identifier {
326 builder = builder.with_identifier(identifier);
327 }
328 builder.build()
329}
330
331fn jsonencode_flow_with_context(err: RuntimeError) -> RuntimeError {
332 let mut builder = build_runtime_error(err.message().to_string()).with_builtin(BUILTIN_NAME);
333 if let Some(identifier) = err.identifier() {
334 builder = builder.with_identifier(identifier.to_string());
335 }
336 builder.with_source(err).build()
337}
338
339#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::io::json::jsonencode")]
340pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
341 name: "jsonencode",
342 shape: ShapeRequirements::Any,
343 constant_strategy: ConstantStrategy::InlineLiteral,
344 elementwise: None,
345 reduction: None,
346 emits_nan: false,
347 notes: "jsonencode is a residency sink and never participates in fusion planning.",
348};
349
350#[derive(Debug, Clone)]
351struct JsonEncodeOptions {
352 pretty_print: bool,
353 convert_inf_and_nan: bool,
354}
355
356impl Default for JsonEncodeOptions {
357 fn default() -> Self {
358 Self {
359 pretty_print: false,
360 convert_inf_and_nan: true,
361 }
362 }
363}
364
365#[derive(Debug, Clone)]
366enum JsonValue {
367 Null,
368 Bool(bool),
369 Number(JsonNumber),
370 String(String),
371 Array(Vec<JsonValue>),
372 Object(Vec<(String, JsonValue)>),
373}
374
375#[derive(Debug, Clone)]
376enum JsonNumber {
377 Float(f64),
378 I64(i64),
379 U64(u64),
380}
381
382#[runtime_builtin(
383 name = "jsonencode",
384 category = "io/json",
385 summary = "Serialize MATLAB values to UTF-8 JSON text.",
386 keywords = "jsonencode,json,serialization,struct,gpu",
387 accel = "cpu",
388 type_resolver(crate::builtins::io::type_resolvers::jsonencode_type),
389 descriptor(crate::builtins::io::json::jsonencode::JSONENCODE_DESCRIPTOR),
390 extensions(crate::builtins::io::json::jsonencode::JSONENCODE_EXTENSIONS),
391 integer_capabilities(crate::builtins::io::json::jsonencode::JSONENCODE_INTEGER_CAPABILITIES),
392 builtin_path = "crate::builtins::io::json::jsonencode"
393)]
394async fn jsonencode_builtin(value: Value, rest: Vec<Value>) -> crate::BuiltinResult<Value> {
395 validate_option_layout(&rest)?;
396 preflight_jsonencode_extensions(&value, &rest)?;
397 let host_value = gather_if_needed_async(&value)
398 .await
399 .map_err(jsonencode_flow_with_context)?;
400 let mut gathered_args = Vec::with_capacity(rest.len());
401 for value in &rest {
402 gathered_args.push(
403 gather_if_needed_async(value)
404 .await
405 .map_err(jsonencode_flow_with_context)?,
406 );
407 }
408
409 preflight_option_extensions(&gathered_args)?;
410 let options = parse_options(&gathered_args)?;
411 let json_value = value_to_json(&host_value, &options)?;
412 let json_string = render_json(&json_value, &options);
413
414 Ok(Value::CharArray(CharArray::new_row(&json_string)))
415}
416
417fn preflight_jsonencode_extensions(value: &Value, rest: &[Value]) -> BuiltinResult<()> {
418 if value_contains_complex(value) {
419 crate::compatibility::ensure_builtin_extension_enabled(
420 &JSONENCODE_COMPLEX_INPUT_EXTENSION,
421 BUILTIN_NAME,
422 )?;
423 }
424 if value_contains_sparse(value) {
425 crate::compatibility::ensure_builtin_extension_enabled(
426 &JSONENCODE_SPARSE_INPUT_EXTENSION,
427 BUILTIN_NAME,
428 )?;
429 }
430 preflight_option_extensions(rest)?;
431 if value_contains_explicit_gpu(value) || rest.iter().any(value_contains_explicit_gpu) {
432 crate::compatibility::ensure_builtin_extension_enabled(
433 &JSONENCODE_RESIDENT_INPUT_EXTENSION,
434 BUILTIN_NAME,
435 )?;
436 }
437 Ok(())
438}
439
440fn validate_option_layout(rest: &[Value]) -> BuiltinResult<()> {
441 if rest.is_empty() {
442 return Ok(());
443 }
444 if rest.len() == 1 {
445 return match &rest[0] {
446 Value::Struct(options) => {
447 for name in options.fields.keys() {
448 ensure_known_option(name)?;
449 }
450 Ok(())
451 }
452 _ => Err(jsonencode_error(&JSONENCODE_ERROR_OPTIONS_CONFIG)),
453 };
454 }
455 if !rest.len().is_multiple_of(2) {
456 return Err(jsonencode_error(&JSONENCODE_ERROR_NAME_VALUE_PAIRS));
457 }
458 for pair in rest.chunks_exact(2) {
459 let name = option_name(&pair[0])?;
460 ensure_known_option(&name)?;
461 }
462 Ok(())
463}
464
465fn ensure_known_option(name: &str) -> BuiltinResult<()> {
466 if name.eq_ignore_ascii_case("PrettyPrint") || name.eq_ignore_ascii_case("ConvertInfAndNaN") {
467 Ok(())
468 } else {
469 Err(jsonencode_error_with(
470 &JSONENCODE_ERROR_UNKNOWN_OPTION,
471 format!("{} ('{}')", JSONENCODE_ERROR_UNKNOWN_OPTION.message, name),
472 ))
473 }
474}
475
476fn preflight_option_extensions(rest: &[Value]) -> BuiltinResult<()> {
477 for (name, option_value) in raw_options(rest)? {
478 if !option_value_is_scalar(option_value) {
479 return Err(jsonencode_error(&JSONENCODE_ERROR_OPTION_VALUE));
480 }
481 if is_typed_integer_option(option_value) {
482 crate::compatibility::ensure_builtin_extension_enabled(
483 &JSONENCODE_TYPED_INTEGER_OPTION_EXTENSION,
484 BUILTIN_NAME,
485 )?;
486 } else if is_floating_numeric_option(option_value)
487 && !(name.eq_ignore_ascii_case("ConvertInfAndNaN")
488 && is_documented_numeric_bool(option_value))
489 {
490 crate::compatibility::ensure_builtin_extension_enabled(
491 &JSONENCODE_NUMERIC_OPTION_EXTENSION,
492 BUILTIN_NAME,
493 )?;
494 } else if is_text_option(option_value) {
495 crate::compatibility::ensure_builtin_extension_enabled(
496 &JSONENCODE_TEXT_OPTION_EXTENSION,
497 BUILTIN_NAME,
498 )?;
499 }
500 }
501 Ok(())
502}
503
504fn raw_options(rest: &[Value]) -> BuiltinResult<Vec<(String, &Value)>> {
505 if let [Value::Struct(options)] = rest {
506 return Ok(options
507 .fields
508 .iter()
509 .map(|(name, value)| (name.clone(), value))
510 .collect());
511 }
512 rest.chunks_exact(2)
513 .map(|pair| option_name(&pair[0]).map(|name| (name, &pair[1])))
514 .collect()
515}
516
517fn option_value_is_scalar(value: &Value) -> bool {
518 match value {
519 Value::Bool(_) | Value::Int(_) | Value::Num(_) | Value::String(_) => true,
520 Value::Tensor(tensor) => tensor::is_scalar_tensor(tensor),
521 Value::LogicalArray(logical) => logical.data.len() == 1,
522 Value::CharArray(chars) => chars.rows == 1,
523 Value::StringArray(strings) => strings.data.len() == 1,
524 Value::GpuTensor(handle) => handle.shape.iter().copied().product::<usize>() == 1,
525 _ => false,
526 }
527}
528
529fn is_documented_numeric_bool(value: &Value) -> bool {
530 let number = match value {
531 Value::Num(number) => Some(*number),
532 Value::Tensor(tensor) if tensor::is_scalar_tensor(tensor) => {
533 Some(tensor::tensor_value_f64(tensor, 0))
534 }
535 _ => None,
536 };
537 matches!(number, Some(0.0 | 1.0))
538}
539
540fn is_typed_integer_option(value: &Value) -> bool {
541 match value {
542 Value::Int(_) => true,
543 Value::Tensor(tensor) => tensor.integer_storage().is_some(),
544 Value::GpuTensor(handle) => runmat_accelerate_api::handle_integer_type(handle).is_some(),
545 _ => false,
546 }
547}
548
549fn is_floating_numeric_option(value: &Value) -> bool {
550 match value {
551 Value::Num(_) => true,
552 Value::Tensor(tensor) => tensor.integer_storage().is_none(),
553 Value::GpuTensor(_) => false,
554 _ => false,
555 }
556}
557
558fn is_text_option(value: &Value) -> bool {
559 matches!(
560 value,
561 Value::String(_) | Value::StringArray(_) | Value::CharArray(_)
562 )
563}
564
565fn value_contains_complex(value: &Value) -> bool {
566 match value {
567 Value::Complex(_, _) | Value::ComplexTensor(_) => true,
568 Value::GpuTensor(handle) => {
569 runmat_accelerate_api::handle_storage(handle)
570 != runmat_accelerate_api::GpuTensorStorage::Real
571 }
572 Value::Cell(cell) => cell.data.iter().any(value_contains_complex),
573 Value::Struct(struct_value) => struct_value.fields.values().any(value_contains_complex),
574 Value::Object(object) => object.properties.values().any(value_contains_complex),
575 _ => false,
576 }
577}
578
579fn value_contains_sparse(value: &Value) -> bool {
580 match value {
581 Value::SparseTensor(_) => true,
582 Value::Cell(cell) => cell.data.iter().any(value_contains_sparse),
583 Value::Struct(value) => value.fields.values().any(value_contains_sparse),
584 Value::Object(value) => value.properties.values().any(value_contains_sparse),
585 _ => false,
586 }
587}
588
589fn value_contains_explicit_gpu(value: &Value) -> bool {
590 match value {
591 Value::GpuTensor(handle) => runmat_accelerate_api::handle_is_explicit(handle),
592 Value::Cell(cell) => cell.data.iter().any(value_contains_explicit_gpu),
593 Value::Struct(value) => value.fields.values().any(value_contains_explicit_gpu),
594 Value::Object(value) => value.properties.values().any(value_contains_explicit_gpu),
595 _ => false,
596 }
597}
598
599fn parse_options(args: &[Value]) -> BuiltinResult<JsonEncodeOptions> {
600 let mut options = JsonEncodeOptions::default();
601 if args.is_empty() {
602 return Ok(options);
603 }
604
605 if args.len() == 1 {
606 if let Value::Struct(struct_value) = &args[0] {
607 apply_struct_options(struct_value, &mut options)?;
608 return Ok(options);
609 }
610 return Err(jsonencode_error(&JSONENCODE_ERROR_OPTIONS_CONFIG));
611 }
612
613 if !args.len().is_multiple_of(2) {
614 return Err(jsonencode_error(&JSONENCODE_ERROR_NAME_VALUE_PAIRS));
615 }
616
617 let mut idx = 0usize;
618 while idx < args.len() {
619 let name = option_name(&args[idx])?;
620 let value = &args[idx + 1];
621 apply_option(&name, value, &mut options)?;
622 idx += 2;
623 }
624
625 Ok(options)
626}
627
628fn apply_struct_options(
629 struct_value: &StructValue,
630 options: &mut JsonEncodeOptions,
631) -> BuiltinResult<()> {
632 for (key, value) in &struct_value.fields {
633 apply_option(key, value, options)?;
634 }
635 Ok(())
636}
637
638fn option_name(value: &Value) -> BuiltinResult<String> {
639 match value {
640 Value::String(s) => Ok(s.clone()),
641 Value::CharArray(ca) if ca.rows == 1 => Ok(ca.data.iter().collect()),
642 Value::StringArray(sa) if sa.data.len() == 1 => Ok(sa.data[0].clone()),
643 _ => Err(jsonencode_error(&JSONENCODE_ERROR_OPTION_NAME)),
644 }
645}
646
647fn apply_option(
648 raw_name: &str,
649 value: &Value,
650 options: &mut JsonEncodeOptions,
651) -> BuiltinResult<()> {
652 let lowered = raw_name.to_ascii_lowercase();
653 match lowered.as_str() {
654 "prettyprint" => {
655 options.pretty_print = coerce_bool(value)?;
656 Ok(())
657 }
658 "convertinfandnan" => {
659 options.convert_inf_and_nan = coerce_bool(value)?;
660 Ok(())
661 }
662 other => Err(jsonencode_error_with(
663 &JSONENCODE_ERROR_UNKNOWN_OPTION,
664 format!("{} ('{}')", JSONENCODE_ERROR_UNKNOWN_OPTION.message, other),
665 )),
666 }
667}
668
669fn coerce_bool(value: &Value) -> BuiltinResult<bool> {
670 match value {
671 Value::Bool(b) => Ok(*b),
672 Value::Int(i) => Ok(!i.is_zero()),
673 Value::Num(n) => bool_from_f64(*n),
674 Value::Tensor(t) => {
675 if tensor::is_scalar_tensor(t) {
676 if let Some(int) = t.integer_storage().and_then(|storage| storage.value_at(0)) {
677 return Ok(!int.is_zero());
678 }
679 bool_from_f64(tensor::tensor_value_f64(t, 0))
680 } else {
681 Err(jsonencode_error(&JSONENCODE_ERROR_OPTION_VALUE))
682 }
683 }
684 Value::LogicalArray(la) => match la.data.len() {
685 1 => Ok(la.data[0] != 0),
686 _ => Err(jsonencode_error(&JSONENCODE_ERROR_OPTION_VALUE)),
687 },
688 Value::CharArray(ca) if ca.rows == 1 => {
689 parse_bool_string(&ca.data.iter().collect::<String>())
690 }
691 Value::String(s) => parse_bool_string(s),
692 Value::StringArray(sa) if sa.data.len() == 1 => parse_bool_string(&sa.data[0]),
693 _ => Err(jsonencode_error(&JSONENCODE_ERROR_OPTION_VALUE)),
694 }
695}
696
697fn bool_from_f64(value: f64) -> BuiltinResult<bool> {
698 if value.is_finite() {
699 Ok(value != 0.0)
700 } else {
701 Err(jsonencode_error(&JSONENCODE_ERROR_OPTION_VALUE))
702 }
703}
704
705fn parse_bool_string(text: &str) -> BuiltinResult<bool> {
706 match text.trim().to_ascii_lowercase().as_str() {
707 "true" | "on" | "yes" | "1" => Ok(true),
708 "false" | "off" | "no" | "0" => Ok(false),
709 _ => Err(jsonencode_error(&JSONENCODE_ERROR_OPTION_VALUE)),
710 }
711}
712
713fn value_to_json(value: &Value, options: &JsonEncodeOptions) -> BuiltinResult<JsonValue> {
714 match value {
715 Value::Num(n) => number_to_json(*n, options),
716 Value::Int(i) => Ok(JsonValue::Number(int_to_number(i))),
717 Value::Bool(b) => Ok(JsonValue::Bool(*b)),
718 Value::LogicalArray(logical) => logical_array_to_json(logical, options),
719 Value::Tensor(tensor) => tensor_to_json(tensor, options),
720 Value::SparseTensor(sparse) => {
721 let total_elements = sparse.rows.checked_mul(sparse.cols).ok_or_else(|| {
722 jsonencode_error_with(
723 &JSONENCODE_ERROR_INTERNAL,
724 "jsonencode: sparse matrix dimensions overflow",
725 )
726 })?;
727 if total_elements > 10_000_000 {
728 return Err(jsonencode_error_with(
729 &JSONENCODE_ERROR_INTERNAL,
730 format!("jsonencode: cannot densify sparse tensor {}x{} ({} elements exceeds safe threshold)", sparse.rows, sparse.cols, total_elements),
731 ));
732 }
733 if sparse.is_logical() {
734 let dense = sparse.to_dense_logical().map_err(|err| {
735 jsonencode_error_with(&JSONENCODE_ERROR_INTERNAL, format!("jsonencode: {err}"))
736 })?;
737 return logical_array_to_json(&dense, options);
738 }
739 let dense = sparse.to_dense().map_err(|err| {
740 jsonencode_error_with(&JSONENCODE_ERROR_INTERNAL, format!("jsonencode: {err}"))
741 })?;
742 tensor_to_json(&dense, options)
743 }
744 Value::Complex(re, im) => complex_scalar_to_json(*re, *im, options),
745 Value::ComplexTensor(ct) => complex_tensor_to_json(ct, options),
746 Value::String(s) => Ok(JsonValue::String(s.clone())),
747 Value::Symbolic(expr) => Ok(JsonValue::String(expr.to_string())),
748 Value::SymbolicArray(array) => symbolic_array_to_json(array, options),
749 Value::StringArray(sa) => string_array_to_json(sa, options),
750 Value::CharArray(ca) => char_array_to_json(ca, options),
751 Value::Struct(sv) => struct_to_json(sv, options),
752 Value::Cell(ca) => cell_array_to_json(ca, options),
753 Value::ObjectArray(array) => array
754 .data()
755 .iter()
756 .map(|value| value_to_json(value, options))
757 .collect::<BuiltinResult<Vec<_>>>()
758 .map(JsonValue::Array),
759 Value::Object(obj) => object_to_json(obj, options),
760 Value::GpuTensor(_) => Err(jsonencode_error(&JSONENCODE_ERROR_UNEXPECTED_GPU)),
761 Value::HandleObject(_)
762 | Value::Listener(_)
763 | Value::FunctionHandle(_)
764 | Value::ExternalFunctionHandle(_)
765 | Value::MethodFunctionHandle(_)
766 | Value::BoundFunctionHandle { .. }
767 | Value::Closure(_)
768 | Value::ClassRef(_)
769 | Value::MException(_)
770 | Value::Future(_)
771 | Value::Task(_)
772 | Value::Pool(_)
773 | Value::Job(_)
774 | Value::Foreign(_)
775 | Value::OutputList(_) => Err(jsonencode_error(&JSONENCODE_ERROR_UNSUPPORTED_TYPE)),
776 }
777}
778
779fn int_to_number(value: &IntValue) -> JsonNumber {
780 match value {
781 IntValue::I8(v) => JsonNumber::I64(*v as i64),
782 IntValue::I16(v) => JsonNumber::I64(*v as i64),
783 IntValue::I32(v) => JsonNumber::I64(*v as i64),
784 IntValue::I64(v) => JsonNumber::I64(*v),
785 IntValue::U8(v) => JsonNumber::U64(*v as u64),
786 IntValue::U16(v) => JsonNumber::U64(*v as u64),
787 IntValue::U32(v) => JsonNumber::U64(*v as u64),
788 IntValue::U64(v) => JsonNumber::U64(*v),
789 }
790}
791
792fn number_to_json(value: f64, options: &JsonEncodeOptions) -> BuiltinResult<JsonValue> {
793 if !value.is_finite() {
794 if options.convert_inf_and_nan {
795 return Ok(JsonValue::Null);
796 }
797 return Err(jsonencode_error(&JSONENCODE_ERROR_INF_NAN));
798 }
799 Ok(JsonValue::Number(JsonNumber::Float(value)))
800}
801
802fn logical_array_to_json(
803 logical: &LogicalArray,
804 _options: &JsonEncodeOptions,
805) -> BuiltinResult<JsonValue> {
806 let keep_dims = compute_keep_dims(&logical.shape, true);
807 if logical.shape.is_empty() || logical.data.is_empty() {
808 return Ok(JsonValue::Array(Vec::new()));
809 }
810 if keep_dims.is_empty() {
811 let first = logical.data.first().copied().unwrap_or(0) != 0;
812 return Ok(JsonValue::Bool(first));
813 }
814 build_strided_array(&logical.shape, &keep_dims, |offset| {
815 Ok(JsonValue::Bool(logical.data[offset] != 0))
816 })
817}
818
819fn tensor_to_json(tensor: &Tensor, options: &JsonEncodeOptions) -> BuiltinResult<JsonValue> {
820 if tensor::tensor_element_len(tensor) == 0 {
821 return Ok(JsonValue::Array(Vec::new()));
822 }
823 let keep_dims = compute_keep_dims(&tensor.shape, true);
824 if keep_dims.is_empty() {
825 return tensor_value_to_json(tensor, 0, options);
826 }
827 build_strided_array(&tensor.shape, &keep_dims, |offset| {
828 tensor_value_to_json(tensor, offset, options)
829 })
830}
831
832fn tensor_value_to_json(
833 tensor: &Tensor,
834 offset: usize,
835 options: &JsonEncodeOptions,
836) -> BuiltinResult<JsonValue> {
837 let value = tensor
838 .numeric_value_at(offset)
839 .expect("index within authoritative numeric storage");
840 match value.into_int_value() {
841 Some(value) => Ok(JsonValue::Number(integer_value_number(&value))),
842 None => number_to_json(value.materialize_f64(), options),
843 }
844}
845
846fn integer_value_number(value: &IntValue) -> JsonNumber {
847 match value {
848 IntValue::I8(value) => JsonNumber::I64(*value as i64),
849 IntValue::I16(value) => JsonNumber::I64(*value as i64),
850 IntValue::I32(value) => JsonNumber::I64(*value as i64),
851 IntValue::I64(value) => JsonNumber::I64(*value),
852 IntValue::U8(value) => JsonNumber::U64(*value as u64),
853 IntValue::U16(value) => JsonNumber::U64(*value as u64),
854 IntValue::U32(value) => JsonNumber::U64(*value as u64),
855 IntValue::U64(value) => JsonNumber::U64(*value),
856 }
857}
858
859fn integer_storage_number(storage: &IntegerStorage, offset: usize) -> JsonNumber {
860 integer_value_number(
861 &storage
862 .value_at(offset)
863 .expect("index within authoritative integer storage"),
864 )
865}
866
867fn complex_scalar_to_json(
868 real: f64,
869 imag: f64,
870 options: &JsonEncodeOptions,
871) -> BuiltinResult<JsonValue> {
872 let real_json = number_to_json(real, options)?;
873 let imag_json = number_to_json(imag, options)?;
874 Ok(JsonValue::Object(vec![
875 ("real".to_string(), real_json),
876 ("imag".to_string(), imag_json),
877 ]))
878}
879
880fn complex_tensor_to_json(
881 ct: &ComplexTensor,
882 options: &JsonEncodeOptions,
883) -> BuiltinResult<JsonValue> {
884 if tensor::complex_tensor_element_len(ct) == 0 {
885 return Ok(JsonValue::Array(Vec::new()));
886 }
887 let keep_dims = compute_keep_dims(&ct.shape, true);
888 if keep_dims.is_empty() {
889 return complex_tensor_value_to_json(ct, 0, options);
890 }
891 build_strided_array(&ct.shape, &keep_dims, |offset| {
892 complex_tensor_value_to_json(ct, offset, options)
893 })
894}
895
896fn complex_tensor_value_to_json(
897 ct: &ComplexTensor,
898 offset: usize,
899 options: &JsonEncodeOptions,
900) -> BuiltinResult<JsonValue> {
901 if let Some(storage) = &ct.integer_storage() {
902 return Ok(JsonValue::Object(vec![
903 (
904 "real".to_string(),
905 JsonValue::Number(integer_storage_number(&storage.real, offset)),
906 ),
907 (
908 "imag".to_string(),
909 JsonValue::Number(integer_storage_number(&storage.imag, offset)),
910 ),
911 ]));
912 }
913
914 let (real, imag) = ct.materialize_f64()[offset];
915 complex_scalar_to_json(real, imag, options)
916}
917
918fn string_array_to_json(
919 sa: &StringArray,
920 _options: &JsonEncodeOptions,
921) -> BuiltinResult<JsonValue> {
922 if sa.data.is_empty() {
923 return Ok(JsonValue::Array(Vec::new()));
924 }
925 let keep_dims = compute_keep_dims(&sa.shape, true);
926 if keep_dims.is_empty() {
927 return Ok(JsonValue::String(sa.data[0].clone()));
928 }
929 build_strided_array(&sa.shape, &keep_dims, |offset| {
930 Ok(JsonValue::String(sa.data[offset].clone()))
931 })
932}
933
934fn symbolic_array_to_json(
935 array: &SymbolicArray,
936 _options: &JsonEncodeOptions,
937) -> BuiltinResult<JsonValue> {
938 if array.data.is_empty() {
939 return Ok(JsonValue::Array(Vec::new()));
940 }
941 let keep_dims = compute_keep_dims(&array.shape, true);
942 if keep_dims.is_empty() {
943 return Ok(JsonValue::String(array.data[0].to_string()));
944 }
945 build_strided_array(&array.shape, &keep_dims, |offset| {
946 Ok(JsonValue::String(array.data[offset].to_string()))
947 })
948}
949
950fn char_array_to_json(ca: &CharArray, _options: &JsonEncodeOptions) -> BuiltinResult<JsonValue> {
951 if ca.rows == 0 {
952 return Ok(JsonValue::Array(Vec::new()));
953 }
954
955 if ca.cols == 0 {
956 if ca.rows == 1 {
957 return Ok(JsonValue::String(String::new()));
958 }
959 let mut rows = Vec::with_capacity(ca.rows);
960 for _ in 0..ca.rows {
961 rows.push(JsonValue::String(String::new()));
962 }
963 return Ok(JsonValue::Array(rows));
964 }
965
966 if ca.rows == 1 {
967 return Ok(JsonValue::String(ca.data.iter().collect()));
968 }
969
970 let mut rows = Vec::with_capacity(ca.rows);
971 for r in 0..ca.rows {
972 let mut row_string = String::with_capacity(ca.cols);
973 for c in 0..ca.cols {
974 row_string.push(ca.data[r * ca.cols + c]);
975 }
976 rows.push(JsonValue::String(row_string));
977 }
978 Ok(JsonValue::Array(rows))
979}
980
981fn struct_to_json(sv: &StructValue, options: &JsonEncodeOptions) -> BuiltinResult<JsonValue> {
982 if sv.fields.is_empty() {
983 return Ok(JsonValue::Object(Vec::new()));
984 }
985 let mut map = BTreeMap::new();
986 for (key, value) in &sv.fields {
987 map.insert(key.clone(), value_to_json(value, options)?);
988 }
989 Ok(JsonValue::Object(map.into_iter().collect()))
990}
991
992fn object_to_json(obj: &ObjectInstance, options: &JsonEncodeOptions) -> BuiltinResult<JsonValue> {
993 let mut map = BTreeMap::new();
994 for (key, value) in &obj.properties {
995 map.insert(key.clone(), value_to_json(value, options)?);
996 }
997 Ok(JsonValue::Object(map.into_iter().collect()))
998}
999
1000fn cell_array_to_json(ca: &CellArray, options: &JsonEncodeOptions) -> BuiltinResult<JsonValue> {
1001 if ca.rows == 0 || ca.cols == 0 {
1002 return Ok(JsonValue::Array(Vec::new()));
1003 }
1004
1005 if ca.rows == 1 && ca.cols == 1 {
1006 let value = ca.get(0, 0).map_err(|e| {
1007 jsonencode_error_with(
1008 &JSONENCODE_ERROR_INTERNAL,
1009 format!("{} ({e})", JSONENCODE_ERROR_INTERNAL.message),
1010 )
1011 })?;
1012 return Ok(JsonValue::Array(vec![value_to_json(&value, options)?]));
1013 }
1014
1015 if ca.rows == 1 {
1016 let mut row = Vec::with_capacity(ca.cols);
1017 for c in 0..ca.cols {
1018 let element = ca.get(0, c).map_err(|e| {
1019 jsonencode_error_with(
1020 &JSONENCODE_ERROR_INTERNAL,
1021 format!("{} ({e})", JSONENCODE_ERROR_INTERNAL.message),
1022 )
1023 })?;
1024 row.push(value_to_json(&element, options)?);
1025 }
1026 return Ok(JsonValue::Array(row));
1027 }
1028
1029 if ca.cols == 1 {
1030 let mut column = Vec::with_capacity(ca.rows);
1031 for r in 0..ca.rows {
1032 let element = ca.get(r, 0).map_err(|e| {
1033 jsonencode_error_with(
1034 &JSONENCODE_ERROR_INTERNAL,
1035 format!("{} ({e})", JSONENCODE_ERROR_INTERNAL.message),
1036 )
1037 })?;
1038 column.push(value_to_json(&element, options)?);
1039 }
1040 return Ok(JsonValue::Array(column));
1041 }
1042
1043 let mut rows = Vec::with_capacity(ca.rows);
1044 for r in 0..ca.rows {
1045 let mut row = Vec::with_capacity(ca.cols);
1046 for c in 0..ca.cols {
1047 let element = ca.get(r, c).map_err(|e| {
1048 jsonencode_error_with(
1049 &JSONENCODE_ERROR_INTERNAL,
1050 format!("{} ({e})", JSONENCODE_ERROR_INTERNAL.message),
1051 )
1052 })?;
1053 row.push(value_to_json(&element, options)?);
1054 }
1055 rows.push(JsonValue::Array(row));
1056 }
1057 Ok(JsonValue::Array(rows))
1058}
1059
1060fn compute_keep_dims(shape: &[usize], drop_singletons: bool) -> Vec<usize> {
1061 let mut keep = Vec::new();
1062 for (idx, &size) in shape.iter().enumerate() {
1063 if size != 1 || !drop_singletons {
1064 keep.push(idx);
1065 }
1066 }
1067 keep
1068}
1069
1070fn compute_strides(shape: &[usize]) -> Vec<usize> {
1071 let mut strides = Vec::with_capacity(shape.len());
1072 let mut acc = 1usize;
1073 for &size in shape {
1074 strides.push(acc);
1075 acc = acc.saturating_mul(size.max(1));
1076 }
1077 strides
1078}
1079
1080fn build_strided_array<F>(
1081 shape: &[usize],
1082 keep_dims: &[usize],
1083 mut fetch: F,
1084) -> BuiltinResult<JsonValue>
1085where
1086 F: FnMut(usize) -> BuiltinResult<JsonValue>,
1087{
1088 if keep_dims.is_empty() {
1089 return fetch(0);
1090 }
1091 if keep_dims.iter().any(|&idx| shape[idx] == 0) {
1092 return Ok(JsonValue::Array(Vec::new()));
1093 }
1094 let strides = compute_strides(shape);
1095 let dims: Vec<usize> = keep_dims.iter().map(|&idx| shape[idx]).collect();
1096 build_nd_array(&dims, |indices| {
1097 let mut offset = 0usize;
1098 for (value, dim_idx) in indices.iter().zip(keep_dims.iter()) {
1099 offset += value * strides[*dim_idx];
1100 }
1101 fetch(offset)
1102 })
1103}
1104
1105fn build_nd_array<F>(dims: &[usize], mut fetch: F) -> BuiltinResult<JsonValue>
1106where
1107 F: FnMut(&[usize]) -> BuiltinResult<JsonValue>,
1108{
1109 if dims.is_empty() {
1110 return fetch(&[]);
1111 }
1112 if dims[0] == 0 {
1113 return Ok(JsonValue::Array(Vec::new()));
1114 }
1115 let mut indices = vec![0usize; dims.len()];
1116 build_nd_array_recursive(dims, 0, &mut indices, &mut fetch)
1117}
1118
1119fn build_nd_array_recursive<F>(
1120 dims: &[usize],
1121 level: usize,
1122 indices: &mut [usize],
1123 fetch: &mut F,
1124) -> BuiltinResult<JsonValue>
1125where
1126 F: FnMut(&[usize]) -> BuiltinResult<JsonValue>,
1127{
1128 let size = dims[level];
1129 if size == 0 {
1130 return Ok(JsonValue::Array(Vec::new()));
1131 }
1132 if level + 1 == dims.len() {
1133 let mut items = Vec::with_capacity(size);
1134 for i in 0..size {
1135 indices[level] = i;
1136 items.push(fetch(indices)?);
1137 }
1138 return Ok(JsonValue::Array(items));
1139 }
1140 let mut items = Vec::with_capacity(size);
1141 for i in 0..size {
1142 indices[level] = i;
1143 items.push(build_nd_array_recursive(dims, level + 1, indices, fetch)?);
1144 }
1145 Ok(JsonValue::Array(items))
1146}
1147
1148fn render_json(value: &JsonValue, options: &JsonEncodeOptions) -> String {
1149 let mut writer = JsonWriter::new(options.pretty_print);
1150 writer.write_value(value);
1151 writer.finish()
1152}
1153
1154struct JsonWriter {
1155 output: String,
1156 pretty: bool,
1157 indent: usize,
1158}
1159
1160impl JsonWriter {
1161 fn new(pretty: bool) -> Self {
1162 Self {
1163 output: String::new(),
1164 pretty,
1165 indent: 0,
1166 }
1167 }
1168
1169 fn finish(self) -> String {
1170 self.output
1171 }
1172
1173 fn write_value(&mut self, value: &JsonValue) {
1174 match value {
1175 JsonValue::Null => self.output.push_str("null"),
1176 JsonValue::Bool(true) => self.output.push_str("true"),
1177 JsonValue::Bool(false) => self.output.push_str("false"),
1178 JsonValue::Number(number) => self.write_number(number),
1179 JsonValue::String(text) => {
1180 self.output.push('"');
1181 self.output.push_str(&escape_json_string(text));
1182 self.output.push('"');
1183 }
1184 JsonValue::Array(items) => self.write_array(items),
1185 JsonValue::Object(fields) => self.write_object(fields),
1186 }
1187 }
1188
1189 fn write_number(&mut self, number: &JsonNumber) {
1190 match number {
1191 JsonNumber::Float(f) => {
1192 if f.is_nan() || !f.is_finite() {
1193 self.output.push_str("null");
1194 } else {
1195 self.output.push_str(&format_number(*f));
1196 }
1197 }
1198 JsonNumber::I64(i) => {
1199 let _ = write!(self.output, "{i}");
1200 }
1201 JsonNumber::U64(u) => {
1202 let _ = write!(self.output, "{u}");
1203 }
1204 }
1205 }
1206
1207 fn write_array(&mut self, items: &[JsonValue]) {
1208 if items.is_empty() {
1209 self.output.push_str("[]");
1210 return;
1211 }
1212 let inline = if self.pretty {
1213 items.iter().all(|item| {
1214 matches!(
1215 item,
1216 JsonValue::Null
1217 | JsonValue::Bool(_)
1218 | JsonValue::Number(_)
1219 | JsonValue::String(_)
1220 )
1221 })
1222 } else {
1223 false
1224 };
1225 if inline {
1226 self.output.push('[');
1227 for (index, item) in items.iter().enumerate() {
1228 self.write_value(item);
1229 if index + 1 < items.len() {
1230 self.output.push(',');
1231 }
1232 }
1233 self.output.push(']');
1234 return;
1235 }
1236 self.output.push('[');
1237 if self.pretty {
1238 self.output.push('\n');
1239 self.indent += 1;
1240 }
1241 for (index, item) in items.iter().enumerate() {
1242 if self.pretty {
1243 self.write_indent();
1244 }
1245 self.write_value(item);
1246 if index + 1 < items.len() {
1247 if self.pretty {
1248 self.output.push_str(",\n");
1249 } else {
1250 self.output.push(',');
1251 }
1252 }
1253 }
1254 if self.pretty {
1255 self.output.push('\n');
1256 if self.indent > 0 {
1257 self.indent -= 1;
1258 }
1259 self.write_indent();
1260 }
1261 self.output.push(']');
1262 }
1263
1264 fn write_object(&mut self, fields: &[(String, JsonValue)]) {
1265 if fields.is_empty() {
1266 self.output.push_str("{}");
1267 return;
1268 }
1269 self.output.push('{');
1270 if self.pretty {
1271 self.output.push('\n');
1272 self.indent += 1;
1273 }
1274 for (index, (key, value)) in fields.iter().enumerate() {
1275 if self.pretty {
1276 self.write_indent();
1277 }
1278 self.output.push('"');
1279 self.output.push_str(&escape_json_string(key));
1280 self.output.push('"');
1281 if self.pretty {
1282 self.output.push_str(": ");
1283 } else {
1284 self.output.push(':');
1285 }
1286 self.write_value(value);
1287 if index + 1 < fields.len() {
1288 if self.pretty {
1289 self.output.push_str(",\n");
1290 } else {
1291 self.output.push(',');
1292 }
1293 }
1294 }
1295 if self.pretty {
1296 self.output.push('\n');
1297 if self.indent > 0 {
1298 self.indent -= 1;
1299 }
1300 self.write_indent();
1301 }
1302 self.output.push('}');
1303 }
1304
1305 fn write_indent(&mut self) {
1306 if self.pretty {
1307 for _ in 0..self.indent {
1308 self.output.push_str(" ");
1309 }
1310 }
1311 }
1312}
1313
1314fn escape_json_string(value: &str) -> String {
1315 let mut escaped = String::with_capacity(value.len());
1316 for ch in value.chars() {
1317 match ch {
1318 '"' => escaped.push_str("\\\""),
1319 '\\' => escaped.push_str("\\\\"),
1320 '\u{08}' => escaped.push_str("\\b"),
1321 '\u{0C}' => escaped.push_str("\\f"),
1322 '\n' => escaped.push_str("\\n"),
1323 '\r' => escaped.push_str("\\r"),
1324 '\t' => escaped.push_str("\\t"),
1325 c if (c as u32) < 0x20 => {
1326 let _ = write!(escaped, "\\u{:04X}", c as u32);
1327 }
1328 _ => escaped.push(ch),
1329 }
1330 }
1331 escaped
1332}
1333
1334fn format_number(value: f64) -> String {
1335 if value.fract() == 0.0 {
1336 format!("{:.0}", value)
1338 } else {
1339 format!("{}", value)
1340 }
1341}
1342
1343#[cfg(test)]
1344pub(crate) mod tests {
1345 use super::*;
1346 use crate::builtins::common::test_support;
1347 use futures::executor::block_on;
1348 use runmat_value::{
1349 CellArray, CharArray, ComplexTensor, LogicalArray, StringArray, StructValue, SymbolicArray,
1350 SymbolicExpr, Tensor,
1351 };
1352
1353 fn as_string(value: Value) -> String {
1354 match value {
1355 Value::CharArray(ca) => ca.data.iter().collect(),
1356 Value::String(s) => s,
1357 other => panic!("expected char array, got {:?}", other),
1358 }
1359 }
1360
1361 fn error_message(err: crate::RuntimeError) -> String {
1362 err.message().to_string()
1363 }
1364
1365 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1366 #[test]
1367 fn jsonencode_descriptor_signatures_cover_core_forms() {
1368 let labels: Vec<&str> = JSONENCODE_DESCRIPTOR
1369 .signatures
1370 .iter()
1371 .map(|sig| sig.label)
1372 .collect();
1373 assert!(labels.contains(&"jsonText = jsonencode(value)"));
1374 assert!(labels.contains(&"jsonText = jsonencode(value, options)"));
1375 assert!(labels.contains(&"jsonText = jsonencode(value, name, optionValue)"));
1376 assert!(labels.contains(&"jsonText = jsonencode(value, nameValuePairs...)"));
1377 }
1378
1379 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1380 #[test]
1381 fn jsonencode_scalar_double() {
1382 let encoded =
1383 block_on(jsonencode_builtin(Value::Num(5.0), Vec::new())).expect("jsonencode");
1384 assert_eq!(as_string(encoded), "5");
1385 }
1386
1387 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1388 #[test]
1389 fn jsonencode_symbolic_value_as_text() {
1390 let expr = SymbolicExpr::div_expr(
1391 SymbolicExpr::function(
1392 runmat_value::SymbolicFunction::Sin,
1393 SymbolicExpr::variable("x"),
1394 ),
1395 SymbolicExpr::variable("x"),
1396 );
1397 let encoded =
1398 block_on(jsonencode_builtin(Value::Symbolic(expr), Vec::new())).expect("jsonencode");
1399
1400 assert_eq!(as_string(encoded), "\"sin(x)/x\"");
1401 }
1402
1403 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1404 #[test]
1405 fn jsonencode_symbolic_array_preserves_matrix_shape() {
1406 let array = SymbolicArray::new(
1407 vec![
1408 SymbolicExpr::variable("a"),
1409 SymbolicExpr::variable("c"),
1410 SymbolicExpr::variable("b"),
1411 SymbolicExpr::variable("d"),
1412 ],
1413 vec![2, 2],
1414 )
1415 .expect("symbolic array");
1416
1417 let encoded = block_on(jsonencode_builtin(Value::SymbolicArray(array), Vec::new()))
1418 .expect("jsonencode");
1419
1420 assert_eq!(as_string(encoded), "[[\"a\",\"b\"],[\"c\",\"d\"]]");
1421 }
1422
1423 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1424 #[test]
1425 fn jsonencode_matrix_pretty_print() {
1426 let tensor = Tensor::new(vec![1.0, 4.0, 2.0, 5.0, 3.0, 6.0], vec![2, 3]).expect("tensor");
1427 let args = vec![Value::from("PrettyPrint"), Value::Bool(true)];
1428 let encoded =
1429 block_on(jsonencode_builtin(Value::Tensor(tensor), args)).expect("jsonencode");
1430 let expected = "[\n [1,2,3],\n [4,5,6]\n]";
1431 assert_eq!(as_string(encoded), expected);
1432 }
1433
1434 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1435 #[test]
1436 fn jsonencode_integer_tensor_preserves_exact_uint64_values() {
1437 let tensor =
1438 Tensor::new_integer(IntegerStorage::U64(vec![u64::MAX, 1_u64 << 63]), vec![1, 2])
1439 .expect("integer tensor");
1440
1441 let encoded =
1442 block_on(jsonencode_builtin(Value::Tensor(tensor), Vec::new())).expect("jsonencode");
1443
1444 assert_eq!(
1445 as_string(encoded),
1446 format!("[{},{}]", u64::MAX, 1_u64 << 63)
1447 );
1448 }
1449
1450 #[test]
1451 fn jsonencode_integer_tensor_serializes_all_eight_classes_exactly() {
1452 let cases = [
1453 (IntegerStorage::I8(vec![i8::MIN]), "-128"),
1454 (IntegerStorage::I16(vec![i16::MIN]), "-32768"),
1455 (IntegerStorage::I32(vec![i32::MIN]), "-2147483648"),
1456 (IntegerStorage::I64(vec![i64::MIN]), "-9223372036854775808"),
1457 (IntegerStorage::U8(vec![u8::MAX]), "255"),
1458 (IntegerStorage::U16(vec![u16::MAX]), "65535"),
1459 (IntegerStorage::U32(vec![u32::MAX]), "4294967295"),
1460 (IntegerStorage::U64(vec![u64::MAX]), "18446744073709551615"),
1461 ];
1462 for (storage, expected) in cases {
1463 let tensor = Tensor::new_integer(storage, vec![1, 1]).expect("integer tensor");
1464 let encoded = block_on(jsonencode_builtin(Value::Tensor(tensor), Vec::new()))
1465 .expect("jsonencode integer tensor");
1466 assert_eq!(as_string(encoded), expected);
1467 }
1468 }
1469
1470 #[test]
1471 fn jsonencode_real_integer_payload_remains_public_and_exact_in_strict_mode() {
1472 let _strict = crate::compatibility::push_runmat_extensions_enabled(false);
1473 let cases = [
1474 (Value::Int(IntValue::I8(i8::MIN)), "-128"),
1475 (Value::Int(IntValue::I16(i16::MIN)), "-32768"),
1476 (Value::Int(IntValue::I32(i32::MIN)), "-2147483648"),
1477 (Value::Int(IntValue::I64(i64::MIN)), "-9223372036854775808"),
1478 (Value::Int(IntValue::U8(u8::MAX)), "255"),
1479 (Value::Int(IntValue::U16(u16::MAX)), "65535"),
1480 (Value::Int(IntValue::U32(u32::MAX)), "4294967295"),
1481 (Value::Int(IntValue::U64(u64::MAX)), "18446744073709551615"),
1482 ];
1483 for (value, expected) in cases {
1484 let encoded = block_on(jsonencode_builtin(value, Vec::new()))
1485 .expect("documented real integer payload");
1486 assert_eq!(as_string(encoded), expected);
1487 }
1488 }
1489
1490 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1491 #[test]
1492 fn jsonencode_struct_round_trip() {
1493 let mut fields = StructValue::new();
1494 fields
1495 .fields
1496 .insert("name".to_string(), Value::from("RunMat"));
1497 fields
1498 .fields
1499 .insert("year".to_string(), Value::Int(IntValue::I32(2025)));
1500 let encoded =
1501 block_on(jsonencode_builtin(Value::Struct(fields), Vec::new())).expect("jsonencode");
1502 assert_eq!(as_string(encoded), "{\"name\":\"RunMat\",\"year\":2025}");
1503 }
1504
1505 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1506 #[test]
1507 fn jsonencode_struct_options_enable_pretty_print() {
1508 let tensor = Tensor::new(vec![1.0, 4.0, 2.0, 5.0], vec![2, 2]).expect("tensor");
1509 let mut opts = StructValue::new();
1510 opts.fields
1511 .insert("PrettyPrint".to_string(), Value::Bool(true));
1512 let encoded = block_on(jsonencode_builtin(
1513 Value::Tensor(tensor),
1514 vec![Value::Struct(opts)],
1515 ))
1516 .expect("jsonencode");
1517 let expected = "[\n [1,2],\n [4,5]\n]";
1518 assert_eq!(as_string(encoded), expected);
1519 }
1520
1521 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1522 #[test]
1523 fn jsonencode_options_accept_scalar_tensor_bool() {
1524 let _runmat = crate::compatibility::push_runmat_extensions_enabled(true);
1525 let tensor_value = Tensor::new(vec![1.0], vec![1, 1]).expect("tensor");
1526 let args = vec![Value::from("PrettyPrint"), Value::Tensor(tensor_value)];
1527 let encoded = block_on(jsonencode_builtin(Value::Num(42.0), args)).expect("jsonencode");
1528 assert_eq!(as_string(encoded), "42");
1529 }
1530
1531 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1532 #[test]
1533 fn jsonencode_options_read_wide_uint64_storage_exactly() {
1534 let _runmat = crate::compatibility::push_runmat_extensions_enabled(true);
1535 let false_option =
1536 Tensor::new_integer(IntegerStorage::U8(vec![0]), vec![1, 1]).expect("integer tensor");
1537 let compact = block_on(jsonencode_builtin(
1538 Value::Tensor(Tensor::new(vec![1.0, 2.0], vec![1, 2]).expect("tensor")),
1539 vec![Value::from("PrettyPrint"), Value::Tensor(false_option)],
1540 ))
1541 .expect("jsonencode");
1542 assert_eq!(as_string(compact), "[1,2]");
1543
1544 let true_option = Tensor::new_integer(IntegerStorage::U64(vec![u64::MAX]), vec![1, 1])
1545 .expect("integer tensor");
1546 let pretty = block_on(jsonencode_builtin(
1547 Value::Tensor(Tensor::new(vec![1.0, 3.0, 2.0, 4.0], vec![2, 2]).expect("tensor")),
1548 vec![Value::from("PrettyPrint"), Value::Tensor(true_option)],
1549 ))
1550 .expect("jsonencode");
1551 assert_eq!(as_string(pretty), "[\n [1,2],\n [3,4]\n]");
1552 }
1553
1554 #[test]
1555 fn jsonencode_option_coercions_are_independently_gated() {
1556 let cases = [
1557 (
1558 Value::Int(IntValue::U64(u64::MAX)),
1559 JSONENCODE_TYPED_INTEGER_OPTION_EXTENSION.error_identifier,
1560 ),
1561 (
1562 Value::Num(1.0),
1563 JSONENCODE_NUMERIC_OPTION_EXTENSION.error_identifier,
1564 ),
1565 (
1566 Value::from("true"),
1567 JSONENCODE_TEXT_OPTION_EXTENSION.error_identifier,
1568 ),
1569 ];
1570 for (option, expected_identifier) in cases {
1571 let _strict = crate::compatibility::push_runmat_extensions_enabled(false);
1572 let error = block_on(jsonencode_builtin(
1573 Value::Num(1.0),
1574 vec![Value::from("PrettyPrint"), option],
1575 ))
1576 .expect_err("option coercion must be gated");
1577 assert_eq!(error.identifier(), expected_identifier);
1578 }
1579 }
1580
1581 #[test]
1582 fn jsonencode_documented_convert_inf_numeric_bool_remains_public() {
1583 let _strict = crate::compatibility::push_runmat_extensions_enabled(false);
1584 let encoded = block_on(jsonencode_builtin(
1585 Value::Num(1.0),
1586 vec![Value::from("ConvertInfAndNaN"), Value::Num(1.0)],
1587 ))
1588 .expect("documented numeric one");
1589 assert_eq!(as_string(encoded), "1");
1590
1591 let error = block_on(jsonencode_builtin(
1592 Value::Num(f64::NAN),
1593 vec![Value::from("ConvertInfAndNaN"), Value::Num(0.0)],
1594 ))
1595 .expect_err("documented numeric zero disables conversion");
1596 assert_eq!(error.message(), JSONENCODE_ERROR_INF_NAN.message);
1597 }
1598
1599 #[test]
1600 fn jsonencode_option_errors_precede_irrelevant_extension_gates() {
1601 let _strict = crate::compatibility::push_runmat_extensions_enabled(false);
1602 let unknown = block_on(jsonencode_builtin(
1603 Value::Num(1.0),
1604 vec![Value::from("Unknown"), Value::Num(1.0)],
1605 ))
1606 .expect_err("unknown name must reject before numeric coercion gate");
1607 assert!(unknown
1608 .message()
1609 .starts_with(JSONENCODE_ERROR_UNKNOWN_OPTION.message));
1610
1611 let nonscalar = Tensor::new(vec![1.0, 0.0], vec![1, 2]).expect("tensor");
1612 let invalid = block_on(jsonencode_builtin(
1613 Value::Num(1.0),
1614 vec![Value::from("PrettyPrint"), Value::Tensor(nonscalar)],
1615 ))
1616 .expect_err("nonscalar value must reject before numeric coercion gate");
1617 assert_eq!(invalid.message(), JSONENCODE_ERROR_OPTION_VALUE.message);
1618 }
1619
1620 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1621 #[test]
1622 fn jsonencode_options_reject_non_scalar_tensor_bool() {
1623 let tensor = Tensor::new(vec![1.0, 0.0], vec![1, 2]).expect("tensor");
1624 let err = block_on(jsonencode_builtin(
1625 Value::Num(1.0),
1626 vec![Value::from("PrettyPrint"), Value::Tensor(tensor)],
1627 ))
1628 .expect_err("expected failure");
1629 assert_eq!(error_message(err), JSONENCODE_ERROR_OPTION_VALUE.message);
1630 }
1631
1632 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1633 #[test]
1634 fn jsonencode_options_accept_scalar_logical_array() {
1635 let logical = LogicalArray::new(vec![1], vec![1]).expect("logical");
1636 let args = vec![Value::from("PrettyPrint"), Value::LogicalArray(logical)];
1637 let encoded = block_on(jsonencode_builtin(Value::Num(7.0), args)).expect("jsonencode");
1638 assert_eq!(as_string(encoded), "7");
1639 }
1640
1641 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1642 #[test]
1643 fn jsonencode_convert_inf_and_nan_controls_null_output() {
1644 let tensor = Tensor::new(vec![1.0, f64::NAN], vec![1, 2]).expect("tensor");
1645 let encoded = block_on(jsonencode_builtin(
1646 Value::Tensor(tensor.clone()),
1647 Vec::new(),
1648 ))
1649 .expect("jsonencode");
1650 assert_eq!(as_string(encoded), "[1,null]");
1651
1652 let err = block_on(jsonencode_builtin(
1653 Value::Tensor(tensor),
1654 vec![Value::from("ConvertInfAndNaN"), Value::Bool(false)],
1655 ))
1656 .expect_err("expected failure");
1657 assert_eq!(error_message(err), JSONENCODE_ERROR_INF_NAN.message);
1658 }
1659
1660 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1661 #[test]
1662 fn jsonencode_cell_array() {
1663 let elements = vec![Value::from(1.0), Value::from("two")];
1664 let cell = CellArray::new(elements, 1, 2).expect("cell");
1665 let encoded =
1666 block_on(jsonencode_builtin(Value::Cell(cell), Vec::new())).expect("jsonencode");
1667 assert_eq!(as_string(encoded), "[1,\"two\"]");
1668 }
1669
1670 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1671 #[test]
1672 fn jsonencode_char_array_zero_rows_is_empty_array() {
1673 let chars = CharArray::new(Vec::new(), 0, 3).expect("char array");
1674 let encoded =
1675 block_on(jsonencode_builtin(Value::CharArray(chars), Vec::new())).expect("jsonencode");
1676 assert_eq!(as_string(encoded), "[]");
1677 }
1678
1679 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1680 #[test]
1681 fn jsonencode_char_array_empty_strings_per_row() {
1682 let chars = CharArray::new(Vec::new(), 2, 0).expect("char array");
1683 let encoded =
1684 block_on(jsonencode_builtin(Value::CharArray(chars), Vec::new())).expect("jsonencode");
1685 let encoded_str = as_string(encoded);
1686 assert_eq!(encoded_str, "[\"\",\"\"]");
1687 }
1688
1689 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1690 #[test]
1691 fn jsonencode_string_array_matrix() {
1692 let sa = StringArray::new(vec!["alpha".to_string(), "beta".to_string()], vec![2, 1])
1693 .expect("string array");
1694 let encoded =
1695 block_on(jsonencode_builtin(Value::StringArray(sa), Vec::new())).expect("jsonencode");
1696 assert_eq!(as_string(encoded), "[\"alpha\",\"beta\"]");
1697 }
1698
1699 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1700 #[test]
1701 fn jsonencode_complex_tensor_outputs_objects() {
1702 let _runmat = crate::compatibility::push_runmat_extensions_enabled(true);
1703 let ct = ComplexTensor::new(vec![(1.0, 2.0), (3.5, -4.0)], vec![2, 1]).expect("complex");
1704 let encoded =
1705 block_on(jsonencode_builtin(Value::ComplexTensor(ct), Vec::new())).expect("jsonencode");
1706 assert_eq!(
1707 as_string(encoded),
1708 "[{\"real\":1,\"imag\":2},{\"real\":3.5,\"imag\":-4}]"
1709 );
1710 }
1711
1712 #[test]
1713 fn jsonencode_typed_complex_integers_preserves_every_class_exactly() {
1714 let _runmat = crate::compatibility::push_runmat_extensions_enabled(true);
1715 let cases = [
1716 (
1717 "int8",
1718 IntegerStorage::I8(vec![-1]),
1719 IntegerStorage::I8(vec![2]),
1720 "{\"real\":-1,\"imag\":2}",
1721 ),
1722 (
1723 "int16",
1724 IntegerStorage::I16(vec![-3]),
1725 IntegerStorage::I16(vec![4]),
1726 "{\"real\":-3,\"imag\":4}",
1727 ),
1728 (
1729 "int32",
1730 IntegerStorage::I32(vec![-5]),
1731 IntegerStorage::I32(vec![6]),
1732 "{\"real\":-5,\"imag\":6}",
1733 ),
1734 (
1735 "int64",
1736 IntegerStorage::I64(vec![-9223372036854775808]),
1737 IntegerStorage::I64(vec![9223372036854775807]),
1738 "{\"real\":-9223372036854775808,\"imag\":9223372036854775807}",
1739 ),
1740 (
1741 "uint8",
1742 IntegerStorage::U8(vec![1]),
1743 IntegerStorage::U8(vec![2]),
1744 "{\"real\":1,\"imag\":2}",
1745 ),
1746 (
1747 "uint16",
1748 IntegerStorage::U16(vec![3]),
1749 IntegerStorage::U16(vec![4]),
1750 "{\"real\":3,\"imag\":4}",
1751 ),
1752 (
1753 "uint32",
1754 IntegerStorage::U32(vec![5]),
1755 IntegerStorage::U32(vec![6]),
1756 "{\"real\":5,\"imag\":6}",
1757 ),
1758 (
1759 "uint64",
1760 IntegerStorage::U64(vec![u64::MAX]),
1761 IntegerStorage::U64(vec![1_u64 << 63]),
1762 "{\"real\":18446744073709551615,\"imag\":9223372036854775808}",
1763 ),
1764 ];
1765
1766 for (class, real, imag, expected) in cases {
1767 let storage = runmat_value::IntegerComplexStorage::new(real, imag)
1768 .expect("matching integer components");
1769 let tensor = ComplexTensor::new_integer(storage, vec![1, 1]).expect("typed complex");
1770 let encoded = block_on(jsonencode_builtin(Value::ComplexTensor(tensor), Vec::new()))
1771 .expect("jsonencode typed complex integer");
1772 assert_eq!(as_string(encoded), expected, "{class}");
1773 }
1774 }
1775
1776 #[test]
1777 fn jsonencode_complex_input_is_rejected_by_compatibility_gate() {
1778 let _strict = crate::compatibility::push_runmat_extensions_enabled(false);
1779 let error = block_on(jsonencode_builtin(Value::Complex(1.0, 2.0), Vec::new()))
1780 .expect_err("complex encoding is a RunMat extension");
1781 assert_eq!(
1782 error.identifier(),
1783 JSONENCODE_COMPLEX_INPUT_EXTENSION.error_identifier
1784 );
1785 }
1786
1787 #[test]
1788 fn jsonencode_nested_complex_and_sparse_inputs_are_compatibility_gated() {
1789 let _strict = crate::compatibility::push_runmat_extensions_enabled(false);
1790 let complex_cell = CellArray::new(vec![Value::Complex(1.0, 2.0)], 1, 1).expect("cell");
1791 let complex_error = block_on(jsonencode_builtin(Value::Cell(complex_cell), Vec::new()))
1792 .expect_err("nested complex input is an extension");
1793 assert_eq!(
1794 complex_error.identifier(),
1795 JSONENCODE_COMPLEX_INPUT_EXTENSION.error_identifier
1796 );
1797
1798 let sparse =
1799 runmat_value::SparseTensor::new(1, 1, vec![0, 1], vec![0], vec![1.0]).expect("sparse");
1800 let mut fields = StructValue::new();
1801 fields
1802 .fields
1803 .insert("value".to_string(), Value::SparseTensor(sparse));
1804 let sparse_error = block_on(jsonencode_builtin(Value::Struct(fields), Vec::new()))
1805 .expect_err("nested sparse input is an extension");
1806 assert_eq!(
1807 sparse_error.identifier(),
1808 JSONENCODE_SPARSE_INPUT_EXTENSION.error_identifier
1809 );
1810 }
1811
1812 #[test]
1813 fn jsonencode_resident_gate_precedes_provider_access() {
1814 let _strict = crate::compatibility::push_runmat_extensions_enabled(false);
1815 let handle = runmat_accelerate_api::GpuTensorHandle {
1816 shape: vec![1, 1],
1817 device_id: u32::MAX,
1818 buffer_id: u64::MAX,
1819 descriptor: Default::default(),
1820 };
1821 let handle = handle.with_provenance(runmat_accelerate_api::GpuHandleProvenance::Explicit);
1822 let error = block_on(jsonencode_builtin(
1823 Value::GpuTensor(handle.clone()),
1824 Vec::new(),
1825 ))
1826 .expect_err("resident extension must gate before owner lookup");
1827 runmat_accelerate_api::clear_handle_metadata(&handle);
1828 assert_eq!(
1829 error.identifier(),
1830 JSONENCODE_RESIDENT_INPUT_EXTENSION.error_identifier
1831 );
1832 }
1833
1834 #[test]
1835 fn jsonencode_integer_metadata_distinguishes_public_real_and_extended_complex() {
1836 assert_eq!(JSONENCODE_INTEGER_CAPABILITIES.len(), 3);
1837 assert_eq!(
1838 JSONENCODE_INTEGER_CAPABILITIES[0].inputs[0].availability,
1839 BuiltinIntegerInputAvailability::Documented
1840 );
1841 assert_eq!(
1842 JSONENCODE_INTEGER_CAPABILITIES[1].inputs[0].availability,
1843 BuiltinIntegerInputAvailability::RunMatOnly
1844 );
1845 assert_eq!(
1846 JSONENCODE_INTEGER_CAPABILITIES[2].inputs[0].availability,
1847 BuiltinIntegerInputAvailability::RunMatOnly
1848 );
1849 assert_eq!(JSONENCODE_EXTENSIONS.len(), 6);
1850 }
1851
1852 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1853 #[test]
1854 fn jsonencode_gpu_tensor_gathers_host_data() {
1855 test_support::with_test_provider(|provider| {
1856 let _strict = crate::compatibility::push_runmat_extensions_enabled(false);
1857 let tensor = Tensor::new(vec![1.0, 0.0, 0.0, 1.0], vec![2, 2]).expect("tensor");
1858 let view = runmat_accelerate_api::HostTensorView {
1859 data: &tensor.materialize_f64(),
1860 shape: &tensor.shape,
1861 };
1862 let handle = provider.upload(&view).expect("upload");
1863 let encoded = block_on(jsonencode_builtin(Value::GpuTensor(handle), Vec::new()))
1864 .expect("jsonencode");
1865 assert_eq!(as_string(encoded), "[[1,0],[0,1]]");
1866 });
1867 }
1868
1869 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1870 #[test]
1871 #[cfg(feature = "wgpu")]
1872 fn jsonencode_gpu_tensor_wgpu_gathers_host_data() {
1873 let ensure = runmat_accelerate::backend::wgpu::provider::ensure_wgpu_provider();
1874 let Some(_) = ensure.ok().flatten() else {
1875 return;
1877 };
1878 let provider = runmat_accelerate_api::provider().expect("wgpu provider");
1879 let tensor = Tensor::new(vec![1.0, 2.0, 3.0], vec![3, 1]).expect("tensor");
1880 let view = runmat_accelerate_api::HostTensorView {
1881 data: &tensor.materialize_f64(),
1882 shape: &tensor.shape,
1883 };
1884 let handle = provider.upload(&view).expect("upload");
1885 let encoded =
1886 block_on(jsonencode_builtin(Value::GpuTensor(handle), Vec::new())).expect("jsonencode");
1887 assert_eq!(as_string(encoded), "[1,2,3]");
1888 }
1889}