Repository: bytecodealliance/wasmtime
Pull request #5594 (2023-01-19 02:56:49)

Cranelift/egraph mid-end: support merging effectful-but-idempotent ops
This is the analogue to #5534 for the egraph framework: it allows for operators that have side-effects (like traps), but are idempotent (will have the same trap or result as an earlier instance of the same operator with the same arguments), to merge in the GVN/deduplication that is inherent in egraph construction.


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index ffa6d57d3..127fcb9f9 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -766,9 +766,12 @@ dependencies = [
  "log",
  "pretty_env_logger",
  "rayon",
+ "serde",
+ "similar",
  "target-lexicon",
  "termcolor",
  "thiserror",
+ "toml",
  "walkdir",
  "wat",
 ]
@@ -3949,6 +3952,33 @@ dependencies = [
  "wasmparser",
 ]
 
+[[package]]
+name = "winch-filetests"
+version = "0.0.0"
+dependencies = [
+ "anyhow",
+ "capstone",
+ "cranelift-codegen",
+ "serde",
+ "similar",
+ "target-lexicon",
+ "toml",
+ "wasmtime-environ",
+ "wat",
+ "winch-codegen",
+ "winch-test-macros",
+]
+
+[[package]]
+name = "winch-test-macros"
+version = "0.0.0"
+dependencies = [
+ "glob",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "winch-tools"
 version = "0.0.0"
@@ -3957,11 +3987,17 @@ dependencies = [
  "capstone",
  "clap 3.2.8",
  "cranelift-codegen",
+ "glob",
+ "serde",
+ "similar",
  "target-lexicon",
+ "toml",
  "wasmparser",
  "wasmtime-environ",
  "wat",
  "winch-codegen",
+ "winch-filetests",
+ "winch-test-macros",
 ]
 
 [[package]]
diff --git a/Cargo.toml b/Cargo.toml
index f7feff55d..e72cddff1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -156,6 +156,8 @@ cranelift-bforest = { path = "cranelift/bforest", version = "0.93.0" }
 cranelift = { path = "cranelift/umbrella", version = "0.93.0" }
 
 winch-codegen = { path = "winch/codegen", version = "=0.4.0" }
+winch-filetests = { path = "winch/filetests" }
+winch-test-macros = { path = "winch/test-macros" }
 
 target-lexicon = { version = "0.12.3", default-features = false, features = ["std"] }
 anyhow = "1.0.22"
@@ -186,6 +188,10 @@ bitflags = "1.2"
 thiserror = "1.0.15"
 async-trait = "0.1.42"
 heck = "0.4"
+similar = "2.1.0"
+toml = "0.5.9"
+serde = "1.0.94"
+glob = "0.3.0"
 
 [features]
 default = [
diff --git a/cranelift/Cargo.toml b/cranelift/Cargo.toml
index a9f780946..a34d6a639 100644
--- a/cranelift/Cargo.toml
+++ b/cranelift/Cargo.toml
@@ -46,6 +46,9 @@ thiserror = { workspace = true }
 walkdir = "2.2"
 anyhow = { workspace = true }
 clap = { workspace = true }
+similar = { workspace = true }
+toml = { workspace = true }
+serde = { workspace = true }
 
 [features]
 default = ["disas", "wasm", "cranelift-codegen/all-arch", "cranelift-codegen/trace-log", "souper-harvest"]
diff --git a/cranelift/codegen/src/egraph.rs b/cranelift/codegen/src/egraph.rs
index 6f8bdbb73..02bfeb013 100644
--- a/cranelift/codegen/src/egraph.rs
+++ b/cranelift/codegen/src/egraph.rs
@@ -7,7 +7,7 @@ use crate::dominator_tree::DominatorTree;
 use crate::egraph::domtree::DomTreeWithChildren;
 use crate::egraph::elaborate::Elaborator;
 use crate::fx::FxHashSet;
-use crate::inst_predicates::is_pure_for_egraph;
+use crate::inst_predicates::{is_mergeable_for_egraph, is_pure_for_egraph};
 use crate::ir::{
     DataFlowGraph, Function, Inst, InstructionData, Type, Value, ValueDef, ValueListPool,
 };
@@ -284,17 +284,59 @@ where
     /// the layout.
     fn optimize_skeleton_inst(&mut self, inst: Inst) -> bool {
         self.stats.skeleton_inst += 1;
-        // Not pure, but may still be a load or store:
-        // process it to see if we can optimize it.
-        if let Some(new_result) =
+
+        // First, can we try to deduplicate? We need to keep some copy
+        // of the instruction around because it's side-effecting, but
+        // we may be able to reuse an earlier instance of it.
+        if is_mergeable_for_egraph(self.func, inst) {
+            let result = self.func.dfg.inst_results(inst)[0];
+            trace!(" -> mergeable side-effecting op {}", inst);
+            let inst = NewOrExistingInst::Existing(inst);
+            let gvn_context = GVNContext {
+                union_find: self.eclasses,
+                value_lists: &self.func.dfg.value_lists,
+            };
+
+            // Does this instruction already exist? If so, add entries to
+            // the value-map to rewrite uses of its results to the results
+            // of the original (existing) instruction. If not, optimize
+            // the new instruction.
+            let key = inst.get_inst_key(&self.func.dfg);
+            if let Some(&orig_result) = self.gvn_map.get(&key, &gvn_context) {
+                // Hit in GVN map -- reuse value.
+                self.value_to_opt_value[result] = orig_result;
+                self.eclasses.union(orig_result, result);
+                trace!(" -> merges result {} to {}", result, orig_result);
+                true
+            } else {
+                // Otherwise, insert it into the value-map.
+                self.value_to_opt_value[result] = result;
+                self.gvn_map.insert(key, result, &gvn_context);
+                trace!(" -> inserts as new (no GVN)");
+                false
+            }
+        }
+        // Otherwise, if a load or store, process it with the alias
+        // analysis to see if we can optimize it (rewrite in terms of
+        // an earlier load or stored value).
+        else if let Some(new_result) =
             self.alias_analysis
                 .process_inst(self.func, self.alias_analysis_state, inst)
         {
             self.stats.alias_analysis_removed += 1;
             let result = self.func.dfg.first_result(inst);
+            trace!(
+                " -> inst {} has result {} replaced with {}",
+                inst,
+                result,
+                new_result
+            );
             self.value_to_opt_value[result] = new_result;
             true
-        } else {
+        }
+        // Otherwise, generic side-effecting op -- always keep it, and
+        // set its results to identity-map to original values.
+        else {
             // Set all results to identity-map to themselves
             // in the value-to-opt-value map.
             for &result in self.func.dfg.inst_results(inst) {
diff --git a/cranelift/codegen/src/inst_predicates.rs b/cranelift/codegen/src/inst_predicates.rs
index 8eea3e383..3c28899bd 100644
--- a/cranelift/codegen/src/inst_predicates.rs
+++ b/cranelift/codegen/src/inst_predicates.rs
@@ -74,6 +74,25 @@ pub fn is_pure_for_egraph(func: &Function, inst: Inst) -> bool {
     has_one_result && (is_readonly_load || (!op.can_load() && !trivially_has_side_effects(op)))
 }
 
+/// Can the given instruction be merged into another copy of itself?
+/// These instructions may have side-effects, but as long as we retain
+/// the first instance of the instruction, the second and further
+/// instances are redundant if they would produce the same trap or
+/// result.
+pub fn is_mergeable_for_egraph(func: &Function, inst: Inst) -> bool {
+    let op = func.dfg.insts[inst].opcode();
+    // We can only merge one-result operators due to the way that GVN
+    // is structured in the egraph implementation.
+    let has_one_result = func.dfg.inst_results(inst).len() == 1;
+    has_one_result
+        // Loads/stores are handled by alias analysis and not
+        // otherwise mergeable.
+        && !op.can_load()
+        && !op.can_store()
+        // Can only have idempotent side-effects.
+        && (!has_side_effect(func, inst) || op.side_effects_idempotent())
+}
+
 /// Does the given instruction have any side-effect as per [has_side_effect], or else is a load,
 /// but not the get_pinned_reg opcode?
 pub fn has_lowering_side_effect(func: &Function, inst: Inst) -> bool {
diff --git a/cranelift/codegen/src/isa/s390x/abi.rs b/cranelift/codegen/src/isa/s390x/abi.rs
index c66d3ac2a..9d27be301 100644
--- a/cranelift/codegen/src/isa/s390x/abi.rs
+++ b/cranelift/codegen/src/isa/s390x/abi.rs
@@ -275,7 +275,6 @@ impl ABIMachineSpec for S390xMachineDeps {
             } else if call_conv.extends_wasmtime() {
                 panic!("i128 args/return values not supported in the Wasmtime ABI");
             } else {
-                assert!(param.extension == ir::ArgumentExtension::None);
                 // We must pass this by implicit reference.
                 if args_or_rets == ArgsOrRets::Rets {
                     // For return values, just force them to memory.
diff --git a/cranelift/codegen/src/isa/s390x/inst.isle b/cranelift/codegen/src/isa/s390x/inst.isle
index 458dfe478..815513f04 100644
--- a/cranelift/codegen/src/isa/s390x/inst.isle
+++ b/cranelift/codegen/src/isa/s390x/inst.isle
@@ -2975,9 +2975,9 @@
 
 ;; Helper to compute the type of an implicitly extended argument/return value.
 (decl abi_ext_ty (ArgumentExtension Type) Type)
-(rule (abi_ext_ty (ArgumentExtension.None) ty) ty)
-(rule (abi_ext_ty (ArgumentExtension.Uext) _) $I64)
-(rule (abi_ext_ty (ArgumentExtension.Sext) _) $I64)
+(rule 0 (abi_ext_ty _ ty) ty)
+(rule 1 (abi_ext_ty (ArgumentExtension.Uext) (gpr32_ty _)) $I64)
+(rule 1 (abi_ext_ty (ArgumentExtension.Sext) (gpr32_ty _)) $I64)
 
 
 ;; Helpers for generating immediate values ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
diff --git a/cranelift/fuzzgen/src/function_generator.rs b/cranelift/fuzzgen/src/function_generator.rs
index 2383fbdb5..f98212c9c 100644
--- a/cranelift/fuzzgen/src/function_generator.rs
+++ b/cranelift/fuzzgen/src/function_generator.rs
@@ -502,6 +502,46 @@ fn valid_for_target(triple: &Triple, op: Opcode, args: &[Type], rets: &[Type]) -
             )
         }
 
+        Architecture::S390x => {
+            exceptions!(
+                (Opcode::IaddCout),
+                (Opcode::Udiv, &[I128, I128]),
+                (Opcode::Sdiv, &[I128, I128]),
+                (Opcode::Urem, &[I128, I128]),
+                (Opcode::Srem, &[I128, I128]),
+                (Opcode::Smin),
+                (Opcode::Smax),
+                (Opcode::Umin),
+                (Opcode::Umax),
+                (Opcode::Band, &[F32, F32]),
+                (Opcode::Band, &[F64, F64]),
+                (Opcode::Bor, &[F32, F32]),
+                (Opcode::Bor, &[F64, F64]),
+                (Opcode::Bxor, &[F32, F32]),
+                (Opcode::Bxor, &[F64, F64]),
+                (Opcode::Bnot, &[F32, F32]),
+                (Opcode::Bnot, &[F64, F64]),
+                (Opcode::BandNot, &[F32, F32]),
+                (Opcode::BandNot, &[F64, F64]),
+                (Opcode::BorNot, &[F32, F32]),
+                (Opcode::BorNot, &[F64, F64]),
+                (Opcode::BxorNot, &[F32, F32]),
+                (Opcode::BxorNot, &[F64, F64]),
+                (Opcode::FcvtToUint, &[F32], &[I128]),
+                (Opcode::FcvtToUint, &[F64], &[I128]),
+                (Opcode::FcvtToUintSat, &[F32], &[I128]),
+                (Opcode::FcvtToUintSat, &[F64], &[I128]),
+                (Opcode::FcvtToSint, &[F32], &[I128]),
+                (Opcode::FcvtToSint, &[F64], &[I128]),
+                (Opcode::FcvtToSintSat, &[F32], &[I128]),
+                (Opcode::FcvtToSintSat, &[F64], &[I128]),
+                (Opcode::FcvtFromUint, &[I128], &[F32]),
+                (Opcode::FcvtFromUint, &[I128], &[F64]),
+                (Opcode::FcvtFromSint, &[I128], &[F32]),
+                (Opcode::FcvtFromSint, &[I128], &[F64]),
+            )
+        }
+
         Architecture::Riscv64(_) => {
             exceptions!(
                 // TODO
diff --git a/crates/component-macro/src/component.rs b/crates/component-macro/src/component.rs
index b87fb6cb3..b054f993c 100644
--- a/crates/component-macro/src/component.rs
+++ b/crates/component-macro/src/component.rs
@@ -1114,6 +1114,14 @@ pub fn expand_flags(flags: &Flags) -> Result<TokenStream> {
                 use std::ops::Not;
                 Self::default().not()
             }
+
+            pub fn contains(&self, other: Self) -> bool {
+                *self & other == other
+            }
+
+            pub fn intersects(&self, other: Self) -> bool {
+                *self & other != Self::empty()
+            }
         }
 
         impl std::cmp::PartialEq for #name {
diff --git a/crates/fuzzing/src/generators/config.rs b/crates/fuzzing/src/generators/config.rs
index 6b0d69eef..f673421dd 100644
--- a/crates/fuzzing/src/generators/config.rs
+++ b/crates/fuzzing/src/generators/config.rs
@@ -97,31 +97,45 @@ impl Config {
         self.module_config.generate(input, default_fuel)
     }
 
-    /// Indicates that this configuration should be spec-test-compliant,
-    /// disabling various features the spec tests assert are disabled.
-    pub fn set_spectest_compliant(&mut self) {
-        let config = &mut self.module_config.config;
-        config.memory64_enabled = false;
-        config.bulk_memory_enabled = true;
-        config.reference_types_enabled = true;
-        config.multi_value_enabled = true;
-        config.simd_enabled = true;
-        config.threads_enabled = false;
-        config.max_memories = 1;
-        config.max_tables = 5;
+    /// Tests whether this configuration is capable of running all spec tests.
+    pub fn is_spectest_compliant(&self) -> bool {
+        let config = &self.module_config.config;
 
-        if let InstanceAllocationStrategy::Pooling(pooling) = &mut self.wasmtime.strategy {
-            // Configure the lower bound of a number of limits to what's
-            // required to actually run the spec tests. Fuzz-generated inputs
-            // may have limits less than these thresholds which would cause the
-            // spec tests to fail which isn't particularly interesting.
-            pooling.instance_memories = 1;
-            pooling.instance_tables = pooling.instance_tables.max(5);
-            pooling.instance_table_elements = pooling.instance_table_elements.max(1_000);
-            pooling.instance_memory_pages = pooling.instance_memory_pages.max(900);
-            pooling.instance_count = pooling.instance_count.max(500);
-            pooling.instance_size = pooling.instance_size.max(64 * 1024);
+        // Check for wasm features that must be disabled to run spec tests
+        if config.memory64_enabled || config.threads_enabled {
+            return false;
+        }
+
+        // Check for wasm features that must be enabled to run spec tests
+        if !config.bulk_memory_enabled
+            || !config.reference_types_enabled
+            || !config.multi_value_enabled
+            || !config.simd_enabled
+        {
+            return false;
+        }
+
+        // Make sure the runtime limits allow for the instantiation of all spec
+        // tests.
+        if config.max_memories < 1 || config.max_tables < 5 {
+            return false;
         }
+
+        if let InstanceAllocationStrategy::Pooling(pooling) = &self.wasmtime.strategy {
+            // Check to see if any item limit is less than the required
+            // threshold to execute the spec tests.
+            if pooling.instance_memories < 1
+                || pooling.instance_tables < 5
+                || pooling.instance_table_elements < 1_000
+                || pooling.instance_memory_pages < 900
+                || pooling.instance_count < 500
+                || pooling.instance_size < 64 * 1024
+            {
+                return false;
+            }
+        }
+
+        true
     }
 
     /// Converts this to a `wasmtime::Config` object
diff --git a/crates/fuzzing/src/oracles.rs b/crates/fuzzing/src/oracles.rs
index 37f389252..e453b2054 100644
--- a/crates/fuzzing/src/oracles.rs
+++ b/crates/fuzzing/src/oracles.rs
@@ -501,9 +501,11 @@ pub fn make_api_calls(api: generators::api::ApiCalls) {
 /// Executes the wast `test` spectest with the `config` specified.
 ///
 /// Ensures that spec tests pass regardless of the `Config`.
-pub fn spectest(mut fuzz_config: generators::Config, test: generators::SpecTest) {
+pub fn spectest(fuzz_config: generators::Config, test: generators::SpecTest) {
     crate::init_fuzzing();
-    fuzz_config.set_spectest_compliant();
+    if !fuzz_config.is_spectest_compliant() {
+        return;
+    }
     log::debug!("running {:?}", test.file);
     let mut wast_context = WastContext::new(fuzz_config.to_store());
     wast_context.register_spectest(false).unwrap();
diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml
index 8ee455957..11b6ff3d0 100644
--- a/fuzz/Cargo.toml
+++ b/fuzz/Cargo.toml
@@ -10,7 +10,7 @@ cargo-fuzz = true
 [dependencies]
 anyhow = { workspace = true }
 once_cell = { workspace = true }
-cranelift-codegen = { workspace = true, features = ["incremental-cache", "x86", "arm64", "riscv64"] }
+cranelift-codegen = { workspace = true, features = ["incremental-cache", "x86", "arm64", "s390x", "riscv64"] }
 cranelift-reader = { workspace = true }
 cranelift-wasm = { workspace = true }
 cranelift-filetests = { workspace = true }
diff --git a/winch/Cargo.toml b/winch/Cargo.toml
index 7f740e49b..d86a0b22a 100644
--- a/winch/Cargo.toml
+++ b/winch/Cargo.toml
@@ -11,9 +11,10 @@ edition.workspace = true
 name = "winch-tools"
 path = "src/main.rs"
 
-
 [dependencies]
 winch-codegen = { workspace = true }
+winch-filetests = { workspace = true }
+winch-test-macros = { workspace = true }
 wasmtime-environ = { workspace = true }
 target-lexicon = { workspace = true }
 anyhow = { workspace = true }
@@ -22,6 +23,10 @@ clap = { workspace = true }
 wat = { workspace = true }
 cranelift-codegen = { workspace = true }
 capstone = { workspace = true }
+similar = { workspace = true }
+toml = { workspace = true }
+serde = { workspace = true }
+glob = { workspace = true }
 
 [features]
 default = ["all-arch"]
diff --git a/winch/src/compile.rs b/winch/src/compile.rs
new file mode 100644
index 000000000..0f1c49239
--- /dev/null
+++ b/winch/src/compile.rs
@@ -0,0 +1,72 @@
+use anyhow::{Context, Result};
+use clap::Parser;
+use cranelift_codegen::settings;
+use std::{fs, path::PathBuf, str::FromStr};
+use target_lexicon::Triple;
+use wasmtime_environ::{
+    wasmparser::{types::Types, Parser as WasmParser, Validator},
+    DefinedFuncIndex, FunctionBodyData, Module, ModuleEnvironment, Tunables,
+};
+use winch_codegen::{lookup, TargetIsa};
+use winch_filetests::disasm::disasm;
+
+#[derive(Parser, Debug)]
+pub struct Options {
+    /// The input file.
+    input: PathBuf,
+
+    /// The target architecture.
+    #[clap(long = "target")]
+    target: String,
+}
+
+pub fn run(opt: &Options) -> Result<()> {
+    let bytes = fs::read(&opt.input)
+        .with_context(|| format!("Failed to read input file {}", opt.input.display()))?;
+    let bytes = wat::parse_bytes(&bytes)?;
+    let triple = Triple::from_str(&opt.target)?;
+    let shared_flags = settings::Flags::new(settings::builder());
+    let isa_builder = lookup(triple)?;
+    let isa = isa_builder.build(shared_flags)?;
+    let mut validator = Validator::new();
+    let parser = WasmParser::new(0);
+    let mut types = Default::default();
+    let tunables = Tunables::default();
+    let mut translation = ModuleEnvironment::new(&tunables, &mut validator, &mut types)
+        .translate(parser, &bytes)
+        .context("Failed to translate WebAssembly module")?;
+    let _ = types.finish();
+
+    let body_inputs = std::mem::take(&mut translation.function_body_inputs);
+    let module = &translation.module;
+    let types = translation.get_types();
+
+    body_inputs
+        .into_iter()
+        .try_for_each(|func| compile(&*isa, module, types, func))?;
+
+    Ok(())
+}
+
+fn compile(
+    isa: &dyn TargetIsa,
+    module: &Module,
+    types: &Types,
+    f: (DefinedFuncIndex, FunctionBodyData<'_>),
+) -> Result<()> {
+    let index = module.func_index(f.0);
+    let sig = types
+        .func_type_at(index.as_u32())
+        .expect(&format!("function type at index {:?}", index.as_u32()));
+    let FunctionBodyData { body, validator } = f.1;
+    let validator = validator.into_validator(Default::default());
+    let buffer = isa
+        .compile_function(&sig, &body, validator)
+        .expect("Couldn't compile function");
+
+    disasm(buffer.data(), isa)?
+        .iter()
+        .for_each(|s| println!("{}", s));
+
+    Ok(())
+}
diff --git a/winch/src/disasm.rs b/winch/src/disasm.rs
deleted file mode 100644
index bcb9924b4..000000000
--- a/winch/src/disasm.rs
+++ /dev/null
@@ -1,61 +0,0 @@
-//! Disassembly utilities.
-
-use anyhow::{bail, Result};
-use capstone::prelude::*;
-use std::fmt::Write;
-use target_lexicon::Architecture;
-use winch_codegen::TargetIsa;
-
-/// Disassemble and print a machine code buffer.
-pub fn print(bytes: &[u8], isa: &dyn TargetIsa) -> Result<()> {
-    let dis = disassembler_for(isa)?;
-    let insts = dis.disasm_all(bytes, 0x0).unwrap();
-
-    for i in insts.iter() {
-        let mut line = String::new();
-
-        write!(&mut line, "{:4x}:\t", i.address()).unwrap();
-
-        let mut bytes_str = String::new();
-        let mut len = 0;
-        let mut first = true;
-        for b in i.bytes() {
-            if !first {
-                write!(&mut bytes_str, " ").unwrap();
-            }
-            write!(&mut bytes_str, "{:02x}", b).unwrap();
-            len += 1;
-            first = false;
-        }
-        write!(&mut line, "{:21}\t", bytes_str).unwrap();
-        if len > 8 {
-            write!(&mut line, "\n\t\t\t\t").unwrap();
-        }
-
-        if let Some(s) = i.mnemonic() {
-            write!(&mut line, "{}\t", s).unwrap();
-        }
-
-        if let Some(s) = i.op_str() {
-            write!(&mut line, "{}", s).unwrap();
-        }
-
-        println!("{}", line);
-    }
-
-    Ok(())
-}
-
-fn disassembler_for(isa: &dyn TargetIsa) -> Result<Capstone> {
-    let disasm = match isa.triple().architecture {
-        Architecture::X86_64 => Capstone::new()
-            .x86()
-            .mode(arch::x86::ArchMode::Mode64)
-            .build()
-            .map_err(|e| anyhow::format_err!("{}", e))?,
-
-        _ => bail!("Unsupported ISA"),
-    };
-
-    Ok(disasm)
-}
diff --git a/winch/src/main.rs b/winch/src/main.rs
index 3e88bbe7e..9984b86d3 100644
--- a/winch/src/main.rs
+++ b/winch/src/main.rs
@@ -1,77 +1,21 @@
-//! Winch CLI tool, meant mostly for testing purposes.
-//!
-//! Reads Wasm in binary/text format and compiles them
-//! to any of the supported architectures using Winch.
+mod compile;
+mod filetests;
 
-use anyhow::{Context, Result};
+use anyhow::Result;
 use clap::Parser;
-use cranelift_codegen::settings;
-use std::{fs, path::PathBuf, str::FromStr};
-use target_lexicon::Triple;
-use wasmtime_environ::{
-    wasmparser::{types::Types, Parser as WasmParser, Validator},
-    DefinedFuncIndex, FunctionBodyData, Module, ModuleEnvironment, Tunables,
-};
-use winch_codegen::{lookup, TargetIsa};
 
-mod disasm;
-
-#[derive(Parser, Debug)]
-struct Options {
-    /// The input file.
-    input: PathBuf,
-
-    /// The target architecture.
-    #[clap(long = "target")]
-    target: String,
+/// Winch compilation and testing tool.
+#[derive(Parser)]
+enum Commands {
+    /// Compile a Wasm module to the specified target architecture.
+    Compile(compile::Options),
+    /// Run the filetests.
+    Test(filetests::Options),
 }
 
 fn main() -> Result<()> {
-    let opt = Options::from_args();
-    let bytes = fs::read(&opt.input)
-        .with_context(|| format!("Failed to read input file {}", opt.input.display()))?;
-    let bytes = wat::parse_bytes(&bytes)?;
-    let triple = Triple::from_str(&opt.target)?;
-    let shared_flags = settings::Flags::new(settings::builder());
-    let isa_builder = lookup(triple)?;
-    let isa = isa_builder.build(shared_flags)?;
-    let mut validator = Validator::new();
-    let parser = WasmParser::new(0);
-    let mut types = Default::default();
-    let tunables = Tunables::default();
-    let mut translation = ModuleEnvironment::new(&tunables, &mut validator, &mut types)
-        .translate(parser, &bytes)
-        .context("Failed to translate WebAssembly module")?;
-    let _ = types.finish();
-
-    let body_inputs = std::mem::take(&mut translation.function_body_inputs);
-    let module = &translation.module;
-    let types = translation.get_types();
-
-    body_inputs
-        .into_iter()
-        .try_for_each(|func| compile(&*isa, module, types, func))?;
-
-    Ok(())
-}
-
-fn compile(
-    isa: &dyn TargetIsa,
-    module: &Module,
-    types: &Types,
-    f: (DefinedFuncIndex, FunctionBodyData<'_>),
-) -> Result<()> {
-    let index = module.func_index(f.0);
-    let sig = types
-        .func_type_at(index.as_u32())
-        .expect(&format!("function type at index {:?}", index.as_u32()));
-    let FunctionBodyData { body, validator } = f.1;
-    let validator = validator.into_validator(Default::default());
-    let buffer = isa
-        .compile_function(&sig, &body, validator)
-        .expect("Couldn't compile function");
-
-    disasm::print(buffer.data(), isa)?;
-
-    Ok(())
+    match Commands::parse() {
+        Commands::Compile(c) => compile::run(&c),
+        Commands::Test(t) => filetests::run(&t),
+    }
 }


Test patch:
diff --git a/cranelift/filetests/Cargo.toml b/cranelift/filetests/Cargo.toml
index 872326ad5..9af750b6a 100644
--- a/cranelift/filetests/Cargo.toml
+++ b/cranelift/filetests/Cargo.toml
@@ -26,10 +26,10 @@ num_cpus = "1.8.0"
 target-lexicon = { workspace = true }
 thiserror = { workspace = true }
 anyhow = { workspace = true }
-similar = "2.1.0"
+similar ={ workspace = true }
 wat.workspace = true
-toml = "0.5.9"
-serde = "1.0.94"
+toml = { workspace = true }
+serde = { workspace = true }
 cranelift-wasm.workspace = true
 wasmparser.workspace = true
 cranelift.workspace = true
diff --git a/cranelift/filetests/filetests/wasm/duplicate-loads-dynamic-memory-egraph.wat b/cranelift/filetests/filetests/wasm/duplicate-loads-dynamic-memory-egraph.wat
new file mode 100644
index 000000000..b6d4e88dc
--- /dev/null
+++ b/cranelift/filetests/filetests/wasm/duplicate-loads-dynamic-memory-egraph.wat
@@ -0,0 +1,92 @@
+;;! target = "x86_64"
+;;!
+;;! optimize = true
+;;!
+;;! settings = [
+;;!   "enable_heap_access_spectre_mitigation=true",
+;;!   "opt_level=speed_and_size",
+;;!   "use_egraphs=true"
+;;! ]
+;;!
+;;! [globals.vmctx]
+;;! type = "i64"
+;;! vmctx = true
+;;!
+;;! [globals.heap_base]
+;;! type = "i64"
+;;! load = { base = "vmctx", offset = 0 }
+;;!
+;;! [globals.heap_bound]
+;;! type = "i64"
+;;! load = { base = "vmctx", offset = 8 }
+;;!
+;;! [[heaps]]
+;;! base = "heap_base"
+;;! min_size = 0
+;;! offset_guard_size = 0xffffffff
+;;! index_type = "i32"
+;;! style = { kind = "dynamic", bound = "heap_bound" }
+
+(module
+  (memory (export "memory") 0)
+  (func (export "load-without-offset") (param i32) (result i32 i32)
+    local.get 0
+    i32.load
+    local.get 0
+    i32.load
+  )
+  (func (export "load-with-offset") (param i32) (result i32 i32)
+    local.get 0
+    i32.load offset=1234
+    local.get 0
+    i32.load offset=1234
+  )
+)
+
+;; function u0:0(i32, i64 vmctx) -> i32, i32 fast {
+;;     gv0 = vmctx
+;;     gv1 = load.i64 notrap aligned gv0+8
+;;     gv2 = load.i64 notrap aligned gv0
+;;
+;;                                 block0(v0: i32, v1: i64):
+;; @0057                               v4 = uextend.i64 v0
+;; @0057                               v5 = iconst.i64 4
+;; @0057                               v6 = uadd_overflow_trap v4, v5, heap_oob  ; v5 = 4
+;; @0057                               v7 = load.i64 notrap aligned v1+8
+;; @0057                               v8 = load.i64 notrap aligned v1
+;; @0057                               v11 = icmp ugt v6, v7
+;; @0057                               v10 = iconst.i64 0
+;; @0057                               v9 = iadd v8, v4
+;; @0057                               v12 = select_spectre_guard v11, v10, v9  ; v10 = 0
+;; @0057                               v13 = load.i32 little heap v12
+;;                                     v2 -> v13
+;; @005f                               jump block1
+;;
+;;                                 block1:
+;; @005f                               return v13, v13
+;; }
+;;
+;; function u0:1(i32, i64 vmctx) -> i32, i32 fast {
+;;     gv0 = vmctx
+;;     gv1 = load.i64 notrap aligned gv0+8
+;;     gv2 = load.i64 notrap aligned gv0
+;;
+;;                                 block0(v0: i32, v1: i64):
+;; @0064                               v4 = uextend.i64 v0
+;; @0064                               v5 = iconst.i64 1238
+;; @0064                               v6 = uadd_overflow_trap v4, v5, heap_oob  ; v5 = 1238
+;; @0064                               v7 = load.i64 notrap aligned v1+8
+;; @0064                               v8 = load.i64 notrap aligned v1
+;; @0064                               v12 = icmp ugt v6, v7
+;; @0064                               v11 = iconst.i64 0
+;; @0064                               v9 = iadd v8, v4
+;;                                     v26 = iconst.i64 1234
+;; @0064                               v10 = iadd v9, v26  ; v26 = 1234
+;; @0064                               v13 = select_spectre_guard v12, v11, v10  ; v11 = 0
+;; @0064                               v14 = load.i32 little heap v13
+;;                                     v2 -> v14
+;; @006e                               jump block1
+;;
+;;                                 block1:
+;; @006e                               return v14, v14
+;; }
diff --git a/cranelift/filetests/filetests/wasm/duplicate-loads-static-memory-egraph.wat b/cranelift/filetests/filetests/wasm/duplicate-loads-static-memory-egraph.wat
new file mode 100644
index 000000000..d434d5a33
--- /dev/null
+++ b/cranelift/filetests/filetests/wasm/duplicate-loads-static-memory-egraph.wat
@@ -0,0 +1,74 @@
+;;! target = "x86_64"
+;;!
+;;! optimize = true
+;;!
+;;! settings = [
+;;!   "enable_heap_access_spectre_mitigation=true",
+;;!   "opt_level=speed_and_size",
+;;!   "use_egraphs=true"
+;;! ]
+;;!
+;;! [globals.vmctx]
+;;! type = "i64"
+;;! vmctx = true
+;;!
+;;! [globals.heap_base]
+;;! type = "i64"
+;;! load = { base = "vmctx", offset = 0, readonly = true }
+;;!
+;;! [[heaps]]
+;;! base = "heap_base"
+;;! min_size = 0x10000
+;;! offset_guard_size = 0xffffffff
+;;! index_type = "i32"
+;;! style = { kind = "static", bound = 0x10000000 }
+
+(module
+  (memory (export "memory") 1)
+  (func (export "load-without-offset") (param i32) (result i32 i32)
+    local.get 0
+    i32.load
+    local.get 0
+    i32.load
+  )
+  (func (export "load-with-offset") (param i32) (result i32 i32)
+    local.get 0
+    i32.load offset=1234
+    local.get 0
+    i32.load offset=1234
+  )
+)
+
+;; function u0:0(i32, i64 vmctx) -> i32, i32 fast {
+;;     gv0 = vmctx
+;;     gv1 = load.i64 notrap aligned readonly gv0
+;;
+;;                                 block0(v0: i32, v1: i64):
+;; @0057                               v5 = load.i64 notrap aligned readonly v1
+;; @0057                               v4 = uextend.i64 v0
+;; @0057                               v6 = iadd v5, v4
+;; @0057                               v7 = load.i32 little heap v6
+;;                                     v2 -> v7
+;; @005f                               jump block1
+;;
+;;                                 block1:
+;; @005f                               return v7, v7
+;; }
+;;
+;; function u0:1(i32, i64 vmctx) -> i32, i32 fast {
+;;     gv0 = vmctx
+;;     gv1 = load.i64 notrap aligned readonly gv0
+;;
+;;                                 block0(v0: i32, v1: i64):
+;; @0064                               v5 = load.i64 notrap aligned readonly v1
+;; @0064                               v4 = uextend.i64 v0
+;; @0064                               v6 = iadd v5, v4
+;;                                     v14 = iconst.i64 1234
+;; @0064                               v7 = iadd v6, v14  ; v14 = 1234
+;; @0064                               v8 = load.i32 little heap v7
+;;                                     v2 -> v8
+;; @006e                               jump block1
+;;
+;;                                 block1:
+;; @006e                               return v8, v8
+;; }
diff --git a/winch/docs/testing.md b/winch/docs/testing.md
new file mode 100644
index 000000000..859db45b7
--- /dev/null
+++ b/winch/docs/testing.md
@@ -0,0 +1,63 @@
+# Testing Winch
+
+Winch is tested through integration testing using the `winch-filetests` crate
+and manual exploratory testing. A CLI is available to run these tests
+conveniently. To add the `winch-tools` binary to your `PATH`, run `cargo install
+--path winch` from the root of `wasmtime`. The CLI provides two commands: `test`
+and `compile`. To see the help text for each command, run `winch-tools test
+--help` or `winch-tools compile --help`.
+
+## Integration Testing (`winch-tools test`)
+
+The `test` command will run a suite of tests that validates Winch output for a
+WebAssembly module is consistent with our expectations.
+
+### Running `test`
+
+Running `winch-tools test` will run all integration tests in the
+`winch-filetests` crate. All arguments following two dashes (`--`) will be
+passed directly to `cargo test -p winch-filetests`. This will allow you to
+configure the tests to run based on your requirements. All tests in the
+`winch-filetests` crate get named in the following convention:
+`winch_filetests_${filepath}`. This makes it possible to filter if you don't
+want to run the entire suite.
+
+If the output of Winch changes for a test in a run due to code updates, the test
+will fail and the difference between the two outputs will be shown. If the new
+output is expected, the tests can be re-run with an `WINCH_TEST_BLESS`
+environment variable set to `1`.
+
+### Adding a test
+
+To add new tests, create a `.wat` file in the `winch/filetests/filetests` folder
+in the following format:
+
+```wat
+;;! target = "x86_64"
+(module
+  (func (result i32)
+    (i32.const 42)
+  )
+)
+```
+
+It is encouraged to use folders to organize tests. For example, tests targeting
+the x86_64 architecture can be placed in the `winch/filetests/filetests/x64`.
+
+The first block of comments are a TOML compatible configuration passed to Winch
+during compilation with a `!` at the start of each line. The body of the file
+will be the subject of the test. A final block of comments is reserved for the
+output of the compilation, and it will be used to compare the output of the
+current run with the output of previous runs.
+
+## Manual Exploratory Tests (`winch-tools compile`)
+
+The `compile` command will run Winch for particular architecture against
+provided input file, and print the disassembled output to the console. Only
+`.wat` files are supported.
+
+### Running `compile`
+
+```bash
+winch-tools compile $wat_file --target $target_triple
+```
diff --git a/winch/filetests/Cargo.toml b/winch/filetests/Cargo.toml
new file mode 100644
index 000000000..9250078b4
--- /dev/null
+++ b/winch/filetests/Cargo.toml
@@ -0,0 +1,22 @@
+[package]
+authors = ["The Winch Project Developers"]
+name = "winch-filetests"
+description = "Tests for the Winch compiler based on a set of known valid files"
+license = "Apache-2.0 WITH LLVM-exception"
+repository = "https://github.com/bytecodealliance/wasmtime"
+version = "0.0.0"
+publish = false
+edition.workspace = true
+
+[dependencies]
+winch-test-macros = {workspace = true}
+target-lexicon = { workspace = true }
+winch-codegen = { workspace = true, features = ['all-arch'] }
+wasmtime-environ = { workspace = true }
+anyhow = { workspace = true }
+wat = { workspace = true }
+similar = { workspace = true }
+toml = { workspace = true }
+serde = { workspace = true }
+cranelift-codegen = { workspace = true }
+capstone = { workspace = true }
diff --git a/winch/filetests/filetests/x64/simple.wat b/winch/filetests/filetests/x64/simple.wat
new file mode 100644
index 000000000..c839e1f31
--- /dev/null
+++ b/winch/filetests/filetests/x64/simple.wat
@@ -0,0 +1,12 @@
+;;! target = "x86_64"
+
+(module
+  (func (result i32)
+    (i32.const 42)
+  )
+)
+;;    0:	 55                   	push	rbp
+;;    1:	 4889e5               	mov	rbp, rsp
+;;    4:	 48c7c02a000000       	mov	rax, 0x2a
+;;    b:	 5d                   	pop	rbp
+;;    c:	 c3                   	ret
diff --git a/winch/filetests/src/disasm.rs b/winch/filetests/src/disasm.rs
new file mode 100644
index 000000000..ee75e4997
--- /dev/null
+++ b/winch/filetests/src/disasm.rs
@@ -0,0 +1,59 @@
+//! Disassembly utilities.
+
+use anyhow::{bail, Result};
+use capstone::prelude::*;
+use std::fmt::Write;
+use target_lexicon::Architecture;
+use winch_codegen::TargetIsa;
+
+/// Disassemble and print a machine code buffer.
+pub fn disasm(bytes: &[u8], isa: &dyn TargetIsa) -> Result<Vec<String>> {
+    let dis = disassembler_for(isa)?;
+    let insts = dis.disasm_all(bytes, 0x0).unwrap();
+
+    let disassembled_lines = insts
+        .iter()
+        .map(|i| {
+            let mut line = String::new();
+
+            write!(&mut line, "{:4x}:\t ", i.address()).unwrap();
+
+            let mut bytes_str = String::new();
+            let mut len = 0;
+            for b in i.bytes() {
+                write!(&mut bytes_str, "{:02x}", b).unwrap();
+                len += 1;
+            }
+            write!(&mut line, "{:21}\t", bytes_str).unwrap();
+            if len > 8 {
+                write!(&mut line, "\n\t\t\t\t").unwrap();
+            }
+
+            if let Some(s) = i.mnemonic() {
+                write!(&mut line, "{}\t", s).unwrap();
+            }
+
+            if let Some(s) = i.op_str() {
+                write!(&mut line, "{}", s).unwrap();
+            }
+
+            line
+        })
+        .collect();
+
+    Ok(disassembled_lines)
+}
+
+fn disassembler_for(isa: &dyn TargetIsa) -> Result<Capstone> {
+    let disasm = match isa.triple().architecture {
+        Architecture::X86_64 => Capstone::new()
+            .x86()
+            .mode(arch::x86::ArchMode::Mode64)
+            .build()
+            .map_err(|e| anyhow::format_err!("{}", e))?,
+
+        _ => bail!("Unsupported ISA"),
+    };
+
+    Ok(disasm)
+}
diff --git a/winch/filetests/src/lib.rs b/winch/filetests/src/lib.rs
new file mode 100644
index 000000000..225445a2a
--- /dev/null
+++ b/winch/filetests/src/lib.rs
@@ -0,0 +1,165 @@
+pub mod disasm;
+
+#[cfg(test)]
+mod test {
+    use super::disasm::disasm;
+    use anyhow::Context;
+    use cranelift_codegen::settings;
+    use serde::{Deserialize, Serialize};
+    use similar::TextDiff;
+    use std::str::FromStr;
+    use target_lexicon::Triple;
+    use wasmtime_environ::{
+        wasmparser::{types::Types, Parser as WasmParser, Validator},
+        DefinedFuncIndex, FunctionBodyData, Module, ModuleEnvironment, Tunables,
+    };
+    use winch_codegen::isa::TargetIsa;
+    use winch_codegen::lookup;
+    use winch_test_macros::generate_file_tests;
+
+    #[derive(Clone, Debug, Serialize, Deserialize)]
+    struct TestConfig {
+        target: String,
+    }
+
+    /// A helper function to parse the test configuration from the top of the file.
+    fn parse_config(wat: &str) -> TestConfig {
+        let config_lines: Vec<_> = wat
+            .lines()
+            .take_while(|l| l.starts_with(";;!"))
+            .map(|l| &l[3..])
+            .collect();
+        let config_text = config_lines.join("\n");
+
+        toml::from_str(&config_text)
+            .context("failed to parse the test configuration")
+            .unwrap()
+    }
+
+    /// A helper function to parse the expected result from the bottom of the file.
+    fn parse_expected_result(wat: &str) -> String {
+        let mut expected_lines: Vec<_> = wat
+            .lines()
+            .rev()
+            .take_while(|l| l.starts_with(";;"))
+            .map(|l| {
+                if l.starts_with(";; ") {
+                    &l[3..]
+                } else {
+                    &l[2..]
+                }
+            })
+            .collect();
+        expected_lines.reverse();
+        expected_lines.join("\n")
+    }
+
+    /// A helper function to rewrite the expected result in the file.
+    fn rewrite_expected(wat: &str, actual: &str) -> String {
+        let old_expectation_line_count = wat
+            .lines()
+            .rev()
+            .take_while(|l| l.starts_with(";;"))
+            .count();
+        let old_wat_line_count = wat.lines().count();
+        let new_wat_lines: Vec<_> = wat
+            .lines()
+            .take(old_wat_line_count - old_expectation_line_count)
+            .map(|l| l.to_string())
+            .chain(actual.lines().map(|l| {
+                if l.is_empty() {
+                    ";;".to_string()
+                } else {
+                    format!(";; {l}")
+                }
+            }))
+            .collect();
+        let mut new_wat = new_wat_lines.join("\n");
+        new_wat.push('\n');
+
+        new_wat
+    }
+
+    #[generate_file_tests]
+    fn run_test(test_path: &str) {
+        let binding = std::fs::read_to_string(test_path).unwrap();
+        let wat = binding.as_str();
+
+        let config = parse_config(wat);
+        let wasm = wat::parse_str(&wat).unwrap();
+        let triple = Triple::from_str(&config.target).unwrap();
+
+        let binding = parse_expected_result(wat);
+        let expected = binding.as_str();
+
+        let shared_flags = settings::Flags::new(settings::builder());
+        let isa_builder = lookup(triple).unwrap();
+        let isa = isa_builder.build(shared_flags).unwrap();
+
+        let mut validator = Validator::new();
+        let parser = WasmParser::new(0);
+        let mut types = Default::default();
+        let tunables = Tunables::default();
+        let mut translation = ModuleEnvironment::new(&tunables, &mut validator, &mut types)
+            .translate(parser, &wasm)
+            .context("Failed to translate WebAssembly module")
+            .unwrap();
+        let _ = types.finish();
+
+        let body_inputs = std::mem::take(&mut translation.function_body_inputs);
+        let module = &translation.module;
+        let types = translation.get_types();
+
+        let binding = body_inputs
+            .into_iter()
+            .flat_map(|func| compile(&*isa, module, types, func))
+            .collect::<Vec<String>>()
+            .join("\n");
+        let actual = binding.as_str();
+
+        if std::env::var("WINCH_TEST_BLESS").unwrap_or_default() == "1" {
+            let new_wat = rewrite_expected(wat, actual);
+
+            std::fs::write(test_path, new_wat)
+                .with_context(|| format!("failed to write file: {}", test_path))
+                .unwrap();
+
+            return;
+        }
+
+        if expected.trim() != actual.trim() {
+            eprintln!(
+                "\n{}",
+                TextDiff::from_lines(expected, actual)
+                    .unified_diff()
+                    .header("expected", "actual")
+            );
+
+            eprintln!(
+                "note: You can re-run with the `WINCH_TEST_BLESS=1` environment variable set to update test expectations.\n"
+            );
+
+            panic!("Did not get the expected translation");
+        }
+    }
+
+    fn compile(
+        isa: &dyn TargetIsa,
+        module: &Module,
+        types: &Types,
+        f: (DefinedFuncIndex, FunctionBodyData<'_>),
+    ) -> Vec<String> {
+        let index = module.func_index(f.0);
+        let sig = types
+            .func_type_at(index.as_u32())
+            .expect(&format!("function type at index {:?}", index.as_u32()));
+        let FunctionBodyData { body, validator } = f.1;
+        let validator = validator.into_validator(Default::default());
+
+        let buffer = isa
+            .compile_function(&sig, &body, validator)
+            .expect("Couldn't compile function");
+
+        disasm(buffer.data(), isa).unwrap()
+    }
+}
diff --git a/winch/src/filetests.rs b/winch/src/filetests.rs
new file mode 100644
index 000000000..6917736c9
--- /dev/null
+++ b/winch/src/filetests.rs
@@ -0,0 +1,25 @@
+use std::process::Command;
+
+use anyhow::Result;
+use clap::Parser;
+
+#[derive(Parser, Debug)]
+pub struct Options {
+    /// Passes extra arguments to `cargo test --package winch-filetests`. For example, to run a single
+    /// test, use `-- --test-threads 1 --test single_test_name`.
+    #[clap(last = true, value_parser)]
+    cargo_test_args: Vec<String>,
+}
+
+pub fn run(opts: &Options) -> Result<()> {
+    Command::new("cargo")
+        .arg("test")
+        .arg("--package")
+        .arg("winch-filetests")
+        .arg("--")
+        .args(&opts.cargo_test_args)
+        .spawn()?
+        .wait()
+        .map(|_| ())
+        .map_err(|e| anyhow::anyhow!("Failed to run cargo test: {}", e))
+}
diff --git a/winch/test-macros/Cargo.toml b/winch/test-macros/Cargo.toml
new file mode 100644
index 000000000..3c69e9373
--- /dev/null
+++ b/winch/test-macros/Cargo.toml
@@ -0,0 +1,18 @@
+[package]
+authors = ["The Winch Project Developers"]
+name = "winch-test-macros"
+description = "Winch test macros"
+license = "Apache-2.0 WITH LLVM-exception"
+repository = "https://github.com/bytecodealliance/wasmtime"
+version = "0.0.0"
+publish = false
+edition.workspace = true
+
+[lib]
+proc-macro = true
+
+[dependencies]
+quote = "1.0"
+syn = { version = "1.0", features = ["full"]}
+proc-macro2 = "1.0"
+glob = { workspace = true }
diff --git a/winch/test-macros/src/lib.rs b/winch/test-macros/src/lib.rs
new file mode 100644
index 000000000..e0d34506b
--- /dev/null
+++ b/winch/test-macros/src/lib.rs
@@ -0,0 +1,82 @@
+extern crate proc_macro;
+
+use std::path::Path;
+
+use glob::glob;
+use proc_macro::TokenStream;
+use quote::quote;
+use syn::ItemFn;
+
+fn get_test_name_for_root(root: &Path, path: &Path) -> String {
+    let test_name = path
+        .strip_prefix(root)
+        .unwrap()
+        .to_str()
+        .unwrap()
+        .replace("/", "_")
+        .replace("\\", "_")
+        .replace(".wat", "");
+
+    format!("winch_filetests_{}", test_name)
+}
+
+/// Generate a test case for every .wat file in the filetests directory.
+/// This should only be used from the filetests crate.
+#[proc_macro_attribute]
+pub fn generate_file_tests(_attr: TokenStream, input: TokenStream) -> TokenStream {
+    // Parse the input as a function.
+    let input = proc_macro2::TokenStream::from(input);
+
+    let fn_ast: ItemFn =
+        syn::parse(input.clone().into()).expect("Failed to parse tokens as function");
+
+    // Get the function's name and body.
+    let name = &fn_ast.sig.ident;
+
+    let filetests_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("../filetests/filetests");
+
+    let test_file_entries = glob(format!("{}/**/*.wat", filetests_dir.to_str().unwrap()).as_str())
+        .expect("Failed to read glob pattern");
+
+    // Create a list of test cases by opening every .wat file in the directory.
+    let test_cases = test_file_entries.map(|entry| {
+        let path = entry.expect("Failed to read glob entry");
+
+        let full = path.to_str().expect("Path for file was empty");
+
+        let test_name = proc_macro2::Ident::new(
+            &get_test_name_for_root(&filetests_dir, &path),
+            proc_macro2::Span::call_site(),
+        );
+        quote! {
+            #[test]
+            fn #test_name() {
+                #name(#full);
+            }
+        }
+    });
+
+    // Assemble the output by combining the function and test cases.
+    let output = quote! {
+        #input
+
+        #(#test_cases)*
+    };
+
+    output.into()
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_get_test_name_for_root_unix() {
+        let root = Path::new("/home/user/Documents/winch/filetests/filetests");
+        let path = Path::new("/home/user/Documents/winch/filetests/filetests/simd/simple.wat");
+
+        let test_name = get_test_name_for_root(root, path);
+
+        assert_eq!(test_name, "winch_filetests_simd_simple");
+    }
+}