Repository: bytecodealliance/wasmtime
Pull request #7385 (2023-10-26 21:01:50)

Add compatibility shims for Wasmtime 13 CLI
This commit introduces a compatibility shim for the Wasmtime 13 CLI and prior. The goal of this commit is to address concerns raised in #7336 and other locations as well. While the new CLI cannot be un-shipped at this point this PR attempts to ameliorate the situation somewhat through a few avenues:

* A complete copy of the old CLI parser is now included in `wasmtime` by default.
* The `WASMTIME_NEW_CLI=0` environment variable can force usage of the old CLI parser for the `run` and `compile` commands.
* The `WASMTIME_NEW_CLI=1` environment variable can force usage of the new CLI parser.
* Otherwise both the old and the new CLI parser are executed. Depending on the result one is selected to be executed, possibly with a warning printed.
* If both CLI parsers succeed but produce the same result, then no warning is emitted and execution continues as usual.
* If both CLI parsers succeed but produce different results then a warning is emitted indicating this. The warning points to #7384 which has further examples of how to squash the warning. The warning also mentions the above env var to turn the warning off. In this situation the old semantics are used at this time instead of the new semantics. It's intended that eventually this will change in the future.
* If the new CLI succeeds and the old CLI fails then the new semantics are executed without warning.
* If the old CLI succeeds and the new CLI fails then a warning is issued and the old semantics are used.
* If both the old and the new CLI fail to parse then the error message for the new CLI is emitted.

Note that this doesn't add up to a perfect transition. The hope is that most of the major cases of change at the very least have something printed. My plan is to land this on `main` and then backport it to the 14.0.0 branch as a 14.0.3 release.

<!--
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/Cargo.lock b/Cargo.lock
index bc06be362..e8403c460 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2262,9 +2262,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
 
 [[package]]
 name = "rustix"
-version = "0.38.14"
+version = "0.38.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "747c788e9ce8e92b12cd485c49ddf90723550b654b32508f979b71a7b1ecda4f"
+checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3"
 dependencies = [
  "bitflags 2.4.1",
  "errno",
@@ -3355,6 +3355,7 @@ dependencies = [
  "env_logger 0.10.0",
  "filecheck",
  "http-body-util",
+ "humantime 2.1.0",
  "hyper",
  "libc",
  "listenfd",
diff --git a/Cargo.toml b/Cargo.toml
index 4a7248fe0..167a46a65 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -48,6 +48,7 @@ serde_json = { workspace = true }
 wasmparser = { workspace = true }
 tracing = { workspace = true }
 log = { workspace = true }
+humantime = { workspace = true }
 
 async-trait = { workspace = true }
 bytes = { workspace = true }
@@ -266,6 +267,7 @@ syn = "2.0.25"
 test-log = { version = "0.2", default-features = false, features = ["trace"] }
 tracing-subscriber = { version = "0.3.1", default-features = false, features = ['fmt', 'env-filter', 'ansi', 'tracing-log'] }
 url = "2.3.1"
+humantime = "2.0.0"
 
 # =============================================================================
 #
@@ -309,6 +311,10 @@ default = [
   # cost, so allow disabling this through disabling of our own `default`
   # feature.
   "clap/default",
+
+  # By default include compatibility with the "old" CLI from Wasmtime 13 and
+  # prior.
+  "old-cli",
 ]
 
 # ========================================
@@ -348,6 +354,9 @@ coredump = ["wasmtime-cli-flags/coredump"]
 addr2line = ["wasmtime/addr2line"]
 debug-builtins = ["wasmtime/debug-builtins"]
 
+# Enables compatibility shims with Wasmtime 13 and prior's CLI.
+old-cli = []
+
 # CLI subcommands for the `wasmtime` executable. See `wasmtime $cmd --help`
 # for more information on each subcommand.
 serve = ["wasi-http", "component-model", "dep:http-body-util"]
diff --git a/cranelift/codegen/src/ir/memtype.rs b/cranelift/codegen/src/ir/memtype.rs
index 403661d89..9a6ca67d9 100644
--- a/cranelift/codegen/src/ir/memtype.rs
+++ b/cranelift/codegen/src/ir/memtype.rs
@@ -51,10 +51,6 @@
 //!
 //! Eventually we plan to also have:
 //!
-//! - A dynamic memory is an untyped blob of storage with a size given
-//!   by a global value (GV). This is otherwise just like the "static
-//!   memory" variant described above.
-//!
 //! - A dynamic array is a sequence of struct memory types, with a
 //!   length given by a global value (GV). This is useful to model,
 //!   e.g., tables.
@@ -67,7 +63,7 @@
 //!   field is not null (all zero bits).
 
 use crate::ir::pcc::Fact;
-use crate::ir::Type;
+use crate::ir::{GlobalValue, Type};
 use alloc::vec::Vec;
 
 #[cfg(feature = "enable-serde")]
@@ -99,6 +95,15 @@ pub enum MemoryTypeData {
         size: u64,
     },
 
+    /// A dynamically-sized untyped blob of memory, with bound given
+    /// by a global value plus some static amount.
+    DynamicMemory {
+        /// Static part of size.
+        size: u64,
+        /// Dynamic part of size.
+        gv: GlobalValue,
+    },
+
     /// A type with no size.
     Empty,
 }
@@ -135,6 +140,9 @@ impl std::fmt::Display for MemoryTypeData {
             Self::Memory { size } => {
                 write!(f, "memory {size:#x}")
             }
+            Self::DynamicMemory { size, gv } => {
+                write!(f, "dynamic_memory {}+{:#x}", gv, size)
+            }
             Self::Empty => {
                 write!(f, "empty")
             }
@@ -175,6 +183,7 @@ impl MemoryTypeData {
         match self {
             Self::Struct { size, .. } => Some(*size),
             Self::Memory { size } => Some(*size),
+            Self::DynamicMemory { .. } => None,
             Self::Empty => Some(0),
         }
     }
diff --git a/cranelift/codegen/src/ir/pcc.rs b/cranelift/codegen/src/ir/pcc.rs
index fe7f08c96..6910ee73e 100644
--- a/cranelift/codegen/src/ir/pcc.rs
+++ b/cranelift/codegen/src/ir/pcc.rs
@@ -47,12 +47,6 @@
 //!
 //! TODO:
 //!
-//! Completeness:
-//! - Propagate facts through optimization (egraph layer).
-//! - Generate facts in cranelift-wasm frontend when lowering memory ops.
-//! - Support bounds-checking-type operations for dynamic memories and
-//!   tables.
-//!
 //! More checks:
 //! - Check that facts on `vmctx` GVs are subsumed by the actual facts
 //!   on the vmctx arg in block0 (function arg).
@@ -65,11 +59,6 @@
 //! Nicer errors:
 //! - attach instruction index or some other identifier to errors
 //!
-//! Refactoring:
-//! - avoid the "default fact" infra everywhere we fetch facts,
-//!   instead doing it in the subsume check (and take the type with
-//!   subsume)?
-//!
 //! Text format cleanup:
 //! - make the bitwidth on `max` facts optional in the CLIF text
 //!   format?
@@ -155,6 +144,19 @@ pub enum Fact {
         max: u64,
     },
 
+    /// A value bounded by a global value.
+    ///
+    /// The range is in `(min_GV + min_offset)..(max_GV +
+    /// max_offset)`, inclusive on the lower and upper bound.
+    DynamicRange {
+        /// The bitwidth of bits we care about, from the LSB upward.
+        bit_width: u16,
+        /// The lower bound, inclusive.
+        min: Expr,
+        /// The upper bound, inclusive.
+        max: Expr,
+    },
+
     /// A pointer to a memory type.
     Mem {
         /// The memory type.
@@ -165,12 +167,215 @@ pub enum Fact {
         max_offset: u64,
     },
 
+    /// A pointer to a memory type, dynamically bounded. The pointer
+    /// is within `(GV_min+offset_min)..(GV_max+offset_max)`
+    /// (inclusive on both ends) in the memory type.
+    DynamicMem {
+        /// The memory type.
+        ty: ir::MemoryType,
+        /// The lower bound, inclusive.
+        min: Expr,
+        /// The upper bound, inclusive.
+        max: Expr,
+        /// This pointer can also be null.
+        nullable: bool,
+    },
+
+    /// A comparison result between two dynamic values with a
+    /// comparison of a certain kind.
+    Compare {
+        /// The kind of comparison.
+        kind: ir::condcodes::IntCC,
+        /// The left-hand side of the comparison.
+        lhs: BaseExpr,
+        /// The right-hand side of the comparison.
+        rhs: BaseExpr,
+    },
+
     /// A "conflict fact": this fact results from merging two other
     /// facts, and it can never be satisfied -- checking any value
     /// against this fact will fail.
     Conflict,
 }
 
+/// A bound expression.
+#[derive(Clone, Debug, Hash, PartialEq, Eq)]
+#[cfg_attr(feature = "enable-serde", derive(Serialize, Deserialize))]
+pub struct Expr {
+    /// The dynamic (base) part.
+    pub base: BaseExpr,
+    /// The static (offset) part.
+    pub offset: i64,
+}
+
+/// The base part of a bound expression.
+#[derive(Clone, Debug, Hash, PartialEq, Eq)]
+#[cfg_attr(feature = "enable-serde", derive(Serialize, Deserialize))]
+pub enum BaseExpr {
+    /// No dynamic part (i.e., zero).
+    None,
+    /// A global value.
+    GlobalValue(ir::GlobalValue),
+    /// An SSA Value as a symbolic value. This can be referenced in
+    /// facts even after we've lowered out of SSA: it becomes simply
+    /// some symbolic value.
+    Value(ir::Value),
+    /// Top of the address space. This is "saturating": the offset
+    /// doesn't matter.
+    Max,
+}
+
+impl BaseExpr {
+    /// Is one base less than or equal to another? (We can't always
+    /// know; in such cases, returns `false`.)
+    fn le(lhs: &BaseExpr, rhs: &BaseExpr) -> bool {
+        // (i) reflexivity; (ii) 0 <= x for all (unsigned) x; (iii) x <= max for all x.
+        lhs == rhs || *lhs == BaseExpr::None || *rhs == BaseExpr::Max
+    }
+
+    /// Compute some BaseExpr that will be less than or equal to both
+    /// inputs. This is a generalization of `min` (but looser).
+    fn min(lhs: &BaseExpr, rhs: &BaseExpr) -> BaseExpr {
+        if lhs == rhs {
+            lhs.clone()
+        } else if *lhs == BaseExpr::Max {
+            rhs.clone()
+        } else if *rhs == BaseExpr::Max {
+            lhs.clone()
+        } else {
+            BaseExpr::None // zero is <= x for all (unsigned) x.
+        }
+    }
+
+    /// Compute some BaseExpr that will be greater than or equal to
+    /// both inputs.
+    fn max(lhs: &BaseExpr, rhs: &BaseExpr) -> BaseExpr {
+        if lhs == rhs {
+            lhs.clone()
+        } else if *lhs == BaseExpr::None {
+            rhs.clone()
+        } else if *rhs == BaseExpr::None {
+            lhs.clone()
+        } else {
+            BaseExpr::Max
+        }
+    }
+}
+
+impl Expr {
+    /// Is one expression definitely less than or equal to another?
+    /// (We can't always know; in such cases, returns `false`.)
+    fn le(lhs: &Expr, rhs: &Expr) -> bool {
+        if rhs.base == BaseExpr::Max {
+            true
+        } else {
+            BaseExpr::le(&lhs.base, &rhs.base) && lhs.offset <= rhs.offset
+        }
+    }
+
+    /// Generalization of `min`: compute some Expr that is less than
+    /// or equal to both inputs.
+    fn min(lhs: &Expr, rhs: &Expr) -> Expr {
+        if lhs.base == BaseExpr::None && lhs.offset == 0 {
+            lhs.clone()
+        } else if rhs.base == BaseExpr::None && rhs.offset == 0 {
+            rhs.clone()
+        } else {
+            Expr {
+                base: BaseExpr::min(&lhs.base, &rhs.base),
+                offset: std::cmp::min(lhs.offset, rhs.offset),
+            }
+        }
+    }
+
+    /// Generalization of `max`: compute some Expr that is greater
+    /// than or equal to both inputs.
+    fn max(lhs: &Expr, rhs: &Expr) -> Expr {
+        if lhs.base == BaseExpr::None && lhs.offset == 0 {
+            rhs.clone()
+        } else if rhs.base == BaseExpr::None && rhs.offset == 0 {
+            lhs.clone()
+        } else {
+            Expr {
+                base: BaseExpr::max(&lhs.base, &rhs.base),
+                offset: std::cmp::max(lhs.offset, rhs.offset),
+            }
+        }
+    }
+
+    /// Add one expression to another.
+    fn add(lhs: &Expr, rhs: &Expr) -> Option<Expr> {
+        if lhs.base == rhs.base {
+            Some(Expr {
+                base: lhs.base.clone(),
+                offset: lhs.offset.checked_add(rhs.offset)?,
+            })
+        } else if lhs.base == BaseExpr::None {
+            Some(Expr {
+                base: rhs.base.clone(),
+                offset: lhs.offset.checked_add(rhs.offset)?,
+            })
+        } else if rhs.base == BaseExpr::None {
+            Some(Expr {
+                base: lhs.base.clone(),
+                offset: lhs.offset.checked_add(rhs.offset)?,
+            })
+        } else {
+            Some(Expr {
+                base: BaseExpr::Max,
+                offset: 0,
+            })
+        }
+    }
+
+    /// Add a static offset to an expression.
+    fn offset(lhs: &Expr, rhs: i64) -> Option<Expr> {
+        let offset = lhs.offset.checked_add(rhs)?;
+        Some(Expr {
+            base: lhs.base.clone(),
+            offset,
+        })
+    }
+}
+
+impl fmt::Display for BaseExpr {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            BaseExpr::None => Ok(()),
+            BaseExpr::Max => write!(f, "max"),
+            BaseExpr::GlobalValue(gv) => write!(f, "{gv}"),
+            BaseExpr::Value(value) => write!(f, "{value}"),
+        }
+    }
+}
+
+impl BaseExpr {
+    /// Does this dynamic_expression take an offset?
+    pub fn is_some(&self) -> bool {
+        match self {
+            BaseExpr::None => false,
+            _ => true,
+        }
+    }
+}
+
+impl fmt::Display for Expr {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(f, "{}", self.base)?;
+        match self.offset {
+            offset if offset > 0 && self.base.is_some() => write!(f, "+{offset:#x}"),
+            offset if offset > 0 => write!(f, "{offset:#x}"),
+            offset if offset < 0 => {
+                let negative_offset = -i128::from(offset); // upcast to support i64::MIN.
+                write!(f, "-{negative_offset:#x}")
+            }
+            0 if self.base.is_some() => Ok(()),
+            0 => write!(f, "0"),
+            _ => unreachable!(),
+        }
+    }
+}
+
 impl fmt::Display for Fact {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         match self {
@@ -178,12 +383,31 @@ impl fmt::Display for Fact {
                 bit_width,
                 min,
                 max,
-            } => write!(f, "range({}, {:#x}, {:#x})", bit_width, min, max),
+            } => write!(f, "range({bit_width}, {min:#x}, {max:#x})"),
+            Fact::DynamicRange {
+                bit_width,
+                min,
+                max,
+            } => {
+                write!(f, "dynamic_range({bit_width}, {min}, {max})")
+            }
             Fact::Mem {
                 ty,
                 min_offset,
                 max_offset,
-            } => write!(f, "mem({}, {:#x}, {:#x})", ty, min_offset, max_offset),
+            } => write!(f, "mem({ty}, {min_offset:#x}, {max_offset:#x})"),
+            Fact::DynamicMem {
+                ty,
+                min,
+                max,
+                nullable,
+            } => {
+                let nullable_flag = if *nullable { ", nullable" } else { "" };
+                write!(f, "dynamic_mem({ty}, {min}, {max}{nullable_flag})")
+            }
+            Fact::Compare { kind, lhs, rhs } => {
+                write!(f, "compare({kind}, {lhs}, {rhs})")
+            }
             Fact::Conflict => write!(f, "conflict"),
         }
     }
@@ -305,6 +529,25 @@ impl Fact {
                 max: std::cmp::min(*max_lhs, *max_rhs),
             },
 
+            (
+                Fact::DynamicRange {
+                    bit_width: bw_lhs,
+                    min: min_lhs,
+                    max: max_lhs,
+                },
+                Fact::DynamicRange {
+                    bit_width: bw_rhs,
+                    min: min_rhs,
+                    max: max_rhs,
+                },
+            ) if bw_lhs == bw_rhs && Expr::le(min_rhs, max_lhs) && Expr::le(min_lhs, max_rhs) => {
+                Fact::DynamicRange {
+                    bit_width: *bw_lhs,
+                    min: Expr::max(min_lhs, min_rhs),
+                    max: Expr::min(max_lhs, max_rhs),
+                }
+            }
+
             (
                 Fact::Mem {
                     ty: ty_lhs,
@@ -327,9 +570,68 @@ impl Fact {
                 }
             }
 
+            (
+                Fact::DynamicMem {
+                    ty: ty_lhs,
+                    min: min_lhs,
+                    max: max_lhs,
+                    nullable: null_lhs,
+                },
+                Fact::DynamicMem {
+                    ty: ty_rhs,
+                    min: min_rhs,
+                    max: max_rhs,
+                    nullable: null_rhs,
+                },
+            ) if ty_lhs == ty_rhs && Expr::le(min_rhs, max_lhs) && Expr::le(min_lhs, max_rhs) => {
+                Fact::DynamicMem {
+                    ty: *ty_lhs,
+                    min: Expr::max(min_lhs, min_rhs),
+                    max: Expr::min(max_lhs, max_rhs),
+                    nullable: *null_lhs && *null_rhs,
+                }
+            }
+
             _ => Fact::Conflict,
         }
     }
+
+    /// Compute the union of two facts, if possible.
+    pub fn union(lhs: &Fact, rhs: &Fact) -> Option<Fact> {
+        match (lhs, rhs) {
+            (lhs, rhs) if lhs == rhs => Some(lhs.clone()),
+
+            (
+                Fact::DynamicMem {
+                    ty: ty_lhs,
+                    min: min_lhs,
+                    max: max_lhs,
+                    nullable: nullable_lhs,
+                },
+                Fact::DynamicMem {
+                    ty: ty_rhs,
+                    min: min_rhs,
+                    max: max_rhs,
+                    nullable: nullable_rhs,
+                },
+            ) if ty_lhs == ty_rhs => Some(Fact::DynamicMem {
+                ty: *ty_lhs,
+                min: Expr::min(min_lhs, min_rhs),
+                max: Expr::max(max_lhs, max_rhs),
+                nullable: *nullable_lhs || *nullable_rhs,
+            }),
+
+            _ => None,
+        }
+    }
+
+    /// Is this fact a single-value range with a symbolic Expr?
+    fn as_symbol(&self) -> Option<&Expr> {
+        match self {
+            Fact::DynamicRange { min, max, .. } if min == max => Some(min),
+            _ => None,
+        }
+    }
 }
 
 macro_rules! ensure {
@@ -346,6 +648,16 @@ macro_rules! bail {
     }};
 }
 
+/// The two kinds of inequalities: "strict" (`<`, `>`) and "loose"
+/// (`<=`, `>=`), the latter of which admit equality.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum InequalityKind {
+    /// Strict inequality: {less,greater}-than.
+    Strict,
+    /// Loose inequality: {less,greater}-than-or-equal.
+    Loose,
+}
+
 /// A "context" in which we can evaluate and derive facts. This
 /// context carries environment/global properties, such as the machine
 /// pointer width.
@@ -392,6 +704,27 @@ impl<'a> FactContext<'a> {
                 bw_lhs >= bw_rhs && max_lhs <= max_rhs && min_lhs >= min_rhs
             }
 
+            (
+                Fact::DynamicRange {
+                    bit_width: bw_lhs,
+                    min: min_lhs,
+                    max: max_lhs,
+                },
+                Fact::DynamicRange {
+                    bit_width: bw_rhs,
+                    min: min_rhs,
+                    max: max_rhs,
+                },
+            ) => {
+                // Nearly same as above, but with dynamic-expression
+                // comparisons. Note that we require equal bitwidths
+                // here: unlike in the static case, we don't have
+                // fixed values for min and max, so we can't lean on
+                // the well-formedness requirements of the static
+                // ranges fitting within the bit-width max.
+                bw_lhs == bw_rhs && Expr::le(max_lhs, max_rhs) && Expr::le(min_rhs, min_lhs)
+            }
+
             (
                 Fact::Mem {
                     ty: ty_lhs,
@@ -409,6 +742,36 @@ impl<'a> FactContext<'a> {
                     && min_offset_lhs >= min_offset_rhs
             }
 
+            (
+                Fact::DynamicMem {
+                    ty: ty_lhs,
+                    min: min_lhs,
+                    max: max_lhs,
+                    nullable: nullable_lhs,
+                },
+                Fact::DynamicMem {
+                    ty: ty_rhs,
+                    min: min_rhs,
+                    max: max_rhs,
+                    nullable: nullable_rhs,
+                },
+            ) => {
+                ty_lhs == ty_rhs
+                    && Expr::le(max_lhs, max_rhs)
+                    && Expr::le(min_rhs, min_lhs)
+                    && (*nullable_lhs || !*nullable_rhs)
+            }
+
+            // Constant zero subsumes nullable DynamicMem pointers.
+            (
+                Fact::Range {
+                    bit_width,
+                    min: 0,
+                    max: 0,
+                },
+                Fact::DynamicMem { nullable: true, .. },
+            ) if *bit_width == self.pointer_width => true,
+
             _ => false,
         }
     }
@@ -431,7 +794,7 @@ impl<'a> FactContext<'a> {
     /// pointer width: e.g., many 64-bit machines can still do 32-bit
     /// adds that wrap at 2^32.
     pub fn add(&self, lhs: &Fact, rhs: &Fact, add_width: u16) -> Option<Fact> {
-        match (lhs, rhs) {
+        let result = match (lhs, rhs) {
             (
                 Fact::Range {
                     bit_width: bw_lhs,
@@ -487,23 +850,125 @@ impl<'a> FactContext<'a> {
                 })
             }
 
+            (
+                Fact::Range {
+                    bit_width: bw_static,
+                    min: min_static,
+                    max: max_static,
+                },
+                Fact::DynamicRange {
+                    bit_width: bw_dynamic,
+                    min: ref min_dynamic,
+                    max: ref max_dynamic,
+                },
+            )
+            | (
+                Fact::DynamicRange {
+                    bit_width: bw_dynamic,
+                    min: ref min_dynamic,
+                    max: ref max_dynamic,
+                },
+                Fact::Range {
+                    bit_width: bw_static,
+                    min: min_static,
+                    max: max_static,
+                },
+            ) if bw_static == bw_dynamic => {
+                let min = Expr::offset(min_dynamic, i64::try_from(*min_static).ok()?)?;
+                let max = Expr::offset(max_dynamic, i64::try_from(*max_static).ok()?)?;
+                Some(Fact::DynamicRange {
+                    bit_width: *bw_dynamic,
+                    min,
+                    max,
+                })
+            }
+
+            (
+                Fact::DynamicMem {
+                    ty,
+                    min: min_mem,
+                    max: max_mem,
+                    nullable,
+                },
+                Fact::DynamicRange {
+                    bit_width,
+                    min: min_range,
+                    max: max_range,
+                },
+            )
+            | (
+                Fact::DynamicRange {
+                    bit_width,
+                    min: min_range,
+                    max: max_range,
+                },
+                Fact::DynamicMem {
+                    ty,
+                    min: min_mem,
+                    max: max_mem,
+                    nullable,
+                },
+            ) if *bit_width == self.pointer_width => {
+                let min = Expr::add(min_mem, min_range)?;
+                let max = Expr::add(max_mem, max_range)?;
+                Some(Fact::DynamicMem {
+                    ty: *ty,
+                    min,
+                    max,
+                    nullable: *nullable,
+                })
+            }
+
+            (
+                Fact::Range {
+                    bit_width: bw_static,
+                    min: min_static,
+                    max: max_static,
+                },
+                Fact::DynamicMem {
+                    ty,
+                    min: ref min_dynamic,
+                    max: ref max_dynamic,
+                    nullable,
+                },
+            )
+            | (
+                Fact::DynamicMem {
+                    ty,
+                    min: ref min_dynamic,
+                    max: ref max_dynamic,
+                    nullable,
+                },
+                Fact::Range {
+                    bit_width: bw_static,
+                    min: min_static,
+                    max: max_static,
+                },
+            ) if *bw_static == self.pointer_width => {
+                let min = Expr::offset(min_dynamic, i64::try_from(*min_static).ok()?)?;
+                let max = Expr::offset(max_dynamic, i64::try_from(*max_static).ok()?)?;
+                Some(Fact::DynamicMem {
+                    ty: *ty,
+                    min,
+                    max,
+                    nullable: *nullable,
+                })
+            }
+
             _ => None,
-        }
+        };
+
+        trace!("add: {lhs:?} + {rhs:?} -> {result:?}");
+        result
     }
 
     /// Computes the `uextend` of a value with the given facts.
     pub fn uextend(&self, fact: &Fact, from_width: u16, to_width: u16) -> Option<Fact> {
-        trace!(
-            "uextend: fact {:?} from {} to {}",
-            fact,
-            from_width,
-            to_width
-        );
         if from_width == to_width {
             return Some(fact.clone());
         }
 
-        match fact {
+        let result = match fact {
             // If the claim is already for a same-or-wider value and the min
             // and max are within range of the narrower value, we can
             // claim the same range.
@@ -521,12 +986,27 @@ impl<'a> FactContext<'a> {
                     max: *max,
                 })
             }
+
+            // If the claim is a dynamic range for the from-width, we
+            // can extend to the to-width.
+            Fact::DynamicRange {
+                bit_width,
+                min,
+                max,
+            } if *bit_width == from_width => Some(Fact::DynamicRange {
+                bit_width: to_width,
+                min: min.clone(),
+                max: max.clone(),
+            }),
+
             // Otherwise, we can at least claim that the value is
             // within the range of `from_width`.
             Fact::Range { .. } => Some(Fact::max_range_for_width_extended(from_width, to_width)),
 
             _ => None,
-        }
+        };
+        trace!("uextend: fact {fact:?} from {from_width} to {to_width} -> {result:?}");
+        result
     }
 
     /// Computes the `sextend` of a value with the given facts.
@@ -588,7 +1068,7 @@ impl<'a> FactContext<'a> {
 
     /// Scales a value with a fact by a known constant.
     pub fn scale(&self, fact: &Fact, width: u16, factor: u32) -> Option<Fact> {
-        match fact {
+        let result = match fact {
             Fact::Range {
                 bit_width,
                 min,
@@ -606,7 +1086,9 @@ impl<'a> FactContext<'a> {
                 })
             }
             _ => None,
-        }
+        };
+        trace!("scale: {fact:?} * {factor} at width {width} -> {result:?}");
+        result
     }
 
     /// Left-shifts a value with a fact by a known constant.
@@ -620,13 +1102,6 @@ impl<'a> FactContext<'a> {
 
     /// Offsets a value with a fact by a known amount.
     pub fn offset(&self, fact: &Fact, width: u16, offset: i64) -> Option<Fact> {
-        trace!(
-            "FactContext::offset: {:?} + {} in width {}",
-            fact,
-            offset,
-            width
-        );
-
         let compute_offset = |base: u64| -> Option<u64> {
             if offset >= 0 {
                 base.checked_add(u64::try_from(offset).unwrap())
@@ -635,7 +1110,7 @@ impl<'a> FactContext<'a> {
             }
         };
 
-        match fact {
+        let result = match fact {
             Fact::Range {
                 bit_width,
                 min,
@@ -643,13 +1118,25 @@ impl<'a> FactContext<'a> {
             } if *bit_width == width => {
                 let min = compute_offset(*min)?;
                 let max = compute_offset(*max)?;
-
                 Some(Fact::Range {
                     bit_width: *bit_width,
                     min,
                     max,
                 })
             }
+            Fact::DynamicRange {
+                bit_width,
+                min,
+                max,
+            } if *bit_width == width => {
+                let min = Expr::offset(min, offset)?;
+                let max = Expr::offset(max, offset)?;
+                Some(Fact::DynamicRange {
+                    bit_width: *bit_width,
+                    min,
+                    max,
+                })
+            }
             Fact::Mem {
                 ty,
                 min_offset: mem_min_offset,
@@ -663,8 +1150,25 @@ impl<'a> FactContext<'a> {
                     max_offset,
                 })
             }
+            Fact::DynamicMem {
+                ty,
+                min,
+                max,
+                nullable,
+            } => {
+                let min = Expr::offset(min, offset)?;
+                let max = Expr::offset(max, offset)?;
+                Some(Fact::DynamicMem {
+                    ty: *ty,
+                    min,
+                    max,
+                    nullable: *nullable,
+                })
+            }
             _ => None,
-        }
+        };
+        trace!("offset: {fact:?} + {offset} in width {width} -> {result:?}");
+        result
     }
 
     /// Check that accessing memory via a pointer with this fact, with
@@ -689,6 +1193,7 @@ impl<'a> FactContext<'a> {
                     | ir::MemoryTypeData::Memory { size } => {
                         ensure!(end_offset <= *size, OutOfBounds)
                     }
+                    ir::MemoryTypeData::DynamicMemory { .. } => bail!(OutOfBounds),
                     ir::MemoryTypeData::Empty => bail!(OutOfBounds),
                 }
                 let specific_ty_and_offset = if min_offset == max_offset {
@@ -698,6 +1203,30 @@ impl<'a> FactContext<'a> {
                 };
                 Ok(specific_ty_and_offset)
             }
+            Fact::DynamicMem {
+                ty,
+                min: _,
+                max:
+                    Expr {
+                        base: BaseExpr::GlobalValue(max_gv),
+                        offset: max_offset,
+                    },
+                nullable: _,
+            } => match &self.function.memory_types[*ty] {
+                ir::MemoryTypeData::DynamicMemory {
+                    gv,
+                    size: mem_static_size,
+                } if gv == max_gv => {
+                    let end_offset = max_offset
+                        .checked_add(i64::from(size))
+                        .ok_or(PccError::Overflow)?;
+                    let mem_static_size =
+                        i64::try_from(*mem_static_size).map_err(|_| PccError::Overflow)?;
+                    ensure!(end_offset <= mem_static_size, OutOfBounds);
+                    Ok(None)
+                }
+                _ => bail!(OutOfBounds),
+            },
             _ => bail!(OutOfBounds),
         }
     }
@@ -755,6 +1284,57 @@ impl<'a> FactContext<'a> {
         }
         Ok(())
     }
+
+    /// Apply a known inequality to rewrite dynamic bounds using transitivity, if possible.
+    ///
+    /// Given that `lhs >= rhs` (if not `strict`) or `lhs > rhs` (if
+    /// `strict`), update `fact`.
+    pub fn apply_inequality(
+        &self,
+        fact: &Fact,
+        lhs: &Fact,
+        rhs: &Fact,
+        kind: InequalityKind,
+    ) -> Fact {
+        match (lhs.as_symbol(), rhs.as_symbol(), fact) {
+            (
+                Some(lhs),
+                Some(rhs),
+                Fact::DynamicMem {
+                    ty,
+                    min,
+                    max,
+                    nullable,
+                },
+            ) if rhs.base == max.base => {
+                let strict_offset = match kind {
+                    InequalityKind::Strict => 1,
+                    InequalityKind::Loose => 0,
+                };
+                if let Some(offset) = max
+                    .offset
+                    .checked_add(lhs.offset)
+                    .and_then(|x| x.checked_sub(rhs.offset))
+                    .and_then(|x| x.checked_sub(strict_offset))
+                {
+                    let new_max = Expr {
+                        base: lhs.base.clone(),
+                        offset,
+                    };
+                    Fact::DynamicMem {
+                        ty: *ty,
+                        min: min.clone(),
+                        max: new_max,
+                        nullable: *nullable,
+                    }
+                } else {
+                    fact.clone()
+                }
+            }
+
+            _ => fact.clone(),
+        }
+    }
 }
 
 fn max_value_for_width(bits: u16) -> u64 {
@@ -779,9 +1359,10 @@ pub fn check_vcode_facts<B: LowerBackend + TargetIsa>(
     // facts, and support the stated output facts.
     for block in 0..vcode.num_blocks() {
         let block = BlockIndex::new(block);
+        let mut flow_state = B::FactFlowState::default();
         for inst in vcode.block_insns(block).iter() {
             // Check any output facts on this inst.
-            if let Err(e) = backend.check_fact(&ctx, vcode, inst) {
+            if let Err(e) = backend.check_fact(&ctx, vcode, inst, &mut flow_state) {
                 log::error!("Error checking instruction: {:?}", vcode[inst]);
                 return Err(e);
             }
diff --git a/cranelift/codegen/src/isa/aarch64/lower.rs b/cranelift/codegen/src/isa/aarch64/lower.rs
index de428e0c4..5ed10a490 100644
--- a/cranelift/codegen/src/isa/aarch64/lower.rs
+++ b/cranelift/codegen/src/isa/aarch64/lower.rs
@@ -136,7 +136,10 @@ impl LowerBackend for AArch64Backend {
         ctx: &FactContext<'_>,
         vcode: &mut VCode<Self::MInst>,
         inst: InsnIndex,
+        state: &mut pcc::FactFlowState,
     ) -> PccResult<()> {
-        pcc::check(ctx, vcode, inst)
+        pcc::check(ctx, vcode, inst, state)
     }
+
+    type FactFlowState = pcc::FactFlowState;
 }
diff --git a/cranelift/codegen/src/isa/aarch64/pcc.rs b/cranelift/codegen/src/isa/aarch64/pcc.rs
index a84604aa0..6a0bbd98d 100644
--- a/cranelift/codegen/src/isa/aarch64/pcc.rs
+++ b/cranelift/codegen/src/isa/aarch64/pcc.rs
@@ -4,7 +4,8 @@ use crate::ir::pcc::*;
 use crate::ir::types::*;
 use crate::ir::MemFlags;
 use crate::ir::Type;
-use crate::isa::aarch64::inst::args::{PairAMode, ShiftOp};
+use crate::isa::aarch64::inst::args::{Cond, PairAMode, ShiftOp};
+use crate::isa::aarch64::inst::regs::zero_reg;
 use crate::isa::aarch64::inst::Inst;
 use crate::isa::aarch64::inst::{ALUOp, MoveWideOp};
 use crate::isa::aarch64::inst::{AMode, ExtendOp};
@@ -26,13 +27,26 @@ fn extend_fact(ctx: &FactContext, value: &Fact, mode: ExtendOp) -> Option<Fact>
     }
 }
 
+/// Flow-state between facts.
+#[derive(Clone, Debug, Default)]
+pub struct FactFlowState {
+    cmp_flags: Option<(Fact, Fact)>,
+}
+
 pub(crate) fn check(
     ctx: &FactContext,
     vcode: &mut VCode<Inst>,
     inst_idx: InsnIndex,
+    state: &mut FactFlowState,
 ) -> PccResult<()> {
     trace!("Checking facts on inst: {:?}", vcode[inst_idx]);
 
+    // We only persist flag state for one instruction, because we
+    // can't exhaustively enumerate all flags-effecting ops; so take
+    // the `cmp_state` here and perhaps use it below but don't let it
+    // remain.
+    let cmp_flags = state.cmp_flags.take();
+
     match vcode[inst_idx] {
         Inst::Args { .. } => {
             // Defs on the args have "axiomatic facts": we trust the
@@ -200,6 +214,20 @@ pub(crate) fn check(
             )
         }),
 
+        Inst::AluRRR {
+            alu_op: ALUOp::SubS,
+            size,
+            rd,
+            rn,
+            rm,
+        } if rd.to_reg() == zero_reg() => {
+            // Compare.
+            let rn = get_fact_or_default(vcode, rn, size.bits().into());
+            let rm = get_fact_or_default(vcode, rm, size.bits().into());
+            state.cmp_flags = Some((rn, rm));
+            Ok(())
+        }
+
         Inst::AluRRR { rd, size, .. }
         | Inst::AluRRImm12 { rd, size, .. }
         | Inst::AluRRRShift { rd, size, .. }
@@ -240,12 +268,9 @@ pub(crate) fn check(
 
         Inst::MovK { rd, rn, imm, .. } => {
             let input = get_fact_or_default(vcode, rn, 64);
-            trace!("MovK: input = {:?}", input);
             if let Some(input_constant) = input.as_const(64) {
-                trace!(" -> input_constant: {}", input_constant);
                 let constant = u64::from(imm.bits) << (imm.shift * 16);
                 let constant = input_constant | constant;
-                trace!(" -> merged constant: {}", constant);
                 check_constant(ctx, vcode, rd, 64, constant)
             } else {
                 check_output(ctx, vcode, rd, &[], |_vcode| {
@@ -254,6 +279,37 @@ pub(crate) fn check(
             }
         }
 
+        Inst::CSel { rd, cond, rn, rm } if cond == Cond::Hs && cmp_flags.is_some() => {
+            let (cmp_lhs, cmp_rhs) = cmp_flags.unwrap();
+            trace!("CSel: cmp {cond:?} ({cmp_lhs:?}, {cmp_rhs:?})");
+
+            check_output(ctx, vcode, rd, &[], |vcode| {
+                // We support transitivity-based reasoning. If the
+                // comparison establishes that
+                //
+                //   (x+K1) <= (y+K2)
+                //
+                // then on the true-side of the select we can edit the maximum
+                // in a DynamicMem or DynamicRange by replacing x's with y's
+                // with appropriate offsets -- this widens the range.
+                //
+                // Likewise, on the false-side of the select we can
+                // replace y's with x's -- this also widens the range. On
+                // the false side we know the inequality is strict, so we
+                // can offset by one.
+
+                // True side: lhs <= rhs, not strict.
+                let rn = get_fact_or_default(vcode, rn, 64);
+                let rn = ctx.apply_inequality(&rn, &cmp_lhs, &cmp_rhs, InequalityKind::Loose);
+                // false side: rhs < lhs, strict.
+                let rm = get_fact_or_default(vcode, rm, 64);
+                let rm = ctx.apply_inequality(&rm, &cmp_rhs, &cmp_lhs, InequalityKind::Strict);
+                let union = Fact::union(&rn, &rm);
+                // Union the two facts.
+                clamp_range(ctx, 64, 64, union)
+            })
+        }
+
         _ if vcode.inst_defines_facts(inst_idx) => Err(PccError::UnsupportedFact),
 
         _ => Ok(()),
diff --git a/cranelift/codegen/src/isa/riscv64/lower.rs b/cranelift/codegen/src/isa/riscv64/lower.rs
index 1477509f3..41b114441 100644
--- a/cranelift/codegen/src/isa/riscv64/lower.rs
+++ b/cranelift/codegen/src/isa/riscv64/lower.rs
@@ -30,4 +30,6 @@ impl LowerBackend for Riscv64Backend {
         // right now riscv64 not support this feature.
         None
     }
+
+    type FactFlowState = ();
 }
diff --git a/cranelift/codegen/src/isa/s390x/lower.rs b/cranelift/codegen/src/isa/s390x/lower.rs
index f4db0a459..eea3c1fdc 100644
--- a/cranelift/codegen/src/isa/s390x/lower.rs
+++ b/cranelift/codegen/src/isa/s390x/lower.rs
@@ -25,4 +25,6 @@ impl LowerBackend for S390xBackend {
     ) -> Option<()> {
         isle::lower_branch(ctx, self, ir_inst, targets)
     }
+
+    type FactFlowState = ();
 }
diff --git a/cranelift/codegen/src/isa/x64/inst/args.rs b/cranelift/codegen/src/isa/x64/inst/args.rs
index d88f3a2ac..1c6c2877d 100644
--- a/cranelift/codegen/src/isa/x64/inst/args.rs
+++ b/cranelift/codegen/src/isa/x64/inst/args.rs
@@ -2007,7 +2007,7 @@ impl fmt::Display for ShiftKind {
 
 /// These indicate condition code tests.  Not all are represented since not all are useful in
 /// compiler-generated code.
-#[derive(Copy, Clone)]
+#[derive(Copy, Clone, PartialEq, Eq)]
 #[repr(u8)]
 pub enum CC {
     ///  overflow
diff --git a/cranelift/codegen/src/isa/x64/lower.rs b/cranelift/codegen/src/isa/x64/lower.rs
index 6b071c48c..ac4f803b5 100644
--- a/cranelift/codegen/src/isa/x64/lower.rs
+++ b/cranelift/codegen/src/isa/x64/lower.rs
@@ -345,7 +345,10 @@ impl LowerBackend for X64Backend {
         ctx: &FactContext<'_>,
         vcode: &mut VCode<Self::MInst>,
         inst: InsnIndex,
+        state: &mut pcc::FactFlowState,
     ) -> PccResult<()> {
-        pcc::check(ctx, vcode, inst)
+        pcc::check(ctx, vcode, inst, state)
     }
+
+    type FactFlowState = pcc::FactFlowState;
 }
diff --git a/cranelift/codegen/src/isa/x64/pcc.rs b/cranelift/codegen/src/isa/x64/pcc.rs
index 3991186c5..16a953325 100644
--- a/cranelift/codegen/src/isa/x64/pcc.rs
+++ b/cranelift/codegen/src/isa/x64/pcc.rs
@@ -4,7 +4,8 @@ use crate::ir::pcc::*;
 use crate::ir::types::*;
 use crate::ir::Type;
 use crate::isa::x64::inst::args::{
-    AluRmiROpcode, Amode, Gpr, Imm8Reg, RegMem, RegMemImm, ShiftKind, SyntheticAmode, ToWritableReg,
+    AluRmiROpcode, Amode, Gpr, Imm8Reg, RegMem, RegMemImm, ShiftKind, SyntheticAmode,
+    ToWritableReg, CC,
 };
 use crate::isa::x64::inst::Inst;
 use crate::machinst::pcc::*;
@@ -32,13 +33,26 @@ fn ensure_no_fact(vcode: &VCode<Inst>, reg: Reg) -> PccResult<()> {
     }
 }
 
+/// Flow-state between facts.
+#[derive(Clone, Debug, Default)]
+pub(crate) struct FactFlowState {
+    cmp_flags: Option<(Fact, Fact)>,
+}
+
 pub(crate) fn check(
     ctx: &FactContext,
     vcode: &mut VCode<Inst>,
     inst_idx: InsnIndex,
+    state: &mut FactFlowState,
 ) -> PccResult<()> {
     trace!("Checking facts on inst: {:?}", vcode[inst_idx]);
 
+    // We only persist flag state for one instruction, because we
+    // can't exhaustively enumerate all flags-effecting ops; so take
+    // the `cmp_state` here and perhaps use it below but don't let it
+    // remain.
+    let cmp_flags = state.cmp_flags.take();
+
     match vcode[inst_idx] {
         Inst::Nop { .. } => Ok(()),
 
@@ -302,8 +316,7 @@ pub(crate) fn check(
             match <&RegMem>::from(src) {
                 RegMem::Reg { reg } => {
                     check_unop(ctx, vcode, 64, dst.to_writable_reg(), *reg, |src| {
-                        let extended = ctx.uextend(src, from_bytes * 8, to_bytes * 8);
-                        clamp_range(ctx, 64, from_bytes * 8, extended)
+                        clamp_range(ctx, 64, from_bytes * 8, Some(src.clone()))
                     })
                 }
                 RegMem::Mem { ref addr } => {
@@ -378,7 +391,7 @@ pub(crate) fn check(
                     )
                 })
             }
-            Imm8Reg::Reg { .. } => undefined_result(ctx, vcode, dst, 64, 64),
+            Imm8Reg::Reg { .. } => undefined_result(ctx, vcode, dst, 64, size.to_bits().into()),
         },
 
         Inst::ShiftR { size, dst, .. } => {
@@ -395,12 +408,23 @@ pub(crate) fn check(
             ensure_no_fact(vcode, dst.to_writable_reg().to_reg())
         }
 
-        Inst::CmpRmiR { size, ref src, .. } => match <&RegMemImm>::from(src) {
+        Inst::CmpRmiR {
+            size, dst, ref src, ..
+        } => match <&RegMemImm>::from(src) {
             RegMemImm::Mem { ref addr } => {
-                check_load(ctx, None, addr, vcode, size.to_type(), 64)?;
+                if let Some(rhs) = check_load(ctx, None, addr, vcode, size.to_type(), 64)? {
+                    let lhs = get_fact_or_default(vcode, dst.to_reg(), 64);
+                    state.cmp_flags = Some((lhs, rhs));
+                }
                 Ok(())
             }
-            RegMemImm::Reg { .. } | RegMemImm::Imm { .. } => Ok(()),
+            RegMemImm::Reg { reg } => {
+                let rhs = get_fact_or_default(vcode, *reg, 64);
+                let lhs = get_fact_or_default(vcode, dst.to_reg(), 64);
+                state.cmp_flags = Some((lhs, rhs));
+                Ok(())
+            }
+            RegMemImm::Imm { .. } => Ok(()),
         },
 
         Inst::Setcc { dst, .. } => undefined_result(ctx, vcode, dst, 64, 64),
@@ -411,16 +435,32 @@ pub(crate) fn check(
             size,
             dst,
             ref consequent,
+            alternative,
+            cc,
             ..
-        } => {
-            match <&RegMem>::from(consequent) {
-                RegMem::Mem { ref addr } => {
-                    check_load(ctx, None, addr, vcode, size.to_type(), 64)?;
-                }
-                RegMem::Reg { .. } => {}
+        } => match <&RegMem>::from(consequent) {
+            RegMem::Mem { ref addr } => {
+                check_load(ctx, None, addr, vcode, size.to_type(), 64)?;
+                Ok(())
             }
-            undefined_result(ctx, vcode, dst, 64, 64)
-        }
+            RegMem::Reg { reg } if cc == CC::NB && cmp_flags.is_some() => {
+                let (cmp_lhs, cmp_rhs) = cmp_flags.unwrap();
+                trace!("lhs = {:?} rhs = {:?}", cmp_lhs, cmp_rhs);
+                let reg = *reg;
+                check_output(ctx, vcode, dst.to_writable_reg(), &[], |vcode| {
+                    // See comments in aarch64::pcc CSel for more details on this.
+                    let in_true = get_fact_or_default(vcode, reg, 64);
+                    let in_true =
+                        ctx.apply_inequality(&in_true, &cmp_lhs, &cmp_rhs, InequalityKind::Loose);
+                    let in_false = get_fact_or_default(vcode, alternative.to_reg(), 64);
+                    let in_false =
+                        ctx.apply_inequality(&in_false, &cmp_rhs, &cmp_lhs, InequalityKind::Strict);
+                    let union = Fact::union(&in_true, &in_false);
+                    clamp_range(ctx, 64, 64, union)
+                })
+            }
+            _ => undefined_result(ctx, vcode, dst, 64, 64),
+        },
 
         Inst::XmmCmove {
             dst,
diff --git a/cranelift/codegen/src/machinst/lower.rs b/cranelift/codegen/src/machinst/lower.rs
index bed1a3989..32d841ebb 100644
--- a/cranelift/codegen/src/machinst/lower.rs
+++ b/cranelift/codegen/src/machinst/lower.rs
@@ -148,6 +148,9 @@ pub trait LowerBackend {
         None
     }
 
+    /// The type of state carried between `check_fact` invocations.
+    type FactFlowState: Default + Clone + Debug;
+
     /// Check any facts about an instruction, given VCode with facts
     /// on VRegs. Takes mutable `VCode` so that it can propagate some
     /// kinds of facts automatically.
@@ -156,6 +159,7 @@ pub trait LowerBackend {
         _ctx: &FactContext<'_>,
         _vcode: &mut VCode<Self::MInst>,
         _inst: InsnIndex,
+        _state: &mut Self::FactFlowState,
     ) -> PccResult<()> {
         Err(PccError::UnimplementedBackend)
     }
diff --git a/cranelift/reader/src/parser.rs b/cranelift/reader/src/parser.rs
index dbd84d857..a3f0ec29a 100644
--- a/cranelift/reader/src/parser.rs
+++ b/cranelift/reader/src/parser.rs
@@ -12,7 +12,7 @@ use cranelift_codegen::entity::{EntityRef, PrimaryMap};
 use cranelift_codegen::ir::entities::{AnyEntity, DynamicType, MemoryType};
 use cranelift_codegen::ir::immediates::{Ieee32, Ieee64, Imm64, Offset32, Uimm32, Uimm64};
 use cranelift_codegen::ir::instructions::{InstructionData, InstructionFormat, VariableArgs};
-use cranelift_codegen::ir::pcc::Fact;
+use cranelift_codegen::ir::pcc::{BaseExpr, Expr, Fact};
 use cranelift_codegen::ir::types;
 use cranelift_codegen::ir::types::INVALID;
 use cranelift_codegen::ir::types::*;
@@ -1716,8 +1716,10 @@ impl<'a> Parser<'a> {
     // memory-type-decl ::= MemoryType(mt) "=" memory-type-desc
     // memory-type-desc ::= "struct" size "{" memory-type-field,* "}"
     //                    | "memory" size
+    //                    | "dynamic_memory" GlobalValue "+" offset
     //                    | "empty"
     // size ::= uimm64
+    // offset ::= uimm64
     fn parse_memory_type_decl(&mut self) -> ParseResult<(MemoryType, MemoryTypeData)> {
         let mt = self.match_mt("expected memory type number: mt«n»")?;
         self.match_token(Token::Equal, "expected '=' in memory type declaration")?;
@@ -1748,6 +1750,18 @@ impl<'a> Parser<'a> {
                 let size: u64 = self.match_uimm64("expected u64 constant value for size in static-memory memory-type declaration")?.into();
                 MemoryTypeData::Memory { size }
             }
+            Some(Token::Identifier("dynamic_memory")) => {
+                self.consume();
+                let gv = self.match_gv(
+                    "expected a global value for `dynamic_memory` memory-type declaration",
+                )?;
+                self.match_token(
+                    Token::Plus,
+                    "expected `+` after global value in `dynamic_memory` memory-type declaration",
+                )?;
+                let size: u64 = self.match_uimm64("expected u64 constant value for size offset in `dynamic_memory` memory-type declaration")?.into();
+                MemoryTypeData::DynamicMemory { gv, size }
+            }
             Some(Token::Identifier("empty")) => {
                 self.consume();
                 MemoryTypeData::Empty
@@ -2166,7 +2180,9 @@ impl<'a> Parser<'a> {
     // Parse a "fact" for proof-carrying code, attached to a value.
     //
     // fact ::= "range" "(" bit-width "," min-value "," max-value ")"
+    //        | "dynamic_range" "(" bit-width "," expr "," expr ")"
     //        | "mem" "(" memory-type "," mt-offset "," mt-offset ")"
+    //        | "dynamic_mem" "(" memory-type "," expr "," expr [ "," "nullable" ] ")"
     //        | "conflict"
     // bit-width ::= uimm64
     // min-value ::= uimm64
@@ -2213,6 +2229,23 @@ impl<'a> Parser<'a> {
                     max: max.into(),
                 })
             }
+            Some(Token::Identifier("dynamic_range")) => {
+                self.consume();
+                self.match_token(Token::LPar, "`dynamic_range` fact needs an opening `(`")?;
+                let bit_width: u64 = self
+                    .match_uimm64("expected a bit-width value for `dynamic_range` fact")?
+                    .into();
+                self.match_token(Token::Comma, "expected a comma")?;
+                let min = self.parse_expr()?;
+                self.match_token(Token::Comma, "expected a comma")?;
+                let max = self.parse_expr()?;
+                self.match_token(Token::RPar, "`dynamic_range` fact needs a closing `)`")?;
+                Ok(Fact::DynamicRange {
+                    bit_width: u16::try_from(bit_width).unwrap(),
+                    min,
+                    max,
+                })
+            }
             Some(Token::Identifier("mem")) => {
                 self.consume();
                 self.match_token(Token::LPar, "expected a `(`")?;
@@ -2224,10 +2257,7 @@ impl<'a> Parser<'a> {
                 let min_offset: u64 = self
                     .match_uimm64("expected a uimm64 minimum pointer offset for `mem` fact")?
                     .into();
-                self.match_token(
-                    Token::Comma,
-                    "expected a comma after memory type in `mem` fact",
-                )?;
+                self.match_token(Token::Comma, "expected a comma after offset in `mem` fact")?;
                 let max_offset: u64 = self
                     .match_uimm64("expected a uimm64 maximum pointer offset for `mem` fact")?
                     .into();
@@ -2238,11 +2268,124 @@ impl<'a> Parser<'a> {
                     max_offset,
                 })
             }
+            Some(Token::Identifier("dynamic_mem")) => {
+                self.consume();
+                self.match_token(Token::LPar, "expected a `(`")?;
+                let ty = self.match_mt("expected a memory type for `dynamic_mem` fact")?;
+                self.match_token(
+                    Token::Comma,
+                    "expected a comma after memory type in `dynamic_mem` fact",
+                )?;
+                let min = self.parse_expr()?;
+                self.match_token(
+                    Token::Comma,
+                    "expected a comma after offset in `dynamic_mem` fact",
+                )?;
+                let max = self.parse_expr()?;
+                let nullable = if self.token() == Some(Token::Comma) {
+                    self.consume();
+                    self.match_token(
+                        Token::Identifier("nullable"),
+                        "expected `nullable` in last optional field of `dynamic_mem`",
+                    )?;
+                    true
+                } else {
+                    false
+                };
+                self.match_token(Token::RPar, "expected a `)`")?;
+                Ok(Fact::DynamicMem {
+                    ty,
+                    min,
+                    max,
+                    nullable,
+                })
+            }
+            Some(Token::Identifier("compare")) => {
+                self.consume();
+                self.match_token(Token::LPar, "expected a `(`")?;
+                let kind = self.match_enum("expected intcc condition code in `compare` fact")?;
+                self.match_token(
+                    Token::Comma,
+                    "expected comma in `compare` fact after condition code",
+                )?;
+                let lhs = self.parse_base_expr()?;
+                self.match_token(Token::Comma, "expected comma in `compare` fact after LHS")?;
+                let rhs = self.parse_base_expr()?;
+                self.match_token(Token::RPar, "expected a `)`")?;
+                Ok(Fact::Compare { kind, lhs, rhs })
+            }
             Some(Token::Identifier("conflict")) => {
                 self.consume();
                 Ok(Fact::Conflict)
             }
-            _ => Err(self.error("expected a `range`, `mem` or `conflict` fact")),
+            _ => Err(self.error(
+                "expected a `range`, 'dynamic_range', `mem`, `dynamic_mem` or `conflict` fact",
+            )),
+        }
+    }
+
+    // Parse a dynamic expression used in some kinds of PCC facts.
+    //
+    // expr ::= base-expr
+    //        | base-expr + uimm64  // but in-range for imm64
+    //        | base-expr - uimm64  // but in-range for imm64
+    //        | imm64
+    fn parse_expr(&mut self) -> ParseResult<Expr> {
+        if let Some(Token::Integer(_)) = self.token() {
+            let offset: i64 = self
+                .match_imm64("expected imm64 for dynamic expression")?
+                .into();
+            Ok(Expr {
+                base: BaseExpr::None,
+                offset,
+            })
+        } else {
+            let base = self.parse_base_expr()?;
+            match self.token() {
+                Some(Token::Plus) => {
+                    self.consume();
+                    let offset: u64 = self
+                        .match_uimm64(
+                            "expected uimm64 in imm64 range for offset in dynamic expression",
+                        )?
+                        .into();
+                    let offset: i64 = i64::try_from(offset).map_err(|_| {
+                        self.error("integer offset in dynamic expression is out of range")
+                    })?;
+                    Ok(Expr { base, offset })
+                }
+                Some(Token::Integer(x)) if x.starts_with("-") => {
+                    let offset: i64 = self
+                        .match_imm64("expected an imm64 range for offset in dynamic expression")?
+                        .into();
+                    Ok(Expr { base, offset })
+                }
+                _ => Ok(Expr { base, offset: 0 }),
+            }
+        }
+    }
+
+    // Parse the base part of a dynamic expression, used in some PCC facts.
+    //
+    // base-expr ::= GlobalValue(base)
+    //             | Value(base)
+    //             | "max"
+    //             | (epsilon)
+    fn parse_base_expr(&mut self) -> ParseResult<BaseExpr> {
+        match self.token() {
+            Some(Token::Identifier("max")) => {
+                self.consume();
+                Ok(BaseExpr::Max)
+            }
+            Some(Token::GlobalValue(..)) => {
+                let gv = self.match_gv("expected global value")?;
+                Ok(BaseExpr::GlobalValue(gv))
+            }
+            Some(Token::Value(..)) => {
+                let value = self.match_value("expected value")?;
+                Ok(BaseExpr::Value(value))
+            }
+            _ => Ok(BaseExpr::None),
         }
     }
 
diff --git a/crates/cli-flags/Cargo.toml b/crates/cli-flags/Cargo.toml
index 0b7665220..249d0e276 100644
--- a/crates/cli-flags/Cargo.toml
+++ b/crates/cli-flags/Cargo.toml
@@ -15,7 +15,7 @@ file-per-thread-logger = { workspace = true, optional = true }
 tracing-subscriber = { workspace = true, optional = true }
 rayon = { version = "1.5.0", optional = true }
 wasmtime = { workspace = true }
-humantime = "2.0.0"
+humantime = { workspace = true }
 
 [features]
 pooling-allocator = []
diff --git a/crates/cli-flags/src/lib.rs b/crates/cli-flags/src/lib.rs
index 31b0232b2..c633f6cbc 100644
--- a/crates/cli-flags/src/lib.rs
+++ b/crates/cli-flags/src/lib.rs
@@ -39,6 +39,7 @@ fn init_file_per_thread_logger(prefix: &'static str) {
 }
 
 wasmtime_option_group! {
+    #[derive(PartialEq, Clone)]
     pub struct OptimizeOptions {
         /// Optimization level of generated code (0-2, s; default: 0)
         pub opt_level: Option<wasmtime::OptLevel>,
@@ -74,6 +75,7 @@ wasmtime_option_group! {
 }
 
 wasmtime_option_group! {
+    #[derive(PartialEq, Clone)]
     pub struct CodegenOptions {
         /// Either `cranelift` or `winch`.
         ///
@@ -103,6 +105,7 @@ wasmtime_option_group! {
 }
 
 wasmtime_option_group! {
+    #[derive(PartialEq, Clone)]
     pub struct DebugOptions {
         /// Enable generation of DWARF debug information in compiled code.
         pub debug_info: Option<bool>,
@@ -122,6 +125,7 @@ wasmtime_option_group! {
 }
 
 wasmtime_option_group! {
+    #[derive(PartialEq, Clone)]
     pub struct WasmOptions {
         /// Enable canonicalization of all NaN values.
         pub nan_canonicalization: Option<bool>,
@@ -212,6 +216,7 @@ wasmtime_option_group! {
 }
 
 wasmtime_option_group! {
+    #[derive(PartialEq, Clone)]
     pub struct WasiOptions {
         /// Enable support for WASI common APIs
         pub common: Option<bool>,
@@ -256,14 +261,14 @@ wasmtime_option_group! {
     }
 }
 
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, PartialEq)]
 pub struct WasiNnGraph {
     pub format: String,
     pub dir: String,
 }
 
 /// Common options for commands that translate WebAssembly modules
-#[derive(Parser)]
+#[derive(Parser, Clone)]
 pub struct CommonOptions {
     // These options groups are used to parse `-O` and such options but aren't
     // the raw form consumed by the CLI. Instead they're pushed into the `pub`
@@ -562,3 +567,31 @@ impl CommonOptions {
         Ok(())
     }
 }
+
+impl PartialEq for CommonOptions {
+    fn eq(&self, other: &CommonOptions) -> bool {
+        let mut me = self.clone();
+        me.configure();
+        let mut other = other.clone();
+        other.configure();
+        let CommonOptions {
+            opts_raw: _,
+            codegen_raw: _,
+            debug_raw: _,
+            wasm_raw: _,
+            wasi_raw: _,
+            configured: _,
+
+            opts,
+            codegen,
+            debug,
+            wasm,
+            wasi,
+        } = me;
+        opts == other.opts
+            && codegen == other.codegen
+            && debug == other.debug
+            && wasm == other.wasm
+            && wasi == other.wasi
+    }
+}
diff --git a/crates/cli-flags/src/opt.rs b/crates/cli-flags/src/opt.rs
index 18a3622bb..6550ce6b0 100644
--- a/crates/cli-flags/src/opt.rs
+++ b/crates/cli-flags/src/opt.rs
@@ -15,6 +15,7 @@ use std::time::Duration;
 #[macro_export]
 macro_rules! wasmtime_option_group {
     (
+        $(#[$attr:meta])*
         pub struct $opts:ident {
             $(
                 $(#[doc = $doc:tt])*
@@ -32,6 +33,7 @@ macro_rules! wasmtime_option_group {
         }
     ) => {
         #[derive(Default, Debug)]
+        $(#[$attr])*
         pub struct $opts {
             $(
                 pub $opt: $container<$payload>,
@@ -41,7 +43,7 @@ macro_rules! wasmtime_option_group {
             )?
         }
 
-        #[derive(Clone, Debug)]
+        #[derive(Clone, Debug,PartialEq)]
         #[allow(non_camel_case_types)]
         enum $option {
             $(
@@ -106,7 +108,7 @@ macro_rules! wasmtime_option_group {
 }
 
 /// Parser registered with clap which handles parsing the `...` in `-O ...`.
-#[derive(Clone, Debug)]
+#[derive(Clone, Debug, PartialEq)]
 pub struct CommaSeparated<T>(pub Vec<T>);
 
 impl<T> ValueParserFactory for CommaSeparated<T>
@@ -366,10 +368,7 @@ impl WasmtimeOptionValue for wasmtime::Strategy {
         match String::parse(val)?.as_str() {
             "cranelift" => Ok(wasmtime::Strategy::Cranelift),
             "winch" => Ok(wasmtime::Strategy::Winch),
-            other => bail!(
-                "unknown optimization level `{}`, only 0,1,2,s accepted",
-                other
-            ),
+            other => bail!("unknown compiler `{other}` only `cranelift` and `winch` accepted",),
         }
     }
 }
diff --git a/crates/fuzzing/src/generators/pooling_config.rs b/crates/fuzzing/src/generators/pooling_config.rs
index 9cdbd1d13..0997597f0 100644
--- a/crates/fuzzing/src/generators/pooling_config.rs
+++ b/crates/fuzzing/src/generators/pooling_config.rs
@@ -1,6 +1,7 @@
 //! Generate instance limits for the pooling allocation strategy.
 
 use arbitrary::{Arbitrary, Unstructured};
+use wasmtime::MpkEnabled;
 
 /// Configuration for `wasmtime::PoolingAllocationStrategy`.
 #[derive(Debug, Clone, Eq, PartialEq, Hash)]
@@ -30,6 +31,9 @@ pub struct PoolingAllocationConfig {
 
     pub async_stack_zeroing: bool,
     pub async_stack_keep_resident: usize,
+
+    pub memory_protection_keys: MpkEnabled,
+    pub max_memory_protection_keys: usize,
 }
 
 impl PoolingAllocationConfig {
@@ -62,6 +66,8 @@ impl PoolingAllocationConfig {
         cfg.async_stack_zeroing(self.async_stack_zeroing);
         cfg.async_stack_keep_resident(self.async_stack_keep_resident);
 
+        cfg.memory_protection_keys(self.memory_protection_keys);
+
         cfg
     }
 }
@@ -104,6 +110,9 @@ impl<'a> Arbitrary<'a> for PoolingAllocationConfig {
 
             async_stack_zeroing: u.arbitrary()?,
             async_stack_keep_resident: u.int_in_range(0..=1 << 20)?,
+
+            memory_protection_keys: u.choose(&[MpkEnabled::Auto, MpkEnabled::Disable])?.clone(),
+            max_memory_protection_keys: u.int_in_range(0..=20)?,
         })
     }
 }
diff --git a/crates/runtime/src/mpk/mod.rs b/crates/runtime/src/mpk/mod.rs
index bfffe2411..f60b3880b 100644
--- a/crates/runtime/src/mpk/mod.rs
+++ b/crates/runtime/src/mpk/mod.rs
@@ -42,7 +42,7 @@ cfg_if::cfg_if! {
 }
 
 /// Describe the tri-state configuration of memory protection keys (MPK).
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
 pub enum MpkEnabled {
     /// Use MPK if supported by the current system; fall back to guard regions
     /// otherwise.
diff --git a/crates/wasi-http/wit/deps/filesystem/types.wit b/crates/wasi-http/wit/deps/filesystem/types.wit
index 81dcde908..37700332b 100644
--- a/crates/wasi-http/wit/deps/filesystem/types.wit
+++ b/crates/wasi-http/wit/deps/filesystem/types.wit
@@ -650,105 +650,6 @@ interface types {
             modes: modes,
         ) -> result<_, error-code>;
 
-        /// Request a shared advisory lock for an open file.
-        ///
-        /// This requests a *shared* lock; more than one shared lock can be held for
-        /// a file at the same time.
-        ///
-        /// If the open file has an exclusive lock, this function downgrades the lock
-        /// to a shared lock. If it has a shared lock, this function has no effect.
-        ///
-        /// This requests an *advisory* lock, meaning that the file could be accessed
-        /// by other programs that don't hold the lock.
-        ///
-        /// It is unspecified how shared locks interact with locks acquired by
-        /// non-WASI programs.
-        ///
-        /// This function blocks until the lock can be acquired.
-        ///
-        /// Not all filesystems support locking; on filesystems which don't support
-        /// locking, this function returns `error-code::unsupported`.
-        ///
-        /// Note: This is similar to `flock(fd, LOCK_SH)` in Unix.
-        lock-shared: func() -> result<_, error-code>;
-
-        /// Request an exclusive advisory lock for an open file.
-        ///
-        /// This requests an *exclusive* lock; no other locks may be held for the
-        /// file while an exclusive lock is held.
-        ///
-        /// If the open file has a shared lock and there are no exclusive locks held
-        /// for the file, this function upgrades the lock to an exclusive lock. If the
-        /// open file already has an exclusive lock, this function has no effect.
-        ///
-        /// This requests an *advisory* lock, meaning that the file could be accessed
-        /// by other programs that don't hold the lock.
-        ///
-        /// It is unspecified whether this function succeeds if the file descriptor
-        /// is not opened for writing. It is unspecified how exclusive locks interact
-        /// with locks acquired by non-WASI programs.
-        ///
-        /// This function blocks until the lock can be acquired.
-        ///
-        /// Not all filesystems support locking; on filesystems which don't support
-        /// locking, this function returns `error-code::unsupported`.
-        ///
-        /// Note: This is similar to `flock(fd, LOCK_EX)` in Unix.
-        lock-exclusive: func() -> result<_, error-code>;
-
-        /// Request a shared advisory lock for an open file.
-        ///
-        /// This requests a *shared* lock; more than one shared lock can be held for
-        /// a file at the same time.
-        ///
-        /// If the open file has an exclusive lock, this function downgrades the lock
-        /// to a shared lock. If it has a shared lock, this function has no effect.
-        ///
-        /// This requests an *advisory* lock, meaning that the file could be accessed
-        /// by other programs that don't hold the lock.
-        ///
-        /// It is unspecified how shared locks interact with locks acquired by
-        /// non-WASI programs.
-        ///
-        /// This function returns `error-code::would-block` if the lock cannot be
-        /// acquired.
-        ///
-        /// Not all filesystems support locking; on filesystems which don't support
-        /// locking, this function returns `error-code::unsupported`.
-        ///
-        /// Note: This is similar to `flock(fd, LOCK_SH | LOCK_NB)` in Unix.
-        try-lock-shared: func() -> result<_, error-code>;
-
-        /// Request an exclusive advisory lock for an open file.
-        ///
-        /// This requests an *exclusive* lock; no other locks may be held for the
-        /// file while an exclusive lock is held.
-        ///
-        /// If the open file has a shared lock and there are no exclusive locks held
-        /// for the file, this function upgrades the lock to an exclusive lock. If the
-        /// open file already has an exclusive lock, this function has no effect.
-        ///
-        /// This requests an *advisory* lock, meaning that the file could be accessed
-        /// by other programs that don't hold the lock.
-        ///
-        /// It is unspecified whether this function succeeds if the file descriptor
-        /// is not opened for writing. It is unspecified how exclusive locks interact
-        /// with locks acquired by non-WASI programs.
-        ///
-        /// This function returns `error-code::would-block` if the lock cannot be
-        /// acquired.
-        ///
-        /// Not all filesystems support locking; on filesystems which don't support
-        /// locking, this function returns `error-code::unsupported`.
-        ///
-        /// Note: This is similar to `flock(fd, LOCK_EX | LOCK_NB)` in Unix.
-        try-lock-exclusive: func() -> result<_, error-code>;
-
-        /// Release a shared or exclusive lock on an open file.
-        ///
-        /// Note: This is similar to `flock(fd, LOCK_UN)` in Unix.
-        unlock: func() -> result<_, error-code>;
-
         /// Test whether two descriptors refer to the same filesystem object.
         ///
         /// In POSIX, this corresponds to testing whether the two descriptors have the
diff --git a/crates/wasi/src/preview2/host/clocks.rs b/crates/wasi/src/preview2/host/clocks.rs
index 1a07013d8..688780edb 100644
--- a/crates/wasi/src/preview2/host/clocks.rs
+++ b/crates/wasi/src/preview2/host/clocks.rs
@@ -55,7 +55,7 @@ impl<T: WasiView> monotonic_clock::Host for T {
     fn subscribe(&mut self, when: Instant, absolute: bool) -> anyhow::Result<Resource<Pollable>> {
         let clock_now = self.ctx().monotonic_clock.now();
         let duration = if absolute {
-            Duration::from_nanos(when - clock_now)
+            Duration::from_nanos(when.saturating_sub(clock_now))
         } else {
             Duration::from_nanos(when)
         };
diff --git a/crates/wasi/src/preview2/host/filesystem.rs b/crates/wasi/src/preview2/host/filesystem.rs
index e762a5466..f011d69e2 100644
--- a/crates/wasi/src/preview2/host/filesystem.rs
+++ b/crates/wasi/src/preview2/host/filesystem.rs
@@ -728,26 +728,6 @@ impl<T: WasiView> HostDescriptor for T {
         todo!("filesystem change_directory_permissions_at is not implemented")
     }
 
-    async fn lock_shared(&mut self, _fd: Resource<types::Descriptor>) -> FsResult<()> {
-        todo!("filesystem lock_shared is not implemented")
-    }
-
-    async fn lock_exclusive(&mut self, _fd: Resource<types::Descriptor>) -> FsResult<()> {
-        todo!("filesystem lock_exclusive is not implemented")
-    }
-
-    async fn try_lock_shared(&mut self, _fd: Resource<types::Descriptor>) -> FsResult<()> {
-        todo!("filesystem try_lock_shared is not implemented")
-    }
-
-    async fn try_lock_exclusive(&mut self, _fd: Resource<types::Descriptor>) -> FsResult<()> {
-        todo!("filesystem try_lock_exclusive is not implemented")
-    }
-
-    async fn unlock(&mut self, _fd: Resource<types::Descriptor>) -> FsResult<()> {
-        todo!("filesystem unlock is not implemented")
-    }
-
     fn read_via_stream(
         &mut self,
         fd: Resource<types::Descriptor>,
diff --git a/crates/wasi/src/preview2/host/filesystem/sync.rs b/crates/wasi/src/preview2/host/filesystem/sync.rs
index 545f7f022..28a9b77f5 100644
--- a/crates/wasi/src/preview2/host/filesystem/sync.rs
+++ b/crates/wasi/src/preview2/host/filesystem/sync.rs
@@ -301,26 +301,6 @@ impl<T: async_filesystem::HostDescriptor> sync_filesystem::HostDescriptor for T
         })
     }
 
-    fn lock_shared(&mut self, fd: Resource<sync_filesystem::Descriptor>) -> FsResult<()> {
-        in_tokio(async { async_filesystem::HostDescriptor::lock_shared(self, fd).await })
-    }
-
-    fn lock_exclusive(&mut self, fd: Resource<sync_filesystem::Descriptor>) -> FsResult<()> {
-        in_tokio(async { async_filesystem::HostDescriptor::lock_exclusive(self, fd).await })
-    }
-
-    fn try_lock_shared(&mut self, fd: Resource<sync_filesystem::Descriptor>) -> FsResult<()> {
-        in_tokio(async { async_filesystem::HostDescriptor::try_lock_shared(self, fd).await })
-    }
-
-    fn try_lock_exclusive(&mut self, fd: Resource<sync_filesystem::Descriptor>) -> FsResult<()> {
-        in_tokio(async { async_filesystem::HostDescriptor::try_lock_exclusive(self, fd).await })
-    }
-
-    fn unlock(&mut self, fd: Resource<sync_filesystem::Descriptor>) -> FsResult<()> {
-        in_tokio(async { async_filesystem::HostDescriptor::unlock(self, fd).await })
-    }
-
     fn read_via_stream(
         &mut self,
         fd: Resource<sync_filesystem::Descriptor>,
diff --git a/crates/wasi/src/preview2/preview1.rs b/crates/wasi/src/preview2/preview1.rs
index 282f50ae7..ab5a9cbd5 100644
--- a/crates/wasi/src/preview2/preview1.rs
+++ b/crates/wasi/src/preview2/preview1.rs
@@ -9,7 +9,7 @@ use crate::preview2::bindings::{
     io::{poll, streams},
 };
 use crate::preview2::{FsError, IsATTY, StreamError, StreamResult, TableError, WasiView};
-use anyhow::{anyhow, bail, Context};
+use anyhow::{bail, Context};
 use std::borrow::Borrow;
 use std::collections::{BTreeMap, HashSet};
 use std::mem::{self, size_of, size_of_val};
@@ -1131,6 +1131,9 @@ impl<
             .map_err(types::Error::trap)?;
         let mut fs_flags = types::Fdflags::empty();
         let mut fs_rights_base = types::Rights::all();
+        if let types::Filetype::Directory = fs_filetype {
+            fs_rights_base &= !types::Rights::FD_SEEK;
+        }
         if !flags.contains(filesystem::DescriptorFlags::READ) {
             fs_rights_base &= !types::Rights::FD_READ;
         }
@@ -2258,14 +2261,11 @@ impl<
 
     #[instrument(skip(self))]
     fn proc_exit(&mut self, status: types::Exitcode) -> anyhow::Error {
-        let status = match status {
-            0 => Ok(()),
-            _ => Err(()),
-        };
-        match self.exit(status) {
-            Err(e) => e,
-            Ok(()) => anyhow!("`exit` did not return an error"),
+        // Check that the status is within WASI's range.
+        if status >= 126 {
+            return anyhow::Error::msg("exit with invalid exit status outside of [0..126)");
         }
+        crate::preview2::I32Exit(status as i32).into()
     }
 
     #[instrument(skip(self))]
diff --git a/crates/wasi/wit/deps/filesystem/types.wit b/crates/wasi/wit/deps/filesystem/types.wit
index 81dcde908..37700332b 100644
--- a/crates/wasi/wit/deps/filesystem/types.wit
+++ b/crates/wasi/wit/deps/filesystem/types.wit
@@ -650,105 +650,6 @@ interface types {
             modes: modes,
         ) -> result<_, error-code>;
 
-        /// Request a shared advisory lock for an open file.
-        ///
-        /// This requests a *shared* lock; more than one shared lock can be held for
-        /// a file at the same time.
-        ///
-        /// If the open file has an exclusive lock, this function downgrades the lock
-        /// to a shared lock. If it has a shared lock, this function has no effect.
-        ///
-        /// This requests an *advisory* lock, meaning that the file could be accessed
-        /// by other programs that don't hold the lock.
-        ///
-        /// It is unspecified how shared locks interact with locks acquired by
-        /// non-WASI programs.
-        ///
-        /// This function blocks until the lock can be acquired.
-        ///
-        /// Not all filesystems support locking; on filesystems which don't support
-        /// locking, this function returns `error-code::unsupported`.
-        ///
-        /// Note: This is similar to `flock(fd, LOCK_SH)` in Unix.
-        lock-shared: func() -> result<_, error-code>;
-
-        /// Request an exclusive advisory lock for an open file.
-        ///
-        /// This requests an *exclusive* lock; no other locks may be held for the
-        /// file while an exclusive lock is held.
-        ///
-        /// If the open file has a shared lock and there are no exclusive locks held
-        /// for the file, this function upgrades the lock to an exclusive lock. If the
-        /// open file already has an exclusive lock, this function has no effect.
-        ///
-        /// This requests an *advisory* lock, meaning that the file could be accessed
-        /// by other programs that don't hold the lock.
-        ///
-        /// It is unspecified whether this function succeeds if the file descriptor
-        /// is not opened for writing. It is unspecified how exclusive locks interact
-        /// with locks acquired by non-WASI programs.
-        ///
-        /// This function blocks until the lock can be acquired.
-        ///
-        /// Not all filesystems support locking; on filesystems which don't support
-        /// locking, this function returns `error-code::unsupported`.
-        ///
-        /// Note: This is similar to `flock(fd, LOCK_EX)` in Unix.
-        lock-exclusive: func() -> result<_, error-code>;
-
-        /// Request a shared advisory lock for an open file.
-        ///
-        /// This requests a *shared* lock; more than one shared lock can be held for
-        /// a file at the same time.
-        ///
-        /// If the open file has an exclusive lock, this function downgrades the lock
-        /// to a shared lock. If it has a shared lock, this function has no effect.
-        ///
-        /// This requests an *advisory* lock, meaning that the file could be accessed
-        /// by other programs that don't hold the lock.
-        ///
-        /// It is unspecified how shared locks interact with locks acquired by
-        /// non-WASI programs.
-        ///
-        /// This function returns `error-code::would-block` if the lock cannot be
-        /// acquired.
-        ///
-        /// Not all filesystems support locking; on filesystems which don't support
-        /// locking, this function returns `error-code::unsupported`.
-        ///
-        /// Note: This is similar to `flock(fd, LOCK_SH | LOCK_NB)` in Unix.
-        try-lock-shared: func() -> result<_, error-code>;
-
-        /// Request an exclusive advisory lock for an open file.
-        ///
-        /// This requests an *exclusive* lock; no other locks may be held for the
-        /// file while an exclusive lock is held.
-        ///
-        /// If the open file has a shared lock and there are no exclusive locks held
-        /// for the file, this function upgrades the lock to an exclusive lock. If the
-        /// open file already has an exclusive lock, this function has no effect.
-        ///
-        /// This requests an *advisory* lock, meaning that the file could be accessed
-        /// by other programs that don't hold the lock.
-        ///
-        /// It is unspecified whether this function succeeds if the file descriptor
-        /// is not opened for writing. It is unspecified how exclusive locks interact
-        /// with locks acquired by non-WASI programs.
-        ///
-        /// This function returns `error-code::would-block` if the lock cannot be
-        /// acquired.
-        ///
-        /// Not all filesystems support locking; on filesystems which don't support
-        /// locking, this function returns `error-code::unsupported`.
-        ///
-        /// Note: This is similar to `flock(fd, LOCK_EX | LOCK_NB)` in Unix.
-        try-lock-exclusive: func() -> result<_, error-code>;
-
-        /// Release a shared or exclusive lock on an open file.
-        ///
-        /// Note: This is similar to `flock(fd, LOCK_UN)` in Unix.
-        unlock: func() -> result<_, error-code>;
-
         /// Test whether two descriptors refer to the same filesystem object.
         ///
         /// In POSIX, this corresponds to testing whether the two descriptors have the
diff --git a/crates/wasmtime/src/config.rs b/crates/wasmtime/src/config.rs
index 9f6e634cb..cc42af4e3 100644
--- a/crates/wasmtime/src/config.rs
+++ b/crates/wasmtime/src/config.rs
@@ -1866,7 +1866,7 @@ impl fmt::Debug for Config {
 ///
 /// This is used as an argument to the [`Config::strategy`] method.
 #[non_exhaustive]
-#[derive(Clone, Debug, Copy)]
+#[derive(PartialEq, Eq, Clone, Debug, Copy)]
 pub enum Strategy {
     /// An indicator that the compilation strategy should be automatically
     /// selected.
diff --git a/src/bin/wasmtime.rs b/src/bin/wasmtime.rs
index 32fcd040b..3ca25c7df 100644
--- a/src/bin/wasmtime.rs
+++ b/src/bin/wasmtime.rs
@@ -7,7 +7,7 @@ use anyhow::Result;
 use clap::Parser;
 
 /// Wasmtime WebAssembly Runtime
-#[derive(Parser)]
+#[derive(Parser, PartialEq)]
 #[clap(
     version,
     after_help = "If a subcommand is not provided, the `run` subcommand will be used.\n\
@@ -40,7 +40,7 @@ struct Wasmtime {
     run: wasmtime_cli::commands::RunCommand,
 }
 
-#[derive(Parser)]
+#[derive(Parser, PartialEq)]
 enum Subcommand {
     /// Runs a WebAssembly module
     Run(wasmtime_cli::commands::RunCommand),
@@ -99,7 +99,11 @@ impl Wasmtime {
 }
 
 fn main() -> Result<()> {
-    Wasmtime::parse().execute()
+    #[cfg(feature = "old-cli")]
+    return old_cli::main();
+
+    #[cfg(not(feature = "old-cli"))]
+    return Wasmtime::parse().execute();
 }
 
 #[test]
@@ -107,3 +111,186 @@ fn verify_cli() {
     use clap::CommandFactory;
     Wasmtime::command().debug_assert()
 }
+
+#[cfg(feature = "old-cli")]
+mod old_cli {
+    use crate::Wasmtime;
+    use anyhow::{bail, Result};
+    use clap::error::ErrorKind;
+    use clap::Parser;
+    use wasmtime_cli::old_cli as old;
+
+    enum WhichCli {
+        Old,
+        New,
+        Unspecified,
+    }
+
+    const DEFAULT_OLD_BEHAVIOR: bool = true;
+
+    fn which_cli() -> Result<WhichCli> {
+        Ok(match std::env::var("WASMTIME_NEW_CLI") {
+            Ok(s) if s == "0" => WhichCli::Old,
+            Ok(s) if s == "1" => WhichCli::New,
+            Ok(_) => bail!("the `WASMTIME_NEW_CLI` should either be `0` or `1`"),
+            Err(_) => WhichCli::Unspecified,
+        })
+    }
+
+    pub fn main() -> Result<()> {
+        match which_cli()? {
+            // If the old or the new CLI is explicitly selected, then run that
+            // variant no questions asked.
+            WhichCli::New => return Wasmtime::parse().execute(),
+            WhichCli::Old => return try_parse_old().unwrap_or_else(|e| e.exit()).execute(),
+
+            // fall through below to run an unspecified version of the CLI
+            WhichCli::Unspecified => {}
+        }
+
+        // Here it's not specified which version of the CLI should be used, so
+        // both the old and the new CLI parsers are used and depending on the
+        // results an execution happens.
+        let new = Wasmtime::try_parse();
+        let old = try_parse_old();
+        match (new, old) {
+            // Both parsers succeeded. This means that it's likely that no
+            // options to configure execution, e.g. `--disable-logging`, were
+            // used in the old parser or the new.
+            //
+            // The result of parsing can still differ though. For example:
+            //
+            //      wasmtime foo.wasm --invoke foo
+            //
+            // would previously be parsed as passing `--invoke` to Wasmtime but
+            // the new parser instead parses that as passing the flag to
+            // `foo.wasm`.
+            //
+            // In this situation use the `DEFAULT_OLD_BEHAVIOR` constant to
+            // dictate which wins and additionally print a warning message.
+            (Ok(new), Ok(old)) => {
+                if new == old {
+                    return new.execute();
+                }
+                if DEFAULT_OLD_BEHAVIOR {
+                    eprintln!(
+                        "\
+warning: this CLI invocation of Wasmtime will be parsed differently in future
+         Wasmtime versions -- see this online issue for more information:
+         https://github.com/bytecodealliance/wasmtime/issues/7384
+
+         Wasmtime will now execute with the old (<= Wasmtime 13) CLI parsing,
+         however this behavior can also be temporarily configured with an
+         environment variable:
+
+         - WASMTIME_NEW_CLI=0 to indicate old semantics are desired and silence this warning, or
+         - WASMTIME_NEW_CLI=1 to indicate new semantics are desired and use the latest behavior\
+"
+                    );
+                    old.execute()
+                } else {
+                    // this error message is not statically reachable due to
+                    // `DEFAULT_OLD_BEHAVIOR=true` at this time, but when that
+                    // changes this should be updated to have a more accurate
+                    // set of text.
+                    assert!(false);
+                    eprintln!(
+                        "\
+warning: this CLI invocation of Wasmtime is parsed differently than it was
+         previously -- see this online issue for more information:
+         https://github.com/bytecodealliance/wasmtime/issues/7384
+
+         Wasmtime will now execute with the new (>= Wasmtime XXX) CLI parsing,
+         however this behavior can also be temporarily configured with an
+         environment variable:
+
+         - WASMTIME_NEW_CLI=0 to indicate old semantics are desired instead of the new, or
+         - WASMTIME_NEW_CLI=1 to indicate new semantics are desired and silences this warning\
+"
+                    );
+                    new.execute()
+                }
+            }
+
+            // Here the new parser succeeded where the old one failed. This
+            // could indicate for example that new options are being passed on
+            // the CLI.
+            //
+            // In this situation assume that the new parser is what's intended
+            // so execute those semantics.
+            (Ok(new), Err(_old)) => new.execute(),
+
+            // Here the new parser failed and the old parser succeeded. This
+            // could indicate for example passing old CLI flags.
+            //
+            // Here assume that the old semantics are desired but emit a warning
+            // indicating that this will change in the future.
+            (Err(_new), Ok(old)) => {
+                eprintln!(
+                    "\
+warning: this CLI invocation of Wasmtime is going to break in the future -- for
+         more information see this issue online:
+         https://github.com/bytecodealliance/wasmtime/issues/7384
+
+         Wasmtime will now execute with the old (<= Wasmtime 13) CLI parsing,
+         however this behavior can also be temporarily configured with an
+         environment variable:
+
+         - WASMTIME_NEW_CLI=0 to indicate old semantics are desired and silence this warning, or
+         - WASMTIME_NEW_CLI=1 to indicate new semantics are desired and see the error\
+"
+                );
+                old.execute()
+            }
+
+            // Both parsers failed to parse the CLI invocation.
+            //
+            // This could mean that someone manually passed an old flag
+            // incorrectly. This could also mean that a new flag was passed
+            // incorrectly. Clap also models `--help` requests as errors here so
+            // this could also mean that a `--help` flag was passed.
+            //
+            // The current assumption in any case is that there's a human
+            // interacting with the CLI at this point. They may or may not be
+            // aware of the old CLI vs new CLI but if we're going to print an
+            // error message then now's probably as good a time as any to nudge
+            // them towards the new CLI. Any preexisting scripts which parsed
+            // the old CLI should not hit this case which means that all old
+            // succesful parses will not go through here.
+            //
+            // In any case, display the error for the new CLI, including new
+            // help text.
+            (Err(new), Err(_old)) => new.exit(),
+        }
+    }
+
+    fn try_parse_old() -> clap::error::Result<Wasmtime> {
+        match old::Wasmtime::try_parse() {
+            Ok(old) => Ok(convert(old)),
+            Err(e) => {
+                if let ErrorKind::InvalidSubcommand | ErrorKind::UnknownArgument = e.kind() {
+                    if let Ok(run) = old::RunCommand::try_parse() {
+                        return Ok(Wasmtime {
+                            subcommand: None,
+                            run: run.convert(),
+                        });
+                    }
+                }
+                Err(e)
+            }
+        }
+    }
+
+    fn convert(old: old::Wasmtime) -> Wasmtime {
+        let subcommand = match old {
+            old::Wasmtime::Compile(c) => crate::Subcommand::Compile(c.convert()),
+            old::Wasmtime::Run(c) => crate::Subcommand::Run(c.convert()),
+        };
+        let mut run = wasmtime_cli::commands::RunCommand::parse_from::<_, &str>(["x", "y"]);
+        run.module_and_args = Vec::new();
+        Wasmtime {
+            subcommand: Some(subcommand),
+            run,
+        }
+    }
+}
diff --git a/src/commands/compile.rs b/src/commands/compile.rs
index d4309d01d..d5f7ef0d3 100644
--- a/src/commands/compile.rs
+++ b/src/commands/compile.rs
@@ -29,7 +29,7 @@ static AFTER_HELP: Lazy<String> = Lazy::new(|| {
 });
 
 /// Compiles a WebAssembly module.
-#[derive(Parser)]
+#[derive(Parser, PartialEq)]
 #[structopt(
     name = "compile",
     version,
@@ -37,23 +37,24 @@ static AFTER_HELP: Lazy<String> = Lazy::new(|| {
 )]
 pub struct CompileCommand {
     #[clap(flatten)]
-    common: CommonOptions,
+    #[allow(missing_docs)]
+    pub common: CommonOptions,
 
     /// The target triple; default is the host triple
     #[clap(long, value_name = "TARGET")]
-    target: Option<String>,
+    pub target: Option<String>,
 
     /// The path of the output compiled module; defaults to <MODULE>.cwasm
     #[clap(short = 'o', long, value_name = "OUTPUT")]
-    output: Option<PathBuf>,
+    pub output: Option<PathBuf>,
 
     /// The directory path to write clif files into, one clif file per wasm function.
     #[clap(long = "emit-clif", value_name = "PATH")]
-    emit_clif: Option<PathBuf>,
+    pub emit_clif: Option<PathBuf>,
 
     /// The path of the WebAssembly to compile
     #[clap(index = 1, value_name = "MODULE")]
-    module: PathBuf,
+    pub module: PathBuf,
 }
 
 impl CompileCommand {
diff --git a/src/commands/config.rs b/src/commands/config.rs
index a3709a47b..b18e90ea8 100644
--- a/src/commands/config.rs
+++ b/src/commands/config.rs
@@ -7,14 +7,14 @@ const CONFIG_NEW_AFTER_HELP: &str =
     "If no file path is specified, the system configuration file path will be used.";
 
 /// Controls Wasmtime configuration settings
-#[derive(Parser)]
+#[derive(Parser, PartialEq)]
 #[clap(name = "config")]
 pub struct ConfigCommand {
     #[clap(subcommand)]
     subcommand: ConfigSubcommand,
 }
 
-#[derive(clap::Subcommand)]
+#[derive(clap::Subcommand, PartialEq)]
 enum ConfigSubcommand {
     /// Creates a new Wasmtime configuration file
     #[clap(after_help = CONFIG_NEW_AFTER_HELP)]
@@ -31,7 +31,7 @@ impl ConfigCommand {
 }
 
 /// Creates a new Wasmtime configuration file
-#[derive(Parser)]
+#[derive(Parser, PartialEq)]
 #[clap(name = "new", after_help = CONFIG_NEW_AFTER_HELP)]
 pub struct ConfigNewCommand {
     /// The path of the new configuration file
diff --git a/src/commands/explore.rs b/src/commands/explore.rs
index ff03dc4a8..6b008253f 100644
--- a/src/commands/explore.rs
+++ b/src/commands/explore.rs
@@ -6,7 +6,7 @@ use std::path::PathBuf;
 use wasmtime_cli_flags::CommonOptions;
 
 /// Explore the compilation of a WebAssembly module to native code.
-#[derive(Parser)]
+#[derive(Parser, PartialEq)]
 #[clap(name = "explore")]
 pub struct ExploreCommand {
     #[clap(flatten)]
diff --git a/src/commands/run.rs b/src/commands/run.rs
index df2fd673d..89fe35e85 100644
--- a/src/commands/run.rs
+++ b/src/commands/run.rs
@@ -54,11 +54,12 @@ fn parse_preloads(s: &str) -> Result<(String, PathBuf)> {
 }
 
 /// Runs a WebAssembly module
-#[derive(Parser)]
+#[derive(Parser, PartialEq)]
 #[structopt(name = "run")]
 pub struct RunCommand {
     #[clap(flatten)]
-    run: RunCommon,
+    #[allow(missing_docs)]
+    pub run: RunCommon,
 
     /// Grant access of a host directory to a guest.
     ///
@@ -67,7 +68,7 @@ pub struct RunCommand {
     /// then the `HOST` directory is opened and made available as the name
     /// `GUEST` in the guest.
     #[clap(long = "dir", value_name = "HOST_DIR[::GUEST_DIR]", value_parser = parse_dirs)]
-    dirs: Vec<(String, String)>,
+    pub dirs: Vec<(String, String)>,
 
     /// Pass an environment variable to the program.
     ///
@@ -77,11 +78,11 @@ pub struct RunCommand {
     /// has in the calling process for the guest, or in other words it will
     /// cause the environment variable `FOO` to be inherited.
     #[clap(long = "env", number_of_values = 1, value_name = "NAME[=VAL]", value_parser = parse_env_var)]
-    vars: Vec<(String, Option<String>)>,
+    pub vars: Vec<(String, Option<String>)>,
 
     /// The name of the function to run
     #[clap(long, value_name = "FUNCTION")]
-    invoke: Option<String>,
+    pub invoke: Option<String>,
 
     /// Load the given WebAssembly module before the main module
     #[clap(
@@ -90,7 +91,7 @@ pub struct RunCommand {
         value_name = "NAME=MODULE_PATH",
         value_parser = parse_preloads,
     )]
-    preloads: Vec<(String, PathBuf)>,
+    pub preloads: Vec<(String, PathBuf)>,
 
     /// The WebAssembly module to run and arguments to pass to it.
     ///
@@ -98,7 +99,7 @@ pub struct RunCommand {
     /// arguments unless the `--invoke` CLI argument is passed in which case
     /// arguments will be interpreted as arguments to the function specified.
     #[clap(value_name = "WASM", trailing_var_arg = true, required = true)]
-    module_and_args: Vec<OsString>,
+    pub module_and_args: Vec<OsString>,
 }
 
 enum CliLinker {
@@ -584,14 +585,22 @@ impl RunCommand {
         if self.run.common.wasi.common != Some(false) {
             match linker {
                 CliLinker::Core(linker) => {
-                    if self.run.common.wasi.preview2 == Some(true) {
-                        preview2::preview1::add_to_linker_sync(linker)?;
-                        self.set_preview2_ctx(store)?;
-                    } else {
-                        wasmtime_wasi::add_to_linker(linker, |host| {
-                            host.preview1_ctx.as_mut().unwrap()
-                        })?;
-                        self.set_preview1_ctx(store)?;
+                    match (self.run.common.wasi.preview2, self.run.common.wasi.threads) {
+                        // If preview2 is explicitly disabled, or if threads
+                        // are enabled, then use the historical preview1
+                        // implementation.
+                        (Some(false), _) | (None, Some(true)) => {
+                            wasmtime_wasi::add_to_linker(linker, |host| {
+                                host.preview1_ctx.as_mut().unwrap()
+                            })?;
+                            self.set_preview1_ctx(store)?;
+                        }
+                        // If preview2 was explicitly requested, always use it.
+                        // Otherwise use it so long as threads are disabled.
+                        (Some(true), _) | (None, Some(false) | None) => {
+                            preview2::preview1::add_to_linker_sync(linker)?;
+                            self.set_preview2_ctx(store)?;
+                        }
                     }
                 }
                 #[cfg(feature = "component-model")]
diff --git a/src/commands/serve.rs b/src/commands/serve.rs
index fc95f7add..e1b2d3625 100644
--- a/src/commands/serve.rs
+++ b/src/commands/serve.rs
@@ -64,7 +64,7 @@ const DEFAULT_ADDR: std::net::SocketAddr = std::net::SocketAddr::new(
 );
 
 /// Runs a WebAssembly module
-#[derive(Parser)]
+#[derive(Parser, PartialEq)]
 #[structopt(name = "serve")]
 pub struct ServeCommand {
     #[clap(flatten)]
diff --git a/src/commands/settings.rs b/src/commands/settings.rs
index 058909f8d..600a23454 100644
--- a/src/commands/settings.rs
+++ b/src/commands/settings.rs
@@ -8,7 +8,7 @@ use std::str::FromStr;
 use wasmtime_environ::{CompilerBuilder, FlagValue, Setting, SettingKind};
 
 /// Displays available Cranelift settings for a target.
-#[derive(Parser)]
+#[derive(Parser, PartialEq)]
 #[clap(name = "run")]
 pub struct SettingsCommand {
     /// The target triple to get the settings for; defaults to the host triple.
diff --git a/src/commands/wast.rs b/src/commands/wast.rs
index a3ac85f68..669b817a5 100644
--- a/src/commands/wast.rs
+++ b/src/commands/wast.rs
@@ -8,7 +8,7 @@ use wasmtime_cli_flags::CommonOptions;
 use wasmtime_wast::WastContext;
 
 /// Runs a WebAssembly test script file
-#[derive(Parser)]
+#[derive(Parser, PartialEq)]
 #[clap(name = "wast", version)]
 pub struct WastCommand {
     #[clap(flatten)]
diff --git a/src/common.rs b/src/common.rs
index 00c04cfb1..2d5f68c6f 100644
--- a/src/common.rs
+++ b/src/common.rs
@@ -35,7 +35,7 @@ impl RunTarget {
 }
 
 /// Common command line arguments for run commands.
-#[derive(Parser)]
+#[derive(Parser, PartialEq)]
 pub struct RunCommon {
     #[clap(flatten)]
     pub common: CommonOptions,
@@ -216,7 +216,7 @@ impl RunCommon {
     }
 }
 
-#[derive(Clone)]
+#[derive(Clone, PartialEq)]
 pub enum Profile {
     Native(wasmtime::ProfilingStrategy),
     Guest { path: String, interval: Duration },
diff --git a/src/lib.rs b/src/lib.rs
index 11473981a..7b657e7e3 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -13,3 +13,6 @@
 pub mod commands;
 
 pub(crate) mod common;
+
+#[cfg(feature = "old-cli")]
+pub mod old_cli;
diff --git a/src/old_cli.rs b/src/old_cli.rs
new file mode 100644
index 000000000..f55fc7b3f
--- /dev/null
+++ b/src/old_cli.rs
@@ -0,0 +1,993 @@
+#![allow(missing_docs)]
+
+use anyhow::{bail, Result};
+use clap::builder::{OsStringValueParser, TypedValueParser};
+use clap::Parser;
+use std::collections::HashMap;
+use std::ffi::OsString;
+use std::path::PathBuf;
+use std::time::Duration;
+
+/// Wasmtime WebAssembly Runtime
+#[derive(Parser)]
+#[clap(
+    version,
+    after_help = "If a subcommand is not provided, the `run` subcommand will be used.\n\
+                  \n\
+                  Usage examples:\n\
+                  \n\
+                  Running a WebAssembly module with a start function:\n\
+                  \n  \
+                  wasmtime example.wasm
+                  \n\
+                  Passing command line arguments to a WebAssembly module:\n\
+                  \n  \
+                  wasmtime example.wasm arg1 arg2 arg3\n\
+                  \n\
+                  Invoking a specific function (e.g. `add`) in a WebAssembly module:\n\
+                  \n  \
+                  wasmtime example.wasm --invoke add 1 2\n"
+)]
+pub enum Wasmtime {
+    /// Compiles a WebAssembly module.
+    Compile(CompileCommand),
+    /// Runs a WebAssembly module
+    Run(RunCommand),
+}
+
+#[test]
+fn verify_cli() {
+    use clap::CommandFactory;
+    Wasmtime::command().debug_assert()
+}
+
+#[derive(Parser)]
+pub enum Subcommand {
+    /// Compiles a WebAssembly module.
+    Compile(CompileCommand),
+    /// Runs a WebAssembly module
+    Run(RunCommand),
+}
+
+#[derive(Parser)]
+#[structopt(name = "run", trailing_var_arg = true)]
+pub struct RunCommand {
+    #[clap(flatten)]
+    common: CommonOptions,
+
+    /// Allow unknown exports when running commands.
+    #[clap(long = "allow-unknown-exports")]
+    allow_unknown_exports: bool,
+
+    /// Allow the main module to import unknown functions, using an
+    /// implementation that immediately traps, when running commands.
+    #[clap(long = "trap-unknown-imports")]
+    trap_unknown_imports: bool,
+
+    /// Allow the main module to import unknown functions, using an
+    /// implementation that returns default values, when running commands.
+    #[clap(long = "default-values-unknown-imports")]
+    default_values_unknown_imports: bool,
+
+    /// Allow executing precompiled WebAssembly modules as `*.cwasm` files.
+    ///
+    /// Note that this option is not safe to pass if the module being passed in
+    /// is arbitrary user input. Only `wasmtime`-precompiled modules generated
+    /// via the `wasmtime compile` command or equivalent should be passed as an
+    /// argument with this option specified.
+    #[clap(long = "allow-precompiled")]
+    allow_precompiled: bool,
+
+    /// Inherit environment variables and file descriptors following the
+    /// systemd listen fd specification (UNIX only)
+    #[clap(long = "listenfd")]
+    listenfd: bool,
+
+    /// Grant access to the given TCP listen socket
+    #[clap(
+        long = "tcplisten",
+        number_of_values = 1,
+        value_name = "SOCKET ADDRESS"
+    )]
+    tcplisten: Vec<String>,
+
+    /// Grant access to the given host directory
+    #[clap(long = "dir", number_of_values = 1, value_name = "DIRECTORY")]
+    dirs: Vec<String>,
+
+    /// Pass an environment variable to the program.
+    ///
+    /// The `--env FOO=BAR` form will set the environment variable named `FOO`
+    /// to the value `BAR` for the guest program using WASI. The `--env FOO`
+    /// form will set the environment variable named `FOO` to the same value it
+    /// has in the calling process for the guest, or in other words it will
+    /// cause the environment variable `FOO` to be inherited.
+    #[clap(long = "env", number_of_values = 1, value_name = "NAME[=VAL]", value_parser = parse_env_var)]
+    vars: Vec<(String, Option<String>)>,
+
+    /// The name of the function to run
+    #[clap(long, value_name = "FUNCTION")]
+    invoke: Option<String>,
+
+    /// Grant access to a guest directory mapped as a host directory
+    #[clap(long = "mapdir", number_of_values = 1, value_name = "GUEST_DIR::HOST_DIR", value_parser = parse_map_dirs)]
+    map_dirs: Vec<(String, String)>,
+
+    /// Pre-load machine learning graphs (i.e., models) for use by wasi-nn.
+    ///
+    /// Each use of the flag will preload a ML model from the host directory
+    /// using the given model encoding. The model will be mapped to the
+    /// directory name: e.g., `--wasi-nn-graph openvino:/foo/bar` will preload
+    /// an OpenVINO model named `bar`. Note that which model encodings are
+    /// available is dependent on the backends implemented in the
+    /// `wasmtime_wasi_nn` crate.
+    #[clap(long = "wasi-nn-graph", value_name = "FORMAT::HOST_DIR", value_parser = parse_graphs)]
+    graphs: Vec<(String, String)>,
+
+    /// The path of the WebAssembly module to run
+    #[clap(
+		required = true,
+        value_name = "MODULE",
+        value_parser = OsStringValueParser::new().try_map(parse_module),
+    )]
+    module: PathBuf,
+
+    /// Load the given WebAssembly module before the main module
+    #[clap(
+        long = "preload",
+        number_of_values = 1,
+        value_name = "NAME=MODULE_PATH",
+        value_parser = parse_preloads,
+    )]
+    preloads: Vec<(String, PathBuf)>,
+
+    /// Maximum execution time of wasm code before timing out (1, 2s, 100ms, etc)
+    #[clap(
+        long = "wasm-timeout",
+        value_name = "TIME",
+        value_parser = parse_dur,
+    )]
+    wasm_timeout: Option<Duration>,
+
+    /// Profiling strategy (valid options are: perfmap, jitdump, vtune, guest)
+    ///
+    /// The perfmap, jitdump, and vtune profiling strategies integrate Wasmtime
+    /// with external profilers such as `perf`. The guest profiling strategy
+    /// enables in-process sampling and will write the captured profile to
+    /// `wasmtime-guest-profile.json` by default which can be viewed at
+    /// https://profiler.firefox.com/.
+    ///
+    /// The `guest` option can be additionally configured as:
+    ///
+    ///     --profile=guest[,path[,interval]]
+    ///
+    /// where `path` is where to write the profile and `interval` is the
+    /// duration between samples. When used with `--wasm-timeout` the timeout
+    /// will be rounded up to the nearest multiple of this interval.
+    #[clap(
+        long,
+        value_name = "STRATEGY",
+        value_parser = parse_profile,
+    )]
+    profile: Option<Profile>,
+
+    /// Enable coredump generation after a WebAssembly trap.
+    #[clap(long = "coredump-on-trap", value_name = "PATH")]
+    coredump_on_trap: Option<String>,
+
+    // NOTE: this must come last for trailing varargs
+    /// The arguments to pass to the module
+    #[clap(value_name = "ARGS")]
+    module_args: Vec<String>,
+
+    /// Maximum size, in bytes, that a linear memory is allowed to reach.
+    ///
+    /// Growth beyond this limit will cause `memory.grow` instructions in
+    /// WebAssembly modules to return -1 and fail.
+    #[clap(long, value_name = "BYTES")]
+    max_memory_size: Option<usize>,
+
+    /// Maximum size, in table elements, that a table is allowed to reach.
+    #[clap(long)]
+    max_table_elements: Option<u32>,
+
+    /// Maximum number of WebAssembly instances allowed to be created.
+    #[clap(long)]
+    max_instances: Option<usize>,
+
+    /// Maximum number of WebAssembly tables allowed to be created.
+    #[clap(long)]
+    max_tables: Option<usize>,
+
+    /// Maximum number of WebAssembly linear memories allowed to be created.
+    #[clap(long)]
+    max_memories: Option<usize>,
+
+    /// Force a trap to be raised on `memory.grow` and `table.grow` failure
+    /// instead of returning -1 from these instructions.
+    ///
+    /// This is not necessarily a spec-compliant option to enable but can be
+    /// useful for tracking down a backtrace of what is requesting so much
+    /// memory, for example.
+    #[clap(long)]
+    trap_on_grow_failure: bool,
+
+    /// Indicates that the implementation of WASI preview1 should be backed by
+    /// the preview2 implementation for components.
+    ///
+    /// This will become the default in the future and this option will be
+    /// removed. For now this is primarily here for testing.
+    #[clap(long)]
+    preview2: bool,
+
+    /// Enables memory error checking.
+    ///
+    /// See wmemcheck.md for documentation on how to use.
+    #[clap(long)]
+    wmemcheck: bool,
+
+    /// Flag for WASI preview2 to inherit the host's network within the guest so
+    /// it has full access to all addresses/ports/etc.
+    #[clap(long)]
+    inherit_network: bool,
+}
+fn parse_module(s: OsString) -> anyhow::Result<PathBuf> {
+    // Do not accept wasmtime subcommand names as the module name
+    match s.to_str() {
+        Some("help") | Some("run") | Some("compile") => {
+            bail!("module name cannot be the same as a subcommand")
+        }
+        _ => Ok(s.into()),
+    }
+}
+
+#[derive(Clone)]
+pub enum Profile {
+    Native(wasmtime::ProfilingStrategy),
+    Guest { path: String, interval: Duration },
+}
+
+fn parse_env_var(s: &str) -> Result<(String, Option<String>)> {
+    let mut parts = s.splitn(2, '=');
+    Ok((
+        parts.next().unwrap().to_string(),
+        parts.next().map(|s| s.to_string()),
+    ))
+}
+
+fn parse_map_dirs(s: &str) -> Result<(String, String)> {
+    let parts: Vec<&str> = s.split("::").collect();
+    if parts.len() != 2 {
+        bail!("must contain exactly one double colon ('::')");
+    }
+    Ok((parts[0].into(), parts[1].into()))
+}
+
+fn parse_graphs(s: &str) -> Result<(String, String)> {
+    let parts: Vec<&str> = s.split("::").collect();
+    if parts.len() != 2 {
+        bail!("must contain exactly one double colon ('::')");
+    }
+    Ok((parts[0].into(), parts[1].into()))
+}
+
+fn parse_dur(s: &str) -> Result<Duration> {
+    // assume an integer without a unit specified is a number of seconds ...
+    if let Ok(val) = s.parse() {
+        return Ok(Duration::from_secs(val));
+    }
+    // ... otherwise try to parse it with units such as `3s` or `300ms`
+    let dur = humantime::parse_duration(s)?;
+    Ok(dur)
+}
+
+fn parse_preloads(s: &str) -> Result<(String, PathBuf)> {
+    let parts: Vec<&str> = s.splitn(2, '=').collect();
+    if parts.len() != 2 {
+        bail!("must contain exactly one equals character ('=')");
+    }
+    Ok((parts[0].into(), parts[1].into()))
+}
+
+fn parse_profile(s: &str) -> Result<Profile> {
+    let parts = s.split(',').collect::<Vec<_>>();
+    match &parts[..] {
+        ["perfmap"] => Ok(Profile::Native(wasmtime::ProfilingStrategy::PerfMap)),
+        ["jitdump"] => Ok(Profile::Native(wasmtime::ProfilingStrategy::JitDump)),
+        ["vtune"] => Ok(Profile::Native(wasmtime::ProfilingStrategy::VTune)),
+        ["guest"] => Ok(Profile::Guest {
+            path: "wasmtime-guest-profile.json".to_string(),
+            interval: Duration::from_millis(10),
+        }),
+        ["guest", path] => Ok(Profile::Guest {
+            path: path.to_string(),
+            interval: Duration::from_millis(10),
+        }),
+        ["guest", path, dur] => Ok(Profile::Guest {
+            path: path.to_string(),
+            interval: parse_dur(dur)?,
+        }),
+        _ => bail!("unknown profiling strategy: {s}"),
+    }
+}
+
+/// Compiles a WebAssembly module.
+#[derive(Parser)]
+pub struct CompileCommand {
+    #[clap(flatten)]
+    pub common: CommonOptions,
+
+    /// The target triple; default is the host triple
+    #[clap(long, value_name = "TARGET")]
+    pub target: Option<String>,
+
+    /// The path of the output compiled module; defaults to <MODULE>.cwasm
+    #[clap(short = 'o', long, value_name = "OUTPUT")]
+    pub output: Option<PathBuf>,
+
+    /// The directory path to write clif files into, one clif file per wasm function.
+    #[clap(long = "emit-clif", value_name = "PATH")]
+    pub emit_clif: Option<PathBuf>,
+
+    /// The path of the WebAssembly to compile
+    #[clap(index = 1, value_name = "MODULE")]
+    pub module: PathBuf,
+}
+
+/// Common options for commands that translate WebAssembly modules
+#[derive(Parser)]
+#[cfg_attr(test, derive(Debug, PartialEq))]
+pub struct CommonOptions {
+    /// Use specified configuration file
+    #[clap(long, value_name = "CONFIG_PATH")]
+    pub config: Option<PathBuf>,
+
+    /// Disable logging
+    #[clap(long, conflicts_with = "log_to_files")]
+    pub disable_logging: bool,
+
+    /// Log to per-thread log files instead of stderr
+    #[clap(long)]
+    pub log_to_files: bool,
+
+    /// Generate debug information
+    #[clap(short = 'g')]
+    pub debug_info: bool,
+
+    /// Disable cache system
+    #[clap(long)]
+    pub disable_cache: bool,
+
+    /// Disable parallel compilation
+    #[clap(long)]
+    pub disable_parallel_compilation: bool,
+
+    /// Enable or disable WebAssembly features
+    #[clap(long, value_name = "FEATURE,FEATURE,...", value_parser = parse_wasm_features)]
+    pub wasm_features: Option<WasmFeatures>,
+
+    /// Enable or disable WASI modules
+    #[clap(long, value_name = "MODULE,MODULE,...", value_parser = parse_wasi_modules)]
+    pub wasi_modules: Option<WasiModules>,
+
+    /// Generate jitdump file (supported on --features=profiling build)
+    /// Run optimization passes on translated functions, on by default
+    #[clap(short = 'O', long)]
+    pub optimize: bool,
+
+    /// Optimization level for generated functions
+    /// Supported levels: 0 (none), 1, 2 (most), or s (size); default is "most"
+    #[clap(
+        long,
+        value_name = "LEVEL",
+        value_parser = parse_opt_level,
+        verbatim_doc_comment,
+    )]
+    pub opt_level: Option<wasmtime::OptLevel>,
+
+    /// Set a Cranelift setting to a given value.
+    /// Use `wasmtime settings` to list Cranelift settings for a target.
+    #[clap(
+        long = "cranelift-set",
+        value_name = "NAME=VALUE",
+        number_of_values = 1,
+        verbatim_doc_comment,
+        value_parser = parse_cranelift_flag,
+    )]
+    pub cranelift_set: Vec<(String, String)>,
+
+    /// Enable a Cranelift boolean setting or preset.
+    /// Use `wasmtime settings` to list Cranelift settings for a target.
+    #[clap(
+        long,
+        value_name = "SETTING",
+        number_of_values = 1,
+        verbatim_doc_comment
+    )]
+    pub cranelift_enable: Vec<String>,
+
+    /// Maximum size in bytes of wasm memory before it becomes dynamically
+    /// relocatable instead of up-front-reserved.
+    #[clap(long, value_name = "MAXIMUM")]
+    pub static_memory_maximum_size: Option<u64>,
+
+    /// Force using a "static" style for all wasm memories
+    #[clap(long)]
+    pub static_memory_forced: bool,
+
+    /// Byte size of the guard region after static memories are allocated
+    #[clap(long, value_name = "SIZE")]
+    pub static_memory_guard_size: Option<u64>,
+
+    /// Byte size of the guard region after dynamic memories are allocated
+    #[clap(long, value_name = "SIZE")]
+    pub dynamic_memory_guard_size: Option<u64>,
+
+    /// Bytes to reserve at the end of linear memory for growth for dynamic
+    /// memories.
+    #[clap(long, value_name = "SIZE")]
+    pub dynamic_memory_reserved_for_growth: Option<u64>,
+
+    /// Enable Cranelift's internal debug verifier (expensive)
+    #[clap(long)]
+    pub enable_cranelift_debug_verifier: bool,
+
+    /// Enable Cranelift's internal NaN canonicalization
+    #[clap(long)]
+    pub enable_cranelift_nan_canonicalization: bool,
+
+    /// Enable execution fuel with N units fuel, where execution will trap after
+    /// running out of fuel.
+    ///
+    /// Most WebAssembly instructions consume 1 unit of fuel. Some instructions,
+    /// such as `nop`, `drop`, `block`, and `loop`, consume 0 units, as any
+    /// execution cost associated with them involves other instructions which do
+    /// consume fuel.
+    #[clap(long, value_name = "N")]
+    pub fuel: Option<u64>,
+
+    /// Executing wasm code will yield when a global epoch counter
+    /// changes, allowing for async operation without blocking the
+    /// executor.
+    #[clap(long)]
+    pub epoch_interruption: bool,
+
+    /// Disable the on-by-default address map from native code to wasm code
+    #[clap(long)]
+    pub disable_address_map: bool,
+
+    /// Disable the default of attempting to initialize linear memory via a
+    /// copy-on-write mapping
+    #[clap(long)]
+    pub disable_memory_init_cow: bool,
+
+    /// Enable the pooling allocator, in place of the on-demand
+    /// allocator.
+    #[cfg(feature = "pooling-allocator")]
+    #[clap(long)]
+    pub pooling_allocator: bool,
+
+    /// Maximum stack size, in bytes, that wasm is allowed to consume before a
+    /// stack overflow is reported.
+    #[clap(long)]
+    pub max_wasm_stack: Option<usize>,
+
+    /// Whether or not to force deterministic and host-independent behavior of
+    /// the relaxed-simd instructions.
+    ///
+    /// By default these instructions may have architecture-specific behavior as
+    /// allowed by the specification, but this can be used to force the behavior
+    /// of these instructions to match the deterministic behavior classified in
+    /// the specification. Note that enabling this option may come at a
+    /// performance cost.
+    #[clap(long)]
+    pub relaxed_simd_deterministic: bool,
+
+    /// Explicitly specify the name of the compiler to use for WebAssembly.
+    ///
+    /// Currently only `cranelift` and `winch` are supported, but not all builds
+    /// of Wasmtime have both built in.
+    #[clap(long)]
+    pub compiler: Option<String>,
+}
+
+fn parse_opt_level(opt_level: &str) -> Result<wasmtime::OptLevel> {
+    match opt_level {
+        "s" => Ok(wasmtime::OptLevel::SpeedAndSize),
+        "0" => Ok(wasmtime::OptLevel::None),
+        "1" => Ok(wasmtime::OptLevel::Speed),
+        "2" => Ok(wasmtime::OptLevel::Speed),
+        other => bail!(
+            "unknown optimization level `{}`, only 0,1,2,s accepted",
+            other
+        ),
+    }
+}
+
+#[derive(Default, Clone, Copy)]
+#[cfg_attr(test, derive(Debug, PartialEq))]
+pub struct WasmFeatures {
+    pub reference_types: Option<bool>,
+    pub multi_value: Option<bool>,
+    pub bulk_memory: Option<bool>,
+    pub simd: Option<bool>,
+    pub relaxed_simd: Option<bool>,
+    pub tail_call: Option<bool>,
+    pub threads: Option<bool>,
+    pub multi_memory: Option<bool>,
+    pub memory64: Option<bool>,
+    pub component_model: Option<bool>,
+    pub function_references: Option<bool>,
+}
+
+const SUPPORTED_WASM_FEATURES: &[(&str, &str)] = &[
+    ("all", "enables all supported WebAssembly features"),
+    (
+        "bulk-memory",
+        "enables support for bulk memory instructions",
+    ),
+    (
+        "multi-memory",
+        "enables support for the multi-memory proposal",
+    ),
+    ("multi-value", "enables support for multi-value functions"),
+    ("reference-types", "enables support for reference types"),
+    ("simd", "enables support for proposed SIMD instructions"),
+    (
+        "relaxed-simd",
+        "enables support for the relaxed simd proposal",
+    ),
+    ("tail-call", "enables support for WebAssembly tail calls"),
+    ("threads", "enables support for WebAssembly threads"),
+    ("memory64", "enables support for 64-bit memories"),
+    ("component-model", "enables support for the component model"),
+    (
+        "function-references",
+        "enables support for typed function references",
+    ),
+];
+
+fn parse_wasm_features(features: &str) -> Result<WasmFeatures> {
+    let features = features.trim();
+
+    let mut all = None;
+    let mut values: HashMap<_, _> = SUPPORTED_WASM_FEATURES
+        .iter()
+        .map(|(name, _)| (name.to_string(), None))
+        .collect();
+
+    if features == "all" {
+        all = Some(true);
+    } else if features == "-all" {
+        all = Some(false);
+    } else {
+        for feature in features.split(',') {
+            let feature = feature.trim();
+
+            if feature.is_empty() {
+                continue;
+            }
+
+            let (feature, value) = if feature.starts_with('-') {
+                (&feature[1..], false)
+            } else {
+                (feature, true)
+            };
+
+            if feature == "all" {
+                bail!("'all' cannot be specified with other WebAssembly features");
+            }
+
+            match values.get_mut(feature) {
+                Some(v) => *v = Some(value),
+                None => bail!("unsupported WebAssembly feature '{}'", feature),
+            }
+        }
+    }
+
+    Ok(WasmFeatures {
+        reference_types: all.or(values["reference-types"]),
+        multi_value: all.or(values["multi-value"]),
+        bulk_memory: all.or(values["bulk-memory"]),
+        simd: all.or(values["simd"]),
+        relaxed_simd: all.or(values["relaxed-simd"]),
+        tail_call: all.or(values["tail-call"]),
+        threads: all.or(values["threads"]),
+        multi_memory: all.or(values["multi-memory"]),
+        memory64: all.or(values["memory64"]),
+        #[cfg(feature = "component-model")]
+        component_model: all.or(values["component-model"]),
+        function_references: all.or(values["function-references"]),
+    })
+}
+
+/// Select which WASI modules are available at runtime for use by Wasm programs.
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub struct WasiModules {
+    /// Enable the wasi-common implementation; eventually this should be split into its separate
+    /// parts once the implementation allows for it (e.g. wasi-fs, wasi-clocks, etc.).
+    pub wasi_common: Option<bool>,
+
+    /// Enable the experimental wasi-nn implementation.
+    pub wasi_nn: Option<bool>,
+
+    /// Enable the experimental wasi-threads implementation.
+    pub wasi_threads: Option<bool>,
+
+    /// Enable the experimental wasi-http implementation
+    pub wasi_http: Option<bool>,
+}
+
+fn parse_wasi_modules(modules: &str) -> Result<WasiModules> {
+    let modules = modules.trim();
+    match modules {
+        "default" => Ok(WasiModules::default()),
+        "-default" => Ok(WasiModules::none()),
+        _ => {
+            // Starting from the default set of WASI modules, enable or disable a list of
+            // comma-separated modules.
+            let mut wasi_modules = WasiModules::default();
+            let mut set = |module: &str, enable: bool| match module {
+                "" => Ok(()),
+                "wasi-common" => Ok(wasi_modules.wasi_common = Some(enable)),
+                "experimental-wasi-nn" => Ok(wasi_modules.wasi_nn = Some(enable)),
+                "experimental-wasi-threads" => Ok(wasi_modules.wasi_threads = Some(enable)),
+                "experimental-wasi-http" => Ok(wasi_modules.wasi_http = Some(enable)),
+                "default" => bail!("'default' cannot be specified with other WASI modules"),
+                _ => bail!("unsupported WASI module '{}'", module),
+            };
+
+            for module in modules.split(',') {
+                let module = module.trim();
+                let (module, value) = if module.starts_with('-') {
+                    (&module[1..], false)
+                } else {
+                    (module, true)
+                };
+                set(module, value)?;
+            }
+
+            Ok(wasi_modules)
+        }
+    }
+}
+
+impl Default for WasiModules {
+    fn default() -> Self {
+        Self {
+            wasi_common: None,
+            wasi_nn: None,
+            wasi_threads: None,
+            wasi_http: None,
+        }
+    }
+}
+
+impl WasiModules {
+    /// Enable no modules.
+    fn none() -> Self {
+        Self {
+            wasi_common: Some(false),
+            wasi_nn: Some(false),
+            wasi_threads: Some(false),
+            wasi_http: Some(false),
+        }
+    }
+}
+
+fn parse_cranelift_flag(name_and_value: &str) -> Result<(String, String)> {
+    let mut split = name_and_value.splitn(2, '=');
+    let name = if let Some(name) = split.next() {
+        name.to_string()
+    } else {
+        bail!("missing name in cranelift flag");
+    };
+    let value = if let Some(value) = split.next() {
+        value.to_string()
+    } else {
+        bail!("missing value in cranelift flag");
+    };
+    Ok((name, value))
+}
+
+impl CompileCommand {
+    pub fn convert(self) -> crate::commands::CompileCommand {
+        let CompileCommand {
+            common,
+            target,
+            output,
+            emit_clif,
+            module,
+        } = self;
+
+        crate::commands::CompileCommand {
+            common: common.convert(),
+            target,
+            output,
+            emit_clif,
+            module,
+        }
+    }
+}
+
+impl RunCommand {
+    pub fn convert(self) -> crate::commands::RunCommand {
+        let RunCommand {
+            common,
+            allow_unknown_exports,
+            trap_unknown_imports,
+            default_values_unknown_imports,
+            allow_precompiled,
+            listenfd,
+            tcplisten,
+            dirs: old_dirs,
+            vars,
+            invoke,
+            map_dirs,
+            graphs,
+            preloads,
+            wasm_timeout,
+            profile,
+            coredump_on_trap,
+            max_memory_size,
+            max_table_elements,
+            max_instances,
+            max_tables,
+            max_memories,
+            trap_on_grow_failure,
+            wmemcheck,
+            module,
+            module_args,
+            preview2,
+            inherit_network,
+        } = self;
+
+        let mut common = common.convert();
+
+        let mut dirs = Vec::new();
+
+        for host in old_dirs {
+            dirs.push((host.clone(), host));
+        }
+
+        if preview2 {
+            common.wasi.preview2 = Some(true);
+        }
+        if wmemcheck {
+            common.wasm.wmemcheck = Some(true);
+        }
+        if trap_on_grow_failure {
+            common.wasm.trap_on_grow_failure = Some(true);
+        }
+        if let Some(max) = max_memories {
+            common.wasm.max_memories = Some(max);
+        }
+        if let Some(max) = max_tables {
+            common.wasm.max_tables = Some(max);
+        }
+        if let Some(max) = max_instances {
+            common.wasm.max_instances = Some(max);
+        }
+        if let Some(max) = max_memory_size {
+            common.wasm.max_memory_size = Some(max);
+        }
+        if let Some(max) = max_table_elements {
+            common.wasm.max_table_elements = Some(max);
+        }
+        if let Some(path) = coredump_on_trap {
+            common.debug.coredump = Some(path);
+        }
+        if let Some(timeout) = wasm_timeout {
+            common.wasm.timeout = Some(timeout);
+        }
+        if trap_unknown_imports {
+            common.wasm.unknown_imports_trap = Some(true);
+        }
+        if default_values_unknown_imports {
+            common.wasm.unknown_imports_default = Some(true);
+        }
+        common.wasi.tcplisten = tcplisten;
+        if listenfd {
+            common.wasi.listenfd = Some(true);
+        }
+        if allow_unknown_exports {
+            common.wasm.unknown_exports_allow = Some(true);
+        }
+        if inherit_network {
+            common.wasi.inherit_network = Some(true);
+        }
+
+        for (format, dir) in graphs {
+            common
+                .wasi
+                .nn_graph
+                .push(wasmtime_cli_flags::WasiNnGraph { format, dir });
+        }
+
+        for (guest, host) in map_dirs {
+            dirs.push((host, guest));
+        }
+
+        let run = crate::common::RunCommon {
+            common,
+            allow_precompiled,
+            profile: profile.map(|p| p.convert()),
+        };
+
+        let mut module_and_args = vec![module.into()];
+        module_and_args.extend(module_args.into_iter().map(|s| s.into()));
+        crate::commands::RunCommand {
+            run,
+            dirs,
+            vars,
+            invoke,
+            preloads,
+            module_and_args,
+        }
+    }
+}
+
+impl CommonOptions {
+    pub fn convert(self) -> wasmtime_cli_flags::CommonOptions {
+        let CommonOptions {
+            config,
+            disable_logging,
+            log_to_files,
+            debug_info,
+            disable_cache,
+            disable_parallel_compilation,
+            wasm_features,
+            wasi_modules,
+            optimize,
+            opt_level,
+            cranelift_set,
+            cranelift_enable,
+            static_memory_maximum_size,
+            static_memory_forced,
+            static_memory_guard_size,
+            dynamic_memory_guard_size,
+            dynamic_memory_reserved_for_growth,
+            enable_cranelift_debug_verifier,
+            enable_cranelift_nan_canonicalization,
+            fuel,
+            epoch_interruption,
+            disable_address_map,
+            disable_memory_init_cow,
+            pooling_allocator,
+            max_wasm_stack,
+            relaxed_simd_deterministic,
+            compiler,
+        } = self;
+
+        let mut ret = wasmtime_cli_flags::CommonOptions::parse_from::<_, String>([]);
+        match compiler.as_deref() {
+            Some("cranelift") => ret.codegen.compiler = Some(wasmtime::Strategy::Cranelift),
+            Some("winch") => ret.codegen.compiler = Some(wasmtime::Strategy::Winch),
+
+            // Plumbing an error up from this point is a bit onerous. Let's
+            // just hope that no one was using this from the old CLI and passing
+            // invalid values.
+            Some(_) => {}
+
+            None => {}
+        }
+        if relaxed_simd_deterministic {
+            ret.wasm.relaxed_simd_deterministic = Some(true);
+        }
+        ret.wasm.max_wasm_stack = max_wasm_stack;
+        if pooling_allocator {
+            ret.opts.pooling_allocator = Some(true);
+        }
+        if disable_memory_init_cow {
+            ret.opts.memory_init_cow = Some(false);
+        }
+        if disable_address_map {
+            ret.debug.address_map = Some(false);
+        }
+        if epoch_interruption {
+            ret.wasm.epoch_interruption = Some(true);
+        }
+        if enable_cranelift_debug_verifier {
+            ret.codegen.cranelift_debug_verifier = Some(true);
+        }
+        if enable_cranelift_nan_canonicalization {
+            ret.wasm.nan_canonicalization = Some(true);
+        }
+        if let Some(fuel) = fuel {
+            ret.wasm.fuel = Some(fuel);
+        }
+        if let Some(amt) = dynamic_memory_reserved_for_growth {
+            ret.opts.dynamic_memory_reserved_for_growth = Some(amt);
+        }
+        if let Some(amt) = dynamic_memory_guard_size {
+            ret.opts.dynamic_memory_guard_size = Some(amt);
+        }
+        if let Some(amt) = static_memory_guard_size {
+            ret.opts.static_memory_guard_size = Some(amt);
+        }
+        if static_memory_forced {
+            ret.opts.static_memory_forced = Some(true);
+        }
+        if let Some(amt) = static_memory_maximum_size {
+            ret.opts.static_memory_maximum_size = Some(amt);
+        }
+        if let Some(level) = opt_level {
+            ret.opts.opt_level = Some(level);
+        }
+        if disable_cache {
+            ret.codegen.cache = Some(false);
+        }
+        if debug_info {
+            ret.debug.debug_info = Some(true);
+        }
+        if optimize {
+            ret.opts.opt_level = Some(wasmtime::OptLevel::Speed);
+        }
+        if disable_parallel_compilation {
+            ret.codegen.parallel_compilation = Some(false);
+        }
+        if log_to_files {
+            ret.debug.log_to_files = Some(true);
+        }
+        if disable_logging {
+            ret.debug.logging = Some(false);
+        }
+        if let Some(path) = config {
+            ret.codegen.cache_config = Some(path.display().to_string());
+        }
+        for (key, val) in cranelift_set {
+            ret.codegen.cranelift.push((key, Some(val)));
+        }
+        for key in cranelift_enable {
+            ret.codegen.cranelift.push((key, None));
+        }
+        if let Some(features) = wasm_features {
+            let WasmFeatures {
+                reference_types,
+                multi_value,
+                bulk_memory,
+                simd,
+                relaxed_simd,
+                tail_call,
+                threads,
+                multi_memory,
+                memory64,
+                component_model,
+                function_references,
+            } = features;
+            ret.wasm.reference_types = reference_types;
+            ret.wasm.multi_value = multi_value;
+            ret.wasm.bulk_memory = bulk_memory;
+            ret.wasm.simd = simd;
+            ret.wasm.relaxed_simd = relaxed_simd;
+            ret.wasm.tail_call = tail_call;
+            ret.wasm.threads = threads;
+            ret.wasm.multi_memory = multi_memory;
+            ret.wasm.memory64 = memory64;
+            ret.wasm.component_model = component_model;
+            ret.wasm.function_references = function_references;
+        }
+        if let Some(modules) = wasi_modules {
+            let WasiModules {
+                wasi_common,
+                wasi_nn,
+                wasi_threads,
+                wasi_http,
+            } = modules;
+            ret.wasi.http = wasi_http;
+            ret.wasi.nn = wasi_nn;
+            ret.wasi.threads = wasi_threads;
+            ret.wasi.common = wasi_common;
+        }
+        ret
+    }
+}
+
+impl Profile {
+    pub fn convert(self) -> crate::common::Profile {
+        match self {
+            Profile::Native(s) => crate::common::Profile::Native(s),
+            Profile::Guest { path, interval } => crate::common::Profile::Guest { path, interval },
+        }
+    }
+}
diff --git a/supply-chain/imports.lock b/supply-chain/imports.lock
index f8318ffec..1114abac6 100644
--- a/supply-chain/imports.lock
+++ b/supply-chain/imports.lock
@@ -3,195 +3,195 @@
 
 [[unpublished.cranelift]]
 version = "0.102.0"
-audited_as = "0.101.1"
+audited_as = "0.101.2"
 
 [[unpublished.cranelift-bforest]]
 version = "0.102.0"
-audited_as = "0.101.1"
+audited_as = "0.101.2"
 
 [[unpublished.cranelift-codegen]]
 version = "0.102.0"
-audited_as = "0.101.1"
+audited_as = "0.101.2"
 
 [[unpublished.cranelift-codegen-meta]]
 version = "0.102.0"
-audited_as = "0.101.1"
+audited_as = "0.101.2"
 
 [[unpublished.cranelift-codegen-shared]]
 version = "0.102.0"
-audited_as = "0.101.1"
+audited_as = "0.101.2"
 
 [[unpublished.cranelift-control]]
 version = "0.102.0"
-audited_as = "0.101.1"
+audited_as = "0.101.2"
 
 [[unpublished.cranelift-entity]]
 version = "0.102.0"
-audited_as = "0.101.1"
+audited_as = "0.101.2"
 
 [[unpublished.cranelift-frontend]]
 version = "0.102.0"
-audited_as = "0.101.1"
+audited_as = "0.101.2"
 
 [[unpublished.cranelift-interpreter]]
 version = "0.102.0"
-audited_as = "0.101.1"
+audited_as = "0.101.2"
 
 [[unpublished.cranelift-isle]]
 version = "0.102.0"
-audited_as = "0.101.1"
+audited_as = "0.101.2"
 
 [[unpublished.cranelift-jit]]
 version = "0.102.0"
-audited_as = "0.101.1"
+audited_as = "0.101.2"
 
 [[unpublished.cranelift-module]]
 version = "0.102.0"
-audited_as = "0.101.1"
+audited_as = "0.101.2"
 
 [[unpublished.cranelift-native]]
 version = "0.102.0"
-audited_as = "0.101.1"
+audited_as = "0.101.2"
 
 [[unpublished.cranelift-object]]
 version = "0.102.0"
-audited_as = "0.101.1"
+audited_as = "0.101.2"
 
 [[unpublished.cranelift-reader]]
 version = "0.102.0"
-audited_as = "0.101.1"
+audited_as = "0.101.2"
 
 [[unpublished.cranelift-serde]]
 version = "0.102.0"
-audited_as = "0.101.1"
+audited_as = "0.101.2"
 
 [[unpublished.cranelift-wasm]]
 version = "0.102.0"
-audited_as = "0.101.1"
+audited_as = "0.101.2"
 
 [[unpublished.wasi-cap-std-sync]]
 version = "15.0.0"
-audited_as = "14.0.1"
+audited_as = "14.0.2"
 
 [[unpublished.wasi-common]]
 version = "15.0.0"
-audited_as = "14.0.1"
+audited_as = "14.0.2"
 
 [[unpublished.wasi-tokio]]
 version = "15.0.0"
-audited_as = "14.0.1"
+audited_as = "14.0.2"
 
 [[unpublished.wasmtime]]
 version = "15.0.0"
-audited_as = "14.0.1"
+audited_as = "14.0.2"
 
 [[unpublished.wasmtime-asm-macros]]
 version = "15.0.0"
-audited_as = "14.0.1"
+audited_as = "14.0.2"
 
 [[unpublished.wasmtime-cache]]
 version = "15.0.0"
-audited_as = "14.0.1"
+audited_as = "14.0.2"
 
 [[unpublished.wasmtime-cli]]
 version = "15.0.0"
-audited_as = "14.0.1"
+audited_as = "14.0.2"
 
 [[unpublished.wasmtime-cli-flags]]
 version = "15.0.0"
-audited_as = "14.0.1"
+audited_as = "14.0.2"
 
 [[unpublished.wasmtime-component-macro]]
 version = "15.0.0"
-audited_as = "14.0.1"
+audited_as = "14.0.2"
 
 [[unpublished.wasmtime-component-util]]
 version = "15.0.0"
-audited_as = "14.0.1"
+audited_as = "14.0.2"
 
 [[unpublished.wasmtime-cranelift]]
 version = "15.0.0"
-audited_as = "14.0.1"
+audited_as = "14.0.2"
 
 [[unpublished.wasmtime-cranelift-shared]]
 version = "15.0.0"
-audited_as = "14.0.1"
+audited_as = "14.0.2"
 
 [[unpublished.wasmtime-environ]]
 version = "15.0.0"
-audited_as = "14.0.1"
+audited_as = "14.0.2"
 
 [[unpublished.wasmtime-explorer]]
 version = "15.0.0"
-audited_as = "14.0.1"
+audited_as = "14.0.2"
 
 [[unpublished.wasmtime-fiber]]
 version = "15.0.0"
-audited_as = "14.0.1"
+audited_as = "14.0.2"
 
 [[unpublished.wasmtime-jit]]
 version = "15.0.0"
-audited_as = "14.0.1"
+audited_as = "14.0.2"
 
 [[unpublished.wasmtime-jit-debug]]
 version = "15.0.0"
-audited_as = "14.0.1"
+audited_as = "14.0.2"
 
 [[unpublished.wasmtime-jit-icache-coherence]]
 version = "15.0.0"
-audited_as = "14.0.1"
+audited_as = "14.0.2"
 
 [[unpublished.wasmtime-runtime]]
 version = "15.0.0"
-audited_as = "14.0.1"
+audited_as = "14.0.2"
 
 [[unpublished.wasmtime-types]]
 version = "15.0.0"
-audited_as = "14.0.1"
+audited_as = "14.0.2"
 
 [[unpublished.wasmtime-wasi]]
 version = "15.0.0"
-audited_as = "14.0.1"
+audited_as = "14.0.2"
 
 [[unpublished.wasmtime-wasi-http]]
 version = "15.0.0"
-audited_as = "14.0.1"
+audited_as = "14.0.2"
 
 [[unpublished.wasmtime-wasi-nn]]
 version = "15.0.0"
-audited_as = "14.0.1"
+audited_as = "14.0.2"
 
 [[unpublished.wasmtime-wasi-threads]]
 version = "15.0.0"
-audited_as = "14.0.1"
+audited_as = "14.0.2"
 
 [[unpublished.wasmtime-wast]]
 version = "15.0.0"
-audited_as = "14.0.1"
+audited_as = "14.0.2"
 
 [[unpublished.wasmtime-winch]]
 version = "15.0.0"
-audited_as = "14.0.1"
+audited_as = "14.0.2"
 
 [[unpublished.wasmtime-wit-bindgen]]
 version = "15.0.0"
-audited_as = "14.0.1"
+audited_as = "14.0.2"
 
 [[unpublished.wasmtime-wmemcheck]]
 version = "15.0.0"
-audited_as = "14.0.1"
+audited_as = "14.0.2"
 
 [[unpublished.wiggle]]
 version = "15.0.0"
-audited_as = "14.0.1"
+audited_as = "14.0.2"
 
 [[unpublished.wiggle-generate]]
 version = "15.0.0"
-audited_as = "14.0.1"
+audited_as = "14.0.2"
 
 [[unpublished.wiggle-macro]]
 version = "15.0.0"
-audited_as = "14.0.1"
+audited_as = "14.0.2"
 
 [[unpublished.wiggle-test]]
 version = "0.0.0"
@@ -199,7 +199,7 @@ audited_as = "0.1.0"
 
 [[unpublished.winch-codegen]]
 version = "0.13.0"
-audited_as = "0.12.1"
+audited_as = "0.12.2"
 
 [[publisher.aho-corasick]]
 version = "1.0.2"
@@ -370,104 +370,104 @@ user-login = "epage"
 user-name = "Ed Page"
 
 [[publisher.cranelift]]
-version = "0.101.1"
-when = "2023-10-23"
+version = "0.101.2"
+when = "2023-10-26"
 user-id = 73222
 user-login = "wasmtime-publish"
 
 [[publisher.cranelift-bforest]]
-version = "0.101.1"
-when = "2023-10-23"
+version = "0.101.2"
+when = "2023-10-26"
 user-id = 73222
 user-login = "wasmtime-publish"
 
 [[publisher.cranelift-codegen]]
-version = "0.101.1"
-when = "2023-10-23"
+version = "0.101.2"
+when = "2023-10-26"
 user-id = 73222
 user-login = "wasmtime-publish"
 
 [[publisher.cranelift-codegen-meta]]
-version = "0.101.1"
-when = "2023-10-23"
+version = "0.101.2"
+when = "2023-10-26"
 user-id = 73222
 user-login = "wasmtime-publish"
 
 [[publisher.cranelift-codegen-shared]]
-version = "0.101.1"
-when = "2023-10-23"
+version = "0.101.2"
+when = "2023-10-26"
 user-id = 73222
 user-login = "wasmtime-publish"
 
 [[publisher.cranelift-control]]
-version = "0.101.1"
-when = "2023-10-23"
+version = "0.101.2"
+when = "2023-10-26"
 user-id = 73222
 user-login = "wasmtime-publish"
 
 [[publisher.cranelift-entity]]
-version = "0.101.1"
-when = "2023-10-23"
+version = "0.101.2"
+when = "2023-10-26"
 user-id = 73222
 user-login = "wasmtime-publish"
 
 [[publisher.cranelift-frontend]]
-version = "0.101.1"
-when = "2023-10-23"
+version = "0.101.2"
+when = "2023-10-26"
 user-id = 73222
 user-login = "wasmtime-publish"
 
 [[publisher.cranelift-interpreter]]
-version = "0.101.1"
-when = "2023-10-23"
+version = "0.101.2"
+when = "2023-10-26"
 user-id = 73222
 user-login = "wasmtime-publish"
 
 [[publisher.cranelift-isle]]
-version = "0.101.1"
-when = "2023-10-23"
+version = "0.101.2"
+when = "2023-10-26"
 user-id = 73222
 user-login = "wasmtime-publish"
 
 [[publisher.cranelift-jit]]
-version = "0.101.1"
-when = "2023-10-23"
+version = "0.101.2"
+when = "2023-10-26"
 user-id = 73222
 user-login = "wasmtime-publish"
 
 [[publisher.cranelift-module]]
-version = "0.101.1"
-when = "2023-10-23"
+version = "0.101.2"
+when = "2023-10-26"
 user-id = 73222
 user-login = "wasmtime-publish"
 
 [[publisher.cranelift-native]]
-version = "0.101.1"
-when = "2023-10-23"
+version = "0.101.2"
+when = "2023-10-26"
 user-id = 73222
 user-login = "wasmtime-publish"
 
 [[publisher.cranelift-object]]
-version = "0.101.1"
-when = "2023-10-23"
+version = "0.101.2"
+when = "2023-10-26"
 user-id = 73222
 user-login = "wasmtime-publish"
 
 [[publisher.cranelift-reader]]
-version = "0.101.1"
-when = "2023-10-23"
+version = "0.101.2"
+when = "2023-10-26"
 user-id = 73222
 user-login = "wasmtime-publish"
 
 [[publisher.cranelift-serde]]
-version = "0.101.1"
-when = "2023-10-23"
+version = "0.101.2"
+when = "2023-10-26"
 user-id = 73222
 user-login = "wasmtime-publish"
 
 [[publisher.cranelift-wasm]]
-version = "0.101.1"
-when = "2023-10-23"
+version = "0.101.2"
+when = "2023-10-26"
 user-id = 73222
 user-login = "wasmtime-publish"
 
@@ -640,8 +640,8 @@ user-login = "BurntSushi"
 user-name = "Andrew Gallant"
 
 [[publisher.rustix]]
-version = "0.38.14"
-when = "2023-09-20"
+version = "0.38.21"
+when = "2023-10-26"
 user-id = 6825
 user-login = "sunfishcode"
 user-name = "Dan Gohman"
@@ -786,20 +786,20 @@ user-login = "BurntSushi"
 user-name = "Andrew Gallant"
 
 [[publisher.wasi-cap-std-sync]]
-version = "14.0.1"
-when = "2023-10-23"
+version = "14.0.2"
+when = "2023-10-26"
 user-id = 73222
 user-login = "wasmtime-publish"
 
 [[publisher.wasi-common]]
-version = "14.0.1"
-when = "2023-10-23"
+version = "14.0.2"
+when = "2023-10-26"
 user-id = 73222
 user-login = "wasmtime-publish"
 
 [[publisher.wasi-tokio]]
-version = "14.0.1"
-when = "2023-10-23"
+version = "14.0.2"
+when = "2023-10-26"
 user-id = 73222
 user-login = "wasmtime-publish"
 
@@ -881,152 +881,152 @@ user-login = "alexcrichton"
 user-name = "Alex Crichton"
 
 [[publisher.wasmtime]]
-version = "14.0.1"
-when = "2023-10-23"
+version = "14.0.2"
+when = "2023-10-26"
 user-id = 73222
 user-login = "wasmtime-publish"
 
 [[publisher.wasmtime-asm-macros]]
-version = "14.0.1"
-when = "2023-10-23"
+version = "14.0.2"
+when = "2023-10-26"
 user-id = 73222
 user-login = "wasmtime-publish"
 
 [[publisher.wasmtime-cache]]
-version = "14.0.1"
-when = "2023-10-23"
+version = "14.0.2"
+when = "2023-10-26"
 user-id = 73222
 user-login = "wasmtime-publish"
 
 [[publisher.wasmtime-cli]]
-version = "14.0.1"
-when = "2023-10-23"
+version = "14.0.2"
+when = "2023-10-26"
 user-id = 73222
 user-login = "wasmtime-publish"
 
 [[publisher.wasmtime-cli-flags]]
-version = "14.0.1"
-when = "2023-10-23"
+version = "14.0.2"
+when = "2023-10-26"
 user-id = 73222
 user-login = "wasmtime-publish"
 
 [[publisher.wasmtime-component-macro]]
-version = "14.0.1"
-when = "2023-10-23"
+version = "14.0.2"
+when = "2023-10-26"
 user-id = 73222
 user-login = "wasmtime-publish"
 
 [[publisher.wasmtime-component-util]]
-version = "14.0.1"
-when = "2023-10-23"
+version = "14.0.2"
+when = "2023-10-26"
 user-id = 73222
 user-login = "wasmtime-publish"
 
 [[publisher.wasmtime-cranelift]]
-version = "14.0.1"
-when = "2023-10-23"
+version = "14.0.2"
+when = "2023-10-26"
 user-id = 73222
 user-login = "wasmtime-publish"
 
 [[publisher.wasmtime-cranelift-shared]]
-version = "14.0.1"
-when = "2023-10-23"
+version = "14.0.2"
+when = "2023-10-26"
 user-id = 73222
 user-login = "wasmtime-publish"
 
 [[publisher.wasmtime-environ]]
-version = "14.0.1"
-when = "2023-10-23"
+version = "14.0.2"
+when = "2023-10-26"
 user-id = 73222
 user-login = "wasmtime-publish"
 
 [[publisher.wasmtime-explorer]]
-version = "14.0.1"
-when = "2023-10-23"
+version = "14.0.2"
+when = "2023-10-26"
 user-id = 73222
 user-login = "wasmtime-publish"
 
 [[publisher.wasmtime-fiber]]
-version = "14.0.1"
-when = "2023-10-23"
+version = "14.0.2"
+when = "2023-10-26"
 user-id = 73222
 user-login = "wasmtime-publish"
 
 [[publisher.wasmtime-jit]]
-version = "14.0.1"
-when = "2023-10-23"
+version = "14.0.2"
+when = "2023-10-26"
 user-id = 73222
 user-login = "wasmtime-publish"
 
 [[publisher.wasmtime-jit-debug]]
-version = "14.0.1"
-when = "2023-10-23"
+version = "14.0.2"
+when = "2023-10-26"
 user-id = 73222
 user-login = "wasmtime-publish"
 
 [[publisher.wasmtime-jit-icache-coherence]]
-version = "14.0.1"
-when = "2023-10-23"
+version = "14.0.2"
+when = "2023-10-26"
 user-id = 73222
 user-login = "wasmtime-publish"
 
 [[publisher.wasmtime-runtime]]
-version = "14.0.1"
-when = "2023-10-23"
+version = "14.0.2"
+when = "2023-10-26"
 user-id = 73222
 user-login = "wasmtime-publish"
 
 [[publisher.wasmtime-types]]
-version = "14.0.1"
-when = "2023-10-23"
+version = "14.0.2"
+when = "2023-10-26"
 user-id = 73222
 user-login = "wasmtime-publish"
 
 [[publisher.wasmtime-wasi]]
-version = "14.0.1"
-when = "2023-10-23"
+version = "14.0.2"
+when = "2023-10-26"
 user-id = 73222
 user-login = "wasmtime-publish"
 
 [[publisher.wasmtime-wasi-http]]
-version = "14.0.1"
-when = "2023-10-23"
+version = "14.0.2"
+when = "2023-10-26"
 user-id = 73222
 user-login = "wasmtime-publish"
 
 [[publisher.wasmtime-wasi-nn]]
-version = "14.0.1"
-when = "2023-10-23"
+version = "14.0.2"
+when = "2023-10-26"
 user-id = 73222
 user-login = "wasmtime-publish"
 
 [[publisher.wasmtime-wasi-threads]]
-version = "14.0.1"
-when = "2023-10-23"
+version = "14.0.2"
+when = "2023-10-26"
 user-id = 73222
 user-login = "wasmtime-publish"
 
 [[publisher.wasmtime-wast]]
-version = "14.0.1"
-when = "2023-10-23"
+version = "14.0.2"
+when = "2023-10-26"
 user-id = 73222
 user-login = "wasmtime-publish"
 
 [[publisher.wasmtime-winch]]
-version = "14.0.1"
-when = "2023-10-23"
+version = "14.0.2"
+when = "2023-10-26"
 user-id = 73222
 user-login = "wasmtime-publish"
 
 [[publisher.wasmtime-wit-bindgen]]
-version = "14.0.1"
-when = "2023-10-23"
+version = "14.0.2"
+when = "2023-10-26"
 user-id = 73222
 user-login = "wasmtime-publish"
 
 [[publisher.wasmtime-wmemcheck]]
-version = "14.0.1"
-when = "2023-10-23"
+version = "14.0.2"
+when = "2023-10-26"
 user-id = 73222
 user-login = "wasmtime-publish"
 
@@ -1045,20 +1045,20 @@ user-login = "alexcrichton"
 user-name = "Alex Crichton"
 
 [[publisher.wiggle]]
-version = "14.0.1"
-when = "2023-10-23"
+version = "14.0.2"
+when = "2023-10-26"
 user-id = 73222
 user-login = "wasmtime-publish"
 
 [[publisher.wiggle-generate]]
-version = "14.0.1"
-when = "2023-10-23"
+version = "14.0.2"
+when = "2023-10-26"
 user-id = 73222
 user-login = "wasmtime-publish"
 
 [[publisher.wiggle-macro]]
-version = "14.0.1"
-when = "2023-10-23"
+version = "14.0.2"
+when = "2023-10-26"
 user-id = 73222
 user-login = "wasmtime-publish"
 
@@ -1077,8 +1077,8 @@ user-login = "BurntSushi"
 user-name = "Andrew Gallant"
 
 [[publisher.winch-codegen]]
-version = "0.12.1"
-when = "2023-10-23"
+version = "0.12.2"
+when = "2023-10-26"
 user-id = 73222
 user-login = "wasmtime-publish"
 


Test patch:
diff --git a/cranelift/filetests/filetests/pcc/succeed/dynamic.clif b/cranelift/filetests/filetests/pcc/succeed/dynamic.clif
new file mode 100644
index 000000000..13f96bc8b
--- /dev/null
+++ b/cranelift/filetests/filetests/pcc/succeed/dynamic.clif
@@ -0,0 +1,42 @@
+test compile
+set enable_pcc=true
+target aarch64
+target x86_64
+
+;; Equivalent to a Wasm `i64.load` from a dynamic memory.
+function %f0(i64 vmctx, i32) -> i64 {
+    gv0 = vmctx
+    gv1 = load.i64 notrap aligned checked gv0+0 ;; base
+    gv2 = load.i64 notrap aligned checked gv0+8 ;; size
+    
+    ;; mock vmctx struct:
+    mt0 = struct 16 {
+        0: i64 readonly ! dynamic_mem(mt1, 0, 0),
+        8: i64 readonly ! dynamic_range(64, gv2, gv2),
+    }
+    ;; mock dynamic memory: dynamic range, plus 2GiB guard
+    mt1 = dynamic_memory gv2 + 0x8000_0000
+
+block0(v0 ! mem(mt0, 0, 0): i64, v1 ! dynamic_range(32, v1, v1): i32):
+    v2 ! dynamic_range(64, v1, v1)            = uextend.i64 v1       ;; extended Wasm offset
+    v3 ! dynamic_mem(mt1, 0, 0)               = global_value.i64 gv1 ;; base
+    v4 ! dynamic_range(64, gv2, gv2)          = global_value.i64 gv2 ;; size
+    v5 ! compare(uge, v1, gv2)                = icmp.i64 uge v2, v4  ;; bounds-check compare of extended Wasm offset to size
+    v6 ! dynamic_mem(mt1, v1, v1)             = iadd.i64 v3, v2      ;; compute access address: memory base plus extended Wasm offset
+    v7 ! dynamic_mem(mt1, 0, 0, nullable)     = iconst.i64 0         ;; null pointer for speculative path
+    v8 ! dynamic_mem(mt1, 0, gv2-1, nullable) = select_spectre_guard v5, v7, v6  ;; if OOB, pick null, otherwise the real address
+    v9                                        = load.i64 checked v8
+    return v9
+}
+
+;; select sees:
+;;  v5 ! compare(uge, v1, gv2)
+;;  v6 ! dynamic_mem(mt1, v1, v1)
+;;  v7 ! dynamic_mem(mt0, 0, 0, nullable)
+;;
+;; preprocess:
+;; v6' (assuming compare is false) = dynamic_mem(mt1, 0, gv2-1)
+;; v7' (assuming compare is true)  = dynamic_mem(mt1, 0, 0, nullable)
+;;
+;; take the union of range and nullability:
+;; dynamic_mem(mt1, 0, gv2-1, nullable)
diff --git a/crates/test-programs/src/bin/preview2_sleep.rs b/crates/test-programs/src/bin/preview2_sleep.rs
index be8a20cb2..a5f4d76a3 100644
--- a/crates/test-programs/src/bin/preview2_sleep.rs
+++ b/crates/test-programs/src/bin/preview2_sleep.rs
@@ -1,15 +1,27 @@
 use test_programs::wasi::{clocks::monotonic_clock, io::poll};
 
 fn main() {
-    // Sleep ten milliseconds.  Note that we call the relevant host functions directly rather than go through
-    // libstd, since we want to ensure we're calling `monotonic_clock::subscribe` with an `absolute` parameter of
-    // `true`, which libstd won't necessarily do (but which e.g. CPython _will_ do).
-    eprintln!("calling subscribe");
-    let p = monotonic_clock::subscribe(monotonic_clock::now() + 10_000_000, true);
-    dbg!(&p as *const _);
-    let list = &[&p];
-    dbg!(list.as_ptr());
-    eprintln!("calling poll");
-    poll::poll_list(list);
-    eprintln!("done");
+    sleep_10ms();
+    sleep_0ms();
+    sleep_backwards_in_time();
+}
+
+fn sleep_10ms() {
+    let dur = 10_000_000;
+    let p = monotonic_clock::subscribe(monotonic_clock::now() + dur, true);
+    poll::poll_one(&p);
+    let p = monotonic_clock::subscribe(dur, false);
+    poll::poll_one(&p);
+}
+
+fn sleep_0ms() {
+    let p = monotonic_clock::subscribe(monotonic_clock::now(), true);
+    poll::poll_one(&p);
+    let p = monotonic_clock::subscribe(0, false);
+    poll::poll_one(&p);
+}
+
+fn sleep_backwards_in_time() {
+    let p = monotonic_clock::subscribe(monotonic_clock::now() - 1, true);
+    poll::poll_one(&p);
 }
diff --git a/tests/all/cli_tests.rs b/tests/all/cli_tests.rs
index 2c8aa8122..049d910f6 100644
--- a/tests/all/cli_tests.rs
+++ b/tests/all/cli_tests.rs
@@ -32,7 +32,7 @@ pub fn get_wasmtime_command() -> Result<Command> {
     // If we're running tests with a "runner" then we might be doing something
     // like cross-emulation, so spin up the emulator rather than the tests
     // itself, which may not be natively executable.
-    let cmd = if let Some((_, runner)) = runner {
+    let mut cmd = if let Some((_, runner)) = runner {
         let mut parts = runner.split_whitespace();
         let mut cmd = Command::new(parts.next().unwrap());
         for arg in parts {
@@ -44,6 +44,10 @@ pub fn get_wasmtime_command() -> Result<Command> {
         Command::new(&me)
     };
 
+    // Ignore this if it's specified in the environment to allow tests to run in
+    // "default mode" by default.
+    cmd.env_remove("WASMTIME_NEW_CLI");
+
     Ok(cmd)
 }
 
@@ -184,7 +188,7 @@ fn run_wasmtime_unreachable_wat() -> Result<()> {
 #[test]
 fn hello_wasi_snapshot0() -> Result<()> {
     let wasm = build_wasm("tests/all/cli_tests/hello_wasi_snapshot0.wat")?;
-    let stdout = run_wasmtime(&["-Ccache=n", wasm.path().to_str().unwrap()])?;
+    let stdout = run_wasmtime(&["-Ccache=n", "-Spreview2=n", wasm.path().to_str().unwrap()])?;
     assert_eq!(stdout, "Hello, world!\n");
     Ok(())
 }
@@ -248,7 +252,10 @@ fn timeout_in_invoke() -> Result<()> {
 #[test]
 fn exit2_wasi_snapshot0() -> Result<()> {
     let wasm = build_wasm("tests/all/cli_tests/exit2_wasi_snapshot0.wat")?;
-    let output = run_wasmtime_for_output(&["-Ccache=n", wasm.path().to_str().unwrap()], None)?;
+    let output = run_wasmtime_for_output(
+        &["-Ccache=n", "-Spreview2=n", wasm.path().to_str().unwrap()],
+        None,
+    )?;
     assert_eq!(output.status.code().unwrap(), 2);
     Ok(())
 }
@@ -266,7 +273,10 @@ fn exit2_wasi_snapshot1() -> Result<()> {
 #[test]
 fn exit125_wasi_snapshot0() -> Result<()> {
     let wasm = build_wasm("tests/all/cli_tests/exit125_wasi_snapshot0.wat")?;
-    let output = run_wasmtime_for_output(&["-Ccache=n", wasm.path().to_str().unwrap()], None)?;
+    let output = run_wasmtime_for_output(
+        &["-Ccache=n", "-Spreview2=n", wasm.path().to_str().unwrap()],
+        None,
+    )?;
     if cfg!(windows) {
         assert_eq!(output.status.code().unwrap(), 1);
     } else {
@@ -292,7 +302,10 @@ fn exit125_wasi_snapshot1() -> Result<()> {
 #[test]
 fn exit126_wasi_snapshot0() -> Result<()> {
     let wasm = build_wasm("tests/all/cli_tests/exit126_wasi_snapshot0.wat")?;
-    let output = run_wasmtime_for_output(&["-Ccache=n", wasm.path().to_str().unwrap()], None)?;
+    let output = run_wasmtime_for_output(
+        &["-Ccache=n", "-Spreview2=n", wasm.path().to_str().unwrap()],
+        None,
+    )?;
     assert_eq!(output.status.code().unwrap(), 1);
     assert!(output.stdout.is_empty());
     assert!(String::from_utf8_lossy(&output.stderr).contains("invalid exit status"));
@@ -439,7 +452,7 @@ fn hello_wasi_snapshot0_from_stdin() -> Result<()> {
     let wasm = build_wasm("tests/all/cli_tests/hello_wasi_snapshot0.wat")?;
     let stdout = {
         let path = wasm.path();
-        let args: &[&str] = &["-Ccache=n", "-"];
+        let args: &[&str] = &["-Ccache=n", "-Spreview2=n", "-"];
         let output = run_wasmtime_for_output(args, Some(path))?;
         if !output.status.success() {
             bail!(
@@ -589,6 +602,7 @@ fn wasm_flags() -> Result<()> {
     // command itself
     let stdout = run_wasmtime(&[
         "run",
+        "--",
         "tests/all/cli_tests/print-arguments.wat",
         "--argument",
         "-for",
@@ -605,7 +619,7 @@ fn wasm_flags() -> Result<()> {
             command\n\
         "
     );
-    let stdout = run_wasmtime(&["run", "tests/all/cli_tests/print-arguments.wat", "-"])?;
+    let stdout = run_wasmtime(&["run", "--", "tests/all/cli_tests/print-arguments.wat", "-"])?;
     assert_eq!(
         stdout,
         "\
@@ -613,7 +627,7 @@ fn wasm_flags() -> Result<()> {
             -\n\
         "
     );
-    let stdout = run_wasmtime(&["run", "tests/all/cli_tests/print-arguments.wat", "--"])?;
+    let stdout = run_wasmtime(&["run", "--", "tests/all/cli_tests/print-arguments.wat", "--"])?;
     assert_eq!(
         stdout,
         "\
@@ -623,6 +637,7 @@ fn wasm_flags() -> Result<()> {
     );
     let stdout = run_wasmtime(&[
         "run",
+        "--",
         "tests/all/cli_tests/print-arguments.wat",
         "--",
         "--",
@@ -998,6 +1013,156 @@ fn preview2_stdin() -> Result<()> {
     Ok(())
 }
 
+#[test]
+fn old_cli_warn_if_ambiguous_flags() -> Result<()> {
+    // This is accepted in the old CLI parser and the new but it's interpreted
+    // differently so a warning should be printed.
+    let output = get_wasmtime_command()?
+        .args(&["tests/all/cli_tests/simple.wat", "--invoke", "get_f32"])
+        .output()?;
+    assert_eq!(String::from_utf8_lossy(&output.stdout), "100\n");
+    assert_eq!(
+        String::from_utf8_lossy(&output.stderr),
+        "\
+warning: this CLI invocation of Wasmtime will be parsed differently in future
+         Wasmtime versions -- see this online issue for more information:
+         https://github.com/bytecodealliance/wasmtime/issues/7384
+
+         Wasmtime will now execute with the old (<= Wasmtime 13) CLI parsing,
+         however this behavior can also be temporarily configured with an
+         environment variable:
+
+         - WASMTIME_NEW_CLI=0 to indicate old semantics are desired and silence this warning, or
+         - WASMTIME_NEW_CLI=1 to indicate new semantics are desired and use the latest behavior
+warning: using `--invoke` with a function that returns values is experimental and may break in the future
+"
+    );
+
+    // Test disabling the warning
+    let output = get_wasmtime_command()?
+        .args(&["tests/all/cli_tests/simple.wat", "--invoke", "get_f32"])
+        .env("WASMTIME_NEW_CLI", "0")
+        .output()?;
+    assert_eq!(String::from_utf8_lossy(&output.stdout), "100\n");
+    assert_eq!(
+        String::from_utf8_lossy(&output.stderr),
+        "\
+warning: using `--invoke` with a function that returns values is experimental and may break in the future
+"
+    );
+
+    // Test forcing the new behavior where nothing happens because the file is
+    // invoked with `--invoke` as its own argument.
+    let output = get_wasmtime_command()?
+        .args(&["tests/all/cli_tests/simple.wat", "--invoke", "get_f32"])
+        .env("WASMTIME_NEW_CLI", "1")
+        .output()?;
+    assert_eq!(String::from_utf8_lossy(&output.stdout), "");
+    assert_eq!(String::from_utf8_lossy(&output.stderr), "");
+
+    // This is unambiguous
+    let output = get_wasmtime_command()?
+        .args(&["--invoke", "get_f32", "tests/all/cli_tests/simple.wat"])
+        .output()?;
+    assert_eq!(String::from_utf8_lossy(&output.stdout), "100\n");
+    assert_eq!(
+        String::from_utf8_lossy(&output.stderr),
+        "\
+warning: using `--invoke` with a function that returns values is experimental and may break in the future
+"
+    );
+
+    // This fails to parse in the old but succeeds in the new, so it should run
+    // under the new semantics with no warning.
+    let output = get_wasmtime_command()?
+        .args(&["run", "tests/all/cli_tests/print-arguments.wat", "--arg"])
+        .output()?;
+    assert_eq!(
+        String::from_utf8_lossy(&output.stdout),
+        "print-arguments.wat\n--arg\n"
+    );
+    assert_eq!(String::from_utf8_lossy(&output.stderr), "");
+
+    // Old behavior can be forced however
+    let output = get_wasmtime_command()?
+        .args(&["run", "tests/all/cli_tests/print-arguments.wat", "--arg"])
+        .env("WASMTIME_NEW_CLI", "0")
+        .output()?;
+    assert!(!output.status.success());
+
+    // This works in both the old and the new, so no warnings
+    let output = get_wasmtime_command()?
+        .args(&["run", "tests/all/cli_tests/print-arguments.wat", "arg"])
+        .output()?;
+    assert_eq!(
+        String::from_utf8_lossy(&output.stdout),
+        "print-arguments.wat\narg\n"
+    );
+    assert_eq!(String::from_utf8_lossy(&output.stderr), "");
+
+    // This works in both the old and the new, so no warnings
+    let output = get_wasmtime_command()?
+        .args(&[
+            "run",
+            "--",
+            "tests/all/cli_tests/print-arguments.wat",
+            "--arg",
+        ])
+        .output()?;
+    assert_eq!(
+        String::from_utf8_lossy(&output.stdout),
+        "print-arguments.wat\n--arg\n"
+    );
+    assert_eq!(String::from_utf8_lossy(&output.stderr), "");
+
+    // Old flags still work, but with a warning
+    let output = get_wasmtime_command()?
+        .args(&[
+            "run",
+            "--max-wasm-stack",
+            "1000000",
+            "tests/all/cli_tests/print-arguments.wat",
+        ])
+        .output()?;
+    assert_eq!(
+        String::from_utf8_lossy(&output.stdout),
+        "print-arguments.wat\n"
+    );
+    assert_eq!(
+        String::from_utf8_lossy(&output.stderr),
+        "\
+warning: this CLI invocation of Wasmtime is going to break in the future -- for
+         more information see this issue online:
+         https://github.com/bytecodealliance/wasmtime/issues/7384
+
+         Wasmtime will now execute with the old (<= Wasmtime 13) CLI parsing,
+         however this behavior can also be temporarily configured with an
+         environment variable:
+
+         - WASMTIME_NEW_CLI=0 to indicate old semantics are desired and silence this warning, or
+         - WASMTIME_NEW_CLI=1 to indicate new semantics are desired and see the error
+"
+    );
+
+    // Old flags warning is suppressible.
+    let output = get_wasmtime_command()?
+        .args(&[
+            "run",
+            "--max-wasm-stack",
+            "1000000",
+            "tests/all/cli_tests/print-arguments.wat",
+        ])
+        .env("WASMTIME_NEW_CLI", "0")
+        .output()?;
+    assert_eq!(
+        String::from_utf8_lossy(&output.stdout),
+        "print-arguments.wat\n"
+    );
+    assert_eq!(String::from_utf8_lossy(&output.stderr), "");
+
+    Ok(())
+}
+
 mod test_programs {
     use super::{get_wasmtime_command, run_wasmtime};
     use anyhow::Result;