Repository: bytecodealliance/wasm-tools
Pull request #2100 (2025-03-14 18:21:47)

Implement `context.{get,set}` intrinsics
This fills out the last missing set of intrinsics from the component-model async proposal. These two intrinsics are pretty simple and use preexisting idioms everywhere for integration into tooling.


Patch:
diff --git a/.github/workflows/playground.yml b/.github/workflows/playground.yml
index 9ce36d4d..9b778bff 100644
--- a/.github/workflows/playground.yml
+++ b/.github/workflows/playground.yml
@@ -28,7 +28,7 @@ jobs:
           node-version: 20
       - uses: bytecodealliance/wasmtime/.github/actions/install-rust@v20.0.0
       - uses: cargo-bins/cargo-binstall@v1.6.9
-      - run: cargo binstall cargo-component -y
+      - run: cargo binstall cargo-component@0.20.0 -y
       - run: rustup component add rustfmt # needed for cargo-component, apparently?
       - run: rustup target add wasm32-wasip1
       - run: npm ci
diff --git a/crates/wasm-encoder/src/component/builder.rs b/crates/wasm-encoder/src/component/builder.rs
index 06b78b64..a2f8a7bd 100644
--- a/crates/wasm-encoder/src/component/builder.rs
+++ b/crates/wasm-encoder/src/component/builder.rs
@@ -407,6 +407,18 @@ impl ComponentBuilder {
         inc(&mut self.core_funcs)
     }
 
+    /// Declares a new `context.get` intrinsic.
+    pub fn context_get(&mut self, i: u32) -> u32 {
+        self.canonical_functions().context_get(i);
+        inc(&mut self.core_funcs)
+    }
+
+    /// Declares a new `context.set` intrinsic.
+    pub fn context_set(&mut self, i: u32) -> u32 {
+        self.canonical_functions().context_set(i);
+        inc(&mut self.core_funcs)
+    }
+
     /// Declares a new `task.yield` intrinsic.
     pub fn yield_(&mut self, async_: bool) -> u32 {
         self.canonical_functions().yield_(async_);
diff --git a/crates/wasm-encoder/src/component/canonicals.rs b/crates/wasm-encoder/src/component/canonicals.rs
index a20fae46..029fa15e 100644
--- a/crates/wasm-encoder/src/component/canonicals.rs
+++ b/crates/wasm-encoder/src/component/canonicals.rs
@@ -200,6 +200,24 @@ impl CanonicalFunctionSection {
         self
     }
 
+    /// Defines a new `context.get` intrinsic of the ith slot.
+    pub fn context_get(&mut self, i: u32) -> &mut Self {
+        self.bytes.push(0x0a);
+        self.bytes.push(0x7f);
+        i.encode(&mut self.bytes);
+        self.num_added += 1;
+        self
+    }
+
+    /// Defines a new `context.set` intrinsic of the ith slot.
+    pub fn context_set(&mut self, i: u32) -> &mut Self {
+        self.bytes.push(0x0b);
+        self.bytes.push(0x7f);
+        i.encode(&mut self.bytes);
+        self.num_added += 1;
+        self
+    }
+
     /// Defines a function which yields control to the host so that other tasks
     /// are able to make progress, if any.
     ///
diff --git a/crates/wasm-encoder/src/reencode/component.rs b/crates/wasm-encoder/src/reencode/component.rs
index bbeecb3e..7f20cbe3 100644
--- a/crates/wasm-encoder/src/reencode/component.rs
+++ b/crates/wasm-encoder/src/reencode/component.rs
@@ -969,6 +969,12 @@ pub mod component_utils {
                     options.iter().map(|o| reencoder.canonical_option(*o)),
                 );
             }
+            wasmparser::CanonicalFunction::ContextGet(i) => {
+                section.context_get(i);
+            }
+            wasmparser::CanonicalFunction::ContextSet(i) => {
+                section.context_set(i);
+            }
             wasmparser::CanonicalFunction::Yield { async_ } => {
                 section.yield_(async_);
             }
diff --git a/crates/wasmparser/src/readers/component/canonicals.rs b/crates/wasmparser/src/readers/component/canonicals.rs
index 5a7be8b1..4b5a0548 100644
--- a/crates/wasmparser/src/readers/component/canonicals.rs
+++ b/crates/wasmparser/src/readers/component/canonicals.rs
@@ -90,6 +90,10 @@ pub enum CanonicalFunction {
         /// The canonical options for the function.
         options: Box<[CanonicalOption]>,
     },
+    /// A `context.get` intrinsic for the `i`th slot of task-local storage.
+    ContextGet(u32),
+    /// A `context.set` intrinsic for the `i`th slot of task-local storage.
+    ContextSet(u32),
     /// A function which yields control to the host so that other tasks are able
     /// to make progress, if any.
     Yield {
@@ -282,6 +286,14 @@ impl<'a> FromReader<'a> for CanonicalFunction {
                 result: crate::read_resultlist(reader)?,
                 options: read_opts(reader)?,
             },
+            0x0a => match reader.read_u8()? {
+                0x7f => CanonicalFunction::ContextGet(reader.read_var_u32()?),
+                x => return reader.invalid_leading_byte(x, "context.get intrinsic type"),
+            },
+            0x0b => match reader.read_u8()? {
+                0x7f => CanonicalFunction::ContextSet(reader.read_var_u32()?),
+                x => return reader.invalid_leading_byte(x, "context.set intrinsic type"),
+            },
             0x0c => CanonicalFunction::Yield {
                 async_: reader.read()?,
             },
diff --git a/crates/wasmparser/src/validator/component.rs b/crates/wasmparser/src/validator/component.rs
index 6ec9993f..2d248adc 100644
--- a/crates/wasmparser/src/validator/component.rs
+++ b/crates/wasmparser/src/validator/component.rs
@@ -997,6 +997,8 @@ impl ComponentState {
             CanonicalFunction::TaskReturn { result, options } => {
                 self.task_return(&result, &options, types, offset, features)
             }
+            CanonicalFunction::ContextGet(i) => self.context_get(i, types, offset, features),
+            CanonicalFunction::ContextSet(i) => self.context_set(i, types, offset, features),
             CanonicalFunction::Yield { async_ } => self.yield_(async_, types, offset, features),
             CanonicalFunction::SubtaskDrop => self.subtask_drop(types, offset, features),
             CanonicalFunction::StreamNew { ty } => self.stream_new(ty, types, offset, features),
@@ -1246,6 +1248,50 @@ impl ComponentState {
         Ok(())
     }
 
+    fn context_get(
+        &mut self,
+        i: u32,
+        types: &mut TypeAlloc,
+        offset: usize,
+        features: &WasmFeatures,
+    ) -> Result<()> {
+        if !features.cm_async() {
+            bail!(
+                offset,
+                "`context.get` requires the component model async feature"
+            )
+        }
+        if i > 2 {
+            bail!(offset, "`context.get` immediate larger than two: {i}")
+        }
+
+        self.core_funcs
+            .push(types.intern_func_type(FuncType::new([], [ValType::I32]), offset));
+        Ok(())
+    }
+
+    fn context_set(
+        &mut self,
+        i: u32,
+        types: &mut TypeAlloc,
+        offset: usize,
+        features: &WasmFeatures,
+    ) -> Result<()> {
+        if !features.cm_async() {
+            bail!(
+                offset,
+                "`context.set` requires the component model async feature"
+            )
+        }
+        if i > 2 {
+            bail!(offset, "`context.set` immediate larger than two: {i}")
+        }
+
+        self.core_funcs
+            .push(types.intern_func_type(FuncType::new([ValType::I32], []), offset));
+        Ok(())
+    }
+
     fn yield_(
         &mut self,
         async_: bool,
diff --git a/crates/wasmprinter/src/component.rs b/crates/wasmprinter/src/component.rs
index d3664de4..08400c49 100644
--- a/crates/wasmprinter/src/component.rs
+++ b/crates/wasmprinter/src/component.rs
@@ -928,6 +928,18 @@ impl Printer<'_, '_> {
                         Ok(())
                     })?;
                 }
+                CanonicalFunction::ContextGet(i) => {
+                    self.print_intrinsic(state, "canon context.get", &|me, _state| {
+                        write!(me.result, " i32 {i}")?;
+                        Ok(())
+                    })?;
+                }
+                CanonicalFunction::ContextSet(i) => {
+                    self.print_intrinsic(state, "canon context.set", &|me, _state| {
+                        write!(me.result, " i32 {i}")?;
+                        Ok(())
+                    })?;
+                }
                 CanonicalFunction::Yield { async_ } => {
                     self.print_intrinsic(state, "canon yield", &|me, _| {
                         if async_ {
diff --git a/crates/wast/src/component/binary.rs b/crates/wast/src/component/binary.rs
index 70786fb1..ac2fba0a 100644
--- a/crates/wast/src/component/binary.rs
+++ b/crates/wast/src/component/binary.rs
@@ -374,6 +374,14 @@ impl<'a> Encoder<'a> {
                         info.opts.iter().map(Into::into),
                     );
                 }
+                CoreFuncKind::ContextGet(i) => {
+                    self.core_func_names.push(name);
+                    self.funcs.context_get(*i);
+                }
+                CoreFuncKind::ContextSet(i) => {
+                    self.core_func_names.push(name);
+                    self.funcs.context_set(*i);
+                }
                 CoreFuncKind::Yield(info) => {
                     self.core_func_names.push(name);
                     self.funcs.yield_(info.async_);
diff --git a/crates/wast/src/component/func.rs b/crates/wast/src/component/func.rs
index c5244754..1967ba3d 100644
--- a/crates/wast/src/component/func.rs
+++ b/crates/wast/src/component/func.rs
@@ -55,6 +55,8 @@ pub enum CoreFuncKind<'a> {
     ThreadAvailableParallelism(CanonThreadAvailableParallelism),
     BackpressureSet,
     TaskReturn(CanonTaskReturn<'a>),
+    ContextGet(u32),
+    ContextSet(u32),
     Yield(CanonYield),
     SubtaskDrop,
     StreamNew(CanonStreamNew<'a>),
@@ -116,6 +118,14 @@ impl<'a> CoreFuncKind<'a> {
             Ok(CoreFuncKind::BackpressureSet)
         } else if l.peek::<kw::task_return>()? {
             Ok(CoreFuncKind::TaskReturn(parser.parse()?))
+        } else if l.peek::<kw::context_get>()? {
+            parser.parse::<kw::context_get>()?;
+            parser.parse::<kw::i32>()?;
+            Ok(CoreFuncKind::ContextGet(parser.parse()?))
+        } else if l.peek::<kw::context_set>()? {
+            parser.parse::<kw::context_set>()?;
+            parser.parse::<kw::i32>()?;
+            Ok(CoreFuncKind::ContextSet(parser.parse()?))
         } else if l.peek::<kw::yield_>()? {
             Ok(CoreFuncKind::Yield(parser.parse()?))
         } else if l.peek::<kw::subtask_drop>()? {
diff --git a/crates/wast/src/component/resolve.rs b/crates/wast/src/component/resolve.rs
index 118ae197..c5855f20 100644
--- a/crates/wast/src/component/resolve.rs
+++ b/crates/wast/src/component/resolve.rs
@@ -401,6 +401,7 @@ impl<'a> Resolver<'a> {
                     }
                     self.canon_opts(&mut info.opts)?;
                 }
+                CoreFuncKind::ContextGet(_) | CoreFuncKind::ContextSet(_) => {}
                 CoreFuncKind::StreamNew(info) => {
                     self.resolve_ns(&mut info.ty, Ns::Type)?;
                 }
diff --git a/crates/wast/src/lib.rs b/crates/wast/src/lib.rs
index effe7729..b7e314ab 100644
--- a/crates/wast/src/lib.rs
+++ b/crates/wast/src/lib.rs
@@ -589,6 +589,8 @@ pub mod kw {
     custom_keyword!(waitable_set_poll = "waitable-set.poll");
     custom_keyword!(waitable_set_drop = "waitable-set.drop");
     custom_keyword!(waitable_join = "waitable.join");
+    custom_keyword!(context_get = "context.get");
+    custom_keyword!(context_set = "context.set");
 }
 
 /// Common annotations used to parse WebAssembly text files.
diff --git a/crates/wit-component/Cargo.toml b/crates/wit-component/Cargo.toml
index e630ab17..73054b20 100644
--- a/crates/wit-component/Cargo.toml
+++ b/crates/wit-component/Cargo.toml
@@ -42,6 +42,7 @@ pretty_assertions = "1.3.0"
 wasmparser = { workspace = true, features = ['std', 'component-model', 'features'] }
 wasmprinter = { workspace = true, features = ['component-model'] }
 wat = { workspace = true, features = ['component-model'] }
+wasm-metadata = { workspace = true, features = ['oci'] }
 
 [target.'cfg(not(target_family = "wasm"))'.dev-dependencies]
 wasmtime = { workspace = true }
diff --git a/crates/wit-component/src/dummy.rs b/crates/wit-component/src/dummy.rs
index 1348c59d..9fd780fa 100644
--- a/crates/wit-component/src/dummy.rs
+++ b/crates/wit-component/src/dummy.rs
@@ -387,6 +387,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)))
+(import "$root" "[context-get-0]" (func (result i32)))
+(import "$root" "[context-get-1]" (func (result i32)))
+(import "$root" "[context-set-0]" (func (param i32)))
+(import "$root" "[context-set-1]" (func (param i32)))
 
 ;; deferred behind 🚝 or 🚟 upstream
 ;;(import "$root" "[async-lower][waitable-set-wait]" (func (param i32 i32) (result i32)))
diff --git a/crates/wit-component/src/encoding.rs b/crates/wit-component/src/encoding.rs
index 979e4c77..cace7b17 100644
--- a/crates/wit-component/src/encoding.rs
+++ b/crates/wit-component/src/encoding.rs
@@ -1811,6 +1811,14 @@ impl<'a> EncodingState<'a> {
                 let index = self.component.waitable_join();
                 Ok((ExportKind::Func, index))
             }
+            Import::ContextGet(n) => {
+                let index = self.component.context_get(*n);
+                Ok((ExportKind::Func, index))
+            }
+            Import::ContextSet(n) => {
+                let index = self.component.context_set(*n);
+                Ok((ExportKind::Func, index))
+            }
         }
     }
 
@@ -2189,7 +2197,9 @@ impl<'a> Shims<'a> {
                 | Import::StreamCloseReadable { .. }
                 | Import::WaitableSetNew
                 | Import::WaitableSetDrop
-                | Import::WaitableJoin => {}
+                | Import::WaitableJoin
+                | Import::ContextGet(_)
+                | Import::ContextSet(_) => {}
 
                 // If `task.return` needs to be indirect then generate a shim
                 // for it, otherwise skip the shim and let it get materialized
diff --git a/crates/wit-component/src/validation.rs b/crates/wit-component/src/validation.rs
index 912b6951..a0164550 100644
--- a/crates/wit-component/src/validation.rs
+++ b/crates/wit-component/src/validation.rs
@@ -255,6 +255,11 @@ pub enum Import {
     /// well.
     ExportedTaskReturn(WorldKey, Option<InterfaceId>, String, Option<Type>),
 
+    /// The `context.get` intrinsic for the nth slot of storage.
+    ContextGet(u32),
+    /// The `context.set` intrinsic for the nth slot of storage.
+    ContextSet(u32),
+
     /// A `canon backpressure.set` intrinsic.
     ///
     /// This allows the guest to dynamically indicate whether it's ready for
@@ -634,6 +639,19 @@ impl ImportMap {
                 return Ok(Import::ErrorContextDebugMessage { encoding });
             }
 
+            if let Some(i) = names.context_get(name) {
+                validate_not_async()?;
+                let expected = FuncType::new([], [ValType::I32]);
+                validate_func_sig(name, &expected, ty)?;
+                return Ok(Import::ContextGet(i));
+            }
+            if let Some(i) = names.context_set(name) {
+                validate_not_async()?;
+                let expected = FuncType::new([ValType::I32], []);
+                validate_func_sig(name, &expected, ty)?;
+                return Ok(Import::ContextSet(i));
+            }
+
             let key = WorldKey::Name(name.to_string());
             if let Some(WorldItem::Function(func)) = world.imports.get(&key) {
                 validate_func(resolve, ty, func, abi)?;
@@ -829,10 +847,8 @@ impl ImportMap {
         let prefixed_payload = |prefix: &str| {
             // parse the `prefix` into `func_name` and `type_index`, bailing out
             // with `None` if anything doesn't match.
-            let suffix = name.strip_prefix(prefix)?;
-            let index = suffix.find(']')?;
-            let func_name = &suffix[index + 1..];
-            let type_index: usize = suffix[..index].parse().ok()?;
+            let (type_index, func_name) = prefixed_integer(name, prefix)?;
+            let type_index = type_index as usize;
 
             // Double-check that `func_name` is indeed a function name within
             // this interface/world. Then additionally double-check that
@@ -1421,6 +1437,8 @@ trait NameMangling {
     fn error_context_new(&self, s: &str) -> Option<StringEncoding>;
     fn error_context_debug_message(&self, s: &str) -> Option<StringEncoding>;
     fn error_context_drop(&self) -> Option<&str>;
+    fn context_get(&self, name: &str) -> Option<u32>;
+    fn context_set(&self, name: &str) -> Option<u32>;
     fn module_to_interface(
         &self,
         module: &str,
@@ -1531,6 +1549,12 @@ impl NameMangling for Standard {
     fn error_context_drop(&self) -> Option<&str> {
         None
     }
+    fn context_get(&self, _: &str) -> Option<u32> {
+        None
+    }
+    fn context_set(&self, _: &str) -> Option<u32> {
+        None
+    }
     fn module_to_interface(
         &self,
         interface: &str,
@@ -1724,6 +1748,22 @@ impl NameMangling for Legacy {
     fn error_context_drop(&self) -> Option<&str> {
         Some("[error-context-drop]")
     }
+    fn context_get(&self, name: &str) -> Option<u32> {
+        let (n, rest) = prefixed_integer(name, "[context-get-")?;
+        if rest.is_empty() {
+            Some(n)
+        } else {
+            None
+        }
+    }
+    fn context_set(&self, name: &str) -> Option<u32> {
+        let (n, rest) = prefixed_integer(name, "[context-set-")?;
+        if rest.is_empty() {
+            Some(n)
+        } else {
+            None
+        }
+    }
     fn module_to_interface(
         &self,
         module: &str,
@@ -2009,6 +2049,17 @@ fn validate_func_sig(name: &str, expected: &FuncType, ty: &wasmparser::FuncType)
     Ok(())
 }
 
+/// Matches `name` as `[${prefix}N]...`, and if found returns `(N, "...")`
+fn prefixed_integer<'a>(name: &'a str, prefix: &str) -> Option<(u32, &'a str)> {
+    assert!(prefix.starts_with("["));
+    assert!(prefix.ends_with("-"));
+    let suffix = name.strip_prefix(prefix)?;
+    let index = suffix.find(']')?;
+    let rest = &suffix[index + 1..];
+    let n = suffix[..index].parse().ok()?;
+    Some((n, rest))
+}
+
 fn get_function<'a>(
     resolve: &'a Resolve,
     world: &'a World,
diff --git a/src/bin/wasm-tools/dump.rs b/src/bin/wasm-tools/dump.rs
index 49bde121..3b461fd2 100644
--- a/src/bin/wasm-tools/dump.rs
+++ b/src/bin/wasm-tools/dump.rs
@@ -423,6 +423,8 @@ impl<'a> Dump<'a> {
                             | CanonicalFunction::ThreadAvailableParallelism
                             | CanonicalFunction::BackpressureSet
                             | CanonicalFunction::TaskReturn { .. }
+                            | CanonicalFunction::ContextGet { .. }
+                            | CanonicalFunction::ContextSet { .. }
                             | CanonicalFunction::WaitableSetNew
                             | CanonicalFunction::WaitableSetWait { .. }
                             | CanonicalFunction::WaitableSetPoll { .. }


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 da1b9368..3ab8509a 100644
--- a/crates/wit-component/tests/components/async-builtins/component.wat
+++ b/crates/wit-component/tests/components/async-builtins/component.wat
@@ -23,18 +23,22 @@
     (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)))
     (memory (;0;) 1)
-    (export "[async-lift-stackful]foo" (func 17))
-    (export "[async-lift-stackful]foo:foo/bar#foo" (func 18))
+    (export "[async-lift-stackful]foo" (func 21))
+    (export "[async-lift-stackful]foo:foo/bar#foo" (func 22))
     (export "memory" (memory 0))
-    (export "cabi_realloc" (func 19))
-    (func (;17;) (type 1) (param i32 i32)
+    (export "cabi_realloc" (func 23))
+    (func (;21;) (type 1) (param i32 i32)
       unreachable
     )
-    (func (;18;) (type 1) (param i32 i32)
+    (func (;22;) (type 1) (param i32 i32)
       unreachable
     )
-    (func (;19;) (type 5) (param i32 i32 i32 i32) (result i32)
+    (func (;23;) (type 5) (param i32 i32 i32 i32) (result i32)
       unreachable
     )
     (@producers
@@ -158,6 +162,10 @@
   (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 instance (;1;)
     (export "[backpressure-set]" (func 0))
     (export "[waitable-set-new]" (func 1))
@@ -174,14 +182,18 @@
     (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))
   )
-  (alias core export 0 "8" (core func (;15;)))
+  (alias core export 0 "8" (core func (;19;)))
   (core instance (;2;)
-    (export "[task-return]foo" (func 15))
+    (export "[task-return]foo" (func 19))
   )
-  (alias core export 0 "9" (core func (;16;)))
+  (alias core export 0 "9" (core func (;20;)))
   (core instance (;3;)
-    (export "[task-return]foo" (func 16))
+    (export "[task-return]foo" (func 20))
   )
   (core instance (;4;) (instantiate 0
       (with "$root" (instance 1))
@@ -191,41 +203,41 @@
   )
   (alias core export 4 "memory" (core memory (;0;)))
   (alias core export 0 "$imports" (core table (;0;)))
-  (core func (;17;) (canon waitable-set.wait (memory 0)))
-  (core func (;18;) (canon waitable-set.poll (memory 0)))
-  (core func (;19;) (canon error-context.new (memory 0) string-encoding=utf8))
-  (core func (;20;) (canon error-context.new (memory 0) string-encoding=utf16))
-  (core func (;21;) (canon error-context.new (memory 0) string-encoding=latin1+utf16))
-  (alias core export 4 "cabi_realloc" (core func (;22;)))
-  (core func (;23;) (canon error-context.debug-message (memory 0) (realloc 22) string-encoding=utf8))
-  (core func (;24;) (canon error-context.debug-message (memory 0) (realloc 22) string-encoding=utf16))
-  (core func (;25;) (canon error-context.debug-message (memory 0) (realloc 22) string-encoding=latin1+utf16))
-  (core func (;26;) (canon task.return (result string) (memory 0) string-encoding=utf8))
-  (core func (;27;) (canon task.return (result string) (memory 0) string-encoding=utf8))
+  (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 (;31;) (canon task.return (result string) (memory 0) string-encoding=utf8))
   (core instance (;5;)
     (export "$imports" (table 0))
-    (export "0" (func 17))
-    (export "1" (func 18))
-    (export "2" (func 19))
-    (export "3" (func 20))
-    (export "4" (func 21))
-    (export "5" (func 23))
-    (export "6" (func 24))
-    (export "7" (func 25))
-    (export "8" (func 26))
-    (export "9" (func 27))
+    (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))
   )
   (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 (;28;)))
-  (func (;0;) (type 0) (canon lift (core func 28) (memory 0) (realloc 22) string-encoding=utf8 async))
+  (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))
   (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 (;29;)))
-  (func (;2;) (type 1) (canon lift (core func 29) (memory 0) (realloc 22) string-encoding=utf8 async))
+  (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))
   (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 7d9555f8..eac3f0a3 100644
--- a/crates/wit-component/tests/components/async-builtins/module.wat
+++ b/crates/wit-component/tests/components/async-builtins/module.wat
@@ -16,6 +16,10 @@
   (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)))
+  (import "$root" "[context-get-0]" (func (result i32)))
+  (import "$root" "[context-get-1]" (func (result i32)))
+  (import "$root" "[context-set-0]" (func (param i32)))
+  (import "$root" "[context-set-1]" (func (param i32)))
   (func (export "[async-lift-stackful]foo") (param i32 i32) unreachable)
   (func (export "[async-lift-stackful]foo:foo/bar#foo") (param i32 i32) unreachable)
   (memory (export "memory") 1)
diff --git a/tests/cli/component-model-async/task-builtins.wast b/tests/cli/component-model-async/task-builtins.wast
index 60ba9975..2a06072f 100644
--- a/tests/cli/component-model-async/task-builtins.wast
+++ b/tests/cli/component-model-async/task-builtins.wast
@@ -212,6 +212,75 @@
   "type mismatch for export `subtask.drop` of module instantiation argument ``"
 )
 
+;; context.{get,set}
+(component
+  (core func $get0 (canon context.get i32 0))
+  (core func $get1 (canon context.get i32 1))
+  (core func $set0 (canon context.set i32 0))
+  (core func $set1 (canon context.set i32 1))
+
+  (core module $m
+    (import "" "get0" (func (result i32)))
+    (import "" "get1" (func (result i32)))
+    (import "" "set0" (func (param i32)))
+    (import "" "set1" (func (param i32)))
+  )
+  (core instance (instantiate $m
+    (with "" (instance
+      (export "get0" (func $get0))
+      (export "get1" (func $get1))
+      (export "set0" (func $set0))
+      (export "set1" (func $set1))
+    ))
+  ))
+)
+
+(assert_invalid
+  (component
+    (core module $m (import "" "" (func (param i32) (result i32))))
+    (core func $f (canon context.get i32 0))
+    (core instance $i (instantiate $m (with "" (instance (export "" (func $f))))))
+  )
+  "found:    [] -> [i32]")
+(assert_invalid
+  (component
+    (core module $m (import "" "" (func (param i32) (result i32))))
+    (core func $f (canon context.set i32 0))
+    (core instance $i (instantiate $m (with "" (instance (export "" (func $f))))))
+  )
+  "found:    [i32] -> []")
+(assert_invalid
+  (component
+    (core func (canon context.get i32 100)))
+  "immediate larger than two: 100")
+(assert_invalid
+  (component
+    (core func (canon context.set i32 100)))
+  "immediate larger than two: 100")
+(assert_malformed
+  (component quote
+    "(core func (canon context.get i64 100))")
+  "expected keyword `i32`")
+(assert_malformed
+  (component quote
+    "(core func (canon context.set i64 100))")
+  "expected keyword `i32`")
+
+(assert_malformed
+  (component binary
+    "\00asm" "\0d\00\01\00" ;; component header
+    "\08\04"                ;; canonicals section, 4 bytes
+    "\01"                   ;; 1 count
+    "\0a\7e\00")            ;; context.get i64 0
+  "invalid leading byte (0x7e) for context.get")
+(assert_malformed
+  (component binary
+    "\00asm" "\0d\00\01\00" ;; component header
+    "\08\04"                ;; canonicals section, 4 bytes
+    "\01"                   ;; 1 count
+    "\0b\7e\00")            ;; context.set i64 0
+  "invalid leading byte (0x7e) for context.set")
+
 ;; different forms of canonical intrinsics
 
 (component
@@ -271,4 +340,10 @@
   (canon error-context.debug-message (memory $m) (realloc $r) (core func))
   (core func (canon error-context.drop))
   (canon error-context.drop (core func))
+
+  (core func (canon context.get i32 0))
+  (canon context.get i32 1 (core func))
+
+  (core func (canon context.set i32 0))
+  (canon context.set i32 1 (core func))
 )
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 b5a83bbf..7ee2dd64 100644
--- a/tests/snapshots/cli/component-model-async/task-builtins.wast.json
+++ b/tests/snapshots/cli/component-model-async/task-builtins.wast.json
@@ -147,9 +147,71 @@
     },
     {
       "type": "module",
-      "line": 217,
+      "line": 216,
       "filename": "task-builtins.22.wasm",
       "module_type": "binary"
+    },
+    {
+      "type": "assert_invalid",
+      "line": 239,
+      "filename": "task-builtins.23.wasm",
+      "module_type": "binary",
+      "text": "found:    [] -> [i32]"
+    },
+    {
+      "type": "assert_invalid",
+      "line": 246,
+      "filename": "task-builtins.24.wasm",
+      "module_type": "binary",
+      "text": "found:    [i32] -> []"
+    },
+    {
+      "type": "assert_invalid",
+      "line": 253,
+      "filename": "task-builtins.25.wasm",
+      "module_type": "binary",
+      "text": "immediate larger than two: 100"
+    },
+    {
+      "type": "assert_invalid",
+      "line": 257,
+      "filename": "task-builtins.26.wasm",
+      "module_type": "binary",
+      "text": "immediate larger than two: 100"
+    },
+    {
+      "type": "assert_malformed",
+      "line": 261,
+      "filename": "task-builtins.27.wat",
+      "module_type": "text",
+      "text": "expected keyword `i32`"
+    },
+    {
+      "type": "assert_malformed",
+      "line": 265,
+      "filename": "task-builtins.28.wat",
+      "module_type": "text",
+      "text": "expected keyword `i32`"
+    },
+    {
+      "type": "assert_malformed",
+      "line": 270,
+      "filename": "task-builtins.29.wasm",
+      "module_type": "binary",
+      "text": "invalid leading byte (0x7e) for context.get"
+    },
+    {
+      "type": "assert_malformed",
+      "line": 277,
+      "filename": "task-builtins.30.wasm",
+      "module_type": "binary",
+      "text": "invalid leading byte (0x7e) for context.set"
+    },
+    {
+      "type": "module",
+      "line": 286,
+      "filename": "task-builtins.31.wasm",
+      "module_type": "binary"
     }
   ]
 }
\ No newline at end of file
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 2816de96..0bacf89e 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,59 +1,24 @@
 (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 $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 (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
+    (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))
     )
   )
-  (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))
 )
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
new file mode 100644
index 00000000..f1fa1237
--- /dev/null
+++ b/tests/snapshots/cli/component-model-async/task-builtins.wast/31.print
@@ -0,0 +1,63 @@
+(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))
+)