fogtix/crates/fogtix-vm/src/lib.rs

324 lines
11 KiB
Rust

#![no_std]
#![forbid(unsafe_code)]
extern crate alloc;
use alloc::{sync::Arc, vec::Vec};
use core::fmt;
use fogtix_bytecode::{consts, Instr, Parse};
pub type Module = Arc<dyn ModuleKind>;
pub trait ModuleKind: Send + Sync {
fn as_slice(&self) -> &[u8];
}
impl<T: AsRef<[u8]> + Send + Sync + ?Sized> ModuleKind for T {
#[inline(always)]
fn as_slice(&self) -> &[u8] {
self.as_ref()
}
}
#[derive(Clone)]
pub struct InstrPtr {
pub m: Module,
pub pos: usize,
}
impl fmt::Display for InstrPtr {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "@{:p}:{:x}", self.m, self.pos)
}
}
impl fmt::Debug for InstrPtr {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
<Self as fmt::Display>::fmt(self, f)
}
}
fn next_instr(m: &Module, pos: usize) -> Option<Result<(usize, Instr), &[u8]>> {
m.as_slice()
.get(pos..)
.map(|nxti_arr| match Instr::parse(nxti_arr) {
Err(_) => Err(&nxti_arr[..core::cmp::min(nxti_arr.len(), 20)]),
Ok((ptr, i)) => Ok((nxti_arr.len() - ptr.len(), i)),
})
}
impl InstrPtr {
#[inline(always)]
pub fn next_instr(&self) -> Option<Result<(usize, Instr), &[u8]>> {
next_instr(&self.m, self.pos)
}
fn is_call2jump(&self) -> bool {
// tail-call optimization (would otherwise require much more opcodes)
matches!(self.next_instr(), Some(Ok((_, Instr::Return))))
}
}
pub type StackEntValue = u64;
#[derive(Clone, Debug)]
pub enum Error {
InvalidJumpTarget {
invoked_by: &'static str,
from: InstrPtr,
to: InstrPtr,
},
InstrpOutOfBounds,
UnparsableInstruction(Vec<u8>),
OutOfFuel,
NotEnoughStacked,
DivisionByZero,
RemoteCall(u64),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidJumpTarget {
invoked_by,
from,
to,
} => {
write!(
f,
"`{}` arrived from `{}` => at non-jump target `{}`",
invoked_by, from, to
)
}
Self::InstrpOutOfBounds => write!(f, "instruction pointer out-of-bounds"),
Self::UnparsableInstruction(x) => write!(f, "reached unparsable instruction: {:x?}", x),
Self::OutOfFuel => write!(f, "out of fuel"),
Self::NotEnoughStacked => write!(f, "not enough operands on stack"),
Self::DivisionByZero => write!(f, "tried to divide by zero"),
Self::RemoteCall(ptr) => write!(f, "tried to call remote @ {:x?}", ptr),
}
}
}
pub struct Process {
pub stack: Vec<StackEntValue>,
pub callstack: Vec<InstrPtr>,
pub instrp: InstrPtr,
}
fn verify_jumptarget_explicit(
jinstr: &'static str,
from: &InstrPtr,
to: &InstrPtr,
) -> Result<(), Error> {
if to.pos == 0 {
Ok(())
} else if let Some(Ok((_, Instr::Label))) = to.next_instr() {
Ok(())
} else {
Err(Error::InvalidJumpTarget {
invoked_by: jinstr,
from: from.clone(),
to: to.clone(),
})
}
}
impl Process {
fn verify_jumptarget(&self, jinstr: &'static str, from: &InstrPtr) -> Result<(), Error> {
verify_jumptarget_explicit(jinstr, from, &self.instrp)
}
fn stpop(&mut self) -> Result<StackEntValue, Error> {
self.stack.pop().ok_or(Error::NotEnoughStacked)
}
pub fn run(&mut self, mut fuel: Option<&mut u64>) -> Result<(), Error> {
loop {
let previptr = self.instrp.pos;
tracing::trace!("previptr = {}", previptr);
if let Some(ref mut x) = fuel {
if **x == 0 {
return Err(Error::OutOfFuel);
}
**x -= 1;
}
let (nxtidelta, nxti) = match next_instr(&self.instrp.m, self.instrp.pos) {
None => return Err(Error::InstrpOutOfBounds),
Some(Err(code)) => return Err(Error::UnparsableInstruction(code.to_vec())),
Some(Ok(x)) => x,
};
self.instrp.pos += nxtidelta;
match nxti {
Instr::Label => {}
Instr::CallRemote => {
if !self.instrp.is_call2jump() {
self.callstack.push(self.instrp.clone());
}
return Err(Error::RemoteCall(self.stpop()?));
}
Instr::CallLocal(x) => {
if !self.instrp.is_call2jump() {
self.callstack.push(self.instrp.clone());
}
self.instrp.pos = match x.try_into() {
Ok(y) => y,
Err(_) => return Err(Error::InstrpOutOfBounds),
};
self.verify_jumptarget(
"call-l",
&InstrPtr {
m: self.instrp.m.clone(),
pos: previptr,
},
)?;
}
Instr::CallLDefer(x) => match x.try_into() {
Ok(pos) => {
let jtip = InstrPtr {
m: self.instrp.m.clone(),
pos,
};
verify_jumptarget_explicit(
"call-l-defer",
&InstrPtr {
m: self.instrp.m.clone(),
pos: previptr,
},
&jtip,
)?;
if self.instrp.is_call2jump() {
self.instrp = jtip;
} else {
self.callstack.push(jtip);
}
}
Err(_) => return Err(Error::InstrpOutOfBounds),
},
Instr::JumpCond(x) => {
let x: usize = match x.try_into() {
Ok(y) => y,
Err(_) => return Err(Error::InstrpOutOfBounds),
};
if self.stpop()? != 0 {
self.instrp.pos = x;
self.verify_jumptarget(
"jump-cond",
&InstrPtr {
m: self.instrp.m.clone(),
pos: previptr,
},
)?;
}
}
Instr::Return => match self.callstack.pop() {
Some(x) => self.instrp = x,
None => break Ok(()),
},
Instr::Push(v) => self.stack.push(v),
Instr::Pop(_) if self.stack.is_empty() => return Err(Error::NotEnoughStacked),
Instr::Pop(cnt) => {
let ssl = self.stack.len() - 1;
let cnt = usize::from(cnt);
if cnt >= ssl {
self.stack = Vec::new();
} else {
self.stack.truncate(ssl - cnt - 1);
}
}
Instr::Dup(delta) => {
let x = match self.stack.len().checked_sub(usize::from(delta) + 1) {
None => return Err(Error::NotEnoughStacked),
// SAFETY: the value x is always smaller than the stack height
Some(x) => self.stack[x],
};
self.stack.push(x);
}
Instr::Swap(delta) => {
let ssl = self.stack.len();
let (y, z) = match ssl.checked_sub(usize::from(delta) + 2) {
None => return Err(Error::NotEnoughStacked),
Some(ltrg) => self.stack[ltrg..].split_at_mut(1),
};
core::mem::swap(&mut y[0], &mut z[0]);
}
Instr::Shift(shdelta) => {
let x = self.stack.last_mut().ok_or(Error::NotEnoughStacked)?;
if shdelta < 0 {
*x <<= (-shdelta) as u8;
} else {
*x >>= (shdelta as u8) + 1;
}
}
Instr::DoMath1(ubo) => {
use consts::MathUnOp as U;
let x = self.stpop()?;
self.stack.push(match ubo {
U::Not => !x,
});
}
Instr::DoMath2(mbo) => {
let b = self.stpop()?;
let a = self.stpop()?;
use consts::MathBinOp as B;
self.stack.push(match mbo {
B::Lt if a < b => 1,
B::Lt => 0,
B::Eq if a == b => 1,
B::Eq => 0,
B::And => a & b,
B::Or => a | b,
B::Xor => a ^ b,
B::Add => a.wrapping_add(b),
B::Sub => a.wrapping_sub(b),
B::Mul => a.wrapping_mul(b),
B::Div => match a.checked_div(b) {
Some(x) => x,
None => return Err(Error::DivisionByZero),
},
B::Rem => match a.checked_rem(b) {
Some(x) => x,
None => return Err(Error::DivisionByZero),
},
B::Srem => match (a as i64).checked_rem(b as i64) {
Some(x) => x as u64,
None => return Err(Error::DivisionByZero),
},
});
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn stack_item_size() {
assert_eq!(core::mem::size_of::<StackEntValue>(), 8);
}
proptest::proptest! {
#![proptest_config(proptest::prelude::ProptestConfig::with_cases(8192))]
#[test]
fn doesnt_crash(inp in proptest::collection::vec(0..=u8::MAX, 0..1024)) {
let inp: Arc<Vec<u8>> = Arc::new(inp);
let module: Module = inp;
let mut p = Process {
stack: Vec::new(),
callstack: Vec::new(),
instrp: InstrPtr {
m: module,
pos: 0,
},
};
let _ = p.run(Some(&mut 2048));
}
}
}