Skip to main content

byteflow/bytecode/
opcode.rs

1//! The Byteflow instruction set architecture (ISA v0).
2//!
3//! Byteflow is register-based (à la Lua 5.x / Dalvik) rather than stack-based
4//! (à la JVM/CPython). Register machines need roughly 40-50% fewer dispatched
5//! instructions than an equivalent stack machine because they avoid PUSH/POP
6//! traffic for every intermediate value, at the cost of slightly larger
7//! instruction words. For an interpreter whose steady-state cost is dominated
8//! by dispatch (branch prediction + icache misses), fewer instructions per
9//! logical operation wins.
10//!
11//! Every opcode fits in a single byte so a `Vec<Instruction>` is dense and
12//! the dispatch table (see `byteflow-vm::interp`) can be a flat jump table
13//! indexed directly by discriminant, with no bounds check in release builds
14//! (enforced instead at decode/verification time, see [`crate::verify`]).
15
16/// A single Byteflow opcode.
17///
18/// Numeric values are part of the stable on-disk ABI (`byteflow-bytecode`
19/// module format, see [`super::chunk::MAGIC`]) — never renumber an existing
20/// variant, only append.
21#[repr(u8)]
22#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
23pub enum Opcode {
24    /// Stop the current Flow's VM loop. Terminal state.
25    Halt = 0x00,
26
27    // ---- data movement ----------------------------------------------------
28    /// `LoadConst ra, kb`  →  `r[a] = constants[b]`
29    LoadConst = 0x01,
30    /// `Move ra, rb`  →  `r[a] = r[b]`
31    Move = 0x02,
32    /// `LoadImm ra, imm`  →  `r[a] = imm as i64` (fast path, skips const pool)
33    LoadImm = 0x03,
34
35    // ---- arithmetic (integer; float variants share the same encoding
36    // ---- but operate on Value::Float, selected by the operand's runtime tag)
37    Add = 0x10,
38    Sub = 0x11,
39    Mul = 0x12,
40    Div = 0x13,
41    Mod = 0x14,
42    Neg = 0x15,
43
44    // ---- comparison → writes a Value::Bool into ra
45    Eq = 0x18,
46    Lt = 0x19,
47    Le = 0x1A,
48
49    // ---- control flow -------------------------------------------------
50    /// `Jump imm` → unconditional relative jump (imm = signed offset in
51    /// instructions from the *next* pc).
52    Jump = 0x20,
53    /// `Branch ra, imm` → jump by `imm` iff `r[a]` is falsy (Bool(false),
54    /// Unit, or Int(0)). This is the only conditional branch; `if/else` and
55    /// loops both lower to Branch + Jump, keeping the interpreter's branch
56    /// predictor state small.
57    Branch = 0x21,
58
59    // ---- procedure calls (native Rust functions registered via FFI, or
60    // ---- other bytecode functions in the same chunk) ------------------
61    /// `Call ra, fb, nc` → call function `fb` with `nc` arguments taken from
62    /// `r[a..a+nc]`, result written back into `r[a]`.
63    Call = 0x30,
64    /// `Return ra` → return `r[a]` to the caller frame (or complete the
65    /// Flow if this is the outermost frame).
66    Return = 0x31,
67    /// `CallNative ra, fb, nc` → like `Call` but `fb` indexes the native
68    /// function table instead of the bytecode function table.
69    CallNative = 0x32,
70
71    // ---- Flow model --------------------------------------------------
72    /// `Spawn ra, fb, nc` → create a new virtual Flow starting at
73    /// function `fb`, passing `nc` arguments taken from `r[a+1..a+1+nc]`
74    /// (deliberately *not* overlapping `r[a]` itself, which is where the
75    /// scheduler writes a **Cap** to the child once created — FlowCap;
76    /// see [`crate::VmResult::Spawn`]).
77    Spawn = 0x40,
78    /// `Yield` → cooperative yield. Control returns to the scheduler, the
79    /// Flow is re-enqueued as `Ready` and may resume on any worker.
80    Yield = 0x41,
81    /// `Sleep ra` → suspend until `r[a]` (interpreted as milliseconds,
82    /// Value::Int) has elapsed. Registered on the timer wheel.
83    Sleep = 0x42,
84    /// `Exit ra` → terminate the Flow, `r[a]` is delivered to `.join()`.
85    Exit = 0x43,
86    /// `SelfPid ra` → `r[a] =` a **self Cap** (`SEND|ASK`) for this flow.
87    /// (Opcode name kept for ABI; the value is `Value::Cap`, not Pid.)
88    /// The VM does not store its own id (it has no scheduler state); this
89    /// is a scheduler effect, same class as `Spawn`/`Receive`.
90    SelfPid = 0x44,
91
92    // ---- messaging --------------------------------------------------------
93    /// `Send ra, rb` → Atomic Hop: deliver `r[b]` (`Message`) to the Cap in
94    /// `r[a]`. Never blocks (mailboxes are unbounded by default).
95    ///
96    /// VM requires Cap + Message; worker resolves Cap (SEND), stamps sender
97    /// + `reply_cap`, then pushes to the resolved mailbox.
98    Send = 0x50,
99    /// `Receive ra` → pop the next message into `r[a]`; if the mailbox is
100    /// empty, suspends the Flow in `Waiting` state until a message
101    /// arrives.
102    Receive = 0x51,
103    /// `ReceiveTimeout ra, rb` → like `Receive` but gives up after `r[b]`
104    /// milliseconds, writing `Value::Unit` into `r[a]` on timeout.
105    ReceiveTimeout = 0x52,
106    /// `ReceiveMatch ra, rb` → **Atomic Hop selective receive**: block until
107    /// a [`crate::Value::Message`] with `tag == r[b]` (as `u16`) is available.
108    /// Non-matching hops stay in the mailbox in FIFO order (skip, don't drop).
109    ReceiveMatch = 0x53,
110    /// `ReceiveMatchImm ra, imm` → like `ReceiveMatch` with an immediate tag
111    /// (`imm` must fit in `u16`).
112    ReceiveMatchImm = 0x54,
113    /// `Ask ra, rb, rc` → **atomic request/reply hop**.
114    ///
115    /// 1. Validate `r[b]` as Cap and `r[c]` as Message (VM).
116    /// 2. Scheduler resolves Cap (ASK), stamps `sender` + mints `reply_cap`.
117    /// 3. Deliver the request to the resolved FlowId (like `Send`).
118    /// 4. Suspend until a reply hop matches
119    ///    `request_id == request.request_id && sender == resolved_FlowId`.
120    /// 5. Write the reply `Message` into `r[a]`.
121    ///
122    /// Append-only ABI slot (`0x55`). No timeout variant in this revision.
123    Ask = 0x55,
124
125    // ---- diagnostics / safety ------------------------------------------
126    /// `Trap imm` → deliberate fault (assertion failure, div-by-zero, bad
127    /// opcode encountered by a corrupt/foreign module, capability
128    /// violation). Propagates to the Flow supervisor as `FlowState::Failed`.
129    Trap = 0x60,
130    /// `Nop` → no-op, used by the assembler to pad jump targets.
131    Nop = 0x61,
132}
133
134impl Opcode {
135    /// Decode a raw byte into an `Opcode`, used when loading foreign/untrusted
136    /// modules. Rejects anything outside the currently defined ISA rather
137    /// than transmuting garbage into a jump-table index (which is exactly
138    /// the class of bug that turns a VM into a code-execution primitive).
139    #[inline]
140    pub fn from_u8(byte: u8) -> Option<Opcode> {
141        use Opcode::*;
142        Some(match byte {
143            0x00 => Halt,
144            0x01 => LoadConst,
145            0x02 => Move,
146            0x03 => LoadImm,
147            0x10 => Add,
148            0x11 => Sub,
149            0x12 => Mul,
150            0x13 => Div,
151            0x14 => Mod,
152            0x15 => Neg,
153            0x18 => Eq,
154            0x19 => Lt,
155            0x1A => Le,
156            0x20 => Jump,
157            0x21 => Branch,
158            0x30 => Call,
159            0x31 => Return,
160            0x32 => CallNative,
161            0x40 => Spawn,
162            0x41 => Yield,
163            0x42 => Sleep,
164            0x43 => Exit,
165            0x44 => SelfPid,
166            0x50 => Send,
167            0x51 => Receive,
168            0x52 => ReceiveTimeout,
169            0x53 => ReceiveMatch,
170            0x54 => ReceiveMatchImm,
171            0x55 => Ask,
172            0x60 => Trap,
173            0x61 => Nop,
174            _ => return None,
175        })
176    }
177
178    #[inline]
179    pub fn as_u8(self) -> u8 {
180        self as u8
181    }
182}
183
184impl std::fmt::Display for Opcode {
185    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
186        let name = match self {
187            Opcode::Halt => "Halt",
188            Opcode::LoadConst => "LoadConst",
189            Opcode::Move => "Move",
190            Opcode::LoadImm => "LoadImm",
191            Opcode::Add => "Add",
192            Opcode::Sub => "Sub",
193            Opcode::Mul => "Mul",
194            Opcode::Div => "Div",
195            Opcode::Mod => "Mod",
196            Opcode::Neg => "Neg",
197            Opcode::Eq => "Eq",
198            Opcode::Lt => "Lt",
199            Opcode::Le => "Le",
200            Opcode::Jump => "Jump",
201            Opcode::Branch => "Branch",
202            Opcode::Call => "Call",
203            Opcode::Return => "Return",
204            Opcode::CallNative => "CallNative",
205            Opcode::Spawn => "Spawn",
206            Opcode::Yield => "Yield",
207            Opcode::Sleep => "Sleep",
208            Opcode::Exit => "Exit",
209            Opcode::SelfPid => "SelfPid",
210            Opcode::Send => "Send",
211            Opcode::Receive => "Receive",
212            Opcode::ReceiveTimeout => "ReceiveTimeout",
213            Opcode::ReceiveMatch => "ReceiveMatch",
214            Opcode::ReceiveMatchImm => "ReceiveMatchImm",
215            Opcode::Ask => "Ask",
216            Opcode::Trap => "Trap",
217            Opcode::Nop => "Nop",
218        };
219        f.write_str(name)
220    }
221}