dev/pbs #8
@ -50,6 +50,14 @@ pub const TRAP_INVALID_FUNC: u32 = 0x0000_000B;
|
|||||||
/// Executed RET with an incorrect stack height (mismatch with function metadata).
|
/// Executed RET with an incorrect stack height (mismatch with function metadata).
|
||||||
pub const TRAP_BAD_RET_SLOTS: u32 = 0x0000_000C;
|
pub const TRAP_BAD_RET_SLOTS: u32 = 0x0000_000C;
|
||||||
|
|
||||||
|
/// Detailed information about a source code span.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||||
|
pub struct SourceSpan {
|
||||||
|
pub file_id: u32,
|
||||||
|
pub start: u32,
|
||||||
|
pub end: u32,
|
||||||
|
}
|
||||||
|
|
||||||
/// Detailed information about a runtime trap.
|
/// Detailed information about a runtime trap.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct TrapInfo {
|
pub struct TrapInfo {
|
||||||
@ -61,6 +69,8 @@ pub struct TrapInfo {
|
|||||||
pub message: String,
|
pub message: String,
|
||||||
/// The absolute Program Counter (PC) address where the trap occurred.
|
/// The absolute Program Counter (PC) address where the trap occurred.
|
||||||
pub pc: u32,
|
pub pc: u32,
|
||||||
|
/// Optional source span information if debug symbols are available.
|
||||||
|
pub span: Option<SourceSpan>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks if an instruction is a jump (branch) instruction.
|
/// Checks if an instruction is a jump (branch) instruction.
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
use crate::pbc::ConstantPoolEntry;
|
use crate::pbc::ConstantPoolEntry;
|
||||||
use crate::opcode::OpCode;
|
use crate::opcode::OpCode;
|
||||||
|
use crate::abi::SourceSpan;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum LoadError {
|
pub enum LoadError {
|
||||||
@ -26,12 +27,33 @@ pub struct FunctionMeta {
|
|||||||
pub max_stack_slots: u16,
|
pub max_stack_slots: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||||
|
pub struct DebugInfo {
|
||||||
|
pub pc_to_span: Vec<(u32, SourceSpan)>, // Sorted by PC
|
||||||
|
pub function_names: Vec<(u32, String)>, // (func_idx, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct Export {
|
||||||
|
pub symbol: String,
|
||||||
|
pub func_idx: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct Import {
|
||||||
|
pub symbol: String,
|
||||||
|
pub relocation_pcs: Vec<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub struct BytecodeModule {
|
pub struct BytecodeModule {
|
||||||
pub version: u16,
|
pub version: u16,
|
||||||
pub const_pool: Vec<ConstantPoolEntry>,
|
pub const_pool: Vec<ConstantPoolEntry>,
|
||||||
pub functions: Vec<FunctionMeta>,
|
pub functions: Vec<FunctionMeta>,
|
||||||
pub code: Vec<u8>,
|
pub code: Vec<u8>,
|
||||||
|
pub debug_info: Option<DebugInfo>,
|
||||||
|
pub exports: Vec<Export>,
|
||||||
|
pub imports: Vec<Import>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct BytecodeLoader;
|
pub struct BytecodeLoader;
|
||||||
@ -95,6 +117,9 @@ impl BytecodeLoader {
|
|||||||
const_pool: Vec::new(),
|
const_pool: Vec::new(),
|
||||||
functions: Vec::new(),
|
functions: Vec::new(),
|
||||||
code: Vec::new(),
|
code: Vec::new(),
|
||||||
|
debug_info: None,
|
||||||
|
exports: Vec::new(),
|
||||||
|
imports: Vec::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
for (kind, offset, length) in sections {
|
for (kind, offset, length) in sections {
|
||||||
@ -109,7 +134,16 @@ impl BytecodeLoader {
|
|||||||
2 => { // Code
|
2 => { // Code
|
||||||
module.code = section_data.to_vec();
|
module.code = section_data.to_vec();
|
||||||
}
|
}
|
||||||
_ => {} // Skip unknown or optional sections like Debug, Exports, Imports for now
|
3 => { // Debug Info
|
||||||
|
module.debug_info = Some(parse_debug_section(section_data)?);
|
||||||
|
}
|
||||||
|
4 => { // Exports
|
||||||
|
module.exports = parse_exports(section_data)?;
|
||||||
|
}
|
||||||
|
5 => { // Imports
|
||||||
|
module.imports = parse_imports(section_data)?;
|
||||||
|
}
|
||||||
|
_ => {} // Skip unknown or optional sections
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -212,6 +246,125 @@ fn parse_functions(data: &[u8]) -> Result<Vec<FunctionMeta>, LoadError> {
|
|||||||
Ok(functions)
|
Ok(functions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_debug_section(data: &[u8]) -> Result<DebugInfo, LoadError> {
|
||||||
|
if data.is_empty() {
|
||||||
|
return Ok(DebugInfo::default());
|
||||||
|
}
|
||||||
|
if data.len() < 8 {
|
||||||
|
return Err(LoadError::MalformedSection);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut pos = 0;
|
||||||
|
|
||||||
|
// PC to Span table
|
||||||
|
let span_count = u32::from_le_bytes(data[pos..pos+4].try_into().unwrap()) as usize;
|
||||||
|
pos += 4;
|
||||||
|
let mut pc_to_span = Vec::with_capacity(span_count);
|
||||||
|
for _ in 0..span_count {
|
||||||
|
if pos + 16 > data.len() {
|
||||||
|
return Err(LoadError::UnexpectedEof);
|
||||||
|
}
|
||||||
|
let pc = u32::from_le_bytes(data[pos..pos+4].try_into().unwrap());
|
||||||
|
let file_id = u32::from_le_bytes(data[pos+4..pos+8].try_into().unwrap());
|
||||||
|
let start = u32::from_le_bytes(data[pos+8..pos+12].try_into().unwrap());
|
||||||
|
let end = u32::from_le_bytes(data[pos+12..pos+16].try_into().unwrap());
|
||||||
|
pc_to_span.push((pc, SourceSpan { file_id, start, end }));
|
||||||
|
pos += 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function names table
|
||||||
|
if pos + 4 > data.len() {
|
||||||
|
return Err(LoadError::UnexpectedEof);
|
||||||
|
}
|
||||||
|
let func_name_count = u32::from_le_bytes(data[pos..pos+4].try_into().unwrap()) as usize;
|
||||||
|
pos += 4;
|
||||||
|
let mut function_names = Vec::with_capacity(func_name_count);
|
||||||
|
for _ in 0..func_name_count {
|
||||||
|
if pos + 8 > data.len() {
|
||||||
|
return Err(LoadError::UnexpectedEof);
|
||||||
|
}
|
||||||
|
let func_idx = u32::from_le_bytes(data[pos..pos+4].try_into().unwrap());
|
||||||
|
let name_len = u32::from_le_bytes(data[pos+4..pos+8].try_into().unwrap()) as usize;
|
||||||
|
pos += 8;
|
||||||
|
if pos + name_len > data.len() {
|
||||||
|
return Err(LoadError::UnexpectedEof);
|
||||||
|
}
|
||||||
|
let name = String::from_utf8_lossy(&data[pos..pos+name_len]).into_owned();
|
||||||
|
function_names.push((func_idx, name));
|
||||||
|
pos += name_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(DebugInfo { pc_to_span, function_names })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_exports(data: &[u8]) -> Result<Vec<Export>, LoadError> {
|
||||||
|
if data.is_empty() {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
if data.len() < 4 {
|
||||||
|
return Err(LoadError::MalformedSection);
|
||||||
|
}
|
||||||
|
let count = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
|
||||||
|
let mut exports = Vec::with_capacity(count);
|
||||||
|
let mut pos = 4;
|
||||||
|
|
||||||
|
for _ in 0..count {
|
||||||
|
if pos + 8 > data.len() {
|
||||||
|
return Err(LoadError::UnexpectedEof);
|
||||||
|
}
|
||||||
|
let func_idx = u32::from_le_bytes(data[pos..pos+4].try_into().unwrap());
|
||||||
|
let name_len = u32::from_le_bytes(data[pos+4..pos+8].try_into().unwrap()) as usize;
|
||||||
|
pos += 8;
|
||||||
|
if pos + name_len > data.len() {
|
||||||
|
return Err(LoadError::UnexpectedEof);
|
||||||
|
}
|
||||||
|
let symbol = String::from_utf8_lossy(&data[pos..pos+name_len]).into_owned();
|
||||||
|
exports.push(Export { symbol, func_idx });
|
||||||
|
pos += name_len;
|
||||||
|
}
|
||||||
|
Ok(exports)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_imports(data: &[u8]) -> Result<Vec<Import>, LoadError> {
|
||||||
|
if data.is_empty() {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
if data.len() < 4 {
|
||||||
|
return Err(LoadError::MalformedSection);
|
||||||
|
}
|
||||||
|
let count = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
|
||||||
|
let mut imports = Vec::with_capacity(count);
|
||||||
|
let mut pos = 4;
|
||||||
|
|
||||||
|
for _ in 0..count {
|
||||||
|
if pos + 8 > data.len() {
|
||||||
|
return Err(LoadError::UnexpectedEof);
|
||||||
|
}
|
||||||
|
let relocation_count = u32::from_le_bytes(data[pos..pos+4].try_into().unwrap()) as usize;
|
||||||
|
let name_len = u32::from_le_bytes(data[pos+4..pos+8].try_into().unwrap()) as usize;
|
||||||
|
pos += 8;
|
||||||
|
|
||||||
|
if pos + name_len > data.len() {
|
||||||
|
return Err(LoadError::UnexpectedEof);
|
||||||
|
}
|
||||||
|
let symbol = String::from_utf8_lossy(&data[pos..pos+name_len]).into_owned();
|
||||||
|
pos += name_len;
|
||||||
|
|
||||||
|
if pos + relocation_count * 4 > data.len() {
|
||||||
|
return Err(LoadError::UnexpectedEof);
|
||||||
|
}
|
||||||
|
let mut relocation_pcs = Vec::with_capacity(relocation_count);
|
||||||
|
for _ in 0..relocation_count {
|
||||||
|
let pc = u32::from_le_bytes(data[pos..pos+4].try_into().unwrap());
|
||||||
|
relocation_pcs.push(pc);
|
||||||
|
pos += 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
imports.push(Import { symbol, relocation_pcs });
|
||||||
|
}
|
||||||
|
Ok(imports)
|
||||||
|
}
|
||||||
|
|
||||||
fn validate_module(module: &BytecodeModule) -> Result<(), LoadError> {
|
fn validate_module(module: &BytecodeModule) -> Result<(), LoadError> {
|
||||||
for func in &module.functions {
|
for func in &module.functions {
|
||||||
// Opcode stream bounds
|
// Opcode stream bounds
|
||||||
|
|||||||
294
crates/prometeu-core/src/virtual_machine/linker.rs
Normal file
294
crates/prometeu-core/src/virtual_machine/linker.rs
Normal file
@ -0,0 +1,294 @@
|
|||||||
|
use crate::virtual_machine::{ProgramImage, Value};
|
||||||
|
use prometeu_bytecode::v0::{BytecodeModule, DebugInfo};
|
||||||
|
use prometeu_bytecode::pbc::ConstantPoolEntry;
|
||||||
|
use prometeu_bytecode::opcode::OpCode;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum LinkError {
|
||||||
|
UnresolvedSymbol(String),
|
||||||
|
DuplicateExport(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Linker;
|
||||||
|
|
||||||
|
impl Linker {
|
||||||
|
pub fn link(modules: &[BytecodeModule]) -> Result<ProgramImage, LinkError> {
|
||||||
|
let mut combined_code = Vec::new();
|
||||||
|
let mut combined_functions = Vec::new();
|
||||||
|
let mut combined_constants = Vec::new();
|
||||||
|
let mut combined_debug_pc_to_span = Vec::new();
|
||||||
|
let mut combined_debug_function_names = Vec::new();
|
||||||
|
|
||||||
|
let mut exports = HashMap::new();
|
||||||
|
|
||||||
|
// Offset mapping for each module
|
||||||
|
let mut module_code_offsets = Vec::with_capacity(modules.len());
|
||||||
|
let mut module_function_offsets = Vec::with_capacity(modules.len());
|
||||||
|
|
||||||
|
// First pass: collect exports and calculate offsets
|
||||||
|
for module in modules {
|
||||||
|
let code_offset = combined_code.len() as u32;
|
||||||
|
let function_offset = combined_functions.len() as u32;
|
||||||
|
|
||||||
|
module_code_offsets.push(code_offset);
|
||||||
|
module_function_offsets.push(function_offset);
|
||||||
|
|
||||||
|
for export in &module.exports {
|
||||||
|
if exports.contains_key(&export.symbol) {
|
||||||
|
return Err(LinkError::DuplicateExport(export.symbol.clone()));
|
||||||
|
}
|
||||||
|
exports.insert(export.symbol.clone(), (function_offset + export.func_idx) as u32);
|
||||||
|
}
|
||||||
|
|
||||||
|
combined_code.extend_from_slice(&module.code);
|
||||||
|
|
||||||
|
for func in &module.functions {
|
||||||
|
let mut linked_func = func.clone();
|
||||||
|
linked_func.code_offset += code_offset;
|
||||||
|
combined_functions.push(linked_func);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second pass: resolve imports and relocate constants/code
|
||||||
|
for (i, module) in modules.iter().enumerate() {
|
||||||
|
let code_offset = module_code_offsets[i] as usize;
|
||||||
|
let const_base = combined_constants.len() as u32;
|
||||||
|
|
||||||
|
// Relocate constant pool entries for this module
|
||||||
|
for entry in &module.const_pool {
|
||||||
|
combined_constants.push(match entry {
|
||||||
|
ConstantPoolEntry::Int32(v) => Value::Int32(*v),
|
||||||
|
ConstantPoolEntry::Int64(v) => Value::Int64(*v),
|
||||||
|
ConstantPoolEntry::Float64(v) => Value::Float(*v),
|
||||||
|
ConstantPoolEntry::Boolean(v) => Value::Boolean(*v),
|
||||||
|
ConstantPoolEntry::String(v) => Value::String(v.clone()),
|
||||||
|
ConstantPoolEntry::Null => Value::Null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Patch relocations for imports
|
||||||
|
for import in &module.imports {
|
||||||
|
let target_func_idx = exports.get(&import.symbol)
|
||||||
|
.ok_or_else(|| LinkError::UnresolvedSymbol(import.symbol.clone()))?;
|
||||||
|
|
||||||
|
for &reloc_pc in &import.relocation_pcs {
|
||||||
|
let absolute_pc = code_offset + reloc_pc as usize;
|
||||||
|
// CALL opcode is 2 bytes, immediate is next 4 bytes
|
||||||
|
let imm_offset = absolute_pc + 2;
|
||||||
|
if imm_offset + 4 <= combined_code.len() {
|
||||||
|
let bytes = target_func_idx.to_le_bytes();
|
||||||
|
combined_code[imm_offset..imm_offset+4].copy_from_slice(&bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Relocate PUSH_CONST instructions
|
||||||
|
if const_base > 0 {
|
||||||
|
let mut pos = code_offset;
|
||||||
|
let end = code_offset + module.code.len();
|
||||||
|
while pos < end {
|
||||||
|
if pos + 2 > end { break; }
|
||||||
|
let op_val = u16::from_le_bytes([combined_code[pos], combined_code[pos+1]]);
|
||||||
|
let opcode = match OpCode::try_from(op_val) {
|
||||||
|
Ok(op) => op,
|
||||||
|
Err(_) => {
|
||||||
|
pos += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
pos += 2;
|
||||||
|
|
||||||
|
match opcode {
|
||||||
|
OpCode::PushConst => {
|
||||||
|
if pos + 4 <= end {
|
||||||
|
let old_idx = u32::from_le_bytes(combined_code[pos..pos+4].try_into().unwrap());
|
||||||
|
let new_idx = old_idx + const_base;
|
||||||
|
combined_code[pos..pos+4].copy_from_slice(&new_idx.to_le_bytes());
|
||||||
|
pos += 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OpCode::PushI32 | OpCode::PushBounded | OpCode::Jmp | OpCode::JmpIfFalse | OpCode::JmpIfTrue
|
||||||
|
| OpCode::GetGlobal | OpCode::SetGlobal | OpCode::GetLocal | OpCode::SetLocal
|
||||||
|
| OpCode::PopN | OpCode::Syscall | OpCode::GateLoad | OpCode::GateStore | OpCode::Call => {
|
||||||
|
pos += 4;
|
||||||
|
}
|
||||||
|
OpCode::PushI64 | OpCode::PushF64 | OpCode::Alloc => {
|
||||||
|
pos += 8;
|
||||||
|
}
|
||||||
|
OpCode::PushBool => {
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle debug info
|
||||||
|
if let Some(debug_info) = &module.debug_info {
|
||||||
|
for (pc, span) in &debug_info.pc_to_span {
|
||||||
|
combined_debug_pc_to_span.push((pc + module_code_offsets[i], span.clone()));
|
||||||
|
}
|
||||||
|
for (func_idx, name) in &debug_info.function_names {
|
||||||
|
combined_debug_function_names.push((func_idx + module_function_offsets[i], name.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let debug_info = if !combined_debug_pc_to_span.is_empty() {
|
||||||
|
Some(DebugInfo {
|
||||||
|
pc_to_span: combined_debug_pc_to_span,
|
||||||
|
function_names: combined_debug_function_names,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(ProgramImage::new(
|
||||||
|
combined_code,
|
||||||
|
combined_constants,
|
||||||
|
combined_functions,
|
||||||
|
debug_info,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use prometeu_bytecode::v0::{BytecodeModule, FunctionMeta, Export, Import};
|
||||||
|
use prometeu_bytecode::opcode::OpCode;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_linker_basic() {
|
||||||
|
// Module 1: defines 'foo', calls 'bar'
|
||||||
|
let mut code1 = Vec::new();
|
||||||
|
// Function 'foo' at offset 0
|
||||||
|
code1.extend_from_slice(&(OpCode::Call as u16).to_le_bytes());
|
||||||
|
code1.extend_from_slice(&0u32.to_le_bytes()); // placeholder for 'bar'
|
||||||
|
code1.extend_from_slice(&(OpCode::Ret as u16).to_le_bytes());
|
||||||
|
|
||||||
|
let m1 = BytecodeModule {
|
||||||
|
version: 0,
|
||||||
|
const_pool: vec![],
|
||||||
|
functions: vec![FunctionMeta {
|
||||||
|
code_offset: 0,
|
||||||
|
code_len: code1.len() as u32,
|
||||||
|
..Default::default()
|
||||||
|
}],
|
||||||
|
code: code1,
|
||||||
|
debug_info: None,
|
||||||
|
exports: vec![Export { symbol: "foo".to_string(), func_idx: 0 }],
|
||||||
|
imports: vec![Import { symbol: "bar".to_string(), relocation_pcs: vec![0] }],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Module 2: defines 'bar'
|
||||||
|
let mut code2 = Vec::new();
|
||||||
|
code2.extend_from_slice(&(OpCode::Ret as u16).to_le_bytes());
|
||||||
|
|
||||||
|
let m2 = BytecodeModule {
|
||||||
|
version: 0,
|
||||||
|
const_pool: vec![],
|
||||||
|
functions: vec![FunctionMeta {
|
||||||
|
code_offset: 0,
|
||||||
|
code_len: code2.len() as u32,
|
||||||
|
..Default::default()
|
||||||
|
}],
|
||||||
|
code: code2,
|
||||||
|
debug_info: None,
|
||||||
|
exports: vec![Export { symbol: "bar".to_string(), func_idx: 0 }],
|
||||||
|
imports: vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = Linker::link(&[m1, m2]).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(result.functions.len(), 2);
|
||||||
|
// 'foo' is func 0, 'bar' is func 1
|
||||||
|
assert_eq!(result.functions[0].code_offset, 0);
|
||||||
|
assert_eq!(result.functions[1].code_offset, 8);
|
||||||
|
|
||||||
|
// Let's check patched code
|
||||||
|
let patched_func_id = u32::from_le_bytes(result.rom[2..6].try_into().unwrap());
|
||||||
|
assert_eq!(patched_func_id, 1); // Points to 'bar'
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_linker_unresolved() {
|
||||||
|
let m1 = BytecodeModule {
|
||||||
|
version: 0,
|
||||||
|
const_pool: vec![],
|
||||||
|
functions: vec![],
|
||||||
|
code: vec![],
|
||||||
|
debug_info: None,
|
||||||
|
exports: vec![],
|
||||||
|
imports: vec![Import { symbol: "missing".to_string(), relocation_pcs: vec![] }],
|
||||||
|
};
|
||||||
|
let result = Linker::link(&[m1]);
|
||||||
|
assert_eq!(result.unwrap_err(), LinkError::UnresolvedSymbol("missing".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_linker_duplicate_export() {
|
||||||
|
let m1 = BytecodeModule {
|
||||||
|
version: 0,
|
||||||
|
const_pool: vec![],
|
||||||
|
functions: vec![],
|
||||||
|
code: vec![],
|
||||||
|
debug_info: None,
|
||||||
|
exports: vec![Export { symbol: "dup".to_string(), func_idx: 0 }],
|
||||||
|
imports: vec![],
|
||||||
|
};
|
||||||
|
let m2 = m1.clone();
|
||||||
|
let result = Linker::link(&[m1, m2]);
|
||||||
|
assert_eq!(result.unwrap_err(), LinkError::DuplicateExport("dup".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_linker_const_relocation() {
|
||||||
|
// Module 1: uses constants
|
||||||
|
let mut code1 = Vec::new();
|
||||||
|
code1.extend_from_slice(&(OpCode::PushConst as u16).to_le_bytes());
|
||||||
|
code1.extend_from_slice(&0u32.to_le_bytes()); // Index 0
|
||||||
|
code1.extend_from_slice(&(OpCode::Ret as u16).to_le_bytes());
|
||||||
|
|
||||||
|
let m1 = BytecodeModule {
|
||||||
|
version: 0,
|
||||||
|
const_pool: vec![ConstantPoolEntry::Int32(42)],
|
||||||
|
functions: vec![FunctionMeta { code_offset: 0, code_len: code1.len() as u32, ..Default::default() }],
|
||||||
|
code: code1,
|
||||||
|
debug_info: None,
|
||||||
|
exports: vec![],
|
||||||
|
imports: vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Module 2: also uses constants
|
||||||
|
let mut code2 = Vec::new();
|
||||||
|
code2.extend_from_slice(&(OpCode::PushConst as u16).to_le_bytes());
|
||||||
|
code2.extend_from_slice(&0u32.to_le_bytes()); // Index 0 (local to module 2)
|
||||||
|
code2.extend_from_slice(&(OpCode::Ret as u16).to_le_bytes());
|
||||||
|
|
||||||
|
let m2 = BytecodeModule {
|
||||||
|
version: 0,
|
||||||
|
const_pool: vec![ConstantPoolEntry::Int32(99)],
|
||||||
|
functions: vec![FunctionMeta { code_offset: 0, code_len: code2.len() as u32, ..Default::default() }],
|
||||||
|
code: code2,
|
||||||
|
debug_info: None,
|
||||||
|
exports: vec![],
|
||||||
|
imports: vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = Linker::link(&[m1, m2]).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(result.constant_pool.len(), 2);
|
||||||
|
assert_eq!(result.constant_pool[0], Value::Int32(42));
|
||||||
|
assert_eq!(result.constant_pool[1], Value::Int32(99));
|
||||||
|
|
||||||
|
// Code for module 1 (starts at 0)
|
||||||
|
let idx1 = u32::from_le_bytes(result.rom[2..6].try_into().unwrap());
|
||||||
|
assert_eq!(idx1, 0);
|
||||||
|
|
||||||
|
// Code for module 2 (starts at 8)
|
||||||
|
let idx2 = u32::from_le_bytes(result.rom[10..14].try_into().unwrap());
|
||||||
|
assert_eq!(idx2, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -24,6 +24,7 @@ pub fn check_local_slot(meta: &FunctionMeta, slot: u32, opcode: u16, pc: u32) ->
|
|||||||
opcode,
|
opcode,
|
||||||
message: format!("Local slot {} out of bounds for function (limit {})", slot, limit),
|
message: format!("Local slot {} out of bounds for function (limit {})", slot, limit),
|
||||||
pc,
|
pc,
|
||||||
|
span: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,14 +7,16 @@ pub mod local_addressing;
|
|||||||
pub mod opcode_spec;
|
pub mod opcode_spec;
|
||||||
pub mod bytecode;
|
pub mod bytecode;
|
||||||
pub mod verifier;
|
pub mod verifier;
|
||||||
|
pub mod linker;
|
||||||
|
|
||||||
use crate::hardware::HardwareBridge;
|
use crate::hardware::HardwareBridge;
|
||||||
pub use program::Program;
|
pub use program::ProgramImage;
|
||||||
pub use prometeu_bytecode::opcode::OpCode;
|
pub use prometeu_bytecode::opcode::OpCode;
|
||||||
pub use value::Value;
|
pub use value::Value;
|
||||||
pub use virtual_machine::{BudgetReport, LogicalFrameEndingReason, VirtualMachine};
|
pub use virtual_machine::{BudgetReport, LogicalFrameEndingReason, VirtualMachine};
|
||||||
pub use prometeu_bytecode::abi::TrapInfo;
|
pub use prometeu_bytecode::abi::TrapInfo;
|
||||||
pub use verifier::VerifierError;
|
pub use verifier::VerifierError;
|
||||||
|
pub use linker::{Linker, LinkError};
|
||||||
|
|
||||||
pub type SyscallId = u32;
|
pub type SyscallId = u32;
|
||||||
|
|
||||||
@ -30,6 +32,7 @@ pub enum VmInitError {
|
|||||||
UnsupportedFormat,
|
UnsupportedFormat,
|
||||||
PpbcParseFailed,
|
PpbcParseFailed,
|
||||||
PbsV0LoadFailed(prometeu_bytecode::v0::LoadError),
|
PbsV0LoadFailed(prometeu_bytecode::v0::LoadError),
|
||||||
|
LinkFailed(LinkError),
|
||||||
EntrypointNotFound,
|
EntrypointNotFound,
|
||||||
VerificationFailed(VerifierError),
|
VerificationFailed(VerifierError),
|
||||||
UnsupportedLegacyCallEncoding,
|
UnsupportedLegacyCallEncoding,
|
||||||
|
|||||||
@ -1,16 +1,18 @@
|
|||||||
use crate::virtual_machine::Value;
|
use crate::virtual_machine::Value;
|
||||||
use prometeu_bytecode::v0::FunctionMeta;
|
use prometeu_bytecode::v0::{FunctionMeta, DebugInfo};
|
||||||
|
use prometeu_bytecode::abi::TrapInfo;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct Program {
|
pub struct ProgramImage {
|
||||||
pub rom: Arc<[u8]>,
|
pub rom: Arc<[u8]>,
|
||||||
pub constant_pool: Arc<[Value]>,
|
pub constant_pool: Arc<[Value]>,
|
||||||
pub functions: Arc<[FunctionMeta]>,
|
pub functions: Arc<[FunctionMeta]>,
|
||||||
|
pub debug_info: Option<DebugInfo>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Program {
|
impl ProgramImage {
|
||||||
pub fn new(rom: Vec<u8>, constant_pool: Vec<Value>, mut functions: Vec<FunctionMeta>) -> Self {
|
pub fn new(rom: Vec<u8>, constant_pool: Vec<Value>, mut functions: Vec<FunctionMeta>, debug_info: Option<DebugInfo>) -> Self {
|
||||||
if functions.is_empty() && !rom.is_empty() {
|
if functions.is_empty() && !rom.is_empty() {
|
||||||
functions.push(FunctionMeta {
|
functions.push(FunctionMeta {
|
||||||
code_offset: 0,
|
code_offset: 0,
|
||||||
@ -22,6 +24,39 @@ impl Program {
|
|||||||
rom: Arc::from(rom),
|
rom: Arc::from(rom),
|
||||||
constant_pool: Arc::from(constant_pool),
|
constant_pool: Arc::from(constant_pool),
|
||||||
functions: Arc::from(functions),
|
functions: Arc::from(functions),
|
||||||
|
debug_info,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn create_trap(&self, code: u32, opcode: u16, mut message: String, pc: u32) -> TrapInfo {
|
||||||
|
let span = self.debug_info.as_ref().and_then(|di| {
|
||||||
|
di.pc_to_span.iter().find(|(p, _)| *p == pc).map(|(_, s)| s.clone())
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(func_idx) = self.find_function_index(pc) {
|
||||||
|
if let Some(func_name) = self.get_function_name(func_idx) {
|
||||||
|
message = format!("{} (in function {})", message, func_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TrapInfo {
|
||||||
|
code,
|
||||||
|
opcode,
|
||||||
|
message,
|
||||||
|
pc,
|
||||||
|
span,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_function_index(&self, pc: u32) -> Option<usize> {
|
||||||
|
self.functions.iter().position(|f| {
|
||||||
|
pc >= f.code_offset && pc < (f.code_offset + f.code_len)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_function_name(&self, func_idx: usize) -> Option<&str> {
|
||||||
|
self.debug_info.as_ref()
|
||||||
|
.and_then(|di| di.function_names.iter().find(|(idx, _)| *idx as usize == func_idx))
|
||||||
|
.map(|(_, name)| name.as_str())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@ use crate::hardware::HardwareBridge;
|
|||||||
use crate::virtual_machine::call_frame::CallFrame;
|
use crate::virtual_machine::call_frame::CallFrame;
|
||||||
use crate::virtual_machine::scope_frame::ScopeFrame;
|
use crate::virtual_machine::scope_frame::ScopeFrame;
|
||||||
use crate::virtual_machine::value::Value;
|
use crate::virtual_machine::value::Value;
|
||||||
use crate::virtual_machine::{NativeInterface, Program, VmInitError};
|
use crate::virtual_machine::{NativeInterface, ProgramImage, VmInitError};
|
||||||
use prometeu_bytecode::opcode::OpCode;
|
use prometeu_bytecode::opcode::OpCode;
|
||||||
use prometeu_bytecode::pbc::{self, ConstantPoolEntry};
|
use prometeu_bytecode::pbc::{self, ConstantPoolEntry};
|
||||||
use prometeu_bytecode::abi::{TrapInfo, TRAP_OOB, TRAP_DIV_ZERO, TRAP_TYPE, TRAP_INVALID_FUNC, TRAP_BAD_RET_SLOTS};
|
use prometeu_bytecode::abi::{TrapInfo, TRAP_OOB, TRAP_DIV_ZERO, TRAP_TYPE, TRAP_INVALID_FUNC, TRAP_BAD_RET_SLOTS};
|
||||||
@ -28,6 +28,11 @@ pub enum LogicalFrameEndingReason {
|
|||||||
Panic(String),
|
Panic(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub enum OpError {
|
||||||
|
Trap(u32, String),
|
||||||
|
Panic(String),
|
||||||
|
}
|
||||||
|
|
||||||
impl From<TrapInfo> for LogicalFrameEndingReason {
|
impl From<TrapInfo> for LogicalFrameEndingReason {
|
||||||
fn from(info: TrapInfo) -> Self {
|
fn from(info: TrapInfo) -> Self {
|
||||||
LogicalFrameEndingReason::Trap(info)
|
LogicalFrameEndingReason::Trap(info)
|
||||||
@ -74,7 +79,7 @@ pub struct VirtualMachine {
|
|||||||
/// Global Variable Store: Variables that persist for the lifetime of the program.
|
/// Global Variable Store: Variables that persist for the lifetime of the program.
|
||||||
pub globals: Vec<Value>,
|
pub globals: Vec<Value>,
|
||||||
/// The loaded executable (Bytecode + Constant Pool), that is the ROM translated.
|
/// The loaded executable (Bytecode + Constant Pool), that is the ROM translated.
|
||||||
pub program: Program,
|
pub program: ProgramImage,
|
||||||
/// Heap Memory: Dynamic allocation pool.
|
/// Heap Memory: Dynamic allocation pool.
|
||||||
pub heap: Vec<Value>,
|
pub heap: Vec<Value>,
|
||||||
/// Total virtual cycles consumed since the VM started.
|
/// Total virtual cycles consumed since the VM started.
|
||||||
@ -94,7 +99,7 @@ impl VirtualMachine {
|
|||||||
call_stack: Vec::new(),
|
call_stack: Vec::new(),
|
||||||
scope_stack: Vec::new(),
|
scope_stack: Vec::new(),
|
||||||
globals: Vec::new(),
|
globals: Vec::new(),
|
||||||
program: Program::new(rom, constant_pool, vec![]),
|
program: ProgramImage::new(rom, constant_pool, vec![], None),
|
||||||
heap: Vec::new(),
|
heap: Vec::new(),
|
||||||
cycles: 0,
|
cycles: 0,
|
||||||
halted: false,
|
halted: false,
|
||||||
@ -107,7 +112,7 @@ impl VirtualMachine {
|
|||||||
pub fn initialize(&mut self, program_bytes: Vec<u8>, entrypoint: &str) -> Result<(), VmInitError> {
|
pub fn initialize(&mut self, program_bytes: Vec<u8>, entrypoint: &str) -> Result<(), VmInitError> {
|
||||||
// Fail fast: reset state upfront. If we return early with an error,
|
// Fail fast: reset state upfront. If we return early with an error,
|
||||||
// the VM is left in a "halted and empty" state.
|
// the VM is left in a "halted and empty" state.
|
||||||
self.program = Program::default();
|
self.program = ProgramImage::default();
|
||||||
self.pc = 0;
|
self.pc = 0;
|
||||||
self.operand_stack.clear();
|
self.operand_stack.clear();
|
||||||
self.call_stack.clear();
|
self.call_stack.clear();
|
||||||
@ -133,29 +138,35 @@ impl VirtualMachine {
|
|||||||
ConstantPoolEntry::String(v) => Value::String(v),
|
ConstantPoolEntry::String(v) => Value::String(v),
|
||||||
ConstantPoolEntry::Null => Value::Null,
|
ConstantPoolEntry::Null => Value::Null,
|
||||||
}).collect();
|
}).collect();
|
||||||
Program::new(pbc_file.rom, cp, vec![])
|
ProgramImage::new(pbc_file.rom, cp, vec![], None)
|
||||||
} else if program_bytes.starts_with(b"PBS\0") {
|
} else if program_bytes.starts_with(b"PBS\0") {
|
||||||
// PBS v0 industrial format
|
// PBS v0 industrial format
|
||||||
match prometeu_bytecode::v0::BytecodeLoader::load(&program_bytes) {
|
match prometeu_bytecode::v0::BytecodeLoader::load(&program_bytes) {
|
||||||
Ok(mut module) => {
|
Ok(module) => {
|
||||||
// Run verifier
|
// Link module(s)
|
||||||
let max_stacks = crate::virtual_machine::verifier::Verifier::verify(&module.code, &module.functions)
|
let mut linked_program = crate::virtual_machine::Linker::link(&[module])
|
||||||
|
.map_err(VmInitError::LinkFailed)?;
|
||||||
|
|
||||||
|
// Run verifier on the linked program
|
||||||
|
// Note: Verifier currently expects code and functions separately.
|
||||||
|
// We need to ensure it works with the linked program.
|
||||||
|
let max_stacks = crate::virtual_machine::verifier::Verifier::verify(&linked_program.rom, &linked_program.functions)
|
||||||
.map_err(VmInitError::VerificationFailed)?;
|
.map_err(VmInitError::VerificationFailed)?;
|
||||||
|
|
||||||
// Apply verified max_stack_slots
|
// Apply verified max_stack_slots
|
||||||
for (func, max_stack) in module.functions.iter_mut().zip(max_stacks) {
|
// Since linked_program.functions is an Arc<[FunctionMeta]>, we need to get a mutable copy if we want to update it.
|
||||||
|
// Or we update it before creating the ProgramImage.
|
||||||
|
|
||||||
|
// Actually, let's look at how we can update max_stack_slots.
|
||||||
|
// ProgramImage holds Arc<[FunctionMeta]>.
|
||||||
|
|
||||||
|
let mut functions = linked_program.functions.as_ref().to_vec();
|
||||||
|
for (func, max_stack) in functions.iter_mut().zip(max_stacks) {
|
||||||
func.max_stack_slots = max_stack;
|
func.max_stack_slots = max_stack;
|
||||||
}
|
}
|
||||||
|
linked_program.functions = std::sync::Arc::from(functions);
|
||||||
|
|
||||||
let cp = module.const_pool.into_iter().map(|entry| match entry {
|
linked_program
|
||||||
ConstantPoolEntry::Int32(v) => Value::Int32(v),
|
|
||||||
ConstantPoolEntry::Int64(v) => Value::Int64(v),
|
|
||||||
ConstantPoolEntry::Float64(v) => Value::Float(v),
|
|
||||||
ConstantPoolEntry::Boolean(v) => Value::Boolean(v),
|
|
||||||
ConstantPoolEntry::String(v) => Value::String(v),
|
|
||||||
ConstantPoolEntry::Null => Value::Null,
|
|
||||||
}).collect();
|
|
||||||
Program::new(module.code, cp, module.functions)
|
|
||||||
}
|
}
|
||||||
Err(prometeu_bytecode::v0::LoadError::InvalidVersion) => return Err(VmInitError::UnsupportedFormat),
|
Err(prometeu_bytecode::v0::LoadError::InvalidVersion) => return Err(VmInitError::UnsupportedFormat),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@ -386,12 +397,7 @@ impl VirtualMachine {
|
|||||||
}
|
}
|
||||||
Value::Boolean(true) => {}
|
Value::Boolean(true) => {}
|
||||||
_ => {
|
_ => {
|
||||||
return Err(LogicalFrameEndingReason::Trap(TrapInfo {
|
return Err(self.trap(TRAP_TYPE, opcode as u16, format!("Expected boolean for JMP_IF_FALSE, got {:?}", val), start_pc as u32));
|
||||||
code: TRAP_TYPE,
|
|
||||||
opcode: opcode as u16,
|
|
||||||
message: format!("Expected boolean for JMP_IF_FALSE, got {:?}", val),
|
|
||||||
pc: start_pc as u32,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -405,12 +411,7 @@ impl VirtualMachine {
|
|||||||
}
|
}
|
||||||
Value::Boolean(false) => {}
|
Value::Boolean(false) => {}
|
||||||
_ => {
|
_ => {
|
||||||
return Err(LogicalFrameEndingReason::Trap(TrapInfo {
|
return Err(self.trap(TRAP_TYPE, opcode as u16, format!("Expected boolean for JMP_IF_TRUE, got {:?}", val), start_pc as u32));
|
||||||
code: TRAP_TYPE,
|
|
||||||
opcode: opcode as u16,
|
|
||||||
message: format!("Expected boolean for JMP_IF_TRUE, got {:?}", val),
|
|
||||||
pc: start_pc as u32,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -433,12 +434,7 @@ impl VirtualMachine {
|
|||||||
OpCode::PushBounded => {
|
OpCode::PushBounded => {
|
||||||
let val = u32::from_le_bytes(instr.imm[0..4].try_into().unwrap());
|
let val = u32::from_le_bytes(instr.imm[0..4].try_into().unwrap());
|
||||||
if val > 0xFFFF {
|
if val > 0xFFFF {
|
||||||
return Err(LogicalFrameEndingReason::Trap(TrapInfo {
|
return Err(self.trap(TRAP_OOB, opcode as u16, format!("Bounded value overflow: {} > 0xFFFF", val), start_pc as u32));
|
||||||
code: TRAP_OOB,
|
|
||||||
opcode: opcode as u16,
|
|
||||||
message: format!("Bounded value overflow: {} > 0xFFFF", val),
|
|
||||||
pc: start_pc as u32,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
self.push(Value::Bounded(val));
|
self.push(Value::Bounded(val));
|
||||||
}
|
}
|
||||||
@ -469,7 +465,7 @@ impl VirtualMachine {
|
|||||||
self.push(a);
|
self.push(a);
|
||||||
self.push(b);
|
self.push(b);
|
||||||
}
|
}
|
||||||
OpCode::Add => self.binary_op(|a, b| match (&a, &b) {
|
OpCode::Add => self.binary_op(opcode, start_pc as u32, |a, b| match (&a, &b) {
|
||||||
(Value::String(_), _) | (_, Value::String(_)) => {
|
(Value::String(_), _) | (_, Value::String(_)) => {
|
||||||
Ok(Value::String(format!("{}{}", a.to_string(), b.to_string())))
|
Ok(Value::String(format!("{}{}", a.to_string(), b.to_string())))
|
||||||
}
|
}
|
||||||
@ -485,19 +481,14 @@ impl VirtualMachine {
|
|||||||
(Value::Bounded(a), Value::Bounded(b)) => {
|
(Value::Bounded(a), Value::Bounded(b)) => {
|
||||||
let res = a.saturating_add(*b);
|
let res = a.saturating_add(*b);
|
||||||
if res > 0xFFFF {
|
if res > 0xFFFF {
|
||||||
Err(LogicalFrameEndingReason::Trap(TrapInfo {
|
Err(OpError::Trap(TRAP_OOB, format!("Bounded addition overflow: {} + {} = {}", a, b, res)))
|
||||||
code: TRAP_OOB,
|
|
||||||
opcode: OpCode::Add as u16,
|
|
||||||
message: format!("Bounded addition overflow: {} + {} = {}", a, b, res),
|
|
||||||
pc: start_pc as u32,
|
|
||||||
}))
|
|
||||||
} else {
|
} else {
|
||||||
Ok(Value::Bounded(res))
|
Ok(Value::Bounded(res))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => Err(LogicalFrameEndingReason::Panic("Invalid types for ADD".into())),
|
_ => Err(OpError::Panic("Invalid types for ADD".into())),
|
||||||
})?,
|
})?,
|
||||||
OpCode::Sub => self.binary_op(|a, b| match (a, b) {
|
OpCode::Sub => self.binary_op(opcode, start_pc as u32, |a, b| match (a, b) {
|
||||||
(Value::Int32(a), Value::Int32(b)) => Ok(Value::Int32(a.wrapping_sub(b))),
|
(Value::Int32(a), Value::Int32(b)) => Ok(Value::Int32(a.wrapping_sub(b))),
|
||||||
(Value::Int64(a), Value::Int64(b)) => Ok(Value::Int64(a.wrapping_sub(b))),
|
(Value::Int64(a), Value::Int64(b)) => Ok(Value::Int64(a.wrapping_sub(b))),
|
||||||
(Value::Int32(a), Value::Int64(b)) => Ok(Value::Int64((a as i64).wrapping_sub(b))),
|
(Value::Int32(a), Value::Int64(b)) => Ok(Value::Int64((a as i64).wrapping_sub(b))),
|
||||||
@ -509,19 +500,14 @@ impl VirtualMachine {
|
|||||||
(Value::Float(a), Value::Int64(b)) => Ok(Value::Float(a - b as f64)),
|
(Value::Float(a), Value::Int64(b)) => Ok(Value::Float(a - b as f64)),
|
||||||
(Value::Bounded(a), Value::Bounded(b)) => {
|
(Value::Bounded(a), Value::Bounded(b)) => {
|
||||||
if a < b {
|
if a < b {
|
||||||
Err(LogicalFrameEndingReason::Trap(TrapInfo {
|
Err(OpError::Trap(TRAP_OOB, format!("Bounded subtraction underflow: {} - {} < 0", a, b)))
|
||||||
code: TRAP_OOB,
|
|
||||||
opcode: OpCode::Sub as u16,
|
|
||||||
message: format!("Bounded subtraction underflow: {} - {} < 0", a, b),
|
|
||||||
pc: start_pc as u32,
|
|
||||||
}))
|
|
||||||
} else {
|
} else {
|
||||||
Ok(Value::Bounded(a - b))
|
Ok(Value::Bounded(a - b))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => Err(LogicalFrameEndingReason::Panic("Invalid types for SUB".into())),
|
_ => Err(OpError::Panic("Invalid types for SUB".into())),
|
||||||
})?,
|
})?,
|
||||||
OpCode::Mul => self.binary_op(|a, b| match (a, b) {
|
OpCode::Mul => self.binary_op(opcode, start_pc as u32, |a, b| match (a, b) {
|
||||||
(Value::Int32(a), Value::Int32(b)) => Ok(Value::Int32(a.wrapping_mul(b))),
|
(Value::Int32(a), Value::Int32(b)) => Ok(Value::Int32(a.wrapping_mul(b))),
|
||||||
(Value::Int64(a), Value::Int64(b)) => Ok(Value::Int64(a.wrapping_mul(b))),
|
(Value::Int64(a), Value::Int64(b)) => Ok(Value::Int64(a.wrapping_mul(b))),
|
||||||
(Value::Int32(a), Value::Int64(b)) => Ok(Value::Int64((a as i64).wrapping_mul(b))),
|
(Value::Int32(a), Value::Int64(b)) => Ok(Value::Int64((a as i64).wrapping_mul(b))),
|
||||||
@ -534,166 +520,96 @@ impl VirtualMachine {
|
|||||||
(Value::Bounded(a), Value::Bounded(b)) => {
|
(Value::Bounded(a), Value::Bounded(b)) => {
|
||||||
let res = a as u64 * b as u64;
|
let res = a as u64 * b as u64;
|
||||||
if res > 0xFFFF {
|
if res > 0xFFFF {
|
||||||
Err(LogicalFrameEndingReason::Trap(TrapInfo {
|
Err(OpError::Trap(TRAP_OOB, format!("Bounded multiplication overflow: {} * {} = {}", a, b, res)))
|
||||||
code: TRAP_OOB,
|
|
||||||
opcode: OpCode::Mul as u16,
|
|
||||||
message: format!("Bounded multiplication overflow: {} * {} = {}", a, b, res),
|
|
||||||
pc: start_pc as u32,
|
|
||||||
}))
|
|
||||||
} else {
|
} else {
|
||||||
Ok(Value::Bounded(res as u32))
|
Ok(Value::Bounded(res as u32))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => Err(LogicalFrameEndingReason::Panic("Invalid types for MUL".into())),
|
_ => Err(OpError::Panic("Invalid types for MUL".into())),
|
||||||
})?,
|
})?,
|
||||||
OpCode::Div => self.binary_op(|a, b| match (a, b) {
|
OpCode::Div => self.binary_op(opcode, start_pc as u32, |a, b| match (a, b) {
|
||||||
(Value::Int32(a), Value::Int32(b)) => {
|
(Value::Int32(a), Value::Int32(b)) => {
|
||||||
if b == 0 {
|
if b == 0 {
|
||||||
return Err(LogicalFrameEndingReason::Trap(TrapInfo {
|
return Err(OpError::Trap(TRAP_DIV_ZERO, "Integer division by zero".into()));
|
||||||
code: TRAP_DIV_ZERO,
|
|
||||||
opcode: OpCode::Div as u16,
|
|
||||||
message: "Integer division by zero".into(),
|
|
||||||
pc: start_pc as u32,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
Ok(Value::Int32(a / b))
|
Ok(Value::Int32(a / b))
|
||||||
}
|
}
|
||||||
(Value::Int64(a), Value::Int64(b)) => {
|
(Value::Int64(a), Value::Int64(b)) => {
|
||||||
if b == 0 {
|
if b == 0 {
|
||||||
return Err(LogicalFrameEndingReason::Trap(TrapInfo {
|
return Err(OpError::Trap(TRAP_DIV_ZERO, "Integer division by zero".into()));
|
||||||
code: TRAP_DIV_ZERO,
|
|
||||||
opcode: OpCode::Div as u16,
|
|
||||||
message: "Integer division by zero".into(),
|
|
||||||
pc: start_pc as u32,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
Ok(Value::Int64(a / b))
|
Ok(Value::Int64(a / b))
|
||||||
}
|
}
|
||||||
(Value::Int32(a), Value::Int64(b)) => {
|
(Value::Int32(a), Value::Int64(b)) => {
|
||||||
if b == 0 {
|
if b == 0 {
|
||||||
return Err(LogicalFrameEndingReason::Trap(TrapInfo {
|
return Err(OpError::Trap(TRAP_DIV_ZERO, "Integer division by zero".into()));
|
||||||
code: TRAP_DIV_ZERO,
|
|
||||||
opcode: OpCode::Div as u16,
|
|
||||||
message: "Integer division by zero".into(),
|
|
||||||
pc: start_pc as u32,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
Ok(Value::Int64(a as i64 / b))
|
Ok(Value::Int64(a as i64 / b))
|
||||||
}
|
}
|
||||||
(Value::Int64(a), Value::Int32(b)) => {
|
(Value::Int64(a), Value::Int32(b)) => {
|
||||||
if b == 0 {
|
if b == 0 {
|
||||||
return Err(LogicalFrameEndingReason::Trap(TrapInfo {
|
return Err(OpError::Trap(TRAP_DIV_ZERO, "Integer division by zero".into()));
|
||||||
code: TRAP_DIV_ZERO,
|
|
||||||
opcode: OpCode::Div as u16,
|
|
||||||
message: "Integer division by zero".into(),
|
|
||||||
pc: start_pc as u32,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
Ok(Value::Int64(a / b as i64))
|
Ok(Value::Int64(a / b as i64))
|
||||||
}
|
}
|
||||||
(Value::Float(a), Value::Float(b)) => {
|
(Value::Float(a), Value::Float(b)) => {
|
||||||
if b == 0.0 {
|
if b == 0.0 {
|
||||||
return Err(LogicalFrameEndingReason::Trap(TrapInfo {
|
return Err(OpError::Trap(TRAP_DIV_ZERO, "Float division by zero".into()));
|
||||||
code: TRAP_DIV_ZERO,
|
|
||||||
opcode: OpCode::Div as u16,
|
|
||||||
message: "Float division by zero".into(),
|
|
||||||
pc: start_pc as u32,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
Ok(Value::Float(a / b))
|
Ok(Value::Float(a / b))
|
||||||
}
|
}
|
||||||
(Value::Int32(a), Value::Float(b)) => {
|
(Value::Int32(a), Value::Float(b)) => {
|
||||||
if b == 0.0 {
|
if b == 0.0 {
|
||||||
return Err(LogicalFrameEndingReason::Trap(TrapInfo {
|
return Err(OpError::Trap(TRAP_DIV_ZERO, "Float division by zero".into()));
|
||||||
code: TRAP_DIV_ZERO,
|
|
||||||
opcode: OpCode::Div as u16,
|
|
||||||
message: "Float division by zero".into(),
|
|
||||||
pc: start_pc as u32,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
Ok(Value::Float(a as f64 / b))
|
Ok(Value::Float(a as f64 / b))
|
||||||
}
|
}
|
||||||
(Value::Float(a), Value::Int32(b)) => {
|
(Value::Float(a), Value::Int32(b)) => {
|
||||||
if b == 0 {
|
if b == 0 {
|
||||||
return Err(LogicalFrameEndingReason::Trap(TrapInfo {
|
return Err(OpError::Trap(TRAP_DIV_ZERO, "Float division by zero".into()));
|
||||||
code: TRAP_DIV_ZERO,
|
|
||||||
opcode: OpCode::Div as u16,
|
|
||||||
message: "Float division by zero".into(),
|
|
||||||
pc: start_pc as u32,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
Ok(Value::Float(a / b as f64))
|
Ok(Value::Float(a / b as f64))
|
||||||
}
|
}
|
||||||
(Value::Int64(a), Value::Float(b)) => {
|
(Value::Int64(a), Value::Float(b)) => {
|
||||||
if b == 0.0 {
|
if b == 0.0 {
|
||||||
return Err(LogicalFrameEndingReason::Trap(TrapInfo {
|
return Err(OpError::Trap(TRAP_DIV_ZERO, "Float division by zero".into()));
|
||||||
code: TRAP_DIV_ZERO,
|
|
||||||
opcode: OpCode::Div as u16,
|
|
||||||
message: "Float division by zero".into(),
|
|
||||||
pc: start_pc as u32,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
Ok(Value::Float(a as f64 / b))
|
Ok(Value::Float(a as f64 / b))
|
||||||
}
|
}
|
||||||
(Value::Float(a), Value::Int64(b)) => {
|
(Value::Float(a), Value::Int64(b)) => {
|
||||||
if b == 0 {
|
if b == 0 {
|
||||||
return Err(LogicalFrameEndingReason::Trap(TrapInfo {
|
return Err(OpError::Trap(TRAP_DIV_ZERO, "Float division by zero".into()));
|
||||||
code: TRAP_DIV_ZERO,
|
|
||||||
opcode: OpCode::Div as u16,
|
|
||||||
message: "Float division by zero".into(),
|
|
||||||
pc: start_pc as u32,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
Ok(Value::Float(a / b as f64))
|
Ok(Value::Float(a / b as f64))
|
||||||
}
|
}
|
||||||
(Value::Bounded(a), Value::Bounded(b)) => {
|
(Value::Bounded(a), Value::Bounded(b)) => {
|
||||||
if b == 0 {
|
if b == 0 {
|
||||||
return Err(LogicalFrameEndingReason::Trap(TrapInfo {
|
return Err(OpError::Trap(TRAP_DIV_ZERO, "Bounded division by zero".into()));
|
||||||
code: TRAP_DIV_ZERO,
|
|
||||||
opcode: OpCode::Div as u16,
|
|
||||||
message: "Bounded division by zero".into(),
|
|
||||||
pc: start_pc as u32,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
Ok(Value::Bounded(a / b))
|
Ok(Value::Bounded(a / b))
|
||||||
}
|
}
|
||||||
_ => Err(LogicalFrameEndingReason::Panic("Invalid types for DIV".into())),
|
_ => Err(OpError::Panic("Invalid types for DIV".into())),
|
||||||
})?,
|
})?,
|
||||||
OpCode::Mod => self.binary_op(|a, b| match (a, b) {
|
OpCode::Mod => self.binary_op(opcode, start_pc as u32, |a, b| match (a, b) {
|
||||||
(Value::Int32(a), Value::Int32(b)) => {
|
(Value::Int32(a), Value::Int32(b)) => {
|
||||||
if b == 0 {
|
if b == 0 {
|
||||||
return Err(LogicalFrameEndingReason::Trap(TrapInfo {
|
return Err(OpError::Trap(TRAP_DIV_ZERO, "Integer modulo by zero".into()));
|
||||||
code: TRAP_DIV_ZERO,
|
|
||||||
opcode: OpCode::Mod as u16,
|
|
||||||
message: "Integer modulo by zero".into(),
|
|
||||||
pc: start_pc as u32,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
Ok(Value::Int32(a % b))
|
Ok(Value::Int32(a % b))
|
||||||
}
|
}
|
||||||
(Value::Int64(a), Value::Int64(b)) => {
|
(Value::Int64(a), Value::Int64(b)) => {
|
||||||
if b == 0 {
|
if b == 0 {
|
||||||
return Err(LogicalFrameEndingReason::Trap(TrapInfo {
|
return Err(OpError::Trap(TRAP_DIV_ZERO, "Integer modulo by zero".into()));
|
||||||
code: TRAP_DIV_ZERO,
|
|
||||||
opcode: OpCode::Mod as u16,
|
|
||||||
message: "Integer modulo by zero".into(),
|
|
||||||
pc: start_pc as u32,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
Ok(Value::Int64(a % b))
|
Ok(Value::Int64(a % b))
|
||||||
}
|
}
|
||||||
(Value::Bounded(a), Value::Bounded(b)) => {
|
(Value::Bounded(a), Value::Bounded(b)) => {
|
||||||
if b == 0 {
|
if b == 0 {
|
||||||
return Err(LogicalFrameEndingReason::Trap(TrapInfo {
|
return Err(OpError::Trap(TRAP_DIV_ZERO, "Bounded modulo by zero".into()));
|
||||||
code: TRAP_DIV_ZERO,
|
|
||||||
opcode: OpCode::Mod as u16,
|
|
||||||
message: "Bounded modulo by zero".into(),
|
|
||||||
pc: start_pc as u32,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
Ok(Value::Bounded(a % b))
|
Ok(Value::Bounded(a % b))
|
||||||
}
|
}
|
||||||
_ => Err(LogicalFrameEndingReason::Panic("Invalid types for MOD".into())),
|
_ => Err(OpError::Panic("Invalid types for MOD".into())),
|
||||||
})?,
|
})?,
|
||||||
OpCode::BoundToInt => {
|
OpCode::BoundToInt => {
|
||||||
let val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
|
let val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
|
||||||
@ -707,44 +623,39 @@ impl VirtualMachine {
|
|||||||
let val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
|
let val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
|
||||||
let int_val = val.as_integer().ok_or_else(|| LogicalFrameEndingReason::Panic("Expected integer for INT_TO_BOUND_CHECKED".into()))?;
|
let int_val = val.as_integer().ok_or_else(|| LogicalFrameEndingReason::Panic("Expected integer for INT_TO_BOUND_CHECKED".into()))?;
|
||||||
if int_val < 0 || int_val > 0xFFFF {
|
if int_val < 0 || int_val > 0xFFFF {
|
||||||
return Err(LogicalFrameEndingReason::Trap(TrapInfo {
|
return Err(self.trap(TRAP_OOB, OpCode::IntToBoundChecked as u16, format!("Integer to bounded conversion out of range: {}", int_val), start_pc as u32));
|
||||||
code: TRAP_OOB,
|
|
||||||
opcode: OpCode::IntToBoundChecked as u16,
|
|
||||||
message: format!("Integer to bounded conversion out of range: {}", int_val),
|
|
||||||
pc: start_pc as u32,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
self.push(Value::Bounded(int_val as u32));
|
self.push(Value::Bounded(int_val as u32));
|
||||||
}
|
}
|
||||||
OpCode::Eq => self.binary_op(|a, b| Ok(Value::Boolean(a == b)))?,
|
OpCode::Eq => self.binary_op(opcode, start_pc as u32, |a, b| Ok(Value::Boolean(a == b)))?,
|
||||||
OpCode::Neq => self.binary_op(|a, b| Ok(Value::Boolean(a != b)))?,
|
OpCode::Neq => self.binary_op(opcode, start_pc as u32, |a, b| Ok(Value::Boolean(a != b)))?,
|
||||||
OpCode::Lt => self.binary_op(|a, b| {
|
OpCode::Lt => self.binary_op(opcode, start_pc as u32, |a, b| {
|
||||||
a.partial_cmp(&b)
|
a.partial_cmp(&b)
|
||||||
.map(|o| Value::Boolean(o == std::cmp::Ordering::Less))
|
.map(|o| Value::Boolean(o == std::cmp::Ordering::Less))
|
||||||
.ok_or_else(|| LogicalFrameEndingReason::Panic("Invalid types for LT".into()))
|
.ok_or_else(|| OpError::Panic("Invalid types for LT".into()))
|
||||||
})?,
|
})?,
|
||||||
OpCode::Gt => self.binary_op(|a, b| {
|
OpCode::Gt => self.binary_op(opcode, start_pc as u32, |a, b| {
|
||||||
a.partial_cmp(&b)
|
a.partial_cmp(&b)
|
||||||
.map(|o| Value::Boolean(o == std::cmp::Ordering::Greater))
|
.map(|o| Value::Boolean(o == std::cmp::Ordering::Greater))
|
||||||
.ok_or_else(|| LogicalFrameEndingReason::Panic("Invalid types for GT".into()))
|
.ok_or_else(|| OpError::Panic("Invalid types for GT".into()))
|
||||||
})?,
|
})?,
|
||||||
OpCode::Lte => self.binary_op(|a, b| {
|
OpCode::Lte => self.binary_op(opcode, start_pc as u32, |a, b| {
|
||||||
a.partial_cmp(&b)
|
a.partial_cmp(&b)
|
||||||
.map(|o| Value::Boolean(o != std::cmp::Ordering::Greater))
|
.map(|o| Value::Boolean(o != std::cmp::Ordering::Greater))
|
||||||
.ok_or_else(|| LogicalFrameEndingReason::Panic("Invalid types for LTE".into()))
|
.ok_or_else(|| OpError::Panic("Invalid types for LTE".into()))
|
||||||
})?,
|
})?,
|
||||||
OpCode::Gte => self.binary_op(|a, b| {
|
OpCode::Gte => self.binary_op(opcode, start_pc as u32, |a, b| {
|
||||||
a.partial_cmp(&b)
|
a.partial_cmp(&b)
|
||||||
.map(|o| Value::Boolean(o != std::cmp::Ordering::Less))
|
.map(|o| Value::Boolean(o != std::cmp::Ordering::Less))
|
||||||
.ok_or_else(|| LogicalFrameEndingReason::Panic("Invalid types for GTE".into()))
|
.ok_or_else(|| OpError::Panic("Invalid types for GTE".into()))
|
||||||
})?,
|
})?,
|
||||||
OpCode::And => self.binary_op(|a, b| match (a, b) {
|
OpCode::And => self.binary_op(opcode, start_pc as u32, |a, b| match (a, b) {
|
||||||
(Value::Boolean(a), Value::Boolean(b)) => Ok(Value::Boolean(a && b)),
|
(Value::Boolean(a), Value::Boolean(b)) => Ok(Value::Boolean(a && b)),
|
||||||
_ => Err(LogicalFrameEndingReason::Panic("Invalid types for AND".into())),
|
_ => Err(OpError::Panic("Invalid types for AND".into())),
|
||||||
})?,
|
})?,
|
||||||
OpCode::Or => self.binary_op(|a, b| match (a, b) {
|
OpCode::Or => self.binary_op(opcode, start_pc as u32, |a, b| match (a, b) {
|
||||||
(Value::Boolean(a), Value::Boolean(b)) => Ok(Value::Boolean(a || b)),
|
(Value::Boolean(a), Value::Boolean(b)) => Ok(Value::Boolean(a || b)),
|
||||||
_ => Err(LogicalFrameEndingReason::Panic("Invalid types for OR".into())),
|
_ => Err(OpError::Panic("Invalid types for OR".into())),
|
||||||
})?,
|
})?,
|
||||||
OpCode::Not => {
|
OpCode::Not => {
|
||||||
let val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
|
let val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
|
||||||
@ -754,40 +665,40 @@ impl VirtualMachine {
|
|||||||
return Err(LogicalFrameEndingReason::Panic("Invalid type for NOT".into()));
|
return Err(LogicalFrameEndingReason::Panic("Invalid type for NOT".into()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
OpCode::BitAnd => self.binary_op(|a, b| match (a, b) {
|
OpCode::BitAnd => self.binary_op(opcode, start_pc as u32, |a, b| match (a, b) {
|
||||||
(Value::Int32(a), Value::Int32(b)) => Ok(Value::Int32(a & b)),
|
(Value::Int32(a), Value::Int32(b)) => Ok(Value::Int32(a & b)),
|
||||||
(Value::Int64(a), Value::Int64(b)) => Ok(Value::Int64(a & b)),
|
(Value::Int64(a), Value::Int64(b)) => Ok(Value::Int64(a & b)),
|
||||||
(Value::Int32(a), Value::Int64(b)) => Ok(Value::Int64((a as i64) & b)),
|
(Value::Int32(a), Value::Int64(b)) => Ok(Value::Int64((a as i64) & b)),
|
||||||
(Value::Int64(a), Value::Int32(b)) => Ok(Value::Int64(a & (b as i64))),
|
(Value::Int64(a), Value::Int32(b)) => Ok(Value::Int64(a & (b as i64))),
|
||||||
_ => Err(LogicalFrameEndingReason::Panic("Invalid types for BitAnd".into())),
|
_ => Err(OpError::Panic("Invalid types for BitAnd".into())),
|
||||||
})?,
|
})?,
|
||||||
OpCode::BitOr => self.binary_op(|a, b| match (a, b) {
|
OpCode::BitOr => self.binary_op(opcode, start_pc as u32, |a, b| match (a, b) {
|
||||||
(Value::Int32(a), Value::Int32(b)) => Ok(Value::Int32(a | b)),
|
(Value::Int32(a), Value::Int32(b)) => Ok(Value::Int32(a | b)),
|
||||||
(Value::Int64(a), Value::Int64(b)) => Ok(Value::Int64(a | b)),
|
(Value::Int64(a), Value::Int64(b)) => Ok(Value::Int64(a | b)),
|
||||||
(Value::Int32(a), Value::Int64(b)) => Ok(Value::Int64((a as i64) | b)),
|
(Value::Int32(a), Value::Int64(b)) => Ok(Value::Int64((a as i64) | b)),
|
||||||
(Value::Int64(a), Value::Int32(b)) => Ok(Value::Int64(a | (b as i64))),
|
(Value::Int64(a), Value::Int32(b)) => Ok(Value::Int64(a | (b as i64))),
|
||||||
_ => Err(LogicalFrameEndingReason::Panic("Invalid types for BitOr".into())),
|
_ => Err(OpError::Panic("Invalid types for BitOr".into())),
|
||||||
})?,
|
})?,
|
||||||
OpCode::BitXor => self.binary_op(|a, b| match (a, b) {
|
OpCode::BitXor => self.binary_op(opcode, start_pc as u32, |a, b| match (a, b) {
|
||||||
(Value::Int32(a), Value::Int32(b)) => Ok(Value::Int32(a ^ b)),
|
(Value::Int32(a), Value::Int32(b)) => Ok(Value::Int32(a ^ b)),
|
||||||
(Value::Int64(a), Value::Int64(b)) => Ok(Value::Int64(a ^ b)),
|
(Value::Int64(a), Value::Int64(b)) => Ok(Value::Int64(a ^ b)),
|
||||||
(Value::Int32(a), Value::Int64(b)) => Ok(Value::Int64((a as i64) ^ b)),
|
(Value::Int32(a), Value::Int64(b)) => Ok(Value::Int64((a as i64) ^ b)),
|
||||||
(Value::Int64(a), Value::Int32(b)) => Ok(Value::Int64(a ^ (b as i64))),
|
(Value::Int64(a), Value::Int32(b)) => Ok(Value::Int64(a ^ (b as i64))),
|
||||||
_ => Err(LogicalFrameEndingReason::Panic("Invalid types for BitXor".into())),
|
_ => Err(OpError::Panic("Invalid types for BitXor".into())),
|
||||||
})?,
|
})?,
|
||||||
OpCode::Shl => self.binary_op(|a, b| match (a, b) {
|
OpCode::Shl => self.binary_op(opcode, start_pc as u32, |a, b| match (a, b) {
|
||||||
(Value::Int32(a), Value::Int32(b)) => Ok(Value::Int32(a.wrapping_shl(b as u32))),
|
(Value::Int32(a), Value::Int32(b)) => Ok(Value::Int32(a.wrapping_shl(b as u32))),
|
||||||
(Value::Int64(a), Value::Int64(b)) => Ok(Value::Int64(a.wrapping_shl(b as u32))),
|
(Value::Int64(a), Value::Int64(b)) => Ok(Value::Int64(a.wrapping_shl(b as u32))),
|
||||||
(Value::Int32(a), Value::Int64(b)) => Ok(Value::Int64((a as i64).wrapping_shl(b as u32))),
|
(Value::Int32(a), Value::Int64(b)) => Ok(Value::Int64((a as i64).wrapping_shl(b as u32))),
|
||||||
(Value::Int64(a), Value::Int32(b)) => Ok(Value::Int64(a.wrapping_shl(b as u32))),
|
(Value::Int64(a), Value::Int32(b)) => Ok(Value::Int64(a.wrapping_shl(b as u32))),
|
||||||
_ => Err(LogicalFrameEndingReason::Panic("Invalid types for Shl".into())),
|
_ => Err(OpError::Panic("Invalid types for Shl".into())),
|
||||||
})?,
|
})?,
|
||||||
OpCode::Shr => self.binary_op(|a, b| match (a, b) {
|
OpCode::Shr => self.binary_op(opcode, start_pc as u32, |a, b| match (a, b) {
|
||||||
(Value::Int32(a), Value::Int32(b)) => Ok(Value::Int32(a.wrapping_shr(b as u32))),
|
(Value::Int32(a), Value::Int32(b)) => Ok(Value::Int32(a.wrapping_shr(b as u32))),
|
||||||
(Value::Int64(a), Value::Int64(b)) => Ok(Value::Int64(a.wrapping_shr(b as u32))),
|
(Value::Int64(a), Value::Int64(b)) => Ok(Value::Int64(a.wrapping_shr(b as u32))),
|
||||||
(Value::Int32(a), Value::Int64(b)) => Ok(Value::Int64((a as i64).wrapping_shr(b as u32))),
|
(Value::Int32(a), Value::Int64(b)) => Ok(Value::Int64((a as i64).wrapping_shr(b as u32))),
|
||||||
(Value::Int64(a), Value::Int32(b)) => Ok(Value::Int64(a.wrapping_shr(b as u32))),
|
(Value::Int64(a), Value::Int32(b)) => Ok(Value::Int64(a.wrapping_shr(b as u32))),
|
||||||
_ => Err(LogicalFrameEndingReason::Panic("Invalid types for Shr".into())),
|
_ => Err(OpError::Panic("Invalid types for Shr".into())),
|
||||||
})?,
|
})?,
|
||||||
OpCode::Neg => {
|
OpCode::Neg => {
|
||||||
let val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
|
let val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
|
||||||
@ -816,7 +727,8 @@ impl VirtualMachine {
|
|||||||
let frame = self.call_stack.last().ok_or_else(|| LogicalFrameEndingReason::Panic("No active call frame".into()))?;
|
let frame = self.call_stack.last().ok_or_else(|| LogicalFrameEndingReason::Panic("No active call frame".into()))?;
|
||||||
let func = &self.program.functions[frame.func_idx];
|
let func = &self.program.functions[frame.func_idx];
|
||||||
|
|
||||||
crate::virtual_machine::local_addressing::check_local_slot(func, slot, opcode as u16, start_pc as u32)?;
|
crate::virtual_machine::local_addressing::check_local_slot(func, slot, opcode as u16, start_pc as u32)
|
||||||
|
.map_err(|trap_info| self.trap(trap_info.code, trap_info.opcode, trap_info.message, trap_info.pc))?;
|
||||||
|
|
||||||
let stack_idx = crate::virtual_machine::local_addressing::local_index(frame, slot);
|
let stack_idx = crate::virtual_machine::local_addressing::local_index(frame, slot);
|
||||||
let val = self.operand_stack.get(stack_idx).cloned().ok_or_else(|| LogicalFrameEndingReason::Panic("Internal error: validated local slot not found in stack".into()))?;
|
let val = self.operand_stack.get(stack_idx).cloned().ok_or_else(|| LogicalFrameEndingReason::Panic("Internal error: validated local slot not found in stack".into()))?;
|
||||||
@ -828,7 +740,8 @@ impl VirtualMachine {
|
|||||||
let frame = self.call_stack.last().ok_or_else(|| LogicalFrameEndingReason::Panic("No active call frame".into()))?;
|
let frame = self.call_stack.last().ok_or_else(|| LogicalFrameEndingReason::Panic("No active call frame".into()))?;
|
||||||
let func = &self.program.functions[frame.func_idx];
|
let func = &self.program.functions[frame.func_idx];
|
||||||
|
|
||||||
crate::virtual_machine::local_addressing::check_local_slot(func, slot, opcode as u16, start_pc as u32)?;
|
crate::virtual_machine::local_addressing::check_local_slot(func, slot, opcode as u16, start_pc as u32)
|
||||||
|
.map_err(|trap_info| self.trap(trap_info.code, trap_info.opcode, trap_info.message, trap_info.pc))?;
|
||||||
|
|
||||||
let stack_idx = crate::virtual_machine::local_addressing::local_index(frame, slot);
|
let stack_idx = crate::virtual_machine::local_addressing::local_index(frame, slot);
|
||||||
self.operand_stack[stack_idx] = val;
|
self.operand_stack[stack_idx] = val;
|
||||||
@ -836,12 +749,7 @@ impl VirtualMachine {
|
|||||||
OpCode::Call => {
|
OpCode::Call => {
|
||||||
let func_id = u32::from_le_bytes(instr.imm[0..4].try_into().unwrap()) as usize;
|
let func_id = u32::from_le_bytes(instr.imm[0..4].try_into().unwrap()) as usize;
|
||||||
let callee = self.program.functions.get(func_id).ok_or_else(|| {
|
let callee = self.program.functions.get(func_id).ok_or_else(|| {
|
||||||
LogicalFrameEndingReason::Trap(TrapInfo {
|
self.trap(TRAP_INVALID_FUNC, opcode as u16, format!("Invalid func_id {}", func_id), start_pc as u32)
|
||||||
code: TRAP_INVALID_FUNC,
|
|
||||||
opcode: opcode as u16,
|
|
||||||
message: format!("Invalid func_id {}", func_id),
|
|
||||||
pc: start_pc as u32,
|
|
||||||
})
|
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
if self.operand_stack.len() < callee.param_slots as usize {
|
if self.operand_stack.len() < callee.param_slots as usize {
|
||||||
@ -874,15 +782,10 @@ impl VirtualMachine {
|
|||||||
let expected_height = frame.stack_base + func.param_slots as usize + func.local_slots as usize + return_slots;
|
let expected_height = frame.stack_base + func.param_slots as usize + func.local_slots as usize + return_slots;
|
||||||
|
|
||||||
if current_height != expected_height {
|
if current_height != expected_height {
|
||||||
return Err(LogicalFrameEndingReason::Trap(TrapInfo {
|
return Err(self.trap(TRAP_BAD_RET_SLOTS, opcode as u16, format!(
|
||||||
code: TRAP_BAD_RET_SLOTS,
|
|
||||||
opcode: opcode as u16,
|
|
||||||
message: format!(
|
|
||||||
"Incorrect stack height at RET in func {}: expected {} slots (stack_base={} + params={} + locals={} + returns={}), got {}",
|
"Incorrect stack height at RET in func {}: expected {} slots (stack_base={} + params={} + locals={} + returns={}), got {}",
|
||||||
frame.func_idx, expected_height, frame.stack_base, func.param_slots, func.local_slots, return_slots, current_height
|
frame.func_idx, expected_height, frame.stack_base, func.param_slots, func.local_slots, return_slots, current_height
|
||||||
),
|
), start_pc as u32));
|
||||||
pc: start_pc as u32,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy return values (preserving order: pop return_slots values, then reverse to push back)
|
// Copy return values (preserving order: pop return_slots values, then reverse to push back)
|
||||||
@ -921,21 +824,11 @@ impl VirtualMachine {
|
|||||||
let ref_val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
|
let ref_val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
|
||||||
if let Value::Gate(base) = ref_val {
|
if let Value::Gate(base) = ref_val {
|
||||||
let val = self.heap.get(base + offset).cloned().ok_or_else(|| {
|
let val = self.heap.get(base + offset).cloned().ok_or_else(|| {
|
||||||
LogicalFrameEndingReason::Trap(TrapInfo {
|
self.trap(prometeu_bytecode::abi::TRAP_OOB, OpCode::GateLoad as u16, format!("Out-of-bounds heap access at offset {}", offset), start_pc as u32)
|
||||||
code: prometeu_bytecode::abi::TRAP_OOB,
|
|
||||||
opcode: OpCode::GateLoad as u16,
|
|
||||||
message: format!("Out-of-bounds heap access at offset {}", offset),
|
|
||||||
pc: start_pc as u32,
|
|
||||||
})
|
|
||||||
})?;
|
})?;
|
||||||
self.push(val);
|
self.push(val);
|
||||||
} else {
|
} else {
|
||||||
return Err(LogicalFrameEndingReason::Trap(TrapInfo {
|
return Err(self.trap(prometeu_bytecode::abi::TRAP_TYPE, OpCode::GateLoad as u16, "Expected gate handle for GATE_LOAD".to_string(), start_pc as u32));
|
||||||
code: prometeu_bytecode::abi::TRAP_TYPE,
|
|
||||||
opcode: OpCode::GateLoad as u16,
|
|
||||||
message: "Expected gate handle for GATE_LOAD".to_string(),
|
|
||||||
pc: start_pc as u32,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
OpCode::GateStore => {
|
OpCode::GateStore => {
|
||||||
@ -944,21 +837,11 @@ impl VirtualMachine {
|
|||||||
let ref_val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
|
let ref_val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
|
||||||
if let Value::Gate(base) = ref_val {
|
if let Value::Gate(base) = ref_val {
|
||||||
if base + offset >= self.heap.len() {
|
if base + offset >= self.heap.len() {
|
||||||
return Err(LogicalFrameEndingReason::Trap(TrapInfo {
|
return Err(self.trap(prometeu_bytecode::abi::TRAP_OOB, OpCode::GateStore as u16, format!("Out-of-bounds heap access at offset {}", offset), start_pc as u32));
|
||||||
code: prometeu_bytecode::abi::TRAP_OOB,
|
|
||||||
opcode: OpCode::GateStore as u16,
|
|
||||||
message: format!("Out-of-bounds heap access at offset {}", offset),
|
|
||||||
pc: start_pc as u32,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
self.heap[base + offset] = val;
|
self.heap[base + offset] = val;
|
||||||
} else {
|
} else {
|
||||||
return Err(LogicalFrameEndingReason::Trap(TrapInfo {
|
return Err(self.trap(prometeu_bytecode::abi::TRAP_TYPE, OpCode::GateStore as u16, "Expected gate handle for GATE_STORE".to_string(), start_pc as u32));
|
||||||
code: prometeu_bytecode::abi::TRAP_TYPE,
|
|
||||||
opcode: OpCode::GateStore as u16,
|
|
||||||
message: "Expected gate handle for GATE_STORE".to_string(),
|
|
||||||
pc: start_pc as u32,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
OpCode::GateBeginPeek | OpCode::GateEndPeek |
|
OpCode::GateBeginPeek | OpCode::GateEndPeek |
|
||||||
@ -975,12 +858,7 @@ impl VirtualMachine {
|
|||||||
let id = u32::from_le_bytes(instr.imm[0..4].try_into().unwrap());
|
let id = u32::from_le_bytes(instr.imm[0..4].try_into().unwrap());
|
||||||
|
|
||||||
let syscall = crate::hardware::syscalls::Syscall::from_u32(id).ok_or_else(|| {
|
let syscall = crate::hardware::syscalls::Syscall::from_u32(id).ok_or_else(|| {
|
||||||
LogicalFrameEndingReason::Trap(TrapInfo {
|
self.trap(prometeu_bytecode::abi::TRAP_INVALID_SYSCALL, OpCode::Syscall as u16, format!("Unknown syscall: 0x{:08X}", id), pc_at_syscall)
|
||||||
code: prometeu_bytecode::abi::TRAP_INVALID_SYSCALL,
|
|
||||||
opcode: OpCode::Syscall as u16,
|
|
||||||
message: format!("Unknown syscall: 0x{:08X}", id),
|
|
||||||
pc: pc_at_syscall,
|
|
||||||
})
|
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let args_count = syscall.args_count();
|
let args_count = syscall.args_count();
|
||||||
@ -988,12 +866,7 @@ impl VirtualMachine {
|
|||||||
let mut args = Vec::with_capacity(args_count);
|
let mut args = Vec::with_capacity(args_count);
|
||||||
for _ in 0..args_count {
|
for _ in 0..args_count {
|
||||||
let v = self.pop().map_err(|_e| {
|
let v = self.pop().map_err(|_e| {
|
||||||
LogicalFrameEndingReason::Trap(TrapInfo {
|
self.trap(prometeu_bytecode::abi::TRAP_STACK_UNDERFLOW, OpCode::Syscall as u16, "Syscall argument stack underflow".to_string(), pc_at_syscall)
|
||||||
code: prometeu_bytecode::abi::TRAP_STACK_UNDERFLOW,
|
|
||||||
opcode: OpCode::Syscall as u16,
|
|
||||||
message: "Syscall argument stack underflow".to_string(),
|
|
||||||
pc: pc_at_syscall,
|
|
||||||
})
|
|
||||||
})?;
|
})?;
|
||||||
args.push(v);
|
args.push(v);
|
||||||
}
|
}
|
||||||
@ -1002,12 +875,7 @@ impl VirtualMachine {
|
|||||||
let stack_height_before = self.operand_stack.len();
|
let stack_height_before = self.operand_stack.len();
|
||||||
let mut ret = crate::virtual_machine::HostReturn::new(&mut self.operand_stack);
|
let mut ret = crate::virtual_machine::HostReturn::new(&mut self.operand_stack);
|
||||||
native.syscall(id, &args, &mut ret, hw).map_err(|fault| match fault {
|
native.syscall(id, &args, &mut ret, hw).map_err(|fault| match fault {
|
||||||
crate::virtual_machine::VmFault::Trap(code, msg) => LogicalFrameEndingReason::Trap(TrapInfo {
|
crate::virtual_machine::VmFault::Trap(code, msg) => self.trap(code, OpCode::Syscall as u16, msg, pc_at_syscall),
|
||||||
code,
|
|
||||||
opcode: OpCode::Syscall as u16,
|
|
||||||
message: msg,
|
|
||||||
pc: pc_at_syscall,
|
|
||||||
}),
|
|
||||||
crate::virtual_machine::VmFault::Panic(msg) => LogicalFrameEndingReason::Panic(msg),
|
crate::virtual_machine::VmFault::Panic(msg) => LogicalFrameEndingReason::Panic(msg),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
@ -1030,6 +898,10 @@ impl VirtualMachine {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn trap(&self, code: u32, opcode: u16, message: String, pc: u32) -> LogicalFrameEndingReason {
|
||||||
|
LogicalFrameEndingReason::Trap(self.program.create_trap(code, opcode, message, pc))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn push(&mut self, val: Value) {
|
pub fn push(&mut self, val: Value) {
|
||||||
self.operand_stack.push(val);
|
self.operand_stack.push(val);
|
||||||
}
|
}
|
||||||
@ -1055,15 +927,20 @@ impl VirtualMachine {
|
|||||||
self.operand_stack.last().ok_or("Stack underflow".into())
|
self.operand_stack.last().ok_or("Stack underflow".into())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn binary_op<F>(&mut self, f: F) -> Result<(), LogicalFrameEndingReason>
|
fn binary_op<F>(&mut self, opcode: OpCode, start_pc: u32, f: F) -> Result<(), LogicalFrameEndingReason>
|
||||||
where
|
where
|
||||||
F: FnOnce(Value, Value) -> Result<Value, LogicalFrameEndingReason>,
|
F: FnOnce(Value, Value) -> Result<Value, OpError>,
|
||||||
{
|
{
|
||||||
let b = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
|
let b = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
|
||||||
let a = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
|
let a = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
|
||||||
let res = f(a, b)?;
|
match f(a, b) {
|
||||||
self.push(res);
|
Ok(res) => {
|
||||||
Ok(())
|
self.push(res);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(OpError::Trap(code, msg)) => Err(self.trap(code, opcode as u16, msg, start_pc)),
|
||||||
|
Err(OpError::Panic(msg)) => Err(LogicalFrameEndingReason::Panic(msg)),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1071,6 +948,7 @@ impl VirtualMachine {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use prometeu_bytecode::v0::FunctionMeta;
|
use prometeu_bytecode::v0::FunctionMeta;
|
||||||
|
use prometeu_bytecode::abi::SourceSpan;
|
||||||
use crate::hardware::HardwareBridge;
|
use crate::hardware::HardwareBridge;
|
||||||
use crate::virtual_machine::{Value, HostReturn, VmFault, expect_int};
|
use crate::virtual_machine::{Value, HostReturn, VmFault, expect_int};
|
||||||
|
|
||||||
@ -1334,7 +1212,7 @@ mod tests {
|
|||||||
];
|
];
|
||||||
|
|
||||||
let mut vm = VirtualMachine {
|
let mut vm = VirtualMachine {
|
||||||
program: Program::new(rom, vec![], functions),
|
program: ProgramImage::new(rom, vec![], functions, None),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
vm.prepare_call("0");
|
vm.prepare_call("0");
|
||||||
@ -1379,7 +1257,7 @@ mod tests {
|
|||||||
];
|
];
|
||||||
|
|
||||||
let mut vm = VirtualMachine {
|
let mut vm = VirtualMachine {
|
||||||
program: Program::new(rom, vec![], functions),
|
program: ProgramImage::new(rom, vec![], functions, None),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
vm.prepare_call("0");
|
vm.prepare_call("0");
|
||||||
@ -1418,7 +1296,7 @@ mod tests {
|
|||||||
];
|
];
|
||||||
|
|
||||||
let mut vm2 = VirtualMachine {
|
let mut vm2 = VirtualMachine {
|
||||||
program: Program::new(rom2, vec![], functions2),
|
program: ProgramImage::new(rom2, vec![], functions2, None),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
vm2.prepare_call("0");
|
vm2.prepare_call("0");
|
||||||
@ -1527,7 +1405,7 @@ mod tests {
|
|||||||
];
|
];
|
||||||
|
|
||||||
let mut vm = VirtualMachine {
|
let mut vm = VirtualMachine {
|
||||||
program: Program::new(rom, vec![], functions),
|
program: ProgramImage::new(rom, vec![], functions, None),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
vm.prepare_call("0");
|
vm.prepare_call("0");
|
||||||
@ -2604,4 +2482,93 @@ mod tests {
|
|||||||
_ => panic!("Expected Trap, got {:?}", report.reason),
|
_ => panic!("Expected Trap, got {:?}", report.reason),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_traceable_trap_with_span() {
|
||||||
|
let mut rom = Vec::new();
|
||||||
|
// 0: PUSH_I32 10 (6 bytes)
|
||||||
|
// 6: PUSH_I32 0 (6 bytes)
|
||||||
|
// 12: DIV (2 bytes)
|
||||||
|
rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes());
|
||||||
|
rom.extend_from_slice(&10i32.to_le_bytes());
|
||||||
|
rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes());
|
||||||
|
rom.extend_from_slice(&0i32.to_le_bytes());
|
||||||
|
rom.extend_from_slice(&(OpCode::Div as u16).to_le_bytes());
|
||||||
|
|
||||||
|
let mut pc_to_span = Vec::new();
|
||||||
|
pc_to_span.push((0, SourceSpan { file_id: 1, start: 10, end: 15 }));
|
||||||
|
pc_to_span.push((6, SourceSpan { file_id: 1, start: 16, end: 20 }));
|
||||||
|
pc_to_span.push((12, SourceSpan { file_id: 1, start: 21, end: 25 }));
|
||||||
|
|
||||||
|
let debug_info = prometeu_bytecode::v0::DebugInfo {
|
||||||
|
pc_to_span,
|
||||||
|
function_names: vec![(0, "main".to_string())],
|
||||||
|
};
|
||||||
|
|
||||||
|
let program = ProgramImage::new(rom, vec![], vec![], Some(debug_info));
|
||||||
|
let mut vm = VirtualMachine {
|
||||||
|
program,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut native = MockNative;
|
||||||
|
let mut hw = MockHardware;
|
||||||
|
|
||||||
|
vm.prepare_call("0");
|
||||||
|
let report = vm.run_budget(100, &mut native, &mut hw).unwrap();
|
||||||
|
match report.reason {
|
||||||
|
LogicalFrameEndingReason::Trap(trap) => {
|
||||||
|
assert_eq!(trap.code, TRAP_DIV_ZERO);
|
||||||
|
assert_eq!(trap.pc, 12);
|
||||||
|
assert_eq!(trap.span, Some(SourceSpan { file_id: 1, start: 21, end: 25 }));
|
||||||
|
}
|
||||||
|
_ => panic!("Expected Trap, got {:?}", report.reason),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_traceable_trap_with_function_name() {
|
||||||
|
let mut rom = Vec::new();
|
||||||
|
// 0: PUSH_I32 10 (6 bytes)
|
||||||
|
// 6: PUSH_I32 0 (6 bytes)
|
||||||
|
// 12: DIV (2 bytes)
|
||||||
|
rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes());
|
||||||
|
rom.extend_from_slice(&10i32.to_le_bytes());
|
||||||
|
rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes());
|
||||||
|
rom.extend_from_slice(&0i32.to_le_bytes());
|
||||||
|
rom.extend_from_slice(&(OpCode::Div as u16).to_le_bytes());
|
||||||
|
|
||||||
|
let pc_to_span = vec![(12, SourceSpan { file_id: 1, start: 21, end: 25 })];
|
||||||
|
let function_names = vec![(0, "math_utils::divide".to_string())];
|
||||||
|
|
||||||
|
let debug_info = prometeu_bytecode::v0::DebugInfo {
|
||||||
|
pc_to_span,
|
||||||
|
function_names,
|
||||||
|
};
|
||||||
|
|
||||||
|
let functions = vec![FunctionMeta {
|
||||||
|
code_offset: 0,
|
||||||
|
code_len: rom.len() as u32,
|
||||||
|
..Default::default()
|
||||||
|
}];
|
||||||
|
|
||||||
|
let program = ProgramImage::new(rom, vec![], functions, Some(debug_info));
|
||||||
|
let mut vm = VirtualMachine {
|
||||||
|
program,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut native = MockNative;
|
||||||
|
let mut hw = MockHardware;
|
||||||
|
|
||||||
|
vm.prepare_call("0");
|
||||||
|
let report = vm.run_budget(100, &mut native, &mut hw).unwrap();
|
||||||
|
match report.reason {
|
||||||
|
LogicalFrameEndingReason::Trap(trap) => {
|
||||||
|
assert_eq!(trap.code, TRAP_DIV_ZERO);
|
||||||
|
assert!(trap.message.contains("math_utils::divide"));
|
||||||
|
}
|
||||||
|
_ => panic!("Expected Trap, got {:?}", report.reason),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,70 +1,3 @@
|
|||||||
## PR-10 — Program image + linker: imports/exports resolved before VM run
|
|
||||||
|
|
||||||
**Why:** Imports are compile-time, but we need an industrial linking model for multi-module PBS.
|
|
||||||
|
|
||||||
### Scope
|
|
||||||
|
|
||||||
* Define in bytecode:
|
|
||||||
|
|
||||||
* `exports`: symbol -> func_id/service entry (as needed)
|
|
||||||
* `imports`: symbol refs -> relocation slots
|
|
||||||
* Implement a **linker** that:
|
|
||||||
|
|
||||||
* builds a `ProgramImage` from N modules
|
|
||||||
* resolves imports to exports
|
|
||||||
* produces a single final `FunctionTable` and code blob
|
|
||||||
|
|
||||||
### Notes
|
|
||||||
|
|
||||||
* VM **does not** do name lookup at runtime.
|
|
||||||
* Linking errors are deterministic: `LINK_UNRESOLVED_SYMBOL`, `LINK_DUP_EXPORT`, etc.
|
|
||||||
|
|
||||||
### Tests
|
|
||||||
|
|
||||||
* two-module link success
|
|
||||||
* unresolved import fails
|
|
||||||
* duplicate export fails
|
|
||||||
|
|
||||||
### Acceptance
|
|
||||||
|
|
||||||
* Multi-module PBS works; “import” is operationalized correctly.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## PR-11 — Canonical integration cartridge + golden bytecode snapshots
|
|
||||||
|
|
||||||
**Why:** One cartridge must be the unbreakable reference.
|
|
||||||
|
|
||||||
### Scope
|
|
||||||
|
|
||||||
* Create `CartridgeCanonical.pbs` that covers:
|
|
||||||
|
|
||||||
* locals
|
|
||||||
* arithmetic
|
|
||||||
* if
|
|
||||||
* function call
|
|
||||||
* syscall clear
|
|
||||||
* input snapshot
|
|
||||||
* Add `golden` artifacts:
|
|
||||||
|
|
||||||
* canonical AST JSON (frontend)
|
|
||||||
* IR Core (optional)
|
|
||||||
* IR VM / bytecode dump
|
|
||||||
* expected VM trace (optional)
|
|
||||||
|
|
||||||
### Tests
|
|
||||||
|
|
||||||
* CI runs cartridge and checks:
|
|
||||||
|
|
||||||
* no traps
|
|
||||||
* deterministic output state
|
|
||||||
|
|
||||||
### Acceptance
|
|
||||||
|
|
||||||
* This cartridge is the “VM heartbeat test”.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## PR-12 — VM test harness: stepper, trace, and property tests
|
## PR-12 — VM test harness: stepper, trace, and property tests
|
||||||
|
|
||||||
**Why:** Industrial quality means test tooling, not just “it runs”.
|
**Why:** Industrial quality means test tooling, not just “it runs”.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user