Repository: bytecodealliance/wasm-tools
Pull request #2095 (2025-03-11 20:29:30)

Gate some async features behind separate feature gates
This is an implementation of WebAssembly/component-model#468 in this repository. This also takes a moment to rename some `component-model-*` feature names to `cm-*` to avoid being so wordy. Additionally this renumbers some features to compress the bit space being used.


Patch:
diff --git a/crates/wasmparser/src/features.rs b/crates/wasmparser/src/features.rs
index fdd693c9..f46f2e89 100644
--- a/crates/wasmparser/src/features.rs
+++ b/crates/wasmparser/src/features.rs
@@ -193,17 +193,13 @@ define_wasm_features! {
         /// The WebAssembly [custom-page-sizes
         /// proposal](https://github.com/WebAssembly/custom-page-sizes).
         pub custom_page_sizes: CUSTOM_PAGE_SIZES(1 << 20) = false;
-        /// Support for the `value` type in the component model proposal.
-        pub component_model_values: COMPONENT_MODEL_VALUES(1 << 21) = false;
-        /// Support for the nested namespaces and projects in component model names.
-        pub component_model_nested_names: COMPONENT_MODEL_NESTED_NAMES(1 << 22) = false;
         /// The WebAssembly legacy exception handling proposal (phase 1)
         ///
         /// # Note
         ///
         /// Support this feature as long as all leading browsers also support it
         /// <https://github.com/WebAssembly/exception-handling/blob/main/proposals/exception-handling/legacy/Exceptions.md>
-        pub legacy_exceptions: LEGACY_EXCEPTIONS(1 << 25) = false;
+        pub legacy_exceptions: LEGACY_EXCEPTIONS(1 << 23) = false;
         /// Whether or not gc types are enabled.
         ///
         /// This feature does not correspond to any WebAssembly proposal nor
@@ -220,13 +216,38 @@ define_wasm_features! {
         /// Note that the `funcref` and `exnref` types are not gated by this
         /// feature. Those are expected to not require a full garbage collector
         /// so are not gated by this.
-        pub gc_types: GC_TYPES(1 << 26) = true;
+        pub gc_types: GC_TYPES(1 << 24) = true;
         /// The WebAssembly [stack-switching proposal](https://github.com/WebAssembly/stack-switching).
-        pub stack_switching: STACK_SWITCHING(1 << 27) = false;
+        pub stack_switching: STACK_SWITCHING(1 << 25) = false;
         /// The WebAssembly [wide-arithmetic proposal](https://github.com/WebAssembly/wide-arithmetic).
-        pub wide_arithmetic: WIDE_ARITHMETIC(1 << 28) = false;
+        pub wide_arithmetic: WIDE_ARITHMETIC(1 << 26) = false;
+
+        /// Support for the `value` type in the component model proposal.
+        ///
+        /// Corresponds to the 🪙 character in
+        /// <https://github.com/WebAssembly/component-model/blob/main/design/mvp/Explainer.md>.
+        pub cm_values: CM_VALUES(1 << 21) = false;
+        /// Support for the nested namespaces and projects in component model names.
+        ///
+        /// Corresponds to the 🪺 character in
+        /// <https://github.com/WebAssembly/component-model/blob/main/design/mvp/Explainer.md>.
+        pub cm_nested_names: CM_NESTED_NAMES(1 << 22) = false;
         /// Support for component model async lift/lower ABI, as well as streams, futures, and errors.
-        pub component_model_async: COMPONENT_MODEL_ASYNC(1 << 29) = false;
+        ///
+        /// Corresponds to the 🔀 character in
+        /// <https://github.com/WebAssembly/component-model/blob/main/design/mvp/Explainer.md>.
+        pub cm_async: CM_ASYNC(1 << 27) = false;
+        /// Gates the "stackful ABI" in the component model async proposal.
+        ///
+        /// Corresponds to the 🚟 character in
+        /// <https://github.com/WebAssembly/component-model/blob/main/design/mvp/Explainer.md>.
+        pub cm_async_stackful: CM_ASYNC_STACKFUL(1 << 28) = false;
+        /// Gates some intrinsics being marked with `async` in the component
+        /// model async proposal.
+        ///
+        /// Corresponds to the 🚝 character in
+        /// <https://github.com/WebAssembly/component-model/blob/main/design/mvp/Explainer.md>.
+        pub cm_async_builtins: CM_ASYNC_BUILTINS(1 << 29) = false;
     }
 }
 
diff --git a/crates/wasmparser/src/validator/component.rs b/crates/wasmparser/src/validator/component.rs
index 4f5cafc1..45af6a10 100644
--- a/crates/wasmparser/src/validator/component.rs
+++ b/crates/wasmparser/src/validator/component.rs
@@ -1149,10 +1149,10 @@ impl ComponentState {
         offset: usize,
         features: &WasmFeatures,
     ) -> Result<()> {
-        if !features.component_model_async() {
+        if !features.cm_async_builtins() {
             bail!(
                 offset,
-                "`resource.drop` as `async` requires the component model async feature"
+                "`resource.drop` as `async` requires the component model async builtins feature"
             )
         }
         self.resource_at(resource, types, offset)?;
@@ -1174,7 +1174,7 @@ impl ComponentState {
         offset: usize,
         features: &WasmFeatures,
     ) -> Result<()> {
-        if !features.component_model_async() {
+        if !features.cm_async() {
             bail!(
                 offset,
                 "`backpressure.set` requires the component model async feature"
@@ -1194,7 +1194,7 @@ impl ComponentState {
         offset: usize,
         features: &WasmFeatures,
     ) -> Result<()> {
-        if !features.component_model_async() {
+        if !features.cm_async() {
             bail!(
                 offset,
                 "`task.return` requires the component model async feature"
@@ -1248,14 +1248,20 @@ impl ComponentState {
 
     fn yield_(
         &mut self,
-        _async_: bool,
+        async_: bool,
         types: &mut TypeAlloc,
         offset: usize,
         features: &WasmFeatures,
     ) -> Result<()> {
-        if !features.component_model_async() {
+        if !features.cm_async() {
             bail!(offset, "`yield` requires the component model async feature")
         }
+        if async_ && !features.cm_async_builtins() {
+            bail!(
+                offset,
+                "async `yield` requires the component model async builtins feature"
+            )
+        }
 
         self.core_funcs
             .push(types.intern_func_type(FuncType::new([], []), offset));
@@ -1268,7 +1274,7 @@ impl ComponentState {
         offset: usize,
         features: &WasmFeatures,
     ) -> Result<()> {
-        if !features.component_model_async() {
+        if !features.cm_async() {
             bail!(
                 offset,
                 "`subtask.drop` requires the component model async feature"
@@ -1287,7 +1293,7 @@ impl ComponentState {
         offset: usize,
         features: &WasmFeatures,
     ) -> Result<()> {
-        if !features.component_model_async() {
+        if !features.cm_async() {
             bail!(
                 offset,
                 "`stream.new` requires the component model async feature"
@@ -1312,7 +1318,7 @@ impl ComponentState {
         offset: usize,
         features: &WasmFeatures,
     ) -> Result<()> {
-        if !features.component_model_async() {
+        if !features.cm_async() {
             bail!(
                 offset,
                 "`stream.read` requires the component model async feature"
@@ -1344,7 +1350,7 @@ impl ComponentState {
         offset: usize,
         features: &WasmFeatures,
     ) -> Result<()> {
-        if !features.component_model_async() {
+        if !features.cm_async() {
             bail!(
                 offset,
                 "`stream.write` requires the component model async feature"
@@ -1369,17 +1375,23 @@ impl ComponentState {
     fn stream_cancel_read(
         &mut self,
         ty: u32,
-        _async_: bool,
+        async_: bool,
         types: &mut TypeAlloc,
         offset: usize,
         features: &WasmFeatures,
     ) -> Result<()> {
-        if !features.component_model_async() {
+        if !features.cm_async() {
             bail!(
                 offset,
                 "`stream.cancel-read` requires the component model async feature"
             )
         }
+        if async_ && !features.cm_async_builtins() {
+            bail!(
+                offset,
+                "async `stream.cancel-read` requires the component model async builtins feature"
+            )
+        }
 
         let ty = self.defined_type_at(ty, offset)?;
         let ComponentDefinedType::Stream(_) = &types[ty] else {
@@ -1394,17 +1406,23 @@ impl ComponentState {
     fn stream_cancel_write(
         &mut self,
         ty: u32,
-        _async_: bool,
+        async_: bool,
         types: &mut TypeAlloc,
         offset: usize,
         features: &WasmFeatures,
     ) -> Result<()> {
-        if !features.component_model_async() {
+        if !features.cm_async() {
             bail!(
                 offset,
                 "`stream.cancel-write` requires the component model async feature"
             )
         }
+        if async_ && !features.cm_async_builtins() {
+            bail!(
+                offset,
+                "async `stream.cancel-write` requires the component model async builtins feature"
+            )
+        }
 
         let ty = self.defined_type_at(ty, offset)?;
         let ComponentDefinedType::Stream(_) = &types[ty] else {
@@ -1423,7 +1441,7 @@ impl ComponentState {
         offset: usize,
         features: &WasmFeatures,
     ) -> Result<()> {
-        if !features.component_model_async() {
+        if !features.cm_async() {
             bail!(
                 offset,
                 "`stream.close-readable` requires the component model async feature"
@@ -1447,7 +1465,7 @@ impl ComponentState {
         offset: usize,
         features: &WasmFeatures,
     ) -> Result<()> {
-        if !features.component_model_async() {
+        if !features.cm_async() {
             bail!(
                 offset,
                 "`stream.close-writable` requires the component model async feature"
@@ -1471,7 +1489,7 @@ impl ComponentState {
         offset: usize,
         features: &WasmFeatures,
     ) -> Result<()> {
-        if !features.component_model_async() {
+        if !features.cm_async() {
             bail!(
                 offset,
                 "`future.new` requires the component model async feature"
@@ -1496,7 +1514,7 @@ impl ComponentState {
         offset: usize,
         features: &WasmFeatures,
     ) -> Result<()> {
-        if !features.component_model_async() {
+        if !features.cm_async() {
             bail!(
                 offset,
                 "`future.read` requires the component model async feature"
@@ -1528,7 +1546,7 @@ impl ComponentState {
         offset: usize,
         features: &WasmFeatures,
     ) -> Result<()> {
-        if !features.component_model_async() {
+        if !features.cm_async() {
             bail!(
                 offset,
                 "`future.write` requires the component model async feature"
@@ -1553,17 +1571,23 @@ impl ComponentState {
     fn future_cancel_read(
         &mut self,
         ty: u32,
-        _async_: bool,
+        async_: bool,
         types: &mut TypeAlloc,
         offset: usize,
         features: &WasmFeatures,
     ) -> Result<()> {
-        if !features.component_model_async() {
+        if !features.cm_async() {
             bail!(
                 offset,
                 "`future.cancel-read` requires the component model async feature"
             )
         }
+        if async_ && !features.cm_async_builtins() {
+            bail!(
+                offset,
+                "async `future.cancel-read` requires the component model async builtins feature"
+            )
+        }
 
         let ty = self.defined_type_at(ty, offset)?;
         let ComponentDefinedType::Future(_) = &types[ty] else {
@@ -1578,17 +1602,23 @@ impl ComponentState {
     fn future_cancel_write(
         &mut self,
         ty: u32,
-        _async_: bool,
+        async_: bool,
         types: &mut TypeAlloc,
         offset: usize,
         features: &WasmFeatures,
     ) -> Result<()> {
-        if !features.component_model_async() {
+        if !features.cm_async() {
             bail!(
                 offset,
                 "`future.cancel-write` requires the component model async feature"
             )
         }
+        if async_ && !features.cm_async_builtins() {
+            bail!(
+                offset,
+                "async `future.cancel-write` requires the component model async builtins feature"
+            )
+        }
 
         let ty = self.defined_type_at(ty, offset)?;
         let ComponentDefinedType::Future(_) = &types[ty] else {
@@ -1607,7 +1637,7 @@ impl ComponentState {
         offset: usize,
         features: &WasmFeatures,
     ) -> Result<()> {
-        if !features.component_model_async() {
+        if !features.cm_async() {
             bail!(
                 offset,
                 "`future.close-readable` requires the component model async feature"
@@ -1631,7 +1661,7 @@ impl ComponentState {
         offset: usize,
         features: &WasmFeatures,
     ) -> Result<()> {
-        if !features.component_model_async() {
+        if !features.cm_async() {
             bail!(
                 offset,
                 "`future.close-writable` requires the component model async feature"
@@ -1655,7 +1685,7 @@ impl ComponentState {
         offset: usize,
         features: &WasmFeatures,
     ) -> Result<()> {
-        if !features.component_model_async() {
+        if !features.cm_async() {
             bail!(
                 offset,
                 "`error-context.new` requires the component model async feature"
@@ -1679,7 +1709,7 @@ impl ComponentState {
         offset: usize,
         features: &WasmFeatures,
     ) -> Result<()> {
-        if !features.component_model_async() {
+        if !features.cm_async() {
             bail!(
                 offset,
                 "`error-context.debug-message` requires the component model async feature"
@@ -1702,7 +1732,7 @@ impl ComponentState {
         offset: usize,
         features: &WasmFeatures,
     ) -> Result<()> {
-        if !features.component_model_async() {
+        if !features.cm_async() {
             bail!(
                 offset,
                 "`error-context.drop` requires the component model async feature"
@@ -1720,7 +1750,7 @@ impl ComponentState {
         offset: usize,
         features: &WasmFeatures,
     ) -> Result<()> {
-        if !features.component_model_async() {
+        if !features.cm_async() {
             bail!(
                 offset,
                 "`waitable-set.new` requires the component model async feature"
@@ -1740,7 +1770,7 @@ impl ComponentState {
         offset: usize,
         features: &WasmFeatures,
     ) -> Result<()> {
-        if !features.component_model_async() {
+        if !features.cm_async() {
             bail!(
                 offset,
                 "`waitable-set.wait` requires the component model async feature"
@@ -1756,18 +1786,24 @@ impl ComponentState {
 
     fn waitable_set_poll(
         &mut self,
-        _async_: bool,
+        async_: bool,
         memory: u32,
         types: &mut TypeAlloc,
         offset: usize,
         features: &WasmFeatures,
     ) -> Result<()> {
-        if !features.component_model_async() {
+        if !features.cm_async() {
             bail!(
                 offset,
                 "`waitable-set.poll` requires the component model async feature"
             )
         }
+        if async_ && !features.cm_async_builtins() {
+            bail!(
+                offset,
+                "async `waitable-set.poll` requires the component model async builtins feature"
+            )
+        }
 
         self.memory_at(memory, offset)?;
 
@@ -1782,7 +1818,7 @@ impl ComponentState {
         offset: usize,
         features: &WasmFeatures,
     ) -> Result<()> {
-        if !features.component_model_async() {
+        if !features.cm_async() {
             bail!(
                 offset,
                 "`waitable-set.drop` requires the component model async feature"
@@ -1800,7 +1836,7 @@ impl ComponentState {
         offset: usize,
         features: &WasmFeatures,
     ) -> Result<()> {
-        if !features.component_model_async() {
+        if !features.cm_async() {
             bail!(
                 offset,
                 "`waitable.join` requires the component model async feature"
@@ -1999,7 +2035,7 @@ impl ComponentState {
         types: &mut TypeList,
         offset: usize,
     ) -> Result<()> {
-        if !features.component_model_values() {
+        if !features.cm_values() {
             bail!(
                 offset,
                 "support for component model `value`s is not enabled"
@@ -2167,7 +2203,7 @@ impl ComponentState {
                             offset,
                         ));
                     } else {
-                        if !features.component_model_async() {
+                        if !features.cm_async() {
                             bail!(
                                 offset,
                                 "canonical option `async` requires the component model async feature"
@@ -2208,19 +2244,45 @@ impl ComponentState {
             }
         }
 
-        if async_ && !allow_async {
-            bail!(offset, "async option not allowed here")
-        }
+        // 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"
+                )
+            }
 
-        if callback.is_some() && !async_ {
-            bail!(offset, "cannot specify callback without lifting async")
-        }
+            // Async + allowed + this is a lift (core_ty present) + stackful ABI
+            (true, true, false, Some(_), false) => {
+                if !features.cm_async_stackful() {
+                    bail!(
+                        offset,
+                        "`async` without `callback` requires the async stackful feature"
+                    )
+                }
+            }
 
-        if post_return.is_some() && async_ {
-            bail!(
-                offset,
-                "cannot specify post-return function when lifting async"
-            )
+            // 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() {
@@ -3400,8 +3462,7 @@ impl ComponentState {
     ) -> Result<ComponentDefinedType> {
         match ty {
             crate::ComponentDefinedType::Primitive(ty) => {
-                if ty == crate::PrimitiveValType::ErrorContext && !features.component_model_async()
-                {
+                if ty == crate::PrimitiveValType::ErrorContext && !features.cm_async() {
                     bail!(
                         offset,
                         "`error-context` requires the component model async feature"
@@ -3445,7 +3506,7 @@ impl ComponentState {
                 self.resource_at(idx, types, offset)?,
             )),
             crate::ComponentDefinedType::Future(ty) => {
-                if !features.component_model_async() {
+                if !features.cm_async() {
                     bail!(
                         offset,
                         "`future` requires the component model async feature"
@@ -3457,7 +3518,7 @@ impl ComponentState {
                 ))
             }
             crate::ComponentDefinedType::Stream(ty) => {
-                if !features.component_model_async() {
+                if !features.cm_async() {
                     bail!(
                         offset,
                         "`stream` requires the component model async feature"
@@ -3906,7 +3967,7 @@ impl ComponentState {
     }
 
     fn check_value_support(&self, features: &WasmFeatures, offset: usize) -> Result<()> {
-        if !features.component_model_values() {
+        if !features.cm_values() {
             bail!(
                 offset,
                 "support for component model `value`s is not enabled"
@@ -4039,7 +4100,7 @@ impl ComponentNameContext {
             ComponentNameKind::AsyncLabel(_)
             | ComponentNameKind::AsyncMethod(_)
             | ComponentNameKind::AsyncStatic(_) => {
-                if !features.component_model_async() {
+                if !features.cm_async() {
                     bail!(
                         offset,
                         "async kebab-names require the component model async feature"
diff --git a/crates/wasmparser/src/validator/names.rs b/crates/wasmparser/src/validator/names.rs
index 4ba430cd..1b4ba635 100644
--- a/crates/wasmparser/src/validator/names.rs
+++ b/crates/wasmparser/src/validator/names.rs
@@ -748,7 +748,7 @@ impl<'a> ComponentNameParser<'a> {
         self.expect_str(":")?;
         self.take_lowercase_kebab()?;
 
-        if self.features.component_model_nested_names() {
+        if self.features.cm_nested_names() {
             // Take the remaining package namespaces and name
             while self.next.starts_with(':') {
                 self.expect_str(":")?;
@@ -761,7 +761,7 @@ impl<'a> ComponentNameParser<'a> {
             self.expect_str("/")?;
             self.take_kebab()?;
 
-            if self.features.component_model_nested_names() {
+            if self.features.cm_nested_names() {
                 while self.next.starts_with('/') {
                     self.expect_str("/")?;
                     self.take_kebab()?;
diff --git a/crates/wit-component/src/dummy.rs b/crates/wit-component/src/dummy.rs
index 80daa051..5cf5f634 100644
--- a/crates/wit-component/src/dummy.rs
+++ b/crates/wit-component/src/dummy.rs
@@ -201,8 +201,10 @@ fn push_imported_future_and_stream_intrinsics(
 (import {module:?} "[future-close-writable-{i}]{name}" (func (param i32 i32)))
 (import {module:?} "[async-lower][future-read-{i}]{name}" (func (param i32 i32) (result i32)))
 (import {module:?} "[async-lower][future-write-{i}]{name}" (func (param i32 i32) (result i32)))
-(import {module:?} "[async-lower][future-cancel-read-{i}]{name}" (func (param i32) (result i32)))
-(import {module:?} "[async-lower][future-cancel-write-{i}]{name}" (func (param i32) (result i32)))
+
+;; deferred behind 🚝
+;;(import {module:?} "[async-lower][future-cancel-read-{i}]{name}" (func (param i32) (result i32)))
+;;(import {module:?} "[async-lower][future-cancel-write-{i}]{name}" (func (param i32) (result i32)))
 "#
                 ));
             }
@@ -218,8 +220,10 @@ fn push_imported_future_and_stream_intrinsics(
 (import {module:?} "[stream-close-writable-{i}]{name}" (func (param i32 i32)))
 (import {module:?} "[async-lower][stream-read-{i}]{name}" (func (param i32 i32 i32) (result i32)))
 (import {module:?} "[async-lower][stream-write-{i}]{name}" (func (param i32 i32 i32) (result i32)))
-(import {module:?} "[async-lower][stream-cancel-read-{i}]{name}" (func (param i32) (result i32)))
-(import {module:?} "[async-lower][stream-cancel-write-{i}]{name}" (func (param i32) (result i32)))
+
+;; deferred behind 🚝
+;;(import {module:?} "[async-lower][stream-cancel-read-{i}]{name}" (func (param i32) (result i32)))
+;;(import {module:?} "[async-lower][stream-cancel-write-{i}]{name}" (func (param i32) (result i32)))
 "#
                 ));
             }
@@ -373,11 +377,9 @@ fn push_root_async_intrinsics(dst: &mut String) {
 (import "$root" "[waitable-set-wait]" (func (param i32 i32) (result i32)))
 (import "$root" "[async-lower][waitable-set-wait]" (func (param i32 i32) (result i32)))
 (import "$root" "[waitable-set-poll]" (func (param i32 i32) (result i32)))
-(import "$root" "[async-lower][waitable-set-poll]" (func (param i32 i32) (result i32)))
 (import "$root" "[waitable-set-drop]" (func (param i32)))
 (import "$root" "[waitable-join]" (func (param i32 i32)))
 (import "$root" "[yield]" (func))
-(import "$root" "[async-lower][yield]" (func))
 (import "$root" "[subtask-drop]" (func (param i32)))
 (import "$root" "[error-context-new-utf8]" (func (param i32 i32) (result i32)))
 (import "$root" "[error-context-new-utf16]" (func (param i32 i32) (result i32)))
@@ -386,6 +388,10 @@ fn push_root_async_intrinsics(dst: &mut String) {
 (import "$root" "[error-context-debug-message-utf16]" (func (param i32 i32)))
 (import "$root" "[error-context-debug-message-latin1+utf16]" (func (param i32 i32)))
 (import "$root" "[error-context-drop]" (func (param i32)))
+
+;; deferred behind 🚝 upstream
+;;(import "$root" "[async-lower][waitable-set-poll]" (func (param i32 i32) (result i32)))
+;;(import "$root" "[async-lower][yield]" (func))
 "#,
     );
 }


Test patch:
diff --git a/tests/cli/dummy-async-export-future-with-named-type.wit b/tests/cli/dummy-async-export-future-with-named-type.wit
index 7f9764fa..bd668335 100644
--- a/tests/cli/dummy-async-export-future-with-named-type.wit
+++ b/tests/cli/dummy-async-export-future-with-named-type.wit
@@ -1,6 +1,6 @@
 // RUN: component embed % --dummy-names legacy --async-callback | \
 //        component new | \
-//        validate -f component-model-async
+//        validate -f cm-async
 
 package a:b;
 
diff --git a/tests/cli/dummy-async-export-using-export.wit b/tests/cli/dummy-async-export-using-export.wit
index 1f95eaf5..2ca7a073 100644
--- a/tests/cli/dummy-async-export-using-export.wit
+++ b/tests/cli/dummy-async-export-using-export.wit
@@ -1,6 +1,6 @@
 // RUN: component embed % --dummy-names legacy --async-callback | \
 //        component new | \
-//        validate -f component-model-async
+//        validate -f cm-async
 
 package a:b;
 
diff --git a/tests/cli/dummy-async-future-with-flags.wit b/tests/cli/dummy-async-future-with-flags.wit
index a36ae9bb..918d8cfa 100644
--- a/tests/cli/dummy-async-future-with-flags.wit
+++ b/tests/cli/dummy-async-future-with-flags.wit
@@ -1,6 +1,6 @@
 // RUN: component embed % --dummy-names legacy --async-callback | \
 //        component new | \
-//        validate -f component-model-async
+//        validate -f cm-async
 
 package y:name;
 
diff --git a/tests/cli/dummy-async-resource-with-stream.wit b/tests/cli/dummy-async-resource-with-stream.wit
index 8ec09a29..a8358525 100644
--- a/tests/cli/dummy-async-resource-with-stream.wit
+++ b/tests/cli/dummy-async-resource-with-stream.wit
@@ -1,6 +1,6 @@
 // RUN: component embed % --dummy-names legacy --async-callback | \
 //        component new | \
-//        validate -f component-model-async
+//        validate -f cm-async
 
 package a:b;
 
diff --git a/tests/cli/dummy-async-resource.wit b/tests/cli/dummy-async-resource.wit
index 5a6b8fff..4f64ca16 100644
--- a/tests/cli/dummy-async-resource.wit
+++ b/tests/cli/dummy-async-resource.wit
@@ -1,6 +1,6 @@
 // RUN: component embed % --dummy-names legacy --async-callback | \
 //        component new | \
-//        validate -f component-model-async
+//        validate -f cm-async
 
 package a:b;
 
diff --git a/tests/cli/validate-unknown-features.wat.stderr b/tests/cli/validate-unknown-features.wat.stderr
index c7799ed9..afd42419 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, component-model-values, component-model-nested-names, legacy-exceptions, gc-types, stack-switching, wide-arithmetic, component-model-async, 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, mvp, wasm1, wasm2, wasm3, all
 
 For more information, try '--help'.
diff --git a/tests/local/missing-features/component-model-async/async-builtins.wast b/tests/local/missing-features/component-model-async/async-builtins.wast
new file mode 100644
index 00000000..d771b607
--- /dev/null
+++ b/tests/local/missing-features/component-model-async/async-builtins.wast
@@ -0,0 +1,52 @@
+;; waitable-set.poll async
+(assert_invalid
+  (component
+    (core module $libc (memory (export "memory") 1))
+    (core instance $libc (instantiate $libc))
+    (core func (canon waitable-set.poll async (memory $libc "memory")))
+  )
+  "requires the component model async builtins feature")
+
+(component
+  (core module $libc (memory (export "memory") 1))
+  (core instance $libc (instantiate $libc))
+  (core func (canon waitable-set.poll (memory $libc "memory")))
+)
+
+;; yield
+(assert_invalid
+  (component (core func (canon yield async)))
+  "requires the component model async builtins feature")
+
+(component (core func (canon yield)))
+
+;; {future,stream}.cancel-{read,write}
+(assert_invalid
+  (component
+    (type $t (future))
+    (core func (canon future.cancel-read $t async)))
+  "requires the component model async builtins feature")
+(assert_invalid
+  (component
+    (type $t (future))
+    (core func (canon future.cancel-write $t async)))
+  "requires the component model async builtins feature")
+(assert_invalid
+  (component
+    (type $t (stream))
+    (core func (canon stream.cancel-read $t async)))
+  "requires the component model async builtins feature")
+(assert_invalid
+  (component
+    (type $t (stream))
+    (core func (canon stream.cancel-write $t async)))
+  "requires the component model async builtins feature")
+
+(component
+  (type $f (future))
+  (type $s (stream))
+  (core func (canon future.cancel-read $f))
+  (core func (canon future.cancel-write $f))
+  (core func (canon stream.cancel-read $s))
+  (core func (canon stream.cancel-write $s))
+)
diff --git a/tests/local/missing-features/component-model-async/async-stackful.wast b/tests/local/missing-features/component-model-async/async-stackful.wast
new file mode 100644
index 00000000..f02fc87a
--- /dev/null
+++ b/tests/local/missing-features/component-model-async/async-stackful.wast
@@ -0,0 +1,10 @@
+(assert_invalid
+  (component
+    (core module $m (func (export "foo") (param i32)))
+    (core instance $i (instantiate $m))
+
+    (func (export "foo") (param "p1" u32) (result u32)
+      (canon lift (core func $i "foo") async)
+    )
+  )
+  "requires the async stackful feature")
diff --git a/tests/local/missing-features/component-model/async.wast b/tests/local/missing-features/component-model/async.wast
index c1bc8997..ba95a6f4 100644
--- a/tests/local/missing-features/component-model/async.wast
+++ b/tests/local/missing-features/component-model/async.wast
@@ -373,7 +373,7 @@
     (type $t (resource (rep i32)))
     (core func $f (canon resource.drop $t async))
   )
-  "requires the component model async feature"
+  "requires the component model async builtins feature"
 )
 
 (assert_invalid
diff --git a/tests/roundtrip.rs b/tests/roundtrip.rs
index 841a76fd..b32298cd 100644
--- a/tests/roundtrip.rs
+++ b/tests/roundtrip.rs
@@ -620,7 +620,7 @@ impl TestState {
         let mut features = WasmFeatures::all()
             & !WasmFeatures::SHARED_EVERYTHING_THREADS
             & !WasmFeatures::COMPONENT_MODEL
-            & !WasmFeatures::COMPONENT_MODEL_NESTED_NAMES
+            & !WasmFeatures::CM_NESTED_NAMES
             & !WasmFeatures::LEGACY_EXCEPTIONS;
         for part in test.iter().filter_map(|t| t.to_str()) {
             match part {
@@ -665,7 +665,7 @@ impl TestState {
                     features.insert(WasmFeatures::MULTI_MEMORY);
                 }
                 "import-extended.wast" => {
-                    features.insert(WasmFeatures::COMPONENT_MODEL_NESTED_NAMES);
+                    features.insert(WasmFeatures::CM_NESTED_NAMES);
                 }
                 "stack-switching" => {
                     features.insert(WasmFeatures::STACK_SWITCHING);
@@ -678,7 +678,7 @@ impl TestState {
                 }
                 "component-model-async" => {
                     features.insert(WasmFeatures::COMPONENT_MODEL);
-                    features.insert(WasmFeatures::COMPONENT_MODEL_ASYNC);
+                    features.insert(WasmFeatures::CM_ASYNC);
                 }
                 _ => {}
             }
diff --git a/tests/snapshots/local/missing-features/component-model-async/async-builtins.wast.json b/tests/snapshots/local/missing-features/component-model-async/async-builtins.wast.json
new file mode 100644
index 00000000..d4940c37
--- /dev/null
+++ b/tests/snapshots/local/missing-features/component-model-async/async-builtins.wast.json
@@ -0,0 +1,65 @@
+{
+  "source_filename": "tests/local/missing-features/component-model-async/async-builtins.wast",
+  "commands": [
+    {
+      "type": "assert_invalid",
+      "line": 3,
+      "filename": "async-builtins.0.wasm",
+      "module_type": "binary",
+      "text": "requires the component model async builtins feature"
+    },
+    {
+      "type": "module",
+      "line": 10,
+      "filename": "async-builtins.1.wasm",
+      "module_type": "binary"
+    },
+    {
+      "type": "assert_invalid",
+      "line": 18,
+      "filename": "async-builtins.2.wasm",
+      "module_type": "binary",
+      "text": "requires the component model async builtins feature"
+    },
+    {
+      "type": "module",
+      "line": 21,
+      "filename": "async-builtins.3.wasm",
+      "module_type": "binary"
+    },
+    {
+      "type": "assert_invalid",
+      "line": 25,
+      "filename": "async-builtins.4.wasm",
+      "module_type": "binary",
+      "text": "requires the component model async builtins feature"
+    },
+    {
+      "type": "assert_invalid",
+      "line": 30,
+      "filename": "async-builtins.5.wasm",
+      "module_type": "binary",
+      "text": "requires the component model async builtins feature"
+    },
+    {
+      "type": "assert_invalid",
+      "line": 35,
+      "filename": "async-builtins.6.wasm",
+      "module_type": "binary",
+      "text": "requires the component model async builtins feature"
+    },
+    {
+      "type": "assert_invalid",
+      "line": 40,
+      "filename": "async-builtins.7.wasm",
+      "module_type": "binary",
+      "text": "requires the component model async builtins feature"
+    },
+    {
+      "type": "module",
+      "line": 45,
+      "filename": "async-builtins.8.wasm",
+      "module_type": "binary"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/tests/snapshots/local/missing-features/component-model-async/async-builtins.wast/1.print b/tests/snapshots/local/missing-features/component-model-async/async-builtins.wast/1.print
new file mode 100644
index 00000000..a4322a5d
--- /dev/null
+++ b/tests/snapshots/local/missing-features/component-model-async/async-builtins.wast/1.print
@@ -0,0 +1,9 @@
+(component
+  (core module $libc (;0;)
+    (memory (;0;) 1)
+    (export "memory" (memory 0))
+  )
+  (core instance $libc (;0;) (instantiate $libc))
+  (alias core export $libc "memory" (core memory (;0;)))
+  (core func (;0;) (canon waitable-set.poll (memory 0)))
+)
diff --git a/tests/snapshots/local/missing-features/component-model-async/async-builtins.wast/3.print b/tests/snapshots/local/missing-features/component-model-async/async-builtins.wast/3.print
new file mode 100644
index 00000000..ffbb762e
--- /dev/null
+++ b/tests/snapshots/local/missing-features/component-model-async/async-builtins.wast/3.print
@@ -0,0 +1,3 @@
+(component
+  (core func (;0;) (canon yield))
+)
diff --git a/tests/snapshots/local/missing-features/component-model-async/async-builtins.wast/8.print b/tests/snapshots/local/missing-features/component-model-async/async-builtins.wast/8.print
new file mode 100644
index 00000000..610a633c
--- /dev/null
+++ b/tests/snapshots/local/missing-features/component-model-async/async-builtins.wast/8.print
@@ -0,0 +1,8 @@
+(component
+  (type $f (;0;) (future))
+  (type $s (;1;) (stream))
+  (core func (;0;) (canon future.cancel-read $f))
+  (core func (;1;) (canon future.cancel-write $f))
+  (core func (;2;) (canon stream.cancel-read $s))
+  (core func (;3;) (canon stream.cancel-write $s))
+)
diff --git a/tests/snapshots/local/missing-features/component-model-async/async-stackful.wast.json b/tests/snapshots/local/missing-features/component-model-async/async-stackful.wast.json
new file mode 100644
index 00000000..6f490fdd
--- /dev/null
+++ b/tests/snapshots/local/missing-features/component-model-async/async-stackful.wast.json
@@ -0,0 +1,12 @@
+{
+  "source_filename": "tests/local/missing-features/component-model-async/async-stackful.wast",
+  "commands": [
+    {
+      "type": "assert_invalid",
+      "line": 2,
+      "filename": "async-stackful.0.wasm",
+      "module_type": "binary",
+      "text": "requires the async stackful feature"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/tests/snapshots/local/missing-features/component-model/async.wast.json b/tests/snapshots/local/missing-features/component-model/async.wast.json
index 517be0d3..febf67e9 100644
--- a/tests/snapshots/local/missing-features/component-model/async.wast.json
+++ b/tests/snapshots/local/missing-features/component-model/async.wast.json
@@ -223,7 +223,7 @@
       "line": 372,
       "filename": "async.31.wasm",
       "module_type": "binary",
-      "text": "requires the component model async feature"
+      "text": "requires the component model async builtins feature"
     },
     {
       "type": "assert_invalid",