Skip to main content

byteflow/bytecode/
program.rs

1//! Primary API for assembling [`Chunk`] programs in host Rust.
2//!
3//! Use [`Program`] to define a module and [`Fn`] to emit each function with
4//! named registers instead of manual `r0` / `emit_*` bookkeeping.
5//!
6//! ```rust
7//! use byteflow::Program;
8//!
9//! let mut program = Program::new("count");
10//! program.function("main", 0, |f| {
11//!     let limit = f.load_int(100);
12//!     let counter = f.load_i32(0);
13//!     f.while_lt(counter, limit, |f| f.add_imm(counter, 1));
14//!     f.return_(counter);
15//! });
16//! let chunk = program.build();
17//! ```
18
19use super::builder::ChunkBuilder;
20
21pub use super::builder::Label;
22use super::chunk::Chunk;
23use super::opcode::Opcode;
24use super::value::Value;
25
26/// Function index returned by [`Program::function`].
27pub type FuncId = u32;
28
29/// A virtual register in the current function.
30#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
31pub struct Reg(u8);
32
33impl Reg {
34    /// Underlying register index in the bytecode function frame.
35    pub const fn index(self) -> u8 {
36        self.0
37    }
38}
39
40impl From<Reg> for u8 {
41    fn from(r: Reg) -> u8 {
42        r.0
43    }
44}
45
46/// Contiguous register window (e.g. four slots for `make_msg` natives).
47#[derive(Clone, Copy, Debug)]
48pub struct RegWindow {
49    base: Reg,
50    len: u8,
51}
52
53impl RegWindow {
54    /// First register in the window (native result lands here for `make_msg`).
55    pub fn base(self) -> Reg {
56        self.base
57    }
58
59    /// Register at offset `i` within the window (`0 <= i < len`).
60    pub fn at(self, i: u8) -> Reg {
61        debug_assert!(i < self.len, "RegWindow index out of bounds");
62        Reg(self.base.0.saturating_add(i))
63    }
64}
65
66/// Assemble one bytecode module.
67pub struct Program {
68    inner: ChunkBuilder,
69}
70
71impl Program {
72    /// Start a new module named `name`.
73    pub fn new(name: impl Into<String>) -> Self {
74        Self {
75            inner: ChunkBuilder::new(name),
76        }
77    }
78
79    /// Define a function and run `body` against a fresh [`Fn`] context.
80    ///
81    /// Returns the function index for [`Fn::spawn`] / [`Fn::call`].
82    pub fn function(
83        &mut self,
84        name: impl Into<String>,
85        arity: u8,
86        body: impl FnOnce(&mut Fn<'_>),
87    ) -> FuncId {
88        let mut f = Fn::open(&mut self.inner, name, arity);
89        let id = f.function;
90        body(&mut f);
91        id
92    }
93
94    /// Define a function with an explicit register-file size.
95    ///
96    /// Useful for fixed register layouts (`reg(255)`) or verification demos
97    /// where `num_registers < arity` must fail static checks.
98    pub fn function_raw(
99        &mut self,
100        name: impl Into<String>,
101        arity: u8,
102        num_registers: u8,
103        body: impl FnOnce(&mut Fn<'_>),
104    ) -> FuncId {
105        let mut f = Fn::open_with(&mut self.inner, name, arity, num_registers);
106        let id = f.function;
107        body(&mut f);
108        id
109    }
110
111    /// Look up a function index by name (available before [`Self::build`]).
112    pub fn function_index(&self, name: &str) -> Option<FuncId> {
113        self.inner.function_index(name)
114    }
115
116    /// Finish assembly and produce an immutable [`Chunk`].
117    pub fn build(self) -> Chunk {
118        self.inner.finish()
119    }
120}
121
122/// Emit instructions for one bytecode function.
123pub struct Fn<'a> {
124    b: &'a mut ChunkBuilder,
125    function: FuncId,
126    next_reg: u8,
127    scratch: Option<Reg>,
128}
129
130impl<'a> Fn<'a> {
131    fn open(b: &'a mut ChunkBuilder, name: impl Into<String>, arity: u8) -> Self {
132        Self::open_with(b, name, arity, arity.max(4))
133    }
134
135    fn open_with(
136        b: &'a mut ChunkBuilder,
137        name: impl Into<String>,
138        arity: u8,
139        num_registers: u8,
140    ) -> Self {
141        let function = b.begin_function(name, arity, num_registers);
142        Self {
143            b,
144            function,
145            next_reg: num_registers,
146            scratch: None,
147        }
148    }
149
150    /// Allocate a fresh local register.
151    pub fn local(&mut self) -> Reg {
152        let reg = Reg(self.next_reg);
153        self.next_reg = self.next_reg.saturating_add(1);
154        self.b.set_num_registers(self.function, self.next_reg);
155        reg
156    }
157
158    /// Bind to an explicit register index without growing the declared frame.
159    pub fn reg(&mut self, index: u8) -> Reg {
160        Reg(index)
161    }
162
163    /// Ensure the register file is at least `count` slots wide.
164    pub fn reserve(&mut self, count: u8) {
165        if count > self.next_reg {
166            self.next_reg = count;
167            self.b.set_num_registers(self.function, self.next_reg);
168        }
169    }
170
171    /// Allocate `count` contiguous locals; useful before [`Self::native_n`].
172    pub fn window(&mut self, count: u8) -> RegWindow {
173        let base_idx = self.next_reg;
174        for _ in 0..count {
175            let _ = self.local();
176        }
177        RegWindow {
178            base: Reg(base_idx),
179            len: count,
180        }
181    }
182
183    /// Load a small immediate into a new local.
184    pub fn load_i32(&mut self, n: i32) -> Reg {
185        let reg = self.local();
186        self.b.emit_load_imm(reg.0, n);
187        reg
188    }
189
190    /// Load a constant-pool integer into a new local.
191    pub fn load_int(&mut self, n: i64) -> Reg {
192        let konst = self.b.const_(Value::Int(n));
193        let reg = self.local();
194        self.b.emit_load_const(reg.0, konst);
195        reg
196    }
197
198    /// Store an immediate into an existing register.
199    pub fn set(&mut self, reg: Reg, imm: i32) {
200        self.b.emit_load_imm(reg.0, imm);
201    }
202
203    /// `dst = src`
204    pub fn mov(&mut self, dst: Reg, src: Reg) {
205        self.b.emit_move(dst.0, src.0);
206    }
207
208    fn binop(&mut self, op: Opcode, lhs: Reg, rhs: Reg) -> Reg {
209        let dst = self.local();
210        self.b.emit_binop(op, dst.0, lhs.0, rhs.0);
211        dst
212    }
213
214    fn binop_imm(&mut self, op: Opcode, lhs: Reg, imm: i32) -> Reg {
215        let rhs = self.temp();
216        self.b.emit_load_imm(rhs.0, imm);
217        self.binop(op, lhs, rhs)
218    }
219
220    /// `dst = lhs + rhs` into a new local.
221    pub fn add(&mut self, lhs: Reg, rhs: Reg) -> Reg {
222        self.binop(Opcode::Add, lhs, rhs)
223    }
224
225    /// `dst += imm` in place.
226    pub fn add_imm(&mut self, dst: Reg, imm: i32) {
227        let tmp = self.temp();
228        self.b.emit_load_imm(tmp.0, imm);
229        self.b.emit_binop(Opcode::Add, dst.0, dst.0, tmp.0);
230    }
231
232    pub fn sub(&mut self, lhs: Reg, rhs: Reg) -> Reg {
233        self.binop(Opcode::Sub, lhs, rhs)
234    }
235
236    pub fn mul(&mut self, lhs: Reg, rhs: Reg) -> Reg {
237        self.binop(Opcode::Mul, lhs, rhs)
238    }
239
240    pub fn div(&mut self, lhs: Reg, rhs: Reg) -> Reg {
241        self.binop(Opcode::Div, lhs, rhs)
242    }
243
244    pub fn modulo(&mut self, lhs: Reg, rhs: Reg) -> Reg {
245        self.binop(Opcode::Mod, lhs, rhs)
246    }
247
248    pub fn neg(&mut self, src: Reg) -> Reg {
249        let dst = self.local();
250        self.b.emit_neg(dst.0, src.0);
251        dst
252    }
253
254    pub fn eq(&mut self, lhs: Reg, rhs: Reg) -> Reg {
255        self.binop(Opcode::Eq, lhs, rhs)
256    }
257
258    pub fn eq_imm(&mut self, lhs: Reg, imm: i32) -> Reg {
259        self.binop_imm(Opcode::Eq, lhs, imm)
260    }
261
262    pub fn lt(&mut self, lhs: Reg, rhs: Reg) -> Reg {
263        self.binop(Opcode::Lt, lhs, rhs)
264    }
265
266    pub fn le(&mut self, lhs: Reg, rhs: Reg) -> Reg {
267        self.binop(Opcode::Le, lhs, rhs)
268    }
269
270    /// While `counter < limit` (interpreter `Branch` skips on falsy).
271    pub fn while_lt<F>(&mut self, counter: Reg, limit: Reg, body: F)
272    where
273        F: FnOnce(&mut Fn<'_>),
274    {
275        let head = self.b.new_label();
276        let done = self.b.new_label();
277        let cond = self.local();
278        self.b.bind_label(head);
279        self.b.emit_binop(Opcode::Lt, cond.0, counter.0, limit.0);
280        self.b.emit_branch(cond.0, done);
281        body(self);
282        self.b.emit_jump(head);
283        self.b.bind_label(done);
284    }
285
286    pub fn label(&mut self) -> Label {
287        self.b.new_label()
288    }
289
290    pub fn bind(&mut self, label: Label) {
291        self.b.bind_label(label);
292    }
293
294    pub fn jump(&mut self, label: Label) {
295        self.b.emit_jump(label);
296    }
297
298    /// Branch when `cond` is falsy (`0`).
299    pub fn branch_if_falsy(&mut self, cond: Reg, target: Label) {
300        self.b.emit_branch(cond.0, target);
301    }
302
303    pub fn return_(&mut self, value: Reg) {
304        self.b.emit_return(value.0);
305    }
306
307    pub fn call(&mut self, function: FuncId, argc: u8) -> Reg {
308        let dst = self.local();
309        self.b.emit_call(dst.0, function, argc);
310        dst
311    }
312
313    pub fn halt(&mut self) {
314        self.b.emit_halt();
315    }
316
317    pub fn yield_(&mut self) {
318        self.b.emit_yield();
319    }
320
321    pub fn sleep(&mut self, millis: Reg) {
322        self.b.emit_sleep(millis.0);
323    }
324
325    pub fn exit(&mut self, reg: Reg) {
326        self.b.emit_exit(reg.0);
327    }
328
329    /// Self Cap (`SEND` / `ASK` target for this flow).
330    ///
331    /// Despite the opcode name (`SelfPid`), the value is a [`crate::Value::Cap`],
332    /// not a Pid. In BEAM terms this is closer to a **reply address** grant
333    /// than to `self()` — use [`Self::hop_sender`] on a *delivered* hop for
334    /// authenticated origin identity.
335    pub fn self_cap(&mut self) -> Reg {
336        let cap = self.local();
337        self.b.emit_self_pid(cap.0);
338        cap
339    }
340
341    /// Alias for [`Self::self_cap`] — preferred name when coming from BEAM.
342    pub fn self_address(&mut self) -> Reg {
343        self.self_cap()
344    }
345
346    pub fn spawn(&mut self, function: FuncId, argc: u8) -> Reg {
347        self.spawn_with_rights(function, argc, crate::bytecode::CapRights::FLOW)
348    }
349
350    /// Spawn a child that receives only `rights` ⊆ parent authority.
351    pub fn spawn_with_rights(&mut self, function: FuncId, argc: u8, rights: crate::bytecode::CapRights) -> Reg {
352        let cap = self.local();
353        self.b.emit_spawn_with_rights(cap.0, function, argc, rights);
354        cap
355    }
356
357    /// Spawn with `rights = NONE` (confined by default).
358    pub fn spawn_confined(&mut self, function: FuncId, argc: u8) -> Reg {
359        self.spawn_with_rights(function, argc, crate::bytecode::CapRights::NONE)
360    }
361
362    /// Spawn into an explicit destination register (e.g. `reg(255)`).
363    pub fn spawn_at(&mut self, dst: Reg, function: FuncId, argc: u8) {
364        self.b.emit_spawn(dst.0, function, argc);
365    }
366
367    /// Attenuate `src` into a new Cap (rights mask is an immediate).
368    pub fn delegate(&mut self, src: Reg, rights: crate::bytecode::CapRights) -> Reg {
369        let dst = self.local();
370        self.b.emit_delegate(dst.0, src.0, rights);
371        dst
372    }
373
374    pub fn send(&mut self, target_cap: Reg, msg: Reg) {
375        self.b.emit_send(target_cap.0, msg.0);
376    }
377
378    pub fn receive(&mut self) -> Reg {
379        let msg = self.local();
380        self.b.emit_receive(msg.0);
381        msg
382    }
383
384    pub fn receive_timeout(&mut self, millis: Reg) -> Reg {
385        let msg = self.local();
386        self.b.emit_receive_timeout(msg.0, millis.0);
387        msg
388    }
389
390    pub fn receive_match(&mut self, tag: Reg) -> Reg {
391        let msg = self.local();
392        self.b.emit_receive_match(msg.0, tag.0);
393        msg
394    }
395
396    pub fn receive_match_imm(&mut self, tag: u16) -> Reg {
397        let msg = self.local();
398        self.b.emit_receive_match_imm(msg.0, tag);
399        msg
400    }
401
402    /// Next per-flow correlation id (`Int`, starts at 1).
403    pub fn fresh_request_id(&mut self) -> Reg {
404        let dst = self.local();
405        self.b.emit_fresh_request_id(dst.0);
406        dst
407    }
408
409    /// Wait for `tag == tag_reg` and `request_id == id_reg` (FIFO skip).
410    pub fn receive_match_corr(&mut self, tag: Reg, request_id: Reg) -> Reg {
411        let msg = self.local();
412        self.b.emit_receive_match_corr(msg.0, tag.0, request_id.0);
413        msg
414    }
415
416    /// Wait for immediate `tag` and `request_id == id_reg` (FIFO skip).
417    pub fn receive_match_corr_imm(&mut self, tag: u16, request_id: Reg) -> Reg {
418        let msg = self.local();
419        self.b.emit_receive_match_corr_imm(msg.0, tag, request_id.0);
420        msg
421    }
422
423    /// Build a hop with a freshly minted `request_id`.
424    pub fn hop_fresh(&mut self, tag: i32, payload: Reg) -> Reg {
425        let req_id = self.fresh_request_id();
426        self.hop(req_id, tag, payload)
427    }
428
429    /// RPC hop: deliver `msg` to `target_cap`, wait for correlated reply.
430    ///
431    /// If the target exits first, dest is a [`crate::TAG_SYS_EXIT`] hop
432    /// (not a hang).
433    pub fn ask(&mut self, target_cap: Reg, msg: Reg) -> Reg {
434        let reply = self.local();
435        self.b.emit_ask(reply.0, target_cap.0, msg.0);
436        reply
437    }
438
439    /// Like [`Self::ask`], but writes `Unit` into the dest if `millis` elapses.
440    pub fn ask_timeout(&mut self, target_cap: Reg, msg: Reg, millis: Reg) -> Reg {
441        let reply = self.local();
442        self.b
443            .emit_ask_timeout(reply.0, target_cap.0, msg.0, millis.0);
444        reply
445    }
446
447    /// One-way watch: when the flow addressed by `target_cap` exits, this
448    /// flow receives a [`crate::TAG_SYS_DOWN`] hop. Returns a monitor ref (`Int`).
449    pub fn monitor(&mut self, target_cap: Reg) -> Reg {
450        let dst = self.local();
451        self.b.emit_monitor(dst.0, target_cap.0);
452        dst
453    }
454
455    pub fn demonitor(&mut self, monitor: Reg) {
456        self.b.emit_demonitor(monitor.0);
457    }
458
459    /// Bidirectional link: abnormal exit of either side kills the peer.
460    pub fn link(&mut self, target_cap: Reg) -> Reg {
461        let dst = self.local();
462        self.b.emit_link(dst.0, target_cap.0);
463        dst
464    }
465
466    pub fn unlink(&mut self, link: Reg) {
467        self.b.emit_unlink(link.0);
468    }
469
470    /// Load a UTF-8 constant into a new local.
471    pub fn load_str(&mut self, s: impl AsRef<str>) -> Reg {
472        let konst = self.b.const_(Value::str(s));
473        let reg = self.local();
474        self.b.emit_load_const(reg.0, konst);
475        reg
476    }
477
478    /// Publish `name` (`Str`) as this flow. Requires `SEND` on self-authority.
479    pub fn register_name(&mut self, name: Reg) {
480        self.b.emit_register_name(name.0);
481    }
482
483    /// Look up `name` (`Str`): SEND Cap for the caller, or `Unit`.
484    pub fn whereis(&mut self, name: Reg) -> Reg {
485        let dst = self.local();
486        self.b.emit_whereis(dst.0, name.0);
487        dst
488    }
489
490    pub fn trap(&mut self, code: i32) {
491        self.b.emit_trap(code);
492    }
493
494    /// Copy `src`, call a one-arg native, return the result in a new local.
495    ///
496    /// The source register is preserved (`CallNative` clobbers its argument slot).
497    pub fn native1_from(&mut self, src: Reg, native: u32) -> Reg {
498        let dst = self.local();
499        self.b.emit_native1_from(dst.0, src.0, native);
500        dst
501    }
502
503    /// Side-effect native on a copy of `src` (e.g. `print`).
504    pub fn native1_on(&mut self, src: Reg, native: u32) {
505        let tmp = self.local();
506        self.b.emit_native1_from(tmp.0, src.0, native);
507    }
508
509    /// `CallNative` with `argc` args already at `base..base+argc`.
510    pub fn native_n(&mut self, base: Reg, native: u32, argc: u8) {
511        self.b.emit_native_n(base.0, native, argc);
512    }
513
514    /// Call a native with `argc` args already in `base..base+argc`.
515    pub fn call_native(&mut self, base: Reg, native: u32, argc: u8) {
516        self.native_n(base, native, argc);
517    }
518
519    /// Call a zero-arg native; returns the result register.
520    pub fn call_native0(&mut self, native: u32) -> Reg {
521        let dst = self.local();
522        self.b.emit_call_native(dst.0, native, 0);
523        dst
524    }
525
526    /// Build an outgoing Atomic Hop (`request_id`, `tag`, `payload`).
527    ///
528    /// The scheduler overwrites `sender` and attaches `reply_cap` on [`Self::send`]
529    /// / [`Self::ask`]. Prefer [`Self::hop_fresh`] so `request_id` is unique.
530    /// Do not forge a sender (see [`crate::docs::security`]).
531    pub fn hop(&mut self, request_id: Reg, tag: i32, payload: Reg) -> Reg {
532        self.make_msg(
533            crate::natives::std_native::MAKE_MSG,
534            request_id,
535            tag,
536            payload,
537        )
538    }
539
540    /// Extract authenticated origin from a delivered hop (`Value::Pid`).
541    pub fn hop_sender(&mut self, msg: Reg) -> Reg {
542        self.native1_from(msg, crate::natives::std_native::MSG_SENDER)
543    }
544
545    pub fn hop_request_id(&mut self, msg: Reg) -> Reg {
546        self.native1_from(msg, crate::natives::std_native::MSG_REQUEST_ID)
547    }
548
549    pub fn hop_tag(&mut self, msg: Reg) -> Reg {
550        self.native1_from(msg, crate::natives::std_native::MSG_TAG)
551    }
552
553    pub fn hop_payload(&mut self, msg: Reg) -> Reg {
554        self.native1_from(msg, crate::natives::std_native::MSG_PAYLOAD)
555    }
556
557    /// Reply address grant minted for the original sender (`Value::Cap`).
558    pub fn hop_reply_cap(&mut self, msg: Reg) -> Reg {
559        self.native1_from(msg, crate::natives::std_native::MSG_REPLY_CAP)
560    }
561
562    /// Build a reply hop echoing `request_id` from `req`.
563    pub fn reply_to(&mut self, req: Reg, tag: i32, payload: Reg) -> Reg {
564        let req_id = self.hop_request_id(req);
565        self.hop(req_id, tag, payload)
566    }
567
568    /// Reply to `req` via its `reply_cap` (typical server pattern).
569    pub fn send_reply(&mut self, req: Reg, tag: i32, payload: Reg) {
570        let reply_cap = self.hop_reply_cap(req);
571        let reply = self.reply_to(req, tag, payload);
572        self.send(reply_cap, reply);
573    }
574
575    /// Build a [`Value::Message`] via `make_msg` at `native_index`.
576    ///
577    /// There is no sender operand — the scheduler stamps identity on `Send`.
578    /// A 4-arg legacy encoding is still accepted by the native (first arg
579    /// discarded) so forged-sender regressions keep compiling.
580    pub fn make_msg(
581        &mut self,
582        native_index: u32,
583        request_id: Reg,
584        tag: i32,
585        payload: Reg,
586    ) -> Reg {
587        let w = self.window(3);
588        self.mov(w.at(0), request_id);
589        self.set(w.at(1), tag);
590        self.mov(w.at(2), payload);
591        self.native_n(w.base(), native_index, 3);
592        w.base()
593    }
594
595    /// Legacy 4-arg `make_msg` (sender slot is ignored by the native).
596    pub fn make_msg_legacy_sender(
597        &mut self,
598        native_index: u32,
599        sender: Reg,
600        request_id: Reg,
601        tag: i32,
602        payload: Reg,
603    ) -> Reg {
604        let w = self.window(4);
605        self.mov(w.at(0), sender);
606        self.mov(w.at(1), request_id);
607        self.set(w.at(2), tag);
608        self.mov(w.at(3), payload);
609        self.native_n(w.base(), native_index, 4);
610        w.base()
611    }
612
613    fn temp(&mut self) -> Reg {
614        if let Some(scratch) = self.scratch {
615            return scratch;
616        }
617        let scratch = self.local();
618        self.scratch = Some(scratch);
619        scratch
620    }
621}
622
623#[cfg(test)]
624mod tests {
625    use super::*;
626    use crate::{NativeTable, Value, Vm, VmResult};
627
628    type TestResult = Result<(), Box<dyn std::error::Error>>;
629
630    fn count_to(n: i64) -> Chunk {
631        let mut program = Program::new("count");
632        program.function("main", 0, |f| {
633            let limit = f.load_int(n);
634            let counter = f.load_i32(0);
635            f.while_lt(counter, limit, |f| f.add_imm(counter, 1));
636            f.return_(counter);
637        });
638        program.build()
639    }
640
641    #[test]
642    fn function_raw_keeps_declared_register_file_for_verify() -> TestResult {
643        let mut program = Program::new("malformed");
644        program.function_raw("main", 3, 1, |f| {
645            let r0 = f.reg(0);
646            f.return_(r0);
647        });
648        let chunk = program.build();
649        assert_eq!(chunk.functions[0].arity, 3);
650        assert_eq!(chunk.functions[0].num_registers, 1);
651        assert!(matches!(
652            crate::verify(&chunk),
653            Err(crate::VerifyError::ArityExceedsRegisters {
654                function: 0,
655                arity: 3,
656                num_registers: 1,
657            })
658        ));
659        Ok(())
660    }
661
662    #[test]
663    fn count_loop_returns_n() -> TestResult {
664        let chunk = count_to(100);
665        let mut vm = Vm::new(std::sync::Arc::new(chunk), NativeTable::empty(), 0, &[])?;
666        assert!(matches!(
667            vm.run(10_000),
668            VmResult::Complete(Value::Int(100))
669        ));
670        Ok(())
671    }
672
673    #[test]
674    fn ping_pong_shape() -> TestResult {
675        let chunk = crate::samples::ping_pong();
676        crate::verify(&chunk)?;
677        assert!(chunk.functions.iter().any(|f| f.name == "main"));
678        assert!(chunk.functions.iter().any(|f| f.name == "pong"));
679        Ok(())
680    }
681}