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