byteflow/bytecode/
verify.rs1use 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}
23
24impl fmt::Display for VerifyError {
25 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26 match self {
27 VerifyError::UnknownOpcode { at, byte } => {
28 write!(f, "unknown opcode 0x{byte:02X} at instruction {at}")
29 }
30 VerifyError::ConstOutOfRange { at, index, len } => write!(
31 f,
32 "instruction {at} references constant {index}, pool has {len} entries"
33 ),
34 VerifyError::FunctionOutOfRange { at, index, len } => write!(
35 f,
36 "instruction {at} references function {index}, table has {len} entries"
37 ),
38 VerifyError::JumpOutOfRange { at, target, len } => write!(
39 f,
40 "instruction {at} jumps to {target}, out of code bounds (len={len})"
41 ),
42 VerifyError::EmptyFunctionTable => write!(f, "chunk has no entry function"),
43 VerifyError::EntryOutOfRange { function, entry, len } => write!(
44 f,
45 "function {function} entry point {entry} is out of code bounds (len={len})"
46 ),
47 }
48 }
49}
50
51impl std::error::Error for VerifyError {}
52
53pub fn verify(chunk: &Chunk) -> Result<(), VerifyError> {
66 if chunk.functions.is_empty() {
67 return Err(VerifyError::EmptyFunctionTable);
68 }
69
70 let len = chunk.code.len();
71
72 for def in &chunk.functions {
73 if def.entry as usize >= len {
74 return Err(VerifyError::EntryOutOfRange {
75 function: chunk.functions.iter().position(|f| f.name == def.name).unwrap_or(0),
76 entry: def.entry,
77 len,
78 });
79 }
80 }
81
82 for (at, instr) in chunk.code.iter().enumerate() {
83 match instr.op {
84 Opcode::LoadConst => {
85 let idx = instr.imm as u32;
86 if idx as usize >= chunk.constants.len() {
87 return Err(VerifyError::ConstOutOfRange {
88 at,
89 index: idx,
90 len: chunk.constants.len(),
91 });
92 }
93 }
94 Opcode::Spawn | Opcode::Call => {
95 let idx = instr.imm as u32;
96 if idx as usize >= chunk.functions.len() {
97 return Err(VerifyError::FunctionOutOfRange {
98 at,
99 index: idx,
100 len: chunk.functions.len(),
101 });
102 }
103 }
104 Opcode::CallNative => {}
109 Opcode::Jump | Opcode::Branch => {
110 let target = at as i64 + 1 + instr.imm as i64;
111 if target < 0 || target as usize > len {
112 return Err(VerifyError::JumpOutOfRange { at, target, len });
115 }
116 }
117 _ => {}
118 }
119 }
120
121 Ok(())
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127 use crate::bytecode::builder::ChunkBuilder;
128 use crate::bytecode::opcode::Opcode;
129
130
131 #[test]
132 fn rejects_empty_function_table() {
133 let chunk = Chunk::default();
134 assert_eq!(verify(&chunk), Err(VerifyError::EmptyFunctionTable));
135 }
136
137 #[test]
138 fn accepts_well_formed_chunk() {
139 let mut b = ChunkBuilder::new("test");
140 b.begin_function("main", 0, 2);
141 let k = b.const_(crate::bytecode::value::Value::Int(41));
142 b.emit_load_const(0, k);
143 b.emit_load_imm(1, 1);
144 b.emit_binop(Opcode::Add, 0, 0, 1);
145 b.emit_return(0);
146 let chunk = b.finish();
147 assert!(verify(&chunk).is_ok());
148 }
149
150 #[test]
151 fn rejects_out_of_range_jump() {
152 use crate::bytecode::instruction::Instruction;
153 let mut chunk = Chunk::default();
154 chunk.functions.push(crate::bytecode::chunk::FunctionDef {
155 name: "main".into(),
156 entry: 0,
157 arity: 0,
158 num_registers: 1,
159 });
160 chunk.code.push(Instruction::only_imm(Opcode::Jump, 999));
161 assert!(matches!(verify(&chunk), Err(VerifyError::JumpOutOfRange { .. })));
162 }
163}