#[cfg(not(feature = "std"))]
use alloc::string::String;
#[cfg(feature = "std")]
use miette::{Diagnostic, NamedSource, SourceSpan};
use thiserror::Error;
#[cfg(feature = "std")]
use crate::bytecode::Program;
#[cfg(feature = "std")]
use crate::disasm::Disassembly;
#[derive(Debug, Error)]
#[expect(
missing_docs,
reason = "error variants are documented via their #[error(...)] display strings"
)]
pub enum Error {
#[error("stack underflow at byte {pos:#06x}")]
StackUnderflow { pos: usize },
#[error("stack overflow at byte {pos:#06x} (limit: 8192)")]
StackOverflow { pos: usize },
#[error("register r{reg} holds {got}, expected {expected}")]
RegisterType {
reg: u8,
expected: &'static str,
got: &'static str,
},
#[error("{0}")]
IncompatibleType(crate::value::IncompatibleTypeError),
#[error("register r{reg} is unset at byte {pos:#06x}")]
UnsetRegister { pos: usize, reg: u8 },
#[error("division by zero at byte {pos:#06x}")]
DivisionByZero { pos: usize },
#[error("vec index {index} out of bounds (len {len}) at byte {pos:#06x}")]
IndexOutOfBounds { pos: usize, index: i64, len: usize },
#[error("loop instruction at byte {pos:#06x} with no active loop")]
NoActiveLoop { pos: usize },
#[error("jump from {pos:#06x} to {target:#010x} is out of bounds")]
BadJumpTarget { pos: usize, target: usize },
#[error("invalid label .{label} at byte {pos:#06x}")]
InvalidLabel { pos: usize, label: u16 },
#[error("bad opcode {byte:#04x} at byte {pos:#06x}")]
BadOpcode { pos: usize, byte: u8 },
#[error("truncated instruction at byte {pos:#06x}")]
TruncatedInstruction { pos: usize },
#[error("calldata index {index} out of range (len {len})")]
CallDataIndex { index: i64, len: usize },
#[error("output index {index} out of range (len {len})")]
OutputIndex { index: i64, len: usize },
#[error("model has {model_size} variables but sample has {sample_len}")]
SizeMismatch {
model_size: usize,
sample_len: usize,
},
#[error("vector length mismatch: {what} has {a} entries but {other} has {b}")]
VecLengthMismatch {
what: &'static str,
a: usize,
other: &'static str,
b: usize,
},
#[error("step limit of {limit} exceeded")]
StepLimitExceeded { limit: u64 },
#[error("invalid shift amount {amount} at byte {pos:#06x}")]
InvalidShift { pos: usize, amount: i64 },
#[error("invalid grid dimensions {rows}x{cols} at byte {pos:#06x}")]
InvalidGridDimensions { pos: usize, rows: i64, cols: i64 },
#[error("XQMX/XSMX requires k >= 2 for the [-k, k-1] domain, got k = {k} at byte {pos:#06x}")]
InvalidDiscreteK { pos: usize, k: i64 },
#[error("unmatched RANGE/ITER at byte {pos:#06x}: no matching NEXT found")]
UnmatchedLoop { pos: usize },
#[error("trace failed at byte {pos:#06x}: {message}")]
TraceFailed { pos: usize, message: String },
}
#[cfg(feature = "std")]
impl Error {
pub fn into_diagnostic(self, program: &Program, name: &str) -> RuntimeDiagnostic {
let disasm_text = Disassembly::from_program(program).to_string();
let span = self
.byte_pos()
.and_then(|pos| find_line_span(&disasm_text, pos));
RuntimeDiagnostic {
inner: self,
disasm: NamedSource::new(name, disasm_text),
span,
}
}
fn byte_pos(&self) -> Option<usize> {
match self {
Self::StackUnderflow { pos }
| Self::StackOverflow { pos }
| Self::DivisionByZero { pos }
| Self::NoActiveLoop { pos }
| Self::BadOpcode { pos, .. }
| Self::TruncatedInstruction { pos }
| Self::InvalidShift { pos, .. }
| Self::InvalidGridDimensions { pos, .. }
| Self::InvalidDiscreteK { pos, .. }
| Self::UnmatchedLoop { pos }
| Self::TraceFailed { pos, .. }
| Self::BadJumpTarget { pos, .. }
| Self::InvalidLabel { pos, .. }
| Self::UnsetRegister { pos, .. }
| Self::IndexOutOfBounds { pos, .. } => Some(*pos),
Self::RegisterType { .. }
| Self::IncompatibleType(_)
| Self::CallDataIndex { .. }
| Self::OutputIndex { .. }
| Self::SizeMismatch { .. }
| Self::VecLengthMismatch { .. }
| Self::StepLimitExceeded { .. } => None,
}
}
}
impl From<crate::value::IncompatibleTypeError> for Error {
fn from(e: crate::value::IncompatibleTypeError) -> Self {
Self::IncompatibleType(e)
}
}
impl From<crate::bytecode::error::StreamError> for Error {
fn from(e: crate::bytecode::error::StreamError) -> Self {
use crate::bytecode::error::StreamError as SE;
match e {
SE::UnknownOpcode { offset, byte } => Self::BadOpcode { pos: offset, byte },
SE::TruncatedInstruction { offset } => Self::TruncatedInstruction { pos: offset },
SE::SeekOutOfBounds { target, .. } => Self::BadJumpTarget { pos: 0, target },
}
}
}
#[cfg(feature = "std")]
#[derive(Debug, Error, Diagnostic)]
#[error("{inner}")]
#[diagnostic(code(xqvm::runtime_error))]
pub struct RuntimeDiagnostic {
inner: Error,
#[source_code]
disasm: NamedSource<String>,
#[label("execution failed here")]
span: Option<SourceSpan>,
}
#[cfg(feature = "std")]
fn find_line_span(text: &str, byte_pos: usize) -> Option<SourceSpan> {
let needle = format!("0x{byte_pos:04X}:");
let match_start = text.find(&needle)?;
let line_start = text[..match_start].rfind('\n').map_or(0, |n| n + 1);
let line_end = text[match_start..]
.find('\n')
.map_or(text.len(), |n| match_start + n);
Some(SourceSpan::from(line_start..line_end))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::value::{IncompatibleTypeError, RegValKind};
#[test]
fn incompatible_type_from_impl_round_trips() {
let inner = IncompatibleTypeError {
expected: &[RegValKind::Model],
actual: RegValKind::Int,
};
let err = Error::from(inner.clone());
assert!(
matches!(err, Error::IncompatibleType(ref e) if e == &inner),
"From<IncompatibleTypeError> should produce Error::IncompatibleType"
);
assert_eq!(
err.to_string(),
"incompatible types: expected model, got int"
);
}
}