Repository: bytecodealliance/wasm-tools
Pull request #2137 (2025-04-10 21:03:03)

add `subtask.cancel` intrinsic
This has not yet been added to the spec, but we've got a general idea of how it will look and behave: https://github.com/WebAssembly/component-model/issues/495

Note that, since there's not spec for it yet, I've picked the next unused binary discriminant in the sequence: 0x24.  That should be easy enough to change if necessary once the spec is updated.

alexcrichton: Mind adding some tests for this as well? For example wit-component tests that the import gets hooked up in addition to `*.wast` tests that it validates and has the right signature and such

Patch:
diff --git a/crates/wasm-encoder/src/component/builder.rs b/crates/wasm-encoder/src/component/builder.rs
index 488db47e..c0c2c000 100644
--- a/crates/wasm-encoder/src/component/builder.rs
+++ b/crates/wasm-encoder/src/component/builder.rs
@@ -431,6 +431,12 @@ impl ComponentBuilder {
         inc(&mut self.core_funcs)
     }
 
+    /// Declares a new `subtask.cancel` intrinsic.
+    pub fn subtask_cancel(&mut self, async_: bool) -> u32 {
+        self.canonical_functions().subtask_cancel(async_);
+        inc(&mut self.core_funcs)
+    }
+
     /// Declares a new `stream.new` intrinsic.
     pub fn stream_new(&mut self, ty: u32) -> u32 {
         self.canonical_functions().stream_new(ty);
diff --git a/crates/wasm-encoder/src/component/canonicals.rs b/crates/wasm-encoder/src/component/canonicals.rs
index 8a184214..efee0cf6 100644
--- a/crates/wasm-encoder/src/component/canonicals.rs
+++ b/crates/wasm-encoder/src/component/canonicals.rs
@@ -246,6 +246,14 @@ impl CanonicalFunctionSection {
         self
     }
 
+    /// Defines a function to cancel an in-progress task.
+    pub fn subtask_cancel(&mut self, async_: bool) -> &mut Self {
+        self.bytes.push(0x24);
+        self.bytes.push(if async_ { 1 } else { 0 });
+        self.num_added += 1;
+        self
+    }
+
     /// Defines a function to create a new `stream` handle of the specified
     /// type.
     pub fn stream_new(&mut self, ty: u32) -> &mut Self {
diff --git a/crates/wasm-encoder/src/reencode/component.rs b/crates/wasm-encoder/src/reencode/component.rs
index 33e7d81c..9b0d89b9 100644
--- a/crates/wasm-encoder/src/reencode/component.rs
+++ b/crates/wasm-encoder/src/reencode/component.rs
@@ -989,6 +989,9 @@ pub mod component_utils {
             wasmparser::CanonicalFunction::SubtaskDrop => {
                 section.subtask_drop();
             }
+            wasmparser::CanonicalFunction::SubtaskCancel { async_ } => {
+                section.subtask_cancel(async_);
+            }
             wasmparser::CanonicalFunction::StreamNew { ty } => {
                 section.stream_new(reencoder.component_type_index(ty));
             }
diff --git a/crates/wasmparser/src/readers/component/canonicals.rs b/crates/wasmparser/src/readers/component/canonicals.rs
index 9ac56c28..b75d0e43 100644
--- a/crates/wasmparser/src/readers/component/canonicals.rs
+++ b/crates/wasmparser/src/readers/component/canonicals.rs
@@ -110,6 +110,12 @@ pub enum CanonicalFunction {
     },
     /// A function to drop a specified task which has completed.
     SubtaskDrop,
+    /// A function to cancel an in-progress task.
+    SubtaskCancel {
+        /// If `false`, block until cancellation completes rather than return
+        /// `BLOCKED`.
+        async_: bool,
+    },
     /// A function to create a new `stream` handle of the specified type.
     StreamNew {
         /// The `stream` type to instantiate.
@@ -359,6 +365,9 @@ impl<'a> FromReader<'a> for CanonicalFunction {
             },
             0x22 => CanonicalFunction::WaitableSetDrop,
             0x23 => CanonicalFunction::WaitableJoin,
+            0x24 => CanonicalFunction::SubtaskCancel {
+                async_: reader.read()?,
+            },
             0x40 => CanonicalFunction::ThreadSpawnRef {
                 func_ty_index: reader.read()?,
             },
diff --git a/crates/wasmparser/src/validator/component.rs b/crates/wasmparser/src/validator/component.rs
index 75f997d1..e2f42778 100644
--- a/crates/wasmparser/src/validator/component.rs
+++ b/crates/wasmparser/src/validator/component.rs
@@ -969,6 +969,9 @@ impl ComponentState {
             CanonicalFunction::ContextSet(i) => self.context_set(i, types, offset),
             CanonicalFunction::Yield { async_ } => self.yield_(async_, types, offset),
             CanonicalFunction::SubtaskDrop => self.subtask_drop(types, offset),
+            CanonicalFunction::SubtaskCancel { async_ } => {
+                self.subtask_cancel(async_, types, offset)
+            }
             CanonicalFunction::StreamNew { ty } => self.stream_new(ty, types, offset),
             CanonicalFunction::StreamRead { ty, options } => {
                 self.stream_read(ty, &options, types, offset)
@@ -1268,6 +1271,25 @@ impl ComponentState {
         Ok(())
     }
 
+    fn subtask_cancel(&mut self, async_: bool, types: &mut TypeAlloc, offset: usize) -> Result<()> {
+        if !self.features.cm_async() {
+            bail!(
+                offset,
+                "`subtask.cancel` requires the component model async feature"
+            )
+        }
+        if async_ && !self.features.cm_async_builtins() {
+            bail!(
+                offset,
+                "async `subtask.cancel` requires the component model async builtins feature"
+            )
+        }
+
+        self.core_funcs
+            .push(types.intern_func_type(FuncType::new([ValType::I32], [ValType::I32]), offset));
+        Ok(())
+    }
+
     fn stream_new(&mut self, ty: u32, types: &mut TypeAlloc, offset: usize) -> Result<()> {
         if !self.features.cm_async() {
             bail!(
diff --git a/crates/wasmprinter/src/component.rs b/crates/wasmprinter/src/component.rs
index ee50cbaa..258cd726 100644
--- a/crates/wasmprinter/src/component.rs
+++ b/crates/wasmprinter/src/component.rs
@@ -953,6 +953,14 @@ impl Printer<'_, '_> {
                 CanonicalFunction::SubtaskDrop => {
                     self.print_intrinsic(state, "canon subtask.drop", &|_, _| Ok(()))?;
                 }
+                CanonicalFunction::SubtaskCancel { async_ } => {
+                    self.print_intrinsic(state, "canon subtask.cancel", &|me, _| {
+                        if async_ {
+                            me.print_type_keyword(" async")?;
+                        }
+                        Ok(())
+                    })?;
+                }
                 CanonicalFunction::StreamNew { ty } => {
                     self.print_intrinsic(state, "canon stream.new ", &|me, state| {
                         me.print_idx(&state.component.type_names, ty)
diff --git a/crates/wast/src/component/binary.rs b/crates/wast/src/component/binary.rs
index b981ee39..43c9a118 100644
--- a/crates/wast/src/component/binary.rs
+++ b/crates/wast/src/component/binary.rs
@@ -396,6 +396,10 @@ impl<'a> Encoder<'a> {
                     self.core_func_names.push(name);
                     self.funcs.subtask_drop();
                 }
+                CoreFuncKind::SubtaskCancel(info) => {
+                    self.core_func_names.push(name);
+                    self.funcs.subtask_cancel(info.async_);
+                }
                 CoreFuncKind::StreamNew(info) => {
                     self.core_func_names.push(name);
                     self.funcs.stream_new(info.ty.into());
diff --git a/crates/wast/src/component/func.rs b/crates/wast/src/component/func.rs
index 0a3f1e26..fc01a337 100644
--- a/crates/wast/src/component/func.rs
+++ b/crates/wast/src/component/func.rs
@@ -60,6 +60,7 @@ pub enum CoreFuncKind<'a> {
     ContextSet(u32),
     Yield(CanonYield),
     SubtaskDrop,
+    SubtaskCancel(CanonSubtaskCancel),
     StreamNew(CanonStreamNew<'a>),
     StreamRead(CanonStreamRead<'a>),
     StreamWrite(CanonStreamWrite<'a>),
@@ -134,6 +135,8 @@ impl<'a> CoreFuncKind<'a> {
         } else if l.peek::<kw::subtask_drop>()? {
             parser.parse::<kw::subtask_drop>()?;
             Ok(CoreFuncKind::SubtaskDrop)
+        } else if l.peek::<kw::subtask_cancel>()? {
+            Ok(CoreFuncKind::SubtaskCancel(parser.parse()?))
         } else if l.peek::<kw::stream_new>()? {
             Ok(CoreFuncKind::StreamNew(parser.parse()?))
         } else if l.peek::<kw::stream_read>()? {
@@ -589,7 +592,7 @@ impl<'a> Parse<'a> for CanonWaitableSetPoll<'a> {
     }
 }
 
-/// Information relating to the `task.yield` intrinsic.
+/// Information relating to the `yield` intrinsic.
 #[derive(Debug)]
 pub struct CanonYield {
     /// If true, the component instance may be reentered during a call to this
@@ -606,6 +609,23 @@ impl<'a> Parse<'a> for CanonYield {
     }
 }
 
+/// Information relating to the `subtask.cancel` intrinsic.
+#[derive(Debug)]
+pub struct CanonSubtaskCancel {
+    /// If false, block until cancel is finished; otherwise return BLOCKED if
+    /// necessary.
+    pub async_: bool,
+}
+
+impl<'a> Parse<'a> for CanonSubtaskCancel {
+    fn parse(parser: Parser<'a>) -> Result<Self> {
+        parser.parse::<kw::subtask_cancel>()?;
+        let async_ = parser.parse::<Option<kw::r#async>>()?.is_some();
+
+        Ok(Self { async_ })
+    }
+}
+
 /// Information relating to the `stream.new` intrinsic.
 #[derive(Debug)]
 pub struct CanonStreamNew<'a> {
diff --git a/crates/wast/src/component/resolve.rs b/crates/wast/src/component/resolve.rs
index 0b8af565..8bd53c84 100644
--- a/crates/wast/src/component/resolve.rs
+++ b/crates/wast/src/component/resolve.rs
@@ -398,6 +398,7 @@ impl<'a> Resolver<'a> {
                 | CoreFuncKind::BackpressureSet
                 | CoreFuncKind::Yield(_)
                 | CoreFuncKind::SubtaskDrop
+                | CoreFuncKind::SubtaskCancel(_)
                 | CoreFuncKind::ErrorContextDrop => {}
                 CoreFuncKind::TaskReturn(info) => {
                     if let Some(ty) = &mut info.result {
diff --git a/crates/wast/src/lib.rs b/crates/wast/src/lib.rs
index acc4d775..8c92d8f9 100644
--- a/crates/wast/src/lib.rs
+++ b/crates/wast/src/lib.rs
@@ -561,6 +561,7 @@ pub mod kw {
     custom_keyword!(task_return = "task.return");
     custom_keyword!(yield_ = "yield");
     custom_keyword!(subtask_drop = "subtask.drop");
+    custom_keyword!(subtask_cancel = "subtask.cancel");
     custom_keyword!(stream_new = "stream.new");
     custom_keyword!(stream_read = "stream.read");
     custom_keyword!(stream_write = "stream.write");
diff --git a/crates/wit-component/src/encoding.rs b/crates/wit-component/src/encoding.rs
index 737c31df..29577843 100644
--- a/crates/wit-component/src/encoding.rs
+++ b/crates/wit-component/src/encoding.rs
@@ -1699,6 +1699,10 @@ impl<'a> EncodingState<'a> {
                 let index = self.component.subtask_drop();
                 Ok((ExportKind::Func, index))
             }
+            Import::SubtaskCancel { async_ } => {
+                let index = self.component.subtask_cancel(*async_);
+                Ok((ExportKind::Func, index))
+            }
             Import::StreamNew(info) => {
                 let ty = self.payload_type_index(info)?;
                 let index = self.component.stream_new(ty);
@@ -2188,6 +2192,7 @@ impl<'a> Shims<'a> {
                 | Import::BackpressureSet
                 | Import::Yield { .. }
                 | Import::SubtaskDrop
+                | Import::SubtaskCancel { .. }
                 | Import::FutureNew(..)
                 | Import::StreamNew(..)
                 | Import::FutureCancelRead { .. }
diff --git a/crates/wit-component/src/encoding/world.rs b/crates/wit-component/src/encoding/world.rs
index fe9325cf..79d4dd84 100644
--- a/crates/wit-component/src/encoding/world.rs
+++ b/crates/wit-component/src/encoding/world.rs
@@ -422,6 +422,7 @@ impl<'a> ComponentWorld<'a> {
                 | Import::WaitableJoin
                 | Import::Yield { .. }
                 | Import::SubtaskDrop
+                | Import::SubtaskCancel { .. }
                 | Import::ErrorContextNew { .. }
                 | Import::ErrorContextDebugMessage { .. }
                 | Import::ErrorContextDrop => {}
diff --git a/crates/wit-component/src/validation.rs b/crates/wit-component/src/validation.rs
index 55e58e66..86888de5 100644
--- a/crates/wit-component/src/validation.rs
+++ b/crates/wit-component/src/validation.rs
@@ -297,9 +297,14 @@ pub enum Import {
 
     /// A `canon subtask.drop` intrinsic.
     ///
-    /// This allows the guest to release its handle to an completed subtask.
+    /// This allows the guest to release its handle to a completed subtask.
     SubtaskDrop,
 
+    /// A `canon subtask.cancel` intrinsic.
+    ///
+    /// This allows the guest to cancel an in-progress subtask.
+    SubtaskCancel { async_: bool },
+
     /// A `canon stream.new` intrinsic.
     ///
     /// This allows the guest to create a new `stream` of the specified type.
@@ -601,6 +606,12 @@ impl ImportMap {
                 return Ok(Import::SubtaskDrop);
             }
 
+            if Some(name) == names.subtask_cancel() {
+                let expected = FuncType::new([ValType::I32], [ValType::I32]);
+                validate_func_sig(name, &expected, ty)?;
+                return Ok(Import::SubtaskCancel { async_ });
+            }
+
             if let Some(encoding) = names.error_context_new(name) {
                 validate_not_async()?;
                 let expected = FuncType::new([ValType::I32; 2], [ValType::I32]);
@@ -1406,6 +1417,7 @@ trait NameMangling {
     fn waitable_join(&self) -> Option<&str>;
     fn yield_(&self) -> Option<&str>;
     fn subtask_drop(&self) -> Option<&str>;
+    fn subtask_cancel(&self) -> Option<&str>;
     fn async_lift_callback_name<'a>(&self, s: &'a str) -> Option<&'a str>;
     fn async_lower_name<'a>(&self, s: &'a str) -> Option<&'a str>;
     fn async_lift_name<'a>(&self, s: &'a str) -> Option<&'a str>;
@@ -1500,6 +1512,9 @@ impl NameMangling for Standard {
     fn subtask_drop(&self) -> Option<&str> {
         None
     }
+    fn subtask_cancel(&self) -> Option<&str> {
+        None
+    }
     fn async_lift_callback_name<'a>(&self, s: &'a str) -> Option<&'a str> {
         _ = s;
         None
@@ -1693,6 +1708,9 @@ impl NameMangling for Legacy {
     fn subtask_drop(&self) -> Option<&str> {
         Some("[subtask-drop]")
     }
+    fn subtask_cancel(&self) -> Option<&str> {
+        Some("[subtask-cancel]")
+    }
     fn async_lift_callback_name<'a>(&self, s: &'a str) -> Option<&'a str> {
         s.strip_prefix("[callback][async-lift]")
     }
diff --git a/src/bin/wasm-tools/dump.rs b/src/bin/wasm-tools/dump.rs
index a6daafb9..75e2efcb 100644
--- a/src/bin/wasm-tools/dump.rs
+++ b/src/bin/wasm-tools/dump.rs
@@ -433,6 +433,7 @@ impl<'a> Dump<'a> {
                             | CanonicalFunction::WaitableJoin
                             | CanonicalFunction::Yield { .. }
                             | CanonicalFunction::SubtaskDrop
+                            | CanonicalFunction::SubtaskCancel { .. }
                             | CanonicalFunction::StreamNew { .. }
                             | CanonicalFunction::StreamRead { .. }
                             | CanonicalFunction::StreamWrite { .. }


Test patch:
diff --git a/crates/wit-component/tests/components/async-builtins/component.wat b/crates/wit-component/tests/components/async-builtins/component.wat
index 3ab8509a..55673a87 100644
--- a/crates/wit-component/tests/components/async-builtins/component.wat
+++ b/crates/wit-component/tests/components/async-builtins/component.wat
@@ -5,7 +5,8 @@
     (type (;2;) (func (result i32)))
     (type (;3;) (func (param i32 i32) (result i32)))
     (type (;4;) (func))
-    (type (;5;) (func (param i32 i32 i32 i32) (result i32)))
+    (type (;5;) (func (param i32) (result i32)))
+    (type (;6;) (func (param i32 i32 i32 i32) (result i32)))
     (import "$root" "[backpressure-set]" (func (;0;) (type 0)))
     (import "[export]$root" "[task-return]foo" (func (;1;) (type 1)))
     (import "[export]foo:foo/bar" "[task-return]foo" (func (;2;) (type 1)))
@@ -16,29 +17,30 @@
     (import "$root" "[waitable-join]" (func (;7;) (type 1)))
     (import "$root" "[yield]" (func (;8;) (type 4)))
     (import "$root" "[subtask-drop]" (func (;9;) (type 0)))
-    (import "$root" "[error-context-new-utf8]" (func (;10;) (type 3)))
-    (import "$root" "[error-context-new-utf16]" (func (;11;) (type 3)))
-    (import "$root" "[error-context-new-latin1+utf16]" (func (;12;) (type 3)))
-    (import "$root" "[error-context-debug-message-utf8]" (func (;13;) (type 1)))
-    (import "$root" "[error-context-debug-message-utf16]" (func (;14;) (type 1)))
-    (import "$root" "[error-context-debug-message-latin1+utf16]" (func (;15;) (type 1)))
-    (import "$root" "[error-context-drop]" (func (;16;) (type 0)))
-    (import "$root" "[context-get-0]" (func (;17;) (type 2)))
-    (import "$root" "[context-get-1]" (func (;18;) (type 2)))
-    (import "$root" "[context-set-0]" (func (;19;) (type 0)))
-    (import "$root" "[context-set-1]" (func (;20;) (type 0)))
+    (import "$root" "[subtask-cancel]" (func (;10;) (type 5)))
+    (import "$root" "[error-context-new-utf8]" (func (;11;) (type 3)))
+    (import "$root" "[error-context-new-utf16]" (func (;12;) (type 3)))
+    (import "$root" "[error-context-new-latin1+utf16]" (func (;13;) (type 3)))
+    (import "$root" "[error-context-debug-message-utf8]" (func (;14;) (type 1)))
+    (import "$root" "[error-context-debug-message-utf16]" (func (;15;) (type 1)))
+    (import "$root" "[error-context-debug-message-latin1+utf16]" (func (;16;) (type 1)))
+    (import "$root" "[error-context-drop]" (func (;17;) (type 0)))
+    (import "$root" "[context-get-0]" (func (;18;) (type 2)))
+    (import "$root" "[context-get-1]" (func (;19;) (type 2)))
+    (import "$root" "[context-set-0]" (func (;20;) (type 0)))
+    (import "$root" "[context-set-1]" (func (;21;) (type 0)))
     (memory (;0;) 1)
-    (export "[async-lift-stackful]foo" (func 21))
-    (export "[async-lift-stackful]foo:foo/bar#foo" (func 22))
+    (export "[async-lift-stackful]foo" (func 22))
+    (export "[async-lift-stackful]foo:foo/bar#foo" (func 23))
     (export "memory" (memory 0))
-    (export "cabi_realloc" (func 23))
-    (func (;21;) (type 1) (param i32 i32)
+    (export "cabi_realloc" (func 24))
+    (func (;22;) (type 1) (param i32 i32)
       unreachable
     )
-    (func (;22;) (type 1) (param i32 i32)
+    (func (;23;) (type 1) (param i32 i32)
       unreachable
     )
-    (func (;23;) (type 5) (param i32 i32 i32 i32) (result i32)
+    (func (;24;) (type 6) (param i32 i32 i32 i32) (result i32)
       unreachable
     )
     (@producers
@@ -155,17 +157,18 @@
   (core func (;5;) (canon waitable.join))
   (core func (;6;) (canon yield))
   (core func (;7;) (canon subtask.drop))
-  (alias core export 0 "2" (core func (;8;)))
-  (alias core export 0 "3" (core func (;9;)))
-  (alias core export 0 "4" (core func (;10;)))
-  (alias core export 0 "5" (core func (;11;)))
-  (alias core export 0 "6" (core func (;12;)))
-  (alias core export 0 "7" (core func (;13;)))
-  (core func (;14;) (canon error-context.drop))
-  (core func (;15;) (canon context.get i32 0))
-  (core func (;16;) (canon context.get i32 1))
-  (core func (;17;) (canon context.set i32 0))
-  (core func (;18;) (canon context.set i32 1))
+  (core func (;8;) (canon subtask.cancel))
+  (alias core export 0 "2" (core func (;9;)))
+  (alias core export 0 "3" (core func (;10;)))
+  (alias core export 0 "4" (core func (;11;)))
+  (alias core export 0 "5" (core func (;12;)))
+  (alias core export 0 "6" (core func (;13;)))
+  (alias core export 0 "7" (core func (;14;)))
+  (core func (;15;) (canon error-context.drop))
+  (core func (;16;) (canon context.get i32 0))
+  (core func (;17;) (canon context.get i32 1))
+  (core func (;18;) (canon context.set i32 0))
+  (core func (;19;) (canon context.set i32 1))
   (core instance (;1;)
     (export "[backpressure-set]" (func 0))
     (export "[waitable-set-new]" (func 1))
@@ -175,25 +178,26 @@
     (export "[waitable-join]" (func 5))
     (export "[yield]" (func 6))
     (export "[subtask-drop]" (func 7))
-    (export "[error-context-new-utf8]" (func 8))
-    (export "[error-context-new-utf16]" (func 9))
-    (export "[error-context-new-latin1+utf16]" (func 10))
-    (export "[error-context-debug-message-utf8]" (func 11))
-    (export "[error-context-debug-message-utf16]" (func 12))
-    (export "[error-context-debug-message-latin1+utf16]" (func 13))
-    (export "[error-context-drop]" (func 14))
-    (export "[context-get-0]" (func 15))
-    (export "[context-get-1]" (func 16))
-    (export "[context-set-0]" (func 17))
-    (export "[context-set-1]" (func 18))
+    (export "[subtask-cancel]" (func 8))
+    (export "[error-context-new-utf8]" (func 9))
+    (export "[error-context-new-utf16]" (func 10))
+    (export "[error-context-new-latin1+utf16]" (func 11))
+    (export "[error-context-debug-message-utf8]" (func 12))
+    (export "[error-context-debug-message-utf16]" (func 13))
+    (export "[error-context-debug-message-latin1+utf16]" (func 14))
+    (export "[error-context-drop]" (func 15))
+    (export "[context-get-0]" (func 16))
+    (export "[context-get-1]" (func 17))
+    (export "[context-set-0]" (func 18))
+    (export "[context-set-1]" (func 19))
   )
-  (alias core export 0 "8" (core func (;19;)))
+  (alias core export 0 "8" (core func (;20;)))
   (core instance (;2;)
-    (export "[task-return]foo" (func 19))
+    (export "[task-return]foo" (func 20))
   )
-  (alias core export 0 "9" (core func (;20;)))
+  (alias core export 0 "9" (core func (;21;)))
   (core instance (;3;)
-    (export "[task-return]foo" (func 20))
+    (export "[task-return]foo" (func 21))
   )
   (core instance (;4;) (instantiate 0
       (with "$root" (instance 1))
@@ -203,41 +207,41 @@
   )
   (alias core export 4 "memory" (core memory (;0;)))
   (alias core export 0 "$imports" (core table (;0;)))
-  (core func (;21;) (canon waitable-set.wait (memory 0)))
-  (core func (;22;) (canon waitable-set.poll (memory 0)))
-  (core func (;23;) (canon error-context.new (memory 0) string-encoding=utf8))
-  (core func (;24;) (canon error-context.new (memory 0) string-encoding=utf16))
-  (core func (;25;) (canon error-context.new (memory 0) string-encoding=latin1+utf16))
-  (alias core export 4 "cabi_realloc" (core func (;26;)))
-  (core func (;27;) (canon error-context.debug-message (memory 0) (realloc 26) string-encoding=utf8))
-  (core func (;28;) (canon error-context.debug-message (memory 0) (realloc 26) string-encoding=utf16))
-  (core func (;29;) (canon error-context.debug-message (memory 0) (realloc 26) string-encoding=latin1+utf16))
-  (core func (;30;) (canon task.return (result string) (memory 0) string-encoding=utf8))
+  (core func (;22;) (canon waitable-set.wait (memory 0)))
+  (core func (;23;) (canon waitable-set.poll (memory 0)))
+  (core func (;24;) (canon error-context.new (memory 0) string-encoding=utf8))
+  (core func (;25;) (canon error-context.new (memory 0) string-encoding=utf16))
+  (core func (;26;) (canon error-context.new (memory 0) string-encoding=latin1+utf16))
+  (alias core export 4 "cabi_realloc" (core func (;27;)))
+  (core func (;28;) (canon error-context.debug-message (memory 0) (realloc 27) string-encoding=utf8))
+  (core func (;29;) (canon error-context.debug-message (memory 0) (realloc 27) string-encoding=utf16))
+  (core func (;30;) (canon error-context.debug-message (memory 0) (realloc 27) string-encoding=latin1+utf16))
   (core func (;31;) (canon task.return (result string) (memory 0) string-encoding=utf8))
+  (core func (;32;) (canon task.return (result string) (memory 0) string-encoding=utf8))
   (core instance (;5;)
     (export "$imports" (table 0))
-    (export "0" (func 21))
-    (export "1" (func 22))
-    (export "2" (func 23))
-    (export "3" (func 24))
-    (export "4" (func 25))
-    (export "5" (func 27))
-    (export "6" (func 28))
-    (export "7" (func 29))
-    (export "8" (func 30))
-    (export "9" (func 31))
+    (export "0" (func 22))
+    (export "1" (func 23))
+    (export "2" (func 24))
+    (export "3" (func 25))
+    (export "4" (func 26))
+    (export "5" (func 28))
+    (export "6" (func 29))
+    (export "7" (func 30))
+    (export "8" (func 31))
+    (export "9" (func 32))
   )
   (core instance (;6;) (instantiate 2
       (with "" (instance 5))
     )
   )
   (type (;0;) (func (param "s" string) (result string)))
-  (alias core export 4 "[async-lift-stackful]foo" (core func (;32;)))
-  (func (;0;) (type 0) (canon lift (core func 32) (memory 0) (realloc 26) string-encoding=utf8 async))
+  (alias core export 4 "[async-lift-stackful]foo" (core func (;33;)))
+  (func (;0;) (type 0) (canon lift (core func 33) (memory 0) (realloc 27) string-encoding=utf8 async))
   (export (;1;) "foo" (func 0))
   (type (;1;) (func (param "s" string) (result string)))
-  (alias core export 4 "[async-lift-stackful]foo:foo/bar#foo" (core func (;33;)))
-  (func (;2;) (type 1) (canon lift (core func 33) (memory 0) (realloc 26) string-encoding=utf8 async))
+  (alias core export 4 "[async-lift-stackful]foo:foo/bar#foo" (core func (;34;)))
+  (func (;2;) (type 1) (canon lift (core func 34) (memory 0) (realloc 27) string-encoding=utf8 async))
   (component (;0;)
     (type (;0;) (func (param "s" string) (result string)))
     (import "import-func-foo" (func (;0;) (type 0)))
diff --git a/crates/wit-component/tests/components/async-builtins/module.wat b/crates/wit-component/tests/components/async-builtins/module.wat
index eac3f0a3..43a727c1 100644
--- a/crates/wit-component/tests/components/async-builtins/module.wat
+++ b/crates/wit-component/tests/components/async-builtins/module.wat
@@ -9,6 +9,7 @@
   (import "$root" "[waitable-join]" (func (param i32 i32)))
   (import "$root" "[yield]" (func))
   (import "$root" "[subtask-drop]" (func (param i32)))
+  (import "$root" "[subtask-cancel]" (func (param i32) (result 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)))
   (import "$root" "[error-context-new-latin1+utf16]" (func (param i32 i32) (result i32)))
diff --git a/tests/cli/component-model-async/task-builtins.wast b/tests/cli/component-model-async/task-builtins.wast
index 9c13d429..9c87157c 100644
--- a/tests/cli/component-model-async/task-builtins.wast
+++ b/tests/cli/component-model-async/task-builtins.wast
@@ -212,6 +212,27 @@
   "type mismatch for export `subtask.drop` of module instantiation argument ``"
 )
 
+;; subtask.cancel
+(component
+  (core module $m
+    (import "" "subtask.cancel" (func $subtask-cancel (param i32) (result i32)))
+  )
+  (core func $subtask-cancel (canon subtask.cancel))
+  (core instance $i (instantiate $m (with "" (instance (export "subtask.cancel" (func $subtask-cancel))))))
+)
+
+;; subtask.cancel; incorrect type
+(assert_invalid
+  (component
+    (core module $m
+      (import "" "subtask.cancel" (func $subtask-cancel (param i32 i32) (result i32)))
+    )
+    (core func $subtask-cancel (canon subtask.cancel))
+    (core instance $i (instantiate $m (with "" (instance (export "subtask.cancel" (func $subtask-cancel))))))
+  )
+  "type mismatch for export `subtask.cancel` of module instantiation argument ``"
+)
+
 ;; context.{get,set}
 (component
   (core func $get0 (canon context.get i32 0))
@@ -290,6 +311,10 @@
   (canon task.return (core func))
   (core func (canon subtask.drop))
   (canon subtask.drop (core func))
+  (core func (canon subtask.cancel))
+  (canon subtask.cancel (core func))
+  (core func (canon subtask.cancel async))
+  (canon subtask.cancel async (core func))
 
   (core module $m
     (memory (export "m") 1)
diff --git a/tests/cli/missing-features/component-model/async.wast b/tests/cli/missing-features/component-model/async.wast
index 56d37cb2..980a7f23 100644
--- a/tests/cli/missing-features/component-model/async.wast
+++ b/tests/cli/missing-features/component-model/async.wast
@@ -124,6 +124,18 @@
   "`subtask.drop` requires the component model async feature"
 )
 
+;; subtask.cancel
+(assert_invalid
+  (component
+    (core module $m
+      (import "" "subtask.cancel" (func $subtask-cancel (param i32)))
+    )
+    (core func $subtask-cancel (canon subtask.cancel))
+    (core instance $i (instantiate $m (with "" (instance (export "subtask.cancel" (func $subtask-cancel))))))
+  )
+  "`subtask.cancel` requires the component model async feature"
+)
+
 ;; stream.new
 (assert_invalid
   (component
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 7ee2dd64..481bcfda 100644
--- a/tests/snapshots/cli/component-model-async/task-builtins.wast.json
+++ b/tests/snapshots/cli/component-model-async/task-builtins.wast.json
@@ -153,64 +153,77 @@
     },
     {
       "type": "assert_invalid",
-      "line": 239,
+      "line": 226,
       "filename": "task-builtins.23.wasm",
       "module_type": "binary",
+      "text": "type mismatch for export `subtask.cancel` of module instantiation argument ``"
+    },
+    {
+      "type": "module",
+      "line": 237,
+      "filename": "task-builtins.24.wasm",
+      "module_type": "binary"
+    },
+    {
+      "type": "assert_invalid",
+      "line": 260,
+      "filename": "task-builtins.25.wasm",
+      "module_type": "binary",
       "text": "found:    [] -> [i32]"
     },
     {
       "type": "assert_invalid",
-      "line": 246,
-      "filename": "task-builtins.24.wasm",
+      "line": 267,
+      "filename": "task-builtins.26.wasm",
       "module_type": "binary",
       "text": "found:    [i32] -> []"
     },
     {
       "type": "assert_invalid",
-      "line": 253,
-      "filename": "task-builtins.25.wasm",
+      "line": 274,
+      "filename": "task-builtins.27.wasm",
       "module_type": "binary",
       "text": "immediate larger than two: 100"
     },
     {
       "type": "assert_invalid",
-      "line": 257,
-      "filename": "task-builtins.26.wasm",
+      "line": 278,
+      "filename": "task-builtins.28.wasm",
       "module_type": "binary",
       "text": "immediate larger than two: 100"
     },
     {
       "type": "assert_malformed",
-      "line": 261,
-      "filename": "task-builtins.27.wat",
+      "line": 282,
+      "filename": "task-builtins.29.wat",
       "module_type": "text",
       "text": "expected keyword `i32`"
     },
     {
       "type": "assert_malformed",
-      "line": 265,
-      "filename": "task-builtins.28.wat",
+      "line": 286,
+      "filename": "task-builtins.30.wat",
       "module_type": "text",
       "text": "expected keyword `i32`"
     },
     {
       "type": "assert_malformed",
-      "line": 270,
-      "filename": "task-builtins.29.wasm",
+      "line": 291,
+      "filename": "task-builtins.31.wasm",
       "module_type": "binary",
       "text": "invalid leading byte (0x7e) for context.get"
     },
     {
       "type": "assert_malformed",
-      "line": 277,
-      "filename": "task-builtins.30.wasm",
+      "line": 298,
+      "filename": "task-builtins.32.wasm",
       "module_type": "binary",
       "text": "invalid leading byte (0x7e) for context.set"
     },
     {
       "type": "module",
-      "line": 286,
-      "filename": "task-builtins.31.wasm",
+      "line": 307,
+      "filename": "task-builtins.33.wasm",
       "module_type": "binary"
     }
   ]
diff --git a/tests/snapshots/cli/component-model-async/task-builtins.wast/22.print b/tests/snapshots/cli/component-model-async/task-builtins.wast/22.print
index 0bacf89e..234b55d3 100644
--- a/tests/snapshots/cli/component-model-async/task-builtins.wast/22.print
+++ b/tests/snapshots/cli/component-model-async/task-builtins.wast/22.print
@@ -1,23 +1,13 @@
 (component
-  (core func $get0 (;0;) (canon context.get i32 0))
-  (core func $get1 (;1;) (canon context.get i32 1))
-  (core func $set0 (;2;) (canon context.set i32 0))
-  (core func $set1 (;3;) (canon context.set i32 1))
   (core module $m (;0;)
-    (type (;0;) (func (result i32)))
-    (type (;1;) (func (param i32)))
-    (import "" "get0" (func (;0;) (type 0)))
-    (import "" "get1" (func (;1;) (type 0)))
-    (import "" "set0" (func (;2;) (type 1)))
-    (import "" "set1" (func (;3;) (type 1)))
+    (type (;0;) (func (param i32) (result i32)))
+    (import "" "subtask.cancel" (func $subtask-cancel (;0;) (type 0)))
   )
+  (core func $subtask-cancel (;0;) (canon subtask.cancel))
   (core instance (;0;)
-    (export "get0" (func $get0))
-    (export "get1" (func $get1))
-    (export "set0" (func $set0))
-    (export "set1" (func $set1))
+    (export "subtask.cancel" (func $subtask-cancel))
   )
-  (core instance (;1;) (instantiate $m
+  (core instance $i (;1;) (instantiate $m
       (with "" (instance 0))
     )
   )
diff --git a/tests/snapshots/cli/component-model-async/task-builtins.wast/24.print b/tests/snapshots/cli/component-model-async/task-builtins.wast/24.print
new file mode 100644
index 00000000..0bacf89e
--- /dev/null
+++ b/tests/snapshots/cli/component-model-async/task-builtins.wast/24.print
@@ -0,0 +1,24 @@
+(component
+  (core func $get0 (;0;) (canon context.get i32 0))
+  (core func $get1 (;1;) (canon context.get i32 1))
+  (core func $set0 (;2;) (canon context.set i32 0))
+  (core func $set1 (;3;) (canon context.set i32 1))
+  (core module $m (;0;)
+    (type (;0;) (func (result i32)))
+    (type (;1;) (func (param i32)))
+    (import "" "get0" (func (;0;) (type 0)))
+    (import "" "get1" (func (;1;) (type 0)))
+    (import "" "set0" (func (;2;) (type 1)))
+    (import "" "set1" (func (;3;) (type 1)))
+  )
+  (core instance (;0;)
+    (export "get0" (func $get0))
+    (export "get1" (func $get1))
+    (export "set0" (func $set0))
+    (export "set1" (func $set1))
+  )
+  (core instance (;1;) (instantiate $m
+      (with "" (instance 0))
+    )
+  )
+)
diff --git a/tests/snapshots/cli/component-model-async/task-builtins.wast/31.print b/tests/snapshots/cli/component-model-async/task-builtins.wast/31.print
deleted file mode 100644
index f1fa1237..00000000
--- a/tests/snapshots/cli/component-model-async/task-builtins.wast/31.print
+++ /dev/null
@@ -1,63 +0,0 @@
-(component
-  (core func (;0;) (canon backpressure.set))
-  (core func (;1;) (canon backpressure.set))
-  (core func (;2;) (canon task.return))
-  (core func (;3;) (canon task.return))
-  (core func (;4;) (canon subtask.drop))
-  (core func (;5;) (canon subtask.drop))
-  (core module $m (;0;)
-    (type (;0;) (func (param i32 i32 i32 i32) (result i32)))
-    (memory (;0;) 1)
-    (export "m" (memory 0))
-    (export "r" (func 0))
-    (func (;0;) (type 0) (param i32 i32 i32 i32) (result i32)
-      unreachable
-    )
-  )
-  (core instance $i (;0;) (instantiate $m))
-  (alias core export $i "m" (core memory $m (;0;)))
-  (alias core export $i "r" (core func $r (;6;)))
-  (type $r (;0;) (resource (rep i32)))
-  (core func (;7;) (canon resource.drop $r async))
-  (core func (;8;) (canon resource.drop $r async))
-  (type $s (;1;) (stream))
-  (type $f (;2;) (future))
-  (core func (;9;) (canon future.new $f))
-  (core func (;10;) (canon future.new $f))
-  (core func (;11;) (canon stream.new $s))
-  (core func (;12;) (canon stream.new $s))
-  (core func (;13;) (canon future.cancel-read $f))
-  (core func (;14;) (canon future.cancel-read $f))
-  (core func (;15;) (canon stream.cancel-read $s))
-  (core func (;16;) (canon stream.cancel-read $s))
-  (core func (;17;) (canon future.cancel-write $f))
-  (core func (;18;) (canon future.cancel-write $f))
-  (core func (;19;) (canon stream.cancel-write $s))
-  (core func (;20;) (canon stream.cancel-write $s))
-  (core func (;21;) (canon future.close-readable $f))
-  (core func (;22;) (canon future.close-readable $f))
-  (core func (;23;) (canon future.close-writable $f))
-  (core func (;24;) (canon future.close-writable $f))
-  (core func (;25;) (canon stream.close-readable $s))
-  (core func (;26;) (canon stream.close-readable $s))
-  (core func (;27;) (canon stream.close-writable $s))
-  (core func (;28;) (canon stream.close-writable $s))
-  (core func (;29;) (canon future.read $f (memory $m)))
-  (core func (;30;) (canon future.read $f (memory $m)))
-  (core func (;31;) (canon future.write $f (memory $m)))
-  (core func (;32;) (canon future.write $f (memory $m)))
-  (core func (;33;) (canon stream.read $s (memory $m)))
-  (core func (;34;) (canon stream.read $s (memory $m)))
-  (core func (;35;) (canon stream.write $s (memory $m)))
-  (core func (;36;) (canon stream.write $s (memory $m)))
-  (core func (;37;) (canon error-context.new (memory $m)))
-  (core func (;38;) (canon error-context.new (memory $m)))
-  (core func (;39;) (canon error-context.debug-message (memory $m) (realloc $r)))
-  (core func (;40;) (canon error-context.debug-message (memory $m) (realloc $r)))
-  (core func (;41;) (canon error-context.drop))
-  (core func (;42;) (canon error-context.drop))
-  (core func (;43;) (canon context.get i32 0))
-  (core func (;44;) (canon context.get i32 1))
-  (core func (;45;) (canon context.set i32 0))
-  (core func (;46;) (canon context.set i32 1))
-)
diff --git a/tests/snapshots/cli/component-model-async/task-builtins.wast/33.print b/tests/snapshots/cli/component-model-async/task-builtins.wast/33.print
new file mode 100644
index 00000000..99bfcb71
--- /dev/null
+++ b/tests/snapshots/cli/component-model-async/task-builtins.wast/33.print
@@ -0,0 +1,67 @@
+(component
+  (core func (;0;) (canon backpressure.set))
+  (core func (;1;) (canon backpressure.set))
+  (core func (;2;) (canon task.return))
+  (core func (;3;) (canon task.return))
+  (core func (;4;) (canon subtask.drop))
+  (core func (;5;) (canon subtask.drop))
+  (core func (;6;) (canon subtask.cancel))
+  (core func (;7;) (canon subtask.cancel))
+  (core func (;8;) (canon subtask.cancel async))
+  (core func (;9;) (canon subtask.cancel async))
+  (core module $m (;0;)
+    (type (;0;) (func (param i32 i32 i32 i32) (result i32)))
+    (memory (;0;) 1)
+    (export "m" (memory 0))
+    (export "r" (func 0))
+    (func (;0;) (type 0) (param i32 i32 i32 i32) (result i32)
+      unreachable
+    )
+  )
+  (core instance $i (;0;) (instantiate $m))
+  (alias core export $i "m" (core memory $m (;0;)))
+  (alias core export $i "r" (core func $r (;10;)))
+  (type $r (;0;) (resource (rep i32)))
+  (core func (;11;) (canon resource.drop $r async))
+  (core func (;12;) (canon resource.drop $r async))
+  (type $s (;1;) (stream))
+  (type $f (;2;) (future))
+  (core func (;13;) (canon future.new $f))
+  (core func (;14;) (canon future.new $f))
+  (core func (;15;) (canon stream.new $s))
+  (core func (;16;) (canon stream.new $s))
+  (core func (;17;) (canon future.cancel-read $f))
+  (core func (;18;) (canon future.cancel-read $f))
+  (core func (;19;) (canon stream.cancel-read $s))
+  (core func (;20;) (canon stream.cancel-read $s))
+  (core func (;21;) (canon future.cancel-write $f))
+  (core func (;22;) (canon future.cancel-write $f))
+  (core func (;23;) (canon stream.cancel-write $s))
+  (core func (;24;) (canon stream.cancel-write $s))
+  (core func (;25;) (canon future.close-readable $f))
+  (core func (;26;) (canon future.close-readable $f))
+  (core func (;27;) (canon future.close-writable $f))
+  (core func (;28;) (canon future.close-writable $f))
+  (core func (;29;) (canon stream.close-readable $s))
+  (core func (;30;) (canon stream.close-readable $s))
+  (core func (;31;) (canon stream.close-writable $s))
+  (core func (;32;) (canon stream.close-writable $s))
+  (core func (;33;) (canon future.read $f (memory $m)))
+  (core func (;34;) (canon future.read $f (memory $m)))
+  (core func (;35;) (canon future.write $f (memory $m)))
+  (core func (;36;) (canon future.write $f (memory $m)))
+  (core func (;37;) (canon stream.read $s (memory $m)))
+  (core func (;38;) (canon stream.read $s (memory $m)))
+  (core func (;39;) (canon stream.write $s (memory $m)))
+  (core func (;40;) (canon stream.write $s (memory $m)))
+  (core func (;41;) (canon error-context.new (memory $m)))
+  (core func (;42;) (canon error-context.new (memory $m)))
+  (core func (;43;) (canon error-context.debug-message (memory $m) (realloc $r)))
+  (core func (;44;) (canon error-context.debug-message (memory $m) (realloc $r)))
+  (core func (;45;) (canon error-context.drop))
+  (core func (;46;) (canon error-context.drop))
+  (core func (;47;) (canon context.get i32 0))
+  (core func (;48;) (canon context.get i32 1))
+  (core func (;49;) (canon context.set i32 0))
+  (core func (;50;) (canon context.set i32 1))
+)
diff --git a/tests/snapshots/cli/missing-features/component-model/async.wast.json b/tests/snapshots/cli/missing-features/component-model/async.wast.json
index b8b79571..af550650 100644
--- a/tests/snapshots/cli/missing-features/component-model/async.wast.json
+++ b/tests/snapshots/cli/missing-features/component-model/async.wast.json
@@ -83,140 +83,147 @@
       "line": 129,
       "filename": "async.11.wasm",
       "module_type": "binary",
-      "text": "requires the component model async feature"
+      "text": "`subtask.cancel` requires the component model async feature"
     },
     {
       "type": "assert_invalid",
-      "line": 142,
+      "line": 141,
       "filename": "async.12.wasm",
       "module_type": "binary",
       "text": "requires the component model async feature"
     },
     {
       "type": "assert_invalid",
-      "line": 157,
+      "line": 154,
       "filename": "async.13.wasm",
       "module_type": "binary",
       "text": "requires the component model async feature"
     },
     {
       "type": "assert_invalid",
-      "line": 172,
+      "line": 169,
       "filename": "async.14.wasm",
       "module_type": "binary",
       "text": "requires the component model async feature"
     },
     {
       "type": "assert_invalid",
-      "line": 185,
+      "line": 184,
       "filename": "async.15.wasm",
       "module_type": "binary",
       "text": "requires the component model async feature"
     },
     {
       "type": "assert_invalid",
-      "line": 198,
+      "line": 197,
       "filename": "async.16.wasm",
       "module_type": "binary",
       "text": "requires the component model async feature"
     },
     {
       "type": "assert_invalid",
-      "line": 211,
+      "line": 210,
       "filename": "async.17.wasm",
       "module_type": "binary",
       "text": "requires the component model async feature"
     },
     {
       "type": "assert_invalid",
-      "line": 224,
+      "line": 223,
       "filename": "async.18.wasm",
       "module_type": "binary",
       "text": "requires the component model async feature"
     },
     {
       "type": "assert_invalid",
-      "line": 237,
+      "line": 236,
       "filename": "async.19.wasm",
       "module_type": "binary",
       "text": "requires the component model async feature"
     },
     {
       "type": "assert_invalid",
-      "line": 252,
+      "line": 249,
       "filename": "async.20.wasm",
       "module_type": "binary",
       "text": "requires the component model async feature"
     },
     {
       "type": "assert_invalid",
-      "line": 267,
+      "line": 264,
       "filename": "async.21.wasm",
       "module_type": "binary",
       "text": "requires the component model async feature"
     },
     {
       "type": "assert_invalid",
-      "line": 280,
+      "line": 279,
       "filename": "async.22.wasm",
       "module_type": "binary",
       "text": "requires the component model async feature"
     },
     {
       "type": "assert_invalid",
-      "line": 293,
+      "line": 292,
       "filename": "async.23.wasm",
       "module_type": "binary",
       "text": "requires the component model async feature"
     },
     {
       "type": "assert_invalid",
-      "line": 306,
+      "line": 305,
       "filename": "async.24.wasm",
       "module_type": "binary",
       "text": "requires the component model async feature"
     },
     {
       "type": "assert_invalid",
-      "line": 319,
+      "line": 318,
       "filename": "async.25.wasm",
       "module_type": "binary",
       "text": "requires the component model async feature"
     },
     {
       "type": "assert_invalid",
-      "line": 323,
+      "line": 331,
       "filename": "async.26.wasm",
       "module_type": "binary",
       "text": "requires the component model async feature"
     },
     {
       "type": "assert_invalid",
-      "line": 327,
+      "line": 335,
       "filename": "async.27.wasm",
       "module_type": "binary",
-      "text": "requires the component model async builtins feature"
+      "text": "requires the component model async feature"
     },
     {
       "type": "assert_invalid",
-      "line": 335,
+      "line": 339,
       "filename": "async.28.wasm",
       "module_type": "binary",
-      "text": "require the component model async feature"
+      "text": "requires the component model async builtins feature"
     },
     {
       "type": "assert_invalid",
-      "line": 340,
+      "line": 347,
       "filename": "async.29.wasm",
       "module_type": "binary",
       "text": "require the component model async feature"
     },
     {
       "type": "assert_invalid",
-      "line": 345,
+      "line": 352,
       "filename": "async.30.wasm",
       "module_type": "binary",
       "text": "require the component model async feature"
+    },
+    {
+      "type": "assert_invalid",
+      "line": 357,
+      "filename": "async.31.wasm",
+      "module_type": "binary",
+      "text": "require the component model async feature"
     }
   ]
 }
\ No newline at end of file