Skip to main content

byteflow/vm/
machine.rs

1use std::sync::Arc;
2use std::time::Duration;
3
4use crate::bytecode::{Chunk, Instruction, Opcode, Value};
5
6use super::fault::Fault;
7use super::frame::Frame;
8use super::native::NativeTable;
9use super::result::VmResult;
10
11/// Hard limit on call nesting. Frames are heap-allocated (see [`Frame`]), so
12/// unbounded recursion would grow the Flow's memory instead of crashing
13/// the worker thread's native stack — which is worse, not better, without a
14/// limit. `4096` comfortably covers real recursive algorithms while keeping
15/// a runaway `fn f() { f() }` a `Fault`, not an OOM.
16pub const MAX_CALL_DEPTH: usize = 4096;
17
18/// One virtual Flow's execution state: call stack + registers. Cheap
19/// enough to construct that spawning a Flow is a handful of small heap
20/// allocations, not a native thread/stack (contrast: a `std::thread` reserves
21/// megabytes of stack whether it uses them or not).
22///
23/// `Vm` owns no scheduler, mailbox, or thread handle — see [`VmResult`] for
24/// why that separation is the whole point.
25pub struct Vm {
26    chunk: Arc<Chunk>,
27    natives: Arc<NativeTable>,
28    frames: Vec<Frame>,
29    /// Lifetime instruction counter, exposed for `FlowMetrics` (design
30    /// notes §26).
31    instructions_executed: u64,
32}
33
34impl Vm {
35    /// Construct a `Vm` ready to run `function` (an index into
36    /// `chunk.functions`) with the given arguments loaded into `r0..argc`.
37    /// `natives` is the FFI table `Opcode::CallNative` dispatches through —
38    /// pass [`NativeTable::empty`] if the chunk never calls out to Rust.
39    pub fn new(chunk: Arc<Chunk>, natives: Arc<NativeTable>, function: u32, args: &[Value]) -> Result<Self, Fault> {
40        let def = chunk
41            .function(function)
42            .ok_or(Fault::BadFunction { index: function, table_size: chunk.functions.len() as u32 })?;
43        let mut frame = Frame::new(function, def.num_registers, None);
44        frame.pc = def.entry as usize;
45        for (i, arg) in args.iter().enumerate().take(def.arity as usize) {
46            frame.registers[i] = arg.clone();
47        }
48        Ok(Vm { chunk, natives, frames: vec![frame], instructions_executed: 0 })
49    }
50
51    pub fn instructions_executed(&self) -> u64 {
52        self.instructions_executed
53    }
54
55    /// Index into `chunk.functions` for the active (top) call frame.
56    /// Useful for diagnostics / supervisor logs when a Flow traps.
57    pub fn current_function(&self) -> u32 {
58        // Category D: frames must be non-empty while the VM is runnable.
59        // We still avoid `.expect` — return 0 as a diagnostic fallback so a
60        // broken invariant cannot panic a worker; the next `run` will trap.
61        debug_assert!(!self.frames.is_empty(), "frames empty while running");
62        match self.frames.last() {
63            Some(frame) => frame.function,
64            None => 0,
65        }
66    }
67
68    /// A cheap `Arc` clone of the chunk this VM is executing. Used by the
69    /// scheduler to construct a child `Vm` for `Opcode::Spawn` without
70    /// needing to know anything about `Chunk`'s internals — every Flow
71    /// spawned (transitively) from the same top-level `spawn()` call shares
72    /// one immutable chunk in memory, never copies it.
73    pub fn chunk_arc(&self) -> Arc<Chunk> {
74        self.chunk.clone()
75    }
76
77    /// A cheap `Arc` clone of this VM's native function table, for the same
78    /// reason as [`Vm::chunk_arc`]: a `Spawn`-created child must dispatch
79    /// `CallNative` through the identical table its parent uses.
80    pub fn natives_arc(&self) -> Arc<NativeTable> {
81        self.natives.clone()
82    }
83
84    /// Deliver a value the scheduler produced on our behalf (a **Cap** from
85    /// `Spawn` / `SelfPid`, or a dequeued mailbox message from a `Receive`)
86    /// into the register the instruction that suspended us was targeting,
87    /// ahead of the next [`Vm::run`] call. A no-op is never valid to skip:
88    /// calling `run` without this after a `Spawn`/`Receive` result leaves the
89    /// destination register holding its previous (stale) value.
90    #[inline]
91    pub fn resume_with(&mut self, dest_reg: u8, value: Value) -> Result<(), Fault> {
92        self.set_reg(dest_reg, value)
93    }
94
95    /// Top call frame. Empty stack is a broken invariant (category D) —
96    /// returned as [`Fault::Invariant`], never as `unwrap`/`expect`.
97    #[inline]
98    fn current(&mut self) -> Result<&mut Frame, Fault> {
99        debug_assert!(!self.frames.is_empty(), "frames empty while running");
100        self.frames
101            .last_mut()
102            .ok_or(Fault::Invariant("empty frame stack while running"))
103    }
104
105    #[inline]
106    fn get_reg(&self, reg: u8) -> Result<Value, Fault> {
107        let frame = self
108            .frames
109            .last()
110            .ok_or(Fault::Invariant("empty frame stack while running"))?;
111        frame
112            .registers
113            .get(reg as usize)
114            .cloned()
115            .ok_or(Fault::RegisterOutOfRange {
116                reg,
117                frame_size: frame.registers.len() as u8,
118            })
119    }
120
121    #[inline]
122    fn set_reg(&mut self, reg: u8, value: Value) -> Result<(), Fault> {
123        let frame = self.current()?;
124        let len = frame.registers.len() as u8;
125        match frame.registers.get_mut(reg as usize) {
126            Some(slot) => {
127                *slot = value;
128                Ok(())
129            }
130            None => Err(Fault::RegisterOutOfRange { reg, frame_size: len }),
131        }
132    }
133
134    /// Fetch the next instruction and advance `pc`.
135    ///
136    /// `Ok(None)` if control fell off the end of the function body without an
137    /// explicit `Return`/`Halt` — treated as an implicit `Return Unit`
138    /// (friendlier to hand-written bytecode that omits a trailing return).
139    /// `Err` if the frame stack is empty (invariant break).
140    ///
141    /// Borrows are split on purpose: hold `pc`, look up `chunk.code`, then
142    /// write `pc+1` — a single `&mut Frame` across `self.chunk` would not
143    /// compile.
144    #[inline]
145    fn fetch(&mut self) -> Result<Option<Instruction>, Fault> {
146        let pc = self.current()?.pc;
147        let instr = self.chunk.code.get(pc).copied();
148        if instr.is_some() {
149            self.current()?.pc = pc + 1;
150        }
151        Ok(instr)
152    }
153
154    fn numeric_binop(&mut self, op: Opcode, dst: u8, lhs: u8, rhs: u8) -> Result<(), Fault> {
155        let a = self.get_reg(lhs)?;
156        let b = self.get_reg(rhs)?;
157        let result = match (op, &a, &b) {
158            (Opcode::Add, Value::Int(x), Value::Int(y)) => Value::Int(x.wrapping_add(*y)),
159            (Opcode::Add, _, _) => Value::Float(as_f64(&a)? + as_f64(&b)?),
160            (Opcode::Sub, Value::Int(x), Value::Int(y)) => Value::Int(x.wrapping_sub(*y)),
161            (Opcode::Sub, _, _) => Value::Float(as_f64(&a)? - as_f64(&b)?),
162            (Opcode::Mul, Value::Int(x), Value::Int(y)) => Value::Int(x.wrapping_mul(*y)),
163            (Opcode::Mul, _, _) => Value::Float(as_f64(&a)? * as_f64(&b)?),
164            (Opcode::Div, Value::Int(x), Value::Int(y)) => {
165                if *y == 0 {
166                    return Err(Fault::DivideByZero);
167                }
168                Value::Int(x.wrapping_div(*y))
169            }
170            (Opcode::Div, _, _) => {
171                let denom = as_f64(&b)?;
172                Value::Float(as_f64(&a)? / denom)
173            }
174            (Opcode::Mod, Value::Int(x), Value::Int(y)) => {
175                if *y == 0 {
176                    return Err(Fault::DivideByZero);
177                }
178                Value::Int(x.wrapping_rem(*y))
179            }
180            (Opcode::Eq, _, _) => Value::Bool(a == b),
181            (Opcode::Lt, Value::Int(x), Value::Int(y)) => Value::Bool(x < y),
182            (Opcode::Lt, _, _) => Value::Bool(as_f64(&a)? < as_f64(&b)?),
183            (Opcode::Le, Value::Int(x), Value::Int(y)) => Value::Bool(x <= y),
184            (Opcode::Le, _, _) => Value::Bool(as_f64(&a)? <= as_f64(&b)?),
185            _ => unreachable!("numeric_binop called with non-arithmetic opcode {op:?}"),
186        };
187        self.set_reg(dst, result)
188    }
189
190    /// Run at most `budget` instructions (cooperative-preemption quantum,
191    /// design notes §10-11), or until the Flow completes / needs an
192    /// effect the scheduler must perform / faults.
193    ///
194    /// Every exit path is captured by [`VmResult`] — this function itself
195    /// never panics on malformed *verified* bytecode; faults are returned,
196    /// not thrown, so a buggy Flow can't take a worker thread down.
197    pub fn run(&mut self, budget: u32) -> VmResult {
198        for _ in 0..budget {
199            self.instructions_executed += 1;
200            let instr = match self.fetch() {
201                Ok(Some(i)) => i,
202                Ok(None) => {
203                    // Fell off the end of a function: implicit `return Unit`.
204                    match self.pop_frame(Value::Unit) {
205                        Ok(Some(result)) => return result,
206                        Ok(None) => continue,
207                        Err(fault) => return VmResult::Trap(fault),
208                    }
209                }
210                Err(fault) => return VmResult::Trap(fault),
211            };
212
213            macro_rules! trap {
214                ($e:expr) => {
215                    match $e {
216                        Ok(v) => v,
217                        Err(fault) => return VmResult::Trap(fault),
218                    }
219                };
220            }
221
222            match instr.op {
223                Opcode::Halt => {
224                    let v = trap!(self.get_reg(0));
225                    return VmResult::Complete(v);
226                }
227                Opcode::Nop => {}
228                Opcode::LoadConst => {
229                    let idx = instr.imm as u32;
230                    let konst = match self.chunk.constant(idx) {
231                        Some(v) => v.clone(),
232                        None => {
233                            return VmResult::Trap(Fault::BadConstant {
234                                index: idx,
235                                pool_size: self.chunk.constants.len() as u32,
236                            })
237                        }
238                    };
239                    trap!(self.set_reg(instr.a, konst));
240                }
241                Opcode::LoadImm => {
242                    trap!(self.set_reg(instr.a, Value::Int(instr.imm as i64)));
243                }
244                Opcode::Move => {
245                    let v = trap!(self.get_reg(instr.b));
246                    trap!(self.set_reg(instr.a, v));
247                }
248                Opcode::Add | Opcode::Sub | Opcode::Mul | Opcode::Div | Opcode::Mod
249                | Opcode::Eq | Opcode::Lt | Opcode::Le => {
250                    trap!(self.numeric_binop(instr.op, instr.a, instr.b, instr.c));
251                }
252                Opcode::Neg => {
253                    let v = trap!(self.get_reg(instr.b));
254                    let negated = match v {
255                        Value::Int(x) => Value::Int(-x),
256                        Value::Float(x) => Value::Float(-x),
257                        other => {
258                            return VmResult::Trap(Fault::TypeMismatch {
259                                expected: "int or float",
260                                got: other.type_name(),
261                            })
262                        }
263                    };
264                    trap!(self.set_reg(instr.a, negated));
265                }
266                Opcode::Jump => {
267                    let frame = trap!(self.current());
268                    let target = frame.pc as i64 + instr.imm as i64;
269                    frame.pc = target as usize;
270                }
271                Opcode::Branch => {
272                    let cond = trap!(self.get_reg(instr.a));
273                    if !cond.is_truthy() {
274                        let frame = trap!(self.current());
275                        let target = frame.pc as i64 + instr.imm as i64;
276                        frame.pc = target as usize;
277                    }
278                }
279                Opcode::Call => {
280                    let function = instr.imm as u32;
281                    let argc = instr.b;
282                    let dst = instr.a;
283                    if self.frames.len() >= MAX_CALL_DEPTH {
284                        return VmResult::Trap(Fault::CallStackOverflow { depth: self.frames.len() });
285                    }
286                    let def = match self.chunk.function(function) {
287                        Some(d) => d.clone(),
288                        None => {
289                            return VmResult::Trap(Fault::BadFunction {
290                                index: function,
291                                table_size: self.chunk.functions.len() as u32,
292                            })
293                        }
294                    };
295                    let mut args = Vec::with_capacity(argc as usize);
296                    for i in 0..argc {
297                        args.push(trap!(self.get_reg(dst + i)));
298                    }
299                    let mut new_frame = Frame::new(function, def.num_registers, Some(dst));
300                    new_frame.pc = def.entry as usize;
301                    for (i, a) in args.into_iter().enumerate().take(def.arity as usize) {
302                        new_frame.registers[i] = a;
303                    }
304                    self.frames.push(new_frame);
305                }
306                Opcode::CallNative => {
307                    let native_index = instr.imm as u32;
308                    let argc = instr.b;
309                    let dst = instr.a;
310                    let native_fn = match self.natives.get(native_index) {
311                        Some(f) => f.clone(),
312                        None => {
313                            return VmResult::Trap(Fault::BadNative {
314                                index: native_index,
315                                table_size: self.natives.len() as u32,
316                            })
317                        }
318                    };
319                    let mut args = Vec::with_capacity(argc as usize);
320                    for i in 0..argc {
321                        args.push(trap!(self.get_reg(dst + i)));
322                    }
323                    // Runs inline on this worker thread — see
324                    // `NativeFn`'s doc comment on why natives must not
325                    // block. This is the actual FFI boundary (design
326                    // notes §30-31): plain Rust on one side, bytecode
327                    // registers on the other, with `Fault::NativeError`
328                    // as the only channel for a native-side failure to
329                    // become a Flow fault instead of a host panic.
330                    match native_fn(&args) {
331                        Ok(value) => trap!(self.set_reg(dst, value)),
332                        Err(fault) => return VmResult::Trap(fault),
333                    }
334                }
335                Opcode::Return => {
336                    let v = trap!(self.get_reg(instr.a));
337                    match self.pop_frame(v) {
338                        Ok(Some(result)) => return result,
339                        Ok(None) => {}
340                        Err(fault) => return VmResult::Trap(fault),
341                    }
342                }
343                Opcode::Spawn => {
344                    let argc = instr.b;
345                    let mut args = Vec::with_capacity(argc as usize);
346                    for i in 0..argc {
347                        // Args live at r[a+1 .. a+1+argc] — deliberately
348                        // offset from `a` itself, which the scheduler will
349                        // overwrite with a Cap to the child once it exists
350                        // (see Opcode::Spawn / FlowCap).
351                        args.push(trap!(self.get_reg(instr.a + 1 + i)));
352                    }
353                    return VmResult::Spawn { function: instr.imm as u32, args, dest_reg: instr.a };
354                }
355                Opcode::Yield => return VmResult::Yield,
356                Opcode::Sleep => {
357                    let millis = trap!(self.get_reg(instr.a));
358                    let ms = match millis.as_int() {
359                        Some(ms) if ms >= 0 => ms as u64,
360                        _ => {
361                            return VmResult::Trap(Fault::TypeMismatch {
362                                expected: "non-negative int",
363                                got: millis.type_name(),
364                            })
365                        }
366                    };
367                    return VmResult::Sleep(Duration::from_millis(ms));
368                }
369                Opcode::Exit => {
370                    let v = trap!(self.get_reg(instr.a));
371                    return VmResult::Complete(v);
372                }
373                Opcode::SelfPid => {
374                    return VmResult::SelfPid { dest_reg: instr.a };
375                }
376                Opcode::Send => {
377                    let target = trap!(self.get_reg(instr.a));
378                    let message = trap!(self.get_reg(instr.b));
379                    let cap = match target.as_cap() {
380                        Some(c) => c,
381                        None => {
382                            return VmResult::Trap(Fault::TypeMismatch {
383                                expected: "cap",
384                                got: target.type_name(),
385                            })
386                        }
387                    };
388                    if message.as_message().is_none() {
389                        return VmResult::Trap(Fault::TypeMismatch {
390                            expected: "message",
391                            got: message.type_name(),
392                        });
393                    }
394                    return VmResult::Send {
395                        target_cap: cap,
396                        message,
397                    };
398                }
399                Opcode::Receive => {
400                    return VmResult::Receive {
401                        dest_reg: instr.a,
402                        timeout: None,
403                        match_tag: None,
404                    };
405                }
406                Opcode::ReceiveTimeout => {
407                    let millis = trap!(self.get_reg(instr.b));
408                    let ms = millis.as_int().unwrap_or(0).max(0) as u64;
409                    return VmResult::Receive {
410                        dest_reg: instr.a,
411                        timeout: Some(Duration::from_millis(ms)),
412                        match_tag: None,
413                    };
414                }
415                Opcode::ReceiveMatch => {
416                    let tag_v = trap!(self.get_reg(instr.b));
417                    let tag = match tag_from_value(&tag_v) {
418                        Ok(t) => t,
419                        Err(f) => return VmResult::Trap(f),
420                    };
421                    return VmResult::Receive {
422                        dest_reg: instr.a,
423                        timeout: None,
424                        match_tag: Some(tag),
425                    };
426                }
427                Opcode::ReceiveMatchImm => {
428                    let tag = match u16::try_from(instr.imm) {
429                        Ok(t) if instr.imm >= 0 => t,
430                        _ => {
431                            return VmResult::Trap(Fault::TypeMismatch {
432                                expected: "tag u16",
433                                got: "imm-out-of-range",
434                            })
435                        }
436                    };
437                    return VmResult::Receive {
438                        dest_reg: instr.a,
439                        timeout: None,
440                        match_tag: Some(tag),
441                    };
442                }
443                Opcode::Ask => {
444                    let target = trap!(self.get_reg(instr.b));
445                    let request = trap!(self.get_reg(instr.c));
446                    let cap = match target.as_cap() {
447                        Some(c) => c,
448                        None => {
449                            return VmResult::Trap(Fault::TypeMismatch {
450                                expected: "cap",
451                                got: target.type_name(),
452                            })
453                        }
454                    };
455                    if request.as_message().is_none() {
456                        return VmResult::Trap(Fault::TypeMismatch {
457                            expected: "message",
458                            got: request.type_name(),
459                        });
460                    }
461                    return VmResult::Ask {
462                        dest_reg: instr.a,
463                        target_cap: cap,
464                        request,
465                    };
466                }
467                Opcode::Trap => return VmResult::Trap(Fault::Explicit(instr.imm)),
468            }
469        }
470        VmResult::Yield
471    }
472
473    /// Pop the current frame, delivering `value` to the caller (or
474    /// finishing the Flow if this was the outermost frame). Returns
475    /// `Ok(Some(VmResult::Complete(_)))` only in the latter case.
476    fn pop_frame(&mut self, value: Value) -> Result<Option<VmResult>, Fault> {
477        let finished = self
478            .frames
479            .pop()
480            .ok_or(Fault::Invariant("pop_frame on empty stack"))?;
481        match finished.dest_reg {
482            Some(dest) => {
483                // Caller frame still on the stack; ignore an out-of-range
484                // dest (shouldn't happen for verified bytecode emitted by
485                // this crate's own builder, but we don't want to panic on
486                // foreign bytecode) by trapping instead.
487                if self.set_reg(dest, value).is_err() {
488                    return Ok(Some(VmResult::Trap(Fault::RegisterOutOfRange {
489                        reg: dest,
490                        frame_size: self
491                            .frames
492                            .last()
493                            .map(|f| f.registers.len() as u8)
494                            .unwrap_or(0),
495                    })));
496                }
497                Ok(None)
498            }
499            None => Ok(Some(VmResult::Complete(value))),
500        }
501    }
502}
503
504#[inline]
505fn as_f64(v: &Value) -> Result<f64, Fault> {
506    match v {
507        Value::Int(i) => Ok(*i as f64),
508        Value::Float(f) => Ok(*f),
509        other => Err(Fault::TypeMismatch { expected: "int or float", got: other.type_name() }),
510    }
511}
512
513/// Decode a Message tag from a register value (`Int` in `0..=u16::MAX`).
514#[inline]
515fn tag_from_value(v: &Value) -> Result<u16, Fault> {
516    match v.as_int() {
517        Some(i) if (0..=i64::from(u16::MAX)).contains(&i) => Ok(i as u16),
518        Some(_) => Err(Fault::TypeMismatch {
519            expected: "tag u16",
520            got: "int-out-of-range",
521        }),
522        None => Err(Fault::TypeMismatch {
523            expected: "int",
524            got: v.type_name(),
525        }),
526    }
527}