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
+
+
+
+
+
+
+
+
+
+
+
+## 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