Repository: bytecodealliance/wasmtime
Pull request #8121 (2024-03-13 18:40:19)

Run all `*.wast` tests in fuzzing
Currently we have a `spectest` fuzzer which uses fuzz input to generate an arbitrary configuration for Wasmtime and then executes the spec test. This ensures that no matter the configuration Wasmtime can pass spec tests. This commit expands this testing to include all `*.wast` tests we have in this repository. While we don't have a ton we still have some significant ones like in #8118 which will only reproduce when turning knobs on CPU features.

<!--
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 045f00d0b..eb48f0ad9 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -354,6 +354,12 @@ jobs:
     - run: rustup component add clippy
     - run: cargo clippy --workspace
 
+    # Re-vendor all WIT files and ensure that they're all up-to-date by ensuring
+    # that there's no git changes.
+    - name: Re-vendor WIT
+      run: ./ci/vendor-wit.sh
+    - run: git diff --exit-code
+
     # common logic to cancel the entire run if this job fails
     - run: gh run cancel ${{ github.run_id }}
       if: failure() && github.event_name != 'pull_request'
@@ -526,12 +532,6 @@ jobs:
         RUST_BACKTRACE: 1
       if: matrix.target == '' && matrix.os != 'windows-latest' && needs.determine.outputs.test-capi
 
-    # Ensure wit definitions are in sync: both wasmtime-wasi and wasmtime-wasi-http need their own
-    # copy of the wit definitions so publishing works, but we need to ensure they are identical copies.
-    - name: Check that the wasi and wasi-http wit directories agree
-      run: |
-        diff -ru crates/wasi/wit crates/wasi-http/wit
-
     # Record some CPU details; this is helpful information if tests fail due
     # to CPU-specific features.
     - name: CPU information
diff --git a/ci/vendor-wit.sh b/ci/vendor-wit.sh
new file mode 100755
index 000000000..d594de918
--- /dev/null
+++ b/ci/vendor-wit.sh
@@ -0,0 +1,33 @@
+#!/bin/sh
+
+# Script to re-vendor the WIT files that Wasmtime uses as defined by a
+# particular tag in upstream repositories.
+#
+# This script is executed on CI to ensure that everything is up-to-date.
+
+# Space-separated list of wasi proposals that are vendored here along with the
+# tag that they're all vendored at.
+#
+# This assumes that the repositories all have the pattern:
+# https://github.com/WebAssembly/wasi-$repo
+# and every repository has a tag `v$tag` here. That is currently done as part
+# of the WASI release process.
+repos="cli clocks filesystem http io random sockets"
+tag=0.2.0
+
+set -ex
+
+dst=crates/wasi/wit/deps
+
+rm -rf $dst
+mkdir -p $dst
+
+for repo in $repos; do
+  mkdir $dst/$repo
+  curl -L https://github.com/WebAssembly/wasi-$repo/archive/refs/tags/v$tag.tar.gz | \
+    tar xzf - --strip-components=2 -C $dst/$repo wasi-$repo-$tag/wit
+  rm -rf $dst/$repo/deps*
+done
+
+rm -rf crates/wasi-http/wit/deps
+cp -r $dst crates/wasi-http/wit
diff --git a/cranelift/codegen/src/isa/riscv64/inst.isle b/cranelift/codegen/src/isa/riscv64/inst.isle
index cbaeea578..8806ac3c5 100644
--- a/cranelift/codegen/src/isa/riscv64/inst.isle
+++ b/cranelift/codegen/src/isa/riscv64/inst.isle
@@ -1644,7 +1644,7 @@
 ;; Helper for emitting the `min` instruction.
 (decl rv_min (XReg XReg) XReg)
 (rule (rv_min rs1 rs2)
-  (alu_rrr (AluOPRRR.Max) rs1 rs2))
+  (alu_rrr (AluOPRRR.Min) rs1 rs2))
 
 ;; Helper for emitting the `minu` instruction.
 (decl rv_minu (XReg XReg) XReg)
diff --git a/cranelift/codegen/src/isa/x64/inst.isle b/cranelift/codegen/src/isa/x64/inst.isle
index ea698b189..0da546b6b 100644
--- a/cranelift/codegen/src/isa/x64/inst.isle
+++ b/cranelift/codegen/src/isa/x64/inst.isle
@@ -1570,7 +1570,7 @@
 (extern constructor xmm_to_xmm_mem xmm_to_xmm_mem)
 
 ;; Construct a new `XmmMem` from an `RegMem`.
-(decl xmm_mem_to_reg_mem (XmmMem) RegMem)
+(decl pure xmm_mem_to_reg_mem (XmmMem) RegMem)
 (extern constructor xmm_mem_to_reg_mem xmm_mem_to_reg_mem)
 
 ;; Convert a `GprMem` to a `RegMem`.
@@ -3761,21 +3761,52 @@
       (xmm_vex_pinsr (AvxOpcode.Vpinsrq) src1 src2 lane))
 
 ;; Helper for creating `roundss` instructions.
+;;
+;; NB: the non-AVX variant of this instruction does not require that the operand
+;; is aligned, but the instruction variant used here requires it to be aligned.
+;; This means that if the operand here points to memory then we'll eagerly load
+;; it, but the eager load will load too much (the full register width of 16
+;; bytes). To fix this a `put_xmm_mem_in_xmm` helper is used to force the
+;; operand to be a register where the load is done with the appropriate type if
+;; it's in memory.
+;;
+;; Some more discussion of this can be found on #8112. Ideally this would use
+;; a variant that allows an unaligned `XmmMem` in the instruction variant.
+;; Either that or the auto-conversion to aligned memory is removed and/or starts
+;; to require a type.
+;;
+;; Also note that the argument here isn't changed to `Xmm` instead of `XmmMem`
+;; because the AVX variant, when available, doesn't need alignment and can
+;; pass through the `src1` argument as-is.
 (decl x64_roundss (XmmMem RoundImm) Xmm)
 (rule (x64_roundss src1 round)
-      (xmm_unary_rm_r_imm (SseOpcode.Roundss) src1 (encode_round_imm round)))
+      (xmm_unary_rm_r_imm (SseOpcode.Roundss) (put_xmm_mem_in_xmm $F32 src1) (encode_round_imm round)))
 (rule 1 (x64_roundss src1 round)
         (if-let $true (use_avx))
         (xmm_unary_rm_r_imm_vex (AvxOpcode.Vroundss) src1 (encode_round_imm round)))
 
 ;; Helper for creating `roundsd` instructions.
+;;
+;; NB: see `x64_roundss` above for `put_xmm_mem_in_xmm` in call.
 (decl x64_roundsd (XmmMem RoundImm) Xmm)
 (rule (x64_roundsd src1 round)
-      (xmm_unary_rm_r_imm (SseOpcode.Roundsd) src1 (encode_round_imm round)))
+      (xmm_unary_rm_r_imm (SseOpcode.Roundsd) (put_xmm_mem_in_xmm $F64 src1) (encode_round_imm round)))
 (rule 1 (x64_roundsd src1 round)
         (if-let $true (use_avx))
         (xmm_unary_rm_r_imm_vex (AvxOpcode.Vroundsd) src1 (encode_round_imm round)))
 
+;; Helper for forcing an `XmmMem` to be placed into an `Xmm` register.
+;;
+;; If it's already in a register it stays there, otherwise it turns into a
+;; load instruction with the destination register as output.
+(decl put_xmm_mem_in_xmm (Type XmmMem) Xmm)
+(rule (put_xmm_mem_in_xmm ty xmm_mem)
+      (if-let (RegMem.Reg r) (xmm_mem_to_reg_mem xmm_mem))
+      (xmm_new r))
+(rule (put_xmm_mem_in_xmm ty xmm_mem)
+      (if-let (RegMem.Mem amode) (xmm_mem_to_reg_mem xmm_mem))
+      (x64_load ty amode (ExtKind.None)))
+
 ;; Helper for creating `roundps` instructions.
 (decl x64_roundps (XmmMem RoundImm) Xmm)
 (rule (x64_roundps src1 round)
diff --git a/crates/fuzzing/build.rs b/crates/fuzzing/build.rs
index 428355beb..8ea2e8aa1 100644
--- a/crates/fuzzing/build.rs
+++ b/crates/fuzzing/build.rs
@@ -1,4 +1,4 @@
-// A small build script to include the contents of the spec test suite into the
+// A small build script to include the contents of the wast test suite into the
 // final fuzzing binary so the fuzzing binary can be run elsewhere and doesn't
 // rely on the original source tree.
 
@@ -9,22 +9,34 @@ fn main() {
     println!("cargo:rerun-if-changed=build.rs");
 
     let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap());
-    let dir = env::current_dir()
-        .unwrap()
-        .join("../../tests/spec_testsuite");
+
+    let dirs = [
+        "tests/spec_testsuite",
+        "tests/misc_testsuite",
+        "tests/misc_testsuite/multi-memory",
+        "tests/misc_testsuite/simd",
+        "tests/misc_testsuite/threads",
+    ];
+    let mut root = env::current_dir().unwrap();
+    root.pop(); // chop off 'fuzzing'
+    root.pop(); // chop off 'crates'
     let mut code = format!("static FILES: &[(&str, &str)] = &[\n");
-    let mut entries = dir
-        .read_dir()
-        .unwrap()
-        .map(|p| p.unwrap().path().display().to_string())
-        .collect::<Vec<_>>();
+
+    let mut entries = Vec::new();
+    for dir in dirs {
+        for entry in root.join(dir).read_dir().unwrap() {
+            let entry = entry.unwrap();
+            let path = entry.path();
+            if path.extension().and_then(|s| s.to_str()) == Some("wast") {
+                entries.push(path);
+            }
+        }
+    }
     entries.sort();
     for path in entries {
-        if !path.ends_with(".wast") {
-            continue;
-        }
+        let path = path.to_str().expect("path is not valid utf-8");
         code.push_str(&format!("({:?}, include_str!({0:?})),\n", path));
     }
     code.push_str("];\n");
-    std::fs::write(out_dir.join("spectests.rs"), code).unwrap();
+    std::fs::write(out_dir.join("wasttests.rs"), code).unwrap();
 }
diff --git a/crates/fuzzing/src/generators.rs b/crates/fuzzing/src/generators.rs
index d7703631d..08a1793db 100644
--- a/crates/fuzzing/src/generators.rs
+++ b/crates/fuzzing/src/generators.rs
@@ -17,10 +17,10 @@ mod memory;
 mod module;
 mod pooling_config;
 mod single_inst_module;
-mod spec_test;
 mod stacks;
 pub mod table_ops;
 mod value;
+mod wast_test;
 
 pub use codegen_settings::CodegenSettings;
 pub use config::CompilerStrategy;
@@ -30,6 +30,6 @@ pub use memory::{MemoryConfig, NormalMemoryConfig, UnalignedMemory, UnalignedMem
 pub use module::ModuleConfig;
 pub use pooling_config::PoolingAllocationConfig;
 pub use single_inst_module::SingleInstModule;
-pub use spec_test::SpecTest;
 pub use stacks::Stacks;
 pub use value::{DiffValue, DiffValueType};
+pub use wast_test::WastTest;
diff --git a/crates/fuzzing/src/generators/config.rs b/crates/fuzzing/src/generators/config.rs
index 0840772fb..7c47300c4 100644
--- a/crates/fuzzing/src/generators/config.rs
+++ b/crates/fuzzing/src/generators/config.rs
@@ -100,12 +100,12 @@ impl Config {
         self.module_config.generate(input, default_fuel)
     }
 
-    /// Tests whether this configuration is capable of running all spec tests.
-    pub fn is_spectest_compliant(&self) -> bool {
+    /// Tests whether this configuration is capable of running all wast tests.
+    pub fn is_wast_test_compliant(&self) -> bool {
         let config = &self.module_config.config;
 
         // Check for wasm features that must be disabled to run spec tests
-        if config.memory64_enabled || config.threads_enabled {
+        if config.memory64_enabled {
             return false;
         }
 
@@ -114,6 +114,8 @@ impl Config {
             || !config.reference_types_enabled
             || !config.multi_value_enabled
             || !config.simd_enabled
+            || !config.threads_enabled
+            || config.max_memories <= 1
         {
             return false;
         }
@@ -412,7 +414,7 @@ pub struct WasmtimeConfig {
     canonicalize_nans: bool,
     interruptable: bool,
     pub(crate) consume_fuel: bool,
-    epoch_interruption: bool,
+    pub(crate) epoch_interruption: bool,
     /// The Wasmtime memory configuration to use.
     pub memory_config: MemoryConfig,
     force_jump_veneers: bool,
diff --git a/crates/fuzzing/src/oracles.rs b/crates/fuzzing/src/oracles.rs
index d10f439de..bb3bc1450 100644
--- a/crates/fuzzing/src/oracles.rs
+++ b/crates/fuzzing/src/oracles.rs
@@ -574,17 +574,31 @@ pub fn make_api_calls(api: generators::api::ApiCalls) {
     }
 }
 
-/// Executes the wast `test` spectest with the `config` specified.
+/// Executes the wast `test` with the `config` specified.
 ///
-/// Ensures that spec tests pass regardless of the `Config`.
-pub fn spectest(fuzz_config: generators::Config, test: generators::SpecTest) {
+/// Ensures that wast tests pass regardless of the `Config`.
+pub fn wast_test(fuzz_config: generators::Config, test: generators::WastTest) {
     crate::init_fuzzing();
-    if !fuzz_config.is_spectest_compliant() {
+    if !fuzz_config.is_wast_test_compliant() {
         return;
     }
+
+    // Fuel and epochs don't play well with threads right now, so exclude any
+    // thread-spawning test if it looks like threads are spawned in that case.
+    if fuzz_config.wasmtime.consume_fuel || fuzz_config.wasmtime.epoch_interruption {
+        if test.contents.contains("(thread") {
+            return;
+        }
+    }
+
     log::debug!("running {:?}", test.file);
     let mut wast_context = WastContext::new(fuzz_config.to_store());
-    wast_context.register_spectest(false).unwrap();
+    wast_context
+        .register_spectest(&wasmtime_wast::SpectestConfig {
+            use_shared_memory: false,
+            suppress_prints: true,
+        })
+        .unwrap();
     wast_context
         .run_buffer(test.file, test.contents.as_bytes())
         .unwrap();
diff --git a/crates/wasi-common/Cargo.toml b/crates/wasi-common/Cargo.toml
index cef69249b..a872837d8 100644
--- a/crates/wasi-common/Cargo.toml
+++ b/crates/wasi-common/Cargo.toml
@@ -93,6 +93,9 @@ tokio = [
 ]
 exit = [ "wasmtime", "dep:libc" ]
 
+[package.metadata.docs.rs]
+all-features = true
+
 [[test]]
 name = "all"
 required-features = ["wasmtime", "sync", "tokio"]
diff --git a/crates/wasi-common/src/lib.rs b/crates/wasi-common/src/lib.rs
index b47fa434e..29c7b2a5a 100644
--- a/crates/wasi-common/src/lib.rs
+++ b/crates/wasi-common/src/lib.rs
@@ -68,6 +68,7 @@
 //! interface to plug in its own implementations of each of these resources.
 
 #![warn(clippy::cast_sign_loss)]
+#![cfg_attr(docsrs, feature(doc_cfg))]
 
 pub mod clocks;
 mod ctx;
@@ -79,9 +80,11 @@ pub mod random;
 pub mod sched;
 pub mod snapshots;
 mod string_array;
+#[cfg_attr(docsrs, doc(cfg(feature = "sync")))]
 #[cfg(feature = "sync")]
 pub mod sync;
 pub mod table;
+#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))]
 #[cfg(feature = "tokio")]
 pub mod tokio;
 
@@ -150,6 +153,7 @@ macro_rules! define_wasi {
 /// Note: this function is designed for usage where it is acceptable for
 /// Wasmtime failures to terminate the parent process, such as in the Wasmtime
 /// CLI; this would not be suitable for use in multi-tenant embeddings.
+#[cfg_attr(docsrs, doc(cfg(feature = "exit")))]
 #[cfg(feature = "exit")]
 pub fn maybe_exit_on_error(e: anyhow::Error) -> anyhow::Error {
     use std::process;
diff --git a/crates/wasi-http/wit/deps/io/poll.wit b/crates/wasi-http/wit/deps/io/poll.wit
index 8139064c3..ddc67f8b7 100644
--- a/crates/wasi-http/wit/deps/io/poll.wit
+++ b/crates/wasi-http/wit/deps/io/poll.wit
@@ -27,9 +27,8 @@ interface poll {
     /// The result `list<u32>` contains one or more indices of handles in the
     /// argument list that is ready for I/O.
     ///
-    /// This function traps if either:
-    /// - the list is empty, or:
-    /// - the list contains more elements than can be indexed with a `u32` value.
+    /// If the list contains more elements than can be indexed with a `u32`
+    /// value, this function traps.
     ///
     /// A timeout can be implemented by adding a pollable from the
     /// wasi-clocks API to the list.
diff --git a/crates/wasi/wit/deps/io/poll.wit b/crates/wasi/wit/deps/io/poll.wit
index 8139064c3..ddc67f8b7 100644
--- a/crates/wasi/wit/deps/io/poll.wit
+++ b/crates/wasi/wit/deps/io/poll.wit
@@ -27,9 +27,8 @@ interface poll {
     /// The result `list<u32>` contains one or more indices of handles in the
     /// argument list that is ready for I/O.
     ///
-    /// This function traps if either:
-    /// - the list is empty, or:
-    /// - the list contains more elements than can be indexed with a `u32` value.
+    /// If the list contains more elements than can be indexed with a `u32`
+    /// value, this function traps.
     ///
     /// A timeout can be implemented by adding a pollable from the
     /// wasi-clocks API to the list.
diff --git a/crates/wast/src/lib.rs b/crates/wast/src/lib.rs
index 9539934e0..d8749e6e1 100644
--- a/crates/wast/src/lib.rs
+++ b/crates/wast/src/lib.rs
@@ -8,7 +8,7 @@ mod core;
 mod spectest;
 mod wast;
 
-pub use crate::spectest::link_spectest;
+pub use crate::spectest::{link_spectest, SpectestConfig};
 pub use crate::wast::WastContext;
 
 /// Version number of this crate.
diff --git a/crates/wast/src/wast.rs b/crates/wast/src/wast.rs
index fcb2b8d47..19b0b3786 100644
--- a/crates/wast/src/wast.rs
+++ b/crates/wast/src/wast.rs
@@ -137,8 +137,8 @@ where
     }
 
     /// Register "spectest" which is used by the spec testsuite.
-    pub fn register_spectest(&mut self, use_shared_memory: bool) -> Result<()> {
-        link_spectest(&mut self.core_linker, &mut self.store, use_shared_memory)?;
+    pub fn register_spectest(&mut self, config: &SpectestConfig) -> Result<()> {
+        link_spectest(&mut self.core_linker, &mut self.store, config)?;
         #[cfg(feature = "component-model")]
         link_component_spectest(&mut self.component_linker)?;
         Ok(())
diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml
index 2eea42d97..c6bce0790 100644
--- a/fuzz/Cargo.toml
+++ b/fuzz/Cargo.toml
@@ -68,8 +68,8 @@ test = false
 doc = false
 
 [[bin]]
-name = "spectests"
-path = "fuzz_targets/spectests.rs"
+name = "wast_tests"
+path = "fuzz_targets/wast_tests.rs"
 test = false
 doc = false
 
diff --git a/src/commands/wast.rs b/src/commands/wast.rs
index 1edeeb4dc..50f8d15ba 100644
--- a/src/commands/wast.rs
+++ b/src/commands/wast.rs
@@ -5,7 +5,7 @@ use clap::Parser;
 use std::path::PathBuf;
 use wasmtime::{Engine, Store};
 use wasmtime_cli_flags::CommonOptions;
-use wasmtime_wast::WastContext;
+use wasmtime_wast::{SpectestConfig, WastContext};
 
 /// Runs a WebAssembly test script file
 #[derive(Parser, PartialEq)]
@@ -28,7 +28,10 @@ impl WastCommand {
         let mut wast_context = WastContext::new(store);
 
         wast_context
-            .register_spectest(true)
+            .register_spectest(&SpectestConfig {
+                use_shared_memory: true,
+                suppress_prints: false,
+            })
             .expect("error instantiating \"spectest\"");
 
         for script in self.scripts.iter() {


Test patch:
diff --git a/cranelift/filetests/filetests/isa/riscv64/smin-zbb.clif b/cranelift/filetests/filetests/isa/riscv64/smin-zbb.clif
index c1aa1db66..b2e674c0a 100644
--- a/cranelift/filetests/filetests/isa/riscv64/smin-zbb.clif
+++ b/cranelift/filetests/filetests/isa/riscv64/smin-zbb.clif
@@ -12,14 +12,14 @@ block0(v0: i8, v1: i8):
 ; block0:
 ;   sext.b a3,a0
 ;   sext.b a5,a1
-;   max a0,a3,a5
+;   min a0,a3,a5
 ;   ret
 ;
 ; Disassembled:
 ; block0: ; offset 0x0
 ;   .byte 0x93, 0x16, 0x45, 0x60
 ;   .byte 0x93, 0x97, 0x45, 0x60
-;   .byte 0x33, 0xe5, 0xf6, 0x0a
+;   .byte 0x33, 0xc5, 0xf6, 0x0a
 ;   ret
 
 function %smin_i16(i16, i16) -> i16{
@@ -32,14 +32,14 @@ block0(v0: i16, v1: i16):
 ; block0:
 ;   sext.h a3,a0
 ;   sext.h a5,a1
-;   max a0,a3,a5
+;   min a0,a3,a5
 ;   ret
 ;
 ; Disassembled:
 ; block0: ; offset 0x0
 ;   .byte 0x93, 0x16, 0x55, 0x60
 ;   .byte 0x93, 0x97, 0x55, 0x60
-;   .byte 0x33, 0xe5, 0xf6, 0x0a
+;   .byte 0x33, 0xc5, 0xf6, 0x0a
 ;   ret
 
 function %smin_i32(i32, i32) -> i32{
@@ -52,14 +52,14 @@ block0(v0: i32, v1: i32):
 ; block0:
 ;   sext.w a3,a0
 ;   sext.w a5,a1
-;   max a0,a3,a5
+;   min a0,a3,a5
 ;   ret
 ;
 ; Disassembled:
 ; block0: ; offset 0x0
 ;   sext.w a3, a0
 ;   sext.w a5, a1
-;   .byte 0x33, 0xe5, 0xf6, 0x0a
+;   .byte 0x33, 0xc5, 0xf6, 0x0a
 ;   ret
 
 function %smin_i64(i64, i64) -> i64{
@@ -70,12 +70,12 @@ block0(v0: i64, v1: i64):
 
 ; VCode:
 ; block0:
-;   max a0,a0,a1
+;   min a0,a0,a1
 ;   ret
 ;
 ; Disassembled:
 ; block0: ; offset 0x0
-;   .byte 0x33, 0x65, 0xb5, 0x0a
+;   .byte 0x33, 0x45, 0xb5, 0x0a
 ;   ret
 
 function %smin_i128(i128, i128) -> i128{
diff --git a/cranelift/filetests/filetests/runtests/integer-minmax.clif b/cranelift/filetests/filetests/runtests/integer-minmax.clif
index a109cfb53..668f257d1 100644
--- a/cranelift/filetests/filetests/runtests/integer-minmax.clif
+++ b/cranelift/filetests/filetests/runtests/integer-minmax.clif
@@ -4,6 +4,7 @@ target aarch64
 target s390x
 target x86_64
 target riscv64
+target riscv64 has_zbb
 target riscv64 has_c has_zcb
 
 
diff --git a/crates/fuzzing/src/generators/spec_test.rs b/crates/fuzzing/src/generators/wast_test.rs
similarity index 72%
rename from crates/fuzzing/src/generators/spec_test.rs
rename to crates/fuzzing/src/generators/wast_test.rs
index 2e11e46a1..f93a38786 100644
--- a/crates/fuzzing/src/generators/spec_test.rs
+++ b/crates/fuzzing/src/generators/wast_test.rs
@@ -3,24 +3,23 @@
 use arbitrary::{Arbitrary, Unstructured};
 
 // See `build.rs` for how the `FILES` array is generated.
-include!(concat!(env!("OUT_DIR"), "/spectests.rs"));
+include!(concat!(env!("OUT_DIR"), "/wasttests.rs"));
 
-/// A spec test from the upstream wast testsuite, arbitrarily chosen from the
-/// list of known spec tests.
+/// A wast test from this repository.
 #[derive(Debug)]
-pub struct SpecTest {
+pub struct WastTest {
     /// The filename of the spec test
     pub file: &'static str,
     /// The `*.wast` contents of the spec test
     pub contents: &'static str,
 }
 
-impl<'a> Arbitrary<'a> for SpecTest {
+impl<'a> Arbitrary<'a> for WastTest {
     fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
         // NB: this does get a uniform value in the provided range.
         let i = u.int_in_range(0..=FILES.len() - 1)?;
         let (file, contents) = FILES[i];
-        Ok(SpecTest { file, contents })
+        Ok(WastTest { file, contents })
     }
 
     fn size_hint(_depth: usize) -> (usize, Option<usize>) {
diff --git a/crates/wast/src/spectest.rs b/crates/wast/src/spectest.rs
index ed8bb1f34..951acc9f7 100644
--- a/crates/wast/src/spectest.rs
+++ b/crates/wast/src/spectest.rs
@@ -1,24 +1,53 @@
 use wasmtime::*;
 
+/// Configuration of how spectest primitives work.
+pub struct SpectestConfig {
+    /// Whether or not to have a `shared_memory` definition.
+    pub use_shared_memory: bool,
+    /// Whether or not spectest functions that print things actually print things.
+    pub suppress_prints: bool,
+}
+
 /// Return an instance implementing the "spectest" interface used in the
 /// spec testsuite.
 pub fn link_spectest<T>(
     linker: &mut Linker<T>,
     store: &mut Store<T>,
-    use_shared_memory: bool,
+    config: &SpectestConfig,
 ) -> Result<()> {
+    let suppress = config.suppress_prints;
     linker.func_wrap("spectest", "print", || {})?;
-    linker.func_wrap("spectest", "print_i32", |val: i32| println!("{}: i32", val))?;
-    linker.func_wrap("spectest", "print_i64", |val: i64| println!("{}: i64", val))?;
-    linker.func_wrap("spectest", "print_f32", |val: f32| println!("{}: f32", val))?;
-    linker.func_wrap("spectest", "print_f64", |val: f64| println!("{}: f64", val))?;
-    linker.func_wrap("spectest", "print_i32_f32", |i: i32, f: f32| {
-        println!("{}: i32", i);
-        println!("{}: f32", f);
+    linker.func_wrap("spectest", "print_i32", move |val: i32| {
+        if !suppress {
+            println!("{}: i32", val)
+        }
+    })?;
+    linker.func_wrap("spectest", "print_i64", move |val: i64| {
+        if !suppress {
+            println!("{}: i64", val)
+        }
+    })?;
+    linker.func_wrap("spectest", "print_f32", move |val: f32| {
+        if !suppress {
+            println!("{}: f32", val)
+        }
+    })?;
+    linker.func_wrap("spectest", "print_f64", move |val: f64| {
+        if !suppress {
+            println!("{}: f64", val)
+        }
     })?;
-    linker.func_wrap("spectest", "print_f64_f64", |f1: f64, f2: f64| {
-        println!("{}: f64", f1);
-        println!("{}: f64", f2);
+    linker.func_wrap("spectest", "print_i32_f32", move |i: i32, f: f32| {
+        if !suppress {
+            println!("{}: i32", i);
+            println!("{}: f32", f);
+        }
+    })?;
+    linker.func_wrap("spectest", "print_f64_f64", move |f1: f64, f2: f64| {
+        if !suppress {
+            println!("{}: f64", f1);
+            println!("{}: f64", f2);
+        }
     })?;
 
     let ty = GlobalType::new(ValType::I32, Mutability::Const);
@@ -45,7 +74,7 @@ pub fn link_spectest<T>(
     let memory = Memory::new(&mut *store, ty)?;
     linker.define(&mut *store, "spectest", "memory", memory)?;
 
-    if use_shared_memory {
+    if config.use_shared_memory {
         let ty = MemoryType::shared(1, 1);
         let memory = Memory::new(&mut *store, ty)?;
         linker.define(&mut *store, "spectest", "shared_memory", memory)?;
diff --git a/fuzz/fuzz_targets/spectests.rs b/fuzz/fuzz_targets/spectests.rs
deleted file mode 100644
index 9ab1df1fe..000000000
--- a/fuzz/fuzz_targets/spectests.rs
+++ /dev/null
@@ -1,9 +0,0 @@
-#![no_main]
-
-use libfuzzer_sys::fuzz_target;
-use wasmtime_fuzzing::generators::{Config, SpecTest};
-
-fuzz_target!(|pair: (Config, SpecTest)| {
-    let (config, test) = pair;
-    wasmtime_fuzzing::oracles::spectest(config, test);
-});
diff --git a/fuzz/fuzz_targets/wast_tests.rs b/fuzz/fuzz_targets/wast_tests.rs
new file mode 100644
index 000000000..ed027cafc
--- /dev/null
+++ b/fuzz/fuzz_targets/wast_tests.rs
@@ -0,0 +1,9 @@
+#![no_main]
+
+use libfuzzer_sys::fuzz_target;
+use wasmtime_fuzzing::generators::{Config, WastTest};
+
+fuzz_target!(|pair: (Config, WastTest)| {
+    let (config, test) = pair;
+    wasmtime_fuzzing::oracles::wast_test(config, test);
+});
diff --git a/tests/all/wast.rs b/tests/all/wast.rs
index e4fe25be1..ef9170666 100644
--- a/tests/all/wast.rs
+++ b/tests/all/wast.rs
@@ -8,7 +8,7 @@ use wasmtime::{
 };
 use wasmtime_environ::WASM_PAGE_SIZE;
 use wasmtime_runtime::MpkEnabled;
-use wasmtime_wast::WastContext;
+use wasmtime_wast::{SpectestConfig, WastContext};
 
 include!(concat!(env!("OUT_DIR"), "/wast_testsuite_tests.rs"));
 
@@ -162,7 +162,10 @@ fn run_wast(wast: &str, strategy: Strategy, pooling: bool) -> anyhow::Result<()>
     for (engine, desc) in engines {
         let store = Store::new(&engine, ());
         let mut wast_context = WastContext::new(store);
-        wast_context.register_spectest(use_shared_memory)?;
+        wast_context.register_spectest(&SpectestConfig {
+            use_shared_memory,
+            suppress_prints: false,
+        })?;
         wast_context
             .run_buffer(wast.to_str().unwrap(), &wast_bytes)
             .with_context(|| format!("failed to run spec test with {desc} engine"))?;
diff --git a/tests/misc_testsuite/float-round-doesnt-load-too-much.wast b/tests/misc_testsuite/float-round-doesnt-load-too-much.wast
new file mode 100644
index 000000000..cecd84a85
--- /dev/null
+++ b/tests/misc_testsuite/float-round-doesnt-load-too-much.wast
@@ -0,0 +1,28 @@
+(module
+  (memory 1)
+  (func (export "ceil") (param i32) (result f64)
+    local.get 0
+    f64.load
+    f64.ceil
+    return)
+  (func (export "trunc") (param i32) (result f64)
+    local.get 0
+    f64.load
+    f64.trunc
+    return)
+  (func (export "floor") (param i32) (result f64)
+    local.get 0
+    f64.load
+    f64.floor
+    return)
+  (func (export "nearest") (param i32) (result f64)
+    local.get 0
+    f64.load
+    f64.nearest
+    return)
+)
+
+(assert_return (invoke "ceil" (i32.const 0xfff8)) (f64.const 0))
+(assert_return (invoke "trunc" (i32.const 0xfff8)) (f64.const 0))
+(assert_return (invoke "floor" (i32.const 0xfff8)) (f64.const 0))
+(assert_return (invoke "nearest" (i32.const 0xfff8)) (f64.const 0))