Skip to main content

doge_runtime/stdlib/
pack.rs

1//! `pack` — the concurrency stdlib module. `zoom` spawns a function onto its own
2//! OS thread (a *pup*), `fetch` waits for its result, and a *bowl* (`bowl`/`drop`/
3//! `sniff`) is a channel pups pass values over. Each pup is its own single-threaded
4//! world: arguments, captures, and globals are deep-copied in, results copied back
5//! (see [`crate::pack`]), so no two threads share a mutable cell — no locks, no
6//! `unsafe`. Every misuse — a non-callable `zoom`, a double `fetch`, a wrong handle
7//! type — is a catchable error rather than a panic.
8
9use std::sync::mpsc::RecvError;
10
11use crate::error::{DogeError, DogeResult};
12use crate::pack::{pack_value, unpack_packed, BowlHandle, PackMode, Packed, PackedError, PupEntry};
13use crate::value::{BowlData, PupState, Value};
14
15/// The stack a pup's thread gets. Matches the large stack the CLI runs the
16/// interpreter on: a pup must be able to nest calls up to the same recursion limit
17/// the main thread allows, and that limit — not a stack overflow — is what stops
18/// runaway recursion inside a pup.
19const PUP_STACK_SIZE: usize = 256 * 1024 * 1024;
20
21/// Spawn a job onto a fresh pup thread and hand back the pup value. The job
22/// produces the packed result (or error) the pup's `fetch` will return. Shared by
23/// both engines: the compiler passes a generated trampoline, the interpreter a
24/// closure that runs a fresh interpreter. A thread the OS refuses to start is a
25/// catchable IOError.
26pub fn spawn_pup<F>(job: F) -> DogeResult
27where
28    F: FnOnce() -> Result<Packed, PackedError> + Send + 'static,
29{
30    std::thread::Builder::new()
31        .stack_size(PUP_STACK_SIZE)
32        .spawn(job)
33        .map(Value::pup)
34        .map_err(|err| DogeError::io_error(format!("cannot start a pup: {err}")))
35}
36
37/// `pack.zoom(f, args)` — spawn `f` onto a new pup, called with the List `args`.
38/// The callee's captures and the caller's top-level variables are snapshotted in;
39/// each argument is transferred in. `f` must be callable and `args` must be a List,
40/// or it is a catchable type error. `entry` is the generated trampoline that
41/// rebuilds a world inside the pup and runs the call.
42pub fn pack_zoom(entry: PupEntry, globals: Packed, f: &Value, args: &Value) -> DogeResult {
43    if !matches!(
44        f,
45        Value::Function(_) | Value::Class(_) | Value::BoundMethod(_)
46    ) {
47        return Err(DogeError::type_error(format!(
48            "pack.zoom needs something callable to run, got {}",
49            f.describe()
50        )));
51    }
52    let items = match args {
53        Value::List(items) => items.borrow(),
54        _ => {
55            return Err(DogeError::type_error(format!(
56                "pack.zoom needs a List of arguments, got {}",
57                args.describe()
58            )))
59        }
60    };
61    let packed_f = pack_value(f, PackMode::Snapshot)?;
62    let mut packed_args = Vec::with_capacity(items.len());
63    for item in items.iter() {
64        packed_args.push(pack_value(item, PackMode::Transfer)?);
65    }
66    drop(items);
67    spawn_pup(move || entry(globals, packed_f, packed_args))
68}
69
70/// `pack.fetch(pup)` — wait for a pup to finish and return its function's result,
71/// or re-raise (catchably) the error the pup hit, with the pup's own file/line
72/// preserved. Fetching a pup twice, or a value that is not a pup, is a catchable
73/// error.
74pub fn pack_fetch(pup: &Value) -> DogeResult {
75    let pup = match pup {
76        Value::Pup(pup) => pup,
77        _ => {
78            return Err(DogeError::type_error(format!(
79                "pack.fetch needs a Pup, got {}",
80                pup.describe()
81            )))
82        }
83    };
84    let handle = match pup.state.replace(PupState::Fetched) {
85        PupState::Running(handle) => handle,
86        PupState::Fetched => {
87            return Err(DogeError::value_error(
88                "this pup was already fetched — a pup's result can only be taken once",
89            ))
90        }
91    };
92    match handle.join() {
93        Ok(Ok(value)) => Ok(unpack_packed(value)),
94        Ok(Err(err)) => Err(err.into_error()),
95        // The pup's thread unwound. Generated code and the interpreter are
96        // panic-free by construction, so this is a Doge compiler bug surfaced as a
97        // catchable error rather than a leaked Rust panic.
98        Err(_) => Err(DogeError::io_error("a pup crashed unexpectedly")),
99    }
100}
101
102/// `pack.bowl()` — open a fresh, empty bowl (channel).
103pub fn pack_bowl() -> DogeResult {
104    Ok(Value::bowl(BowlHandle::new()))
105}
106
107/// `pack.drop(bowl, value)` — send `value` into a bowl (transferring it, so a
108/// socket moves along). Returns `none`. A non-bowl handle is a catchable type
109/// error; a bowl whose every reader is gone is a catchable IOError rather than a
110/// silent loss.
111pub fn pack_drop(bowl: &Value, value: &Value) -> DogeResult {
112    let bowl = bowl_arg("drop", bowl)?;
113    let packed = pack_value(value, PackMode::Transfer)?;
114    bowl.handle
115        .sender
116        .send(packed)
117        .map(|()| Value::None)
118        .map_err(|_| DogeError::io_error("this bowl has no readers left"))
119}
120
121/// `pack.sniff(bowl)` — block until a value arrives in the bowl, then return it.
122/// A bowl that is empty and can never receive another value again (every writer is
123/// gone) is a catchable IOError rather than a forever-block. A non-bowl handle is a
124/// catchable type error.
125pub fn pack_sniff(bowl: &Value) -> DogeResult {
126    let bowl = bowl_arg("sniff", bowl)?;
127    let receiver = bowl
128        .handle
129        .receiver
130        .lock()
131        .unwrap_or_else(|poison| poison.into_inner());
132    match receiver.recv() {
133        Ok(value) => Ok(unpack_packed(value)),
134        Err(RecvError) => Err(DogeError::io_error(
135            "this bowl is empty and every writer is gone — nothing left to sniff",
136        )),
137    }
138}
139
140/// A bowl argument as its shared data, or a catchable type error naming the member.
141fn bowl_arg<'a>(fname: &str, value: &'a Value) -> DogeResult<&'a BowlData> {
142    match value {
143        Value::Bowl(bowl) => Ok(bowl),
144        _ => Err(DogeError::type_error(format!(
145            "pack.{fname} needs a Bowl, got {}",
146            value.describe()
147        ))),
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154    use crate::error::ErrorKind;
155    use crate::pack::finish_pup;
156
157    /// A stand-in trampoline: zoom's argument checks fire before it is ever
158    /// invoked, so it only needs the [`PupEntry`] shape.
159    fn stub_entry(_globals: Packed, _f: Packed, _args: Vec<Packed>) -> Result<Packed, PackedError> {
160        finish_pup(Ok(Value::None))
161    }
162
163    #[test]
164    fn a_pup_returns_its_value_through_fetch() {
165        let pup = spawn_pup(|| finish_pup(Ok(Value::int(49)))).unwrap();
166        assert!(crate::values_equal(
167            &pack_fetch(&pup).unwrap(),
168            &Value::int(49)
169        ));
170    }
171
172    #[test]
173    fn a_pups_error_is_re_raised_by_fetch() {
174        let pup = spawn_pup(|| finish_pup(Err(DogeError::value_error("pup went bad")))).unwrap();
175        let err = pack_fetch(&pup).unwrap_err();
176        assert_eq!(err.kind, ErrorKind::ValueError);
177        assert_eq!(err.message, "pup went bad");
178    }
179
180    #[test]
181    fn fetching_twice_is_a_catchable_error() {
182        let pup = spawn_pup(|| finish_pup(Ok(Value::None))).unwrap();
183        pack_fetch(&pup).unwrap();
184        assert_eq!(pack_fetch(&pup).unwrap_err().kind, ErrorKind::ValueError);
185    }
186
187    #[test]
188    fn fetch_and_bowl_ops_reject_wrong_handles() {
189        assert_eq!(
190            pack_fetch(&Value::int(1)).unwrap_err().kind,
191            ErrorKind::TypeError
192        );
193        assert_eq!(
194            pack_drop(&Value::int(1), &Value::None).unwrap_err().kind,
195            ErrorKind::TypeError
196        );
197        assert_eq!(
198            pack_sniff(&Value::int(1)).unwrap_err().kind,
199            ErrorKind::TypeError
200        );
201    }
202
203    #[test]
204    fn a_bowl_carries_a_value_between_threads() {
205        let bowl = pack_bowl().unwrap();
206        // Only the channel handle (which is Send) crosses to the pup, exactly as a
207        // real dropped/zoomed bowl does; the pup rebuilds its bowl value from it,
208        // drops a value in, and the main thread sniffs it out of the same channel.
209        let handle = match &bowl {
210            Value::Bowl(bowl) => bowl.handle.clone(),
211            _ => unreachable!(),
212        };
213        let pup = spawn_pup(move || {
214            pack_drop(&Value::bowl(handle), &Value::str("treat")).ok();
215            finish_pup(Ok(Value::None))
216        })
217        .unwrap();
218        assert!(matches!(pack_sniff(&bowl).unwrap(), Value::Str(s) if &*s == "treat"));
219        pack_fetch(&pup).unwrap();
220    }
221
222    #[test]
223    fn zoom_rejects_a_non_callable_and_non_list() {
224        assert_eq!(
225            pack_zoom(
226                stub_entry,
227                Packed::None,
228                &Value::int(3),
229                &Value::list(vec![])
230            )
231            .unwrap_err()
232            .kind,
233            ErrorKind::TypeError
234        );
235        // A callable but a non-List argument bundle.
236        let f = Value::function(0, "worker", vec![]);
237        assert_eq!(
238            pack_zoom(stub_entry, Packed::None, &f, &Value::int(3))
239                .unwrap_err()
240                .kind,
241            ErrorKind::TypeError
242        );
243    }
244
245    #[test]
246    fn sending_a_pup_across_a_boundary_is_rejected() {
247        let inner = spawn_pup(|| finish_pup(Ok(Value::None))).unwrap();
248        // Dropping a pup into a bowl tries to pack it — a catchable type error.
249        let bowl = pack_bowl().unwrap();
250        assert_eq!(
251            pack_drop(&bowl, &inner).unwrap_err().kind,
252            ErrorKind::TypeError
253        );
254        pack_fetch(&inner).unwrap();
255    }
256}