use super::{BytecodeProgram, OpCode, Operand};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VerifyError {
TrustedOpcodeBadOperand {
function_name: String,
opcode: OpCode,
instruction_offset: usize,
},
V2MissingFrameDescriptor {
function_name: String,
opcode: OpCode,
instruction_offset: usize,
},
V2FieldOffsetTooLarge {
function_name: String,
opcode: OpCode,
instruction_offset: usize,
offset: u16,
},
V2MissingFieldOffset {
function_name: String,
opcode: OpCode,
instruction_offset: usize,
},
}
impl std::fmt::Display for VerifyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
VerifyError::TrustedOpcodeBadOperand {
function_name,
opcode,
instruction_offset,
} => write!(
f,
"Trusted opcode {:?} at offset {} in function '{}' has the wrong operand shape",
opcode, instruction_offset, function_name
),
VerifyError::V2MissingFrameDescriptor {
function_name,
opcode,
instruction_offset,
} => write!(
f,
"V2 typed opcode {:?} at offset {} in function '{}' has no FrameDescriptor",
opcode, instruction_offset, function_name
),
VerifyError::V2FieldOffsetTooLarge {
function_name,
opcode,
instruction_offset,
offset,
} => write!(
f,
"V2 field opcode {:?} at offset {} in function '{}': byte offset {} exceeds maximum (4096)",
opcode, instruction_offset, function_name, offset
),
VerifyError::V2MissingFieldOffset {
function_name,
opcode,
instruction_offset,
} => write!(
f,
"V2 field opcode {:?} at offset {} in function '{}': missing FieldOffset operand",
opcode, instruction_offset, function_name
),
}
}
}
impl std::error::Error for VerifyError {}
pub fn verify_trusted_opcodes(program: &BytecodeProgram) -> Result<(), Vec<VerifyError>> {
let mut errors = Vec::new();
for func in &program.functions {
let start = func.entry_point;
let end = program
.functions
.iter()
.filter(|f| f.entry_point > start)
.map(|f| f.entry_point)
.min()
.unwrap_or(program.instructions.len());
for offset in start..end {
let Some(instruction) = program.instructions.get(offset) else {
break;
};
if !instruction.opcode.is_trusted() {
continue;
}
let operand_ok = match instruction.opcode {
OpCode::LoadLocalTrusted => {
matches!(instruction.operand, Some(Operand::Local(_)))
}
OpCode::JumpIfFalseTrusted => {
matches!(instruction.operand, Some(Operand::Offset(_)))
}
_ => true,
};
if !operand_ok {
errors.push(VerifyError::TrustedOpcodeBadOperand {
function_name: func.name.clone(),
opcode: instruction.opcode,
instruction_offset: offset,
});
}
}
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
const MAX_FIELD_OFFSET: u16 = 4096;
fn is_v2_field_op(op: OpCode) -> bool {
matches!(
op,
OpCode::FieldLoadF64
| OpCode::FieldLoadI64
| OpCode::FieldLoadI32
| OpCode::FieldLoadBool
| OpCode::FieldLoadPtr
| OpCode::FieldStoreF64
| OpCode::FieldStoreI64
| OpCode::FieldStoreI32
)
}
pub fn verify_v2_typed_opcodes(program: &BytecodeProgram) -> Result<(), Vec<VerifyError>> {
let mut errors = Vec::new();
for func in &program.functions {
let start = func.entry_point;
let end = program
.functions
.iter()
.filter(|f| f.entry_point > start)
.map(|f| f.entry_point)
.min()
.unwrap_or(program.instructions.len());
for offset in start..end {
let Some(instruction) = program.instructions.get(offset) else {
break;
};
if !instruction.opcode.is_v2_typed() {
continue;
}
if func.frame_descriptor.is_none() {
errors.push(VerifyError::V2MissingFrameDescriptor {
function_name: func.name.clone(),
opcode: instruction.opcode,
instruction_offset: offset,
});
continue;
}
if is_v2_field_op(instruction.opcode) {
match &instruction.operand {
Some(Operand::FieldOffset(off)) => {
if *off > MAX_FIELD_OFFSET {
errors.push(VerifyError::V2FieldOffsetTooLarge {
function_name: func.name.clone(),
opcode: instruction.opcode,
instruction_offset: offset,
offset: *off,
});
}
}
_ => {
errors.push(VerifyError::V2MissingFieldOffset {
function_name: func.name.clone(),
opcode: instruction.opcode,
instruction_offset: offset,
});
}
}
}
}
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bytecode::{Function, Instruction, OpCode};
use crate::type_tracking::{FrameDescriptor, NativeKind};
fn make_program(functions: Vec<Function>, instructions: Vec<Instruction>) -> BytecodeProgram {
let mut prog = BytecodeProgram::new();
prog.functions = functions;
prog.instructions = instructions;
prog
}
#[test]
fn no_trusted_opcodes_passes() {
let func = Function {
name: "main".to_string(),
arity: 0,
param_names: vec![],
locals_count: 2,
entry_point: 0,
body_length: 2,
is_closure: false,
captures_count: 0,
is_async: false,
ref_params: vec![],
ref_mutates: vec![],
mutable_captures: vec![],
frame_descriptor: None,
osr_entry_points: vec![],
mir_data: None,
};
let instructions = vec![
Instruction::simple(OpCode::AddInt),
Instruction::simple(OpCode::ReturnValue),
];
let prog = make_program(vec![func], instructions);
assert!(verify_trusted_opcodes(&prog).is_ok());
}
#[test]
fn trusted_opcode_no_frame_descriptor_is_not_a_violation() {
use crate::bytecode::Operand;
let func = Function {
name: "load_trusted".to_string(),
arity: 2,
param_names: vec!["a".to_string(), "b".to_string()],
locals_count: 2,
entry_point: 0,
body_length: 3,
is_closure: false,
captures_count: 0,
is_async: false,
ref_params: vec![],
ref_mutates: vec![],
mutable_captures: vec![],
frame_descriptor: None,
osr_entry_points: vec![],
mir_data: None,
};
let instructions = vec![
Instruction::new(OpCode::LoadLocalTrusted, Some(Operand::Local(0))),
Instruction::new(OpCode::JumpIfFalseTrusted, Some(Operand::Offset(1))),
Instruction::simple(OpCode::ReturnValue),
];
let prog = make_program(vec![func], instructions);
assert!(
verify_trusted_opcodes(&prog).is_ok(),
"trusted opcodes with no FrameDescriptor must pass (WS-10b stale-rule removal)"
);
}
#[test]
fn trusted_opcode_bad_operand_is_a_violation() {
use crate::bytecode::Operand;
let func = Function {
name: "bad_load".to_string(),
arity: 0,
param_names: vec![],
locals_count: 1,
entry_point: 0,
body_length: 2,
is_closure: false,
captures_count: 0,
is_async: false,
ref_params: vec![],
ref_mutates: vec![],
mutable_captures: vec![],
frame_descriptor: Some(FrameDescriptor::from_slots(vec![NativeKind::Int64])),
osr_entry_points: vec![],
mir_data: None,
};
let instructions = vec![
Instruction::new(OpCode::LoadLocalTrusted, Some(Operand::Offset(3))),
Instruction::simple(OpCode::ReturnValue),
];
let prog = make_program(vec![func], instructions);
let errs = verify_trusted_opcodes(&prog).unwrap_err();
assert_eq!(errs.len(), 1);
assert!(matches!(
&errs[0],
VerifyError::TrustedOpcodeBadOperand { .. }
));
}
#[test]
fn trusted_opcode_with_valid_operand_passes() {
use crate::bytecode::Operand;
let func = Function {
name: "load_trusted".to_string(),
arity: 2,
param_names: vec!["a".to_string(), "b".to_string()],
locals_count: 2,
entry_point: 0,
body_length: 2,
is_closure: false,
captures_count: 0,
is_async: false,
ref_params: vec![],
ref_mutates: vec![],
mutable_captures: vec![],
frame_descriptor: Some(FrameDescriptor::from_slots(vec![
NativeKind::Int64,
NativeKind::Int64,
])),
osr_entry_points: vec![],
mir_data: None,
};
let instructions = vec![
Instruction::new(OpCode::LoadLocalTrusted, Some(Operand::Local(0))),
Instruction::simple(OpCode::ReturnValue),
];
let prog = make_program(vec![func], instructions);
assert!(verify_trusted_opcodes(&prog).is_ok());
}
#[test]
fn is_trusted_method() {
assert!(OpCode::LoadLocalTrusted.is_trusted());
assert!(OpCode::JumpIfFalseTrusted.is_trusted());
assert!(!OpCode::AddInt.is_trusted());
assert!(!OpCode::Halt.is_trusted());
}
#[test]
fn trusted_variant_mapping() {
assert_eq!(
OpCode::LoadLocal.trusted_variant(),
Some(OpCode::LoadLocalTrusted)
);
assert_eq!(
OpCode::JumpIfFalse.trusted_variant(),
Some(OpCode::JumpIfFalseTrusted)
);
assert_eq!(OpCode::Halt.trusted_variant(), None);
assert_eq!(OpCode::AddInt.trusted_variant(), None);
}
#[test]
fn v2_no_typed_opcodes_passes() {
let func = Function {
name: "main".to_string(),
arity: 0,
param_names: vec![],
locals_count: 2,
entry_point: 0,
body_length: 2,
is_closure: false,
captures_count: 0,
is_async: false,
ref_params: vec![],
ref_mutates: vec![],
mutable_captures: vec![],
frame_descriptor: None,
osr_entry_points: vec![],
mir_data: None,
};
let instructions = vec![
Instruction::simple(OpCode::PushNull),
Instruction::simple(OpCode::ReturnValue),
];
let prog = make_program(vec![func], instructions);
assert!(verify_v2_typed_opcodes(&prog).is_ok());
}
#[test]
fn v2_typed_array_op_missing_frame_descriptor() {
let func = Function {
name: "array_fn".to_string(),
arity: 0,
param_names: vec![],
locals_count: 1,
entry_point: 0,
body_length: 2,
is_closure: false,
captures_count: 0,
is_async: false,
ref_params: vec![],
ref_mutates: vec![],
mutable_captures: vec![],
frame_descriptor: None,
osr_entry_points: vec![],
mir_data: None,
};
let instructions = vec![
Instruction::simple(OpCode::TypedArrayGetF64),
Instruction::simple(OpCode::ReturnValue),
];
let prog = make_program(vec![func], instructions);
let errs = verify_v2_typed_opcodes(&prog).unwrap_err();
assert_eq!(errs.len(), 1);
assert!(matches!(
&errs[0],
VerifyError::V2MissingFrameDescriptor { .. }
));
}
#[test]
fn v2_typed_array_op_with_frame_descriptor_passes() {
let func = Function {
name: "array_fn".to_string(),
arity: 0,
param_names: vec![],
locals_count: 1,
entry_point: 0,
body_length: 2,
is_closure: false,
captures_count: 0,
is_async: false,
ref_params: vec![],
ref_mutates: vec![],
mutable_captures: vec![],
frame_descriptor: Some(FrameDescriptor::from_slots(vec![NativeKind::Int64])),
osr_entry_points: vec![],
mir_data: None,
};
let instructions = vec![
Instruction::simple(OpCode::TypedArrayGetF64),
Instruction::simple(OpCode::ReturnValue),
];
let prog = make_program(vec![func], instructions);
assert!(verify_v2_typed_opcodes(&prog).is_ok());
}
#[test]
fn v2_field_load_valid_offset() {
use crate::bytecode::Operand;
let func = Function {
name: "field_fn".to_string(),
arity: 0,
param_names: vec![],
locals_count: 1,
entry_point: 0,
body_length: 2,
is_closure: false,
captures_count: 0,
is_async: false,
ref_params: vec![],
ref_mutates: vec![],
mutable_captures: vec![],
frame_descriptor: Some(FrameDescriptor::from_slots(vec![NativeKind::Int64])),
osr_entry_points: vec![],
mir_data: None,
};
let instructions = vec![
Instruction::new(OpCode::FieldLoadF64, Some(Operand::FieldOffset(16))),
Instruction::simple(OpCode::ReturnValue),
];
let prog = make_program(vec![func], instructions);
assert!(verify_v2_typed_opcodes(&prog).is_ok());
}
#[test]
fn v2_field_load_offset_too_large() {
use crate::bytecode::Operand;
let func = Function {
name: "field_fn".to_string(),
arity: 0,
param_names: vec![],
locals_count: 1,
entry_point: 0,
body_length: 2,
is_closure: false,
captures_count: 0,
is_async: false,
ref_params: vec![],
ref_mutates: vec![],
mutable_captures: vec![],
frame_descriptor: Some(FrameDescriptor::from_slots(vec![NativeKind::Int64])),
osr_entry_points: vec![],
mir_data: None,
};
let instructions = vec![
Instruction::new(OpCode::FieldLoadF64, Some(Operand::FieldOffset(5000))),
Instruction::simple(OpCode::ReturnValue),
];
let prog = make_program(vec![func], instructions);
let errs = verify_v2_typed_opcodes(&prog).unwrap_err();
assert_eq!(errs.len(), 1);
assert!(matches!(
&errs[0],
VerifyError::V2FieldOffsetTooLarge { offset: 5000, .. }
));
}
#[test]
fn v2_field_load_missing_operand() {
let func = Function {
name: "field_fn".to_string(),
arity: 0,
param_names: vec![],
locals_count: 1,
entry_point: 0,
body_length: 2,
is_closure: false,
captures_count: 0,
is_async: false,
ref_params: vec![],
ref_mutates: vec![],
mutable_captures: vec![],
frame_descriptor: Some(FrameDescriptor::from_slots(vec![NativeKind::Int64])),
osr_entry_points: vec![],
mir_data: None,
};
let instructions = vec![
Instruction::simple(OpCode::FieldLoadI64),
Instruction::simple(OpCode::ReturnValue),
];
let prog = make_program(vec![func], instructions);
let errs = verify_v2_typed_opcodes(&prog).unwrap_err();
assert_eq!(errs.len(), 1);
assert!(matches!(
&errs[0],
VerifyError::V2MissingFieldOffset { .. }
));
}
#[test]
fn v2_i32_arithmetic_missing_frame_descriptor() {
let func = Function {
name: "i32_fn".to_string(),
arity: 0,
param_names: vec![],
locals_count: 2,
entry_point: 0,
body_length: 2,
is_closure: false,
captures_count: 0,
is_async: false,
ref_params: vec![],
ref_mutates: vec![],
mutable_captures: vec![],
frame_descriptor: None,
osr_entry_points: vec![],
mir_data: None,
};
let instructions = vec![
Instruction::simple(OpCode::AddI32),
Instruction::simple(OpCode::ReturnValue),
];
let prog = make_program(vec![func], instructions);
let errs = verify_v2_typed_opcodes(&prog).unwrap_err();
assert_eq!(errs.len(), 1);
assert!(matches!(
&errs[0],
VerifyError::V2MissingFrameDescriptor { .. }
));
}
#[test]
fn v2_is_v2_typed_method() {
assert!(OpCode::TypedArrayGetF64.is_v2_typed());
assert!(OpCode::FieldLoadF64.is_v2_typed());
assert!(OpCode::AddI32.is_v2_typed());
assert!(OpCode::NewTypedStruct.is_v2_typed());
assert!(!OpCode::Halt.is_v2_typed());
assert!(!OpCode::AddInt.is_v2_typed());
assert!(!OpCode::LoadLocal.is_v2_typed());
}
#[test]
fn v11a_ownership_opcodes_pass_both_verifiers() {
use crate::bytecode::Operand;
let func = Function {
name: "own_fn".to_string(),
arity: 0,
param_names: vec![],
locals_count: 1,
entry_point: 0,
body_length: 4,
is_closure: false,
captures_count: 0,
is_async: false,
ref_params: vec![],
ref_mutates: vec![],
mutable_captures: vec![],
frame_descriptor: None,
osr_entry_points: vec![],
mir_data: None,
};
let instructions = vec![
Instruction::new(OpCode::MoveLocal, Some(Operand::Local(0))),
Instruction::new(OpCode::CloneLocal, Some(Operand::Local(0))),
Instruction::new(OpCode::DropLocal, Some(Operand::Local(0))),
Instruction::simple(OpCode::ReturnValue),
];
let prog = make_program(vec![func], instructions);
assert!(
verify_trusted_opcodes(&prog).is_ok(),
"V1.1A ownership opcodes should pass trusted verification"
);
assert!(
verify_v2_typed_opcodes(&prog).is_ok(),
"V1.1A ownership opcodes should pass v2-typed verification"
);
}
#[test]
fn v12a_promote_to_shared_passes_both_verifiers() {
let func = Function {
name: "promote_shared_fn".to_string(),
arity: 0,
param_names: vec![],
locals_count: 0,
entry_point: 0,
body_length: 2,
is_closure: false,
captures_count: 0,
is_async: false,
ref_params: vec![],
ref_mutates: vec![],
mutable_captures: vec![],
frame_descriptor: None,
osr_entry_points: vec![],
mir_data: None,
};
let instructions = vec![
Instruction::simple(OpCode::PromoteToShared),
Instruction::simple(OpCode::ReturnValue),
];
let prog = make_program(vec![func], instructions);
assert!(
verify_trusted_opcodes(&prog).is_ok(),
"V1.2A PromoteToShared should pass trusted verification"
);
assert!(
verify_v2_typed_opcodes(&prog).is_ok(),
"V1.2A PromoteToShared should pass v2-typed verification"
);
}
#[test]
fn r51a_typed_bitwise_opcodes_pass_both_verifiers() {
let func = Function {
name: "bitwise_fn".to_string(),
arity: 0,
param_names: vec![],
locals_count: 0,
entry_point: 0,
body_length: 7,
is_closure: false,
captures_count: 0,
is_async: false,
ref_params: vec![],
ref_mutates: vec![],
mutable_captures: vec![],
frame_descriptor: None,
osr_entry_points: vec![],
mir_data: None,
};
let instructions = vec![
Instruction::simple(OpCode::BitAndInt),
Instruction::simple(OpCode::BitOrInt),
Instruction::simple(OpCode::BitXorInt),
Instruction::simple(OpCode::BitShlInt),
Instruction::simple(OpCode::BitShrInt),
Instruction::simple(OpCode::BitNotInt),
Instruction::simple(OpCode::ReturnValue),
];
let prog = make_program(vec![func], instructions);
assert!(
verify_trusted_opcodes(&prog).is_ok(),
"R5.1A typed bitwise opcodes should pass trusted verification"
);
assert!(
verify_v2_typed_opcodes(&prog).is_ok(),
"R5.1A typed bitwise opcodes should pass v2-typed verification"
);
}
#[test]
fn v2_multiple_errors_collected() {
let func = Function {
name: "multi_err".to_string(),
arity: 0,
param_names: vec![],
locals_count: 2,
entry_point: 0,
body_length: 3,
is_closure: false,
captures_count: 0,
is_async: false,
ref_params: vec![],
ref_mutates: vec![],
mutable_captures: vec![],
frame_descriptor: None,
osr_entry_points: vec![],
mir_data: None,
};
let instructions = vec![
Instruction::simple(OpCode::AddI32),
Instruction::simple(OpCode::TypedArrayGetI64),
Instruction::simple(OpCode::ReturnValue),
];
let prog = make_program(vec![func], instructions);
let errs = verify_v2_typed_opcodes(&prog).unwrap_err();
assert_eq!(errs.len(), 2);
}
}