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    /// Like [`Self::emit_ask`], with a timeout register in `imm`.
164    pub fn emit_ask_timeout(
165        &mut self,
166        dest: u8,
167        target_cap_reg: u8,
168        msg_reg: u8,
169        millis_reg: u8,
170    ) {
171        self.emit(Instruction::new(
172            Opcode::AskTimeout,
173            dest,
174            target_cap_reg,
175            msg_reg,
176            i32::from(millis_reg),
177        ));
178    }
179
180    pub fn emit_monitor(&mut self, dest: u8, target_cap_reg: u8) {
181        self.emit(Instruction::abc(Opcode::Monitor, dest, target_cap_reg, 0));
182    }
183
184    pub fn emit_demonitor(&mut self, monitor_reg: u8) {
185        self.emit(Instruction::abc(Opcode::Demonitor, monitor_reg, 0, 0));
186    }
187
188    pub fn emit_link(&mut self, dest: u8, target_cap_reg: u8) {
189        self.emit(Instruction::abc(Opcode::Link, dest, target_cap_reg, 0));
190    }
191
192    pub fn emit_unlink(&mut self, link_reg: u8) {
193        self.emit(Instruction::abc(Opcode::Unlink, link_reg, 0, 0));
194    }
195
196    pub fn emit_trap(&mut self, code: i32) {
197        self.emit(Instruction::only_imm(Opcode::Trap, code));
198    }
199
200    pub fn emit_call(&mut self, dst: u8, function: u32, argc: u8) {
201        self.emit(Instruction::new(Opcode::Call, dst, argc, 0, function as i32));
202    }
203
204    /// Emit a call through the runtime's native (FFI) function table
205    /// (design notes §30-31). `native_index` is resolved by name against a
206    /// [`crate::NativeTable`] at the call site — the assembler has no
207    /// knowledge of what natives exist, on purpose (see
208    /// [`crate::verify`]'s note on why `CallNative` targets aren't
209    /// range-checked statically).
210    pub fn emit_call_native(&mut self, dst: u8, native_index: u32, argc: u8) {
211        self.emit(Instruction::new(
212            Opcode::CallNative,
213            dst,
214            argc,
215            0,
216            native_index as i32,
217        ));
218    }
219
220    /// Move `src` into `dst`, then `CallNative(dst, native_index, 1)`.
221    ///
222    /// # The contract this exists to protect: `CallNative` clobbers its argument
223    ///
224    /// `Opcode::CallNative ra, fb, nc` reads `nc` arguments from
225    /// `r[a..a+nc]` and writes the result back into `r[a]`. For `nc == 1`
226    /// the argument and result are the same slot — calling a one-arg native
227    /// straight on a register you still need destroys it.
228    ///
229    /// The textbook case is unpacking several fields from one `Message` in
230    /// `r0` (`msg_sender`, `msg_tag`, …). `emit_native1_from` always operates
231    /// on a **copy** (`dst`), so `src` survives:
232    ///
233    /// ```text
234    /// b.emit_native1_from(1, 0, native_msg_sender);   // r1 = sender(r0)
235    /// b.emit_native1_from(2, 0, native_msg_request_id);
236    /// ```
237    ///
238    /// If you don't need `src` afterwards, call `emit_call_native` directly —
239    /// the `Move` would be pure overhead. See [`crate::emit_native1_from`] for
240    /// the macro-sugar form that forwards here.
241    pub fn emit_native1_from(&mut self, dst: u8, src: u8, native_index: u32) {
242        self.emit_move(dst, src);
243        self.emit_call_native(dst, native_index, 1);
244    }
245
246    /// `CallNative(base, native_index, argc)` when `argc` args are **already**
247    /// contiguous at `r[base..base+argc]`.
248    ///
249    /// No behavior beyond [`Self::emit_call_native`] — exists so the call site
250    /// reads as "args already packed". See [`crate::emit_native_n`].
251    pub fn emit_native_n(&mut self, base: u8, native_index: u32, argc: u8) {
252        self.emit_call_native(base, native_index, argc);
253    }
254
255    pub fn emit_return(&mut self, reg: u8) {
256        self.emit(Instruction::abc(Opcode::Return, reg, 0, 0));
257    }
258
259    /// Mark the start of a bytecode function at the current position and
260    /// register it in the function table under `name`. Returns the function
261    /// index, usable with [`ChunkBuilder::emit_call`]/[`ChunkBuilder::emit_spawn`]
262    /// even before the function's body is emitted (functions may call
263    /// themselves or each other, forward or backward).
264    pub fn begin_function(&mut self, name: impl Into<String>, arity: u8, num_registers: u8) -> u32 {
265        let name = name.into();
266        let entry = self.code.len() as u32;
267        let idx = self.functions.len() as u32;
268        self.functions.push(FunctionDef {
269            name: name.clone(),
270            entry,
271            arity,
272            num_registers,
273        });
274        self.fn_starts.insert(name, idx);
275        idx
276    }
277
278    pub fn function_index(&self, name: &str) -> Option<u32> {
279        self.fn_starts.get(name).copied()
280    }
281
282    /// Patch the register-file size of an already-`begin_function`'d
283    /// function. Exists for assemblers whose register count is only known
284    /// *after* emitting the body — `begin_function` must still be called
285    /// first so `entry` captures the current code cursor.
286    pub fn set_num_registers(&mut self, function_index: u32, num_registers: u8) {
287        if let Some(def) = self.functions.get_mut(function_index as usize) {
288            def.num_registers = num_registers;
289        }
290    }
291
292    /// Resolve every pending jump against its bound label and produce the
293    /// final immutable [`Chunk`].
294    ///
295    /// An unbound label is a host assembly bug. This method does **not**
296    /// panic: the jump is left with relative offset `0` (falls through).
297    /// [`crate::verify`] / [`crate::Runtime::new`] then reject or run a
298    /// no-op jump rather than taking the process down at assemble time.
299    pub fn finish(mut self) -> Chunk {
300        for (idx, label) in self.pending_jumps.drain(..) {
301            match self.label_targets.get(&label) {
302                Some(target) => {
303                    // Relative offset from the instruction *after* this jump.
304                    let offset = *target as i64 - (idx as i64 + 1);
305                    self.code[idx].imm = offset as i32;
306                }
307                None => {
308                    // Fail-closed without panic: identity jump (offset 0).
309                    self.code[idx].imm = 0;
310                }
311            }
312        }
313        Chunk {
314            name: self.name,
315            constants: self.constants,
316            code: self.code,
317            functions: self.functions,
318        }
319    }
320}
321#[cfg(test)]
322mod tests {
323    use super::*;
324
325    #[test]
326    fn emit_native1_from_never_clobbers_source_register() {
327        let mut b = ChunkBuilder::new("clobber-test");
328        b.begin_function("main", 0, 8);
329        b.emit_native1_from(1, 0, 10);
330        b.emit_native1_from(2, 0, 11);
331        b.emit_native1_from(3, 0, 12);
332        let chunk = b.finish();
333
334        assert_eq!(chunk.code.len(), 6);
335        for (move_idx, call_idx, expected_native) in
336            [(0usize, 1usize, 10i32), (2, 3, 11), (4, 5, 12)]
337        {
338            assert_eq!(chunk.code[move_idx].op, Opcode::Move);
339            assert_eq!(chunk.code[move_idx].b, 0);
340            assert_eq!(chunk.code[call_idx].op, Opcode::CallNative);
341            assert_ne!(chunk.code[call_idx].a, 0);
342            assert_eq!(chunk.code[call_idx].imm, expected_native);
343        }
344    }
345
346    #[test]
347    fn emit_native_n_is_a_plain_call_native_with_no_extra_instructions() {
348        let mut b = ChunkBuilder::new("native-n-test");
349        b.begin_function("main", 0, 8);
350        b.emit_load_imm(1, 7);
351        b.emit_load_imm(2, 1);
352        b.emit_native_n(1, 99, 2);
353        let chunk = b.finish();
354
355        assert_eq!(chunk.code.len(), 3);
356        assert_eq!(chunk.code[2].op, Opcode::CallNative);
357        assert_eq!(chunk.code[2].a, 1);
358        assert_eq!(chunk.code[2].b, 2);
359        assert_eq!(chunk.code[2].imm, 99);
360    }
361
362    #[test]
363    fn macro_forms_produce_identical_bytecode_to_the_methods() {
364        let mut via_method = ChunkBuilder::new("via-method");
365        via_method.begin_function("main", 0, 8);
366        via_method.emit_native1_from(1, 0, 10);
367        via_method.emit_native_n(1, 99, 2);
368
369        let mut via_macro = ChunkBuilder::new("via-macro");
370        via_macro.begin_function("main", 0, 8);
371        crate::emit_native1_from!(via_macro, 1, 0, 10);
372        crate::emit_native_n!(via_macro, 1, 99, 2);
373
374        assert_eq!(via_method.finish().code, via_macro.finish().code);
375    }
376}