Repository: bytecodealliance/wasm-tools
Pull request #2166 (2025-04-28 23:32:00)

wasmparser(CM+GC): Allow declaring a core function type when lowering
When lowering a component function into a core function, allow the declaration of which core function type the lowered function (or lowered CM intrinsic) should have. Right now, this is effectively just double checking that the declared type matches the actual lowered type. After future PRs, this will allow declaring a Wasm GC-using function type, and lowering will adjust accordingly. This also lets the Wasm declare which of the "equivalent" function types it wants to lower to in the case that there are multiple rec groups containing the "same" function type, or in the face of function type subtyping. Again, subsequent PRs will start adding support for rec groups and subtyping and such to CM core types.

alexcrichton: Ah the test failures are something I was slightly worried about where this is peeking for just `core` and not `core type` which is ambiguous with `(core func ...)` and I wasn't sure if we used that anywhere, but looks like we do in `task.return`
fitzgen: Yeah I am renaming the canonical option to `core-type` to work around this

Patch:
diff --git a/crates/wasm-encoder/src/component/canonicals.rs b/crates/wasm-encoder/src/component/canonicals.rs
index acf2713b..5bec980d 100644
--- a/crates/wasm-encoder/src/component/canonicals.rs
+++ b/crates/wasm-encoder/src/component/canonicals.rs
@@ -27,6 +27,8 @@ pub enum CanonicalOption {
     /// The function to use if the async lifting of a function should receive task/stream/future progress events
     /// using a callback.
     Callback(u32),
+    /// The core function type to lower a component function into.
+    CoreType(u32),
 }
 
 impl Encode for CanonicalOption {
@@ -54,6 +56,10 @@ impl Encode for CanonicalOption {
                 sink.push(0x07);
                 idx.encode(sink);
             }
+            Self::CoreType(idx) => {
+                sink.push(0x08);
+                idx.encode(sink);
+            }
         }
     }
 }
diff --git a/crates/wasm-encoder/src/reencode/component.rs b/crates/wasm-encoder/src/reencode/component.rs
index 7fd3578d..4b0ff50f 100644
--- a/crates/wasm-encoder/src/reencode/component.rs
+++ b/crates/wasm-encoder/src/reencode/component.rs
@@ -1367,6 +1367,9 @@ pub mod component_utils {
             wasmparser::CanonicalOption::Callback(u) => {
                 crate::component::CanonicalOption::Callback(reencoder.function_index(u))
             }
+            wasmparser::CanonicalOption::CoreType(u) => {
+                crate::component::CanonicalOption::CoreType(reencoder.type_index(u))
+            }
         }
     }
 
diff --git a/crates/wasmparser/src/readers/component/canonicals.rs b/crates/wasmparser/src/readers/component/canonicals.rs
index 083167cd..6e578938 100644
--- a/crates/wasmparser/src/readers/component/canonicals.rs
+++ b/crates/wasmparser/src/readers/component/canonicals.rs
@@ -28,6 +28,8 @@ pub enum CanonicalOption {
     /// The function to use if the async lifting of a function should receive task/stream/future progress events
     /// using a callback.
     Callback(u32),
+    /// The core function type to lower this component function to.
+    CoreType(u32),
 }
 
 /// Represents a canonical function in a WebAssembly component.
@@ -401,6 +403,7 @@ impl<'a> FromReader<'a> for CanonicalOption {
             0x05 => CanonicalOption::PostReturn(reader.read_var_u32()?),
             0x06 => CanonicalOption::Async,
             0x07 => CanonicalOption::Callback(reader.read_var_u32()?),
+            0x08 => CanonicalOption::CoreType(reader.read_var_u32()?),
             x => return reader.invalid_leading_byte(x, "canonical option"),
         })
     }
diff --git a/crates/wasmparser/src/validator/component.rs b/crates/wasmparser/src/validator/component.rs
index 13267bb7..174c4b7a 100644
--- a/crates/wasmparser/src/validator/component.rs
+++ b/crates/wasmparser/src/validator/component.rs
@@ -238,6 +238,7 @@ pub(crate) struct CanonicalOptions {
     pub(crate) realloc: Option<u32>,
     pub(crate) post_return: Option<u32>,
     pub(crate) concurrency: Concurrency,
+    pub(crate) core_type: Option<CoreTypeId>,
 }
 
 impl CanonicalOptions {
@@ -341,8 +342,50 @@ impl CanonicalOptions {
             }
         }
 
+        if self.core_type.is_some() {
+            bail!(
+                offset,
+                "canonical option `core-type` is not allowed in `canon lift`"
+            )
+        }
+
         Ok(self)
     }
+
+    pub(crate) fn check_core_type(
+        &self,
+        types: &mut TypeAlloc,
+        actual: FuncType,
+        offset: usize,
+    ) -> Result<CoreTypeId> {
+        if let Some(declared_id) = self.core_type {
+            let declared = types[declared_id].unwrap_func();
+
+            if actual.params() != declared.params() {
+                bail!(
+                    offset,
+                    "declared core type has `{:?}` parameter types, but actual lowering has \
+                     `{:?}` parameter types",
+                    declared.params(),
+                    actual.params(),
+                );
+            }
+
+            if actual.results() != declared.results() {
+                bail!(
+                    offset,
+                    "declared core type has `{:?}` result types, but actual lowering has \
+                     `{:?}` result types",
+                    declared.results(),
+                    actual.results(),
+                );
+            }
+
+            Ok(declared_id)
+        } else {
+            Ok(types.intern_func_type(actual, offset))
+        }
+    }
 }
 
 impl ComponentState {
@@ -1185,6 +1228,7 @@ impl ComponentState {
         let options = self.check_options(types, options, offset)?;
         options.check_lift(types, self, core_ty, offset)?;
         let func_ty = ty.lower(types, &options, Abi::Lift, offset)?;
+        debug_assert!(options.core_type.is_none());
         let lowered_core_ty_id = types.intern_func_type(func_ty, offset);
 
         if core_ty_id == lowered_core_ty_id {
@@ -1241,9 +1285,9 @@ impl ComponentState {
         let options = self.check_options(types, options, offset)?;
         options.check_lower(offset)?;
         let func_ty = ty.lower(types, &options, Abi::Lower, offset)?;
-        let core_ty_id = types.intern_func_type(func_ty, offset);
+        let ty_id = options.check_core_type(types, func_ty, offset)?;
 
-        self.core_funcs.push(core_ty_id);
+        self.core_funcs.push(ty_id);
         Ok(())
     }
 
@@ -1349,9 +1393,9 @@ impl ComponentState {
 
         let func_ty = func_ty.lower(types, &options, Abi::Lower, offset)?;
         assert!(func_ty.results().is_empty());
+        let ty_id = options.check_core_type(types, func_ty, offset)?;
 
-        self.core_funcs
-            .push(types.intern_func_type(func_ty, offset));
+        self.core_funcs.push(ty_id);
         Ok(())
     }
 
@@ -1485,13 +1529,18 @@ impl ComponentState {
             bail!(offset, "`stream.read` requires a stream type")
         };
 
-        self.check_options(types, options, offset)?
+        let ty_id = self
+            .check_options(types, options, offset)?
             .require_memory(offset)?
             .require_realloc_if(offset, || elem_ty.is_some_and(|ty| ty.contains_ptr(types)))?
-            .check_lower(offset)?;
+            .check_lower(offset)?
+            .check_core_type(
+                types,
+                FuncType::new([ValType::I32; 3], [ValType::I32]),
+                offset,
+            )?;
 
-        self.core_funcs
-            .push(types.intern_func_type(FuncType::new([ValType::I32; 3], [ValType::I32]), offset));
+        self.core_funcs.push(ty_id);
         Ok(())
     }
 
@@ -1514,12 +1563,17 @@ impl ComponentState {
             bail!(offset, "`stream.write` requires a stream type")
         };
 
-        self.check_options(types, options, offset)?
+        let ty_id = self
+            .check_options(types, options, offset)?
             .require_memory(offset)?
-            .check_lower(offset)?;
+            .check_lower(offset)?
+            .check_core_type(
+                types,
+                FuncType::new([ValType::I32; 3], [ValType::I32]),
+                offset,
+            )?;
 
-        self.core_funcs
-            .push(types.intern_func_type(FuncType::new([ValType::I32; 3], [ValType::I32]), offset));
+        self.core_funcs.push(ty_id);
         Ok(())
     }
 
@@ -1666,13 +1720,18 @@ impl ComponentState {
             bail!(offset, "`future.read` requires a future type")
         };
 
-        self.check_options(types, options, offset)?
+        let ty_id = self
+            .check_options(types, options, offset)?
             .require_memory(offset)?
             .require_realloc_if(offset, || elem_ty.is_some_and(|ty| ty.contains_ptr(types)))?
-            .check_lower(offset)?;
+            .check_lower(offset)?
+            .check_core_type(
+                types,
+                FuncType::new([ValType::I32; 2], [ValType::I32]),
+                offset,
+            )?;
 
-        self.core_funcs
-            .push(types.intern_func_type(FuncType::new([ValType::I32; 2], [ValType::I32]), offset));
+        self.core_funcs.push(ty_id);
         Ok(())
     }
 
@@ -1695,12 +1754,17 @@ impl ComponentState {
             bail!(offset, "`future.write` requires a future type")
         };
 
-        self.check_options(types, &options, offset)?
+        let ty_id = self
+            .check_options(types, &options, offset)?
             .require_memory(offset)?
-            .check_lower(offset)?;
+            .check_lower(offset)?
+            .check_core_type(
+                types,
+                FuncType::new([ValType::I32; 2], [ValType::I32]),
+                offset,
+            )?;
 
-        self.core_funcs
-            .push(types.intern_func_type(FuncType::new([ValType::I32; 2], [ValType::I32]), offset));
+        self.core_funcs.push(ty_id);
         Ok(())
     }
 
@@ -1823,13 +1887,18 @@ impl ComponentState {
             )
         }
 
-        self.check_options(types, &options, offset)?
+        let ty_id = self
+            .check_options(types, &options, offset)?
             .require_memory(offset)?
             .require_sync(offset, "error-context.new")?
-            .check_lower(offset)?;
+            .check_lower(offset)?
+            .check_core_type(
+                types,
+                FuncType::new([ValType::I32; 2], [ValType::I32]),
+                offset,
+            )?;
 
-        self.core_funcs
-            .push(types.intern_func_type(FuncType::new([ValType::I32; 2], [ValType::I32]), offset));
+        self.core_funcs.push(ty_id);
         Ok(())
     }
 
@@ -1846,14 +1915,15 @@ impl ComponentState {
             )
         }
 
-        self.check_options(types, &options, offset)?
+        let ty_id = self
+            .check_options(types, &options, offset)?
             .require_memory(offset)?
             .require_realloc(offset)?
             .require_sync(offset, "error-context.debug-message")?
-            .check_lower(offset)?;
+            .check_lower(offset)?
+            .check_core_type(types, FuncType::new([ValType::I32; 2], []), offset)?;
 
-        self.core_funcs
-            .push(types.intern_func_type(FuncType::new([ValType::I32; 2], []), offset));
+        self.core_funcs.push(ty_id);
         Ok(())
     }
 
@@ -2264,6 +2334,7 @@ impl ComponentState {
                 CanonicalOption::PostReturn(_) => "post-return",
                 CanonicalOption::Async => "async",
                 CanonicalOption::Callback(_) => "callback",
+                CanonicalOption::CoreType(_) => "core type",
             }
         }
 
@@ -2273,6 +2344,7 @@ impl ComponentState {
         let mut post_return = None;
         let mut is_async = false;
         let mut callback = None;
+        let mut core_type = None;
 
         for option in options {
             match option {
@@ -2366,6 +2438,43 @@ impl ComponentState {
                         }
                     }
                 }
+                CanonicalOption::CoreType(idx) => {
+                    core_type = match core_type {
+                        None => {
+                            if !self.features.cm_gc() {
+                                bail!(
+                                    offset,
+                                    "canonical option `core type` requires the component model gc feature"
+                                )
+                            }
+                            let ty = match self.core_type_at(*idx, offset)? {
+                                ComponentCoreTypeId::Sub(ty) => ty,
+                                ComponentCoreTypeId::Module(_) => return Err(BinaryReaderError::new(
+                                    "canonical option `core type` must reference a core function \
+                                     type",
+                                    offset,
+                                )),
+                            };
+                            match &types[ty].composite_type.inner {
+                                CompositeInnerType::Func(_) => {}
+                                CompositeInnerType::Array(_) |
+                                CompositeInnerType::Struct(_) |
+                                CompositeInnerType::Cont(_) => return Err(BinaryReaderError::new(
+                                    "canonical option `core type` must reference a core function \
+                                     type",
+                                    offset,
+                                )),
+                            }
+                            Some(ty)
+                        }
+                        Some(_) => {
+                            return Err(BinaryReaderError::new(
+                                "canonical option `core type` is specified more than once",
+                                offset,
+                            ))
+                        }
+                    };
+                }
             }
         }
 
@@ -2385,6 +2494,7 @@ impl ComponentState {
             realloc,
             post_return,
             concurrency,
+            core_type,
         })
     }
 
diff --git a/crates/wasmprinter/src/component.rs b/crates/wasmprinter/src/component.rs
index a834a10c..eee550a7 100644
--- a/crates/wasmprinter/src/component.rs
+++ b/crates/wasmprinter/src/component.rs
@@ -830,6 +830,11 @@ impl Printer<'_, '_> {
                     self.print_idx(&state.core.func_names, *idx)?;
                     self.end_group()?;
                 }
+                CanonicalOption::CoreType(idx) => {
+                    self.start_group("core-type ")?;
+                    self.print_idx(&state.core.type_names, *idx)?;
+                    self.end_group()?;
+                }
             }
         }
         Ok(())
diff --git a/crates/wast/src/component/binary.rs b/crates/wast/src/component/binary.rs
index b80455f6..21313521 100644
--- a/crates/wast/src/component/binary.rs
+++ b/crates/wast/src/component/binary.rs
@@ -975,6 +975,7 @@ impl From<&CanonOpt<'_>> for wasm_encoder::CanonicalOption {
             CanonOpt::PostReturn(f) => Self::PostReturn(f.idx.into()),
             CanonOpt::Async => Self::Async,
             CanonOpt::Callback(f) => Self::Callback(f.idx.into()),
+            CanonOpt::CoreType(t) => Self::CoreType(t.idx.into()),
         }
     }
 }
diff --git a/crates/wast/src/component/func.rs b/crates/wast/src/component/func.rs
index f35a9f5a..900fd151 100644
--- a/crates/wast/src/component/func.rs
+++ b/crates/wast/src/component/func.rs
@@ -949,6 +949,14 @@ pub enum CanonOpt<'a> {
     Async,
     /// Use the specified function to deliver async events to stackless coroutines.
     Callback(CoreItemRef<'a, kw::func>),
+    /// Lower this component function into the specified core function type.
+    CoreType(CoreItemRef<'a, kw::r#type>),
+}
+
+impl Default for kw::r#type {
+    fn default() -> Self {
+        Self(Span::from_offset(0))
+    }
 }
 
 impl<'a> Parse<'a> for CanonOpt<'a> {
@@ -986,6 +994,11 @@ impl<'a> Parse<'a> for CanonOpt<'a> {
                     Ok(CanonOpt::Callback(
                         parser.parse::<IndexOrCoreRef<'_, _>>()?.0,
                     ))
+                } else if l.peek::<kw::core_type>()? {
+                    parser.parse::<kw::core_type>()?;
+                    Ok(CanonOpt::CoreType(
+                        parser.parse::<IndexOrCoreRef<'_, _>>()?.0,
+                    ))
                 } else {
                     Err(l.error())
                 }
@@ -1008,6 +1021,7 @@ impl Peek for CanonOpt<'_> {
                         || kw::realloc::peek(next)?
                         || kw::post_return::peek(next)?
                         || kw::callback::peek(next)?
+                        || kw::core_type::peek(next)?
                 }
                 None => false,
             })
diff --git a/crates/wast/src/component/resolve.rs b/crates/wast/src/component/resolve.rs
index 95ef0ef6..67b5cf65 100644
--- a/crates/wast/src/component/resolve.rs
+++ b/crates/wast/src/component/resolve.rs
@@ -486,6 +486,7 @@ impl<'a> Resolver<'a> {
                 CanonOpt::Realloc(r) | CanonOpt::PostReturn(r) | CanonOpt::Callback(r) => {
                     self.core_item_ref(r)?
                 }
+                CanonOpt::CoreType(t) => self.core_item_ref(t)?,
             }
         }
 
diff --git a/crates/wast/src/lib.rs b/crates/wast/src/lib.rs
index cb4a1771..366c3847 100644
--- a/crates/wast/src/lib.rs
+++ b/crates/wast/src/lib.rs
@@ -544,6 +544,7 @@ pub mod kw {
     custom_keyword!(post_return = "post-return");
     custom_keyword!(with);
     custom_keyword!(core);
+    custom_keyword!(core_type = "core-type");
     custom_keyword!(true_ = "true");
     custom_keyword!(false_ = "false");
     custom_keyword!(language);


Test patch:
diff --git a/tests/cli/component-model-gc/core-type.wat b/tests/cli/component-model-gc/core-type.wat
new file mode 100644
index 00000000..b5e84874
--- /dev/null
+++ b/tests/cli/component-model-gc/core-type.wat
@@ -0,0 +1,41 @@
+;; RUN: wast --assert default --snapshot tests/snapshots % -f gc,cm-gc
+
+(component
+  (import "f" (func $f (param "x" u32) (param "y" u32) (result u32)))
+  (core type $ty (func (param i32 i32) (result i32)))
+  (core func (canon lower (func $f) (core-type $ty)))
+)
+
+(assert_invalid
+  (component
+    (core module $m
+      (memory (export "memory") 1)
+      (func (export "f") (result i32) unreachable)
+    )
+    (core instance $i (instantiate $m))
+
+    (core type $ty (func (result i32)))
+    (func (export "f") (result u32)
+      (canon lift (core func $i "f") (core-type $ty))
+    )
+  )
+  "canonical option `core-type` is not allowed in `canon lift`"
+)
+
+(assert_invalid
+  (component
+    (import "f" (func $f (param "x" u32) (param "y" u32) (result u32)))
+    (core type $ty (func (param i64 i32) (result i32)))
+    (core func (canon lower (func $f) (core-type $ty)))
+  )
+  "declared core type has `[I64, I32]` parameter types, but actual lowering has `[I32, I32]` parameter types"
+)
+
+(assert_invalid
+  (component
+    (import "f" (func $f (param "x" u32) (param "y" u32) (result u32)))
+    (core type $ty (func (param i32 i32) (result i64)))
+    (core func (canon lower (func $f) (core-type $ty)))
+  )
+  "declared core type has `[I64]` result types, but actual lowering has `[I32]` result types"
+)
diff --git a/tests/snapshots/cli/component-model-gc/core-type.wat.json b/tests/snapshots/cli/component-model-gc/core-type.wat.json
new file mode 100644
index 00000000..b47108bb
--- /dev/null
+++ b/tests/snapshots/cli/component-model-gc/core-type.wat.json
@@ -0,0 +1,32 @@
+{
+  "source_filename": "tests/cli/component-model-gc/core-type.wat",
+  "commands": [
+    {
+      "type": "module",
+      "line": 3,
+      "filename": "core-type.0.wasm",
+      "module_type": "binary"
+    },
+    {
+      "type": "assert_invalid",
+      "line": 10,
+      "filename": "core-type.1.wasm",
+      "module_type": "binary",
+      "text": "canonical option `core-type` is not allowed in `canon lift`"
+    },
+    {
+      "type": "assert_invalid",
+      "line": 26,
+      "filename": "core-type.2.wasm",
+      "module_type": "binary",
+      "text": "declared core type has `[I64, I32]` parameter types, but actual lowering has `[I32, I32]` parameter types"
+    },
+    {
+      "type": "assert_invalid",
+      "line": 35,
+      "filename": "core-type.3.wasm",
+      "module_type": "binary",
+      "text": "declared core type has `[I64]` result types, but actual lowering has `[I32]` result types"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/tests/snapshots/cli/component-model-gc/core-type.wat/0.print b/tests/snapshots/cli/component-model-gc/core-type.wat/0.print
new file mode 100644
index 00000000..b38ce886
--- /dev/null
+++ b/tests/snapshots/cli/component-model-gc/core-type.wat/0.print
@@ -0,0 +1,6 @@
+(component
+  (type (;0;) (func (param "x" u32) (param "y" u32) (result u32)))
+  (import "f" (func $f (;0;) (type 0)))
+  (core type $ty (;0;) (func (param i32 i32) (result i32)))
+  (core func (;0;) (canon lower (func $f) (core-type $ty)))
+)