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_spawn_with_rights(dst, function, argc, crate::bytecode::CapRights::FLOW);
110    }
111
112    /// Bytecode spawn with an explicit rights request (`instr.c`). `NONE` is
113    /// confined: the child inherits no power until a later `Delegate`.
114    pub fn emit_spawn_with_rights(
115        &mut self,
116        dst: u8,
117        function: u32,
118        argc: u8,
119        rights: crate::bytecode::CapRights,
120    ) {
121        self.emit(Instruction::new(
122            Opcode::Spawn,
123            dst,
124            argc,
125            rights.bits_u8(),
126            function as i32,
127        ));
128    }
129
130    pub fn emit_delegate(&mut self, dst: u8, src_cap: u8, rights: crate::bytecode::CapRights) {
131        self.emit(Instruction::new(
132            Opcode::Delegate,
133            dst,
134            src_cap,
135            255,
136            rights.bits() as i32,
137        ));
138    }
139
140    pub fn emit_yield(&mut self) {
141        self.emit(Instruction::nullary(Opcode::Yield));
142    }
143
144    pub fn emit_sleep(&mut self, millis_reg: u8) {
145        self.emit(Instruction::abc(Opcode::Sleep, millis_reg, 0, 0));
146    }
147
148    pub fn emit_exit(&mut self, reg: u8) {
149        self.emit(Instruction::abc(Opcode::Exit, reg, 0, 0));
150    }
151
152    /// Write a **self Cap** (`SEND|ASK`) into `dst` (opcode still named `SelfPid`).
153    pub fn emit_self_pid(&mut self, dst: u8) {
154        self.emit(Instruction::abc(Opcode::SelfPid, dst, 0, 0));
155    }
156
157    /// Fire-and-forget Atomic Hop: `r[target_cap_reg]` must be Cap; `r[msg_reg]` Message.
158    pub fn emit_send(&mut self, target_cap_reg: u8, msg_reg: u8) {
159        self.emit(Instruction::abc(Opcode::Send, target_cap_reg, msg_reg, 0));
160    }
161
162    pub fn emit_receive(&mut self, dst: u8) {
163        self.emit(Instruction::abc(Opcode::Receive, dst, 0, 0));
164    }
165
166    pub fn emit_receive_timeout(&mut self, dst: u8, millis_reg: u8) {
167        self.emit(Instruction::abc(Opcode::ReceiveTimeout, dst, millis_reg, 0));
168    }
169
170    /// Selective Atomic Hop: wait for `Message` with `tag == r[tag_reg]`.
171    pub fn emit_receive_match(&mut self, dst: u8, tag_reg: u8) {
172        self.emit(Instruction::abc(Opcode::ReceiveMatch, dst, tag_reg, 0));
173    }
174
175    /// Selective Atomic Hop with an immediate `u16` tag.
176    pub fn emit_receive_match_imm(&mut self, dst: u8, tag: u16) {
177        self.emit(Instruction::a_imm(Opcode::ReceiveMatchImm, dst, i32::from(tag)));
178    }
179
180    /// Atomic request/reply hop: deliver `r[msg_reg]` to `r[target_cap_reg]` (Cap),
181    /// then wait for a correlated reply into `dst`.
182    ///
183    /// Encoding: `Ask ra, rb, rc` → `a=dest`, `b=target Cap`, `c=request Message`.
184    ///
185    /// The worker authenticates the request (`sender` + `reply_cap`) before delivery
186    /// and completes only when the reply’s `sender` equals the **resolved FlowId**.
187    pub fn emit_ask(&mut self, dest: u8, target_cap_reg: u8, msg_reg: u8) {
188        self.emit(Instruction::abc(Opcode::Ask, dest, target_cap_reg, msg_reg));
189    }
190
191    /// Like [`Self::emit_ask`], with a timeout register in `imm`.
192    pub fn emit_ask_timeout(
193        &mut self,
194        dest: u8,
195        target_cap_reg: u8,
196        msg_reg: u8,
197        millis_reg: u8,
198    ) {
199        self.emit(Instruction::new(
200            Opcode::AskTimeout,
201            dest,
202            target_cap_reg,
203            msg_reg,
204            i32::from(millis_reg),
205        ));
206    }
207
208    pub fn emit_monitor(&mut self, dest: u8, target_cap_reg: u8) {
209        self.emit(Instruction::abc(Opcode::Monitor, dest, target_cap_reg, 0));
210    }
211
212    pub fn emit_demonitor(&mut self, monitor_reg: u8) {
213        self.emit(Instruction::abc(Opcode::Demonitor, monitor_reg, 0, 0));
214    }
215
216    pub fn emit_link(&mut self, dest: u8, target_cap_reg: u8) {
217        self.emit(Instruction::abc(Opcode::Link, dest, target_cap_reg, 0));
218    }
219
220    pub fn emit_unlink(&mut self, link_reg: u8) {
221        self.emit(Instruction::abc(Opcode::Unlink, link_reg, 0, 0));
222    }
223
224    pub fn emit_trap(&mut self, code: i32) {
225        self.emit(Instruction::only_imm(Opcode::Trap, code));
226    }
227
228    pub fn emit_call(&mut self, dst: u8, function: u32, argc: u8) {
229        self.emit(Instruction::new(Opcode::Call, dst, argc, 0, function as i32));
230    }
231
232    /// Emit a call through the runtime's native (FFI) function table
233    /// (design notes §30-31). `native_index` is resolved by name against a
234    /// [`crate::NativeTable`] at the call site — the assembler has no
235    /// knowledge of what natives exist, on purpose (see
236    /// [`crate::verify`]'s note on why `CallNative` targets aren't
237    /// range-checked statically).
238    pub fn emit_call_native(&mut self, dst: u8, native_index: u32, argc: u8) {
239        self.emit(Instruction::new(
240            Opcode::CallNative,
241            dst,
242            argc,
243            0,
244            native_index as i32,
245        ));
246    }
247
248    /// Move `src` into `dst`, then `CallNative(dst, native_index, 1)`.
249    ///
250    /// # The contract this exists to protect: `CallNative` clobbers its argument
251    ///
252    /// `Opcode::CallNative ra, fb, nc` reads `nc` arguments from
253    /// `r[a..a+nc]` and writes the result back into `r[a]`. For `nc == 1`
254    /// the argument and result are the same slot — calling a one-arg native
255    /// straight on a register you still need destroys it.
256    ///
257    /// The textbook case is unpacking several fields from one `Message` in
258    /// `r0` (`msg_sender`, `msg_tag`, …). `emit_native1_from` always operates
259    /// on a **copy** (`dst`), so `src` survives:
260    ///
261    /// ```text
262    /// b.emit_native1_from(1, 0, native_msg_sender);   // r1 = sender(r0)
263    /// b.emit_native1_from(2, 0, native_msg_request_id);
264    /// ```
265    ///
266    /// If you don't need `src` afterwards, call `emit_call_native` directly —
267    /// the `Move` would be pure overhead. See [`crate::emit_native1_from`] for
268    /// the macro-sugar form that forwards here.
269    pub fn emit_native1_from(&mut self, dst: u8, src: u8, native_index: u32) {
270        self.emit_move(dst, src);
271        self.emit_call_native(dst, native_index, 1);
272    }
273
274    /// `CallNative(base, native_index, argc)` when `argc` args are **already**
275    /// contiguous at `r[base..base+argc]`.
276    ///
277    /// No behavior beyond [`Self::emit_call_native`] — exists so the call site
278    /// reads as "args already packed". See [`crate::emit_native_n`].
279    pub fn emit_native_n(&mut self, base: u8, native_index: u32, argc: u8) {
280        self.emit_call_native(base, native_index, argc);
281    }
282
283    pub fn emit_return(&mut self, reg: u8) {
284        self.emit(Instruction::abc(Opcode::Return, reg, 0, 0));
285    }
286
287    /// Mark the start of a bytecode function at the current position and
288    /// register it in the function table under `name`. Returns the function
289    /// index, usable with [`ChunkBuilder::emit_call`]/[`ChunkBuilder::emit_spawn`]
290    /// even before the function's body is emitted (functions may call
291    /// themselves or each other, forward or backward).
292    pub fn begin_function(&mut self, name: impl Into<String>, arity: u8, num_registers: u8) -> u32 {
293        let name = name.into();
294        let entry = self.code.len() as u32;
295        let idx = self.functions.len() as u32;
296        self.functions.push(FunctionDef {
297            name: name.clone(),
298            entry,
299            arity,
300            num_registers,
301        });
302        self.fn_starts.insert(name, idx);
303        idx
304    }
305
306    pub fn function_index(&self, name: &str) -> Option<u32> {
307        self.fn_starts.get(name).copied()
308    }
309
310    /// Patch the register-file size of an already-`begin_function`'d
311    /// function. Exists for assemblers whose register count is only known
312    /// *after* emitting the body — `begin_function` must still be called
313    /// first so `entry` captures the current code cursor.
314    pub fn set_num_registers(&mut self, function_index: u32, num_registers: u8) {
315        if let Some(def) = self.functions.get_mut(function_index as usize) {
316            def.num_registers = num_registers;
317        }
318    }
319
320    /// Resolve every pending jump against its bound label and produce the
321    /// final immutable [`Chunk`].
322    ///
323    /// An unbound label is a host assembly bug. This method does **not**
324    /// panic: the jump is left with relative offset `0` (falls through).
325    /// [`crate::verify`] / [`crate::Runtime::new`] then reject or run a
326    /// no-op jump rather than taking the process down at assemble time.
327    pub fn finish(mut self) -> Chunk {
328        for (idx, label) in self.pending_jumps.drain(..) {
329            match self.label_targets.get(&label) {
330                Some(target) => {
331                    // Relative offset from the instruction *after* this jump.
332                    let offset = *target as i64 - (idx as i64 + 1);
333                    self.code[idx].imm = offset as i32;
334                }
335                None => {
336                    // Fail-closed without panic: identity jump (offset 0).
337                    self.code[idx].imm = 0;
338                }
339            }
340        }
341        Chunk {
342            name: self.name,
343            constants: self.constants,
344            code: self.code,
345            functions: self.functions,
346        }
347    }
348}
349#[cfg(test)]
350mod tests {
351    use super::*;
352
353    #[test]
354    fn emit_native1_from_never_clobbers_source_register() {
355        let mut b = ChunkBuilder::new("clobber-test");
356        b.begin_function("main", 0, 8);
357        b.emit_native1_from(1, 0, 10);
358        b.emit_native1_from(2, 0, 11);
359        b.emit_native1_from(3, 0, 12);
360        let chunk = b.finish();
361
362        assert_eq!(chunk.code.len(), 6);
363        for (move_idx, call_idx, expected_native) in
364            [(0usize, 1usize, 10i32), (2, 3, 11), (4, 5, 12)]
365        {
366            assert_eq!(chunk.code[move_idx].op, Opcode::Move);
367            assert_eq!(chunk.code[move_idx].b, 0);
368            assert_eq!(chunk.code[call_idx].op, Opcode::CallNative);
369            assert_ne!(chunk.code[call_idx].a, 0);
370            assert_eq!(chunk.code[call_idx].imm, expected_native);
371        }
372    }
373
374    #[test]
375    fn emit_native_n_is_a_plain_call_native_with_no_extra_instructions() {
376        let mut b = ChunkBuilder::new("native-n-test");
377        b.begin_function("main", 0, 8);
378        b.emit_load_imm(1, 7);
379        b.emit_load_imm(2, 1);
380        b.emit_native_n(1, 99, 2);
381        let chunk = b.finish();
382
383        assert_eq!(chunk.code.len(), 3);
384        assert_eq!(chunk.code[2].op, Opcode::CallNative);
385        assert_eq!(chunk.code[2].a, 1);
386        assert_eq!(chunk.code[2].b, 2);
387        assert_eq!(chunk.code[2].imm, 99);
388    }
389
390    #[test]
391    fn macro_forms_produce_identical_bytecode_to_the_methods() {
392        let mut via_method = ChunkBuilder::new("via-method");
393        via_method.begin_function("main", 0, 8);
394        via_method.emit_native1_from(1, 0, 10);
395        via_method.emit_native_n(1, 99, 2);
396
397        let mut via_macro = ChunkBuilder::new("via-macro");
398        via_macro.begin_function("main", 0, 8);
399        crate::emit_native1_from!(via_macro, 1, 0, 10);
400        crate::emit_native_n!(via_macro, 1, 99, 2);
401
402        assert_eq!(via_method.finish().code, via_macro.finish().code);
403    }
404}