Repository: bytecodealliance/wasm-tools
Pull request #1134 (2023-07-19 22:01:17)

GC: wasmparser: recursive type groups support
Introduce support for recursive types as part of the ongoing WebAssembly GC proposal implementation. 

Matching rules for subtypes had to be updated to accommodate recursive types.



Patch:
diff --git a/crates/wasm-mutate/src/info.rs b/crates/wasm-mutate/src/info.rs
index 54b0f646..3d2415ee 100644
--- a/crates/wasm-mutate/src/info.rs
+++ b/crates/wasm-mutate/src/info.rs
@@ -92,13 +92,19 @@ impl<'a> ModuleInfo<'a> {
                     info.section(SectionId::Type.into(), reader.range(), input_wasm);
 
                     // Save function types
-                    for ty in reader {
-                        if let Ok(st) = ty {
-                            if st.is_final || st.supertype_idx.is_some() {
+                    for rec_group in reader {
+                        if let Ok(rg) = rec_group {
+                            if rg.types().len() != 1 {
                                 return Err(Error::unsupported("GC types not supported yet"));
                             }
-                            let typeinfo = TypeInfo::try_from(st.structural_type).unwrap();
-                            info.types_map.push(typeinfo);
+                            for st in rg.types() {
+                                if st.is_final || st.supertype_idx.is_some() {
+                                    return Err(Error::unsupported("GC types not supported yet"));
+                                }
+                                let typeinfo =
+                                    TypeInfo::try_from(st.clone().structural_type).unwrap();
+                                info.types_map.push(typeinfo);
+                            }
                         }
                     }
                 }
diff --git a/crates/wasm-mutate/src/mutators/add_type.rs b/crates/wasm-mutate/src/mutators/add_type.rs
index 6de1719e..ce289412 100644
--- a/crates/wasm-mutate/src/mutators/add_type.rs
+++ b/crates/wasm-mutate/src/mutators/add_type.rs
@@ -55,28 +55,30 @@ impl Mutator for AddTypeMutator {
         if let Some(old_types) = config.info().get_type_section() {
             // Copy the existing types section over into the encoder.
             let reader = wasmparser::TypeSectionReader::new(old_types.data, 0)?;
-            for ty in reader {
-                match ty?.structural_type {
-                    wasmparser::StructuralType::Func(ty) => {
-                        let params = ty
-                            .params()
-                            .iter()
-                            .copied()
-                            .map(map_type)
-                            .collect::<Result<Vec<_>, _>>()?;
-                        let results = ty
-                            .results()
-                            .iter()
-                            .copied()
-                            .map(map_type)
-                            .collect::<Result<Vec<_>, _>>()?;
-                        types.function(params, results);
-                    }
-                    wasmparser::StructuralType::Array(_) => {
-                        return Err(Error::unsupported("Array types are not supported yet."));
-                    }
-                    wasmparser::StructuralType::Struct(_) => {
-                        return Err(Error::unsupported("Struct types are not supported yet."));
+            for rec_group in reader {
+                for ty in rec_group?.types() {
+                    match ty.clone().structural_type {
+                        wasmparser::StructuralType::Func(ty) => {
+                            let params = ty
+                                .params()
+                                .iter()
+                                .copied()
+                                .map(map_type)
+                                .collect::<Result<Vec<_>, _>>()?;
+                            let results = ty
+                                .results()
+                                .iter()
+                                .copied()
+                                .map(map_type)
+                                .collect::<Result<Vec<_>, _>>()?;
+                            types.function(params, results);
+                        }
+                        wasmparser::StructuralType::Array(_) => {
+                            return Err(Error::unsupported("Array types are not supported yet."));
+                        }
+                        wasmparser::StructuralType::Struct(_) => {
+                            return Err(Error::unsupported("Struct types are not supported yet."));
+                        }
                     }
                 }
             }
diff --git a/crates/wasm-mutate/src/mutators/remove_item.rs b/crates/wasm-mutate/src/mutators/remove_item.rs
index 96e8515f..b6e84851 100644
--- a/crates/wasm-mutate/src/mutators/remove_item.rs
+++ b/crates/wasm-mutate/src/mutators/remove_item.rs
@@ -136,7 +136,12 @@ impl RemoveItem {
                         0,
                         TypeSectionReader::new(section.data, 0)?,
                         Item::Type,
-                        |me, ty, section| me.translate_type_def(ty.structural_type, section),
+                        |me, rec_group, section| {
+                            for ty in rec_group.types() {
+                                me.translate_type_def(ty.clone().structural_type, section)?;
+                            }
+                            Ok(())
+                        },
                     )?;
                 },
 
diff --git a/crates/wasm-smith/src/core.rs b/crates/wasm-smith/src/core.rs
index e4468a8e..6943f8b8 100644
--- a/crates/wasm-smith/src/core.rs
+++ b/crates/wasm-smith/src/core.rs
@@ -553,8 +553,10 @@ impl Module {
             match payload.expect("could not parse the available import payload") {
                 wasmparser::Payload::TypeSection(type_reader) => {
                     for ty in type_reader {
-                        let ty = ty.expect("could not parse type section");
-                        available_types.push((ty.structural_type, None));
+                        let rec_group = ty.expect("could not parse type section");
+                        for ty in rec_group.types() {
+                            available_types.push((ty.clone().structural_type, None));
+                        }
                     }
                 }
                 wasmparser::Payload::ImportSection(import_reader) => {
diff --git a/crates/wasmparser/src/readers/core/types.rs b/crates/wasmparser/src/readers/core/types.rs
index 6d89c3ec..17075a07 100644
--- a/crates/wasmparser/src/readers/core/types.rs
+++ b/crates/wasmparser/src/readers/core/types.rs
@@ -13,12 +13,14 @@
  * limitations under the License.
  */
 
+use std::fmt::{self, Debug, Write};
+use std::slice;
+
 use crate::limits::{
     MAX_WASM_FUNCTION_PARAMS, MAX_WASM_FUNCTION_RETURNS, MAX_WASM_STRUCT_FIELDS,
-    MAX_WASM_SUPERTYPES,
+    MAX_WASM_SUPERTYPES, MAX_WASM_TYPES,
 };
 use crate::{BinaryReader, BinaryReaderError, FromReader, Result, SectionLimited};
-use std::fmt::{self, Debug, Write};
 
 /// Represents the types of values in a WebAssembly module.
 #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
@@ -54,7 +56,13 @@ const _: () = {
 };
 
 pub(crate) trait Inherits {
-    fn inherits<'a, F>(&self, other: &Self, type_at: &F) -> bool
+    fn inherits<'a, F>(
+        &self,
+        other: &Self,
+        self_base_idx: Option<u32>,
+        other_base_idx: Option<u32>,
+        type_at: &F,
+    ) -> bool
     where
         F: Fn(u32) -> &'a SubType;
 }
@@ -99,12 +107,20 @@ impl ValType {
 }
 
 impl Inherits for ValType {
-    fn inherits<'a, F>(&self, other: &Self, type_at: &F) -> bool
+    fn inherits<'a, F>(
+        &self,
+        other: &Self,
+        self_base_idx: Option<u32>,
+        other_base_idx: Option<u32>,
+        type_at: &F,
+    ) -> bool
     where
         F: Fn(u32) -> &'a SubType,
     {
         match (self, other) {
-            (Self::Ref(r1), Self::Ref(r2)) => r1.inherits(r2, type_at),
+            (Self::Ref(r1), Self::Ref(r2)) => {
+                r1.inherits(r2, self_base_idx, other_base_idx, type_at)
+            }
             (
                 s @ (Self::I32 | Self::I64 | Self::F32 | Self::F64 | Self::V128 | Self::Ref(_)),
                 o,
@@ -535,13 +551,24 @@ impl RefType {
 }
 
 impl Inherits for RefType {
-    fn inherits<'a, F>(&self, other: &Self, type_at: &F) -> bool
+    fn inherits<'a, F>(
+        &self,
+        other: &Self,
+        self_base_idx: Option<u32>,
+        other_base_idx: Option<u32>,
+        type_at: &F,
+    ) -> bool
     where
         F: Fn(u32) -> &'a SubType,
     {
         self == other
             || ((other.is_nullable() || !self.is_nullable())
-                && self.heap_type().inherits(&other.heap_type(), type_at))
+                && self.heap_type().inherits(
+                    &other.heap_type(),
+                    self_base_idx,
+                    other_base_idx,
+                    type_at,
+                ))
     }
 }
 
@@ -630,14 +657,33 @@ pub enum HeapType {
     I31,
 }
 
+fn choose_base(base_idx: Option<u32>, idx: u32) -> Option<u32> {
+    if base_idx == None || idx == base_idx.unwrap() || idx < base_idx.unwrap() {
+        Some(idx)
+    } else {
+        base_idx
+    }
+}
+
 impl Inherits for HeapType {
-    fn inherits<'a, F>(&self, other: &Self, type_at: &F) -> bool
+    fn inherits<'a, F>(
+        &self,
+        other: &Self,
+        self_base_idx: Option<u32>,
+        other_base_idx: Option<u32>,
+        type_at: &F,
+    ) -> bool
     where
         F: Fn(u32) -> &'a SubType,
     {
         match (self, other) {
             (HeapType::Indexed(a), HeapType::Indexed(b)) => {
-                a == b || type_at(*a).inherits(type_at(*b), type_at)
+                a == b || {
+                    let a_base = choose_base(self_base_idx, *a);
+                    let b_base = choose_base(other_base_idx, *b);
+                    (a_base == self_base_idx && b_base == other_base_idx)
+                        || type_at(*a).inherits(type_at(*b), a_base, b_base, type_at)
+                }
             }
             (HeapType::Indexed(a), HeapType::Func) => match type_at(*a).structural_type {
                 StructuralType::Func(_) => true,
@@ -771,15 +817,48 @@ pub struct SubType {
     pub structural_type: StructuralType,
 }
 
+/// Represents a recursive type group in a WebAssembly module.
+#[derive(Debug, Clone)]
+pub struct RecGroup {
+    items: RecGroupItems,
+}
+
+#[derive(Debug, Clone)]
+enum RecGroupItems {
+    /// The list of subtypes in the recursive type group.
+    Many(Vec<SubType>),
+    /// A single subtype in the recursive type group.
+    Single(SubType),
+}
+
+impl RecGroup {
+    /// Returns the list of subtypes in the recursive type group.
+    pub fn types(&self) -> &[SubType] {
+        match &self.items {
+            RecGroupItems::Many(types) => types,
+            RecGroupItems::Single(ty) => slice::from_ref(ty),
+        }
+    }
+}
+
 impl Inherits for SubType {
-    fn inherits<'a, F>(&self, other: &Self, type_at: &F) -> bool
+    fn inherits<'a, F>(
+        &self,
+        other: &Self,
+        self_base_idx: Option<u32>,
+        other_base_idx: Option<u32>,
+        type_at: &F,
+    ) -> bool
     where
         F: Fn(u32) -> &'a SubType,
     {
         !other.is_final
-            && self
-                .structural_type
-                .inherits(&other.structural_type, type_at)
+            && self.structural_type.inherits(
+                &other.structural_type,
+                self_base_idx,
+                other_base_idx,
+                type_at,
+            )
     }
 }
 
@@ -813,14 +892,26 @@ pub struct StructType {
 }
 
 impl Inherits for StructuralType {
-    fn inherits<'a, F>(&self, other: &Self, type_at: &F) -> bool
+    fn inherits<'a, F>(
+        &self,
+        other: &Self,
+        self_base_idx: Option<u32>,
+        other_base_idx: Option<u32>,
+        type_at: &F,
+    ) -> bool
     where
         F: Fn(u32) -> &'a SubType,
     {
         match (self, other) {
-            (StructuralType::Func(a), StructuralType::Func(b)) => a.inherits(b, type_at),
-            (StructuralType::Array(a), StructuralType::Array(b)) => a.inherits(b, type_at),
-            (StructuralType::Struct(a), StructuralType::Struct(b)) => a.inherits(b, type_at),
+            (StructuralType::Func(a), StructuralType::Func(b)) => {
+                a.inherits(b, self_base_idx, other_base_idx, type_at)
+            }
+            (StructuralType::Array(a), StructuralType::Array(b)) => {
+                a.inherits(b, self_base_idx, other_base_idx, type_at)
+            }
+            (StructuralType::Struct(a), StructuralType::Struct(b)) => {
+                a.inherits(b, self_base_idx, other_base_idx, type_at)
+            }
             (StructuralType::Func(_), _) => false,
             (StructuralType::Array(_), _) => false,
             (StructuralType::Struct(_), _) => false,
@@ -900,7 +991,13 @@ impl FuncType {
 }
 
 impl Inherits for FuncType {
-    fn inherits<'a, F>(&self, other: &Self, type_at: &F) -> bool
+    fn inherits<'a, F>(
+        &self,
+        other: &Self,
+        self_base_idx: Option<u32>,
+        other_base_idx: Option<u32>,
+        type_at: &F,
+    ) -> bool
     where
         F: Fn(u32) -> &'a SubType,
     {
@@ -912,47 +1009,78 @@ impl Inherits for FuncType {
                 .params()
                 .iter()
                 .zip(other.params())
-                .fold(true, |r, (a, b)| r && b.inherits(a, type_at))
+                .fold(true, |r, (a, b)| r && b.inherits(a, self_base_idx, other_base_idx, type_at))
             && self
                 .results()
                 .iter()
                 .zip(other.results())
-                .fold(true, |r, (a, b)| r && a.inherits(b, type_at))
+                .fold(true, |r, (a, b)| r && a.inherits(b, self_base_idx, other_base_idx, type_at))
     }
 }
 
 impl Inherits for ArrayType {
-    fn inherits<'a, F>(&self, other: &Self, type_at: &F) -> bool
+    fn inherits<'a, F>(
+        &self,
+        other: &Self,
+        self_base_idx: Option<u32>,
+        other_base_idx: Option<u32>,
+        type_at: &F,
+    ) -> bool
     where
         F: Fn(u32) -> &'a SubType,
     {
-        self.0.inherits(&other.0, type_at)
+        self.0
+            .inherits(&other.0, self_base_idx, other_base_idx, type_at)
     }
 }
 
 impl Inherits for FieldType {
-    fn inherits<'a, F>(&self, other: &Self, type_at: &F) -> bool
+    fn inherits<'a, F>(
+        &self,
+        other: &Self,
+        self_base_idx: Option<u32>,
+        other_base_idx: Option<u32>,
+        type_at: &F,
+    ) -> bool
     where
         F: Fn(u32) -> &'a SubType,
     {
-        (other.mutable || !self.mutable) && self.element_type.inherits(&other.element_type, type_at)
+        (other.mutable || !self.mutable)
+            && self.element_type.inherits(
+                &other.element_type,
+                self_base_idx,
+                other_base_idx,
+                type_at,
+            )
     }
 }
 
 impl Inherits for StorageType {
-    fn inherits<'a, F>(&self, other: &Self, type_at: &F) -> bool
+    fn inherits<'a, F>(
+        &self,
+        other: &Self,
+        self_base_idx: Option<u32>,
+        other_base_idx: Option<u32>,
+        type_at: &F,
+    ) -> bool
     where
         F: Fn(u32) -> &'a SubType,
     {
         match (self, other) {
-            (Self::Val(a), Self::Val(b)) => a.inherits(b, type_at),
+            (Self::Val(a), Self::Val(b)) => a.inherits(b, self_base_idx, other_base_idx, type_at),
             (a @ (Self::I8 | Self::I16 | Self::Val(_)), b) => a == b,
         }
     }
 }
 
 impl Inherits for StructType {
-    fn inherits<'a, F>(&self, other: &Self, type_at: &F) -> bool
+    fn inherits<'a, F>(
+        &self,
+        other: &Self,
+        self_base_idx: Option<u32>,
+        other_base_idx: Option<u32>,
+        type_at: &F,
+    ) -> bool
     where
         F: Fn(u32) -> &'a SubType,
     {
@@ -962,7 +1090,9 @@ impl Inherits for StructType {
                 .fields
                 .iter()
                 .zip(other.fields.iter())
-                .fold(true, |r, (a, b)| r && a.inherits(b, type_at))
+                .fold(true, |r, (a, b)| {
+                    r && a.inherits(b, self_base_idx, other_base_idx, type_at)
+                })
     }
 }
 
@@ -1044,7 +1174,7 @@ pub struct TagType {
 }
 
 /// A reader for the type section of a WebAssembly module.
-pub type TypeSectionReader<'a> = SectionLimited<'a, SubType>;
+pub type TypeSectionReader<'a> = SectionLimited<'a, RecGroup>;
 
 impl<'a> FromReader<'a> for StructuralType {
     fn from_reader(reader: &mut BinaryReader<'a>) -> Result<Self> {
@@ -1064,6 +1194,23 @@ fn read_structural_type(
     })
 }
 
+impl<'a> FromReader<'a> for RecGroup {
+    fn from_reader(reader: &mut BinaryReader<'a>) -> Result<Self> {
+        Ok(match reader.peek()? {
+            0x4f => {
+                reader.read_u8()?;
+                let types = reader.read_iter(MAX_WASM_TYPES, "rec group types")?;
+                RecGroup {
+                    items: RecGroupItems::Many(types.collect::<Result<_>>()?),
+                }
+            }
+            _ => RecGroup {
+                items: RecGroupItems::Single(reader.read()?),
+            },
+        })
+    }
+}
+
 impl<'a> FromReader<'a> for SubType {
     fn from_reader(reader: &mut BinaryReader<'a>) -> Result<Self> {
         let pos = reader.original_position();
diff --git a/crates/wasmparser/src/validator.rs b/crates/wasmparser/src/validator.rs
index 48f404ff..2b747f83 100644
--- a/crates/wasmparser/src/validator.rs
+++ b/crates/wasmparser/src/validator.rs
@@ -611,7 +611,8 @@ impl Validator {
                 state
                     .module
                     .assert_mut()
-                    .add_type(def, features, types, offset, false /* checked above */)
+                    .add_types(def.types(), features, types, offset, true)?;
+                Ok(())
             },
         )
     }
diff --git a/crates/wasmparser/src/validator/component.rs b/crates/wasmparser/src/validator/component.rs
index 9c5eaa15..b0d98e82 100644
--- a/crates/wasmparser/src/validator/component.rs
+++ b/crates/wasmparser/src/validator/component.rs
@@ -1427,7 +1427,7 @@ impl ComponentState {
         for decl in decls {
             match decl {
                 crate::ModuleTypeDeclaration::Type(ty) => {
-                    state.add_type(ty, features, types, offset, true)?;
+                    state.add_types(&[ty], features, types, offset, true)?;
                 }
                 crate::ModuleTypeDeclaration::Export { name, ty } => {
                     let ty = state.check_type_ref(&ty, features, types, offset)?;
diff --git a/crates/wasmparser/src/validator/core.rs b/crates/wasmparser/src/validator/core.rs
index 9ee45044..c123b8b0 100644
--- a/crates/wasmparser/src/validator/core.rs
+++ b/crates/wasmparser/src/validator/core.rs
@@ -1,10 +1,10 @@
 //! State relating to validating a WebAssembly module.
 //!
-use super::{
-    check_max, combine_type_sizes,
-    operators::{ty_to_str, OperatorValidator, OperatorValidatorAllocations},
-    types::{EntityType, Type, TypeAlloc, TypeId, TypeList},
-};
+use std::mem;
+use std::{collections::HashSet, sync::Arc};
+
+use indexmap::IndexMap;
+
 use crate::limits::*;
 use crate::readers::Inherits;
 use crate::validator::core::arc::MaybeOwned;
@@ -14,9 +14,12 @@ use crate::{
     SubType, Table, TableInit, TableType, TagType, TypeRef, ValType, VisitOperator, WasmFeatures,
     WasmModuleResources,
 };
-use indexmap::IndexMap;
-use std::mem;
-use std::{collections::HashSet, sync::Arc};
+
+use super::{
+    check_max, combine_type_sizes,
+    operators::{ty_to_str, OperatorValidator, OperatorValidatorAllocations},
+    types::{EntityType, Type, TypeAlloc, TypeId, TypeList},
+};
 
 // Section order for WebAssembly modules.
 //
@@ -493,56 +496,77 @@ pub(crate) struct Module {
 }
 
 impl Module {
-    pub fn add_type(
+    pub fn add_types(
         &mut self,
-        ty: SubType,
+        types_added: &[SubType],
         features: &WasmFeatures,
         types: &mut TypeAlloc,
         offset: usize,
         check_limit: bool,
     ) -> Result<()> {
         if check_limit {
-            check_max(self.types.len(), 1, MAX_WASM_TYPES, "types", offset)?;
+            check_max(
+                self.types.len(),
+                types_added.len() as u32,
+                MAX_WASM_TYPES,
+                "types",
+                offset,
+            )?;
         }
-        let ty = self.check_subtype(ty, features, types, offset)?;
+        let idx_types: Vec<_> = types_added
+            .iter()
+            .map(|ty| {
+                let id = types.push_ty(Type::Sub(ty.clone()));
+                if features.gc {
+                    // make types in a rec group resolvable by index before validation:
+                    // this is needed to support recursive types in the GC proposal
+                    self.types.push(id);
+                }
+                (id, ty)
+            })
+            .collect();
 
-        let id = types.push_ty(ty);
-        self.types.push(id);
+        for (id, ty) in idx_types {
+            self.check_subtype(id.index as u32, ty.clone(), features, types, offset)?;
+            if !features.gc {
+                self.types.push(id);
+            }
+        }
         Ok(())
     }
 
     fn check_subtype(
         &mut self,
+        type_index: u32,
         ty: SubType,
         features: &WasmFeatures,
         types: &mut TypeAlloc,
         offset: usize,
     ) -> Result<Type> {
         if !features.gc && (ty.is_final || ty.supertype_idx.is_some()) {
-            return Err(BinaryReaderError::new(
-                "gc proposal must be enabled to use subtypes",
-                offset,
-            ));
+            bail!(offset, "gc proposal must be enabled to use subtypes");
         }
 
         self.check_structural_type(&ty.structural_type, features, offset)?;
 
-        if let Some(type_index) = ty.supertype_idx {
+        if let Some(supertype_index) = ty.supertype_idx {
             // Check the supertype exists, is not final, and the subtype matches it.
-            match self.type_at(types, type_index, offset)? {
+            if supertype_index >= type_index {
+                bail!(
+                    offset,
+                    "unknown type {type_index}: type index out of bounds"
+                );
+            }
+            match self.type_at(types, supertype_index, offset)? {
                 Type::Sub(st) => {
-                    if !&ty.inherits(st, &|idx| self.subtype_at(types, idx, offset).unwrap()) {
-                        return Err(BinaryReaderError::new(
-                            "subtype must match supertype",
-                            offset,
-                        ));
+                    if !&ty.inherits(st, Some(type_index), Some(supertype_index), &|idx| {
+                        self.subtype_at(types, idx, offset).unwrap()
+                    }) {
+                        bail!(offset, "subtype must match supertype");
                     }
                 }
                 _ => {
-                    return Err(BinaryReaderError::new(
-                        "supertype must be a non-final subtype itself",
-                        offset,
-                    ));
+                    bail!(offset, "supertype must be a non-final subtype itself");
                 }
             };
         }
@@ -926,7 +950,9 @@ impl Module {
     /// E.g. a non-nullable reference can be assigned to a nullable reference, but not vice versa.
     /// Or an indexed func ref is assignable to a generic func ref, but not vice versa.
     pub(crate) fn matches(&self, ty1: ValType, ty2: ValType, types: &TypeList) -> bool {
-        ty1.inherits(&ty2, &|idx| self.subtype_at(types, idx, 0).unwrap())
+        ty1.inherits(&ty2, None, None, &|idx| {
+            self.subtype_at(types, idx, 0).unwrap()
+        })
     }
 
     fn check_tag_type(
diff --git a/crates/wasmprinter/src/lib.rs b/crates/wasmprinter/src/lib.rs
index e68dc703..4a87795c 100644
--- a/crates/wasmprinter/src/lib.rs
+++ b/crates/wasmprinter/src/lib.rs
@@ -662,6 +662,16 @@ impl Printer {
         Ok(())
     }
 
+    fn print_rec(&mut self, state: &mut State, offset: usize, rg: RecGroup) -> Result<()> {
+        self.start_group("rec");
+        for ty in rg.types() {
+            self.newline(offset + 2);
+            self.print_type(state, ty.clone())?;
+        }
+        self.end_group(); // `rec`
+        Ok(())
+    }
+
     fn print_type(&mut self, state: &mut State, ty: SubType) -> Result<()> {
         self.start_group("type ");
         self.print_name(&state.core.type_names, state.core.types.len() as u32)?;
@@ -730,9 +740,13 @@ impl Printer {
 
     fn print_types(&mut self, state: &mut State, parser: TypeSectionReader<'_>) -> Result<()> {
         for ty in parser.into_iter_with_offsets() {
-            let (offset, ty) = ty?;
+            let (offset, rec_group) = ty?;
             self.newline(offset);
-            self.print_type(state, ty)?;
+            if rec_group.types().len() == 1 {
+                self.print_type(state, rec_group.types()[0].clone())?;
+            } else {
+                self.print_rec(state, offset, rec_group)?;
+            }
         }
 
         Ok(())
diff --git a/crates/wit-component/src/gc.rs b/crates/wit-component/src/gc.rs
index 3e85e00e..49a01471 100644
--- a/crates/wit-component/src/gc.rs
+++ b/crates/wit-component/src/gc.rs
@@ -245,8 +245,10 @@ impl<'a> Module<'a> {
                 }
                 Payload::End(_) => {}
                 Payload::TypeSection(s) => {
-                    for ty in s {
-                        self.types.push(ty?);
+                    for rec_group in s {
+                        for ty in rec_group?.types() {
+                            self.types.push(ty.clone());
+                        }
                     }
                 }
                 Payload::ImportSection(s) => {
diff --git a/crates/wit-component/src/linking/metadata.rs b/crates/wit-component/src/linking/metadata.rs
index d1f1d2ed..63a487f8 100644
--- a/crates/wit-component/src/linking/metadata.rs
+++ b/crates/wit-component/src/linking/metadata.rs
@@ -344,8 +344,16 @@ impl<'a> Metadata<'a> {
                 Payload::TypeSection(reader) => {
                     types = reader
                         .into_iter()
+                        .flat_map(|r| match r {
+                            Err(e) => vec![Err(e)],
+                            Ok(rg) => rg
+                                .types()
+                                .iter()
+                                .map(|st| Ok(st.clone()))
+                                .collect::<Vec<_>>(),
+                        })
                         .filter_map(|r| {
-                            r.map(|ty| match ty.structural_type {
+                            r.map(|ty| match ty.clone().structural_type {
                                 StructuralType::Func(ty) => Some(ty),
                                 StructuralType::Array(_) | StructuralType::Struct(_) => None,
                             })


Test patch:
diff --git a/crates/wasm-smith/tests/core.rs b/crates/wasm-smith/tests/core.rs
index c63b9644..ada3efab 100644
--- a/crates/wasm-smith/tests/core.rs
+++ b/crates/wasm-smith/tests/core.rs
@@ -129,14 +129,16 @@ fn smoke_test_imports_config() {
                 let payload = payload.unwrap();
                 if let wasmparser::Payload::TypeSection(rdr) = payload {
                     // Gather the signature types to later check function types against.
-                    for ty in rdr {
-                        match ty.unwrap().structural_type {
-                            wasmparser::StructuralType::Func(ft) => sig_types.push(ft),
-                            wasmparser::StructuralType::Array(_) => {
-                                unimplemented!("Array types are not supported yet.")
-                            }
-                            wasmparser::StructuralType::Struct(_) => {
-                                unimplemented!("Struct types are not supported yet.")
+                    for rec_group in rdr {
+                        for ty in rec_group.unwrap().types() {
+                            match ty.clone().structural_type {
+                                wasmparser::StructuralType::Func(ft) => sig_types.push(ft),
+                                wasmparser::StructuralType::Array(_) => {
+                                    unimplemented!("Array types are not supported yet.")
+                                }
+                                wasmparser::StructuralType::Struct(_) => {
+                                    unimplemented!("Struct types are not supported yet.")
+                                }
                             }
                         }
                     }
diff --git a/tests/cli/dump/alias.wat.stdout b/tests/cli/dump/alias.wat.stdout
index 3f11da98..b84d1a53 100644
--- a/tests/cli/dump/alias.wat.stdout
+++ b/tests/cli/dump/alias.wat.stdout
@@ -30,7 +30,7 @@
         | 01 00 00 00
    0x57 | 01 04       | type section
    0x59 | 01          | 1 count
-   0x5a | 60 00 00    | [type 0] SubType { is_final: false, supertype_idx: None, structural_type: Func(FuncType { params: [], returns: [] }) }
+   0x5a | 60 00 00    | [type 0] RecGroup { items: Single(SubType { is_final: false, supertype_idx: None, structural_type: Func(FuncType { params: [], returns: [] }) }) }
    0x5d | 03 02       | func section
    0x5f | 01          | 1 count
    0x60 | 00          | [func 0] type 0
diff --git a/tests/cli/dump/alias2.wat.stdout b/tests/cli/dump/alias2.wat.stdout
index 661b1d7a..08bee8f6 100644
--- a/tests/cli/dump/alias2.wat.stdout
+++ b/tests/cli/dump/alias2.wat.stdout
@@ -126,7 +126,7 @@
          | 01 00 00 00
    0x158 | 01 04       | type section
    0x15a | 01          | 1 count
-   0x15b | 60 00 00    | [type 0] SubType { is_final: false, supertype_idx: None, structural_type: Func(FuncType { params: [], returns: [] }) }
+   0x15b | 60 00 00    | [type 0] RecGroup { items: Single(SubType { is_final: false, supertype_idx: None, structural_type: Func(FuncType { params: [], returns: [] }) }) }
    0x15e | 03 02       | func section
    0x160 | 01          | 1 count
    0x161 | 00          | [func 0] type 0
@@ -162,7 +162,7 @@
          | 01 00 00 00
    0x1a2 | 01 04       | type section
    0x1a4 | 01          | 1 count
-   0x1a5 | 60 00 00    | [type 0] SubType { is_final: false, supertype_idx: None, structural_type: Func(FuncType { params: [], returns: [] }) }
+   0x1a5 | 60 00 00    | [type 0] RecGroup { items: Single(SubType { is_final: false, supertype_idx: None, structural_type: Func(FuncType { params: [], returns: [] }) }) }
    0x1a8 | 02 19       | import section
    0x1aa | 04          | 4 count
    0x1ab | 00 01 31 00 | import [func 0] Import { module: "", name: "1", ty: Func(0) }
diff --git a/tests/cli/dump/blockty.wat.stdout b/tests/cli/dump/blockty.wat.stdout
index b17b3908..48152cde 100644
--- a/tests/cli/dump/blockty.wat.stdout
+++ b/tests/cli/dump/blockty.wat.stdout
@@ -2,12 +2,12 @@
       | 01 00 00 00
   0x8 | 01 17       | type section
   0xa | 05          | 5 count
-  0xb | 60 00 00    | [type 0] SubType { is_final: false, supertype_idx: None, structural_type: Func(FuncType { params: [], returns: [] }) }
-  0xe | 60 00 01 7f | [type 1] SubType { is_final: false, supertype_idx: None, structural_type: Func(FuncType { params: [], returns: [I32] }) }
- 0x12 | 60 01 7f 00 | [type 2] SubType { is_final: false, supertype_idx: None, structural_type: Func(FuncType { params: [I32], returns: [] }) }
- 0x16 | 60 01 7f 01 | [type 3] SubType { is_final: false, supertype_idx: None, structural_type: Func(FuncType { params: [I32], returns: [I32] }) }
+  0xb | 60 00 00    | [type 0] RecGroup { items: Single(SubType { is_final: false, supertype_idx: None, structural_type: Func(FuncType { params: [], returns: [] }) }) }
+  0xe | 60 00 01 7f | [type 1] RecGroup { items: Single(SubType { is_final: false, supertype_idx: None, structural_type: Func(FuncType { params: [], returns: [I32] }) }) }
+ 0x12 | 60 01 7f 00 | [type 2] RecGroup { items: Single(SubType { is_final: false, supertype_idx: None, structural_type: Func(FuncType { params: [I32], returns: [] }) }) }
+ 0x16 | 60 01 7f 01 | [type 3] RecGroup { items: Single(SubType { is_final: false, supertype_idx: None, structural_type: Func(FuncType { params: [I32], returns: [I32] }) }) }
       | 7f         
- 0x1b | 60 01 7f 02 | [type 4] SubType { is_final: false, supertype_idx: None, structural_type: Func(FuncType { params: [I32], returns: [I32, I32] }) }
+ 0x1b | 60 01 7f 02 | [type 4] RecGroup { items: Single(SubType { is_final: false, supertype_idx: None, structural_type: Func(FuncType { params: [I32], returns: [I32, I32] }) }) }
       | 7f 7f      
  0x21 | 03 02       | func section
  0x23 | 01          | 1 count
diff --git a/tests/cli/dump/bundled.wat.stdout b/tests/cli/dump/bundled.wat.stdout
index e5c9b097..f2af8832 100644
--- a/tests/cli/dump/bundled.wat.stdout
+++ b/tests/cli/dump/bundled.wat.stdout
@@ -25,7 +25,7 @@
          | 01 00 00 00
     0x54 | 01 09       | type section
     0x56 | 01          | 1 count
-    0x57 | 60 04 7f 7f | [type 0] SubType { is_final: false, supertype_idx: None, structural_type: Func(FuncType { params: [I32, I32, I32, I32], returns: [I32] }) }
+    0x57 | 60 04 7f 7f | [type 0] RecGroup { items: Single(SubType { is_final: false, supertype_idx: None, structural_type: Func(FuncType { params: [I32, I32, I32, I32], returns: [I32] }) }) }
          | 7f 7f 01 7f
     0x5f | 03 02       | func section
     0x61 | 01          | 1 count
@@ -61,9 +61,9 @@
          | 01 00 00 00
     0xa0 | 01 09       | type section
     0xa2 | 02          | 2 count
-    0xa3 | 60 02 7f 7f | [type 0] SubType { is_final: false, supertype_idx: None, structural_type: Func(FuncType { params: [I32, I32], returns: [] }) }
+    0xa3 | 60 02 7f 7f | [type 0] RecGroup { items: Single(SubType { is_final: false, supertype_idx: None, structural_type: Func(FuncType { params: [I32, I32], returns: [] }) }) }
          | 00         
-    0xa8 | 60 00 00    | [type 1] SubType { is_final: false, supertype_idx: None, structural_type: Func(FuncType { params: [], returns: [] }) }
+    0xa8 | 60 00 00    | [type 1] RecGroup { items: Single(SubType { is_final: false, supertype_idx: None, structural_type: Func(FuncType { params: [], returns: [] }) }) }
     0xab | 02 12       | import section
     0xad | 01          | 1 count
     0xae | 09 77 61 73 | import [func 0] Import { module: "wasi-file", name: "read", ty: Func(0) }
@@ -103,9 +103,9 @@
          | 01 00 00 00
    0x101 | 01 0c       | type section
    0x103 | 02          | 2 count
-   0x104 | 60 02 7f 7f | [type 0] SubType { is_final: false, supertype_idx: None, structural_type: Func(FuncType { params: [I32, I32], returns: [] }) }
+   0x104 | 60 02 7f 7f | [type 0] RecGroup { items: Single(SubType { is_final: false, supertype_idx: None, structural_type: Func(FuncType { params: [I32, I32], returns: [] }) }) }
          | 00         
-   0x109 | 60 03 7f 7f | [type 1] SubType { is_final: false, supertype_idx: None, structural_type: Func(FuncType { params: [I32, I32, I32], returns: [] }) }
+   0x109 | 60 03 7f 7f | [type 1] RecGroup { items: Single(SubType { is_final: false, supertype_idx: None, structural_type: Func(FuncType { params: [I32, I32, I32], returns: [] }) }) }
          | 7f 00      
    0x10f | 02 12       | import section
    0x111 | 01          | 1 count
diff --git a/tests/cli/dump/component-expand-bundle.wat.stdout b/tests/cli/dump/component-expand-bundle.wat.stdout
index 985491a8..ef3b0124 100644
--- a/tests/cli/dump/component-expand-bundle.wat.stdout
+++ b/tests/cli/dump/component-expand-bundle.wat.stdout
@@ -5,7 +5,7 @@
         | 01 00 00 00
    0x12 | 01 04       | type section
    0x14 | 01          | 1 count
-   0x15 | 60 00 00    | [type 0] SubType { is_final: false, supertype_idx: None, structural_type: Func(FuncType { params: [], returns: [] }) }
+   0x15 | 60 00 00    | [type 0] RecGroup { items: Single(SubType { is_final: false, supertype_idx: None, structural_type: Func(FuncType { params: [], returns: [] }) }) }
    0x18 | 03 02       | func section
    0x1a | 01          | 1 count
    0x1b | 00          | [func 0] type 0
@@ -28,7 +28,7 @@
         | 01 00 00 00
    0x3d | 01 04       | type section
    0x3f | 01          | 1 count
-   0x40 | 60 00 00    | [type 0] SubType { is_final: false, supertype_idx: None, structural_type: Func(FuncType { params: [], returns: [] }) }
+   0x40 | 60 00 00    | [type 0] RecGroup { items: Single(SubType { is_final: false, supertype_idx: None, structural_type: Func(FuncType { params: [], returns: [] }) }) }
    0x43 | 02 06       | import section
    0x45 | 01          | 1 count
    0x46 | 00 01 61 00 | import [func 0] Import { module: "", name: "a", ty: Func(0) }
diff --git a/tests/cli/dump/import-modules.wat.stdout b/tests/cli/dump/import-modules.wat.stdout
index 3006c2a2..8b2cae6e 100644
--- a/tests/cli/dump/import-modules.wat.stdout
+++ b/tests/cli/dump/import-modules.wat.stdout
@@ -14,7 +14,7 @@
         | 01 00 00 00
    0x2a | 01 04       | type section
    0x2c | 01          | 1 count
-   0x2d | 60 00 00    | [type 0] SubType { is_final: false, supertype_idx: None, structural_type: Func(FuncType { params: [], returns: [] }) }
+   0x2d | 60 00 00    | [type 0] RecGroup { items: Single(SubType { is_final: false, supertype_idx: None, structural_type: Func(FuncType { params: [], returns: [] }) }) }
    0x30 | 03 02       | func section
    0x32 | 01          | 1 count
    0x33 | 00          | [func 0] type 0
diff --git a/tests/cli/dump/names.wat.stdout b/tests/cli/dump/names.wat.stdout
index a8e33bb1..96f641bf 100644
--- a/tests/cli/dump/names.wat.stdout
+++ b/tests/cli/dump/names.wat.stdout
@@ -2,7 +2,7 @@
       | 01 00 00 00
   0x8 | 01 05       | type section
   0xa | 01          | 1 count
-  0xb | 60 01 7f 00 | [type 0] SubType { is_final: false, supertype_idx: None, structural_type: Func(FuncType { params: [I32], returns: [] }) }
+  0xb | 60 01 7f 00 | [type 0] RecGroup { items: Single(SubType { is_final: false, supertype_idx: None, structural_type: Func(FuncType { params: [I32], returns: [] }) }) }
   0xf | 03 02       | func section
  0x11 | 01          | 1 count
  0x12 | 00          | [func 0] type 0
diff --git a/tests/cli/dump/select.wat.stdout b/tests/cli/dump/select.wat.stdout
index fd63bd00..3a6212e1 100644
--- a/tests/cli/dump/select.wat.stdout
+++ b/tests/cli/dump/select.wat.stdout
@@ -2,7 +2,7 @@
       | 01 00 00 00
   0x8 | 01 04       | type section
   0xa | 01          | 1 count
-  0xb | 60 00 00    | [type 0] SubType { is_final: false, supertype_idx: None, structural_type: Func(FuncType { params: [], returns: [] }) }
+  0xb | 60 00 00    | [type 0] RecGroup { items: Single(SubType { is_final: false, supertype_idx: None, structural_type: Func(FuncType { params: [], returns: [] }) }) }
   0xe | 03 02       | func section
  0x10 | 01          | 1 count
  0x11 | 00          | [func 0] type 0
diff --git a/tests/cli/dump/simple.wat.stdout b/tests/cli/dump/simple.wat.stdout
index b7e11a87..2cfb2d3e 100644
--- a/tests/cli/dump/simple.wat.stdout
+++ b/tests/cli/dump/simple.wat.stdout
@@ -2,8 +2,8 @@
       | 01 00 00 00
   0x8 | 01 08       | type section
   0xa | 02          | 2 count
-  0xb | 60 01 7f 00 | [type 0] SubType { is_final: false, supertype_idx: None, structural_type: Func(FuncType { params: [I32], returns: [] }) }
-  0xf | 60 00 00    | [type 1] SubType { is_final: false, supertype_idx: None, structural_type: Func(FuncType { params: [], returns: [] }) }
+  0xb | 60 01 7f 00 | [type 0] RecGroup { items: Single(SubType { is_final: false, supertype_idx: None, structural_type: Func(FuncType { params: [I32], returns: [] }) }) }
+  0xf | 60 00 00    | [type 1] RecGroup { items: Single(SubType { is_final: false, supertype_idx: None, structural_type: Func(FuncType { params: [], returns: [] }) }) }
  0x12 | 02 07       | import section
  0x14 | 01          | 1 count
  0x15 | 01 6d 01 6e | import [func 0] Import { module: "m", name: "n", ty: Func(0) }
diff --git a/tests/cli/dump/try-delegate.wat.stdout b/tests/cli/dump/try-delegate.wat.stdout
index c74f4765..dc09a05d 100644
--- a/tests/cli/dump/try-delegate.wat.stdout
+++ b/tests/cli/dump/try-delegate.wat.stdout
@@ -2,7 +2,7 @@
       | 01 00 00 00
   0x8 | 01 04       | type section
   0xa | 01          | 1 count
-  0xb | 60 00 00    | [type 0] SubType { is_final: false, supertype_idx: None, structural_type: Func(FuncType { params: [], returns: [] }) }
+  0xb | 60 00 00    | [type 0] RecGroup { items: Single(SubType { is_final: false, supertype_idx: None, structural_type: Func(FuncType { params: [], returns: [] }) }) }
   0xe | 03 02       | func section
  0x10 | 01          | 1 count
  0x11 | 00          | [func 0] type 0
diff --git a/tests/local/gc/gc-rec-groups-invalid.wast b/tests/local/gc/gc-rec-groups-invalid.wast
new file mode 100644
index 00000000..b2de1893
--- /dev/null
+++ b/tests/local/gc/gc-rec-groups-invalid.wast
@@ -0,0 +1,16 @@
+;; --enable-gc
+
+(assert_invalid
+  (module
+    (rec
+      (type $t1 (struct (field (ref $t2))))
+      (type $t2 (struct (field (ref $t3)))) ;; <--- unknown type
+    )
+
+    (rec
+      (type $t3 (struct (field (ref $t4))))
+      (type $t4 (sub $t2 (struct (field (ref $t3)))))
+    )
+  )
+  "unknown type"
+)
diff --git a/tests/local/gc/gc-rec-sub.wat b/tests/local/gc/gc-rec-sub.wat
index cfea4f73..c4deded8 100644
--- a/tests/local/gc/gc-rec-sub.wat
+++ b/tests/local/gc/gc-rec-sub.wat
@@ -39,4 +39,16 @@
     (type (sub final $a (func)))
   )
   (type (sub $a (func)))
+
+  (rec
+    (type $t1 (struct (field (ref $t2))))
+    (type $t2 (struct (field (ref $t1))))
+  )
+
+  (rec
+    (type $t3 (struct (field (ref $t4))))
+    (type $t4 (sub $t2 (struct (field (ref $t3)))))
+  )
+
+  (rec)
 )
diff --git a/tests/local/gc/type-equivalence.wast b/tests/local/gc/type-equivalence.wast
new file mode 100644
index 00000000..3f785fe7
--- /dev/null
+++ b/tests/local/gc/type-equivalence.wast
@@ -0,0 +1,134 @@
+;; Cloned from tests/testsuite/proposals/function-references/type-equivalence.wast
+;; and modified for the GC proposal.
+
+;; Syntactic types (validation time)
+
+;; Simple types.
+
+(module
+  (type $t1 (func (param f32 f32) (result f32)))
+  (type $t2 (func (param $x f32) (param $y f32) (result f32)))
+
+  (func $f1 (param $r (ref $t1)) (call $f2 (local.get $r)))
+  (func $f2 (param $r (ref $t2)) (call $f1 (local.get $r)))
+)
+
+
+;; Indirect types.
+
+(module
+  (type $s0 (func (param i32) (result f32)))
+  (type $s1 (func (param i32 (ref $s0)) (result (ref $s0))))
+  (type $s2 (func (param i32 (ref $s0)) (result (ref $s0))))
+  (type $t1 (func (param (ref $s1)) (result (ref $s2))))
+  (type $t2 (func (param (ref $s2)) (result (ref $s1))))
+
+  (func $f1 (param $r (ref $t1)) (call $f2 (local.get $r)))
+  (func $f2 (param $r (ref $t2)) (call $f1 (local.get $r)))
+)
+
+
+;; Recursive types.
+
+;; This case has been made valid by the GC proposal:
+;; every standalone type is now a recursive type group of size 1.
+(module
+  (type $t (func (result (ref $t))))
+)
+
+(assert_invalid
+  (module
+    (type $t1 (func (param (ref $t2))))
+    (type $t2 (func (param (ref $t1))))
+  )
+  "unknown type"
+)
+
+
+;; Semantic types (run time)
+
+;; Simple types.
+
+(module
+  (type $t1 (func (param f32 f32)))
+  (type $t2 (func (param $x f32) (param $y f32)))
+
+  (func $f1 (type $t1))
+  (func $f2 (type $t2))
+  (table funcref (elem $f1 $f2))
+
+  (func (export "run")
+    (call_indirect (type $t1) (f32.const 1) (f32.const 2) (i32.const 1))
+    (call_indirect (type $t2) (f32.const 1) (f32.const 2) (i32.const 0))
+  )
+)
+(assert_return (invoke "run"))
+
+
+;; Indirect types.
+
+(module
+  (type $s0 (func (param i32)))
+  (type $s1 (func (param i32 (ref $s0))))
+  (type $s2 (func (param i32 (ref $s0))))
+  (type $t1 (func (param (ref $s1))))
+  (type $t2 (func (param (ref $s2))))
+
+  (func $s1 (type $s1))
+  (func $s2 (type $s2))
+  (func $f1 (type $t1))
+  (func $f2 (type $t2))
+  (table funcref (elem $f1 $f2 $s1 $s2))
+
+  (func (export "run")
+    (call_indirect (type $t1) (ref.func $s1) (i32.const 0))
+    (call_indirect (type $t1) (ref.func $s1) (i32.const 1))
+    (call_indirect (type $t1) (ref.func $s2) (i32.const 0))
+    (call_indirect (type $t1) (ref.func $s2) (i32.const 1))
+    (call_indirect (type $t2) (ref.func $s1) (i32.const 0))
+    (call_indirect (type $t2) (ref.func $s1) (i32.const 1))
+    (call_indirect (type $t2) (ref.func $s2) (i32.const 0))
+    (call_indirect (type $t2) (ref.func $s2) (i32.const 1))
+  )
+)
+(assert_return (invoke "run"))
+
+
+;; Semantic types (link time)
+
+;; Simple types.
+
+(module
+  (type $t1 (func (param f32 f32) (result f32)))
+  (func (export "f") (param (ref $t1)))
+)
+(register "M")
+(module
+  (type $t2 (func (param $x f32) (param $y f32) (result f32)))
+  (func (import "M" "f") (param (ref $t2)))
+)
+
+
+;; Indirect types.
+
+(module
+  (type $s0 (func (param i32) (result f32)))
+  (type $s1 (func (param i32 (ref $s0)) (result (ref $s0))))
+  (type $s2 (func (param i32 (ref $s0)) (result (ref $s0))))
+  (type $t1 (func (param (ref $s1)) (result (ref $s2))))
+  (type $t2 (func (param (ref $s2)) (result (ref $s1))))
+  (func (export "f1") (param (ref $t1)))
+  (func (export "f2") (param (ref $t1)))
+)
+(register "M")
+(module
+  (type $s0 (func (param i32) (result f32)))
+  (type $s1 (func (param i32 (ref $s0)) (result (ref $s0))))
+  (type $s2 (func (param i32 (ref $s0)) (result (ref $s0))))
+  (type $t1 (func (param (ref $s1)) (result (ref $s2))))
+  (type $t2 (func (param (ref $s2)) (result (ref $s1))))
+  (func (import "M" "f1") (param (ref $t1)))
+  (func (import "M" "f1") (param (ref $t2)))
+  (func (import "M" "f2") (param (ref $t1)))
+  (func (import "M" "f2") (param (ref $t1)))
+)
diff --git a/tests/roundtrip.rs b/tests/roundtrip.rs
index 89d3bf93..41e01585 100644
--- a/tests/roundtrip.rs
+++ b/tests/roundtrip.rs
@@ -146,7 +146,6 @@ fn skip_test(test: &Path, contents: &[u8]) -> bool {
         "multi-memory/memory_copy1.wast",
         // the GC proposal isn't implemented yet
         "gc/gc-array.wat",
-        "gc/gc-rec-sub.wat",
         "gc/gc-ref-global-import.wat",
         "gc/gc-struct.wat",
         "gc/let.wat",
diff --git a/tests/snapshots/local/gc/gc-rec-sub.wat.print b/tests/snapshots/local/gc/gc-rec-sub.wat.print
new file mode 100644
index 00000000..ecb42e64
--- /dev/null
+++ b/tests/snapshots/local/gc/gc-rec-sub.wat.print
@@ -0,0 +1,38 @@
+(module
+  (type (;0;) (func))
+  (rec
+    (type (;1;) (func))
+    (type (;2;) (func))
+  )
+  (type (;3;) (struct))
+  (rec
+    (type (;4;) (struct))
+    (type (;5;) (struct))
+  )
+  (type (;6;) (array i32))
+  (rec
+    (type (;7;) (array i32))
+    (type (;8;) (array i32))
+  )
+  (rec
+    (type (;9;) (func))
+    (type (;10;) (struct))
+    (type (;11;) (array i32))
+  )
+  (rec
+    (type $a (;12;) (func))
+    (type (;13;) (sub $a (;12;) (func)))
+  )
+  (type (;14;) (sub $a (;12;) (func)))
+  (type (;15;) (sub final $a (;12;) (func)))
+  (type (;16;) (sub $a (;12;) (func)))
+  (rec
+    (type $t1 (;17;) (struct (field (ref 18))))
+    (type $t2 (;18;) (struct (field (ref 17))))
+  )
+  (rec
+    (type $t3 (;19;) (struct (field (ref 20))))
+    (type $t4 (;20;) (sub $t2 (;18;) (struct (field (ref 19)))))
+  )
+  (rec)
+)
\ No newline at end of file
diff --git a/tests/snapshots/local/gc/type-equivalence.wast/0.print b/tests/snapshots/local/gc/type-equivalence.wast/0.print
new file mode 100644
index 00000000..e6e986b4
--- /dev/null
+++ b/tests/snapshots/local/gc/type-equivalence.wast/0.print
@@ -0,0 +1,14 @@
+(module
+  (type $t1 (;0;) (func (param f32 f32) (result f32)))
+  (type $t2 (;1;) (func (param f32 f32) (result f32)))
+  (type (;2;) (func (param (ref 0))))
+  (type (;3;) (func (param (ref 1))))
+  (func $f1 (;0;) (type 2) (param $r (ref 0))
+    local.get $r
+    call $f2
+  )
+  (func $f2 (;1;) (type 3) (param $r (ref 1))
+    local.get $r
+    call $f1
+  )
+)
\ No newline at end of file
diff --git a/tests/snapshots/local/gc/type-equivalence.wast/1.print b/tests/snapshots/local/gc/type-equivalence.wast/1.print
new file mode 100644
index 00000000..4677912b
--- /dev/null
+++ b/tests/snapshots/local/gc/type-equivalence.wast/1.print
@@ -0,0 +1,17 @@
+(module
+  (type $s0 (;0;) (func (param i32) (result f32)))
+  (type $s1 (;1;) (func (param i32 (ref 0)) (result (ref 0))))
+  (type $s2 (;2;) (func (param i32 (ref 0)) (result (ref 0))))
+  (type $t1 (;3;) (func (param (ref 1)) (result (ref 2))))
+  (type $t2 (;4;) (func (param (ref 2)) (result (ref 1))))
+  (type (;5;) (func (param (ref 3))))
+  (type (;6;) (func (param (ref 4))))
+  (func $f1 (;0;) (type 5) (param $r (ref 3))
+    local.get $r
+    call $f2
+  )
+  (func $f2 (;1;) (type 6) (param $r (ref 4))
+    local.get $r
+    call $f1
+  )
+)
\ No newline at end of file
diff --git a/tests/snapshots/local/gc/type-equivalence.wast/10.print b/tests/snapshots/local/gc/type-equivalence.wast/10.print
new file mode 100644
index 00000000..ff5f8b66
--- /dev/null
+++ b/tests/snapshots/local/gc/type-equivalence.wast/10.print
@@ -0,0 +1,5 @@
+(module
+  (type $t2 (;0;) (func (param f32 f32) (result f32)))
+  (type (;1;) (func (param (ref 0))))
+  (import "M" "f" (func (;0;) (type 1)))
+)
\ No newline at end of file
diff --git a/tests/snapshots/local/gc/type-equivalence.wast/11.print b/tests/snapshots/local/gc/type-equivalence.wast/11.print
new file mode 100644
index 00000000..8dce052a
--- /dev/null
+++ b/tests/snapshots/local/gc/type-equivalence.wast/11.print
@@ -0,0 +1,12 @@
+(module
+  (type $s0 (;0;) (func (param i32) (result f32)))
+  (type $s1 (;1;) (func (param i32 (ref 0)) (result (ref 0))))
+  (type $s2 (;2;) (func (param i32 (ref 0)) (result (ref 0))))
+  (type $t1 (;3;) (func (param (ref 1)) (result (ref 2))))
+  (type $t2 (;4;) (func (param (ref 2)) (result (ref 1))))
+  (type (;5;) (func (param (ref 3))))
+  (func (;0;) (type 5) (param (ref 3)))
+  (func (;1;) (type 5) (param (ref 3)))
+  (export "f1" (func 0))
+  (export "f2" (func 1))
+)
\ No newline at end of file
diff --git a/tests/snapshots/local/gc/type-equivalence.wast/13.print b/tests/snapshots/local/gc/type-equivalence.wast/13.print
new file mode 100644
index 00000000..bc2fc848
--- /dev/null
+++ b/tests/snapshots/local/gc/type-equivalence.wast/13.print
@@ -0,0 +1,13 @@
+(module
+  (type $s0 (;0;) (func (param i32) (result f32)))
+  (type $s1 (;1;) (func (param i32 (ref 0)) (result (ref 0))))
+  (type $s2 (;2;) (func (param i32 (ref 0)) (result (ref 0))))
+  (type $t1 (;3;) (func (param (ref 1)) (result (ref 2))))
+  (type $t2 (;4;) (func (param (ref 2)) (result (ref 1))))
+  (type (;5;) (func (param (ref 3))))
+  (type (;6;) (func (param (ref 4))))
+  (import "M" "f1" (func (;0;) (type 5)))
+  (import "M" "f1" (func (;1;) (type 6)))
+  (import "M" "f2" (func (;2;) (type 5)))
+  (import "M" "f2" (func (;3;) (type 5)))
+)
\ No newline at end of file
diff --git a/tests/snapshots/local/gc/type-equivalence.wast/2.print b/tests/snapshots/local/gc/type-equivalence.wast/2.print
new file mode 100644
index 00000000..b063e708
--- /dev/null
+++ b/tests/snapshots/local/gc/type-equivalence.wast/2.print
@@ -0,0 +1,3 @@
+(module
+  (type $t (;0;) (func (result (ref 0))))
+)
\ No newline at end of file
diff --git a/tests/snapshots/local/gc/type-equivalence.wast/4.print b/tests/snapshots/local/gc/type-equivalence.wast/4.print
new file mode 100644
index 00000000..51cfc602
--- /dev/null
+++ b/tests/snapshots/local/gc/type-equivalence.wast/4.print
@@ -0,0 +1,20 @@
+(module
+  (type $t1 (;0;) (func (param f32 f32)))
+  (type $t2 (;1;) (func (param f32 f32)))
+  (type (;2;) (func))
+  (func $f1 (;0;) (type $t1) (param f32 f32))
+  (func $f2 (;1;) (type $t2) (param f32 f32))
+  (func (;2;) (type 2)
+    f32.const 0x1p+0 (;=1;)
+    f32.const 0x1p+1 (;=2;)
+    i32.const 1
+    call_indirect (type $t1)
+    f32.const 0x1p+0 (;=1;)
+    f32.const 0x1p+1 (;=2;)
+    i32.const 0
+    call_indirect (type $t2)
+  )
+  (table (;0;) 2 2 funcref)
+  (export "run" (func 2))
+  (elem (;0;) (i32.const 0) func $f1 $f2)
+)
\ No newline at end of file
diff --git a/tests/snapshots/local/gc/type-equivalence.wast/6.print b/tests/snapshots/local/gc/type-equivalence.wast/6.print
new file mode 100644
index 00000000..790198e7
--- /dev/null
+++ b/tests/snapshots/local/gc/type-equivalence.wast/6.print
@@ -0,0 +1,41 @@
+(module
+  (type $s0 (;0;) (func (param i32)))
+  (type $s1 (;1;) (func (param i32 (ref 0))))
+  (type $s2 (;2;) (func (param i32 (ref 0))))
+  (type $t1 (;3;) (func (param (ref 1))))
+  (type $t2 (;4;) (func (param (ref 2))))
+  (type (;5;) (func))
+  (func $s1 (;0;) (type $s1) (param i32 (ref 0)))
+  (func $s2 (;1;) (type $s2) (param i32 (ref 0)))
+  (func $f1 (;2;) (type $t1) (param (ref 1)))
+  (func $f2 (;3;) (type $t2) (param (ref 2)))
+  (func (;4;) (type 5)
+    ref.func $s1
+    i32.const 0
+    call_indirect (type $t1)
+    ref.func $s1
+    i32.const 1
+    call_indirect (type $t1)
+    ref.func $s2
+    i32.const 0
+    call_indirect (type $t1)
+    ref.func $s2
+    i32.const 1
+    call_indirect (type $t1)
+    ref.func $s1
+    i32.const 0
+    call_indirect (type $t2)
+    ref.func $s1
+    i32.const 1
+    call_indirect (type $t2)
+    ref.func $s2
+    i32.const 0
+    call_indirect (type $t2)
+    ref.func $s2
+    i32.const 1
+    call_indirect (type $t2)
+  )
+  (table (;0;) 4 4 funcref)
+  (export "run" (func 4))
+  (elem (;0;) (i32.const 0) func $f1 $f2 $s1 $s2)
+)
\ No newline at end of file
diff --git a/tests/snapshots/local/gc/type-equivalence.wast/8.print b/tests/snapshots/local/gc/type-equivalence.wast/8.print
new file mode 100644
index 00000000..018898ab
--- /dev/null
+++ b/tests/snapshots/local/gc/type-equivalence.wast/8.print
@@ -0,0 +1,6 @@
+(module
+  (type $t1 (;0;) (func (param f32 f32) (result f32)))
+  (type (;1;) (func (param (ref 0))))
+  (func (;0;) (type 1) (param (ref 0)))
+  (export "f" (func 0))
+)
\ No newline at end of file