Repository: bytecodealliance/wasmtime
Pull request #8537 (2024-05-03 15:04:41)

Use a `SecondaryMap` instead of a `HashMap` for the type-to-rec-group map
Should be a little more efficient because we can avoid hashing.

<!--
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/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/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/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)))
+)