Repository: bytecodealliance/wasmtime
Pull request #8532 (2024-05-03 00:44:06)

Use rustdoc's `doc_auto_cfg` feature instead of `doc_cfg`
This commit removes all our `#[cfg_attr(..., doc(cfg(...)))]` annotations throughout Wasmtime and `wasmtime-wasi`. These are all replaced with `feature(doc_auto_cfg)` which automatically infers the attribute to show rather than requiring us to duplicate it. Spot-checking the docs this looks just-as-readable while being much easier to maintain over time.

<!--
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
-->


github-actions[bot]: #### Label Messager: wasmtime:config

It looks like you are changing Wasmtime's configuration options. Make sure to
complete this check list:

* [ ] If you added a new `Config` method, you wrote extensive documentation for
      it.

  <details>

  Our documentation should be of the following form:

  ```text
  Short, simple summary sentence.

  More details. These details can be multiple paragraphs. There should be
  information about not just the method, but its parameters and results as
  well.

  Is this method fallible? If so, when can it return an error?

  Can this method panic? If so, when does it panic?

  # Example

  Optional example here.
  ```

  </details>

* [ ] If you added a new `Config` method, or modified an existing one, you
  ensured that this configuration is exercised by the fuzz targets.

  <details>

  For example, if you expose a new strategy for allocating the next instance
  slot inside the pooling allocator, you should ensure that at least one of our
  fuzz targets exercises that new strategy.

  Often, all that is required of you is to ensure that there is a knob for this
  configuration option in [`wasmtime_fuzzing::Config`][fuzzing-config] (or one
  of its nested `struct`s).

  Rarely, this may require authoring a new fuzz target to specifically test this
  configuration. See [our docs on fuzzing][fuzzing-docs] for more details.

  </details>

* [ ] If you are enabling a configuration option by default, make sure that it
  has been fuzzed for at least two weeks before turning it on by default.

[fuzzing-config]: https://github.com/bytecodealliance/wasmtime/blob/ca0e8d0a1d8cefc0496dba2f77a670571d8fdcab/crates/fuzzing/src/generators.rs#L182-L194
[fuzzing-docs]: https://docs.wasmtime.dev/contributing-fuzzing.html


--------------------------------------------------------------------------------

<details>

To modify this label's message, edit the <code>.github/label-messager/wasmtime-config.md</code> file.

To add new label messages or remove existing label messages, edit the
<code>.github/label-messager.json</code> configuration file.

[Learn more.](https://github.com/bytecodealliance/label-messager-action)

</details>

Patch:
diff --git a/cranelift/codegen/meta/src/isa/riscv64.rs b/cranelift/codegen/meta/src/isa/riscv64.rs
index 9a24efd91..fe4b545c0 100644
--- a/cranelift/codegen/meta/src/isa/riscv64.rs
+++ b/cranelift/codegen/meta/src/isa/riscv64.rs
@@ -56,6 +56,14 @@ pub(crate) fn define() -> TargetIsa {
         "Double-precision floating point",
         true,
     );
+
+    let _has_zfa = setting.add_bool(
+        "has_zfa",
+        "has extension Zfa?",
+        "Zfa: Extension for Additional Floating-Point Instructions",
+        false,
+    );
+
     let _has_v = setting.add_bool(
         "has_v",
         "has extension V?",
diff --git a/cranelift/codegen/src/isa/riscv64/inst.isle b/cranelift/codegen/src/isa/riscv64/inst.isle
index b77e66922..8a4d37959 100644
--- a/cranelift/codegen/src/isa/riscv64/inst.isle
+++ b/cranelift/codegen/src/isa/riscv64/inst.isle
@@ -597,6 +597,12 @@
   (FeqD)
   (FltD)
   (FleD)
+
+  ;; Zfa Extension
+  (FminmS)
+  (FmaxmS)
+  (FminmD)
+  (FmaxmD)
 ))
 
 
@@ -1035,6 +1041,9 @@
 (decl pure has_v () bool)
 (extern constructor has_v has_v)
 
+(decl pure has_zfa () bool)
+(extern constructor has_zfa has_zfa)
+
 (decl pure has_zbkb () bool)
 (extern constructor has_zbkb has_zbkb)
 
@@ -1554,6 +1563,17 @@
 (rule (rv_fmax $F32 rs1 rs2) (fpu_rrr (FpuOPRRR.FmaxS) $F32 (FRM.RTZ) rs1 rs2))
 (rule (rv_fmax $F64 rs1 rs2) (fpu_rrr (FpuOPRRR.FmaxD) $F64 (FRM.RTZ) rs1 rs2))
 
+;; `Zfa` Extension Instructions
+
+;; Helper for emitting the `fminm` instruction.
+(decl rv_fminm (Type FReg FReg) FReg)
+(rule (rv_fminm $F32 rs1 rs2) (fpu_rrr (FpuOPRRR.FminmS) $F32 (FRM.RDN) rs1 rs2))
+(rule (rv_fminm $F64 rs1 rs2) (fpu_rrr (FpuOPRRR.FminmD) $F64 (FRM.RDN) rs1 rs2))
+
+;; Helper for emitting the `fmaxm` instruction.
+(decl rv_fmaxm (Type FReg FReg) FReg)
+(rule (rv_fmaxm $F32 rs1 rs2) (fpu_rrr (FpuOPRRR.FmaxmS) $F32 (FRM.RUP) rs1 rs2))
+(rule (rv_fmaxm $F64 rs1 rs2) (fpu_rrr (FpuOPRRR.FmaxmD) $F64 (FRM.RUP) rs1 rs2))
 
 ;; `Zba` Extension Instructions
 
diff --git a/cranelift/codegen/src/isa/riscv64/inst/args.rs b/cranelift/codegen/src/isa/riscv64/inst/args.rs
index 4a0487889..ffa10121c 100644
--- a/cranelift/codegen/src/isa/riscv64/inst/args.rs
+++ b/cranelift/codegen/src/isa/riscv64/inst/args.rs
@@ -575,6 +575,10 @@ impl FpuOPRRR {
             Self::FeqD => "feq.d",
             Self::FltD => "flt.d",
             Self::FleD => "fle.d",
+            Self::FminmS => "fminm.s",
+            Self::FmaxmS => "fmaxm.s",
+            Self::FminmD => "fminm.d",
+            Self::FmaxmD => "fmaxm.d",
         }
     }
 
@@ -591,7 +595,9 @@ impl FpuOPRRR {
             | Self::FmaxS
             | Self::FeqS
             | Self::FltS
-            | Self::FleS => 0b1010011,
+            | Self::FleS
+            | Self::FminmS
+            | Self::FmaxmS => 0b1010011,
 
             Self::FaddD
             | Self::FsubD
@@ -604,7 +610,9 @@ impl FpuOPRRR {
             | Self::FmaxD
             | Self::FeqD
             | Self::FltD
-            | Self::FleD => 0b1010011,
+            | Self::FleD
+            | Self::FminmD
+            | Self::FmaxmD => 0b1010011,
         }
     }
 
@@ -636,6 +644,11 @@ impl FpuOPRRR {
             Self::FeqD => 0b1010001,
             Self::FltD => 0b1010001,
             Self::FleD => 0b1010001,
+
+            Self::FminmS => 0b0010100,
+            Self::FmaxmS => 0b0010100,
+            Self::FminmD => 0b0010101,
+            Self::FmaxmD => 0b0010101,
         }
     }
     pub fn is_32(self) -> bool {
diff --git a/cranelift/codegen/src/isa/riscv64/lower.isle b/cranelift/codegen/src/isa/riscv64/lower.isle
index a0abd498d..bb8b14fc3 100644
--- a/cranelift/codegen/src/isa/riscv64/lower.isle
+++ b/cranelift/codegen/src/isa/riscv64/lower.isle
@@ -1744,13 +1744,19 @@
         (min FReg (rv_fmin ty x y)))
     (gen_select_freg is_ordered min nan)))
 
+;; With Zfa we can use the special `fminm` that precisely matches the expected
+;; NaN behavior.
+(rule 1 (lower (has_type (ty_scalar_float ty) (fmin x y)))
+  (if-let $true (has_zfa))
+  (rv_fminm ty x y))
+
 ;; vfmin does almost the right thing, but it does not handle NaN's correctly.
 ;; We should return a NaN if any of the inputs is a NaN, but vfmin returns the
 ;; number input instead.
 ;;
 ;; TODO: We can improve this by using a masked `fmin` instruction that modifies
 ;; the canonical nan register. That way we could avoid the `vmerge.vv` instruction.
-(rule 1 (lower (has_type (ty_vec_fits_in_register ty) (fmin x y)))
+(rule 2 (lower (has_type (ty_vec_fits_in_register ty) (fmin x y)))
   (let ((is_not_nan VReg (gen_fcmp_mask ty (FloatCC.Ordered) x y))
         (nan XReg (imm $I64 (canonical_nan_u64 (lane_type ty))))
         (vec_nan VReg (rv_vmv_vx nan ty))
@@ -1770,6 +1776,11 @@
         (max FReg (rv_fmax ty x y)))
     (gen_select_freg is_ordered max nan)))
 
+;; With Zfa we can use the special `fmaxm` that precisely matches the expected
+;; NaN behavior.
+(rule 1 (lower (has_type (ty_scalar_float ty) (fmax x y)))
+  (if-let $true (has_zfa))
+  (rv_fmaxm ty x y))
 
 ;; vfmax does almost the right thing, but it does not handle NaN's correctly.
 ;; We should return a NaN if any of the inputs is a NaN, but vfmax returns the
@@ -1777,7 +1788,7 @@
 ;;
 ;; TODO: We can improve this by using a masked `fmax` instruction that modifies
 ;; the canonical nan register. That way we could avoid the `vmerge.vv` instruction.
-(rule 1 (lower (has_type (ty_vec_fits_in_register ty) (fmax x y)))
+(rule 2 (lower (has_type (ty_vec_fits_in_register ty) (fmax x y)))
   (let ((is_not_nan VReg (gen_fcmp_mask ty (FloatCC.Ordered) x y))
         (nan XReg (imm $I64 (canonical_nan_u64 (lane_type ty))))
         (vec_nan VReg (rv_vmv_vx nan ty))
diff --git a/cranelift/codegen/src/isa/riscv64/lower/isle.rs b/cranelift/codegen/src/isa/riscv64/lower/isle.rs
index 3bdd6f0a8..64f32bd47 100644
--- a/cranelift/codegen/src/isa/riscv64/lower/isle.rs
+++ b/cranelift/codegen/src/isa/riscv64/lower/isle.rs
@@ -361,6 +361,10 @@ impl generated_code::Context for RV64IsleContext<'_, '_, MInst, Riscv64Backend>
         self.backend.isa_flags.has_m()
     }
 
+    fn has_zfa(&mut self) -> bool {
+        self.backend.isa_flags.has_zfa()
+    }
+
     fn has_zbkb(&mut self) -> bool {
         self.backend.isa_flags.has_zbkb()
     }
diff --git a/crates/cranelift/src/func_environ.rs b/crates/cranelift/src/func_environ.rs
index 7d11944bc..e98b412b7 100644
--- a/crates/cranelift/src/func_environ.rs
+++ b/crates/cranelift/src/func_environ.rs
@@ -1461,7 +1461,7 @@ impl<'a, 'func, 'module_env> Call<'a, 'func, 'module_env> {
                 return CheckIndirectCallTypeSignature::StaticTrap;
             }
 
-            // Engine-indexed types don't show up until runtime and it's a wasm
+            // Engine-indexed types don't show up until runtime and it's a Wasm
             // validation error to perform a call through a non-function table,
             // so these cases are dynamically not reachable.
             WasmHeapType::ConcreteFunc(EngineOrModuleTypeIndex::Engine(_))
@@ -1471,6 +1471,8 @@ impl<'a, 'func, 'module_env> Call<'a, 'func, 'module_env> {
             | WasmHeapType::I31
             | WasmHeapType::Array
             | WasmHeapType::ConcreteArray(_)
+            | WasmHeapType::Struct
+            | WasmHeapType::ConcreteStruct(_)
             | WasmHeapType::None => {
                 unreachable!()
             }
diff --git a/crates/environ/src/compile/module_environ.rs b/crates/environ/src/compile/module_environ.rs
index 518e02c70..d8f8368f4 100644
--- a/crates/environ/src/compile/module_environ.rs
+++ b/crates/environ/src/compile/module_environ.rs
@@ -22,7 +22,7 @@ use wasmparser::{
     FuncToValidate, FunctionBody, NameSectionReader, Naming, Operator, Parser, Payload, TypeRef,
     Validator, ValidatorResources, WasmFeatures,
 };
-use wasmtime_types::{ConstExpr, ConstOp, ModuleInternedTypeIndex};
+use wasmtime_types::{ConstExpr, ConstOp, ModuleInternedTypeIndex, WasmHeapTopType};
 
 /// Object containing the standalone environment information.
 pub struct ModuleEnvironment<'a, 'data> {
@@ -1268,19 +1268,15 @@ impl ModuleTranslation<'_> {
                 .table
                 .wasm_ty
                 .heap_type
+                .top()
             {
-                WasmHeapType::Func | WasmHeapType::ConcreteFunc(_) | WasmHeapType::NoFunc => {}
+                WasmHeapTopType::Func => {}
                 // If this is not a funcref table, then we can't support a
                 // pre-computed table of function indices. Technically this
                 // initializer won't trap so we could continue processing
                 // segments, but that's left as a future optimization if
                 // necessary.
-                WasmHeapType::Extern
-                | WasmHeapType::Any
-                | WasmHeapType::I31
-                | WasmHeapType::Array
-                | WasmHeapType::ConcreteArray(_)
-                | WasmHeapType::None => break,
+                WasmHeapTopType::Any | WasmHeapTopType::Extern => break,
             }
 
             // Function indices can be optimized here, but fully general
diff --git a/crates/environ/src/compile/module_types.rs b/crates/environ/src/compile/module_types.rs
index 009adcd65..25e3fc779 100644
--- a/crates/environ/src/compile/module_types.rs
+++ b/crates/environ/src/compile/module_types.rs
@@ -311,12 +311,13 @@ impl TypeConvert for WasmparserTypeConverter<'_> {
                     match &ty.composite_type {
                         WasmCompositeType::Array(_) => WasmHeapType::ConcreteArray(index),
                         WasmCompositeType::Func(_) => WasmHeapType::ConcreteFunc(index),
+                        WasmCompositeType::Struct(_) => WasmHeapType::ConcreteStruct(index),
                     }
                 } else if let Some((wasmparser_types, _)) = self.rec_group_context.as_ref() {
                     match &wasmparser_types[id].composite_type {
                         wasmparser::CompositeType::Array(_) => WasmHeapType::ConcreteArray(index),
                         wasmparser::CompositeType::Func(_) => WasmHeapType::ConcreteFunc(index),
-                        wasmparser::CompositeType::Struct(_) => unreachable!(),
+                        wasmparser::CompositeType::Struct(_) => WasmHeapType::ConcreteStruct(index),
                     }
                 } else {
                     panic!("forward reference to type outside of rec group?")
@@ -337,6 +338,7 @@ impl TypeConvert for WasmparserTypeConverter<'_> {
                     match &ty.composite_type {
                         WasmCompositeType::Array(_) => WasmHeapType::ConcreteArray(index),
                         WasmCompositeType::Func(_) => WasmHeapType::ConcreteFunc(index),
+                        WasmCompositeType::Struct(_) => WasmHeapType::ConcreteStruct(index),
                     }
                 } else if let Some((parser_types, rec_group)) = self.rec_group_context.as_ref() {
                     let rec_group_index = interned.index() - self.types.types.len_types();
@@ -347,7 +349,7 @@ impl TypeConvert for WasmparserTypeConverter<'_> {
                     match &parser_types[id].composite_type {
                         wasmparser::CompositeType::Array(_) => WasmHeapType::ConcreteArray(index),
                         wasmparser::CompositeType::Func(_) => WasmHeapType::ConcreteFunc(index),
-                        wasmparser::CompositeType::Struct(_) => unreachable!(),
+                        wasmparser::CompositeType::Struct(_) => WasmHeapType::ConcreteStruct(index),
                     }
                 } else {
                     panic!("forward reference to type outside of rec group?")
diff --git a/crates/environ/src/component/types_builder.rs b/crates/environ/src/component/types_builder.rs
index ad91d4be7..15cfc2b44 100644
--- a/crates/environ/src/component/types_builder.rs
+++ b/crates/environ/src/component/types_builder.rs
@@ -173,13 +173,12 @@ impl ComponentTypesBuilder {
     pub fn find_resource_drop_signature(&self) -> Option<ModuleInternedTypeIndex> {
         self.module_types
             .wasm_types()
-            .find(|(_, ty)| match &ty.composite_type {
-                wasmtime_types::WasmCompositeType::Array(_) => false,
-                wasmtime_types::WasmCompositeType::Func(sig) => {
+            .find(|(_, ty)| {
+                ty.as_func().map_or(false, |sig| {
                     sig.params().len() == 1
                         && sig.returns().len() == 0
                         && sig.params()[0] == WasmValType::I32
-                }
+                })
             })
             .map(|(i, _)| i)
     }
diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs
index 045442ac2..cb3d83a43 100644
--- a/crates/types/src/lib.rs
+++ b/crates/types/src/lib.rs
@@ -388,16 +388,21 @@ impl EngineOrModuleTypeIndex {
 /// WebAssembly heap type -- equivalent of `wasmparser`'s HeapType
 #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
 pub enum WasmHeapType {
+    // External types.
     Extern,
 
+    // Function types.
     Func,
     ConcreteFunc(EngineOrModuleTypeIndex),
     NoFunc,
 
+    // Internal types.
     Any,
     I31,
     Array,
     ConcreteArray(EngineOrModuleTypeIndex),
+    Struct,
+    ConcreteStruct(EngineOrModuleTypeIndex),
     None,
 }
 
@@ -423,6 +428,8 @@ impl fmt::Display for WasmHeapType {
             Self::I31 => write!(f, "i31"),
             Self::Array => write!(f, "array"),
             Self::ConcreteArray(i) => write!(f, "array {i}"),
+            Self::Struct => write!(f, "struct"),
+            Self::ConcreteStruct(i) => write!(f, "struct {i}"),
             Self::None => write!(f, "none"),
         }
     }
@@ -436,6 +443,7 @@ impl TypeTrace for WasmHeapType {
         match *self {
             Self::ConcreteArray(i) => func(i),
             Self::ConcreteFunc(i) => func(i),
+            Self::ConcreteStruct(i) => func(i),
             _ => Ok(()),
         }
     }
@@ -447,6 +455,7 @@ impl TypeTrace for WasmHeapType {
         match self {
             Self::ConcreteArray(i) => func(i),
             Self::ConcreteFunc(i) => func(i),
+            Self::ConcreteStruct(i) => func(i),
             _ => Ok(()),
         }
     }
@@ -490,6 +499,8 @@ impl WasmHeapType {
             | WasmHeapType::I31
             | WasmHeapType::Array
             | WasmHeapType::ConcreteArray(_)
+            | WasmHeapType::Struct
+            | WasmHeapType::ConcreteStruct(_)
             | WasmHeapType::None => WasmHeapTopType::Any,
         }
     }
@@ -676,16 +687,61 @@ impl TypeTrace for WasmArrayType {
     }
 }
 
+/// A concrete struct type.
+#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
+pub struct WasmStructType {
+    pub fields: Box<[WasmFieldType]>,
+}
+
+impl TypeTrace for WasmStructType {
+    fn trace<F, E>(&self, func: &mut F) -> Result<(), E>
+    where
+        F: FnMut(EngineOrModuleTypeIndex) -> Result<(), E>,
+    {
+        for f in self.fields.iter() {
+            f.trace(func)?;
+        }
+        Ok(())
+    }
+
+    fn trace_mut<F, E>(&mut self, func: &mut F) -> Result<(), E>
+    where
+        F: FnMut(&mut EngineOrModuleTypeIndex) -> Result<(), E>,
+    {
+        for f in self.fields.iter_mut() {
+            f.trace_mut(func)?;
+        }
+        Ok(())
+    }
+}
+
 /// A function, array, or struct type.
 #[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
 pub enum WasmCompositeType {
     Array(WasmArrayType),
     Func(WasmFuncType),
-    //
-    // TODO: struct types.
+    Struct(WasmStructType),
 }
 
 impl WasmCompositeType {
+    #[inline]
+    pub fn is_array(&self) -> bool {
+        matches!(self, Self::Array(_))
+    }
+
+    #[inline]
+    pub fn as_array(&self) -> Option<&WasmArrayType> {
+        match self {
+            WasmCompositeType::Array(f) => Some(f),
+            _ => None,
+        }
+    }
+
+    #[inline]
+    pub fn unwrap_array(&self) -> &WasmArrayType {
+        self.as_array().unwrap()
+    }
+
     #[inline]
     pub fn is_func(&self) -> bool {
         matches!(self, Self::Func(_))
@@ -705,21 +761,21 @@ impl WasmCompositeType {
     }
 
     #[inline]
-    pub fn is_array(&self) -> bool {
-        matches!(self, Self::Array(_))
+    pub fn is_struct(&self) -> bool {
+        matches!(self, Self::Struct(_))
     }
 
     #[inline]
-    pub fn as_array(&self) -> Option<&WasmArrayType> {
+    pub fn as_struct(&self) -> Option<&WasmStructType> {
         match self {
-            WasmCompositeType::Array(f) => Some(f),
+            WasmCompositeType::Struct(f) => Some(f),
             _ => None,
         }
     }
 
     #[inline]
-    pub fn unwrap_array(&self) -> &WasmArrayType {
-        self.as_array().unwrap()
+    pub fn unwrap_struct(&self) -> &WasmStructType {
+        self.as_struct().unwrap()
     }
 }
 
@@ -731,6 +787,7 @@ impl TypeTrace for WasmCompositeType {
         match self {
             WasmCompositeType::Array(a) => a.trace(func),
             WasmCompositeType::Func(f) => f.trace(func),
+            WasmCompositeType::Struct(a) => a.trace(func),
         }
     }
 
@@ -741,6 +798,7 @@ impl TypeTrace for WasmCompositeType {
         match self {
             WasmCompositeType::Array(a) => a.trace_mut(func),
             WasmCompositeType::Func(f) => f.trace_mut(func),
+            WasmCompositeType::Struct(a) => a.trace_mut(func),
         }
     }
 }
@@ -784,6 +842,21 @@ impl WasmSubType {
     pub fn unwrap_array(&self) -> &WasmArrayType {
         self.composite_type.unwrap_array()
     }
+
+    #[inline]
+    pub fn is_struct(&self) -> bool {
+        self.composite_type.is_struct()
+    }
+
+    #[inline]
+    pub fn as_struct(&self) -> Option<&WasmStructType> {
+        self.composite_type.as_struct()
+    }
+
+    #[inline]
+    pub fn unwrap_struct(&self) -> &WasmStructType {
+        self.composite_type.unwrap_struct()
+    }
 }
 
 impl TypeTrace for WasmSubType {
@@ -1394,7 +1467,19 @@ pub trait TypeConvert {
             wasmparser::CompositeType::Array(a) => {
                 Ok(WasmCompositeType::Array(self.convert_array_type(a)))
             }
-            wasmparser::CompositeType::Struct(_) => Err(wasm_unsupported!("wasm gc: struct types")),
+            wasmparser::CompositeType::Struct(s) => {
+                Ok(WasmCompositeType::Struct(self.convert_struct_type(s)))
+            }
+        }
+    }
+
+    fn convert_struct_type(&self, ty: &wasmparser::StructType) -> WasmStructType {
+        WasmStructType {
+            fields: ty
+                .fields
+                .iter()
+                .map(|f| self.convert_field_type(f))
+                .collect(),
         }
     }
 
@@ -1462,13 +1547,13 @@ pub trait TypeConvert {
             wasmparser::HeapType::Any => WasmHeapType::Any,
             wasmparser::HeapType::I31 => WasmHeapType::I31,
             wasmparser::HeapType::Array => WasmHeapType::Array,
+            wasmparser::HeapType::Struct => WasmHeapType::Struct,
             wasmparser::HeapType::None => WasmHeapType::None,
 
             wasmparser::HeapType::Exn
             | wasmparser::HeapType::NoExn
             | wasmparser::HeapType::NoExtern
-            | wasmparser::HeapType::Eq
-            | wasmparser::HeapType::Struct => {
+            | wasmparser::HeapType::Eq => {
                 unimplemented!("unsupported heap type {ty:?}");
             }
         }
diff --git a/crates/wasi/src/ctx.rs b/crates/wasi/src/ctx.rs
index f2007f2bd..8959bc8bb 100644
--- a/crates/wasi/src/ctx.rs
+++ b/crates/wasi/src/ctx.rs
@@ -515,7 +515,6 @@ impl WasiCtxBuilder {
     /// usage of this method is not allowed and should use a second builder
     /// instead.
     #[cfg(feature = "preview1")]
-    #[cfg_attr(docsrs, doc(cfg(feature = "preview1")))]
     pub fn build_p1(&mut self) -> crate::preview1::WasiP1Ctx {
         let wasi = self.build();
         crate::preview1::WasiP1Ctx::new(wasi)
diff --git a/crates/wasi/src/lib.rs b/crates/wasi/src/lib.rs
index fd2449398..0bc65d724 100644
--- a/crates/wasi/src/lib.rs
+++ b/crates/wasi/src/lib.rs
@@ -178,7 +178,7 @@
 //! [`wasi:sockets/udp`]: bindings::sockets::udp::Host
 //! [async]: https://docs.rs/wasmtime/latest/wasmtime/struct.Config.html#method.async_support
 
-#![cfg_attr(docsrs, feature(doc_cfg))]
+#![cfg_attr(docsrs, feature(doc_auto_cfg))]
 
 use wasmtime::component::Linker;
 
diff --git a/crates/wasi/src/preview0.rs b/crates/wasi/src/preview0.rs
index c1992cb5a..995a88691 100644
--- a/crates/wasi/src/preview0.rs
+++ b/crates/wasi/src/preview0.rs
@@ -3,8 +3,6 @@
 //! This module is purely here for backwards compatibility in the Wasmtime CLI.
 //! You probably want to use [`preview1`](crate::preview1) instead.
 
-#![cfg_attr(docsrs, doc(cfg(feature = "preview1")))]
-
 use crate::preview0::types::Error;
 use crate::preview1::types as snapshot1_types;
 use crate::preview1::wasi_snapshot_preview1::WasiSnapshotPreview1 as Snapshot1;
diff --git a/crates/wasi/src/preview1.rs b/crates/wasi/src/preview1.rs
index 57fd0ebf5..36075e8a4 100644
--- a/crates/wasi/src/preview1.rs
+++ b/crates/wasi/src/preview1.rs
@@ -63,8 +63,6 @@
 //! }
 //! ```
 
-#![cfg_attr(docsrs, doc(cfg(feature = "preview1")))]
-
 use crate::bindings::{
     cli::{
         stderr, stdin, stdout, terminal_input, terminal_output, terminal_stderr, terminal_stdin,
diff --git a/crates/wasmtime/src/compile/code_builder.rs b/crates/wasmtime/src/compile/code_builder.rs
index 58afd663f..6d4d421b3 100644
--- a/crates/wasmtime/src/compile/code_builder.rs
+++ b/crates/wasmtime/src/compile/code_builder.rs
@@ -239,7 +239,6 @@ impl<'a> CodeBuilder<'a> {
     /// compiles a serialized [`Component`](crate::component::Component)
     /// instead of a module.
     #[cfg(feature = "component-model")]
-    #[cfg_attr(docsrs, doc(cfg(feature = "component-model")))]
     pub fn compile_component_serialized(&self) -> Result<Vec<u8>> {
         let bytes = self.wasm_binary()?;
         let (v, _) = super::build_component_artifacts(self.engine, &bytes, None)?;
diff --git a/crates/wasmtime/src/compile/runtime.rs b/crates/wasmtime/src/compile/runtime.rs
index cd7d1c286..0aa974da9 100644
--- a/crates/wasmtime/src/compile/runtime.rs
+++ b/crates/wasmtime/src/compile/runtime.rs
@@ -78,7 +78,6 @@ impl<'a> CodeBuilder<'a> {
     ///
     /// Note that this method will cache compilations if the `cache` feature is
     /// enabled and turned on in [`Config`](crate::Config).
-    #[cfg_attr(docsrs, doc(cfg(feature = "runtime")))]
     pub fn compile_module(&self) -> Result<Module> {
         let (code, info_and_types) = self.compile_cached(super::build_artifacts)?;
         Module::from_parts(self.engine, code, info_and_types)
@@ -87,10 +86,6 @@ impl<'a> CodeBuilder<'a> {
     /// Same as [`CodeBuilder::compile_module`] except that it compiles a
     /// [`Component`] instead of a module.
     #[cfg(feature = "component-model")]
-    #[cfg_attr(
-        docsrs,
-        doc(cfg(all(feature = "runtime", feature = "component-model")))
-    )]
     pub fn compile_component(&self) -> Result<Component> {
         let (code, artifacts) = self.compile_cached(super::build_component_artifacts)?;
         Component::from_parts(self.engine, code, artifacts)
diff --git a/crates/wasmtime/src/config.rs b/crates/wasmtime/src/config.rs
index e29d291cd..e063c2650 100644
--- a/crates/wasmtime/src/config.rs
+++ b/crates/wasmtime/src/config.rs
@@ -303,7 +303,6 @@ impl Config {
     ///
     /// This method will error if the given target triple is not supported.
     #[cfg(any(feature = "cranelift", feature = "winch"))]
-    #[cfg_attr(docsrs, doc(cfg(any(feature = "cranelift", feature = "winch"))))]
     pub fn target(&mut self, target: &str) -> Result<&mut Self> {
         self.compiler_config.target =
             Some(target_lexicon::Triple::from_str(target).map_err(|e| anyhow::anyhow!(e))?);
@@ -414,7 +413,6 @@ impl Config {
     /// it. If Wasmtime doesn't support exactly what you'd like just yet, please
     /// feel free to open an issue!
     #[cfg(feature = "async")]
-    #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
     pub fn async_support(&mut self, enable: bool) -> &mut Self {
         self.async_support = enable;
         self
@@ -691,7 +689,6 @@ impl Config {
     /// The `Engine::new` method will fail if the value for this option is
     /// smaller than the [`Config::max_wasm_stack`] option.
     #[cfg(feature = "async")]
-    #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
     pub fn async_stack_size(&mut self, size: usize) -> &mut Self {
         self.async_stack_size = size;
         self
@@ -732,7 +729,6 @@ impl Config {
     /// [threads]: https://github.com/webassembly/threads
     /// [wasi-threads]: https://github.com/webassembly/wasi-threads
     #[cfg(feature = "threads")]
-    #[cfg_attr(docsrs, doc(cfg(feature = "threads")))]
     pub fn wasm_threads(&mut self, enable: bool) -> &mut Self {
         self.features.set(WasmFeatures::THREADS, enable);
         self
@@ -755,7 +751,6 @@ impl Config {
     ///
     /// [proposal]: https://github.com/webassembly/reference-types
     #[cfg(feature = "gc")]
-    #[cfg_attr(docsrs, doc(cfg(feature = "gc")))]
     pub fn wasm_reference_types(&mut self, enable: bool) -> &mut Self {
         self.features.set(WasmFeatures::REFERENCE_TYPES, enable);
         self
@@ -775,7 +770,6 @@ impl Config {
     ///
     /// [proposal]: https://github.com/WebAssembly/function-references
     #[cfg(feature = "gc")]
-    #[cfg_attr(docsrs, doc(cfg(feature = "gc")))]
     pub fn wasm_function_references(&mut self, enable: bool) -> &mut Self {
         self.features.set(WasmFeatures::FUNCTION_REFERENCES, enable);
         self
@@ -797,7 +791,6 @@ impl Config {
     ///
     /// [proposal]: https://github.com/WebAssembly/gc
     #[cfg(feature = "gc")]
-    #[cfg_attr(docsrs, doc(cfg(feature = "gc")))]
     pub fn wasm_gc(&mut self, enable: bool) -> &mut Self {
         self.features.set(WasmFeatures::GC, enable);
         self
@@ -1010,7 +1003,6 @@ impl Config {
     ///
     /// The default value for this is `Strategy::Auto`.
     #[cfg(any(feature = "cranelift", feature = "winch"))]
-    #[cfg_attr(docsrs, doc(cfg(any(feature = "cranelift", feature = "winch"))))]
     pub fn strategy(&mut self, strategy: Strategy) -> &mut Self {
         self.compiler_config.strategy = strategy;
         self
@@ -1044,7 +1036,6 @@ impl Config {
     ///
     /// The default value for this is `false`
     #[cfg(any(feature = "cranelift", feature = "winch"))]
-    #[cfg_attr(docsrs, doc(cfg(any(feature = "cranelift", feature = "winch"))))]
     pub fn cranelift_debug_verifier(&mut self, enable: bool) -> &mut Self {
         let val = if enable { "true" } else { "false" };
         self.compiler_config
@@ -1061,7 +1052,6 @@ impl Config {
     ///
     /// The default value for this is `OptLevel::None`.
     #[cfg(any(feature = "cranelift", feature = "winch"))]
-    #[cfg_attr(docsrs, doc(cfg(any(feature = "cranelift", feature = "winch"))))]
     pub fn cranelift_opt_level(&mut self, level: OptLevel) -> &mut Self {
         let val = match level {
             OptLevel::None => "none",
@@ -1088,7 +1078,6 @@ impl Config {
     ///
     /// The default value for this is `false`
     #[cfg(any(feature = "cranelift", feature = "winch"))]
-    #[cfg_attr(docsrs, doc(cfg(any(feature = "cranelift", feature = "winch"))))]
     pub fn cranelift_nan_canonicalization(&mut self, enable: bool) -> &mut Self {
         let val = if enable { "true" } else { "false" };
         self.compiler_config
@@ -1111,7 +1100,6 @@ impl Config {
     /// over a trail of "breadcrumbs" or facts at each intermediate
     /// value. Thus, it is appropriate to enable in production.
     #[cfg(any(feature = "cranelift", feature = "winch"))]
-    #[cfg_attr(docsrs, doc(cfg(any(feature = "cranelift", feature = "winch"))))]
     pub fn cranelift_pcc(&mut self, enable: bool) -> &mut Self {
         let val = if enable { "true" } else { "false" };
         self.compiler_config
@@ -1137,7 +1125,6 @@ impl Config {
     /// cause `Engine::new` fail if the flag's name does not exist, or the value is not appropriate
     /// for the flag type.
     #[cfg(any(feature = "cranelift", feature = "winch"))]
-    #[cfg_attr(docsrs, doc(cfg(any(feature = "cranelift", feature = "winch"))))]
     pub unsafe fn cranelift_flag_enable(&mut self, flag: &str) -> &mut Self {
         self.compiler_config.flags.insert(flag.to_string());
         self
@@ -1163,7 +1150,6 @@ impl Config {
     /// For example, feature `wasm_backtrace` will set `unwind_info` to `true`, but if it's
     /// manually set to false then it will fail.
     #[cfg(any(feature = "cranelift", feature = "winch"))]
-    #[cfg_attr(docsrs, doc(cfg(any(feature = "cranelift", feature = "winch"))))]
     pub unsafe fn cranelift_flag_set(&mut self, name: &str, value: &str) -> &mut Self {
         self.compiler_config
             .settings
@@ -1190,7 +1176,6 @@ impl Config {
     ///
     /// [docs]: https://bytecodealliance.github.io/wasmtime/cli-cache.html
     #[cfg(feature = "cache")]
-    #[cfg_attr(docsrs, doc(cfg(feature = "cache")))]
     pub fn cache_config_load(&mut self, path: impl AsRef<Path>) -> Result<&mut Self> {
         self.cache_config = CacheConfig::from_file(Some(path.as_ref()))?;
         Ok(self)
@@ -1207,7 +1192,6 @@ impl Config {
     /// This method is only available when the `cache` feature of this crate is
     /// enabled.
     #[cfg(feature = "cache")]
-    #[cfg_attr(docsrs, doc(cfg(feature = "cache")))]
     pub fn disable_cache(&mut self) -> &mut Self {
         self.cache_config = CacheConfig::new_cache_disabled();
         self
@@ -1235,7 +1219,6 @@ impl Config {
     ///
     /// [docs]: https://bytecodealliance.github.io/wasmtime/cli-cache.html
     #[cfg(feature = "cache")]
-    #[cfg_attr(docsrs, doc(cfg(feature = "cache")))]
     pub fn cache_config_load_default(&mut self) -> Result<&mut Self> {
         self.cache_config = CacheConfig::from_file(None)?;
         Ok(self)
@@ -1256,7 +1239,6 @@ impl Config {
     /// Custom memory creators are used when creating creating async instance stacks for
     /// the on-demand instance allocation strategy.
     #[cfg(feature = "async")]
-    #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
     pub fn with_host_stack(&mut self, stack_creator: Arc<dyn StackCreator>) -> &mut Self {
         self.stack_creator = Some(Arc::new(StackCreatorProxy(stack_creator)));
         self
@@ -1568,7 +1550,6 @@ impl Config {
     ///
     /// By default parallel compilation is enabled.
     #[cfg(feature = "parallel-compilation")]
-    #[cfg_attr(docsrs, doc(cfg(feature = "parallel-compilation")))]
     pub fn parallel_compilation(&mut self, parallel: bool) -> &mut Self {
         self.parallel_compilation = parallel;
         self
@@ -1672,7 +1653,6 @@ impl Config {
     ///
     /// This option is disabled by default.
     #[cfg(feature = "coredump")]
-    #[cfg_attr(docsrs, doc(cfg(feature = "coredump")))]
     pub fn coredump_on_trap(&mut self, enable: bool) -> &mut Self {
         self.coredump_on_trap = enable;
         self
@@ -2372,7 +2352,6 @@ impl PoolingAllocationConfig {
     ///
     /// [`call_async`]: crate::TypedFunc::call_async
     #[cfg(feature = "async")]
-    #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
     pub fn async_stack_zeroing(&mut self, enable: bool) -> &mut Self {
         self.config.async_stack_zeroing = enable;
         self
@@ -2390,7 +2369,6 @@ impl PoolingAllocationConfig {
     /// Note that when using this option the memory with async stacks will
     /// never be decommitted.
     #[cfg(feature = "async")]
-    #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
     pub fn async_stack_keep_resident(&mut self, size: usize) -> &mut Self {
         let size = round_up_to_pages(size as u64) as usize;
         self.config.async_stack_keep_resident = size;
@@ -2770,7 +2748,6 @@ impl PoolingAllocationConfig {
     /// entry in the pool contains the space needed for each GC heap used by a
     /// store.
     #[cfg(feature = "gc")]
-    #[cfg_attr(docsrs, doc(cfg(feature = "gc")))]
     pub fn total_gc_heaps(&mut self, count: u32) -> &mut Self {
         self.config.limits.total_gc_heaps = count;
         self
diff --git a/crates/wasmtime/src/engine.rs b/crates/wasmtime/src/engine.rs
index 47f8dd419..540aa35c5 100644
--- a/crates/wasmtime/src/engine.rs
+++ b/crates/wasmtime/src/engine.rs
@@ -496,7 +496,6 @@ impl Engine {
 }
 
 #[cfg(any(feature = "cranelift", feature = "winch"))]
-#[cfg_attr(docsrs, doc(cfg(any(feature = "cranelift", feature = "winch"))))]
 impl Engine {
     pub(crate) fn compiler(&self) -> &dyn wasmtime_environ::Compiler {
         &*self.inner.compiler
@@ -533,7 +532,6 @@ impl Engine {
     /// Same as [`Engine::precompile_module`] except for a
     /// [`Component`](crate::component::Component)
     #[cfg(feature = "component-model")]
-    #[cfg_attr(docsrs, doc(cfg(feature = "component-model")))]
     pub fn precompile_component(&self, bytes: &[u8]) -> Result<Vec<u8>> {
         crate::CodeBuilder::new(self)
             .wasm(bytes, None)?
@@ -575,7 +573,6 @@ pub enum Precompiled {
 }
 
 #[cfg(feature = "runtime")]
-#[cfg_attr(docsrs, doc(cfg(feature = "runtime")))]
 impl Engine {
     /// Eagerly initialize thread-local functionality shared by all [`Engine`]s.
     ///
@@ -667,7 +664,6 @@ impl Engine {
     /// compatible [`Config`]s. If this Hash matches between two [`Engine`]s then binaries
     /// from one are guaranteed to deserialize in the other.
     #[cfg(any(feature = "cranelift", feature = "winch"))]
-    #[cfg_attr(docsrs, doc(cfg(feature = "cranelift")))] // see build.rs
     pub fn precompile_compatibility_hash(&self) -> impl std::hash::Hash + '_ {
         crate::compile::HashedEngineCompileEnv(self)
     }
diff --git a/crates/wasmtime/src/lib.rs b/crates/wasmtime/src/lib.rs
index 53771a359..8b9c1288b 100644
--- a/crates/wasmtime/src/lib.rs
+++ b/crates/wasmtime/src/lib.rs
@@ -262,7 +262,7 @@
 #![deny(missing_docs)]
 #![doc(test(attr(deny(warnings))))]
 #![doc(test(attr(allow(dead_code, unused_variables, unused_mut))))]
-#![cfg_attr(docsrs, feature(doc_cfg))]
+#![cfg_attr(docsrs, feature(doc_auto_cfg))]
 #![cfg_attr(not(feature = "default"), allow(dead_code, unused_imports))]
 // Allow broken links when the default features is disabled because most of our
 // documentation is written for the "one build" of the `main` branch which has
diff --git a/crates/wasmtime/src/runtime.rs b/crates/wasmtime/src/runtime.rs
index af43f0b39..487fcb073 100644
--- a/crates/wasmtime/src/runtime.rs
+++ b/crates/wasmtime/src/runtime.rs
@@ -1,5 +1,3 @@
-#![cfg_attr(docsrs, doc(cfg(feature = "runtime")))]
-
 #[macro_use]
 pub(crate) mod func;
 
diff --git a/crates/wasmtime/src/runtime/component/component.rs b/crates/wasmtime/src/runtime/component/component.rs
index 3cb1cb967..0235a66ea 100644
--- a/crates/wasmtime/src/runtime/component/component.rs
+++ b/crates/wasmtime/src/runtime/component/component.rs
@@ -147,7 +147,6 @@ impl Component {
     /// # Ok(())
     /// # }
     #[cfg(any(feature = "cranelift", feature = "winch"))]
-    #[cfg_attr(docsrs, doc(cfg(any(feature = "cranelift", feature = "winch"))))]
     pub fn new(engine: &Engine, bytes: impl AsRef<[u8]>) -> Result<Component> {
         crate::CodeBuilder::new(engine)
             .wasm(bytes.as_ref(), None)?
@@ -160,7 +159,6 @@ impl Component {
     /// This is a convenience function for reading the contents of `file` on
     /// disk and then calling [`Component::new`].
     #[cfg(any(feature = "cranelift", feature = "winch"))]
-    #[cfg_attr(docsrs, doc(cfg(any(feature = "cranelift", feature = "winch"))))]
     pub fn from_file(engine: &Engine, file: impl AsRef<Path>) -> Result<Component> {
         crate::CodeBuilder::new(engine)
             .wasm_file(file.as_ref())?
@@ -177,7 +175,6 @@ impl Component {
     ///
     /// For more information on semantics and errors see [`Component::new`].
     #[cfg(any(feature = "cranelift", feature = "winch"))]
-    #[cfg_attr(docsrs, doc(cfg(any(feature = "cranelift", feature = "winch"))))]
     pub fn from_binary(engine: &Engine, binary: &[u8]) -> Result<Component> {
         crate::CodeBuilder::new(engine)
             .wasm(binary, None)?
diff --git a/crates/wasmtime/src/runtime/component/func.rs b/crates/wasmtime/src/runtime/component/func.rs
index 04300abe3..444925575 100644
--- a/crates/wasmtime/src/runtime/component/func.rs
+++ b/crates/wasmtime/src/runtime/component/func.rs
@@ -349,7 +349,6 @@ impl Func {
     /// only works with functions defined within an asynchronous store. Also
     /// panics if `store` does not own this function.
     #[cfg(feature = "async")]
-    #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
     pub async fn call_async<T>(
         &self,
         mut store: impl AsContextMut<Data = T>,
@@ -609,7 +608,6 @@ impl Func {
     /// Panics if this is called on a function in a synchronous store. This
     /// only works with functions defined within an asynchronous store.
     #[cfg(feature = "async")]
-    #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
     pub async fn post_return_async<T: Send>(
         &self,
         mut store: impl AsContextMut<Data = T>,
diff --git a/crates/wasmtime/src/runtime/component/func/typed.rs b/crates/wasmtime/src/runtime/component/func/typed.rs
index a328087e5..9994b4457 100644
--- a/crates/wasmtime/src/runtime/component/func/typed.rs
+++ b/crates/wasmtime/src/runtime/component/func/typed.rs
@@ -170,7 +170,6 @@ where
     /// only works with functions defined within an asynchronous store. Also
     /// panics if `store` does not own this function.
     #[cfg(feature = "async")]
-    #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
     pub async fn call_async<T>(
         &self,
         mut store: impl AsContextMut<Data = T>,
@@ -336,7 +335,6 @@ where
 
     /// See [`Func::post_return_async`]
     #[cfg(feature = "async")]
-    #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
     pub async fn post_return_async<T: Send>(
         &self,
         store: impl AsContextMut<Data = T>,
diff --git a/crates/wasmtime/src/runtime/component/instance.rs b/crates/wasmtime/src/runtime/component/instance.rs
index 2138b4d79..b17092cbc 100644
--- a/crates/wasmtime/src/runtime/component/instance.rs
+++ b/crates/wasmtime/src/runtime/component/instance.rs
@@ -591,7 +591,6 @@ impl<T> InstancePre<T> {
     //
     // TODO: needs more docs
     #[cfg(feature = "async")]
-    #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
     pub async fn instantiate_async(
         &self,
         mut store: impl AsContextMut<Data = T>,
diff --git a/crates/wasmtime/src/runtime/component/linker.rs b/crates/wasmtime/src/runtime/component/linker.rs
index 4a885f027..745e4b942 100644
--- a/crates/wasmtime/src/runtime/component/linker.rs
+++ b/crates/wasmtime/src/runtime/component/linker.rs
@@ -319,7 +319,6 @@ impl<T> Linker<T> {
     /// can return an error if something goes wrong during instantiation such as
     /// a runtime trap or a runtime limit being exceeded.
     #[cfg(feature = "async")]
-    #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
     pub async fn instantiate_async(
         &self,
         store: impl AsContextMut<Data = T>,
@@ -384,7 +383,6 @@ impl<T> LinkerInstance<'_, T> {
     /// This is exactly like [`Self::func_wrap`] except it takes an async
     /// host function.
     #[cfg(feature = "async")]
-    #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
     pub fn func_wrap_async<Params, Return, F>(&mut self, name: &str, f: F) -> Result<()>
     where
         F: for<'a> Fn(
@@ -522,7 +520,6 @@ impl<T> LinkerInstance<'_, T> {
     /// This is exactly like [`Self::func_new`] except it takes an async
     /// host function.
     #[cfg(feature = "async")]
-    #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
     pub fn func_new_async<F>(&mut self, name: &str, f: F) -> Result<()>
     where
         F: for<'a> Fn(
diff --git a/crates/wasmtime/src/runtime/component/mod.rs b/crates/wasmtime/src/runtime/component/mod.rs
index 721f1e80e..7276b5cde 100644
--- a/crates/wasmtime/src/runtime/component/mod.rs
+++ b/crates/wasmtime/src/runtime/component/mod.rs
@@ -97,7 +97,6 @@
 //!
 //! See the docs for [`bindgen!`] for more information on how to use it.
 
-#![cfg_attr(docsrs, doc(cfg(feature = "component-model")))]
 // rustdoc appears to lie about a warning above, so squelch it for now.
 #![allow(rustdoc::redundant_explicit_links)]
 
diff --git a/crates/wasmtime/src/runtime/component/resources.rs b/crates/wasmtime/src/runtime/component/resources.rs
index 789329523..b2f8d08b1 100644
--- a/crates/wasmtime/src/runtime/component/resources.rs
+++ b/crates/wasmtime/src/runtime/component/resources.rs
@@ -978,7 +978,6 @@ impl ResourceAny {
     /// Same as [`ResourceAny::resource_drop`] except for use with async stores
     /// to execute the destructor asynchronously.
     #[cfg(feature = "async")]
-    #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
     pub async fn resource_drop_async<T>(self, mut store: impl AsContextMut<Data = T>) -> Result<()>
     where
         T: Send,
diff --git a/crates/wasmtime/src/runtime/externals/global.rs b/crates/wasmtime/src/runtime/externals/global.rs
index 36da5f5b7..0cc329576 100644
--- a/crates/wasmtime/src/runtime/externals/global.rs
+++ b/crates/wasmtime/src/runtime/externals/global.rs
@@ -135,6 +135,8 @@ impl Global {
 
                         HeapType::Any
                         | HeapType::I31
+                        | HeapType::Struct
+                        | HeapType::ConcreteStruct(_)
                         | HeapType::Array
                         | HeapType::ConcreteArray(_) => definition
                             .as_gc_ref()
diff --git a/crates/wasmtime/src/runtime/externals/table.rs b/crates/wasmtime/src/runtime/externals/table.rs
index e7397750e..359e61f88 100644
--- a/crates/wasmtime/src/runtime/externals/table.rs
+++ b/crates/wasmtime/src/runtime/externals/table.rs
@@ -77,7 +77,6 @@ impl Table {
         Table::_new(store.as_context_mut().0, ty, init)
     }
 
-    #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
     /// Async variant of [`Table::new`]. You must use this variant with
     /// [`Store`](`crate::Store`)s which have a
     /// [`ResourceLimiterAsync`](`crate::ResourceLimiterAsync`).
@@ -265,7 +264,6 @@ impl Table {
         }
     }
 
-    #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
     /// Async variant of [`Table::grow`]. Required when using a
     /// [`ResourceLimiterAsync`](`crate::ResourceLimiterAsync`).
     ///
diff --git a/crates/wasmtime/src/runtime/func.rs b/crates/wasmtime/src/runtime/func.rs
index c0c70bf08..79cadf450 100644
--- a/crates/wasmtime/src/runtime/func.rs
+++ b/crates/wasmtime/src/runtime/func.rs
@@ -368,7 +368,6 @@ macro_rules! generate_wrap_async_func {
         /// This function will panic if called with a non-asynchronous store.
         #[allow(non_snake_case)]
         #[cfg(feature = "async")]
-        #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
         pub fn [<wrap $num _async>]<T, $($args,)* R>(
             store: impl AsContextMut<Data = T>,
             func: impl for<'a> Fn(Caller<'a, T>, $($args),*) -> Box<dyn Future<Output = R> + Send + 'a> + Send + Sync + 'static,
@@ -445,7 +444,6 @@ impl Func {
     /// Panics if the given function type is not associated with this store's
     /// engine.
     #[cfg(any(feature = "cranelift", feature = "winch"))]
-    #[cfg_attr(docsrs, doc(cfg(any(feature = "cranelift", feature = "winch"))))]
     pub fn new<T>(
         store: impl AsContextMut<Data = T>,
         ty: FuncType,
@@ -489,7 +487,6 @@ impl Func {
     /// Panics if the given function type is not associated with this store's
     /// engine.
     #[cfg(any(feature = "cranelift", feature = "winch"))]
-    #[cfg_attr(docsrs, doc(cfg(any(feature = "cranelift", feature = "winch"))))]
     pub unsafe fn new_unchecked<T>(
         mut store: impl AsContextMut<Data = T>,
         ty: FuncType,
@@ -574,7 +571,6 @@ impl Func {
     /// # }
     /// ```
     #[cfg(all(feature = "async", feature = "cranelift"))]
-    #[cfg_attr(docsrs, doc(cfg(all(feature = "async", feature = "cranelift"))))]
     pub fn new_async<T, F>(store: impl AsContextMut<Data = T>, ty: FuncType, func: F) -> Func
     where
         F: for<'a> Fn(
@@ -1113,7 +1109,6 @@ impl Func {
     /// only works with functions defined within an asynchronous store. Also
     /// panics if `store` does not own this function.
     #[cfg(feature = "async")]
-    #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
     pub async fn call_async<T>(
         &self,
         mut store: impl AsContextMut<Data = T>,
@@ -2080,7 +2075,6 @@ impl<T> Caller<'_, T> {
     ///
     /// Same as [`Store::gc`](crate::Store::gc).
     #[cfg(feature = "gc")]
-    #[cfg_attr(docsrs, doc(cfg(feature = "gc")))]
     pub fn gc(&mut self) {
         self.store.gc()
     }
@@ -2089,7 +2083,6 @@ impl<T> Caller<'_, T> {
     ///
     /// Same as [`Store::gc_async`](crate::Store::gc_async).
     #[cfg(all(feature = "async", feature = "gc"))]
-    #[cfg_attr(docsrs, doc(cfg(feature = "gc")))]
     pub async fn gc_async(&mut self)
     where
         T: Send,
diff --git a/crates/wasmtime/src/runtime/func/typed.rs b/crates/wasmtime/src/runtime/func/typed.rs
index faacb3a15..1e486c516 100644
--- a/crates/wasmtime/src/runtime/func/typed.rs
+++ b/crates/wasmtime/src/runtime/func/typed.rs
@@ -125,7 +125,6 @@ where
     ///
     /// [`Trap`]: crate::Trap
     #[cfg(feature = "async")]
-    #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
     pub async fn call_async<T>(
         &self,
         mut store: impl AsContextMut<Data = T>,
diff --git a/crates/wasmtime/src/runtime/instance.rs b/crates/wasmtime/src/runtime/instance.rs
index 80db9340a..38b251954 100644
--- a/crates/wasmtime/src/runtime/instance.rs
+++ b/crates/wasmtime/src/runtime/instance.rs
@@ -145,7 +145,6 @@ impl Instance {
     /// This function will also panic, like [`Instance::new`], if any [`Extern`]
     /// specified does not belong to `store`.
     #[cfg(feature = "async")]
-    #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
     pub async fn new_async<T>(
         mut store: impl AsContextMut<Data = T>,
         module: &Module,
@@ -888,7 +887,6 @@ impl<T> InstancePre<T> {
     /// Panics if any import closed over by this [`InstancePre`] isn't owned by
     /// `store`, or if `store` does not have async support enabled.
     #[cfg(feature = "async")]
-    #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
     pub async fn instantiate_async(
         &self,
         mut store: impl AsContextMut<Data = T>,
diff --git a/crates/wasmtime/src/runtime/linker.rs b/crates/wasmtime/src/runtime/linker.rs
index d9546071b..ff643367e 100644
--- a/crates/wasmtime/src/runtime/linker.rs
+++ b/crates/wasmtime/src/runtime/linker.rs
@@ -140,7 +140,6 @@ macro_rules! generate_wrap_async_func {
         /// [`Func::wrapN_async`](crate::Func::wrap1_async).
         #[allow(non_snake_case)]
         #[cfg(feature = "async")]
-        #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
         pub fn [<func_wrap $num _async>]<$($args,)* R>(
             &mut self,
             module: &str,
@@ -275,7 +274,6 @@ impl<T> Linker<T> {
     /// # }
     /// ```
     #[cfg(any(feature = "cranelift", feature = "winch"))]
-    #[cfg_attr(docsrs, doc(cfg(any(feature = "cranelift", feature = "winch"))))]
     pub fn define_unknown_imports_as_traps(&mut self, module: &Module) -> anyhow::Result<()> {
         for import in module.imports() {
             if let Err(import_err) = self._get_by_import(&import) {
@@ -311,7 +309,6 @@ impl<T> Linker<T> {
     /// # }
     /// ```
     #[cfg(any(feature = "cranelift", feature = "winch"))]
-    #[cfg_attr(docsrs, doc(cfg(any(feature = "cranelift", feature = "winch"))))]
     pub fn define_unknown_imports_as_default_values(
         &mut self,
         module: &Module,
@@ -430,7 +427,6 @@ impl<T> Linker<T> {
     /// Panics if the given function type is not associated with the same engine
     /// as this linker.
     #[cfg(any(feature = "cranelift", feature = "winch"))]
-    #[cfg_attr(docsrs, doc(cfg(any(feature = "cranelift", feature = "winch"))))]
     pub fn func_new(
         &mut self,
         module: &str,
@@ -454,7 +450,6 @@ impl<T> Linker<T> {
     /// Panics if the given function type is not associated with the same engine
     /// as this linker.
     #[cfg(any(feature = "cranelift", feature = "winch"))]
-    #[cfg_attr(docsrs, doc(cfg(any(feature = "cranelift", feature = "winch"))))]
     pub unsafe fn func_new_unchecked(
         &mut self,
         module: &str,
@@ -483,7 +478,6 @@ impl<T> Linker<T> {
     /// * If the given function type is not associated with the same engine as
     ///   this linker.
     #[cfg(all(feature = "async", feature = "cranelift"))]
-    #[cfg_attr(docsrs, doc(cfg(all(feature = "async", feature = "cranelift"))))]
     pub fn func_new_async<F>(
         &mut self,
         module: &str,
@@ -786,7 +780,6 @@ impl<T> Linker<T> {
     /// # }
     /// ```
     #[cfg(any(feature = "cranelift", feature = "winch"))]
-    #[cfg_attr(docsrs, doc(cfg(any(feature = "cranelift", feature = "winch"))))]
     pub fn module(
         &mut self,
         mut store: impl AsContextMut<Data = T>,
@@ -857,7 +850,6 @@ impl<T> Linker<T> {
     ///
     /// This is the same as [`Linker::module`], except for async `Store`s.
     #[cfg(all(feature = "async", feature = "cranelift"))]
-    #[cfg_attr(docsrs, doc(cfg(all(feature = "async", feature = "cranelift"))))]
     pub async fn module_async(
         &mut self,
         mut store: impl AsContextMut<Data = T>,
@@ -1130,7 +1122,6 @@ impl<T> Linker<T> {
     /// Attempts to instantiate the `module` provided. This is the same as
     /// [`Linker::instantiate`], except for async `Store`s.
     #[cfg(feature = "async")]
-    #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
     pub async fn instantiate_async(
         &self,
         mut store: impl AsContextMut<Data = T>,
diff --git a/crates/wasmtime/src/runtime/memory.rs b/crates/wasmtime/src/runtime/memory.rs
index 4962ffc96..45eb45c22 100644
--- a/crates/wasmtime/src/runtime/memory.rs
+++ b/crates/wasmtime/src/runtime/memory.rs
@@ -237,7 +237,6 @@ impl Memory {
         Self::_new(store.as_context_mut().0, ty)
     }
 
-    #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
     /// Async variant of [`Memory::new`]. You must use this variant with
     /// [`Store`](`crate::Store`)s which have a
     /// [`ResourceLimiterAsync`](`crate::ResourceLimiterAsync`).
@@ -519,7 +518,6 @@ impl Memory {
         }
     }
 
-    #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
     /// Async variant of [`Memory::grow`]. Required when using a
     /// [`ResourceLimiterAsync`](`crate::ResourceLimiterAsync`).
     ///
@@ -726,7 +724,6 @@ impl SharedMemory {
     /// `maximum` number of 64K-sized pages. This call allocates the necessary
     /// pages on the system.
     #[cfg(feature = "threads")]
-    #[cfg_attr(docsrs, doc(cfg(feature = "threads")))]
     pub fn new(engine: &Engine, ty: MemoryType) -> Result<Self> {
         if !ty.is_shared() {
             bail!("shared memory must have the `shared` flag enabled on its memory type")
diff --git a/crates/wasmtime/src/runtime/module.rs b/crates/wasmtime/src/runtime/module.rs
index 22818f5b7..7f39308f8 100644
--- a/crates/wasmtime/src/runtime/module.rs
+++ b/crates/wasmtime/src/runtime/module.rs
@@ -235,7 +235,6 @@ impl Module {
     /// # }
     /// ```
     #[cfg(any(feature = "cranelift", feature = "winch"))]
-    #[cfg_attr(docsrs, doc(cfg(any(feature = "cranelift", feature = "winch"))))]
     pub fn new(engine: &Engine, bytes: impl AsRef<[u8]>) -> Result<Module> {
         crate::CodeBuilder::new(engine)
             .wasm(bytes.as_ref(), None)?
@@ -271,7 +270,6 @@ impl Module {
     /// # }
     /// ```
     #[cfg(any(feature = "cranelift", feature = "winch"))]
-    #[cfg_attr(docsrs, doc(cfg(any(feature = "cranelift", feature = "winch"))))]
     pub fn from_file(engine: &Engine, file: impl AsRef<Path>) -> Result<Module> {
         crate::CodeBuilder::new(engine)
             .wasm_file(file.as_ref())?
@@ -310,7 +308,6 @@ impl Module {
     /// # }
     /// ```
     #[cfg(any(feature = "cranelift", feature = "winch"))]
-    #[cfg_attr(docsrs, doc(cfg(any(feature = "cranelift", feature = "winch"))))]
     pub fn from_binary(engine: &Engine, binary: &[u8]) -> Result<Module> {
         crate::CodeBuilder::new(engine)
             .wasm(binary, None)?
@@ -340,7 +337,6 @@ impl Module {
     /// reflect the current state of the file, not necessarily the original
     /// state of the file.
     #[cfg(any(feature = "cranelift", feature = "winch"))]
-    #[cfg_attr(docsrs, doc(cfg(any(feature = "cranelift", feature = "winch"))))]
     pub unsafe fn from_trusted_file(engine: &Engine, file: impl AsRef<Path>) -> Result<Module> {
         let mmap = MmapVec::from_file(file.as_ref())?;
         if &mmap[0..4] == b"\x7fELF" {
@@ -550,7 +546,6 @@ impl Module {
     /// this method can be useful to get the serialized version without
     /// compiling twice.
     #[cfg(any(feature = "cranelift", feature = "winch"))]
-    #[cfg_attr(docsrs, doc(cfg(any(feature = "cranelift", feature = "winch"))))]
     pub fn serialize(&self) -> Result<Vec<u8>> {
         // The current representation of compiled modules within a compiled
         // component means that it cannot be serialized. The mmap returned here
diff --git a/crates/wasmtime/src/runtime/store.rs b/crates/wasmtime/src/runtime/store.rs
index 45e06e6c4..3f409d8b7 100644
--- a/crates/wasmtime/src/runtime/store.rs
+++ b/crates/wasmtime/src/runtime/store.rs
@@ -710,7 +710,6 @@ impl<T> Store<T> {
     /// [`Store`] configured via
     /// [`Config::async_support`](crate::Config::async_support).
     #[cfg(feature = "async")]
-    #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
     pub fn limiter_async(
         &mut self,
         mut limiter: impl FnMut(&mut T) -> &mut (dyn crate::ResourceLimiterAsync)
@@ -734,7 +733,6 @@ impl<T> Store<T> {
         inner.limiter = Some(ResourceLimiterInner::Async(Box::new(limiter)));
     }
 
-    #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
     /// Configures an async function that runs on calls and returns between
     /// WebAssembly and host code. For the non-async equivalent of this method,
     /// see [`Store::call_hook`].
@@ -968,7 +966,6 @@ impl<T> Store<T> {
         self.inner.epoch_deadline_callback(Box::new(callback));
     }
 
-    #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
     /// Configures epoch-deadline expiration to yield to the async
     /// caller and the update the deadline.
     ///
@@ -1101,7 +1098,6 @@ impl<'a, T> StoreContextMut<'a, T> {
         self.0.epoch_deadline_trap();
     }
 
-    #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
     /// Configures epoch-deadline expiration to yield to the async
     /// caller and the update the deadline.
     ///
diff --git a/crates/wasmtime/src/runtime/type_registry.rs b/crates/wasmtime/src/runtime/type_registry.rs
index a41d34e69..5f222d36e 100644
--- a/crates/wasmtime/src/runtime/type_registry.rs
+++ b/crates/wasmtime/src/runtime/type_registry.rs
@@ -20,7 +20,7 @@ use std::{
 };
 use wasmtime_environ::{
     iter_entity_range, EngineOrModuleTypeIndex, ModuleInternedTypeIndex, ModuleTypes, PrimaryMap,
-    TypeTrace, VMSharedTypeIndex, WasmRecGroup, WasmSubType,
+    SecondaryMap, TypeTrace, VMSharedTypeIndex, WasmRecGroup, WasmSubType,
 };
 use wasmtime_slab::{Id as SlabId, Slab};
 
@@ -293,7 +293,7 @@ impl RegisteredType {
             let inner = engine.signatures().0.read().unwrap();
 
             let ty = inner.types.get(id)?.clone();
-            let entry = inner.type_to_rec_group[&index].clone();
+            let entry = inner.type_to_rec_group[index].clone().unwrap();
 
             // NB: make sure to incref while the lock is held to prevent:
             //
@@ -436,7 +436,7 @@ struct TypeRegistryInner {
 
     // A map that lets you walk backwards from a `VMSharedTypeIndex` to its
     // `RecGroupEntry`.
-    type_to_rec_group: HashMap<VMSharedTypeIndex, RecGroupEntry>,
+    type_to_rec_group: SecondaryMap<VMSharedTypeIndex, Option<RecGroupEntry>>,
 
     // An explicit stack of entries that we are in the middle of dropping. Used
     // to avoid recursion when dropping a type that is holding the last
@@ -543,7 +543,7 @@ impl TypeRegistryInner {
         // while this rec group is still alive.
         hash_consing_key
             .trace_engine_indices::<_, ()>(&mut |index| {
-                let entry = &self.type_to_rec_group[&index];
+                let entry = &self.type_to_rec_group[index].as_ref().unwrap();
                 entry.incref(
                     "new cross-group type reference to existing type in `register_rec_group`",
                 );
@@ -583,11 +583,11 @@ impl TypeRegistryInner {
         let is_new_entry = self.hash_consing_map.insert(entry.clone());
         debug_assert!(is_new_entry);
 
-        // Now that we've construct the entry, we can update the reverse
+        // Now that we've constructed the entry, we can update the reverse
         // type-to-rec-group map.
-        for ty in entry.0.shared_type_indices.iter() {
-            let old_entry = self.type_to_rec_group.insert(*ty, entry.clone());
-            debug_assert!(old_entry.is_none());
+        for ty in entry.0.shared_type_indices.iter().copied() {
+            debug_assert!(self.type_to_rec_group[ty].is_none());
+            self.type_to_rec_group[ty] = Some(entry.clone());
         }
 
         entry
@@ -714,12 +714,12 @@ impl TypeRegistryInner {
                 .0
                 .hash_consing_key
                 .trace_engine_indices::<_, ()>(&mut |other_index| {
-                    let other_entry = self.type_to_rec_group[&other_index].clone();
+                    let other_entry = self.type_to_rec_group[other_index].as_ref().unwrap();
                     if other_entry.decref(
                         "referenced by dropped entry in \
                          `TypeCollection::unregister_entry`",
                     ) {
-                        self.drop_stack.push(other_entry);
+                        self.drop_stack.push(other_entry.clone());
                     }
                     Ok(())
                 })
@@ -734,13 +734,13 @@ impl TypeRegistryInner {
 
             // Similarly, remove the rec group's types from the registry, as
             // well as their entries from the reverse type-to-rec-group map.
-            for ty in entry.0.shared_type_indices.iter() {
+            for ty in entry.0.shared_type_indices.iter().copied() {
                 log::trace!("removing {ty:?} from registry");
 
-                let removed_entry = self.type_to_rec_group.remove(ty);
+                let removed_entry = self.type_to_rec_group[ty].take();
                 debug_assert_eq!(removed_entry.unwrap(), entry);
 
-                let id = shared_type_index_to_slab_id(*ty);
+                let id = shared_type_index_to_slab_id(ty);
                 self.types.dealloc(id);
             }
 
@@ -769,7 +769,7 @@ impl Drop for TypeRegistryInner {
             "type registry not empty: types slab is not empty"
         );
         assert!(
-            type_to_rec_group.is_empty(),
+            type_to_rec_group.is_empty() || type_to_rec_group.values().all(|x| x.is_none()),
             "type registry not empty: type-to-rec-group map is not empty"
         );
         assert!(
diff --git a/crates/wasmtime/src/runtime/types.rs b/crates/wasmtime/src/runtime/types.rs
index 4d501b50c..822a0986f 100644
--- a/crates/wasmtime/src/runtime/types.rs
+++ b/crates/wasmtime/src/runtime/types.rs
@@ -1,9 +1,9 @@
-use anyhow::{bail, Result};
+use anyhow::{bail, ensure, Result};
 use std::fmt::{self, Display};
 use wasmtime_environ::{
     EngineOrModuleTypeIndex, EntityType, Global, Memory, ModuleTypes, Table, TypeTrace,
     VMSharedTypeIndex, WasmArrayType, WasmCompositeType, WasmFieldType, WasmFuncType, WasmHeapType,
-    WasmRefType, WasmStorageType, WasmSubType, WasmValType,
+    WasmRefType, WasmStorageType, WasmStructType, WasmSubType, WasmValType,
 };
 
 use crate::{type_registry::RegisteredType, Engine};
@@ -474,7 +474,8 @@ pub enum HeapType {
     /// This is a subtype of `any` and a supertype of `none`.
     I31,
 
-    /// The abstract `array` heap type represents a reference to any kind of array.
+    /// The abstract `array` heap type represents a reference to any kind of
+    /// array.
     ///
     /// This is a subtype of `any` and a supertype of all concrete array types,
     /// as well as a supertype of the abstract `none` heap type.
@@ -490,6 +491,23 @@ pub enum HeapType {
     // TODO: add docs for subtype of `eq` once we add that heap type
     ConcreteArray(ArrayType),
 
+    /// The abstract `struct` heap type represents a reference to any kind of
+    /// struct.
+    ///
+    /// This is a subtype of `any` and a supertype of all concrete struct types,
+    /// as well as a supertype of the abstract `none` heap type.
+    //
+    // TODO: add docs for subtype of `eq` once we add that heap type
+    Struct,
+
+    /// A reference to an struct of a specific, concrete type.
+    ///
+    /// These are subtypes of the `struct` heap type (therefore also a subtype
+    /// of `any`) and supertypes of the `none` heap type.
+    //
+    // TODO: add docs for subtype of `eq` once we add that heap type
+    ConcreteStruct(StructType),
+
     /// The abstract `none` heap type represents the null internal reference.
     ///
     /// This is the bottom type for the internal type hierarchy, and therefore
@@ -506,9 +524,11 @@ impl Display for HeapType {
             HeapType::Any => write!(f, "any"),
             HeapType::I31 => write!(f, "i31"),
             HeapType::Array => write!(f, "array"),
+            HeapType::Struct => write!(f, "struct"),
             HeapType::None => write!(f, "none"),
             HeapType::ConcreteFunc(ty) => write!(f, "(concrete func {:?})", ty.type_index()),
             HeapType::ConcreteArray(ty) => write!(f, "(concrete array {:?})", ty.type_index()),
+            HeapType::ConcreteStruct(ty) => write!(f, "(concrete struct {:?})", ty.type_index()),
         }
     }
 }
@@ -630,6 +650,8 @@ impl HeapType {
             | HeapType::I31
             | HeapType::Array
             | HeapType::ConcreteArray(_)
+            | HeapType::Struct
+            | HeapType::ConcreteStruct(_)
             | HeapType::None => HeapType::Any,
         }
     }
@@ -672,6 +694,8 @@ impl HeapType {
                 HeapType::None
                 | HeapType::ConcreteArray(_)
                 | HeapType::Array
+                | HeapType::ConcreteStruct(_)
+                | HeapType::Struct
                 | HeapType::I31
                 | HeapType::Any,
             ) => true,
@@ -684,6 +708,13 @@ impl HeapType {
             (HeapType::Array, HeapType::Array | HeapType::Any) => true,
             (HeapType::Array, _) => false,
 
+            (HeapType::ConcreteStruct(_), HeapType::Struct | HeapType::Any) => true,
+            (HeapType::ConcreteStruct(a), HeapType::ConcreteStruct(b)) => a.matches(b),
+            (HeapType::ConcreteStruct(_), _) => false,
+
+            (HeapType::Struct, HeapType::Struct | HeapType::Any) => true,
+            (HeapType::Struct, _) => false,
+
             (HeapType::I31, HeapType::I31 | HeapType::Any) => true,
             (HeapType::I31, _) => false,
 
@@ -724,9 +755,11 @@ impl HeapType {
             | HeapType::Any
             | HeapType::I31
             | HeapType::Array
+            | HeapType::Struct
             | HeapType::None => true,
             HeapType::ConcreteFunc(ty) => ty.comes_from_same_engine(engine),
             HeapType::ConcreteArray(ty) => ty.comes_from_same_engine(engine),
+            HeapType::ConcreteStruct(ty) => ty.comes_from_same_engine(engine),
         }
     }
 
@@ -738,6 +771,7 @@ impl HeapType {
             HeapType::Any => WasmHeapType::Any,
             HeapType::I31 => WasmHeapType::I31,
             HeapType::Array => WasmHeapType::Array,
+            HeapType::Struct => WasmHeapType::Struct,
             HeapType::None => WasmHeapType::None,
             HeapType::ConcreteFunc(f) => {
                 WasmHeapType::ConcreteFunc(EngineOrModuleTypeIndex::Engine(f.type_index()))
@@ -745,6 +779,9 @@ impl HeapType {
             HeapType::ConcreteArray(a) => {
                 WasmHeapType::ConcreteArray(EngineOrModuleTypeIndex::Engine(a.type_index()))
             }
+            HeapType::ConcreteStruct(a) => {
+                WasmHeapType::ConcreteStruct(EngineOrModuleTypeIndex::Engine(a.type_index()))
+            }
         }
     }
 
@@ -756,6 +793,7 @@ impl HeapType {
             WasmHeapType::Any => HeapType::Any,
             WasmHeapType::I31 => HeapType::I31,
             WasmHeapType::Array => HeapType::Array,
+            WasmHeapType::Struct => HeapType::Struct,
             WasmHeapType::None => HeapType::None,
             WasmHeapType::ConcreteFunc(EngineOrModuleTypeIndex::Engine(idx)) => {
                 HeapType::ConcreteFunc(FuncType::from_shared_type_index(engine, *idx))
@@ -763,11 +801,16 @@ impl HeapType {
             WasmHeapType::ConcreteArray(EngineOrModuleTypeIndex::Engine(idx)) => {
                 HeapType::ConcreteArray(ArrayType::from_shared_type_index(engine, *idx))
             }
+            WasmHeapType::ConcreteStruct(EngineOrModuleTypeIndex::Engine(idx)) => {
+                HeapType::ConcreteStruct(StructType::from_shared_type_index(engine, *idx))
+            }
 
             WasmHeapType::ConcreteFunc(EngineOrModuleTypeIndex::Module(_))
             | WasmHeapType::ConcreteFunc(EngineOrModuleTypeIndex::RecGroup(_))
             | WasmHeapType::ConcreteArray(EngineOrModuleTypeIndex::Module(_))
-            | WasmHeapType::ConcreteArray(EngineOrModuleTypeIndex::RecGroup(_)) => {
+            | WasmHeapType::ConcreteArray(EngineOrModuleTypeIndex::RecGroup(_))
+            | WasmHeapType::ConcreteStruct(EngineOrModuleTypeIndex::Module(_))
+            | WasmHeapType::ConcreteStruct(EngineOrModuleTypeIndex::RecGroup(_)) => {
                 panic!("HeapType::from_wasm_type on non-canonicalized-for-runtime-usage heap type")
             }
         }
@@ -777,12 +820,15 @@ impl HeapType {
         match self {
             HeapType::ConcreteFunc(f) => Some(&f.registered_type),
             HeapType::ConcreteArray(a) => Some(&a.registered_type),
+            HeapType::ConcreteStruct(a) => Some(&a.registered_type),
+
             HeapType::Extern
             | HeapType::Func
             | HeapType::NoFunc
             | HeapType::Any
             | HeapType::I31
             | HeapType::Array
+            | HeapType::Struct
             | HeapType::None => None,
         }
     }
@@ -1085,6 +1131,183 @@ impl FieldType {
     }
 }
 
+/// The type of a WebAssembly struct.
+///
+/// WebAssembly structs are a static, fixed-length, ordered sequence of
+/// fields. Fields are named by index, not an identifier. Each field is mutable
+/// or constant and stores unpacked [`Val`][crate::Val]s or packed 8-/16-bit
+/// integers.
+///
+/// # Subtyping and Equality
+///
+/// `StructType` does not implement `Eq`, because reference types have a
+/// subtyping relationship, and so 99.99% of the time you actually want to check
+/// whether one type matches (i.e. is a subtype of) another type. You can use
+/// the [`StructType::matches`] method to perform these types of checks. If,
+/// however, you are in that 0.01% scenario where you need to check precise
+/// equality between types, you can use the [`StructType::eq`] method.
+//
+// TODO: Once we have array values, update above docs with a reference to the
+// future `Array::matches_ty` method
+#[derive(Debug, Clone, Hash)]
+pub struct StructType {
+    registered_type: RegisteredType,
+}
+
+impl StructType {
+    /// Construct a new `StructType` with the given field type's mutability and
+    /// storage type.
+    ///
+    /// The result will be associated with the given engine, and attempts to use
+    /// it with other engines will panic (for example, checking whether it is a
+    /// subtype of another struct type that is associated with a different
+    /// engine).
+    ///
+    /// Returns an error if the number of fields exceeds the implementation
+    /// limit.
+    pub fn new(engine: &Engine, fields: impl IntoIterator<Item = FieldType>) -> Result<Self> {
+        // Same as in `FuncType::new`: we must prevent any `RegisteredType`s
+        // from being reclaimed while constructing this struct type.
+        let mut registrations = smallvec::SmallVec::<[_; 4]>::new();
+
+        let fields = fields
+            .into_iter()
+            .map(|ty: FieldType| {
+                if let Some(r) = ty.element_type.as_val_type().and_then(|v| v.as_ref()) {
+                    if let Some(r) = r.heap_type().as_registered_type() {
+                        registrations.push(r.clone());
+                    }
+                }
+                ty.to_wasm_field_type()
+            })
+            .collect();
+
+        Self::from_wasm_struct_type(engine, WasmStructType { fields })
+    }
+
+    /// Get the engine that this struct type is associated with.
+    pub fn engine(&self) -> &Engine {
+        self.registered_type.engine()
+    }
+
+    /// Get the `i`th field type.
+    ///
+    /// Returns `None` if `i` is out of bounds.
+    pub fn field(&self, i: usize) -> Option<FieldType> {
+        let engine = self.engine();
+        self.as_wasm_struct_type()
+            .fields
+            .get(i)
+            .map(|ty| FieldType::from_wasm_field_type(engine, ty))
+    }
+
+    /// Returns the list of field types for this function.
+    #[inline]
+    pub fn fields(&self) -> impl ExactSizeIterator<Item = FieldType> + '_ {
+        let engine = self.engine();
+        self.as_wasm_struct_type()
+            .fields
+            .iter()
+            .map(|ty| FieldType::from_wasm_field_type(engine, ty))
+    }
+
+    /// Does this struct type match the other struct type?
+    ///
+    /// That is, is this function type a subtype of the other struct type?
+    ///
+    /// # Panics
+    ///
+    /// Panics if either type is associated with a different engine from the
+    /// other.
+    pub fn matches(&self, other: &StructType) -> bool {
+        assert!(self.comes_from_same_engine(other.engine()));
+
+        // Avoid matching on structure for subtyping checks when we have
+        // precisely the same type.
+        if self.type_index() == other.type_index() {
+            return true;
+        }
+
+        self.fields().len() >= other.fields().len()
+            && self
+                .fields()
+                .zip(other.fields())
+                .all(|(a, b)| a.matches(&b))
+    }
+
+    /// Is struct type `a` precisely equal to struct type `b`?
+    ///
+    /// Returns `false` even if `a` is a subtype of `b` or vice versa, if they
+    /// are not exactly the same struct type.
+    ///
+    /// # Panics
+    ///
+    /// Panics if either type is associated with a different engine from the
+    /// other.
+    pub fn eq(a: &StructType, b: &StructType) -> bool {
+        assert!(a.comes_from_same_engine(b.engine()));
+        a.type_index() == b.type_index()
+    }
+
+    pub(crate) fn comes_from_same_engine(&self, engine: &Engine) -> bool {
+        Engine::same(self.registered_type.engine(), engine)
+    }
+
+    pub(crate) fn type_index(&self) -> VMSharedTypeIndex {
+        self.registered_type.index()
+    }
+
+    pub(crate) fn as_wasm_struct_type(&self) -> &WasmStructType {
+        self.registered_type.unwrap_struct()
+    }
+
+    /// Construct a `StructType` from a `WasmStructType`.
+    ///
+    /// This method should only be used when something has already registered --
+    /// and is *keeping registered* -- any other concrete Wasm types referenced
+    /// by the given `WasmStructType`.
+    ///
+    /// For example, this method may be called to convert an struct type from
+    /// within a Wasm module's `ModuleTypes` since the Wasm module itself is
+    /// holding a strong reference to all of its types, including any `(ref null
+    /// <index>)` types used as the element type for this struct type.
+    pub(crate) fn from_wasm_struct_type(engine: &Engine, ty: WasmStructType) -> Result<StructType> {
+        const MAX_FIELDS: usize = 10_000;
+        let fields_len = ty.fields.len();
+        ensure!(
+            fields_len <= MAX_FIELDS,
+            "attempted to define a struct type with {fields_len} fields, but \
+             that is more than the maximum supported number of fields \
+             ({MAX_FIELDS})",
+        );
+
+        let ty = RegisteredType::new(
+            engine,
+            WasmSubType {
+                // TODO:
+                //
+                // is_final: true,
+                // supertype: None,
+                composite_type: WasmCompositeType::Struct(ty),
+            },
+        );
+        Ok(Self {
+            registered_type: ty,
+        })
+    }
+
+    pub(crate) fn from_shared_type_index(engine: &Engine, index: VMSharedTypeIndex) -> StructType {
+        let ty = RegisteredType::root(engine, index).expect(
+            "VMSharedTypeIndex is not registered in the Engine! Wrong \
+             engine? Didn't root the index somewhere?",
+        );
+        assert!(ty.is_struct());
+        Self {
+            registered_type: ty,
+        }
+    }
+}
+
 /// The type of a WebAssembly array.
 ///
 /// WebAssembly arrays are dynamically-sized, but not resizable. They contain
diff --git a/crates/wasmtime/src/runtime/types/matching.rs b/crates/wasmtime/src/runtime/types/matching.rs
index aa15532f2..f7ed606ef 100644
--- a/crates/wasmtime/src/runtime/types/matching.rs
+++ b/crates/wasmtime/src/runtime/types/matching.rs
@@ -2,7 +2,7 @@ use crate::{linker::DefinitionType, Engine, FuncType};
 use anyhow::{anyhow, bail, Result};
 use wasmtime_environ::{
     EntityType, Global, Memory, ModuleTypes, Table, TypeTrace, VMSharedTypeIndex,
-    WasmCompositeType, WasmHeapType, WasmRefType, WasmSubType, WasmValType,
+    WasmCompositeType, WasmFieldType, WasmHeapType, WasmRefType, WasmSubType, WasmValType,
 };
 
 pub struct MatchCx<'a> {
@@ -114,16 +114,17 @@ fn concrete_type_mismatch(
     expected: &WasmSubType,
     actual: &WasmSubType,
 ) -> anyhow::Error {
+    let render_field = |ty: &WasmFieldType| {
+        if ty.mutable {
+            format!("(mut {})", ty.element_type)
+        } else {
+            ty.element_type.to_string()
+        }
+    };
+
     let render = |ty: &WasmSubType| match &ty.composite_type {
         WasmCompositeType::Array(ty) => {
-            format!(
-                "(array {})",
-                if ty.0.mutable {
-                    format!("(mut {})", ty.0.element_type)
-                } else {
-                    ty.0.element_type.to_string()
-                }
-            )
+            format!("(array {})", render_field(&ty.0))
         }
         WasmCompositeType::Func(ty) => {
             let params = if ty.params().is_empty() {
@@ -152,6 +153,14 @@ fn concrete_type_mismatch(
             };
             format!("(func{params}{returns})")
         }
+        WasmCompositeType::Struct(ty) => {
+            let mut s = "(struct".to_string();
+            for f in ty.fields.iter() {
+                s.push_str(&format!(" {}", render_field(f)));
+            }
+            s.push(')');
+            s
+        }
     };
 
     anyhow!(
@@ -226,8 +235,9 @@ fn match_heap(expected: WasmHeapType, actual: WasmHeapType, desc: &str) -> Resul
     let result = match (actual, expected) {
         // TODO: Wasm GC introduces subtyping between function types, so it will
         // no longer suffice to check whether canonicalized type IDs are equal.
-        (H::ConcreteFunc(actual), H::ConcreteFunc(expected)) => actual == expected,
         (H::ConcreteArray(actual), H::ConcreteArray(expected)) => actual == expected,
+        (H::ConcreteFunc(actual), H::ConcreteFunc(expected)) => actual == expected,
+        (H::ConcreteStruct(actual), H::ConcreteStruct(expected)) => actual == expected,
 
         (H::NoFunc, H::NoFunc) => true,
         (_, H::NoFunc) => false,
@@ -241,7 +251,16 @@ fn match_heap(expected: WasmHeapType, actual: WasmHeapType, desc: &str) -> Resul
         (H::Extern, H::Extern) => true,
         (_, H::Extern) => false,
 
-        (H::Any | H::I31 | H::Array | H::ConcreteArray(_) | H::None, H::Any) => true,
+        (
+            H::Any
+            | H::I31
+            | H::Array
+            | H::ConcreteArray(_)
+            | H::Struct
+            | H::ConcreteStruct(_)
+            | H::None,
+            H::Any,
+        ) => true,
         (_, H::Any) => false,
 
         (H::I31 | H::None, H::I31) => true,
@@ -253,6 +272,12 @@ fn match_heap(expected: WasmHeapType, actual: WasmHeapType, desc: &str) -> Resul
         (H::None, H::ConcreteArray(_)) => true,
         (_, H::ConcreteArray(_)) => false,
 
+        (H::Struct | H::ConcreteStruct(_) | H::None, H::Struct) => true,
+        (_, H::Struct) => false,
+
+        (H::None, H::ConcreteStruct(_)) => true,
+        (_, H::ConcreteStruct(_)) => false,
+
         (H::None, H::None) => true,
         (_, H::None) => false,
     };
diff --git a/crates/wasmtime/src/runtime/values.rs b/crates/wasmtime/src/runtime/values.rs
index efcd1fb99..740e2c7bf 100644
--- a/crates/wasmtime/src/runtime/values.rs
+++ b/crates/wasmtime/src/runtime/values.rs
@@ -251,7 +251,9 @@ impl Val {
                     HeapType::Any
                     | HeapType::I31
                     | HeapType::Array
-                    | HeapType::ConcreteArray(_) => {
+                    | HeapType::ConcreteArray(_)
+                    | HeapType::Struct
+                    | HeapType::ConcreteStruct(_) => {
                         AnyRef::from_raw(store, raw.get_anyref()).into()
                     }
 


Test patch:
diff --git a/ci/build-test-matrix.js b/ci/build-test-matrix.js
index 92bfedfc2..38ad67c73 100644
--- a/ci/build-test-matrix.js
+++ b/ci/build-test-matrix.js
@@ -107,7 +107,7 @@ const array = [
     "target": "riscv64gc-unknown-linux-gnu",
     "gcc_package": "gcc-riscv64-linux-gnu",
     "gcc": "riscv64-linux-gnu-gcc",
-    "qemu": "qemu-riscv64 -cpu rv64,v=true,vlen=256,vext_spec=v1.0,zba=true,zbb=true,zbc=true,zbs=true,zbkb=true,zcb=true -L /usr/riscv64-linux-gnu",
+    "qemu": "qemu-riscv64 -cpu rv64,v=true,vlen=256,vext_spec=v1.0,Zfa=true,zba=true,zbb=true,zbc=true,zbs=true,zbkb=true,zcb=true -L /usr/riscv64-linux-gnu",
     "qemu_target": "riscv64-linux-user",
     "name": "Test Linux riscv64",
     "filter": "linux-riscv64",
diff --git a/cranelift/filetests/filetests/isa/riscv64/zfa.clif b/cranelift/filetests/filetests/isa/riscv64/zfa.clif
new file mode 100644
index 000000000..60193e684
--- /dev/null
+++ b/cranelift/filetests/filetests/isa/riscv64/zfa.clif
@@ -0,0 +1,70 @@
+test compile precise-output
+set unwind_info=false
+target riscv64 has_zfa
+
+
+function %fminm_s(f32, f32) -> f32 {
+block0(v0: f32, v1: f32):
+  v2 = fmin.f32 v0, v1
+  return v2
+}
+
+; VCode:
+; block0:
+;   fminm.s fa0,fa0,fa1,rdn
+;   ret
+;
+; Disassembled:
+; block0: ; offset 0x0
+;   .byte 0x53, 0x25, 0xb5, 0x28
+;   ret
+
+
+function %fminm_d(f64, f64) -> f64 {
+block0(v0: f64, v1: f64):
+  v2 = fmin.f64 v0, v1
+  return v2
+}
+
+; VCode:
+; block0:
+;   fminm.d fa0,fa0,fa1,rdn
+;   ret
+;
+; Disassembled:
+; block0: ; offset 0x0
+;   .byte 0x53, 0x25, 0xb5, 0x2a
+;   ret
+
+function %fmaxm_s(f32, f32) -> f32 {
+block0(v0: f32, v1: f32):
+  v2 = fmax.f32 v0, v1
+  return v2
+}
+
+; VCode:
+; block0:
+;   fmaxm.s fa0,fa0,fa1,rup
+;   ret
+;
+; Disassembled:
+; block0: ; offset 0x0
+;   .byte 0x53, 0x35, 0xb5, 0x28
+;   ret
+
+function %fmaxm_d(f64, f64) -> f64 {
+block0(v0: f64, v1: f64):
+  v2 = fmax.f64 v0, v1
+  return v2
+}
+
+; VCode:
+; block0:
+;   fmaxm.d fa0,fa0,fa1,rup
+;   ret
+;
+; Disassembled:
+; block0: ; offset 0x0
+;   .byte 0x53, 0x35, 0xb5, 0x2a
+;   ret
+
diff --git a/cranelift/filetests/filetests/runtests/fmax-pseudo.clif b/cranelift/filetests/filetests/runtests/fmax-pseudo.clif
index 2abf227f8..dcc923adb 100644
--- a/cranelift/filetests/filetests/runtests/fmax-pseudo.clif
+++ b/cranelift/filetests/filetests/runtests/fmax-pseudo.clif
@@ -4,6 +4,7 @@ target x86_64
 target x86_64 has_avx
 target aarch64
 target riscv64
+target riscv64 has_zfa
 target riscv64 has_c has_zcb
 target s390x
 
diff --git a/cranelift/filetests/filetests/runtests/fmax.clif b/cranelift/filetests/filetests/runtests/fmax.clif
index da7e23e04..e6d0edc83 100644
--- a/cranelift/filetests/filetests/runtests/fmax.clif
+++ b/cranelift/filetests/filetests/runtests/fmax.clif
@@ -5,6 +5,7 @@ target x86_64 has_avx
 target aarch64
 target s390x
 target riscv64
+target riscv64 has_zfa
 target riscv64 has_c has_zcb
 
 function %fmax_f32(f32, f32) -> f32 {
diff --git a/cranelift/filetests/filetests/runtests/fmin-pseudo.clif b/cranelift/filetests/filetests/runtests/fmin-pseudo.clif
index 44ef7b197..e6b60b427 100644
--- a/cranelift/filetests/filetests/runtests/fmin-pseudo.clif
+++ b/cranelift/filetests/filetests/runtests/fmin-pseudo.clif
@@ -4,6 +4,7 @@ target x86_64
 target x86_64 has_avx
 target aarch64
 target riscv64
+target riscv64 has_zfa
 target riscv64 has_c has_zcb
 target s390x
 
diff --git a/cranelift/filetests/filetests/runtests/fmin.clif b/cranelift/filetests/filetests/runtests/fmin.clif
index 72c171896..4e261502e 100644
--- a/cranelift/filetests/filetests/runtests/fmin.clif
+++ b/cranelift/filetests/filetests/runtests/fmin.clif
@@ -5,6 +5,7 @@ target x86_64 has_avx
 target aarch64
 target s390x
 target riscv64
+target riscv64 has_zfa
 target riscv64 has_c has_zcb
 
 function %fmin_f32(f32, f32) -> f32 {
diff --git a/tests/all/types.rs b/tests/all/types.rs
index 7ce9f436d..f058f14d8 100644
--- a/tests/all/types.rs
+++ b/tests/all/types.rs
@@ -23,3 +23,81 @@ fn basic_array_types() -> Result<()> {
     }
     Ok(())
 }
+
+#[test]
+fn empty_struct_type() -> Result<()> {
+    let engine = Engine::default();
+    let struct_ty = StructType::new(&engine, [])?;
+    assert_eq!(struct_ty.fields().len(), 0);
+    assert!(struct_ty.field(0).is_none());
+    Ok(())
+}
+
+#[test]
+fn basic_struct_types() -> Result<()> {
+    let engine = Engine::default();
+
+    let field_types = || {
+        [Mutability::Const, Mutability::Var]
+            .into_iter()
+            .flat_map(|mutability| {
+                [
+                    StorageType::I8,
+                    StorageType::I16,
+                    StorageType::ValType(ValType::I32),
+                    StorageType::ValType(
+                        RefType::new(true, FuncType::new(&engine, [], []).into()).into(),
+                    ),
+                ]
+                .into_iter()
+                .map(move |storage_ty| FieldType::new(mutability, storage_ty))
+            })
+    };
+
+    let struct_ty = StructType::new(&engine, field_types())?;
+
+    assert_eq!(struct_ty.fields().len(), field_types().count());
+    for ((i, expected), actual) in field_types().enumerate().zip(struct_ty.fields()) {
+        assert!(FieldType::eq(&expected, &actual));
+        assert!(FieldType::eq(&expected, &struct_ty.field(i).unwrap()));
+    }
+    assert!(struct_ty.field(struct_ty.fields().len()).is_none());
+
+    Ok(())
+}
+
+#[test]
+fn struct_type_matches() -> Result<()> {
+    let engine = Engine::default();
+
+    let field = |heap_ty| {
+        FieldType::new(
+            Mutability::Var,
+            StorageType::ValType(RefType::new(true, heap_ty).into()),
+        )
+    };
+
+    let super_ty = StructType::new(&engine, [field(HeapType::Func)])?;
+
+    // Depth.
+    let sub_ty = StructType::new(&engine, [field(HeapType::NoFunc)])?;
+    assert!(sub_ty.matches(&super_ty));
+
+    // Width.
+    let sub_ty = StructType::new(&engine, [field(HeapType::Func), field(HeapType::Extern)])?;
+    assert!(sub_ty.matches(&super_ty));
+
+    // Depth and width.
+    let sub_ty = StructType::new(&engine, [field(HeapType::NoFunc), field(HeapType::Extern)])?;
+    assert!(sub_ty.matches(&super_ty));
+
+    // Not depth.
+    let not_sub_ty = StructType::new(&engine, [field(HeapType::Extern)])?;
+    assert!(!not_sub_ty.matches(&super_ty));
+
+    // Not width.
+    let not_sub_ty = StructType::new(&engine, [])?;
+    assert!(!not_sub_ty.matches(&super_ty));
+
+    Ok(())
+}
diff --git a/tests/misc_testsuite/gc/array-types.wast b/tests/misc_testsuite/gc/array-types.wast
index 3851f172d..83e5a2441 100644
--- a/tests/misc_testsuite/gc/array-types.wast
+++ b/tests/misc_testsuite/gc/array-types.wast
@@ -6,6 +6,7 @@
   (type (array f32))
   (type (array f64))
   (type (array anyref))
+  (type (array (ref struct)))
   (type (array (ref 0)))
   (type (array (ref null 1)))
   (type (array (mut i8)))
@@ -15,6 +16,7 @@
   (type (array (mut i32)))
   (type (array (mut i64)))
   (type (array (mut anyref)))
+  (type (array (mut (ref struct))))
   (type (array (mut (ref 0))))
   (type (array (mut (ref null i31))))
 )
diff --git a/tests/misc_testsuite/gc/struct-types.wast b/tests/misc_testsuite/gc/struct-types.wast
new file mode 100644
index 000000000..5b272e080
--- /dev/null
+++ b/tests/misc_testsuite/gc/struct-types.wast
@@ -0,0 +1,10 @@
+(module
+  (type (struct))
+  (type (struct (field)))
+  (type (struct (field i8)))
+  (type (struct (field i8 i8 i8 i8)))
+  (type (struct (field $x1 i32) (field $y1 i32)))
+  (type (struct (field i8 i16 i32 i64 f32 f64 anyref funcref (ref 0) (ref null 1))))
+  (type (struct (field i32 i64 i8) (field) (field) (field (ref null i31) anyref structref arrayref)))
+  (type (struct (field $x2 i32) (field f32 f64) (field $y2 i32)))
+)