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