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    /// RPC hop: deliver `msg` to `target_cap`, wait for correlated reply.
403    ///
404    /// If the target exits first, dest is a [`crate::TAG_SYS_EXIT`] hop
405    /// (not a hang).
406    pub fn ask(&mut self, target_cap: Reg, msg: Reg) -> Reg {
407        let reply = self.local();
408        self.b.emit_ask(reply.0, target_cap.0, msg.0);
409        reply
410    }
411
412    /// Like [`Self::ask`], but writes `Unit` into the dest if `millis` elapses.
413    pub fn ask_timeout(&mut self, target_cap: Reg, msg: Reg, millis: Reg) -> Reg {
414        let reply = self.local();
415        self.b
416            .emit_ask_timeout(reply.0, target_cap.0, msg.0, millis.0);
417        reply
418    }
419
420    /// One-way watch: when the flow addressed by `target_cap` exits, this
421    /// flow receives a [`crate::TAG_SYS_DOWN`] hop. Returns a monitor ref (`Int`).
422    pub fn monitor(&mut self, target_cap: Reg) -> Reg {
423        let dst = self.local();
424        self.b.emit_monitor(dst.0, target_cap.0);
425        dst
426    }
427
428    pub fn demonitor(&mut self, monitor: Reg) {
429        self.b.emit_demonitor(monitor.0);
430    }
431
432    /// Bidirectional link: abnormal exit of either side kills the peer.
433    pub fn link(&mut self, target_cap: Reg) -> Reg {
434        let dst = self.local();
435        self.b.emit_link(dst.0, target_cap.0);
436        dst
437    }
438
439    pub fn unlink(&mut self, link: Reg) {
440        self.b.emit_unlink(link.0);
441    }
442
443    pub fn trap(&mut self, code: i32) {
444        self.b.emit_trap(code);
445    }
446
447    /// Copy `src`, call a one-arg native, return the result in a new local.
448    ///
449    /// The source register is preserved (`CallNative` clobbers its argument slot).
450    pub fn native1_from(&mut self, src: Reg, native: u32) -> Reg {
451        let dst = self.local();
452        self.b.emit_native1_from(dst.0, src.0, native);
453        dst
454    }
455
456    /// Side-effect native on a copy of `src` (e.g. `print`).
457    pub fn native1_on(&mut self, src: Reg, native: u32) {
458        let tmp = self.local();
459        self.b.emit_native1_from(tmp.0, src.0, native);
460    }
461
462    /// `CallNative` with `argc` args already at `base..base+argc`.
463    pub fn native_n(&mut self, base: Reg, native: u32, argc: u8) {
464        self.b.emit_native_n(base.0, native, argc);
465    }
466
467    /// Call a native with `argc` args already in `base..base+argc`.
468    pub fn call_native(&mut self, base: Reg, native: u32, argc: u8) {
469        self.native_n(base, native, argc);
470    }
471
472    /// Call a zero-arg native; returns the result register.
473    pub fn call_native0(&mut self, native: u32) -> Reg {
474        let dst = self.local();
475        self.b.emit_call_native(dst.0, native, 0);
476        dst
477    }
478
479    /// Build an outgoing Atomic Hop (`request_id`, `tag`, `payload`).
480    ///
481    /// The scheduler overwrites `sender` and mints `reply_cap` on [`Self::send`]
482    /// / [`Self::ask`] — do not forge a sender (see [`crate::docs::security`]).
483    pub fn hop(&mut self, request_id: Reg, tag: i32, payload: Reg) -> Reg {
484        self.make_msg(
485            crate::natives::std_native::MAKE_MSG,
486            request_id,
487            tag,
488            payload,
489        )
490    }
491
492    /// Extract authenticated origin from a delivered hop (`Value::Pid`).
493    pub fn hop_sender(&mut self, msg: Reg) -> Reg {
494        self.native1_from(msg, crate::natives::std_native::MSG_SENDER)
495    }
496
497    pub fn hop_request_id(&mut self, msg: Reg) -> Reg {
498        self.native1_from(msg, crate::natives::std_native::MSG_REQUEST_ID)
499    }
500
501    pub fn hop_tag(&mut self, msg: Reg) -> Reg {
502        self.native1_from(msg, crate::natives::std_native::MSG_TAG)
503    }
504
505    pub fn hop_payload(&mut self, msg: Reg) -> Reg {
506        self.native1_from(msg, crate::natives::std_native::MSG_PAYLOAD)
507    }
508
509    /// Reply address grant minted for the original sender (`Value::Cap`).
510    pub fn hop_reply_cap(&mut self, msg: Reg) -> Reg {
511        self.native1_from(msg, crate::natives::std_native::MSG_REPLY_CAP)
512    }
513
514    /// Build a reply hop echoing `request_id` from `req`.
515    pub fn reply_to(&mut self, req: Reg, tag: i32, payload: Reg) -> Reg {
516        let req_id = self.hop_request_id(req);
517        self.hop(req_id, tag, payload)
518    }
519
520    /// Reply to `req` via its `reply_cap` (typical server pattern).
521    pub fn send_reply(&mut self, req: Reg, tag: i32, payload: Reg) {
522        let reply_cap = self.hop_reply_cap(req);
523        let reply = self.reply_to(req, tag, payload);
524        self.send(reply_cap, reply);
525    }
526
527    /// Build a [`Value::Message`] via `make_msg` at `native_index`.
528    ///
529    /// There is no sender operand — the scheduler stamps identity on `Send`.
530    /// A 4-arg legacy encoding is still accepted by the native (first arg
531    /// discarded) so forged-sender regressions keep compiling.
532    pub fn make_msg(
533        &mut self,
534        native_index: u32,
535        request_id: Reg,
536        tag: i32,
537        payload: Reg,
538    ) -> Reg {
539        let w = self.window(3);
540        self.mov(w.at(0), request_id);
541        self.set(w.at(1), tag);
542        self.mov(w.at(2), payload);
543        self.native_n(w.base(), native_index, 3);
544        w.base()
545    }
546
547    /// Legacy 4-arg `make_msg` (sender slot is ignored by the native).
548    pub fn make_msg_legacy_sender(
549        &mut self,
550        native_index: u32,
551        sender: Reg,
552        request_id: Reg,
553        tag: i32,
554        payload: Reg,
555    ) -> Reg {
556        let w = self.window(4);
557        self.mov(w.at(0), sender);
558        self.mov(w.at(1), request_id);
559        self.set(w.at(2), tag);
560        self.mov(w.at(3), payload);
561        self.native_n(w.base(), native_index, 4);
562        w.base()
563    }
564
565    fn temp(&mut self) -> Reg {
566        if let Some(scratch) = self.scratch {
567            return scratch;
568        }
569        let scratch = self.local();
570        self.scratch = Some(scratch);
571        scratch
572    }
573}
574
575#[cfg(test)]
576mod tests {
577    use super::*;
578    use crate::{NativeTable, Value, Vm, VmResult};
579
580    type TestResult = Result<(), Box<dyn std::error::Error>>;
581
582    fn count_to(n: i64) -> Chunk {
583        let mut program = Program::new("count");
584        program.function("main", 0, |f| {
585            let limit = f.load_int(n);
586            let counter = f.load_i32(0);
587            f.while_lt(counter, limit, |f| f.add_imm(counter, 1));
588            f.return_(counter);
589        });
590        program.build()
591    }
592
593    #[test]
594    fn function_raw_keeps_declared_register_file_for_verify() -> TestResult {
595        let mut program = Program::new("malformed");
596        program.function_raw("main", 3, 1, |f| {
597            let r0 = f.reg(0);
598            f.return_(r0);
599        });
600        let chunk = program.build();
601        assert_eq!(chunk.functions[0].arity, 3);
602        assert_eq!(chunk.functions[0].num_registers, 1);
603        assert!(matches!(
604            crate::verify(&chunk),
605            Err(crate::VerifyError::ArityExceedsRegisters {
606                function: 0,
607                arity: 3,
608                num_registers: 1,
609            })
610        ));
611        Ok(())
612    }
613
614    #[test]
615    fn count_loop_returns_n() -> TestResult {
616        let chunk = count_to(100);
617        let mut vm = Vm::new(std::sync::Arc::new(chunk), NativeTable::empty(), 0, &[])?;
618        assert!(matches!(
619            vm.run(10_000),
620            VmResult::Complete(Value::Int(100))
621        ));
622        Ok(())
623    }
624
625    #[test]
626    fn ping_pong_shape() -> TestResult {
627        let chunk = crate::samples::ping_pong();
628        crate::verify(&chunk)?;
629        assert!(chunk.functions.iter().any(|f| f.name == "main"));
630        assert!(chunk.functions.iter().any(|f| f.name == "pong"));
631        Ok(())
632    }
633}