Repository: bytecodealliance/wasmtime
Pull request #9014 (2024-07-25 22:59:15)

Get Pulley tests passing under MIRI; run Pulley tests under MIRI in CI
<!--
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/workflows/main.yml b/.github/workflows/main.yml
index 14ed47a85..ada3daffb 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -433,6 +433,7 @@ jobs:
     - run: rustup target add x86_64-unknown-none
     - run: cargo check --target x86_64-unknown-none -p wasmtime --no-default-features --features runtime,gc,component-model
     - run: cargo check --target x86_64-unknown-none -p cranelift-control --no-default-features
+    - run: cargo check --target x86_64-unknown-none -p pulley-interpreter --features encode,decode,disas,interp
 
     # Check that wasmtime compiles with panic=abort since there's some `#[cfg]`
     # for specifically panic=abort there.
@@ -604,22 +605,30 @@ jobs:
 
     - run: cargo fetch --locked
 
-    - uses: actions/cache@v4
-      with:
-        path: ${{ runner.tool_cache }}/qemu
-        key: qemu-${{ matrix.target }}-${{ env.QEMU_BUILD_VERSION }}-patchcpuinfo
-      if: matrix.target != '' && matrix.os == 'ubuntu-latest'
     - name: Install cross-compilation tools
       run: |
         set -ex
+
         sudo apt-get update
         sudo apt-get install -y ${{ matrix.gcc_package }} ninja-build
 
         # Configure Cargo for cross compilation and tell it how it can run
         # cross executables
         upcase=$(echo ${{ matrix.target }} | awk '{ print toupper($0) }' | sed 's/-/_/g')
-        echo CARGO_TARGET_${upcase}_RUNNER=${{ runner.tool_cache }}/qemu/bin/${{ matrix.qemu }} >> $GITHUB_ENV
         echo CARGO_TARGET_${upcase}_LINKER=${{ matrix.gcc }} >> $GITHUB_ENV
+      if: matrix.gcc != ''
+
+    - uses: actions/cache@v4
+      with:
+        path: ${{ runner.tool_cache }}/qemu
+        key: qemu-${{ matrix.target }}-${{ env.QEMU_BUILD_VERSION }}-patchcpuinfo
+      if: matrix.qemu != ''
+    - name: Install qemu
+      run: |
+        set -ex
+
+        upcase=$(echo ${{ matrix.target }} | awk '{ print toupper($0) }' | sed 's/-/_/g')
+        echo CARGO_TARGET_${upcase}_RUNNER=${{ runner.tool_cache }}/qemu/bin/${{ matrix.qemu }} >> $GITHUB_ENV
 
         # QEMU emulation is not always the speediest, so total testing time
         # goes down if we build the libs in release mode when running tests.
@@ -643,7 +652,7 @@ jobs:
         ./configure --target-list=${{ matrix.qemu_target }} --prefix=${{ runner.tool_cache}}/qemu --disable-tools --disable-slirp --disable-fdt --disable-capstone --disable-docs
         ninja -C build install
         touch ${{ runner.tool_cache }}/qemu/built
-      if: matrix.gcc != ''
+      if: matrix.qemu != ''
 
     # Record some CPU details; this is helpful information if tests fail due
     # to CPU-specific features.
@@ -817,7 +826,7 @@ jobs:
       if: failure() && github.event_name != 'pull_request'
       env:
         GH_TOKEN: ${{ github.token }}
-  
+
   build-preview1-component-adapter-provider:
     name: Build wasi-preview1-component-adapter-provider
     needs: build-preview1-component-adapter
@@ -830,7 +839,7 @@ jobs:
     - uses: ./.github/actions/build-adapter-provider
       with:
         run-id: ${{ github.run_id }}
-    
+
 
     # common logic to cancel the entire run if this job fails
     - run: gh run cancel ${{ github.run_id }}
@@ -970,7 +979,11 @@ jobs:
   miri:
     strategy:
       matrix:
-        crate: ["wasmtime", "wasmtime-cli", "wasmtime-environ"]
+        crate:
+          - "wasmtime"
+          - "wasmtime-cli"
+          - "wasmtime-environ"
+          - "pulley-interpreter --all-features"
     needs: determine
     if: needs.determine.outputs.run-full && github.repository == 'bytecodealliance/wasmtime'
     name: Miri
diff --git a/Cargo.lock b/Cargo.lock
index 04a71a75c..0b793e88e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2148,6 +2148,7 @@ dependencies = [
  "arbitrary",
  "env_logger",
  "log",
+ "sptr",
 ]
 
 [[package]]
diff --git a/cranelift/codegen/src/ir/immediates.rs b/cranelift/codegen/src/ir/immediates.rs
index 24982c644..f96d678b8 100644
--- a/cranelift/codegen/src/ir/immediates.rs
+++ b/cranelift/codegen/src/ir/immediates.rs
@@ -468,65 +468,338 @@ impl FromStr for Offset32 {
     }
 }
 
-/// An IEEE binary16 immediate floating point value, represented as a u16
-/// containing the bit pattern.
-///
-/// We specifically avoid using a f16 here since some architectures may silently alter floats.
-/// See: <https://github.com/bytecodealliance/wasmtime/pull/2251#discussion_r498508646>
-///
-/// The [PartialEq] and [Hash] implementations are over the underlying bit pattern, but
-/// [PartialOrd] respects IEEE754 semantics.
-///
-/// All bit patterns are allowed.
-#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
-#[cfg_attr(feature = "enable-serde", derive(Serialize, Deserialize))]
-#[repr(C)]
-pub struct Ieee16(u16);
+// FIXME(rust-lang/rust#83527): Replace with `${ignore()}` once it is stabilised.
+macro_rules! ignore {
+    ($($t:tt)*) => {};
+}
+
+macro_rules! ieee_float {
+    (
+        name = $name:ident,
+        bits = $bits:literal,
+        significand_bits = $significand_bits:literal,
+        bits_ty = $bits_ty:ident,
+        float_ty = $float_ty:ident,
+        $(as_float = $as_float:ident,)?
+        $(rust_type_not_stable = $rust_type_not_stable:ident,)?
+    ) => {
+        /// An IEEE
+        #[doc = concat!("binary", stringify!($bits))]
+        /// immediate floating point value, represented as a
+        #[doc = stringify!($bits_ty)]
+        /// containing the bit pattern.
+        ///
+        /// We specifically avoid using a
+        #[doc = stringify!($float_ty)]
+        /// here since some architectures may silently alter floats.
+        /// See: <https://github.com/bytecodealliance/wasmtime/pull/2251#discussion_r498508646>
+        ///
+        /// The [PartialEq] and [Hash] implementations are over the underlying bit pattern, but
+        /// [PartialOrd] respects IEEE754 semantics.
+        ///
+        /// All bit patterns are allowed.
+        #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
+        #[cfg_attr(feature = "enable-serde", derive(Serialize, Deserialize))]
+        #[repr(C)]
+        pub struct $name {
+            bits: $bits_ty
+        }
 
-/// An IEEE binary32 immediate floating point value, represented as a u32
-/// containing the bit pattern.
-///
-/// We specifically avoid using a f32 here since some architectures may silently alter floats.
-/// See: <https://github.com/bytecodealliance/wasmtime/pull/2251#discussion_r498508646>
-///
-/// The [PartialEq] and [Hash] implementations are over the underlying bit pattern, but
-/// [PartialOrd] respects IEEE754 semantics.
-///
-/// All bit patterns are allowed.
-#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
-#[cfg_attr(feature = "enable-serde", derive(Serialize, Deserialize))]
-#[repr(C)]
-pub struct Ieee32(u32);
+        impl $name {
+            const BITS: u8 = $bits;
+            const SIGNIFICAND_BITS: u8 = $significand_bits;
+            const EXPONENT_BITS: u8 = Self::BITS - Self::SIGNIFICAND_BITS - 1;
+            const SIGN_MASK: $bits_ty = 1 << (Self::EXPONENT_BITS + Self::SIGNIFICAND_BITS);
+            const SIGNIFICAND_MASK: $bits_ty = $bits_ty::MAX >> (Self::EXPONENT_BITS + 1);
+            const EXPONENT_MASK: $bits_ty = !Self::SIGN_MASK & !Self::SIGNIFICAND_MASK;
+
+            /// Create a new
+            #[doc = concat!("`", stringify!($name), "`")]
+            /// containing the bits of `bits`.
+            pub fn with_bits(bits: $bits_ty) -> Self {
+                Self { bits }
+            }
 
-/// An IEEE binary64 immediate floating point value, represented as a u64
-/// containing the bit pattern.
-///
-/// We specifically avoid using a f64 here since some architectures may silently alter floats.
-/// See: <https://github.com/bytecodealliance/wasmtime/pull/2251#discussion_r498508646>
-///
-/// The [PartialEq] and [Hash] implementations are over the underlying bit pattern, but
-/// [PartialOrd] respects IEEE754 semantics.
-///
-/// All bit patterns are allowed.
-#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
-#[cfg_attr(feature = "enable-serde", derive(Serialize, Deserialize))]
-#[repr(C)]
-pub struct Ieee64(u64);
+            /// Get the bitwise representation.
+            pub fn bits(self) -> $bits_ty {
+                self.bits
+            }
 
-/// An IEEE binary128 immediate floating point value, represented as a u128
-/// containing the bit pattern.
-///
-/// We specifically avoid using a f128 here since some architectures may silently alter floats.
-/// See: <https://github.com/bytecodealliance/wasmtime/pull/2251#discussion_r498508646>
-///
-/// The [PartialEq] and [Hash] implementations are over the underlying bit pattern, but
-/// [PartialOrd] respects IEEE754 semantics.
-///
-/// All bit patterns are allowed.
-#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
-#[cfg_attr(feature = "enable-serde", derive(Serialize, Deserialize))]
-#[repr(C)]
-pub struct Ieee128(u128);
+            $(
+                /// Create a new
+                #[doc = concat!("`", stringify!($name), "`")]
+                /// representing the number `x`.
+                pub fn with_float(x: $float_ty) -> Self {
+                    Self::with_bits(x.to_bits())
+                }
+
+                /// Converts `self` to a Rust
+                #[doc = concat!("`", stringify!($float_ty), "`.")]
+                pub fn $as_float(self) -> $float_ty {
+                    $float_ty::from_bits(self.bits())
+                }
+            )?
+
+            /// Computes the absolute value of `self`.
+            pub fn abs(self) -> Self {
+                Self::with_bits(self.bits() & !Self::SIGN_MASK)
+            }
+
+            /// Returns a number composed of the magnitude of `self` and the sign of `sign`.
+            pub fn copysign(self, sign: Self) -> Self {
+                Self::with_bits((self.bits() & !Self::SIGN_MASK) | (sign.bits() & Self::SIGN_MASK))
+            }
+
+            /// Create an
+            #[doc = concat!("`", stringify!($name), "`")]
+            /// number representing `2.0^n`.
+            pub fn pow2<I: Into<i32>>(n: I) -> Self {
+                let n = n.into();
+                let w = Self::EXPONENT_BITS;
+                let t = Self::SIGNIFICAND_BITS;
+                let bias = (1 << (w - 1)) - 1;
+                let exponent = n + bias;
+                assert!(exponent > 0, "Underflow n={}", n);
+                assert!(exponent < (1 << w) + 1, "Overflow n={}", n);
+                Self::with_bits((exponent as $bits_ty) << t)
+            }
+
+            /// Create an
+            #[doc = concat!("`", stringify!($name), "`")]
+            /// number representing the greatest negative value not convertible from
+            #[doc = concat!("`", stringify!($float_ty), "`")]
+            /// to a signed integer with width n.
+            pub fn fcvt_to_sint_negative_overflow<I: Into<i32>>(n: I) -> Self {
+                let n = n.into();
+                debug_assert!(n < i32::from(Self::BITS));
+                debug_assert!(i32::from(Self::SIGNIFICAND_BITS) + 1 - n < i32::from(Self::BITS));
+                Self::with_bits((1 << (Self::BITS - 1)) | Self::pow2(n - 1).bits() | (1 << (i32::from(Self::SIGNIFICAND_BITS) + 1 - n)))
+            }
+
+            /// Check if the value is a NaN. For
+            #[doc = concat!("`", stringify!($name), "`,")]
+            /// this means checking that all the exponent bits are set and the significand is non-zero.
+            pub fn is_nan(self) -> bool {
+                self.abs().bits() > Self::EXPONENT_MASK
+            }
+
+            /// Returns true if `self` has a negative sign, including -0.0, NaNs with negative sign bit and negative infinity.
+            pub fn is_negative(self) -> bool {
+                self.bits() & Self::SIGN_MASK == Self::SIGN_MASK
+            }
+
+            /// Returns `true` if `self` is positive or negative zero.
+            pub fn is_zero(self) -> bool {
+                self.abs().bits() == 0
+            }
+
+            /// Returns `None` if `self` is a NaN and `Some(self)` otherwise.
+            pub fn non_nan(self) -> Option<Self> {
+                Some(self).filter(|f| !f.is_nan())
+            }
+
+            $(
+                /// Returns the square root of `self`.
+                pub fn sqrt(self) -> Self {
+                    Self::with_float(self.$as_float().sqrt())
+                }
+
+                /// Returns the smallest integer greater than or equal to `self`.
+                pub fn ceil(self) -> Self {
+                    Self::with_float(self.$as_float().ceil())
+                }
+
+                /// Returns the largest integer less than or equal to `self`.
+                pub fn floor(self) -> Self {
+                    Self::with_float(self.$as_float().floor())
+                }
+
+                /// Returns the integer part of `self`. This means that non-integer numbers are always truncated towards zero.
+                pub fn trunc(self) -> Self {
+                    Self::with_float(self.$as_float().trunc())
+                }
+
+                /// Returns the nearest integer to `self`. Rounds half-way cases to the number
+                /// with an even least significant digit.
+                pub fn round_ties_even(self) -> Self {
+                    Self::with_float(self.$as_float().round_ties_even())
+                }
+            )?
+        }
+
+        impl PartialOrd for $name {
+            fn partial_cmp(&self, rhs: &Self) -> Option<Ordering> {
+                $(self.$as_float().partial_cmp(&rhs.$as_float()))?
+                $(
+                    ignore!($rust_type_not_stable);
+                    // FIXME(#8312): Use builtin Rust comparisons once `f16` and `f128` support is stabalised.
+                    if self.is_nan() || rhs.is_nan() {
+                        // One of the floats is a NaN.
+                        return None;
+                    }
+                    if self.is_zero() || rhs.is_zero() {
+                        // Zeros are always equal regardless of sign.
+                        return Some(Ordering::Equal);
+                    }
+                    let lhs_positive = !self.is_negative();
+                    let rhs_positive = !rhs.is_negative();
+                    if lhs_positive != rhs_positive {
+                        // Different signs: negative < positive
+                        return lhs_positive.partial_cmp(&rhs_positive);
+                    }
+                    // Finite or infinity will order correctly with an integer comparison of the bits.
+                    if lhs_positive {
+                        self.bits().partial_cmp(&rhs.bits())
+                    } else {
+                        // Reverse the comparison when both floats are negative.
+                        rhs.bits().partial_cmp(&self.bits())
+                    }
+                )?
+            }
+        }
+
+        impl Display for $name {
+            fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+                format_float(u128::from(self.bits()), Self::EXPONENT_BITS, Self::SIGNIFICAND_BITS, f)
+            }
+        }
+
+        impl FromStr for $name {
+            type Err = &'static str;
+
+            fn from_str(s: &str) -> Result<Self, &'static str> {
+                match parse_float(s, Self::EXPONENT_BITS, Self::SIGNIFICAND_BITS) {
+                    Ok(b) => Ok(Self::with_bits(b.try_into().unwrap())),
+                    Err(s) => Err(s),
+                }
+            }
+        }
+
+        impl IntoBytes for $name {
+            fn into_bytes(self) -> Vec<u8> {
+                self.bits().to_le_bytes().to_vec()
+            }
+        }
+
+        impl Neg for $name {
+            type Output = Self;
+
+            fn neg(self) -> Self {
+                Self::with_bits(self.bits() ^ Self::SIGN_MASK)
+            }
+        }
+
+
+
+        $(
+            impl From<$float_ty> for $name {
+                fn from(x: $float_ty) -> Self {
+                    Self::with_float(x)
+                }
+            }
+
+            impl Add for $name {
+                type Output = Self;
+
+                fn add(self, rhs: Self) -> Self {
+                    Self::with_float(self.$as_float() + rhs.$as_float())
+                }
+            }
+
+            impl Sub for $name {
+                type Output = Self;
+
+                fn sub(self, rhs: Self) -> Self {
+                    Self::with_float(self.$as_float() - rhs.$as_float())
+                }
+            }
+
+            impl Mul for $name {
+                type Output = Self;
+
+                fn mul(self, rhs: Self) -> Self {
+                    Self::with_float(self.$as_float() * rhs.$as_float())
+                }
+            }
+
+            impl Div for $name {
+                type Output = Self;
+
+                fn div(self, rhs: Self) -> Self::Output {
+                    Self::with_float(self.$as_float() / rhs.$as_float())
+                }
+            }
+        )?
+
+        impl BitAnd for $name {
+            type Output = Self;
+
+            fn bitand(self, rhs: Self) -> Self {
+                Self::with_bits(self.bits() & rhs.bits())
+            }
+        }
+
+        impl BitOr for $name {
+            type Output = Self;
+
+            fn bitor(self, rhs: Self) -> Self {
+                Self::with_bits(self.bits() | rhs.bits())
+            }
+        }
+
+        impl BitXor for $name {
+            type Output = Self;
+
+            fn bitxor(self, rhs: Self) -> Self {
+                Self::with_bits(self.bits() ^ rhs.bits())
+            }
+        }
+
+        impl Not for $name {
+            type Output = Self;
+
+            fn not(self) -> Self {
+                Self::with_bits(!self.bits())
+            }
+        }
+    };
+}
+
+ieee_float! {
+    name = Ieee16,
+    bits = 16,
+    significand_bits = 10,
+    bits_ty = u16,
+    float_ty = f16,
+    rust_type_not_stable = rust_type_not_stable,
+}
+
+ieee_float! {
+    name = Ieee32,
+    bits = 32,
+    significand_bits = 23,
+    bits_ty = u32,
+    float_ty = f32,
+    as_float = as_f32,
+}
+
+ieee_float! {
+    name = Ieee64,
+    bits = 64,
+    significand_bits = 52,
+    bits_ty = u64,
+    float_ty = f64,
+    as_float = as_f64,
+}
+
+ieee_float! {
+    name = Ieee128,
+    bits = 128,
+    significand_bits = 112,
+    bits_ty = u128,
+    float_ty = f128,
+    rust_type_not_stable = rust_type_not_stable,
+}
 
 /// Format a floating point number in a way that is reasonably human-readable, and that can be
 /// converted back to binary without any rounding issues. The hexadecimal formatting of normal and
@@ -765,651 +1038,10 @@ fn parse_float(s: &str, w: u8, t: u8) -> Result<u128, &'static str> {
     }
 }
 
-impl Ieee16 {
-    const SIGNIFICAND_BITS: u8 = 10;
-    const EXPONENT_BITS: u8 = 5;
-    const SIGN_MASK: u16 = 1 << (Self::EXPONENT_BITS + Self::SIGNIFICAND_BITS);
-    const SIGNIFICAND_MASK: u16 = u16::MAX >> (Self::EXPONENT_BITS + 1);
-    const EXPONENT_MASK: u16 = !Self::SIGN_MASK & !Self::SIGNIFICAND_MASK;
-
-    /// Create a new `Ieee16` containing the bits of `x`.
-    pub fn with_bits(x: u16) -> Self {
-        Self(x)
-    }
-
-    /// Get the bitwise representation.
-    pub fn bits(self) -> u16 {
-        self.0
-    }
-
-    /// Computes the absolute value of self.
-    pub fn abs(self) -> Self {
-        Self::with_bits(self.bits() & !Self::SIGN_MASK)
-    }
-
-    /// Returns a number composed of the magnitude of self and the sign of sign.
-    pub fn copysign(self, sign: Self) -> Self {
-        Self::with_bits((self.bits() & !Self::SIGN_MASK) | (sign.bits() & Self::SIGN_MASK))
-    }
-
-    /// Returns true if self is positive or negative zero
-    pub fn is_zero(&self) -> bool {
-        self.partial_cmp(&Self::with_bits(0)) == Some(Ordering::Equal)
-    }
-}
-
-impl PartialOrd for Ieee16 {
-    fn partial_cmp(&self, rhs: &Self) -> Option<Ordering> {
-        // FIXME(#8312): Use Rust `f16` comparisons once `f16` support is stabalised.
-        let lhs_abs = self.bits() & !Self::SIGN_MASK;
-        let rhs_abs = rhs.bits() & !Self::SIGN_MASK;
-        if (lhs_abs & Self::EXPONENT_MASK == Self::EXPONENT_MASK
-            && lhs_abs & Self::SIGNIFICAND_MASK != 0)
-            && (rhs_abs & Self::EXPONENT_MASK == Self::EXPONENT_MASK
-                && rhs_abs & Self::SIGNIFICAND_MASK != 0)
-        {
-            // One of the floats is a NaN.
-            return None;
-        }
-        if lhs_abs == 0 && rhs_abs == 0 {
-            // Zeros are always equal regardless of sign.
-            return Some(Ordering::Equal);
-        }
-        let lhs_positive = self.bits() & Self::SIGN_MASK == 0;
-        let rhs_positive = rhs.bits() & Self::SIGN_MASK == 0;
-        if lhs_positive != rhs_positive {
-            // Different signs: negative < positive
-            return lhs_positive.partial_cmp(&rhs_positive);
-        }
-        // Finite or infinity will order correctly with an integer comparison of the bits.
-        if lhs_positive {
-            lhs_abs.partial_cmp(&rhs_abs)
-        } else {
-            // Reverse the comparison when both floats are negative.
-            rhs_abs.partial_cmp(&lhs_abs)
-        }
-    }
-}
-
-impl Display for Ieee16 {
-    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
-        let bits: u16 = self.0;
-        format_float(
-            u128::from(bits),
-            Self::EXPONENT_BITS,
-            Self::SIGNIFICAND_BITS,
-            f,
-        )
-    }
-}
-
-impl FromStr for Ieee16 {
-    type Err = &'static str;
-
-    fn from_str(s: &str) -> Result<Self, &'static str> {
-        match parse_float(s, Self::EXPONENT_BITS, Self::SIGNIFICAND_BITS) {
-            Ok(b) => Ok(Self(b as u16)),
-            Err(s) => Err(s),
-        }
-    }
-}
-
-impl IntoBytes for Ieee16 {
-    fn into_bytes(self) -> Vec<u8> {
-        self.bits().to_le_bytes().to_vec()
-    }
-}
-
-impl Neg for Ieee16 {
-    type Output = Self;
-
-    fn neg(self) -> Self {
-        Self::with_bits(self.bits() ^ Self::SIGN_MASK)
-    }
-}
-
-impl Ieee32 {
-    /// Create a new `Ieee32` containing the bits of `x`.
-    pub fn with_bits(x: u32) -> Self {
-        Self(x)
-    }
-
-    /// Create an `Ieee32` number representing `2.0^n`.
-    pub fn pow2<I: Into<i32>>(n: I) -> Self {
-        let n = n.into();
-        let w = 8;
-        let t = 23;
-        let bias = (1 << (w - 1)) - 1;
-        let exponent = (n + bias) as u32;
-        assert!(exponent > 0, "Underflow n={}", n);
-        assert!(exponent < (1 << w) + 1, "Overflow n={}", n);
-        Self(exponent << t)
-    }
-
-    /// Create an `Ieee32` number representing the greatest negative value
-    /// not convertible from f32 to a signed integer with width n.
-    pub fn fcvt_to_sint_negative_overflow<I: Into<i32>>(n: I) -> Self {
-        let n = n.into();
-        debug_assert!(n < 32);
-        debug_assert!(23 + 1 - n < 32);
-        Self::with_bits((1u32 << (32 - 1)) | Self::pow2(n - 1).0 | (1u32 << (23 + 1 - n)))
-    }
-
-    /// Return self negated.
-    pub fn neg(self) -> Self {
-        Self(self.0 ^ (1 << 31))
-    }
-
-    /// Create a new `Ieee32` representing the number `x`.
-    pub fn with_float(x: f32) -> Self {
-        Self(x.to_bits())
-    }
-
-    /// Get the bitwise representation.
-    pub fn bits(self) -> u32 {
-        self.0
-    }
-
-    /// Check if the value is a NaN.
-    pub fn is_nan(&self) -> bool {
-        self.as_f32().is_nan()
-    }
-
-    /// Returns `None` if `self` is a NaN and `Some(self)` otherwise.
-    pub fn non_nan(self) -> Option<Self> {
-        Some(self).filter(|f| !f.is_nan())
-    }
-
-    /// Converts Self to a rust f32
-    pub fn as_f32(self) -> f32 {
-        f32::from_bits(self.0)
-    }
-
-    /// Returns the square root of self.
-    pub fn sqrt(self) -> Self {
-        Self::with_float(self.as_f32().sqrt())
-    }
-
-    /// Computes the absolute value of self.
-    pub fn abs(self) -> Self {
-        Self(self.0 & !(1u32 << 31))
-    }
-
-    /// Returns a number composed of the magnitude of self and the sign of sign.
-    pub fn copysign(self, sign: Self) -> Self {
-        if self.is_negative() == sign.is_negative() {
-            self
-        } else {
-            self.neg()
-        }
-    }
-
-    /// Returns true if self has a negative sign, including -0.0, NaNs with negative sign bit and negative infinity.
-    pub fn is_negative(&self) -> bool {
-        self.0 & (1 << 31) != 0
-    }
-
-    /// Returns true if self is positive or negative zero
-    pub fn is_zero(&self) -> bool {
-        self.as_f32() == 0.0
-    }
-
-    /// Returns the smallest integer greater than or equal to `self`.
-    pub fn ceil(self) -> Self {
-        Self::with_float(self.as_f32().ceil())
-    }
-
-    /// Returns the largest integer less than or equal to `self`.
-    pub fn floor(self) -> Self {
-        Self::with_float(self.as_f32().floor())
-    }
-
-    /// Returns the integer part of `self`. This means that non-integer numbers are always truncated towards zero.
-    pub fn trunc(self) -> Self {
-        Self::with_float(self.as_f32().trunc())
-    }
-
-    /// Returns the nearest integer to `self`. Rounds half-way cases to the number
-    /// with an even least significant digit.
-    pub fn round_ties_even(self) -> Self {
-        Self::with_float(self.as_f32().round_ties_even())
-    }
-}
-
-impl PartialOrd for Ieee32 {
-    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
-        self.as_f32().partial_cmp(&other.as_f32())
-    }
-}
-
-impl Display for Ieee32 {
-    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
-        let bits: u32 = self.0;
-        format_float(u128::from(bits), 8, 23, f)
-    }
-}
-
-impl FromStr for Ieee32 {
-    type Err = &'static str;
-
-    fn from_str(s: &str) -> Result<Self, &'static str> {
-        match parse_float(s, 8, 23) {
-            Ok(b) => Ok(Self(b as u32)),
-            Err(s) => Err(s),
-        }
-    }
-}
-
-impl From<f32> for Ieee32 {
-    fn from(x: f32) -> Self {
-        Self::with_float(x)
-    }
-}
-
-impl IntoBytes for Ieee32 {
-    fn into_bytes(self) -> Vec<u8> {
-        self.0.to_le_bytes().to_vec()
-    }
-}
-
-impl Neg for Ieee32 {
-    type Output = Ieee32;
-
-    fn neg(self) -> Self::Output {
-        Self(self.0 ^ (1 << 31))
-    }
-}
-
-impl Add for Ieee32 {
-    type Output = Ieee32;
-
-    fn add(self, rhs: Self) -> Self::Output {
-        Self::with_float(self.as_f32() + rhs.as_f32())
-    }
-}
-
-impl Sub for Ieee32 {
-    type Output = Ieee32;
-
-    fn sub(self, rhs: Self) -> Self::Output {
-        Self::with_float(self.as_f32() - rhs.as_f32())
-    }
-}
-
-impl Mul for Ieee32 {
-    type Output = Ieee32;
-
-    fn mul(self, rhs: Self) -> Self::Output {
-        Self::with_float(self.as_f32() * rhs.as_f32())
-    }
-}
-
-impl Div for Ieee32 {
-    type Output = Ieee32;
-
-    fn div(self, rhs: Self) -> Self::Output {
-        Self::with_float(self.as_f32() / rhs.as_f32())
-    }
-}
-
-impl BitAnd for Ieee32 {
-    type Output = Ieee32;
-
-    fn bitand(self, rhs: Self) -> Self::Output {
-        Self::with_bits(self.bits() & rhs.bits())
-    }
-}
-
-impl BitOr for Ieee32 {
-    type Output = Ieee32;
-
-    fn bitor(self, rhs: Self) -> Self::Output {
-        Self::with_bits(self.bits() | rhs.bits())
-    }
-}
-
-impl BitXor for Ieee32 {
-    type Output = Ieee32;
-
-    fn bitxor(self, rhs: Self) -> Self::Output {
-        Self::with_bits(self.bits() ^ rhs.bits())
-    }
-}
-
-impl Not for Ieee32 {
-    type Output = Ieee32;
-
-    fn not(self) -> Self::Output {
-        Self::with_bits(!self.bits())
-    }
-}
-
-impl Ieee64 {
-    /// Create a new `Ieee64` containing the bits of `x`.
-    pub fn with_bits(x: u64) -> Self {
-        Self(x)
-    }
-
-    /// Create an `Ieee64` number representing `2.0^n`.
-    pub fn pow2<I: Into<i64>>(n: I) -> Self {
-        let n = n.into();
-        let w = 11;
-        let t = 52;
-        let bias = (1 << (w - 1)) - 1;
-        let exponent = (n + bias) as u64;
-        assert!(exponent > 0, "Underflow n={}", n);
-        assert!(exponent < (1 << w) + 1, "Overflow n={}", n);
-        Self(exponent << t)
-    }
-
-    /// Create an `Ieee64` number representing the greatest negative value
-    /// not convertible from f64 to a signed integer with width n.
-    pub fn fcvt_to_sint_negative_overflow<I: Into<i64>>(n: I) -> Self {
-        let n = n.into();
-        debug_assert!(n < 64);
-        debug_assert!(52 + 1 - n < 64);
-        Self::with_bits((1u64 << (64 - 1)) | Self::pow2(n - 1).0 | (1u64 << (52 + 1 - n)))
-    }
-
-    /// Return self negated.
-    pub fn neg(self) -> Self {
-        Self(self.0 ^ (1 << 63))
-    }
-
-    /// Create a new `Ieee64` representing the number `x`.
-    pub fn with_float(x: f64) -> Self {
-        Self(x.to_bits())
-    }
-
-    /// Get the bitwise representation.
-    pub fn bits(self) -> u64 {
-        self.0
-    }
-
-    /// Check if the value is a NaN. For [Ieee64], this means checking that the 11 exponent bits are
-    /// all set.
-    pub fn is_nan(&self) -> bool {
-        self.as_f64().is_nan()
-    }
-
-    /// Returns `None` if `self` is a NaN and `Some(self)` otherwise.
-    pub fn non_nan(self) -> Option<Self> {
-        Some(self).filter(|f| !f.is_nan())
-    }
-
-    /// Converts Self to a rust f64
-    pub fn as_f64(self) -> f64 {
-        f64::from_bits(self.0)
-    }
-
-    /// Returns the square root of self.
-    pub fn sqrt(self) -> Self {
-        Self::with_float(self.as_f64().sqrt())
-    }
-
-    /// Computes the absolute value of self.
-    pub fn abs(self) -> Self {
-        Self(self.0 & !(1u64 << 63))
-    }
-
-    /// Returns a number composed of the magnitude of self and the sign of sign.
-    pub fn copysign(self, sign: Self) -> Self {
-        if self.is_negative() == sign.is_negative() {
-            self
-        } else {
-            self.neg()
-        }
-    }
-
-    /// Returns true if self has a negative sign, including -0.0, NaNs with negative sign bit and negative infinity.
-    pub fn is_negative(&self) -> bool {
-        self.0 & (1 << 63) != 0
-    }
-
-    /// Returns true if self is positive or negative zero
-    pub fn is_zero(&self) -> bool {
-        self.as_f64() == 0.0
-    }
-
-    /// Returns the smallest integer greater than or equal to `self`.
-    pub fn ceil(self) -> Self {
-        Self::with_float(self.as_f64().ceil())
-    }
-
-    /// Returns the largest integer less than or equal to `self`.
-    pub fn floor(self) -> Self {
-        Self::with_float(self.as_f64().floor())
-    }
-
-    /// Returns the integer part of `self`. This means that non-integer numbers are always truncated towards zero.
-    pub fn trunc(self) -> Self {
-        Self::with_float(self.as_f64().trunc())
-    }
-
-    /// Returns the nearest integer to `self`. Rounds half-way cases to the number
-    /// with an even least significant digit.
-    pub fn round_ties_even(self) -> Self {
-        Self::with_float(self.as_f64().round_ties_even())
-    }
-}
-
-impl PartialOrd for Ieee64 {
-    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
-        self.as_f64().partial_cmp(&other.as_f64())
-    }
-}
-
-impl Display for Ieee64 {
-    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
-        let bits: u64 = self.0;
-        format_float(u128::from(bits), 11, 52, f)
-    }
-}
-
-impl FromStr for Ieee64 {
-    type Err = &'static str;
-
-    fn from_str(s: &str) -> Result<Self, &'static str> {
-        match parse_float(s, 11, 52) {
-            Ok(b) => Ok(Self(b as u64)),
-            Err(s) => Err(s),
-        }
-    }
-}
-
-impl From<f64> for Ieee64 {
-    fn from(x: f64) -> Self {
-        Self::with_float(x)
-    }
-}
-
-impl From<u64> for Ieee64 {
-    fn from(x: u64) -> Self {
-        Self::with_float(f64::from_bits(x))
-    }
-}
-
-impl IntoBytes for Ieee64 {
-    fn into_bytes(self) -> Vec<u8> {
-        self.0.to_le_bytes().to_vec()
-    }
-}
-
-impl Neg for Ieee64 {
-    type Output = Ieee64;
-
-    fn neg(self) -> Self::Output {
-        Self(self.0 ^ (1 << 63))
-    }
-}
-
-impl Add for Ieee64 {
-    type Output = Ieee64;
-
-    fn add(self, rhs: Self) -> Self::Output {
-        Self::with_float(self.as_f64() + rhs.as_f64())
-    }
-}
-
-impl Sub for Ieee64 {
-    type Output = Ieee64;
-
-    fn sub(self, rhs: Self) -> Self::Output {
-        Self::with_float(self.as_f64() - rhs.as_f64())
-    }
-}
-
-impl Mul for Ieee64 {
-    type Output = Ieee64;
-
-    fn mul(self, rhs: Self) -> Self::Output {
-        Self::with_float(self.as_f64() * rhs.as_f64())
-    }
-}
-
-impl Div for Ieee64 {
-    type Output = Ieee64;
-
-    fn div(self, rhs: Self) -> Self::Output {
-        Self::with_float(self.as_f64() / rhs.as_f64())
-    }
-}
-
-impl BitAnd for Ieee64 {
-    type Output = Ieee64;
-
-    fn bitand(self, rhs: Self) -> Self::Output {
-        Self::with_bits(self.bits() & rhs.bits())
-    }
-}
-
-impl BitOr for Ieee64 {
-    type Output = Ieee64;
-
-    fn bitor(self, rhs: Self) -> Self::Output {
-        Self::with_bits(self.bits() | rhs.bits())
-    }
-}
-
-impl BitXor for Ieee64 {
-    type Output = Ieee64;
-
-    fn bitxor(self, rhs: Self) -> Self::Output {
-        Self::with_bits(self.bits() ^ rhs.bits())
-    }
-}
-
-impl Not for Ieee64 {
-    type Output = Ieee64;
-
-    fn not(self) -> Self::Output {
-        Self::with_bits(!self.bits())
-    }
-}
-
-impl Ieee128 {
-    const SIGNIFICAND_BITS: u8 = 112;
-    const EXPONENT_BITS: u8 = 15;
-    const SIGN_MASK: u128 = 1 << (Self::EXPONENT_BITS + Self::SIGNIFICAND_BITS);
-    const SIGNIFICAND_MASK: u128 = u128::MAX >> (Self::EXPONENT_BITS + 1);
-    const EXPONENT_MASK: u128 = !Self::SIGN_MASK & !Self::SIGNIFICAND_MASK;
-
-    /// Create a new `Ieee128` containing the bits of `x`.
-    pub fn with_bits(x: u128) -> Self {
-        Self(x)
-    }
-
-    /// Get the bitwise representation.
-    pub fn bits(self) -> u128 {
-        self.0
-    }
-
-    /// Computes the absolute value of self.
-    pub fn abs(self) -> Self {
-        Self::with_bits(self.bits() & !Self::SIGN_MASK)
-    }
-
-    /// Returns a number composed of the magnitude of self and the sign of sign.
-    pub fn copysign(self, sign: Self) -> Self {
-        Self::with_bits((self.bits() & !Self::SIGN_MASK) | (sign.bits() & Self::SIGN_MASK))
-    }
-
-    /// Returns true if self is positive or negative zero
-    pub fn is_zero(&self) -> bool {
-        self.partial_cmp(&Self::with_bits(0)) == Some(Ordering::Equal)
-    }
-}
-
-impl PartialOrd for Ieee128 {
-    fn partial_cmp(&self, rhs: &Self) -> Option<Ordering> {
-        // FIXME(#8312): Use Rust `f128` comparisons once `f128` support is stabalised.
-        let lhs_abs = self.bits() & !Self::SIGN_MASK;
-        let rhs_abs = rhs.bits() & !Self::SIGN_MASK;
-        if (lhs_abs & Self::EXPONENT_MASK == Self::EXPONENT_MASK
-            && lhs_abs & Self::SIGNIFICAND_MASK != 0)
-            && (rhs_abs & Self::EXPONENT_MASK == Self::EXPONENT_MASK
-                && rhs_abs & Self::SIGNIFICAND_MASK != 0)
-        {
-            // One of the floats is a NaN.
-            return None;
-        }
-        if lhs_abs == 0 && rhs_abs == 0 {
-            // Zeros are always equal regardless of sign.
-            return Some(Ordering::Equal);
-        }
-        let lhs_positive = self.bits() & Self::SIGN_MASK == 0;
-        let rhs_positive = rhs.bits() & Self::SIGN_MASK == 0;
-        if lhs_positive != rhs_positive {
-            // Different signs: negative < positive
-            return lhs_positive.partial_cmp(&rhs_positive);
-        }
-        // Finite or infinity will order correctly with an integer comparison of the bits.
-        if lhs_positive {
-            lhs_abs.partial_cmp(&rhs_abs)
-        } else {
-            // Reverse the comparison when both floats are negative.
-            rhs_abs.partial_cmp(&lhs_abs)
-        }
-    }
-}
-
-impl Display for Ieee128 {
-    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
-        let bits: u128 = self.0;
-        format_float(bits, Self::EXPONENT_BITS, Self::SIGNIFICAND_BITS, f)
-    }
-}
-
-impl FromStr for Ieee128 {
-    type Err = &'static str;
-
-    fn from_str(s: &str) -> Result<Self, &'static str> {
-        match parse_float(s, Self::EXPONENT_BITS, Self::SIGNIFICAND_BITS) {
-            Ok(b) => Ok(Self(b)),
-            Err(s) => Err(s),
-        }
-    }
-}
-
-impl IntoBytes for Ieee128 {
-    fn into_bytes(self) -> Vec<u8> {
-        self.bits().to_le_bytes().to_vec()
-    }
-}
-
-impl Neg for Ieee128 {
-    type Output = Self;
-
-    fn neg(self) -> Self {
-        Self::with_bits(self.bits() ^ Self::SIGN_MASK)
-    }
-}
-
 #[cfg(test)]
 mod tests {
     use super::*;
     use alloc::string::ToString;
-    use core::mem;
     use core::{f32, f64};
 
     #[test]
@@ -1625,11 +1257,11 @@ mod tests {
             "-NaN"
         );
         // Construct some qNaNs with payloads.
-        assert_eq!(Ieee16(0x7e01).to_string(), "+NaN:0x1");
-        assert_eq!(Ieee16(0x7f01).to_string(), "+NaN:0x101");
+        assert_eq!(Ieee16::with_bits(0x7e01).to_string(), "+NaN:0x1");
+        assert_eq!(Ieee16::with_bits(0x7f01).to_string(), "+NaN:0x101");
         // Signaling NaNs.
-        assert_eq!(Ieee16(0x7c01).to_string(), "+sNaN:0x1");
-        assert_eq!(Ieee16(0x7d01).to_string(), "+sNaN:0x101");
+        assert_eq!(Ieee16::with_bits(0x7c01).to_string(), "+sNaN:0x1");
+        assert_eq!(Ieee16::with_bits(0x7d01).to_string(), "+sNaN:0x101");
     }
 
     #[test]
@@ -1703,6 +1335,32 @@ mod tests {
         parse_err::<Ieee16>("sNaN:0x301", "Invalid sNaN payload");
     }
 
+    #[test]
+    fn pow2_ieee16() {
+        assert_eq!(Ieee16::pow2(0).to_string(), "0x1.000p0");
+        assert_eq!(Ieee16::pow2(1).to_string(), "0x1.000p1");
+        assert_eq!(Ieee16::pow2(-1).to_string(), "0x1.000p-1");
+        assert_eq!(Ieee16::pow2(15).to_string(), "0x1.000p15");
+        assert_eq!(Ieee16::pow2(-14).to_string(), "0x1.000p-14");
+
+        assert_eq!((-Ieee16::pow2(1)).to_string(), "-0x1.000p1");
+    }
+
+    #[test]
+    fn fcvt_to_sint_negative_overflow_ieee16() {
+        // FIXME(#8312): Replace with commented out version once Rust f16 support is stabilised.
+        // let n = 8;
+        // assert_eq!(
+        //     -((1u16 << (n - 1)) as f16) - 1.0,
+        //     Ieee16::fcvt_to_sint_negative_overflow(n).as_f16()
+        // );
+        let n = 8;
+        assert_eq!(
+            "-0x1.020p7",
+            Ieee16::fcvt_to_sint_negative_overflow(n).to_string()
+        );
+    }
+
     #[test]
     fn format_ieee32() {
         assert_eq!(Ieee32::with_float(0.0).to_string(), "0.0");
@@ -1735,11 +1393,11 @@ mod tests {
         assert_eq!(Ieee32::with_float(f32::NAN).to_string(), "+NaN");
         assert_eq!(Ieee32::with_float(-f32::NAN).to_string(), "-NaN");
         // Construct some qNaNs with payloads.
-        assert_eq!(Ieee32(0x7fc00001).to_string(), "+NaN:0x1");
-        assert_eq!(Ieee32(0x7ff00001).to_string(), "+NaN:0x300001");
+        assert_eq!(Ieee32::with_bits(0x7fc00001).to_string(), "+NaN:0x1");
+        assert_eq!(Ieee32::with_bits(0x7ff00001).to_string(), "+NaN:0x300001");
         // Signaling NaNs.
-        assert_eq!(Ieee32(0x7f800001).to_string(), "+sNaN:0x1");
-        assert_eq!(Ieee32(0x7fa00001).to_string(), "+sNaN:0x200001");
+        assert_eq!(Ieee32::with_bits(0x7f800001).to_string(), "+sNaN:0x1");
+        assert_eq!(Ieee32::with_bits(0x7fa00001).to_string(), "+sNaN:0x200001");
     }
 
     #[test]
@@ -1821,15 +1479,17 @@ mod tests {
         assert_eq!(Ieee32::pow2(127).to_string(), "0x1.000000p127");
         assert_eq!(Ieee32::pow2(-126).to_string(), "0x1.000000p-126");
 
-        assert_eq!(Ieee32::pow2(1).neg().to_string(), "-0x1.000000p1");
+        assert_eq!((-Ieee32::pow2(1)).to_string(), "-0x1.000000p1");
     }
 
     #[test]
     fn fcvt_to_sint_negative_overflow_ieee32() {
-        for n in &[8, 16] {
-            assert_eq!(-((1u32 << (n - 1)) as f32) - 1.0, unsafe {
-                mem::transmute(Ieee32::fcvt_to_sint_negative_overflow(*n))
-            });
+        for n in [8, 16] {
+            assert_eq!(
+                -((1u32 << (n - 1)) as f32) - 1.0,
+                Ieee32::fcvt_to_sint_negative_overflow(n).as_f32(),
+                "n = {n}"
+            );
         }
     }
 
@@ -1871,15 +1531,21 @@ mod tests {
         assert_eq!(Ieee64::with_float(f64::NAN).to_string(), "+NaN");
         assert_eq!(Ieee64::with_float(-f64::NAN).to_string(), "-NaN");
         // Construct some qNaNs with payloads.
-        assert_eq!(Ieee64(0x7ff8000000000001).to_string(), "+NaN:0x1");
         assert_eq!(
-            Ieee64(0x7ffc000000000001).to_string(),
+            Ieee64::with_bits(0x7ff8000000000001).to_string(),
+            "+NaN:0x1"
+        );
+        assert_eq!(
+            Ieee64::with_bits(0x7ffc000000000001).to_string(),
             "+NaN:0x4000000000001"
         );
         // Signaling NaNs.
-        assert_eq!(Ieee64(0x7ff0000000000001).to_string(), "+sNaN:0x1");
         assert_eq!(
-            Ieee64(0x7ff4000000000001).to_string(),
+            Ieee64::with_bits(0x7ff0000000000001).to_string(),
+            "+sNaN:0x1"
+        );
+        assert_eq!(
+            Ieee64::with_bits(0x7ff4000000000001).to_string(),
             "+sNaN:0x4000000000001"
         );
     }
@@ -1959,15 +1625,17 @@ mod tests {
         assert_eq!(Ieee64::pow2(1023).to_string(), "0x1.0000000000000p1023");
         assert_eq!(Ieee64::pow2(-1022).to_string(), "0x1.0000000000000p-1022");
 
-        assert_eq!(Ieee64::pow2(1).neg().to_string(), "-0x1.0000000000000p1");
+        assert_eq!((-Ieee64::pow2(1)).to_string(), "-0x1.0000000000000p1");
     }
 
     #[test]
     fn fcvt_to_sint_negative_overflow_ieee64() {
-        for n in &[8, 16, 32] {
-            assert_eq!(-((1u64 << (n - 1)) as f64) - 1.0, unsafe {
-                mem::transmute(Ieee64::fcvt_to_sint_negative_overflow(*n))
-            });
+        for n in [8, 16, 32] {
+            assert_eq!(
+                -((1u64 << (n - 1)) as f64) - 1.0,
+                Ieee64::fcvt_to_sint_negative_overflow(n).as_f64(),
+                "n = {n}"
+            );
         }
     }
 
@@ -2037,20 +1705,20 @@ mod tests {
         );
         // Construct some qNaNs with payloads.
         assert_eq!(
-            Ieee128(0x7fff8000000000000000000000000001).to_string(),
+            Ieee128::with_bits(0x7fff8000000000000000000000000001).to_string(),
             "+NaN:0x1"
         );
         assert_eq!(
-            Ieee128(0x7fffc000000000000000000000000001).to_string(),
+            Ieee128::with_bits(0x7fffc000000000000000000000000001).to_string(),
             "+NaN:0x4000000000000000000000000001"
         );
         // Signaling NaNs.
         assert_eq!(
-            Ieee128(0x7fff0000000000000000000000000001).to_string(),
+            Ieee128::with_bits(0x7fff0000000000000000000000000001).to_string(),
             "+sNaN:0x1"
         );
         assert_eq!(
-            Ieee128(0x7fff4000000000000000000000000001).to_string(),
+            Ieee128::with_bits(0x7fff4000000000000000000000000001).to_string(),
             "+sNaN:0x4000000000000000000000000001"
         );
     }
@@ -2154,4 +1822,57 @@ mod tests {
             "Invalid sNaN payload",
         );
     }
+
+    #[test]
+    fn pow2_ieee128() {
+        assert_eq!(
+            Ieee128::pow2(0).to_string(),
+            "0x1.0000000000000000000000000000p0"
+        );
+        assert_eq!(
+            Ieee128::pow2(1).to_string(),
+            "0x1.0000000000000000000000000000p1"
+        );
+        assert_eq!(
+            Ieee128::pow2(-1).to_string(),
+            "0x1.0000000000000000000000000000p-1"
+        );
+        assert_eq!(
+            Ieee128::pow2(16383).to_string(),
+            "0x1.0000000000000000000000000000p16383"
+        );
+        assert_eq!(
+            Ieee128::pow2(-16382).to_string(),
+            "0x1.0000000000000000000000000000p-16382"
+        );
+
+        assert_eq!(
+            (-Ieee128::pow2(1)).to_string(),
+            "-0x1.0000000000000000000000000000p1"
+        );
+    }
+
+    #[test]
+    fn fcvt_to_sint_negative_overflow_ieee128() {
+        // FIXME(#8312): Replace with commented out version once Rust f128 support is stabilised.
+        // for n in [8, 16, 32, 64] {
+        //     assert_eq!(
+        //         -((1u128 << (n - 1)) as f128) - 1.0,
+        //         Ieee128::fcvt_to_sint_negative_overflow(n).as_f128(),
+        //         "n = {n}"
+        //     );
+        // }
+        for (n, expected) in [
+            (8, "-0x1.0200000000000000000000000000p7"),
+            (16, "-0x1.0002000000000000000000000000p15"),
+            (32, "-0x1.0000000200000000000000000000p31"),
+            (64, "-0x1.0000000000000002000000000000p63"),
+        ] {
+            assert_eq!(
+                expected,
+                Ieee128::fcvt_to_sint_negative_overflow(n).to_string(),
+                "n = {n}"
+            );
+        }
+    }
 }
diff --git a/cranelift/codegen/src/isa/x64/inst/emit.rs b/cranelift/codegen/src/isa/x64/inst/emit.rs
index d46e18a40..45ae37c96 100644
--- a/cranelift/codegen/src/isa/x64/inst/emit.rs
+++ b/cranelift/codegen/src/isa/x64/inst/emit.rs
@@ -3577,7 +3577,7 @@ pub(crate) fn emit(
                 let output_bits = dst_size.to_bits();
                 match *src_size {
                     OperandSize::Size32 => {
-                        let cst = Ieee32::pow2(output_bits - 1).neg().bits();
+                        let cst = (-Ieee32::pow2(output_bits - 1)).bits();
                         let inst = Inst::imm(OperandSize::Size32, cst as u64, tmp_gpr);
                         inst.emit(sink, info, state);
                     }
@@ -3588,7 +3588,7 @@ pub(crate) fn emit(
                             no_overflow_cc = CC::NBE; // >
                             Ieee64::fcvt_to_sint_negative_overflow(output_bits)
                         } else {
-                            Ieee64::pow2(output_bits - 1).neg()
+                            -Ieee64::pow2(output_bits - 1)
                         };
                         let inst = Inst::imm(OperandSize::Size64, cst.bits(), tmp_gpr);
                         inst.emit(sink, info, state);
diff --git a/cranelift/codegen/src/isle_prelude.rs b/cranelift/codegen/src/isle_prelude.rs
index 530cfb139..ce6879cb7 100644
--- a/cranelift/codegen/src/isle_prelude.rs
+++ b/cranelift/codegen/src/isle_prelude.rs
@@ -1007,7 +1007,7 @@ macro_rules! isle_common_prelude_methods {
         }
 
         fn f32_neg(&mut self, n: Ieee32) -> Ieee32 {
-            n.neg()
+            -n
         }
 
         fn f32_abs(&mut self, n: Ieee32) -> Ieee32 {
@@ -1075,7 +1075,7 @@ macro_rules! isle_common_prelude_methods {
         }
 
         fn f64_neg(&mut self, n: Ieee64) -> Ieee64 {
-            n.neg()
+            -n
         }
 
         fn f64_abs(&mut self, n: Ieee64) -> Ieee64 {
diff --git a/cranelift/codegen/src/verifier/mod.rs b/cranelift/codegen/src/verifier/mod.rs
index 457c9e3a9..f750280f6 100644
--- a/cranelift/codegen/src/verifier/mod.rs
+++ b/cranelift/codegen/src/verifier/mod.rs
@@ -1929,7 +1929,7 @@ mod tests {
         // Build instruction "f64const 0.0" (missing one required result)
         let inst = func.dfg.make_inst(InstructionData::UnaryIeee64 {
             opcode: Opcode::F64const,
-            imm: 0.into(),
+            imm: 0.0.into(),
         });
         func.layout.append_inst(inst, block0);
 
diff --git a/cranelift/interpreter/src/value.rs b/cranelift/interpreter/src/value.rs
index d72ee5714..737e6a008 100644
--- a/cranelift/interpreter/src/value.rs
+++ b/cranelift/interpreter/src/value.rs
@@ -4,6 +4,7 @@
 #![allow(trivial_numeric_casts)]
 
 use core::fmt::{self, Display, Formatter};
+use core::ops::Neg;
 use cranelift_codegen::data_value::{DataValue, DataValueCastFailure};
 use cranelift_codegen::ir::immediates::{Ieee128, Ieee16, Ieee32, Ieee64};
 use cranelift_codegen::ir::{types, Type};
diff --git a/pulley/Cargo.toml b/pulley/Cargo.toml
index ff8c0dd55..fc9401c17 100644
--- a/pulley/Cargo.toml
+++ b/pulley/Cargo.toml
@@ -14,6 +14,7 @@ workspace = true
 [dependencies]
 arbitrary = { workspace = true, optional = true }
 log = { workspace = true }
+sptr = { workspace = true }
 
 [dev-dependencies]
 env_logger = { workspace = true }
diff --git a/pulley/src/decode.rs b/pulley/src/decode.rs
index 9d609ca44..dcc219836 100644
--- a/pulley/src/decode.rs
+++ b/pulley/src/decode.rs
@@ -402,7 +402,7 @@ impl Decoder {
         V: OpVisitor<BytecodeStream = SafeBytecodeStream<'a>> + ExtendedOpVisitor,
     {
         let mut decoder = Decoder::new();
-        let mut results = vec![];
+        let mut results = Vec::new();
 
         while !visitor.bytecode().as_slice().is_empty() {
             results.push(decoder.decode_one(visitor)?);
diff --git a/pulley/src/interp.rs b/pulley/src/interp.rs
index efd5fa022..d341aa7ec 100644
--- a/pulley/src/interp.rs
+++ b/pulley/src/interp.rs
@@ -6,8 +6,10 @@ use crate::regs::*;
 use crate::ExtendedOpcode;
 use alloc::string::ToString;
 use alloc::{vec, vec::Vec};
+use core::fmt;
 use core::mem;
 use core::ptr::{self, NonNull};
+use sptr::Strict;
 
 const DEFAULT_STACK_SIZE: usize = 1 << 20; // 1 MiB
 
@@ -192,18 +194,94 @@ pub enum Val {
     VReg(VRegVal),
 }
 
+impl fmt::LowerHex for Val {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Val::XReg(v) => fmt::LowerHex::fmt(v, f),
+            Val::FReg(v) => fmt::LowerHex::fmt(v, f),
+            Val::VReg(v) => fmt::LowerHex::fmt(v, f),
+        }
+    }
+}
+
+impl From<XRegVal> for Val {
+    fn from(value: XRegVal) -> Self {
+        Val::XReg(value)
+    }
+}
+
+impl From<u64> for Val {
+    fn from(value: u64) -> Self {
+        XRegVal::new_u64(value).into()
+    }
+}
+
+impl From<u32> for Val {
+    fn from(value: u32) -> Self {
+        XRegVal::new_u32(value).into()
+    }
+}
+
+impl From<i64> for Val {
+    fn from(value: i64) -> Self {
+        XRegVal::new_i64(value).into()
+    }
+}
+
+impl From<i32> for Val {
+    fn from(value: i32) -> Self {
+        XRegVal::new_i32(value).into()
+    }
+}
+
+impl<T> From<*mut T> for Val {
+    fn from(value: *mut T) -> Self {
+        XRegVal::new_ptr(value).into()
+    }
+}
+
+impl From<FRegVal> for Val {
+    fn from(value: FRegVal) -> Self {
+        Val::FReg(value)
+    }
+}
+
+impl From<f64> for Val {
+    fn from(value: f64) -> Self {
+        FRegVal::new_f64(value).into()
+    }
+}
+
+impl From<f32> for Val {
+    fn from(value: f32) -> Self {
+        FRegVal::new_f32(value).into()
+    }
+}
+
+impl From<VRegVal> for Val {
+    fn from(value: VRegVal) -> Self {
+        Val::VReg(value)
+    }
+}
+
 /// 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 {
+impl fmt::Debug for XRegVal {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         f.debug_struct("XRegVal")
             .field("as_u64", &self.get_u64())
             .finish()
     }
 }
 
+impl fmt::LowerHex for XRegVal {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        fmt::LowerHex::fmt(&self.get_u64(), f)
+    }
+}
+
 // 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)]
@@ -212,8 +290,7 @@ union XRegUnion {
     u32: u32,
     i64: i64,
     u64: u64,
-    isize: isize,
-    usize: usize,
+    ptr: *mut u8,
 }
 
 impl Default for XRegVal {
@@ -248,76 +325,55 @@ impl XRegVal {
         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 {
+    pub fn new_ptr<T>(ptr: *mut T) -> Self {
         let mut val = XRegVal::default();
-        val.set_usize(x);
+        val.set_ptr(ptr);
         val
     }
 
     pub fn get_i32(&self) -> i32 {
         let x = unsafe { self.0.i32 };
-        i32::from_le_bytes(x.to_ne_bytes())
+        i32::from_le(x)
     }
 
     pub fn get_u32(&self) -> u32 {
         let x = unsafe { self.0.u32 };
-        u32::from_le_bytes(x.to_ne_bytes())
+        u32::from_le(x)
     }
 
     pub fn get_i64(&self) -> i64 {
         let x = unsafe { self.0.i64 };
-        i64::from_le_bytes(x.to_ne_bytes())
+        i64::from_le(x)
     }
 
     pub fn get_u64(&self) -> u64 {
         let x = unsafe { self.0.u64 };
-        u64::from_le_bytes(x.to_ne_bytes())
+        u64::from_le(x)
     }
 
-    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 get_ptr<T>(&self) -> *mut T {
+        let ptr = unsafe { self.0.ptr };
+        Strict::map_addr(ptr, |p| usize::from_le(p)).cast()
     }
 
     pub fn set_i32(&mut self, x: i32) {
-        let x = i32::from_ne_bytes(x.to_le_bytes());
-        self.0.i32 = x;
+        self.0.i32 = x.to_le();
     }
 
     pub fn set_u32(&mut self, x: u32) {
-        let x = u32::from_ne_bytes(x.to_le_bytes());
-        self.0.u32 = x;
+        self.0.u32 = x.to_le();
     }
 
     pub fn set_i64(&mut self, x: i64) {
-        let x = i64::from_ne_bytes(x.to_le_bytes());
-        self.0.i64 = x;
+        self.0.i64 = x.to_le();
     }
 
     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;
+        self.0.u64 = x.to_le();
     }
 
-    pub fn set_usize(&mut self, x: usize) {
-        let x = usize::from_ne_bytes(x.to_le_bytes());
-        self.0.usize = x;
+    pub fn set_ptr<T>(&mut self, ptr: *mut T) {
+        self.0.ptr = Strict::map_addr(ptr, |p| p.to_le()).cast();
     }
 }
 
@@ -325,8 +381,8 @@ impl XRegVal {
 #[derive(Copy, Clone)]
 pub struct FRegVal(FRegUnion);
 
-impl core::fmt::Debug for FRegVal {
-    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+impl fmt::Debug for FRegVal {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         f.debug_struct("FRegVal")
             .field("as_f32", &self.get_f32())
             .field("as_f64", &self.get_f64())
@@ -334,6 +390,12 @@ impl core::fmt::Debug for FRegVal {
     }
 }
 
+impl fmt::LowerHex for FRegVal {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        fmt::LowerHex::fmt(&self.get_f64().to_bits(), f)
+    }
+}
+
 // 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)]
@@ -385,14 +447,20 @@ impl FRegVal {
 #[derive(Copy, Clone)]
 pub struct VRegVal(VRegUnion);
 
-impl core::fmt::Debug for VRegVal {
-    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+impl fmt::Debug for VRegVal {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         f.debug_struct("VRegVal")
             .field("as_u128", &unsafe { self.0.u128 })
             .finish()
     }
 }
 
+impl fmt::LowerHex for VRegVal {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        fmt::LowerHex::fmt(unsafe { &self.0.u128 }, f)
+    }
+}
+
 #[derive(Copy, Clone)]
 union VRegUnion {
     // TODO: need to figure out how we are going to handle portability of lane
@@ -418,8 +486,8 @@ pub struct MachineState {
 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 {
+impl fmt::Debug for MachineState {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         let MachineState {
             x_regs,
             f_regs,
@@ -429,8 +497,8 @@ impl core::fmt::Debug for MachineState {
 
         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 {
+        impl<R: fmt::Debug> fmt::Debug for RegMap<'_, R> {
+            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
                 let mut f = f.debug_map();
                 for (i, r) in self.0.iter().enumerate() {
                     f.entry(&(self.1)(i as u8), r);
@@ -466,8 +534,13 @@ impl MachineState {
             stack,
         };
 
-        let sp = state.stack.last().unwrap() as *const u8 as usize;
-        state.set_x(XReg::SP, XRegVal::new_usize(sp));
+        // Take care to construct SP such that we preserve pointer provenance
+        // for the whole stack.
+        let len = state.stack.len();
+        let sp = &mut state.stack[..];
+        let sp = sp.as_mut_ptr();
+        let sp = unsafe { sp.add(len) };
+        state.set_x(XReg::SP, XRegVal::new_ptr(sp));
 
         state.set_x(XReg::FP, XRegVal::new_i64(-1));
         state.set_x(XReg::LR, XRegVal::new_i64(-1));
@@ -602,7 +675,7 @@ impl OpVisitor for InterpreterVisitor<'_> {
         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;
+            let return_addr = self.state.x(XReg::LR).get_ptr();
             self.pc = unsafe { UnsafeBytecodeStream::new(return_addr) };
             // log::trace!("returning to {return_addr:#p}");
             Continuation::Continue
@@ -834,99 +907,100 @@ impl OpVisitor for InterpreterVisitor<'_> {
     }
 
     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) };
+        let ptr = self.state.x(ptr).get_ptr::<u32>();
+        let val = unsafe { ptr::read_unaligned(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) };
+        let ptr = self.state.x(ptr).get_ptr::<i32>();
+        let val = unsafe { ptr::read_unaligned(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) };
+        let ptr = self.state.x(ptr).get_ptr::<u64>();
+        let val = unsafe { ptr::read_unaligned(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) };
+        let val = unsafe {
+            self.state
+                .x(ptr)
+                .get_ptr::<u32>()
+                .byte_offset(offset.into())
+                .read_unaligned()
+        };
         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) };
+        let val = unsafe {
+            self.state
+                .x(ptr)
+                .get_ptr::<i32>()
+                .byte_offset(offset.into())
+                .read_unaligned()
+        };
         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) };
+        let val = unsafe {
+            self.state
+                .x(ptr)
+                .get_ptr::<u64>()
+                .byte_offset(offset.into())
+                .read_unaligned()
+        };
         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 ptr = self.state.x(ptr).get_ptr::<u32>();
         let val = self.state.x(src).get_u32();
         unsafe {
-            ptr::write(ptr, val);
+            ptr::write_unaligned(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 ptr = self.state.x(ptr).get_ptr::<u64>();
         let val = self.state.x(src).get_u64();
         unsafe {
-            ptr::write(ptr, val);
+            ptr::write_unaligned(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);
+            self.state
+                .x(ptr)
+                .get_ptr::<u32>()
+                .byte_offset(offset.into())
+                .write_unaligned(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);
+            self.state
+                .x(ptr)
+                .get_ptr::<u64>()
+                .byte_offset(offset.into())
+                .write_unaligned(val);
         }
         Continuation::Continue
     }
diff --git a/pulley/src/opcode.rs b/pulley/src/opcode.rs
index 922cbf26a..3b79481e0 100644
--- a/pulley/src/opcode.rs
+++ b/pulley/src/opcode.rs
@@ -87,6 +87,7 @@ macro_rules! define_extended_opcode {
 for_each_extended_op!(define_extended_opcode);
 
 impl ExtendedOpcode {
+    #[cfg_attr(not(feature = "interp"), allow(unused))]
     pub(crate) const ENCODED_SIZE_OF_TRAP: usize = 3;
 
     /// Create a new `ExtendedOpcode` from the given bytes.


Test patch:
diff --git a/ci/build-test-matrix.js b/ci/build-test-matrix.js
index 18692260e..f1236ed9f 100644
--- a/ci/build-test-matrix.js
+++ b/ci/build-test-matrix.js
@@ -121,7 +121,27 @@ const FULL_MATRIX = [
     "name": "Test Linux riscv64",
     "filter": "linux-riscv64",
     "isa": "riscv64",
-  }
+  },
+  {
+    "name": "Test Pulley on i686-unknown-linux-gnu",
+    "32-bit": true,
+    "os": "ubuntu-latest",
+    "target": "i686-unknown-linux-gnu",
+    "gcc_package": "gcc-i686-linux-gnu",
+    "gcc": "i686-linux-gnu-gcc",
+    "filter": "pulley",
+  },
+  {
+    "name": "Test Pulley on armv7-unknown-linux-gnueabihf",
+    "32-bit": true,
+    "os": "ubuntu-latest",
+    "target": "armv7-unknown-linux-gnueabihf",
+    "gcc_package": "gcc-arm-linux-gnueabihf",
+    "gcc": "arm-linux-gnueabihf-gcc",
+    "qemu": "qemu-arm -L /usr/arm-linux-gnueabihf -E LD_LIBRARY_PATH=/usr/arm-linux-gnueabihf/lib",
+    "qemu_target": "arm-linux-user",
+    "filter": "pulley",
+  },
 ];
 
 /// Get the workspace's full list of member crates.
@@ -198,6 +218,20 @@ async function shard(configs) {
   // created above.
   const sharded = [];
   for (const config of configs) {
+    // Special case 32-bit configs. Only Pulley runs on 32-bit targets.
+    if (config["32-bit"] === true) {
+      sharded.push(Object.assign(
+        {},
+        config,
+        {
+          bucket: members
+            .map(c => c.indexOf("pulley") !== -1 ? `--package ${c}` : `--exclude ${c}`)
+            .join(" "),
+        }
+      ));
+      continue;
+    }
+
     for (const bucket of buckets) {
       sharded.push(Object.assign(
         {},
@@ -255,6 +289,11 @@ async function main() {
       return config.isa !== undefined;
     }
 
+    // If any Pulley source file was modified, then run Pulley-specific configs.
+    if (names.includes("pulley") && config.filter == "pulley") {
+      return true;
+    }
+
     // If the commit explicitly asks for this test config, then include it.
     if (config.filter && commits.includes(`prtest:${config.filter}`)) {
       return true;
diff --git a/pulley/tests/all/interp.rs b/pulley/tests/all/interp.rs
index 132d84ef0..57946f395 100644
--- a/pulley/tests/all/interp.rs
+++ b/pulley/tests/all/interp.rs
@@ -1,5 +1,6 @@
 //! Interpreter tests.
 
+use interp::Val;
 use pulley_interpreter::{interp::Vm, *};
 use std::{cell::UnsafeCell, fmt::Debug, ptr::NonNull};
 
@@ -15,29 +16,32 @@ fn encoded(ops: &[Op]) -> Vec<u8> {
 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]), &[], [])?;
+    let _ = vm.call(NonNull::from(&ops[..]).cast(), &[], [])?;
     Ok(())
 }
 
-unsafe fn assert_one<R0, R1>(
-    xs: impl IntoIterator<Item = (R0, u64)>,
+unsafe fn assert_one<R0, R1, V>(
+    xs: impl IntoIterator<Item = (R0, V)>,
     op: impl Into<Op> + Debug,
     result: R1,
     expected: u64,
 ) where
     R0: Into<AnyReg>,
     R1: Into<AnyReg>,
+    V: Into<Val>,
 {
     eprintln!("=======================================================");
     let mut vm = Vm::new();
 
     for (reg, val) in xs {
         let reg = reg.into();
+        let val = val.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!(),
+        match (reg, val) {
+            (AnyReg::X(r), Val::XReg(v)) => *vm.state_mut().x_mut(r) = v,
+            (AnyReg::F(r), Val::FReg(v)) => *vm.state_mut().f_mut(r) = v,
+            (AnyReg::V(_), Val::VReg(_)) => todo!(),
+            (kind, val) => panic!("register kind and value mismatch: {kind:?} and {val:?}"),
         }
     }
 
@@ -71,7 +75,7 @@ fn xconst8() {
     for (expected, imm) in [(42u64, 42u8), (u64::from(u8::MAX), u8::MAX)] {
         unsafe {
             assert_one(
-                [(x(0), 0x1234567812345678)],
+                [(x(0), 0x1234567812345678u64)],
                 Xconst8 { dst: x(0), imm },
                 x(0),
                 expected,
@@ -85,7 +89,7 @@ fn xconst16() {
     for (expected, imm) in [(42u64, 42u16), (u64::from(u16::MAX), u16::MAX)] {
         unsafe {
             assert_one(
-                [(x(0), 0x1234567812345678)],
+                [(x(0), 0x1234567812345678u64)],
                 Xconst16 { dst: x(0), imm },
                 x(0),
                 expected,
@@ -99,7 +103,7 @@ fn xconst32() {
     for (expected, imm) in [(42u64, 42u32), (u64::from(u32::MAX), u32::MAX)] {
         unsafe {
             assert_one(
-                [(x(0), 0x1234567812345678)],
+                [(x(0), 0x1234567812345678u64)],
                 Xconst32 { dst: x(0), imm },
                 x(0),
                 expected,
@@ -113,7 +117,7 @@ fn xconst64() {
     for (expected, imm) in [(42u64, 42u64), (u64::MAX, u64::MAX)] {
         unsafe {
             assert_one(
-                [(x(0), 0x1234567812345678)],
+                [(x(0), 0x1234567812345678u64)],
                 Xconst64 { dst: x(0), imm },
                 x(0),
                 expected,
@@ -491,19 +495,19 @@ fn load32_u() {
     let a = UnsafeCell::new(11u32);
     let b = UnsafeCell::new(22u32);
     let c = UnsafeCell::new(33u32);
-    let d = UnsafeCell::new(i32::MIN);
+    let d = UnsafeCell::new(i32::MIN as u32);
 
     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),
+        (11, a.get()),
+        (22, b.get()),
+        (33, c.get()),
+        (i32::MIN as u32 as u64, d.get()),
     ] {
         unsafe {
             assert_one(
                 [
-                    (x(0), 0x1234567812345678),
-                    (x(1), u64::try_from(addr).unwrap()),
+                    (x(0), Val::from(0x1234567812345678u64)),
+                    (x(1), Val::from(addr.cast::<u8>())),
                 ],
                 Load32U {
                     dst: x(0),
@@ -521,19 +525,19 @@ fn load32_s() {
     let a = UnsafeCell::new(11u32);
     let b = UnsafeCell::new(22u32);
     let c = UnsafeCell::new(33u32);
-    let d = UnsafeCell::new(-1i32);
+    let d = UnsafeCell::new(-1i32 as u32);
 
     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),
+        (11, a.get()),
+        (22, b.get()),
+        (33, c.get()),
+        (-1i64 as u64, d.get()),
     ] {
         unsafe {
             assert_one(
                 [
-                    (x(0), 0x1234567812345678),
-                    (x(1), u64::try_from(addr).unwrap()),
+                    (x(0), Val::from(0x1234567812345678u64)),
+                    (x(1), Val::from(addr.cast::<u8>())),
                 ],
                 Load32S {
                     dst: x(0),
@@ -551,19 +555,19 @@ fn load64() {
     let a = UnsafeCell::new(11u64);
     let b = UnsafeCell::new(22u64);
     let c = UnsafeCell::new(33u64);
-    let d = UnsafeCell::new(-1i64);
+    let d = UnsafeCell::new(-1i64 as u64);
 
     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),
+        (11, a.get()),
+        (22, b.get()),
+        (33, c.get()),
+        (-1i64 as u64, d.get()),
     ] {
         unsafe {
             assert_one(
                 [
-                    (x(0), 0x1234567812345678),
-                    (x(1), u64::try_from(addr).unwrap()),
+                    (x(0), Val::from(0x1234567812345678u64)),
+                    (x(1), Val::from(addr)),
                 ],
                 Load64 {
                     dst: x(0),
@@ -581,23 +585,23 @@ 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]);
+    let d = UnsafeCell::new([i32::MIN as u32, i32::MAX as u32]);
 
     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),
+        (11, a.get(), 0),
+        (22, a.get(), 4),
+        (33, b.get(), 0),
+        (44, b.get(), 4),
+        (55, c.get(), 0),
+        (66, c.get(), 4),
+        (i32::MIN as u32 as u64, d.get(), 0),
+        (i32::MAX as u32 as u64, d.get(), 4),
     ] {
         unsafe {
             assert_one(
                 [
-                    (x(0), 0x1234567812345678),
-                    (x(1), u64::try_from(addr).unwrap()),
+                    (x(0), Val::from(0x1234567812345678u64)),
+                    (x(1), Val::from(addr.cast::<u8>())),
                 ],
                 Load32UOffset8 {
                     dst: x(0),
@@ -616,23 +620,24 @@ 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]);
+    let d = UnsafeCell::new([-1i32 as u32, i32::MAX as u32]);
 
     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),
+        (11, a.get(), 0),
+        (22, a.get(), 4),
+        (33, b.get(), 0),
+        (44, b.get(), 4),
+        (55, c.get(), 0),
+        (55, unsafe { c.get().byte_add(4) }, -4),
+        (66, c.get(), 4),
+        (-1i64 as u64, d.get(), 0),
+        (i32::MAX as u32 as u64, d.get(), 4),
     ] {
         unsafe {
             assert_one(
                 [
-                    (x(0), 0x1234567812345678),
-                    (x(1), u64::try_from(addr).unwrap()),
+                    (x(0), Val::from(0x1234567812345678u64)),
+                    (x(1), Val::from(addr.cast::<u8>())),
                 ],
                 Load32SOffset8 {
                     dst: x(0),
@@ -651,23 +656,23 @@ 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]);
+    let d = UnsafeCell::new([-1i64 as u64, i64::MAX as u64]);
 
     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),
+        (11, a.get(), 0),
+        (22, a.get(), 8),
+        (33, b.get(), 0),
+        (44, b.get(), 8),
+        (55, c.get(), 0),
+        (66, c.get(), 8),
+        (-1i64 as u64, d.get(), 0),
+        (i64::MAX as u64, d.get(), 8),
     ] {
         unsafe {
             assert_one(
                 [
-                    (x(0), 0x1234567812345678),
-                    (x(1), u64::try_from(addr).unwrap()),
+                    (x(0), Val::from(0x1234567812345678u64)),
+                    (x(1), Val::from(addr)),
                 ],
                 Load64Offset8 {
                     dst: x(0),
@@ -689,13 +694,13 @@ fn store32() {
 
     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),
+            (0x11111111u32, a.get()),
+            (0x22222222, b.get().byte_add(4)),
+            (0x33333333, c.get().byte_add(2)),
         ] {
             let val = val as u64;
             assert_one(
-                [(x(0), u64::try_from(addr).unwrap()), (x(1), val)],
+                [(x(0), Val::from(addr)), (x(1), Val::from(val))],
                 Store32 {
                     ptr: x(0),
                     src: x(1),
@@ -733,12 +738,12 @@ fn store64() {
 
     unsafe {
         for (val, addr) in [
-            (0x1111111111111111u64, a.get() as usize),
-            (0x2222222222222222, b.get() as usize),
-            (0x3333333333333333, c.get() as usize),
+            (0x1111111111111111u64, a.get()),
+            (0x2222222222222222, b.get()),
+            (0x3333333333333333, c.get()),
         ] {
             assert_one(
-                [(x(0), u64::try_from(addr).unwrap()), (x(1), val)],
+                [(x(0), Val::from(addr)), (x(1), Val::from(val))],
                 Store64 {
                     ptr: x(0),
                     src: x(1),
@@ -776,13 +781,13 @@ fn store32_offset8() {
 
     unsafe {
         for (val, addr, offset) in [
-            (0x11111111u32, a.get() as usize, 0),
-            (0x22222222, b.get() as usize, 4),
-            (0x33333333, c.get() as usize, 2),
+            (0x11111111u32, a.get(), 0),
+            (0x22222222, b.get(), 4),
+            (0x33333333, c.get(), 2),
         ] {
             let val = val as u64;
             assert_one(
-                [(x(0), u64::try_from(addr).unwrap()), (x(1), val)],
+                [(x(0), Val::from(addr)), (x(1), Val::from(val))],
                 Store32SOffset8 {
                     ptr: x(0),
                     src: x(1),
@@ -819,12 +824,12 @@ fn store64_offset8() {
 
     unsafe {
         for (val, addr, offset) in [
-            (0x1111111111111111u64, a.get() as usize, 0),
-            (0x2222222222222222, a.get() as usize, 8),
-            (0x3333333333333333, a.get() as usize, 16),
+            (0x1111111111111111u64, a.get(), 0),
+            (0x2222222222222222, a.get(), 8),
+            (0x3333333333333333, a.get(), 16),
         ] {
             assert_one(
-                [(x(0), u64::try_from(addr).unwrap()), (x(1), val)],
+                [(x(0), Val::from(addr)), (x(1), Val::from(val))],
                 Store64Offset8 {
                     ptr: x(0),
                     src: x(1),
@@ -868,7 +873,6 @@ fn bitcast_int_from_float_32() {
         f32::EPSILON,
         f32::MIN_POSITIVE,
     ] {
-        let val = val.to_bits() as u64;
         unsafe {
             assert_one(
                 [(f(0), val)],
@@ -877,7 +881,7 @@ fn bitcast_int_from_float_32() {
                     src: f(0),
                 },
                 x(0),
-                val,
+                val.to_bits() as u64,
             );
         }
     }
@@ -897,7 +901,6 @@ fn bitcast_int_from_float_64() {
         f64::EPSILON,
         f64::MIN_POSITIVE,
     ] {
-        let val = val.to_bits();
         unsafe {
             assert_one(
                 [(f(0), val)],
@@ -906,7 +909,7 @@ fn bitcast_int_from_float_64() {
                     src: f(0),
                 },
                 x(0),
-                val,
+                val.to_bits(),
             );
         }
     }