Skip to main content

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}