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        use crate::bytecode::Operand;
176        let func = Function {
177            name: "load_trusted".to_string(),
178            arity: 2,
179            param_names: vec!["a".to_string(), "b".to_string()],
180            locals_count: 2,
181            entry_point: 0,
182            body_length: 2,
183            is_closure: false,
184            captures_count: 0,
185            is_async: false,
186            ref_params: vec![],
187            ref_mutates: vec![],
188            mutable_captures: vec![],
189            frame_descriptor: None,
190            osr_entry_points: vec![],
191        };
192        let instructions = vec![
193            Instruction::new(OpCode::LoadLocalTrusted, Some(Operand::Local(0))),
194            Instruction::simple(OpCode::ReturnValue),
195        ];
196        let prog = make_program(vec![func], instructions);
197        let errs = verify_trusted_opcodes(&prog).unwrap_err();
198        assert_eq!(errs.len(), 1);
199        assert!(matches!(
200            &errs[0],
201            VerifyError::MissingFrameDescriptor { .. }
202        ));
203    }
204
205    #[test]
206    fn trusted_opcode_with_valid_frame_descriptor() {
207        use crate::bytecode::Operand;
208        let func = Function {
209            name: "load_trusted".to_string(),
210            arity: 2,
211            param_names: vec!["a".to_string(), "b".to_string()],
212            locals_count: 2,
213            entry_point: 0,
214            body_length: 2,
215            is_closure: false,
216            captures_count: 0,
217            is_async: false,
218            ref_params: vec![],
219            ref_mutates: vec![],
220            mutable_captures: vec![],
221            frame_descriptor: Some(FrameDescriptor::from_slots(vec![
222                SlotKind::Int64,
223                SlotKind::Int64,
224            ])),
225            osr_entry_points: vec![],
226        };
227        let instructions = vec![
228            Instruction::new(OpCode::LoadLocalTrusted, Some(Operand::Local(0))),
229            Instruction::simple(OpCode::ReturnValue),
230        ];
231        let prog = make_program(vec![func], instructions);
232        assert!(verify_trusted_opcodes(&prog).is_ok());
233    }
234
235    #[test]
236    fn is_trusted_method() {
237        assert!(OpCode::LoadLocalTrusted.is_trusted());
238        assert!(OpCode::JumpIfFalseTrusted.is_trusted());
239        assert!(!OpCode::AddInt.is_trusted());
240        assert!(!OpCode::Add.is_trusted());
241    }
242
243    #[test]
244    fn trusted_variant_mapping() {
245        assert_eq!(
246            OpCode::LoadLocal.trusted_variant(),
247            Some(OpCode::LoadLocalTrusted)
248        );
249        assert_eq!(
250            OpCode::JumpIfFalse.trusted_variant(),
251            Some(OpCode::JumpIfFalseTrusted)
252        );
253        assert_eq!(OpCode::Add.trusted_variant(), None);
254        assert_eq!(OpCode::AddInt.trusted_variant(), None);
255    }
256}