Repository: bytecodealliance/wasmtime
Pull request #7470 (2023-11-03 12:59:10)

cranelift: Make inline stackprobe unroll sequence valgrind compliant
👋 Hey,

This PR alters our stackprobe unroll sequences to move the stackpointer before writing to the stack. I don't think there was anything wrong with what we were doing before but it makes valgrind really unhappy. (See #7454)

I've tested this with cg_clif on x86, and it now cleanly passes valgrind for the original reported testcase.

Fixes: #7454
prtest:full


Patch:
diff --git a/cranelift/codegen/src/isa/aarch64/abi.rs b/cranelift/codegen/src/isa/aarch64/abi.rs
index b5d6e56fc..d1279921d 100644
--- a/cranelift/codegen/src/isa/aarch64/abi.rs
+++ b/cranelift/codegen/src/isa/aarch64/abi.rs
@@ -677,43 +677,9 @@ impl ABIMachineSpec for AArch64MachineDeps {
 
         let probe_count = align_to(frame_size, guard_size) / guard_size;
         if probe_count <= PROBE_MAX_UNROLL {
-            // When manually unrolling stick an instruction that stores 0 at a
-            // constant offset relative to the stack pointer. This will
-            // turn into something like `movn tmp, #n ; stur xzr [sp, tmp]`.
-            //
-            // Note that this may actually store beyond the stack size for the
-            // last item but that's ok since it's unused stack space and if
-            // that faults accidentally we're so close to faulting it shouldn't
-            // make too much difference to fault there.
-            insts.reserve(probe_count as usize);
-            for i in 0..probe_count {
-                let offset = (guard_size * (i + 1)) as i64;
-                insts.push(Self::gen_store_stack(
-                    StackAMode::SPOffset(-offset, I8),
-                    zero_reg(),
-                    I32,
-                ));
-            }
+            Self::gen_probestack_unroll(insts, guard_size, probe_count)
         } else {
-            // The non-unrolled version uses two temporary registers. The
-            // `start` contains the current offset from sp and counts downwards
-            // during the loop by increments of `guard_size`. The `end` is
-            // the size of the frame and where we stop.
-            //
-            // Note that this emission is all post-regalloc so it should be ok
-            // to use the temporary registers here as input/output as the loop
-            // itself is not allowed to use the registers.
-            let start = writable_spilltmp_reg();
-            let end = writable_tmp2_reg();
-            // `gen_inline_probestack` is called after regalloc2, so it's acceptable to reuse
-            // `start` and `end` as temporaries in load_constant.
-            insts.extend(Inst::load_constant(start, 0, &mut |_| start));
-            insts.extend(Inst::load_constant(end, frame_size.into(), &mut |_| end));
-            insts.push(Inst::StackProbeLoop {
-                start,
-                end: end.to_reg(),
-                step: Imm12::maybe_from_u64(guard_size.into()).unwrap(),
-            });
+            Self::gen_probestack_loop(insts, frame_size, guard_size)
         }
     }
 
@@ -1210,6 +1176,52 @@ impl ABIMachineSpec for AArch64MachineDeps {
     }
 }
 
+impl AArch64MachineDeps {
+    fn gen_probestack_unroll(insts: &mut SmallInstVec<Inst>, guard_size: u32, probe_count: u32) {
+        // When manually unrolling adjust the stack pointer and then write a zero
+        // to the stack at that offset. This generates something like
+        // `sub sp, sp, #1, lsl #12` followed by `stur wzr, [sp]`.
+        //
+        // We do this because valgrind expects us to never write beyond the stack
+        // pointer and associated redzone.
+        // See: https://github.com/bytecodealliance/wasmtime/issues/7454
+        for _ in 0..probe_count {
+            insts.extend(Self::gen_sp_reg_adjust(-(guard_size as i32)));
+
+            insts.push(Self::gen_store_stack(
+                StackAMode::SPOffset(0, I8),
+                zero_reg(),
+                I32,
+            ));
+        }
+
+        // Restore the stack pointer to its original value
+        insts.extend(Self::gen_sp_reg_adjust((guard_size * probe_count) as i32));
+    }
+
+    fn gen_probestack_loop(insts: &mut SmallInstVec<Inst>, frame_size: u32, guard_size: u32) {
+        // The non-unrolled version uses two temporary registers. The
+        // `start` contains the current offset from sp and counts downwards
+        // during the loop by increments of `guard_size`. The `end` is
+        // the size of the frame and where we stop.
+        //
+        // Note that this emission is all post-regalloc so it should be ok
+        // to use the temporary registers here as input/output as the loop
+        // itself is not allowed to use the registers.
+        let start = writable_spilltmp_reg();
+        let end = writable_tmp2_reg();
+        // `gen_inline_probestack` is called after regalloc2, so it's acceptable to reuse
+        // `start` and `end` as temporaries in load_constant.
+        insts.extend(Inst::load_constant(start, 0, &mut |_| start));
+        insts.extend(Inst::load_constant(end, frame_size.into(), &mut |_| end));
+        insts.push(Inst::StackProbeLoop {
+            start,
+            end: end.to_reg(),
+            step: Imm12::maybe_from_u64(guard_size.into()).unwrap(),
+        });
+    }
+}
+
 fn select_api_key(
     isa_flags: &aarch64_settings::Flags,
     call_conv: isa::CallConv,
diff --git a/cranelift/codegen/src/isa/riscv64/abi.rs b/cranelift/codegen/src/isa/riscv64/abi.rs
index d119d0f8e..095ef13f1 100644
--- a/cranelift/codegen/src/isa/riscv64/abi.rs
+++ b/cranelift/codegen/src/isa/riscv64/abi.rs
@@ -718,10 +718,20 @@ impl ABIMachineSpec for Riscv64MachineDeps {
         // Number of probes that we need to perform
         let probe_count = align_to(frame_size, guard_size) / guard_size;
 
+        // Must be a caller-saved register that is not an argument.
+        let tmp = match call_conv {
+            isa::CallConv::Tail => Writable::from_reg(x_reg(1)),
+            _ => Writable::from_reg(x_reg(28)), // t3
+        };
+
         if probe_count <= PROBE_MAX_UNROLL {
-            Self::gen_probestack_unroll(insts, guard_size, probe_count)
+            Self::gen_probestack_unroll(insts, tmp, guard_size, probe_count)
         } else {
-            Self::gen_probestack_loop(insts, call_conv, guard_size, probe_count)
+            insts.push(Inst::StackProbeLoop {
+                guard_size,
+                probe_count,
+                tmp,
+            });
         }
     }
 }
@@ -1052,33 +1062,41 @@ fn create_reg_enviroment() -> MachineEnv {
 }
 
 impl Riscv64MachineDeps {
-    fn gen_probestack_unroll(insts: &mut SmallInstVec<Inst>, guard_size: u32, probe_count: u32) {
-        insts.reserve(probe_count as usize);
-        for i in 0..probe_count {
-            let offset = (guard_size * (i + 1)) as i64;
+    fn gen_probestack_unroll(
+        insts: &mut SmallInstVec<Inst>,
+        tmp: Writable<Reg>,
+        guard_size: u32,
+        probe_count: u32,
+    ) {
+        // When manually unrolling adjust the stack pointer and then write a zero
+        // to the stack at that offset.
+        //
+        // We do this because valgrind expects us to never write beyond the stack
+        // pointer and associated redzone.
+        // See: https://github.com/bytecodealliance/wasmtime/issues/7454
+
+        // Store the adjust amount in a register upfront, so we don't have to
+        // reload it for each probe. It's worth loading this as a negative and
+        // using an `add` instruction since we have compressed versions of `add`
+        // but not the `sub` instruction.
+        insts.extend(Inst::load_constant_u64(tmp, (-(guard_size as i64)) as u64));
+
+        for _ in 0..probe_count {
+            insts.push(Inst::AluRRR {
+                alu_op: AluOPRRR::Add,
+                rd: writable_stack_reg(),
+                rs1: stack_reg(),
+                rs2: tmp.to_reg(),
+            });
+
             insts.push(Self::gen_store_stack(
-                StackAMode::SPOffset(-offset, I8),
+                StackAMode::SPOffset(0, I8),
                 zero_reg(),
                 I32,
             ));
         }
-    }
 
-    fn gen_probestack_loop(
-        insts: &mut SmallInstVec<Inst>,
-        call_conv: isa::CallConv,
-        guard_size: u32,
-        probe_count: u32,
-    ) {
-        // Must be a caller-saved register that is not an argument.
-        let tmp = match call_conv {
-            isa::CallConv::Tail => Writable::from_reg(x_reg(1)),
-            _ => Writable::from_reg(x_reg(28)), // t3
-        };
-        insts.push(Inst::StackProbeLoop {
-            guard_size,
-            probe_count,
-            tmp,
-        });
+        // Restore the stack pointer to its original value
+        insts.extend(Self::gen_sp_reg_adjust((guard_size * probe_count) as i32));
     }
 }
diff --git a/cranelift/codegen/src/isa/x64/abi.rs b/cranelift/codegen/src/isa/x64/abi.rs
index d99933500..49c54d529 100644
--- a/cranelift/codegen/src/isa/x64/abi.rs
+++ b/cranelift/codegen/src/isa/x64/abi.rs
@@ -33,17 +33,23 @@ pub struct X64ABIMachineSpec;
 impl X64ABIMachineSpec {
     fn gen_probestack_unroll(insts: &mut SmallInstVec<Inst>, guard_size: u32, probe_count: u32) {
         insts.reserve(probe_count as usize);
-        for i in 0..probe_count {
-            let offset = (guard_size * (i + 1)) as i64;
+        for _ in 0..probe_count {
+            // "Allocate" stack space for the probe by decrementing the stack pointer before
+            // the write. This is required to make valgrind happy.
+            // See: https://github.com/bytecodealliance/wasmtime/issues/7454
+            insts.extend(Self::gen_sp_reg_adjust(-(guard_size as i32)));
 
             // TODO: It would be nice if we could store the imm 0, but we don't have insts for those
             // so store the stack pointer. Any register will do, since the stack is undefined at this point
             insts.push(Self::gen_store_stack(
-                StackAMode::SPOffset(-offset, I8),
+                StackAMode::SPOffset(0, I8),
                 regs::rsp(),
                 I32,
             ));
         }
+
+        // Restore the stack pointer to its original value
+        insts.extend(Self::gen_sp_reg_adjust((guard_size * probe_count) as i32));
     }
 
     fn gen_probestack_loop(
@@ -523,8 +529,8 @@ impl ABIMachineSpec for X64ABIMachineSpec {
         // Unroll at most n consecutive probes, before falling back to using a loop
         //
         // This was number was picked because the loop version is 38 bytes long. We can fit
-        // 5 inline probes in that space, so unroll if its beneficial in terms of code size.
-        const PROBE_MAX_UNROLL: u32 = 5;
+        // 4 inline probes in that space, so unroll if its beneficial in terms of code size.
+        const PROBE_MAX_UNROLL: u32 = 4;
 
         // Number of probes that we need to perform
         let probe_count = align_to(frame_size, guard_size) / guard_size;
diff --git a/cranelift/codegen/src/machinst/vcode.rs b/cranelift/codegen/src/machinst/vcode.rs
index c743fcd20..90bb2a286 100644
--- a/cranelift/codegen/src/machinst/vcode.rs
+++ b/cranelift/codegen/src/machinst/vcode.rs
@@ -1738,6 +1738,15 @@ impl VCodeConstants {
     pub fn get(&self, c: VCodeConstant) -> &VCodeConstantData {
         &self.constants[c]
     }
+
+    /// Checks if the given [VCodeConstantData] is registered as
+    /// used by the pool.
+    pub fn pool_uses(&self, constant: &VCodeConstantData) -> bool {
+        match constant {
+            VCodeConstantData::Pool(c, _) => self.pool_uses.contains_key(c),
+            _ => false,
+        }
+    }
 }
 
 /// A use of a constant by one or more VCode instructions; see [VCodeConstants].
diff --git a/cranelift/codegen/src/opts/shifts.isle b/cranelift/codegen/src/opts/shifts.isle
index acf53946c..272c5c90b 100644
--- a/cranelift/codegen/src/opts/shifts.isle
+++ b/cranelift/codegen/src/opts/shifts.isle
@@ -268,3 +268,43 @@
       (rotl ty x (isub_uextend y z)))
 (rule (simplify (rotl ty (rotr ty x y @ (value_type kty)) z @ (value_type kty)))
       (rotr ty x (isub_uextend y z)))
+
+;; Convert shifts into rotates. We always normalize into a rotate left.
+;;
+;; (bor (ishl x k1) (ushr x k2)) == (rotl x k1) if k2 == ty_bits - k1
+;; (bor (ushr x k2) (ishl x k1)) == (rotl x k1) if k2 == ty_bits - k1
+;;
+;; TODO: This rule is restricted to scalars since no backend currently
+;; supports SIMD rotates.
+(rule (simplify (bor (ty_int ty)
+                      (ishl ty x k @ (iconst _ (u64_from_imm64 k1)))
+                      (ushr ty x (iconst _ (u64_from_imm64 k2)))))
+      (if-let $true (u64_eq k2 (u64_sub (ty_bits_u64 (lane_type ty)) k1)))
+      (rotl ty x k))
+(rule (simplify (bor (ty_int ty)
+                      (ushr ty x (iconst _ (u64_from_imm64 k2)))
+                      (ishl ty x k @ (iconst _ (u64_from_imm64 k1)))))
+      (if-let $true (u64_eq k2 (u64_sub (ty_bits_u64 (lane_type ty)) k1)))
+      (rotl ty x k))
+
+;; Normalize the shift amount. Some rules can't fire unless the shift amount
+;; is normalized. This also helps us materialize fewer and smaller constants.
+;;
+;; (op x k) == (op x (and k (ty_shift_mask ty)))
+;;
+;; where `op` is one of ishl, ushr, sshr, rotl, rotr
+(rule (simplify (ishl ty x (iconst kty (u64_from_imm64 k))))
+      (if-let $false (u64_eq k (u64_and k (ty_shift_mask ty))))
+      (ishl ty x (iconst kty (imm64 (u64_and k (ty_shift_mask ty))))))
+(rule (simplify (ushr ty x (iconst kty (u64_from_imm64 k))))
+      (if-let $false (u64_eq k (u64_and k (ty_shift_mask ty))))
+      (ushr ty x (iconst kty (imm64 (u64_and k (ty_shift_mask ty))))))
+(rule (simplify (sshr ty x (iconst kty (u64_from_imm64 k))))
+      (if-let $false (u64_eq k (u64_and k (ty_shift_mask ty))))
+      (sshr ty x (iconst kty (imm64 (u64_and k (ty_shift_mask ty))))))
+(rule (simplify (rotr ty x (iconst kty (u64_from_imm64 k))))
+      (if-let $false (u64_eq k (u64_and k (ty_shift_mask ty))))
+      (rotr ty x (iconst kty (imm64 (u64_and k (ty_shift_mask ty))))))
+(rule (simplify (rotl ty x (iconst kty (u64_from_imm64 k))))
+      (if-let $false (u64_eq k (u64_and k (ty_shift_mask ty))))
+      (rotl ty x (iconst kty (imm64 (u64_and k (ty_shift_mask ty))))))
diff --git a/crates/c-api/include/wasmtime.h b/crates/c-api/include/wasmtime.h
index 44e4e3d2c..ce7d46e0f 100644
--- a/crates/c-api/include/wasmtime.h
+++ b/crates/c-api/include/wasmtime.h
@@ -181,6 +181,7 @@
 #define WASMTIME_API_H
 
 #include <wasi.h>
+// IWYU pragma: begin_exports
 #include <wasmtime/config.h>
 #include <wasmtime/engine.h>
 #include <wasmtime/error.h>
@@ -196,6 +197,7 @@
 #include <wasmtime/trap.h>
 #include <wasmtime/val.h>
 #include <wasmtime/async.h>
+// IWYU pragma: end_exports
 
 /**
  * \brief Wasmtime version string.
diff --git a/crates/wasmtime/src/compiler.rs b/crates/wasmtime/src/compiler.rs
index 33856f4b2..5022ef9da 100644
--- a/crates/wasmtime/src/compiler.rs
+++ b/crates/wasmtime/src/compiler.rs
@@ -28,7 +28,7 @@ use std::collections::{btree_map, BTreeMap, BTreeSet};
 use std::{any::Any, collections::HashMap};
 use wasmtime_environ::{
     Compiler, DefinedFuncIndex, FuncIndex, FunctionBodyData, ModuleTranslation, ModuleType,
-    ModuleTypes, PrimaryMap, SignatureIndex, StaticModuleIndex, Tunables, WasmFunctionInfo,
+    ModuleTypes, PrimaryMap, SignatureIndex, StaticModuleIndex, WasmFunctionInfo,
 };
 use wasmtime_jit::{CompiledFunctionInfo, CompiledModuleInfo};
 
@@ -447,8 +447,7 @@ impl FunctionIndices {
     pub fn link_and_append_code<'a>(
         mut self,
         mut obj: object::write::Object<'static>,
-        tunables: &'a Tunables,
-        compiler: &dyn Compiler,
+        engine: &'a Engine,
         compiled_funcs: Vec<(String, Box<dyn Any + Send>)>,
         translations: PrimaryMap<StaticModuleIndex, ModuleTranslation<'_>>,
     ) -> Result<(wasmtime_jit::ObjectBuilder<'a>, Artifacts)> {
@@ -457,6 +456,8 @@ impl FunctionIndices {
         // The result is a vector parallel to `compiled_funcs` where
         // `symbol_ids_and_locs[i]` is the symbol ID and function location of
         // `compiled_funcs[i]`.
+        let compiler = engine.compiler();
+        let tunables = &engine.config().tunables;
         let symbol_ids_and_locs = compiler.append_code(
             &mut obj,
             &compiled_funcs,
@@ -559,7 +560,22 @@ impl FunctionIndices {
 
         artifacts.modules = translations
             .into_iter()
-            .map(|(module, translation)| {
+            .map(|(module, mut translation)| {
+                // If configured attempt to use static memory initialization which
+                // can either at runtime be implemented as a single memcpy to
+                // initialize memory or otherwise enabling virtual-memory-tricks
+                // such as mmap'ing from a file to get copy-on-write.
+                if engine.config().memory_init_cow {
+                    let align = compiler.page_size_align();
+                    let max_always_allowed = engine.config().memory_guaranteed_dense_image_size;
+                    translation.try_static_init(align, max_always_allowed);
+                }
+
+                // Attempt to convert table initializer segments to
+                // FuncTable representation where possible, to enable
+                // table lazy init.
+                translation.try_func_table_init();
+
                 let funcs: PrimaryMap<DefinedFuncIndex, CompiledFunctionInfo> =
                     wasm_functions_for_module(&mut wasm_functions, module)
                         .map(|(key, wasm_func_index)| {
diff --git a/crates/wasmtime/src/component/component.rs b/crates/wasmtime/src/component/component.rs
index 013f93bd7..a57165ed4 100644
--- a/crates/wasmtime/src/component/component.rs
+++ b/crates/wasmtime/src/component/component.rs
@@ -206,8 +206,7 @@ impl Component {
 
         let (mut object, compilation_artifacts) = function_indices.link_and_append_code(
             object,
-            &engine.config().tunables,
-            compiler,
+            engine,
             compiled_funcs,
             module_translations,
         )?;
@@ -473,3 +472,34 @@ impl ComponentRuntimeInfo for ComponentInner {
         }
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use crate::component::Component;
+    use crate::{Config, Engine};
+    use wasmtime_environ::MemoryInitialization;
+
+    #[test]
+    fn cow_on_by_default() {
+        let mut config = Config::new();
+        config.wasm_component_model(true);
+        let engine = Engine::new(&config).unwrap();
+        let component = Component::new(
+            &engine,
+            r#"
+                (component
+                    (core module
+                        (memory 1)
+                        (data (i32.const 100) "abcd")
+                    )
+                )
+            "#,
+        )
+        .unwrap();
+
+        for (_, module) in component.inner.static_modules.iter() {
+            let init = &module.env_module().memory_initialization;
+            assert!(matches!(init, MemoryInitialization::Static { .. }));
+        }
+    }
+}
diff --git a/crates/wasmtime/src/module.rs b/crates/wasmtime/src/module.rs
index 38a51fcd7..a5553ff94 100644
--- a/crates/wasmtime/src/module.rs
+++ b/crates/wasmtime/src/module.rs
@@ -402,7 +402,6 @@ impl Module {
         use crate::compiler::CompileInputs;
 
         let tunables = &engine.config().tunables;
-        let compiler = engine.compiler();
 
         // First a `ModuleEnvironment` is created which records type information
         // about the wasm module. This is where the WebAssembly is parsed and
@@ -437,25 +436,9 @@ impl Module {
         engine.append_compiler_info(&mut object);
         engine.append_bti(&mut object);
 
-        // If configured attempt to use static memory initialization which
-        // can either at runtime be implemented as a single memcpy to
-        // initialize memory or otherwise enabling virtual-memory-tricks
-        // such as mmap'ing from a file to get copy-on-write.
-        if engine.config().memory_init_cow {
-            let align = engine.compiler().page_size_align();
-            let max_always_allowed = engine.config().memory_guaranteed_dense_image_size;
-            translation.try_static_init(align, max_always_allowed);
-        }
-
-        // Attempt to convert table initializer segments to
-        // FuncTable representation where possible, to enable
-        // table lazy init.
-        translation.try_func_table_init();
-
         let (mut object, compilation_artifacts) = function_indices.link_and_append_code(
             object,
-            tunables,
-            compiler,
+            engine,
             compiled_funcs,
             std::iter::once(translation).collect(),
         )?;
@@ -1361,3 +1344,27 @@ fn memory_images(engine: &Engine, module: &CompiledModule) -> Result<Option<Modu
     };
     ModuleMemoryImages::new(module.module(), module.code_memory().wasm_data(), mmap)
 }
+
+#[cfg(test)]
+mod tests {
+    use crate::{Engine, Module};
+    use wasmtime_environ::MemoryInitialization;
+
+    #[test]
+    fn cow_on_by_default() {
+        let engine = Engine::default();
+        let module = Module::new(
+            &engine,
+            r#"
+                (module
+                    (memory 1)
+                    (data (i32.const 100) "abcd")
+                )
+            "#,
+        )
+        .unwrap();
+
+        let init = &module.env_module().memory_initialization;
+        assert!(matches!(init, MemoryInitialization::Static { .. }));
+    }
+}
diff --git a/src/commands/serve.rs b/src/commands/serve.rs
index 31276a1aa..927cc755b 100644
--- a/src/commands/serve.rs
+++ b/src/commands/serve.rs
@@ -373,12 +373,16 @@ impl hyper::service::Service<Request> for ProxyHandler {
                 wasmtime_wasi_http::proxy::Proxy::instantiate_pre(&mut store, &inner.instance_pre)
                     .await?;
 
-            proxy
+            if let Err(e) = proxy
                 .wasi_http_incoming_handler()
                 .call_handle(store, req, out)
-                .await?;
+                .await
+            {
+                log::error!("[{req_id}] :: {:#?}", e);
+                return Err(e);
+            }
 
-            Ok::<_, anyhow::Error>(())
+            Ok(())
         });
 
         Box::pin(async move {
diff --git a/winch/codegen/src/isa/x64/asm.rs b/winch/codegen/src/isa/x64/asm.rs
index 98f7723c6..bd791784e 100644
--- a/winch/codegen/src/isa/x64/asm.rs
+++ b/winch/codegen/src/isa/x64/asm.rs
@@ -202,8 +202,14 @@ impl Assembler {
                 // instrunction.
                 let constant_data = pool.get(*c);
                 let data = VCodeConstantData::Pool(*c, constant_data.clone());
+                // If the constaant data is not marked as used, it will be
+                // inserted, therefore, it needs to be registered.
+                let needs_registration = !constants.pool_uses(&data);
                 let constant = constants.insert(VCodeConstantData::Pool(*c, constant_data.clone()));
-                buffer.register_constant(&constant, &data);
+
+                if needs_registration {
+                    buffer.register_constant(&constant, &data);
+                }
                 SyntheticAmode::ConstantOffset(constant)
             }
         }


Test patch:
diff --git a/cranelift/filetests/filetests/egraph/shifts.clif b/cranelift/filetests/filetests/egraph/shifts.clif
index dbe984c84..fbe308ddb 100644
--- a/cranelift/filetests/filetests/egraph/shifts.clif
+++ b/cranelift/filetests/filetests/egraph/shifts.clif
@@ -74,9 +74,9 @@ block0(v0: i8):
     v2 = sshr v0, v1
     v3 = ishl v2, v1
     return v3
-    ; check: v4 = iconst.i8 224
-    ; check: v5 = band v0, v4
-    ; check: return v5
+    ; check: v8 = iconst.i8 224
+    ; check: v9 = band v0, v8
+    ; check: return v9
 }
 
 function %sextend_shift_32_64_unsigned(i32) -> i64 {
@@ -596,9 +596,9 @@ block0(v0: i128):
     return v3
 }
 
-; check: v4 = iconst.i64 0
-; nextln: v5 = uextend.i128 v4  ; v4 = 0
-; nextln: return v5
+; check: v7 = iconst.i64 0
+; nextln: v8 = uextend.i128 v7
+; nextln: return v8
 
 
 function %i128_ishl_overflow_becomes_0(i128) -> i128 {
@@ -609,9 +609,9 @@ block0(v0: i128):
     return v3
 }
 
-; check: v4 = iconst.i64 0
-; nextln: v5 = uextend.i128 v4  ; v4 = 0
-; nextln: return v5
+; check: v7 = iconst.i64 0
+; nextln: v8 = uextend.i128 v7
+; nextln: return v8
 
 function %simd_shift_does_not_panic(i8x16) -> i8x16 {
 block0(v0: i8x16):
@@ -636,11 +636,10 @@ block0(v0: i8x16):
     return v5
 }
 
-; check: v1 = iconst.i16 30
-; nextln: v3 = sshr v0, v1  ; v1 = 30
-; nextln: v6 = iconst.i16 6
-; nextln: v7 = sshr v3, v6  ; v6 = 6
-; check: return v7
+; check: v6 = iconst.i16 6
+; check: v7 = sshr v0, v6
+; check: v9 = sshr v7, v6
+; check: return v9
 
 
 function %rotr_rotr_prop(i8) -> i8 {
@@ -758,3 +757,57 @@ block0(v0: i8, v1: i8):
 }
 
 ; check: return v0
+
+function %shifts_to_rotl(i64) -> i64 {
+block0(v0: i64):
+    v1 = iconst.i16 3
+    v2 = iconst.i32 61
+    v3 = ishl v0, v1
+    v4 = ushr v0, v2
+    v5 = bor v3, v4
+    return v5
+}
+
+; check: v1 = iconst.i16 3
+; nextln: v6 = rotl v0, v1  ; v1 = 3
+; check: return v6
+
+
+function %shifts_to_rotl_reverse(i64) -> i64 {
+block0(v0: i64):
+    v1 = iconst.i16 32
+    v2 = iconst.i32 32
+    v3 = ishl v0, v1
+    v4 = ushr v0, v2
+    v5 = bor v4, v3
+    return v5
+}
+
+; check: v1 = iconst.i16 32
+; nextln: v6 = rotl v0, v1  ; v1 = 32
+; check: return v6
+
+function %shifts_to_rotr_equivalent(i64) -> i64 {
+block0(v0: i64):
+    v1 = iconst.i16 3
+    v2 = iconst.i32 61
+    v3 = ishl v0, v2
+    v4 = ushr v0, v1
+    v5 = bor v3, v4
+    return v5
+}
+
+; check: v2 = iconst.i32 61
+; nextln: v6 = rotl v0, v2  ; v2 = 61
+; check: return v6
+
+function %ishl_normalize_amount(i64) -> i64 {
+block0(v0: i64):
+    v1 = iconst.i32 99
+    v2 = ishl v0, v1
+    return v2
+}
+
+; check: v3 = iconst.i32 35
+; check: v4 = ishl v0, v3
+; check: return v4
diff --git a/cranelift/filetests/filetests/isa/aarch64/inline-probestack.clif b/cranelift/filetests/filetests/isa/aarch64/inline-probestack.clif
index 2ec7431b9..eee45fd76 100644
--- a/cranelift/filetests/filetests/isa/aarch64/inline-probestack.clif
+++ b/cranelift/filetests/filetests/isa/aarch64/inline-probestack.clif
@@ -47,9 +47,13 @@ block0:
 ; VCode:
 ;   stp fp, lr, [sp, #-16]!
 ;   mov fp, sp
-;   movn x16, #4095 ; str wzr, [sp, x16, SXTX]
-;   movn x16, #8191 ; str wzr, [sp, x16, SXTX]
-;   movn x16, #12287 ; str wzr, [sp, x16, SXTX]
+;   sub sp, sp, #4096
+;   str wzr, [sp]
+;   sub sp, sp, #4096
+;   str wzr, [sp]
+;   sub sp, sp, #4096
+;   str wzr, [sp]
+;   add sp, sp, #12288
 ;   sub sp, sp, #12288
 ; block0:
 ;   mov x0, sp
@@ -61,14 +65,15 @@ block0:
 ; block0: ; offset 0x0
 ;   stp x29, x30, [sp, #-0x10]!
 ;   mov x29, sp
-;   mov x16, #-0x1000
-;   str wzr, [sp, x16, sxtx]
-;   mov x16, #-0x2000
-;   str wzr, [sp, x16, sxtx]
-;   mov x16, #-0x3000
-;   str wzr, [sp, x16, sxtx]
+;   sub sp, sp, #1, lsl #12
+;   stur wzr, [sp]
+;   sub sp, sp, #1, lsl #12
+;   stur wzr, [sp]
+;   sub sp, sp, #1, lsl #12
+;   stur wzr, [sp]
+;   add sp, sp, #3, lsl #12
 ;   sub sp, sp, #3, lsl #12
-; block1: ; offset 0x24
+; block1: ; offset 0x28
 ;   mov x0, sp
 ;   add sp, sp, #3, lsl #12
 ;   ldp x29, x30, [sp], #0x10
diff --git a/cranelift/filetests/filetests/isa/riscv64/c-inline-probestack.clif b/cranelift/filetests/filetests/isa/riscv64/c-inline-probestack.clif
new file mode 100644
index 000000000..6d94c2f81
--- /dev/null
+++ b/cranelift/filetests/filetests/isa/riscv64/c-inline-probestack.clif
@@ -0,0 +1,162 @@
+test compile precise-output
+set enable_probestack=true
+set probestack_strategy=inline
+; This is the default and is equivalent to a page size of 4096
+set probestack_size_log2=12
+target riscv64 has_c
+
+
+; If the stack size is just one page, we can avoid the stack probe entirely
+function %single_page() -> i64 system_v {
+ss0 = explicit_slot 2048
+
+block0:
+  v1 = stack_addr.i64 ss0
+  return v1
+}
+
+; VCode:
+;   addi sp,sp,-16
+;   sd ra,8(sp)
+;   sd fp,0(sp)
+;   mv fp,sp
+;   addi sp,sp,-2048
+; block0:
+;   load_addr a0,0(nominal_sp)
+;   lui t6,1
+;   addi t6,t6,-2048
+;   add sp,t6,sp
+;   ld ra,8(sp)
+;   ld fp,0(sp)
+;   addi sp,sp,16
+;   ret
+;
+; Disassembled:
+; block0: ; offset 0x0
+;   c.addi16sp sp, -0x10
+;   c.sdsp ra, 8(sp)
+;   c.sdsp s0, 0(sp)
+;   c.mv s0, sp
+;   addi sp, sp, -0x800
+; block1: ; offset 0xc
+;   c.mv a0, sp
+;   c.lui t6, 1
+;   addi t6, t6, -0x800
+;   add sp, t6, sp
+;   c.ldsp ra, 8(sp)
+;   c.ldsp s0, 0(sp)
+;   c.addi16sp sp, 0x10
+;   c.jr ra
+
+function %unrolled() -> i64 system_v {
+ss0 = explicit_slot 12288
+
+block0:
+  v1 = stack_addr.i64 ss0
+  return v1
+}
+
+; VCode:
+;   addi sp,sp,-16
+;   sd ra,8(sp)
+;   sd fp,0(sp)
+;   mv fp,sp
+;   lui t3,-1
+;   add sp,sp,t3
+;   sw zero,0(sp)
+;   add sp,sp,t3
+;   sw zero,0(sp)
+;   add sp,sp,t3
+;   sw zero,0(sp)
+;   lui t6,3
+;   add sp,t6,sp
+;   lui t6,-3
+;   add sp,t6,sp
+; block0:
+;   load_addr a0,0(nominal_sp)
+;   lui t6,3
+;   add sp,t6,sp
+;   ld ra,8(sp)
+;   ld fp,0(sp)
+;   addi sp,sp,16
+;   ret
+;
+; Disassembled:
+; block0: ; offset 0x0
+;   c.addi16sp sp, -0x10
+;   c.sdsp ra, 8(sp)
+;   c.sdsp s0, 0(sp)
+;   c.mv s0, sp
+;   c.lui t3, 0xfffff
+;   c.add sp, t3
+;   c.swsp zero, 0(sp)
+;   c.add sp, t3
+;   c.swsp zero, 0(sp)
+;   c.add sp, t3
+;   c.swsp zero, 0(sp)
+;   c.lui t6, 3
+;   add sp, t6, sp
+;   c.lui t6, 0xffffd
+;   add sp, t6, sp
+; block1: ; offset 0x22
+;   c.mv a0, sp
+;   c.lui t6, 3
+;   add sp, t6, sp
+;   c.ldsp ra, 8(sp)
+;   c.ldsp s0, 0(sp)
+;   c.addi16sp sp, 0x10
+;   c.jr ra
+
+function %large() -> i64 system_v {
+ss0 = explicit_slot 100000
+
+block0:
+  v1 = stack_addr.i64 ss0
+  return v1
+}
+
+; VCode:
+;   addi sp,sp,-16
+;   sd ra,8(sp)
+;   sd fp,0(sp)
+;   mv fp,sp
+;   inline_stack_probe##guard_size=4096 probe_count=25 tmp=t3
+;   lui t6,-24
+;   addi t6,t6,-1696
+;   add sp,t6,sp
+; block0:
+;   load_addr a0,0(nominal_sp)
+;   lui t6,24
+;   addi t6,t6,1696
+;   add sp,t6,sp
+;   ld ra,8(sp)
+;   ld fp,0(sp)
+;   addi sp,sp,16
+;   ret
+;
+; Disassembled:
+; block0: ; offset 0x0
+;   c.addi16sp sp, -0x10
+;   c.sdsp ra, 8(sp)
+;   c.sdsp s0, 0(sp)
+;   c.mv s0, sp
+;   c.lui t6, 0x19
+;   c.lui t3, 1
+;   bgeu t3, t6, 0x12
+;   sub t5, sp, t6
+;   sb zero, 0(t5)
+;   sub t6, t6, t3
+;   c.j -0x10
+;   c.lui t6, 0xfffe8
+;   addi t6, t6, -0x6a0
+;   add sp, t6, sp
+; block1: ; offset 0x28
+;   c.mv a0, sp
+;   c.lui t6, 0x18
+;   addi t6, t6, 0x6a0
+;   add sp, t6, sp
+;   c.ldsp ra, 8(sp)
+;   c.ldsp s0, 0(sp)
+;   c.addi16sp sp, 0x10
+;   c.jr ra
+
diff --git a/cranelift/filetests/filetests/isa/riscv64/inline-probestack.clif b/cranelift/filetests/filetests/isa/riscv64/inline-probestack.clif
new file mode 100644
index 000000000..6b8177668
--- /dev/null
+++ b/cranelift/filetests/filetests/isa/riscv64/inline-probestack.clif
@@ -0,0 +1,162 @@
+test compile precise-output
+set enable_probestack=true
+set probestack_strategy=inline
+; This is the default and is equivalent to a page size of 4096
+set probestack_size_log2=12
+target riscv64
+
+
+; If the stack size is just one page, we can avoid the stack probe entirely
+function %single_page() -> i64 system_v {
+ss0 = explicit_slot 2048
+
+block0:
+  v1 = stack_addr.i64 ss0
+  return v1
+}
+
+; VCode:
+;   addi sp,sp,-16
+;   sd ra,8(sp)
+;   sd fp,0(sp)
+;   mv fp,sp
+;   addi sp,sp,-2048
+; block0:
+;   load_addr a0,0(nominal_sp)
+;   lui t6,1
+;   addi t6,t6,-2048
+;   add sp,t6,sp
+;   ld ra,8(sp)
+;   ld fp,0(sp)
+;   addi sp,sp,16
+;   ret
+;
+; Disassembled:
+; block0: ; offset 0x0
+;   addi sp, sp, -0x10
+;   sd ra, 8(sp)
+;   sd s0, 0(sp)
+;   mv s0, sp
+;   addi sp, sp, -0x800
+; block1: ; offset 0x14
+;   mv a0, sp
+;   lui t6, 1
+;   addi t6, t6, -0x800
+;   add sp, t6, sp
+;   ld ra, 8(sp)
+;   ld s0, 0(sp)
+;   addi sp, sp, 0x10
+;   ret
+
+function %unrolled() -> i64 system_v {
+ss0 = explicit_slot 12288
+
+block0:
+  v1 = stack_addr.i64 ss0
+  return v1
+}
+
+; VCode:
+;   addi sp,sp,-16
+;   sd ra,8(sp)
+;   sd fp,0(sp)
+;   mv fp,sp
+;   lui t3,-1
+;   add sp,sp,t3
+;   sw zero,0(sp)
+;   add sp,sp,t3
+;   sw zero,0(sp)
+;   add sp,sp,t3
+;   sw zero,0(sp)
+;   lui t6,3
+;   add sp,t6,sp
+;   lui t6,-3
+;   add sp,t6,sp
+; block0:
+;   load_addr a0,0(nominal_sp)
+;   lui t6,3
+;   add sp,t6,sp
+;   ld ra,8(sp)
+;   ld fp,0(sp)
+;   addi sp,sp,16
+;   ret
+;
+; Disassembled:
+; block0: ; offset 0x0
+;   addi sp, sp, -0x10
+;   sd ra, 8(sp)
+;   sd s0, 0(sp)
+;   mv s0, sp
+;   lui t3, 0xfffff
+;   add sp, sp, t3
+;   sw zero, 0(sp)
+;   add sp, sp, t3
+;   sw zero, 0(sp)
+;   add sp, sp, t3
+;   sw zero, 0(sp)
+;   lui t6, 3
+;   add sp, t6, sp
+;   lui t6, 0xffffd
+;   add sp, t6, sp
+; block1: ; offset 0x3c
+;   mv a0, sp
+;   lui t6, 3
+;   add sp, t6, sp
+;   ld ra, 8(sp)
+;   ld s0, 0(sp)
+;   addi sp, sp, 0x10
+;   ret
+
+function %large() -> i64 system_v {
+ss0 = explicit_slot 100000
+
+block0:
+  v1 = stack_addr.i64 ss0
+  return v1
+}
+
+; VCode:
+;   addi sp,sp,-16
+;   sd ra,8(sp)
+;   sd fp,0(sp)
+;   mv fp,sp
+;   inline_stack_probe##guard_size=4096 probe_count=25 tmp=t3
+;   lui t6,-24
+;   addi t6,t6,-1696
+;   add sp,t6,sp
+; block0:
+;   load_addr a0,0(nominal_sp)
+;   lui t6,24
+;   addi t6,t6,1696
+;   add sp,t6,sp
+;   ld ra,8(sp)
+;   ld fp,0(sp)
+;   addi sp,sp,16
+;   ret
+;
+; Disassembled:
+; block0: ; offset 0x0
+;   addi sp, sp, -0x10
+;   sd ra, 8(sp)
+;   sd s0, 0(sp)
+;   mv s0, sp
+;   lui t6, 0x19
+;   lui t3, 1
+;   bgeu t3, t6, 0x14
+;   sub t5, sp, t6
+;   sb zero, 0(t5)
+;   sub t6, t6, t3
+;   j -0x10
+;   lui t6, 0xfffe8
+;   addi t6, t6, -0x6a0
+;   add sp, t6, sp
+; block1: ; offset 0x38
+;   mv a0, sp
+;   lui t6, 0x18
+;   addi t6, t6, 0x6a0
+;   add sp, t6, sp
+;   ld ra, 8(sp)
+;   ld s0, 0(sp)
+;   addi sp, sp, 0x10
+;   ret
+
diff --git a/cranelift/filetests/filetests/isa/riscv64/wasm/zbb.wat b/cranelift/filetests/filetests/isa/riscv64/wasm/zbb.wat
index e307e6a6a..f0cc3f4cc 100644
--- a/cranelift/filetests/filetests/isa/riscv64/wasm/zbb.wat
+++ b/cranelift/filetests/filetests/isa/riscv64/wasm/zbb.wat
@@ -76,7 +76,7 @@
 ;; block0:
 ;;   j label1
 ;; block1:
-;;   roriw a0,a0,100
+;;   roriw a0,a0,4
 ;;   ret
 ;;
 ;; function u0:7:
diff --git a/cranelift/filetests/filetests/isa/x64/inline-probestack-large.clif b/cranelift/filetests/filetests/isa/x64/inline-probestack-large.clif
index adeab023c..c2193fd97 100644
--- a/cranelift/filetests/filetests/isa/x64/inline-probestack-large.clif
+++ b/cranelift/filetests/filetests/isa/x64/inline-probestack-large.clif
@@ -50,9 +50,13 @@ block0:
 ; VCode:
 ;   pushq   %rbp
 ;   movq    %rsp, %rbp
-;   movl    %esp, -65536(%rsp)
-;   movl    %esp, -131072(%rsp)
-;   movl    %esp, -196608(%rsp)
+;   subq    %rsp, $65536, %rsp
+;   movl    %esp, 0(%rsp)
+;   subq    %rsp, $65536, %rsp
+;   movl    %esp, 0(%rsp)
+;   subq    %rsp, $65536, %rsp
+;   movl    %esp, 0(%rsp)
+;   addq    %rsp, $196608, %rsp
 ;   subq    %rsp, $196608, %rsp
 ; block0:
 ;   lea     rsp(0 + virtual offset), %rax
@@ -65,11 +69,15 @@ block0:
 ; block0: ; offset 0x0
 ;   pushq %rbp
 ;   movq %rsp, %rbp
-;   movl %esp, -0x10000(%rsp)
-;   movl %esp, -0x20000(%rsp)
-;   movl %esp, -0x30000(%rsp)
+;   subq $0x10000, %rsp
+;   movl %esp, (%rsp)
+;   subq $0x10000, %rsp
+;   movl %esp, (%rsp)
+;   subq $0x10000, %rsp
+;   movl %esp, (%rsp)
+;   addq $0x30000, %rsp
 ;   subq $0x30000, %rsp
-; block1: ; offset 0x20
+; block1: ; offset 0x30
 ;   leaq (%rsp), %rax
 ;   addq $0x30000, %rsp
 ;   movq %rbp, %rsp
diff --git a/cranelift/filetests/filetests/isa/x64/inline-probestack.clif b/cranelift/filetests/filetests/isa/x64/inline-probestack.clif
index c55c87000..afb5b93fe 100644
--- a/cranelift/filetests/filetests/isa/x64/inline-probestack.clif
+++ b/cranelift/filetests/filetests/isa/x64/inline-probestack.clif
@@ -49,9 +49,13 @@ block0:
 ; VCode:
 ;   pushq   %rbp
 ;   movq    %rsp, %rbp
-;   movl    %esp, -4096(%rsp)
-;   movl    %esp, -8192(%rsp)
-;   movl    %esp, -12288(%rsp)
+;   subq    %rsp, $4096, %rsp
+;   movl    %esp, 0(%rsp)
+;   subq    %rsp, $4096, %rsp
+;   movl    %esp, 0(%rsp)
+;   subq    %rsp, $4096, %rsp
+;   movl    %esp, 0(%rsp)
+;   addq    %rsp, $12288, %rsp
 ;   subq    %rsp, $12288, %rsp
 ; block0:
 ;   lea     rsp(0 + virtual offset), %rax
@@ -64,11 +68,15 @@ block0:
 ; block0: ; offset 0x0
 ;   pushq %rbp
 ;   movq %rsp, %rbp
-;   movl %esp, -0x1000(%rsp)
-;   movl %esp, -0x2000(%rsp)
-;   movl %esp, -0x3000(%rsp)
+;   subq $0x1000, %rsp
+;   movl %esp, (%rsp)
+;   subq $0x1000, %rsp
+;   movl %esp, (%rsp)
+;   subq $0x1000, %rsp
+;   movl %esp, (%rsp)
+;   addq $0x3000, %rsp
 ;   subq $0x3000, %rsp
-; block1: ; offset 0x20
+; block1: ; offset 0x30
 ;   leaq (%rsp), %rax
 ;   addq $0x3000, %rsp
 ;   movq %rbp, %rsp
diff --git a/tests/misc_testsuite/winch/local_get.wast b/tests/misc_testsuite/winch/local_get.wast
new file mode 100644
index 000000000..16e06113d
--- /dev/null
+++ b/tests/misc_testsuite/winch/local_get.wast
@@ -0,0 +1,103 @@
+;; Test `local.get` operator
+
+(module
+  ;; Typing
+
+  (func (export "type-local-i32") (result i32) (local i32) (local.get 0))
+  (func (export "type-local-i64") (result i64) (local i64) (local.get 0))
+  (func (export "type-local-f32") (result f32) (local f32) (local.get 0))
+  (func (export "type-local-f64") (result f64) (local f64) (local.get 0))
+
+  (func (export "type-param-i32") (param i32) (result i32) (local.get 0))
+  (func (export "type-param-i64") (param i64) (result i64) (local.get 0))
+  (func (export "type-param-f32") (param f32) (result f32) (local.get 0))
+  (func (export "type-param-f64") (param f64) (result f64) (local.get 0))
+
+  (func (export "type-mixed") (param i64 f32 f64 i32 i32)
+    (local f32 i64 i64 f64)
+    (drop (i64.eqz (local.get 0)))
+    (drop (f32.neg (local.get 1)))
+    (drop (f64.neg (local.get 2)))
+    (drop (i32.eqz (local.get 3)))
+    (drop (i32.eqz (local.get 4)))
+    (drop (f32.neg (local.get 5)))
+    (drop (i64.eqz (local.get 6)))
+    (drop (i64.eqz (local.get 7)))
+    (drop (f64.neg (local.get 8)))
+  )
+
+
+  ;; As parameter of control constructs and instructions
+
+  (func (export "as-block-value") (param i32) (result i32)
+    (block (result i32) (local.get 0))
+  )
+  (func (export "as-loop-value") (param i32) (result i32)
+    (loop (result i32) (local.get 0))
+  )
+  (func (export "as-br-value") (param i32) (result i32)
+    (block (result i32) (br 0 (local.get 0)))
+  )
+  (func (export "as-br_if-value") (param i32) (result i32)
+    (block $l0 (result i32) (br_if $l0 (local.get 0) (i32.const 1)))
+  )
+
+  (func (export "as-br_if-value-cond") (param i32) (result i32)
+    (block (result i32)
+      (br_if 0 (local.get 0) (local.get 0))
+    )
+  )
+  (func (export "as-br_table-value") (param i32) (result i32)
+    (block
+      (block
+        (block
+          (br_table 0 1 2 (local.get 0))
+          (return (i32.const 0))
+        )
+        (return (i32.const 1))
+      )
+      (return (i32.const 2))
+    )
+    (i32.const 3)
+  )
+
+  (func (export "as-return-value") (param i32) (result i32)
+    (return (local.get 0))
+  )
+
+  (func (export "as-if-then") (param i32) (result i32)
+    (if (result i32) (local.get 0) (then (local.get 0)) (else (i32.const 0)))
+  )
+  (func (export "as-if-else") (param i32) (result i32)
+    (if (result i32) (local.get 0) (then (i32.const 1)) (else (local.get 0)))
+  )
+)
+
+(assert_return (invoke "type-local-i32") (i32.const 0))
+(assert_return (invoke "type-local-i64") (i64.const 0))
+(assert_return (invoke "type-local-f32") (f32.const 0))
+(assert_return (invoke "type-local-f64") (f64.const 0))
+
+(assert_return (invoke "type-param-i32" (i32.const 2)) (i32.const 2))
+(assert_return (invoke "type-param-i64" (i64.const 3)) (i64.const 3))
+(assert_return (invoke "type-param-f32" (f32.const 4.4)) (f32.const 4.4))
+(assert_return (invoke "type-param-f64" (f64.const 5.5)) (f64.const 5.5))
+
+(assert_return (invoke "as-block-value" (i32.const 6)) (i32.const 6))
+(assert_return (invoke "as-loop-value" (i32.const 7)) (i32.const 7))
+
+(assert_return (invoke "as-br-value" (i32.const 8)) (i32.const 8))
+(assert_return (invoke "as-br_if-value" (i32.const 9)) (i32.const 9))
+(assert_return (invoke "as-br_if-value-cond" (i32.const 10)) (i32.const 10))
+(assert_return (invoke "as-br_table-value" (i32.const 1)) (i32.const 2))
+
+(assert_return (invoke "as-return-value" (i32.const 0)) (i32.const 0))
+
+(assert_return (invoke "as-if-then" (i32.const 1)) (i32.const 1))
+(assert_return (invoke "as-if-else" (i32.const 0)) (i32.const 0))
+
+(assert_return
+  (invoke "type-mixed"
+    (i64.const 1) (f32.const 2.2) (f64.const 3.3) (i32.const 4) (i32.const 5)
+  )
+)
diff --git a/tests/misc_testsuite/winch/local_set.wast b/tests/misc_testsuite/winch/local_set.wast
new file mode 100644
index 000000000..f368f7bf3
--- /dev/null
+++ b/tests/misc_testsuite/winch/local_set.wast
@@ -0,0 +1,97 @@
+;; Test `local.set` operator
+
+(module
+  ;; Typing
+
+  (func (export "type-local-i32") (local i32) (local.set 0 (i32.const 0)))
+  (func (export "type-local-i64") (local i64) (local.set 0 (i64.const 0)))
+  (func (export "type-local-f32") (local f32) (local.set 0 (f32.const 0)))
+  (func (export "type-local-f64") (local f64) (local.set 0 (f64.const 0)))
+
+  (func (export "type-param-i32") (param i32) (local.set 0 (i32.const 10)))
+  (func (export "type-param-i64") (param i64) (local.set 0 (i64.const 11)))
+  (func (export "type-param-f32") (param f32) (local.set 0 (f32.const 11.1)))
+  (func (export "type-param-f64") (param f64) (local.set 0 (f64.const 12.2)))
+
+  (func (export "type-mixed") (param i64 f32 f64 i32 i32) (local f32 i64 i64 f64)
+    (local.set 0 (i64.const 0))
+    (local.set 1 (f32.const 0))
+    (local.set 2 (f64.const 0))
+    (local.set 3 (i32.const 0))
+    (local.set 4 (i32.const 0))
+    (local.set 5 (f32.const 0))
+    (local.set 6 (i64.const 0))
+    (local.set 7 (i64.const 0))
+    (local.set 8 (f64.const 0))
+  )
+
+  ;; As parameter of control constructs and instructions
+
+  (func (export "as-block-value") (param i32)
+    (block (local.set 0 (i32.const 1)))
+  )
+  (func (export "as-loop-value") (param i32)
+    (loop (local.set 0 (i32.const 3)))
+  )
+
+  (func (export "as-br-value") (param i32)
+    (block (br 0 (local.set 0 (i32.const 9))))
+  )
+  (func (export "as-br_if-value") (param i32)
+    (block
+      (br_if 0 (local.set 0 (i32.const 8)) (i32.const 1))
+    )
+  )
+  (func (export "as-br_if-value-cond") (param i32)
+    (block
+      (br_if 0 (i32.const 6) (local.set 0 (i32.const 9)))
+    )
+  )
+  (func (export "as-br_table-value") (param i32)
+    (block
+      (br_table 0 (local.set 0 (i32.const 10)) (i32.const 1))
+    )
+  )
+
+  (func (export "as-return-value") (param i32)
+    (return (local.set 0 (i32.const 7)))
+  )
+
+  (func (export "as-if-then") (param i32)
+    (if (local.get 0) (then (local.set 0 (i32.const 3))))
+  )
+  (func (export "as-if-else") (param i32)
+    (if (local.get 0) (then) (else (local.set 0 (i32.const 1))))
+  )
+)
+
+(assert_return (invoke "type-local-i32"))
+(assert_return (invoke "type-local-i64"))
+(assert_return (invoke "type-local-f32"))
+(assert_return (invoke "type-local-f64"))
+
+(assert_return (invoke "type-param-i32" (i32.const 2)))
+(assert_return (invoke "type-param-i64" (i64.const 3)))
+(assert_return (invoke "type-param-f32" (f32.const 4.4)))
+(assert_return (invoke "type-param-f64" (f64.const 5.5)))
+
+(assert_return (invoke "as-block-value" (i32.const 0)))
+(assert_return (invoke "as-loop-value" (i32.const 0)))
+
+(assert_return (invoke "as-br-value" (i32.const 0)))
+(assert_return (invoke "as-br_if-value" (i32.const 0)))
+(assert_return (invoke "as-br_if-value-cond" (i32.const 0)))
+(assert_return (invoke "as-br_table-value" (i32.const 0)))
+
+(assert_return (invoke "as-return-value" (i32.const 0)))
+
+(assert_return (invoke "as-if-then" (i32.const 1)))
+(assert_return (invoke "as-if-else" (i32.const 0)))
+
+(assert_return
+  (invoke "type-mixed"
+    (i64.const 1) (f32.const 2.2) (f64.const 3.3) (i32.const 4) (i32.const 5)
+  )
+)
+
+