Skip to main content

byteflow/
samples.rs

1//! Built-in demo chunks assembled with [`crate::Program`].
2//!
3//! Use these as runnable specs of the messaging contract (and as regression
4//! tests). Prefer copying a sample over inventing hop register layouts from
5//! scratch.
6//!
7//! | Sample | Shows |
8//! |--------|--------|
9//! | [`add_forty_two`] | Scalar VM path (no natives) |
10//! | [`ping_pong`] | Cap spawn + Atomic Hop round-trip |
11//! | [`atomic_request_reply`] | Tagged REQ/REP + `print` |
12//! | [`selective_receive`] | `ReceiveMatch` FIFO skip |
13//! | [`ask_reply`] | `Ask` RPC hop |
14//! | [`ask_timeout_expires`] | `AskTimeout` writes `Unit` when the server stays silent |
15//! | [`ask_target_exits`] | `Ask` dest is `TAG_SYS_EXIT` when the server dies first |
16//! | [`server_loop`] | BEAM-style receive → handle → reply loop |
17//! | [`forged_sender_send`] / [`forged_sender_ask`] | S1: forged `make_msg` sender dies |
18//! | [`boom`] | Immediate trap (supervisor demos) |
19//! | [`monitor_down`] | Monitor → [`crate::TAG_SYS_DOWN`] on child exit |
20//! | [`waiting_send`] | `WAITING_SEND`: second hop parks until the first is received |
21//!
22//! Hop samples require [`crate::std_native_table`].
23
24use crate::{Chunk, Program};
25use crate::natives::std_native;
26
27/// Native indices (must match [`crate::std_native_map`]).
28const N_PRINT: u32 = std_native::PRINT;
29const N_MAKE_MSG: u32 = std_native::MAKE_MSG;
30
31/// Protocol tags for Atomic Hop samples.
32pub const TAG_REQ: i32 = 1;
33pub const TAG_REP: i32 = 2;
34pub const TAG_PING: i32 = 10;
35pub const TAG_PONG: i32 = 11;
36/// Decoy hop for [`selective_receive`] — must be skipped by `ReceiveMatch`.
37pub const TAG_JUNK: i32 = 99;
38
39/// `41 + 1; return` — the 60-second sanity chunk.
40pub fn add_forty_two() -> Chunk {
41    let mut p = Program::new("add-forty-two");
42    p.function("main", 0, |f| {
43        let a = f.load_i32(41);
44        let b = f.load_i32(1);
45        let sum = f.add(a, b);
46        f.return_(sum);
47    });
48    p.build()
49}
50
51/// Two flows, one **Atomic Hop** round-trip: `main` sends a `Message` to
52/// `pong`, `pong` replies with payload+1 via `msg_reply_cap`, `main` returns
53/// that payload (`2`).
54pub fn ping_pong() -> Chunk {
55    let mut p = Program::new("ping-pong");
56    let pong = p.function("pong", 0, |f| {
57        let msg = f.receive();
58        let payload = f.hop_payload(msg);
59        f.add_imm(payload, 1);
60        f.send_reply(msg, TAG_PONG, payload);
61        f.exit(payload);
62    });
63    p.function("main", 0, |f| {
64        let child = f.spawn(pong, 0);
65        let req_id = f.load_i32(1);
66        let payload = f.load_i32(1);
67        let req = f.hop(req_id, TAG_PING, payload);
68        f.send(child, req);
69        let reply = f.receive();
70        let out = f.hop_payload(reply);
71        f.return_(out);
72    });
73    p.build()
74}
75
76/// Atomic request-reply with [`crate::Value::Message`] (one envelope per hop).
77pub fn atomic_request_reply() -> Chunk {
78    let mut p = Program::new("atomic-request-reply");
79    let server = p.function("server", 0, |f| {
80        let msg = f.receive();
81        f.native1_on(msg, N_PRINT);
82        let payload = f.hop_payload(msg);
83        f.add_imm(payload, 1);
84        f.send_reply(msg, TAG_REP, payload);
85        f.exit(payload);
86    });
87    p.function("main", 0, |f| {
88        let server_cap = f.spawn(server, 0);
89        let req_id = f.load_i32(1);
90        let payload = f.load_i32(41);
91        let req = f.hop(req_id, TAG_REQ, payload);
92        f.native1_on(req, N_PRINT);
93        f.send(server_cap, req);
94        let reply = f.receive();
95        f.native1_on(reply, N_PRINT);
96        let out = f.hop_payload(reply);
97        f.return_(out);
98    });
99    p.build()
100}
101
102/// Selective Atomic Hop: server waits for `TAG_REQ` while a `TAG_JUNK` hop
103/// sits ahead in the mailbox (FIFO skip, not drop).
104pub fn selective_receive() -> Chunk {
105    let mut p = Program::new("selective-receive");
106    let server = p.function("server", 0, |f| {
107        let msg = f.receive_match_imm(TAG_REQ as u16);
108        let payload = f.hop_payload(msg);
109        f.add_imm(payload, 1);
110        f.send_reply(msg, TAG_REP, payload);
111        let junk = f.receive();
112        let tag = f.hop_tag(junk);
113        let is_junk = f.eq_imm(tag, TAG_JUNK);
114        let trap_lbl = f.label();
115        f.branch_if_falsy(is_junk, trap_lbl);
116        f.exit(payload);
117        f.bind(trap_lbl);
118        f.trap(2);
119    });
120    p.function("main", 0, |f| {
121        let server_cap = f.spawn(server, 0);
122        let req_id = f.load_i32(1);
123        let zero = f.load_i32(0);
124        let junk = f.hop(req_id, TAG_JUNK, zero);
125        f.send(server_cap, junk);
126        let payload = f.load_i32(41);
127        let req = f.hop(req_id, TAG_REQ, payload);
128        f.send(server_cap, req);
129        let reply = f.receive_match_imm(TAG_REP as u16);
130        let out = f.hop_payload(reply);
131        f.return_(out);
132    });
133    p.build()
134}
135
136/// Atomic request/reply via `Ask` (RPC hop).
137pub fn ask_reply() -> Chunk {
138    let mut p = Program::new("ask-reply");
139    let server = p.function("server", 0, |f| {
140        let msg = f.receive_match_imm(TAG_REQ as u16);
141        let payload = f.hop_payload(msg);
142        f.add_imm(payload, 1);
143        f.send_reply(msg, TAG_REP, payload);
144        f.exit(payload);
145    });
146    p.function("main", 0, |f| {
147        let server_cap = f.spawn(server, 0);
148        let req_id = f.load_i32(1);
149        let payload = f.load_i32(41);
150        let req = f.hop(req_id, TAG_REQ, payload);
151        let reply = f.ask(server_cap, req);
152        let out = f.hop_payload(reply);
153        f.return_(out);
154    });
155    p.build()
156}
157
158/// `AskTimeout` against a server that never replies → `Unit`.
159pub fn ask_timeout_expires() -> Chunk {
160    let mut p = Program::new("ask-timeout");
161    let server = p.function("server", 0, |f| {
162        let _msg = f.receive();
163        let ms = f.load_i32(10_000);
164        f.sleep(ms);
165        let zero = f.load_i32(0);
166        f.return_(zero);
167    });
168    p.function("main", 0, |f| {
169        let server_cap = f.spawn(server, 0);
170        let req_id = f.load_i32(1);
171        let payload = f.load_i32(0);
172        let req = f.hop(req_id, TAG_REQ, payload);
173        let ms = f.load_i32(40);
174        let reply = f.ask_timeout(server_cap, req, ms);
175        f.return_(reply);
176    });
177    p.build()
178}
179
180/// Server takes the request and exits; client `Ask` must not hang.
181pub fn ask_target_exits() -> Chunk {
182    let mut p = Program::new("ask-target-exits");
183    let server = p.function("server", 0, |f| {
184        let _msg = f.receive();
185        let z = f.load_i32(0);
186        f.return_(z);
187    });
188    p.function("main", 0, |f| {
189        let server_cap = f.spawn(server, 0);
190        let req_id = f.load_i32(1);
191        let payload = f.load_i32(0);
192        let req = f.hop(req_id, TAG_REQ, payload);
193        let reply = f.ask(server_cap, req);
194        let tag = f.hop_tag(reply);
195        f.return_(tag);
196    });
197    p.build()
198}
199
200/// BEAM-style server loop: `receive_match` → handle → `send_reply` → repeat.
201pub fn server_loop() -> Chunk {
202    let mut p = Program::new("server-loop");
203    let server = p.function("server", 0, |f| {
204        let loop_lbl = f.label();
205        f.bind(loop_lbl);
206        let req = f.receive_match_imm(TAG_REQ as u16);
207        let payload = f.hop_payload(req);
208        f.add_imm(payload, 1);
209        f.send_reply(req, TAG_REP, payload);
210        f.jump(loop_lbl);
211    });
212    p.function("main", 0, |f| {
213        let server_cap = f.spawn(server, 0);
214        let req_id = f.load_i32(1);
215        let payload = f.load_i32(41);
216        let req = f.hop(req_id, TAG_REQ, payload);
217        f.send(server_cap, req);
218        let reply = f.receive_match_imm(TAG_REP as u16);
219        let out = f.hop_payload(reply);
220        f.return_(out);
221    });
222    p.build()
223}
224
225/// Security regression: forged `make_msg` sender must not survive `Send`.
226pub fn forged_sender_send() -> Chunk {
227    let mut p = Program::new("forged-sender-send");
228    let server = p.function("server", 0, |f| {
229        let msg = f.receive();
230        let sender = f.hop_sender(msg);
231        f.send_reply(msg, TAG_REP, sender);
232        f.exit(sender);
233    });
234    p.function("main", 0, |f| {
235        let server_cap = f.spawn(server, 0);
236        let forged = f.load_i32(999);
237        let req_id = f.load_i32(1);
238        let zero = f.load_i32(0);
239        let req = f.make_msg_legacy_sender(N_MAKE_MSG, forged, req_id, TAG_REQ, zero);
240        f.send(server_cap, req);
241        let reply = f.receive();
242        let out = f.hop_payload(reply);
243        f.return_(out);
244    });
245    p.build()
246}
247
248/// Same security property as [`forged_sender_send`], via `Ask`.
249pub fn forged_sender_ask() -> Chunk {
250    let mut p = Program::new("forged-sender-ask");
251    let server = p.function("server", 0, |f| {
252        let msg = f.receive_match_imm(TAG_REQ as u16);
253        let sender = f.hop_sender(msg);
254        f.send_reply(msg, TAG_REP, sender);
255        f.exit(sender);
256    });
257    p.function("main", 0, |f| {
258        let server_cap = f.spawn(server, 0);
259        let forged = f.load_i32(999);
260        let req_id = f.load_i32(1);
261        let zero = f.load_i32(0);
262        let req = f.make_msg_legacy_sender(N_MAKE_MSG, forged, req_id, TAG_REQ, zero);
263        let reply = f.ask(server_cap, req);
264        let out = f.hop_payload(reply);
265        f.return_(out);
266    });
267    p.build()
268}
269
270/// Child sleeps then exits; parent monitors and returns the `DOWN` reason (`0` = normal).
271pub fn monitor_down() -> Chunk {
272    let mut p = Program::new("monitor-down");
273    let child = p.function("child", 0, |f| {
274        let ms = f.load_i32(40);
275        f.sleep(ms);
276        let z = f.load_i32(0);
277        f.return_(z);
278    });
279    p.function("main", 0, |f| {
280        let cap = f.spawn(child, 0);
281        let mon = f.monitor(cap);
282        let msg = f.receive_match_imm(crate::TAG_SYS_DOWN);
283        let id = f.hop_request_id(msg);
284        let ok = f.eq(id, mon);
285        let trap = f.label();
286        f.branch_if_falsy(ok, trap);
287        let out = f.hop_payload(msg);
288        f.return_(out);
289        f.bind(trap);
290        f.trap(3);
291    });
292    p.build()
293}
294
295/// Client arity 1 (`r0` = server Cap) sends two hops; server drains both.
296/// With mailbox capacity 1 the second send parks (`WAITING_SEND`).
297pub fn waiting_send() -> Chunk {
298    let mut p = Program::new("waiting-send");
299    p.function("server", 0, |f| {
300        let ms = f.load_i32(50);
301        f.sleep(ms);
302        let _first = f.receive();
303        let second = f.receive();
304        let out = f.hop_payload(second);
305        f.return_(out);
306    });
307    p.function("client", 1, |f| {
308        let server = f.reg(0);
309        let req_id = f.load_i32(1);
310        let one = f.load_i32(1);
311        let first = f.hop(req_id, TAG_REQ, one);
312        f.send(server, first);
313        let forty_two = f.load_i32(42);
314        let second = f.hop(req_id, TAG_REQ, forty_two);
315        f.send(server, second);
316        f.exit(second);
317    });
318    p.build()
319}
320
321/// Immediate `Trap` — used to show [`crate::Supervisor`] restart.
322pub fn boom() -> Chunk {
323    let mut p = Program::new("boom");
324    p.function("boom", 0, |f| f.trap(1));
325    p.build()
326}
327
328#[cfg(test)]
329mod tests {
330    use super::*;
331    use crate::{
332        bytecode::CapId, decode, encode, std_native_table, verify, FlowOutcome, Runtime,
333        RuntimeConfig, Value,
334    };
335
336    fn tiny(chunk: Chunk) -> Result<Runtime, crate::SpawnError> {
337        Runtime::with_config(
338            chunk,
339            RuntimeConfig {
340                workers: 1,
341                quantum: 10_000,
342                mailbox: crate::MailboxConfig::DEFAULT,
343                ..Default::default()
344            },
345        )
346    }
347
348    fn tiny_natives(chunk: Chunk) -> Result<Runtime, crate::SpawnError> {
349        Runtime::with_natives_and_config(
350            chunk,
351            std_native_table(),
352            RuntimeConfig {
353                workers: 1,
354                quantum: 10_000,
355                mailbox: crate::MailboxConfig::DEFAULT,
356                ..Default::default()
357            },
358        )
359    }
360
361    #[test]
362    fn add_forty_two_joins_42() -> Result<(), Box<dyn std::error::Error>> {
363        let rt = tiny(add_forty_two())?;
364        let idx = rt.function_index("main").ok_or("main")?;
365        let outcome = rt.spawn(idx, &[])?.join();
366        rt.shutdown();
367        assert!(matches!(outcome, FlowOutcome::Completed(Value::Int(42))));
368        Ok(())
369    }
370
371    #[test]
372    fn ping_pong_joins_2() -> Result<(), Box<dyn std::error::Error>> {
373        let chunk = ping_pong();
374        assert!(verify(&chunk).is_ok());
375        let bytes = encode(&chunk);
376        let chunk = decode(&bytes)?;
377        let rt = tiny_natives(chunk)?;
378        let idx = rt.function_index("main").ok_or("main")?;
379        let outcome = rt.spawn(idx, &[])?.join();
380        let sent = rt.metrics().messages_sent;
381        rt.shutdown();
382        assert!(matches!(outcome, FlowOutcome::Completed(Value::Int(2))));
383        assert!(sent >= 2);
384        Ok(())
385    }
386
387    #[test]
388    fn atomic_request_reply_joins_42() -> Result<(), Box<dyn std::error::Error>> {
389        let chunk = atomic_request_reply();
390        assert!(verify(&chunk).is_ok());
391        let bytes = encode(&chunk);
392        let chunk = decode(&bytes)?;
393        let rt = tiny_natives(chunk)?;
394        let idx = rt.function_index("main").ok_or("main")?;
395        let outcome = rt.spawn(idx, &[])?.join();
396        let sent = rt.metrics().messages_sent;
397        rt.shutdown();
398        assert!(
399            matches!(outcome, FlowOutcome::Completed(Value::Int(42))),
400            "got {outcome:?}"
401        );
402        assert!(sent >= 1);
403        Ok(())
404    }
405
406    #[test]
407    fn selective_receive_skips_junk_tag() -> Result<(), Box<dyn std::error::Error>> {
408        let chunk = selective_receive();
409        assert!(verify(&chunk).is_ok());
410        let bytes = encode(&chunk);
411        let chunk = decode(&bytes)?;
412        let rt = tiny_natives(chunk)?;
413        let idx = rt.function_index("main").ok_or("main")?;
414        let outcome = rt.spawn(idx, &[])?.join();
415        rt.shutdown();
416        assert!(
417            matches!(outcome, FlowOutcome::Completed(Value::Int(42))),
418            "got {outcome:?}"
419        );
420        Ok(())
421    }
422
423    #[test]
424    fn ask_reply_joins_42() -> Result<(), Box<dyn std::error::Error>> {
425        let chunk = ask_reply();
426        assert!(verify(&chunk).is_ok());
427        let bytes = encode(&chunk);
428        let chunk = decode(&bytes)?;
429        let rt = tiny_natives(chunk)?;
430        let idx = rt.function_index("main").ok_or("main")?;
431        let outcome = rt.spawn(idx, &[])?.join();
432        let sent = rt.metrics().messages_sent;
433        rt.shutdown();
434        assert!(
435            matches!(outcome, FlowOutcome::Completed(Value::Int(42))),
436            "got {outcome:?}"
437        );
438        assert!(sent >= 2);
439        Ok(())
440    }
441
442    #[test]
443    fn ask_timeout_writes_unit() -> Result<(), Box<dyn std::error::Error>> {
444        let chunk = ask_timeout_expires();
445        assert!(verify(&chunk).is_ok());
446        let rt = tiny_natives(chunk)?;
447        let idx = rt.function_index("main").ok_or("main")?;
448        let outcome = rt.spawn(idx, &[])?.join();
449        rt.shutdown();
450        assert!(
451            matches!(outcome, FlowOutcome::Completed(Value::Unit)),
452            "got {outcome:?}"
453        );
454        Ok(())
455    }
456
457    #[test]
458    fn ask_target_exit_writes_sys_exit() -> Result<(), Box<dyn std::error::Error>> {
459        let chunk = ask_target_exits();
460        assert!(verify(&chunk).is_ok());
461        let rt = tiny_natives(chunk)?;
462        let idx = rt.function_index("main").ok_or("main")?;
463        let outcome = rt.spawn(idx, &[])?.join();
464        rt.shutdown();
465        assert!(
466            matches!(
467                outcome,
468                FlowOutcome::Completed(Value::Int(n)) if n == i64::from(crate::TAG_SYS_EXIT)
469            ),
470            "got {outcome:?}"
471        );
472        Ok(())
473    }
474
475    #[test]
476    fn server_loop_joins_42() -> Result<(), Box<dyn std::error::Error>> {
477        let chunk = server_loop();
478        assert!(verify(&chunk).is_ok());
479        let rt = tiny_natives(chunk)?;
480        let idx = rt.function_index("main").ok_or("main")?;
481        let outcome = rt.spawn(idx, &[])?.join();
482        rt.shutdown();
483        assert!(
484            matches!(outcome, FlowOutcome::Completed(Value::Int(42))),
485            "got {outcome:?}"
486        );
487        Ok(())
488    }
489
490    #[test]
491    fn send_overwrites_forged_sender() -> Result<(), Box<dyn std::error::Error>> {
492        let chunk = forged_sender_send();
493        assert!(verify(&chunk).is_ok());
494        let rt = tiny_natives(chunk)?;
495        let idx = rt.function_index("main").ok_or("main")?;
496        let outcome = rt.spawn(idx, &[])?.join();
497        rt.shutdown();
498        match outcome {
499            FlowOutcome::Completed(Value::Pid(n)) => {
500                assert_ne!(n, 999, "forged make_msg sender must not survive Send");
501                assert!(n >= 1, "authenticated sender must be a live flow id");
502                Ok(())
503            }
504            other => Err(format!("expected Completed(Pid), got {other:?}").into()),
505        }
506    }
507
508    #[test]
509    fn ask_overwrites_forged_request_sender() -> Result<(), Box<dyn std::error::Error>> {
510        let chunk = forged_sender_ask();
511        assert!(verify(&chunk).is_ok());
512        let rt = tiny_natives(chunk)?;
513        let idx = rt.function_index("main").ok_or("main")?;
514        let outcome = rt.spawn(idx, &[])?.join();
515        rt.shutdown();
516        match outcome {
517            FlowOutcome::Completed(Value::Pid(n)) => {
518                assert_ne!(n, 999, "forged make_msg sender must not survive Ask");
519                assert!(n >= 1, "authenticated sender must be a live flow id");
520                Ok(())
521            }
522            other => Err(format!("expected Completed(Pid), got {other:?}").into()),
523        }
524    }
525
526    #[test]
527    fn send_scalar_target_traps() -> Result<(), Box<dyn std::error::Error>> {
528        let mut p = Program::new("bad-cap-target");
529        p.function("main", 0, |f| {
530            let bad_cap = f.load_i32(99);
531            let req_id = f.load_i32(1);
532            let payload = f.load_i32(1);
533            let msg = f.hop(req_id, TAG_PING, payload);
534            f.send(bad_cap, msg);
535            f.return_(msg);
536        });
537        let rt = tiny_natives(p.build())?;
538        let outcome = rt.spawn(0, &[])?.join();
539        rt.shutdown();
540        assert!(
541            matches!(outcome, FlowOutcome::Failed(_)),
542            "non-Cap Send target must fail, got {outcome:?}"
543        );
544        Ok(())
545    }
546
547    #[test]
548    fn send_scalar_is_not_an_atomic_hop() -> Result<(), Box<dyn std::error::Error>> {
549        let mut p = Program::new("bad-hop");
550        p.function("main", 0, |f| {
551            let cap = f.self_cap();
552            let scalar = f.load_i32(99);
553            f.send(cap, scalar);
554            f.return_(scalar);
555        });
556        let rt = tiny(p.build())?;
557        let outcome = rt.spawn(0, &[])?.join();
558        rt.shutdown();
559        assert!(
560            matches!(outcome, FlowOutcome::Failed(_)),
561            "scalar Send must trap, got {outcome:?}"
562        );
563        Ok(())
564    }
565
566    #[test]
567    fn monitor_down_joins_normal_reason() -> Result<(), Box<dyn std::error::Error>> {
568        let chunk = monitor_down();
569        assert!(verify(&chunk).is_ok());
570        let rt = tiny_natives(chunk)?;
571        let idx = rt.function_index("main").ok_or("main")?;
572        let outcome = rt.spawn(idx, &[])?.join();
573        rt.shutdown();
574        assert!(
575            matches!(outcome, FlowOutcome::Completed(Value::Int(0))),
576            "DOWN reason should be Normal (0), got {outcome:?}"
577        );
578        Ok(())
579    }
580
581    #[test]
582    fn waiting_send_second_hop_arrives() -> Result<(), Box<dyn std::error::Error>> {
583        let cap = crate::MailboxCapacity::new(1).ok_or("cap")?;
584        let rt = Runtime::with_natives_and_config(
585            waiting_send(),
586            std_native_table(),
587            RuntimeConfig {
588                workers: 1,
589                quantum: 10_000,
590                mailbox: crate::MailboxConfig::new(cap, crate::OverflowPolicy::Reject),
591                ..Default::default()
592            },
593        )?;
594        let server = rt.function_index("server").ok_or("server")?;
595        let client = rt.function_index("client").ok_or("client")?;
596        let server_h = rt.spawn(server, &[])?;
597        let server_cap = rt.mint_cap(server_h.id())?;
598        rt.spawn(client, &[Value::Cap(server_cap)])?;
599        let outcome = server_h.join();
600        rt.shutdown();
601        assert!(
602            matches!(outcome, FlowOutcome::Completed(Value::Int(42))),
603            "second hop should be admitted after the first pop, got {outcome:?}"
604        );
605        Ok(())
606    }
607
608    #[test]
609    fn link_kills_peer_on_fault() -> Result<(), Box<dyn std::error::Error>> {
610        let mut p = Program::new("link-kill");
611        p.function("park", 0, |f| {
612            let _ = f.receive();
613            f.trap(9);
614        });
615        p.function("boom", 0, |f| f.trap(1));
616        let rt = tiny(p.build())?;
617        let park = rt.function_index("park").ok_or("park")?;
618        let boom = rt.function_index("boom").ok_or("boom")?;
619        let parked = rt.spawn(park, &[])?;
620        let killer = rt.spawn(boom, &[])?;
621        rt.link(parked.id(), killer.id())?;
622        let boom_out = killer.join();
623        let park_out = parked.join();
624        rt.shutdown();
625        assert!(matches!(boom_out, FlowOutcome::Failed(_)), "{boom_out:?}");
626        assert!(
627            matches!(park_out, FlowOutcome::Failed(_)),
628            "linked peer must die on fault, got {park_out:?}"
629        );
630        Ok(())
631    }
632
633    #[test]
634    fn linked_exit_down_carries_link_reason() -> Result<(), Box<dyn std::error::Error>> {
635        let mut p = Program::new("link-down-reason");
636        p.function("watcher", 0, |f| {
637            let msg = f.receive_match_imm(crate::TAG_SYS_DOWN);
638            let out = f.hop_payload(msg);
639            f.return_(out);
640        });
641        p.function("park", 0, |f| {
642            let _ = f.receive();
643            f.trap(9);
644        });
645        p.function("boom", 0, |f| f.trap(1));
646        let rt = tiny_natives(p.build())?;
647        let watcher = rt.spawn(rt.function_index("watcher").ok_or("watcher")?, &[])?;
648        let parked = rt.spawn(rt.function_index("park").ok_or("park")?, &[])?;
649        let killer = rt.spawn(rt.function_index("boom").ok_or("boom")?, &[])?;
650        rt.monitor(watcher.id(), parked.id())?;
651        rt.link(parked.id(), killer.id())?;
652        let _ = killer.join();
653        let watched = watcher.join();
654        let _ = parked.join();
655        rt.shutdown();
656        assert!(
657            matches!(
658                watched,
659                FlowOutcome::Completed(Value::Int(n)) if n == crate::FlowExitReason::Link.as_u64() as i64
660            ),
661            "DOWN payload must be Link, got {watched:?}"
662        );
663        Ok(())
664    }
665
666    #[test]
667    fn monitor_dead_owner_is_rejected() -> Result<(), Box<dyn std::error::Error>> {
668        let rt = tiny(add_forty_two())?;
669        let first = rt.spawn(0, &[])?;
670        let dead = first.id();
671        let done = first.join();
672        assert!(matches!(done, FlowOutcome::Completed(_)));
673        let live = rt.spawn(0, &[])?;
674        let err = rt.monitor(dead, live.id());
675        live.join();
676        rt.shutdown();
677        assert!(
678            matches!(err, Err(crate::LifecycleError::NoSuchFlow(_))),
679            "{err:?}"
680        );
681        Ok(())
682    }
683
684    #[test]
685    fn forged_cap_cannot_be_registered() -> Result<(), Box<dyn std::error::Error>> {
686        let rt = tiny(add_forty_two())?;
687        let err = rt.register_name("svc", CapId::from_raw(99_999));
688        rt.shutdown();
689        assert_eq!(err, Err(crate::LifecycleError::InvalidCapability));
690        Ok(())
691    }
692
693    #[test]
694    fn registry_clears_on_exit() -> Result<(), Box<dyn std::error::Error>> {
695        let mut p = Program::new("reg");
696        p.function("main", 0, |f| {
697            let ms = f.load_i32(80);
698            f.sleep(ms);
699            let z = f.load_i32(1);
700            f.return_(z);
701        });
702        let rt = tiny(p.build())?;
703        let h = rt.spawn(0, &[])?;
704        let cap = rt.mint_cap(h.id())?;
705        rt.register_name("svc", cap)?;
706        assert_eq!(rt.whereis("svc")?, Some(cap));
707        let _ = h.join();
708        assert_eq!(rt.whereis("svc")?, None);
709        rt.shutdown();
710        Ok(())
711    }
712}