Repository: bytecodealliance/wasm-tools
Pull request #1380 (2024-01-18 20:47:07)

Add composability for general import
Motivation:

Currently WASI-Virt is unable to operate on components created via `cargo-component` if their worlds define types, rather than importing them from an interface.  For example if a component uses the following in its wit

```
world foo {
    type name = u32;
}
```

it must instead be written as

```
interface types {
    type name = u32;
}

world foo {
    import types;
    export types;
}
```

otherwise one will encounter this error when using `WASI-Virt`

```
Error: Unable to compose virtualized adapter into component.
Make sure virtualizations are enabled and being used.

Caused by:
    component `component.wasm` has a non-instance import named `name`
```

This is due to `WASI-Virt`'s usage of `wasm-compose`, which has a limitation that is no longer needed described below.

Currently `wasm-compose` assumes every import in a dependency should be an instance resolved to an instantiation.  Since originally developed, it has become more common for components to import types directly alongside instances to be composed with one another.  This change removes the check that any imports in dependencies being resolved are instance imports, enabling components that directly import types be used in a composition.




Patch:
diff --git a/crates/wasm-compose/src/composer.rs b/crates/wasm-compose/src/composer.rs
index e846ce12..c2bc1644 100644
--- a/crates/wasm-compose/src/composer.rs
+++ b/crates/wasm-compose/src/composer.rs
@@ -407,15 +407,7 @@ impl<'a> CompositionGraphBuilder<'a> {
         let count = queue.len();
 
         // Push a dependency for every instance import
-        'outer: for (import, name, ty) in component.imports() {
-            match ty {
-                ComponentTypeRef::Instance(_) => {}
-                _ => bail!(
-                    "component `{path}` has a non-instance import named `{name}`",
-                    path = component.path().unwrap().display()
-                ),
-            }
-
+        'outer: for (import, name, _) in component.imports() {
             log::debug!("adding dependency for argument `{name}` (import index {import}) from instance `{instance_name}` to the queue", import = import.0);
 
             // Search for a matching definition export for this import
diff --git a/crates/wasmparser/src/binary_reader.rs b/crates/wasmparser/src/binary_reader.rs
index 032402fe..620694ce 100644
--- a/crates/wasmparser/src/binary_reader.rs
+++ b/crates/wasmparser/src/binary_reader.rs
@@ -1809,7 +1809,7 @@ impl<'a> VisitOperator<'a> for OperatorFactory<'a> {
 /// Iterator returned from [`BinaryReader::read_iter`].
 pub struct BinaryReaderIter<'a, 'me, T: FromReader<'a>> {
     remaining: usize,
-    reader: &'me mut BinaryReader<'a>,
+    pub(crate) reader: &'me mut BinaryReader<'a>,
     _marker: marker::PhantomData<T>,
 }
 
diff --git a/crates/wasmparser/src/readers/core/types.rs b/crates/wasmparser/src/readers/core/types.rs
index 6845426d..3fa8ed74 100644
--- a/crates/wasmparser/src/readers/core/types.rs
+++ b/crates/wasmparser/src/readers/core/types.rs
@@ -306,13 +306,13 @@ pub struct RecGroup {
 
 #[derive(Debug, Clone)]
 enum RecGroupInner {
-    Implicit(SubType),
-    Explicit(Vec<SubType>),
+    Implicit((usize, SubType)),
+    Explicit(Vec<(usize, SubType)>),
 }
 
 impl RecGroup {
     /// Create an explicit `RecGroup` for the given types.
-    pub(crate) fn explicit(types: Vec<SubType>) -> Self {
+    pub(crate) fn explicit(types: Vec<(usize, SubType)>) -> Self {
         RecGroup {
             inner: RecGroupInner::Explicit(types),
         }
@@ -320,51 +320,59 @@ impl RecGroup {
 
     /// Create an implicit `RecGroup` for a type that was not contained
     /// in a `(rec ...)`.
-    pub(crate) fn implicit(ty: SubType) -> Self {
+    pub(crate) fn implicit(offset: usize, ty: SubType) -> Self {
         RecGroup {
-            inner: RecGroupInner::Implicit(ty),
+            inner: RecGroupInner::Implicit((offset, ty)),
         }
     }
 
     /// Is this an explicit recursion group?
     pub fn is_explicit_rec_group(&self) -> bool {
-        matches!(self.inner, RecGroupInner::Explicit(_))
+        matches!(self.inner, RecGroupInner::Explicit(..))
     }
 
     /// Returns the list of subtypes in the recursive type group.
-    pub fn types(&self) -> &[SubType] {
-        match &self.inner {
+    pub fn types(&self) -> impl ExactSizeIterator<Item = &SubType> + '_ {
+        let types = match &self.inner {
             RecGroupInner::Implicit(ty) => std::slice::from_ref(ty),
             RecGroupInner::Explicit(types) => types,
-        }
+        };
+        types.iter().map(|(_, ty)| ty)
     }
 
     /// Return a mutable borrow of the list of subtypes in this
     /// recursive type group.
-    pub(crate) fn types_mut(&mut self) -> &mut [SubType] {
-        match &mut self.inner {
+    pub(crate) fn types_mut(&mut self) -> impl ExactSizeIterator<Item = &mut SubType> + '_ {
+        let types = match &mut self.inner {
             RecGroupInner::Implicit(ty) => std::slice::from_mut(ty),
             RecGroupInner::Explicit(types) => types,
-        }
+        };
+        types.iter_mut().map(|(_, ty)| ty)
     }
 
     /// Returns an owning iterator of all subtypes in this recursion
     /// group.
     pub fn into_types(self) -> impl ExactSizeIterator<Item = SubType> {
+        self.into_types_and_offsets().map(|(_, ty)| ty)
+    }
+
+    /// Returns an owning iterator of all subtypes in this recursion
+    /// group, along with their offset.
+    pub fn into_types_and_offsets(self) -> impl ExactSizeIterator<Item = (usize, SubType)> {
         return match self.inner {
-            RecGroupInner::Implicit(ty) => Iter::Implicit(Some(ty)),
+            RecGroupInner::Implicit(tup) => Iter::Implicit(Some(tup)),
             RecGroupInner::Explicit(types) => Iter::Explicit(types.into_iter()),
         };
 
         enum Iter {
-            Implicit(Option<SubType>),
-            Explicit(std::vec::IntoIter<SubType>),
+            Implicit(Option<(usize, SubType)>),
+            Explicit(std::vec::IntoIter<(usize, SubType)>),
         }
 
         impl Iterator for Iter {
-            type Item = SubType;
+            type Item = (usize, SubType);
 
-            fn next(&mut self) -> Option<SubType> {
+            fn next(&mut self) -> Option<(usize, SubType)> {
                 match self {
                     Self::Implicit(ty) => ty.take(),
                     Self::Explicit(types) => types.next(),
@@ -386,13 +394,19 @@ impl RecGroup {
 
 impl Hash for RecGroup {
     fn hash<H: Hasher>(&self, hasher: &mut H) {
-        self.types().hash(hasher)
+        let types = self.types();
+        types.len().hash(hasher);
+        for ty in types {
+            ty.hash(hasher);
+        }
     }
 }
 
 impl PartialEq for RecGroup {
     fn eq(&self, other: &RecGroup) -> bool {
-        self.types() == other.types()
+        let self_tys = self.types();
+        let other_tys = other.types();
+        self_tys.len() == other_tys.len() && self_tys.zip(other_tys).all(|(a, b)| a == b)
     }
 }
 
@@ -1533,10 +1547,19 @@ impl<'a> FromReader<'a> for RecGroup {
         match reader.peek()? {
             0x4e => {
                 reader.read_u8()?;
-                let types = reader.read_iter(MAX_WASM_TYPES, "rec group types")?;
-                Ok(RecGroup::explicit(types.collect::<Result<_>>()?))
+                let mut iter = reader.read_iter(MAX_WASM_TYPES, "rec group types")?;
+                let mut types = Vec::with_capacity(iter.size_hint().0);
+                let mut offset = iter.reader.original_position();
+                while let Some(ty) = iter.next() {
+                    types.push((offset, ty?));
+                    offset = iter.reader.original_position();
+                }
+                Ok(RecGroup::explicit(types))
             }
-            _ => Ok(RecGroup::implicit(reader.read()?)),
+            _ => Ok(RecGroup::implicit(
+                reader.original_position(),
+                reader.read()?,
+            )),
         }
     }
 }
diff --git a/crates/wasmparser/src/validator/component.rs b/crates/wasmparser/src/validator/component.rs
index b05609d2..83e08f85 100644
--- a/crates/wasmparser/src/validator/component.rs
+++ b/crates/wasmparser/src/validator/component.rs
@@ -263,7 +263,8 @@ impl ComponentState {
     ) -> Result<()> {
         let id = match ty {
             crate::CoreType::Sub(sub) => {
-                let (_is_new, group_id) = types.intern_canonical_rec_group(RecGroup::implicit(sub));
+                let (_is_new, group_id) =
+                    types.intern_canonical_rec_group(RecGroup::implicit(offset, sub));
                 let id = types[group_id].start;
                 ComponentCoreTypeId::Sub(id)
             }
@@ -1010,7 +1011,8 @@ impl ComponentState {
             composite_type: CompositeType::Func(info.into_func_type()),
         };
 
-        let (_is_new, group_id) = types.intern_canonical_rec_group(RecGroup::implicit(lowered_ty));
+        let (_is_new, group_id) =
+            types.intern_canonical_rec_group(RecGroup::implicit(offset, lowered_ty));
         let id = types[group_id].start;
         self.core_funcs.push(id);
 
@@ -1029,7 +1031,8 @@ impl ComponentState {
             supertype_idx: None,
             composite_type: CompositeType::Func(FuncType::new([rep], [ValType::I32])),
         };
-        let (_is_new, group_id) = types.intern_canonical_rec_group(RecGroup::implicit(core_ty));
+        let (_is_new, group_id) =
+            types.intern_canonical_rec_group(RecGroup::implicit(offset, core_ty));
         let id = types[group_id].start;
         self.core_funcs.push(id);
         Ok(())
@@ -1047,7 +1050,8 @@ impl ComponentState {
             supertype_idx: None,
             composite_type: CompositeType::Func(FuncType::new([ValType::I32], [])),
         };
-        let (_is_new, group_id) = types.intern_canonical_rec_group(RecGroup::implicit(core_ty));
+        let (_is_new, group_id) =
+            types.intern_canonical_rec_group(RecGroup::implicit(offset, core_ty));
         let id = types[group_id].start;
         self.core_funcs.push(id);
         Ok(())
@@ -1065,7 +1069,8 @@ impl ComponentState {
             supertype_idx: None,
             composite_type: CompositeType::Func(FuncType::new([ValType::I32], [rep])),
         };
-        let (_is_new, group_id) = types.intern_canonical_rec_group(RecGroup::implicit(core_ty));
+        let (_is_new, group_id) =
+            types.intern_canonical_rec_group(RecGroup::implicit(offset, core_ty));
         let id = types[group_id].start;
         self.core_funcs.push(id);
         Ok(())
@@ -1485,7 +1490,13 @@ impl ComponentState {
         for decl in decls {
             match decl {
                 crate::ModuleTypeDeclaration::Type(ty) => {
-                    state.add_types(RecGroup::implicit(ty), features, types, offset, true)?;
+                    state.add_types(
+                        RecGroup::implicit(offset, ty),
+                        features,
+                        types,
+                        offset,
+                        true,
+                    )?;
                 }
                 crate::ModuleTypeDeclaration::Export { name, mut ty } => {
                     let ty = state.check_type_ref(&mut ty, features, types, offset)?;
diff --git a/crates/wasmparser/src/validator/core/canonical.rs b/crates/wasmparser/src/validator/core/canonical.rs
index 6e04148f..4e4b9e97 100644
--- a/crates/wasmparser/src/validator/core/canonical.rs
+++ b/crates/wasmparser/src/validator/core/canonical.rs
@@ -150,9 +150,9 @@ impl<'a> TypeCanonicalizer<'a> {
         self.rec_group_start = u32::try_from(self.module.types.len()).unwrap();
         self.rec_group_len = u32::try_from(rec_group.types().len()).unwrap();
 
-        for (rec_group_index, ty) in rec_group.types_mut().iter_mut().enumerate() {
-            let rec_group_index = u32::try_from(rec_group_index).unwrap();
-            let type_index = self.rec_group_start + rec_group_index;
+        for (rec_group_local_index, ty) in rec_group.types_mut().enumerate() {
+            let rec_group_local_index = u32::try_from(rec_group_local_index).unwrap();
+            let type_index = self.rec_group_start + rec_group_local_index;
 
             if let Some(sup) = ty.supertype_idx.as_mut() {
                 if sup.as_module_index().map_or(false, |i| i >= type_index) {
diff --git a/src/bin/wasm-tools/dump.rs b/src/bin/wasm-tools/dump.rs
index 92230561..467beda4 100644
--- a/src/bin/wasm-tools/dump.rs
+++ b/src/bin/wasm-tools/dump.rs
@@ -48,6 +48,7 @@ struct Indices {
     core_tags: u32,
     core_modules: u32,
     core_instances: u32,
+    rec_groups: u32,
 
     // Component indexes
     types: u32,
@@ -101,8 +102,17 @@ impl<'a> Dump<'a> {
                     write!(self.state, "version {} ({:?})", num, encoding)?;
                     self.color_print(range.end)?;
                 }
-                Payload::TypeSection(s) => self.section(s, "type", |me, end, t| {
-                    write!(me.state, "[type {}] {:?}", inc(&mut i.core_types), t)?;
+                Payload::TypeSection(s) => self.section(s, "type", |me, end, rec_group| {
+                    let rec_group_index = inc(&mut i.rec_groups);
+                    let explicit = rec_group.is_explicit_rec_group();
+                    let kind = if explicit { "explicit" } else { "implicit" };
+                    writeln!(me.dst, "--- rec group {rec_group_index} ({kind}) ---")?;
+                    for (offset, ty) in rec_group.into_types_and_offsets() {
+                        if explicit {
+                            me.print(offset)?;
+                        }
+                        write!(me.state, "[type {}] {ty:?}", inc(&mut i.core_types))?;
+                    }
                     me.print(end)
                 })?,
                 Payload::ImportSection(s) => self.section(s, "import", |me, end, imp| {


Test patch:
diff --git a/crates/wasm-compose/tests/compositions/not-instance-import/b.wat b/crates/wasm-compose/tests/compositions/not-instance-import/b.wat
new file mode 100644
index 00000000..54bec96f
--- /dev/null
+++ b/crates/wasm-compose/tests/compositions/not-instance-import/b.wat
@@ -0,0 +1,3 @@
+(component
+  (instance)
+)
\ No newline at end of file
diff --git a/crates/wasm-compose/tests/compositions/not-instance-import/composed.wat b/crates/wasm-compose/tests/compositions/not-instance-import/composed.wat
new file mode 100644
index 00000000..02e8a2b1
--- /dev/null
+++ b/crates/wasm-compose/tests/compositions/not-instance-import/composed.wat
@@ -0,0 +1,21 @@
+(component
+  (type (;0;) (func))
+  (import "a" (func (;0;) (type 0)))
+  (component (;0;)
+    (type (;0;) (func))
+    (import "a" (func (;0;) (type 0)))
+    (type (;1;)
+      (instance)
+    )
+    (import "locked-dep=<foo:add@1.0.0>" (instance (;0;) (type 1)))
+  )
+  (component (;1;)
+    (instance (;0;))
+  )
+  (instance (;0;) (instantiate 1))
+  (instance (;1;) (instantiate 0
+      (with "locked-dep=<foo:add@1.0.0>" (instance 0))
+      (with "a" (func 0))
+    )
+  )
+)
\ No newline at end of file
diff --git a/crates/wasm-compose/tests/compositions/not-instance-import/config.yml b/crates/wasm-compose/tests/compositions/not-instance-import/config.yml
new file mode 100644
index 00000000..cc228540
--- /dev/null
+++ b/crates/wasm-compose/tests/compositions/not-instance-import/config.yml
@@ -0,0 +1,6 @@
+instantiations:
+  root:
+    arguments:
+      locked-dep=<foo:add@1.0.0>: a1
+  a1:
+    dependency: b
diff --git a/crates/wasm-compose/tests/compositions/not-instance-import/error.txt b/crates/wasm-compose/tests/compositions/not-instance-import/error.txt
deleted file mode 100644
index 712f641e..00000000
--- a/crates/wasm-compose/tests/compositions/not-instance-import/error.txt
+++ /dev/null
@@ -1,1 +0,0 @@
-component `tests/compositions/not-instance-import/root.wat` has a non-instance import named `a`
diff --git a/crates/wasm-compose/tests/compositions/not-instance-import/root.wat b/crates/wasm-compose/tests/compositions/not-instance-import/root.wat
index 6e38697e..eac878a0 100644
--- a/crates/wasm-compose/tests/compositions/not-instance-import/root.wat
+++ b/crates/wasm-compose/tests/compositions/not-instance-import/root.wat
@@ -1,3 +1,4 @@
 (component
   (import "a" (func))
+  (import "locked-dep=<foo:add@1.0.0>" (instance))
 )
diff --git a/tests/cli/dump/alias.wat.stdout b/tests/cli/dump/alias.wat.stdout
index 6e91d663..408c8678 100644
--- a/tests/cli/dump/alias.wat.stdout
+++ b/tests/cli/dump/alias.wat.stdout
@@ -30,7 +30,8 @@
         | 01 00 00 00
    0x57 | 01 04       | type section
    0x59 | 01          | 1 count
-   0x5a | 60 00 00    | [type 0] RecGroup { inner: Implicit(SubType { is_final: true, supertype_idx: None, composite_type: Func(FuncType { params: [], results: [] }) }) }
+--- rec group 0 (implicit) ---
+   0x5a | 60 00 00    | [type 0] SubType { is_final: true, supertype_idx: None, composite_type: Func(FuncType { params: [], results: [] }) }
    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 4b8450e8..657a30ad 100644
--- a/tests/cli/dump/alias2.wat.stdout
+++ b/tests/cli/dump/alias2.wat.stdout
@@ -126,7 +126,8 @@
          | 01 00 00 00
    0x158 | 01 04       | type section
    0x15a | 01          | 1 count
-   0x15b | 60 00 00    | [type 0] RecGroup { inner: Implicit(SubType { is_final: true, supertype_idx: None, composite_type: Func(FuncType { params: [], results: [] }) }) }
+--- rec group 0 (implicit) ---
+   0x15b | 60 00 00    | [type 0] SubType { is_final: true, supertype_idx: None, composite_type: Func(FuncType { params: [], results: [] }) }
    0x15e | 03 02       | func section
    0x160 | 01          | 1 count
    0x161 | 00          | [func 0] type 0
@@ -162,7 +163,8 @@
          | 01 00 00 00
    0x1a2 | 01 04       | type section
    0x1a4 | 01          | 1 count
-   0x1a5 | 60 00 00    | [type 0] RecGroup { inner: Implicit(SubType { is_final: true, supertype_idx: None, composite_type: Func(FuncType { params: [], results: [] }) }) }
+--- rec group 0 (implicit) ---
+   0x1a5 | 60 00 00    | [type 0] SubType { is_final: true, supertype_idx: None, composite_type: Func(FuncType { params: [], results: [] }) }
    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 6bc841ea..ed70b520 100644
--- a/tests/cli/dump/blockty.wat.stdout
+++ b/tests/cli/dump/blockty.wat.stdout
@@ -2,12 +2,17 @@
       | 01 00 00 00
   0x8 | 01 17       | type section
   0xa | 05          | 5 count
-  0xb | 60 00 00    | [type 0] RecGroup { inner: Implicit(SubType { is_final: true, supertype_idx: None, composite_type: Func(FuncType { params: [], results: [] }) }) }
-  0xe | 60 00 01 7f | [type 1] RecGroup { inner: Implicit(SubType { is_final: true, supertype_idx: None, composite_type: Func(FuncType { params: [], results: [I32] }) }) }
- 0x12 | 60 01 7f 00 | [type 2] RecGroup { inner: Implicit(SubType { is_final: true, supertype_idx: None, composite_type: Func(FuncType { params: [I32], results: [] }) }) }
- 0x16 | 60 01 7f 01 | [type 3] RecGroup { inner: Implicit(SubType { is_final: true, supertype_idx: None, composite_type: Func(FuncType { params: [I32], results: [I32] }) }) }
+--- rec group 0 (implicit) ---
+  0xb | 60 00 00    | [type 0] SubType { is_final: true, supertype_idx: None, composite_type: Func(FuncType { params: [], results: [] }) }
+--- rec group 1 (implicit) ---
+  0xe | 60 00 01 7f | [type 1] SubType { is_final: true, supertype_idx: None, composite_type: Func(FuncType { params: [], results: [I32] }) }
+--- rec group 2 (implicit) ---
+ 0x12 | 60 01 7f 00 | [type 2] SubType { is_final: true, supertype_idx: None, composite_type: Func(FuncType { params: [I32], results: [] }) }
+--- rec group 3 (implicit) ---
+ 0x16 | 60 01 7f 01 | [type 3] SubType { is_final: true, supertype_idx: None, composite_type: Func(FuncType { params: [I32], results: [I32] }) }
       | 7f         
- 0x1b | 60 01 7f 02 | [type 4] RecGroup { inner: Implicit(SubType { is_final: true, supertype_idx: None, composite_type: Func(FuncType { params: [I32], results: [I32, I32] }) }) }
+--- rec group 4 (implicit) ---
+ 0x1b | 60 01 7f 02 | [type 4] SubType { is_final: true, supertype_idx: None, composite_type: Func(FuncType { params: [I32], results: [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 7564ea09..f8226a09 100644
--- a/tests/cli/dump/bundled.wat.stdout
+++ b/tests/cli/dump/bundled.wat.stdout
@@ -25,7 +25,8 @@
          | 01 00 00 00
     0x54 | 01 09       | type section
     0x56 | 01          | 1 count
-    0x57 | 60 04 7f 7f | [type 0] RecGroup { inner: Implicit(SubType { is_final: true, supertype_idx: None, composite_type: Func(FuncType { params: [I32, I32, I32, I32], results: [I32] }) }) }
+--- rec group 0 (implicit) ---
+    0x57 | 60 04 7f 7f | [type 0] SubType { is_final: true, supertype_idx: None, composite_type: Func(FuncType { params: [I32, I32, I32, I32], results: [I32] }) }
          | 7f 7f 01 7f
     0x5f | 03 02       | func section
     0x61 | 01          | 1 count
@@ -61,9 +62,11 @@
          | 01 00 00 00
     0xa0 | 01 09       | type section
     0xa2 | 02          | 2 count
-    0xa3 | 60 02 7f 7f | [type 0] RecGroup { inner: Implicit(SubType { is_final: true, supertype_idx: None, composite_type: Func(FuncType { params: [I32, I32], results: [] }) }) }
+--- rec group 0 (implicit) ---
+    0xa3 | 60 02 7f 7f | [type 0] SubType { is_final: true, supertype_idx: None, composite_type: Func(FuncType { params: [I32, I32], results: [] }) }
          | 00         
-    0xa8 | 60 00 00    | [type 1] RecGroup { inner: Implicit(SubType { is_final: true, supertype_idx: None, composite_type: Func(FuncType { params: [], results: [] }) }) }
+--- rec group 1 (implicit) ---
+    0xa8 | 60 00 00    | [type 1] SubType { is_final: true, supertype_idx: None, composite_type: Func(FuncType { params: [], results: [] }) }
     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 +106,11 @@
          | 01 00 00 00
    0x101 | 01 0c       | type section
    0x103 | 02          | 2 count
-   0x104 | 60 02 7f 7f | [type 0] RecGroup { inner: Implicit(SubType { is_final: true, supertype_idx: None, composite_type: Func(FuncType { params: [I32, I32], results: [] }) }) }
+--- rec group 0 (implicit) ---
+   0x104 | 60 02 7f 7f | [type 0] SubType { is_final: true, supertype_idx: None, composite_type: Func(FuncType { params: [I32, I32], results: [] }) }
          | 00         
-   0x109 | 60 03 7f 7f | [type 1] RecGroup { inner: Implicit(SubType { is_final: true, supertype_idx: None, composite_type: Func(FuncType { params: [I32, I32, I32], results: [] }) }) }
+--- rec group 1 (implicit) ---
+   0x109 | 60 03 7f 7f | [type 1] SubType { is_final: true, supertype_idx: None, composite_type: Func(FuncType { params: [I32, I32, I32], results: [] }) }
          | 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 4fcd4afc..aa82e1f0 100644
--- a/tests/cli/dump/component-expand-bundle.wat.stdout
+++ b/tests/cli/dump/component-expand-bundle.wat.stdout
@@ -5,7 +5,8 @@
         | 01 00 00 00
    0x12 | 01 04       | type section
    0x14 | 01          | 1 count
-   0x15 | 60 00 00    | [type 0] RecGroup { inner: Implicit(SubType { is_final: true, supertype_idx: None, composite_type: Func(FuncType { params: [], results: [] }) }) }
+--- rec group 0 (implicit) ---
+   0x15 | 60 00 00    | [type 0] SubType { is_final: true, supertype_idx: None, composite_type: Func(FuncType { params: [], results: [] }) }
    0x18 | 03 02       | func section
    0x1a | 01          | 1 count
    0x1b | 00          | [func 0] type 0
@@ -28,7 +29,8 @@
         | 01 00 00 00
    0x3d | 01 04       | type section
    0x3f | 01          | 1 count
-   0x40 | 60 00 00    | [type 0] RecGroup { inner: Implicit(SubType { is_final: true, supertype_idx: None, composite_type: Func(FuncType { params: [], results: [] }) }) }
+--- rec group 0 (implicit) ---
+   0x40 | 60 00 00    | [type 0] SubType { is_final: true, supertype_idx: None, composite_type: Func(FuncType { params: [], results: [] }) }
    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 d74315c5..3e647545 100644
--- a/tests/cli/dump/import-modules.wat.stdout
+++ b/tests/cli/dump/import-modules.wat.stdout
@@ -14,7 +14,8 @@
         | 01 00 00 00
    0x2a | 01 04       | type section
    0x2c | 01          | 1 count
-   0x2d | 60 00 00    | [type 0] RecGroup { inner: Implicit(SubType { is_final: true, supertype_idx: None, composite_type: Func(FuncType { params: [], results: [] }) }) }
+--- rec group 0 (implicit) ---
+   0x2d | 60 00 00    | [type 0] SubType { is_final: true, supertype_idx: None, composite_type: Func(FuncType { params: [], results: [] }) }
    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 32927bfc..3585c91e 100644
--- a/tests/cli/dump/names.wat.stdout
+++ b/tests/cli/dump/names.wat.stdout
@@ -2,7 +2,8 @@
       | 01 00 00 00
   0x8 | 01 05       | type section
   0xa | 01          | 1 count
-  0xb | 60 01 7f 00 | [type 0] RecGroup { inner: Implicit(SubType { is_final: true, supertype_idx: None, composite_type: Func(FuncType { params: [I32], results: [] }) }) }
+--- rec group 0 (implicit) ---
+  0xb | 60 01 7f 00 | [type 0] SubType { is_final: true, supertype_idx: None, composite_type: Func(FuncType { params: [I32], results: [] }) }
   0xf | 03 02       | func section
  0x11 | 01          | 1 count
  0x12 | 00          | [func 0] type 0
diff --git a/tests/cli/dump/rec-group.wat b/tests/cli/dump/rec-group.wat
new file mode 100644
index 00000000..c2eedd3c
--- /dev/null
+++ b/tests/cli/dump/rec-group.wat
@@ -0,0 +1,21 @@
+;; RUN: dump %
+
+(module
+  (rec
+    (type (func (param i32 i32) (result f64)))
+  )
+  (rec
+    (type (sub (struct (field i32))))
+    (type (sub (struct (field (mut i32)))))
+    (type (sub (struct (field (mut i32)
+                              (mut i64)
+                              (mut f32)
+                              (mut f64)
+                              (mut v128)
+                              (mut funcref)
+                              (mut externref)
+                              (mut (ref null 2))))))
+  )
+  (type $a (sub (array i32)))
+  (type $b (sub $a (array i32)))
+)
diff --git a/tests/cli/dump/rec-group.wat.stdout b/tests/cli/dump/rec-group.wat.stdout
new file mode 100644
index 00000000..7d4e5fa9
--- /dev/null
+++ b/tests/cli/dump/rec-group.wat.stdout
@@ -0,0 +1,33 @@
+  0x0 | 00 61 73 6d | version 1 (Module)
+      | 01 00 00 00
+  0x8 | 01 37       | type section
+  0xa | 04          | 4 count
+--- rec group 0 (explicit) ---
+  0xb | 4e 01       | 
+  0xd | 60 02 7f 7f | [type 0] SubType { is_final: true, supertype_idx: None, composite_type: Func(FuncType { params: [I32, I32], results: [F64] }) }
+      | 01 7c      
+--- rec group 1 (explicit) ---
+ 0x13 | 4e 03       | 
+ 0x15 | 50 00 5f 01 | [type 1] SubType { is_final: false, supertype_idx: None, composite_type: Struct(StructType { fields: [FieldType { element_type: Val(I32), mutable: false }] }) }
+      | 7f 00      
+ 0x1b | 50 00 5f 01 | [type 2] SubType { is_final: false, supertype_idx: None, composite_type: Struct(StructType { fields: [FieldType { element_type: Val(I32), mutable: true }] }) }
+      | 7f 01      
+ 0x21 | 50 00 5f 08 | [type 3] SubType { is_final: false, supertype_idx: None, composite_type: Struct(StructType { fields: [FieldType { element_type: Val(I32), mutable: true }, FieldType { element_type: Val(I64), mutable: true }, FieldType { element_type: Val(F32), mutable: true }, FieldType { element_type: Val(F64), mutable: true }, FieldType { element_type: Val(V128), mutable: true }, FieldType { element_type: Val(Ref(funcref)), mutable: true }, FieldType { element_type: Val(Ref(externref)), mutable: true }, FieldType { element_type: Val(Ref((ref null (module 2)))), mutable: true }] }) }
+      | 7f 01 7e 01
+      | 7d 01 7c 01
+      | 7b 01 70 01
+      | 6f 01 63 02
+      | 01         
+--- rec group 2 (implicit) ---
+ 0x36 | 50 00 5e 7f | [type 4] SubType { is_final: false, supertype_idx: None, composite_type: Array(ArrayType(FieldType { element_type: Val(I32), mutable: false })) }
+      | 00         
+--- rec group 3 (implicit) ---
+ 0x3b | 50 01 04 5e | [type 5] SubType { is_final: false, supertype_idx: Some(CoreTypeIndex { kind: "module", index: 4 }), composite_type: Array(ArrayType(FieldType { element_type: Val(I32), mutable: false })) }
+      | 7f 00      
+ 0x41 | 00 0e       | custom section
+ 0x43 | 04 6e 61 6d | name: "name"
+      | 65         
+ 0x48 | 04 07       | type name section
+ 0x4a | 02          | 2 count
+ 0x4b | 04 01 61    | Naming { index: 4, name: "a" }
+ 0x4e | 05 01 62    | Naming { index: 5, name: "b" }
diff --git a/tests/cli/dump/select.wat.stdout b/tests/cli/dump/select.wat.stdout
index 2368e115..48f950ae 100644
--- a/tests/cli/dump/select.wat.stdout
+++ b/tests/cli/dump/select.wat.stdout
@@ -2,7 +2,8 @@
       | 01 00 00 00
   0x8 | 01 04       | type section
   0xa | 01          | 1 count
-  0xb | 60 00 00    | [type 0] RecGroup { inner: Implicit(SubType { is_final: true, supertype_idx: None, composite_type: Func(FuncType { params: [], results: [] }) }) }
+--- rec group 0 (implicit) ---
+  0xb | 60 00 00    | [type 0] SubType { is_final: true, supertype_idx: None, composite_type: Func(FuncType { params: [], results: [] }) }
   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 d9c3f0df..d94396db 100644
--- a/tests/cli/dump/simple.wat.stdout
+++ b/tests/cli/dump/simple.wat.stdout
@@ -2,8 +2,10 @@
       | 01 00 00 00
   0x8 | 01 08       | type section
   0xa | 02          | 2 count
-  0xb | 60 01 7f 00 | [type 0] RecGroup { inner: Implicit(SubType { is_final: true, supertype_idx: None, composite_type: Func(FuncType { params: [I32], results: [] }) }) }
-  0xf | 60 00 00    | [type 1] RecGroup { inner: Implicit(SubType { is_final: true, supertype_idx: None, composite_type: Func(FuncType { params: [], results: [] }) }) }
+--- rec group 0 (implicit) ---
+  0xb | 60 01 7f 00 | [type 0] SubType { is_final: true, supertype_idx: None, composite_type: Func(FuncType { params: [I32], results: [] }) }
+--- rec group 1 (implicit) ---
+  0xf | 60 00 00    | [type 1] SubType { is_final: true, supertype_idx: None, composite_type: Func(FuncType { params: [], results: [] }) }
  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) }