Repository: bytecodealliance/wasmtime
Pull request #9008 (2024-07-24 23:17:00)

Introduce the `pulley-interpreter` crate
This commit is the first step towards implementing https://github.com/bytecodealliance/rfcs/pull/35

This commit introduces the `pulley-interpreter` crate which contains the Pulley bytecode definition, encoder, decoder, disassembler, and interpreter.

This is still very much a work in progress! It is expected that we will tweak encodings and bytecode definitions, that we will overhaul the interpreter (to, for example, optionally support the unstable Rust `explicit_tail_calls` feature), and otherwise make large changes. This is just a starting point to get the ball rolling.

Subsequent commits and pull requests will do things like add the Cranelift backend to produce Pulley bytecode from Wasm as well as the runtime integration to run the Pulley interpreter inside Wasmtime.

Dedicated CI for Pulley, ensuring that it builds and runs on things like 32-bit `nostd` platforms, will also come in follow ups.

<!--
Please make sure you include the following information:

- If this work has been discussed elsewhere, please include a link to that
  conversation. If it was discussed in an issue, just mention "issue #...".

- Explain why this change is needed. If the details are in an issue already,
  this can be brief.

Our development process is documented in the Wasmtime book:
https://docs.wasmtime.dev/contributing-development-process.html

Please ensure all communication follows the code of conduct:
https://github.com/bytecodealliance/wasmtime/blob/main/CODE_OF_CONDUCT.md
-->



Patch:
diff --git a/.github/actions/install-rust/action.yml b/.github/actions/install-rust/action.yml
index 7f02874e8..2a350f248 100644
--- a/.github/actions/install-rust/action.yml
+++ b/.github/actions/install-rust/action.yml
@@ -23,7 +23,7 @@ runs:
         elif [ "${{ inputs.toolchain }}" = "msrv" ]; then
           echo "version=1.$msrv.0" >> "$GITHUB_OUTPUT"
         elif [ "${{ inputs.toolchain }}" = "wasmtime-ci-pinned-nightly" ]; then
-          echo "version=nightly-2024-06-13" >> "$GITHUB_OUTPUT"
+          echo "version=nightly-2024-07-25" >> "$GITHUB_OUTPUT"
         else
           echo "version=${{ inputs.toolchain }}" >> "$GITHUB_OUTPUT"
         fi
@@ -59,4 +59,4 @@ runs:
 
     - name: Install the WASI target
       shell: bash
-      run: rustup target add wasm32-wasi wasm32-unknown-unknown
+      run: rustup target add wasm32-wasip1 wasm32-unknown-unknown
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index c0bfc835d..14ed47a85 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -587,7 +587,7 @@ jobs:
         toolchain: ${{ matrix.rust }}
 
     # Install targets in order to build various tests throughout the repo
-    - run: rustup target add wasm32-wasi wasm32-unknown-unknown ${{ matrix.target }}
+    - run: rustup target add wasm32-wasip1 wasm32-unknown-unknown ${{ matrix.target }}
     - run: echo CARGO_BUILD_TARGET=${{ matrix.target }} >> $GITHUB_ENV
       if: matrix.target != ''
 
@@ -715,7 +715,7 @@ jobs:
       if: (matrix.os == 'windows-latest') && (matrix.feature == 'winml')
 
     # Install Rust targets.
-    - run: rustup target add wasm32-wasi
+    - run: rustup target add wasm32-wasip1
 
     # Run the tests!
     - run: cargo test -p wasmtime-wasi-nn --features ${{ matrix.feature }}
@@ -765,7 +765,7 @@ jobs:
       with:
         submodules: true
     - uses: ./.github/actions/install-rust
-    - run: rustup target add wasm32-wasi wasm32-unknown-unknown
+    - run: rustup target add wasm32-wasip1 wasm32-unknown-unknown
     - run: |
         sudo apt-get update && sudo apt-get install -y gdb lldb-15 llvm
         # workaround for https://bugs.launchpad.net/ubuntu/+source/llvm-defaults/+bug/1972855
@@ -795,7 +795,7 @@ jobs:
       with:
         submodules: true
     - uses: ./.github/actions/install-rust
-    - run: rustup target add wasm32-wasi wasm32-unknown-unknown
+    - run: rustup target add wasm32-wasip1 wasm32-unknown-unknown
 
     - name: Install wasm-tools
       run: |
@@ -881,8 +881,8 @@ jobs:
       with:
         submodules: true
     - uses: ./.github/actions/install-rust
-    - run: rustup target add wasm32-wasi wasm32-unknown-unknown
-    - run: cargo build --target wasm32-wasi --no-default-features --features compile,cranelift,all-arch
+    - run: rustup target add wasm32-wasip1 wasm32-unknown-unknown
+    - run: cargo build --target wasm32-wasip1 --no-default-features --features compile,cranelift,all-arch
       env:
         VERSION: ${{ github.sha }}
 
@@ -903,7 +903,7 @@ jobs:
       with:
         submodules: true
     - uses: ./.github/actions/install-rust
-    - run: rustup target add wasm32-wasi
+    - run: rustup target add wasm32-wasip1
     - run: cargo test --benches --release
 
     # common logic to cancel the entire run if this job fails
diff --git a/Cargo.lock b/Cargo.lock
index 6d29899f4..04a71a75c 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 3f4e263d6..6b41c5c99 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -160,7 +160,7 @@ authors = ["The Wasmtime Project Developers"]
 edition = "2021"
 # Wasmtime's current policy is that this number can be no larger than the
 # current stable release of Rust minus 2.
-rust-version = "1.77.0"
+rust-version = "1.78.0"
 
 [workspace.lints.rust]
 # Turn on some lints which are otherwise allow-by-default in rustc.
@@ -211,6 +211,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/README.md b/README.md
index 7ebb09b39..add5f9622 100644
--- a/README.md
+++ b/README.md
@@ -54,8 +54,8 @@ fn main() {
 and compile/run it with:
 
 ```sh
-$ rustup target add wasm32-wasi
-$ rustc hello.rs --target wasm32-wasi
+$ rustup target add wasm32-wasip1
+$ rustc hello.rs --target wasm32-wasip1
 $ wasmtime hello.wasm
 Hello, world!
 ```
diff --git a/benches/instantiation.rs b/benches/instantiation.rs
index 938159985..5020478ad 100644
--- a/benches/instantiation.rs
+++ b/benches/instantiation.rs
@@ -185,7 +185,7 @@ fn build_wasi_example() {
             "-p",
             "example-wasi-wasm",
             "--target",
-            "wasm32-wasi",
+            "wasm32-wasip1",
         ])
         .spawn()
         .expect("failed to run cargo to build WASI example")
@@ -193,11 +193,11 @@ fn build_wasi_example() {
         .expect("failed to wait for cargo to build")
         .success()
     {
-        panic!("failed to build WASI example for target `wasm32-wasi`");
+        panic!("failed to build WASI example for target `wasm32-wasip1`");
     }
 
     std::fs::copy(
-        "target/wasm32-wasi/release/wasi.wasm",
+        "target/wasm32-wasip1/release/wasi.wasm",
         "benches/instantiation/wasi.wasm",
     )
     .expect("failed to copy WASI example module");
diff --git a/cranelift/codegen/src/isa/x64/encoding/evex.rs b/cranelift/codegen/src/isa/x64/encoding/evex.rs
index 3805e7215..d675250c9 100644
--- a/cranelift/codegen/src/isa/x64/encoding/evex.rs
+++ b/cranelift/codegen/src/isa/x64/encoding/evex.rs
@@ -552,8 +552,8 @@ mod tests {
                 regs::xmm9(),
                 Amode::ImmRegRegShift {
                     simm32: 0,
-                    base: Gpr::new(regs::rbx()).unwrap(),
-                    index: Gpr::new(regs::rsi()).unwrap(),
+                    base: Gpr::unwrap_new(regs::rbx()),
+                    index: Gpr::unwrap_new(regs::rsi()),
                     shift: 3,
                     flags: MemFlags::trusted(),
                 }
@@ -565,8 +565,8 @@ mod tests {
                 regs::xmm13(),
                 Amode::ImmRegRegShift {
                     simm32: 1,
-                    base: Gpr::new(regs::r11()).unwrap(),
-                    index: Gpr::new(regs::rdi()).unwrap(),
+                    base: Gpr::unwrap_new(regs::r11()),
+                    index: Gpr::unwrap_new(regs::rdi()),
                     shift: 2,
                     flags: MemFlags::trusted(),
                 }
@@ -580,8 +580,8 @@ mod tests {
                 regs::xmm5(),
                 Amode::ImmRegRegShift {
                     simm32: 128,
-                    base: Gpr::new(regs::rsp()).unwrap(),
-                    index: Gpr::new(regs::r10()).unwrap(),
+                    base: Gpr::unwrap_new(regs::rsp()),
+                    index: Gpr::unwrap_new(regs::r10()),
                     shift: 1,
                     flags: MemFlags::trusted(),
                 }
@@ -593,8 +593,8 @@ mod tests {
                 regs::xmm6(),
                 Amode::ImmRegRegShift {
                     simm32: 112,
-                    base: Gpr::new(regs::rbp()).unwrap(),
-                    index: Gpr::new(regs::r13()).unwrap(),
+                    base: Gpr::unwrap_new(regs::rbp()),
+                    index: Gpr::unwrap_new(regs::r13()),
                     shift: 0,
                     flags: MemFlags::trusted(),
                 }
@@ -606,8 +606,8 @@ mod tests {
                 regs::xmm7(),
                 Amode::ImmRegRegShift {
                     simm32: 0,
-                    base: Gpr::new(regs::rbp()).unwrap(),
-                    index: Gpr::new(regs::r13()).unwrap(),
+                    base: Gpr::unwrap_new(regs::rbp()),
+                    index: Gpr::unwrap_new(regs::r13()),
                     shift: 0,
                     flags: MemFlags::trusted(),
                 }
diff --git a/cranelift/codegen/src/isa/x64/encoding/vex.rs b/cranelift/codegen/src/isa/x64/encoding/vex.rs
index 1124a6b5b..44942570b 100644
--- a/cranelift/codegen/src/isa/x64/encoding/vex.rs
+++ b/cranelift/codegen/src/isa/x64/encoding/vex.rs
@@ -465,8 +465,8 @@ mod tests {
         let dst = regs::xmm2().to_real_reg().unwrap().hw_enc();
         let src1 = regs::xmm1().to_real_reg().unwrap().hw_enc();
         let src2 = Amode::ImmRegRegShift {
-            base: Gpr::new(regs::rax()).unwrap(),
-            index: Gpr::new(regs::r13()).unwrap(),
+            base: Gpr::unwrap_new(regs::rax()),
+            index: Gpr::unwrap_new(regs::r13()),
             flags: MemFlags::trusted(),
             simm32: 100,
             shift: 2,
diff --git a/cranelift/codegen/src/isa/x64/inst.isle b/cranelift/codegen/src/isa/x64/inst.isle
index 56a03c51c..2cd101be3 100644
--- a/cranelift/codegen/src/isa/x64/inst.isle
+++ b/cranelift/codegen/src/isa/x64/inst.isle
@@ -1629,11 +1629,23 @@
 
 ;; Put a value into a GPR.
 ;;
-;; Asserts that the value goes into a GPR.
+;; Moves the value into a GPR if it is a type that would naturally go into an
+;; XMM register.
 (decl put_in_gpr (Value) Gpr)
+
+;; Case for when the value naturally lives in a GPR.
 (rule (put_in_gpr val)
+      (if-let (value_type ty) val)
+      (if-let (type_register_class (RegisterClass.Gpr _)) ty)
       (gpr_new (put_in_reg val)))
 
+;; Case for when the value naturally lives in an XMM register and we must
+;; bitcast it from an XMM into a GPR.
+(rule (put_in_gpr val)
+      (if-let (value_type ty) val)
+      (if-let (type_register_class (RegisterClass.Xmm)) ty)
+      (bitcast_xmm_to_gpr ty (xmm_new (put_in_reg val))))
+
 ;; Put a value into a `GprMem`.
 ;;
 ;; Asserts that the value goes into a GPR.
diff --git a/cranelift/codegen/src/isa/x64/inst/args.rs b/cranelift/codegen/src/isa/x64/inst/args.rs
index 02f45ee64..71adffc2b 100644
--- a/cranelift/codegen/src/isa/x64/inst/args.rs
+++ b/cranelift/codegen/src/isa/x64/inst/args.rs
@@ -65,6 +65,21 @@ macro_rules! newtype_of_reg {
                 }
             }
 
+            /// Like `Self::new(r).unwrap()` but with a better panic message on
+            /// failure.
+            pub fn unwrap_new($check_reg: Reg) -> Self {
+                if $check {
+                    Self($check_reg)
+                } else {
+                    panic!(
+                        "cannot construct {} from register {:?} with register class {:?}",
+                        stringify!($newtype_reg),
+                        $check_reg,
+                        $check_reg.class(),
+                    )
+                }
+            }
+
             /// Get this newtype's underlying `Reg`.
             pub fn to_reg(self) -> Reg {
                 self.0
@@ -154,8 +169,26 @@ macro_rules! newtype_of_reg {
                                 None
                             }
                         }
-                        RegMem::Reg { reg: $check_reg } if $check => Some(Self(rm)),
-                        RegMem::Reg { reg: _ } => None,
+                        RegMem::Reg { reg } => Some($newtype_reg::new(reg)?.into()),
+                    }
+                }
+
+                /// Like `Self::new(rm).unwrap()` but with better panic messages
+                /// in case of failure.
+                pub fn unwrap_new(rm: RegMem) -> Self {
+                    match rm {
+                        RegMem::Mem { addr } => {
+                            $(
+                                if $aligned && !addr.aligned() {
+                                    panic!(
+                                        "cannot create {} from an unaligned memory address: {addr:?}",
+                                        stringify!($newtype_reg_mem),
+                                    );
+                                }
+                            )?
+                            Self(RegMem::Mem { addr })
+                        }
+                        RegMem::Reg { reg } => $newtype_reg::unwrap_new(reg).into(),
                     }
                 }
 
@@ -218,8 +251,29 @@ macro_rules! newtype_of_reg {
                                 None
                             }
                         }
-                        RegMemImm::Reg { reg: $check_reg } if $check => Some(Self(rmi)),
-                        RegMemImm::Reg { reg: _ } => None,
+                        RegMemImm::Reg { reg } => Some($newtype_reg::new(reg)?.into()),
+                    }
+                }
+
+                /// Like `Self::new(rmi).unwrap()` but with better panic
+                /// messages in case of failure.
+                pub fn unwrap_new(rmi: RegMemImm) -> Self {
+                    match rmi {
+                        RegMemImm::Imm { .. } => Self(rmi),
+                        RegMemImm::Mem { addr } => {
+                            $(
+                                if $aligned_imm && !addr.aligned() {
+                                    panic!(
+                                        "cannot construct {} from unaligned memory address: {:?}",
+                                        stringify!($newtype_reg_mem_imm),
+                                        addr,
+                                    );
+                                }
+                            )?
+                            Self(RegMemImm::Mem { addr })
+
+                        }
+                        RegMemImm::Reg { reg } => $newtype_reg::unwrap_new(reg).into(),
                     }
                 }
 
@@ -260,8 +314,16 @@ macro_rules! newtype_of_reg {
             pub fn new(imm8_reg: Imm8Reg) -> Option<Self> {
                 match imm8_reg {
                     Imm8Reg::Imm8 { .. } => Some(Self(imm8_reg)),
-                    Imm8Reg::Reg { reg: $check_reg } if $check => Some(Self(imm8_reg)),
-                    Imm8Reg::Reg { reg: _ } => None,
+                    Imm8Reg::Reg { reg } => Some($newtype_reg::new(reg)?.into()),
+                }
+            }
+
+            /// Like `Self::new(imm8_reg).unwrap()` but with better panic
+            /// messages on failure.
+            pub fn unwrap_new(imm8_reg: Imm8Reg) -> Self {
+                match imm8_reg {
+                    Imm8Reg::Imm8 { .. } => Self(imm8_reg),
+                    Imm8Reg::Reg { reg } => $newtype_reg::unwrap_new(reg).into(),
                 }
             }
 
diff --git a/cranelift/codegen/src/isa/x64/inst/emit.rs b/cranelift/codegen/src/isa/x64/inst/emit.rs
index 5c5ea82c1..d46e18a40 100644
--- a/cranelift/codegen/src/isa/x64/inst/emit.rs
+++ b/cranelift/codegen/src/isa/x64/inst/emit.rs
@@ -32,7 +32,7 @@ fn emit_signed_cvt(
         op,
         dst,
         src1: dst.to_reg(),
-        src2: GprMem::new(RegMem::reg(src)).unwrap(),
+        src2: GprMem::unwrap_new(RegMem::reg(src)),
         src2_size: OperandSize::Size64,
     }
     .emit(sink, info, state);
@@ -788,18 +788,18 @@ pub(crate) fn emit(
                     DivSignedness::Signed,
                     TrapCode::IntegerDivisionByZero,
                     RegMem::reg(divisor),
-                    Gpr::new(regs::rax()).unwrap(),
-                    Writable::from_reg(Gpr::new(regs::rax()).unwrap()),
+                    Gpr::unwrap_new(regs::rax()),
+                    Writable::from_reg(Gpr::unwrap_new(regs::rax())),
                 ),
                 _ => Inst::div(
                     size,
                     DivSignedness::Signed,
                     TrapCode::IntegerDivisionByZero,
                     RegMem::reg(divisor),
-                    Gpr::new(regs::rax()).unwrap(),
-                    Gpr::new(regs::rdx()).unwrap(),
-                    Writable::from_reg(Gpr::new(regs::rax()).unwrap()),
-                    Writable::from_reg(Gpr::new(regs::rdx()).unwrap()),
+                    Gpr::unwrap_new(regs::rax()),
+                    Gpr::unwrap_new(regs::rdx()),
+                    Writable::from_reg(Gpr::unwrap_new(regs::rax())),
+                    Writable::from_reg(Gpr::unwrap_new(regs::rdx())),
                 ),
             };
             inst.emit(sink, info, state);
@@ -884,7 +884,7 @@ pub(crate) fn emit(
         Inst::MovFromPReg { src, dst } => {
             let src: Reg = (*src).into();
             debug_assert!([regs::rsp(), regs::rbp(), regs::pinned_reg()].contains(&src));
-            let src = Gpr::new(src).unwrap();
+            let src = Gpr::unwrap_new(src);
             let size = OperandSize::Size64;
             let dst = WritableGpr::from_writable_reg(dst.to_writable_reg()).unwrap();
             Inst::MovRR { size, src, dst }.emit(sink, info, state);
@@ -892,7 +892,7 @@ pub(crate) fn emit(
 
         Inst::MovToPReg { src, dst } => {
             let src = src.to_reg();
-            let src = Gpr::new(src).unwrap();
+            let src = Gpr::unwrap_new(src);
             let dst: Reg = (*dst).into();
             debug_assert!([regs::rsp(), regs::rbp(), regs::pinned_reg()].contains(&dst));
             let dst = WritableGpr::from_writable_reg(Writable::from_reg(dst)).unwrap();
@@ -1866,8 +1866,8 @@ pub(crate) fn emit(
                 ExtMode::LQ,
                 RegMem::mem(Amode::imm_reg_reg_shift(
                     0,
-                    Gpr::new(tmp1.to_reg()).unwrap(),
-                    Gpr::new(idx).unwrap(),
+                    Gpr::unwrap_new(tmp1.to_reg()),
+                    Gpr::unwrap_new(idx),
                     2,
                 )),
                 tmp2,
@@ -1939,7 +1939,7 @@ pub(crate) fn emit(
             emit(
                 &Inst::XmmUnaryRmRUnaligned {
                     op: *op,
-                    src: XmmMem::new(src.clone().into()).unwrap(),
+                    src: XmmMem::unwrap_new(src.clone().into()),
                     dst: *dst,
                 },
                 sink,
@@ -2097,7 +2097,7 @@ pub(crate) fn emit(
                 op: *op,
                 dst: *dst,
                 src1: *src1,
-                src2: XmmMem::new(src2.clone().to_reg_mem()).unwrap(),
+                src2: XmmMem::unwrap_new(src2.clone().to_reg_mem()),
             },
             sink,
             info,
@@ -3397,7 +3397,7 @@ pub(crate) fn emit(
             let inst = Inst::shift_r(
                 OperandSize::Size64,
                 ShiftKind::ShiftRightLogical,
-                Imm8Gpr::new(Imm8Reg::Imm8 { imm: 1 }).unwrap(),
+                Imm8Gpr::unwrap_new(Imm8Reg::Imm8 { imm: 1 }),
                 tmp_gpr1.to_reg(),
                 tmp_gpr1,
             );
diff --git a/cranelift/codegen/src/isa/x64/inst/mod.rs b/cranelift/codegen/src/isa/x64/inst/mod.rs
index ee79de792..a8f55c483 100644
--- a/cranelift/codegen/src/isa/x64/inst/mod.rs
+++ b/cranelift/codegen/src/isa/x64/inst/mod.rs
@@ -215,8 +215,8 @@ impl Inst {
         Self::AluRmiR {
             size,
             op,
-            src1: Gpr::new(dst.to_reg()).unwrap(),
-            src2: GprMemImm::new(src).unwrap(),
+            src1: Gpr::unwrap_new(dst.to_reg()),
+            src2: GprMemImm::unwrap_new(src),
             dst: WritableGpr::from_writable_reg(dst).unwrap(),
         }
     }
@@ -238,7 +238,7 @@ impl Inst {
         Self::UnaryRmR {
             size,
             op,
-            src: GprMem::new(src).unwrap(),
+            src: GprMem::unwrap_new(src),
             dst: WritableGpr::from_writable_reg(dst).unwrap(),
         }
     }
@@ -247,7 +247,7 @@ impl Inst {
         debug_assert_eq!(src.to_reg().class(), RegClass::Int);
         Inst::Not {
             size,
-            src: Gpr::new(src.to_reg()).unwrap(),
+            src: Gpr::unwrap_new(src.to_reg()),
             dst: WritableGpr::from_writable_reg(src).unwrap(),
         }
     }
@@ -267,7 +267,7 @@ impl Inst {
             size,
             sign,
             trap,
-            divisor: GprMem::new(divisor).unwrap(),
+            divisor: GprMem::unwrap_new(divisor),
             dividend_lo,
             dividend_hi,
             dst_quotient,
@@ -286,7 +286,7 @@ impl Inst {
         Inst::Div8 {
             sign,
             trap,
-            divisor: GprMem::new(divisor).unwrap(),
+            divisor: GprMem::unwrap_new(divisor),
             dividend,
             dst,
         }
@@ -312,7 +312,7 @@ impl Inst {
         debug_assert!(size.is_one_of(&[OperandSize::Size32, OperandSize::Size64]));
         debug_assert!(src.class() == RegClass::Int);
         debug_assert!(dst.to_reg().class() == RegClass::Int);
-        let src = Gpr::new(src).unwrap();
+        let src = Gpr::unwrap_new(src);
         let dst = WritableGpr::from_writable_reg(dst).unwrap();
         Inst::MovRR { size, src, dst }
     }
@@ -323,7 +323,7 @@ impl Inst {
         debug_assert!(dst.to_reg().class() == RegClass::Float);
         Inst::XmmUnaryRmR {
             op,
-            src: XmmMemAligned::new(src).unwrap(),
+            src: XmmMemAligned::unwrap_new(src),
             dst: WritableXmm::from_writable_reg(dst).unwrap(),
         }
     }
@@ -333,8 +333,8 @@ impl Inst {
         debug_assert!(dst.to_reg().class() == RegClass::Float);
         Inst::XmmRmR {
             op,
-            src1: Xmm::new(dst.to_reg()).unwrap(),
-            src2: XmmMemAligned::new(src).unwrap(),
+            src1: Xmm::unwrap_new(dst.to_reg()),
+            src2: XmmMemAligned::unwrap_new(src),
             dst: WritableXmm::from_writable_reg(dst).unwrap(),
         }
     }
@@ -346,9 +346,9 @@ impl Inst {
         debug_assert!(dst.to_reg().class() == RegClass::Float);
         Inst::XmmRmRVex3 {
             op,
-            src3: XmmMem::new(src3).unwrap(),
-            src2: Xmm::new(src2).unwrap(),
-            src1: Xmm::new(dst.to_reg()).unwrap(),
+            src3: XmmMem::unwrap_new(src3),
+            src2: Xmm::unwrap_new(src2),
+            src1: Xmm::unwrap_new(dst.to_reg()),
             dst: WritableXmm::from_writable_reg(dst).unwrap(),
         }
     }
@@ -357,7 +357,7 @@ impl Inst {
         debug_assert!(src.class() == RegClass::Float);
         Inst::XmmMovRM {
             op,
-            src: Xmm::new(src).unwrap(),
+            src: Xmm::unwrap_new(src),
             dst: dst.into(),
         }
     }
@@ -373,7 +373,7 @@ impl Inst {
         debug_assert!(dst_size.is_one_of(&[OperandSize::Size32, OperandSize::Size64]));
         Inst::XmmToGpr {
             op,
-            src: Xmm::new(src).unwrap(),
+            src: Xmm::unwrap_new(src),
             dst: WritableGpr::from_writable_reg(dst).unwrap(),
             dst_size,
         }
@@ -390,7 +390,7 @@ impl Inst {
         debug_assert!(dst.to_reg().class() == RegClass::Float);
         Inst::GprToXmm {
             op,
-            src: GprMem::new(src).unwrap(),
+            src: GprMem::unwrap_new(src),
             dst: WritableXmm::from_writable_reg(dst).unwrap(),
             src_size,
         }
@@ -399,8 +399,8 @@ impl Inst {
     pub(crate) fn xmm_cmp_rm_r(op: SseOpcode, src1: Reg, src2: RegMem) -> Inst {
         src2.assert_regclass_is(RegClass::Float);
         debug_assert!(src1.class() == RegClass::Float);
-        let src2 = XmmMemAligned::new(src2).unwrap();
-        let src1 = Xmm::new(src1).unwrap();
+        let src2 = XmmMemAligned::unwrap_new(src2);
+        let src1 = Xmm::unwrap_new(src1);
         Inst::XmmCmpRmR { op, src1, src2 }
     }
 
@@ -419,8 +419,8 @@ impl Inst {
         Inst::XmmMinMaxSeq {
             size,
             is_min,
-            lhs: Xmm::new(lhs).unwrap(),
-            rhs: Xmm::new(rhs).unwrap(),
+            lhs: Xmm::unwrap_new(lhs),
+            rhs: Xmm::unwrap_new(rhs),
             dst: WritableXmm::from_writable_reg(dst).unwrap(),
         }
     }
@@ -428,7 +428,7 @@ impl Inst {
     pub(crate) fn movzx_rm_r(ext_mode: ExtMode, src: RegMem, dst: Writable<Reg>) -> Inst {
         src.assert_regclass_is(RegClass::Int);
         debug_assert!(dst.to_reg().class() == RegClass::Int);
-        let src = GprMem::new(src).unwrap();
+        let src = GprMem::unwrap_new(src);
         let dst = WritableGpr::from_writable_reg(dst).unwrap();
         Inst::MovzxRmR { ext_mode, src, dst }
     }
@@ -436,7 +436,7 @@ impl Inst {
     pub(crate) fn movsx_rm_r(ext_mode: ExtMode, src: RegMem, dst: Writable<Reg>) -> Inst {
         src.assert_regclass_is(RegClass::Int);
         debug_assert!(dst.to_reg().class() == RegClass::Int);
-        let src = GprMem::new(src).unwrap();
+        let src = GprMem::unwrap_new(src);
         let dst = WritableGpr::from_writable_reg(dst).unwrap();
         Inst::MovsxRmR { ext_mode, src, dst }
     }
@@ -453,7 +453,7 @@ impl Inst {
         debug_assert!(src.class() == RegClass::Int);
         Inst::MovRM {
             size,
-            src: Gpr::new(src).unwrap(),
+            src: Gpr::unwrap_new(src),
             dst: dst.into(),
         }
     }
@@ -481,7 +481,7 @@ impl Inst {
         Inst::ShiftR {
             size,
             kind,
-            src: Gpr::new(src).unwrap(),
+            src: Gpr::unwrap_new(src),
             num_bits,
             dst: WritableGpr::from_writable_reg(dst).unwrap(),
         }
@@ -494,8 +494,8 @@ impl Inst {
         debug_assert_eq!(src1.class(), RegClass::Int);
         Inst::CmpRmiR {
             size,
-            src1: Gpr::new(src1).unwrap(),
-            src2: GprMemImm::new(src2).unwrap(),
+            src1: Gpr::unwrap_new(src1),
+            src2: GprMemImm::unwrap_new(src2),
             opcode: CmpOpcode::Cmp,
         }
     }
@@ -518,15 +518,15 @@ impl Inst {
         Inst::Cmove {
             size,
             cc,
-            consequent: GprMem::new(src).unwrap(),
-            alternative: Gpr::new(dst.to_reg()).unwrap(),
+            consequent: GprMem::unwrap_new(src),
+            alternative: Gpr::unwrap_new(dst.to_reg()),
             dst: WritableGpr::from_writable_reg(dst).unwrap(),
         }
     }
 
     pub(crate) fn push64(src: RegMemImm) -> Inst {
         src.assert_regclass_is(RegClass::Int);
-        let src = GprMemImm::new(src).unwrap();
+        let src = GprMemImm::unwrap_new(src);
         Inst::Push64 { src }
     }
 
diff --git a/cranelift/codegen/src/isa/x64/lower.rs b/cranelift/codegen/src/isa/x64/lower.rs
index 34b8ca61d..a063854b2 100644
--- a/cranelift/codegen/src/isa/x64/lower.rs
+++ b/cranelift/codegen/src/isa/x64/lower.rs
@@ -291,8 +291,8 @@ fn lower_to_amode(ctx: &mut Lower<Inst>, spec: InsnInput, offset: i32) -> Amode
 
         return Amode::imm_reg_reg_shift(
             offset,
-            Gpr::new(base).unwrap(),
-            Gpr::new(index).unwrap(),
+            Gpr::unwrap_new(base),
+            Gpr::unwrap_new(index),
             shift,
         )
         .with_flags(flags);
diff --git a/cranelift/codegen/src/isa/x64/lower/isle.rs b/cranelift/codegen/src/isa/x64/lower/isle.rs
index f5a4f8f61..f9f0e9182 100644
--- a/cranelift/codegen/src/isa/x64/lower/isle.rs
+++ b/cranelift/codegen/src/isa/x64/lower/isle.rs
@@ -161,7 +161,7 @@ impl Context for IsleContext<'_, '_, MInst, X64Backend> {
         if let Some(c) = inputs.constant {
             let ty = self.lower_ctx.dfg().value_type(val);
             if let Some(imm) = to_simm32(c as i64, ty) {
-                return XmmMemImm::new(imm.to_reg_mem_imm()).unwrap();
+                return XmmMemImm::unwrap_new(imm.to_reg_mem_imm());
             }
         }
 
@@ -170,7 +170,7 @@ impl Context for IsleContext<'_, '_, MInst, X64Backend> {
             RegMem::Mem { addr } => RegMemImm::Mem { addr },
         };
 
-        XmmMemImm::new(res).unwrap()
+        XmmMemImm::unwrap_new(res)
     }
 
     fn put_in_xmm_mem(&mut self, val: Value) -> XmmMem {
@@ -183,11 +183,10 @@ impl Context for IsleContext<'_, '_, MInst, X64Backend> {
             // NOTE: this is where behavior differs from `put_in_reg_mem`, as we always force
             // constants to be 16 bytes when a constant will be used in place of an xmm register.
             let vcode_constant = self.emit_u128_le_const(c as u128);
-            return XmmMem::new(RegMem::mem(SyntheticAmode::ConstantOffset(vcode_constant)))
-                .unwrap();
+            return XmmMem::unwrap_new(RegMem::mem(SyntheticAmode::ConstantOffset(vcode_constant)));
         }
 
-        XmmMem::new(self.put_in_reg_mem(val)).unwrap()
+        XmmMem::unwrap_new(self.put_in_reg_mem(val))
     }
 
     fn put_in_reg_mem(&mut self, val: Value) -> RegMem {
@@ -306,10 +305,9 @@ impl Context for IsleContext<'_, '_, MInst, X64Backend> {
     #[inline]
     fn const_to_type_masked_imm8(&mut self, c: u64, ty: Type) -> Imm8Gpr {
         let mask = self.shift_mask(ty) as u64;
-        Imm8Gpr::new(Imm8Reg::Imm8 {
+        Imm8Gpr::unwrap_new(Imm8Reg::Imm8 {
             imm: (c & mask) as u8,
         })
-        .unwrap()
     }
 
     #[inline]
@@ -458,7 +456,7 @@ impl Context for IsleContext<'_, '_, MInst, X64Backend> {
 
     #[inline]
     fn writable_reg_to_xmm(&mut self, r: WritableReg) -> WritableXmm {
-        Writable::from_reg(Xmm::new(r.to_reg()).unwrap())
+        Writable::from_reg(Xmm::unwrap_new(r.to_reg()))
     }
 
     #[inline]
@@ -488,7 +486,7 @@ impl Context for IsleContext<'_, '_, MInst, X64Backend> {
 
     #[inline]
     fn xmm_mem_to_xmm_mem_imm(&mut self, r: &XmmMem) -> XmmMemImm {
-        XmmMemImm::new(r.clone().to_reg_mem().into()).unwrap()
+        XmmMemImm::unwrap_new(r.clone().to_reg_mem().into())
     }
 
     #[inline]
@@ -508,17 +506,17 @@ impl Context for IsleContext<'_, '_, MInst, X64Backend> {
 
     #[inline]
     fn reg_mem_to_xmm_mem(&mut self, rm: &RegMem) -> XmmMem {
-        XmmMem::new(rm.clone()).unwrap()
+        XmmMem::unwrap_new(rm.clone())
     }
 
     #[inline]
     fn gpr_mem_imm_new(&mut self, rmi: &RegMemImm) -> GprMemImm {
-        GprMemImm::new(rmi.clone()).unwrap()
+        GprMemImm::unwrap_new(rmi.clone())
     }
 
     #[inline]
     fn xmm_mem_imm_new(&mut self, rmi: &RegMemImm) -> XmmMemImm {
-        XmmMemImm::new(rmi.clone()).unwrap()
+        XmmMemImm::unwrap_new(rmi.clone())
     }
 
     #[inline]
@@ -538,27 +536,27 @@ impl Context for IsleContext<'_, '_, MInst, X64Backend> {
 
     #[inline]
     fn xmm_new(&mut self, r: Reg) -> Xmm {
-        Xmm::new(r).unwrap()
+        Xmm::unwrap_new(r)
     }
 
     #[inline]
     fn gpr_new(&mut self, r: Reg) -> Gpr {
-        Gpr::new(r).unwrap()
+        Gpr::unwrap_new(r)
     }
 
     #[inline]
     fn reg_mem_to_gpr_mem(&mut self, rm: &RegMem) -> GprMem {
-        GprMem::new(rm.clone()).unwrap()
+        GprMem::unwrap_new(rm.clone())
     }
 
     #[inline]
     fn reg_to_gpr_mem(&mut self, r: Reg) -> GprMem {
-        GprMem::new(RegMem::reg(r)).unwrap()
+        GprMem::unwrap_new(RegMem::reg(r))
     }
 
     #[inline]
     fn imm8_reg_to_imm8_gpr(&mut self, ir: &Imm8Reg) -> Imm8Gpr {
-        Imm8Gpr::new(ir.clone()).unwrap()
+        Imm8Gpr::unwrap_new(ir.clone())
     }
 
     #[inline]
@@ -578,12 +576,12 @@ impl Context for IsleContext<'_, '_, MInst, X64Backend> {
 
     #[inline]
     fn imm8_to_imm8_gpr(&mut self, imm: u8) -> Imm8Gpr {
-        Imm8Gpr::new(Imm8Reg::Imm8 { imm }).unwrap()
+        Imm8Gpr::unwrap_new(Imm8Reg::Imm8 { imm })
     }
 
     fn gpr_from_imm8_gpr(&mut self, val: &Imm8Gpr) -> Option<Gpr> {
         match val.as_imm8_reg() {
-            &Imm8Reg::Reg { reg } => Some(Gpr::new(reg).unwrap()),
+            &Imm8Reg::Reg { reg } => Some(Gpr::unwrap_new(reg)),
             Imm8Reg::Imm8 { .. } => None,
         }
     }
@@ -986,7 +984,7 @@ impl Context for IsleContext<'_, '_, MInst, X64Backend> {
     }
 
     fn xmi_imm(&mut self, imm: u32) -> XmmMemImm {
-        XmmMemImm::new(RegMemImm::imm(imm)).unwrap()
+        XmmMemImm::unwrap_new(RegMemImm::imm(imm))
     }
 
     fn insert_i8x16_lane_hole(&mut self, hole_idx: u8) -> VCodeConstant {
@@ -1000,10 +998,10 @@ impl IsleContext<'_, '_, MInst, X64Backend> {
         let tmp = self.lower_ctx.alloc_tmp(types::F32X4).only_reg().unwrap();
         self.lower_ctx.emit(MInst::XmmUnaryRmRUnaligned {
             op: SseOpcode::Movdqu,
-            src: XmmMem::new(RegMem::mem(addr)).unwrap(),
-            dst: Writable::from_reg(Xmm::new(tmp.to_reg()).unwrap()),
+            src: XmmMem::unwrap_new(RegMem::mem(addr)),
+            dst: Writable::from_reg(Xmm::unwrap_new(tmp.to_reg())),
         });
-        Xmm::new(tmp.to_reg()).unwrap()
+        Xmm::unwrap_new(tmp.to_reg())
     }
 }
 
@@ -1041,12 +1039,9 @@ const I8X16_USHR_MASKS: [u8; 128] = [
 #[inline]
 fn to_simm32(constant: i64, ty: Type) -> Option<GprMemImm> {
     if ty.bits() <= 32 || constant == ((constant << 32) >> 32) {
-        Some(
-            GprMemImm::new(RegMemImm::Imm {
-                simm32: constant as u32,
-            })
-            .unwrap(),
-        )
+        Some(GprMemImm::unwrap_new(RegMemImm::Imm {
+            simm32: constant as u32,
+        }))
     } else {
         None
     }
diff --git a/cranelift/fuzzgen/src/function_generator.rs b/cranelift/fuzzgen/src/function_generator.rs
index 61c110cc3..f6e5350e1 100644
--- a/cranelift/fuzzgen/src/function_generator.rs
+++ b/cranelift/fuzzgen/src/function_generator.rs
@@ -1898,6 +1898,14 @@ where
             let var = Variable::new(id);
             builder.declare_var(var, ty);
             builder.def_var(var, value);
+
+            // Randomly declare variables as needing a stack map.
+            // We limit these to only types that have fewer than 16 bytes
+            // since the stack map mechanism does not support larger types.
+            if ty.bytes() <= 16 && self.u.arbitrary()? {
+                builder.declare_var_needs_stack_map(var);
+            }
+
             self.resources
                 .vars
                 .entry(ty)
diff --git a/crates/fuzzing/src/generators/component_types.rs b/crates/fuzzing/src/generators/component_types.rs
index 9db7d46c8..b184fa28e 100644
--- a/crates/fuzzing/src/generators/component_types.rs
+++ b/crates/fuzzing/src/generators/component_types.rs
@@ -127,6 +127,7 @@ where
 
     let mut config = Config::new();
     config.wasm_component_model(true);
+    config.wasm_component_model_multiple_returns(true);
     config.debug_adapter_modules(input.arbitrary()?);
     let engine = Engine::new(&config).unwrap();
     let wat = declarations.make_component();
diff --git a/crates/wasi-common/README.md b/crates/wasi-common/README.md
index c406f3813..4d0dcf2df 100644
--- a/crates/wasi-common/README.md
+++ b/crates/wasi-common/README.md
@@ -44,10 +44,10 @@ so stay tuned!
 
 ## Development hints
 When testing the crate, you may want to enable and run full wasm32 integration testsuite. This
-requires `wasm32-wasi` target installed which can be done as follows using [rustup]
+requires `wasm32-wasip1` target installed which can be done as follows using [rustup]
 
 ```
-rustup target add wasm32-wasi
+rustup target add wasm32-wasip1
 ```
 
 [rustup]: https://rustup.rs
diff --git a/crates/wasi-nn/README.md b/crates/wasi-nn/README.md
index c726c7c90..2f080f34f 100644
--- a/crates/wasi-nn/README.md
+++ b/crates/wasi-nn/README.md
@@ -35,5 +35,5 @@ To use the WIT-based ABI, compile with `--features component-model` and use `was
 
 An end-to-end example demonstrating ML classification is included in [examples]:
 `examples/classification-example` contains a standalone Rust project that uses
-the [wasi-nn] APIs and is compiled to the `wasm32-wasi` target using the
+the [wasi-nn] APIs and is compiled to the `wasm32-wasip1` target using the
 high-level `wasi-nn` [bindings].
diff --git a/crates/wasi-nn/examples/classification-component-onnx/Cargo.toml b/crates/wasi-nn/examples/classification-component-onnx/Cargo.toml
index ab4f0b8ba..32a34a4ea 100644
--- a/crates/wasi-nn/examples/classification-component-onnx/Cargo.toml
+++ b/crates/wasi-nn/examples/classification-component-onnx/Cargo.toml
@@ -29,7 +29,7 @@ ndarray = "0.15.3"
 
 
 
-# This crate is built with the wasm32-wasi target, so it's separate
+# This crate is built with the wasm32-wasip1 target, so it's separate
 # from the main Wasmtime build, so use this directive to exclude it
 # from the parent directory's workspace.
 [workspace]
diff --git a/crates/wasi-nn/examples/classification-component-onnx/README.md b/crates/wasi-nn/examples/classification-component-onnx/README.md
index e399f2279..a87b33c81 100644
--- a/crates/wasi-nn/examples/classification-component-onnx/README.md
+++ b/crates/wasi-nn/examples/classification-component-onnx/README.md
@@ -1,6 +1,6 @@
 # Onnx Backend Classification Component Example
 
-This example demonstrates how to use the `wasi-nn` crate to run a classification using the 
+This example demonstrates how to use the `wasi-nn` crate to run a classification using the
 [ONNX Runtime](https://onnxruntime.ai/) backend from a WebAssembly component.
 
 ## Build
@@ -15,9 +15,9 @@ In the wasmtime root directory, run the following command to build the wasmtime
 cargo build --features component-model,wasi-nn,wasmtime-wasi-nn/onnx
 
 # run the component with wasmtime
-./target/debug/wasmtime run \ 
-  --wasm-features component-model \ 
+./target/debug/wasmtime run \
+  --wasm-features component-model \
   --wasi-modules=experimental-wasi-nn \
   --mapdir fixture::./crates/wasi-nn/examples/classification-component-onnx/fixture \
-  ./crates/wasi-nn/examples/classification-component-onnx/target/wasm32-wasi/debug/classification_component_onnx.wasm
-```
\ No newline at end of file
+  ./crates/wasi-nn/examples/classification-component-onnx/target/wasm32-wasip1/debug/classification_component_onnx.wasm
+```
diff --git a/crates/wasi-nn/examples/classification-example-named/Cargo.toml b/crates/wasi-nn/examples/classification-example-named/Cargo.toml
index b4653659b..2aeb93971 100644
--- a/crates/wasi-nn/examples/classification-example-named/Cargo.toml
+++ b/crates/wasi-nn/examples/classification-example-named/Cargo.toml
@@ -9,7 +9,7 @@ publish = false
 [dependencies]
 wasi-nn = "0.5.0"
 
-# This crate is built with the wasm32-wasi target, so it's separate
+# This crate is built with the wasm32-wasip1 target, so it's separate
 # from the main Wasmtime build, so use this directive to exclude it
 # from the parent directory's workspace.
 [workspace]
diff --git a/crates/wasi-nn/examples/classification-example-named/README.md b/crates/wasi-nn/examples/classification-example-named/README.md
index aa56ad0cb..f0b4e0fa4 100644
--- a/crates/wasi-nn/examples/classification-example-named/README.md
+++ b/crates/wasi-nn/examples/classification-example-named/README.md
@@ -1,2 +1,2 @@
-This example project demonstrates using the `wasi-nn` API to perform ML inference. It consists of Rust code that is 
-built using the `wasm32-wasi` target. See `ci/run-wasi-nn-example.sh` for how this is used.
+This example project demonstrates using the `wasi-nn` API to perform ML inference. It consists of Rust code that is
+built using the `wasm32-wasip1` target. See `ci/run-wasi-nn-example.sh` for how this is used.
diff --git a/crates/wasi-nn/examples/classification-example-winml/Cargo.toml b/crates/wasi-nn/examples/classification-example-winml/Cargo.toml
index db91d49df..b0a5e8de2 100644
--- a/crates/wasi-nn/examples/classification-example-winml/Cargo.toml
+++ b/crates/wasi-nn/examples/classification-example-winml/Cargo.toml
@@ -11,7 +11,7 @@ wasi-nn = "0.6.0"
 image = { version = "0.24.6", default-features = false, features = ["png"] }
 ndarray = "0.15.3"
 
-# This crate is built with the wasm32-wasi target, so it's separate
+# This crate is built with the wasm32-wasip1 target, so it's separate
 # from the main Wasmtime build, so use this directive to exclude it
 # from the parent directory's workspace.
 [workspace]
diff --git a/crates/wasi-nn/examples/classification-example-winml/README.md b/crates/wasi-nn/examples/classification-example-winml/README.md
index 745b955b9..bacb17850 100644
--- a/crates/wasi-nn/examples/classification-example-winml/README.md
+++ b/crates/wasi-nn/examples/classification-example-winml/README.md
@@ -23,18 +23,18 @@ To run this example, perform the following steps on Windows 10 v1803 and later:
     set PROJECT_DIR=%CD%
     cd crates\wasi-nn\examples\classification-example-winml
     ```
-1. Install the `wasm32-wasi` Rust target:
+1. Install the `wasm32-wasip1` Rust target:
     ```
-    rustup target add wasm32-wasi
+    rustup target add wasm32-wasip1
     ```
-1. Compile this example; the `wasm32-wasi` output is a WebAssembly file:
+1. Compile this example; the `wasm32-wasip1` output is a WebAssembly file:
     ```
-    cargo build --release --target=wasm32-wasi
+    cargo build --release --target=wasm32-wasip1
     ```
 1. Run the sample; the fixture directory containing the model and image must be
    mapped in to be accessible to WebAssembly.
     ```
-    %PROJECT_DIR%\target\release\wasmtime.exe --dir fixture::fixture -S nn target\wasm32-wasi\release\wasi-nn-example-winml.wasm
+    %PROJECT_DIR%\target\release\wasmtime.exe --dir fixture::fixture -S nn target\wasm32-wasip1\release\wasi-nn-example-winml.wasm
     ```
 1. The example will print the top 5 classification results. To run with a
    different image or ONNX model, modify the files in the `fixture` directory
diff --git a/crates/wasi-nn/examples/classification-example/Cargo.toml b/crates/wasi-nn/examples/classification-example/Cargo.toml
index 7cedd6252..0d0ad0e58 100644
--- a/crates/wasi-nn/examples/classification-example/Cargo.toml
+++ b/crates/wasi-nn/examples/classification-example/Cargo.toml
@@ -9,7 +9,7 @@ publish = false
 [dependencies]
 wasi-nn = "0.1.0"
 
-# This crate is built with the wasm32-wasi target, so it's separate
+# This crate is built with the wasm32-wasip1 target, so it's separate
 # from the main Wasmtime build, so use this directive to exclude it
 # from the parent directory's workspace.
 [workspace]
diff --git a/crates/wasi-nn/examples/classification-example/README.md b/crates/wasi-nn/examples/classification-example/README.md
index aa56ad0cb..f0b4e0fa4 100644
--- a/crates/wasi-nn/examples/classification-example/README.md
+++ b/crates/wasi-nn/examples/classification-example/README.md
@@ -1,2 +1,2 @@
-This example project demonstrates using the `wasi-nn` API to perform ML inference. It consists of Rust code that is 
-built using the `wasm32-wasi` target. See `ci/run-wasi-nn-example.sh` for how this is used.
+This example project demonstrates using the `wasi-nn` API to perform ML inference. It consists of Rust code that is
+built using the `wasm32-wasip1` target. See `ci/run-wasi-nn-example.sh` for how this is used.
diff --git a/crates/wasi-preview1-component-adapter/README.md b/crates/wasi-preview1-component-adapter/README.md
index ec0834318..ffe369360 100644
--- a/crates/wasi-preview1-component-adapter/README.md
+++ b/crates/wasi-preview1-component-adapter/README.md
@@ -45,7 +45,7 @@ $ cat foo.rs
 fn main() {
     println!("Hello, world!");
 }
-$ rustc foo.rs --target wasm32-wasi
+$ rustc foo.rs --target wasm32-wasip1
 $ wasm-tools print foo.wasm | grep '(import'
   (import "wasi_snapshot_preview1" "fd_write" (func ...
   (import "wasi_snapshot_preview1" "environ_get" (func ...
diff --git a/crates/wasmtime/src/runtime/vm/sys/miri/traphandlers.rs b/crates/wasmtime/src/runtime/vm/sys/miri/traphandlers.rs
index 912542ae0..320a17861 100644
--- a/crates/wasmtime/src/runtime/vm/sys/miri/traphandlers.rs
+++ b/crates/wasmtime/src/runtime/vm/sys/miri/traphandlers.rs
@@ -1,37 +1,30 @@
-// For MIRI, set up just enough of a setjmp/longjmp with catching panics
-// to get a few tests working that use this.
+// For MIRI, there's no way to implement longjmp/setjmp. The only possible way
+// to implement this is with panic/catch_panic, but the entrypoint into Rust
+// from wasm is defined as `extern "C"` which isn't allowed to panic. That
+// means that panicking here triggers UB which gets routed to `libc::abort()`.
+//
+// This maens that on MIRI all tests which trap are configured to be skipped at
+// this time.
 //
 // Note that no actual JIT code runs in MIRI so this is purely here for
 // host-to-host calls.
 
 use crate::runtime::vm::VMContext;
 
-struct WasmtimeLongjmp;
-
 pub fn wasmtime_setjmp(
     _jmp_buf: *mut *const u8,
     callback: extern "C" fn(*mut u8, *mut VMContext),
     payload: *mut u8,
     callee: *mut VMContext,
 ) -> i32 {
-    use std::panic::{self, AssertUnwindSafe};
-    let result = panic::catch_unwind(AssertUnwindSafe(|| {
-        callback(payload, callee);
-    }));
-    match result {
-        Ok(()) => 1,
-        Err(e) => {
-            if e.is::<WasmtimeLongjmp>() {
-                0
-            } else {
-                panic::resume_unwind(e)
-            }
-        }
-    }
+    callback(payload, callee);
+    1
 }
 
 pub fn wasmtime_longjmp(_jmp_buf: *const u8) -> ! {
-    std::panic::panic_any(WasmtimeLongjmp)
+    unsafe {
+        libc::abort();
+    }
 }
 
 #[allow(missing_docs)]
diff --git a/docs/WASI-tutorial.md b/docs/WASI-tutorial.md
index b419c6eb7..6e9e7a227 100644
--- a/docs/WASI-tutorial.md
+++ b/docs/WASI-tutorial.md
@@ -154,14 +154,14 @@ Let's put this source in the main file of our crate `src/main.rs`.
 In order to build it, we first need to install a WASI-enabled Rust toolchain:
 
 ```
-$ rustup target add wasm32-wasi
-$ cargo build --target wasm32-wasi
+$ rustup target add wasm32-wasip1
+$ cargo build --target wasm32-wasip1
 ```
 
-We should now have the WebAssembly module created in `target/wasm32-wasi/debug`:
+We should now have the WebAssembly module created in `target/wasm32-wasip1/debug`:
 
 ```
-$ file target/wasm32-wasi/debug/demo.wasm
+$ file target/wasm32-wasip1/debug/demo.wasm
 demo.wasm: WebAssembly (wasm) binary module version 0x1 (MVP)
 ```
 
diff --git a/docs/contributing-implementing-wasm-proposals.md b/docs/contributing-implementing-wasm-proposals.md
index 13fc927be..5de4590f9 100644
--- a/docs/contributing-implementing-wasm-proposals.md
+++ b/docs/contributing-implementing-wasm-proposals.md
@@ -130,7 +130,7 @@ The [cap-std](https://github.com/bytecodealliance/cap-std) repository contains
 crates which implement the capability-based version of the Rust standard library
 and extensions to that functionality. Once the functionality has been added to
 the relevant crates of that repository, they can be added into wasmtime by
-including them in the preview2 directory of the [wasi crate](https://github.com/bytecodealliance/wasmtime/tree/main/crates/wasi). 
+including them in the preview2 directory of the [wasi crate](https://github.com/bytecodealliance/wasmtime/tree/main/crates/wasi).
 
 Currently, WebAssembly modules which rely on preview2 ABI cannot be directly
 executed by the wasmtime command. The following steps allow for testing such
@@ -139,11 +139,11 @@ changes.
 1. Build wasmtime with the changes `cargo build --release`
 
 2. Create a simple Webassembly module to test the new component functionality by
-compiling your test code to the `wasm32-wasi` build target.
+compiling your test code to the `wasm32-wasip1` build target.
 
 3. Build the [wasi-preview1-component-adapter](https://github.com/bytecodealliance/wasmtime/tree/main/crates/wasi-preview1-component-adapter)
 as a command adapter. `cargo build -p wasi-preview1-component-adapter --target
-wasm32-wasi --release --features command --no-default-features`
+wasm32-wasip1 --release --features command --no-default-features`
 
 4. Use [wasm-tools](https://github.com/bytecodealliance/wasm-tools) to convert
 the test module to a component. `wasm-tools component new --adapt
diff --git a/docs/examples-coredump.md b/docs/examples-coredump.md
index 419d2eac5..f0e8c8bdf 100644
--- a/docs/examples-coredump.md
+++ b/docs/examples-coredump.md
@@ -5,7 +5,7 @@ The following steps describe how to debug using Wasm coredump in Wasmtime:
 1. Compile your WebAssembly with debug info enabled; for example:
 
     ```sh
-    $ rustc foo.rs --target=wasm32-wasi -C debuginfo=2
+    $ rustc foo.rs --target=wasm32-wasip1 -C debuginfo=2
     ```
 
 <details>
diff --git a/docs/examples-debugging-core-dumps.md b/docs/examples-debugging-core-dumps.md
index 5f03f368a..58c196639 100644
--- a/docs/examples-debugging-core-dumps.md
+++ b/docs/examples-debugging-core-dumps.md
@@ -35,7 +35,7 @@ fn baz(x: u32) {
 We can compile it to Wasm with the following command:
 
 ```shell-session
-$ rustc --target wasm32-wasi -o ./trap.wasm ./trap.rs
+$ rustc --target wasm32-wasip1 -o ./trap.wasm ./trap.rs
 ```
 
 Next, we can run it in Wasmtime and capture a core dump when it traps:
diff --git a/docs/examples-profiling-perf.md b/docs/examples-profiling-perf.md
index 2c4605db6..32ebfe00a 100644
--- a/docs/examples-profiling-perf.md
+++ b/docs/examples-profiling-perf.md
@@ -162,7 +162,7 @@ fn fib(n: u32) -> u32 {
 To collect perf information for this wasm module we'll execute:
 
 ```sh
-$ rustc --target wasm32-wasi fib.rs -O
+$ rustc --target wasm32-wasip1 fib.rs -O
 $ perf record -k mono wasmtime --profile=jitdump fib.wasm
 fib(42) = 267914296
 [ perf record: Woken up 1 times to write data ]
diff --git a/docs/examples-profiling-vtune.md b/docs/examples-profiling-vtune.md
index 9fb45b385..3e1716e4c 100644
--- a/docs/examples-profiling-vtune.md
+++ b/docs/examples-profiling-vtune.md
@@ -92,7 +92,7 @@ fn fib(n: u32) -> u32 {
 We compile the example to Wasm:
 
 ```sh
-$ rustc --target wasm32-wasi fib.rs -C opt-level=z -C lto=yes
+$ rustc --target wasm32-wasip1 fib.rs -C opt-level=z -C lto=yes
 ```
 
 Then we execute the Wasmtime runtime (built with the `vtune` feature and
@@ -101,7 +101,7 @@ application, `vtune`, which must already be installed and available on the
 path. To collect hot spot profiling information, we execute:
 
 ```sh
-$ rustc --target wasm32-wasi fib.rs -C opt-level=z -C lto=yes
+$ rustc --target wasm32-wasip1 fib.rs -C opt-level=z -C lto=yes
 $ vtune -run-pass-thru=--no-altstack -v -collect hotspots target/debug/wasmtime --profile=vtune fib.wasm
 fib(45) = 1134903170
 amplxe: Collection stopped.
diff --git a/docs/examples-rust-wasi.md b/docs/examples-rust-wasi.md
index 61b4c1251..ee5dfa43a 100644
--- a/docs/examples-rust-wasi.md
+++ b/docs/examples-rust-wasi.md
@@ -14,14 +14,14 @@ WebAssembly module.
 
 ### WebAssembly module source code
 
-For this WASI example, this Hello World program is compiled to a WebAssembly module using the WASI Preview 1 API. 
+For this WASI example, this Hello World program is compiled to a WebAssembly module using the WASI Preview 1 API.
 
 `wasi.rs`
 ```rust
 {{#include ../examples/wasi/wasm/wasi.rs}}
 ```
 
-Building this program generates `target/wasm32-wasi/debug/wasi.wasm`, used below.
+Building this program generates `target/wasm32-wasip1/debug/wasi.wasm`, used below.
 
 ### Invoke the WASM module
 
@@ -98,4 +98,4 @@ This does not require any change to the WebAssembly module, it's just the WASI A
 ```
 
 You can also [browse this source code online][code2] and clone the wasmtime
-repository to run the example locally.
\ No newline at end of file
+repository to run the example locally.
diff --git a/docs/stability-tiers.md b/docs/stability-tiers.md
index b294270ef..92892deb2 100644
--- a/docs/stability-tiers.md
+++ b/docs/stability-tiers.md
@@ -88,7 +88,7 @@ For explanations of what each tier means see below.
 |----------------------|-----------------------------------|-----------------------------|
 | Target               | `aarch64-pc-windows-msvc`         | CI testing, unwinding, full-time maintainer |
 | Target               | `riscv64gc-unknown-linux-gnu`     | full-time maintainer        |
-| Target               | `wasm32-wasi` [^3]                | Supported but not tested    |
+| Target               | `wasm32-wasip1` [^3]              | Supported but not tested    |
 | Target               | `aarch64-linux-android`           | CI testing, full-time maintainer |
 | Target               | `x86_64-linux-android`            | CI testing, full-time maintainer |
 | Target               | `x86_64-unknown-linux-musl` [^4]  | CI testing, full-time maintainer |
diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt
index 148890711..84a02e46f 100644
--- a/examples/CMakeLists.txt
+++ b/examples/CMakeLists.txt
@@ -62,8 +62,8 @@ create_target(wasi wasi/main.c)
 # Add rust tests
 create_rust_test(anyref)
 create_rust_wasm(fib-debug wasm32-unknown-unknown)
-create_rust_wasm(tokio wasm32-wasi)
-create_rust_wasm(wasi wasm32-wasi)
+create_rust_wasm(tokio wasm32-wasip1)
+create_rust_wasm(wasi wasm32-wasip1)
 create_rust_wasm(component wasm32-unknown-unknown)
 create_rust_test(epochs)
 create_rust_test(externref)
diff --git a/examples/component/main.rs b/examples/component/main.rs
index 025e857e3..0a30a5d26 100644
--- a/examples/component/main.rs
+++ b/examples/component/main.rs
@@ -41,7 +41,7 @@ fn main() -> Result<()> {
     let engine = Engine::new(Config::new().wasm_component_model(true))?;
 
     // NOTE: The wasm32-unknown-unknown target is used here for simplicity, real world use cases
-    // should probably use the wasm32-wasi target, and enable wasi preview2 within the component
+    // should probably use the wasm32-wasip1 target, and enable wasi preview2 within the component
     // model.
     let component = convert_to_component("target/wasm32-unknown-unknown/debug/guest.wasm")?;
 
diff --git a/examples/tokio/main.rs b/examples/tokio/main.rs
index b4a3d1724..f35138fe7 100644
--- a/examples/tokio/main.rs
+++ b/examples/tokio/main.rs
@@ -54,7 +54,7 @@ impl Environment {
         config.consume_fuel(true);
 
         let engine = Engine::new(&config)?;
-        let module = Module::from_file(&engine, "target/wasm32-wasi/debug/tokio-wasi.wasm")?;
+        let module = Module::from_file(&engine, "target/wasm32-wasip1/debug/tokio-wasi.wasm")?;
 
         // A `Linker` is shared in the environment amongst all stores, and this
         // linker is used to instantiate the `module` above. This example only
diff --git a/examples/wasi-async/main.rs b/examples/wasi-async/main.rs
index 57ad304d7..b1c3a4d19 100644
--- a/examples/wasi-async/main.rs
+++ b/examples/wasi-async/main.rs
@@ -33,7 +33,7 @@ async fn main() -> Result<()> {
 
     // Instantiate our 'Hello World' wasm module.
     // Note: This is a module built against the preview1 WASI API.
-    let module = Module::from_file(&engine, "target/wasm32-wasi/debug/wasi.wasm")?;
+    let module = Module::from_file(&engine, "target/wasm32-wasip1/debug/wasi.wasm")?;
     let func = linker
         .module_async(&mut store, "", &module)
         .await?
diff --git a/examples/wasi/main.c b/examples/wasi/main.c
index 520259d53..c933be4dc 100644
--- a/examples/wasi/main.c
+++ b/examples/wasi/main.c
@@ -48,7 +48,7 @@ int main() {
 
   wasm_byte_vec_t wasm;
   // Load our input file to parse it next
-  FILE *file = fopen("target/wasm32-wasi/debug/wasi.wasm", "rb");
+  FILE *file = fopen("target/wasm32-wasip1/debug/wasi.wasm", "rb");
   if (!file) {
     printf("> Error loading file!\n");
     exit(1);
diff --git a/examples/wasi/main.rs b/examples/wasi/main.rs
index 79583e564..c6a359243 100644
--- a/examples/wasi/main.rs
+++ b/examples/wasi/main.rs
@@ -25,7 +25,7 @@ fn main() -> Result<()> {
     let mut store = Store::new(&engine, wasi);
 
     // Instantiate our module with the imports we've created, and run it.
-    let module = Module::from_file(&engine, "target/wasm32-wasi/debug/wasi.wasm")?;
+    let module = Module::from_file(&engine, "target/wasm32-wasip1/debug/wasi.wasm")?;
     linker.module(&mut store, "", &module)?;
     linker
         .get_default(&mut store, "")?
diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml
index bba9c0383..58d21c1b1 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 000000000..d3bebbff4
--- /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 000000000..ff8c0dd55
--- /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 000000000..a20cde1da
--- /dev/null
+++ b/pulley/README.md
@@ -0,0 +1,122 @@
+<div align="center">
+  <h1>Pulley</h1>
+
+  <h3>Portable, Universal, Low-Level Execution strategY</h3>
+
+  <p>
+    <strong>A portable bytecode and fast interpreter</strong>
+  </p>
+
+  <strong>A <a href="https://bytecodealliance.org/">Bytecode Alliance</a> project</strong>
+
+  <p>
+    <a href="https://github.com/bytecodealliance/wasmtime/actions?query=workflow%3ACI"><img src="https://github.com/bytecodealliance/wasmtime/workflows/CI/badge.svg" alt="build status" /></a>
+    <a href="https://bytecodealliance.zulipchat.com/#narrow/stream/217126-wasmtime"><img src="https://img.shields.io/badge/zulip-join_chat-brightgreen.svg" alt="zulip chat" /></a>
+    <img src="https://img.shields.io/badge/rustc-stable+-green.svg" alt="supported rustc stable" />
+    <a href="https://docs.rs/pulley-interpreter"><img src="https://docs.rs/pulley-interpreter/badge.svg" alt="Documentation Status" /></a>
+  </p>
+
+  <h3>
+    <a href="https://bytecodealliance.zulipchat.com/#narrow/stream/217126-wasmtime">Chat</a>
+  </h3>
+</div>
+
+## 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 000000000..1a45eee77
--- /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 000000000..095943bbd
--- /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 000000000..1ef5ee10d
--- /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<Op>) {
+    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 000000000..b041d1676
--- /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 000000000..3f88db1b7
--- /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<Op>) {
+    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 000000000..9d609ca44
--- /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<T, E = DecodingError> = core::result::Result<T, E>;
+
+/// 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<const N: usize>(&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<const N: usize>(&mut self) -> Result<[u8; N], Self::Error> {
+        let bytes = *self
+            .bytecode
+            .first_chunk::<N>()
+            .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<const N: usize>(&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<T>(bytecode: &mut T) -> Result<Self, T::Error>
+    where
+        T: BytecodeStream;
+}
+
+impl Decode for u8 {
+    fn decode<T>(bytecode: &mut T) -> Result<Self, T::Error>
+    where
+        T: BytecodeStream,
+    {
+        bytecode.read::<1>().map(|a| a[0])
+    }
+}
+
+impl Decode for u16 {
+    fn decode<T>(bytecode: &mut T) -> Result<Self, T::Error>
+    where
+        T: BytecodeStream,
+    {
+        Ok(u16::from_le_bytes(bytecode.read()?))
+    }
+}
+
+impl Decode for u32 {
+    fn decode<T>(bytecode: &mut T) -> Result<Self, T::Error>
+    where
+        T: BytecodeStream,
+    {
+        Ok(u32::from_le_bytes(bytecode.read()?))
+    }
+}
+
+impl Decode for u64 {
+    fn decode<T>(bytecode: &mut T) -> Result<Self, T::Error>
+    where
+        T: BytecodeStream,
+    {
+        Ok(u64::from_le_bytes(bytecode.read()?))
+    }
+}
+
+impl Decode for i8 {
+    fn decode<T>(bytecode: &mut T) -> Result<Self, T::Error>
+    where
+        T: BytecodeStream,
+    {
+        bytecode.read::<1>().map(|a| a[0] as i8)
+    }
+}
+
+impl Decode for i16 {
+    fn decode<T>(bytecode: &mut T) -> Result<Self, T::Error>
+    where
+        T: BytecodeStream,
+    {
+        Ok(i16::from_le_bytes(bytecode.read()?))
+    }
+}
+
+impl Decode for i32 {
+    fn decode<T>(bytecode: &mut T) -> Result<Self, T::Error>
+    where
+        T: BytecodeStream,
+    {
+        Ok(i32::from_le_bytes(bytecode.read()?))
+    }
+}
+
+impl Decode for i64 {
+    fn decode<T>(bytecode: &mut T) -> Result<Self, T::Error>
+    where
+        T: BytecodeStream,
+    {
+        Ok(i64::from_le_bytes(bytecode.read()?))
+    }
+}
+
+impl Decode for XReg {
+    fn decode<T>(bytecode: &mut T) -> Result<Self, T::Error>
+    where
+        T: BytecodeStream,
+    {
+        let byte = u8::decode(bytecode)?;
+        XReg::new(byte).ok_or_else(|| bytecode.invalid_reg(byte))
+    }
+}
+
+impl Decode for FReg {
+    fn decode<T>(bytecode: &mut T) -> Result<Self, T::Error>
+    where
+        T: BytecodeStream,
+    {
+        let byte = u8::decode(bytecode)?;
+        FReg::new(byte).ok_or_else(|| bytecode.invalid_reg(byte))
+    }
+}
+
+impl Decode for VReg {
+    fn decode<T>(bytecode: &mut T) -> Result<Self, T::Error>
+    where
+        T: BytecodeStream,
+    {
+        let byte = u8::decode(bytecode)?;
+        VReg::new(byte).ok_or_else(|| bytecode.invalid_reg(byte))
+    }
+}
+
+impl Decode for PcRelOffset {
+    fn decode<T>(bytecode: &mut T) -> Result<Self, T::Error>
+    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<Vec<V::Return>>
+    where
+        V: OpVisitor<BytecodeStream = SafeBytecodeStream<'a>> + 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<V>(
+                &mut self,
+                visitor: &mut V,
+            ) -> Result<V::Return, <V::BytecodeStream as BytecodeStream>::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<F, T, V1, V2> OpVisitor for SequencedVisitor<'_, F, V1, V2>
+        where
+            F: FnMut(V1::Return, V2::Return) -> T,
+            V1: OpVisitor,
+            V2: OpVisitor<BytecodeStream = V1::BytecodeStream>,
+        {
+            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<V>(
+            visitor: &mut V,
+        ) -> Result<V::Return, <V::BytecodeStream as BytecodeStream>::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<F, T, V1, V2> ExtendedOpVisitor for SequencedVisitor<'_, F, V1, V2>
+        where
+            F: FnMut(V1::Return, V2::Return) -> T,
+            V1: ExtendedOpVisitor,
+            V2: ExtendedOpVisitor<BytecodeStream = V1::BytecodeStream>,
+        {
+            $(
+                $( #[$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 000000000..f7f2b9241
--- /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<String> {
+        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 000000000..fa2a3597a
--- /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<E>(&self, sink: &mut E)
+    where
+        E: Extend<u8>;
+}
+
+impl Encode for u8 {
+    fn encode<E>(&self, sink: &mut E)
+    where
+        E: Extend<u8>,
+    {
+        sink.extend(core::iter::once(*self));
+    }
+}
+
+impl Encode for u16 {
+    fn encode<E>(&self, sink: &mut E)
+    where
+        E: Extend<u8>,
+    {
+        sink.extend(self.to_le_bytes());
+    }
+}
+
+impl Encode for u32 {
+    fn encode<E>(&self, sink: &mut E)
+    where
+        E: Extend<u8>,
+    {
+        sink.extend(self.to_le_bytes());
+    }
+}
+
+impl Encode for u64 {
+    fn encode<E>(&self, sink: &mut E)
+    where
+        E: Extend<u8>,
+    {
+        sink.extend(self.to_le_bytes());
+    }
+}
+
+impl Encode for i8 {
+    fn encode<E>(&self, sink: &mut E)
+    where
+        E: Extend<u8>,
+    {
+        sink.extend(core::iter::once(*self as u8));
+    }
+}
+
+impl Encode for i16 {
+    fn encode<E>(&self, sink: &mut E)
+    where
+        E: Extend<u8>,
+    {
+        sink.extend(self.to_le_bytes());
+    }
+}
+
+impl Encode for i32 {
+    fn encode<E>(&self, sink: &mut E)
+    where
+        E: Extend<u8>,
+    {
+        sink.extend(self.to_le_bytes());
+    }
+}
+
+impl Encode for i64 {
+    fn encode<E>(&self, sink: &mut E)
+    where
+        E: Extend<u8>,
+    {
+        sink.extend(self.to_le_bytes());
+    }
+}
+
+impl Encode for XReg {
+    fn encode<E>(&self, sink: &mut E)
+    where
+        E: Extend<u8>,
+    {
+        sink.extend(core::iter::once(u8::try_from(self.index()).unwrap()));
+    }
+}
+
+impl Encode for FReg {
+    fn encode<E>(&self, sink: &mut E)
+    where
+        E: Extend<u8>,
+    {
+        sink.extend(core::iter::once(u8::try_from(self.index()).unwrap()));
+    }
+}
+
+impl Encode for VReg {
+    fn encode<E>(&self, sink: &mut E)
+    where
+        E: Extend<u8>,
+    {
+        sink.extend(core::iter::once(u8::try_from(self.index()).unwrap()));
+    }
+}
+
+impl Encode for PcRelOffset {
+    fn encode<E>(&self, sink: &mut E)
+    where
+        E: Extend<u8>,
+    {
+        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<E>(into: &mut E $( $( , $field : impl Into<$field_ty> )* )? )
+            where
+                E: Extend<u8>,
+            {
+                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<E>(into: &mut E $( $( , $field : impl Into<$field_ty> )* )? )
+            where
+                E: Extend<u8>,
+            {
+                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 000000000..cd331e2cd
--- /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<Self> {
+        // 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<i32> for PcRelOffset {
+    #[inline]
+    fn from(offset: i32) -> Self {
+        PcRelOffset(offset)
+    }
+}
+
+impl From<PcRelOffset> 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 000000000..efd5fa022
--- /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<u8>) -> 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<u8> {
+        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<u8>,
+        args: &[Val],
+        rets: impl IntoIterator<Item = RegType> + 'a,
+    ) -> Result<impl Iterator<Item = Val> + '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<u8>,
+}
+
+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<R: core::fmt::Debug> 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<u8>) -> 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 000000000..b0a62dc1c
--- /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<T> { 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>() -> 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 000000000..38c59b301
--- /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<ExtendedOp> 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<E>(&self, into: &mut E)
+            where
+                E: Extend<u8>,
+            {
+                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<E>(&self, into: &mut E)
+                where
+                    E: Extend<u8>,
+                {
+                    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<E>(&self, into: &mut E)
+            where
+                E: Extend<u8>,
+            {
+                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<E>(&self, into: &mut E)
+                where
+                    E: Extend<u8>,
+                {
+                    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<B> {
+    bytecode: B,
+}
+
+#[cfg(feature = "decode")]
+impl<B> MaterializeOpsVisitor<B> {
+    /// 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<B: crate::decode::BytecodeStream> crate::decode::OpVisitor for MaterializeOpsVisitor<B> {
+            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<B: crate::decode::BytecodeStream> crate::decode::ExtendedOpVisitor for MaterializeOpsVisitor<B> {
+            $(
+                $( #[$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 000000000..922cbf26a
--- /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<Self> {
+        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<Self> {
+        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 000000000..a7339f699
--- /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<u8> = $range;
+
+                /// Construct a new register of this class.
+                #[inline]
+                pub fn new(index: u8) -> Option<Self> {
+                    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<Self> {
+                    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<XReg> for AnyReg {
+    fn from(x: XReg) -> Self {
+        Self::X(x)
+    }
+}
+
+impl From<FReg> for AnyReg {
+    fn from(f: FReg) -> Self {
+        Self::F(f)
+    }
+}
+
+impl From<VReg> 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<Self> {
+        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<u8> = 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/scripts/publish.rs b/scripts/publish.rs
index 2ce8a0be1..20322dc63 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 115ac2c4b..290afc7a9 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
 
diff --git a/tests/misc_testsuite/winch/simd_linking.wast b/tests/misc_testsuite/winch/_simd_linking.wast
similarity index 100%
rename from tests/misc_testsuite/winch/simd_linking.wast
rename to tests/misc_testsuite/winch/_simd_linking.wast
diff --git a/tests/misc_testsuite/winch/simd_load.wast b/tests/misc_testsuite/winch/_simd_load.wast
similarity index 100%
rename from tests/misc_testsuite/winch/simd_load.wast
rename to tests/misc_testsuite/winch/_simd_load.wast
diff --git a/tests/misc_testsuite/winch/simd_multivalue.wast b/tests/misc_testsuite/winch/_simd_multivalue.wast
similarity index 100%
rename from tests/misc_testsuite/winch/simd_multivalue.wast
rename to tests/misc_testsuite/winch/_simd_multivalue.wast
diff --git a/tests/misc_testsuite/winch/simd_store.wast b/tests/misc_testsuite/winch/_simd_store.wast
similarity index 100%
rename from tests/misc_testsuite/winch/simd_store.wast
rename to tests/misc_testsuite/winch/_simd_store.wast
diff --git a/winch/codegen/src/isa/x64/asm.rs b/winch/codegen/src/isa/x64/asm.rs
index 40e804956..e52b11360 100644
--- a/winch/codegen/src/isa/x64/asm.rs
+++ b/winch/codegen/src/isa/x64/asm.rs
@@ -59,31 +59,31 @@ impl From<Reg> for WritableXmm {
 
 impl From<Reg> for Gpr {
     fn from(reg: Reg) -> Self {
-        Gpr::new(reg.into()).expect("valid gpr")
+        Gpr::unwrap_new(reg.into())
     }
 }
 
 impl From<Reg> for GprMem {
     fn from(value: Reg) -> Self {
-        GprMem::new(value.into()).expect("valid gpr")
+        GprMem::unwrap_new(value.into())
     }
 }
 
 impl From<Reg> for GprMemImm {
     fn from(reg: Reg) -> Self {
-        GprMemImm::new(reg.into()).expect("valid gpr")
+        GprMemImm::unwrap_new(reg.into())
     }
 }
 
 impl From<Reg> for Imm8Gpr {
     fn from(value: Reg) -> Self {
-        Imm8Gpr::new(Imm8Reg::Reg { reg: value.into() }).expect("valid Imm8Gpr")
+        Imm8Gpr::unwrap_new(Imm8Reg::Reg { reg: value.into() })
     }
 }
 
 impl From<Reg> for Xmm {
     fn from(reg: Reg) -> Self {
-        Xmm::new(reg.into()).expect("valid Xmm register")
+        Xmm::unwrap_new(reg.into())
     }
 }
 
@@ -348,7 +348,7 @@ impl Assembler {
             let reg_mem = RegMem::mem(src);
             self.emit(Inst::MovzxRmR {
                 ext_mode: ext,
-                src: GprMem::new(reg_mem).expect("valid memory address"),
+                src: GprMem::unwrap_new(reg_mem),
                 dst: dst.into(),
             });
         } else {
@@ -378,7 +378,7 @@ impl Assembler {
         let reg_mem = RegMem::mem(src);
         self.emit(Inst::MovsxRmR {
             ext_mode: ext.into(),
-            src: GprMem::new(reg_mem).expect("valid memory address"),
+            src: GprMem::unwrap_new(reg_mem),
             dst: dst.into(),
         })
     }
@@ -426,7 +426,7 @@ impl Assembler {
 
         self.emit(Inst::XmmUnaryRmRUnaligned {
             op,
-            src: XmmMem::new(src.into()).expect("valid xmm unaligned"),
+            src: XmmMem::unwrap_new(src.into()),
             dst: dst.into(),
         });
     }
@@ -452,7 +452,7 @@ impl Assembler {
         );
         self.emit(Inst::XmmUnaryRmRUnaligned {
             op,
-            src: XmmMem::new(RegMem::mem(src)).expect("valid xmm unaligned"),
+            src: XmmMem::unwrap_new(RegMem::mem(src)),
             dst: dst.into(),
         });
     }
@@ -497,7 +497,7 @@ impl Assembler {
         self.emit(Inst::XmmCmove {
             ty,
             cc: cc.into(),
-            consequent: Xmm::new(src.into()).expect("valid Xmm"),
+            consequent: Xmm::unwrap_new(src.into()),
             alternative: dst.into(),
             dst: dst.into(),
         })
@@ -522,7 +522,7 @@ impl Assembler {
             size: size.into(),
             op: AluRmiROpcode::Sub,
             src1: dst.into(),
-            src2: GprMemImm::new(imm).expect("valid immediate"),
+            src2: GprMemImm::unwrap_new(imm),
             dst: dst.into(),
         });
     }
@@ -544,7 +544,7 @@ impl Assembler {
             size: size.into(),
             op: AluRmiROpcode::And,
             src1: dst.into(),
-            src2: GprMemImm::new(imm).expect("valid immediate"),
+            src2: GprMemImm::unwrap_new(imm),
             dst: dst.into(),
         });
     }
@@ -713,7 +713,7 @@ impl Assembler {
 
         self.emit(Inst::XmmRmRUnaligned {
             op,
-            src2: Xmm::new(src.into()).expect("valid xmm unaligned").into(),
+            src2: Xmm::unwrap_new(src.into()).into(),
             src1: dst.into(),
             dst: dst.into(),
         });
@@ -736,7 +736,7 @@ impl Assembler {
             size: size.into(),
             op: AluRmiROpcode::Or,
             src1: dst.into(),
-            src2: GprMemImm::new(imm).expect("valid immediate"),
+            src2: GprMemImm::unwrap_new(imm),
             dst: dst.into(),
         });
     }
@@ -774,7 +774,7 @@ impl Assembler {
             size: size.into(),
             op: AluRmiROpcode::Xor,
             src1: dst.into(),
-            src2: GprMemImm::new(imm).expect("valid immediate"),
+            src2: GprMemImm::unwrap_new(imm),
             dst: dst.into(),
         });
     }
@@ -814,7 +814,7 @@ impl Assembler {
             size: size.into(),
             kind: kind.into(),
             src: dst.into(),
-            num_bits: Imm8Gpr::new(imm).expect("valid immediate"),
+            num_bits: Imm8Gpr::unwrap_new(imm),
             dst: dst.into(),
         });
     }
@@ -836,7 +836,7 @@ impl Assembler {
                 self.emit(Inst::CmpRmiR {
                     size: size.into(),
                     src1: divisor.into(),
-                    src2: GprMemImm::new(RegMemImm::imm(0)).unwrap(),
+                    src2: GprMemImm::unwrap_new(RegMemImm::imm(0)),
                     opcode: CmpOpcode::Cmp,
                 });
                 self.emit(Inst::TrapIf {
@@ -871,7 +871,7 @@ impl Assembler {
             sign: kind.into(),
             size: size.into(),
             trap,
-            divisor: GprMem::new(RegMem::reg(divisor.into())).unwrap(),
+            divisor: GprMem::unwrap_new(RegMem::reg(divisor.into())),
             dividend_lo: dst.0.into(),
             dividend_hi: dst.1.into(),
             dst_quotient: dst.0.into(),
@@ -921,7 +921,7 @@ impl Assembler {
                     sign: DivSignedness::Unsigned,
                     trap: TrapCode::IntegerDivisionByZero,
                     size: size.into(),
-                    divisor: GprMem::new(RegMem::reg(divisor.into())).unwrap(),
+                    divisor: GprMem::unwrap_new(RegMem::reg(divisor.into())),
                     dividend_lo: dst.0.into(),
                     dividend_hi: dst.1.into(),
                     dst_quotient: dst.0.into(),
@@ -959,7 +959,7 @@ impl Assembler {
             size: size.into(),
             op: AluRmiROpcode::Add,
             src1: dst.into(),
-            src2: GprMemImm::new(imm).expect("valid immediate"),
+            src2: GprMemImm::unwrap_new(imm),
             dst: dst.into(),
         });
     }
@@ -982,7 +982,7 @@ impl Assembler {
             size: size.into(),
             opcode: CmpOpcode::Cmp,
             src1: src1.into(),
-            src2: GprMemImm::new(imm).expect("valid immediate"),
+            src2: GprMemImm::unwrap_new(imm),
         });
     }
 


Test patch:
diff --git a/cranelift/codegen/src/isa/x64/inst/emit_tests.rs b/cranelift/codegen/src/isa/x64/inst/emit_tests.rs
index 5dd3dd505..5ac1fbe12 100644
--- a/cranelift/codegen/src/isa/x64/inst/emit_tests.rs
+++ b/cranelift/codegen/src/isa/x64/inst/emit_tests.rs
@@ -23,7 +23,7 @@ impl Inst {
         debug_assert_eq!(src.to_reg().class(), RegClass::Int);
         Inst::Neg {
             size,
-            src: Gpr::new(src.to_reg()).unwrap(),
+            src: Gpr::unwrap_new(src.to_reg()),
             dst: WritableGpr::from_writable_reg(src).unwrap(),
         }
     }
@@ -33,7 +33,7 @@ impl Inst {
         debug_assert!(dst.to_reg().class() == RegClass::Float);
         Inst::XmmUnaryRmRImm {
             op,
-            src: XmmMemAligned::new(src).unwrap(),
+            src: XmmMemAligned::unwrap_new(src),
             imm,
             dst: WritableXmm::from_writable_reg(dst).unwrap(),
         }
@@ -44,7 +44,7 @@ impl Inst {
         debug_assert!(dst.to_reg().class() == RegClass::Float);
         Inst::XmmUnaryRmREvex {
             op,
-            src: XmmMem::new(src).unwrap(),
+            src: XmmMem::unwrap_new(src),
             dst: WritableXmm::from_writable_reg(dst).unwrap(),
         }
     }
@@ -54,8 +54,8 @@ impl Inst {
         debug_assert!(dst.to_reg().class() == RegClass::Float);
         Inst::XmmRmiReg {
             opcode,
-            src1: Xmm::new(dst.to_reg()).unwrap(),
-            src2: XmmMemAlignedImm::new(src).unwrap(),
+            src1: Xmm::unwrap_new(dst.to_reg()),
+            src2: XmmMemAlignedImm::unwrap_new(src),
             dst: WritableXmm::from_writable_reg(dst).unwrap(),
         }
     }
@@ -67,8 +67,8 @@ impl Inst {
         debug_assert!(dst.to_reg().class() == RegClass::Float);
         Inst::XmmRmREvex {
             op,
-            src1: Xmm::new(src1).unwrap(),
-            src2: XmmMem::new(src2).unwrap(),
+            src1: Xmm::unwrap_new(src1),
+            src2: XmmMem::unwrap_new(src2),
             dst: WritableXmm::from_writable_reg(dst).unwrap(),
         }
     }
@@ -87,9 +87,9 @@ impl Inst {
         debug_assert!(dst.to_reg().class() == RegClass::Float);
         Inst::XmmRmREvex3 {
             op,
-            src1: Xmm::new(src1).unwrap(),
-            src2: Xmm::new(src2).unwrap(),
-            src3: XmmMem::new(src3).unwrap(),
+            src1: Xmm::unwrap_new(src1),
+            src2: Xmm::unwrap_new(src2),
+            src3: XmmMem::unwrap_new(src3),
             dst: WritableXmm::from_writable_reg(dst).unwrap(),
         }
     }
@@ -100,7 +100,7 @@ impl Inst {
         debug_assert!(dst.to_reg().class() == RegClass::Float);
         Inst::XmmUnaryRmR {
             op,
-            src: XmmMemAligned::new(src).unwrap(),
+            src: XmmMemAligned::unwrap_new(src),
             dst: WritableXmm::from_writable_reg(dst).unwrap(),
         }
     }
@@ -113,7 +113,7 @@ impl Inst {
 
     fn bswap(size: OperandSize, dst: Writable<Reg>) -> Inst {
         debug_assert!(dst.to_reg().class() == RegClass::Int);
-        let src = Gpr::new(dst.to_reg()).unwrap();
+        let src = Gpr::unwrap_new(dst.to_reg());
         let dst = WritableGpr::from_writable_reg(dst).unwrap();
         Inst::Bswap { size, src, dst }
     }
@@ -139,9 +139,9 @@ impl Inst {
     fn xmm_rm_r_blend(op: SseOpcode, src2: RegMem, dst: Writable<Reg>) -> Inst {
         Inst::XmmRmRBlend {
             op,
-            src1: Xmm::new(dst.to_reg()).unwrap(),
-            src2: XmmMemAligned::new(src2).unwrap(),
-            mask: Xmm::new(regs::xmm0()).unwrap(),
+            src1: Xmm::unwrap_new(dst.to_reg()),
+            src2: XmmMemAligned::unwrap_new(src2),
+            mask: Xmm::unwrap_new(regs::xmm0()),
             dst: WritableXmm::from_writable_reg(dst).unwrap(),
         }
     }
@@ -814,7 +814,7 @@ fn test_x64_emit() {
     // Addr_IRRS, offset max simm8
     insns.push((
         Inst::mov64_m_r(
-            Amode::imm_reg_reg_shift(127, Gpr::new(rax).unwrap(), Gpr::new(rax).unwrap(), 0),
+            Amode::imm_reg_reg_shift(127, Gpr::unwrap_new(rax), Gpr::unwrap_new(rax), 0),
             w_r11,
         ),
         "4C8B5C007F",
@@ -822,7 +822,7 @@ fn test_x64_emit() {
     ));
     insns.push((
         Inst::mov64_m_r(
-            Amode::imm_reg_reg_shift(127, Gpr::new(rdi).unwrap(), Gpr::new(rax).unwrap(), 1),
+            Amode::imm_reg_reg_shift(127, Gpr::unwrap_new(rdi), Gpr::unwrap_new(rax), 1),
             w_r11,
         ),
         "4C8B5C477F",
@@ -830,7 +830,7 @@ fn test_x64_emit() {
     ));
     insns.push((
         Inst::mov64_m_r(
-            Amode::imm_reg_reg_shift(127, Gpr::new(r8).unwrap(), Gpr::new(rax).unwrap(), 2),
+            Amode::imm_reg_reg_shift(127, Gpr::unwrap_new(r8), Gpr::unwrap_new(rax), 2),
             w_r11,
         ),
         "4D8B5C807F",
@@ -838,7 +838,7 @@ fn test_x64_emit() {
     ));
     insns.push((
         Inst::mov64_m_r(
-            Amode::imm_reg_reg_shift(127, Gpr::new(r15).unwrap(), Gpr::new(rax).unwrap(), 3),
+            Amode::imm_reg_reg_shift(127, Gpr::unwrap_new(r15), Gpr::unwrap_new(rax), 3),
             w_r11,
         ),
         "4D8B5CC77F",
@@ -846,7 +846,7 @@ fn test_x64_emit() {
     ));
     insns.push((
         Inst::mov64_m_r(
-            Amode::imm_reg_reg_shift(127, Gpr::new(rax).unwrap(), Gpr::new(rdi).unwrap(), 3),
+            Amode::imm_reg_reg_shift(127, Gpr::unwrap_new(rax), Gpr::unwrap_new(rdi), 3),
             w_r11,
         ),
         "4C8B5CF87F",
@@ -854,7 +854,7 @@ fn test_x64_emit() {
     ));
     insns.push((
         Inst::mov64_m_r(
-            Amode::imm_reg_reg_shift(127, Gpr::new(rdi).unwrap(), Gpr::new(rdi).unwrap(), 2),
+            Amode::imm_reg_reg_shift(127, Gpr::unwrap_new(rdi), Gpr::unwrap_new(rdi), 2),
             w_r11,
         ),
         "4C8B5CBF7F",
@@ -862,7 +862,7 @@ fn test_x64_emit() {
     ));
     insns.push((
         Inst::mov64_m_r(
-            Amode::imm_reg_reg_shift(127, Gpr::new(r8).unwrap(), Gpr::new(rdi).unwrap(), 1),
+            Amode::imm_reg_reg_shift(127, Gpr::unwrap_new(r8), Gpr::unwrap_new(rdi), 1),
             w_r11,
         ),
         "4D8B5C787F",
@@ -870,7 +870,7 @@ fn test_x64_emit() {
     ));
     insns.push((
         Inst::mov64_m_r(
-            Amode::imm_reg_reg_shift(127, Gpr::new(r15).unwrap(), Gpr::new(rdi).unwrap(), 0),
+            Amode::imm_reg_reg_shift(127, Gpr::unwrap_new(r15), Gpr::unwrap_new(rdi), 0),
             w_r11,
         ),
         "4D8B5C3F7F",
@@ -881,7 +881,7 @@ fn test_x64_emit() {
     // Addr_IRRS, offset min simm8
     insns.push((
         Inst::mov64_m_r(
-            Amode::imm_reg_reg_shift(-128i32, Gpr::new(rax).unwrap(), Gpr::new(r8).unwrap(), 2),
+            Amode::imm_reg_reg_shift(-128i32, Gpr::unwrap_new(rax), Gpr::unwrap_new(r8), 2),
             w_r11,
         ),
         "4E8B5C8080",
@@ -889,7 +889,7 @@ fn test_x64_emit() {
     ));
     insns.push((
         Inst::mov64_m_r(
-            Amode::imm_reg_reg_shift(-128i32, Gpr::new(rdi).unwrap(), Gpr::new(r8).unwrap(), 3),
+            Amode::imm_reg_reg_shift(-128i32, Gpr::unwrap_new(rdi), Gpr::unwrap_new(r8), 3),
             w_r11,
         ),
         "4E8B5CC780",
@@ -897,7 +897,7 @@ fn test_x64_emit() {
     ));
     insns.push((
         Inst::mov64_m_r(
-            Amode::imm_reg_reg_shift(-128i32, Gpr::new(r8).unwrap(), Gpr::new(r8).unwrap(), 0),
+            Amode::imm_reg_reg_shift(-128i32, Gpr::unwrap_new(r8), Gpr::unwrap_new(r8), 0),
             w_r11,
         ),
         "4F8B5C0080",
@@ -905,7 +905,7 @@ fn test_x64_emit() {
     ));
     insns.push((
         Inst::mov64_m_r(
-            Amode::imm_reg_reg_shift(-128i32, Gpr::new(r15).unwrap(), Gpr::new(r8).unwrap(), 1),
+            Amode::imm_reg_reg_shift(-128i32, Gpr::unwrap_new(r15), Gpr::unwrap_new(r8), 1),
             w_r11,
         ),
         "4F8B5C4780",
@@ -913,7 +913,7 @@ fn test_x64_emit() {
     ));
     insns.push((
         Inst::mov64_m_r(
-            Amode::imm_reg_reg_shift(-128i32, Gpr::new(rax).unwrap(), Gpr::new(r15).unwrap(), 1),
+            Amode::imm_reg_reg_shift(-128i32, Gpr::unwrap_new(rax), Gpr::unwrap_new(r15), 1),
             w_r11,
         ),
         "4E8B5C7880",
@@ -921,7 +921,7 @@ fn test_x64_emit() {
     ));
     insns.push((
         Inst::mov64_m_r(
-            Amode::imm_reg_reg_shift(-128i32, Gpr::new(rdi).unwrap(), Gpr::new(r15).unwrap(), 0),
+            Amode::imm_reg_reg_shift(-128i32, Gpr::unwrap_new(rdi), Gpr::unwrap_new(r15), 0),
             w_r11,
         ),
         "4E8B5C3F80",
@@ -929,7 +929,7 @@ fn test_x64_emit() {
     ));
     insns.push((
         Inst::mov64_m_r(
-            Amode::imm_reg_reg_shift(-128i32, Gpr::new(r8).unwrap(), Gpr::new(r15).unwrap(), 3),
+            Amode::imm_reg_reg_shift(-128i32, Gpr::unwrap_new(r8), Gpr::unwrap_new(r15), 3),
             w_r11,
         ),
         "4F8B5CF880",
@@ -937,7 +937,7 @@ fn test_x64_emit() {
     ));
     insns.push((
         Inst::mov64_m_r(
-            Amode::imm_reg_reg_shift(-128i32, Gpr::new(r15).unwrap(), Gpr::new(r15).unwrap(), 2),
+            Amode::imm_reg_reg_shift(-128i32, Gpr::unwrap_new(r15), Gpr::unwrap_new(r15), 2),
             w_r11,
         ),
         "4F8B5CBF80",
@@ -948,12 +948,7 @@ fn test_x64_emit() {
     // Addr_IRRS, offset large positive simm32
     insns.push((
         Inst::mov64_m_r(
-            Amode::imm_reg_reg_shift(
-                0x4f6625be,
-                Gpr::new(rax).unwrap(),
-                Gpr::new(rax).unwrap(),
-                0,
-            ),
+            Amode::imm_reg_reg_shift(0x4f6625be, Gpr::unwrap_new(rax), Gpr::unwrap_new(rax), 0),
             w_r11,
         ),
         "4C8B9C00BE25664F",
@@ -961,12 +956,7 @@ fn test_x64_emit() {
     ));
     insns.push((
         Inst::mov64_m_r(
-            Amode::imm_reg_reg_shift(
-                0x4f6625be,
-                Gpr::new(rdi).unwrap(),
-                Gpr::new(rax).unwrap(),
-                1,
-            ),
+            Amode::imm_reg_reg_shift(0x4f6625be, Gpr::unwrap_new(rdi), Gpr::unwrap_new(rax), 1),
             w_r11,
         ),
         "4C8B9C47BE25664F",
@@ -974,7 +964,7 @@ fn test_x64_emit() {
     ));
     insns.push((
         Inst::mov64_m_r(
-            Amode::imm_reg_reg_shift(0x4f6625be, Gpr::new(r8).unwrap(), Gpr::new(rax).unwrap(), 2),
+            Amode::imm_reg_reg_shift(0x4f6625be, Gpr::unwrap_new(r8), Gpr::unwrap_new(rax), 2),
             w_r11,
         ),
         "4D8B9C80BE25664F",
@@ -982,12 +972,7 @@ fn test_x64_emit() {
     ));
     insns.push((
         Inst::mov64_m_r(
-            Amode::imm_reg_reg_shift(
-                0x4f6625be,
-                Gpr::new(r15).unwrap(),
-                Gpr::new(rax).unwrap(),
-                3,
-            ),
+            Amode::imm_reg_reg_shift(0x4f6625be, Gpr::unwrap_new(r15), Gpr::unwrap_new(rax), 3),
             w_r11,
         ),
         "4D8B9CC7BE25664F",
@@ -995,12 +980,7 @@ fn test_x64_emit() {
     ));
     insns.push((
         Inst::mov64_m_r(
-            Amode::imm_reg_reg_shift(
-                0x4f6625be,
-                Gpr::new(rax).unwrap(),
-                Gpr::new(rdi).unwrap(),
-                3,
-            ),
+            Amode::imm_reg_reg_shift(0x4f6625be, Gpr::unwrap_new(rax), Gpr::unwrap_new(rdi), 3),
             w_r11,
         ),
         "4C8B9CF8BE25664F",
@@ -1008,12 +988,7 @@ fn test_x64_emit() {
     ));
     insns.push((
         Inst::mov64_m_r(
-            Amode::imm_reg_reg_shift(
-                0x4f6625be,
-                Gpr::new(rdi).unwrap(),
-                Gpr::new(rdi).unwrap(),
-                2,
-            ),
+            Amode::imm_reg_reg_shift(0x4f6625be, Gpr::unwrap_new(rdi), Gpr::unwrap_new(rdi), 2),
             w_r11,
         ),
         "4C8B9CBFBE25664F",
@@ -1021,7 +996,7 @@ fn test_x64_emit() {
     ));
     insns.push((
         Inst::mov64_m_r(
-            Amode::imm_reg_reg_shift(0x4f6625be, Gpr::new(r8).unwrap(), Gpr::new(rdi).unwrap(), 1),
+            Amode::imm_reg_reg_shift(0x4f6625be, Gpr::unwrap_new(r8), Gpr::unwrap_new(rdi), 1),
             w_r11,
         ),
         "4D8B9C78BE25664F",
@@ -1029,12 +1004,7 @@ fn test_x64_emit() {
     ));
     insns.push((
         Inst::mov64_m_r(
-            Amode::imm_reg_reg_shift(
-                0x4f6625be,
-                Gpr::new(r15).unwrap(),
-                Gpr::new(rdi).unwrap(),
-                0,
-            ),
+            Amode::imm_reg_reg_shift(0x4f6625be, Gpr::unwrap_new(r15), Gpr::unwrap_new(rdi), 0),
             w_r11,
         ),
         "4D8B9C3FBE25664F",
@@ -1045,12 +1015,7 @@ fn test_x64_emit() {
     // Addr_IRRS, offset large negative simm32
     insns.push((
         Inst::mov64_m_r(
-            Amode::imm_reg_reg_shift(
-                -0x264d1690i32,
-                Gpr::new(rax).unwrap(),
-                Gpr::new(r8).unwrap(),
-                2,
-            ),
+            Amode::imm_reg_reg_shift(-0x264d1690i32, Gpr::unwrap_new(rax), Gpr::unwrap_new(r8), 2),
             w_r11,
         ),
         "4E8B9C8070E9B2D9",
@@ -1058,12 +1023,7 @@ fn test_x64_emit() {
     ));
     insns.push((
         Inst::mov64_m_r(
-            Amode::imm_reg_reg_shift(
-                -0x264d1690i32,
-                Gpr::new(rdi).unwrap(),
-                Gpr::new(r8).unwrap(),
-                3,
-            ),
+            Amode::imm_reg_reg_shift(-0x264d1690i32, Gpr::unwrap_new(rdi), Gpr::unwrap_new(r8), 3),
             w_r11,
         ),
         "4E8B9CC770E9B2D9",
@@ -1071,12 +1031,7 @@ fn test_x64_emit() {
     ));
     insns.push((
         Inst::mov64_m_r(
-            Amode::imm_reg_reg_shift(
-                -0x264d1690i32,
-                Gpr::new(r8).unwrap(),
-                Gpr::new(r8).unwrap(),
-                0,
-            ),
+            Amode::imm_reg_reg_shift(-0x264d1690i32, Gpr::unwrap_new(r8), Gpr::unwrap_new(r8), 0),
             w_r11,
         ),
         "4F8B9C0070E9B2D9",
@@ -1084,12 +1039,7 @@ fn test_x64_emit() {
     ));
     insns.push((
         Inst::mov64_m_r(
-            Amode::imm_reg_reg_shift(
-                -0x264d1690i32,
-                Gpr::new(r15).unwrap(),
-                Gpr::new(r8).unwrap(),
-                1,
-            ),
+            Amode::imm_reg_reg_shift(-0x264d1690i32, Gpr::unwrap_new(r15), Gpr::unwrap_new(r8), 1),
             w_r11,
         ),
         "4F8B9C4770E9B2D9",
@@ -1099,8 +1049,8 @@ fn test_x64_emit() {
         Inst::mov64_m_r(
             Amode::imm_reg_reg_shift(
                 -0x264d1690i32,
-                Gpr::new(rax).unwrap(),
-                Gpr::new(r15).unwrap(),
+                Gpr::unwrap_new(rax),
+                Gpr::unwrap_new(r15),
                 1,
             ),
             w_r11,
@@ -1112,8 +1062,8 @@ fn test_x64_emit() {
         Inst::mov64_m_r(
             Amode::imm_reg_reg_shift(
                 -0x264d1690i32,
-                Gpr::new(rdi).unwrap(),
-                Gpr::new(r15).unwrap(),
+                Gpr::unwrap_new(rdi),
+                Gpr::unwrap_new(r15),
                 0,
             ),
             w_r11,
@@ -1123,12 +1073,7 @@ fn test_x64_emit() {
     ));
     insns.push((
         Inst::mov64_m_r(
-            Amode::imm_reg_reg_shift(
-                -0x264d1690i32,
-                Gpr::new(r8).unwrap(),
-                Gpr::new(r15).unwrap(),
-                3,
-            ),
+            Amode::imm_reg_reg_shift(-0x264d1690i32, Gpr::unwrap_new(r8), Gpr::unwrap_new(r15), 3),
             w_r11,
         ),
         "4F8B9CF870E9B2D9",
@@ -1138,8 +1083,8 @@ fn test_x64_emit() {
         Inst::mov64_m_r(
             Amode::imm_reg_reg_shift(
                 -0x264d1690i32,
-                Gpr::new(r15).unwrap(),
-                Gpr::new(r15).unwrap(),
+                Gpr::unwrap_new(r15),
+                Gpr::unwrap_new(r15),
                 2,
             ),
             w_r11,
@@ -1671,7 +1616,7 @@ fn test_x64_emit() {
             size: OperandSize::Size32,
             op: AluRmiROpcode::Add,
             src1_dst: Amode::imm_reg(99, rdi).into(),
-            src2: Gpr::new(r12).unwrap(),
+            src2: Gpr::unwrap_new(r12),
         },
         "44016763",
         "addl    %r12d, 99(%rdi)",
@@ -1681,14 +1626,9 @@ fn test_x64_emit() {
         Inst::AluRM {
             size: OperandSize::Size64,
             op: AluRmiROpcode::Add,
-            src1_dst: Amode::imm_reg_reg_shift(
-                0,
-                Gpr::new(rbp).unwrap(),
-                Gpr::new(rax).unwrap(),
-                3,
-            )
-            .into(),
-            src2: Gpr::new(rax).unwrap(),
+            src1_dst: Amode::imm_reg_reg_shift(0, Gpr::unwrap_new(rbp), Gpr::unwrap_new(rax), 3)
+                .into(),
+            src2: Gpr::unwrap_new(rax),
         },
         "480144C500",
         "addq    %rax, 0(%rbp,%rax,8)",
@@ -1699,7 +1639,7 @@ fn test_x64_emit() {
             size: OperandSize::Size32,
             op: AluRmiROpcode::Sub,
             src1_dst: Amode::imm_reg(0, rsp).into(),
-            src2: Gpr::new(rcx).unwrap(),
+            src2: Gpr::unwrap_new(rcx),
         },
         "290C24",
         "subl    %ecx, 0(%rsp)",
@@ -1710,7 +1650,7 @@ fn test_x64_emit() {
             size: OperandSize::Size64,
             op: AluRmiROpcode::Sub,
             src1_dst: Amode::imm_reg(0, rbp).into(),
-            src2: Gpr::new(rax).unwrap(),
+            src2: Gpr::unwrap_new(rax),
         },
         "48294500",
         "subq    %rax, 0(%rbp)",
@@ -1721,7 +1661,7 @@ fn test_x64_emit() {
             size: OperandSize::Size32,
             op: AluRmiROpcode::And,
             src1_dst: Amode::imm_reg(0, rsp).into(),
-            src2: Gpr::new(rcx).unwrap(),
+            src2: Gpr::unwrap_new(rcx),
         },
         "210C24",
         "andl    %ecx, 0(%rsp)",
@@ -1732,7 +1672,7 @@ fn test_x64_emit() {
             size: OperandSize::Size64,
             op: AluRmiROpcode::And,
             src1_dst: Amode::imm_reg(0, rbp).into(),
-            src2: Gpr::new(rax).unwrap(),
+            src2: Gpr::unwrap_new(rax),
         },
         "48214500",
         "andq    %rax, 0(%rbp)",
@@ -1743,7 +1683,7 @@ fn test_x64_emit() {
             size: OperandSize::Size32,
             op: AluRmiROpcode::Or,
             src1_dst: Amode::imm_reg(0, rsp).into(),
-            src2: Gpr::new(rcx).unwrap(),
+            src2: Gpr::unwrap_new(rcx),
         },
         "090C24",
         "orl     %ecx, 0(%rsp)",
@@ -1754,7 +1694,7 @@ fn test_x64_emit() {
             size: OperandSize::Size64,
             op: AluRmiROpcode::Or,
             src1_dst: Amode::imm_reg(0, rbp).into(),
-            src2: Gpr::new(rax).unwrap(),
+            src2: Gpr::unwrap_new(rax),
         },
         "48094500",
         "orq     %rax, 0(%rbp)",
@@ -1765,7 +1705,7 @@ fn test_x64_emit() {
             size: OperandSize::Size32,
             op: AluRmiROpcode::Xor,
             src1_dst: Amode::imm_reg(0, rsp).into(),
-            src2: Gpr::new(rcx).unwrap(),
+            src2: Gpr::unwrap_new(rcx),
         },
         "310C24",
         "xorl    %ecx, 0(%rsp)",
@@ -1776,7 +1716,7 @@ fn test_x64_emit() {
             size: OperandSize::Size64,
             op: AluRmiROpcode::Xor,
             src1_dst: Amode::imm_reg(0, rbp).into(),
-            src2: Gpr::new(rax).unwrap(),
+            src2: Gpr::unwrap_new(rax),
         },
         "48314500",
         "xorq    %rax, 0(%rbp)",
@@ -1787,7 +1727,7 @@ fn test_x64_emit() {
             size: OperandSize::Size16,
             op: AluRmiROpcode::Add,
             src1_dst: Amode::imm_reg(0, rbp).into(),
-            src2: Gpr::new(rax).unwrap(),
+            src2: Gpr::unwrap_new(rax),
         },
         "66014500",
         "addw    %ax, 0(%rbp)",
@@ -1797,7 +1737,7 @@ fn test_x64_emit() {
             size: OperandSize::Size16,
             op: AluRmiROpcode::Sub,
             src1_dst: Amode::imm_reg(0, rbp).into(),
-            src2: Gpr::new(r12).unwrap(),
+            src2: Gpr::unwrap_new(r12),
         },
         "6644296500",
         "subw    %r12w, 0(%rbp)",
@@ -1808,7 +1748,7 @@ fn test_x64_emit() {
             size: OperandSize::Size8,
             op: AluRmiROpcode::Add,
             src1_dst: Amode::imm_reg(0, rbp).into(),
-            src2: Gpr::new(rax).unwrap(),
+            src2: Gpr::unwrap_new(rax),
         },
         "004500",
         "addb    %al, 0(%rbp)",
@@ -1818,7 +1758,7 @@ fn test_x64_emit() {
             size: OperandSize::Size8,
             op: AluRmiROpcode::Sub,
             src1_dst: Amode::imm_reg(0, rbp).into(),
-            src2: Gpr::new(rbp).unwrap(),
+            src2: Gpr::unwrap_new(rbp),
         },
         "40286D00",
         "subb    %bpl, 0(%rbp)",
@@ -1828,7 +1768,7 @@ fn test_x64_emit() {
             size: OperandSize::Size8,
             op: AluRmiROpcode::Xor,
             src1_dst: Amode::imm_reg(0, rbp).into(),
-            src2: Gpr::new(r10).unwrap(),
+            src2: Gpr::unwrap_new(r10),
         },
         "44305500",
         "xorb    %r10b, 0(%rbp)",
@@ -1838,7 +1778,7 @@ fn test_x64_emit() {
             size: OperandSize::Size8,
             op: AluRmiROpcode::And,
             src1_dst: Amode::imm_reg(0, rbp).into(),
-            src2: Gpr::new(r15).unwrap(),
+            src2: Gpr::unwrap_new(r15),
         },
         "44207D00",
         "andb    %r15b, 0(%rbp)",
@@ -1942,10 +1882,10 @@ fn test_x64_emit() {
             DivSignedness::Signed,
             TrapCode::IntegerDivisionByZero,
             RegMem::reg(regs::rsi()),
-            Gpr::new(regs::rax()).unwrap(),
-            Gpr::new(regs::rdx()).unwrap(),
-            WritableGpr::from_reg(Gpr::new(regs::rax()).unwrap()),
-            WritableGpr::from_reg(Gpr::new(regs::rdx()).unwrap()),
+            Gpr::unwrap_new(regs::rax()),
+            Gpr::unwrap_new(regs::rdx()),
+            WritableGpr::from_reg(Gpr::unwrap_new(regs::rax())),
+            WritableGpr::from_reg(Gpr::unwrap_new(regs::rdx())),
         ),
         "F7FE",
         "idiv    %eax, %edx, %esi, %eax, %edx ; trap=int_divz",
@@ -1956,10 +1896,10 @@ fn test_x64_emit() {
             DivSignedness::Signed,
             TrapCode::IntegerDivisionByZero,
             RegMem::reg(regs::r15()),
-            Gpr::new(regs::rax()).unwrap(),
-            Gpr::new(regs::rdx()).unwrap(),
-            WritableGpr::from_reg(Gpr::new(regs::rax()).unwrap()),
-            WritableGpr::from_reg(Gpr::new(regs::rdx()).unwrap()),
+            Gpr::unwrap_new(regs::rax()),
+            Gpr::unwrap_new(regs::rdx()),
+            WritableGpr::from_reg(Gpr::unwrap_new(regs::rax())),
+            WritableGpr::from_reg(Gpr::unwrap_new(regs::rdx())),
         ),
         "49F7FF",
         "idiv    %rax, %rdx, %r15, %rax, %rdx ; trap=int_divz",
@@ -1970,10 +1910,10 @@ fn test_x64_emit() {
             DivSignedness::Unsigned,
             TrapCode::IntegerDivisionByZero,
             RegMem::reg(regs::r14()),
-            Gpr::new(regs::rax()).unwrap(),
-            Gpr::new(regs::rdx()).unwrap(),
-            WritableGpr::from_reg(Gpr::new(regs::rax()).unwrap()),
-            WritableGpr::from_reg(Gpr::new(regs::rdx()).unwrap()),
+            Gpr::unwrap_new(regs::rax()),
+            Gpr::unwrap_new(regs::rdx()),
+            WritableGpr::from_reg(Gpr::unwrap_new(regs::rax())),
+            WritableGpr::from_reg(Gpr::unwrap_new(regs::rdx())),
         ),
         "41F7F6",
         "div     %eax, %edx, %r14d, %eax, %edx ; trap=int_divz",
@@ -1984,10 +1924,10 @@ fn test_x64_emit() {
             DivSignedness::Unsigned,
             TrapCode::IntegerDivisionByZero,
             RegMem::reg(regs::rdi()),
-            Gpr::new(regs::rax()).unwrap(),
-            Gpr::new(regs::rdx()).unwrap(),
-            WritableGpr::from_reg(Gpr::new(regs::rax()).unwrap()),
-            WritableGpr::from_reg(Gpr::new(regs::rdx()).unwrap()),
+            Gpr::unwrap_new(regs::rax()),
+            Gpr::unwrap_new(regs::rdx()),
+            WritableGpr::from_reg(Gpr::unwrap_new(regs::rax())),
+            WritableGpr::from_reg(Gpr::unwrap_new(regs::rdx())),
         ),
         "48F7F7",
         "div     %rax, %rdx, %rdi, %rax, %rdx ; trap=int_divz",
@@ -1997,8 +1937,8 @@ fn test_x64_emit() {
             DivSignedness::Unsigned,
             TrapCode::IntegerDivisionByZero,
             RegMem::reg(regs::rax()),
-            Gpr::new(regs::rax()).unwrap(),
-            WritableGpr::from_reg(Gpr::new(regs::rax()).unwrap()),
+            Gpr::unwrap_new(regs::rax()),
+            WritableGpr::from_reg(Gpr::unwrap_new(regs::rax())),
         ),
         "F6F0",
         "div     %al, %al, %al ; trap=int_divz",
@@ -2008,8 +1948,8 @@ fn test_x64_emit() {
             DivSignedness::Unsigned,
             TrapCode::IntegerDivisionByZero,
             RegMem::reg(regs::rsi()),
-            Gpr::new(regs::rax()).unwrap(),
-            WritableGpr::from_reg(Gpr::new(regs::rax()).unwrap()),
+            Gpr::unwrap_new(regs::rax()),
+            WritableGpr::from_reg(Gpr::unwrap_new(regs::rax())),
         ),
         "40F6F6",
         "div     %al, %sil, %al ; trap=int_divz",
@@ -2259,7 +2199,7 @@ fn test_x64_emit() {
     // Mov64_M_R
     insns.push((
         Inst::mov64_m_r(
-            Amode::imm_reg_reg_shift(179, Gpr::new(rax).unwrap(), Gpr::new(rbx).unwrap(), 0),
+            Amode::imm_reg_reg_shift(179, Gpr::unwrap_new(rax), Gpr::unwrap_new(rbx), 0),
             w_rcx,
         ),
         "488B8C18B3000000",
@@ -2267,7 +2207,7 @@ fn test_x64_emit() {
     ));
     insns.push((
         Inst::mov64_m_r(
-            Amode::imm_reg_reg_shift(179, Gpr::new(rax).unwrap(), Gpr::new(rbx).unwrap(), 0),
+            Amode::imm_reg_reg_shift(179, Gpr::unwrap_new(rax), Gpr::unwrap_new(rbx), 0),
             w_r8,
         ),
         "4C8B8418B3000000",
@@ -2275,7 +2215,7 @@ fn test_x64_emit() {
     ));
     insns.push((
         Inst::mov64_m_r(
-            Amode::imm_reg_reg_shift(179, Gpr::new(rax).unwrap(), Gpr::new(r9).unwrap(), 0),
+            Amode::imm_reg_reg_shift(179, Gpr::unwrap_new(rax), Gpr::unwrap_new(r9), 0),
             w_rcx,
         ),
         "4A8B8C08B3000000",
@@ -2283,7 +2223,7 @@ fn test_x64_emit() {
     ));
     insns.push((
         Inst::mov64_m_r(
-            Amode::imm_reg_reg_shift(179, Gpr::new(rax).unwrap(), Gpr::new(r9).unwrap(), 0),
+            Amode::imm_reg_reg_shift(179, Gpr::unwrap_new(rax), Gpr::unwrap_new(r9), 0),
             w_r8,
         ),
         "4E8B8408B3000000",
@@ -2291,7 +2231,7 @@ fn test_x64_emit() {
     ));
     insns.push((
         Inst::mov64_m_r(
-            Amode::imm_reg_reg_shift(179, Gpr::new(r10).unwrap(), Gpr::new(rbx).unwrap(), 0),
+            Amode::imm_reg_reg_shift(179, Gpr::unwrap_new(r10), Gpr::unwrap_new(rbx), 0),
             w_rcx,
         ),
         "498B8C1AB3000000",
@@ -2299,7 +2239,7 @@ fn test_x64_emit() {
     ));
     insns.push((
         Inst::mov64_m_r(
-            Amode::imm_reg_reg_shift(179, Gpr::new(r10).unwrap(), Gpr::new(rbx).unwrap(), 0),
+            Amode::imm_reg_reg_shift(179, Gpr::unwrap_new(r10), Gpr::unwrap_new(rbx), 0),
             w_r8,
         ),
         "4D8B841AB3000000",
@@ -2307,7 +2247,7 @@ fn test_x64_emit() {
     ));
     insns.push((
         Inst::mov64_m_r(
-            Amode::imm_reg_reg_shift(179, Gpr::new(r10).unwrap(), Gpr::new(r9).unwrap(), 0),
+            Amode::imm_reg_reg_shift(179, Gpr::unwrap_new(r10), Gpr::unwrap_new(r9), 0),
             w_rcx,
         ),
         "4B8B8C0AB3000000",
@@ -2315,7 +2255,7 @@ fn test_x64_emit() {
     ));
     insns.push((
         Inst::mov64_m_r(
-            Amode::imm_reg_reg_shift(179, Gpr::new(r10).unwrap(), Gpr::new(r9).unwrap(), 0),
+            Amode::imm_reg_reg_shift(179, Gpr::unwrap_new(r10), Gpr::unwrap_new(r9), 0),
             w_r8,
         ),
         "4F8B840AB3000000",
@@ -2336,7 +2276,7 @@ fn test_x64_emit() {
     ));
     insns.push((
         Inst::lea(
-            Amode::imm_reg_reg_shift(179, Gpr::new(r10).unwrap(), Gpr::new(r9).unwrap(), 0),
+            Amode::imm_reg_reg_shift(179, Gpr::unwrap_new(r10), Gpr::unwrap_new(r9), 0),
             w_r8,
         ),
         "4F8D840AB3000000",
@@ -2963,7 +2903,7 @@ fn test_x64_emit() {
         Inst::shift_r(
             OperandSize::Size32,
             ShiftKind::ShiftLeft,
-            Imm8Gpr::new(Imm8Reg::Reg { reg: regs::rcx() }).unwrap(),
+            Imm8Gpr::unwrap_new(Imm8Reg::Reg { reg: regs::rcx() }),
             rdi,
             w_rdi,
         ),
@@ -2974,7 +2914,7 @@ fn test_x64_emit() {
         Inst::shift_r(
             OperandSize::Size32,
             ShiftKind::ShiftLeft,
-            Imm8Gpr::new(Imm8Reg::Reg { reg: regs::rcx() }).unwrap(),
+            Imm8Gpr::unwrap_new(Imm8Reg::Reg { reg: regs::rcx() }),
             r12,
             w_r12,
         ),
@@ -2985,7 +2925,7 @@ fn test_x64_emit() {
         Inst::shift_r(
             OperandSize::Size32,
             ShiftKind::ShiftLeft,
-            Imm8Gpr::new(Imm8Reg::Imm8 { imm: 2 }).unwrap(),
+            Imm8Gpr::unwrap_new(Imm8Reg::Imm8 { imm: 2 }),
             r8,
             w_r8,
         ),
@@ -2996,7 +2936,7 @@ fn test_x64_emit() {
         Inst::shift_r(
             OperandSize::Size32,
             ShiftKind::ShiftLeft,
-            Imm8Gpr::new(Imm8Reg::Imm8 { imm: 31 }).unwrap(),
+            Imm8Gpr::unwrap_new(Imm8Reg::Imm8 { imm: 31 }),
             r13,
             w_r13,
         ),
@@ -3007,7 +2947,7 @@ fn test_x64_emit() {
         Inst::shift_r(
             OperandSize::Size64,
             ShiftKind::ShiftLeft,
-            Imm8Gpr::new(Imm8Reg::Reg { reg: regs::rcx() }).unwrap(),
+            Imm8Gpr::unwrap_new(Imm8Reg::Reg { reg: regs::rcx() }),
             r13,
             w_r13,
         ),
@@ -3018,7 +2958,7 @@ fn test_x64_emit() {
         Inst::shift_r(
             OperandSize::Size64,
             ShiftKind::ShiftLeft,
-            Imm8Gpr::new(Imm8Reg::Reg { reg: regs::rcx() }).unwrap(),
+            Imm8Gpr::unwrap_new(Imm8Reg::Reg { reg: regs::rcx() }),
             rdi,
             w_rdi,
         ),
@@ -3029,7 +2969,7 @@ fn test_x64_emit() {
         Inst::shift_r(
             OperandSize::Size64,
             ShiftKind::ShiftLeft,
-            Imm8Gpr::new(Imm8Reg::Imm8 { imm: 2 }).unwrap(),
+            Imm8Gpr::unwrap_new(Imm8Reg::Imm8 { imm: 2 }),
             r8,
             w_r8,
         ),
@@ -3040,7 +2980,7 @@ fn test_x64_emit() {
         Inst::shift_r(
             OperandSize::Size64,
             ShiftKind::ShiftLeft,
-            Imm8Gpr::new(Imm8Reg::Imm8 { imm: 3 }).unwrap(),
+            Imm8Gpr::unwrap_new(Imm8Reg::Imm8 { imm: 3 }),
             rbx,
             w_rbx,
         ),
@@ -3051,7 +2991,7 @@ fn test_x64_emit() {
         Inst::shift_r(
             OperandSize::Size64,
             ShiftKind::ShiftLeft,
-            Imm8Gpr::new(Imm8Reg::Imm8 { imm: 63 }).unwrap(),
+            Imm8Gpr::unwrap_new(Imm8Reg::Imm8 { imm: 63 }),
             r13,
             w_r13,
         ),
@@ -3062,7 +3002,7 @@ fn test_x64_emit() {
         Inst::shift_r(
             OperandSize::Size32,
             ShiftKind::ShiftRightLogical,
-            Imm8Gpr::new(Imm8Reg::Reg { reg: regs::rcx() }).unwrap(),
+            Imm8Gpr::unwrap_new(Imm8Reg::Reg { reg: regs::rcx() }),
             rdi,
             w_rdi,
         ),
@@ -3073,7 +3013,7 @@ fn test_x64_emit() {
         Inst::shift_r(
             OperandSize::Size32,
             ShiftKind::ShiftRightLogical,
-            Imm8Gpr::new(Imm8Reg::Imm8 { imm: 2 }).unwrap(),
+            Imm8Gpr::unwrap_new(Imm8Reg::Imm8 { imm: 2 }),
             r8,
             w_r8,
         ),
@@ -3084,7 +3024,7 @@ fn test_x64_emit() {
         Inst::shift_r(
             OperandSize::Size32,
             ShiftKind::ShiftRightLogical,
-            Imm8Gpr::new(Imm8Reg::Imm8 { imm: 31 }).unwrap(),
+            Imm8Gpr::unwrap_new(Imm8Reg::Imm8 { imm: 31 }),
             r13,
             w_r13,
         ),
@@ -3095,7 +3035,7 @@ fn test_x64_emit() {
         Inst::shift_r(
             OperandSize::Size64,
             ShiftKind::ShiftRightLogical,
-            Imm8Gpr::new(Imm8Reg::Reg { reg: regs::rcx() }).unwrap(),
+            Imm8Gpr::unwrap_new(Imm8Reg::Reg { reg: regs::rcx() }),
             rdi,
             w_rdi,
         ),
@@ -3106,7 +3046,7 @@ fn test_x64_emit() {
         Inst::shift_r(
             OperandSize::Size64,
             ShiftKind::ShiftRightLogical,
-            Imm8Gpr::new(Imm8Reg::Imm8 { imm: 2 }).unwrap(),
+            Imm8Gpr::unwrap_new(Imm8Reg::Imm8 { imm: 2 }),
             r8,
             w_r8,
         ),
@@ -3117,7 +3057,7 @@ fn test_x64_emit() {
         Inst::shift_r(
             OperandSize::Size64,
             ShiftKind::ShiftRightLogical,
-            Imm8Gpr::new(Imm8Reg::Imm8 { imm: 63 }).unwrap(),
+            Imm8Gpr::unwrap_new(Imm8Reg::Imm8 { imm: 63 }),
             r13,
             w_r13,
         ),
@@ -3128,7 +3068,7 @@ fn test_x64_emit() {
         Inst::shift_r(
             OperandSize::Size32,
             ShiftKind::ShiftRightArithmetic,
-            Imm8Gpr::new(Imm8Reg::Reg { reg: regs::rcx() }).unwrap(),
+            Imm8Gpr::unwrap_new(Imm8Reg::Reg { reg: regs::rcx() }),
             rdi,
             w_rdi,
         ),
@@ -3139,7 +3079,7 @@ fn test_x64_emit() {
         Inst::shift_r(
             OperandSize::Size32,
             ShiftKind::ShiftRightArithmetic,
-            Imm8Gpr::new(Imm8Reg::Imm8 { imm: 2 }).unwrap(),
+            Imm8Gpr::unwrap_new(Imm8Reg::Imm8 { imm: 2 }),
             r8,
             w_r8,
         ),
@@ -3150,7 +3090,7 @@ fn test_x64_emit() {
         Inst::shift_r(
             OperandSize::Size32,
             ShiftKind::ShiftRightArithmetic,
-            Imm8Gpr::new(Imm8Reg::Imm8 { imm: 31 }).unwrap(),
+            Imm8Gpr::unwrap_new(Imm8Reg::Imm8 { imm: 31 }),
             r13,
             w_r13,
         ),
@@ -3161,7 +3101,7 @@ fn test_x64_emit() {
         Inst::shift_r(
             OperandSize::Size64,
             ShiftKind::ShiftRightArithmetic,
-            Imm8Gpr::new(Imm8Reg::Reg { reg: regs::rcx() }).unwrap(),
+            Imm8Gpr::unwrap_new(Imm8Reg::Reg { reg: regs::rcx() }),
             rdi,
             w_rdi,
         ),
@@ -3172,7 +3112,7 @@ fn test_x64_emit() {
         Inst::shift_r(
             OperandSize::Size64,
             ShiftKind::ShiftRightArithmetic,
-            Imm8Gpr::new(Imm8Reg::Imm8 { imm: 2 }).unwrap(),
+            Imm8Gpr::unwrap_new(Imm8Reg::Imm8 { imm: 2 }),
             r8,
             w_r8,
         ),
@@ -3183,7 +3123,7 @@ fn test_x64_emit() {
         Inst::shift_r(
             OperandSize::Size64,
             ShiftKind::ShiftRightArithmetic,
-            Imm8Gpr::new(Imm8Reg::Imm8 { imm: 63 }).unwrap(),
+            Imm8Gpr::unwrap_new(Imm8Reg::Imm8 { imm: 63 }),
             r13,
             w_r13,
         ),
@@ -3194,7 +3134,7 @@ fn test_x64_emit() {
         Inst::shift_r(
             OperandSize::Size64,
             ShiftKind::RotateLeft,
-            Imm8Gpr::new(Imm8Reg::Reg { reg: regs::rcx() }).unwrap(),
+            Imm8Gpr::unwrap_new(Imm8Reg::Reg { reg: regs::rcx() }),
             r8,
             w_r8,
         ),
@@ -3205,7 +3145,7 @@ fn test_x64_emit() {
         Inst::shift_r(
             OperandSize::Size32,
             ShiftKind::RotateLeft,
-            Imm8Gpr::new(Imm8Reg::Imm8 { imm: 3 }).unwrap(),
+            Imm8Gpr::unwrap_new(Imm8Reg::Imm8 { imm: 3 }),
             r9,
             w_r9,
         ),
@@ -3216,7 +3156,7 @@ fn test_x64_emit() {
         Inst::shift_r(
             OperandSize::Size32,
             ShiftKind::RotateRight,
-            Imm8Gpr::new(Imm8Reg::Reg { reg: regs::rcx() }).unwrap(),
+            Imm8Gpr::unwrap_new(Imm8Reg::Reg { reg: regs::rcx() }),
             rsi,
             w_rsi,
         ),
@@ -3227,7 +3167,7 @@ fn test_x64_emit() {
         Inst::shift_r(
             OperandSize::Size64,
             ShiftKind::RotateRight,
-            Imm8Gpr::new(Imm8Reg::Imm8 { imm: 5 }).unwrap(),
+            Imm8Gpr::unwrap_new(Imm8Reg::Imm8 { imm: 5 }),
             r15,
             w_r15,
         ),
@@ -3238,7 +3178,7 @@ fn test_x64_emit() {
         Inst::shift_r(
             OperandSize::Size8,
             ShiftKind::RotateRight,
-            Imm8Gpr::new(Imm8Reg::Reg { reg: regs::rcx() }).unwrap(),
+            Imm8Gpr::unwrap_new(Imm8Reg::Reg { reg: regs::rcx() }),
             rsi,
             w_rsi,
         ),
@@ -3249,7 +3189,7 @@ fn test_x64_emit() {
         Inst::shift_r(
             OperandSize::Size8,
             ShiftKind::RotateRight,
-            Imm8Gpr::new(Imm8Reg::Reg { reg: regs::rcx() }).unwrap(),
+            Imm8Gpr::unwrap_new(Imm8Reg::Reg { reg: regs::rcx() }),
             rax,
             w_rax,
         ),
@@ -3260,7 +3200,7 @@ fn test_x64_emit() {
         Inst::shift_r(
             OperandSize::Size8,
             ShiftKind::RotateRight,
-            Imm8Gpr::new(Imm8Reg::Imm8 { imm: 5 }).unwrap(),
+            Imm8Gpr::unwrap_new(Imm8Reg::Imm8 { imm: 5 }),
             r15,
             w_r15,
         ),
@@ -3271,7 +3211,7 @@ fn test_x64_emit() {
         Inst::shift_r(
             OperandSize::Size16,
             ShiftKind::RotateRight,
-            Imm8Gpr::new(Imm8Reg::Reg { reg: regs::rcx() }).unwrap(),
+            Imm8Gpr::unwrap_new(Imm8Reg::Reg { reg: regs::rcx() }),
             rsi,
             w_rsi,
         ),
@@ -3282,7 +3222,7 @@ fn test_x64_emit() {
         Inst::shift_r(
             OperandSize::Size16,
             ShiftKind::RotateRight,
-            Imm8Gpr::new(Imm8Reg::Imm8 { imm: 5 }).unwrap(),
+            Imm8Gpr::unwrap_new(Imm8Reg::Imm8 { imm: 5 }),
             r15,
             w_r15,
         ),
@@ -3685,8 +3625,8 @@ fn test_x64_emit() {
             CC::NO,
             RegMem::mem(Amode::imm_reg_reg_shift(
                 37,
-                Gpr::new(rdi).unwrap(),
-                Gpr::new(rsi).unwrap(),
+                Gpr::unwrap_new(rdi),
+                Gpr::unwrap_new(rsi),
                 2,
             )),
             w_r15,
@@ -3732,8 +3672,8 @@ fn test_x64_emit() {
     insns.push((
         Inst::push64(RegMemImm::mem(Amode::imm_reg_reg_shift(
             321,
-            Gpr::new(rsi).unwrap(),
-            Gpr::new(rcx).unwrap(),
+            Gpr::unwrap_new(rsi),
+            Gpr::unwrap_new(rcx),
             3,
         ))),
         "FFB4CE41010000",
@@ -3742,8 +3682,8 @@ fn test_x64_emit() {
     insns.push((
         Inst::push64(RegMemImm::mem(Amode::imm_reg_reg_shift(
             321,
-            Gpr::new(r9).unwrap(),
-            Gpr::new(rbx).unwrap(),
+            Gpr::unwrap_new(r9),
+            Gpr::unwrap_new(rbx),
             2,
         ))),
         "41FFB49941010000",
@@ -3817,8 +3757,8 @@ fn test_x64_emit() {
     insns.push((
         call_unknown(RegMem::mem(Amode::imm_reg_reg_shift(
             321,
-            Gpr::new(rsi).unwrap(),
-            Gpr::new(rcx).unwrap(),
+            Gpr::unwrap_new(rsi),
+            Gpr::unwrap_new(rcx),
             3,
         ))),
         "FF94CE41010000",
@@ -3827,8 +3767,8 @@ fn test_x64_emit() {
     insns.push((
         call_unknown(RegMem::mem(Amode::imm_reg_reg_shift(
             321,
-            Gpr::new(r10).unwrap(),
-            Gpr::new(rdx).unwrap(),
+            Gpr::unwrap_new(r10),
+            Gpr::unwrap_new(rdx),
             2,
         ))),
         "41FF949241010000",
@@ -3897,8 +3837,8 @@ fn test_x64_emit() {
     insns.push((
         Inst::jmp_unknown(RegMem::mem(Amode::imm_reg_reg_shift(
             321,
-            Gpr::new(rsi).unwrap(),
-            Gpr::new(rcx).unwrap(),
+            Gpr::unwrap_new(rsi),
+            Gpr::unwrap_new(rcx),
             3,
         ))),
         "FFA4CE41010000",
@@ -3907,8 +3847,8 @@ fn test_x64_emit() {
     insns.push((
         Inst::jmp_unknown(RegMem::mem(Amode::imm_reg_reg_shift(
             321,
-            Gpr::new(r10).unwrap(),
-            Gpr::new(rdx).unwrap(),
+            Gpr::unwrap_new(r10),
+            Gpr::unwrap_new(rdx),
             2,
         ))),
         "41FFA49241010000",
@@ -3987,8 +3927,8 @@ fn test_x64_emit() {
             SseOpcode::Addss,
             RegMem::mem(Amode::imm_reg_reg_shift(
                 123,
-                Gpr::new(r10).unwrap(),
-                Gpr::new(rdx).unwrap(),
+                Gpr::unwrap_new(r10),
+                Gpr::unwrap_new(rdx),
                 2,
             )),
             w_xmm0,
@@ -4017,8 +3957,8 @@ fn test_x64_emit() {
             SseOpcode::Subss,
             RegMem::mem(Amode::imm_reg_reg_shift(
                 321,
-                Gpr::new(r10).unwrap(),
-                Gpr::new(rax).unwrap(),
+                Gpr::unwrap_new(r10),
+                Gpr::unwrap_new(rax),
                 3,
             )),
             w_xmm10,
@@ -4820,9 +4760,9 @@ fn test_x64_emit() {
     insns.push((
         Inst::XmmRmiRVex {
             op: AvxOpcode::Vpmaxub,
-            dst: Writable::from_reg(Xmm::new(xmm13).unwrap()),
-            src1: Xmm::new(xmm1).unwrap(),
-            src2: XmmMemImm::new(xmm12.into()).unwrap(),
+            dst: Writable::from_reg(Xmm::unwrap_new(xmm13)),
+            src1: Xmm::unwrap_new(xmm1),
+            src2: XmmMemImm::unwrap_new(xmm12.into()),
         },
         "C44171DEEC",
         "vpmaxub %xmm1, %xmm12, %xmm13",
@@ -4832,17 +4772,16 @@ fn test_x64_emit() {
     insns.push((
         Inst::XmmRmiRVex {
             op: AvxOpcode::Vpmaxub,
-            dst: Writable::from_reg(Xmm::new(xmm13).unwrap()),
-            src1: Xmm::new(xmm1).unwrap(),
-            src2: XmmMemImm::new(RegMemImm::Mem {
+            dst: Writable::from_reg(Xmm::unwrap_new(xmm13)),
+            src1: Xmm::unwrap_new(xmm1),
+            src2: XmmMemImm::unwrap_new(RegMemImm::Mem {
                 addr: Amode::ImmReg {
                     simm32: 10,
                     base: rax,
                     flags: MemFlags::trusted(),
                 }
                 .into(),
-            })
-            .unwrap(),
+            }),
         },
         "C571DE680A",
         "vpmaxub %xmm1, 10(%rax), %xmm13",
@@ -4852,9 +4791,9 @@ fn test_x64_emit() {
     insns.push((
         Inst::XmmRmiRVex {
             op: AvxOpcode::Vpsrlw,
-            dst: Writable::from_reg(Xmm::new(xmm13).unwrap()),
-            src1: Xmm::new(xmm1).unwrap(),
-            src2: XmmMemImm::new(RegMemImm::Imm { simm32: 36 }).unwrap(),
+            dst: Writable::from_reg(Xmm::unwrap_new(xmm13)),
+            src1: Xmm::unwrap_new(xmm1),
+            src2: XmmMemImm::unwrap_new(RegMemImm::Imm { simm32: 36 }),
         },
         "C59171D124",
         "vpsrlw  %xmm1, $36, %xmm13",
@@ -4866,9 +4805,9 @@ fn test_x64_emit() {
     insns.push((
         Inst::XmmRmiRVex {
             op: AvxOpcode::Vmulsd,
-            dst: Writable::from_reg(Xmm::new(xmm13).unwrap()),
-            src1: Xmm::new(xmm1).unwrap(),
-            src2: XmmMemImm::new(xmm12.into()).unwrap(),
+            dst: Writable::from_reg(Xmm::unwrap_new(xmm13)),
+            src1: Xmm::unwrap_new(xmm1),
+            src2: XmmMemImm::unwrap_new(xmm12.into()),
         },
         "C51B59E9",
         "vmulsd  %xmm1, %xmm12, %xmm13",
@@ -4876,9 +4815,9 @@ fn test_x64_emit() {
     insns.push((
         Inst::XmmRmiRVex {
             op: AvxOpcode::Vmulsd,
-            dst: Writable::from_reg(Xmm::new(xmm13).unwrap()),
-            src1: Xmm::new(xmm12).unwrap(),
-            src2: XmmMemImm::new(xmm1.into()).unwrap(),
+            dst: Writable::from_reg(Xmm::unwrap_new(xmm13)),
+            src1: Xmm::unwrap_new(xmm12),
+            src2: XmmMemImm::unwrap_new(xmm1.into()),
         },
         "C51B59E9",
         "vmulsd  %xmm12, %xmm1, %xmm13",
@@ -4889,9 +4828,9 @@ fn test_x64_emit() {
     insns.push((
         Inst::XmmVexPinsr {
             op: AvxOpcode::Vpinsrb,
-            dst: Writable::from_reg(Xmm::new(xmm13).unwrap()),
-            src1: Xmm::new(xmm14).unwrap(),
-            src2: GprMem::new(RegMem::reg(r15)).unwrap(),
+            dst: Writable::from_reg(Xmm::unwrap_new(xmm13)),
+            src1: Xmm::unwrap_new(xmm14),
+            src2: GprMem::unwrap_new(RegMem::reg(r15)),
             imm: 2,
         },
         "C4430920EF02",
@@ -4901,12 +4840,11 @@ fn test_x64_emit() {
     // ========================================================
     // Pertaining to atomics.
     let am1: SyntheticAmode =
-        Amode::imm_reg_reg_shift(321, Gpr::new(r10).unwrap(), Gpr::new(rdx).unwrap(), 2).into();
+        Amode::imm_reg_reg_shift(321, Gpr::unwrap_new(r10), Gpr::unwrap_new(rdx), 2).into();
     // `am2` doesn't contribute any 1 bits to the rex prefix, so we must use it when testing
     // for retention of the apparently-redundant rex prefix in the 8-bit case.
     let am2: SyntheticAmode =
-        Amode::imm_reg_reg_shift(-12345i32, Gpr::new(rcx).unwrap(), Gpr::new(rsi).unwrap(), 3)
-            .into();
+        Amode::imm_reg_reg_shift(-12345i32, Gpr::unwrap_new(rcx), Gpr::unwrap_new(rsi), 3).into();
     // Use `r9` with a 0 offset.
     let am3: SyntheticAmode = Amode::imm_reg(0, r9).into();
 
diff --git a/cranelift/filetests/filetests/isa/x64/sink-load-store-of-bitwise-op-on-float.clif b/cranelift/filetests/filetests/isa/x64/sink-load-store-of-bitwise-op-on-float.clif
new file mode 100644
index 000000000..085868bda
--- /dev/null
+++ b/cranelift/filetests/filetests/isa/x64/sink-load-store-of-bitwise-op-on-float.clif
@@ -0,0 +1,177 @@
+test compile precise-output
+target x86_64
+
+function %bor0(i64, f32) {
+block0(v0: i64, v1: f32):
+    v2 = load.f32 v0
+    v3 = bor v1, v2
+    store v3, v0
+    return
+}
+
+; VCode:
+;   pushq   %rbp
+;   movq    %rsp, %rbp
+; block0:
+;   movd    %xmm0, %ecx
+;   orl     %ecx, 0(%rdi)
+;   movq    %rbp, %rsp
+;   popq    %rbp
+;   ret
+;
+; Disassembled:
+; block0: ; offset 0x0
+;   pushq %rbp
+;   movq %rsp, %rbp
+; block1: ; offset 0x4
+;   movd %xmm0, %ecx
+;   orl %ecx, (%rdi) ; trap: heap_oob
+;   movq %rbp, %rsp
+;   popq %rbp
+;   retq
+
+function %bor1(i64, f32) {
+block0(v0: i64, v1: f32):
+    v2 = load.f32 v0
+    v3 = bor v2, v1
+    store v3, v0
+    return
+}
+
+; VCode:
+;   pushq   %rbp
+;   movq    %rsp, %rbp
+; block0:
+;   movd    %xmm0, %ecx
+;   orl     %ecx, 0(%rdi)
+;   movq    %rbp, %rsp
+;   popq    %rbp
+;   ret
+;
+; Disassembled:
+; block0: ; offset 0x0
+;   pushq %rbp
+;   movq %rsp, %rbp
+; block1: ; offset 0x4
+;   movd %xmm0, %ecx
+;   orl %ecx, (%rdi) ; trap: heap_oob
+;   movq %rbp, %rsp
+;   popq %rbp
+;   retq
+
+function %band0(i64, f32) {
+block0(v0: i64, v1: f32):
+    v2 = load.f32 v0
+    v3 = band v1, v2
+    store v3, v0
+    return
+}
+
+; VCode:
+;   pushq   %rbp
+;   movq    %rsp, %rbp
+; block0:
+;   movd    %xmm0, %ecx
+;   andl    %ecx, 0(%rdi)
+;   movq    %rbp, %rsp
+;   popq    %rbp
+;   ret
+;
+; Disassembled:
+; block0: ; offset 0x0
+;   pushq %rbp
+;   movq %rsp, %rbp
+; block1: ; offset 0x4
+;   movd %xmm0, %ecx
+;   andl %ecx, (%rdi) ; trap: heap_oob
+;   movq %rbp, %rsp
+;   popq %rbp
+;   retq
+
+function %band1(i64, f32) {
+block0(v0: i64, v1: f32):
+    v2 = load.f32 v0
+    v3 = band v2, v1
+    store v3, v0
+    return
+}
+
+; VCode:
+;   pushq   %rbp
+;   movq    %rsp, %rbp
+; block0:
+;   movd    %xmm0, %ecx
+;   andl    %ecx, 0(%rdi)
+;   movq    %rbp, %rsp
+;   popq    %rbp
+;   ret
+;
+; Disassembled:
+; block0: ; offset 0x0
+;   pushq %rbp
+;   movq %rsp, %rbp
+; block1: ; offset 0x4
+;   movd %xmm0, %ecx
+;   andl %ecx, (%rdi) ; trap: heap_oob
+;   movq %rbp, %rsp
+;   popq %rbp
+;   retq
+
+function %bxor0(i64, f32) {
+block0(v0: i64, v1: f32):
+    v2 = load.f32 v0
+    v3 = bxor v1, v2
+    store v3, v0
+    return
+}
+
+; VCode:
+;   pushq   %rbp
+;   movq    %rsp, %rbp
+; block0:
+;   movd    %xmm0, %ecx
+;   xorl    %ecx, 0(%rdi)
+;   movq    %rbp, %rsp
+;   popq    %rbp
+;   ret
+;
+; Disassembled:
+; block0: ; offset 0x0
+;   pushq %rbp
+;   movq %rsp, %rbp
+; block1: ; offset 0x4
+;   movd %xmm0, %ecx
+;   xorl %ecx, (%rdi) ; trap: heap_oob
+;   movq %rbp, %rsp
+;   popq %rbp
+;   retq
+
+function %bxor1(i64, f32) {
+block0(v0: i64, v1: f32):
+    v2 = load.f32 v0
+    v3 = bxor v2, v1
+    store v3, v0
+    return
+}
+
+; VCode:
+;   pushq   %rbp
+;   movq    %rsp, %rbp
+; block0:
+;   movd    %xmm0, %ecx
+;   xorl    %ecx, 0(%rdi)
+;   movq    %rbp, %rsp
+;   popq    %rbp
+;   ret
+;
+; Disassembled:
+; block0: ; offset 0x0
+;   pushq %rbp
+;   movq %rsp, %rbp
+; block1: ; offset 0x4
+;   movd %xmm0, %ecx
+;   xorl %ecx, (%rdi) ; trap: heap_oob
+;   movq %rbp, %rsp
+;   popq %rbp
+;   retq
+
diff --git a/crates/test-programs/artifacts/build.rs b/crates/test-programs/artifacts/build.rs
index ea47fd4cc..69fbe56b7 100644
--- a/crates/test-programs/artifacts/build.rs
+++ b/crates/test-programs/artifacts/build.rs
@@ -30,7 +30,7 @@ fn build_and_generate_tests() {
     // Build the test programs:
     let mut cmd = cargo();
     cmd.arg("build")
-        .arg("--target=wasm32-wasi")
+        .arg("--target=wasm32-wasip1")
         .arg("--package=test-programs")
         .env("CARGO_TARGET_DIR", &out_dir)
         .env("CARGO_PROFILE_DEV_DEBUG", "2")
@@ -59,7 +59,7 @@ fn build_and_generate_tests() {
     for target in targets {
         let camel = target.to_shouty_snake_case();
         let wasm = out_dir
-            .join("wasm32-wasi")
+            .join("wasm32-wasip1")
             .join("debug")
             .join(format!("{target}.wasm"));
 
diff --git a/docs/contributing-testing.md b/docs/contributing-testing.md
index acb8841d9..c8cff4347 100644
--- a/docs/contributing-testing.md
+++ b/docs/contributing-testing.md
@@ -8,13 +8,13 @@ can't build it!
 
 ## Installing `wasm32` Targets
 
-To compile the tests, you'll need the `wasm32-wasi` and
+To compile the tests, you'll need the `wasm32-wasip1` and
 `wasm32-unknown-unknown` targets installed, which, assuming you're using
 [rustup.rs](https://rustup.rs) to manage your Rust versions, can be done as
 follows:
 
 ```shell
-rustup target add wasm32-wasi wasm32-unknown-unknown
+rustup target add wasm32-wasip1 wasm32-unknown-unknown
 ```
 
 ## Running All Tests
diff --git a/pulley/tests/all/disas.rs b/pulley/tests/all/disas.rs
new file mode 100644
index 000000000..b91ecf224
--- /dev/null
+++ b/pulley/tests/all/disas.rs
@@ -0,0 +1,101 @@
+//! Disassembly tests.
+
+use pulley_interpreter::*;
+
+fn encoded(ops: &[Op]) -> Vec<u8> {
+    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 000000000..132d84ef0
--- /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<u8> {
+    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<R0, R1>(
+    xs: impl IntoIterator<Item = (R0, u64)>,
+    op: impl Into<Op> + Debug,
+    result: R1,
+    expected: u64,
+) where
+    R0: Into<AnyReg>,
+    R1: Into<AnyReg>,
+{
+    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 000000000..38e113a17
--- /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/tests/all/call_hook.rs b/tests/all/call_hook.rs
index 4be45fcc7..ef565f626 100644
--- a/tests/all/call_hook.rs
+++ b/tests/all/call_hook.rs
@@ -748,7 +748,7 @@ async fn drop_suspended_async_hook() -> Result<(), Error> {
             assert_eq!(*state, 0);
             *state += 1;
             let _dec = Decrement(state);
-            loop {
+            for _ in 0.. {
                 tokio::task::yield_now().await;
             }
         })
diff --git a/tests/all/func.rs b/tests/all/func.rs
index a29decf8d..842045c11 100644
--- a/tests/all/func.rs
+++ b/tests/all/func.rs
@@ -690,6 +690,7 @@ fn import_works() -> Result<()> {
 }
 
 #[test]
+#[cfg_attr(miri, ignore)]
 fn trap_smoke() -> Result<()> {
     let mut store = Store::<()>::default();
     let f = Func::wrap(&mut store, || -> Result<()> { bail!("test") });
diff --git a/tests/all/host_funcs.rs b/tests/all/host_funcs.rs
index f16d129ff..8a61c74a0 100644
--- a/tests/all/host_funcs.rs
+++ b/tests/all/host_funcs.rs
@@ -454,6 +454,7 @@ fn call_wasm_many_args() -> Result<()> {
 }
 
 #[test]
+#[cfg_attr(miri, ignore)]
 fn trap_smoke() -> Result<()> {
     let engine = Engine::default();
     let mut linker = Linker::<()>::new(&engine);
diff --git a/tests/all/traps.rs b/tests/all/traps.rs
index 7c356adad..e9b7ec41a 100644
--- a/tests/all/traps.rs
+++ b/tests/all/traps.rs
@@ -406,7 +406,7 @@ fn rust_panic_import() -> Result<()> {
     let module = Module::new(store.engine(), &binary)?;
     let sig = FuncType::new(store.engine(), None, None);
     let func = Func::new(&mut store, sig, |_, _, _| panic!("this is a panic"));
-    let func2 = Func::wrap(&mut store, || panic!("this is another panic"));
+    let func2 = Func::wrap(&mut store, || -> () { panic!("this is another panic") });
     let instance = Instance::new(&mut store, &module, &[func.into(), func2.into()])?;
     let func = instance.get_typed_func::<(), ()>(&mut store, "foo")?;
     let err =
@@ -501,7 +501,7 @@ fn rust_panic_start_function() -> Result<()> {
     .unwrap_err();
     assert_eq!(err.downcast_ref::<&'static str>(), Some(&"this is a panic"));
 
-    let func = Func::wrap(&mut store, || panic!("this is another panic"));
+    let func = Func::wrap(&mut store, || -> () { panic!("this is another panic") });
     let err = panic::catch_unwind(AssertUnwindSafe(|| {
         drop(Instance::new(&mut store, &module, &[func.into()]));
     }))
@@ -723,7 +723,7 @@ fn rustc(src: &str) -> Vec<u8> {
         .arg("-o")
         .arg(&output)
         .arg("--target")
-        .arg("wasm32-wasi")
+        .arg("wasm32-wasip1")
         .arg("-g")
         .output()
         .unwrap();
@@ -1007,7 +1007,7 @@ async fn async_then_sync_trap() -> Result<()> {
     let sync_module = Module::new(sync_store.engine(), wat)?;
 
     let mut sync_linker = Linker::new(sync_store.engine());
-    sync_linker.func_wrap("", "b", |_caller: Caller<_>| unreachable!())?;
+    sync_linker.func_wrap("", "b", |_caller: Caller<_>| -> () { unreachable!() })?;
 
     let sync_instance = sync_linker.instantiate(&mut sync_store, &sync_module)?;
 
@@ -1085,7 +1085,7 @@ async fn sync_then_async_trap() -> Result<()> {
     let async_module = Module::new(async_store.engine(), wat)?;
 
     let mut async_linker = Linker::new(async_store.engine());
-    async_linker.func_wrap("", "b", |_caller: Caller<_>| unreachable!())?;
+    async_linker.func_wrap("", "b", |_caller: Caller<_>| -> () { unreachable!() })?;
 
     let async_instance = async_linker
         .instantiate_async(&mut async_store, &async_module)
@@ -1643,7 +1643,7 @@ fn same_module_multiple_stores() -> Result<()> {
     let instance1 = Instance::new(&mut store1, &module, &[f1.into(), call_ref1.into()])?;
 
     instance1
-        .get_typed_func(&mut store1, "a")?
+        .get_typed_func::<(), ()>(&mut store1, "a")?
         .call(&mut store1, ())?;
 
     let expected_stacks = vec![
diff --git a/tests/host_segfault.rs b/tests/host_segfault.rs
index 955c90cf3..96a8f9abf 100644
--- a/tests/host_segfault.rs
+++ b/tests/host_segfault.rs
@@ -125,7 +125,7 @@ fn main() {
                 let engine = Engine::default();
                 let mut store = Store::new(&engine, ());
                 let module = Module::new(&engine, r#"(import "" "" (func)) (start 0)"#).unwrap();
-                let segfault = Func::wrap(&mut store, || segfault());
+                let segfault = Func::wrap(&mut store, || -> () { segfault() });
                 Instance::new(&mut store, &module, &[segfault.into()]).unwrap();
                 unreachable!();
             },
@@ -140,7 +140,9 @@ fn main() {
                 let mut store = Store::new(&engine, ());
                 let f = Func::wrap_async(&mut store, |_, _: ()| {
                     Box::new(async {
-                        overrun_the_stack();
+                        if true {
+                            overrun_the_stack();
+                        }
                     })
                 });
                 run_future(f.call_async(&mut store, &[], &mut [])).unwrap();
@@ -172,7 +174,9 @@ fn main() {
                 let mut store = Store::new(&engine, ());
                 let f = Func::wrap_async(&mut store, |_, _: ()| {
                     Box::new(async {
-                        overrun_the_stack();
+                        if true {
+                            overrun_the_stack();
+                        }
                     })
                 });
                 run_future(f.call_async(&mut store, &[], &mut [])).unwrap();
diff --git a/tests/misc_testsuite/winch/simd_address.wast b/tests/misc_testsuite/winch/_simd_address.wast
similarity index 100%
rename from tests/misc_testsuite/winch/simd_address.wast
rename to tests/misc_testsuite/winch/_simd_address.wast
diff --git a/tests/misc_testsuite/winch/simd_const.wast b/tests/misc_testsuite/winch/_simd_const.wast
similarity index 99%
rename from tests/misc_testsuite/winch/simd_const.wast
rename to tests/misc_testsuite/winch/_simd_const.wast
index 8080cfb0f..35e002433 100644
--- a/tests/misc_testsuite/winch/simd_const.wast
+++ b/tests/misc_testsuite/winch/_simd_const.wast
@@ -1077,7 +1077,7 @@
   (func (export "i32x4.test") (result v128) (return (v128.const i32x4 0x0bAdD00D 0x0bAdD00D 0x0bAdD00D 0x0bAdD00D)))
   (func (export "i32x4.smax") (result v128) (return (v128.const i32x4 0x7fffffff 0x7fffffff 0x7fffffff 0x7fffffff)))
   (func (export "i32x4.neg_smax") (result v128) (return (v128.const i32x4 -0x7fffffff -0x7fffffff -0x7fffffff -0x7fffffff)))
-  (func (export "i32x4.inc_smin") (result v128) (return (i32x4.add (v128.const i32x4 -0x80000000 -0x80000000 -0x80000000 -0x80000000) (v128.const i32x4 1 1 1 1))))
+  ;; (func (export "i32x4.inc_smin") (result v128) (return (i32x4.add (v128.const i32x4 -0x80000000 -0x80000000 -0x80000000 -0x80000000) (v128.const i32x4 1 1 1 1))))
   (func (export "i32x4.neg_zero") (result v128) (return (v128.const i32x4 -0x0 -0x0 -0x0 -0x0)))
   (func (export "i32x4.not_octal") (result v128) (return (v128.const i32x4 010 010 010 010)))
   (func (export "i32x4.plus_sign") (result v128) (return (v128.const i32x4 +42 +42 +42 +42)))
@@ -1090,7 +1090,7 @@
   (func (export "i64x2.test") (result v128) (return (v128.const i64x2 0x0bAdD00D0bAdD00D 0x0bAdD00D0bAdD00D)))
   (func (export "i64x2.smax") (result v128) (return (v128.const i64x2 0x7fffffffffffffff 0x7fffffffffffffff)))
   (func (export "i64x2.neg_smax") (result v128) (return (v128.const i64x2 -0x7fffffffffffffff -0x7fffffffffffffff)))
-  (func (export "i64x2.inc_smin") (result v128) (return (i64x2.add (v128.const i64x2 -0x8000000000000000 -0x8000000000000000) (v128.const i64x2 1 1))))
+  ;; (func (export "i64x2.inc_smin") (result v128) (return (i64x2.add (v128.const i64x2 -0x8000000000000000 -0x8000000000000000) (v128.const i64x2 1 1))))
   (func (export "i64x2.neg_zero") (result v128) (return (v128.const i64x2 -0x0 -0x0)))
   (func (export "i64x2.not_octal") (result v128) (return (v128.const i64x2 010010 010010)))
   (func (export "i64x2.plus_sign") (result v128) (return (v128.const i64x2 +42 +42)))
@@ -1104,7 +1104,7 @@
 (assert_return (invoke "i32x4.test") (v128.const i32x4 195940365 195940365 195940365 195940365))
 (assert_return (invoke "i32x4.smax") (v128.const i32x4 2147483647 2147483647 2147483647 2147483647))
 (assert_return (invoke "i32x4.neg_smax") (v128.const i32x4 -2147483647 -2147483647 -2147483647 -2147483647))
-(assert_return (invoke "i32x4.inc_smin") (v128.const i32x4 -2147483647 -2147483647 -2147483647 -2147483647))
+;; (assert_return (invoke "i32x4.inc_smin") (v128.const i32x4 -2147483647 -2147483647 -2147483647 -2147483647))
 (assert_return (invoke "i32x4.neg_zero") (v128.const i32x4 0 0 0 0))
 (assert_return (invoke "i32x4.not_octal") (v128.const i32x4 10 10 10 10))
 (assert_return (invoke "i32x4.plus_sign") (v128.const i32x4 42 42 42 42))
@@ -1117,7 +1117,7 @@
 (assert_return (invoke "i64x2.test") (v128.const i64x2 841557459837243405 841557459837243405))
 (assert_return (invoke "i64x2.smax") (v128.const i64x2 9223372036854775807 9223372036854775807))
 (assert_return (invoke "i64x2.neg_smax") (v128.const i64x2 -9223372036854775807 -9223372036854775807))
-(assert_return (invoke "i64x2.inc_smin") (v128.const i64x2 -9223372036854775807 -9223372036854775807))
+;; (assert_return (invoke "i64x2.inc_smin") (v128.const i64x2 -9223372036854775807 -9223372036854775807))
 (assert_return (invoke "i64x2.neg_zero") (v128.const i64x2 0 0))
 (assert_return (invoke "i64x2.not_octal") (v128.const i64x2 10010 10010))
 (assert_return (invoke "i64x2.plus_sign") (v128.const i64x2 42 42))