fogtix/crates/fogtix-bytecode/src/instr.rs

178 lines
6 KiB
Rust

use crate::consts::{MathBinOp, MathUnOp, OpType};
use core::fmt;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Instr {
// control flow
/// pops the descriptor table pointer from the stack,
/// then hands execution off to the descriptor table
CallRemote(u16),
/// calls the destination $0 inside of the current module
CallLocal(u64),
/// calls the destination $0 inside of the current module after the
/// next `Return`, by inserting the call into the call stack,
/// but continuing execution normally.
/// A classic `Jump(i)` is then just `[CallLDefer(i), Return]`.
CallLDefer(u64),
/// pops the top stack value and if it is non-zero,
/// jumps to the specified destination inside of the current module
JumpCond(u64),
/// pops the current call frame
Return,
// basic stack operations
/// pushes the given value to the stack
Push(u64),
/// pops the top $0+1 values from the stack
Pop(u16),
/// duplicates the top-$0 stack value, pushing it to the top of the stack
Dup(u16),
/// swaps the top stack value with the top-1-$0 stack value
/// (`Swap(0)` swaps the two top-most stack values)
Swap(u16),
/// shifts the top stack value by $0 ($0 is a signed integer)
Shift(i8),
// indirect stack operations
/// duplicates the top-(*top) stack value, pushing it to the top of the stack
/// this does not change the stack height
/// (the top value is replaced by the value it points to)
DupFrom,
/// basic math operations
DoMath1(MathUnOp),
/// binary math operations, stack top is like: `... lhs rhs $`
DoMath2(MathBinOp),
}
impl Instr {
#[inline]
pub fn typ(&self) -> OpType {
match self {
Instr::CallRemote(_) => OpType::CallRemote,
Instr::CallLocal(_) => OpType::CallLocal,
Instr::CallLDefer(_) => OpType::CallLDefer,
Instr::JumpCond(_) => OpType::JumpCond,
Instr::Return => OpType::Return,
Instr::Push(_) => OpType::Push,
Instr::Pop(_) => OpType::Pop,
Instr::Dup(_) => OpType::Dup,
Instr::Swap(_) => OpType::Swap,
Instr::Shift(_) => OpType::Shift,
Instr::DupFrom => OpType::DupFrom,
Instr::DoMath1(_) => OpType::DoMath1,
Instr::DoMath2(_) => OpType::DoMath2,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ParseError;
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "unable to parse instruction from buffer")
}
}
#[cfg(feature = "std")]
impl std::error::Error for ParseError {}
impl From<()> for ParseError {
fn from(_: ()) -> Self {
Self
}
}
impl<'a> crate::Parse<'a> for Instr {
type Err = ParseError;
fn parse(inp: &'a [u8]) -> Result<(&'a [u8], Self), ParseError> {
if inp.is_empty() {
return Err(ParseError);
}
let (inp, otyp) = OpType::parse(inp)?;
use crate::ParseExt;
match otyp {
OpType::CallRemote => u16::parse_map(inp, Instr::CallRemote),
OpType::CallLocal => u64::parse_map(inp, Instr::CallLocal),
OpType::CallLDefer => u64::parse_map(inp, Instr::CallLDefer),
OpType::JumpCond => u64::parse_map(inp, Instr::JumpCond),
OpType::Push => u64::parse_map(inp, Instr::Push),
OpType::Pop => u16::parse_map(inp, Instr::Pop),
OpType::Dup => u16::parse_map(inp, Instr::Dup),
OpType::Swap => u16::parse_map(inp, Instr::Swap),
OpType::Shift => i8::parse_map(inp, Instr::Shift),
OpType::DoMath1 => MathUnOp::parse_map(inp, Instr::DoMath1),
OpType::DoMath2 => MathBinOp::parse_map(inp, Instr::DoMath2),
OpType::Return => Ok((inp, Instr::Return)),
OpType::DupFrom => Ok((inp, Instr::DupFrom)),
}
.map_err(|()| ParseError)
}
}
#[cfg(any(test, feature = "std"))]
impl Instr {
pub fn write_to<W: std::io::Write>(&self, mut writer: W) -> std::io::Result<()> {
use int_enum::IntEnum;
writer.write_all(&self.typ().int_value().to_be_bytes())?;
match self {
Instr::CallRemote(val) => writer.write_all(&val.to_be_bytes()),
Instr::CallLocal(val) | Instr::CallLDefer(val) | Instr::JumpCond(val) => {
writer.write_all(&val.to_be_bytes())
}
Instr::Push(val) => writer.write_all(&val.to_be_bytes()),
Instr::Pop(val) | Instr::Dup(val) | Instr::Swap(val) => {
writer.write_all(&val.to_be_bytes())
}
Instr::Shift(shv) => writer.write_all(&shv.to_be_bytes()),
Instr::DoMath1(val) => writer.write_all(&val.int_value().to_be_bytes()),
Instr::DoMath2(val) => writer.write_all(&val.int_value().to_be_bytes()),
Instr::Return | Instr::DupFrom => Ok(()),
}
}
}
pub fn next_instr(m: &[u8], pos: usize) -> Option<Result<(usize, Instr), &[u8]>> {
use crate::Parse;
m.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)),
})
}
pub fn is_call2jump(m: &[u8], pos: usize) -> bool {
// tail-call optimization (would otherwise require much more opcodes)
matches!(next_instr(m, pos), Some(Ok((_, Instr::Return))))
}
#[cfg(test)]
mod tests {
use super::*;
proptest::proptest! {
#![proptest_config(proptest::prelude::ProptestConfig::with_cases(4096))]
#[test]
fn doesnt_crash(inp in proptest::collection::vec(0..=u8::MAX, 0..1024)) {
let mut inp = &inp[..];
while let Ok((xinp, v)) = <Instr as crate::Parse<'_>>::parse(inp) {
let mut buf = alloc::vec::Vec::with_capacity(inp.len() - xinp.len());
v.write_to(&mut buf).unwrap();
assert_eq!(buf[..], inp[..inp.len() - xinp.len()]);
inp = xinp;
}
}
}
}