diff --git a/Cargo.lock b/Cargo.lock index 6d29899f4f23..04a71a75c1d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2141,6 +2141,24 @@ dependencies = [ "cc", ] +[[package]] +name = "pulley-interpreter" +version = "0.1.0" +dependencies = [ + "arbitrary", + "env_logger", + "log", +] + +[[package]] +name = "pulley-interpreter-fuzz" +version = "0.0.0" +dependencies = [ + "env_logger", + "log", + "pulley-interpreter", +] + [[package]] name = "quick-error" version = "1.2.3" @@ -3717,6 +3735,7 @@ dependencies = [ "libfuzzer-sys", "once_cell", "proc-macro2", + "pulley-interpreter-fuzz", "quote", "rand", "smallvec", diff --git a/Cargo.toml b/Cargo.toml index 130227eae782..51d2279ac309 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -210,6 +210,7 @@ wasmtime-fuzzing = { path = "crates/fuzzing" } wasmtime-jit-icache-coherence = { path = "crates/jit-icache-coherence", version = "=24.0.0" } wasmtime-wit-bindgen = { path = "crates/wit-bindgen", version = "=24.0.0" } test-programs-artifacts = { path = 'crates/test-programs/artifacts' } +pulley-interpreter-fuzz = { path = 'pulley/fuzz' } cranelift-wasm = { path = "cranelift/wasm", version = "0.111.0" } cranelift-codegen = { path = "cranelift/codegen", version = "0.111.0", default-features = false, features = ["std", "unwind", "trace-log"] } diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index bba9c038352b..58d21c1b1e2e 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -24,6 +24,7 @@ cranelift-native = { workspace = true } cranelift-control = { workspace = true } libfuzzer-sys = { workspace = true, features = ["arbitrary-derive"] } target-lexicon = { workspace = true } +pulley-interpreter-fuzz = { workspace = true } smallvec = { workspace = true } wasmparser = { workspace = true } wasmtime = { workspace = true, features = ["winch"] } @@ -122,3 +123,10 @@ path = "fuzz_targets/memory_accesses.rs" test = false doc = false bench = false + +[[bin]] +name = "pulley" +path = "fuzz_targets/pulley.rs" +test = false +doc = false +bench = false diff --git a/fuzz/fuzz_targets/pulley.rs b/fuzz/fuzz_targets/pulley.rs new file mode 100644 index 000000000000..d3bebbff4ee3 --- /dev/null +++ b/fuzz/fuzz_targets/pulley.rs @@ -0,0 +1,21 @@ +#![no_main] + +use libfuzzer_sys::{arbitrary::*, fuzz_target}; +use pulley_interpreter_fuzz::{interp, roundtrip}; + +fuzz_target!(|data| { + let _ = fuzz(data); +}); + +fn fuzz(data: &[u8]) -> Result<()> { + let _ = env_logger::try_init(); + + let mut u = Unstructured::new(data); + match u.int_in_range(0..=1)? { + 0 => roundtrip(Arbitrary::arbitrary_take_rest(u)?), + 1 => interp(Arbitrary::arbitrary_take_rest(u)?), + _ => unreachable!(), + } + + Ok(()) +} diff --git a/pulley/Cargo.toml b/pulley/Cargo.toml new file mode 100644 index 000000000000..ff8c0dd5580c --- /dev/null +++ b/pulley/Cargo.toml @@ -0,0 +1,30 @@ +[package] +authors = ["The Pulley Project Developers"] +description = "The Pulley interpreter, its bytecode definition, encoder, decoder, and etc..." +edition.workspace = true +license = "Apache-2.0 WITH LLVM-exception" +name = "pulley-interpreter" +readme = "./README.md" +repository = "https://github.com/bytecodealliance/wasmtime/tree/main/pulley" +version = "0.1.0" + +[lints] +workspace = true + +[dependencies] +arbitrary = { workspace = true, optional = true } +log = { workspace = true } + +[dev-dependencies] +env_logger = { workspace = true } + +[features] +std = [] +arbitrary = ["dep:arbitrary", "arbitrary/derive", "std"] +encode = [] +decode = [] +disas = ["decode"] +interp = ["decode"] + +[package.metadata.docs.rs] +all-features = true diff --git a/pulley/README.md b/pulley/README.md new file mode 100644 index 000000000000..a20cde1da0b8 --- /dev/null +++ b/pulley/README.md @@ -0,0 +1,122 @@ +
+

Pulley

+ +

Portable, Universal, Low-Level Execution strategY

+ +

+ A portable bytecode and fast interpreter +

+ + A Bytecode Alliance project + +

+ build status + zulip chat + supported rustc stable + Documentation Status +

+ +

+ Chat +

+
+ +## About + +Pulley is a portable bytecode and fast interpreter for use in Wasmtime. + +Pulley's primary goal is portability and its secondary goal is fast +interpretation. + +Pulley is not intended to be a simple reference interpreter, support dynamically +switching to just-in-time compiled code, or even to be the very fastest +interpreter in the world. + +For more details on Pulley's motivation, goals, and non-goals, see [the Bytecode +Alliance RFC that originally proposed Pulley][rfc]. + +[rfc]: https://github.com/bytecodealliance/rfcs/blob/main/accepted/pulley.md + +## Status + +Pulley is very much still a work in progress! Expect the details of the bytecode +to change, instructions to appear and disappear, and APIs to be overhauled. + +## Example + +Here is the disassembly of `f(a, b) = a + b` in Pulley today: + +``` + 0: 11 1f f0 ff ff ff ff ff ff ff xconst64 x31, 18446744073709551600 + a: 12 20 20 1f xadd32 sp, sp, x31 + e: 32 20 08 21 store64_offset8 sp, 8, lr + 12: 30 20 22 store64 sp, fp + 15: 0b 22 20 xmov fp, sp + 18: 12 00 00 01 xadd32 x0, x0, x1 + 1c: 0b 20 22 xmov sp, fp + 1f: 25 21 20 08 load64_offset8 lr, sp, 8 + 23: 22 22 20 load64 fp, sp + 26: 0e 1f 10 xconst8 x31, 16 + 29: 12 20 20 1f xadd32 sp, sp, x31 + 2d: 00 ret +``` + +Note that there are a number of things that could be improved here: + +* We could avoid allocating a deallocating a stack frame because this function's + body doesn't use any stack slots. +* We could sign-extend, rather than zero-extend, constants so that `-16` has a + single-byte encoding instead of an eight-byte encoding. +* We could collapse the whole prologue and epilogue instruction sequences into + super-instructions, since they are identical (modulo the frame size immediate) + for all functions. + +As mentioned above, Pulley is very much a work in progress. + +## Principles + +What follows are some general, incomplete, and sometimes-conflicting principles +that we try and follow when designing the Pulley bytecode format and its +interpreter: + +* The bytecode should be simple and fast to decode in software. For example, we + should avoid overly-complicated bitpacking, and only reach for that kind of + thing when benchmarks and profiles show it to be of benefit. + +* The interpreter never materializes `enum Instruction { .. }` values. Instead, + it decodes immediates and operands as needed in each opcode handler. This + avoids constructing unnecessary temporary storage and branching on opcode + multiple times. + +* Because we never materialize `enum Instruction { .. }` values, we don't have + to worry about unused padding or one very-large instruction inflating the size + of all the rest of our small instructions. To put it concisely: we can lean + into a variable-length encoding where some instructions require only a single + byte and others require many. This helps keep the bytecode compact and + cache-efficient. + +* We lean into defining super-instructions (sometimes called "macro ops") that + perform the work of multiple operations in a single instruction. The more work + we do in each turn of the interpreter loop the less we are impacted by its + overhead. Additionally, Cranelift, as the primary Pulley bytecode producer, + can leverage ISLE lowering patterns to easily identify opportunites for + emitting super-instructions. + +* We do not, in general, define sub-opcodes. There should be only one branch, on + the initial opcode, when evaluating any given instruction. For example, we do + *not* have a generic `load` instruction that is followed by a sub-opcode to + discriminate between different addressing modes. Instead, we have many + different kinds of `load` instructions, one for each of our addressing modes. + + The one exception is the split between regular and extended ops. Regular ops + are a single `u8` opcode, where `255` is reserved for all extended ops, and a + `u16` opcode follows after the `255` regular opcode. This keeps the most + common instructions extra small, and provides a pressure release valve for + defining an unbounded number of additional, colder, ops. + +* We strive to cut down on boilerplate as much as possible, and try to avoid + matching on every opcode repeatedly throughout the whole code base. We do this + via heavy `macro_rules` usage where we define the bytecode inside a + higher-order macro and then automatically derive a disassembler, decoder, + encoder, etc... from that definition. This also avoids any kind of drift where + the encoder and decoder get out of sync with each other, for example. diff --git a/pulley/fuzz/.gitignore b/pulley/fuzz/.gitignore new file mode 100644 index 000000000000..1a45eee7760d --- /dev/null +++ b/pulley/fuzz/.gitignore @@ -0,0 +1,4 @@ +target +corpus +artifacts +coverage diff --git a/pulley/fuzz/Cargo.toml b/pulley/fuzz/Cargo.toml new file mode 100644 index 000000000000..095943bbd927 --- /dev/null +++ b/pulley/fuzz/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "pulley-interpreter-fuzz" +version = "0.0.0" +publish = false +edition.workspace = true + +[lints] +workspace = true + +[dependencies] +pulley-interpreter = { path = "..", features = ["encode", "decode", "disas", "interp", "arbitrary"] } +env_logger = { workspace = true } +log = { workspace = true } diff --git a/pulley/fuzz/src/interp.rs b/pulley/fuzz/src/interp.rs new file mode 100644 index 000000000000..1ef5ee10d079 --- /dev/null +++ b/pulley/fuzz/src/interp.rs @@ -0,0 +1,112 @@ +use pulley_interpreter::{ + interp::Vm, + op::{self, ExtendedOp, Op}, + *, +}; +use std::ptr::NonNull; + +pub fn interp(ops: Vec) { + let _ = env_logger::try_init(); + + log::trace!("input: {ops:#?}"); + + let mut ops = ops; + ops.retain(|op| op_is_safe_for_fuzzing(op)); + // Make sure that we end with a `ret` so that the interpreter returns + // control to us instead of continuing off the end of the ops and into + // undefined memory. + ops.push(Op::Ret(op::Ret {})); + + log::trace!("filtered to only safe ops: {ops:#?}"); + + let mut encoded = vec![]; + for op in &ops { + op.encode(&mut encoded); + } + log::trace!("encoded: {encoded:?}"); + + let mut vm = Vm::new(); + unsafe { + let args = &[]; + let rets = &[]; + match vm.call(NonNull::from(&encoded[0]), args, rets.into_iter().copied()) { + Ok(rets) => assert_eq!(rets.count(), 0), + Err(pc) => { + let pc = pc as usize; + + let start = &encoded[0] as *const u8 as usize; + let end = encoded.last().unwrap() as *const u8 as usize; + assert!( + start <= pc && pc < end, + "pc should be in range {start:#018x}..{end:#018x}, got {pc:#018x}" + ); + + let index = pc - start; + assert_eq!(encoded[index], Opcode::ExtendedOp as u8); + let [a, b] = (ExtendedOpcode::Trap as u16).to_le_bytes(); + assert_eq!(encoded[index + 1], a); + assert_eq!(encoded[index + 2], b); + } + }; + } +} + +fn op_is_safe_for_fuzzing(op: &Op) -> bool { + match op { + Op::Ret(_) => true, + Op::Jump(_) => false, + Op::BrIf(_) => false, + Op::BrIfNot(_) => false, + Op::BrIfXeq32(_) => false, + Op::BrIfXneq32(_) => false, + Op::BrIfXult32(_) => false, + Op::BrIfXulteq32(_) => false, + Op::BrIfXslt32(_) => false, + Op::BrIfXslteq32(_) => false, + Op::Xmov(op::Xmov { dst, .. }) => !dst.is_special(), + Op::Fmov(_) => true, + Op::Vmov(_) => true, + Op::Xconst8(op::Xconst8 { dst, .. }) => !dst.is_special(), + Op::Xconst16(op::Xconst16 { dst, .. }) => !dst.is_special(), + Op::Xconst32(op::Xconst32 { dst, .. }) => !dst.is_special(), + Op::Xconst64(op::Xconst64 { dst, .. }) => !dst.is_special(), + Op::Xadd32(op::Xadd32 { dst, .. }) => !dst.is_special(), + Op::Xadd64(op::Xadd64 { dst, .. }) => !dst.is_special(), + Op::Load32U(_) => false, + Op::Load32S(_) => false, + Op::Load64(_) => false, + Op::Load32UOffset8(_) => false, + Op::Load32SOffset8(_) => false, + Op::Load64Offset8(_) => false, + Op::Store32(_) => false, + Op::Store64(_) => false, + Op::Store32SOffset8(_) => false, + Op::Store64Offset8(_) => false, + Op::BitcastIntFromFloat32(op::BitcastIntFromFloat32 { dst, .. }) => !dst.is_special(), + Op::BitcastIntFromFloat64(op::BitcastIntFromFloat64 { dst, .. }) => !dst.is_special(), + Op::BitcastFloatFromInt32(_) => true, + Op::BitcastFloatFromInt64(_) => true, + Op::ExtendedOp(op) => extended_op_is_safe_for_fuzzing(op), + Op::Call(_) => false, + Op::Xeq64(Xeq64 { dst, .. }) => !dst.is_special(), + Op::Xneq64(Xneq64 { dst, .. }) => !dst.is_special(), + Op::Xslt64(Xslt64 { dst, .. }) => !dst.is_special(), + Op::Xslteq64(Xslteq64 { dst, .. }) => !dst.is_special(), + Op::Xult64(Xult64 { dst, .. }) => !dst.is_special(), + Op::Xulteq64(Xulteq64 { dst, .. }) => !dst.is_special(), + Op::Xeq32(Xeq32 { dst, .. }) => !dst.is_special(), + Op::Xneq32(Xneq32 { dst, .. }) => !dst.is_special(), + Op::Xslt32(Xslt32 { dst, .. }) => !dst.is_special(), + Op::Xslteq32(Xslteq32 { dst, .. }) => !dst.is_special(), + Op::Xult32(Xult32 { dst, .. }) => !dst.is_special(), + Op::Xulteq32(Xulteq32 { dst, .. }) => !dst.is_special(), + } +} + +fn extended_op_is_safe_for_fuzzing(op: &ExtendedOp) -> bool { + match op { + ExtendedOp::Trap(_) => true, + ExtendedOp::Nop(_) => true, + ExtendedOp::GetSp(_) => true, + } +} diff --git a/pulley/fuzz/src/lib.rs b/pulley/fuzz/src/lib.rs new file mode 100644 index 000000000000..b041d1676312 --- /dev/null +++ b/pulley/fuzz/src/lib.rs @@ -0,0 +1,5 @@ +mod roundtrip; +pub use roundtrip::*; + +mod interp; +pub use interp::*; diff --git a/pulley/fuzz/src/roundtrip.rs b/pulley/fuzz/src/roundtrip.rs new file mode 100644 index 000000000000..3f88db1b7dd8 --- /dev/null +++ b/pulley/fuzz/src/roundtrip.rs @@ -0,0 +1,25 @@ +use pulley_interpreter::{ + decode::{Decoder, SafeBytecodeStream}, + op::{MaterializeOpsVisitor, Op}, +}; + +pub fn roundtrip(ops: Vec) { + let _ = env_logger::try_init(); + + log::trace!("input: {ops:#?}"); + + let mut encoded = vec![]; + for op in &ops { + op.encode(&mut encoded); + } + log::trace!("encoded: {encoded:?}"); + + let mut materializer = MaterializeOpsVisitor::new(SafeBytecodeStream::new(&encoded)); + let decoded = Decoder::decode_all(&mut materializer).expect("should decode okay"); + log::trace!("decoded: {decoded:#?}"); + + assert_eq!( + decoded, ops, + "`decode(encode(ops))` should be equal to the original `ops`" + ); +} diff --git a/pulley/src/decode.rs b/pulley/src/decode.rs new file mode 100644 index 000000000000..9d609ca44f8c --- /dev/null +++ b/pulley/src/decode.rs @@ -0,0 +1,628 @@ +//! Decoding support for pulley bytecode. + +use alloc::vec::Vec; + +use crate::imms::*; +use crate::opcode::*; +use crate::regs::*; + +/// Either an `Ok(T)` or an `Err(DecodingError)`. +pub type Result = core::result::Result; + +/// An error when decoding Pulley bytecode. +pub enum DecodingError { + /// Reached the end of the bytecode stream before we finished decoding a + /// single bytecode. + UnexpectedEof { + /// The position in the bytecode stream where this error occurred. + position: usize, + }, + + /// Found an invalid opcode. + InvalidOpcode { + /// The position in the bytecode stream where this error occurred. + position: usize, + /// The invalid opcode that was found. + code: u8, + }, + + /// Found an invalid extended opcode. + InvalidExtendedOpcode { + /// The position in the bytecode stream where this error occurred. + position: usize, + /// The invalid extended opcode that was found. + code: u16, + }, + + /// Found an invalid register. + InvalidReg { + /// The position in the bytecode stream where this error occurred. + position: usize, + /// The invalid register that was found. + reg: u8, + }, +} + +impl core::fmt::Debug for DecodingError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + core::fmt::Display::fmt(self, f) + } +} + +impl core::fmt::Display for DecodingError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::UnexpectedEof { position } => { + write!(f, "unexpected end-of-file at bytecode offset {position:#x}") + } + Self::InvalidOpcode { position, code } => { + write!( + f, + "found invalid opcode {code:#x} at bytecode offset {position:#x}" + ) + } + Self::InvalidExtendedOpcode { position, code } => { + write!( + f, + "found invalid opcode {code:#x} at bytecode offset {position:#x}" + ) + } + Self::InvalidReg { position, reg } => { + write!( + f, + "found invalid register {reg:#x} at bytecode offset {position:#x}" + ) + } + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for DecodingError {} + +/// An abstraction over any kind of bytecode stream. +/// +/// There are two primary implementations: +/// +/// 1. `SafeBytecodeStream`: A thin wrapper around an index into a `&[u8]`. This +/// implementation is 100% safe code. +/// +/// 2. `UnsafeBytecodeStream`: A thin wrapper over a raw pointer. This +/// implementation is wildly unsafe and will result in memory unsafety and +/// other terrors when given invalid bytecode, or even valid bytecode +/// encoding a program that itself does not preserve memory safety. +pub trait BytecodeStream: Copy { + /// The type of error that this bytecode stream produces on invalid + /// operations. + type Error; + + /// Create an "unexpected end-of-stream" error at the current position. + fn unexpected_eof(&self) -> Self::Error; + + /// Create an "invalid opcode" error at the current position. + fn invalid_opcode(&self, code: u8) -> Self::Error; + + /// Create an "invalid extended opcode" error at the current position. + fn invalid_extended_opcode(&self, code: u16) -> Self::Error; + + /// Create an "invalid register" error at the current position. + fn invalid_reg(&self, reg: u8) -> Self::Error; + + /// Read `N` bytes from this bytecode stream, advancing the stream's + /// position at the same time. + fn read(&mut self) -> Result<[u8; N], Self::Error>; +} + +/// A 100% safe implementation of a bytecode stream. +/// +/// This is a thin wrapper around an index into a `&[u8]`. +#[derive(Clone, Copy, Debug)] +pub struct SafeBytecodeStream<'a> { + bytecode: &'a [u8], + position: usize, +} + +impl<'a> SafeBytecodeStream<'a> { + /// Create a new `SafeBytecodeStream` from the given slice and with an + /// initial position pointing at the start of the slice. + pub fn new(bytecode: &'a [u8]) -> Self { + Self { + bytecode, + position: 0, + } + } + + /// Get this stream's current position within its underlying slice. + pub fn position(&self) -> usize { + self.position + } + + /// Get this stream's underlying bytecode slice. + pub fn as_slice(&self) -> &[u8] { + &self.bytecode + } +} + +impl BytecodeStream for SafeBytecodeStream<'_> { + fn read(&mut self) -> Result<[u8; N], Self::Error> { + let bytes = *self + .bytecode + .first_chunk::() + .ok_or_else(|| self.unexpected_eof())?; + self.bytecode = &self.bytecode[N..]; + self.position += N; + Ok(bytes) + } + + type Error = DecodingError; + + fn unexpected_eof(&self) -> Self::Error { + DecodingError::UnexpectedEof { + position: self.position, + } + } + + fn invalid_opcode(&self, code: u8) -> Self::Error { + DecodingError::InvalidOpcode { + position: self.position - 1, + code, + } + } + + fn invalid_extended_opcode(&self, code: u16) -> Self::Error { + DecodingError::InvalidExtendedOpcode { + position: self.position, + code, + } + } + + fn invalid_reg(&self, reg: u8) -> Self::Error { + DecodingError::InvalidReg { + position: self.position, + reg, + } + } +} + +/// An uninhabited type that cannot be constructed at runtime. +#[derive(Debug)] +pub enum Uninhabited {} + +/// An unsafe bytecode stream. +/// +/// This is a wrapper over a raw pointer to bytecode somewhere in memory. +#[derive(Clone, Copy, Debug)] +pub struct UnsafeBytecodeStream(*mut u8); + +impl UnsafeBytecodeStream { + /// Construct a new `UnsafeBytecodeStream` pointing at the given PC. + /// + /// # Safety + /// + /// The given `pc` must point to valid Pulley bytecode, and it is the + /// caller's responsibility to ensure that the resulting + /// `UnsafeBytecodeStream` is only used to access the valid bytecode. For + /// example, if the current bytecode instruction unconditionally jumps to a + /// new PC, this stream must not be used to read just after the + /// unconditional jump instruction because there is no guarantee that that + /// memory is part of the bytecode stream or not. + pub unsafe fn new(pc: *mut u8) -> Self { + assert!(!pc.is_null()); + UnsafeBytecodeStream(pc) + } + + /// Get a new `UnsafeBytecodeStream` pointing at the bytecode that is at the + /// given relative offset from this stream's current position. + /// + /// # Safety + /// + /// Same as the `new` constructor. May only be used when it is guaranteed + /// that the address at `self._as_ptr() + offset` contains valid Pulley + /// bytecode. + pub unsafe fn offset(&self, offset: isize) -> Self { + UnsafeBytecodeStream(self.0.offset(offset)) + } + + /// Get this stream's underlying raw pointer. + pub fn as_ptr(&self) -> *mut u8 { + self.0 + } +} + +impl BytecodeStream for UnsafeBytecodeStream { + fn read(&mut self) -> Result<[u8; N], Self::Error> { + debug_assert!(!self.0.is_null()); + let bytes = unsafe { self.0.cast::<[u8; N]>().read() }; + self.0 = unsafe { self.0.add(N) }; + Ok(bytes) + } + + type Error = Uninhabited; + + fn unexpected_eof(&self) -> Self::Error { + unsafe { crate::unreachable_unchecked() } + } + + fn invalid_opcode(&self, _code: u8) -> Self::Error { + unsafe { crate::unreachable_unchecked() } + } + + fn invalid_extended_opcode(&self, _code: u16) -> Self::Error { + unsafe { crate::unreachable_unchecked() } + } + + fn invalid_reg(&self, _reg: u8) -> Self::Error { + unsafe { crate::unreachable_unchecked() } + } +} + +/// Anything that can be decoded from a bytecode stream, e.g. opcodes, +/// immediates, registers, etc... +trait Decode: Sized { + fn decode(bytecode: &mut T) -> Result + where + T: BytecodeStream; +} + +impl Decode for u8 { + fn decode(bytecode: &mut T) -> Result + where + T: BytecodeStream, + { + bytecode.read::<1>().map(|a| a[0]) + } +} + +impl Decode for u16 { + fn decode(bytecode: &mut T) -> Result + where + T: BytecodeStream, + { + Ok(u16::from_le_bytes(bytecode.read()?)) + } +} + +impl Decode for u32 { + fn decode(bytecode: &mut T) -> Result + where + T: BytecodeStream, + { + Ok(u32::from_le_bytes(bytecode.read()?)) + } +} + +impl Decode for u64 { + fn decode(bytecode: &mut T) -> Result + where + T: BytecodeStream, + { + Ok(u64::from_le_bytes(bytecode.read()?)) + } +} + +impl Decode for i8 { + fn decode(bytecode: &mut T) -> Result + where + T: BytecodeStream, + { + bytecode.read::<1>().map(|a| a[0] as i8) + } +} + +impl Decode for i16 { + fn decode(bytecode: &mut T) -> Result + where + T: BytecodeStream, + { + Ok(i16::from_le_bytes(bytecode.read()?)) + } +} + +impl Decode for i32 { + fn decode(bytecode: &mut T) -> Result + where + T: BytecodeStream, + { + Ok(i32::from_le_bytes(bytecode.read()?)) + } +} + +impl Decode for i64 { + fn decode(bytecode: &mut T) -> Result + where + T: BytecodeStream, + { + Ok(i64::from_le_bytes(bytecode.read()?)) + } +} + +impl Decode for XReg { + fn decode(bytecode: &mut T) -> Result + where + T: BytecodeStream, + { + let byte = u8::decode(bytecode)?; + XReg::new(byte).ok_or_else(|| bytecode.invalid_reg(byte)) + } +} + +impl Decode for FReg { + fn decode(bytecode: &mut T) -> Result + where + T: BytecodeStream, + { + let byte = u8::decode(bytecode)?; + FReg::new(byte).ok_or_else(|| bytecode.invalid_reg(byte)) + } +} + +impl Decode for VReg { + fn decode(bytecode: &mut T) -> Result + where + T: BytecodeStream, + { + let byte = u8::decode(bytecode)?; + VReg::new(byte).ok_or_else(|| bytecode.invalid_reg(byte)) + } +} + +impl Decode for PcRelOffset { + fn decode(bytecode: &mut T) -> Result + where + T: BytecodeStream, + { + i32::decode(bytecode).map(|x| Self::from(x)) + } +} + +/// A Pulley bytecode decoder. +/// +/// Does not materialize bytecode instructions, instead all decoding methods are +/// given an `OpVisitor` implementation and the appropriate visitor methods are +/// called upon decoding an instruction. This minimizes the amount of times we +/// branch on the opcode, avoids constructing temporary storage, and plays well +/// with our variable-length instruction encoding. +#[derive(Default)] +pub struct Decoder { + _private: (), +} + +impl Decoder { + /// Create a new decoder. + pub fn new() -> Self { + Self::default() + } + + /// Decode all instructions in the visitor's bytecode stream. + /// + /// The associated visitor method is invoked after each instruction is + /// decoded. + pub fn decode_all<'a, V>(visitor: &mut V) -> Result> + where + V: OpVisitor> + ExtendedOpVisitor, + { + let mut decoder = Decoder::new(); + let mut results = vec![]; + + while !visitor.bytecode().as_slice().is_empty() { + results.push(decoder.decode_one(visitor)?); + } + + Ok(results) + } +} + +/// An `OpVisitor` combinator to sequence one visitor and then another. +pub struct SequencedVisitor<'a, F, V1, V2> { + join: F, + v1: &'a mut V1, + v2: &'a mut V2, +} + +impl<'a, F, V1, V2> SequencedVisitor<'a, F, V1, V2> { + /// Create a new sequenced visitor. + /// + /// The given `join` function is used to combine the results of each + /// sub-visitor so that it can be returned from this combined visitor. + pub fn new(join: F, v1: &'a mut V1, v2: &'a mut V2) -> Self { + SequencedVisitor { join, v1, v2 } + } +} + +macro_rules! define_decoder { + ( + $( + $( #[$attr:meta] )* + $snake_name:ident = $name:ident $( { + $( + $( #[$field_attr:meta] )* + $field:ident : $field_ty:ty + ),* + } )? ; + )* + ) => { + impl Decoder { + /// Decode one instruction from the visitor's bytestream. + /// + /// Upon decoding, the visitor's associated callback is invoked and + /// the results returned. + #[inline(always)] + pub fn decode_one( + &mut self, + visitor: &mut V, + ) -> Result::Error> + where + V: OpVisitor + ExtendedOpVisitor, + { + visitor.before_visit(); + + let byte = u8::decode(visitor.bytecode())?; + let opcode = Opcode::new(byte).ok_or_else(|| { + visitor.bytecode().invalid_opcode(byte) + })?; + + match opcode { + $( + Opcode::$name => { + $( + $( + let $field = <$field_ty>::decode( + visitor.bytecode(), + )?; + )* + )? + + let ret = visitor.$snake_name($( $( $field ),* )?); + visitor.after_visit(); + Ok(ret) + }, + )* + Opcode::ExtendedOp => { + decode_one_extended(visitor) + } + } + } + } + + /// Callbacks upon decoding instructions from bytecode. + /// + /// Implement this trait for your type, give an instance of your type to + /// a `Decoder` method, and the `Decoder` will invoke the associated + /// method for each instruction that it decodes. For example, if the + /// `Decoder` decodes an `xadd32` instruction, then it will invoke the + /// `xadd32` visitor method, passing along any decoded immediates, + /// operands, etc... as arguments. + pub trait OpVisitor { + /// The type of this visitor's bytecode stream. + type BytecodeStream: BytecodeStream; + + /// Get this visitor's underlying bytecode stream. + fn bytecode(&mut self) -> &mut Self::BytecodeStream; + + /// The type of values returned by each visitor method. + type Return; + + /// A callback invoked before starting to decode an instruction. + /// + /// Does nothing by default. + fn before_visit(&mut self) {} + + /// A callback invoked after an instruction has been completely + /// decoded. + /// + /// Does nothing by default. + fn after_visit(&mut self) {} + + $( + $( #[$attr] )* + fn $snake_name(&mut self $( $( , $field : $field_ty )* )? ) -> Self::Return; + )* + } + + impl OpVisitor for SequencedVisitor<'_, F, V1, V2> + where + F: FnMut(V1::Return, V2::Return) -> T, + V1: OpVisitor, + V2: OpVisitor, + { + type BytecodeStream = V1::BytecodeStream; + + fn bytecode(&mut self) -> &mut Self::BytecodeStream { + self.v1.bytecode() + } + + type Return = T; + + fn before_visit(&mut self) { + self.v1.before_visit(); + self.v2.before_visit(); + } + + fn after_visit(&mut self) { + *self.v2.bytecode() = *self.v1.bytecode(); + self.v1.after_visit(); + self.v2.after_visit(); + } + + $( + $( #[$attr] )* + fn $snake_name(&mut self $( $( , $field : $field_ty )* )? ) -> Self::Return { + let a = self.v1.$snake_name( $( $( $field , )* )? ); + let b = self.v2.$snake_name( $( $( $field , )* )? ); + (self.join)(a, b) + } + )* + } + }; +} +for_each_op!(define_decoder); + +macro_rules! define_extended_decoder { + ( + $( + $( #[$attr:meta] )* + $snake_name:ident = $name:ident $( { + $( + $( #[$field_attr:meta] )* + $field:ident : $field_ty:ty + ),* + } )? ; + )* + ) => { + /// Like `OpVisitor` but for extended operations. + pub trait ExtendedOpVisitor: OpVisitor { + $( + $( #[$attr] )* + fn $snake_name(&mut self $( $( , $field : $field_ty )* )? ) -> Self::Return; + )* + } + + fn decode_one_extended( + visitor: &mut V, + ) -> Result::Error> + where + V: ExtendedOpVisitor, + { + let code = u16::decode(visitor.bytecode())?; + let opcode = ExtendedOpcode::new(code).ok_or_else(|| { + visitor.bytecode().invalid_extended_opcode(code) + })?; + + match opcode { + $( + ExtendedOpcode::$name => { + $( + $( + let $field = <$field_ty>::decode( + visitor.bytecode(), + )?; + )* + )? + + let ret = visitor.$snake_name($( $( $field ),* )?); + visitor.after_visit(); + Ok(ret) + } + )* + } + } + + + impl ExtendedOpVisitor for SequencedVisitor<'_, F, V1, V2> + where + F: FnMut(V1::Return, V2::Return) -> T, + V1: ExtendedOpVisitor, + V2: ExtendedOpVisitor, + { + $( + $( #[$attr] )* + fn $snake_name(&mut self $( $( , $field : $field_ty )* )? ) -> Self::Return { + let a = self.v1.$snake_name( $( $( $field , )* )? ); + let b = self.v2.$snake_name( $( $( $field , )* )? ); + (self.join)(a, b) + } + )* + } + }; +} +for_each_extended_op!(define_extended_decoder); diff --git a/pulley/src/disas.rs b/pulley/src/disas.rs new file mode 100644 index 000000000000..f7f2b92418b8 --- /dev/null +++ b/pulley/src/disas.rs @@ -0,0 +1,233 @@ +//! Disassembly support for pulley bytecode. + +use crate::decode::*; +use crate::imms::*; +use crate::regs::*; +use alloc::string::String; +use core::fmt::Write; + +/// A Pulley bytecode disassembler. +/// +/// This is implemented as an `OpVisitor`, where you pass a `Disassembler` to a +/// `Decoder` in order to disassemble instructions from a bytecode stream. +/// +/// Alternatively, you can use the `Disassembler::disassemble_all` method to +/// disassemble a complete bytecode stream. +pub struct Disassembler<'a> { + raw_bytecode: &'a [u8], + bytecode: SafeBytecodeStream<'a>, + disas: String, + start: usize, + temp: String, +} + +impl<'a> Disassembler<'a> { + /// Disassemble every instruction in the given bytecode stream. + pub fn disassemble_all(bytecode: &'a [u8]) -> Result { + let mut disas = Self::new(bytecode); + Decoder::decode_all(&mut disas)?; + Ok(disas.disas) + } + + /// Create a new `Disassembler` that can be used to incrementally + /// disassemble instructions from the given bytecode stream. + pub fn new(bytecode: &'a [u8]) -> Self { + Self { + raw_bytecode: bytecode, + bytecode: SafeBytecodeStream::new(bytecode), + disas: String::new(), + start: 0, + temp: String::new(), + } + } + + /// Get the disassembly thus far. + pub fn disas(&self) -> &str { + &self.disas + } +} + +/// Anything inside an instruction that can be disassembled: registers, +/// immediates, etc... +trait Disas { + fn disas(&self, position: usize, disas: &mut String); +} + +impl Disas for XReg { + fn disas(&self, _position: usize, disas: &mut String) { + write!(disas, "{self}").unwrap(); + } +} + +impl Disas for FReg { + fn disas(&self, _position: usize, disas: &mut String) { + write!(disas, "{self}").unwrap(); + } +} + +impl Disas for VReg { + fn disas(&self, _position: usize, disas: &mut String) { + write!(disas, "{self}").unwrap(); + } +} + +impl Disas for i8 { + fn disas(&self, _position: usize, disas: &mut String) { + write!(disas, "{self}").unwrap(); + } +} + +impl Disas for i16 { + fn disas(&self, _position: usize, disas: &mut String) { + write!(disas, "{self}").unwrap(); + } +} + +impl Disas for i32 { + fn disas(&self, _position: usize, disas: &mut String) { + write!(disas, "{self}").unwrap(); + } +} + +impl Disas for i64 { + fn disas(&self, _position: usize, disas: &mut String) { + write!(disas, "{self}").unwrap(); + } +} + +impl Disas for u8 { + fn disas(&self, _position: usize, disas: &mut String) { + write!(disas, "{self}").unwrap(); + } +} + +impl Disas for u16 { + fn disas(&self, _position: usize, disas: &mut String) { + write!(disas, "{self}").unwrap(); + } +} + +impl Disas for u32 { + fn disas(&self, _position: usize, disas: &mut String) { + write!(disas, "{self}").unwrap(); + } +} + +impl Disas for u64 { + fn disas(&self, _position: usize, disas: &mut String) { + write!(disas, "{self}").unwrap(); + } +} + +impl Disas for PcRelOffset { + fn disas(&self, position: usize, disas: &mut String) { + let offset = isize::try_from(i32::from(*self)).unwrap(); + let target = position.wrapping_add(offset as usize); + write!(disas, "{offset:#x} // target = {target:#x}").unwrap() + } +} + +macro_rules! impl_disas { + ( + $( + $( #[$attr:meta] )* + $snake_name:ident = $name:ident $( { + $( + $( #[$field_attr:meta] )* + $field:ident : $field_ty:ty + ),* + } )? ; + )* + ) => { + impl<'a> OpVisitor for Disassembler<'a> { + type BytecodeStream = SafeBytecodeStream<'a>; + + fn bytecode(&mut self) -> &mut Self::BytecodeStream { + &mut self.bytecode + } + + type Return = (); + + fn before_visit(&mut self) { + self.start = self.bytecode.position(); + } + + fn after_visit(&mut self) { + let size = self.bytecode.position() - self.start; + + write!(&mut self.disas, "{:8x}: ", self.start).unwrap(); + let mut need_space = false; + for byte in &self.raw_bytecode[self.start..][..size] { + write!(&mut self.disas, "{}{byte:02x}", if need_space { " " } else { "" }).unwrap(); + need_space = true; + } + for _ in 0..11_usize.saturating_sub(size) { + write!(&mut self.disas, " ").unwrap(); + } + + self.disas.push_str(&self.temp); + self.temp.clear(); + + self.disas.push('\n'); + } + + $( + fn $snake_name(&mut self $( $( , $field : $field_ty )* )? ) { + let mnemonic = stringify!($snake_name); + write!(&mut self.temp, "{mnemonic}").unwrap(); + $( + let mut need_comma = false; + $( + let val = $field; + if need_comma { + write!(&mut self.temp, ",").unwrap(); + } + write!(&mut self.temp, " ").unwrap(); + val.disas(self.start, &mut self.temp); + #[allow(unused_assignments)] + { need_comma = true; } + )* + )? + } + )* + } + }; +} +for_each_op!(impl_disas); + +macro_rules! impl_extended_disas { + ( + $( + $( #[$attr:meta] )* + $snake_name:ident = $name:ident $( { + $( + $( #[$field_attr:meta] )* + $field:ident : $field_ty:ty + ),* + } )? ; + )* + ) => { + impl ExtendedOpVisitor for Disassembler<'_> { + $( + fn $snake_name(&mut self $( $( , $field : $field_ty )* )? ) { + let mnemonic = stringify!($snake_name); + write!(&mut self.temp, "{mnemonic}").unwrap(); + $( + let mut need_comma = false; + $( + let val = $field; + if need_comma { + write!(&mut self.temp, ",").unwrap(); + } + write!(&mut self.temp, " ").unwrap(); + val.disas(self.start, &mut self.temp); + #[allow(unused_assignments)] + { need_comma = true; } + )* + )? + } + )* + } + }; +} +for_each_extended_op!(impl_extended_disas); diff --git a/pulley/src/encode.rs b/pulley/src/encode.rs new file mode 100644 index 000000000000..fa2a3597a8e2 --- /dev/null +++ b/pulley/src/encode.rs @@ -0,0 +1,180 @@ +//! Encoding support for pulley bytecode. + +use crate::imms::*; +use crate::opcode::{ExtendedOpcode, Opcode}; +use crate::regs::*; + +trait Encode { + fn encode(&self, sink: &mut E) + where + E: Extend; +} + +impl Encode for u8 { + fn encode(&self, sink: &mut E) + where + E: Extend, + { + sink.extend(core::iter::once(*self)); + } +} + +impl Encode for u16 { + fn encode(&self, sink: &mut E) + where + E: Extend, + { + sink.extend(self.to_le_bytes()); + } +} + +impl Encode for u32 { + fn encode(&self, sink: &mut E) + where + E: Extend, + { + sink.extend(self.to_le_bytes()); + } +} + +impl Encode for u64 { + fn encode(&self, sink: &mut E) + where + E: Extend, + { + sink.extend(self.to_le_bytes()); + } +} + +impl Encode for i8 { + fn encode(&self, sink: &mut E) + where + E: Extend, + { + sink.extend(core::iter::once(*self as u8)); + } +} + +impl Encode for i16 { + fn encode(&self, sink: &mut E) + where + E: Extend, + { + sink.extend(self.to_le_bytes()); + } +} + +impl Encode for i32 { + fn encode(&self, sink: &mut E) + where + E: Extend, + { + sink.extend(self.to_le_bytes()); + } +} + +impl Encode for i64 { + fn encode(&self, sink: &mut E) + where + E: Extend, + { + sink.extend(self.to_le_bytes()); + } +} + +impl Encode for XReg { + fn encode(&self, sink: &mut E) + where + E: Extend, + { + sink.extend(core::iter::once(u8::try_from(self.index()).unwrap())); + } +} + +impl Encode for FReg { + fn encode(&self, sink: &mut E) + where + E: Extend, + { + sink.extend(core::iter::once(u8::try_from(self.index()).unwrap())); + } +} + +impl Encode for VReg { + fn encode(&self, sink: &mut E) + where + E: Extend, + { + sink.extend(core::iter::once(u8::try_from(self.index()).unwrap())); + } +} + +impl Encode for PcRelOffset { + fn encode(&self, sink: &mut E) + where + E: Extend, + { + i32::from(*self).encode(sink); + } +} + +macro_rules! impl_encoders { + ( + $( + $( #[$attr:meta] )* + $snake_name:ident = $name:ident $( { + $( + $( #[$field_attr:meta] )* + $field:ident : $field_ty:ty + ),* + } )? ; + )* + ) => { + $( + $( #[$attr] )* + pub fn $snake_name(into: &mut E $( $( , $field : impl Into<$field_ty> )* )? ) + where + E: Extend, + { + into.extend(core::iter::once(Opcode::$name as u8)); + $( + $( + $field.into().encode(into); + )* + )? + } + )* + }; +} +for_each_op!(impl_encoders); + +macro_rules! impl_extended_encoders { + ( + $( + $( #[$attr:meta] )* + $snake_name:ident = $name:ident $( { + $( + $( #[$field_attr:meta] )* + $field:ident : $field_ty:ty + ),* + } )? ; + )* + ) => { + $( + $( #[$attr] )* + pub fn $snake_name(into: &mut E $( $( , $field : impl Into<$field_ty> )* )? ) + where + E: Extend, + { + into.extend(core::iter::once(Opcode::ExtendedOp as u8)); + into.extend((ExtendedOpcode::$name as u16).to_le_bytes()); + $( + $( + $field.into().encode(into); + )* + )? + } + )* + }; +} +for_each_extended_op!(impl_extended_encoders); diff --git a/pulley/src/imms.rs b/pulley/src/imms.rs new file mode 100644 index 000000000000..cd331e2cd639 --- /dev/null +++ b/pulley/src/imms.rs @@ -0,0 +1,31 @@ +//! Immediates. + +/// A PC-relative offset. +/// +/// This is relative to the start of this offset's containing instruction. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct PcRelOffset(i32); + +#[cfg(feature = "arbitrary")] +impl<'a> arbitrary::Arbitrary<'a> for PcRelOffset { + fn arbitrary(_u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + // We can't possibly choose valid offsets for jumping to, so just use + // zero as the least dangerous option. It is up to whoever is generating + // arbitrary ops to clean this up. + Ok(Self(0)) + } +} + +impl From for PcRelOffset { + #[inline] + fn from(offset: i32) -> Self { + PcRelOffset(offset) + } +} + +impl From for i32 { + #[inline] + fn from(offset: PcRelOffset) -> Self { + offset.0 + } +} diff --git a/pulley/src/interp.rs b/pulley/src/interp.rs new file mode 100644 index 000000000000..efd5fa0226c8 --- /dev/null +++ b/pulley/src/interp.rs @@ -0,0 +1,981 @@ +//! Interpretation of pulley bytecode. + +use crate::decode::*; +use crate::imms::*; +use crate::regs::*; +use crate::ExtendedOpcode; +use alloc::string::ToString; +use alloc::{vec, vec::Vec}; +use core::mem; +use core::ptr::{self, NonNull}; + +const DEFAULT_STACK_SIZE: usize = 1 << 20; // 1 MiB + +/// A virtual machine for interpreting Pulley bytecode. +pub struct Vm { + decoder: Decoder, + state: MachineState, +} + +impl Default for Vm { + fn default() -> Self { + Vm::new() + } +} + +impl Vm { + /// Create a new virtual machine with the default stack size. + pub fn new() -> Self { + Self::with_stack(vec![0; DEFAULT_STACK_SIZE]) + } + + /// Create a new virtual machine with the given stack. + pub fn with_stack(stack: Vec) -> Self { + Self { + decoder: Decoder::new(), + state: MachineState::with_stack(stack), + } + } + + /// Get a shared reference to this VM's machine state. + pub fn state(&self) -> &MachineState { + &self.state + } + + /// Get an exclusive reference to this VM's machine state. + pub fn state_mut(&mut self) -> &mut MachineState { + &mut self.state + } + + /// Consumer this VM and return its stack storage. + pub fn into_stack(self) -> Vec { + self.state.stack + } + + /// Call a bytecode function. + /// + /// The given `func` must point to the beginning of a valid Pulley bytecode + /// function. + /// + /// The given `args` must match the number and type of arguments that + /// function expects. + /// + /// The given `rets` must match the function's actual return types. + /// + /// Returns either the resulting values, or the PC at which a trap was + /// raised. + pub unsafe fn call<'a>( + &'a mut self, + func: NonNull, + args: &[Val], + rets: impl IntoIterator + 'a, + ) -> Result + 'a, *mut u8> { + // NB: make sure this method stays in sync with + // `Pbc64MachineDeps::compute_arg_locs`! + + let mut x_args = (0..16).map(|x| XReg::unchecked_new(x)); + let mut f_args = (0..16).map(|f| FReg::unchecked_new(f)); + let mut v_args = (0..16).map(|v| VReg::unchecked_new(v)); + + for arg in args { + match arg { + Val::XReg(val) => match x_args.next() { + Some(reg) => self.state.set_x(reg, *val), + None => todo!("stack slots"), + }, + Val::FReg(val) => match f_args.next() { + Some(reg) => self.state.set_f(reg, *val), + None => todo!("stack slots"), + }, + Val::VReg(val) => match v_args.next() { + Some(reg) => self.state.set_v(reg, *val), + None => todo!("stack slots"), + }, + } + } + + self.run(func.as_ptr())?; + + let mut x_rets = (0..16).map(|x| XReg::unchecked_new(x)); + let mut f_rets = (0..16).map(|f| FReg::unchecked_new(f)); + let mut v_rets = (0..16).map(|v| VReg::unchecked_new(v)); + + Ok(rets.into_iter().map(move |ty| match ty { + RegType::XReg => match x_rets.next() { + Some(reg) => Val::XReg(self.state.get_x(reg)), + None => todo!("stack slots"), + }, + RegType::FReg => match f_rets.next() { + Some(reg) => Val::FReg(self.state.get_f(reg)), + None => todo!("stack slots"), + }, + RegType::VReg => match v_rets.next() { + Some(reg) => Val::VReg(self.state.get_v(reg)), + None => todo!("stack slots"), + }, + })) + } + + unsafe fn run(&mut self, pc: *mut u8) -> Result<(), *mut u8> { + let mut visitor = InterpreterVisitor { + state: &mut self.state, + pc: UnsafeBytecodeStream::new(pc), + }; + + loop { + let continuation = self.decoder.decode_one(&mut visitor).unwrap(); + + // Really wish we had `feature(explicit_tail_calls)`... + match continuation { + Continuation::Continue => { + continue; + } + + // Out-of-line slow paths marked `cold` and `inline(never)` to + // improve codegen. + Continuation::Trap => { + let pc = visitor.pc.as_ptr(); + return self.trap(pc); + } + Continuation::ReturnToHost => return self.return_to_host(), + Continuation::HostCall => return self.host_call(), + } + } + } + + #[cold] + #[inline(never)] + fn return_to_host(&self) -> Result<(), *mut u8> { + Ok(()) + } + + #[cold] + #[inline(never)] + fn trap(&self, pc: *mut u8) -> Result<(), *mut u8> { + // We are given the VM's PC upon having executed a trap instruction, + // which is actually pointing to the next instruction after the + // trap. Back the PC up to point exactly at the trap. + let trap_pc = unsafe { pc.byte_sub(ExtendedOpcode::ENCODED_SIZE_OF_TRAP) }; + Err(trap_pc) + } + + #[cold] + #[inline(never)] + fn host_call(&self) -> Result<(), *mut u8> { + todo!() + } +} + +/// The type of a register in the Pulley machine state. +#[derive(Clone, Copy, Debug)] +pub enum RegType { + /// An `x` register: integers. + XReg, + + /// An `f` register: floats. + FReg, + + /// A `v` register: vectors. + VReg, +} + +/// A value that can be stored in a register. +#[derive(Clone, Copy, Debug)] +pub enum Val { + /// An `x` register value: integers. + XReg(XRegVal), + + /// An `f` register value: floats. + FReg(FRegVal), + + /// A `v` register value: vectors. + VReg(VRegVal), +} + +/// An `x` register value: integers. +#[derive(Copy, Clone)] +pub struct XRegVal(XRegUnion); + +impl core::fmt::Debug for XRegVal { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("XRegVal") + .field("as_u64", &self.get_u64()) + .finish() + } +} + +// NB: we always store these in little endian, so we have to `from_le_bytes` +// whenever we read and `to_le_bytes` whenever we store. +#[derive(Copy, Clone)] +union XRegUnion { + i32: i32, + u32: u32, + i64: i64, + u64: u64, + isize: isize, + usize: usize, +} + +impl Default for XRegVal { + fn default() -> Self { + Self(unsafe { mem::zeroed() }) + } +} + +#[allow(missing_docs)] +impl XRegVal { + pub fn new_i32(x: i32) -> Self { + let mut val = XRegVal::default(); + val.set_i32(x); + val + } + + pub fn new_u32(x: u32) -> Self { + let mut val = XRegVal::default(); + val.set_u32(x); + val + } + + pub fn new_i64(x: i64) -> Self { + let mut val = XRegVal::default(); + val.set_i64(x); + val + } + + pub fn new_u64(x: u64) -> Self { + let mut val = XRegVal::default(); + val.set_u64(x); + val + } + + pub fn new_isize(x: isize) -> Self { + let mut val = XRegVal::default(); + val.set_isize(x); + val + } + + pub fn new_usize(x: usize) -> Self { + let mut val = XRegVal::default(); + val.set_usize(x); + val + } + + pub fn get_i32(&self) -> i32 { + let x = unsafe { self.0.i32 }; + i32::from_le_bytes(x.to_ne_bytes()) + } + + pub fn get_u32(&self) -> u32 { + let x = unsafe { self.0.u32 }; + u32::from_le_bytes(x.to_ne_bytes()) + } + + pub fn get_i64(&self) -> i64 { + let x = unsafe { self.0.i64 }; + i64::from_le_bytes(x.to_ne_bytes()) + } + + pub fn get_u64(&self) -> u64 { + let x = unsafe { self.0.u64 }; + u64::from_le_bytes(x.to_ne_bytes()) + } + + pub fn get_isize(&self) -> isize { + let x = unsafe { self.0.isize }; + isize::from_le_bytes(x.to_ne_bytes()) + } + + pub fn get_usize(&self) -> usize { + let x = unsafe { self.0.usize }; + usize::from_le_bytes(x.to_ne_bytes()) + } + + pub fn set_i32(&mut self, x: i32) { + let x = i32::from_ne_bytes(x.to_le_bytes()); + self.0.i32 = x; + } + + pub fn set_u32(&mut self, x: u32) { + let x = u32::from_ne_bytes(x.to_le_bytes()); + self.0.u32 = x; + } + + pub fn set_i64(&mut self, x: i64) { + let x = i64::from_ne_bytes(x.to_le_bytes()); + self.0.i64 = x; + } + + pub fn set_u64(&mut self, x: u64) { + let x = u64::from_ne_bytes(x.to_le_bytes()); + self.0.u64 = x; + } + + pub fn set_isize(&mut self, x: isize) { + let x = isize::from_ne_bytes(x.to_le_bytes()); + self.0.isize = x; + } + + pub fn set_usize(&mut self, x: usize) { + let x = usize::from_ne_bytes(x.to_le_bytes()); + self.0.usize = x; + } +} + +/// An `f` register value: floats. +#[derive(Copy, Clone)] +pub struct FRegVal(FRegUnion); + +impl core::fmt::Debug for FRegVal { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("FRegVal") + .field("as_f32", &self.get_f32()) + .field("as_f64", &self.get_f64()) + .finish() + } +} + +// NB: we always store these in little endian, so we have to `from_le_bytes` +// whenever we read and `to_le_bytes` whenever we store. +#[derive(Copy, Clone)] +union FRegUnion { + f32: u32, + f64: u64, +} + +impl Default for FRegVal { + fn default() -> Self { + Self(unsafe { mem::zeroed() }) + } +} + +#[allow(missing_docs)] +impl FRegVal { + pub fn new_f32(f: f32) -> Self { + let mut val = Self::default(); + val.set_f32(f); + val + } + + pub fn new_f64(f: f64) -> Self { + let mut val = Self::default(); + val.set_f64(f); + val + } + + pub fn get_f32(&self) -> f32 { + let val = unsafe { self.0.f32 }; + f32::from_le_bytes(val.to_ne_bytes()) + } + + pub fn get_f64(&self) -> f64 { + let val = unsafe { self.0.f64 }; + f64::from_le_bytes(val.to_ne_bytes()) + } + + pub fn set_f32(&mut self, val: f32) { + self.0.f32 = u32::from_ne_bytes(val.to_le_bytes()); + } + + pub fn set_f64(&mut self, val: f64) { + self.0.f64 = u64::from_ne_bytes(val.to_le_bytes()); + } +} + +/// A `v` register value: vectors. +#[derive(Copy, Clone)] +pub struct VRegVal(VRegUnion); + +impl core::fmt::Debug for VRegVal { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("VRegVal") + .field("as_u128", &unsafe { self.0.u128 }) + .finish() + } +} + +#[derive(Copy, Clone)] +union VRegUnion { + // TODO: need to figure out how we are going to handle portability of lane + // ordering on top of each lane's endianness. + u128: u128, +} + +impl Default for VRegVal { + fn default() -> Self { + Self(unsafe { mem::zeroed() }) + } +} + +/// The machine state for a Pulley virtual machine: the various registers and +/// stack. +pub struct MachineState { + x_regs: [XRegVal; XReg::RANGE.end as usize], + f_regs: [FRegVal; FReg::RANGE.end as usize], + v_regs: [VRegVal; VReg::RANGE.end as usize], + stack: Vec, +} + +unsafe impl Send for MachineState {} +unsafe impl Sync for MachineState {} + +impl core::fmt::Debug for MachineState { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + let MachineState { + x_regs, + f_regs, + v_regs, + stack: _, + } = self; + + struct RegMap<'a, R>(&'a [R], fn(u8) -> alloc::string::String); + + impl core::fmt::Debug for RegMap<'_, R> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + let mut f = f.debug_map(); + for (i, r) in self.0.iter().enumerate() { + f.entry(&(self.1)(i as u8), r); + } + f.finish() + } + } + + f.debug_struct("MachineState") + .field( + "x_regs", + &RegMap(x_regs, |i| XReg::new(i).unwrap().to_string()), + ) + .field( + "f_regs", + &RegMap(f_regs, |i| FReg::new(i).unwrap().to_string()), + ) + .field( + "v_regs", + &RegMap(v_regs, |i| VReg::new(i).unwrap().to_string()), + ) + .finish_non_exhaustive() + } +} + +impl MachineState { + fn with_stack(stack: Vec) -> Self { + assert!(stack.len() > 0); + let mut state = Self { + x_regs: [Default::default(); XReg::RANGE.end as usize], + f_regs: Default::default(), + v_regs: Default::default(), + stack, + }; + + let sp = state.stack.last().unwrap() as *const u8 as usize; + state.set_x(XReg::SP, XRegVal::new_usize(sp)); + + state.set_x(XReg::FP, XRegVal::new_i64(-1)); + state.set_x(XReg::LR, XRegVal::new_i64(-1)); + + state + } + + /// Get a shared reference to the value of the given `x` register. + #[inline(always)] + pub fn x(&self, x: XReg) -> &XRegVal { + debug_assert!(x.index() < self.x_regs.len()); + unsafe { self.x_regs.get_unchecked(x.index()) } + } + + /// Get an exclusive reference to the value of the given `x` register. + #[inline(always)] + pub fn x_mut(&mut self, x: XReg) -> &mut XRegVal { + debug_assert!(x.index() < self.x_regs.len()); + unsafe { self.x_regs.get_unchecked_mut(x.index()) } + } + + /// Copy the value of the given `x` register. + #[inline(always)] + pub fn get_x(&self, x: XReg) -> XRegVal { + *self.x(x) + } + + /// Set the value of the given `x` register. + #[inline(always)] + pub fn set_x(&mut self, x: XReg, val: XRegVal) { + debug_assert!(x.index() < self.x_regs.len()); + unsafe { + *self.x_regs.get_unchecked_mut(x.index()) = val; + } + } + + /// Get a shared reference to the value of the given `f` register. + #[inline(always)] + pub fn f(&self, f: FReg) -> &FRegVal { + debug_assert!(f.index() < self.f_regs.len()); + unsafe { self.f_regs.get_unchecked(f.index()) } + } + + /// Get an exclusive reference to the value of the given `f` register. + #[inline(always)] + pub fn f_mut(&mut self, f: FReg) -> &mut FRegVal { + debug_assert!(f.index() < self.f_regs.len()); + unsafe { self.f_regs.get_unchecked_mut(f.index()) } + } + + /// Copy the value of the given `f` register. + #[inline(always)] + pub fn get_f(&self, f: FReg) -> FRegVal { + debug_assert!(f.index() < self.f_regs.len()); + unsafe { *self.f_regs.get_unchecked(f.index()) } + } + + /// Set the value of the given `f` register. + #[inline(always)] + pub fn set_f(&mut self, f: FReg, val: FRegVal) { + debug_assert!(f.index() < self.f_regs.len()); + unsafe { + *self.f_regs.get_unchecked_mut(f.index()) = val; + } + } + + /// Get a shared reference to the value of the given `v` register. + #[inline(always)] + pub fn v(&self, v: VReg) -> &VRegVal { + debug_assert!(v.index() < self.v_regs.len()); + unsafe { self.v_regs.get_unchecked(v.index()) } + } + + /// Get an exclusive reference to the value of the given `v` register. + #[inline(always)] + pub fn v_mut(&mut self, v: VReg) -> &mut VRegVal { + debug_assert!(v.index() < self.v_regs.len()); + unsafe { self.v_regs.get_unchecked_mut(v.index()) } + } + + /// Copy the value of the given `v` register. + #[inline(always)] + pub fn get_v(&self, v: VReg) -> VRegVal { + debug_assert!(v.index() < self.v_regs.len()); + unsafe { *self.v_regs.get_unchecked(v.index()) } + } + + /// Set the value of the given `v` register. + #[inline(always)] + pub fn set_v(&mut self, v: VReg, val: VRegVal) { + debug_assert!(v.index() < self.v_regs.len()); + unsafe { + *self.v_regs.get_unchecked_mut(v.index()) = val; + } + } +} + +enum Continuation { + Continue, + ReturnToHost, + Trap, + + #[allow(dead_code)] + HostCall, +} + +struct InterpreterVisitor<'a> { + state: &'a mut MachineState, + pc: UnsafeBytecodeStream, +} + +impl InterpreterVisitor<'_> { + #[inline(always)] + fn pc_rel_jump(&mut self, offset: PcRelOffset, inst_size: isize) -> Continuation { + let offset = isize::try_from(i32::from(offset)).unwrap(); + self.pc = unsafe { self.pc.offset(offset - inst_size) }; + Continuation::Continue + } +} + +#[doc(hidden)] +impl OpVisitor for InterpreterVisitor<'_> { + type BytecodeStream = UnsafeBytecodeStream; + + fn bytecode(&mut self) -> &mut Self::BytecodeStream { + &mut self.pc + } + + type Return = Continuation; + + fn ret(&mut self) -> Self::Return { + if self.state.x(XReg::LR).get_u64() == u64::MAX { + Continuation::ReturnToHost + } else { + let return_addr = self.state.x(XReg::LR).get_usize() as *mut u8; + self.pc = unsafe { UnsafeBytecodeStream::new(return_addr) }; + // log::trace!("returning to {return_addr:#p}"); + Continuation::Continue + } + } + + fn call(&mut self, offset: PcRelOffset) -> Self::Return { + let return_addr = u64::try_from(self.pc.as_ptr() as usize).unwrap(); + self.state.x_mut(XReg::LR).set_u64(return_addr); + self.pc_rel_jump(offset, 5) + } + + fn jump(&mut self, offset: PcRelOffset) -> Self::Return { + self.pc_rel_jump(offset, 5) + } + + fn br_if(&mut self, cond: XReg, offset: PcRelOffset) -> Self::Return { + let cond = self.state.x(cond).get_u64(); + if cond != 0 { + self.pc_rel_jump(offset, 6) + } else { + Continuation::Continue + } + } + + fn br_if_not(&mut self, cond: XReg, offset: PcRelOffset) -> Self::Return { + let cond = self.state.x(cond).get_u64(); + if cond == 0 { + self.pc_rel_jump(offset, 6) + } else { + Continuation::Continue + } + } + + fn br_if_xeq32(&mut self, a: XReg, b: XReg, offset: PcRelOffset) -> Self::Return { + let a = self.state.x(a).get_u32(); + let b = self.state.x(b).get_u32(); + if a == b { + self.pc_rel_jump(offset, 7) + } else { + Continuation::Continue + } + } + + fn br_if_xneq32(&mut self, a: XReg, b: XReg, offset: PcRelOffset) -> Self::Return { + let a = self.state.x(a).get_u32(); + let b = self.state.x(b).get_u32(); + if a != b { + self.pc_rel_jump(offset, 7) + } else { + Continuation::Continue + } + } + + fn br_if_xslt32(&mut self, a: XReg, b: XReg, offset: PcRelOffset) -> Self::Return { + let a = self.state.x(a).get_i32(); + let b = self.state.x(b).get_i32(); + if a < b { + self.pc_rel_jump(offset, 7) + } else { + Continuation::Continue + } + } + + fn br_if_xslteq32(&mut self, a: XReg, b: XReg, offset: PcRelOffset) -> Self::Return { + let a = self.state.x(a).get_i32(); + let b = self.state.x(b).get_i32(); + if a <= b { + self.pc_rel_jump(offset, 7) + } else { + Continuation::Continue + } + } + + fn br_if_xult32(&mut self, a: XReg, b: XReg, offset: PcRelOffset) -> Self::Return { + let a = self.state.x(a).get_u32(); + let b = self.state.x(b).get_u32(); + if a < b { + self.pc_rel_jump(offset, 7) + } else { + Continuation::Continue + } + } + + fn br_if_xulteq32(&mut self, a: XReg, b: XReg, offset: PcRelOffset) -> Self::Return { + let a = self.state.x(a).get_u32(); + let b = self.state.x(b).get_u32(); + if a <= b { + self.pc_rel_jump(offset, 7) + } else { + Continuation::Continue + } + } + + fn xmov(&mut self, dst: XReg, src: XReg) -> Self::Return { + let val = self.state.get_x(src); + self.state.set_x(dst, val); + Continuation::Continue + } + + fn fmov(&mut self, dst: FReg, src: FReg) -> Self::Return { + let val = self.state.get_f(src); + self.state.set_f(dst, val); + Continuation::Continue + } + + fn vmov(&mut self, dst: VReg, src: VReg) -> Self::Return { + let val = self.state.get_v(src); + self.state.set_v(dst, val); + Continuation::Continue + } + + fn xconst8(&mut self, dst: XReg, imm: u8) -> Self::Return { + self.state.x_mut(dst).set_u64(u64::from(imm)); + Continuation::Continue + } + + fn xconst16(&mut self, dst: XReg, imm: u16) -> Self::Return { + self.state.x_mut(dst).set_u64(u64::from(imm)); + Continuation::Continue + } + + fn xconst32(&mut self, dst: XReg, imm: u32) -> Self::Return { + self.state.x_mut(dst).set_u64(u64::from(imm)); + Continuation::Continue + } + + fn xconst64(&mut self, dst: XReg, imm: u64) -> Self::Return { + self.state.x_mut(dst).set_u64(imm); + Continuation::Continue + } + + fn xadd32(&mut self, dst: XReg, src1: XReg, src2: XReg) -> Self::Return { + let a = self.state.x(src1).get_u32(); + let b = self.state.x(src2).get_u32(); + self.state.x_mut(dst).set_u32(a.wrapping_add(b)); + Continuation::Continue + } + + fn xadd64(&mut self, dst: XReg, src1: XReg, src2: XReg) -> Self::Return { + let a = self.state.x(src1).get_u64(); + let b = self.state.x(src2).get_u64(); + self.state.x_mut(dst).set_u64(a.wrapping_add(b)); + Continuation::Continue + } + + fn xeq64(&mut self, dst: XReg, src1: XReg, src2: XReg) -> Self::Return { + let a = self.state.x(src1).get_u64(); + let b = self.state.x(src2).get_u64(); + self.state.x_mut(dst).set_u64(u64::from(a == b)); + Continuation::Continue + } + + fn xneq64(&mut self, dst: XReg, src1: XReg, src2: XReg) -> Self::Return { + let a = self.state.x(src1).get_u64(); + let b = self.state.x(src2).get_u64(); + self.state.x_mut(dst).set_u64(u64::from(a != b)); + Continuation::Continue + } + + fn xslt64(&mut self, dst: XReg, src1: XReg, src2: XReg) -> Self::Return { + let a = self.state.x(src1).get_i64(); + let b = self.state.x(src2).get_i64(); + self.state.x_mut(dst).set_u64(u64::from(a < b)); + Continuation::Continue + } + + fn xslteq64(&mut self, dst: XReg, src1: XReg, src2: XReg) -> Self::Return { + let a = self.state.x(src1).get_i64(); + let b = self.state.x(src2).get_i64(); + self.state.x_mut(dst).set_u64(u64::from(a <= b)); + Continuation::Continue + } + + fn xult64(&mut self, dst: XReg, src1: XReg, src2: XReg) -> Self::Return { + let a = self.state.x(src1).get_u64(); + let b = self.state.x(src2).get_u64(); + self.state.x_mut(dst).set_u64(u64::from(a < b)); + Continuation::Continue + } + + fn xulteq64(&mut self, dst: XReg, src1: XReg, src2: XReg) -> Self::Return { + let a = self.state.x(src1).get_u64(); + let b = self.state.x(src2).get_u64(); + self.state.x_mut(dst).set_u64(u64::from(a <= b)); + Continuation::Continue + } + + fn xeq32(&mut self, dst: XReg, src1: XReg, src2: XReg) -> Self::Return { + let a = self.state.x(src1).get_u32(); + let b = self.state.x(src2).get_u32(); + self.state.x_mut(dst).set_u64(u64::from(a == b)); + Continuation::Continue + } + + fn xneq32(&mut self, dst: XReg, src1: XReg, src2: XReg) -> Self::Return { + let a = self.state.x(src1).get_u32(); + let b = self.state.x(src2).get_u32(); + self.state.x_mut(dst).set_u64(u64::from(a != b)); + Continuation::Continue + } + + fn xslt32(&mut self, dst: XReg, src1: XReg, src2: XReg) -> Self::Return { + let a = self.state.x(src1).get_i32(); + let b = self.state.x(src2).get_i32(); + self.state.x_mut(dst).set_u64(u64::from(a < b)); + Continuation::Continue + } + + fn xslteq32(&mut self, dst: XReg, src1: XReg, src2: XReg) -> Self::Return { + let a = self.state.x(src1).get_i32(); + let b = self.state.x(src2).get_i32(); + self.state.x_mut(dst).set_u64(u64::from(a <= b)); + Continuation::Continue + } + + fn xult32(&mut self, dst: XReg, src1: XReg, src2: XReg) -> Self::Return { + let a = self.state.x(src1).get_u32(); + let b = self.state.x(src2).get_u32(); + self.state.x_mut(dst).set_u64(u64::from(a < b)); + Continuation::Continue + } + + fn xulteq32(&mut self, dst: XReg, src1: XReg, src2: XReg) -> Self::Return { + let a = self.state.x(src1).get_u32(); + let b = self.state.x(src2).get_u32(); + self.state.x_mut(dst).set_u64(u64::from(a <= b)); + Continuation::Continue + } + + fn load32_u(&mut self, dst: XReg, ptr: XReg) -> Self::Return { + let ptr = self.state.x(ptr).get_usize(); + let ptr = ptr as *mut u32; + let val = unsafe { ptr::read(ptr) }; + self.state.x_mut(dst).set_u64(u64::from(val)); + Continuation::Continue + } + + fn load32_s(&mut self, dst: XReg, ptr: XReg) -> Self::Return { + let ptr = self.state.x(ptr).get_usize(); + let ptr = ptr as *mut i32; + let val = unsafe { ptr::read(ptr) }; + self.state.x_mut(dst).set_i64(i64::from(val)); + Continuation::Continue + } + + fn load64(&mut self, dst: XReg, ptr: XReg) -> Self::Return { + let ptr = self.state.x(ptr).get_usize(); + let ptr = ptr as *mut u64; + let val = unsafe { ptr::read(ptr) }; + self.state.x_mut(dst).set_u64(val); + Continuation::Continue + } + + fn load32_u_offset8(&mut self, dst: XReg, ptr: XReg, offset: i8) -> Self::Return { + let ptr = self.state.x(ptr).get_usize(); + let offset = isize::from(offset); + let ptr = ptr.wrapping_add(offset as usize); + let ptr = ptr as *mut u32; + let val = unsafe { ptr::read(ptr) }; + self.state.x_mut(dst).set_u64(u64::from(val)); + Continuation::Continue + } + + fn load32_s_offset8(&mut self, dst: XReg, ptr: XReg, offset: i8) -> Self::Return { + let ptr = self.state.x(ptr).get_usize(); + let offset = isize::from(offset); + let ptr = ptr.wrapping_add(offset as usize); + let ptr = ptr as *mut i32; + let val = unsafe { ptr::read(ptr) }; + self.state.x_mut(dst).set_i64(i64::from(val)); + Continuation::Continue + } + + fn load64_offset8(&mut self, dst: XReg, ptr: XReg, offset: i8) -> Self::Return { + let ptr = self.state.x(ptr).get_usize(); + let offset = isize::from(offset); + let ptr = ptr.wrapping_add(offset as usize); + let ptr = ptr as *mut u64; + let val = unsafe { ptr::read(ptr) }; + self.state.x_mut(dst).set_u64(val); + Continuation::Continue + } + + fn store32(&mut self, ptr: XReg, src: XReg) -> Self::Return { + let ptr = self.state.x(ptr).get_usize(); + let ptr = ptr as *mut u32; + let val = self.state.x(src).get_u32(); + unsafe { + ptr::write(ptr, val); + } + Continuation::Continue + } + + fn store64(&mut self, ptr: XReg, src: XReg) -> Self::Return { + let ptr = self.state.x(ptr).get_usize(); + let ptr = ptr as *mut u64; + let val = self.state.x(src).get_u64(); + unsafe { + ptr::write(ptr, val); + } + Continuation::Continue + } + + fn store32_offset8(&mut self, ptr: XReg, offset: i8, src: XReg) -> Self::Return { + let ptr = self.state.x(ptr).get_usize(); + let offset = isize::from(offset); + let ptr = ptr.wrapping_add(offset as usize); + let ptr = ptr as *mut u32; + let val = self.state.x(src).get_u32(); + unsafe { + ptr::write(ptr, val); + } + Continuation::Continue + } + + fn store64_offset8(&mut self, ptr: XReg, offset: i8, src: XReg) -> Self::Return { + let ptr = self.state.x(ptr).get_usize(); + let offset = isize::from(offset); + let ptr = ptr.wrapping_add(offset as usize); + let ptr = ptr as *mut u64; + let val = self.state.x(src).get_u64(); + unsafe { + ptr::write(ptr, val); + } + Continuation::Continue + } + + fn bitcast_int_from_float_32(&mut self, dst: XReg, src: FReg) -> Self::Return { + let val = self.state.f(src).get_f32(); + self.state + .x_mut(dst) + .set_u64(u32::from_ne_bytes(val.to_ne_bytes()).into()); + Continuation::Continue + } + + fn bitcast_int_from_float_64(&mut self, dst: XReg, src: FReg) -> Self::Return { + let val = self.state.f(src).get_f64(); + self.state + .x_mut(dst) + .set_u64(u64::from_ne_bytes(val.to_ne_bytes())); + Continuation::Continue + } + + fn bitcast_float_from_int_32(&mut self, dst: FReg, src: XReg) -> Self::Return { + let val = self.state.x(src).get_u32(); + self.state + .f_mut(dst) + .set_f32(f32::from_ne_bytes(val.to_ne_bytes())); + Continuation::Continue + } + + fn bitcast_float_from_int_64(&mut self, dst: FReg, src: XReg) -> Self::Return { + let val = self.state.x(src).get_u64(); + self.state + .f_mut(dst) + .set_f64(f64::from_ne_bytes(val.to_ne_bytes())); + Continuation::Continue + } +} + +impl ExtendedOpVisitor for InterpreterVisitor<'_> { + fn nop(&mut self) -> Self::Return { + Continuation::Continue + } + + fn trap(&mut self) -> Self::Return { + Continuation::Trap + } + + fn get_sp(&mut self, dst: XReg) -> Self::Return { + let sp = self.state.x(XReg::SP).get_u64(); + self.state.x_mut(dst).set_u64(sp); + Continuation::Continue + } +} diff --git a/pulley/src/lib.rs b/pulley/src/lib.rs new file mode 100644 index 000000000000..b0a62dc1c3c1 --- /dev/null +++ b/pulley/src/lib.rs @@ -0,0 +1,184 @@ +//! The pulley bytecode for fast interpreters. + +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![deny(missing_docs)] +#![no_std] + +#[cfg(feature = "std")] +#[macro_use] +extern crate std; + +#[allow(unused_extern_crates)] // Some cfg's don't use this. +extern crate alloc; + +// TODO: make `struct BinaryOpRegs { dst: T, src1: T, src2: T }` or something +// and pack it into 2 bytes (5 bits for each operand; 2**5 = 32 possible +// registers, and then only special isntructions to access special x registers) + +/// Calls the given macro with each opcode. +macro_rules! for_each_op { + ( $macro:ident ) => { + $macro! { + /// Transfer control the address in the `lr` register. + ret = Ret; + + /// Transfer control to the PC at the given offset and set the `lr` + /// register to the PC just after this instruction. + call = Call { offset: PcRelOffset }; + + /// Unconditionally transfer control to the PC at the given offset. + jump = Jump { offset: PcRelOffset }; + + /// Conditionally transfer control to the given PC offset if `cond` + /// contains a non-zero value. + br_if = BrIf { cond: XReg, offset: PcRelOffset }; + + /// Conditionally transfer control to the given PC offset if `cond` + /// contains a zero value. + br_if_not = BrIfNot { cond: XReg, offset: PcRelOffset }; + + /// Branch if `a == b`. + br_if_xeq32 = BrIfXeq32 { a: XReg, b: XReg, offset: PcRelOffset }; + /// Branch if `a != `b. + br_if_xneq32 = BrIfXneq32 { a: XReg, b: XReg, offset: PcRelOffset }; + /// Branch if signed `a < b`. + br_if_xslt32 = BrIfXslt32 { a: XReg, b: XReg, offset: PcRelOffset }; + /// Branch if signed `a <= b`. + br_if_xslteq32 = BrIfXslteq32 { a: XReg, b: XReg, offset: PcRelOffset }; + /// Branch if unsigned `a < b`. + br_if_xult32 = BrIfXult32 { a: XReg, b: XReg, offset: PcRelOffset }; + /// Branch if unsigned `a <= b`. + br_if_xulteq32 = BrIfXulteq32 { a: XReg, b: XReg, offset: PcRelOffset }; + + /// Move between `x` registers. + xmov = Xmov { dst: XReg, src: XReg }; + /// Move between `f` registers. + fmov = Fmov { dst: FReg, src: FReg }; + /// Move between `v` registers. + vmov = Vmov { dst: VReg, src: VReg }; + + /// Set `dst = zero_extend(imm8)`. + xconst8 = Xconst8 { dst: XReg, imm: u8 }; + /// Set `dst = zero_extend(imm16)`. + xconst16 = Xconst16 { dst: XReg, imm: u16 }; + /// Set `dst = zero_extend(imm32)`. + xconst32 = Xconst32 { dst: XReg, imm: u32 }; + /// Set `dst = imm64`. + xconst64 = Xconst64 { dst: XReg, imm: u64 }; + + /// 32-bit wrapping addition: `low32(dst) = low32(src1) + low32(src2)`. + /// + /// The upper 32-bits of `dst` are unmodified. + xadd32 = Xadd32 { dst: XReg, src1: XReg, src2: XReg }; + + /// 64-bit wrapping addition: `dst = src1 + src2`. + xadd64 = Xadd64 { dst: XReg, src1: XReg, src2: XReg }; + + /// 64-bit equality. + xeq64 = Xeq64 { dst: XReg, src1: XReg, src2: XReg }; + /// 64-bit inequality. + xneq64 = Xneq64 { dst: XReg, src1: XReg, src2: XReg }; + /// 64-bit signed less-than. + xslt64 = Xslt64 { dst: XReg, src1: XReg, src2: XReg }; + /// 64-bit signed less-than-equal. + xslteq64 = Xslteq64 { dst: XReg, src1: XReg, src2: XReg }; + /// 64-bit unsigned less-than. + xult64 = Xult64 { dst: XReg, src1: XReg, src2: XReg }; + /// 64-bit unsigned less-than-equal. + xulteq64 = Xulteq64 { dst: XReg, src1: XReg, src2: XReg }; + /// 32-bit equality. + xeq32 = Xeq32 { dst: XReg, src1: XReg, src2: XReg }; + /// 32-bit inequality. + xneq32 = Xneq32 { dst: XReg, src1: XReg, src2: XReg }; + /// 32-bit signed less-than. + xslt32 = Xslt32 { dst: XReg, src1: XReg, src2: XReg }; + /// 32-bit signed less-than-equal. + xslteq32 = Xslteq32 { dst: XReg, src1: XReg, src2: XReg }; + /// 32-bit unsigned less-than. + xult32 = Xult32 { dst: XReg, src1: XReg, src2: XReg }; + /// 32-bit unsigned less-than-equal. + xulteq32 = Xulteq32 { dst: XReg, src1: XReg, src2: XReg }; + + /// `dst = zero_extend(load32(ptr))` + load32_u = Load32U { dst: XReg, ptr: XReg }; + /// `dst = sign_extend(load32(ptr))` + load32_s = Load32S { dst: XReg, ptr: XReg }; + /// `dst = load64(ptr)` + load64 = Load64 { dst: XReg, ptr: XReg }; + + /// `dst = zero_extend(load32(ptr + offset8))` + load32_u_offset8 = Load32UOffset8 { dst: XReg, ptr: XReg, offset: i8 }; + /// `dst = sign_extend(load32(ptr + offset8))` + load32_s_offset8 = Load32SOffset8 { dst: XReg, ptr: XReg, offset: i8 }; + /// `dst = load64(ptr + offset8)` + load64_offset8 = Load64Offset8 { dst: XReg, ptr: XReg, offset: i8 }; + + /// `*ptr = low32(src)` + store32 = Store32 { ptr: XReg, src: XReg }; + /// `*ptr = src` + store64 = Store64 { ptr: XReg, src: XReg }; + + /// `*(ptr + sign_extend(offset8)) = low32(src)` + store32_offset8 = Store32SOffset8 { ptr: XReg, offset: i8, src: XReg }; + /// `*(ptr + sign_extend(offset8)) = src` + store64_offset8 = Store64Offset8 { ptr: XReg, offset: i8, src: XReg }; + + /// `low32(dst) = bitcast low32(src) as i32` + bitcast_int_from_float_32 = BitcastIntFromFloat32 { dst: XReg, src: FReg }; + /// `dst = bitcast src as i64` + bitcast_int_from_float_64 = BitcastIntFromFloat64 { dst: XReg, src: FReg }; + /// `low32(dst) = bitcast low32(src) as f32` + bitcast_float_from_int_32 = BitcastFloatFromInt32 { dst: FReg, src: XReg }; + /// `dst = bitcast src as f64` + bitcast_float_from_int_64 = BitcastFloatFromInt64 { dst: FReg, src: XReg }; + } + }; +} + +/// Calls the given macro with each extended opcode. +macro_rules! for_each_extended_op { + ( $macro:ident ) => { + $macro! { + /// Raise a trap. + trap = Trap; + + /// Do nothing. + nop = Nop; + + /// Copy the special `sp` stack pointer register into an `x` register. + get_sp = GetSp { dst: XReg }; + } + }; +} + +#[cfg(feature = "decode")] +pub mod decode; +#[cfg(feature = "disas")] +pub mod disas; +#[cfg(feature = "encode")] +pub mod encode; +#[cfg(feature = "interp")] +pub mod interp; + +pub mod regs; +pub use regs::*; + +pub mod imms; +pub use imms::*; + +pub mod op; +pub use op::*; + +pub mod opcode; +pub use opcode::*; + +#[allow(dead_code)] // Unused in some `cfg`s. +pub(crate) unsafe fn unreachable_unchecked() -> T { + #[cfg(debug_assertions)] + unreachable!(); + + #[cfg_attr(debug_assertions, allow(unreachable_code))] + unsafe { + core::hint::unreachable_unchecked() + } +} diff --git a/pulley/src/op.rs b/pulley/src/op.rs new file mode 100644 index 000000000000..38c59b301d2d --- /dev/null +++ b/pulley/src/op.rs @@ -0,0 +1,256 @@ +//! Pulley bytecode operations with their operands. + +use crate::imms::*; +use crate::regs::*; + +macro_rules! define_op { + ( + $( + $( #[$attr:meta] )* + $snake_name:ident = $name:ident $( { $( $field:ident : $field_ty:ty ),* } )? ; + )* + ) => { + /// A complete, materialized operation/instruction. + /// + /// This type is useful for debugging, writing tests, etc... but is not + /// actually ever used by the interpreter, encoder, or decoder, all of + /// which avoid materializing ops. + #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] + #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] + pub enum Op { + $( + $( #[$attr] )* + $name($name), + )* + /// An extended operation/instruction. + ExtendedOp(ExtendedOp), + } + + $( + $( #[$attr] )* + #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] + #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] + pub struct $name { $( + $( + // TODO: add doc comments to all fields and update all + // the macros to match them. + #[allow(missing_docs)] + pub $field : $field_ty, + )* + )? } + + impl From<$name> for Op { + #[inline] + fn from(op: $name) -> Self { + Self::$name(op) + } + } + )* + }; +} +for_each_op!(define_op); + +impl From for Op { + #[inline] + fn from(op: ExtendedOp) -> Self { + Op::ExtendedOp(op) + } +} + +macro_rules! define_extended_op { + ( + $( + $( #[$attr:meta] )* + $snake_name:ident = $name:ident $( { $( $field:ident : $field_ty:ty ),* } )? ; + )* + ) => { + /// An extended operation/instruction. + /// + /// These tend to be colder than `Op`s. + #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] + #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] + pub enum ExtendedOp { + $( + $( #[$attr] )* + $name($name), + )* + } + + $( + $( #[$attr] )* + #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] + #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] + pub struct $name { $( + $( + // TODO: add doc comments to all fields and update all + // the macros to match them. + #[allow(missing_docs)] + pub $field : $field_ty, + )* + )? } + + impl From<$name> for Op { + #[inline] + fn from(op: $name) -> Self { + Self::ExtendedOp(ExtendedOp::$name(op)) + } + } + + impl From<$name> for ExtendedOp { + #[inline] + fn from(op: $name) -> Self { + Self::$name(op) + } + } + )* + }; +} +for_each_extended_op!(define_extended_op); + +macro_rules! define_op_encode { + ( + $( + $( #[$attr:meta] )* + $snake_name:ident = $name:ident $( { $( $field:ident : $field_ty:ty ),* } )? ; + )* + ) => { + impl Op { + /// Encode this op into the given sink. + #[cfg(feature = "encode")] + pub fn encode(&self, into: &mut E) + where + E: Extend, + { + match self { + $( + Self::$name(op) => op.encode(into), + )* + Self::ExtendedOp(op) => op.encode(into), + } + } + } + + $( + impl $name { + /// Encode this + #[doc = concat!("`", stringify!($name), "`")] + /// op into the given sink. + #[cfg(feature = "encode")] + pub fn encode(&self, into: &mut E) + where + E: Extend, + { + crate::encode::$snake_name(into $( $( , self.$field )* )?); + } + } + )* + }; +} +for_each_op!(define_op_encode); + +macro_rules! define_extended_op_encode { + ( + $( + $( #[$attr:meta] )* + $snake_name:ident = $name:ident $( { $( $field:ident : $field_ty:ty ),* } )? ; + )* + ) => { + impl ExtendedOp { + /// Encode this extended op into the given sink. + #[cfg(feature = "encode")] + pub fn encode(&self, into: &mut E) + where + E: Extend, + { + match self { + $( + Self::$name(op) => op.encode(into), + )* + } + } + } + + $( + impl $name { + /// Encode this + #[doc = concat!("`", stringify!($name), "`")] + /// op into the given sink. + #[cfg(feature = "encode")] + pub fn encode(&self, into: &mut E) + where + E: Extend, + { + crate::encode::$snake_name(into $( $( , self.$field )* )?); + } + } + )* + }; +} +for_each_extended_op!(define_extended_op_encode); + +/// A visitor that materializes whole `Op`s as it decodes the bytecode stream. +#[cfg(feature = "decode")] +#[derive(Default)] +pub struct MaterializeOpsVisitor { + bytecode: B, +} + +#[cfg(feature = "decode")] +impl MaterializeOpsVisitor { + /// Create a new op-materializing visitor for the given bytecode. + pub fn new(bytecode: B) -> Self { + Self { bytecode } + } +} + +macro_rules! define_materialize_op_visitor { + ( + $( + $( #[$attr:meta] )* + $snake_name:ident = $name:ident $( { $( $field:ident : $field_ty:ty ),* } )? ; + )* + ) => { + #[cfg(feature = "decode")] + impl crate::decode::OpVisitor for MaterializeOpsVisitor { + type BytecodeStream = B; + + fn bytecode(&mut self) -> &mut Self::BytecodeStream { + &mut self.bytecode + } + + type Return = crate::op::Op; + + $( + $( #[$attr] )* + fn $snake_name(&mut self $( $( , $field : $field_ty )* )? ) -> Self::Return { + crate::op::Op::$name(crate::op::$name { $( $( + $field, + )* )? }) + } + )* + } + }; +} +for_each_op!(define_materialize_op_visitor); + +macro_rules! define_materialize_extended_op_visitor { + ( + $( + $( #[$attr:meta] )* + $snake_name:ident = $name:ident $( { $( $field:ident : $field_ty:ty ),* } )? ; + )* + ) => { + #[cfg(feature = "decode")] + impl crate::decode::ExtendedOpVisitor for MaterializeOpsVisitor { + $( + $( #[$attr] )* + fn $snake_name(&mut self $( $( , $field : $field_ty )* )? ) -> Self::Return { + crate::op::ExtendedOp::$name(crate::op::$name { $( $( + $field, + )* )? }).into() + } + )* + } + }; +} +for_each_extended_op!(define_materialize_extended_op_visitor); diff --git a/pulley/src/opcode.rs b/pulley/src/opcode.rs new file mode 100644 index 000000000000..922cbf26a113 --- /dev/null +++ b/pulley/src/opcode.rs @@ -0,0 +1,125 @@ +//! Pulley opcodes (without operands). + +macro_rules! define_opcode { + ( + $( + $( #[$attr:meta] )* + $snake_name:ident = $name:ident $( { + $( + $( #[$field_attr:meta] )* + $field:ident : $field_ty:ty + ),* + } )? ; + )* + ) => { + /// An opcode without its immediates and operands. + #[repr(u8)] + #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub enum Opcode { + $( + $( #[$attr] )* + $name, + )* + /// The extended-op opcode. An `ExtendedOpcode` follows this opcode. + ExtendedOp, + } + + impl Opcode { + /// The value of the maximum defined opcode. + pub const MAX: u8 = define_opcode!( @max $( $name )* ) + 1; + } + }; + + ( @max $x:ident ) => { 0 }; + ( @max $x:ident $( $xs:ident )* ) => { 1 + define_opcode!(@max $( $xs )* ) }; +} +for_each_op!(define_opcode); + +impl Opcode { + /// Create a new `Opcode` from the given byte. + /// + /// Returns `None` if `byte` is not a valid opcode. + pub fn new(byte: u8) -> Option { + if byte <= Self::MAX { + Some(unsafe { Self::unchecked_new(byte) }) + } else { + None + } + } + + /// Like `new` but does not check whether `byte` is a valid opcode. + /// + /// # Safety + /// + /// It is unsafe to pass a `byte` that is not a valid opcode. + pub unsafe fn unchecked_new(byte: u8) -> Self { + debug_assert!(byte <= Self::MAX); + core::mem::transmute(byte) + } +} + +macro_rules! define_extended_opcode { + ( + $( + $( #[$attr:meta] )* + $snake_name:ident = $name:ident $( { $( $field:ident : $field_ty:ty ),* } )? ; + )* + ) => { + /// An extended opcode. + #[repr(u16)] + #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub enum ExtendedOpcode { + $( + $( #[$attr] )* + $name, + )* + } + + impl ExtendedOpcode { + /// The value of the maximum defined extended opcode. + pub const MAX: u16 = define_opcode!( @max $( $name )* ); + } + }; + + ( @max $x:ident ) => { 0 }; + ( @max $x:ident $( $xs:ident )* ) => { 1 + define_opcode!(@max $( $xs )* ) }; +} +for_each_extended_op!(define_extended_opcode); + +impl ExtendedOpcode { + pub(crate) const ENCODED_SIZE_OF_TRAP: usize = 3; + + /// Create a new `ExtendedOpcode` from the given bytes. + /// + /// Returns `None` if `bytes` is not a valid extended opcode. + pub fn new(bytes: u16) -> Option { + if bytes <= Self::MAX { + Some(unsafe { Self::unchecked_new(bytes) }) + } else { + None + } + } + + /// Like `new` but does not check whether `bytes` is a valid opcode. + /// + /// # Safety + /// + /// It is unsafe to pass `bytes` that is not a valid opcode. + pub unsafe fn unchecked_new(byte: u16) -> Self { + debug_assert!(byte <= Self::MAX); + core::mem::transmute(byte) + } +} + +#[cfg(all(test, feature = "encode"))] +mod tests { + use super::*; + use alloc::vec::Vec; + + #[test] + fn encoded_size_of_trap() { + let mut buf = Vec::new(); + crate::encode::trap(&mut buf); + assert_eq!(ExtendedOpcode::ENCODED_SIZE_OF_TRAP, buf.len()); + } +} diff --git a/pulley/src/regs.rs b/pulley/src/regs.rs new file mode 100644 index 000000000000..a7339f699418 --- /dev/null +++ b/pulley/src/regs.rs @@ -0,0 +1,201 @@ +//! Pulley registers. + +use core::{fmt, ops::Range}; + +macro_rules! define_registers { + ( + $( + $( #[$attr:meta] )* + pub struct $name:ident = $range:expr; + )* +) => { + $( + $( #[ $attr ] )* + #[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct $name(u8); + + impl fmt::Debug for $name { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> core::fmt::Result { + fmt::Display::fmt(self, f) + } + } + + impl $name { + /// The valid register range for this register class. + pub const RANGE: Range = $range; + + /// Construct a new register of this class. + #[inline] + pub fn new(index: u8) -> Option { + if Self::RANGE.start <= index && index < Self::RANGE.end { + Some(unsafe { Self::unchecked_new(index) }) + } else { + None + } + } + + /// Construct a new register of this class without checking that + /// `index` is a valid register index. + #[inline] + pub unsafe fn unchecked_new(index: u8) -> Self { + debug_assert!(Self::RANGE.start <= index && index < Self::RANGE.end); + Self(index) + } + + /// Get this register's index. + #[inline] + pub fn index(&self) -> usize { + usize::from(self.0) + } + } + + #[cfg(feature = "arbitrary")] + impl<'a> arbitrary::Arbitrary<'a> for $name { + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + let index = u.int_in_range(Self::RANGE.start..=Self::RANGE.end - 1)?; + Ok(Self(index)) + } + } + )* + } +} + +define_registers! { + /// An `x` register: integers. + pub struct XReg = 0..37; + + /// An `f` register: floats. + pub struct FReg = 0..32; + + /// A `v` register: vectors. + pub struct VReg = 0..32; +} + +/// Any register, regardless of class. +/// +/// Never appears inside an instruction -- instructions always name a particular +/// class of register -- but this is useful for testing and things like that. +#[allow(missing_docs)] +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum AnyReg { + X(XReg), + F(FReg), + V(VReg), +} + +impl From for AnyReg { + fn from(x: XReg) -> Self { + Self::X(x) + } +} + +impl From for AnyReg { + fn from(f: FReg) -> Self { + Self::F(f) + } +} + +impl From for AnyReg { + fn from(v: VReg) -> Self { + Self::V(v) + } +} + +impl fmt::Display for AnyReg { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + AnyReg::X(r) => fmt::Display::fmt(r, f), + AnyReg::F(r) => fmt::Display::fmt(r, f), + AnyReg::V(r) => fmt::Display::fmt(r, f), + } + } +} + +impl fmt::Debug for AnyReg { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> core::fmt::Result { + fmt::Display::fmt(self, f) + } +} + +#[cfg(feature = "arbitrary")] +impl<'a> arbitrary::Arbitrary<'a> for AnyReg { + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + match u.int_in_range(0..=2)? { + 0 => Ok(AnyReg::X(u.arbitrary()?)), + 1 => Ok(AnyReg::F(u.arbitrary()?)), + 2 => Ok(AnyReg::V(u.arbitrary()?)), + _ => unreachable!(), + } + } +} + +impl XReg { + /// The valid special register range. + pub const SPECIAL_RANGE: Range = 32..37; + + /// The special `sp` stack pointer register. + pub const SP: Self = Self(32); + + /// The special `lr` link register. + pub const LR: Self = Self(33); + + /// The special `fp` frame pointer register. + pub const FP: Self = Self(34); + + /// The special `spilltmp0` scratch register. + pub const SPILL_TMP_0: Self = Self(35); + + /// The special `spilltmp1` scratch register. + pub const SPILL_TMP_1: Self = Self(36); + + /// Is this `x` register a special register? + pub fn is_special(&self) -> bool { + self.0 >= Self::SPECIAL_RANGE.start + } +} + +impl fmt::Display for XReg { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + x if *x == Self::SP => write!(f, "sp"), + x if *x == Self::LR => write!(f, "lr"), + x if *x == Self::FP => write!(f, "fp"), + x if *x == Self::SPILL_TMP_0 => write!(f, "spilltmp0"), + x if *x == Self::SPILL_TMP_1 => write!(f, "spilltmp1"), + Self(x) => write!(f, "x{x}"), + } + } +} + +impl fmt::Display for FReg { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "f{}", self.0) + } +} + +impl fmt::Display for VReg { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "v{}", self.0) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn special_x_regs() { + assert!(XReg::SP.is_special()); + assert!(XReg::LR.is_special()); + assert!(XReg::FP.is_special()); + assert!(XReg::SPILL_TMP_0.is_special()); + assert!(XReg::SPILL_TMP_1.is_special()); + } + + #[test] + fn not_special_x_regs() { + for i in 0..32 { + assert!(!XReg::new(i).unwrap().is_special()); + } + } +} diff --git a/pulley/tests/all/disas.rs b/pulley/tests/all/disas.rs new file mode 100644 index 000000000000..b91ecf224ff1 --- /dev/null +++ b/pulley/tests/all/disas.rs @@ -0,0 +1,101 @@ +//! Disassembly tests. + +use pulley_interpreter::*; + +fn encoded(ops: &[Op]) -> Vec { + let mut encoded = vec![]; + for op in ops { + op.encode(&mut encoded); + } + log::trace!("encoded: {encoded:?}"); + encoded +} + +fn assert_disas(ops: &[Op], expected: &str) { + let expected = expected.trim(); + eprintln!("=== expected ===\n{expected}"); + + let bytecode = encoded(ops); + + let actual = disas::Disassembler::disassemble_all(&bytecode).expect("decoding failed"); + let actual = actual.trim(); + eprintln!("=== actual ===\n{actual}"); + + assert_eq!(expected, actual); +} + +#[test] +fn simple() { + let x0 = XReg::new(0).unwrap(); + let x1 = XReg::new(1).unwrap(); + let x31 = XReg::new(31).unwrap(); + + assert_disas( + &[ + // Prologue. + Op::Xconst64(Xconst64 { + dst: x31, + imm: -16i64 as u64, + }), + Op::Xadd32(Xadd32 { + dst: XReg::SP, + src1: XReg::SP, + src2: x31, + }), + Op::Store64Offset8(Store64Offset8 { + ptr: XReg::SP, + offset: 8, + src: XReg::LR, + }), + Op::Store64(Store64 { + ptr: XReg::SP, + src: XReg::FP, + }), + Op::Xmov(Xmov { + dst: XReg::FP, + src: XReg::SP, + }), + // Function body. + Op::Xadd32(Xadd32 { + dst: x0, + src1: x0, + src2: x1, + }), + // Epilogue. + Op::Xmov(Xmov { + dst: XReg::SP, + src: XReg::FP, + }), + Op::Load64Offset8(Load64Offset8 { + dst: XReg::LR, + ptr: XReg::SP, + offset: 8, + }), + Op::Load64(Load64 { + dst: XReg::FP, + ptr: XReg::SP, + }), + Op::Xconst8(Xconst8 { dst: x31, imm: 16 }), + Op::Xadd32(Xadd32 { + dst: XReg::SP, + src1: XReg::SP, + src2: x31, + }), + Op::Ret(Ret {}), + ], + r#" + 0: 11 1f f0 ff ff ff ff ff ff ff xconst64 x31, 18446744073709551600 + a: 12 20 20 1f xadd32 sp, sp, x31 + e: 29 20 08 21 store64_offset8 sp, 8, lr + 12: 27 20 22 store64 sp, fp + 15: 0b 22 20 xmov fp, sp + 18: 12 00 00 01 xadd32 x0, x0, x1 + 1c: 0b 20 22 xmov sp, fp + 1f: 25 21 20 08 load64_offset8 lr, sp, 8 + 23: 22 22 20 load64 fp, sp + 26: 0e 1f 10 xconst8 x31, 16 + 29: 12 20 20 1f xadd32 sp, sp, x31 + 2d: 00 ret + "#, + ); +} diff --git a/pulley/tests/all/interp.rs b/pulley/tests/all/interp.rs new file mode 100644 index 000000000000..132d84ef05c5 --- /dev/null +++ b/pulley/tests/all/interp.rs @@ -0,0 +1,993 @@ +//! Interpreter tests. + +use pulley_interpreter::{interp::Vm, *}; +use std::{cell::UnsafeCell, fmt::Debug, ptr::NonNull}; + +fn encoded(ops: &[Op]) -> Vec { + let mut encoded = vec![]; + for op in ops { + op.encode(&mut encoded); + } + log::trace!("encoded: {encoded:?}"); + encoded +} + +unsafe fn run(vm: &mut Vm, ops: &[Op]) -> Result<(), *mut u8> { + let _ = env_logger::try_init(); + let ops = encoded(ops); + let _ = vm.call(NonNull::from(&ops[0]), &[], [])?; + Ok(()) +} + +unsafe fn assert_one( + xs: impl IntoIterator, + op: impl Into + Debug, + result: R1, + expected: u64, +) where + R0: Into, + R1: Into, +{ + eprintln!("======================================================="); + let mut vm = Vm::new(); + + for (reg, val) in xs { + let reg = reg.into(); + eprintln!("{reg} = {val:#018x}"); + match reg { + AnyReg::X(r) => vm.state_mut().x_mut(r).set_u64(val), + AnyReg::F(r) => vm.state_mut().f_mut(r).set_f64(f64::from_bits(val)), + AnyReg::V(_) => todo!(), + } + } + + eprintln!("op = {op:?}"); + let op = op.into(); + + run(&mut vm, &[op, Op::Ret(Ret {})]).expect("should not trap"); + + eprintln!("expected = {expected:#018x}"); + + let actual = match result.into() { + AnyReg::X(r) => vm.state_mut().x(r).get_u64(), + AnyReg::F(r) => vm.state_mut().f(r).get_f64().to_bits(), + AnyReg::V(_) => todo!(), + }; + eprintln!("actual = {actual:#018x}"); + + assert_eq!(expected, actual); +} + +fn x(x: u8) -> XReg { + XReg::new(x).unwrap() +} + +fn f(f: u8) -> FReg { + FReg::new(f).unwrap() +} + +#[test] +fn xconst8() { + for (expected, imm) in [(42u64, 42u8), (u64::from(u8::MAX), u8::MAX)] { + unsafe { + assert_one( + [(x(0), 0x1234567812345678)], + Xconst8 { dst: x(0), imm }, + x(0), + expected, + ); + } + } +} + +#[test] +fn xconst16() { + for (expected, imm) in [(42u64, 42u16), (u64::from(u16::MAX), u16::MAX)] { + unsafe { + assert_one( + [(x(0), 0x1234567812345678)], + Xconst16 { dst: x(0), imm }, + x(0), + expected, + ); + } + } +} + +#[test] +fn xconst32() { + for (expected, imm) in [(42u64, 42u32), (u64::from(u32::MAX), u32::MAX)] { + unsafe { + assert_one( + [(x(0), 0x1234567812345678)], + Xconst32 { dst: x(0), imm }, + x(0), + expected, + ); + } + } +} + +#[test] +fn xconst64() { + for (expected, imm) in [(42u64, 42u64), (u64::MAX, u64::MAX)] { + unsafe { + assert_one( + [(x(0), 0x1234567812345678)], + Xconst64 { dst: x(0), imm }, + x(0), + expected, + ); + } + } +} + +#[test] +fn xadd32() { + for (expected, a, b) in [ + (42u64 | 0x1234567800000000, 10u64, 32u64), + (0x1234567800000000, u32::MAX as _, 1), + ] { + unsafe { + assert_one( + [(x(0), 0x1234567812345678), (x(1), a), (x(2), b)], + Xadd32 { + dst: x(0), + src1: x(1), + src2: x(2), + }, + x(0), + expected, + ); + } + } +} + +#[test] +fn xadd64() { + for (expected, a, b) in [(42u64, 10u64, 32u64), (0, u64::MAX, 1)] { + unsafe { + assert_one( + [(x(0), 0x1234567812345678), (x(1), a), (x(2), b)], + Xadd64 { + dst: x(0), + src1: x(1), + src2: x(2), + }, + x(0), + expected, + ); + } + } +} + +#[test] +fn xeq64() { + for (expected, a, b) in [ + (1u64, 0u64, 0u64), + (0, 0, 1), + (1, u64::MAX, u64::MAX), + (0, u64::MAX, u64::MAX - 1), + ] { + unsafe { + assert_one( + [(x(0), 0x1234567812345678), (x(1), a), (x(2), b)], + Xeq64 { + dst: x(0), + src1: x(1), + src2: x(2), + }, + x(0), + expected, + ); + } + } +} + +#[test] +fn xneq64() { + for (expected, a, b) in [ + (0u64, 0u64, 0u64), + (1, 0, 1), + (0, u64::MAX, u64::MAX), + (1, u64::MAX, u64::MAX - 1), + ] { + unsafe { + assert_one( + [(x(0), 0x1234567812345678), (x(1), a), (x(2), b)], + Xneq64 { + dst: x(0), + src1: x(1), + src2: x(2), + }, + x(0), + expected, + ); + } + } +} + +#[test] +fn xslt64() { + for (expected, a, b) in [ + (0u64, 0u64, 0u64), + (0, 1, 0), + (1, 0, 1), + (0, 0, -1 as _), + (1, -1 as _, 0), + (0, i64::MAX as u64, i64::MAX as u64), + (0, i64::MAX as u64, i64::MAX as u64 - 1), + (1, i64::MAX as u64 - 1, i64::MAX as u64), + (0, i64::MIN as u64, i64::MIN as u64), + (0, i64::MIN as u64 + 1, i64::MIN as u64), + (1, i64::MIN as u64, i64::MIN as u64 + 1), + ] { + unsafe { + assert_one( + [(x(0), 0x1234567812345678), (x(1), a), (x(2), b)], + Xslt64 { + dst: x(0), + src1: x(1), + src2: x(2), + }, + x(0), + expected, + ); + } + } +} + +#[test] +fn xslteq64() { + for (expected, a, b) in [ + (1u64, 0u64, 0u64), + (0, 1, 0), + (1, 0, 1), + (0, 0, -1 as _), + (1, -1 as _, 0), + (1, i64::MAX as u64, i64::MAX as u64), + (0, i64::MAX as u64, i64::MAX as u64 - 1), + (1, i64::MAX as u64 - 1, i64::MAX as u64), + (1, i64::MIN as u64, i64::MIN as u64), + (0, i64::MIN as u64 + 1, i64::MIN as u64), + (1, i64::MIN as u64, i64::MIN as u64 + 1), + ] { + unsafe { + assert_one( + [(x(0), 0x1234567812345678), (x(1), a), (x(2), b)], + Xslteq64 { + dst: x(0), + src1: x(1), + src2: x(2), + }, + x(0), + expected, + ); + } + } +} + +#[test] +fn xult64() { + for (expected, a, b) in [ + (0u64, 0u64, 0u64), + (0, 1, 0), + (1, 0, 1), + (0, u64::MAX, u64::MAX), + (0, u64::MAX, u64::MAX - 1), + (1, u64::MAX - 1, u64::MAX), + (0, i64::MIN as u64, 0), + (1, 0, i64::MIN as u64), + ] { + unsafe { + assert_one( + [(x(0), 0x1234567812345678), (x(1), a), (x(2), b)], + Xult64 { + dst: x(0), + src1: x(1), + src2: x(2), + }, + x(0), + expected, + ); + } + } +} + +#[test] +fn xulteq64() { + for (expected, a, b) in [ + (1u64, 0u64, 0u64), + (0, 1, 0), + (1, 0, 1), + (1, u64::MAX, u64::MAX), + (0, u64::MAX, u64::MAX - 1), + (1, u64::MAX - 1, u64::MAX), + (0, i64::MIN as u64, 0), + (1, 0, i64::MIN as u64), + ] { + unsafe { + assert_one( + [(x(0), 0x1234567812345678), (x(1), a), (x(2), b)], + Xulteq64 { + dst: x(0), + src1: x(1), + src2: x(2), + }, + x(0), + expected, + ); + } + } +} + +#[test] +fn xeq32() { + for (expected, a, b) in [ + (1u64, 0u64, 0u64), + (0, 0, 1), + (1, u64::MAX, u64::MAX), + (0, u64::MAX, u64::MAX - 1), + (1, 0xffffffff00000001, 1), + (0, 0xffffffff00000000, 1), + (0, 0xffffffff00000001, 0), + (1, 0xffffffff00000000, 0), + ] { + unsafe { + assert_one( + [(x(0), 0x1234567812345678), (x(1), a), (x(2), b)], + Xeq32 { + dst: x(0), + src1: x(1), + src2: x(2), + }, + x(0), + expected, + ); + } + } +} + +#[test] +fn xneq32() { + for (expected, a, b) in [ + (0u64, 0u64, 0u64), + (1, 0, 1), + (0, u64::MAX, u64::MAX), + (1, u64::MAX, u64::MAX - 1), + (0, 0xffffffff00000000, 0), + ] { + unsafe { + assert_one( + [(x(0), 0x1234567812345678), (x(1), a), (x(2), b)], + Xneq32 { + dst: x(0), + src1: x(1), + src2: x(2), + }, + x(0), + expected, + ); + } + } +} + +#[test] +fn xslt32() { + for (expected, a, b) in [ + (0u64, 0u64, 0u64), + (0, 1, 0), + (1, 0, 1), + (0, 0, -1 as _), + (1, -1 as _, 0), + (0, i64::MAX as u64, i64::MAX as u64), + (0, i64::MAX as u64, i64::MAX as u64 - 1), + (1, i64::MAX as u64 - 1, i64::MAX as u64), + (0, i64::MIN as u64, i64::MIN as u64), + (0, i64::MIN as u64 + 1, i64::MIN as u64), + (1, i64::MIN as u64, i64::MIN as u64 + 1), + (1, 0x00000000ffffffff, 0), + (0, 0, 0x00000000ffffffff), + ] { + unsafe { + assert_one( + [(x(0), 0x1234567812345678), (x(1), a), (x(2), b)], + Xslt32 { + dst: x(0), + src1: x(1), + src2: x(2), + }, + x(0), + expected, + ); + } + } +} + +#[test] +fn xslteq32() { + for (expected, a, b) in [ + (1u64, 0u64, 0u64), + (0, 1, 0), + (1, 0, 1), + (0, 0, -1 as _), + (1, -1 as _, 0), + (1, i64::MAX as u64, i64::MAX as u64), + (0, i64::MAX as u64, i64::MAX as u64 - 1), + (1, i64::MAX as u64 - 1, i64::MAX as u64), + (1, i64::MIN as u64, i64::MIN as u64), + (0, i64::MIN as u64 + 1, i64::MIN as u64), + (1, i64::MIN as u64, i64::MIN as u64 + 1), + ] { + unsafe { + assert_one( + [(x(0), 0x1234567812345678), (x(1), a), (x(2), b)], + Xslteq32 { + dst: x(0), + src1: x(1), + src2: x(2), + }, + x(0), + expected, + ); + } + } +} + +#[test] +fn xult32() { + for (expected, a, b) in [ + (0u64, 0u64, 0u64), + (0, 1, 0), + (1, 0, 1), + (0, 0x00000000ffffffff, 0xfffffffffffffffe), + (1, 0xfffffffffffffffe, 0x00000000ffffffff), + (0, 0x00000000ffffffff, 0xffffffffffffffff), + (0, 0xfffffffffffffffe, 0x00000000fffffffe), + ] { + unsafe { + assert_one( + [(x(0), 0x1234567812345678), (x(1), a), (x(2), b)], + Xult32 { + dst: x(0), + src1: x(1), + src2: x(2), + }, + x(0), + expected, + ); + } + } +} + +#[test] +fn xulteq32() { + for (expected, a, b) in [ + (1u64, 0u64, 0u64), + (0, 1, 0), + (1, 0, 1), + (0, 0x00000000ffffffff, 0xfffffffffffffffe), + (1, 0xfffffffffffffffe, 0x00000000ffffffff), + (1, 0x00000000ffffffff, 0xffffffffffffffff), + (1, 0xfffffffffffffffe, 0x00000000fffffffe), + ] { + unsafe { + assert_one( + [(x(0), 0x1234567812345678), (x(1), a), (x(2), b)], + Xulteq32 { + dst: x(0), + src1: x(1), + src2: x(2), + }, + x(0), + expected, + ); + } + } +} + +#[test] +fn load32_u() { + let a = UnsafeCell::new(11u32); + let b = UnsafeCell::new(22u32); + let c = UnsafeCell::new(33u32); + let d = UnsafeCell::new(i32::MIN); + + for (expected, addr) in [ + (11, a.get() as usize), + (22, b.get() as usize), + (33, c.get() as usize), + (i32::MIN as u32 as u64, d.get() as usize), + ] { + unsafe { + assert_one( + [ + (x(0), 0x1234567812345678), + (x(1), u64::try_from(addr).unwrap()), + ], + Load32U { + dst: x(0), + ptr: x(1), + }, + x(0), + expected, + ); + } + } +} + +#[test] +fn load32_s() { + let a = UnsafeCell::new(11u32); + let b = UnsafeCell::new(22u32); + let c = UnsafeCell::new(33u32); + let d = UnsafeCell::new(-1i32); + + for (expected, addr) in [ + (11, a.get() as usize), + (22, b.get() as usize), + (33, c.get() as usize), + (-1i64 as u64, d.get() as usize), + ] { + unsafe { + assert_one( + [ + (x(0), 0x1234567812345678), + (x(1), u64::try_from(addr).unwrap()), + ], + Load32S { + dst: x(0), + ptr: x(1), + }, + x(0), + expected, + ); + } + } +} + +#[test] +fn load64() { + let a = UnsafeCell::new(11u64); + let b = UnsafeCell::new(22u64); + let c = UnsafeCell::new(33u64); + let d = UnsafeCell::new(-1i64); + + for (expected, addr) in [ + (11, a.get() as usize), + (22, b.get() as usize), + (33, c.get() as usize), + (-1i64 as u64, d.get() as usize), + ] { + unsafe { + assert_one( + [ + (x(0), 0x1234567812345678), + (x(1), u64::try_from(addr).unwrap()), + ], + Load64 { + dst: x(0), + ptr: x(1), + }, + x(0), + expected, + ); + } + } +} + +#[test] +fn load32_u_offset8() { + let a = UnsafeCell::new([11u32, 22]); + let b = UnsafeCell::new([33u32, 44]); + let c = UnsafeCell::new([55u32, 66]); + let d = UnsafeCell::new([i32::MIN, i32::MAX]); + + for (expected, addr, offset) in [ + (11, a.get() as usize, 0), + (22, a.get() as usize, 4), + (33, b.get() as usize, 0), + (44, b.get() as usize, 4), + (55, c.get() as usize, 0), + (66, c.get() as usize, 4), + (i32::MIN as u32 as u64, d.get() as usize, 0), + (i32::MAX as u32 as u64, d.get() as usize, 4), + ] { + unsafe { + assert_one( + [ + (x(0), 0x1234567812345678), + (x(1), u64::try_from(addr).unwrap()), + ], + Load32UOffset8 { + dst: x(0), + ptr: x(1), + offset, + }, + x(0), + expected, + ); + } + } +} + +#[test] +fn load32_s_offset8() { + let a = UnsafeCell::new([11u32, 22]); + let b = UnsafeCell::new([33u32, 44]); + let c = UnsafeCell::new([55u32, 66]); + let d = UnsafeCell::new([-1i32, i32::MAX]); + + for (expected, addr, offset) in [ + (11, a.get() as usize, 0), + (22, a.get() as usize, 4), + (33, b.get() as usize, 0), + (44, b.get() as usize, 4), + (55, c.get() as usize, 0), + (66, c.get() as usize, 4), + (-1i64 as u64, d.get() as usize, 0), + (i32::MAX as u32 as u64, d.get() as usize, 4), + ] { + unsafe { + assert_one( + [ + (x(0), 0x1234567812345678), + (x(1), u64::try_from(addr).unwrap()), + ], + Load32SOffset8 { + dst: x(0), + ptr: x(1), + offset, + }, + x(0), + expected, + ); + } + } +} + +#[test] +fn load64_offset8() { + let a = UnsafeCell::new([11u64, 22]); + let b = UnsafeCell::new([33u64, 44]); + let c = UnsafeCell::new([55u64, 66]); + let d = UnsafeCell::new([-1i64, i64::MAX]); + + for (expected, addr, offset) in [ + (11, a.get() as usize, 0), + (22, a.get() as usize, 8), + (33, b.get() as usize, 0), + (44, b.get() as usize, 8), + (55, c.get() as usize, 0), + (66, c.get() as usize, 8), + (-1i64 as u64, d.get() as usize, 0), + (i64::MAX as u64, d.get() as usize, 8), + ] { + unsafe { + assert_one( + [ + (x(0), 0x1234567812345678), + (x(1), u64::try_from(addr).unwrap()), + ], + Load64Offset8 { + dst: x(0), + ptr: x(1), + offset, + }, + x(0), + expected, + ); + } + } +} + +#[test] +fn store32() { + let a = UnsafeCell::new([0x12u8, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78]); + let b = UnsafeCell::new([0x12u8, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78]); + let c = UnsafeCell::new([0x12u8, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78]); + + unsafe { + for (val, addr) in [ + (0x11111111u32, a.get() as usize), + (0x22222222, b.get().byte_add(4) as usize), + (0x33333333, c.get().byte_add(2) as usize), + ] { + let val = val as u64; + assert_one( + [(x(0), u64::try_from(addr).unwrap()), (x(1), val)], + Store32 { + ptr: x(0), + src: x(1), + }, + x(1), + val, + ); + } + } + + let a = u64::from_be_bytes(a.into_inner()); + let expected = 0x1111111112345678u64; + eprintln!("expected(a) = {expected:#018x}"); + eprintln!("actual(a) = {a:#018x}"); + assert_eq!(a, expected); + + let b = u64::from_be_bytes(b.into_inner()); + let expected = 0x1234567822222222u64; + eprintln!("expected(b) = {expected:#018x}"); + eprintln!("actual(b) = {b:#018x}"); + assert_eq!(b, expected); + + let c = u64::from_be_bytes(c.into_inner()); + let expected = 0x1234333333335678u64; + eprintln!("expected(c) = {expected:#018x}"); + eprintln!("actual(c) = {c:#018x}"); + assert_eq!(c, expected); +} + +#[test] +fn store64() { + let a = UnsafeCell::new(0x1234567812345678); + let b = UnsafeCell::new(0x1234567812345678); + let c = UnsafeCell::new(0x1234567812345678); + + unsafe { + for (val, addr) in [ + (0x1111111111111111u64, a.get() as usize), + (0x2222222222222222, b.get() as usize), + (0x3333333333333333, c.get() as usize), + ] { + assert_one( + [(x(0), u64::try_from(addr).unwrap()), (x(1), val)], + Store64 { + ptr: x(0), + src: x(1), + }, + x(1), + val, + ); + } + } + + let a = a.into_inner(); + let expected = 0x1111111111111111u64; + eprintln!("expected(a) = {expected:#018x}"); + eprintln!("actual(a) = {a:#018x}"); + assert_eq!(a, expected); + + let b = b.into_inner(); + let expected = 0x2222222222222222u64; + eprintln!("expected(b) = {expected:#018x}"); + eprintln!("actual(b) = {b:#018x}"); + assert_eq!(b, expected); + + let c = c.into_inner(); + let expected = 0x3333333333333333u64; + eprintln!("expected(c) = {expected:#018x}"); + eprintln!("actual(c) = {c:#018x}"); + assert_eq!(c, expected); +} + +#[test] +fn store32_offset8() { + let a = UnsafeCell::new([0x12u8, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78]); + let b = UnsafeCell::new([0x12u8, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78]); + let c = UnsafeCell::new([0x12u8, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78]); + + unsafe { + for (val, addr, offset) in [ + (0x11111111u32, a.get() as usize, 0), + (0x22222222, b.get() as usize, 4), + (0x33333333, c.get() as usize, 2), + ] { + let val = val as u64; + assert_one( + [(x(0), u64::try_from(addr).unwrap()), (x(1), val)], + Store32SOffset8 { + ptr: x(0), + src: x(1), + offset, + }, + x(1), + val, + ); + } + } + + let a = u64::from_be_bytes(a.into_inner()); + let expected = 0x1111111112345678u64; + eprintln!("expected(a) = {expected:#018x}"); + eprintln!("actual(a) = {a:#018x}"); + assert_eq!(a, expected); + + let b = u64::from_be_bytes(b.into_inner()); + let expected = 0x1234567822222222u64; + eprintln!("expected(b) = {expected:#018x}"); + eprintln!("actual(b) = {b:#018x}"); + assert_eq!(b, expected); + + let c = u64::from_be_bytes(c.into_inner()); + let expected = 0x1234333333335678u64; + eprintln!("expected(c) = {expected:#018x}"); + eprintln!("actual(c) = {c:#018x}"); + assert_eq!(c, expected); +} + +#[test] +fn store64_offset8() { + let a = UnsafeCell::new([0x1234567812345678, 0x1234567812345678, 0x1234567812345678]); + + unsafe { + for (val, addr, offset) in [ + (0x1111111111111111u64, a.get() as usize, 0), + (0x2222222222222222, a.get() as usize, 8), + (0x3333333333333333, a.get() as usize, 16), + ] { + assert_one( + [(x(0), u64::try_from(addr).unwrap()), (x(1), val)], + Store64Offset8 { + ptr: x(0), + src: x(1), + offset, + }, + x(1), + val, + ); + } + } + + let [a, b, c] = a.into_inner(); + + let expected = 0x1111111111111111u64; + eprintln!("expected(a) = {expected:#018x}"); + eprintln!("actual(a) = {a:#018x}"); + assert_eq!(a, expected); + + let expected = 0x2222222222222222u64; + eprintln!("expected(b) = {expected:#018x}"); + eprintln!("actual(b) = {b:#018x}"); + assert_eq!(b, expected); + + let expected = 0x3333333333333333u64; + eprintln!("expected(c) = {expected:#018x}"); + eprintln!("actual(c) = {c:#018x}"); + assert_eq!(c, expected); +} + +#[test] +fn bitcast_int_from_float_32() { + for val in [ + 0.0, + 1.0, + 9.87654321, + f32::MAX, + f32::MIN, + f32::NAN, + f32::INFINITY, + f32::NEG_INFINITY, + f32::EPSILON, + f32::MIN_POSITIVE, + ] { + let val = val.to_bits() as u64; + unsafe { + assert_one( + [(f(0), val)], + BitcastIntFromFloat32 { + dst: x(0), + src: f(0), + }, + x(0), + val, + ); + } + } +} + +#[test] +fn bitcast_int_from_float_64() { + for val in [ + 0.0, + 1.0, + 9.87654321, + f64::MAX, + f64::MIN, + f64::NAN, + f64::INFINITY, + f64::NEG_INFINITY, + f64::EPSILON, + f64::MIN_POSITIVE, + ] { + let val = val.to_bits(); + unsafe { + assert_one( + [(f(0), val)], + BitcastIntFromFloat64 { + dst: x(0), + src: f(0), + }, + x(0), + val, + ); + } + } +} + +#[test] +fn bitcast_float_from_int_32() { + for val in [ + 0.0, + 1.0, + 9.87654321, + f32::MAX, + f32::MIN, + f32::NAN, + f32::INFINITY, + f32::NEG_INFINITY, + f32::EPSILON, + f32::MIN_POSITIVE, + ] { + let val = val.to_bits() as u64; + unsafe { + assert_one( + [(x(0), val)], + BitcastFloatFromInt32 { + dst: f(0), + src: x(0), + }, + f(0), + val, + ); + } + } +} + +#[test] +fn bitcast_float_from_int_64() { + for val in [ + 0.0, + 1.0, + 9.87654321, + f64::MAX, + f64::MIN, + f64::NAN, + f64::INFINITY, + f64::NEG_INFINITY, + f64::EPSILON, + f64::MIN_POSITIVE, + ] { + let val = val.to_bits(); + unsafe { + assert_one( + [(x(0), val)], + BitcastFloatFromInt64 { + dst: f(0), + src: x(0), + }, + f(0), + val, + ); + } + } +} + +#[test] +fn trap() { + let mut vm = Vm::new(); + let dst = XReg::new(0).unwrap(); + + unsafe { + run( + &mut vm, + &[ + Op::Xconst16(Xconst16 { dst, imm: 1 }), + Op::ExtendedOp(ExtendedOp::Trap(Trap {})), + Op::Xconst16(Xconst16 { dst, imm: 2 }), + Op::Ret(Ret {}), + ], + ) + .unwrap_err(); + } + + // `dst` should not have been written to the second time. + assert_eq!(vm.state().x(dst).get_u32(), 1); +} diff --git a/pulley/tests/all/main.rs b/pulley/tests/all/main.rs new file mode 100644 index 000000000000..38e113a178b6 --- /dev/null +++ b/pulley/tests/all/main.rs @@ -0,0 +1,5 @@ +#[cfg(feature = "disas")] +mod disas; + +#[cfg(feature = "interp")] +mod interp; diff --git a/scripts/publish.rs b/scripts/publish.rs index 245a912ee050..7197b1589262 100644 --- a/scripts/publish.rs +++ b/scripts/publish.rs @@ -18,6 +18,7 @@ use std::time::Duration; // note that this list must be topologically sorted by dependencies const CRATES_TO_PUBLISH: &[&str] = &[ // cranelift + "pulley-interpreter", "cranelift-bitset", "cranelift-isle", "cranelift-entity", diff --git a/supply-chain/config.toml b/supply-chain/config.toml index 115ac2c4b49a..290afc7a9905 100644 --- a/supply-chain/config.toml +++ b/supply-chain/config.toml @@ -79,6 +79,12 @@ audit-as-crates-io = true [policy.isle-fuzz] criteria = [] +[policy.pulley-interpreter] +audit-as-crates-io = true + +[policy.pulley-interpreter-fuzz] +criteria = [] + [policy.wasi-common] audit-as-crates-io = true