use std::rc::Rc;
use std::sync::{mpsc, Arc, Mutex};
use bigdecimal::BigDecimal;
use num_bigint::BigInt;
use crate::error::{DogeError, DogeResult, ErrorKind, ErrorLocation};
use crate::ordered_map::OrderedMap;
use crate::value::{ObjectData, SocketData, SocketState, Value};
const PACK_DEPTH_LIMIT: usize = 500;
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum PackMode {
Transfer,
Snapshot,
}
#[derive(Debug)]
pub enum Packed {
Int(BigInt),
Float(f64),
Decimal(BigDecimal),
Str(String),
Bytes(Vec<u8>),
Bool(bool),
None,
List(Vec<Packed>),
Dict(Vec<(String, Packed)>),
Object {
class_id: u32,
class_name: String,
fields: Vec<(String, Packed)>,
},
Function {
fn_id: u32,
name: String,
captures: Vec<Packed>,
},
Class {
fn_id: u32,
name: String,
},
BoundMethod {
receiver: Box<Packed>,
method: String,
},
Error {
kind: ErrorKind,
message: String,
file: String,
line: u32,
},
Socket(SocketState),
Bowl(BowlHandle),
}
#[derive(Clone, Debug)]
pub struct BowlHandle {
pub(crate) sender: mpsc::Sender<Packed>,
pub(crate) receiver: Arc<Mutex<mpsc::Receiver<Packed>>>,
}
impl BowlHandle {
pub fn new() -> BowlHandle {
let (sender, receiver) = mpsc::channel();
BowlHandle {
sender,
receiver: Arc::new(Mutex::new(receiver)),
}
}
}
impl Default for BowlHandle {
fn default() -> Self {
BowlHandle::new()
}
}
#[derive(Debug)]
pub struct PackedError {
kind: ErrorKind,
message: String,
location: Option<(String, u32)>,
}
impl PackedError {
pub fn from_error(err: DogeError) -> PackedError {
PackedError {
kind: err.kind,
message: err.message,
location: err.location.map(|loc| (loc.file.to_string(), loc.line)),
}
}
pub fn into_error(self) -> DogeError {
DogeError {
kind: self.kind,
message: self.message,
location: self.location.map(|(file, line)| ErrorLocation {
file: Rc::from(file),
line,
}),
}
}
}
pub type PupEntry = fn(Packed, Packed, Vec<Packed>) -> Result<Packed, PackedError>;
pub fn pack_value(value: &Value, mode: PackMode) -> DogeResult<Packed> {
pack_at(value, mode, 0)
}
pub fn pack_snapshot(value: &Value) -> Packed {
pack_value(value, PackMode::Snapshot).unwrap_or(Packed::None)
}
fn pack_at(value: &Value, mode: PackMode, depth: usize) -> DogeResult<Packed> {
if depth >= PACK_DEPTH_LIMIT {
return Err(DogeError::value_error(
"cannot send a value nested that deeply (or referring to itself) to a pup",
));
}
let next = depth + 1;
Ok(match value {
Value::Int(n) => Packed::Int(n.clone()),
Value::Float(f) => Packed::Float(*f),
Value::Decimal(d) => Packed::Decimal(d.clone()),
Value::Str(s) => Packed::Str(s.to_string()),
Value::Bytes(b) => Packed::Bytes(b.to_vec()),
Value::Bool(b) => Packed::Bool(*b),
Value::None => Packed::None,
Value::List(items) => {
let mut out = Vec::with_capacity(items.borrow().len());
for item in items.borrow().iter() {
out.push(pack_at(item, mode, next)?);
}
Packed::List(out)
}
Value::Dict(entries) => {
let entries = entries.borrow();
let mut out = Vec::with_capacity(entries.len());
for (key, val) in entries.iter() {
out.push((key.to_string(), pack_at(val, mode, next)?));
}
Packed::Dict(out)
}
Value::Object(obj) => {
let obj = obj.borrow();
let mut fields = Vec::with_capacity(obj.fields.len());
for (name, val) in obj.fields.iter() {
fields.push((name.clone(), pack_at(val, mode, next)?));
}
Packed::Object {
class_id: obj.class_id,
class_name: obj.class_name.to_string(),
fields,
}
}
Value::Function(func) => {
let mut captures = Vec::with_capacity(func.captures.len());
for cell in func.captures.iter() {
captures.push(pack_at(&cell.borrow(), mode, next)?);
}
Packed::Function {
fn_id: func.fn_id,
name: func.name.to_string(),
captures,
}
}
Value::Class(class) => Packed::Class {
fn_id: class.fn_id,
name: class.name.to_string(),
},
Value::BoundMethod(method) => Packed::BoundMethod {
receiver: Box::new(pack_at(&method.receiver, mode, next)?),
method: method.method.to_string(),
},
Value::Error(err) => Packed::Error {
kind: err.kind,
message: err.message.to_string(),
file: err.file.to_string(),
line: err.line,
},
Value::Socket(socket) => Packed::Socket(pack_socket(socket, mode)),
Value::Bowl(bowl) => Packed::Bowl(bowl.handle.clone()),
Value::Pup(_) => {
return Err(DogeError::type_error("cannot send a Pup to another pup"));
}
})
}
fn pack_socket(socket: &Rc<SocketData>, mode: PackMode) -> SocketState {
match mode {
PackMode::Transfer => socket.state.replace(SocketState::Closed),
PackMode::Snapshot => SocketState::Closed,
}
}
pub fn unpack_packed(packed: Packed) -> Value {
match packed {
Packed::Int(n) => Value::Int(n),
Packed::Float(f) => Value::Float(f),
Packed::Decimal(d) => Value::Decimal(d),
Packed::Str(s) => Value::str(s),
Packed::Bytes(b) => Value::bytes(b),
Packed::Bool(b) => Value::Bool(b),
Packed::None => Value::None,
Packed::List(items) => Value::list(items.into_iter().map(unpack_packed).collect()),
Packed::Dict(pairs) => {
let mut entries = OrderedMap::new();
for (key, val) in pairs {
entries.insert(key, unpack_packed(val));
}
Value::dict(entries)
}
Packed::Object {
class_id,
class_name,
fields,
} => {
let fields = fields
.into_iter()
.map(|(name, val)| (name, unpack_packed(val)))
.collect();
Value::Object(Rc::new(std::cell::RefCell::new(ObjectData {
class_id,
class_name: Rc::from(class_name.as_str()),
fields,
})))
}
Packed::Function {
fn_id,
name,
captures,
} => {
let cells = captures
.into_iter()
.map(|c| Rc::new(std::cell::RefCell::new(unpack_packed(c))))
.collect();
Value::function(fn_id, &name, cells)
}
Packed::Class { fn_id, name } => Value::class(fn_id, &name),
Packed::BoundMethod { receiver, method } => {
Value::bound_method(unpack_packed(*receiver), &method)
}
Packed::Error {
kind,
message,
file,
line,
} => Value::error(kind, &message, Rc::from(file.as_str()), line),
Packed::Socket(state) => Value::socket(state),
Packed::Bowl(handle) => Value::bowl(handle),
}
}
pub fn unpack_globals(globals: Packed) -> Vec<Value> {
match globals {
Packed::List(items) => items.into_iter().map(unpack_packed).collect(),
_ => Vec::new(),
}
}
pub fn finish_pup(result: DogeResult<Value>) -> Result<Packed, PackedError> {
match result {
Ok(value) => pack_value(&value, PackMode::Transfer).map_err(PackedError::from_error),
Err(err) => Err(PackedError::from_error(err)),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn round_trip(value: &Value) -> Value {
unpack_packed(pack_value(value, PackMode::Transfer).unwrap())
}
#[test]
fn scalars_and_collections_survive_a_round_trip() {
let mut map = OrderedMap::new();
map.insert("k".to_string(), Value::int(1));
let value = Value::list(vec![
Value::int(7),
Value::Float(2.5),
Value::decimal(bigdecimal::BigDecimal::from(3)),
Value::str("wow"),
Value::Bool(true),
Value::None,
Value::dict(map),
]);
assert!(crate::values_equal(&value, &round_trip(&value)));
}
#[test]
fn a_copy_shares_nothing_with_the_original() {
let inner = Value::list(vec![Value::int(1)]);
let copy = round_trip(&inner);
if let Value::List(items) = © {
items.borrow_mut().push(Value::int(2));
}
let Value::List(original) = &inner else {
unreachable!()
};
assert_eq!(original.borrow().len(), 1);
}
#[test]
fn a_pup_cannot_be_packed() {
let err = pack_value(&fake_pup(), PackMode::Transfer).unwrap_err();
assert_eq!(err.kind, ErrorKind::TypeError);
}
fn fake_pup() -> Value {
let handle = std::thread::spawn(|| Ok(Packed::None));
Value::pup(handle)
}
#[test]
fn a_deeply_nested_value_is_a_catchable_error_not_a_hang() {
let handle = std::thread::Builder::new()
.stack_size(64 * 1024 * 1024)
.spawn(|| {
let list = Value::list(vec![]);
if let Value::List(items) = &list {
items.borrow_mut().push(list.clone());
}
pack_value(&list, PackMode::Transfer).unwrap_err().kind
})
.unwrap();
assert_eq!(handle.join().unwrap(), ErrorKind::ValueError);
}
#[test]
fn transfer_moves_a_socket_but_snapshot_leaves_it_open() {
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("binds a loopback port");
let socket = Value::socket(SocketState::Listener(listener));
let _ = pack_value(&socket, PackMode::Snapshot).unwrap();
let Value::Socket(handle) = &socket else {
unreachable!()
};
assert!(
matches!(&*handle.state.borrow(), SocketState::Listener(_)),
"snapshot leaves the original open"
);
let _ = pack_value(&socket, PackMode::Transfer).unwrap();
assert!(
matches!(&*handle.state.borrow(), SocketState::Closed),
"transfer closes the original"
);
}
}