Skip to main content

sui_bytecode/
chunk.rs

1//! Bytecode container — holds instructions and a constant pool.
2//!
3//! A `Chunk` is the unit of compiled code. Each Nix expression compiles
4//! to one top-level chunk; lambdas produce nested chunks stored in the
5//! constant pool.
6
7use crate::error::CompileError;
8use crate::intern::Symbol;
9use crate::opcode::OpCode;
10use crate::value::VMValue;
11
12/// A compiled bytecode chunk.
13///
14/// Contains the instruction stream, a constant pool for literal values
15/// and nested function chunks, and source line information for error
16/// reporting.
17#[derive(Clone, Default)]
18pub struct Chunk {
19    /// The raw bytecode instruction stream.
20    pub code: Vec<u8>,
21    /// Constant pool: literals, string keys, nested chunks.
22    pub constants: Vec<VMValue>,
23    /// Source line number for each byte in `code` (1:1 mapping).
24    /// Used for error messages. Line 0 means "unknown".
25    pub lines: Vec<u32>,
26    /// Pre-resolved symbols for constant pool entries that are string keys.
27    ///
28    /// Maps constant index -> interned Symbol. Populated at compile time
29    /// so the VM can skip the `intern()` call on every `GetAttr`/`HasAttr`
30    /// dispatch. Only entries used as attrset keys are populated.
31    pub key_symbols: Vec<Option<Symbol>>,
32    /// The source file this chunk was compiled from (if known).
33    /// Used for error diagnostics — not set for inline expressions.
34    pub source_file: Option<String>,
35}
36
37impl Chunk {
38    /// Create an empty chunk.
39    #[must_use]
40    pub fn new() -> Self {
41        Self {
42            code: Vec::new(),
43            constants: Vec::new(),
44            lines: Vec::new(),
45            key_symbols: Vec::new(),
46            source_file: None,
47        }
48    }
49
50    /// Write a single byte to the instruction stream.
51    pub fn write_byte(&mut self, byte: u8, line: u32) {
52        self.code.push(byte);
53        self.lines.push(line);
54    }
55
56    /// Write an opcode to the instruction stream.
57    pub fn write_op(&mut self, op: OpCode, line: u32) {
58        self.write_byte(op as u8, line);
59    }
60
61    /// Write a u16 operand as two little-endian bytes.
62    pub fn write_u16(&mut self, value: u16, line: u32) {
63        let bytes = value.to_le_bytes();
64        self.write_byte(bytes[0], line);
65        self.write_byte(bytes[1], line);
66    }
67
68    /// Write a u32 operand as four little-endian bytes.
69    pub fn write_u32(&mut self, value: u32, line: u32) {
70        let bytes = value.to_le_bytes();
71        for &b in &bytes {
72            self.write_byte(b, line);
73        }
74    }
75
76    /// Add a constant to the pool and return its index.
77    ///
78    /// Returns an error if the pool exceeds `u16::MAX` entries.
79    pub fn add_constant(&mut self, value: VMValue) -> Result<u16, CompileError> {
80        if self.constants.len() >= u16::MAX as usize {
81            return Err(CompileError::ConstantPoolOverflow);
82        }
83        let idx = self.constants.len() as u16;
84        self.constants.push(value);
85        self.key_symbols.push(None);
86        Ok(idx)
87    }
88
89    /// Add a constant string and its pre-interned symbol to the pool.
90    ///
91    /// The symbol is stored in `key_symbols` so the VM can skip the
92    /// `intern()` call when resolving attribute keys at runtime.
93    pub fn add_key_constant(&mut self, value: VMValue, sym: Symbol) -> Result<u16, CompileError> {
94        if self.constants.len() >= u16::MAX as usize {
95            return Err(CompileError::ConstantPoolOverflow);
96        }
97        let idx = self.constants.len() as u16;
98        self.constants.push(value);
99        self.key_symbols.push(Some(sym));
100        Ok(idx)
101    }
102
103    /// Read a u16 operand from the bytecode at the given offset.
104    ///
105    /// Returns the value and the offset past the two bytes.
106    #[must_use]
107    pub fn read_u16(&self, offset: usize) -> u16 {
108        u16::from_le_bytes([self.code[offset], self.code[offset + 1]])
109    }
110
111    /// Read a u32 operand from the bytecode at the given offset.
112    #[must_use]
113    pub fn read_u32(&self, offset: usize) -> u32 {
114        u32::from_le_bytes([
115            self.code[offset],
116            self.code[offset + 1],
117            self.code[offset + 2],
118            self.code[offset + 3],
119        ])
120    }
121
122    /// Return the current length of the bytecode stream.
123    #[must_use]
124    pub fn len(&self) -> usize {
125        self.code.len()
126    }
127
128    /// Whether the chunk contains no instructions.
129    #[must_use]
130    pub fn is_empty(&self) -> bool {
131        self.code.is_empty()
132    }
133
134    /// Patch a u16 value at the given offset in the bytecode stream.
135    ///
136    /// Used for back-patching jump targets after the target offset is known.
137    pub fn patch_u16(&mut self, offset: usize, value: u16) {
138        let bytes = value.to_le_bytes();
139        self.code[offset] = bytes[0];
140        self.code[offset + 1] = bytes[1];
141    }
142}
143
144impl std::fmt::Debug for Chunk {
145    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
146        writeln!(f, "Chunk ({} bytes, {} constants)", self.code.len(), self.constants.len())?;
147        let mut offset = 0;
148        while offset < self.code.len() {
149            let byte = self.code[offset];
150            let line = self.lines.get(offset).copied().unwrap_or(0);
151            if let Some(op) = OpCode::from_byte(byte) {
152                write!(f, "  {offset:04}  L{line:<4}  {op:?}")?;
153                offset += 1;
154                // Print inline operands for opcodes that have them. The
155                // per-opcode u16-operand count comes from the generated
156                // `OpCode::disasm_operands` classifier (one authored column
157                // in the `opcodes!` table), not a hand-kept bucket list —
158                // so "add an opcode, forget its arity in the disassembler"
159                // is no longer a drift possibility.
160                match op.disasm_operands() {
161                    1 => {
162                        if offset + 1 < self.code.len() {
163                            let operand = self.read_u16(offset);
164                            write!(f, " {operand}")?;
165                            offset += 2;
166                        }
167                    }
168                    // Two u16 operands.
169                    2 => {
170                        if offset + 3 < self.code.len() {
171                            let slot = self.read_u16(offset);
172                            let key = self.read_u16(offset + 2);
173                            write!(f, " slot={slot} key={key}")?;
174                            offset += 4;
175                        }
176                    }
177                    _ => {}
178                }
179                writeln!(f)?;
180            } else {
181                writeln!(f, "  {offset:04}  L{line:<4}  <unknown {byte}>")?;
182                offset += 1;
183            }
184        }
185        Ok(())
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192
193    #[test]
194    fn write_and_read_constant() {
195        let mut chunk = Chunk::new();
196        let idx = chunk.add_constant(VMValue::Int(42)).unwrap();
197        assert_eq!(idx, 0);
198        assert_eq!(chunk.constants[0], VMValue::Int(42));
199    }
200
201    #[test]
202    fn write_and_read_u16() {
203        let mut chunk = Chunk::new();
204        chunk.write_u16(0x1234, 1);
205        assert_eq!(chunk.read_u16(0), 0x1234);
206    }
207
208    #[test]
209    fn patch_u16() {
210        let mut chunk = Chunk::new();
211        // Write a placeholder.
212        chunk.write_u16(0xFFFF, 1);
213        // Patch it.
214        chunk.patch_u16(0, 0x0042);
215        assert_eq!(chunk.read_u16(0), 0x0042);
216    }
217
218    #[test]
219    fn write_op_and_line_tracking() {
220        let mut chunk = Chunk::new();
221        chunk.write_op(OpCode::Null, 5);
222        chunk.write_op(OpCode::Return, 5);
223        assert_eq!(chunk.code.len(), 2);
224        assert_eq!(chunk.lines.len(), 2);
225        assert_eq!(chunk.lines[0], 5);
226        assert_eq!(chunk.lines[1], 5);
227    }
228
229    #[test]
230    fn debug_format() {
231        let mut chunk = Chunk::new();
232        chunk.write_op(OpCode::Null, 1);
233        chunk.write_op(OpCode::Return, 1);
234        let debug = format!("{chunk:?}");
235        assert!(debug.contains("Null"));
236        assert!(debug.contains("Return"));
237    }
238
239    /// Byte-pin the disassembler output across all three operand-arity
240    /// classes (0 / 1 / 2 u16 operands) so the P2 rewire — which drives the
241    /// arity off the generated `OpCode::disasm_operands` classifier instead
242    /// of a hand-kept bucket match — is proven byte-identical to the
243    /// pre-refactor formatter. A single character drift here fails the test.
244    #[test]
245    fn disassembly_output_is_byte_identical() {
246        let mut chunk = Chunk::new();
247        // 1-operand opcode: `Constant 7`.
248        chunk.write_op(OpCode::Constant, 1);
249        chunk.write_u16(7, 1);
250        // 2-operand opcode: `GetLocalAttr slot=3 key=9`.
251        chunk.write_op(OpCode::GetLocalAttr, 1);
252        chunk.write_u16(3, 1);
253        chunk.write_u16(9, 1);
254        // 0-operand opcode: `Return`.
255        chunk.write_op(OpCode::Return, 2);
256
257        let expected = "\
258Chunk (9 bytes, 0 constants)
259  0000  L1     Constant 7
260  0003  L1     GetLocalAttr slot=3 key=9
261  0008  L2     Return
262";
263        assert_eq!(format!("{chunk:?}"), expected);
264    }
265}