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    pub fn self_cap(&mut self) -> Reg {
331        let cap = self.local();
332        self.b.emit_self_pid(cap.0);
333        cap
334    }
335
336    pub fn spawn(&mut self, function: FuncId, argc: u8) -> Reg {
337        let cap = self.local();
338        self.spawn_at(cap, function, argc);
339        cap
340    }
341
342    /// Spawn into an explicit destination register (e.g. `reg(255)`).
343    pub fn spawn_at(&mut self, dst: Reg, function: FuncId, argc: u8) {
344        self.b.emit_spawn(dst.0, function, argc);
345    }
346
347    pub fn send(&mut self, target_cap: Reg, msg: Reg) {
348        self.b.emit_send(target_cap.0, msg.0);
349    }
350
351    pub fn receive(&mut self) -> Reg {
352        let msg = self.local();
353        self.b.emit_receive(msg.0);
354        msg
355    }
356
357    pub fn receive_timeout(&mut self, millis: Reg) -> Reg {
358        let msg = self.local();
359        self.b.emit_receive_timeout(msg.0, millis.0);
360        msg
361    }
362
363    pub fn receive_match(&mut self, tag: Reg) -> Reg {
364        let msg = self.local();
365        self.b.emit_receive_match(msg.0, tag.0);
366        msg
367    }
368
369    pub fn receive_match_imm(&mut self, tag: u16) -> Reg {
370        let msg = self.local();
371        self.b.emit_receive_match_imm(msg.0, tag);
372        msg
373    }
374
375    /// RPC hop: deliver `msg` to `target_cap`, wait for correlated reply.
376    pub fn ask(&mut self, target_cap: Reg, msg: Reg) -> Reg {
377        let reply = self.local();
378        self.b.emit_ask(reply.0, target_cap.0, msg.0);
379        reply
380    }
381
382    pub fn trap(&mut self, code: i32) {
383        self.b.emit_trap(code);
384    }
385
386    /// Copy `src`, call a one-arg native, return the result in a new local.
387    ///
388    /// The source register is preserved (`CallNative` clobbers its argument slot).
389    pub fn native1_from(&mut self, src: Reg, native: u32) -> Reg {
390        let dst = self.local();
391        self.b.emit_native1_from(dst.0, src.0, native);
392        dst
393    }
394
395    /// Side-effect native on a copy of `src` (e.g. `print`).
396    pub fn native1_on(&mut self, src: Reg, native: u32) {
397        let tmp = self.local();
398        self.b.emit_native1_from(tmp.0, src.0, native);
399    }
400
401    /// `CallNative` with `argc` args already at `base..base+argc`.
402    pub fn native_n(&mut self, base: Reg, native: u32, argc: u8) {
403        self.b.emit_native_n(base.0, native, argc);
404    }
405
406    /// Call a native with `argc` args already in `base..base+argc`.
407    pub fn call_native(&mut self, base: Reg, native: u32, argc: u8) {
408        self.native_n(base, native, argc);
409    }
410
411    /// Call a zero-arg native; returns the result register.
412    pub fn call_native0(&mut self, native: u32) -> Reg {
413        let dst = self.local();
414        self.b.emit_call_native(dst.0, native, 0);
415        dst
416    }
417
418    /// Build a [`Value::Message`] via `make_msg` at `native_index`.
419    ///
420    /// Index `2` in [`crate::std_native_map`]. Args are packed into a
421    /// contiguous four-register window; returns the message register.
422    pub fn make_msg(
423        &mut self,
424        native_index: u32,
425        sender: Reg,
426        request_id: Reg,
427        tag: i32,
428        payload: Reg,
429    ) -> Reg {
430        let w = self.window(4);
431        self.mov(w.at(0), sender);
432        self.mov(w.at(1), request_id);
433        self.set(w.at(2), tag);
434        self.mov(w.at(3), payload);
435        self.native_n(w.base(), native_index, 4);
436        w.base()
437    }
438
439    fn temp(&mut self) -> Reg {
440        if let Some(scratch) = self.scratch {
441            return scratch;
442        }
443        let scratch = self.local();
444        self.scratch = Some(scratch);
445        scratch
446    }
447}
448
449#[cfg(test)]
450mod tests {
451    use super::*;
452    use crate::{NativeTable, Value, Vm, VmResult};
453
454    type TestResult = Result<(), Box<dyn std::error::Error>>;
455
456    fn count_to(n: i64) -> Chunk {
457        let mut program = Program::new("count");
458        program.function("main", 0, |f| {
459            let limit = f.load_int(n);
460            let counter = f.load_i32(0);
461            f.while_lt(counter, limit, |f| f.add_imm(counter, 1));
462            f.return_(counter);
463        });
464        program.build()
465    }
466
467    #[test]
468    fn function_raw_keeps_declared_register_file_for_verify() -> TestResult {
469        let mut program = Program::new("malformed");
470        program.function_raw("main", 3, 1, |f| {
471            let r0 = f.reg(0);
472            f.return_(r0);
473        });
474        let chunk = program.build();
475        assert_eq!(chunk.functions[0].arity, 3);
476        assert_eq!(chunk.functions[0].num_registers, 1);
477        assert!(matches!(
478            crate::verify(&chunk),
479            Err(crate::VerifyError::ArityExceedsRegisters {
480                function: 0,
481                arity: 3,
482                num_registers: 1,
483            })
484        ));
485        Ok(())
486    }
487
488    #[test]
489    fn count_loop_returns_n() -> TestResult {
490        let chunk = count_to(100);
491        let mut vm = Vm::new(std::sync::Arc::new(chunk), NativeTable::empty(), 0, &[])?;
492        assert!(matches!(
493            vm.run(10_000),
494            VmResult::Complete(Value::Int(100))
495        ));
496        Ok(())
497    }
498
499    #[test]
500    fn ping_pong_shape() -> TestResult {
501        let chunk = crate::samples::ping_pong();
502        crate::verify(&chunk)?;
503        assert!(chunk.functions.iter().any(|f| f.name == "main"));
504        assert!(chunk.functions.iter().any(|f| f.name == "pong"));
505        Ok(())
506    }
507}