Skip to main content

shape_vm/bytecode/
verifier.rs

1//! Bytecode verifier for trusted opcodes.
2//!
3//! Validates that trusted opcode invariants hold:
4//! - Every trusted opcode appears inside a function with a `FrameDescriptor`
5//! - The FrameDescriptor has no `SlotKind::Unknown` entries for the relevant operands
6
7use super::{BytecodeProgram, OpCode};
8use crate::type_tracking::SlotKind;
9
10/// Errors produced by the bytecode verifier.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum VerifyError {
13    /// A trusted opcode was found in a function that has no FrameDescriptor.
14    MissingFrameDescriptor {
15        function_name: String,
16        opcode: OpCode,
17        instruction_offset: usize,
18    },
19    /// A trusted opcode operand slot has `SlotKind::Unknown` in the FrameDescriptor.
20    UnknownSlotKind {
21        function_name: String,
22        opcode: OpCode,
23        instruction_offset: usize,
24        slot_index: usize,
25    },
26}
27
28impl std::fmt::Display for VerifyError {
29    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30        match self {
31            VerifyError::MissingFrameDescriptor {
32                function_name,
33                opcode,
34                instruction_offset,
35            } => write!(
36                f,
37                "Trusted opcode {:?} at offset {} in function '{}' has no FrameDescriptor",
38                opcode, instruction_offset, function_name
39            ),
40            VerifyError::UnknownSlotKind {
41                function_name,
42                opcode,
43                instruction_offset,
44                slot_index,
45            } => write!(
46                f,
47                "Trusted opcode {:?} at offset {} in function '{}': slot {} has Unknown kind",
48                opcode, instruction_offset, function_name, slot_index
49            ),
50        }
51    }
52}
53
54impl std::error::Error for VerifyError {}
55
56/// Verify that all trusted opcodes in a program have valid FrameDescriptors.
57///
58/// Returns `Ok(())` if all trusted opcodes pass verification, or a list of
59/// all violations found.
60pub fn verify_trusted_opcodes(program: &BytecodeProgram) -> Result<(), Vec<VerifyError>> {
61    let mut errors = Vec::new();
62
63    for func in &program.functions {
64        // Collect instruction offsets that belong to this function.
65        // Functions store their entry_point and instructions run until the next
66        // function or end of program. We scan the instruction stream from
67        // entry_point looking for trusted opcodes.
68        let start = func.entry_point;
69        // Find the end: next function's entry_point or end of instructions
70        let end = program
71            .functions
72            .iter()
73            .filter(|f| f.entry_point > start)
74            .map(|f| f.entry_point)
75            .min()
76            .unwrap_or(program.instructions.len());
77
78        for offset in start..end {
79            let Some(instruction) = program.instructions.get(offset) else {
80                break;
81            };
82            if !instruction.opcode.is_trusted() {
83                continue;
84            }
85
86            // Check FrameDescriptor exists
87            let Some(ref fd) = func.frame_descriptor else {
88                errors.push(VerifyError::MissingFrameDescriptor {
89                    function_name: func.name.clone(),
90                    opcode: instruction.opcode,
91                    instruction_offset: offset,
92                });
93                continue;
94            };
95
96            // Check that the descriptor has at least some non-Unknown slots.
97            // For trusted arithmetic, we don't know which specific stack slots
98            // feed the operands (they come from the stack, not named locals),
99            // so we verify the frame descriptor itself is populated (non-empty
100            // and not all Unknown).
101            if fd.is_empty() || fd.is_all_unknown() {
102                // All slots unknown — the compiler shouldn't have emitted trusted ops
103                for (idx, slot) in fd.slots.iter().enumerate() {
104                    if *slot == SlotKind::Unknown {
105                        errors.push(VerifyError::UnknownSlotKind {
106                            function_name: func.name.clone(),
107                            opcode: instruction.opcode,
108                            instruction_offset: offset,
109                            slot_index: idx,
110                        });
111                        break; // one error per instruction is sufficient
112                    }
113                }
114                // If fd is empty, emit a generic error
115                if fd.is_empty() {
116                    errors.push(VerifyError::UnknownSlotKind {
117                        function_name: func.name.clone(),
118                        opcode: instruction.opcode,
119                        instruction_offset: offset,
120                        slot_index: 0,
121                    });
122                }
123            }
124        }
125    }
126
127    if errors.is_empty() {
128        Ok(())
129    } else {
130        Err(errors)
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137    use crate::bytecode::{Function, Instruction, OpCode};
138    use crate::type_tracking::FrameDescriptor;
139
140    fn make_program(functions: Vec<Function>, instructions: Vec<Instruction>) -> BytecodeProgram {
141        let mut prog = BytecodeProgram::new();
142        prog.functions = functions;
143        prog.instructions = instructions;
144        prog
145    }
146
147    #[test]
148    fn no_trusted_opcodes_passes() {
149        let func = Function {
150            name: "main".to_string(),
151            arity: 0,
152            param_names: vec![],
153            locals_count: 2,
154            entry_point: 0,
155            body_length: 2,
156            is_closure: false,
157            captures_count: 0,
158            is_async: false,
159            ref_params: vec![],
160            ref_mutates: vec![],
161            mutable_captures: vec![],
162            frame_descriptor: None,
163            osr_entry_points: vec![],
164        };
165        let instructions = vec![
166            Instruction::simple(OpCode::AddInt),
167            Instruction::simple(OpCode::ReturnValue),
168        ];
169        let prog = make_program(vec![func], instructions);
170        assert!(verify_trusted_opcodes(&prog).is_ok());
171    }
172
173    #[test]
174    fn trusted_opcode_missing_frame_descriptor() {
175        let func = Function {
176            name: "add_trusted".to_string(),
177            arity: 2,
178            param_names: vec!["a".to_string(), "b".to_string()],
179            locals_count: 2,
180            entry_point: 0,
181            body_length: 2,
182            is_closure: false,
183            captures_count: 0,
184            is_async: false,
185            ref_params: vec![],
186            ref_mutates: vec![],
187            mutable_captures: vec![],
188            frame_descriptor: None,
189            osr_entry_points: vec![],
190        };
191        let instructions = vec![
192            Instruction::simple(OpCode::AddIntTrusted),
193            Instruction::simple(OpCode::ReturnValue),
194        ];
195        let prog = make_program(vec![func], instructions);
196        let errs = verify_trusted_opcodes(&prog).unwrap_err();
197        assert_eq!(errs.len(), 1);
198        assert!(matches!(
199            &errs[0],
200            VerifyError::MissingFrameDescriptor { .. }
201        ));
202    }
203
204    #[test]
205    fn trusted_opcode_with_valid_frame_descriptor() {
206        let func = Function {
207            name: "add_trusted".to_string(),
208            arity: 2,
209            param_names: vec!["a".to_string(), "b".to_string()],
210            locals_count: 2,
211            entry_point: 0,
212            body_length: 2,
213            is_closure: false,
214            captures_count: 0,
215            is_async: false,
216            ref_params: vec![],
217            ref_mutates: vec![],
218            mutable_captures: vec![],
219            frame_descriptor: Some(FrameDescriptor::from_slots(vec![
220                SlotKind::Int64,
221                SlotKind::Int64,
222            ])),
223            osr_entry_points: vec![],
224        };
225        let instructions = vec![
226            Instruction::simple(OpCode::AddIntTrusted),
227            Instruction::simple(OpCode::ReturnValue),
228        ];
229        let prog = make_program(vec![func], instructions);
230        assert!(verify_trusted_opcodes(&prog).is_ok());
231    }
232
233    #[test]
234    fn is_trusted_method() {
235        assert!(OpCode::AddIntTrusted.is_trusted());
236        assert!(OpCode::DivNumberTrusted.is_trusted());
237        assert!(!OpCode::AddInt.is_trusted());
238        assert!(!OpCode::Add.is_trusted());
239    }
240
241    #[test]
242    fn trusted_variant_mapping() {
243        assert_eq!(
244            OpCode::AddInt.trusted_variant(),
245            Some(OpCode::AddIntTrusted)
246        );
247        assert_eq!(
248            OpCode::DivNumber.trusted_variant(),
249            Some(OpCode::DivNumberTrusted)
250        );
251        assert_eq!(OpCode::Add.trusted_variant(), None);
252        assert_eq!(OpCode::AddDecimal.trusted_variant(), None);
253    }
254}