Skip to main content

byteflow/
samples.rs

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