use std::sync::mpsc::RecvError;
use crate::error::{DogeError, DogeResult};
use crate::pack::{pack_value, unpack_packed, BowlHandle, PackMode, Packed, PackedError, PupEntry};
use crate::value::{BowlData, PupState, Value};
const PUP_STACK_SIZE: usize = 256 * 1024 * 1024;
pub fn spawn_pup<F>(job: F) -> DogeResult
where
F: FnOnce() -> Result<Packed, PackedError> + Send + 'static,
{
std::thread::Builder::new()
.stack_size(PUP_STACK_SIZE)
.spawn(job)
.map(Value::pup)
.map_err(|err| DogeError::io_error(format!("cannot start a pup: {err}")))
}
pub fn pack_zoom(entry: PupEntry, globals: Packed, f: &Value, args: &Value) -> DogeResult {
if !matches!(
f,
Value::Function(_) | Value::Class(_) | Value::BoundMethod(_)
) {
return Err(DogeError::type_error(format!(
"pack.zoom needs something callable to run, got {}",
f.describe()
)));
}
let items = match args {
Value::List(items) => items.borrow(),
_ => {
return Err(DogeError::type_error(format!(
"pack.zoom needs a List of arguments, got {}",
args.describe()
)))
}
};
let packed_f = pack_value(f, PackMode::Snapshot)?;
let mut packed_args = Vec::with_capacity(items.len());
for item in items.iter() {
packed_args.push(pack_value(item, PackMode::Transfer)?);
}
drop(items);
spawn_pup(move || entry(globals, packed_f, packed_args))
}
pub fn pack_fetch(pup: &Value) -> DogeResult {
let pup = match pup {
Value::Pup(pup) => pup,
_ => {
return Err(DogeError::type_error(format!(
"pack.fetch needs a Pup, got {}",
pup.describe()
)))
}
};
let handle = match pup.state.replace(PupState::Fetched) {
PupState::Running(handle) => handle,
PupState::Fetched => {
return Err(DogeError::value_error(
"this pup was already fetched — a pup's result can only be taken once",
))
}
};
match handle.join() {
Ok(Ok(value)) => Ok(unpack_packed(value)),
Ok(Err(err)) => Err(err.into_error()),
Err(_) => Err(DogeError::io_error("a pup crashed unexpectedly")),
}
}
pub fn pack_bowl() -> DogeResult {
Ok(Value::bowl(BowlHandle::new()))
}
pub fn pack_drop(bowl: &Value, value: &Value) -> DogeResult {
let bowl = bowl_arg("drop", bowl)?;
let packed = pack_value(value, PackMode::Transfer)?;
bowl.handle
.sender
.send(packed)
.map(|()| Value::None)
.map_err(|_| DogeError::io_error("this bowl has no readers left"))
}
pub fn pack_sniff(bowl: &Value) -> DogeResult {
let bowl = bowl_arg("sniff", bowl)?;
let receiver = bowl
.handle
.receiver
.lock()
.unwrap_or_else(|poison| poison.into_inner());
match receiver.recv() {
Ok(value) => Ok(unpack_packed(value)),
Err(RecvError) => Err(DogeError::io_error(
"this bowl is empty and every writer is gone — nothing left to sniff",
)),
}
}
fn bowl_arg<'a>(fname: &str, value: &'a Value) -> DogeResult<&'a BowlData> {
match value {
Value::Bowl(bowl) => Ok(bowl),
_ => Err(DogeError::type_error(format!(
"pack.{fname} needs a Bowl, got {}",
value.describe()
))),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::ErrorKind;
use crate::pack::finish_pup;
fn stub_entry(_globals: Packed, _f: Packed, _args: Vec<Packed>) -> Result<Packed, PackedError> {
finish_pup(Ok(Value::None))
}
#[test]
fn a_pup_returns_its_value_through_fetch() {
let pup = spawn_pup(|| finish_pup(Ok(Value::int(49)))).unwrap();
assert!(crate::values_equal(
&pack_fetch(&pup).unwrap(),
&Value::int(49)
));
}
#[test]
fn a_pups_error_is_re_raised_by_fetch() {
let pup = spawn_pup(|| finish_pup(Err(DogeError::value_error("pup went bad")))).unwrap();
let err = pack_fetch(&pup).unwrap_err();
assert_eq!(err.kind, ErrorKind::ValueError);
assert_eq!(err.message, "pup went bad");
}
#[test]
fn fetching_twice_is_a_catchable_error() {
let pup = spawn_pup(|| finish_pup(Ok(Value::None))).unwrap();
pack_fetch(&pup).unwrap();
assert_eq!(pack_fetch(&pup).unwrap_err().kind, ErrorKind::ValueError);
}
#[test]
fn fetch_and_bowl_ops_reject_wrong_handles() {
assert_eq!(
pack_fetch(&Value::int(1)).unwrap_err().kind,
ErrorKind::TypeError
);
assert_eq!(
pack_drop(&Value::int(1), &Value::None).unwrap_err().kind,
ErrorKind::TypeError
);
assert_eq!(
pack_sniff(&Value::int(1)).unwrap_err().kind,
ErrorKind::TypeError
);
}
#[test]
fn a_bowl_carries_a_value_between_threads() {
let bowl = pack_bowl().unwrap();
let handle = match &bowl {
Value::Bowl(bowl) => bowl.handle.clone(),
_ => unreachable!(),
};
let pup = spawn_pup(move || {
pack_drop(&Value::bowl(handle), &Value::str("treat")).ok();
finish_pup(Ok(Value::None))
})
.unwrap();
assert!(matches!(pack_sniff(&bowl).unwrap(), Value::Str(s) if &*s == "treat"));
pack_fetch(&pup).unwrap();
}
#[test]
fn zoom_rejects_a_non_callable_and_non_list() {
assert_eq!(
pack_zoom(
stub_entry,
Packed::None,
&Value::int(3),
&Value::list(vec![])
)
.unwrap_err()
.kind,
ErrorKind::TypeError
);
let f = Value::function(0, "worker", vec![]);
assert_eq!(
pack_zoom(stub_entry, Packed::None, &f, &Value::int(3))
.unwrap_err()
.kind,
ErrorKind::TypeError
);
}
#[test]
fn sending_a_pup_across_a_boundary_is_rejected() {
let inner = spawn_pup(|| finish_pup(Ok(Value::None))).unwrap();
let bowl = pack_bowl().unwrap();
assert_eq!(
pack_drop(&bowl, &inner).unwrap_err().kind,
ErrorKind::TypeError
);
pack_fetch(&inner).unwrap();
}
}