Skip to main content

byteflow/bytecode/
builder.rs

1use std::collections::HashMap;
2
3use super::chunk::{Chunk, FunctionDef};
4use super::instruction::Instruction;
5use super::opcode::Opcode;
6use super::value::Value;
7
8/// An unresolved jump target, patched to a relative offset once its address
9/// is known (see [`ChunkBuilder::bind_label`]).
10#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
11pub struct Label(u32);
12
13/// Fluent assembler for [`Chunk`]s.
14///
15/// This exists because hand-computing relative jump offsets (design notes
16/// §7 shows raw opcodes) is exactly the kind of bookkeeping that produces
17/// off-by-one bytecode bugs that only show up as a wrong branch at runtime.
18/// The builder defers that arithmetic: emit a `Jump`/`Branch` against a
19/// [`Label`], bind the label once you know where it lands, and the builder
20/// back-patches every use.
21///
22/// This is the *only* supported way to hand-author a `Chunk` in this crate;
23/// a source-level compiler (design notes' long-term "Rust → bytecode" path)
24/// would sit on top of this same API.
25pub struct ChunkBuilder {
26    name: String,
27    constants: Vec<Value>,
28    code: Vec<Instruction>,
29    functions: Vec<FunctionDef>,
30    next_label: u32,
31    label_targets: HashMap<Label, u32>,
32    /// (instruction index, label) pairs awaiting patch.
33    pending_jumps: Vec<(usize, Label)>,
34    fn_starts: HashMap<String, u32>,
35}
36
37impl ChunkBuilder {
38    pub fn new(name: impl Into<String>) -> Self {
39        ChunkBuilder {
40            name: name.into(),
41            constants: Vec::new(),
42            code: Vec::new(),
43            functions: Vec::new(),
44            next_label: 0,
45            label_targets: HashMap::new(),
46            pending_jumps: Vec::new(),
47            fn_starts: HashMap::new(),
48        }
49    }
50
51    pub fn const_(&mut self, v: Value) -> u32 {
52        // Constant deduplication keeps hot small-int/bool literals from
53        // bloating the pool across a large generated function.
54        if let Some(pos) = self.constants.iter().position(|c| c == &v) {
55            return pos as u32;
56        }
57        self.constants.push(v);
58        (self.constants.len() - 1) as u32
59    }
60
61    pub fn new_label(&mut self) -> Label {
62        let l = Label(self.next_label);
63        self.next_label += 1;
64        l
65    }
66
67    /// Bind `label` to the *next* instruction that will be emitted.
68    pub fn bind_label(&mut self, label: Label) {
69        self.label_targets.insert(label, self.code.len() as u32);
70    }
71
72    fn emit(&mut self, instr: Instruction) -> usize {
73        self.code.push(instr);
74        self.code.len() - 1
75    }
76
77    pub fn emit_halt(&mut self) {
78        self.emit(Instruction::nullary(Opcode::Halt));
79    }
80
81    pub fn emit_load_const(&mut self, dst: u8, konst: u32) {
82        self.emit(Instruction::new(Opcode::LoadConst, dst, 0, 0, konst as i32));
83    }
84
85    pub fn emit_load_imm(&mut self, dst: u8, imm: i32) {
86        self.emit(Instruction::a_imm(Opcode::LoadImm, dst, imm));
87    }
88
89    pub fn emit_move(&mut self, dst: u8, src: u8) {
90        self.emit(Instruction::abc(Opcode::Move, dst, src, 0));
91    }
92
93    pub fn emit_binop(&mut self, op: Opcode, dst: u8, lhs: u8, rhs: u8) {
94        debug_assert!(matches!(
95            op,
96            Opcode::Add | Opcode::Sub | Opcode::Mul | Opcode::Div | Opcode::Mod
97                | Opcode::Eq | Opcode::Lt | Opcode::Le
98        ));
99        self.emit(Instruction::abc(op, dst, lhs, rhs));
100    }
101
102    pub fn emit_neg(&mut self, dst: u8, src: u8) {
103        self.emit(Instruction::abc(Opcode::Neg, dst, src, 0));
104    }
105
106    pub fn emit_jump(&mut self, target: Label) {
107        let idx = self.emit(Instruction::only_imm(Opcode::Jump, 0));
108        self.pending_jumps.push((idx, target));
109    }
110
111    pub fn emit_branch(&mut self, cond: u8, target: Label) {
112        let idx = self.emit(Instruction::a_imm(Opcode::Branch, cond, 0));
113        self.pending_jumps.push((idx, target));
114    }
115
116    pub fn emit_spawn(&mut self, dst: u8, function: u32, argc: u8) {
117        self.emit(Instruction::new(Opcode::Spawn, dst, argc, 0, function as i32));
118    }
119
120    pub fn emit_yield(&mut self) {
121        self.emit(Instruction::nullary(Opcode::Yield));
122    }
123
124    pub fn emit_sleep(&mut self, millis_reg: u8) {
125        self.emit(Instruction::abc(Opcode::Sleep, millis_reg, 0, 0));
126    }
127
128    pub fn emit_exit(&mut self, reg: u8) {
129        self.emit(Instruction::abc(Opcode::Exit, reg, 0, 0));
130    }
131
132    /// Write a **self Cap** (`SEND|ASK`) into `dst` (opcode still named `SelfPid`).
133    pub fn emit_self_pid(&mut self, dst: u8) {
134        self.emit(Instruction::abc(Opcode::SelfPid, dst, 0, 0));
135    }
136
137    /// Fire-and-forget Atomic Hop: `r[target_cap_reg]` must be Cap; `r[msg_reg]` Message.
138    pub fn emit_send(&mut self, target_cap_reg: u8, msg_reg: u8) {
139        self.emit(Instruction::abc(Opcode::Send, target_cap_reg, msg_reg, 0));
140    }
141
142    pub fn emit_receive(&mut self, dst: u8) {
143        self.emit(Instruction::abc(Opcode::Receive, dst, 0, 0));
144    }
145
146    pub fn emit_receive_timeout(&mut self, dst: u8, millis_reg: u8) {
147        self.emit(Instruction::abc(Opcode::ReceiveTimeout, dst, millis_reg, 0));
148    }
149
150    /// Selective Atomic Hop: wait for `Message` with `tag == r[tag_reg]`.
151    pub fn emit_receive_match(&mut self, dst: u8, tag_reg: u8) {
152        self.emit(Instruction::abc(Opcode::ReceiveMatch, dst, tag_reg, 0));
153    }
154
155    /// Selective Atomic Hop with an immediate `u16` tag.
156    pub fn emit_receive_match_imm(&mut self, dst: u8, tag: u16) {
157        self.emit(Instruction::a_imm(Opcode::ReceiveMatchImm, dst, i32::from(tag)));
158    }
159
160    /// Atomic request/reply hop: deliver `r[msg_reg]` to `r[target_cap_reg]` (Cap),
161    /// then wait for a correlated reply into `dst`.
162    ///
163    /// Encoding: `Ask ra, rb, rc` → `a=dest`, `b=target Cap`, `c=request Message`.
164    ///
165    /// The worker authenticates the request (`sender` + `reply_cap`) before delivery
166    /// and completes only when the reply’s `sender` equals the **resolved FlowId**.
167    pub fn emit_ask(&mut self, dest: u8, target_cap_reg: u8, msg_reg: u8) {
168        self.emit(Instruction::abc(Opcode::Ask, dest, target_cap_reg, msg_reg));
169    }
170
171    pub fn emit_trap(&mut self, code: i32) {
172        self.emit(Instruction::only_imm(Opcode::Trap, code));
173    }
174
175    pub fn emit_call(&mut self, dst: u8, function: u32, argc: u8) {
176        self.emit(Instruction::new(Opcode::Call, dst, argc, 0, function as i32));
177    }
178
179    /// Emit a call through the runtime's native (FFI) function table
180    /// (design notes §30-31). `native_index` is resolved by name against a
181    /// [`crate::NativeTable`] at the call site — the assembler has no
182    /// knowledge of what natives exist, on purpose (see
183    /// [`crate::verify`]'s note on why `CallNative` targets aren't
184    /// range-checked statically).
185    pub fn emit_call_native(&mut self, dst: u8, native_index: u32, argc: u8) {
186        self.emit(Instruction::new(
187            Opcode::CallNative,
188            dst,
189            argc,
190            0,
191            native_index as i32,
192        ));
193    }
194
195    /// Move `src` into `dst`, then `CallNative(dst, native_index, 1)`.
196    ///
197    /// # The contract this exists to protect: `CallNative` clobbers its argument
198    ///
199    /// `Opcode::CallNative ra, fb, nc` reads `nc` arguments from
200    /// `r[a..a+nc]` and writes the result back into `r[a]`. For `nc == 1`
201    /// the argument and result are the same slot — calling a one-arg native
202    /// straight on a register you still need destroys it.
203    ///
204    /// The textbook case is unpacking several fields from one `Message` in
205    /// `r0` (`msg_sender`, `msg_tag`, …). `emit_native1_from` always operates
206    /// on a **copy** (`dst`), so `src` survives:
207    ///
208    /// ```text
209    /// b.emit_native1_from(1, 0, native_msg_sender);   // r1 = sender(r0)
210    /// b.emit_native1_from(2, 0, native_msg_request_id);
211    /// ```
212    ///
213    /// If you don't need `src` afterwards, call `emit_call_native` directly —
214    /// the `Move` would be pure overhead. See [`crate::emit_native1_from`] for
215    /// the macro-sugar form that forwards here.
216    pub fn emit_native1_from(&mut self, dst: u8, src: u8, native_index: u32) {
217        self.emit_move(dst, src);
218        self.emit_call_native(dst, native_index, 1);
219    }
220
221    /// `CallNative(base, native_index, argc)` when `argc` args are **already**
222    /// contiguous at `r[base..base+argc]`.
223    ///
224    /// No behavior beyond [`Self::emit_call_native`] — exists so the call site
225    /// reads as "args already packed". See [`crate::emit_native_n`].
226    pub fn emit_native_n(&mut self, base: u8, native_index: u32, argc: u8) {
227        self.emit_call_native(base, native_index, argc);
228    }
229
230    pub fn emit_return(&mut self, reg: u8) {
231        self.emit(Instruction::abc(Opcode::Return, reg, 0, 0));
232    }
233
234    /// Mark the start of a bytecode function at the current position and
235    /// register it in the function table under `name`. Returns the function
236    /// index, usable with [`ChunkBuilder::emit_call`]/[`ChunkBuilder::emit_spawn`]
237    /// even before the function's body is emitted (functions may call
238    /// themselves or each other, forward or backward).
239    pub fn begin_function(&mut self, name: impl Into<String>, arity: u8, num_registers: u8) -> u32 {
240        let name = name.into();
241        let entry = self.code.len() as u32;
242        let idx = self.functions.len() as u32;
243        self.functions.push(FunctionDef {
244            name: name.clone(),
245            entry,
246            arity,
247            num_registers,
248        });
249        self.fn_starts.insert(name, idx);
250        idx
251    }
252
253    pub fn function_index(&self, name: &str) -> Option<u32> {
254        self.fn_starts.get(name).copied()
255    }
256
257    /// Patch the register-file size of an already-`begin_function`'d
258    /// function. Exists for assemblers whose register count is only known
259    /// *after* emitting the body — `begin_function` must still be called
260    /// first so `entry` captures the current code cursor.
261    pub fn set_num_registers(&mut self, function_index: u32, num_registers: u8) {
262        if let Some(def) = self.functions.get_mut(function_index as usize) {
263            def.num_registers = num_registers;
264        }
265    }
266
267    /// Resolve every pending jump against its bound label and produce the
268    /// final immutable [`Chunk`]. Panics (a build-time bug, not a runtime
269    /// fault) if a label was referenced but never bound.
270    pub fn finish(mut self) -> Chunk {
271        for (idx, label) in self.pending_jumps.drain(..) {
272            let target = match self.label_targets.get(&label) {
273                Some(t) => *t,
274                None => panic!(
275                    "byteflow-bytecode: unbound label {label:?} in chunk '{}'",
276                    self.name
277                ),
278            };
279            // Relative offset from the instruction *after* this jump.
280            let offset = target as i64 - (idx as i64 + 1);
281            self.code[idx].imm = offset as i32;
282        }
283        Chunk {
284            name: self.name,
285            constants: self.constants,
286            code: self.code,
287            functions: self.functions,
288        }
289    }
290}
291#[cfg(test)]
292mod tests {
293    use super::*;
294
295    #[test]
296    fn emit_native1_from_never_clobbers_source_register() {
297        let mut b = ChunkBuilder::new("clobber-test");
298        b.begin_function("main", 0, 8);
299        b.emit_native1_from(1, 0, 10);
300        b.emit_native1_from(2, 0, 11);
301        b.emit_native1_from(3, 0, 12);
302        let chunk = b.finish();
303
304        assert_eq!(chunk.code.len(), 6);
305        for (move_idx, call_idx, expected_native) in
306            [(0usize, 1usize, 10i32), (2, 3, 11), (4, 5, 12)]
307        {
308            assert_eq!(chunk.code[move_idx].op, Opcode::Move);
309            assert_eq!(chunk.code[move_idx].b, 0);
310            assert_eq!(chunk.code[call_idx].op, Opcode::CallNative);
311            assert_ne!(chunk.code[call_idx].a, 0);
312            assert_eq!(chunk.code[call_idx].imm, expected_native);
313        }
314    }
315
316    #[test]
317    fn emit_native_n_is_a_plain_call_native_with_no_extra_instructions() {
318        let mut b = ChunkBuilder::new("native-n-test");
319        b.begin_function("main", 0, 8);
320        b.emit_load_imm(1, 7);
321        b.emit_load_imm(2, 1);
322        b.emit_native_n(1, 99, 2);
323        let chunk = b.finish();
324
325        assert_eq!(chunk.code.len(), 3);
326        assert_eq!(chunk.code[2].op, Opcode::CallNative);
327        assert_eq!(chunk.code[2].a, 1);
328        assert_eq!(chunk.code[2].b, 2);
329        assert_eq!(chunk.code[2].imm, 99);
330    }
331
332    #[test]
333    fn macro_forms_produce_identical_bytecode_to_the_methods() {
334        let mut via_method = ChunkBuilder::new("via-method");
335        via_method.begin_function("main", 0, 8);
336        via_method.emit_native1_from(1, 0, 10);
337        via_method.emit_native_n(1, 99, 2);
338
339        let mut via_macro = ChunkBuilder::new("via-macro");
340        via_macro.begin_function("main", 0, 8);
341        crate::emit_native1_from!(via_macro, 1, 0, 10);
342        crate::emit_native_n!(via_macro, 1, 99, 2);
343
344        assert_eq!(via_method.finish().code, via_macro.finish().code);
345    }
346}