1use super::chunk::Chunk;
2use super::opcode::Opcode;
3use std::fmt;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
15pub enum VerifyError {
16 UnknownOpcode { at: usize, byte: u8 },
17 ConstOutOfRange { at: usize, index: u32, len: usize },
18 FunctionOutOfRange { at: usize, index: u32, len: usize },
19 JumpOutOfRange { at: usize, target: i64, len: usize },
20 EmptyFunctionTable,
21 EntryOutOfRange { function: usize, entry: u32, len: usize },
22 ArityExceedsRegisters {
29 function: usize,
30 arity: u8,
31 num_registers: u8,
32 },
33}
34
35impl fmt::Display for VerifyError {
36 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37 match self {
38 VerifyError::UnknownOpcode { at, byte } => {
39 write!(f, "unknown opcode 0x{byte:02X} at instruction {at}")
40 }
41 VerifyError::ConstOutOfRange { at, index, len } => write!(
42 f,
43 "instruction {at} references constant {index}, pool has {len} entries"
44 ),
45 VerifyError::FunctionOutOfRange { at, index, len } => write!(
46 f,
47 "instruction {at} references function {index}, table has {len} entries"
48 ),
49 VerifyError::JumpOutOfRange { at, target, len } => write!(
50 f,
51 "instruction {at} jumps to {target}, out of code bounds (len={len})"
52 ),
53 VerifyError::EmptyFunctionTable => write!(f, "chunk has no entry function"),
54 VerifyError::EntryOutOfRange { function, entry, len } => write!(
55 f,
56 "function {function} entry point {entry} is out of code bounds (len={len})"
57 ),
58 VerifyError::ArityExceedsRegisters { function, arity, num_registers } => write!(
59 f,
60 "function {function} declares arity {arity} but only {num_registers} registers"
61 ),
62 }
63 }
64}
65
66impl std::error::Error for VerifyError {}
67
68pub fn verify(chunk: &Chunk) -> Result<(), VerifyError> {
87 if chunk.functions.is_empty() {
88 return Err(VerifyError::EmptyFunctionTable);
89 }
90
91 let len = chunk.code.len();
92
93 for (function, def) in chunk.functions.iter().enumerate() {
97 if def.entry as usize >= len {
98 return Err(VerifyError::EntryOutOfRange {
99 function,
100 entry: def.entry,
101 len,
102 });
103 }
104 if def.arity > def.num_registers {
105 return Err(VerifyError::ArityExceedsRegisters {
106 function,
107 arity: def.arity,
108 num_registers: def.num_registers,
109 });
110 }
111 }
112
113 for (at, instr) in chunk.code.iter().enumerate() {
114 match instr.op {
115 Opcode::LoadConst => {
116 let idx = instr.imm as u32;
117 if idx as usize >= chunk.constants.len() {
118 return Err(VerifyError::ConstOutOfRange {
119 at,
120 index: idx,
121 len: chunk.constants.len(),
122 });
123 }
124 }
125 Opcode::Spawn | Opcode::Call => {
126 let idx = instr.imm as u32;
127 if idx as usize >= chunk.functions.len() {
128 return Err(VerifyError::FunctionOutOfRange {
129 at,
130 index: idx,
131 len: chunk.functions.len(),
132 });
133 }
134 }
135 Opcode::CallNative => {}
140 Opcode::Jump | Opcode::Branch => {
141 let target = at as i64 + 1 + instr.imm as i64;
142 if target < 0 || target as usize > len {
143 return Err(VerifyError::JumpOutOfRange { at, target, len });
146 }
147 }
148 _ => {}
149 }
150 }
151
152 Ok(())
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158 use crate::bytecode::builder::ChunkBuilder;
159 use crate::bytecode::opcode::Opcode;
160
161
162 #[test]
163 fn rejects_empty_function_table() {
164 let chunk = Chunk::default();
165 assert_eq!(verify(&chunk), Err(VerifyError::EmptyFunctionTable));
166 }
167
168 #[test]
169 fn accepts_well_formed_chunk() {
170 let mut b = ChunkBuilder::new("test");
171 b.begin_function("main", 0, 2);
172 let k = b.const_(crate::bytecode::value::Value::Int(41));
173 b.emit_load_const(0, k);
174 b.emit_load_imm(1, 1);
175 b.emit_binop(Opcode::Add, 0, 0, 1);
176 b.emit_return(0);
177 let chunk = b.finish();
178 assert!(verify(&chunk).is_ok());
179 }
180
181 #[test]
185 fn rejects_arity_larger_than_the_register_file() {
186 let mut b = ChunkBuilder::new("test");
187 b.begin_function("main", 3, 1);
188 b.emit_return(0);
189 let chunk = b.finish();
190 assert_eq!(
191 verify(&chunk),
192 Err(VerifyError::ArityExceedsRegisters {
193 function: 0,
194 arity: 3,
195 num_registers: 1,
196 })
197 );
198 }
199
200 #[test]
201 fn accepts_arity_equal_to_the_register_file() {
202 let mut b = ChunkBuilder::new("test");
203 b.begin_function("main", 2, 2);
204 b.emit_return(0);
205 let chunk = b.finish();
206 assert!(verify(&chunk).is_ok());
207 }
208
209 #[test]
210 fn rejects_out_of_range_jump() {
211 use crate::bytecode::instruction::Instruction;
212 let mut chunk = Chunk::default();
213 chunk.functions.push(crate::bytecode::chunk::FunctionDef {
214 name: "main".into(),
215 entry: 0,
216 arity: 0,
217 num_registers: 1,
218 });
219 chunk.code.push(Instruction::only_imm(Opcode::Jump, 999));
220 assert!(matches!(verify(&chunk), Err(VerifyError::JumpOutOfRange { .. })));
221 }
222}