Skip to main content

doge_runtime/
pack.rs

1//! The `Send`-able boundary representation a value takes when it crosses a pup
2//! (thread) boundary. The runtime is single-threaded by design (`Rc`/`RefCell`),
3//! so a `Value` cannot itself move to another thread. [`pack_value`] deep-copies a
4//! value into an owned [`Packed`] tree with no `Rc`, which *is* `Send`; the other
5//! side rebuilds a fresh `Value` with [`unpack_packed`]. Copying at the boundary
6//! is the whole memory model: each pup is its own single-threaded world, so no two
7//! threads ever share a mutable cell and no locks or `unsafe` are needed.
8//!
9//! Two values behave specially because sharing, not copying, is the point:
10//! - A **bowl** (channel) is not copied — both sides get a [`BowlHandle`] to the
11//!   same channel, so a value dropped on one side can be sniffed on the other.
12//! - A **socket** moves in [`PackMode::Transfer`] positions (the sender's handle
13//!   becomes closed) and arrives closed in [`PackMode::Snapshot`] positions.
14//!
15//! A **pup** cannot cross a boundary at all — packing one is a catchable error.
16
17use std::rc::Rc;
18use std::sync::{mpsc, Arc, Mutex};
19
20use crate::error::{DogeError, DogeResult, ErrorKind, ErrorLocation};
21use crate::ordered_map::OrderedMap;
22use crate::value::{ObjectData, SocketData, SocketState, Value};
23
24/// How deep a value may nest before packing stops with a catchable error — the
25/// guard that turns a self-referential value into an error instead of an unbounded
26/// recursion. Deliberately far below the call recursion limit: packing may run on
27/// the ordinary main-thread stack (whoever calls `pack.zoom`), not the large pup
28/// stack, and real data never nests anywhere near this deep.
29const PACK_DEPTH_LIMIT: usize = 500;
30
31/// How a socket is treated when its containing value is packed.
32#[derive(Clone, Copy, PartialEq, Eq)]
33pub enum PackMode {
34    /// An explicitly sent position (a `zoom` argument, a `drop` payload): a socket
35    /// *moves* — the original handle is replaced with a closed one.
36    Transfer,
37    /// An ambient position (the globals snapshot, a closure's captures): a socket
38    /// is *not* taken from the sender; the pup receives a closed copy instead.
39    Snapshot,
40}
41
42/// An owned, `Send` mirror of a [`Value`], holding no `Rc` so it can move to
43/// another thread. Built by [`pack_value`], consumed by [`unpack_packed`].
44#[derive(Debug)]
45pub enum Packed {
46    Int(i64),
47    Float(f64),
48    Str(String),
49    Bool(bool),
50    None,
51    List(Vec<Packed>),
52    Dict(Vec<(String, Packed)>),
53    Object {
54        class_id: u32,
55        class_name: String,
56        fields: Vec<(String, Packed)>,
57    },
58    Function {
59        fn_id: u32,
60        name: String,
61        captures: Vec<Packed>,
62    },
63    Class {
64        fn_id: u32,
65        name: String,
66    },
67    BoundMethod {
68        receiver: Box<Packed>,
69        method: String,
70    },
71    Error {
72        kind: ErrorKind,
73        message: String,
74        file: String,
75        line: u32,
76    },
77    Socket(SocketState),
78    Bowl(BowlHandle),
79}
80
81/// The clonable half of a bowl: a sender plus a shared receiver over the same
82/// channel. Cloning a handle is how both sides of a pup boundary reach one
83/// channel — a bowl is deliberately shared, not copied. The channel carries
84/// [`Packed`] values, since every message crosses a thread boundary.
85#[derive(Clone, Debug)]
86pub struct BowlHandle {
87    pub(crate) sender: mpsc::Sender<Packed>,
88    pub(crate) receiver: Arc<Mutex<mpsc::Receiver<Packed>>>,
89}
90
91impl BowlHandle {
92    /// A fresh, empty channel.
93    pub fn new() -> BowlHandle {
94        let (sender, receiver) = mpsc::channel();
95        BowlHandle {
96            sender,
97            receiver: Arc::new(Mutex::new(receiver)),
98        }
99    }
100}
101
102impl Default for BowlHandle {
103    fn default() -> Self {
104        BowlHandle::new()
105    }
106}
107
108/// A [`DogeError`] flattened for the trip back from a pup to its fetcher: no `Rc`,
109/// so it is `Send`. Rebuilt into a real error (with its original raise site
110/// preserved, so `oh no` sees the same file/line) by [`PackedError::into_error`].
111#[derive(Debug)]
112pub struct PackedError {
113    kind: ErrorKind,
114    message: String,
115    location: Option<(String, u32)>,
116}
117
118impl PackedError {
119    /// Flatten an error the pup raised so it can travel back to the fetcher.
120    pub fn from_error(err: DogeError) -> PackedError {
121        PackedError {
122            kind: err.kind,
123            message: err.message,
124            location: err.location.map(|loc| (loc.file.to_string(), loc.line)),
125        }
126    }
127
128    /// Rebuild the error on the fetching side, so `pack.fetch` re-raises exactly
129    /// what the pup raised.
130    pub fn into_error(self) -> DogeError {
131        DogeError {
132            kind: self.kind,
133            message: self.message,
134            location: self.location.map(|(file, line)| ErrorLocation {
135                file: Rc::from(file),
136                line,
137            }),
138        }
139    }
140}
141
142/// The signature of the generated trampoline a pup runs: build a fresh world from
143/// the globals snapshot, unpack the callee and its arguments, run the call, and
144/// pack the result (or the error) for the trip home. A plain `fn` pointer, so it
145/// is `Send` and can be handed to a spawned thread.
146pub type PupEntry = fn(Packed, Packed, Vec<Packed>) -> Result<Packed, PackedError>;
147
148/// Deep-copy a value into its `Send` boundary form. A `Pup` cannot cross (a
149/// catchable type error), and a structure nested past [`PACK_DEPTH_LIMIT`] — which
150/// a self-referential value hits — is a catchable value error rather than an
151/// unbounded copy. `mode` decides only how a socket is treated.
152pub fn pack_value(value: &Value, mode: PackMode) -> DogeResult<Packed> {
153    pack_at(value, mode, 0)
154}
155
156/// Snapshot a value for an ambient position (the globals a pup inherits), never
157/// failing: a value that cannot be packed — a `Pup`, or one nested too deep —
158/// simply arrives as `none` in the pup rather than blocking the spawn, since the
159/// pup likely never touches that binding.
160pub fn pack_snapshot(value: &Value) -> Packed {
161    pack_value(value, PackMode::Snapshot).unwrap_or(Packed::None)
162}
163
164fn pack_at(value: &Value, mode: PackMode, depth: usize) -> DogeResult<Packed> {
165    if depth >= PACK_DEPTH_LIMIT {
166        return Err(DogeError::value_error(
167            "cannot send a value nested that deeply (or referring to itself) to a pup",
168        ));
169    }
170    let next = depth + 1;
171    Ok(match value {
172        Value::Int(n) => Packed::Int(*n),
173        Value::Float(f) => Packed::Float(*f),
174        Value::Str(s) => Packed::Str(s.to_string()),
175        Value::Bool(b) => Packed::Bool(*b),
176        Value::None => Packed::None,
177        Value::List(items) => {
178            let mut out = Vec::with_capacity(items.borrow().len());
179            for item in items.borrow().iter() {
180                out.push(pack_at(item, mode, next)?);
181            }
182            Packed::List(out)
183        }
184        Value::Dict(entries) => {
185            let entries = entries.borrow();
186            let mut out = Vec::with_capacity(entries.len());
187            for (key, val) in entries.iter() {
188                out.push((key.to_string(), pack_at(val, mode, next)?));
189            }
190            Packed::Dict(out)
191        }
192        Value::Object(obj) => {
193            let obj = obj.borrow();
194            let mut fields = Vec::with_capacity(obj.fields.len());
195            for (name, val) in obj.fields.iter() {
196                fields.push((name.clone(), pack_at(val, mode, next)?));
197            }
198            Packed::Object {
199                class_id: obj.class_id,
200                class_name: obj.class_name.to_string(),
201                fields,
202            }
203        }
204        Value::Function(func) => {
205            let mut captures = Vec::with_capacity(func.captures.len());
206            for cell in func.captures.iter() {
207                captures.push(pack_at(&cell.borrow(), mode, next)?);
208            }
209            Packed::Function {
210                fn_id: func.fn_id,
211                name: func.name.to_string(),
212                captures,
213            }
214        }
215        Value::Class(class) => Packed::Class {
216            fn_id: class.fn_id,
217            name: class.name.to_string(),
218        },
219        Value::BoundMethod(method) => Packed::BoundMethod {
220            receiver: Box::new(pack_at(&method.receiver, mode, next)?),
221            method: method.method.to_string(),
222        },
223        Value::Error(err) => Packed::Error {
224            kind: err.kind,
225            message: err.message.to_string(),
226            file: err.file.to_string(),
227            line: err.line,
228        },
229        Value::Socket(socket) => Packed::Socket(pack_socket(socket, mode)),
230        Value::Bowl(bowl) => Packed::Bowl(bowl.handle.clone()),
231        Value::Pup(_) => {
232            return Err(DogeError::type_error("cannot send a Pup to another pup"));
233        }
234    })
235}
236
237/// A socket's state for the pup: taken (leaving a closed handle behind) when it is
238/// explicitly transferred, or a fresh closed handle when it is merely snapshotted.
239fn pack_socket(socket: &Rc<SocketData>, mode: PackMode) -> SocketState {
240    match mode {
241        PackMode::Transfer => socket.state.replace(SocketState::Closed),
242        PackMode::Snapshot => SocketState::Closed,
243    }
244}
245
246/// Rebuild a fresh [`Value`] from its boundary form on the receiving thread, with
247/// brand-new `Rc`s and cells — no sharing survives the trip, which is exactly the
248/// copy semantics a pup boundary promises.
249pub fn unpack_packed(packed: Packed) -> Value {
250    match packed {
251        Packed::Int(n) => Value::Int(n),
252        Packed::Float(f) => Value::Float(f),
253        Packed::Str(s) => Value::str(s),
254        Packed::Bool(b) => Value::Bool(b),
255        Packed::None => Value::None,
256        Packed::List(items) => Value::list(items.into_iter().map(unpack_packed).collect()),
257        Packed::Dict(pairs) => {
258            let mut entries = OrderedMap::new();
259            for (key, val) in pairs {
260                entries.insert(key, unpack_packed(val));
261            }
262            Value::dict(entries)
263        }
264        Packed::Object {
265            class_id,
266            class_name,
267            fields,
268        } => {
269            let fields = fields
270                .into_iter()
271                .map(|(name, val)| (name, unpack_packed(val)))
272                .collect();
273            Value::Object(Rc::new(std::cell::RefCell::new(ObjectData {
274                class_id,
275                class_name: Rc::from(class_name.as_str()),
276                fields,
277            })))
278        }
279        Packed::Function {
280            fn_id,
281            name,
282            captures,
283        } => {
284            let cells = captures
285                .into_iter()
286                .map(|c| Rc::new(std::cell::RefCell::new(unpack_packed(c))))
287                .collect();
288            Value::function(fn_id, &name, cells)
289        }
290        Packed::Class { fn_id, name } => Value::class(fn_id, &name),
291        Packed::BoundMethod { receiver, method } => {
292            Value::bound_method(unpack_packed(*receiver), &method)
293        }
294        Packed::Error {
295            kind,
296            message,
297            file,
298            line,
299        } => Value::error(kind, &message, Rc::from(file.as_str()), line),
300        Packed::Socket(state) => Value::socket(state),
301        Packed::Bowl(handle) => Value::bowl(handle),
302    }
303}
304
305/// Unpack a globals snapshot into the values a pup's fresh `Env` fields take, in
306/// the order they were packed. Anything that is not a list snapshot yields no
307/// values, so the fields fall back to `none`.
308pub fn unpack_globals(globals: Packed) -> Vec<Value> {
309    match globals {
310        Packed::List(items) => items.into_iter().map(unpack_packed).collect(),
311        _ => Vec::new(),
312    }
313}
314
315/// Pack a call's outcome for the trip back from a pup to its fetcher: the return
316/// value is *transferred* (the pup is finishing, so a socket in the result moves
317/// home), and a raised error is flattened. A return value that cannot be packed
318/// becomes the fetched error.
319pub fn finish_pup(result: DogeResult<Value>) -> Result<Packed, PackedError> {
320    match result {
321        Ok(value) => pack_value(&value, PackMode::Transfer).map_err(PackedError::from_error),
322        Err(err) => Err(PackedError::from_error(err)),
323    }
324}
325
326#[cfg(test)]
327mod tests {
328    use super::*;
329
330    fn round_trip(value: &Value) -> Value {
331        unpack_packed(pack_value(value, PackMode::Transfer).unwrap())
332    }
333
334    #[test]
335    fn scalars_and_collections_survive_a_round_trip() {
336        let mut map = OrderedMap::new();
337        map.insert("k".to_string(), Value::Int(1));
338        let value = Value::list(vec![
339            Value::Int(7),
340            Value::Float(2.5),
341            Value::str("wow"),
342            Value::Bool(true),
343            Value::None,
344            Value::dict(map),
345        ]);
346        assert!(crate::values_equal(&value, &round_trip(&value)));
347    }
348
349    #[test]
350    fn a_copy_shares_nothing_with_the_original() {
351        let inner = Value::list(vec![Value::Int(1)]);
352        let copy = round_trip(&inner);
353        // Mutating the copy must not touch the original — the trip severed sharing.
354        if let Value::List(items) = &copy {
355            items.borrow_mut().push(Value::Int(2));
356        }
357        let Value::List(original) = &inner else {
358            unreachable!()
359        };
360        assert_eq!(original.borrow().len(), 1);
361    }
362
363    #[test]
364    fn a_pup_cannot_be_packed() {
365        // A bowl stands in for any un-sendable handle here; a real Pup needs a
366        // spawned thread, but the type-error path is what matters.
367        let err = pack_value(&fake_pup(), PackMode::Transfer).unwrap_err();
368        assert_eq!(err.kind, ErrorKind::TypeError);
369    }
370
371    fn fake_pup() -> Value {
372        // A pup whose thread has already finished, built without spawning.
373        let handle = std::thread::spawn(|| Ok(Packed::None));
374        Value::pup(handle)
375    }
376
377    #[test]
378    fn a_deeply_nested_value_is_a_catchable_error_not_a_hang() {
379        // A list that contains itself: packing must stop with an error, not recurse
380        // forever. Run on a generous stack so the depth guard — not a stack
381        // overflow — is what stops it, exactly as it would in a real program.
382        let handle = std::thread::Builder::new()
383            .stack_size(64 * 1024 * 1024)
384            .spawn(|| {
385                let list = Value::list(vec![]);
386                if let Value::List(items) = &list {
387                    items.borrow_mut().push(list.clone());
388                }
389                pack_value(&list, PackMode::Transfer).unwrap_err().kind
390            })
391            .unwrap();
392        assert_eq!(handle.join().unwrap(), ErrorKind::ValueError);
393    }
394
395    #[test]
396    fn transfer_moves_a_socket_but_snapshot_leaves_it_open() {
397        let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("binds a loopback port");
398        let socket = Value::socket(SocketState::Listener(listener));
399
400        // A snapshot leaves the original socket untouched (still a live listener).
401        let _ = pack_value(&socket, PackMode::Snapshot).unwrap();
402        let Value::Socket(handle) = &socket else {
403            unreachable!()
404        };
405        assert!(
406            matches!(&*handle.state.borrow(), SocketState::Listener(_)),
407            "snapshot leaves the original open"
408        );
409
410        // A transfer takes the live handle, leaving the original closed.
411        let _ = pack_value(&socket, PackMode::Transfer).unwrap();
412        assert!(
413            matches!(&*handle.state.borrow(), SocketState::Closed),
414            "transfer closes the original"
415        );
416    }
417}