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}