sui_bytecode/opcode.rs
1//! Bytecode instruction set for the Nix evaluator.
2//!
3//! A stack-based instruction set: operands are pushed/popped from the
4//! value stack, and inline operands (constant indices, jump offsets,
5//! counts) are encoded as 16-bit values following the opcode byte.
6//!
7//! ## One authored table — the `opcodes!` macro
8//!
9//! The entire instruction set is declared **once** in the `opcodes! { … }`
10//! table below. From that single table the macro generates, by
11//! construction:
12//!
13//! * the `#[repr(u8)]` `OpCode` enum (each variant pinned to its wire byte),
14//! * [`OpCode::from_byte`] — the `u8 -> Option<OpCode>` decoder,
15//! * [`OpCode::to_byte`] — the inverse `OpCode -> u8`,
16//! * [`OpCode::ALL`] — the exhaustive variant list, and
17//! * [`OpCode::disasm_operands`] — the disassembler's u16-operand arity
18//! (0/1/2) for each opcode.
19//!
20//! This kills the drift class the hand-transcribed instruction set carried:
21//! previously the enum, a hand-written `from_byte` match, and a hand-written
22//! roundtrip-test array each restated the byte↔variant map, and the test
23//! array **silently passed** when a new variant was omitted (it only checked
24//! what was listed). Now `OpCode::ALL` and the `disasm_operands` match are
25//! generated from the same table as the enum — a new opcode is a single new
26//! row, and the exhaustive roundtrip test (driven off `ALL`, not a parallel
27//! array) cannot skip it. `disasm_operands` is a `match self { … }` over
28//! every variant, so a missing operand-arity column is a **compile error**.
29
30/// Declare the bytecode instruction set from one table.
31///
32/// Each row is `Variant = <byte>, operands = <n>;`, optionally preceded by
33/// doc comments / attributes (which pass through onto the enum variant).
34/// `<byte>` is the wire byte (`#[repr(u8)]` discriminant); `<n>` is the
35/// number of inline **u16** operands the disassembler prints for that
36/// opcode (0, 1, or 2) — a per-opcode property that was previously implicit
37/// in a hand-written classification match in `chunk.rs`.
38macro_rules! opcodes {
39 (
40 $(
41 $(#[$vmeta:meta])*
42 $name:ident = $byte:literal , operands = $operands:literal ;
43 )*
44 ) => {
45 /// Bytecode instructions for the Nix VM.
46 ///
47 /// Each variant occupies exactly one byte (`#[repr(u8)]`). Inline
48 /// operands (constant pool index, jump offset, element count) follow
49 /// the opcode in the bytecode stream as 16-bit little-endian values.
50 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
51 #[repr(u8)]
52 pub enum OpCode {
53 $(
54 $(#[$vmeta])*
55 $name = $byte,
56 )*
57 }
58
59 impl OpCode {
60 /// Every opcode variant, in declaration order. Generated from the
61 /// same table as the enum, so it can never omit a variant — the
62 /// exhaustive roundtrip test drives off this list.
63 pub const ALL: &'static [OpCode] = &[ $( OpCode::$name ),* ];
64
65 /// Convert a raw byte to an opcode.
66 pub fn from_byte(byte: u8) -> Option<OpCode> {
67 match byte {
68 $( $byte => Some(OpCode::$name), )*
69 _ => None,
70 }
71 }
72
73 /// The wire byte for this opcode (inverse of [`Self::from_byte`]).
74 ///
75 /// Equivalent to `self as u8`; provided as the named paired
76 /// inverse so the round-trip is spelled out at call sites.
77 #[must_use]
78 pub fn to_byte(self) -> u8 {
79 self as u8
80 }
81
82 /// Number of inline **u16** operands the disassembler prints for
83 /// this opcode (0, 1, or 2). This is the arity the `Chunk` Debug
84 /// formatter uses to advance past inline operands; it is an
85 /// exhaustive `match self`, so adding an opcode without an
86 /// operand-arity column is a compile error.
87 #[must_use]
88 pub fn disasm_operands(self) -> u8 {
89 match self {
90 $( OpCode::$name => $operands, )*
91 }
92 }
93 }
94 };
95}
96
97opcodes! {
98 // ── Constants ───────────────────────────────────────────────
99 /// Push a constant from the constant pool.
100 /// Operand: u16 constant index.
101 Constant = 0, operands = 1;
102 /// Push `null`.
103 Null = 1, operands = 0;
104 /// Push `true`.
105 True = 2, operands = 0;
106 /// Push `false`.
107 False = 3, operands = 0;
108
109 // ── Arithmetic ─────────────────────────────────────────────
110 /// Pop two values, push their sum (int+int, float+float, int+float).
111 Add = 10, operands = 0;
112 /// Pop two values, push their difference.
113 Sub = 11, operands = 0;
114 /// Pop two values, push their product.
115 Mul = 12, operands = 0;
116 /// Pop two values, push their quotient. Errors on division by zero.
117 Div = 13, operands = 0;
118 /// Pop one value, push its arithmetic negation.
119 Negate = 14, operands = 0;
120
121 // ── Logical ────────────────────────────────────────────────
122 /// Pop one bool, push its logical negation.
123 Not = 20, operands = 0;
124 /// Pop two bools, push logical AND (short-circuit handled at compile time).
125 And = 21, operands = 0;
126 /// Pop two bools, push logical OR (short-circuit handled at compile time).
127 Or = 22, operands = 0;
128 /// Pop two values, push `a -> b` (logical implication: `!a || b`).
129 Implication = 23, operands = 0;
130
131 // ── Comparison ─────────────────────────────────────────────
132 /// Pop two values, push `true` if equal.
133 Equal = 30, operands = 0;
134 /// Pop two values, push `true` if not equal.
135 NotEqual = 31, operands = 0;
136 /// Pop two values, push `true` if left < right.
137 Less = 32, operands = 0;
138 /// Pop two values, push `true` if left > right.
139 Greater = 33, operands = 0;
140 /// Pop two values, push `true` if left <= right.
141 LessEqual = 34, operands = 0;
142 /// Pop two values, push `true` if left >= right.
143 GreaterEqual = 35, operands = 0;
144
145 // ── Strings ────────────────────────────────────────────────
146 /// Pop N string parts, concatenate into one string.
147 /// Operand: u16 part count.
148 Interpolate = 40, operands = 1;
149
150 // ── Variables ──────────────────────────────────────────────
151 /// Push a local variable by stack slot index.
152 /// Operand: u16 slot index (relative to current frame's stack base).
153 GetLocal = 50, operands = 1;
154 /// Set a local variable by stack slot index.
155 /// Operand: u16 slot index.
156 SetLocal = 51, operands = 1;
157 /// Push an upvalue from the current closure's upvalue array.
158 /// Operand: u8 upvalue index.
159 GetUpvalue = 52, operands = 1;
160 /// Set an upvalue in the current closure's upvalue array.
161 /// Operand: u8 upvalue index.
162 SetUpvalue = 53, operands = 1;
163
164 // ── With scopes ────────────────────────────────────────────
165 /// Push the TOS value onto the with-scope stack.
166 PushWith = 54, operands = 0;
167 /// Pop from the with-scope stack.
168 PopWith = 55, operands = 0;
169 /// Look up a name in the with-scope stack (innermost first).
170 /// Operand: u16 constant index for the variable name string.
171 LookupWith = 56, operands = 1;
172
173 // ── Attribute sets ─────────────────────────────────────────
174 /// Pop N key-value pairs (key on top, value below), construct attrset.
175 /// Operand: u16 pair count.
176 MakeAttrs = 60, operands = 1;
177 /// Pop attrset and key (string constant index), push `attrset.key`.
178 /// Operand: u16 constant index for the key name.
179 GetAttr = 61, operands = 1;
180 /// Pop attrset and key (string constant index), push bool.
181 /// Operand: u16 constant index for the key name.
182 HasAttr = 62, operands = 1;
183 /// Pop two attrsets, push merged result (right overrides left, `//`).
184 UpdateAttrs = 63, operands = 0;
185 /// Pop attrset, key constant, and default value, push value or default.
186 /// Stack order (top to bottom): default, attrset.
187 /// Operand: u16 constant index for the key name.
188 SelectOrDefault = 64, operands = 1;
189 /// Dynamic attribute access: pop string key, pop attrset, push `attrset.key`.
190 /// No inline operand — key comes from the stack at runtime.
191 DynGetAttr = 65, operands = 0;
192 /// Dynamic hasattr: pop string key, pop attrset, push bool.
193 /// No inline operand — key comes from the stack at runtime.
194 DynHasAttr = 66, operands = 0;
195 /// Dynamic select-or-default: pop default, key, attrset; push value or default.
196 /// Stack order (top to bottom): default, key (string), attrset.
197 /// No inline operand — key comes from the stack at runtime.
198 DynSelectOrDefault = 67, operands = 0;
199
200 // ── Lists ──────────────────────────────────────────────────
201 /// Pop N values, construct a list.
202 /// Operand: u16 element count.
203 MakeList = 70, operands = 1;
204 /// Pop two lists, push concatenated result (`++`).
205 Concat = 71, operands = 0;
206
207 // ── Functions ──────────────────────────────────────────────
208 /// Create a closure from a sub-chunk.
209 /// Operand: u16 constant index pointing to the function's `Chunk`.
210 /// Followed by u16 upvalue count, then for each upvalue:
211 /// u8 (1 = local, 0 = upvalue of enclosing), u16 index.
212 MakeClosure = 80, operands = 1;
213 /// Pop function and argument, call the function.
214 Call = 81, operands = 0;
215 /// Return from the current call frame.
216 Return = 82, operands = 0;
217 /// Pop function and argument, tail-call: reuse the current frame.
218 /// Semantically identical to Call but does not grow the call stack.
219 TailCall = 83, operands = 0;
220
221 // ── Control flow ───────────────────────────────────────────
222 /// Unconditional jump.
223 /// Operand: u16 absolute target offset.
224 Jump = 90, operands = 1;
225 /// Pop condition; if false, jump to target.
226 /// Operand: u16 absolute target offset.
227 JumpIfFalse = 91, operands = 1;
228 /// Pop condition; if true, jump to target.
229 /// Operand: u16 absolute target offset.
230 JumpIfTrue = 92, operands = 1;
231
232 // ── Assertions & Throw ──────────────────────────────────────
233 /// Pop condition; if false, raise `AssertionFailed`.
234 Assert = 100, operands = 0;
235 /// Pop a string from stack and raise `Throw(msg)`.
236 /// Used for deferred search path errors caught by tryEval.
237 Throw = 101, operands = 0;
238
239 // ── Stack manipulation ─────────────────────────────────────
240 /// Discard the top of the stack.
241 Pop = 110, operands = 0;
242 /// Duplicate the top of the stack.
243 Dup = 111, operands = 0;
244
245 // ── Superinstructions ─────────────────────────────────────
246 /// Fused `GetLocal` + `GetAttr`: push `stack[base+slot].key`.
247 /// Operands: u16 local slot, u16 key constant index.
248 GetLocalAttr = 120, operands = 2;
249 /// Fused `GetLocal` + `Call`: call `stack[base+slot]` with TOS as arg.
250 /// Operand: u16 local slot.
251 GetLocalCall = 121, operands = 1;
252
253 // ── Builtins ─────────────────────────────────────────────────
254 /// Push the `builtins` attribute set onto the stack.
255 PushBuiltins = 130, operands = 1;
256 /// Call a builtin function by index.
257 /// Operand: u16 builtin index, u16 arg count.
258 CallBuiltin = 131, operands = 2;
259
260 // ── Thunks (lazy evaluation) ─────────────────────────────────
261 /// Create a thunk wrapping a sub-chunk (lazy value).
262 /// Operand: u16 constant index, u16 upvalue count,
263 /// then for each upvalue: u8 is_local, u16 index.
264 MakeThunk = 140, operands = 1;
265 /// Force the top of stack: if it is a thunk, evaluate and replace.
266 Force = 141, operands = 1;
267 /// Patch upvalues of a thunk in a local slot.
268 /// Operand: u16 slot, u16 upvalue count,
269 /// then for each: u8 is_local, u16 index.
270 PatchThunkUpvalues = 142, operands = 0;
271 /// Create a lazy thunk from a source span (deferred compilation).
272 /// Operand: u16 source_constant_idx, u32 offset, u32 length,
273 /// u16 base_dir_constant_idx, u16 upvalue count,
274 /// then for each upvalue: u8 is_local, u16 index.
275 MakeLazyThunk = 143, operands = 0;
276
277 // ── Import ───────────────────────────────────────────────────
278 /// Pop a path from the stack, import the file, push the result.
279 Import = 150, operands = 1;
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285
286 /// The byte value pinned to every opcode variant, as of the pre-macro
287 /// hand-written `#[repr(u8)]` enum. This table is the byte-identical
288 /// oracle: the `opcodes!` table must reproduce every one of these bytes,
289 /// or the VM's bytecode would silently change wire format. Do **not**
290 /// "fix" a mismatch by editing this array — a diff here means a byte
291 /// changed in the instruction set, which breaks every compiled program.
292 const PINNED_BYTES: &[(OpCode, u8)] = &[
293 (OpCode::Constant, 0),
294 (OpCode::Null, 1),
295 (OpCode::True, 2),
296 (OpCode::False, 3),
297 (OpCode::Add, 10),
298 (OpCode::Sub, 11),
299 (OpCode::Mul, 12),
300 (OpCode::Div, 13),
301 (OpCode::Negate, 14),
302 (OpCode::Not, 20),
303 (OpCode::And, 21),
304 (OpCode::Or, 22),
305 (OpCode::Implication, 23),
306 (OpCode::Equal, 30),
307 (OpCode::NotEqual, 31),
308 (OpCode::Less, 32),
309 (OpCode::Greater, 33),
310 (OpCode::LessEqual, 34),
311 (OpCode::GreaterEqual, 35),
312 (OpCode::Interpolate, 40),
313 (OpCode::GetLocal, 50),
314 (OpCode::SetLocal, 51),
315 (OpCode::GetUpvalue, 52),
316 (OpCode::SetUpvalue, 53),
317 (OpCode::PushWith, 54),
318 (OpCode::PopWith, 55),
319 (OpCode::LookupWith, 56),
320 (OpCode::MakeAttrs, 60),
321 (OpCode::GetAttr, 61),
322 (OpCode::HasAttr, 62),
323 (OpCode::UpdateAttrs, 63),
324 (OpCode::SelectOrDefault, 64),
325 (OpCode::DynGetAttr, 65),
326 (OpCode::DynHasAttr, 66),
327 (OpCode::DynSelectOrDefault, 67),
328 (OpCode::MakeList, 70),
329 (OpCode::Concat, 71),
330 (OpCode::MakeClosure, 80),
331 (OpCode::Call, 81),
332 (OpCode::Return, 82),
333 (OpCode::TailCall, 83),
334 (OpCode::Jump, 90),
335 (OpCode::JumpIfFalse, 91),
336 (OpCode::JumpIfTrue, 92),
337 (OpCode::Assert, 100),
338 (OpCode::Throw, 101),
339 (OpCode::Pop, 110),
340 (OpCode::Dup, 111),
341 (OpCode::GetLocalAttr, 120),
342 (OpCode::GetLocalCall, 121),
343 (OpCode::PushBuiltins, 130),
344 (OpCode::CallBuiltin, 131),
345 (OpCode::MakeThunk, 140),
346 (OpCode::Force, 141),
347 (OpCode::PatchThunkUpvalues, 142),
348 (OpCode::MakeLazyThunk, 143),
349 (OpCode::Import, 150),
350 ];
351
352 /// The disassembler u16-operand arity pinned to every opcode, as of the
353 /// pre-macro hand-written classification match in `chunk.rs`. The
354 /// `opcodes!` `operands = N` column must reproduce these exactly, or the
355 /// `Chunk` Debug output changes.
356 const PINNED_OPERANDS: &[(OpCode, u8)] = &[
357 // one printed u16 (advance +2 in the disassembler)
358 (OpCode::Constant, 1),
359 (OpCode::GetLocal, 1),
360 (OpCode::SetLocal, 1),
361 (OpCode::GetUpvalue, 1),
362 (OpCode::SetUpvalue, 1),
363 (OpCode::LookupWith, 1),
364 (OpCode::GetAttr, 1),
365 (OpCode::HasAttr, 1),
366 (OpCode::SelectOrDefault, 1),
367 (OpCode::MakeAttrs, 1),
368 (OpCode::MakeList, 1),
369 (OpCode::Interpolate, 1),
370 (OpCode::MakeClosure, 1),
371 (OpCode::Jump, 1),
372 (OpCode::JumpIfFalse, 1),
373 (OpCode::JumpIfTrue, 1),
374 (OpCode::GetLocalCall, 1),
375 (OpCode::PushBuiltins, 1),
376 (OpCode::MakeThunk, 1),
377 (OpCode::Force, 1),
378 (OpCode::Import, 1),
379 // two printed u16 (advance +4)
380 (OpCode::GetLocalAttr, 2),
381 (OpCode::CallBuiltin, 2),
382 // everything else: zero printed operands (the `_ => {}` arm)
383 (OpCode::Null, 0),
384 (OpCode::True, 0),
385 (OpCode::False, 0),
386 (OpCode::Add, 0),
387 (OpCode::Sub, 0),
388 (OpCode::Mul, 0),
389 (OpCode::Div, 0),
390 (OpCode::Negate, 0),
391 (OpCode::Not, 0),
392 (OpCode::And, 0),
393 (OpCode::Or, 0),
394 (OpCode::Implication, 0),
395 (OpCode::Equal, 0),
396 (OpCode::NotEqual, 0),
397 (OpCode::Less, 0),
398 (OpCode::Greater, 0),
399 (OpCode::LessEqual, 0),
400 (OpCode::GreaterEqual, 0),
401 (OpCode::PushWith, 0),
402 (OpCode::PopWith, 0),
403 (OpCode::UpdateAttrs, 0),
404 (OpCode::DynGetAttr, 0),
405 (OpCode::DynHasAttr, 0),
406 (OpCode::DynSelectOrDefault, 0),
407 (OpCode::Concat, 0),
408 (OpCode::Call, 0),
409 (OpCode::Return, 0),
410 (OpCode::TailCall, 0),
411 (OpCode::Assert, 0),
412 (OpCode::Throw, 0),
413 (OpCode::Pop, 0),
414 (OpCode::Dup, 0),
415 (OpCode::PatchThunkUpvalues, 0),
416 (OpCode::MakeLazyThunk, 0),
417 ];
418
419 /// The generated `OpCode::ALL` covers exactly 57 opcodes — the full
420 /// instruction set. A count check turns "the macro dropped a row" into a
421 /// failing test rather than a silent gap.
422 #[test]
423 fn all_covers_full_instruction_set() {
424 assert_eq!(OpCode::ALL.len(), 57, "OpCode::ALL must list every opcode");
425 }
426
427 /// Every opcode's byte is byte-identical to the pre-macro hand table.
428 #[test]
429 fn opcode_bytes_are_byte_identical() {
430 assert_eq!(
431 PINNED_BYTES.len(),
432 OpCode::ALL.len(),
433 "PINNED_BYTES must pin every opcode"
434 );
435 for &(op, byte) in PINNED_BYTES {
436 assert_eq!(op.to_byte(), byte, "byte for {op:?} changed");
437 assert_eq!(op as u8, byte, "`as u8` for {op:?} changed");
438 assert_eq!(
439 OpCode::from_byte(byte),
440 Some(op),
441 "from_byte({byte}) must decode to {op:?}"
442 );
443 }
444 }
445
446 /// Exhaustive round-trip: every opcode in the generated `ALL` list
447 /// decodes back to itself. Because `ALL` is generated from the same
448 /// table as the enum (not a parallel hand-kept array), a new variant
449 /// cannot be omitted here — closing the silent-pass hole the old
450 /// `roundtrip_all_opcodes` array had.
451 #[test]
452 fn roundtrip_all_opcodes() {
453 for &op in OpCode::ALL {
454 let byte = op.to_byte();
455 let decoded = OpCode::from_byte(byte)
456 .unwrap_or_else(|| panic!("failed to decode opcode byte {byte} for {op:?}"));
457 assert_eq!(decoded, op, "roundtrip failed for {op:?}");
458 }
459 }
460
461 /// The disassembler operand-arity column is byte-identical to the
462 /// pre-macro `chunk.rs` classification (0/1/2 printed u16 operands).
463 #[test]
464 fn disasm_operands_are_byte_identical() {
465 assert_eq!(
466 PINNED_OPERANDS.len(),
467 OpCode::ALL.len(),
468 "PINNED_OPERANDS must pin every opcode"
469 );
470 for &(op, ar) in PINNED_OPERANDS {
471 assert_eq!(op.disasm_operands(), ar, "operand arity for {op:?} changed");
472 }
473 }
474
475 #[test]
476 fn invalid_byte_returns_none() {
477 assert!(OpCode::from_byte(255).is_none());
478 assert!(OpCode::from_byte(200).is_none());
479 assert!(OpCode::from_byte(5).is_none());
480 }
481}