use std::collections::BTreeMap;
use std::fmt::Write as FmtWrite;
use runmat_builtins::{
CellArray, CharArray, ComplexTensor, IntValue, LogicalArray, ObjectInstance, StringArray,
StructValue, Tensor, Value,
};
use runmat_macros::runtime_builtin;
use crate::builtins::common::spec::{
BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
ReductionNaN, ResidencyPolicy, ShapeRequirements,
};
use crate::{build_runtime_error, gather_if_needed_async, BuiltinResult, RuntimeError};
const OPTION_NAME_ERROR: &str = "jsonencode: option names must be character vectors or strings";
const OPTION_VALUE_ERROR: &str = "jsonencode: option value must be scalar logical or numeric";
const INF_NAN_ERROR: &str = "jsonencode: ConvertInfAndNaN must be true to encode NaN or Inf values";
const UNSUPPORTED_TYPE_ERROR: &str =
"jsonencode: unsupported input type; expected numeric, logical, string, struct, cell, or object data";
#[allow(clippy::too_many_lines)]
#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::io::json::jsonencode")]
pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
name: "jsonencode",
op_kind: GpuOpKind::Custom("serialization"),
supported_precisions: &[],
broadcast: BroadcastSemantics::None,
provider_hooks: &[],
constant_strategy: ConstantStrategy::InlineLiteral,
residency: ResidencyPolicy::GatherImmediately,
nan_mode: ReductionNaN::Include,
two_pass_threshold: None,
workgroup_size: None,
accepts_nan_mode: false,
notes:
"Serialization sink that gathers GPU data to host memory before emitting UTF-8 JSON text.",
};
fn jsonencode_error(message: impl Into<String>) -> RuntimeError {
build_runtime_error(message)
.with_builtin("jsonencode")
.build()
}
fn jsonencode_flow_with_context(err: RuntimeError) -> RuntimeError {
let mut builder = build_runtime_error(err.message().to_string()).with_builtin("jsonencode");
if let Some(identifier) = err.identifier() {
builder = builder.with_identifier(identifier.to_string());
}
builder.with_source(err).build()
}
#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::io::json::jsonencode")]
pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
name: "jsonencode",
shape: ShapeRequirements::Any,
constant_strategy: ConstantStrategy::InlineLiteral,
elementwise: None,
reduction: None,
emits_nan: false,
notes: "jsonencode is a residency sink and never participates in fusion planning.",
};
#[derive(Debug, Clone)]
struct JsonEncodeOptions {
pretty_print: bool,
convert_inf_and_nan: bool,
}
impl Default for JsonEncodeOptions {
fn default() -> Self {
Self {
pretty_print: false,
convert_inf_and_nan: true,
}
}
}
#[derive(Debug, Clone)]
enum JsonValue {
Null,
Bool(bool),
Number(JsonNumber),
String(String),
Array(Vec<JsonValue>),
Object(Vec<(String, JsonValue)>),
}
#[derive(Debug, Clone)]
enum JsonNumber {
Float(f64),
I64(i64),
U64(u64),
}
#[runtime_builtin(
name = "jsonencode",
category = "io/json",
summary = "Serialize MATLAB values to UTF-8 JSON text.",
keywords = "jsonencode,json,serialization,struct,gpu",
accel = "cpu",
type_resolver(crate::builtins::io::type_resolvers::jsonencode_type),
builtin_path = "crate::builtins::io::json::jsonencode"
)]
async fn jsonencode_builtin(value: Value, rest: Vec<Value>) -> crate::BuiltinResult<Value> {
let host_value = gather_if_needed_async(&value)
.await
.map_err(jsonencode_flow_with_context)?;
let mut gathered_args = Vec::with_capacity(rest.len());
for value in &rest {
gathered_args.push(
gather_if_needed_async(value)
.await
.map_err(jsonencode_flow_with_context)?,
);
}
let options = parse_options(&gathered_args)?;
let json_value = value_to_json(&host_value, &options)?;
let json_string = render_json(&json_value, &options);
Ok(Value::CharArray(CharArray::new_row(&json_string)))
}
fn parse_options(args: &[Value]) -> BuiltinResult<JsonEncodeOptions> {
let mut options = JsonEncodeOptions::default();
if args.is_empty() {
return Ok(options);
}
if args.len() == 1 {
if let Value::Struct(struct_value) = &args[0] {
apply_struct_options(struct_value, &mut options)?;
return Ok(options);
}
return Err(jsonencode_error(
"jsonencode: expected name/value pairs or options struct",
));
}
if !args.len().is_multiple_of(2) {
return Err(jsonencode_error(
"jsonencode: name/value pairs must come in pairs",
));
}
let mut idx = 0usize;
while idx < args.len() {
let name = option_name(&args[idx])?;
let value = &args[idx + 1];
apply_option(&name, value, &mut options)?;
idx += 2;
}
Ok(options)
}
fn apply_struct_options(
struct_value: &StructValue,
options: &mut JsonEncodeOptions,
) -> BuiltinResult<()> {
for (key, value) in &struct_value.fields {
apply_option(key, value, options)?;
}
Ok(())
}
fn option_name(value: &Value) -> BuiltinResult<String> {
match value {
Value::String(s) => Ok(s.clone()),
Value::CharArray(ca) if ca.rows == 1 => Ok(ca.data.iter().collect()),
Value::StringArray(sa) if sa.data.len() == 1 => Ok(sa.data[0].clone()),
_ => Err(jsonencode_error(OPTION_NAME_ERROR)),
}
}
fn apply_option(
raw_name: &str,
value: &Value,
options: &mut JsonEncodeOptions,
) -> BuiltinResult<()> {
let lowered = raw_name.to_ascii_lowercase();
match lowered.as_str() {
"prettyprint" => {
options.pretty_print = coerce_bool(value)?;
Ok(())
}
"convertinfandnan" => {
options.convert_inf_and_nan = coerce_bool(value)?;
Ok(())
}
other => Err(jsonencode_error(format!(
"jsonencode: unknown option '{}'",
other
))),
}
}
fn coerce_bool(value: &Value) -> BuiltinResult<bool> {
match value {
Value::Bool(b) => Ok(*b),
Value::Int(i) => Ok(i.to_i64() != 0),
Value::Num(n) => bool_from_f64(*n),
Value::Tensor(t) => {
if t.data.len() == 1 {
bool_from_f64(t.data[0])
} else {
Err(jsonencode_error(OPTION_VALUE_ERROR))
}
}
Value::LogicalArray(la) => match la.data.len() {
1 => Ok(la.data[0] != 0),
_ => Err(jsonencode_error(OPTION_VALUE_ERROR)),
},
Value::CharArray(ca) if ca.rows == 1 => {
parse_bool_string(&ca.data.iter().collect::<String>())
}
Value::String(s) => parse_bool_string(s),
Value::StringArray(sa) if sa.data.len() == 1 => parse_bool_string(&sa.data[0]),
_ => Err(jsonencode_error(OPTION_VALUE_ERROR)),
}
}
fn bool_from_f64(value: f64) -> BuiltinResult<bool> {
if value.is_finite() {
Ok(value != 0.0)
} else {
Err(jsonencode_error(OPTION_VALUE_ERROR))
}
}
fn parse_bool_string(text: &str) -> BuiltinResult<bool> {
match text.trim().to_ascii_lowercase().as_str() {
"true" | "on" | "yes" | "1" => Ok(true),
"false" | "off" | "no" | "0" => Ok(false),
_ => Err(jsonencode_error(OPTION_VALUE_ERROR)),
}
}
fn value_to_json(value: &Value, options: &JsonEncodeOptions) -> BuiltinResult<JsonValue> {
match value {
Value::Num(n) => number_to_json(*n, options),
Value::Int(i) => Ok(JsonValue::Number(int_to_number(i))),
Value::Bool(b) => Ok(JsonValue::Bool(*b)),
Value::LogicalArray(logical) => logical_array_to_json(logical, options),
Value::Tensor(tensor) => tensor_to_json(tensor, options),
Value::Complex(re, im) => complex_scalar_to_json(*re, *im, options),
Value::ComplexTensor(ct) => complex_tensor_to_json(ct, options),
Value::String(s) => Ok(JsonValue::String(s.clone())),
Value::StringArray(sa) => string_array_to_json(sa, options),
Value::CharArray(ca) => char_array_to_json(ca, options),
Value::Struct(sv) => struct_to_json(sv, options),
Value::Cell(ca) => cell_array_to_json(ca, options),
Value::Object(obj) => object_to_json(obj, options),
Value::GpuTensor(_) => Err(jsonencode_error(
"jsonencode: unexpected gpuArray handle after gather pass",
)),
Value::HandleObject(_)
| Value::Listener(_)
| Value::FunctionHandle(_)
| Value::Closure(_)
| Value::ClassRef(_)
| Value::MException(_)
| Value::OutputList(_) => Err(jsonencode_error(UNSUPPORTED_TYPE_ERROR)),
}
}
fn int_to_number(value: &IntValue) -> JsonNumber {
match value {
IntValue::I8(v) => JsonNumber::I64(*v as i64),
IntValue::I16(v) => JsonNumber::I64(*v as i64),
IntValue::I32(v) => JsonNumber::I64(*v as i64),
IntValue::I64(v) => JsonNumber::I64(*v),
IntValue::U8(v) => JsonNumber::U64(*v as u64),
IntValue::U16(v) => JsonNumber::U64(*v as u64),
IntValue::U32(v) => JsonNumber::U64(*v as u64),
IntValue::U64(v) => JsonNumber::U64(*v),
}
}
fn number_to_json(value: f64, options: &JsonEncodeOptions) -> BuiltinResult<JsonValue> {
if !value.is_finite() {
if options.convert_inf_and_nan {
return Ok(JsonValue::Null);
}
return Err(jsonencode_error(INF_NAN_ERROR));
}
Ok(JsonValue::Number(JsonNumber::Float(value)))
}
fn logical_array_to_json(
logical: &LogicalArray,
_options: &JsonEncodeOptions,
) -> BuiltinResult<JsonValue> {
let keep_dims = compute_keep_dims(&logical.shape, true);
if logical.shape.is_empty() || logical.data.is_empty() {
return Ok(JsonValue::Array(Vec::new()));
}
if keep_dims.is_empty() {
let first = logical.data.first().copied().unwrap_or(0) != 0;
return Ok(JsonValue::Bool(first));
}
build_strided_array(&logical.shape, &keep_dims, |offset| {
Ok(JsonValue::Bool(logical.data[offset] != 0))
})
}
fn tensor_to_json(tensor: &Tensor, options: &JsonEncodeOptions) -> BuiltinResult<JsonValue> {
if tensor.data.is_empty() {
return Ok(JsonValue::Array(Vec::new()));
}
let keep_dims = compute_keep_dims(&tensor.shape, true);
if keep_dims.is_empty() {
return number_to_json(tensor.data[0], options);
}
build_strided_array(&tensor.shape, &keep_dims, |offset| {
number_to_json(tensor.data[offset], options)
})
}
fn complex_scalar_to_json(
real: f64,
imag: f64,
options: &JsonEncodeOptions,
) -> BuiltinResult<JsonValue> {
let real_json = number_to_json(real, options)?;
let imag_json = number_to_json(imag, options)?;
Ok(JsonValue::Object(vec![
("real".to_string(), real_json),
("imag".to_string(), imag_json),
]))
}
fn complex_tensor_to_json(
ct: &ComplexTensor,
options: &JsonEncodeOptions,
) -> BuiltinResult<JsonValue> {
if ct.data.is_empty() {
return Ok(JsonValue::Array(Vec::new()));
}
let keep_dims = compute_keep_dims(&ct.shape, true);
if keep_dims.is_empty() {
let (re, im) = ct.data[0];
return complex_scalar_to_json(re, im, options);
}
build_strided_array(&ct.shape, &keep_dims, |offset| {
let (re, im) = ct.data[offset];
complex_scalar_to_json(re, im, options)
})
}
fn string_array_to_json(
sa: &StringArray,
_options: &JsonEncodeOptions,
) -> BuiltinResult<JsonValue> {
if sa.data.is_empty() {
return Ok(JsonValue::Array(Vec::new()));
}
let keep_dims = compute_keep_dims(&sa.shape, true);
if keep_dims.is_empty() {
return Ok(JsonValue::String(sa.data[0].clone()));
}
build_strided_array(&sa.shape, &keep_dims, |offset| {
Ok(JsonValue::String(sa.data[offset].clone()))
})
}
fn char_array_to_json(ca: &CharArray, _options: &JsonEncodeOptions) -> BuiltinResult<JsonValue> {
if ca.rows == 0 {
return Ok(JsonValue::Array(Vec::new()));
}
if ca.cols == 0 {
if ca.rows == 1 {
return Ok(JsonValue::String(String::new()));
}
let mut rows = Vec::with_capacity(ca.rows);
for _ in 0..ca.rows {
rows.push(JsonValue::String(String::new()));
}
return Ok(JsonValue::Array(rows));
}
if ca.rows == 1 {
return Ok(JsonValue::String(ca.data.iter().collect()));
}
let mut rows = Vec::with_capacity(ca.rows);
for r in 0..ca.rows {
let mut row_string = String::with_capacity(ca.cols);
for c in 0..ca.cols {
row_string.push(ca.data[r * ca.cols + c]);
}
rows.push(JsonValue::String(row_string));
}
Ok(JsonValue::Array(rows))
}
fn struct_to_json(sv: &StructValue, options: &JsonEncodeOptions) -> BuiltinResult<JsonValue> {
if sv.fields.is_empty() {
return Ok(JsonValue::Object(Vec::new()));
}
let mut map = BTreeMap::new();
for (key, value) in &sv.fields {
map.insert(key.clone(), value_to_json(value, options)?);
}
Ok(JsonValue::Object(map.into_iter().collect()))
}
fn object_to_json(obj: &ObjectInstance, options: &JsonEncodeOptions) -> BuiltinResult<JsonValue> {
let mut map = BTreeMap::new();
for (key, value) in &obj.properties {
map.insert(key.clone(), value_to_json(value, options)?);
}
Ok(JsonValue::Object(map.into_iter().collect()))
}
fn cell_array_to_json(ca: &CellArray, options: &JsonEncodeOptions) -> BuiltinResult<JsonValue> {
if ca.rows == 0 || ca.cols == 0 {
return Ok(JsonValue::Array(Vec::new()));
}
if ca.rows == 1 && ca.cols == 1 {
let value = ca
.get(0, 0)
.map_err(|e| jsonencode_error(format!("jsonencode: {e}")))?;
return Ok(JsonValue::Array(vec![value_to_json(&value, options)?]));
}
if ca.rows == 1 {
let mut row = Vec::with_capacity(ca.cols);
for c in 0..ca.cols {
let element = ca
.get(0, c)
.map_err(|e| jsonencode_error(format!("jsonencode: {e}")))?;
row.push(value_to_json(&element, options)?);
}
return Ok(JsonValue::Array(row));
}
if ca.cols == 1 {
let mut column = Vec::with_capacity(ca.rows);
for r in 0..ca.rows {
let element = ca
.get(r, 0)
.map_err(|e| jsonencode_error(format!("jsonencode: {e}")))?;
column.push(value_to_json(&element, options)?);
}
return Ok(JsonValue::Array(column));
}
let mut rows = Vec::with_capacity(ca.rows);
for r in 0..ca.rows {
let mut row = Vec::with_capacity(ca.cols);
for c in 0..ca.cols {
let element = ca
.get(r, c)
.map_err(|e| jsonencode_error(format!("jsonencode: {e}")))?;
row.push(value_to_json(&element, options)?);
}
rows.push(JsonValue::Array(row));
}
Ok(JsonValue::Array(rows))
}
fn compute_keep_dims(shape: &[usize], drop_singletons: bool) -> Vec<usize> {
let mut keep = Vec::new();
for (idx, &size) in shape.iter().enumerate() {
if size != 1 || !drop_singletons {
keep.push(idx);
}
}
keep
}
fn compute_strides(shape: &[usize]) -> Vec<usize> {
let mut strides = Vec::with_capacity(shape.len());
let mut acc = 1usize;
for &size in shape {
strides.push(acc);
acc = acc.saturating_mul(size.max(1));
}
strides
}
fn build_strided_array<F>(
shape: &[usize],
keep_dims: &[usize],
mut fetch: F,
) -> BuiltinResult<JsonValue>
where
F: FnMut(usize) -> BuiltinResult<JsonValue>,
{
if keep_dims.is_empty() {
return fetch(0);
}
if keep_dims.iter().any(|&idx| shape[idx] == 0) {
return Ok(JsonValue::Array(Vec::new()));
}
let strides = compute_strides(shape);
let dims: Vec<usize> = keep_dims.iter().map(|&idx| shape[idx]).collect();
build_nd_array(&dims, |indices| {
let mut offset = 0usize;
for (value, dim_idx) in indices.iter().zip(keep_dims.iter()) {
offset += value * strides[*dim_idx];
}
fetch(offset)
})
}
fn build_nd_array<F>(dims: &[usize], mut fetch: F) -> BuiltinResult<JsonValue>
where
F: FnMut(&[usize]) -> BuiltinResult<JsonValue>,
{
if dims.is_empty() {
return fetch(&[]);
}
if dims[0] == 0 {
return Ok(JsonValue::Array(Vec::new()));
}
let mut indices = vec![0usize; dims.len()];
build_nd_array_recursive(dims, 0, &mut indices, &mut fetch)
}
fn build_nd_array_recursive<F>(
dims: &[usize],
level: usize,
indices: &mut [usize],
fetch: &mut F,
) -> BuiltinResult<JsonValue>
where
F: FnMut(&[usize]) -> BuiltinResult<JsonValue>,
{
let size = dims[level];
if size == 0 {
return Ok(JsonValue::Array(Vec::new()));
}
if level + 1 == dims.len() {
let mut items = Vec::with_capacity(size);
for i in 0..size {
indices[level] = i;
items.push(fetch(indices)?);
}
return Ok(JsonValue::Array(items));
}
let mut items = Vec::with_capacity(size);
for i in 0..size {
indices[level] = i;
items.push(build_nd_array_recursive(dims, level + 1, indices, fetch)?);
}
Ok(JsonValue::Array(items))
}
fn render_json(value: &JsonValue, options: &JsonEncodeOptions) -> String {
let mut writer = JsonWriter::new(options.pretty_print);
writer.write_value(value);
writer.finish()
}
struct JsonWriter {
output: String,
pretty: bool,
indent: usize,
}
impl JsonWriter {
fn new(pretty: bool) -> Self {
Self {
output: String::new(),
pretty,
indent: 0,
}
}
fn finish(self) -> String {
self.output
}
fn write_value(&mut self, value: &JsonValue) {
match value {
JsonValue::Null => self.output.push_str("null"),
JsonValue::Bool(true) => self.output.push_str("true"),
JsonValue::Bool(false) => self.output.push_str("false"),
JsonValue::Number(number) => self.write_number(number),
JsonValue::String(text) => {
self.output.push('"');
self.output.push_str(&escape_json_string(text));
self.output.push('"');
}
JsonValue::Array(items) => self.write_array(items),
JsonValue::Object(fields) => self.write_object(fields),
}
}
fn write_number(&mut self, number: &JsonNumber) {
match number {
JsonNumber::Float(f) => {
if f.is_nan() || !f.is_finite() {
self.output.push_str("null");
} else {
self.output.push_str(&format_number(*f));
}
}
JsonNumber::I64(i) => {
let _ = write!(self.output, "{i}");
}
JsonNumber::U64(u) => {
let _ = write!(self.output, "{u}");
}
}
}
fn write_array(&mut self, items: &[JsonValue]) {
if items.is_empty() {
self.output.push_str("[]");
return;
}
let inline = if self.pretty {
items.iter().all(|item| {
matches!(
item,
JsonValue::Null
| JsonValue::Bool(_)
| JsonValue::Number(_)
| JsonValue::String(_)
)
})
} else {
false
};
if inline {
self.output.push('[');
for (index, item) in items.iter().enumerate() {
self.write_value(item);
if index + 1 < items.len() {
self.output.push(',');
}
}
self.output.push(']');
return;
}
self.output.push('[');
if self.pretty {
self.output.push('\n');
self.indent += 1;
}
for (index, item) in items.iter().enumerate() {
if self.pretty {
self.write_indent();
}
self.write_value(item);
if index + 1 < items.len() {
if self.pretty {
self.output.push_str(",\n");
} else {
self.output.push(',');
}
}
}
if self.pretty {
self.output.push('\n');
if self.indent > 0 {
self.indent -= 1;
}
self.write_indent();
}
self.output.push(']');
}
fn write_object(&mut self, fields: &[(String, JsonValue)]) {
if fields.is_empty() {
self.output.push_str("{}");
return;
}
self.output.push('{');
if self.pretty {
self.output.push('\n');
self.indent += 1;
}
for (index, (key, value)) in fields.iter().enumerate() {
if self.pretty {
self.write_indent();
}
self.output.push('"');
self.output.push_str(&escape_json_string(key));
self.output.push('"');
if self.pretty {
self.output.push_str(": ");
} else {
self.output.push(':');
}
self.write_value(value);
if index + 1 < fields.len() {
if self.pretty {
self.output.push_str(",\n");
} else {
self.output.push(',');
}
}
}
if self.pretty {
self.output.push('\n');
if self.indent > 0 {
self.indent -= 1;
}
self.write_indent();
}
self.output.push('}');
}
fn write_indent(&mut self) {
if self.pretty {
for _ in 0..self.indent {
self.output.push_str(" ");
}
}
}
}
fn escape_json_string(value: &str) -> String {
let mut escaped = String::with_capacity(value.len());
for ch in value.chars() {
match ch {
'"' => escaped.push_str("\\\""),
'\\' => escaped.push_str("\\\\"),
'\u{08}' => escaped.push_str("\\b"),
'\u{0C}' => escaped.push_str("\\f"),
'\n' => escaped.push_str("\\n"),
'\r' => escaped.push_str("\\r"),
'\t' => escaped.push_str("\\t"),
c if (c as u32) < 0x20 => {
let _ = write!(escaped, "\\u{:04X}", c as u32);
}
_ => escaped.push(ch),
}
}
escaped
}
fn format_number(value: f64) -> String {
if value.fract() == 0.0 {
format!("{:.0}", value)
} else {
format!("{}", value)
}
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::builtins::common::test_support;
use futures::executor::block_on;
use runmat_builtins::{
CellArray, CharArray, ComplexTensor, LogicalArray, StringArray, StructValue, Tensor,
};
fn as_string(value: Value) -> String {
match value {
Value::CharArray(ca) => ca.data.iter().collect(),
Value::String(s) => s,
other => panic!("expected char array, got {:?}", other),
}
}
fn error_message(err: crate::RuntimeError) -> String {
err.message().to_string()
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn jsonencode_scalar_double() {
let encoded =
block_on(jsonencode_builtin(Value::Num(5.0), Vec::new())).expect("jsonencode");
assert_eq!(as_string(encoded), "5");
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn jsonencode_matrix_pretty_print() {
let tensor = Tensor::new(vec![1.0, 4.0, 2.0, 5.0, 3.0, 6.0], vec![2, 3]).expect("tensor");
let args = vec![Value::from("PrettyPrint"), Value::Bool(true)];
let encoded =
block_on(jsonencode_builtin(Value::Tensor(tensor), args)).expect("jsonencode");
let expected = "[\n [1,2,3],\n [4,5,6]\n]";
assert_eq!(as_string(encoded), expected);
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn jsonencode_struct_round_trip() {
let mut fields = StructValue::new();
fields
.fields
.insert("name".to_string(), Value::from("RunMat"));
fields
.fields
.insert("year".to_string(), Value::Int(IntValue::I32(2025)));
let encoded =
block_on(jsonencode_builtin(Value::Struct(fields), Vec::new())).expect("jsonencode");
assert_eq!(as_string(encoded), "{\"name\":\"RunMat\",\"year\":2025}");
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn jsonencode_struct_options_enable_pretty_print() {
let tensor = Tensor::new(vec![1.0, 4.0, 2.0, 5.0], vec![2, 2]).expect("tensor");
let mut opts = StructValue::new();
opts.fields
.insert("PrettyPrint".to_string(), Value::Bool(true));
let encoded = block_on(jsonencode_builtin(
Value::Tensor(tensor),
vec![Value::Struct(opts)],
))
.expect("jsonencode");
let expected = "[\n [1,2],\n [4,5]\n]";
assert_eq!(as_string(encoded), expected);
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn jsonencode_options_accept_scalar_tensor_bool() {
let tensor_value = Tensor::new(vec![1.0], vec![1, 1]).expect("tensor");
let args = vec![Value::from("PrettyPrint"), Value::Tensor(tensor_value)];
let encoded = block_on(jsonencode_builtin(Value::Num(42.0), args)).expect("jsonencode");
assert_eq!(as_string(encoded), "42");
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn jsonencode_options_reject_non_scalar_tensor_bool() {
let tensor = Tensor::new(vec![1.0, 0.0], vec![1, 2]).expect("tensor");
let err = block_on(jsonencode_builtin(
Value::Num(1.0),
vec![Value::from("PrettyPrint"), Value::Tensor(tensor)],
))
.expect_err("expected failure");
assert_eq!(error_message(err), OPTION_VALUE_ERROR);
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn jsonencode_options_accept_scalar_logical_array() {
let logical = LogicalArray::new(vec![1], vec![1]).expect("logical");
let args = vec![Value::from("PrettyPrint"), Value::LogicalArray(logical)];
let encoded = block_on(jsonencode_builtin(Value::Num(7.0), args)).expect("jsonencode");
assert_eq!(as_string(encoded), "7");
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn jsonencode_convert_inf_and_nan_controls_null_output() {
let tensor = Tensor::new(vec![1.0, f64::NAN], vec![1, 2]).expect("tensor");
let encoded = block_on(jsonencode_builtin(
Value::Tensor(tensor.clone()),
Vec::new(),
))
.expect("jsonencode");
assert_eq!(as_string(encoded), "[1,null]");
let err = block_on(jsonencode_builtin(
Value::Tensor(tensor),
vec![Value::from("ConvertInfAndNaN"), Value::Bool(false)],
))
.expect_err("expected failure");
assert_eq!(error_message(err), INF_NAN_ERROR);
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn jsonencode_cell_array() {
let elements = vec![Value::from(1.0), Value::from("two")];
let cell = CellArray::new(elements, 1, 2).expect("cell");
let encoded =
block_on(jsonencode_builtin(Value::Cell(cell), Vec::new())).expect("jsonencode");
assert_eq!(as_string(encoded), "[1,\"two\"]");
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn jsonencode_char_array_zero_rows_is_empty_array() {
let chars = CharArray::new(Vec::new(), 0, 3).expect("char array");
let encoded =
block_on(jsonencode_builtin(Value::CharArray(chars), Vec::new())).expect("jsonencode");
assert_eq!(as_string(encoded), "[]");
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn jsonencode_char_array_empty_strings_per_row() {
let chars = CharArray::new(Vec::new(), 2, 0).expect("char array");
let encoded =
block_on(jsonencode_builtin(Value::CharArray(chars), Vec::new())).expect("jsonencode");
let encoded_str = as_string(encoded);
assert_eq!(encoded_str, "[\"\",\"\"]");
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn jsonencode_string_array_matrix() {
let sa = StringArray::new(vec!["alpha".to_string(), "beta".to_string()], vec![2, 1])
.expect("string array");
let encoded =
block_on(jsonencode_builtin(Value::StringArray(sa), Vec::new())).expect("jsonencode");
assert_eq!(as_string(encoded), "[\"alpha\",\"beta\"]");
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn jsonencode_complex_tensor_outputs_objects() {
let ct = ComplexTensor::new(vec![(1.0, 2.0), (3.5, -4.0)], vec![2, 1]).expect("complex");
let encoded =
block_on(jsonencode_builtin(Value::ComplexTensor(ct), Vec::new())).expect("jsonencode");
assert_eq!(
as_string(encoded),
"[{\"real\":1,\"imag\":2},{\"real\":3.5,\"imag\":-4}]"
);
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn jsonencode_gpu_tensor_gathers_host_data() {
test_support::with_test_provider(|provider| {
let tensor = Tensor::new(vec![1.0, 0.0, 0.0, 1.0], vec![2, 2]).expect("tensor");
let view = runmat_accelerate_api::HostTensorView {
data: &tensor.data,
shape: &tensor.shape,
};
let handle = provider.upload(&view).expect("upload");
let encoded = block_on(jsonencode_builtin(Value::GpuTensor(handle), Vec::new()))
.expect("jsonencode");
assert_eq!(as_string(encoded), "[[1,0],[0,1]]");
});
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
#[cfg(feature = "wgpu")]
fn jsonencode_gpu_tensor_wgpu_gathers_host_data() {
let ensure = runmat_accelerate::backend::wgpu::provider::ensure_wgpu_provider();
let Some(_) = ensure.ok().flatten() else {
return;
};
let provider = runmat_accelerate_api::provider().expect("wgpu provider");
let tensor = Tensor::new(vec![1.0, 2.0, 3.0], vec![3, 1]).expect("tensor");
let view = runmat_accelerate_api::HostTensorView {
data: &tensor.data,
shape: &tensor.shape,
};
let handle = provider.upload(&view).expect("upload");
let encoded =
block_on(jsonencode_builtin(Value::GpuTensor(handle), Vec::new())).expect("jsonencode");
assert_eq!(as_string(encoded), "[1,2,3]");
}
}