Skip to main content

shape_vm/bytecode/
verifier.rs

1//! Bytecode verifier for trusted and v2 typed opcodes.
2//!
3//! Validates that trusted opcode invariants hold:
4//! - Every trusted opcode carries the operand shape its handler requires
5//!   (`LoadLocalTrusted` → `Operand::Local`, `JumpIfFalseTrusted` →
6//!   `Operand::Offset`).
7//!
8//! Also validates v2 typed opcode invariants:
9//! - Typed array ops require a FrameDescriptor with non-Unknown slots
10//! - Typed field ops have FieldOffset operands with reasonable byte offsets
11//! - Sized integer (i32) ops require a FrameDescriptor with non-Unknown slots
12//!
13//! ## WS-10b — stale `MissingFrameDescriptor` rule removed (2026-05-22)
14//!
15//! `verify_trusted_opcodes` previously errored `MissingFrameDescriptor` /
16//! `UnknownSlotKind` for any trusted opcode in a function whose
17//! `Function.frame_descriptor` was `None` / empty. That rule encoded the
18//! **pre-ADR-006 §2.7.7 trusted-opcode contract**, when trusted opcodes
19//! skipped runtime tag-bit validation and relied on descriptor-supplied
20//! slot-kind metadata to justify the skip.
21//!
22//! Post-§2.7.7 only two trusted opcodes survive — `LoadLocalTrusted`
23//! (0xD7) and `JumpIfFalseTrusted` (0xD8). Their executors
24//! (`executor/variables/mod.rs::op_load_local_trusted`,
25//! `executor/control_flow/mod.rs::op_jump_if_false_trusted`) source slot
26//! kind from the §2.7.7 stack parallel-`Vec<NativeKind>` track, NOT the
27//! `FrameDescriptor`. `LoadLocalTrusted` is byte-for-byte identical to
28//! non-trusted `LoadLocal`; `JumpIfFalseTrusted` pops the kinded
29//! condition slot directly. `current_frame_descriptor()` has zero VM
30//! executor call sites — the descriptor is consumed at runtime only by
31//! the JIT, which already has an explicit absent-descriptor fallback
32//! (`shape-jit::worker.rs`, `mir_compiler/v2_call_abi.rs`).
33//!
34//! The stale rule fired 16 false positives on every program run (stdlib
35//! prelude functions with an unannotated / `any`-typed local whose whole
36//! frame the storage-hint pass could not prove). It enforced nothing —
37//! `load_program` only `eprintln!`'d — but printed "Bytecode verification
38//! failed" on a clean prelude. The rule is dropped; `verify_trusted_opcodes`
39//! now verifies the still-meaningful invariant (operand shape). The
40//! `verify_v2_typed_opcodes` pass — which checks real v2 invariants — is
41//! unchanged and keeps its enforcement structure.
42
43use super::{BytecodeProgram, OpCode, Operand};
44
45/// Errors produced by the bytecode verifier.
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub enum VerifyError {
48    /// A trusted opcode carries the wrong operand shape for its handler
49    /// (e.g. `LoadLocalTrusted` without an `Operand::Local`).
50    TrustedOpcodeBadOperand {
51        function_name: String,
52        opcode: OpCode,
53        instruction_offset: usize,
54    },
55    /// A v2 typed opcode was found in a function without a FrameDescriptor.
56    V2MissingFrameDescriptor {
57        function_name: String,
58        opcode: OpCode,
59        instruction_offset: usize,
60    },
61    /// A v2 typed field opcode has an unreasonable byte offset (> 4096).
62    V2FieldOffsetTooLarge {
63        function_name: String,
64        opcode: OpCode,
65        instruction_offset: usize,
66        offset: u16,
67    },
68    /// A v2 typed field opcode is missing its FieldOffset operand.
69    V2MissingFieldOffset {
70        function_name: String,
71        opcode: OpCode,
72        instruction_offset: usize,
73    },
74}
75
76impl std::fmt::Display for VerifyError {
77    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78        match self {
79            VerifyError::TrustedOpcodeBadOperand {
80                function_name,
81                opcode,
82                instruction_offset,
83            } => write!(
84                f,
85                "Trusted opcode {:?} at offset {} in function '{}' has the wrong operand shape",
86                opcode, instruction_offset, function_name
87            ),
88            VerifyError::V2MissingFrameDescriptor {
89                function_name,
90                opcode,
91                instruction_offset,
92            } => write!(
93                f,
94                "V2 typed opcode {:?} at offset {} in function '{}' has no FrameDescriptor",
95                opcode, instruction_offset, function_name
96            ),
97            VerifyError::V2FieldOffsetTooLarge {
98                function_name,
99                opcode,
100                instruction_offset,
101                offset,
102            } => write!(
103                f,
104                "V2 field opcode {:?} at offset {} in function '{}': byte offset {} exceeds maximum (4096)",
105                opcode, instruction_offset, function_name, offset
106            ),
107            VerifyError::V2MissingFieldOffset {
108                function_name,
109                opcode,
110                instruction_offset,
111            } => write!(
112                f,
113                "V2 field opcode {:?} at offset {} in function '{}': missing FieldOffset operand",
114                opcode, instruction_offset, function_name
115            ),
116        }
117    }
118}
119
120impl std::error::Error for VerifyError {}
121
122/// Verify that all trusted opcodes in a program are well-formed.
123///
124/// Post-ADR-006 §2.7.7 the two surviving trusted opcodes (`LoadLocalTrusted`,
125/// `JumpIfFalseTrusted`) source slot kind from the stack
126/// parallel-`Vec<NativeKind>` track, not the `FrameDescriptor` — see the
127/// module doc comment (`WS-10b`). The descriptor-presence rule that used to
128/// live here was stale (it fired 16 false positives on a clean stdlib
129/// prelude) and is removed. The verifier still checks the invariant that is
130/// still real: each trusted opcode carries the operand shape its executor
131/// requires, so a future malformed trusted opcode still surfaces.
132///
133/// Returns `Ok(())` if all trusted opcodes pass verification, or a list of
134/// all violations found.
135pub fn verify_trusted_opcodes(program: &BytecodeProgram) -> Result<(), Vec<VerifyError>> {
136    let mut errors = Vec::new();
137
138    for func in &program.functions {
139        // Collect instruction offsets that belong to this function.
140        // Functions store their entry_point and instructions run until the next
141        // function or end of program. We scan the instruction stream from
142        // entry_point looking for trusted opcodes.
143        let start = func.entry_point;
144        // Find the end: next function's entry_point or end of instructions
145        let end = program
146            .functions
147            .iter()
148            .filter(|f| f.entry_point > start)
149            .map(|f| f.entry_point)
150            .min()
151            .unwrap_or(program.instructions.len());
152
153        for offset in start..end {
154            let Some(instruction) = program.instructions.get(offset) else {
155                break;
156            };
157            if !instruction.opcode.is_trusted() {
158                continue;
159            }
160
161            // Operand-shape check — the still-meaningful trusted-opcode
162            // invariant. `LoadLocalTrusted` indexes a local; `JumpIfFalseTrusted`
163            // branches by a signed offset. A mismatch is a real malformed-bytecode
164            // bug the executor would reject with `VMError::InvalidOperand` at
165            // runtime; surface it statically.
166            let operand_ok = match instruction.opcode {
167                OpCode::LoadLocalTrusted => {
168                    matches!(instruction.operand, Some(Operand::Local(_)))
169                }
170                OpCode::JumpIfFalseTrusted => {
171                    matches!(instruction.operand, Some(Operand::Offset(_)))
172                }
173                // Any future trusted opcode without an operand-shape rule
174                // is conservatively accepted here; add its rule alongside
175                // its executor.
176                _ => true,
177            };
178            if !operand_ok {
179                errors.push(VerifyError::TrustedOpcodeBadOperand {
180                    function_name: func.name.clone(),
181                    opcode: instruction.opcode,
182                    instruction_offset: offset,
183                });
184            }
185        }
186    }
187
188    if errors.is_empty() {
189        Ok(())
190    } else {
191        Err(errors)
192    }
193}
194
195/// Maximum reasonable byte offset for v2 typed field access.
196/// Structs larger than 4096 bytes are unlikely and probably indicate a bug.
197const MAX_FIELD_OFFSET: u16 = 4096;
198
199/// Returns true if the opcode is a v2 typed field load/store that requires a FieldOffset operand.
200fn is_v2_field_op(op: OpCode) -> bool {
201    matches!(
202        op,
203        OpCode::FieldLoadF64
204            | OpCode::FieldLoadI64
205            | OpCode::FieldLoadI32
206            | OpCode::FieldLoadBool
207            | OpCode::FieldLoadPtr
208            | OpCode::FieldStoreF64
209            | OpCode::FieldStoreI64
210            | OpCode::FieldStoreI32
211    )
212}
213
214/// Verify that all v2 typed opcodes have valid invariants.
215///
216/// Checks:
217/// - Typed array ops, field ops, and i32 arithmetic appear in functions with FrameDescriptors
218/// - Field load/store ops have a FieldOffset operand with a reasonable byte offset (<= 4096)
219///
220/// Returns `Ok(())` if all v2 typed opcodes pass, or a list of all violations.
221pub fn verify_v2_typed_opcodes(program: &BytecodeProgram) -> Result<(), Vec<VerifyError>> {
222    let mut errors = Vec::new();
223
224    for func in &program.functions {
225        let start = func.entry_point;
226        let end = program
227            .functions
228            .iter()
229            .filter(|f| f.entry_point > start)
230            .map(|f| f.entry_point)
231            .min()
232            .unwrap_or(program.instructions.len());
233
234        for offset in start..end {
235            let Some(instruction) = program.instructions.get(offset) else {
236                break;
237            };
238            if !instruction.opcode.is_v2_typed() {
239                continue;
240            }
241
242            // All v2 typed opcodes require a FrameDescriptor
243            if func.frame_descriptor.is_none() {
244                errors.push(VerifyError::V2MissingFrameDescriptor {
245                    function_name: func.name.clone(),
246                    opcode: instruction.opcode,
247                    instruction_offset: offset,
248                });
249                continue;
250            }
251
252            // Field load/store ops: validate FieldOffset operand
253            if is_v2_field_op(instruction.opcode) {
254                match &instruction.operand {
255                    Some(Operand::FieldOffset(off)) => {
256                        if *off > MAX_FIELD_OFFSET {
257                            errors.push(VerifyError::V2FieldOffsetTooLarge {
258                                function_name: func.name.clone(),
259                                opcode: instruction.opcode,
260                                instruction_offset: offset,
261                                offset: *off,
262                            });
263                        }
264                    }
265                    _ => {
266                        errors.push(VerifyError::V2MissingFieldOffset {
267                            function_name: func.name.clone(),
268                            opcode: instruction.opcode,
269                            instruction_offset: offset,
270                        });
271                    }
272                }
273            }
274        }
275    }
276
277    if errors.is_empty() {
278        Ok(())
279    } else {
280        Err(errors)
281    }
282}
283
284#[cfg(test)]
285mod tests {
286    use super::*;
287    use crate::bytecode::{Function, Instruction, OpCode};
288    use crate::type_tracking::{FrameDescriptor, NativeKind};
289
290    fn make_program(functions: Vec<Function>, instructions: Vec<Instruction>) -> BytecodeProgram {
291        let mut prog = BytecodeProgram::new();
292        prog.functions = functions;
293        prog.instructions = instructions;
294        prog
295    }
296
297    #[test]
298    fn no_trusted_opcodes_passes() {
299        let func = Function {
300            name: "main".to_string(),
301            arity: 0,
302            param_names: vec![],
303            locals_count: 2,
304            entry_point: 0,
305            body_length: 2,
306            is_closure: false,
307            captures_count: 0,
308            is_async: false,
309            ref_params: vec![],
310            ref_mutates: vec![],
311            mutable_captures: vec![],
312            frame_descriptor: None,
313            osr_entry_points: vec![],
314            mir_data: None,
315        };
316        let instructions = vec![
317            Instruction::simple(OpCode::AddInt),
318            Instruction::simple(OpCode::ReturnValue),
319        ];
320        let prog = make_program(vec![func], instructions);
321        assert!(verify_trusted_opcodes(&prog).is_ok());
322    }
323
324    /// WS-10b: post-ADR-006 §2.7.7 a trusted opcode in a function with NO
325    /// `FrameDescriptor` is NOT a violation — the surviving trusted opcodes
326    /// (`LoadLocalTrusted`, `JumpIfFalseTrusted`) source slot kind from the
327    /// §2.7.7 stack parallel-`NativeKind` track, not the descriptor. The
328    /// stale `MissingFrameDescriptor` rule that fired 16 false positives on
329    /// every program run is removed. This is the regression guard for that
330    /// removal: well-formed trusted opcodes with `frame_descriptor: None`
331    /// pass clean.
332    #[test]
333    fn trusted_opcode_no_frame_descriptor_is_not_a_violation() {
334        use crate::bytecode::Operand;
335        let func = Function {
336            name: "load_trusted".to_string(),
337            arity: 2,
338            param_names: vec!["a".to_string(), "b".to_string()],
339            locals_count: 2,
340            entry_point: 0,
341            body_length: 3,
342            is_closure: false,
343            captures_count: 0,
344            is_async: false,
345            ref_params: vec![],
346            ref_mutates: vec![],
347            mutable_captures: vec![],
348            frame_descriptor: None,
349            osr_entry_points: vec![],
350            mir_data: None,
351        };
352        let instructions = vec![
353            Instruction::new(OpCode::LoadLocalTrusted, Some(Operand::Local(0))),
354            Instruction::new(OpCode::JumpIfFalseTrusted, Some(Operand::Offset(1))),
355            Instruction::simple(OpCode::ReturnValue),
356        ];
357        let prog = make_program(vec![func], instructions);
358        assert!(
359            verify_trusted_opcodes(&prog).is_ok(),
360            "trusted opcodes with no FrameDescriptor must pass (WS-10b stale-rule removal)"
361        );
362    }
363
364    /// WS-10b: the still-meaningful trusted-opcode invariant — operand shape.
365    /// `LoadLocalTrusted` must carry an `Operand::Local`; a wrong operand
366    /// shape is a real malformed-bytecode bug and is still surfaced.
367    #[test]
368    fn trusted_opcode_bad_operand_is_a_violation() {
369        use crate::bytecode::Operand;
370        let func = Function {
371            name: "bad_load".to_string(),
372            arity: 0,
373            param_names: vec![],
374            locals_count: 1,
375            entry_point: 0,
376            body_length: 2,
377            is_closure: false,
378            captures_count: 0,
379            is_async: false,
380            ref_params: vec![],
381            ref_mutates: vec![],
382            mutable_captures: vec![],
383            frame_descriptor: Some(FrameDescriptor::from_slots(vec![NativeKind::Int64])),
384            osr_entry_points: vec![],
385            mir_data: None,
386        };
387        // LoadLocalTrusted carrying an Offset operand instead of Local — malformed.
388        let instructions = vec![
389            Instruction::new(OpCode::LoadLocalTrusted, Some(Operand::Offset(3))),
390            Instruction::simple(OpCode::ReturnValue),
391        ];
392        let prog = make_program(vec![func], instructions);
393        let errs = verify_trusted_opcodes(&prog).unwrap_err();
394        assert_eq!(errs.len(), 1);
395        assert!(matches!(
396            &errs[0],
397            VerifyError::TrustedOpcodeBadOperand { .. }
398        ));
399    }
400
401    #[test]
402    fn trusted_opcode_with_valid_operand_passes() {
403        use crate::bytecode::Operand;
404        let func = Function {
405            name: "load_trusted".to_string(),
406            arity: 2,
407            param_names: vec!["a".to_string(), "b".to_string()],
408            locals_count: 2,
409            entry_point: 0,
410            body_length: 2,
411            is_closure: false,
412            captures_count: 0,
413            is_async: false,
414            ref_params: vec![],
415            ref_mutates: vec![],
416            mutable_captures: vec![],
417            frame_descriptor: Some(FrameDescriptor::from_slots(vec![
418                NativeKind::Int64,
419                NativeKind::Int64,
420            ])),
421            osr_entry_points: vec![],
422            mir_data: None,
423        };
424        let instructions = vec![
425            Instruction::new(OpCode::LoadLocalTrusted, Some(Operand::Local(0))),
426            Instruction::simple(OpCode::ReturnValue),
427        ];
428        let prog = make_program(vec![func], instructions);
429        assert!(verify_trusted_opcodes(&prog).is_ok());
430    }
431
432    #[test]
433    fn is_trusted_method() {
434        assert!(OpCode::LoadLocalTrusted.is_trusted());
435        assert!(OpCode::JumpIfFalseTrusted.is_trusted());
436        assert!(!OpCode::AddInt.is_trusted());
437        assert!(!OpCode::Halt.is_trusted());
438    }
439
440    #[test]
441    fn trusted_variant_mapping() {
442        assert_eq!(
443            OpCode::LoadLocal.trusted_variant(),
444            Some(OpCode::LoadLocalTrusted)
445        );
446        assert_eq!(
447            OpCode::JumpIfFalse.trusted_variant(),
448            Some(OpCode::JumpIfFalseTrusted)
449        );
450        assert_eq!(OpCode::Halt.trusted_variant(), None);
451        assert_eq!(OpCode::AddInt.trusted_variant(), None);
452    }
453
454    // ===== v2 typed opcode verification tests =====
455
456    #[test]
457    fn v2_no_typed_opcodes_passes() {
458        let func = Function {
459            name: "main".to_string(),
460            arity: 0,
461            param_names: vec![],
462            locals_count: 2,
463            entry_point: 0,
464            body_length: 2,
465            is_closure: false,
466            captures_count: 0,
467            is_async: false,
468            ref_params: vec![],
469            ref_mutates: vec![],
470            mutable_captures: vec![],
471            frame_descriptor: None,
472            osr_entry_points: vec![],
473            mir_data: None,
474        };
475        let instructions = vec![
476            Instruction::simple(OpCode::PushNull),
477            Instruction::simple(OpCode::ReturnValue),
478        ];
479        let prog = make_program(vec![func], instructions);
480        assert!(verify_v2_typed_opcodes(&prog).is_ok());
481    }
482
483    #[test]
484    fn v2_typed_array_op_missing_frame_descriptor() {
485        let func = Function {
486            name: "array_fn".to_string(),
487            arity: 0,
488            param_names: vec![],
489            locals_count: 1,
490            entry_point: 0,
491            body_length: 2,
492            is_closure: false,
493            captures_count: 0,
494            is_async: false,
495            ref_params: vec![],
496            ref_mutates: vec![],
497            mutable_captures: vec![],
498            frame_descriptor: None,
499            osr_entry_points: vec![],
500            mir_data: None,
501        };
502        let instructions = vec![
503            Instruction::simple(OpCode::TypedArrayGetF64),
504            Instruction::simple(OpCode::ReturnValue),
505        ];
506        let prog = make_program(vec![func], instructions);
507        let errs = verify_v2_typed_opcodes(&prog).unwrap_err();
508        assert_eq!(errs.len(), 1);
509        assert!(matches!(
510            &errs[0],
511            VerifyError::V2MissingFrameDescriptor { .. }
512        ));
513    }
514
515    #[test]
516    fn v2_typed_array_op_with_frame_descriptor_passes() {
517        let func = Function {
518            name: "array_fn".to_string(),
519            arity: 0,
520            param_names: vec![],
521            locals_count: 1,
522            entry_point: 0,
523            body_length: 2,
524            is_closure: false,
525            captures_count: 0,
526            is_async: false,
527            ref_params: vec![],
528            ref_mutates: vec![],
529            mutable_captures: vec![],
530            frame_descriptor: Some(FrameDescriptor::from_slots(vec![NativeKind::Int64])),
531            osr_entry_points: vec![],
532            mir_data: None,
533        };
534        let instructions = vec![
535            Instruction::simple(OpCode::TypedArrayGetF64),
536            Instruction::simple(OpCode::ReturnValue),
537        ];
538        let prog = make_program(vec![func], instructions);
539        assert!(verify_v2_typed_opcodes(&prog).is_ok());
540    }
541
542    #[test]
543    fn v2_field_load_valid_offset() {
544        use crate::bytecode::Operand;
545        let func = Function {
546            name: "field_fn".to_string(),
547            arity: 0,
548            param_names: vec![],
549            locals_count: 1,
550            entry_point: 0,
551            body_length: 2,
552            is_closure: false,
553            captures_count: 0,
554            is_async: false,
555            ref_params: vec![],
556            ref_mutates: vec![],
557            mutable_captures: vec![],
558            frame_descriptor: Some(FrameDescriptor::from_slots(vec![NativeKind::Int64])),
559            osr_entry_points: vec![],
560            mir_data: None,
561        };
562        let instructions = vec![
563            Instruction::new(OpCode::FieldLoadF64, Some(Operand::FieldOffset(16))),
564            Instruction::simple(OpCode::ReturnValue),
565        ];
566        let prog = make_program(vec![func], instructions);
567        assert!(verify_v2_typed_opcodes(&prog).is_ok());
568    }
569
570    #[test]
571    fn v2_field_load_offset_too_large() {
572        use crate::bytecode::Operand;
573        let func = Function {
574            name: "field_fn".to_string(),
575            arity: 0,
576            param_names: vec![],
577            locals_count: 1,
578            entry_point: 0,
579            body_length: 2,
580            is_closure: false,
581            captures_count: 0,
582            is_async: false,
583            ref_params: vec![],
584            ref_mutates: vec![],
585            mutable_captures: vec![],
586            frame_descriptor: Some(FrameDescriptor::from_slots(vec![NativeKind::Int64])),
587            osr_entry_points: vec![],
588            mir_data: None,
589        };
590        let instructions = vec![
591            Instruction::new(OpCode::FieldLoadF64, Some(Operand::FieldOffset(5000))),
592            Instruction::simple(OpCode::ReturnValue),
593        ];
594        let prog = make_program(vec![func], instructions);
595        let errs = verify_v2_typed_opcodes(&prog).unwrap_err();
596        assert_eq!(errs.len(), 1);
597        assert!(matches!(
598            &errs[0],
599            VerifyError::V2FieldOffsetTooLarge { offset: 5000, .. }
600        ));
601    }
602
603    #[test]
604    fn v2_field_load_missing_operand() {
605        let func = Function {
606            name: "field_fn".to_string(),
607            arity: 0,
608            param_names: vec![],
609            locals_count: 1,
610            entry_point: 0,
611            body_length: 2,
612            is_closure: false,
613            captures_count: 0,
614            is_async: false,
615            ref_params: vec![],
616            ref_mutates: vec![],
617            mutable_captures: vec![],
618            frame_descriptor: Some(FrameDescriptor::from_slots(vec![NativeKind::Int64])),
619            osr_entry_points: vec![],
620            mir_data: None,
621        };
622        let instructions = vec![
623            Instruction::simple(OpCode::FieldLoadI64),
624            Instruction::simple(OpCode::ReturnValue),
625        ];
626        let prog = make_program(vec![func], instructions);
627        let errs = verify_v2_typed_opcodes(&prog).unwrap_err();
628        assert_eq!(errs.len(), 1);
629        assert!(matches!(
630            &errs[0],
631            VerifyError::V2MissingFieldOffset { .. }
632        ));
633    }
634
635    #[test]
636    fn v2_i32_arithmetic_missing_frame_descriptor() {
637        let func = Function {
638            name: "i32_fn".to_string(),
639            arity: 0,
640            param_names: vec![],
641            locals_count: 2,
642            entry_point: 0,
643            body_length: 2,
644            is_closure: false,
645            captures_count: 0,
646            is_async: false,
647            ref_params: vec![],
648            ref_mutates: vec![],
649            mutable_captures: vec![],
650            frame_descriptor: None,
651            osr_entry_points: vec![],
652            mir_data: None,
653        };
654        let instructions = vec![
655            Instruction::simple(OpCode::AddI32),
656            Instruction::simple(OpCode::ReturnValue),
657        ];
658        let prog = make_program(vec![func], instructions);
659        let errs = verify_v2_typed_opcodes(&prog).unwrap_err();
660        assert_eq!(errs.len(), 1);
661        assert!(matches!(
662            &errs[0],
663            VerifyError::V2MissingFrameDescriptor { .. }
664        ));
665    }
666
667    #[test]
668    fn v2_is_v2_typed_method() {
669        assert!(OpCode::TypedArrayGetF64.is_v2_typed());
670        assert!(OpCode::FieldLoadF64.is_v2_typed());
671        assert!(OpCode::AddI32.is_v2_typed());
672        assert!(OpCode::NewTypedStruct.is_v2_typed());
673        assert!(!OpCode::Halt.is_v2_typed());
674        assert!(!OpCode::AddInt.is_v2_typed());
675        assert!(!OpCode::LoadLocal.is_v2_typed());
676    }
677
678    /// V1.1A: the new MoveLocal/CloneLocal/DropLocal opcodes are not trusted
679    /// and are not v2-typed. Both verifier passes should accept them as no-ops
680    /// (they pass through without the respective FrameDescriptor requirements).
681    /// V1.1B will add an ownership-specific verifier pass; until then, these
682    /// opcodes are unreachable in execution, so no verification is required.
683    #[test]
684    fn v11a_ownership_opcodes_pass_both_verifiers() {
685        use crate::bytecode::Operand;
686        let func = Function {
687            name: "own_fn".to_string(),
688            arity: 0,
689            param_names: vec![],
690            locals_count: 1,
691            entry_point: 0,
692            body_length: 4,
693            is_closure: false,
694            captures_count: 0,
695            is_async: false,
696            ref_params: vec![],
697            ref_mutates: vec![],
698            mutable_captures: vec![],
699            frame_descriptor: None,
700            osr_entry_points: vec![],
701            mir_data: None,
702        };
703        let instructions = vec![
704            Instruction::new(OpCode::MoveLocal, Some(Operand::Local(0))),
705            Instruction::new(OpCode::CloneLocal, Some(Operand::Local(0))),
706            Instruction::new(OpCode::DropLocal, Some(Operand::Local(0))),
707            Instruction::simple(OpCode::ReturnValue),
708        ];
709        let prog = make_program(vec![func], instructions);
710        assert!(
711            verify_trusted_opcodes(&prog).is_ok(),
712            "V1.1A ownership opcodes should pass trusted verification"
713        );
714        assert!(
715            verify_v2_typed_opcodes(&prog).is_ok(),
716            "V1.1A ownership opcodes should pass v2-typed verification"
717        );
718    }
719
720    /// V1.2A: the new `PromoteToShared` opcode is not trusted and not
721    /// v2-typed. Both verifier passes accept it as a no-op — the opcode
722    /// operates on top-of-stack with no operand, identical in shape to
723    /// `PromoteToOwned`, and needs no FrameDescriptor. V1.2B adds the
724    /// handler; until then reaching this opcode panics in dispatch.
725    #[test]
726    fn v12a_promote_to_shared_passes_both_verifiers() {
727        let func = Function {
728            name: "promote_shared_fn".to_string(),
729            arity: 0,
730            param_names: vec![],
731            locals_count: 0,
732            entry_point: 0,
733            body_length: 2,
734            is_closure: false,
735            captures_count: 0,
736            is_async: false,
737            ref_params: vec![],
738            ref_mutates: vec![],
739            mutable_captures: vec![],
740            frame_descriptor: None,
741            osr_entry_points: vec![],
742            mir_data: None,
743        };
744        let instructions = vec![
745            Instruction::simple(OpCode::PromoteToShared),
746            Instruction::simple(OpCode::ReturnValue),
747        ];
748        let prog = make_program(vec![func], instructions);
749        assert!(
750            verify_trusted_opcodes(&prog).is_ok(),
751            "V1.2A PromoteToShared should pass trusted verification"
752        );
753        assert!(
754            verify_v2_typed_opcodes(&prog).is_ok(),
755            "V1.2A PromoteToShared should pass v2-typed verification"
756        );
757    }
758
759    /// R5.1A: the six new typed bitwise opcodes
760    /// (BitAndInt/BitOrInt/BitXorInt/BitShlInt/BitShrInt/BitNotInt) are not
761    /// trusted and not v2-typed. Both verifier passes accept them as no-ops
762    /// (no FrameDescriptor requirement), matching the behavior of the
763    /// existing int-typed arithmetic family (AddInt/SubInt/MulInt). R5.1B
764    /// will add executor handlers; until then these opcodes are unreachable
765    /// via dispatch — reaching them panics.
766    #[test]
767    fn r51a_typed_bitwise_opcodes_pass_both_verifiers() {
768        let func = Function {
769            name: "bitwise_fn".to_string(),
770            arity: 0,
771            param_names: vec![],
772            locals_count: 0,
773            entry_point: 0,
774            body_length: 7,
775            is_closure: false,
776            captures_count: 0,
777            is_async: false,
778            ref_params: vec![],
779            ref_mutates: vec![],
780            mutable_captures: vec![],
781            frame_descriptor: None,
782            osr_entry_points: vec![],
783            mir_data: None,
784        };
785        let instructions = vec![
786            Instruction::simple(OpCode::BitAndInt),
787            Instruction::simple(OpCode::BitOrInt),
788            Instruction::simple(OpCode::BitXorInt),
789            Instruction::simple(OpCode::BitShlInt),
790            Instruction::simple(OpCode::BitShrInt),
791            Instruction::simple(OpCode::BitNotInt),
792            Instruction::simple(OpCode::ReturnValue),
793        ];
794        let prog = make_program(vec![func], instructions);
795        assert!(
796            verify_trusted_opcodes(&prog).is_ok(),
797            "R5.1A typed bitwise opcodes should pass trusted verification"
798        );
799        assert!(
800            verify_v2_typed_opcodes(&prog).is_ok(),
801            "R5.1A typed bitwise opcodes should pass v2-typed verification"
802        );
803    }
804
805    #[test]
806    fn v2_multiple_errors_collected() {
807        let func = Function {
808            name: "multi_err".to_string(),
809            arity: 0,
810            param_names: vec![],
811            locals_count: 2,
812            entry_point: 0,
813            body_length: 3,
814            is_closure: false,
815            captures_count: 0,
816            is_async: false,
817            ref_params: vec![],
818            ref_mutates: vec![],
819            mutable_captures: vec![],
820            frame_descriptor: None,
821            osr_entry_points: vec![],
822            mir_data: None,
823        };
824        let instructions = vec![
825            Instruction::simple(OpCode::AddI32),
826            Instruction::simple(OpCode::TypedArrayGetI64),
827            Instruction::simple(OpCode::ReturnValue),
828        ];
829        let prog = make_program(vec![func], instructions);
830        let errs = verify_v2_typed_opcodes(&prog).unwrap_err();
831        assert_eq!(errs.len(), 2);
832    }
833}