Repository: bytecodealliance/wasmtime
Pull request #9099 (2024-08-09 21:34:31)

pulley: superinstructions for pushing/popping list of registers
Adds superinstructions for pushing/popping any subset of XRegs in one go

github-actions[bot]: #### Subscribe to Label Action

cc @fitzgen

<details>
This issue or pull request has been labeled: "cranelift", "pulley"

Thus the following users have been cc'd because of the following labels:

* fitzgen: pulley

To subscribe or unsubscribe from this label, edit the <code>.github/subscribe-to-label.json</code> configuration file.

[Learn more.](https://github.com/bytecodealliance/subscribe-to-label-action)
</details>
fitzgen: And then once https://github.com/bytecodealliance/wasmtime/pull/9088 is rebased and lands, we can do the same with this one. Thanks again for making these improvements @Kmeakin!
fitzgen: This needs a rebase after #9088 
alexcrichton: I believe the error in CI is that in the `scripts/publish.rs` file the `cranelift-bitset` crate needs to be reordered before the `pulley` family of crates since they now depend on `cranelift-bitset`
fitzgen: Side note, it will be a little faster to iterate here to add an empty commit with the message `prtest:full` so that the full CI checks happen and we don't have to wait for the merge queue.
fitzgen: I'm not totally sure what is going on here, new-ish dependencies for new-ish are always a little annoying to get set up correctly so that when we publish we don't run into any issues. I guess we are effectively front-loading the issues, rather than dealing with them at publish time, which is probably the right choice. Anyways, I think you should be able to repro locally with

```shell-session
$ rustc scripts/publish.rs
$ ./publish verify
```

Note that if you have a `.cargo` directory in your wasmtime checkout already, you'll want to move it somewhere else temporarily while running the above.
fitzgen: > I believe the error in CI is that in the `scripts/publish.rs` file the `cranelift-bitset` crate needs to be reordered before the `pulley` family of crates since they now depend on `cranelift-bitset`

^ Did the fix for this perhaps get lost in a rebase?

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index d4cba3dc3..745aaf366 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -584,6 +584,7 @@ dependencies = [
 name = "cranelift-bitset"
 version = "0.112.0"
 dependencies = [
+ "arbitrary",
  "serde",
  "serde_derive",
 ]
@@ -2145,6 +2146,7 @@ name = "pulley-interpreter"
 version = "0.1.0"
 dependencies = [
  "arbitrary",
+ "cranelift-bitset",
  "env_logger",
  "log",
  "sptr",
diff --git a/cranelift/bitset/Cargo.toml b/cranelift/bitset/Cargo.toml
index 841ac8f62..2e6d2dd06 100644
--- a/cranelift/bitset/Cargo.toml
+++ b/cranelift/bitset/Cargo.toml
@@ -13,8 +13,10 @@ rust-version.workspace = true
 workspace = true
 
 [dependencies]
+arbitrary = { workspace = true, optional = true }
 serde = { workspace = true, optional = true }
 serde_derive = { workspace = true, optional = true }
 
 [features]
 enable-serde = ["dep:serde", "dep:serde_derive"]
+arbitrary = ["dep:arbitrary"]
diff --git a/cranelift/bitset/src/scalar.rs b/cranelift/bitset/src/scalar.rs
index 2d959016c..07649c297 100644
--- a/cranelift/bitset/src/scalar.rs
+++ b/cranelift/bitset/src/scalar.rs
@@ -350,6 +350,33 @@ where
         self.0 = T::from(0);
     }
 
+    /// Remove and return the smallest value in the bitset.
+    ///
+    /// # Example
+    ///
+    /// ```
+    /// use cranelift_bitset::ScalarBitSet;
+    ///
+    /// let mut bitset = ScalarBitSet::<u64>::new();
+    ///
+    /// bitset.insert(0);
+    /// bitset.insert(24);
+    /// bitset.insert(13);
+    /// bitset.insert(36);
+    ///
+    /// assert_eq!(bitset.pop_min(), Some(0));
+    /// assert_eq!(bitset.pop_min(), Some(13));
+    /// assert_eq!(bitset.pop_min(), Some(24));
+    /// assert_eq!(bitset.pop_min(), Some(36));
+    /// assert_eq!(bitset.pop_min(), None);
+    /// ```
+    #[inline]
+    pub fn pop_min(&mut self) -> Option<u8> {
+        let min = self.min()?;
+        self.remove(min);
+        Some(min)
+    }
+
     /// Remove and return the largest value in the bitset.
     ///
     /// # Example
@@ -364,14 +391,14 @@ where
     /// bitset.insert(13);
     /// bitset.insert(36);
     ///
-    /// assert_eq!(bitset.pop(), Some(36));
-    /// assert_eq!(bitset.pop(), Some(24));
-    /// assert_eq!(bitset.pop(), Some(13));
-    /// assert_eq!(bitset.pop(), Some(0));
-    /// assert_eq!(bitset.pop(), None);
+    /// assert_eq!(bitset.pop_max(), Some(36));
+    /// assert_eq!(bitset.pop_max(), Some(24));
+    /// assert_eq!(bitset.pop_max(), Some(13));
+    /// assert_eq!(bitset.pop_max(), Some(0));
+    /// assert_eq!(bitset.pop_max(), None);
     /// ```
     #[inline]
-    pub fn pop(&mut self) -> Option<u8> {
+    pub fn pop_max(&mut self) -> Option<u8> {
         let max = self.max()?;
         self.remove(max);
         Some(max)
@@ -458,11 +485,8 @@ where
     /// );
     /// ```
     #[inline]
-    pub fn iter(&self) -> Iter<T> {
-        Iter {
-            value: self.0,
-            index: 0,
-        }
+    pub fn iter(self) -> Iter<T> {
+        Iter { bitset: self }
     }
 }
 
@@ -494,6 +518,12 @@ where
     }
 }
 
+impl<T: ScalarBitSetStorage> From<T> for ScalarBitSet<T> {
+    fn from(bits: T) -> Self {
+        Self(bits)
+    }
+}
+
 /// A trait implemented by all integers that can be used as the backing storage
 /// for a [`ScalarBitSet`].
 ///
@@ -550,8 +580,7 @@ impl_storage!(usize);
 
 /// An iterator over the elements in a [`ScalarBitSet`].
 pub struct Iter<T> {
-    value: T,
-    index: u8,
+    bitset: ScalarBitSet<T>,
 }
 
 impl<T> Iterator for Iter<T>
@@ -562,14 +591,36 @@ where
 
     #[inline]
     fn next(&mut self) -> Option<u8> {
-        if self.value == T::from(0) {
-            None
-        } else {
-            let trailing_zeros = self.value.trailing_zeros();
-            let elem = self.index + trailing_zeros;
-            self.index += trailing_zeros + 1;
-            self.value = self.value >> (trailing_zeros + 1);
-            Some(elem)
-        }
+        self.bitset.pop_min()
+    }
+}
+
+impl<T> DoubleEndedIterator for Iter<T>
+where
+    T: ScalarBitSetStorage,
+{
+    #[inline]
+    fn next_back(&mut self) -> Option<Self::Item> {
+        self.bitset.pop_max()
+    }
+}
+
+impl<T> ExactSizeIterator for Iter<T>
+where
+    T: ScalarBitSetStorage,
+{
+    #[inline]
+    fn len(&self) -> usize {
+        usize::from(self.bitset.len())
+    }
+}
+
+#[cfg(feature = "arbitrary")]
+impl<'a, T> arbitrary::Arbitrary<'a> for ScalarBitSet<T>
+where
+    T: ScalarBitSetStorage + arbitrary::Arbitrary<'a>,
+{
+    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
+        T::arbitrary(u).map(Self)
     }
 }
diff --git a/pulley/Cargo.toml b/pulley/Cargo.toml
index 89ffa0861..b8c9f3138 100644
--- a/pulley/Cargo.toml
+++ b/pulley/Cargo.toml
@@ -14,6 +14,7 @@ workspace = true
 
 [dependencies]
 arbitrary = { workspace = true, optional = true }
+cranelift-bitset = { workspace = true } 
 log = { workspace = true }
 sptr = { workspace = true }
 
@@ -22,7 +23,7 @@ env_logger = { workspace = true }
 
 [features]
 std = []
-arbitrary = ["dep:arbitrary", "arbitrary/derive", "std"]
+arbitrary = ["dep:arbitrary", "arbitrary/derive", "std", "cranelift-bitset/arbitrary"]
 encode = []
 decode = []
 disas = ["decode"]
diff --git a/pulley/README.md b/pulley/README.md
index 49aba7b82..8460c4d90 100644
--- a/pulley/README.md
+++ b/pulley/README.md
@@ -47,27 +47,16 @@ to change, instructions to appear and disappear, and APIs to be overhauled.
 Here is the disassembly of `f(a, b) = a + b` in Pulley today:
 
 ```
-       0: 0e 1a f0                        xconst8 x26, -16
-       3: 12 7b 6b                        xadd32 sp, sp, x26
-       6: 2c 1b 08 1c                     store64_offset8 sp, 8, lr
-       a: 2a 1b 1d                        store64 sp, fp
-       d: 0b 1d 1b                        xmov fp, sp
-      10: 12 00 04                        xadd32 x0, x0, x1
-      13: 0b 1b 1d                        xmov sp, fp
-      16: 25 1c 1b 08                     load64_offset8 lr, sp, 8
-      1a: 22 1d 1b                        load64 fp, sp
-      1d: 0e 1a 10                        xconst8 x26, 16
-      20: 12 7b 6b                        xadd32 sp, sp, x26
-      23: 00                              ret
+       0: 2f                              push_frame
+       1: 12 00 04                        xadd32 x0, x0, x1
+       4: 30                              pop_frame
+       5: 00                              ret
 ```
 
 Note that there are a number of things that could be improved here:
 
 * We could avoid allocating and deallocating a stack frame because this function's
   body doesn't use any stack slots.
-* 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.
 
diff --git a/pulley/fuzz/src/interp.rs b/pulley/fuzz/src/interp.rs
index 9acdb7625..424cfc588 100644
--- a/pulley/fuzz/src/interp.rs
+++ b/pulley/fuzz/src/interp.rs
@@ -105,6 +105,11 @@ fn op_is_safe_for_fuzzing(op: &Op) -> bool {
         | Op::Xslteq32(Xslteq32 { operands, .. })
         | Op::Xult32(Xult32 { operands, .. })
         | Op::Xulteq32(Xulteq32 { operands, .. }) => !operands.dst.is_special(),
+        Op::PushFrame(_) | Op::PopFrame(_) => false,
+        Op::XPush32(_) | Op::XPush64(_) => false,
+        Op::XPop32(_) | Op::XPop64(_) => false,
+        Op::XPush32Many(_) | Op::XPush64Many(_) => false,
+        Op::XPop32Many(_) | Op::XPop64Many(_) => false,
     }
 }
 
diff --git a/pulley/src/decode.rs b/pulley/src/decode.rs
index 74be7e153..09f4da80d 100644
--- a/pulley/src/decode.rs
+++ b/pulley/src/decode.rs
@@ -1,6 +1,8 @@
 //! Decoding support for pulley bytecode.
 
 use alloc::vec::Vec;
+use cranelift_bitset::scalar::ScalarBitSetStorage;
+use cranelift_bitset::ScalarBitSet;
 
 use crate::imms::*;
 use crate::opcode::*;
@@ -384,6 +386,24 @@ impl<R: Reg> Decode for BinaryOperands<R> {
     }
 }
 
+impl<S: Decode + ScalarBitSetStorage> Decode for ScalarBitSet<S> {
+    fn decode<T>(bytecode: &mut T) -> Result<Self, T::Error>
+    where
+        T: BytecodeStream,
+    {
+        S::decode(bytecode).map(ScalarBitSet::from)
+    }
+}
+
+impl<R: Reg + Decode> Decode for RegSet<R> {
+    fn decode<T>(bytecode: &mut T) -> Result<Self, T::Error>
+    where
+        T: BytecodeStream,
+    {
+        ScalarBitSet::decode(bytecode).map(Self::from)
+    }
+}
+
 /// A Pulley bytecode decoder.
 ///
 /// Does not materialize bytecode instructions, instead all decoding methods are
diff --git a/pulley/src/disas.rs b/pulley/src/disas.rs
index 6900cfa39..912b1756d 100644
--- a/pulley/src/disas.rs
+++ b/pulley/src/disas.rs
@@ -127,13 +127,26 @@ impl Disas for PcRelOffset {
     }
 }
 
-impl<R: Disas> Disas for BinaryOperands<R> {
-    fn disas(&self, position: usize, disas: &mut String) {
-        self.dst.disas(position, disas);
-        write!(disas, ", ").unwrap();
-        self.src1.disas(position, disas);
+fn disas_list<T: Disas>(position: usize, disas: &mut String, iter: impl IntoIterator<Item = T>) {
+    let mut iter = iter.into_iter();
+    let Some(first) = iter.next() else { return };
+    first.disas(position, disas);
+
+    for item in iter {
         write!(disas, ", ").unwrap();
-        self.src2.disas(position, disas);
+        item.disas(position, disas);
+    }
+}
+
+impl<R: Reg + Disas> Disas for BinaryOperands<R> {
+    fn disas(&self, position: usize, disas: &mut String) {
+        disas_list(position, disas, [self.dst, self.src1, self.src2])
+    }
+}
+
+impl<R: Reg + Disas> Disas for RegSet<R> {
+    fn disas(&self, position: usize, disas: &mut String) {
+        disas_list(position, disas, *self)
     }
 }
 
diff --git a/pulley/src/encode.rs b/pulley/src/encode.rs
index 686ca9946..dcc5894c5 100644
--- a/pulley/src/encode.rs
+++ b/pulley/src/encode.rs
@@ -87,7 +87,7 @@ impl Encode for XReg {
     where
         E: Extend<u8>,
     {
-        sink.extend(core::iter::once(u8::try_from(self.index()).unwrap()));
+        sink.extend(core::iter::once(self.to_u8()));
     }
 }
 
@@ -96,7 +96,7 @@ impl Encode for FReg {
     where
         E: Extend<u8>,
     {
-        sink.extend(core::iter::once(u8::try_from(self.index()).unwrap()));
+        sink.extend(core::iter::once(self.to_u8()));
     }
 }
 
@@ -105,7 +105,7 @@ impl Encode for VReg {
     where
         E: Extend<u8>,
     {
-        sink.extend(core::iter::once(u8::try_from(self.index()).unwrap()));
+        sink.extend(core::iter::once(self.to_u8()));
     }
 }
 
@@ -127,6 +127,15 @@ impl<R: Reg> Encode for BinaryOperands<R> {
     }
 }
 
+impl<R: Reg + Encode> Encode for RegSet<R> {
+    fn encode<E>(&self, sink: &mut E)
+    where
+        E: Extend<u8>,
+    {
+        self.to_bitset().0.encode(sink);
+    }
+}
+
 macro_rules! impl_encoders {
     (
         $(
diff --git a/pulley/src/interp.rs b/pulley/src/interp.rs
index d03d6ab99..4eed9bfe5 100644
--- a/pulley/src/interp.rs
+++ b/pulley/src/interp.rs
@@ -569,6 +569,21 @@ impl MachineState {
 
         state
     }
+
+    /// `*sp = val; sp += size_of::<T>()`
+    fn push<T>(&mut self, val: T) {
+        let sp = self[XReg::sp].get_ptr::<T>();
+        unsafe { sp.write_unaligned(val) }
+        self[XReg::sp].set_ptr(sp.wrapping_add(1));
+    }
+
+    /// `ret = *sp; sp -= size_of::<T>()`
+    fn pop<T>(&mut self) -> T {
+        let sp = self[XReg::sp].get_ptr::<T>();
+        let val = unsafe { sp.read_unaligned() };
+        self[XReg::sp].set_ptr(sp.wrapping_sub(1));
+        val
+    }
 }
 
 enum Continuation {
@@ -988,6 +1003,74 @@ impl OpVisitor for InterpreterVisitor<'_> {
         Continuation::Continue
     }
 
+    fn xpush32(&mut self, src: XReg) -> Self::Return {
+        self.state.push(self.state[src].get_u32());
+        Continuation::Continue
+    }
+
+    fn xpush32_many(&mut self, srcs: RegSet<XReg>) -> Self::Return {
+        for src in srcs {
+            self.xpush32(src);
+        }
+        Continuation::Continue
+    }
+
+    fn xpush64(&mut self, src: XReg) -> Self::Return {
+        self.state.push(self.state[src].get_u64());
+        Continuation::Continue
+    }
+
+    fn xpush64_many(&mut self, srcs: RegSet<XReg>) -> Self::Return {
+        for src in srcs {
+            self.xpush64(src);
+        }
+        Continuation::Continue
+    }
+
+    fn xpop32(&mut self, dst: XReg) -> Self::Return {
+        let val = self.state.pop();
+        self.state[dst].set_u32(val);
+        Continuation::Continue
+    }
+
+    fn xpop32_many(&mut self, dsts: RegSet<XReg>) -> Self::Return {
+        for dst in dsts.into_iter().rev() {
+            self.xpop32(dst);
+        }
+        Continuation::Continue
+    }
+
+    fn xpop64(&mut self, dst: XReg) -> Self::Return {
+        let val = self.state.pop();
+        self.state[dst].set_u64(val);
+        Continuation::Continue
+    }
+
+    fn xpop64_many(&mut self, dsts: RegSet<XReg>) -> Self::Return {
+        for dst in dsts.into_iter().rev() {
+            self.xpop64(dst);
+        }
+        Continuation::Continue
+    }
+
+    /// `push lr; push fp; fp = sp`
+    fn push_frame(&mut self) -> Self::Return {
+        self.state.push(self.state[XReg::lr].get_ptr::<u8>());
+        self.state.push(self.state[XReg::fp].get_ptr::<u8>());
+        self.state[XReg::fp] = self.state[XReg::sp];
+        Continuation::Continue
+    }
+
+    /// `sp = fp; pop fp; pop lr`
+    fn pop_frame(&mut self) -> Self::Return {
+        self.state[XReg::sp] = self.state[XReg::fp];
+        let fp = self.state.pop();
+        let lr = self.state.pop();
+        self.state[XReg::fp].set_ptr::<u8>(fp);
+        self.state[XReg::lr].set_ptr::<u8>(lr);
+        Continuation::Continue
+    }
+
     fn bitcast_int_from_float_32(&mut self, dst: XReg, src: FReg) -> Self::Return {
         let val = self.state[src].get_f32();
         self.state[dst].set_u64(u32::from_ne_bytes(val.to_ne_bytes()).into());
diff --git a/pulley/src/lib.rs b/pulley/src/lib.rs
index 819f69fc2..0b454f593 100644
--- a/pulley/src/lib.rs
+++ b/pulley/src/lib.rs
@@ -131,6 +131,29 @@ macro_rules! for_each_op {
             /// `*(ptr + sign_extend(offset64)) = src`
             store64_offset64 = Store64Offset64 { ptr: XReg, offset: i64, src: XReg };
 
+            /// `push lr; push fp; fp = sp`
+            push_frame = PushFrame ;
+            /// `sp = fp; pop fp; pop lr`
+            pop_frame = PopFrame ;
+
+            /// `*sp = low32(src); sp += 4`
+            xpush32 = XPush32 { src: XReg };
+            /// `for src in srcs { xpush32 src }`
+            xpush32_many = XPush32Many { srcs: RegSet<XReg> };
+            /// `*sp = src; sp += 8`
+            xpush64 = XPush64 { src: XReg };
+            /// `for src in srcs { xpush64 src }`
+            xpush64_many = XPush64Many { srcs: RegSet<XReg> };
+
+            /// `*dst = *sp; sp -= 4`
+            xpop32 = XPop32 { dst: XReg };
+            /// `for dst in dsts.rev() { xpop32 dst }`
+            xpop32_many = XPop32Many { dsts: RegSet<XReg> };
+            /// `*dst = *sp; sp -= 8`
+            xpop64 = XPop64 { dst: XReg };
+            /// `for dst in dsts.rev() { xpop64 dst }`
+            xpop64_many = XPop64Many { dsts: RegSet<XReg> };
+
             /// `low32(dst) = bitcast low32(src) as i32`
             bitcast_int_from_float_32 = BitcastIntFromFloat32 { dst: XReg, src: FReg };
             /// `dst = bitcast src as i64`
diff --git a/pulley/src/op.rs b/pulley/src/op.rs
index 38c59b301..9caa63fff 100644
--- a/pulley/src/op.rs
+++ b/pulley/src/op.rs
@@ -15,7 +15,7 @@ macro_rules! define_op {
         /// 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)]
+        #[derive(Clone, Copy, Debug, PartialEq, Eq)]
         #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
         pub enum Op {
             $(
@@ -28,7 +28,7 @@ macro_rules! define_op {
 
         $(
             $( #[$attr] )*
-            #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+            #[derive(Clone, Copy, Debug, PartialEq, Eq)]
             #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
             pub struct $name { $(
                 $(
diff --git a/pulley/src/regs.rs b/pulley/src/regs.rs
index 062e5eb1f..2b651aae8 100644
--- a/pulley/src/regs.rs
+++ b/pulley/src/regs.rs
@@ -1,8 +1,11 @@
 //! Pulley registers.
 
 use core::hash::Hash;
+use core::marker::PhantomData;
 use core::{fmt, ops::Range};
 
+use cranelift_bitset::ScalarBitSet;
+
 /// Trait for common register operations.
 pub trait Reg: Sized + Copy + Eq + Ord + Hash + Into<AnyReg> + fmt::Debug + fmt::Display {
     /// Range of valid register indices.
@@ -159,9 +162,9 @@ impl fmt::Debug for AnyReg {
 pub struct BinaryOperands<R> {
     /// The destination register, packed in bits 0..5.
     pub dst: R,
-    /// The frist source register, packed in bits 5..10.
+    /// The first source register, packed in bits 5..10.
     pub src1: R,
-    /// The frist source register, packed in bits 10..15.
+    /// The second source register, packed in bits 10..15.
     pub src2: R,
 }
 
@@ -193,6 +196,97 @@ impl<R: Reg> BinaryOperands<R> {
     }
 }
 
+/// A set of registers, packed into a 32-bit bitset.
+pub struct RegSet<R> {
+    bitset: ScalarBitSet<u32>,
+    phantom: PhantomData<R>,
+}
+
+impl<R: Reg> RegSet<R> {
+    /// Create a `RegSet` from a `ScalarBitSet`.
+    pub fn from_bitset(bitset: ScalarBitSet<u32>) -> Self {
+        Self {
+            bitset,
+            phantom: PhantomData,
+        }
+    }
+
+    /// Convert a `RegSet` into a `ScalarBitSet`.
+    pub fn to_bitset(self) -> ScalarBitSet<u32> {
+        self.bitset
+    }
+}
+
+impl<R: Reg> From<ScalarBitSet<u32>> for RegSet<R> {
+    fn from(bitset: ScalarBitSet<u32>) -> Self {
+        Self {
+            bitset,
+            phantom: PhantomData,
+        }
+    }
+}
+
+impl<R: Reg> Into<ScalarBitSet<u32>> for RegSet<R> {
+    fn into(self) -> ScalarBitSet<u32> {
+        self.bitset
+    }
+}
+
+impl<R: Reg> IntoIterator for RegSet<R> {
+    type Item = R;
+    type IntoIter = core::iter::FilterMap<cranelift_bitset::scalar::Iter<u32>, fn(u8) -> Option<R>>;
+
+    fn into_iter(self) -> Self::IntoIter {
+        self.bitset.into_iter().filter_map(R::new)
+    }
+}
+
+impl<R: Reg> FromIterator<R> for RegSet<R> {
+    fn from_iter<I: IntoIterator<Item = R>>(iter: I) -> Self {
+        let mut set = ScalarBitSet::new();
+        for reg in iter {
+            set.insert(reg.to_u8());
+        }
+        RegSet::from(set)
+    }
+}
+
+impl<R: Reg> Default for RegSet<R> {
+    fn default() -> Self {
+        Self {
+            bitset: Default::default(),
+            phantom: Default::default(),
+        }
+    }
+}
+
+impl<R: Reg> Copy for RegSet<R> {}
+impl<R: Reg> Clone for RegSet<R> {
+    fn clone(&self) -> Self {
+        *self
+    }
+}
+
+impl<R: Reg> PartialEq for RegSet<R> {
+    fn eq(&self, other: &Self) -> bool {
+        self.bitset == other.bitset
+    }
+}
+impl<R: Reg> Eq for RegSet<R> {}
+
+impl<R: Reg> fmt::Debug for RegSet<R> {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.debug_set().entries(self.into_iter()).finish()
+    }
+}
+
+#[cfg(feature = "arbitrary")]
+impl<'a, R: Reg> arbitrary::Arbitrary<'a> for RegSet<R> {
+    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
+        ScalarBitSet::arbitrary(u).map(Self::from)
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
diff --git a/scripts/publish.rs b/scripts/publish.rs
index c5fbac21e..c72a1cb60 100644
--- a/scripts/publish.rs
+++ b/scripts/publish.rs
@@ -18,9 +18,9 @@ use std::time::Duration;
 // note that this list must be topologically sorted by dependencies
 const CRATES_TO_PUBLISH: &[&str] = &[
     // pulley
+    "cranelift-bitset",
     "pulley-interpreter",
     // cranelift
-    "cranelift-bitset",
     "cranelift-isle",
     "cranelift-entity",
     "wasmtime-types",


Test patch:
diff --git a/cranelift/filetests/filetests/isa/pulley32/get_stack_pointer.clif b/cranelift/filetests/filetests/isa/pulley32/get_stack_pointer.clif
index f2e7899af..1bae8c297 100644
--- a/cranelift/filetests/filetests/isa/pulley32/get_stack_pointer.clif
+++ b/cranelift/filetests/filetests/isa/pulley32/get_stack_pointer.clif
@@ -13,6 +13,5 @@ block0:
 ;   ret
 ;
 ; Disassembled:
-;        0: 33 02 00 00                     get_sp x0
+;        0: 3d 02 00 00                     get_sp x0
 ;        4: 00                              ret
-
diff --git a/cranelift/filetests/filetests/isa/pulley32/trap.clif b/cranelift/filetests/filetests/isa/pulley32/trap.clif
index 84a07dab6..983c35ce4 100644
--- a/cranelift/filetests/filetests/isa/pulley32/trap.clif
+++ b/cranelift/filetests/filetests/isa/pulley32/trap.clif
@@ -11,7 +11,7 @@ block0:
 ;   trap // code = User(0)
 ;
 ; Disassembled:
-;        0: 33 00 00                        trap
+;        0: 3d 00 00                        trap
 
 function %trapnz(i64) {
 block0(v0: i64):
@@ -36,7 +36,7 @@ block0(v0: i64):
 ;        3: 14 03 0c                        xeq64 x3, x0, x3
 ;        6: 03 03 07 00 00 00               br_if x3, 0x7    // target = 0xd
 ;        c: 00                              ret
-;        d: 33 00 00                        trap
+;        d: 3d 00 00                        trap
 
 function %trapz(i64) {
 block0(v0: i64):
@@ -61,5 +61,4 @@ block0(v0: i64):
 ;        3: 14 03 0c                        xeq64 x3, x0, x3
 ;        6: 04 03 07 00 00 00               br_if_not x3, 0x7    // target = 0xd
 ;        c: 00                              ret
-;        d: 33 00 00                        trap
-
+;        d: 3d 00 00                        trap
diff --git a/cranelift/filetests/filetests/isa/pulley64/get_stack_pointer.clif b/cranelift/filetests/filetests/isa/pulley64/get_stack_pointer.clif
index b95a382e4..f2520abae 100644
--- a/cranelift/filetests/filetests/isa/pulley64/get_stack_pointer.clif
+++ b/cranelift/filetests/filetests/isa/pulley64/get_stack_pointer.clif
@@ -13,5 +13,5 @@ block0:
 ;   ret
 ;
 ; Disassembled:
-;        0: 33 02 00 00                     get_sp x0
+;        0: 3d 02 00 00                     get_sp x0
 ;        4: 00                              ret
diff --git a/cranelift/filetests/filetests/isa/pulley64/trap.clif b/cranelift/filetests/filetests/isa/pulley64/trap.clif
index ee639b348..7615f2a74 100644
--- a/cranelift/filetests/filetests/isa/pulley64/trap.clif
+++ b/cranelift/filetests/filetests/isa/pulley64/trap.clif
@@ -11,7 +11,7 @@ block0:
 ;   trap // code = User(0)
 ;
 ; Disassembled:
-;        0: 33 00 00                        trap
+;        0: 3d 00 00                        trap
 
 function %trapnz(i64) {
 block0(v0: i64):
@@ -36,7 +36,7 @@ block0(v0: i64):
 ;        3: 14 03 0c                        xeq64 x3, x0, x3
 ;        6: 03 03 07 00 00 00               br_if x3, 0x7    // target = 0xd
 ;        c: 00                              ret
-;        d: 33 00 00                        trap
+;        d: 3d 00 00                        trap
 
 function %trapz(i64) {
 block0(v0: i64):
@@ -61,5 +61,4 @@ block0(v0: i64):
 ;        3: 14 03 0c                        xeq64 x3, x0, x3
 ;        6: 04 03 07 00 00 00               br_if_not x3, 0x7    // target = 0xd
 ;        c: 00                              ret
-;        d: 33 00 00                        trap
-
+;        d: 3d 00 00                        trap
diff --git a/pulley/tests/all/disas.rs b/pulley/tests/all/disas.rs
index f5751b574..eb2b49a56 100644
--- a/pulley/tests/all/disas.rs
+++ b/pulley/tests/all/disas.rs
@@ -29,29 +29,36 @@ fn simple() {
     assert_disas(
         &[
             // Prologue.
-            Op::Xconst8(Xconst8 {
-                dst: XReg::x26,
-                imm: -16i8,
-            }),
+            Op::PushFrame(PushFrame {}),
+            // Function body.
             Op::Xadd32(Xadd32 {
                 operands: BinaryOperands {
-                    dst: XReg::sp,
-                    src1: XReg::sp,
-                    src2: XReg::x26,
+                    dst: XReg::x0,
+                    src1: XReg::x0,
+                    src2: XReg::x1,
                 },
             }),
-            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,
+            // Epilogue.
+            Op::PopFrame(PopFrame {}),
+            Op::Ret(Ret {}),
+        ],
+        r#"
+       0: 2f                              push_frame
+       1: 12 00 04                        xadd32 x0, x0, x1
+       4: 30                              pop_frame
+       5: 00                              ret
+        "#,
+    );
+}
+
+#[test]
+fn push_pop_many() {
+    assert_disas(
+        &[
+            // Prologue.
+            Op::PushFrame(PushFrame {}),
+            Op::XPush32Many(XPush32Many {
+                srcs: RegSet::from_iter([XReg::x0, XReg::x1, XReg::x2, XReg::x3, XReg::x4]),
             }),
             // Function body.
             Op::Xadd32(Xadd32 {
@@ -62,45 +69,19 @@ fn simple() {
                 },
             }),
             // 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: XReg::x26,
-                imm: 16,
-            }),
-            Op::Xadd32(Xadd32 {
-                operands: BinaryOperands {
-                    dst: XReg::sp,
-                    src1: XReg::sp,
-                    src2: XReg::x26,
-                },
+            Op::XPop32Many(XPop32Many {
+                dsts: RegSet::from_iter([XReg::x0, XReg::x1, XReg::x2, XReg::x3, XReg::x4]),
             }),
+            Op::PopFrame(PopFrame {}),
             Op::Ret(Ret {}),
         ],
         r#"
-       0: 0e 1a f0                        xconst8 x26, -16
-       3: 12 7b 6b                        xadd32 sp, sp, x26
-       6: 2c 1b 08 1c                     store64_offset8 sp, 8, lr
-       a: 2a 1b 1d                        store64 sp, fp
-       d: 0b 1d 1b                        xmov fp, sp
-      10: 12 00 04                        xadd32 x0, x0, x1
-      13: 0b 1b 1d                        xmov sp, fp
-      16: 25 1c 1b 08                     load64_offset8 lr, sp, 8
-      1a: 22 1d 1b                        load64 fp, sp
-      1d: 0e 1a 10                        xconst8 x26, 16
-      20: 12 7b 6b                        xadd32 sp, sp, x26
-      23: 00                              ret
+       0: 2f                              push_frame
+       1: 32 1f 00 00 00                  xpush32_many x0, x1, x2, x3, x4
+       6: 12 00 04                        xadd32 x0, x0, x1
+       9: 36 1f 00 00 00                  xpop32_many x0, x1, x2, x3, x4
+       e: 30                              pop_frame
+       f: 00                              ret
         "#,
     );
 }