use crate::heap_value::HeapValue;
use crate::tags;
use crate::value_word::ValueWord;
use std::collections::BTreeMap;
use std::fmt;
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(tag = "type", content = "value")]
pub enum ExternalValue {
Number(f64),
Int(i64),
Bool(bool),
String(String),
None,
Unit,
Array(Vec<ExternalValue>),
Object(BTreeMap<String, ExternalValue>),
TypedObject {
name: String,
fields: BTreeMap<String, ExternalValue>,
},
Enum {
name: String,
variant: String,
data: Box<ExternalValue>,
},
Duration { secs: u64, nanos: u32 },
Time(String),
Decimal(String),
Error(String),
Ok(Box<ExternalValue>),
Range {
start: Option<Box<ExternalValue>>,
end: Option<Box<ExternalValue>>,
inclusive: bool,
},
DataTable { rows: usize, columns: Vec<String> },
Opaque(String),
}
pub trait SchemaLookup {
fn type_name(&self, schema_id: u64) -> Option<&str>;
fn field_names(&self, schema_id: u64) -> Option<Vec<&str>>;
}
pub struct NoSchemaLookup;
impl SchemaLookup for NoSchemaLookup {
fn type_name(&self, _schema_id: u64) -> Option<&str> {
std::option::Option::None
}
fn field_names(&self, _schema_id: u64) -> Option<Vec<&str>> {
std::option::Option::None
}
}
pub fn nb_to_external(nb: &ValueWord, schemas: &dyn SchemaLookup) -> ExternalValue {
let bits = nb.raw_bits();
if !tags::is_tagged(bits) {
let f = f64::from_bits(bits);
return if f.is_nan() {
ExternalValue::Number(f64::NAN)
} else {
ExternalValue::Number(f)
};
}
match tags::get_tag(bits) {
tags::TAG_INT => ExternalValue::Int(tags::sign_extend_i48(tags::get_payload(bits))),
tags::TAG_BOOL => ExternalValue::Bool(tags::get_payload(bits) != 0),
tags::TAG_NONE => ExternalValue::None,
tags::TAG_UNIT => ExternalValue::Unit,
tags::TAG_FUNCTION => {
ExternalValue::Opaque(format!("<function:{}>", tags::get_payload(bits) as u16))
}
tags::TAG_MODULE_FN => {
ExternalValue::Opaque(format!("<module_fn:{}>", tags::get_payload(bits) as u32))
}
tags::TAG_REF => ExternalValue::Opaque("<ref>".to_string()),
tags::TAG_HEAP => {
if let Some(hv) = nb.as_heap_ref() {
heap_to_external(hv, schemas)
} else {
ExternalValue::Opaque("<invalid_heap>".to_string())
}
}
_ => ExternalValue::Opaque("<unknown_tag>".to_string()),
}
}
fn heap_to_external(hv: &HeapValue, schemas: &dyn SchemaLookup) -> ExternalValue {
match hv {
HeapValue::String(s) => ExternalValue::String((**s).clone()),
HeapValue::Array(arr) => {
let items: Vec<ExternalValue> =
arr.iter().map(|v| nb_to_external(v, schemas)).collect();
ExternalValue::Array(items)
}
HeapValue::TypedObject {
schema_id,
slots,
heap_mask,
} => {
let type_name = schemas
.type_name(*schema_id)
.unwrap_or("unknown")
.to_string();
let field_names_opt = schemas.field_names(*schema_id);
let mut fields = BTreeMap::new();
let names: Vec<String> = if let Some(names) = field_names_opt {
names.iter().map(|s| s.to_string()).collect()
} else {
(0..slots.len()).map(|i| format!("_{i}")).collect()
};
for (i, name) in names.into_iter().enumerate() {
if i >= slots.len() {
break;
}
let is_heap = (heap_mask >> i) & 1 == 1;
let ev = if is_heap {
let nb_val = slots[i].as_heap_nb();
nb_to_external(&nb_val, schemas)
} else {
ExternalValue::Number(slots[i].as_f64())
};
fields.insert(name, ev);
}
ExternalValue::TypedObject {
name: type_name,
fields,
}
}
HeapValue::Closure { function_id, .. } => {
ExternalValue::Opaque(format!("<closure:{function_id}>"))
}
HeapValue::Decimal(d) => ExternalValue::Decimal(d.to_string()),
HeapValue::BigInt(i) => ExternalValue::Int(*i),
HeapValue::HostClosure(_) => ExternalValue::Opaque("<host_closure>".to_string()),
HeapValue::DataTable(dt) => ExternalValue::DataTable {
rows: dt.row_count(),
columns: dt.column_names().iter().map(|s| s.to_string()).collect(),
},
HeapValue::TypedTable { table, .. } => ExternalValue::DataTable {
rows: table.row_count(),
columns: table.column_names().iter().map(|s| s.to_string()).collect(),
},
HeapValue::RowView { .. } => ExternalValue::Opaque("<row_view>".to_string()),
HeapValue::ColumnRef { .. } => ExternalValue::Opaque("<column_ref>".to_string()),
HeapValue::IndexedTable { table, .. } => ExternalValue::DataTable {
rows: table.row_count(),
columns: table.column_names().iter().map(|s| s.to_string()).collect(),
},
HeapValue::ProjectedRef(..) => ExternalValue::Opaque("<ref>".to_string()),
HeapValue::Range {
start,
end,
inclusive,
} => ExternalValue::Range {
start: start.as_ref().map(|v| Box::new(nb_to_external(v, schemas))),
end: end.as_ref().map(|v| Box::new(nb_to_external(v, schemas))),
inclusive: *inclusive,
},
HeapValue::Enum(e) => ExternalValue::Enum {
name: e.enum_name.clone(),
variant: e.variant.clone(),
data: Box::new(match &e.payload {
crate::enums::EnumPayload::Unit => ExternalValue::None,
crate::enums::EnumPayload::Tuple(nbs) => {
if nbs.len() == 1 {
nb_to_external(&nbs[0], schemas)
} else {
ExternalValue::Array(
nbs.iter().map(|v| nb_to_external(v, schemas)).collect(),
)
}
}
crate::enums::EnumPayload::Struct(fields) => {
let mut map = BTreeMap::new();
for (k, v) in fields {
map.insert(k.clone(), nb_to_external(v, schemas));
}
ExternalValue::Object(map)
}
}),
},
HeapValue::Some(v) => nb_to_external(v, schemas),
HeapValue::Ok(v) => ExternalValue::Ok(Box::new(nb_to_external(v, schemas))),
HeapValue::Err(v) => ExternalValue::Error(format!("{:?}", nb_to_external(v, schemas))),
HeapValue::Future(id) => ExternalValue::Opaque(format!("<future:{id}>")),
HeapValue::TaskGroup { kind, task_ids } => {
ExternalValue::Opaque(format!("<task_group:kind={kind},tasks={}>", task_ids.len()))
}
HeapValue::TraitObject { value, .. } => nb_to_external(value, schemas),
HeapValue::ExprProxy(s) => ExternalValue::Opaque(format!("<expr_proxy:{s}>")),
HeapValue::FilterExpr(_) => ExternalValue::Opaque("<filter_expr>".to_string()),
HeapValue::Time(t) => ExternalValue::Time(t.to_rfc3339()),
HeapValue::Duration(d) => {
let secs = d.value as u64;
ExternalValue::Duration { secs, nanos: 0 }
}
HeapValue::TimeSpan(ts) => ExternalValue::Duration {
secs: ts.num_seconds().unsigned_abs(),
nanos: (ts.subsec_nanos().unsigned_abs()),
},
HeapValue::Timeframe(tf) => ExternalValue::String(format!("{tf:?}")),
HeapValue::TimeReference(_) => ExternalValue::Opaque("<time_reference>".to_string()),
HeapValue::DateTimeExpr(_) => ExternalValue::Opaque("<datetime_expr>".to_string()),
HeapValue::DataDateTimeRef(_) => ExternalValue::Opaque("<data_datetime_ref>".to_string()),
HeapValue::TypeAnnotation(_) => ExternalValue::Opaque("<type_annotation>".to_string()),
HeapValue::TypeAnnotatedValue { type_name, value } => {
let inner = nb_to_external(value, schemas);
ExternalValue::TypedObject {
name: type_name.clone(),
fields: BTreeMap::from([("value".to_string(), inner)]),
}
}
HeapValue::PrintResult(pr) => ExternalValue::String(pr.rendered.clone()),
HeapValue::SimulationCall(data) => ExternalValue::Opaque(format!(
"<simulation_call:{} params={}>",
data.name,
data.params.len()
)),
HeapValue::FunctionRef { name, .. } => {
ExternalValue::Opaque(format!("<function_ref:{name}>"))
}
HeapValue::DataReference(data) => {
let mut fields = BTreeMap::new();
fields.insert(
"datetime".to_string(),
ExternalValue::Time(data.datetime.to_rfc3339()),
);
fields.insert("id".to_string(), ExternalValue::String(data.id.clone()));
fields.insert(
"timeframe".to_string(),
ExternalValue::String(format!("{:?}", data.timeframe)),
);
ExternalValue::Object(fields)
}
HeapValue::Instant(t) => ExternalValue::Opaque(format!("<instant:{:?}>", t.elapsed())),
HeapValue::IoHandle(data) => {
let status = if data.is_open() { "open" } else { "closed" };
ExternalValue::Opaque(format!("<io_handle:{}:{}>", data.path, status))
}
HeapValue::NativeScalar(v) => {
if let Some(i) = v.as_i64() {
ExternalValue::Int(i)
} else {
ExternalValue::Number(v.as_f64())
}
}
HeapValue::NativeView(v) => ExternalValue::Opaque(format!(
"<{}:{}@0x{:x}>",
if v.mutable { "cmut" } else { "cview" },
v.layout.name,
v.ptr
)),
HeapValue::HashMap(d) => {
let mut fields = BTreeMap::new();
for (k, v) in d.keys.iter().zip(d.values.iter()) {
fields.insert(format!("{}", k), nb_to_external(v, schemas));
}
ExternalValue::Object(fields)
}
HeapValue::Set(d) => {
ExternalValue::Array(d.items.iter().map(|v| nb_to_external(v, schemas)).collect())
}
HeapValue::Deque(d) => {
ExternalValue::Array(d.items.iter().map(|v| nb_to_external(v, schemas)).collect())
}
HeapValue::PriorityQueue(d) => {
ExternalValue::Array(d.items.iter().map(|v| nb_to_external(v, schemas)).collect())
}
HeapValue::Content(node) => ExternalValue::String(format!("{}", node)),
HeapValue::SharedCell(arc) => nb_to_external(&arc.read().unwrap(), schemas),
HeapValue::IntArray(a) => {
ExternalValue::Array(a.iter().map(|&v| ExternalValue::Int(v)).collect())
}
HeapValue::FloatArray(a) => {
ExternalValue::Array(a.iter().map(|&v| ExternalValue::Number(v)).collect())
}
HeapValue::BoolArray(a) => {
ExternalValue::Array(a.iter().map(|&v| ExternalValue::Bool(v != 0)).collect())
}
HeapValue::I8Array(a) => {
ExternalValue::Array(a.iter().map(|&v| ExternalValue::Int(v as i64)).collect())
}
HeapValue::I16Array(a) => {
ExternalValue::Array(a.iter().map(|&v| ExternalValue::Int(v as i64)).collect())
}
HeapValue::I32Array(a) => {
ExternalValue::Array(a.iter().map(|&v| ExternalValue::Int(v as i64)).collect())
}
HeapValue::U8Array(a) => {
ExternalValue::Array(a.iter().map(|&v| ExternalValue::Int(v as i64)).collect())
}
HeapValue::U16Array(a) => {
ExternalValue::Array(a.iter().map(|&v| ExternalValue::Int(v as i64)).collect())
}
HeapValue::U32Array(a) => {
ExternalValue::Array(a.iter().map(|&v| ExternalValue::Int(v as i64)).collect())
}
HeapValue::U64Array(a) => {
ExternalValue::Array(a.iter().map(|&v| ExternalValue::Int(v as i64)).collect())
}
HeapValue::F32Array(a) => {
ExternalValue::Array(a.iter().map(|&v| ExternalValue::Number(v as f64)).collect())
}
HeapValue::Matrix(m) => {
ExternalValue::Opaque(format!("<Mat<number>:{}x{}>", m.rows, m.cols))
}
HeapValue::Iterator(_) => ExternalValue::Opaque("<iterator>".to_string()),
HeapValue::Generator(_) => ExternalValue::Opaque("<generator>".to_string()),
HeapValue::Mutex(_) => ExternalValue::Opaque("<mutex>".to_string()),
HeapValue::Atomic(a) => {
ExternalValue::Int(a.inner.load(std::sync::atomic::Ordering::Relaxed))
}
HeapValue::Channel(c) => {
if c.is_sender() {
ExternalValue::Opaque("<channel:sender>".to_string())
} else {
ExternalValue::Opaque("<channel:receiver>".to_string())
}
}
HeapValue::Lazy(l) => {
if let Ok(guard) = l.value.lock() {
if let Some(val) = guard.as_ref() {
return nb_to_external(val, schemas);
}
}
ExternalValue::Opaque("<lazy:uninitialized>".to_string())
}
HeapValue::Char(c) => ExternalValue::String(c.to_string()),
HeapValue::FloatArraySlice {
parent,
offset,
len,
} => {
let slice = &parent.data[*offset as usize..(*offset + *len) as usize];
ExternalValue::Array(slice.iter().map(|&v| ExternalValue::Number(v)).collect())
}
}
}
pub fn external_to_nb(ev: &ExternalValue, schemas: &dyn SchemaLookup) -> ValueWord {
let _ = schemas; match ev {
ExternalValue::Number(n) => ValueWord::from_f64(*n),
ExternalValue::Int(i) => ValueWord::from_i64(*i),
ExternalValue::Bool(b) => ValueWord::from_bool(*b),
ExternalValue::String(s) => ValueWord::from_string(std::sync::Arc::new(s.clone())),
ExternalValue::None => ValueWord::none(),
ExternalValue::Unit => ValueWord::unit(),
ExternalValue::Array(items) => {
let nbs: Vec<ValueWord> = items.iter().map(|v| external_to_nb(v, schemas)).collect();
ValueWord::from_array(std::sync::Arc::new(nbs))
}
ExternalValue::Decimal(s) => {
if let Ok(d) = s.parse::<rust_decimal::Decimal>() {
ValueWord::from_decimal(d)
} else {
ValueWord::from_string(std::sync::Arc::new(s.clone()))
}
}
ExternalValue::Ok(inner) => ValueWord::from_ok(external_to_nb(inner, schemas)),
ExternalValue::Error(msg) => {
ValueWord::from_err(ValueWord::from_string(std::sync::Arc::new(msg.clone())))
}
ExternalValue::Range {
start,
end,
inclusive,
} => ValueWord::from_range(
start.as_ref().map(|v| external_to_nb(v, schemas)),
end.as_ref().map(|v| external_to_nb(v, schemas)),
*inclusive,
),
ExternalValue::Object(_)
| ExternalValue::TypedObject { .. }
| ExternalValue::Enum { .. }
| ExternalValue::Duration { .. }
| ExternalValue::Time(_)
| ExternalValue::DataTable { .. }
| ExternalValue::Opaque(_) => ValueWord::none(),
}
}
impl fmt::Display for ExternalValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ExternalValue::Number(n) => {
if n.is_nan() {
write!(f, "NaN")
} else if n.is_infinite() {
if n.is_sign_positive() {
write!(f, "Infinity")
} else {
write!(f, "-Infinity")
}
} else if *n == (*n as i64) as f64 && n.is_finite() {
write!(f, "{}", *n as i64)
} else {
write!(f, "{n}")
}
}
ExternalValue::Int(i) => write!(f, "{i}"),
ExternalValue::Bool(b) => write!(f, "{b}"),
ExternalValue::String(s) => write!(f, "{s}"),
ExternalValue::None => write!(f, "none"),
ExternalValue::Unit => write!(f, "()"),
ExternalValue::Array(items) => {
write!(f, "[")?;
for (i, item) in items.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{item}")?;
}
write!(f, "]")
}
ExternalValue::Object(fields) => {
write!(f, "{{")?;
for (i, (k, v)) in fields.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{k}: {v}")?;
}
write!(f, "}}")
}
ExternalValue::TypedObject { name, fields } => {
write!(f, "{name} {{")?;
for (i, (k, v)) in fields.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{k}: {v}")?;
}
write!(f, "}}")
}
ExternalValue::Enum {
name,
variant,
data,
} => {
write!(f, "{name}::{variant}")?;
if **data != ExternalValue::None {
write!(f, "({data})")?;
}
Ok(())
}
ExternalValue::Duration { secs, nanos } => {
if *nanos > 0 {
write!(f, "{secs}.{:09}s", nanos)
} else {
write!(f, "{secs}s")
}
}
ExternalValue::Time(iso) => write!(f, "{iso}"),
ExternalValue::Decimal(d) => write!(f, "{d}"),
ExternalValue::Error(msg) => write!(f, "Error({msg})"),
ExternalValue::Ok(inner) => write!(f, "Ok({inner})"),
ExternalValue::Range {
start,
end,
inclusive,
} => {
if let Some(s) = start {
write!(f, "{s}")?;
}
if *inclusive {
write!(f, "..=")?;
} else {
write!(f, "..")?;
}
if let Some(e) = end {
write!(f, "{e}")?;
}
Ok(())
}
ExternalValue::DataTable { rows, columns } => {
write!(f, "DataTable({rows} rows, {} cols)", columns.len())
}
ExternalValue::Opaque(desc) => write!(f, "{desc}"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::value_word::ValueWord;
#[test]
fn test_number_roundtrip() {
let nb = ValueWord::from_f64(3.14);
let ev = nb_to_external(&nb, &NoSchemaLookup);
assert!(matches!(ev, ExternalValue::Number(n) if (n - 3.14).abs() < f64::EPSILON));
let back = external_to_nb(&ev, &NoSchemaLookup);
assert!((back.as_f64().unwrap() - 3.14).abs() < f64::EPSILON);
}
#[test]
fn test_int_roundtrip() {
let nb = ValueWord::from_i64(42);
let ev = nb_to_external(&nb, &NoSchemaLookup);
assert_eq!(ev, ExternalValue::Int(42));
let back = external_to_nb(&ev, &NoSchemaLookup);
assert_eq!(back.as_i64().unwrap(), 42);
}
#[test]
fn test_bool_roundtrip() {
let nb = ValueWord::from_bool(true);
let ev = nb_to_external(&nb, &NoSchemaLookup);
assert_eq!(ev, ExternalValue::Bool(true));
}
#[test]
fn test_string_roundtrip() {
let nb = ValueWord::from_string(std::sync::Arc::new("hello".to_string()));
let ev = nb_to_external(&nb, &NoSchemaLookup);
assert_eq!(ev, ExternalValue::String("hello".to_string()));
let back = external_to_nb(&ev, &NoSchemaLookup);
assert_eq!(back.as_str().unwrap(), "hello");
}
#[test]
fn test_none_and_unit() {
assert_eq!(
nb_to_external(&ValueWord::none(), &NoSchemaLookup),
ExternalValue::None
);
assert_eq!(
nb_to_external(&ValueWord::unit(), &NoSchemaLookup),
ExternalValue::Unit
);
}
#[test]
fn test_function_is_opaque() {
let nb = ValueWord::from_function(42);
let ev = nb_to_external(&nb, &NoSchemaLookup);
assert!(matches!(ev, ExternalValue::Opaque(_)));
}
#[test]
fn test_array() {
let arr = vec![ValueWord::from_i64(1), ValueWord::from_i64(2)];
let nb = ValueWord::from_array(std::sync::Arc::new(arr));
let ev = nb_to_external(&nb, &NoSchemaLookup);
assert_eq!(
ev,
ExternalValue::Array(vec![ExternalValue::Int(1), ExternalValue::Int(2)])
);
}
#[test]
fn test_display() {
assert_eq!(format!("{}", ExternalValue::Number(3.14)), "3.14");
assert_eq!(format!("{}", ExternalValue::Int(42)), "42");
assert_eq!(format!("{}", ExternalValue::Bool(true)), "true");
assert_eq!(format!("{}", ExternalValue::String("hi".into())), "hi");
assert_eq!(format!("{}", ExternalValue::None), "none");
assert_eq!(format!("{}", ExternalValue::Unit), "()");
assert_eq!(
format!(
"{}",
ExternalValue::Array(vec![ExternalValue::Int(1), ExternalValue::Int(2)])
),
"[1, 2]"
);
}
#[test]
fn test_serde_json_roundtrip() {
let ev = ExternalValue::TypedObject {
name: "Candle".to_string(),
fields: BTreeMap::from([
("open".to_string(), ExternalValue::Number(100.0)),
("close".to_string(), ExternalValue::Number(105.5)),
]),
};
let json = serde_json::to_string(&ev).unwrap();
let back: ExternalValue = serde_json::from_str(&json).unwrap();
assert_eq!(ev, back);
}
}