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        let cap = self.local();
348        self.spawn_at(cap, function, argc);
349        cap
350    }
351
352    /// Spawn into an explicit destination register (e.g. `reg(255)`).
353    pub fn spawn_at(&mut self, dst: Reg, function: FuncId, argc: u8) {
354        self.b.emit_spawn(dst.0, function, argc);
355    }
356
357    pub fn send(&mut self, target_cap: Reg, msg: Reg) {
358        self.b.emit_send(target_cap.0, msg.0);
359    }
360
361    pub fn receive(&mut self) -> Reg {
362        let msg = self.local();
363        self.b.emit_receive(msg.0);
364        msg
365    }
366
367    pub fn receive_timeout(&mut self, millis: Reg) -> Reg {
368        let msg = self.local();
369        self.b.emit_receive_timeout(msg.0, millis.0);
370        msg
371    }
372
373    pub fn receive_match(&mut self, tag: Reg) -> Reg {
374        let msg = self.local();
375        self.b.emit_receive_match(msg.0, tag.0);
376        msg
377    }
378
379    pub fn receive_match_imm(&mut self, tag: u16) -> Reg {
380        let msg = self.local();
381        self.b.emit_receive_match_imm(msg.0, tag);
382        msg
383    }
384
385    /// RPC hop: deliver `msg` to `target_cap`, wait for correlated reply.
386    ///
387    /// If the target exits first, dest is a [`crate::TAG_SYS_EXIT`] hop
388    /// (not a hang).
389    pub fn ask(&mut self, target_cap: Reg, msg: Reg) -> Reg {
390        let reply = self.local();
391        self.b.emit_ask(reply.0, target_cap.0, msg.0);
392        reply
393    }
394
395    /// Like [`Self::ask`], but writes `Unit` into the dest if `millis` elapses.
396    pub fn ask_timeout(&mut self, target_cap: Reg, msg: Reg, millis: Reg) -> Reg {
397        let reply = self.local();
398        self.b
399            .emit_ask_timeout(reply.0, target_cap.0, msg.0, millis.0);
400        reply
401    }
402
403    /// One-way watch: when the flow addressed by `target_cap` exits, this
404    /// flow receives a [`crate::TAG_SYS_DOWN`] hop. Returns a monitor ref (`Int`).
405    pub fn monitor(&mut self, target_cap: Reg) -> Reg {
406        let dst = self.local();
407        self.b.emit_monitor(dst.0, target_cap.0);
408        dst
409    }
410
411    pub fn demonitor(&mut self, monitor: Reg) {
412        self.b.emit_demonitor(monitor.0);
413    }
414
415    /// Bidirectional link: abnormal exit of either side kills the peer.
416    pub fn link(&mut self, target_cap: Reg) -> Reg {
417        let dst = self.local();
418        self.b.emit_link(dst.0, target_cap.0);
419        dst
420    }
421
422    pub fn unlink(&mut self, link: Reg) {
423        self.b.emit_unlink(link.0);
424    }
425
426    pub fn trap(&mut self, code: i32) {
427        self.b.emit_trap(code);
428    }
429
430    /// Copy `src`, call a one-arg native, return the result in a new local.
431    ///
432    /// The source register is preserved (`CallNative` clobbers its argument slot).
433    pub fn native1_from(&mut self, src: Reg, native: u32) -> Reg {
434        let dst = self.local();
435        self.b.emit_native1_from(dst.0, src.0, native);
436        dst
437    }
438
439    /// Side-effect native on a copy of `src` (e.g. `print`).
440    pub fn native1_on(&mut self, src: Reg, native: u32) {
441        let tmp = self.local();
442        self.b.emit_native1_from(tmp.0, src.0, native);
443    }
444
445    /// `CallNative` with `argc` args already at `base..base+argc`.
446    pub fn native_n(&mut self, base: Reg, native: u32, argc: u8) {
447        self.b.emit_native_n(base.0, native, argc);
448    }
449
450    /// Call a native with `argc` args already in `base..base+argc`.
451    pub fn call_native(&mut self, base: Reg, native: u32, argc: u8) {
452        self.native_n(base, native, argc);
453    }
454
455    /// Call a zero-arg native; returns the result register.
456    pub fn call_native0(&mut self, native: u32) -> Reg {
457        let dst = self.local();
458        self.b.emit_call_native(dst.0, native, 0);
459        dst
460    }
461
462    /// Build an outgoing Atomic Hop (`request_id`, `tag`, `payload`).
463    ///
464    /// The scheduler overwrites `sender` and mints `reply_cap` on [`Self::send`]
465    /// / [`Self::ask`] — do not forge a sender (see [`crate::docs::security`]).
466    pub fn hop(&mut self, request_id: Reg, tag: i32, payload: Reg) -> Reg {
467        let placeholder = self.load_i32(0);
468        self.make_msg(
469            crate::natives::std_native::MAKE_MSG,
470            placeholder,
471            request_id,
472            tag,
473            payload,
474        )
475    }
476
477    /// Extract authenticated origin from a delivered hop (`Value::Pid`).
478    pub fn hop_sender(&mut self, msg: Reg) -> Reg {
479        self.native1_from(msg, crate::natives::std_native::MSG_SENDER)
480    }
481
482    pub fn hop_request_id(&mut self, msg: Reg) -> Reg {
483        self.native1_from(msg, crate::natives::std_native::MSG_REQUEST_ID)
484    }
485
486    pub fn hop_tag(&mut self, msg: Reg) -> Reg {
487        self.native1_from(msg, crate::natives::std_native::MSG_TAG)
488    }
489
490    pub fn hop_payload(&mut self, msg: Reg) -> Reg {
491        self.native1_from(msg, crate::natives::std_native::MSG_PAYLOAD)
492    }
493
494    /// Reply address grant minted for the original sender (`Value::Cap`).
495    pub fn hop_reply_cap(&mut self, msg: Reg) -> Reg {
496        self.native1_from(msg, crate::natives::std_native::MSG_REPLY_CAP)
497    }
498
499    /// Build a reply hop echoing `request_id` from `req`.
500    pub fn reply_to(&mut self, req: Reg, tag: i32, payload: Reg) -> Reg {
501        let req_id = self.hop_request_id(req);
502        self.hop(req_id, tag, payload)
503    }
504
505    /// Reply to `req` via its `reply_cap` (typical server pattern).
506    pub fn send_reply(&mut self, req: Reg, tag: i32, payload: Reg) {
507        let reply_cap = self.hop_reply_cap(req);
508        let reply = self.reply_to(req, tag, payload);
509        self.send(reply_cap, reply);
510    }
511
512    /// Build a [`Value::Message`] via `make_msg` at `native_index`.
513    ///
514    /// Prefer [`Self::hop`] for outgoing messages. This low-level helper keeps
515    /// the explicit `sender` slot for security regressions and legacy bytecode.
516    /// Index `2` in [`crate::std_native_map`]. Args are packed into a
517    /// contiguous four-register window; returns the message register.
518    pub fn make_msg(
519        &mut self,
520        native_index: u32,
521        sender: Reg,
522        request_id: Reg,
523        tag: i32,
524        payload: Reg,
525    ) -> Reg {
526        let w = self.window(4);
527        self.mov(w.at(0), sender);
528        self.mov(w.at(1), request_id);
529        self.set(w.at(2), tag);
530        self.mov(w.at(3), payload);
531        self.native_n(w.base(), native_index, 4);
532        w.base()
533    }
534
535    fn temp(&mut self) -> Reg {
536        if let Some(scratch) = self.scratch {
537            return scratch;
538        }
539        let scratch = self.local();
540        self.scratch = Some(scratch);
541        scratch
542    }
543}
544
545#[cfg(test)]
546mod tests {
547    use super::*;
548    use crate::{NativeTable, Value, Vm, VmResult};
549
550    type TestResult = Result<(), Box<dyn std::error::Error>>;
551
552    fn count_to(n: i64) -> Chunk {
553        let mut program = Program::new("count");
554        program.function("main", 0, |f| {
555            let limit = f.load_int(n);
556            let counter = f.load_i32(0);
557            f.while_lt(counter, limit, |f| f.add_imm(counter, 1));
558            f.return_(counter);
559        });
560        program.build()
561    }
562
563    #[test]
564    fn function_raw_keeps_declared_register_file_for_verify() -> TestResult {
565        let mut program = Program::new("malformed");
566        program.function_raw("main", 3, 1, |f| {
567            let r0 = f.reg(0);
568            f.return_(r0);
569        });
570        let chunk = program.build();
571        assert_eq!(chunk.functions[0].arity, 3);
572        assert_eq!(chunk.functions[0].num_registers, 1);
573        assert!(matches!(
574            crate::verify(&chunk),
575            Err(crate::VerifyError::ArityExceedsRegisters {
576                function: 0,
577                arity: 3,
578                num_registers: 1,
579            })
580        ));
581        Ok(())
582    }
583
584    #[test]
585    fn count_loop_returns_n() -> TestResult {
586        let chunk = count_to(100);
587        let mut vm = Vm::new(std::sync::Arc::new(chunk), NativeTable::empty(), 0, &[])?;
588        assert!(matches!(
589            vm.run(10_000),
590            VmResult::Complete(Value::Int(100))
591        ));
592        Ok(())
593    }
594
595    #[test]
596    fn ping_pong_shape() -> TestResult {
597        let chunk = crate::samples::ping_pong();
598        crate::verify(&chunk)?;
599        assert!(chunk.functions.iter().any(|f| f.name == "main"));
600        assert!(chunk.functions.iter().any(|f| f.name == "pong"));
601        Ok(())
602    }
603}