Repository: bytecodealliance/wasm-tools
Pull request #2217 (2025-06-03 20:12:11)

wasmparser(CM+GC): Add support for lowering variants, options, and results to GC types
This completes prototype support for GC in the component model by filling out the lowering of sum types (variants, options, and results) to GC types.

alexcrichton: Thanks for writing this up and the proposal upstream as well, cleared up all possible questions I'd have about variants 👍 

Patch:
diff --git a/crates/wasmparser/src/validator/component_types.rs b/crates/wasmparser/src/validator/component_types.rs
index c29dcbd8..6d9d36fc 100644
--- a/crates/wasmparser/src/validator/component_types.rs
+++ b/crates/wasmparser/src/validator/component_types.rs
@@ -1341,6 +1341,7 @@ pub struct RecordType {
     /// The map of record fields.
     pub fields: IndexMap<KebabString, ComponentValType>,
 }
+
 impl RecordType {
     fn lower_gc(
         &self,
@@ -1370,6 +1371,7 @@ pub struct VariantType {
     /// The map of variant cases.
     pub cases: IndexMap<KebabString, VariantCase>,
 }
+
 impl VariantType {
     fn lower_gc(
         &self,
@@ -1378,12 +1380,36 @@ impl VariantType {
         options: &CanonicalOptions,
         offset: usize,
         core: ArgOrField,
-    ) -> core::result::Result<(), BinaryReaderError> {
-        let _ = (types, abi, options, offset, core);
-        todo!("variant types and CM+GC")
+    ) -> Result<()> {
+        lower_gc_sum_type(types, abi, options, offset, core, "variant")
     }
 }
 
+/// Common helper for lowering sum types (variants, options, and results) to
+/// core GC types.
+fn lower_gc_sum_type(
+    types: &TypeList,
+    _abi: Abi,
+    _options: &CanonicalOptions,
+    offset: usize,
+    core: ArgOrField,
+    kind: &str,
+) -> Result<()> {
+    if let Some(id) = core.as_concrete_ref() {
+        if let CompositeInnerType::Struct(ty) = &types[id].composite_type.inner {
+            if ty.fields.is_empty() {
+                return Ok(());
+            }
+        }
+    }
+
+    bail!(
+        offset,
+        "expected to lower component `{kind}` type to core `(ref null? (struct))`, \
+         but found `{core}`",
+    )
+}
+
 /// Represents a tuple type.
 #[derive(Debug, Clone)]
 pub struct TupleType {
@@ -1392,6 +1418,7 @@ pub struct TupleType {
     /// The types of the tuple.
     pub types: Box<[ComponentValType]>,
 }
+
 impl TupleType {
     fn lower_gc(
         &self,
@@ -1400,7 +1427,7 @@ impl TupleType {
         options: &CanonicalOptions,
         offset: usize,
         core: ArgOrField,
-    ) -> core::result::Result<(), BinaryReaderError> {
+    ) -> Result<()> {
         lower_gc_product_type(
             self.types.iter(),
             types,
@@ -1666,9 +1693,13 @@ impl ComponentDefinedType {
                 }
             }
 
-            ComponentDefinedType::Option(_) => todo!("option types and CM+GC"),
+            ComponentDefinedType::Option(_) => {
+                lower_gc_sum_type(types, abi, options, offset, core, "option")
+            }
 
-            ComponentDefinedType::Result { .. } => todo!("result types and CM+GC"),
+            ComponentDefinedType::Result { .. } => {
+                lower_gc_sum_type(types, abi, options, offset, core, "result")
+            }
 
             ComponentDefinedType::Own(_)
             | ComponentDefinedType::Borrow(_)


Test patch:
diff --git a/tests/cli/component-model-gc/option-types.wast b/tests/cli/component-model-gc/option-types.wast
new file mode 100644
index 00000000..9fc3fc60
--- /dev/null
+++ b/tests/cli/component-model-gc/option-types.wast
@@ -0,0 +1,86 @@
+;; RUN: wast --assert default --snapshot tests/snapshots % -f gc,cm-gc
+
+;; Basic.
+(component
+  (type $opt (option u32))
+
+  (core type $opt (struct))
+  (core type $ty (func (param (ref $opt))))
+
+  (import "i" (instance $i
+                (export "ty" (type $opt' (eq $opt)))
+                (export "f" (func (param "x" $opt')))))
+  (core func (canon lower (func $i "f") gc (core-type $ty)))
+)
+
+;; With a nullable reference.
+(component
+  (type $opt (option u32))
+
+  (core type $opt (struct))
+  (core type $ty (func (param (ref null $opt))))
+
+  (import "i" (instance $i
+                (export "ty" (type $opt' (eq $opt)))
+                (export "f" (func (param "x" $opt')))))
+  (core func (canon lower (func $i "f") gc (core-type $ty)))
+)
+
+;; With a custom rec group.
+(component
+  (type $opt (option u32))
+
+  (core rec
+    (type $opt (struct))
+    (type $ty (func (param (ref $opt)))))
+
+  (import "i" (instance $i
+                (export "ty" (type $opt' (eq $opt)))
+                (export "f" (func (param "x" $opt')))))
+  (core func (canon lower (func $i "f") gc (core-type $ty)))
+)
+
+;; With a custom subtype.
+(component
+  (type $opt (option u32))
+
+  (core type $base (sub (struct)))
+  (core type $opt (sub $base (struct)))
+  (core type $ty (func (param (ref $opt))))
+
+  (import "i" (instance $i
+                (export "ty" (type $opt' (eq $opt)))
+                (export "f" (func (param "x" $opt')))))
+  (core func (canon lower (func $i "f") gc (core-type $ty)))
+)
+
+;; Unexpected field.
+(assert_invalid
+  (component
+    (type $opt (option u32))
+
+    (core type $opt (struct (field i32)))
+    (core type $ty (func (param (ref $opt))))
+
+    (import "i" (instance $i
+                          (export "ty" (type $opt' (eq $opt)))
+                          (export "f" (func (param "x" $opt')))))
+    (core func (canon lower (func $i "f") gc (core-type $ty)))
+  )
+  "expected to lower component `option` type to core `(ref null? (struct))`"
+)
+
+;; Lowering into a non-struct.
+(assert_invalid
+  (component
+    (type $opt (option u32))
+
+    (core type $ty (func (param externref)))
+
+    (import "i" (instance $i
+                          (export "ty" (type $opt' (eq $opt)))
+                          (export "f" (func (param "x" $opt')))))
+    (core func (canon lower (func $i "f") gc (core-type $ty)))
+  )
+  "expected to lower component `option` type to core `(ref null? (struct))`"
+)
diff --git a/tests/cli/component-model-gc/result-types.wast b/tests/cli/component-model-gc/result-types.wast
new file mode 100644
index 00000000..e8a02ae8
--- /dev/null
+++ b/tests/cli/component-model-gc/result-types.wast
@@ -0,0 +1,86 @@
+;; RUN: wast --assert default --snapshot tests/snapshots % -f gc,cm-gc
+
+;; Basic.
+(component
+  (type $result (result u32 (error u8)))
+
+  (core type $result (struct))
+  (core type $ty (func (param (ref $result))))
+
+  (import "i" (instance $i
+                (export "ty" (type $result' (eq $result)))
+                (export "f" (func (param "x" $result')))))
+  (core func (canon lower (func $i "f") gc (core-type $ty)))
+)
+
+;; With a nullable reference.
+(component
+  (type $result (result u32 (error u8)))
+
+  (core type $result (struct))
+  (core type $ty (func (param (ref null $result))))
+
+  (import "i" (instance $i
+                (export "ty" (type $result' (eq $result)))
+                (export "f" (func (param "x" $result')))))
+  (core func (canon lower (func $i "f") gc (core-type $ty)))
+)
+
+;; With a custom rec group.
+(component
+  (type $result (result u32 (error u8)))
+
+  (core rec
+    (type $result (struct))
+    (type $ty (func (param (ref null $result)))))
+
+  (import "i" (instance $i
+                (export "ty" (type $result' (eq $result)))
+                (export "f" (func (param "x" $result')))))
+  (core func (canon lower (func $i "f") gc (core-type $ty)))
+)
+
+;; With a custom subtype.
+(component
+  (type $result (result u32 (error u8)))
+
+  (core type $base (sub (struct)))
+  (core type $result (sub $base (struct)))
+  (core type $ty (func (param (ref null $result))))
+
+  (import "i" (instance $i
+                (export "ty" (type $result' (eq $result)))
+                (export "f" (func (param "x" $result')))))
+  (core func (canon lower (func $i "f") gc (core-type $ty)))
+)
+
+;; Unexpected fields in struct type.
+(assert_invalid
+  (component
+    (type $result (result u32 (error u8)))
+
+    (core type $result (struct (field i8) (field i8)))
+    (core type $ty (func (param (ref $result))))
+
+    (import "i" (instance $i
+                          (export "ty" (type $result' (eq $result)))
+                          (export "f" (func (param "x" $result')))))
+    (core func (canon lower (func $i "f") gc (core-type $ty)))
+  )
+  "expected to lower component `result` type to core `(ref null? (struct))`"
+)
+
+;; Lowering into a non-struct.
+(assert_invalid
+  (component
+    (type $result (result u32 (error u8)))
+
+    (core type $ty (func (param externref)))
+
+    (import "i" (instance $i
+                          (export "ty" (type $result' (eq $result)))
+                          (export "f" (func (param "x" $result')))))
+    (core func (canon lower (func $i "f") gc (core-type $ty)))
+  )
+  "expected to lower component `result` type to core `(ref null? (struct))`"
+)
diff --git a/tests/cli/component-model-gc/variant-types.wast b/tests/cli/component-model-gc/variant-types.wast
new file mode 100644
index 00000000..4a66c066
--- /dev/null
+++ b/tests/cli/component-model-gc/variant-types.wast
@@ -0,0 +1,98 @@
+;; RUN: wast --assert default --snapshot tests/snapshots % -f gc,cm-gc
+
+;; Basic.
+(component
+  (type $variant (variant (case "a" bool)
+                          (case "b" bool)
+                          (case "c" u8)))
+
+  (core type $variant (struct))
+  (core type $ty (func (param (ref $variant))))
+
+  (import "i" (instance $i
+                (export "ty" (type $variant' (eq $variant)))
+                (export "f" (func (param "x" $variant')))))
+  (core func (canon lower (func $i "f") gc (core-type $ty)))
+)
+
+;; With a nullable reference.
+(component
+  (type $variant (variant (case "a" bool)
+                          (case "b" bool)
+                          (case "c" u8)))
+
+  (core type $variant (struct))
+  (core type $ty (func (param (ref null $variant))))
+
+  (import "i" (instance $i
+                (export "ty" (type $variant' (eq $variant)))
+                (export "f" (func (param "x" $variant')))))
+  (core func (canon lower (func $i "f") gc (core-type $ty)))
+)
+
+;; With a custom rec group.
+(component
+  (type $variant (variant (case "a" bool)
+                          (case "b" bool)
+                          (case "c" u8)))
+
+  (core rec
+    (type $variant (struct))
+    (type $ty (func (param (ref null $variant)))))
+
+  (import "i" (instance $i
+                (export "ty" (type $variant' (eq $variant)))
+                (export "f" (func (param "x" $variant')))))
+  (core func (canon lower (func $i "f") gc (core-type $ty)))
+)
+
+;; With a custom subtype.
+(component
+  (type $variant (variant (case "a" bool)
+                          (case "b" bool)
+                          (case "c" u8)))
+
+  (core type $base (sub (struct)))
+  (core type $variant (sub $base (struct)))
+  (core type $ty (func (param (ref null $variant))))
+
+  (import "i" (instance $i
+                (export "ty" (type $variant' (eq $variant)))
+                (export "f" (func (param "x" $variant')))))
+  (core func (canon lower (func $i "f") gc (core-type $ty)))
+)
+
+;; Unexpected field in struct.
+(assert_invalid
+  (component
+    (type $variant (variant (case "a" bool)
+                            (case "b" bool)
+                            (case "c" u8)))
+
+    (core type $variant (struct (field i8)))
+    (core type $ty (func (param (ref $variant))))
+
+    (import "i" (instance $i
+                          (export "ty" (type $variant' (eq $variant)))
+                          (export "f" (func (param "x" $variant')))))
+    (core func (canon lower (func $i "f") gc (core-type $ty)))
+  )
+  "expected to lower component `variant` type to core `(ref null? (struct))`"
+)
+
+;; Lowering into a non-struct.
+(assert_invalid
+  (component
+    (type $variant (variant (case "a" bool)
+                            (case "b" bool)
+                            (case "c" u8)))
+
+    (core type $ty (func (param externref)))
+
+    (import "i" (instance $i
+                          (export "ty" (type $variant' (eq $variant)))
+                          (export "f" (func (param "x" $variant')))))
+    (core func (canon lower (func $i "f") gc (core-type $ty)))
+  )
+  "expected to lower component `variant` type to core `(ref null? (struct))`"
+)
diff --git a/tests/snapshots/cli/component-model-gc/option-types.wast.json b/tests/snapshots/cli/component-model-gc/option-types.wast.json
new file mode 100644
index 00000000..a0092d02
--- /dev/null
+++ b/tests/snapshots/cli/component-model-gc/option-types.wast.json
@@ -0,0 +1,43 @@
+{
+  "source_filename": "tests/cli/component-model-gc/option-types.wast",
+  "commands": [
+    {
+      "type": "module",
+      "line": 4,
+      "filename": "option-types.0.wasm",
+      "module_type": "binary"
+    },
+    {
+      "type": "module",
+      "line": 17,
+      "filename": "option-types.1.wasm",
+      "module_type": "binary"
+    },
+    {
+      "type": "module",
+      "line": 30,
+      "filename": "option-types.2.wasm",
+      "module_type": "binary"
+    },
+    {
+      "type": "module",
+      "line": 44,
+      "filename": "option-types.3.wasm",
+      "module_type": "binary"
+    },
+    {
+      "type": "assert_invalid",
+      "line": 59,
+      "filename": "option-types.4.wasm",
+      "module_type": "binary",
+      "text": "expected to lower component `option` type to core `(ref null? (struct))`"
+    },
+    {
+      "type": "assert_invalid",
+      "line": 75,
+      "filename": "option-types.5.wasm",
+      "module_type": "binary",
+      "text": "expected to lower component `option` type to core `(ref null? (struct))`"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/tests/snapshots/cli/component-model-gc/option-types.wast/0.print b/tests/snapshots/cli/component-model-gc/option-types.wast/0.print
new file mode 100644
index 00000000..5a355d59
--- /dev/null
+++ b/tests/snapshots/cli/component-model-gc/option-types.wast/0.print
@@ -0,0 +1,16 @@
+(component
+  (type $opt (;0;) (option u32))
+  (core type $opt (;0;) (struct))
+  (core type $ty (;1;) (func (param (ref $opt))))
+  (type (;1;)
+    (instance
+      (alias outer 1 $opt (type (;0;)))
+      (export (;1;) "ty" (type (eq 0)))
+      (type (;2;) (func (param "x" 1)))
+      (export (;0;) "f" (func (type 2)))
+    )
+  )
+  (import "i" (instance $i (;0;) (type 1)))
+  (alias export $i "f" (func (;0;)))
+  (core func (;0;) (canon lower (func 0) gc (core-type $ty)))
+)
diff --git a/tests/snapshots/cli/component-model-gc/option-types.wast/1.print b/tests/snapshots/cli/component-model-gc/option-types.wast/1.print
new file mode 100644
index 00000000..dba99a66
--- /dev/null
+++ b/tests/snapshots/cli/component-model-gc/option-types.wast/1.print
@@ -0,0 +1,16 @@
+(component
+  (type $opt (;0;) (option u32))
+  (core type $opt (;0;) (struct))
+  (core type $ty (;1;) (func (param (ref null $opt))))
+  (type (;1;)
+    (instance
+      (alias outer 1 $opt (type (;0;)))
+      (export (;1;) "ty" (type (eq 0)))
+      (type (;2;) (func (param "x" 1)))
+      (export (;0;) "f" (func (type 2)))
+    )
+  )
+  (import "i" (instance $i (;0;) (type 1)))
+  (alias export $i "f" (func (;0;)))
+  (core func (;0;) (canon lower (func 0) gc (core-type $ty)))
+)
diff --git a/tests/snapshots/cli/component-model-gc/option-types.wast/2.print b/tests/snapshots/cli/component-model-gc/option-types.wast/2.print
new file mode 100644
index 00000000..78772a67
--- /dev/null
+++ b/tests/snapshots/cli/component-model-gc/option-types.wast/2.print
@@ -0,0 +1,18 @@
+(component
+  (type $opt (;0;) (option u32))
+  (core rec
+    (type $opt (;0;) (struct))
+    (type $ty (;1;) (func (param (ref $opt))))
+  )
+  (type (;1;)
+    (instance
+      (alias outer 1 $opt (type (;0;)))
+      (export (;1;) "ty" (type (eq 0)))
+      (type (;2;) (func (param "x" 1)))
+      (export (;0;) "f" (func (type 2)))
+    )
+  )
+  (import "i" (instance $i (;0;) (type 1)))
+  (alias export $i "f" (func (;0;)))
+  (core func (;0;) (canon lower (func 0) gc (core-type $ty)))
+)
diff --git a/tests/snapshots/cli/component-model-gc/option-types.wast/3.print b/tests/snapshots/cli/component-model-gc/option-types.wast/3.print
new file mode 100644
index 00000000..0db9af2b
--- /dev/null
+++ b/tests/snapshots/cli/component-model-gc/option-types.wast/3.print
@@ -0,0 +1,17 @@
+(component
+  (type $opt (;0;) (option u32))
+  (core type $base (;0;) (sub (struct)))
+  (core type $opt (;1;) (sub $base (struct)))
+  (core type $ty (;2;) (func (param (ref $opt))))
+  (type (;1;)
+    (instance
+      (alias outer 1 $opt (type (;0;)))
+      (export (;1;) "ty" (type (eq 0)))
+      (type (;2;) (func (param "x" 1)))
+      (export (;0;) "f" (func (type 2)))
+    )
+  )
+  (import "i" (instance $i (;0;) (type 1)))
+  (alias export $i "f" (func (;0;)))
+  (core func (;0;) (canon lower (func 0) gc (core-type $ty)))
+)
diff --git a/tests/snapshots/cli/component-model-gc/result-types.wast.json b/tests/snapshots/cli/component-model-gc/result-types.wast.json
new file mode 100644
index 00000000..282c0e77
--- /dev/null
+++ b/tests/snapshots/cli/component-model-gc/result-types.wast.json
@@ -0,0 +1,43 @@
+{
+  "source_filename": "tests/cli/component-model-gc/result-types.wast",
+  "commands": [
+    {
+      "type": "module",
+      "line": 4,
+      "filename": "result-types.0.wasm",
+      "module_type": "binary"
+    },
+    {
+      "type": "module",
+      "line": 17,
+      "filename": "result-types.1.wasm",
+      "module_type": "binary"
+    },
+    {
+      "type": "module",
+      "line": 30,
+      "filename": "result-types.2.wasm",
+      "module_type": "binary"
+    },
+    {
+      "type": "module",
+      "line": 44,
+      "filename": "result-types.3.wasm",
+      "module_type": "binary"
+    },
+    {
+      "type": "assert_invalid",
+      "line": 59,
+      "filename": "result-types.4.wasm",
+      "module_type": "binary",
+      "text": "expected to lower component `result` type to core `(ref null? (struct))`"
+    },
+    {
+      "type": "assert_invalid",
+      "line": 75,
+      "filename": "result-types.5.wasm",
+      "module_type": "binary",
+      "text": "expected to lower component `result` type to core `(ref null? (struct))`"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/tests/snapshots/cli/component-model-gc/result-types.wast/0.print b/tests/snapshots/cli/component-model-gc/result-types.wast/0.print
new file mode 100644
index 00000000..7b825eab
--- /dev/null
+++ b/tests/snapshots/cli/component-model-gc/result-types.wast/0.print
@@ -0,0 +1,16 @@
+(component
+  (type $result (;0;) (result u32 (error u8)))
+  (core type $result (;0;) (struct))
+  (core type $ty (;1;) (func (param (ref $result))))
+  (type (;1;)
+    (instance
+      (alias outer 1 $result (type (;0;)))
+      (export (;1;) "ty" (type (eq 0)))
+      (type (;2;) (func (param "x" 1)))
+      (export (;0;) "f" (func (type 2)))
+    )
+  )
+  (import "i" (instance $i (;0;) (type 1)))
+  (alias export $i "f" (func (;0;)))
+  (core func (;0;) (canon lower (func 0) gc (core-type $ty)))
+)
diff --git a/tests/snapshots/cli/component-model-gc/result-types.wast/1.print b/tests/snapshots/cli/component-model-gc/result-types.wast/1.print
new file mode 100644
index 00000000..2d847014
--- /dev/null
+++ b/tests/snapshots/cli/component-model-gc/result-types.wast/1.print
@@ -0,0 +1,16 @@
+(component
+  (type $result (;0;) (result u32 (error u8)))
+  (core type $result (;0;) (struct))
+  (core type $ty (;1;) (func (param (ref null $result))))
+  (type (;1;)
+    (instance
+      (alias outer 1 $result (type (;0;)))
+      (export (;1;) "ty" (type (eq 0)))
+      (type (;2;) (func (param "x" 1)))
+      (export (;0;) "f" (func (type 2)))
+    )
+  )
+  (import "i" (instance $i (;0;) (type 1)))
+  (alias export $i "f" (func (;0;)))
+  (core func (;0;) (canon lower (func 0) gc (core-type $ty)))
+)
diff --git a/tests/snapshots/cli/component-model-gc/result-types.wast/2.print b/tests/snapshots/cli/component-model-gc/result-types.wast/2.print
new file mode 100644
index 00000000..cad8d68f
--- /dev/null
+++ b/tests/snapshots/cli/component-model-gc/result-types.wast/2.print
@@ -0,0 +1,18 @@
+(component
+  (type $result (;0;) (result u32 (error u8)))
+  (core rec
+    (type $result (;0;) (struct))
+    (type $ty (;1;) (func (param (ref null $result))))
+  )
+  (type (;1;)
+    (instance
+      (alias outer 1 $result (type (;0;)))
+      (export (;1;) "ty" (type (eq 0)))
+      (type (;2;) (func (param "x" 1)))
+      (export (;0;) "f" (func (type 2)))
+    )
+  )
+  (import "i" (instance $i (;0;) (type 1)))
+  (alias export $i "f" (func (;0;)))
+  (core func (;0;) (canon lower (func 0) gc (core-type $ty)))
+)
diff --git a/tests/snapshots/cli/component-model-gc/result-types.wast/3.print b/tests/snapshots/cli/component-model-gc/result-types.wast/3.print
new file mode 100644
index 00000000..f20fd9d3
--- /dev/null
+++ b/tests/snapshots/cli/component-model-gc/result-types.wast/3.print
@@ -0,0 +1,17 @@
+(component
+  (type $result (;0;) (result u32 (error u8)))
+  (core type $base (;0;) (sub (struct)))
+  (core type $result (;1;) (sub $base (struct)))
+  (core type $ty (;2;) (func (param (ref null $result))))
+  (type (;1;)
+    (instance
+      (alias outer 1 $result (type (;0;)))
+      (export (;1;) "ty" (type (eq 0)))
+      (type (;2;) (func (param "x" 1)))
+      (export (;0;) "f" (func (type 2)))
+    )
+  )
+  (import "i" (instance $i (;0;) (type 1)))
+  (alias export $i "f" (func (;0;)))
+  (core func (;0;) (canon lower (func 0) gc (core-type $ty)))
+)
diff --git a/tests/snapshots/cli/component-model-gc/variant-types.wast.json b/tests/snapshots/cli/component-model-gc/variant-types.wast.json
new file mode 100644
index 00000000..f4cb75f1
--- /dev/null
+++ b/tests/snapshots/cli/component-model-gc/variant-types.wast.json
@@ -0,0 +1,43 @@
+{
+  "source_filename": "tests/cli/component-model-gc/variant-types.wast",
+  "commands": [
+    {
+      "type": "module",
+      "line": 4,
+      "filename": "variant-types.0.wasm",
+      "module_type": "binary"
+    },
+    {
+      "type": "module",
+      "line": 19,
+      "filename": "variant-types.1.wasm",
+      "module_type": "binary"
+    },
+    {
+      "type": "module",
+      "line": 34,
+      "filename": "variant-types.2.wasm",
+      "module_type": "binary"
+    },
+    {
+      "type": "module",
+      "line": 50,
+      "filename": "variant-types.3.wasm",
+      "module_type": "binary"
+    },
+    {
+      "type": "assert_invalid",
+      "line": 67,
+      "filename": "variant-types.4.wasm",
+      "module_type": "binary",
+      "text": "expected to lower component `variant` type to core `(ref null? (struct))`"
+    },
+    {
+      "type": "assert_invalid",
+      "line": 85,
+      "filename": "variant-types.5.wasm",
+      "module_type": "binary",
+      "text": "expected to lower component `variant` type to core `(ref null? (struct))`"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/tests/snapshots/cli/component-model-gc/variant-types.wast/0.print b/tests/snapshots/cli/component-model-gc/variant-types.wast/0.print
new file mode 100644
index 00000000..312c5e88
--- /dev/null
+++ b/tests/snapshots/cli/component-model-gc/variant-types.wast/0.print
@@ -0,0 +1,16 @@
+(component
+  (type $variant (;0;) (variant (case "a" bool) (case "b" bool) (case "c" u8)))
+  (core type $variant (;0;) (struct))
+  (core type $ty (;1;) (func (param (ref $variant))))
+  (type (;1;)
+    (instance
+      (alias outer 1 $variant (type (;0;)))
+      (export (;1;) "ty" (type (eq 0)))
+      (type (;2;) (func (param "x" 1)))
+      (export (;0;) "f" (func (type 2)))
+    )
+  )
+  (import "i" (instance $i (;0;) (type 1)))
+  (alias export $i "f" (func (;0;)))
+  (core func (;0;) (canon lower (func 0) gc (core-type $ty)))
+)
diff --git a/tests/snapshots/cli/component-model-gc/variant-types.wast/1.print b/tests/snapshots/cli/component-model-gc/variant-types.wast/1.print
new file mode 100644
index 00000000..12668ccf
--- /dev/null
+++ b/tests/snapshots/cli/component-model-gc/variant-types.wast/1.print
@@ -0,0 +1,16 @@
+(component
+  (type $variant (;0;) (variant (case "a" bool) (case "b" bool) (case "c" u8)))
+  (core type $variant (;0;) (struct))
+  (core type $ty (;1;) (func (param (ref null $variant))))
+  (type (;1;)
+    (instance
+      (alias outer 1 $variant (type (;0;)))
+      (export (;1;) "ty" (type (eq 0)))
+      (type (;2;) (func (param "x" 1)))
+      (export (;0;) "f" (func (type 2)))
+    )
+  )
+  (import "i" (instance $i (;0;) (type 1)))
+  (alias export $i "f" (func (;0;)))
+  (core func (;0;) (canon lower (func 0) gc (core-type $ty)))
+)
diff --git a/tests/snapshots/cli/component-model-gc/variant-types.wast/2.print b/tests/snapshots/cli/component-model-gc/variant-types.wast/2.print
new file mode 100644
index 00000000..0e186b41
--- /dev/null
+++ b/tests/snapshots/cli/component-model-gc/variant-types.wast/2.print
@@ -0,0 +1,18 @@
+(component
+  (type $variant (;0;) (variant (case "a" bool) (case "b" bool) (case "c" u8)))
+  (core rec
+    (type $variant (;0;) (struct))
+    (type $ty (;1;) (func (param (ref null $variant))))
+  )
+  (type (;1;)
+    (instance
+      (alias outer 1 $variant (type (;0;)))
+      (export (;1;) "ty" (type (eq 0)))
+      (type (;2;) (func (param "x" 1)))
+      (export (;0;) "f" (func (type 2)))
+    )
+  )
+  (import "i" (instance $i (;0;) (type 1)))
+  (alias export $i "f" (func (;0;)))
+  (core func (;0;) (canon lower (func 0) gc (core-type $ty)))
+)
diff --git a/tests/snapshots/cli/component-model-gc/variant-types.wast/3.print b/tests/snapshots/cli/component-model-gc/variant-types.wast/3.print
new file mode 100644
index 00000000..86c64f77
--- /dev/null
+++ b/tests/snapshots/cli/component-model-gc/variant-types.wast/3.print
@@ -0,0 +1,17 @@
+(component
+  (type $variant (;0;) (variant (case "a" bool) (case "b" bool) (case "c" u8)))
+  (core type $base (;0;) (sub (struct)))
+  (core type $variant (;1;) (sub $base (struct)))
+  (core type $ty (;2;) (func (param (ref null $variant))))
+  (type (;1;)
+    (instance
+      (alias outer 1 $variant (type (;0;)))
+      (export (;1;) "ty" (type (eq 0)))
+      (type (;2;) (func (param "x" 1)))
+      (export (;0;) "f" (func (type 2)))
+    )
+  )
+  (import "i" (instance $i (;0;) (type 1)))
+  (alias export $i "f" (func (;0;)))
+  (core func (;0;) (canon lower (func 0) gc (core-type $ty)))
+)