Repository: bytecodealliance/wasm-tools
Pull request #2165 (2025-04-28 22:12:05)

Add a feature flag for GC in the component model
This is the first baby step towards integrating support for Wasm GC into the component model and its canonical ABI.


Patch:
diff --git a/crates/wasmparser/src/features.rs b/crates/wasmparser/src/features.rs
index c0d5e8e0..9e5ae374 100644
--- a/crates/wasmparser/src/features.rs
+++ b/crates/wasmparser/src/features.rs
@@ -139,7 +139,7 @@ define_wasm_features! {
     /// has any associated constants. When `features` are disabled all values
     /// for proposals are fixed at compile time to their defaults.
     #[derive(Hash, Debug, Copy, Clone, Eq, PartialEq)]
-    pub struct WasmFeatures: u32 {
+    pub struct WasmFeatures: u64 {
         /// The WebAssembly `mutable-global` proposal.
         pub mutable_global: MUTABLE_GLOBAL(1) = true;
         /// The WebAssembly `saturating-float-to-int` proposal.
@@ -259,6 +259,11 @@ define_wasm_features! {
         /// Corresponds to the 🔧 character in
         /// <https://github.com/WebAssembly/component-model/blob/main/design/mvp/Explainer.md>.
         pub cm_fixed_size_list: CM_FIXED_SIZE_LIST(1 << 31) = false;
+        /// Support for Wasm GC in the component model proposal.
+        ///
+        /// Corresponds to the 🛸 character in
+        /// <https://github.com/WebAssembly/component-model/blob/main/design/mvp/Explainer.md>.
+        pub cm_gc: CM_GC(1 << 32) = false;
     }
 }
 
diff --git a/crates/wasmparser/src/validator/component.rs b/crates/wasmparser/src/validator/component.rs
index aef3ca0d..13267bb7 100644
--- a/crates/wasmparser/src/validator/component.rs
+++ b/crates/wasmparser/src/validator/component.rs
@@ -7,8 +7,8 @@ use super::{
         ComponentCoreModuleTypeId, ComponentCoreTypeId, ComponentDefinedType,
         ComponentDefinedTypeId, ComponentEntityType, ComponentFuncType, ComponentFuncTypeId,
         ComponentInstanceType, ComponentInstanceTypeId, ComponentType, ComponentTypeId,
-        ComponentValType, Context, CoreInstanceTypeKind, InstanceType, LoweringInfo, ModuleType,
-        RecordType, Remap, Remapping, ResourceId, SubtypeCx, TupleType, VariantCase, VariantType,
+        ComponentValType, Context, CoreInstanceTypeKind, InstanceType, ModuleType, RecordType,
+        Remap, Remapping, ResourceId, SubtypeCx, TupleType, VariantCase, VariantType,
     },
     core::{InternRecGroup, Module},
     types::{CoreTypeId, EntityType, TypeAlloc, TypeInfo, TypeList},
@@ -207,6 +207,144 @@ impl ExternKind {
     }
 }
 
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
+pub(crate) enum Concurrency {
+    /// Synchronous.
+    #[default]
+    Sync,
+
+    /// Asynchronous.
+    Async {
+        /// When present, this is the function index of the async callback. When
+        /// omitted, we are either using stack-switching based asynchrony or are
+        /// in an operation that does not support the `callback` option (like
+        /// lowering).
+        callback: Option<u32>,
+    },
+}
+
+impl Concurrency {
+    pub(crate) fn is_sync(&self) -> bool {
+        matches!(self, Self::Sync)
+    }
+
+    pub(crate) fn is_async(&self) -> bool {
+        !self.is_sync()
+    }
+}
+
+pub(crate) struct CanonicalOptions {
+    pub(crate) memory: Option<u32>,
+    pub(crate) realloc: Option<u32>,
+    pub(crate) post_return: Option<u32>,
+    pub(crate) concurrency: Concurrency,
+}
+
+impl CanonicalOptions {
+    pub(crate) fn require_sync(&self, offset: usize, where_: &str) -> Result<&Self> {
+        if !self.concurrency.is_sync() {
+            bail!(offset, "cannot specify `async` option on `{where_}`")
+        }
+        Ok(self)
+    }
+
+    pub(crate) fn require_memory(&self, offset: usize) -> Result<&Self> {
+        if self.memory.is_none() {
+            bail!(offset, "canonical option `memory` is required");
+        }
+        Ok(self)
+    }
+
+    pub(crate) fn require_realloc(&self, offset: usize) -> Result<&Self> {
+        // Memory is always required when `realloc` is required.
+        self.require_memory(offset)?;
+
+        if self.realloc.is_none() {
+            bail!(offset, "canonical option `realloc` is required")
+        }
+
+        Ok(self)
+    }
+
+    pub(crate) fn require_memory_if(
+        &self,
+        offset: usize,
+        when: impl Fn() -> bool,
+    ) -> Result<&Self> {
+        if self.memory.is_none() && when() {
+            self.require_memory(offset)?;
+        }
+        Ok(self)
+    }
+
+    pub(crate) fn require_realloc_if(
+        &self,
+        offset: usize,
+        when: impl Fn() -> bool,
+    ) -> Result<&Self> {
+        if self.realloc.is_none() && when() {
+            self.require_realloc(offset)?;
+        }
+        Ok(self)
+    }
+
+    pub(crate) fn check_lower(&self, offset: usize) -> Result<&Self> {
+        if self.post_return.is_some() {
+            bail!(
+                offset,
+                "canonical option `post-return` cannot be specified for lowerings"
+            );
+        }
+
+        if let Concurrency::Async { callback: Some(_) } = self.concurrency {
+            bail!(
+                offset,
+                "canonical option `callback` cannot be specified for lowerings"
+            );
+        }
+
+        Ok(self)
+    }
+
+    pub(crate) fn check_lift(
+        &self,
+        types: &TypeList,
+        state: &ComponentState,
+        core_ty: &FuncType,
+        offset: usize,
+    ) -> Result<&Self> {
+        if let Some(idx) = self.post_return {
+            let func_ty = types[state.core_function_at(idx, offset)?].unwrap_func();
+            if func_ty.params() != core_ty.results() || !func_ty.results().is_empty() {
+                bail!(offset, "canonical option `post-return` uses a core function with an incorrect signature");
+            }
+        }
+
+        match self.concurrency {
+            Concurrency::Sync => {}
+
+            Concurrency::Async { callback: None } if !state.features.cm_async_stackful() => {
+                bail!(offset, "requires the async stackful feature")
+            }
+            Concurrency::Async { callback: None } => {}
+
+            Concurrency::Async {
+                callback: Some(idx),
+            } => {
+                let func_ty = types[state.core_function_at(idx, offset)?].unwrap_func();
+                if func_ty.params() != [ValType::I32; 3] && func_ty.params() != [ValType::I32] {
+                    return Err(BinaryReaderError::new(
+                        "canonical option `callback` uses a core function with an incorrect signature",
+                        offset,
+                    ));
+                }
+            }
+        }
+
+        Ok(self)
+    }
+}
+
 impl ComponentState {
     pub fn new(kind: ComponentKind, features: WasmFeatures) -> Self {
         Self {
@@ -1035,41 +1173,58 @@ impl ComponentState {
         core_func_index: u32,
         type_index: u32,
         options: &[CanonicalOption],
-        types: &TypeList,
+        types: &mut TypeAlloc,
         offset: usize,
     ) -> Result<()> {
         let ty = self.function_type_at(type_index, types, offset)?;
-        let core_ty = types[self.core_function_at(core_func_index, offset)?].unwrap_func();
+        let core_ty_id = self.core_function_at(core_func_index, offset)?;
+        let core_ty = types[core_ty_id].unwrap_func();
 
         // Lifting a function is for an export, so match the expected canonical ABI
         // export signature
-        let info = ty.lower(types, Abi::for_lift(options));
-        self.check_options(Some(core_ty), &info, options, types, offset, true)?;
+        let options = self.check_options(types, options, offset)?;
+        options.check_lift(types, self, core_ty, offset)?;
+        let func_ty = ty.lower(types, &options, Abi::Lift, offset)?;
+        let lowered_core_ty_id = types.intern_func_type(func_ty, offset);
+
+        if core_ty_id == lowered_core_ty_id {
+            self.funcs
+                .push(self.types[type_index as usize].unwrap_func());
+            return Ok(());
+        }
 
-        if core_ty.params() != info.params.as_slice() {
+        let ty = types[core_ty_id].unwrap_func();
+        let lowered_ty = types[lowered_core_ty_id].unwrap_func();
+
+        if lowered_ty.params() != ty.params() {
             bail!(
                 offset,
-                "lowered parameter types `{:?}` do not match parameter types \
-                 `{:?}` of core function {core_func_index}",
-                info.params.as_slice(),
-                core_ty.params(),
+                "lowered parameter types `{:?}` do not match parameter types `{:?}` of \
+                     core function {core_func_index}",
+                lowered_ty.params(),
+                ty.params()
             );
         }
 
-        if core_ty.results() != info.results.as_slice() {
+        if lowered_ty.results() != ty.results() {
             bail!(
                 offset,
-                "lowered result types `{:?}` do not match result types \
-                 `{:?}` of core function {core_func_index}",
-                info.results.as_slice(),
-                core_ty.results()
+                "lowered result types `{:?}` do not match result types `{:?}` of \
+                     core function {core_func_index}",
+                lowered_ty.results(),
+                ty.results()
             );
         }
 
-        self.funcs
-            .push(self.types[type_index as usize].unwrap_func());
-
-        Ok(())
+        // Otherwise, must be different rec groups or subtyping (which isn't
+        // supported yet) or something.
+        bail!(
+            offset,
+            "lowered function type `{:?}` does not match type `{:?}` of \
+                 core function {core_func_index}",
+            types[lowered_core_ty_id],
+            types[core_ty_id],
+        );
     }
 
     fn lower_function(
@@ -1083,20 +1238,12 @@ impl ComponentState {
 
         // Lowering a function is for an import, so use a function type that matches
         // the expected canonical ABI import signature.
-        let info = ty.lower(
-            types,
-            if options.contains(&CanonicalOption::Async) {
-                Abi::LowerAsync
-            } else {
-                Abi::LowerSync
-            },
-        );
-
-        self.check_options(None, &info, options, types, offset, true)?;
-
-        let id = types.intern_func_type(info.into_func_type(), offset);
-        self.core_funcs.push(id);
+        let options = self.check_options(types, options, offset)?;
+        options.check_lower(offset)?;
+        let func_ty = ty.lower(types, &options, Abi::Lower, offset)?;
+        let core_ty_id = types.intern_func_type(func_ty, offset);
 
+        self.core_funcs.push(core_ty_id);
         Ok(())
     }
 
@@ -1166,21 +1313,7 @@ impl ComponentState {
             )
         }
 
-        for option in options {
-            let invalid = match option {
-                CanonicalOption::UTF8
-                | CanonicalOption::UTF16
-                | CanonicalOption::CompactUTF16
-                | CanonicalOption::Memory(_) => continue,
-                CanonicalOption::Realloc(_) => "realloc",
-                CanonicalOption::PostReturn(_) => "post-return",
-                CanonicalOption::Async => "async",
-                CanonicalOption::Callback(_) => "callback",
-            };
-            bail!(offset, "cannot specify `{invalid}` option on `task.return`")
-        }
-
-        let info = ComponentFuncType {
+        let func_ty = ComponentFuncType {
             info: TypeInfo::new(),
             params: result
                 .iter()
@@ -1199,15 +1332,26 @@ impl ComponentState {
                 })
                 .collect::<Result<_>>()?,
             result: None,
-        }
-        .lower(types, Abi::LowerSync);
+        };
 
-        self.check_options(None, &info, options, types, offset, true)?;
+        let options = self.check_options(types, options, offset)?;
+        if options.realloc.is_some() {
+            bail!(offset, "cannot specify `realloc` option on `task.return`")
+        }
+        if options.post_return.is_some() {
+            bail!(
+                offset,
+                "cannot specify `post-return` option on `task.return`"
+            )
+        }
+        options.check_lower(offset)?;
+        options.require_sync(offset, "task.return")?;
 
-        assert!(info.results.iter().next().is_none());
+        let func_ty = func_ty.lower(types, &options, Abi::Lower, offset)?;
+        assert!(func_ty.results().is_empty());
 
         self.core_funcs
-            .push(types.intern_func_type(FuncType::new(info.params.iter(), []), offset));
+            .push(types.intern_func_type(func_ty, offset));
         Ok(())
     }
 
@@ -1337,16 +1481,14 @@ impl ComponentState {
         }
 
         let ty = self.defined_type_at(ty, offset)?;
-        let ComponentDefinedType::Stream(payload_type) = &types[ty] else {
+        let ComponentDefinedType::Stream(elem_ty) = &types[ty] else {
             bail!(offset, "`stream.read` requires a stream type")
         };
 
-        let mut info = LoweringInfo::default();
-        info.requires_memory = true;
-        info.requires_realloc = payload_type
-            .map(|ty| ty.contains_ptr(types))
-            .unwrap_or_default();
-        self.check_options(None, &info, options, types, offset, true)?;
+        self.check_options(types, options, offset)?
+            .require_memory(offset)?
+            .require_realloc_if(offset, || elem_ty.is_some_and(|ty| ty.contains_ptr(types)))?
+            .check_lower(offset)?;
 
         self.core_funcs
             .push(types.intern_func_type(FuncType::new([ValType::I32; 3], [ValType::I32]), offset));
@@ -1372,10 +1514,9 @@ impl ComponentState {
             bail!(offset, "`stream.write` requires a stream type")
         };
 
-        let mut info = LoweringInfo::default();
-        info.requires_memory = true;
-        info.requires_realloc = false;
-        self.check_options(None, &info, options, types, offset, true)?;
+        self.check_options(types, options, offset)?
+            .require_memory(offset)?
+            .check_lower(offset)?;
 
         self.core_funcs
             .push(types.intern_func_type(FuncType::new([ValType::I32; 3], [ValType::I32]), offset));
@@ -1521,16 +1662,14 @@ impl ComponentState {
         }
 
         let ty = self.defined_type_at(ty, offset)?;
-        let ComponentDefinedType::Future(payload_type) = &types[ty] else {
+        let ComponentDefinedType::Future(elem_ty) = &types[ty] else {
             bail!(offset, "`future.read` requires a future type")
         };
 
-        let mut info = LoweringInfo::default();
-        info.requires_memory = true;
-        info.requires_realloc = payload_type
-            .map(|ty| ty.contains_ptr(types))
-            .unwrap_or_default();
-        self.check_options(None, &info, options, types, offset, true)?;
+        self.check_options(types, options, offset)?
+            .require_memory(offset)?
+            .require_realloc_if(offset, || elem_ty.is_some_and(|ty| ty.contains_ptr(types)))?
+            .check_lower(offset)?;
 
         self.core_funcs
             .push(types.intern_func_type(FuncType::new([ValType::I32; 2], [ValType::I32]), offset));
@@ -1556,10 +1695,9 @@ impl ComponentState {
             bail!(offset, "`future.write` requires a future type")
         };
 
-        let mut info = LoweringInfo::default();
-        info.requires_memory = true;
-        info.requires_realloc = false;
-        self.check_options(None, &info, &options, types, offset, true)?;
+        self.check_options(types, &options, offset)?
+            .require_memory(offset)?
+            .check_lower(offset)?;
 
         self.core_funcs
             .push(types.intern_func_type(FuncType::new([ValType::I32; 2], [ValType::I32]), offset));
@@ -1685,10 +1823,10 @@ impl ComponentState {
             )
         }
 
-        let mut info = LoweringInfo::default();
-        info.requires_memory = true;
-        info.requires_realloc = false;
-        self.check_options(None, &info, &options, types, offset, false)?;
+        self.check_options(types, &options, offset)?
+            .require_memory(offset)?
+            .require_sync(offset, "error-context.new")?
+            .check_lower(offset)?;
 
         self.core_funcs
             .push(types.intern_func_type(FuncType::new([ValType::I32; 2], [ValType::I32]), offset));
@@ -1708,10 +1846,11 @@ impl ComponentState {
             )
         }
 
-        let mut info = LoweringInfo::default();
-        info.requires_memory = true;
-        info.requires_realloc = true;
-        self.check_options(None, &info, &options, types, offset, false)?;
+        self.check_options(types, &options, offset)?
+            .require_memory(offset)?
+            .require_realloc(offset)?
+            .require_sync(offset, "error-context.debug-message")?
+            .check_lower(offset)?;
 
         self.core_funcs
             .push(types.intern_func_type(FuncType::new([ValType::I32; 2], []), offset));
@@ -2111,13 +2250,10 @@ impl ComponentState {
 
     fn check_options(
         &self,
-        core_ty: Option<&FuncType>,
-        info: &LoweringInfo,
-        options: &[CanonicalOption],
         types: &TypeList,
+        options: &[CanonicalOption],
         offset: usize,
-        allow_async: bool,
-    ) -> Result<()> {
+    ) -> Result<CanonicalOptions> {
         fn display(option: CanonicalOption) -> &'static str {
             match option {
                 CanonicalOption::UTF8 => "utf8",
@@ -2135,7 +2271,7 @@ impl ComponentState {
         let mut memory = None;
         let mut realloc = None;
         let mut post_return = None;
-        let mut async_ = false;
+        let mut is_async = false;
         let mut callback = None;
 
         for option in options {
@@ -2170,10 +2306,11 @@ impl ComponentState {
                 CanonicalOption::Realloc(idx) => {
                     realloc = match realloc {
                         None => {
-                            let ty = types[self.core_function_at(*idx, offset)?].unwrap_func();
-                            if ty.params()
+                            let ty_id = self.core_function_at(*idx, offset)?;
+                            let func_ty = types[ty_id].unwrap_func();
+                            if func_ty.params()
                                 != [ValType::I32, ValType::I32, ValType::I32, ValType::I32]
-                                || ty.results() != [ValType::I32]
+                                || func_ty.results() != [ValType::I32]
                             {
                                 return Err(BinaryReaderError::new(
                                     "canonical option `realloc` uses a core function with an incorrect signature",
@@ -2192,24 +2329,7 @@ impl ComponentState {
                 }
                 CanonicalOption::PostReturn(idx) => {
                     post_return = match post_return {
-                        None => {
-                            let core_ty = core_ty.ok_or_else(|| {
-                                BinaryReaderError::new(
-                                    "canonical option `post-return` cannot be specified for lowerings",
-                                    offset,
-                                )
-                            })?;
-
-                            let ty = types[self.core_function_at(*idx, offset)?].unwrap_func();
-
-                            if ty.params() != core_ty.results() || !ty.results().is_empty() {
-                                return Err(BinaryReaderError::new(
-                                    "canonical option `post-return` uses a core function with an incorrect signature",
-                                    offset,
-                                ));
-                            }
-                            Some(*idx)
-                        }
+                        None => Some(*idx),
                         Some(_) => {
                             return Err(BinaryReaderError::new(
                                 "canonical option `post-return` is specified more than once",
@@ -2219,7 +2339,7 @@ impl ComponentState {
                     }
                 }
                 CanonicalOption::Async => {
-                    if async_ {
+                    if is_async {
                         return Err(BinaryReaderError::new(
                             "canonical option `async` is specified more than once",
                             offset,
@@ -2232,29 +2352,12 @@ impl ComponentState {
                             );
                         }
 
-                        async_ = true;
+                        is_async = true;
                     }
                 }
                 CanonicalOption::Callback(idx) => {
                     callback = match callback {
-                        None => {
-                            if core_ty.is_none() {
-                                return Err(BinaryReaderError::new(
-                                    "canonical option `callback` cannot be specified for lowerings",
-                                    offset,
-                                ));
-                            }
-
-                            let ty = types[self.core_function_at(*idx, offset)?].unwrap_func();
-
-                            if ty.params() != [ValType::I32; 3] && ty.params() != [ValType::I32] {
-                                return Err(BinaryReaderError::new(
-                                    "canonical option `callback` uses a core function with an incorrect signature",
-                                    offset,
-                                ));
-                            }
-                            Some(*idx)
-                        }
+                        None => Some(*idx),
                         Some(_) => {
                             return Err(BinaryReaderError::new(
                                 "canonical option `callback` is specified more than once",
@@ -2266,62 +2369,23 @@ impl ComponentState {
             }
         }
 
-        // Validate various combinations of options with respect to async.
-        // Modeled as a `match` here to double-check that everything is
-        // exhaustive at compile-time.
-        match (
-            async_,
-            allow_async,
-            callback.is_some(),
-            core_ty,
-            post_return.is_some(),
-        ) {
-            (true, false, ..) => bail!(offset, "async option not allowed here"),
-            (false, _, true, _, _) => {
-                bail!(offset, "cannot specify callback without lifting async")
-            }
-            (true, true, _, _, true) => {
-                bail!(
-                    offset,
-                    "cannot specify post-return function when lifting async"
-                )
+        let concurrency = match (is_async, callback, post_return.is_some()) {
+            (false, Some(_), _) => {
+                bail!(offset, "cannot specify callback without async")
             }
-
-            // Async + allowed + this is a lift (core_ty present) + stackful ABI
-            (true, true, false, Some(_), false) => {
-                if !self.features.cm_async_stackful() {
-                    bail!(
-                        offset,
-                        "`async` without `callback` requires the async stackful feature"
-                    )
-                }
+            (true, _, true) => {
+                bail!(offset, "cannot specify post-return function in async")
             }
+            (false, None, _) => Concurrency::Sync,
+            (true, callback, false) => Concurrency::Async { callback },
+        };
 
-            // Async + allowed + this is a lower (no core_ty)
-            (true, true, false, None, false) => {}
-
-            // Not async, no callback, this is ok
-            (false, _, false, _, _) => {}
-
-            // Async + allowed + callback ABI
-            (true, true, true, _, false) => {}
-        }
-
-        if info.requires_memory && memory.is_none() {
-            return Err(BinaryReaderError::new(
-                "canonical option `memory` is required",
-                offset,
-            ));
-        }
-
-        if info.requires_realloc && realloc.is_none() {
-            return Err(BinaryReaderError::new(
-                "canonical option `realloc` is required",
-                offset,
-            ));
-        }
-
-        Ok(())
+        Ok(CanonicalOptions {
+            memory,
+            realloc,
+            post_return,
+            concurrency,
+        })
     }
 
     fn check_type_ref(
diff --git a/crates/wasmparser/src/validator/component_types.rs b/crates/wasmparser/src/validator/component_types.rs
index 2f14b371..99101f1a 100644
--- a/crates/wasmparser/src/validator/component_types.rs
+++ b/crates/wasmparser/src/validator/component_types.rs
@@ -1,6 +1,7 @@
 //! Types relating to type information provided by validation.
 
 use super::component::ExternKind;
+use super::{CanonicalOptions, Concurrency};
 use crate::prelude::*;
 use crate::validator::names::KebabString;
 use crate::validator::types::{
@@ -8,8 +9,7 @@ use crate::validator::types::{
     Types, TypesKind, TypesRef, TypesRefKind,
 };
 use crate::{
-    BinaryReaderError, CanonicalOption, FuncType, MemoryType, PrimitiveValType, Result, TableType,
-    ValType,
+    BinaryReaderError, FuncType, MemoryType, PrimitiveValType, Result, TableType, ValType,
 };
 use core::fmt;
 use core::ops::Index;
@@ -96,16 +96,15 @@ impl fmt::Debug for LoweredTypes {
     }
 }
 
-/// Represents information about a component function type lowering.
+/// Represents a component function type's in-progress lowering into a core
+/// type.
 #[derive(Debug)]
-pub(crate) struct LoweringInfo {
-    pub(crate) params: LoweredTypes,
-    pub(crate) results: LoweredTypes,
-    pub(crate) requires_memory: bool,
-    pub(crate) requires_realloc: bool,
+struct LoweredSignature {
+    params: LoweredTypes,
+    results: LoweredTypes,
 }
 
-impl LoweringInfo {
+impl LoweredSignature {
     pub(crate) fn into_func_type(self) -> FuncType {
         FuncType::new(
             self.params.as_slice().iter().copied(),
@@ -114,13 +113,11 @@ impl LoweringInfo {
     }
 }
 
-impl Default for LoweringInfo {
+impl Default for LoweredSignature {
     fn default() -> Self {
         Self {
             params: LoweredTypes::new(MAX_FLAT_FUNC_PARAMS),
             results: LoweredTypes::new(MAX_FLAT_FUNC_RESULTS),
-            requires_memory: false,
-            requires_realloc: false,
         }
     }
 }
@@ -903,67 +900,35 @@ impl TypeData for ComponentFuncType {
     }
 }
 
-#[derive(Copy, Clone, Debug)]
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
 pub(crate) enum Abi {
-    /// Use to generate the core wasm signature of a component model function
-    /// that is `canon lower`'d with the synchronous ABI option set.
-    LowerSync,
-    /// Use to generate the core wasm signature of a component model function
-    /// that is `canon lower`'d with the asynchronous ABI option set.
-    LowerAsync,
-    /// Use to generate the core wasm signature that when `canon lift`'d with
-    /// the synchronous ABI option set will generate a component model function
-    /// type.
-    LiftSync,
-    /// Use to generate the core wasm signature that when `canon lift`'d with
-    /// the asynchronous + callback ABI options set will generate a component
-    /// model function type.
-    LiftAsync,
-    /// Use to generate the core wasm signature that when `canon lift`'d with
-    /// the asynchronou ABI option set will generate a component
-    /// model function type.
-    LiftAsyncStackful,
-}
-
-impl Abi {
-    pub(crate) fn for_lift(options: &[CanonicalOption]) -> Abi {
-        if options.contains(&CanonicalOption::Async) {
-            if options
-                .iter()
-                .any(|v| matches!(v, CanonicalOption::Callback(_)))
-            {
-                Abi::LiftAsync
-            } else {
-                Abi::LiftAsyncStackful
-            }
-        } else {
-            Abi::LiftSync
-        }
-    }
+    Lift,
+    Lower,
 }
 
 impl ComponentFuncType {
     /// Lowers the component function type to core parameter and result types for the
     /// canonical ABI.
-    pub(crate) fn lower(&self, types: &TypeList, abi: Abi) -> LoweringInfo {
-        let mut info = LoweringInfo::default();
+    pub(crate) fn lower(
+        &self,
+        types: &TypeList,
+        options: &CanonicalOptions,
+        abi: Abi,
+        offset: usize,
+    ) -> Result<FuncType> {
+        let mut sig = LoweredSignature::default();
 
-        let is_lower = match abi {
-            Abi::LowerAsync => {
-                for _ in 0..2 {
-                    info.params.push(ValType::I32);
-                }
-                info.results.push(ValType::I32);
-                info.requires_memory = true;
-                info.requires_realloc = self
-                    .result
-                    .map(|ty| ty.contains_ptr(types))
-                    .unwrap_or(false);
-                return info;
-            }
-            Abi::LowerSync => true,
-            Abi::LiftSync | Abi::LiftAsync | Abi::LiftAsyncStackful => false,
-        };
+        if abi == Abi::Lower && options.concurrency.is_async() {
+            for _ in 0..2 {
+                sig.params.push(ValType::I32);
+            }
+            sig.results.push(ValType::I32);
+            options.require_memory(offset)?;
+            if self.result.is_some_and(|ty| ty.contains_ptr(types)) {
+                options.require_realloc(offset)?;
+            }
+            return Ok(sig.into_func_type());
+        }
 
         for (_, ty) in self.params.iter() {
             // Check to see if `ty` has a pointer somewhere in it, needed for
@@ -972,68 +937,70 @@ impl ComponentFuncType {
             // lifted functions must specify `realloc` as well. Lifted functions
             // gain their memory requirement through the final clause of this
             // function.
-            if is_lower {
-                if !info.requires_memory {
-                    info.requires_memory = ty.contains_ptr(types);
+            match abi {
+                Abi::Lower => {
+                    options.require_memory_if(offset, || ty.contains_ptr(types))?;
                 }
-            } else {
-                if !info.requires_realloc {
-                    info.requires_realloc = ty.contains_ptr(types);
+                Abi::Lift => {
+                    options.require_realloc_if(offset, || ty.contains_ptr(types))?;
                 }
             }
 
-            if !ty.push_wasm_types(types, &mut info.params) {
+            if !ty.push_wasm_types(types, &mut sig.params) {
                 // Too many parameters to pass directly
                 // Function will have a single pointer parameter to pass the arguments
                 // via linear memory
-                info.params.clear();
-                assert!(info.params.push(ValType::I32));
-                info.requires_memory = true;
+                sig.params.clear();
+                assert!(sig.params.push(ValType::I32));
+                options.require_memory(offset)?;
 
                 // We need realloc as well when lifting a function
-                if !is_lower {
-                    info.requires_realloc = true;
+                if let Abi::Lift = abi {
+                    options.require_realloc(offset)?;
                 }
                 break;
             }
         }
 
-        match abi {
-            Abi::LowerAsync => unreachable!(),
-            Abi::LowerSync | Abi::LiftSync => {
+        match (abi, options.concurrency) {
+            (Abi::Lower, Concurrency::Async { .. }) => {
+                unreachable!("special-cased at the start of the function")
+            }
+            (Abi::Lower | Abi::Lift, Concurrency::Sync) => {
                 if let Some(ty) = &self.result {
                     // Results of lowered functions that contains pointers must be
                     // allocated by the callee meaning that realloc is required.
                     // Results of lifted function are allocated by the guest which
                     // means that no realloc option is necessary.
-                    if is_lower && !info.requires_realloc {
-                        info.requires_realloc = ty.contains_ptr(types);
-                    }
-
-                    if !ty.push_wasm_types(types, &mut info.results) {
-                        // Too many results to return directly, either a retptr parameter will be used (import)
-                        // or a single pointer will be returned (export)
-                        info.results.clear();
-                        if is_lower {
-                            info.params.max = MAX_LOWERED_TYPES;
-                            assert!(info.params.push(ValType::I32));
-                        } else {
-                            assert!(info.results.push(ValType::I32));
+                    options.require_realloc_if(offset, || {
+                        abi == Abi::Lower && ty.contains_ptr(types)
+                    })?;
+
+                    if !ty.push_wasm_types(types, &mut sig.results) {
+                        // Too many results to return directly, either a retptr
+                        // parameter will be used (import) or a single pointer
+                        // will be returned (export).
+                        sig.results.clear();
+                        options.require_memory(offset)?;
+                        match abi {
+                            Abi::Lower => {
+                                sig.params.max = MAX_LOWERED_TYPES;
+                                assert!(sig.params.push(ValType::I32));
+                            }
+                            Abi::Lift => {
+                                assert!(sig.results.push(ValType::I32));
+                            }
                         }
-                        info.requires_memory = true;
                     }
                 }
             }
-            Abi::LiftAsync => {
-                info.results.push(ValType::I32);
+            (Abi::Lift, Concurrency::Async { callback: Some(_) }) => {
+                sig.results.push(ValType::I32);
             }
-            Abi::LiftAsyncStackful => {}
+            (Abi::Lift, Concurrency::Async { callback: None }) => {}
         }
 
-        // Memory is always required when realloc is required
-        info.requires_memory |= info.requires_realloc;
-
-        info
+        Ok(sig.into_func_type())
     }
 }
 
diff --git a/crates/wasmparser/src/validator/types.rs b/crates/wasmparser/src/validator/types.rs
index 9105e009..fe3bb1dd 100644
--- a/crates/wasmparser/src/validator/types.rs
+++ b/crates/wasmparser/src/validator/types.rs
@@ -947,7 +947,7 @@ impl TypeList {
     /// [`Self::intern_canonical_rec_group`].
     pub fn intern_sub_type(&mut self, sub_ty: SubType, offset: usize) -> CoreTypeId {
         let (_is_new, group_id) =
-            self.intern_canonical_rec_group(false, RecGroup::implicit(offset, sub_ty));
+            self.intern_canonical_rec_group(true, RecGroup::implicit(offset, sub_ty));
         self[group_id].start
     }
 


Test patch:
diff --git a/tests/cli/component-model-async/lift.wast b/tests/cli/component-model-async/lift.wast
index a37b1c8b..dbc08bed 100644
--- a/tests/cli/component-model-async/lift.wast
+++ b/tests/cli/component-model-async/lift.wast
@@ -25,7 +25,7 @@
       (canon lift (core func $i "foo") async (post-return (func $i "post-return-foo")))
     )
   )
-  "cannot specify post-return function when lifting async"
+  "cannot specify post-return function in async"
 )
 
 ;; async lift; with callback
@@ -71,7 +71,7 @@
       (canon lift (core func $i "foo") async (callback (func $i "callback")) (post-return (func $i "post-return-foo")))
     )
   )
-  "cannot specify post-return function when lifting async"
+  "cannot specify post-return function in async"
 )
 
 ;; async lift; with incorrectly-typed core function
@@ -118,5 +118,5 @@
       (canon lift (core func $i "foo") (callback (func $i "callback")))
     )
   )
-  "cannot specify callback without lifting async"
+  "cannot specify callback without async"
 )
diff --git a/tests/cli/component-model-async/task-builtins.wast b/tests/cli/component-model-async/task-builtins.wast
index 2cb8ed64..9d364d5b 100644
--- a/tests/cli/component-model-async/task-builtins.wast
+++ b/tests/cli/component-model-async/task-builtins.wast
@@ -39,7 +39,7 @@
     (core func $f (canon backpressure.set))
     (core func $task-return (canon task.return (result u32) (callback $f)))
   )
-  "cannot specify `callback` option on `task.return`")
+  "cannot specify callback without async")
 
 (assert_invalid
   (component
diff --git a/tests/cli/validate-unknown-features.wat.stderr b/tests/cli/validate-unknown-features.wat.stderr
index 6c52f3ea..b14cec9e 100644
--- a/tests/cli/validate-unknown-features.wat.stderr
+++ b/tests/cli/validate-unknown-features.wat.stderr
@@ -1,4 +1,4 @@
 error: invalid value 'unknown' for '--features <FEATURES>': unknown feature `unknown`
-Valid features: mutable-global, saturating-float-to-int, sign-extension, reference-types, multi-value, bulk-memory, simd, relaxed-simd, threads, shared-everything-threads, tail-call, floats, multi-memory, exceptions, memory64, extended-const, component-model, function-references, memory-control, gc, custom-page-sizes, legacy-exceptions, gc-types, stack-switching, wide-arithmetic, cm-values, cm-nested-names, cm-async, cm-async-stackful, cm-async-builtins, cm-error-context, cm-fixed-size-list, mvp, wasm1, wasm2, wasm3, all
+Valid features: mutable-global, saturating-float-to-int, sign-extension, reference-types, multi-value, bulk-memory, simd, relaxed-simd, threads, shared-everything-threads, tail-call, floats, multi-memory, exceptions, memory64, extended-const, component-model, function-references, memory-control, gc, custom-page-sizes, legacy-exceptions, gc-types, stack-switching, wide-arithmetic, cm-values, cm-nested-names, cm-async, cm-async-stackful, cm-async-builtins, cm-error-context, cm-fixed-size-list, cm-gc, mvp, wasm1, wasm2, wasm3, all
 
 For more information, try '--help'.
diff --git a/tests/snapshots/cli/component-model-async/lift.wast.json b/tests/snapshots/cli/component-model-async/lift.wast.json
index d65d106b..8a2e0682 100644
--- a/tests/snapshots/cli/component-model-async/lift.wast.json
+++ b/tests/snapshots/cli/component-model-async/lift.wast.json
@@ -12,7 +12,7 @@
       "line": 17,
       "filename": "lift.1.wasm",
       "module_type": "binary",
-      "text": "cannot specify post-return function when lifting async"
+      "text": "cannot specify post-return function in async"
     },
     {
       "type": "module",
@@ -32,7 +32,7 @@
       "line": 62,
       "filename": "lift.4.wasm",
       "module_type": "binary",
-      "text": "cannot specify post-return function when lifting async"
+      "text": "cannot specify post-return function in async"
     },
     {
       "type": "assert_invalid",
@@ -53,7 +53,7 @@
       "line": 110,
       "filename": "lift.7.wasm",
       "module_type": "binary",
-      "text": "cannot specify callback without lifting async"
+      "text": "cannot specify callback without async"
     }
   ]
 }
\ No newline at end of file
diff --git a/tests/snapshots/cli/component-model-async/task-builtins.wast.json b/tests/snapshots/cli/component-model-async/task-builtins.wast.json
index 7f9ba4c3..9d38e0ce 100644
--- a/tests/snapshots/cli/component-model-async/task-builtins.wast.json
+++ b/tests/snapshots/cli/component-model-async/task-builtins.wast.json
@@ -32,7 +32,7 @@
       "line": 38,
       "filename": "task-builtins.4.wasm",
       "module_type": "binary",
-      "text": "cannot specify `callback` option on `task.return`"
+      "text": "cannot specify callback without async"
     },
     {
       "type": "assert_invalid",