Skip to main content

byteflow/
samples.rs

1//! Built-in demo chunks assembled with [`crate::ChunkBuilder`].
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//! | [`forged_sender_send`] / [`forged_sender_ask`] | S1: forged `make_msg` sender dies |
15//! | [`boom`] | Immediate trap (supervisor demos) |
16//!
17//! Hop samples require [`crate::std_native_table`].
18
19use crate::{emit_native1_from, emit_native_n, Chunk, ChunkBuilder, Opcode};
20
21/// Native indices (must match [`crate::std_native_map`]).
22///
23/// Hard-coded here so the sample stays self-contained without looking up
24/// the map at assembly time ÔÇö the stability test in `natives` guards drift.
25const N_PRINT: u32 = 0;
26const N_MAKE_MSG: u32 = 2;
27const N_MSG_SENDER: u32 = 3;
28const N_MSG_REQUEST_ID: u32 = 4;
29const N_MSG_TAG: u32 = 5;
30const N_MSG_PAYLOAD: u32 = 6;
31const N_MSG_REPLY_CAP: u32 = 7;
32
33/// Protocol tags for Atomic Hop samples.
34///
35/// Opaque to the VM; only this sample (and its clients) interpret them.
36pub const TAG_REQ: i32 = 1;
37pub const TAG_REP: i32 = 2;
38pub const TAG_PING: i32 = 10;
39pub const TAG_PONG: i32 = 11;
40/// Decoy hop for [`selective_receive`] ÔÇö must be skipped by `ReceiveMatch`.
41pub const TAG_JUNK: i32 = 99;
42
43/// `r0 = 41 + 1; return r0` ÔÇö the 60-second sanity chunk.
44pub fn add_forty_two() -> Chunk {
45    let mut b = ChunkBuilder::new("add-forty-two");
46    b.begin_function("main", 0, 2);
47    b.emit_load_imm(0, 41);
48    b.emit_load_imm(1, 1);
49    b.emit_binop(Opcode::Add, 0, 0, 1);
50    b.emit_return(0);
51    b.finish()
52}
53
54/// Two flows, one **Atomic Hop** round-trip: `main` sends a `Message` to
55/// `pong`, `pong` replies with payload+1 via `msg_reply_cap`, `main` returns
56/// that payload (`2`).
57///
58/// Requires [`crate::std_native_table`] (`make_msg` / `msg_*`).
59///
60/// Every `Send` carries exactly one [`crate::Value::Message`] and targets a
61/// [`crate::Value::Cap`] ÔÇö bare ints / pids trap (Atomic Hop + FlowCap).
62pub fn ping_pong() -> Chunk {
63    let mut b = ChunkBuilder::new("ping-pong");
64
65    // pong: receive Message, reply payload+1 via reply_cap
66    // r0 = request Message
67    // r1 = reply Cap
68    // r2 = request_id
69    // r3 = payload (+1), then make_msg result
70    // r4..r7 = make_msg arg window
71    let pong = b.begin_function("pong", 0, 8);
72    b.emit_receive(0);
73    emit_native1_from!(b, 1, 0, N_MSG_REPLY_CAP);
74    emit_native1_from!(b, 2, 0, N_MSG_REQUEST_ID);
75    emit_native1_from!(b, 3, 0, N_MSG_PAYLOAD);
76    b.emit_load_imm(7, 1);
77    b.emit_binop(Opcode::Add, 3, 3, 7);
78    b.emit_self_pid(4);
79    b.emit_move(5, 2);
80    b.emit_load_imm(6, TAG_PONG);
81    b.emit_move(7, 3);
82    emit_native_n!(b, 4, N_MAKE_MSG, 4);
83    b.emit_send(1, 4);
84    b.emit_exit(4);
85
86    // main: spawn pong (Cap), hop Message{tag=PING, payload=1}, return reply payload
87    b.begin_function("main", 0, 8);
88    b.emit_self_pid(1);
89    b.emit_spawn(0, pong, 0);
90    b.emit_move(2, 1);
91    b.emit_load_imm(3, 1);
92    b.emit_load_imm(4, TAG_PING);
93    b.emit_load_imm(5, 1);
94    emit_native_n!(b, 2, N_MAKE_MSG, 4);
95    b.emit_send(0, 2);
96    b.emit_receive(6);
97    emit_native1_from!(b, 4, 6, N_MSG_PAYLOAD);
98    b.emit_return(4);
99
100    b.finish()
101}
102
103/// Atomic request-reply with [`crate::Value::Message`] (one envelope per hop).
104///
105/// Requires [`crate::std_native_table`] (`make_msg` / `msg_*` / `print`).
106///
107/// # Why "Atomic Hop"
108///
109/// Correlation (`request_id`) and reply routing (`sender`) travel in a
110/// **single** mailbox value. Classic actor runtimes often allow any scalar
111/// on `Send`; Byteflow rejects that ÔÇö every hop is a typed envelope.
112///
113/// # Protocol
114///
115/// 1. `main` spawns `server`, builds
116///    `Message { id=1, tag=REQ, payload=41 }` (sender placeholder ignored)
117/// 2. `server` receives, logs via `print`, replies
118///    `tag=REP, payload=42` via `msg_reply_cap` (echoing `request_id`)
119/// 3. `main` returns the reply payload `42`
120///
121/// # Register discipline (`CallNative` clobbers `r[a]`)
122///
123/// `CallNative ra, , nc` reads args from `r[a..a+nc]` and writes the
124/// result into `r[a]`. Keeping the original `Message` in `r0` therefore
125/// means every unpack is `Move ri, r0` then `CallNative ri, msg_*, 1`.
126/// `make_msg` needs four **contiguous** arg registers; the server rearranges
127/// into `r4..r7` before the call.
128pub fn atomic_request_reply() -> Chunk {
129    let mut b = ChunkBuilder::new("atomic-request-reply");
130
131    // --- server -----------------------------------------------------------
132    // r0  = request Message (never overwritten until Exit)
133    // r1  = reply Cap (from msg_reply_cap)
134    // r2  = request_id
135    // r3  = tag
136    // r4  = payload, then make_msg result (reply Message)
137    // r5  = self Cap, then make_msg arg slot
138    // r6  = TAG_REP for make_msg
139    // r7  = scratch (print copy, +1 imm, payload for make_msg)
140    let server = b.begin_function("server", 0, 8);
141    b.emit_receive(0);
142    emit_native1_from!(b, 7, 0, N_PRINT);
143    emit_native1_from!(b, 1, 0, N_MSG_REPLY_CAP);
144    emit_native1_from!(b, 2, 0, N_MSG_REQUEST_ID);
145    emit_native1_from!(b, 3, 0, N_MSG_TAG);
146    emit_native1_from!(b, 4, 0, N_MSG_PAYLOAD);
147    b.emit_load_imm(7, 1);
148    b.emit_binop(Opcode::Add, 4, 4, 7);
149    b.emit_self_pid(5);
150    b.emit_move(7, 4);
151    b.emit_move(4, 5);
152    b.emit_move(5, 2);
153    b.emit_load_imm(6, TAG_REP);
154    emit_native_n!(b, 4, N_MAKE_MSG, 4);
155    emit_native1_from!(b, 7, 4, N_PRINT);
156    b.emit_send(1, 4);
157    b.emit_exit(4);
158
159    // --- main -------------------------------------------------------------
160    // r0 = server Cap
161    // r1 = self Cap
162    // r2..r5 = make_msg(self, 1, TAG_REQ, 41)  r2 becomes the request Message
163    // r6 = reply Message
164    // r7 = scratch for print / unpack
165    b.begin_function("main", 0, 8);
166    b.emit_self_pid(1);
167    b.emit_spawn(0, server, 0);
168    b.emit_move(2, 1);
169    b.emit_load_imm(3, 1);
170    b.emit_load_imm(4, TAG_REQ);
171    b.emit_load_imm(5, 41);
172    emit_native_n!(b, 2, N_MAKE_MSG, 4);
173    emit_native1_from!(b, 7, 2, N_PRINT);
174    b.emit_send(0, 2);
175    b.emit_receive(6);
176    emit_native1_from!(b, 7, 6, N_PRINT);
177    emit_native1_from!(b, 4, 6, N_MSG_PAYLOAD);
178    b.emit_return(4);
179
180    b.finish()
181}
182
183/// Selective Atomic Hop: server waits for `TAG_REQ` while a `TAG_JUNK` hop
184/// sits ahead in the mailbox (FIFO skip, not drop).
185///
186/// Requires [`crate::std_native_table`].
187///
188/// 1. `main` sends junk (`tag=TAG_JUNK`), then request (`tag=TAG_REQ`, payload=41)
189/// 2. `server` does `ReceiveMatchImm TAG_REQ` ÔÇö must see payload 41, not junk
190/// 3. replies `TAG_REP` / 42; then classic `Receive` drains the leftover junk
191/// 4. `main` returns reply payload `42`
192pub fn selective_receive() -> Chunk {
193    let mut b = ChunkBuilder::new("selective-receive");
194
195    // server: match TAG_REQ, reply 42 via reply_cap, then drain junk
196    let server = b.begin_function("server", 0, 8);
197    b.emit_receive_match_imm(0, TAG_REQ as u16);
198    emit_native1_from!(b, 1, 0, N_MSG_REPLY_CAP);
199    emit_native1_from!(b, 2, 0, N_MSG_REQUEST_ID);
200    emit_native1_from!(b, 4, 0, N_MSG_PAYLOAD);
201    b.emit_load_imm(7, 1);
202    b.emit_binop(Opcode::Add, 4, 4, 7);
203    b.emit_self_pid(5);
204    b.emit_move(7, 4);
205    b.emit_move(4, 5);
206    b.emit_move(5, 2);
207    b.emit_load_imm(6, TAG_REP);
208    emit_native_n!(b, 4, N_MAKE_MSG, 4);
209    b.emit_send(1, 4);
210    // leftover TAG_JUNK must still be waiting
211    b.emit_receive(0);
212    emit_native1_from!(b, 3, 0, N_MSG_TAG);
213    b.emit_load_imm(7, TAG_JUNK);
214    b.emit_binop(Opcode::Eq, 3, 3, 7);
215    // Branch jumps when falsy: not-equal  trap
216    let trap_lbl = b.new_label();
217    b.emit_branch(3, trap_lbl);
218    b.emit_exit(4);
219    b.bind_label(trap_lbl);
220    b.emit_trap(2);
221
222    b.begin_function("main", 0, 8);
223    b.emit_self_pid(1);
224    b.emit_spawn(0, server, 0);
225    // junk hop first
226    b.emit_move(2, 1);
227    b.emit_load_imm(3, 1);
228    b.emit_load_imm(4, TAG_JUNK);
229    b.emit_load_imm(5, 0);
230    emit_native_n!(b, 2, N_MAKE_MSG, 4);
231    b.emit_send(0, 2);
232    // real request
233    b.emit_move(2, 1);
234    b.emit_load_imm(3, 1);
235    b.emit_load_imm(4, TAG_REQ);
236    b.emit_load_imm(5, 41);
237    emit_native_n!(b, 2, N_MAKE_MSG, 4);
238    b.emit_send(0, 2);
239    b.emit_receive_match_imm(6, TAG_REP as u16);
240    emit_native1_from!(b, 4, 6, N_MSG_PAYLOAD);
241    b.emit_return(4);
242
243    b.finish()
244}
245
246/// Atomic request/reply via [`Opcode::Ask`] (RPC hop).
247///
248/// Requires [`crate::std_native_table`].
249///
250/// 1. `main` builds `Message { id=1, tag=REQ, payload=41 }` and `Ask`s the server Cap
251/// 2. `server` `ReceiveMatchImm TAG_REQ`, replies via `msg_reply_cap` with
252///    `TAG_REP` / payload 42 and the same `request_id`
253/// 3. `Ask` resumes with the reply; `main` returns payload `42`
254pub fn ask_reply() -> Chunk {
255    let mut b = ChunkBuilder::new("ask-reply");
256
257    let server = b.begin_function("server", 0, 8);
258    b.emit_receive_match_imm(0, TAG_REQ as u16);
259    emit_native1_from!(b, 1, 0, N_MSG_REPLY_CAP);
260    emit_native1_from!(b, 2, 0, N_MSG_REQUEST_ID);
261    emit_native1_from!(b, 4, 0, N_MSG_PAYLOAD);
262    b.emit_load_imm(7, 1);
263    b.emit_binop(Opcode::Add, 4, 4, 7);
264    b.emit_self_pid(5);
265    b.emit_move(7, 4);
266    b.emit_move(4, 5);
267    b.emit_move(5, 2);
268    b.emit_load_imm(6, TAG_REP);
269    emit_native_n!(b, 4, N_MAKE_MSG, 4);
270    b.emit_send(1, 4);
271    b.emit_exit(4);
272
273    // main: Ask r6, r0 (server Cap), r2 (request Message)
274    b.begin_function("main", 0, 8);
275    b.emit_self_pid(1);
276    b.emit_spawn(0, server, 0);
277    b.emit_move(2, 1);
278    b.emit_load_imm(3, 1);
279    b.emit_load_imm(4, TAG_REQ);
280    b.emit_load_imm(5, 41);
281    emit_native_n!(b, 2, N_MAKE_MSG, 4);
282    b.emit_ask(6, 0, 2);
283    emit_native1_from!(b, 4, 6, N_MSG_PAYLOAD);
284    b.emit_return(4);
285
286    b.finish()
287}
288
289/// Security regression sample: forged `make_msg` sender must not survive `Send`.
290///
291/// # What this proves
292///
293/// Invariant **S1** from `docs/security.md`: structural Atomic Hop typing
294/// alone cannot stop a module from writing `sender = 999` into a
295/// [`crate::Message`]. The scheduler overwrites that field on bytecode
296/// `Send`, so the serverÔÇÖs `msg_sender` / echoed payload reflects the **real**
297/// client flow id.
298///
299/// # Protocol
300///
301/// 1. `main` builds a request with forged sender `999` and `Send`s it.
302/// 2. `server` reads the delivered hop, puts authenticated `msg_sender` into
303///    the reply `payload`, and answers.
304/// 3. `main` returns that payload as `Int`.
305///
306/// Unit tests assert the returned id is not `999` (and is a plausible live
307/// flow id). Requires [`crate::std_native_table`].
308pub fn forged_sender_send() -> Chunk {
309    let mut b = ChunkBuilder::new("forged-sender-send");
310
311    let server = b.begin_function("server", 0, 8);
312    b.emit_receive(0);
313    emit_native1_from!(b, 1, 0, N_MSG_REPLY_CAP);
314    emit_native1_from!(b, 2, 0, N_MSG_REQUEST_ID);
315    emit_native1_from!(b, 3, 0, N_MSG_SENDER);
316    // reply: payload = authenticated sender FlowId
317    b.emit_self_pid(4);
318    b.emit_move(5, 2);
319    b.emit_load_imm(6, TAG_REP);
320    b.emit_move(7, 3);
321    emit_native_n!(b, 4, N_MAKE_MSG, 4);
322    b.emit_send(1, 4);
323    b.emit_exit(4);
324
325    b.begin_function("main", 0, 8);
326    b.emit_self_pid(1);
327    b.emit_spawn(0, server, 0);
328    // Deliberately forge sender = 999
329    b.emit_load_imm(2, 999);
330    b.emit_load_imm(3, 1);
331    b.emit_load_imm(4, TAG_REQ);
332    b.emit_load_imm(5, 0);
333    emit_native_n!(b, 2, N_MAKE_MSG, 4);
334    b.emit_send(0, 2);
335    b.emit_receive(6);
336    emit_native1_from!(b, 4, 6, N_MSG_PAYLOAD);
337    b.emit_return(4);
338
339    b.finish()
340}
341
342/// Same security property as [`forged_sender_send`], via [`Opcode::Ask`].
343///
344/// Covers the request half of **S1** on the RPC path: a forged
345/// `make_msg.sender` is stamped away before the server observes the hop.
346/// Reply authenticity (**S2**, `sender == target`) is covered separately by
347/// mailbox unit tests (`ask_requires_reply_from_target`).
348pub fn forged_sender_ask() -> Chunk {
349    let mut b = ChunkBuilder::new("forged-sender-ask");
350
351    let server = b.begin_function("server", 0, 8);
352    b.emit_receive_match_imm(0, TAG_REQ as u16);
353    emit_native1_from!(b, 1, 0, N_MSG_REPLY_CAP);
354    emit_native1_from!(b, 2, 0, N_MSG_REQUEST_ID);
355    emit_native1_from!(b, 3, 0, N_MSG_SENDER);
356    b.emit_self_pid(4);
357    b.emit_move(5, 2);
358    b.emit_load_imm(6, TAG_REP);
359    b.emit_move(7, 3);
360    emit_native_n!(b, 4, N_MAKE_MSG, 4);
361    b.emit_send(1, 4);
362    b.emit_exit(4);
363
364    b.begin_function("main", 0, 8);
365    b.emit_self_pid(1);
366    b.emit_spawn(0, server, 0);
367    b.emit_load_imm(2, 999);
368    b.emit_load_imm(3, 1);
369    b.emit_load_imm(4, TAG_REQ);
370    b.emit_load_imm(5, 0);
371    emit_native_n!(b, 2, N_MAKE_MSG, 4);
372    b.emit_ask(6, 0, 2);
373    emit_native1_from!(b, 4, 6, N_MSG_PAYLOAD);
374    b.emit_return(4);
375
376    b.finish()
377}
378
379/// Immediate `Trap` ÔÇö used to show [`crate::Supervisor`] restart.
380pub fn boom() -> Chunk {
381    let mut b = ChunkBuilder::new("boom");
382    b.begin_function("boom", 0, 1);
383    b.emit_trap(1);
384    b.finish()
385}
386
387#[cfg(test)]
388mod tests {
389    use super::*;
390    use crate::{
391        decode, encode, std_native_table, verify, FlowOutcome, Runtime, RuntimeConfig, Value,
392    };
393
394    fn tiny(chunk: Chunk) -> Runtime {
395        Runtime::with_config(
396            chunk,
397            RuntimeConfig {
398                workers: 1,
399                quantum: 10_000,
400            },
401        )
402        .expect("runtime")
403    }
404
405    fn tiny_natives(chunk: Chunk) -> Runtime {
406        Runtime::with_natives_and_config(
407            chunk,
408            std_native_table(),
409            RuntimeConfig {
410                workers: 1,
411                quantum: 10_000,
412            },
413        )
414        .expect("runtime")
415    }
416
417    #[test]
418    fn add_forty_two_joins_42() {
419        let rt = tiny(add_forty_two());
420        let idx = rt.function_index("main").expect("main");
421        let outcome = rt.spawn(idx, &[]).expect("spawn").join();
422        rt.shutdown();
423        assert!(matches!(outcome, FlowOutcome::Completed(Value::Int(42))));
424    }
425
426    #[test]
427    fn ping_pong_joins_2() {
428        let chunk = ping_pong();
429        assert!(verify(&chunk).is_ok());
430        let bytes = encode(&chunk);
431        let chunk = decode(&bytes).expect("decode");
432        let rt = tiny_natives(chunk);
433        let idx = rt.function_index("main").expect("main");
434        let outcome = rt.spawn(idx, &[]).expect("spawn").join();
435        let sent = rt.metrics().messages_sent;
436        rt.shutdown();
437        assert!(matches!(outcome, FlowOutcome::Completed(Value::Int(2))));
438        assert!(sent >= 2);
439    }
440
441    #[test]
442    fn atomic_request_reply_joins_42() {
443        let chunk = atomic_request_reply();
444        assert!(verify(&chunk).is_ok());
445        let bytes = encode(&chunk);
446        let chunk = decode(&bytes).expect("decode");
447        let rt = tiny_natives(chunk);
448        let idx = rt.function_index("main").expect("main");
449        let outcome = rt.spawn(idx, &[]).expect("spawn").join();
450        let sent = rt.metrics().messages_sent;
451        rt.shutdown();
452        assert!(
453            matches!(outcome, FlowOutcome::Completed(Value::Int(42))),
454            "got {outcome:?}"
455        );
456        assert!(sent >= 1);
457    }
458
459    #[test]
460    fn selective_receive_skips_junk_tag() {
461        let chunk = selective_receive();
462        assert!(verify(&chunk).is_ok());
463        let bytes = encode(&chunk);
464        let chunk = decode(&bytes).expect("decode");
465        let rt = tiny_natives(chunk);
466        let idx = rt.function_index("main").expect("main");
467        let outcome = rt.spawn(idx, &[]).expect("spawn").join();
468        rt.shutdown();
469        assert!(
470            matches!(outcome, FlowOutcome::Completed(Value::Int(42))),
471            "got {outcome:?}"
472        );
473    }
474
475    #[test]
476    fn ask_reply_joins_42() {
477        let chunk = ask_reply();
478        assert!(verify(&chunk).is_ok());
479        let bytes = encode(&chunk);
480        let chunk = decode(&bytes).expect("decode");
481        let rt = tiny_natives(chunk);
482        let idx = rt.function_index("main").expect("main");
483        let outcome = rt.spawn(idx, &[]).expect("spawn").join();
484        let sent = rt.metrics().messages_sent;
485        rt.shutdown();
486        assert!(
487            matches!(outcome, FlowOutcome::Completed(Value::Int(42))),
488            "got {outcome:?}"
489        );
490        // request + reply
491        assert!(sent >= 2);
492    }
493
494    #[test]
495    fn send_overwrites_forged_sender() {
496        // S1: make_msg(sender=999, ) + Send  receiver must not see 999.
497        let chunk = forged_sender_send();
498        assert!(verify(&chunk).is_ok());
499        let rt = tiny_natives(chunk);
500        let idx = rt.function_index("main").expect("main");
501        let outcome = rt.spawn(idx, &[]).expect("spawn").join();
502        rt.shutdown();
503        match outcome {
504            FlowOutcome::Completed(Value::Int(n)) => {
505                assert_ne!(n, 999, "forged make_msg sender must not survive Send");
506                assert!(n >= 1, "authenticated sender must be a live flow id");
507            }
508            other => panic!("expected Completed(Int), got {other:?}"),
509        }
510    }
511
512    #[test]
513    fn ask_overwrites_forged_request_sender() {
514        // S1 on the Ask request path (same forge, RPC hop).
515        let chunk = forged_sender_ask();
516        assert!(verify(&chunk).is_ok());
517        let rt = tiny_natives(chunk);
518        let idx = rt.function_index("main").expect("main");
519        let outcome = rt.spawn(idx, &[]).expect("spawn").join();
520        rt.shutdown();
521        match outcome {
522            FlowOutcome::Completed(Value::Int(n)) => {
523                assert_ne!(n, 999, "forged make_msg sender must not survive Ask");
524                assert!(n >= 1, "authenticated sender must be a live flow id");
525            }
526            other => panic!("expected Completed(Int), got {other:?}"),
527        }
528    }
529
530    #[test]
531    fn send_scalar_target_traps() {
532        let mut b = ChunkBuilder::new("bad-cap-target");
533        b.begin_function("main", 0, 6);
534        b.emit_load_imm(0, 99); // Int ÔÇö not Cap
535        b.emit_load_imm(1, 0);
536        b.emit_load_imm(2, 1);
537        b.emit_load_imm(3, TAG_PING);
538        b.emit_load_imm(4, 1);
539        emit_native_n!(b, 1, N_MAKE_MSG, 4);
540        b.emit_send(0, 1);
541        b.emit_return(1);
542        let rt = tiny_natives(b.finish());
543        let outcome = rt.spawn(0, &[]).expect("spawn").join();
544        rt.shutdown();
545        assert!(
546            matches!(outcome, FlowOutcome::Failed(_)),
547            "non-Cap Send target must fail, got {outcome:?}"
548        );
549    }
550
551    #[test]
552    fn send_scalar_is_not_an_atomic_hop() {
553        let mut b = ChunkBuilder::new("bad-hop");
554        b.begin_function("main", 0, 2);
555        b.emit_self_pid(0);
556        b.emit_load_imm(1, 99);
557        b.emit_send(0, 1);
558        b.emit_return(1);
559        let rt = tiny(b.finish());
560        let outcome = rt.spawn(0, &[]).expect("spawn").join();
561        rt.shutdown();
562        assert!(
563            matches!(outcome, FlowOutcome::Failed(_)),
564            "scalar Send must trap, got {outcome:?}"
565        );
566    }
567}