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