Repository: bytecodealliance/wasmtime
Pull request #5697 (2023-02-02 22:49:05)

Cranelift: Introduce support for `return_call` in the interpreter
Co-Authored-By: Jamey Sharp <jsharp@fastly.com>

<!--

Please ensure that the following steps are all taken care of before submitting
the PR.

- [ ] This has been discussed in issue #..., or if not, please tell us why
  here.
- [ ] A short description of what this does, why it is needed; if the
  description becomes long, the matter should probably be discussed in an issue
  first.
- [ ] This PR contains test cases, if meaningful.
- [ ] A reviewer from the core maintainer team has been assigned for this PR.
  If you don't know who could review this, please indicate so. The list of
  suggested reviewers on the right can help you.

Please ensure all communication adheres to the [code of
conduct](https://github.com/bytecodealliance/wasmtime/blob/master/CODE_OF_CONDUCT.md).
-->



Patch:
diff --git a/cranelift/codegen/src/isle_prelude.rs b/cranelift/codegen/src/isle_prelude.rs
index 2b6f729f7..26788399c 100644
--- a/cranelift/codegen/src/isle_prelude.rs
+++ b/cranelift/codegen/src/isle_prelude.rs
@@ -81,27 +81,42 @@ macro_rules! isle_common_prelude_methods {
         }
 
         #[inline]
-        fn u64_shl(&mut self, x: u64, y: u64) -> u64 {
-            x << y
+        fn imm64_shl(&mut self, ty: Type, x: Imm64, y: Imm64) -> Imm64 {
+            // Mask off any excess shift bits.
+            let shift_mask = (ty.bits() - 1) as u64;
+            let y = (y.bits() as u64) & shift_mask;
+
+            // Mask the result to `ty` bits.
+            let ty_mask = self.ty_mask(ty) as i64;
+            Imm64::new((x.bits() << y) & ty_mask)
         }
 
         #[inline]
         fn imm64_ushr(&mut self, ty: Type, x: Imm64, y: Imm64) -> Imm64 {
-            let shift = u32::checked_sub(64, ty.bits()).unwrap_or(0);
-            let mask = u64::MAX >> shift;
-            let x = (x.bits() as u64) & mask;
-            let y = (y.bits() as u64) & mask;
+            let ty_mask = self.ty_mask(ty);
+            let x = (x.bits() as u64) & ty_mask;
+
+            // Mask off any excess shift bits.
+            let shift_mask = (ty.bits() - 1) as u64;
+            let y = (y.bits() as u64) & shift_mask;
+
+            // NB: No need to mask off high bits because they are already zero.
             Imm64::new((x >> y) as i64)
         }
 
         #[inline]
         fn imm64_sshr(&mut self, ty: Type, x: Imm64, y: Imm64) -> Imm64 {
+            // Sign extend `x` from `ty.bits()`-width to the full 64 bits.
             let shift = u32::checked_sub(64, ty.bits()).unwrap_or(0);
-            let mask = u64::MAX >> shift;
-            let x = (x.bits() as u64) & mask;
-            let y = (y.bits() as u64) & mask;
-            let sext = |v| ((v << shift) as i64) >> shift;
-            Imm64::new(sext(x) >> sext(y))
+            let x = (x.bits() << shift) >> shift;
+
+            // Mask off any excess shift bits.
+            let shift_mask = (ty.bits() - 1) as i64;
+            let y = y.bits() & shift_mask;
+
+            // Mask off sign bits that aren't part of `ty`.
+            let ty_mask = self.ty_mask(ty) as i64;
+            Imm64::new((x >> y) & ty_mask)
         }
 
         #[inline]
@@ -179,14 +194,12 @@ macro_rules! isle_common_prelude_methods {
 
         #[inline]
         fn ty_mask(&mut self, ty: Type) -> u64 {
-            match ty.bits() {
-                1 => 1,
-                8 => 0xff,
-                16 => 0xffff,
-                32 => 0xffff_ffff,
-                64 => 0xffff_ffff_ffff_ffff,
-                _ => unimplemented!(),
-            }
+            let ty_bits = ty.bits();
+            debug_assert_ne!(ty_bits, 0);
+            let shift = 64_u64
+                .checked_sub(ty_bits.into())
+                .expect("unimplemented for > 64 bits");
+            u64::MAX >> shift
         }
 
         fn fits_in_16(&mut self, ty: Type) -> Option<Type> {
diff --git a/cranelift/codegen/src/opts/algebraic.isle b/cranelift/codegen/src/opts/algebraic.isle
index 88c46465c..3b6b067a9 100644
--- a/cranelift/codegen/src/opts/algebraic.isle
+++ b/cranelift/codegen/src/opts/algebraic.isle
@@ -191,15 +191,15 @@
 ;; `(x >> k) << k` is the same as masking off the bottom `k` bits (regardless if
 ;; this is a signed or unsigned shift right).
 (rule (simplify (ishl (fits_in_64 ty)
-                      (ushr ty x (iconst _ (u64_from_imm64 k)))
-                      (iconst _ (u64_from_imm64 k))))
-      (let ((mask u64 (u64_shl 0xFFFFFFFFFFFFFFFF k)))
-        (band ty x (iconst ty (imm64_masked ty mask)))))
+                      (ushr ty x (iconst _ k))
+                      (iconst _ k)))
+      (let ((mask Imm64 (imm64_shl ty (imm64 0xFFFF_FFFF_FFFF_FFFF) k)))
+        (band ty x (iconst ty mask))))
 (rule (simplify (ishl (fits_in_64 ty)
-                      (sshr ty x (iconst _ (u64_from_imm64 k)))
-                      (iconst _ (u64_from_imm64 k))))
-      (let ((mask u64 (u64_shl 0xFFFFFFFFFFFFFFFF k)))
-        (band ty x (iconst ty (imm64_masked ty mask)))))
+                      (sshr ty x (iconst _ k))
+                      (iconst _ k)))
+      (let ((mask Imm64 (imm64_shl ty (imm64 0xFFFF_FFFF_FFFF_FFFF) k)))
+        (band ty x (iconst ty mask))))
 
 ;; Rematerialize ALU-op-with-imm and iconsts in each block where they're
 ;; used. This is neutral (add-with-imm) or positive (iconst) for
diff --git a/cranelift/codegen/src/opts/cprop.isle b/cranelift/codegen/src/opts/cprop.isle
index 38ea51400..ee843087e 100644
--- a/cranelift/codegen/src/opts/cprop.isle
+++ b/cranelift/codegen/src/opts/cprop.isle
@@ -56,18 +56,18 @@
       (subsume (iconst ty (imm64_masked ty (u64_not k)))))
 
 (rule (simplify (ishl (fits_in_64 ty)
-                      (iconst ty (u64_from_imm64 k1))
-                      (iconst ty (u64_from_imm64 k2))))
-      (subsume (iconst ty (imm64_masked ty (u64_shl k1 k2)))))
+                      (iconst ty k1)
+                      (iconst _ k2)))
+      (subsume (iconst ty (imm64_shl ty k1 k2))))
 
 (rule (simplify (ushr (fits_in_64 ty)
                       (iconst ty k1)
-                      (iconst ty k2)))
+                      (iconst _ k2)))
       (subsume (iconst ty (imm64_ushr ty k1 k2))))
 
 (rule (simplify (sshr (fits_in_64 ty)
                       (iconst ty k1)
-                      (iconst ty k2)))
+                      (iconst _ k2)))
       (subsume (iconst ty (imm64_sshr ty k1 k2))))
 
 (rule (simplify
diff --git a/cranelift/codegen/src/prelude.isle b/cranelift/codegen/src/prelude.isle
index 0a0cab580..6f603ff63 100644
--- a/cranelift/codegen/src/prelude.isle
+++ b/cranelift/codegen/src/prelude.isle
@@ -129,8 +129,8 @@
 (decl pure u64_xor (u64 u64) u64)
 (extern constructor u64_xor u64_xor)
 
-(decl pure u64_shl (u64 u64) u64)
-(extern constructor u64_shl u64_shl)
+(decl pure imm64_shl (Type Imm64 Imm64) Imm64)
+(extern constructor imm64_shl imm64_shl)
 
 (decl pure imm64_ushr (Type Imm64 Imm64) Imm64)
 (extern constructor imm64_ushr imm64_ushr)
diff --git a/cranelift/interpreter/src/interpreter.rs b/cranelift/interpreter/src/interpreter.rs
index 85f9fca53..7a5c00a16 100644
--- a/cranelift/interpreter/src/interpreter.rs
+++ b/cranelift/interpreter/src/interpreter.rs
@@ -125,6 +125,11 @@ impl<'a> Interpreter<'a> {
                         .set_all(function.dfg.inst_results(inst), returned_arguments);
                     maybe_inst = layout.next_inst(inst)
                 }
+                ControlFlow::ReturnCall(callee, args) => {
+                    self.state.pop_frame();
+                    let rets = self.call(callee, &args)?.unwrap_return();
+                    return Ok(ControlFlow::Return(rets.into()));
+                }
                 ControlFlow::Return(returned_values) => {
                     self.state.pop_frame();
                     return Ok(ControlFlow::Return(returned_values));
diff --git a/cranelift/interpreter/src/step.rs b/cranelift/interpreter/src/step.rs
index 1bee1a2d1..78868f2af 100644
--- a/cranelift/interpreter/src/step.rs
+++ b/cranelift/interpreter/src/step.rs
@@ -34,6 +34,15 @@ fn validate_signature_params(sig: &[AbiParam], args: &[impl Value]) -> bool {
         })
 }
 
+// Helper for summing a sequence of values.
+fn sum<V: Value>(head: V, tail: SmallVec<[V; 1]>) -> ValueResult<i128> {
+    let mut acc = head;
+    for t in tail {
+        acc = Value::add(acc, t)?;
+    }
+    acc.into_int()
+}
+
 /// Interpret a single Cranelift instruction. Note that program traps and interpreter errors are
 /// distinct: a program trap results in `Ok(Flow::Trap(...))` whereas an interpretation error (e.g.
 /// the types of two values are incompatible) results in `Err(...)`.
@@ -267,14 +276,75 @@ where
         }
     };
 
-    // Helper for summing a sequence of values.
-    fn sum<V: Value>(head: V, tail: SmallVec<[V; 1]>) -> ValueResult<i128> {
-        let mut acc = head;
-        for t in tail {
-            acc = Value::add(acc, t)?;
+    // Perform a call operation.
+    //
+    // The returned `ControlFlow` variant is determined by the given function
+    // argument, which should make either a `ControlFlow::Call` or a
+    // `ControlFlow::ReturnCall`.
+    let do_call = |make_ctrl_flow: fn(&'a Function, SmallVec<[V; 1]>) -> ControlFlow<'a, V>|
+     -> Result<ControlFlow<'a, V>, StepError> {
+        let func_ref = if let InstructionData::Call { func_ref, .. } = inst {
+            func_ref
+        } else {
+            unreachable!()
+        };
+
+        let curr_func = state.get_current_function();
+        let ext_data = curr_func
+            .dfg
+            .ext_funcs
+            .get(func_ref)
+            .ok_or(StepError::UnknownFunction(func_ref))?;
+
+        let signature = if let Some(sig) = curr_func.dfg.signatures.get(ext_data.signature) {
+            sig
+        } else {
+            return Ok(ControlFlow::Trap(CraneliftTrap::User(
+                TrapCode::BadSignature,
+            )));
+        };
+
+        let args = args()?;
+
+        // Check the types of the arguments. This is usually done by the verifier, but nothing
+        // guarantees that the user has ran that.
+        let args_match = validate_signature_params(&signature.params[..], &args[..]);
+        if !args_match {
+            return Ok(ControlFlow::Trap(CraneliftTrap::User(
+                TrapCode::BadSignature,
+            )));
         }
-        acc.into_int()
-    }
+
+        Ok(match ext_data.name {
+            // These functions should be registered in the regular function store
+            ExternalName::User(_) | ExternalName::TestCase(_) => {
+                let function = state
+                    .get_function(func_ref)
+                    .ok_or(StepError::UnknownFunction(func_ref))?;
+
+                make_ctrl_flow(function, args)
+            }
+            ExternalName::LibCall(libcall) => {
+                debug_assert_ne!(inst.opcode(), Opcode::ReturnCall, "Cannot tail call to libcalls");
+                let libcall_handler = state.get_libcall_handler();
+
+                // We don't transfer control to a libcall, we just execute it and return the results
+                let res = libcall_handler(libcall, args);
+                let res = match res {
+                    Err(trap) => return Ok(ControlFlow::Trap(CraneliftTrap::User(trap))),
+                    Ok(rets) => rets,
+                };
+
+                // Check that what the handler returned is what we expect.
+                if validate_signature_params(&signature.returns[..], &res[..]) {
+                    ControlFlow::Assign(res)
+                } else {
+                    ControlFlow::Trap(CraneliftTrap::User(TrapCode::BadSignature))
+                }
+            }
+            ExternalName::KnownSymbol(_) => unimplemented!(),
+        })
+    };
 
     // Interpret a Cranelift instruction.
     Ok(match inst.opcode() {
@@ -328,70 +398,9 @@ where
         Opcode::Trapnz => trap_when(arg(0)?.into_bool()?, CraneliftTrap::User(trap_code())),
         Opcode::ResumableTrapnz => trap_when(arg(0)?.into_bool()?, CraneliftTrap::Resumable),
         Opcode::Return => ControlFlow::Return(args()?),
-        Opcode::Call => {
-            let func_ref = if let InstructionData::Call { func_ref, .. } = inst {
-                func_ref
-            } else {
-                unreachable!()
-            };
-
-            let curr_func = state.get_current_function();
-            let ext_data = curr_func
-                .dfg
-                .ext_funcs
-                .get(func_ref)
-                .ok_or(StepError::UnknownFunction(func_ref))?;
-
-            let signature = if let Some(sig) = curr_func.dfg.signatures.get(ext_data.signature) {
-                sig
-            } else {
-                return Ok(ControlFlow::Trap(CraneliftTrap::User(
-                    TrapCode::BadSignature,
-                )));
-            };
-
-            let args = args()?;
-
-            // Check the types of the arguments. This is usually done by the verifier, but nothing
-            // guarantees that the user has ran that.
-            let args_match = validate_signature_params(&signature.params[..], &args[..]);
-            if !args_match {
-                return Ok(ControlFlow::Trap(CraneliftTrap::User(
-                    TrapCode::BadSignature,
-                )));
-            }
-
-            match ext_data.name {
-                // These functions should be registered in the regular function store
-                ExternalName::User(_) | ExternalName::TestCase(_) => {
-                    let function = state
-                        .get_function(func_ref)
-                        .ok_or(StepError::UnknownFunction(func_ref))?;
-
-                    ControlFlow::Call(function, args)
-                }
-                ExternalName::LibCall(libcall) => {
-                    let libcall_handler = state.get_libcall_handler();
-
-                    // We don't transfer control to a libcall, we just execute it and return the results
-                    let res = libcall_handler(libcall, args);
-                    let res = match res {
-                        Err(trap) => return Ok(ControlFlow::Trap(CraneliftTrap::User(trap))),
-                        Ok(rets) => rets,
-                    };
-
-                    // Check that what the handler returned is what we expect.
-                    if validate_signature_params(&signature.returns[..], &res[..]) {
-                        ControlFlow::Assign(res)
-                    } else {
-                        ControlFlow::Trap(CraneliftTrap::User(TrapCode::BadSignature))
-                    }
-                }
-                ExternalName::KnownSymbol(_) => unimplemented!(),
-            }
-        }
+        Opcode::Call => do_call(ControlFlow::Call)?,
         Opcode::CallIndirect => unimplemented!("CallIndirect"),
-        Opcode::ReturnCall => unimplemented!("ReturnCall"),
+        Opcode::ReturnCall => do_call(ControlFlow::ReturnCall)?,
         Opcode::ReturnCallIndirect => unimplemented!("ReturnCallIndirect"),
         Opcode::FuncAddr => unimplemented!("FuncAddr"),
         Opcode::Load
@@ -1288,6 +1297,8 @@ pub enum ControlFlow<'a, V> {
     ContinueAt(Block, SmallVec<[V; 1]>),
     /// Indicates a call the given [Function] with the supplied arguments.
     Call(&'a Function, SmallVec<[V; 1]>),
+    /// Indicates a tail call to the given [Function] with the supplied arguments.
+    ReturnCall(&'a Function, SmallVec<[V; 1]>),
     /// Return from the current function with the given parameters, e.g.: `return [v1, v2]`.
     Return(SmallVec<[V; 1]>),
     /// Stop with a program-generated trap; note that these are distinct from errors that may occur
diff --git a/cranelift/wasm/src/func_translator.rs b/cranelift/wasm/src/func_translator.rs
index 17d8f33b3..a7c6d7b3b 100644
--- a/cranelift/wasm/src/func_translator.rs
+++ b/cranelift/wasm/src/func_translator.rs
@@ -117,6 +117,7 @@ impl FuncTranslator {
         parse_function_body(validator, reader, &mut builder, &mut self.state, environ)?;
 
         builder.finalize();
+        log::trace!("translated Wasm to CLIF:\n{}", func.display());
         Ok(())
     }
 }
diff --git a/crates/jit-icache-coherence/Cargo.toml b/crates/jit-icache-coherence/Cargo.toml
index 3741e6e60..5fdb5bf42 100644
--- a/crates/jit-icache-coherence/Cargo.toml
+++ b/crates/jit-icache-coherence/Cargo.toml
@@ -21,3 +21,11 @@ features = [
 
 [target.'cfg(any(target_os = "linux", target_os = "macos", target_os = "freebsd", target_os = "android"))'.dependencies]
 libc = "0.2.42"
+
+[features]
+# Most modern CPUs are SMP (multicore). However, when only one core is present,
+# some aspects of coherence are much cheaper. For example, RISC-V can use
+# one instruction `fence.i` rather than a syscall that invokes all other cores.
+# This feature enables such optimizations, but the resulting program will *only*
+# be safe to run on one-core systems.
+one-core = []
\ No newline at end of file
diff --git a/crates/jit-icache-coherence/src/libc.rs b/crates/jit-icache-coherence/src/libc.rs
index 001e9251f..557cd0692 100644
--- a/crates/jit-icache-coherence/src/libc.rs
+++ b/crates/jit-icache-coherence/src/libc.rs
@@ -91,6 +91,46 @@ mod details {
         Ok(())
     }
 }
+#[cfg(all(target_arch = "riscv64", target_os = "linux"))]
+fn riscv_flush_icache(start: u64, end: u64) -> Result<()> {
+    cfg_if::cfg_if! {
+        if #[cfg(feature = "one-core")] {
+            use std::arch::asm;
+            unsafe {
+                asm!("fence.i");
+            };
+            Ok(())
+        } else {
+            match unsafe {
+                libc::syscall(
+                    {
+                        // The syscall isn't defined in `libc`, so we definfe the syscall number here.
+                        // https://github.com/torvalds/linux/search?q=__NR_arch_specific_syscall
+                        #[allow(non_upper_case_globals)]
+                        const  __NR_arch_specific_syscall :i64 = 244;
+                        // https://github.com/torvalds/linux/blob/5bfc75d92efd494db37f5c4c173d3639d4772966/tools/arch/riscv/include/uapi/asm/unistd.h#L40
+                        #[allow(non_upper_case_globals)]
+                        const sys_riscv_flush_icache :i64 =  __NR_arch_specific_syscall + 15;
+                        sys_riscv_flush_icache
+                    },
+                    // Currently these parameters are not used, but they are still defined.
+                    start, // start
+                    end, // end
+                    {
+                        #[allow(non_snake_case)]
+                        const SYS_RISCV_FLUSH_ICACHE_LOCAL :i64 = 1;
+                        #[allow(non_snake_case)]
+                        const SYS_RISCV_FLUSH_ICACHE_ALL :i64 = SYS_RISCV_FLUSH_ICACHE_LOCAL;
+                        SYS_RISCV_FLUSH_ICACHE_ALL
+                    }, // flags
+                )
+            } {
+                0 => { Ok(()) }
+                _ => Err(std::io::Error::last_os_error()),
+            }
+        }
+    }
+}
 
 pub(crate) use details::*;
 
@@ -103,6 +143,7 @@ pub(crate) fn clear_cache(_ptr: *const c_void, _len: usize) -> Result<()> {
     // We should call some implementation of `clear_cache` here.
     //
     // See: https://github.com/bytecodealliance/wasmtime/issues/3310
-
+    #[cfg(all(target_arch = "riscv64", target_os = "linux"))]
+    riscv_flush_icache(_ptr as u64, (_ptr as u64) + (_len as u64))?;
     Ok(())
 }


Test patch:
diff --git a/cranelift/filetests/filetests/egraph/algebraic.clif b/cranelift/filetests/filetests/egraph/algebraic.clif
index 5b2360047..ded7e52b0 100644
--- a/cranelift/filetests/filetests/egraph/algebraic.clif
+++ b/cranelift/filetests/filetests/egraph/algebraic.clif
@@ -87,6 +87,17 @@ block0(v0: i64):
     ; return v5
 }
 
+function %signed_shift_right_shift_left_i8_mask_rhs(i8) -> i8 {
+block0(v0: i8):
+    v1 = iconst.i8 0xf5
+    v2 = sshr v0, v1
+    v3 = ishl v2, v1
+    return v3
+    ; check: v4 = iconst.i8 224
+    ; check: v5 = band v0, v4
+    ; return v5
+}
+
 function %or_and_y_with_not_y_i8(i8, i8) -> i8 {
 block0(v0: i8, v1: i8):
     v2 = band v0, v1
diff --git a/cranelift/filetests/filetests/egraph/cprop.clif b/cranelift/filetests/filetests/egraph/cprop.clif
index afecd7fc1..972fb0dce 100644
--- a/cranelift/filetests/filetests/egraph/cprop.clif
+++ b/cranelift/filetests/filetests/egraph/cprop.clif
@@ -34,6 +34,28 @@ block0:
 ; check: v3 = iconst.i8 4
 ; check: return v3
 
+function %ishl_i8_i16() -> i8 {
+block0:
+    v0 = iconst.i8 1
+    v1 = iconst.i16 0xf2
+    v2 = ishl v0, v1
+    return v2
+}
+
+; check: v3 = iconst.i8 4
+; check: return v3
+
+function %ishl_i16_i8() -> i16 {
+block0:
+    v0 = iconst.i16 1
+    v1 = iconst.i8 0xf2
+    v2 = ishl v0, v1
+    return v2
+}
+
+; check: v3 = iconst.i16 4
+; check: return v3
+
 function %ushr() -> i8 {
 block0:
     v0 = iconst.i8 -1
@@ -45,6 +67,28 @@ block0:
 ; check: v3 = iconst.i8 63
 ; check: return v3
 
+function %ushr_i8_i16() -> i8 {
+block0:
+    v0 = iconst.i8 -1
+    v1 = iconst.i16 0xf2
+    v2 = ushr v0, v1
+    return v2
+}
+
+; check: v3 = iconst.i8 63
+; check: return v3
+
+function %ushr_i16_i8() -> i16 {
+block0:
+    v0 = iconst.i16 -1
+    v1 = iconst.i8 0xf2
+    v2 = ushr v0, v1
+    return v2
+}
+
+; check: v3 = iconst.i16 0x3fff
+; check: return v3
+
 function %sshr() -> i8 {
 block0:
     v0 = iconst.i8 0xf0
@@ -53,7 +97,29 @@ block0:
     return v2
 }
 
-; check: v3 = iconst.i8 -4
+; check: v3 = iconst.i8 252
+; check: return v3
+
+function %sshr_i8_i16() -> i8 {
+block0:
+    v0 = iconst.i8 0xf0
+    v1 = iconst.i16 0xf2
+    v2 = sshr v0, v1
+    return v2
+}
+
+; check: v3 = iconst.i8 252
+; check: return v3
+
+function %sshr_i16_i8() -> i16 {
+block0:
+    v0 = iconst.i16 0xfff0
+    v1 = iconst.i8 0xf2
+    v2 = sshr v0, v1
+    return v2
+}
+
+; check: v3 = iconst.i16 0xfffc
 ; check: return v3
 
 function %icmp_eq_i32() -> i8 {
diff --git a/cranelift/filetests/filetests/runtests/issue-5690.clif b/cranelift/filetests/filetests/runtests/issue-5690.clif
new file mode 100644
index 000000000..6906c35d4
--- /dev/null
+++ b/cranelift/filetests/filetests/runtests/issue-5690.clif
@@ -0,0 +1,29 @@
+test interpret
+test run
+set opt_level=speed
+set enable_simd=true
+set enable_safepoints=true
+set unwind_info=false
+set preserve_frame_pointers=true
+set machine_code_cfg_info=true
+set enable_table_access_spectre_mitigation=false
+target aarch64
+target x86_64
+
+function %u1() -> i64 sext, f64, i8, i8 sext, i8 sext system_v {
+block0:
+    v0 = f64const 0x1.8373638ff3738p-124
+    v1 = iconst.i8 53
+    v2 = iconst.i64 0x4445_00ff_ffff_ffff
+    v3 = iconst.i8 0
+    v4 = iconst.i16 0
+    v5 = iconst.i32 0
+    v6 = iconst.i64 0
+    v7 = uextend.i128 v6
+    v8 = ishl v2, v2
+    v9 = rotr v1, v1
+    nop
+    return v8, v0, v9, v9, v9
+}
+
+; run: %u1() == [-9223372036854775808, 0x1.8373638ff3738p-124, -87, -87, -87]
diff --git a/cranelift/filetests/filetests/runtests/return-call.clif b/cranelift/filetests/filetests/runtests/return-call.clif
new file mode 100644
index 000000000..4f317a41e
--- /dev/null
+++ b/cranelift/filetests/filetests/runtests/return-call.clif
@@ -0,0 +1,68 @@
+test interpret
+;; test run
+;; target x86_64
+;; target aarch64
+;; target aarch64 sign_return_address
+;; target aarch64 has_pauth sign_return_address
+;; target s390x
+
+;;;; Test passing `i64`s ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+function %callee_i64(i64) -> i64 tail {
+block0(v0: i64):
+    v1 = iadd_imm.i64 v0, 10
+    return v1
+}
+
+function %call_i64(i64) -> i64 tail {
+    fn0 = %callee_i64(i64) -> i64 tail
+
+block0(v0: i64):
+    return_call fn0(v0)
+}
+; run: %call_i64(10) == 20
+
+;;;; Test colocated tail calls ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+function %colocated_i64(i64) -> i64 tail {
+    fn0 = colocated %callee_i64(i64) -> i64 tail
+
+block0(v0: i64):
+    return_call fn0(v0)
+}
+; run: %colocated_i64(10) == 20
+
+;;;; Test passing `f64`s ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+function %callee_f64(f64) -> f64 tail {
+block0(v0: f64):
+    v1 = f64const 0x10.0
+    v2 = fadd.f64 v0, v1
+    return v2
+}
+
+function %call_f64(f64) -> f64 tail {
+    fn0 = %callee_f64(f64) -> f64 tail
+
+block0(v0: f64):
+    return_call fn0(v0)
+}
+; run: %call_f64(0x10.0) == 0x20.0
+
+;;;; Test passing `i8`s ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+function %callee_i8(i8) -> i8 tail {
+block0(v0: i8):
+    v1 = iconst.i8 0
+    v2 = icmp eq v0, v1
+    return v2
+}
+
+function %call_i8(i8) -> i8 tail {
+    fn0 = %callee_i8(i8) -> i8 tail
+
+block0(v0: i8):
+    return_call fn0(v0)
+}
+; run: %call_i8(1) == 0
+; run: %call_i8(0) == 1
diff --git a/cranelift/filetests/filetests/wasm/issue-5696.wat b/cranelift/filetests/filetests/wasm/issue-5696.wat
new file mode 100644
index 000000000..d8a2ddaab
--- /dev/null
+++ b/cranelift/filetests/filetests/wasm/issue-5696.wat
@@ -0,0 +1,20 @@
+;;! target = "x86_64"
+;;! optimize = true
+;;! settings = ["opt_level=speed"]
+
+(module
+  (func (;0;) (param i64) (result i64)
+    i64.const 32
+    i64.const -19
+    i64.shr_u
+    ;; call 0
+  )
+)
+;; function u0:0(i64, i64 vmctx) -> i64 fast {
+;;                                 block0(v0: i64, v1: i64):
+;; @001e                               jump block1
+;;
+;;                                 block1:
+;;                                     v6 = iconst.i64 0
+;; @001e                               return v6  ; v6 = 0
+;; }
diff --git a/cranelift/filetests/src/test_wasm.rs b/cranelift/filetests/src/test_wasm.rs
index 45665653a..b246178e3 100644
--- a/cranelift/filetests/src/test_wasm.rs
+++ b/cranelift/filetests/src/test_wasm.rs
@@ -24,6 +24,7 @@ pub fn run(path: &Path, wat: &str) -> Result<()> {
 
     let config: TestConfig =
         toml::from_str(&config_text).context("failed to parse the test configuration")?;
+    log::debug!("Wasm test config = {config:#?}");
 
     config
         .validate()