From 04b6bade763ce2045139accf306a217524fbe9ef Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Tue, 23 Dec 2025 02:49:23 -0500 Subject: [PATCH 01/26] Add support for map type Signed-off-by: Yordis Prieto --- crates/c-api/CMakeLists.txt | 8 +- .../include/wasmtime/component/types/val.h | 43 +++ .../include/wasmtime/component/types/val.hh | 40 ++ crates/c-api/include/wasmtime/component/val.h | 16 + crates/c-api/src/component/types/val.rs | 29 ++ crates/c-api/src/component/val.rs | 42 +++ crates/c-api/tests/component/types.cc | 9 + crates/cli-flags/src/lib.rs | 3 + crates/environ/src/component/types.rs | 18 +- crates/environ/src/component/types_builder.rs | 41 ++- crates/environ/src/fact/trampoline.rs | 277 +++++++++++++- crates/fuzzing/src/generators/config.rs | 3 + crates/fuzzing/src/generators/module.rs | 2 + crates/fuzzing/src/oracles/component_api.rs | 9 +- crates/test-util/src/component.rs | 1 + crates/test-util/src/component_fuzz.rs | 60 ++- crates/test-util/src/wasmtime_wast.rs | 3 + crates/test-util/src/wast.rs | 1 + crates/wasmtime/src/config.rs | 11 + .../src/runtime/component/func/typed.rs | 1 + .../wasmtime/src/runtime/component/types.rs | 45 ++- .../wasmtime/src/runtime/component/values.rs | 136 ++++++- crates/wasmtime/src/runtime/wave/component.rs | 11 +- crates/wast/src/component.rs | 1 + crates/wit-bindgen/src/lib.rs | 22 +- crates/wit-bindgen/src/rust.rs | 8 +- crates/wit-bindgen/src/types.rs | 6 +- tests/all/component_model/dynamic.rs | 341 ++++++++++++++++++ tests/all/component_model/func.rs | 281 +++++++++++++++ .../misc_testsuite/component-model/types.wast | 6 + 30 files changed, 1446 insertions(+), 28 deletions(-) diff --git a/crates/c-api/CMakeLists.txt b/crates/c-api/CMakeLists.txt index 3ca05cd48cb5..a6e0bd01641f 100644 --- a/crates/c-api/CMakeLists.txt +++ b/crates/c-api/CMakeLists.txt @@ -32,7 +32,13 @@ else() endif() endif() -set(WASMTIME_TARGET_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../target/${WASMTIME_TARGET}/${WASMTIME_BUILD_TYPE}) +# Respect CARGO_TARGET_DIR if set, allowing users to customize where Cargo +# outputs build artifacts +if(DEFINED ENV{CARGO_TARGET_DIR}) + set(WASMTIME_TARGET_DIR $ENV{CARGO_TARGET_DIR}/${WASMTIME_TARGET}/${WASMTIME_BUILD_TYPE}) +else() + set(WASMTIME_TARGET_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../target/${WASMTIME_TARGET}/${WASMTIME_BUILD_TYPE}) +endif() if(WASMTIME_TARGET MATCHES "apple") set(WASMTIME_SHARED_FILES libwasmtime.dylib) diff --git a/crates/c-api/include/wasmtime/component/types/val.h b/crates/c-api/include/wasmtime/component/types/val.h index 85301056ad6f..d7d9d009cfc2 100644 --- a/crates/c-api/include/wasmtime/component/types/val.h +++ b/crates/c-api/include/wasmtime/component/types/val.h @@ -335,6 +335,43 @@ WASM_API_EXTERN bool wasmtime_component_stream_type_ty( const wasmtime_component_stream_type_t *ty, struct wasmtime_component_valtype_t *type_ret); +// ----------- maps ------------------------------------------------------------ + +/// \brief Opaque type representing a component map type. +typedef struct wasmtime_component_map_type wasmtime_component_map_type_t; + +/// \brief Clones a component map type. +/// +/// The returned pointer must be deallocated with +/// `wasmtime_component_map_type_delete`. +WASM_API_EXTERN wasmtime_component_map_type_t * +wasmtime_component_map_type_clone(const wasmtime_component_map_type_t *ty); + +/// \brief Compares two component map types for equality. +WASM_API_EXTERN bool +wasmtime_component_map_type_equal(const wasmtime_component_map_type_t *a, + const wasmtime_component_map_type_t *b); + +/// \brief Deallocates a component map type. +WASM_API_EXTERN void +wasmtime_component_map_type_delete(wasmtime_component_map_type_t *ptr); + +/// \brief Returns the key type of a component map type. +/// +/// The returned type must be deallocated with +/// `wasmtime_component_valtype_delete`. +WASM_API_EXTERN void +wasmtime_component_map_type_key(const wasmtime_component_map_type_t *ty, + struct wasmtime_component_valtype_t *type_ret); + +/// \brief Returns the value type of a component map type. +/// +/// The returned type must be deallocated with +/// `wasmtime_component_valtype_delete`. +WASM_API_EXTERN void wasmtime_component_map_type_value( + const wasmtime_component_map_type_t *ty, + struct wasmtime_component_valtype_t *type_ret); + // ----------- valtype --------------------------------------------------------- /// \brief Value of #wasmtime_component_valtype_kind_t meaning that @@ -415,6 +452,9 @@ WASM_API_EXTERN bool wasmtime_component_stream_type_ty( /// \brief Value of #wasmtime_component_valtype_kind_t meaning that /// #wasmtime_component_valtype_t is an `error context` WIT type. #define WASMTIME_COMPONENT_VALTYPE_ERROR_CONTEXT 25 +/// \brief Value of #wasmtime_component_valtype_kind_t meaning that +/// #wasmtime_component_valtype_t is a `map` WIT type. +#define WASMTIME_COMPONENT_VALTYPE_MAP 26 /// \brief Discriminant used in #wasmtime_component_valtype_t::kind typedef uint8_t wasmtime_component_valtype_kind_t; @@ -457,6 +497,9 @@ typedef union wasmtime_component_valtype_union { /// Field used if #wasmtime_component_valtype_t::kind is /// #WASMTIME_COMPONENT_VALTYPE_STREAM wasmtime_component_stream_type_t *stream; + /// Field used if #wasmtime_component_valtype_t::kind is + /// #WASMTIME_COMPONENT_VALTYPE_MAP + wasmtime_component_map_type_t *map; } wasmtime_component_valtype_union_t; /// \brief Represents a single value type in the component model. diff --git a/crates/c-api/include/wasmtime/component/types/val.hh b/crates/c-api/include/wasmtime/component/types/val.hh index 8cf31b62f1c4..6a93b3de5c3a 100644 --- a/crates/c-api/include/wasmtime/component/types/val.hh +++ b/crates/c-api/include/wasmtime/component/types/val.hh @@ -178,6 +178,19 @@ class StreamType { std::optional ty() const; }; +/** + * \brief Represents a component map type. + */ +class MapType { + WASMTIME_CLONE_EQUAL_WRAPPER(MapType, wasmtime_component_map_type); + + /// Returns the key type of this map type. + ValType key() const; + + /// Returns the value type of this map type. + ValType value() const; +}; + /** * \brief Represents a component value type. */ @@ -382,6 +395,12 @@ public: ty.of.stream = stream.capi_release(); } + /// Creates a map value type. + ValType(MapType map) { + ty.kind = WASMTIME_COMPONENT_VALTYPE_MAP; + ty.of.map = map.capi_release(); + } + /// Returns the kind of this value type. wasmtime_component_valtype_kind_t kind() const { return ty.kind; } @@ -481,6 +500,9 @@ public: return ty.kind == WASMTIME_COMPONENT_VALTYPE_ERROR_CONTEXT; } + /// Returns true if this is a map type. + bool is_map() const { return ty.kind == WASMTIME_COMPONENT_VALTYPE_MAP; } + /// Returns the list type, asserting that this is indeed a list. const ListType &list() const { assert(is_list()); @@ -553,6 +575,12 @@ public: return *StreamType::from_capi(&ty.of.stream); } + /// Returns the map type, asserting that this is indeed a map. + const MapType &map() const { + assert(is_map()); + return *MapType::from_capi(&ty.of.map); + } + /// \brief Returns the underlying C API pointer. const wasmtime_component_valtype_t *capi() const { return &ty; } /// \brief Returns the underlying C API pointer. @@ -640,6 +668,18 @@ inline std::optional StreamType::ty() const { return std::nullopt; } +inline ValType MapType::key() const { + wasmtime_component_valtype_t type_ret; + wasmtime_component_map_type_key(ptr.get(), &type_ret); + return ValType(std::move(type_ret)); +} + +inline ValType MapType::value() const { + wasmtime_component_valtype_t type_ret; + wasmtime_component_map_type_value(ptr.get(), &type_ret); + return ValType(std::move(type_ret)); +} + } // namespace component } // namespace wasmtime diff --git a/crates/c-api/include/wasmtime/component/val.h b/crates/c-api/include/wasmtime/component/val.h index 2021651273f9..c6a31469f7b2 100644 --- a/crates/c-api/include/wasmtime/component/val.h +++ b/crates/c-api/include/wasmtime/component/val.h @@ -266,9 +266,13 @@ typedef uint8_t wasmtime_component_valkind_t; /// \brief Value of #wasmtime_component_valkind_t meaning that /// #wasmtime_component_val_t is a resource #define WASMTIME_COMPONENT_RESOURCE 21 +/// \brief Value of #wasmtime_component_valkind_t meaning that +/// #wasmtime_component_val_t is a map +#define WASMTIME_COMPONENT_MAP 22 struct wasmtime_component_val; struct wasmtime_component_valrecord_entry; +struct wasmtime_component_valmap_entry; #define DECLARE_VEC(name, type) \ /** \brief A vec of a type */ \ @@ -296,6 +300,7 @@ DECLARE_VEC(wasmtime_component_valrecord, struct wasmtime_component_valrecord_entry) DECLARE_VEC(wasmtime_component_valtuple, struct wasmtime_component_val) DECLARE_VEC(wasmtime_component_valflags, wasm_name_t) +DECLARE_VEC(wasmtime_component_valmap, struct wasmtime_component_valmap_entry) #undef DECLARE_VEC @@ -366,6 +371,8 @@ typedef union { wasmtime_component_valresult_t result; /// Field used if #wasmtime_component_val_t::kind is #WASMTIME_COMPONENT_FLAGS wasmtime_component_valflags_t flags; + /// Field used if #wasmtime_component_val_t::kind is #WASMTIME_COMPONENT_MAP + wasmtime_component_valmap_t map; /// Field used if #wasmtime_component_val_t::kind is /// #WASMTIME_COMPONENT_RESOURCE wasmtime_component_resource_any_t *resource; @@ -389,6 +396,15 @@ typedef struct wasmtime_component_valrecord_entry { wasmtime_component_val_t val; } wasmtime_component_valrecord_entry_t; +/// \brief A pair of a key and a value that represents one entry in a value +/// with kind #WASMTIME_COMPONENT_MAP +typedef struct wasmtime_component_valmap_entry { + /// The key of this entry + wasmtime_component_val_t key; + /// The value of this entry + wasmtime_component_val_t value; +} wasmtime_component_valmap_entry_t; + /// \brief Allocates a new `wasmtime_component_val_t` on the heap, initializing /// it with the contents of `val`. /// diff --git a/crates/c-api/src/component/types/val.rs b/crates/c-api/src/component/types/val.rs index b705ada33031..c3e4f1630bb0 100644 --- a/crates/c-api/src/component/types/val.rs +++ b/crates/c-api/src/component/types/val.rs @@ -31,6 +31,7 @@ pub enum wasmtime_component_valtype_t { Future(Box), Stream(Box), ErrorContext, + Map(Box), } impl From for wasmtime_component_valtype_t { @@ -61,6 +62,7 @@ impl From for wasmtime_component_valtype_t { Type::Borrow(ty) => Self::Borrow(Box::new(ty.into())), Type::Future(ty) => Self::Future(Box::new(ty.into())), Type::Stream(ty) => Self::Stream(Box::new(ty.into())), + Type::Map(ty) => Self::Map(Box::new(ty.into())), Type::ErrorContext => Self::ErrorContext, } } @@ -108,6 +110,33 @@ pub extern "C" fn wasmtime_component_list_type_element( type_ret.write(ty.ty.ty().into()); } +type_wrapper! { + #[derive(PartialEq)] + pub struct wasmtime_component_map_type_t { + pub(crate) ty: Map, + } + + clone: wasmtime_component_map_type_clone, + delete: wasmtime_component_map_type_delete, + equal: wasmtime_component_map_type_equal, +} + +#[unsafe(no_mangle)] +pub extern "C" fn wasmtime_component_map_type_key( + ty: &wasmtime_component_map_type_t, + type_ret: &mut MaybeUninit, +) { + type_ret.write(ty.ty.key().into()); +} + +#[unsafe(no_mangle)] +pub extern "C" fn wasmtime_component_map_type_value( + ty: &wasmtime_component_map_type_t, + type_ret: &mut MaybeUninit, +) { + type_ret.write(ty.ty.value().into()); +} + type_wrapper! { #[derive(PartialEq)] pub struct wasmtime_component_record_type_t { diff --git a/crates/c-api/src/component/val.rs b/crates/c-api/src/component/val.rs index 00c2c6a63963..274de0a8f35d 100644 --- a/crates/c-api/src/component/val.rs +++ b/crates/c-api/src/component/val.rs @@ -45,6 +45,15 @@ crate::declare_vecs! { copy: wasmtime_component_valflags_copy, delete: wasmtime_component_valflags_delete, ) + ( + name: wasmtime_component_valmap_t, + ty: wasmtime_component_valmap_entry_t, + new: wasmtime_component_valmap_new, + empty: wasmtime_component_valmap_new_empty, + uninit: wasmtime_component_valmap_new_uninit, + copy: wasmtime_component_valmap_copy, + delete: wasmtime_component_valmap_delete, + ) } impl From<&wasmtime_component_vallist_t> for Vec { @@ -63,6 +72,29 @@ impl From<&[Val]> for wasmtime_component_vallist_t { } } +impl From<&wasmtime_component_valmap_t> for Vec<(Val, Val)> { + fn from(value: &wasmtime_component_valmap_t) -> Self { + value + .as_slice() + .iter() + .map(|entry| (Val::from(&entry.key), Val::from(&entry.value))) + .collect() + } +} + +impl From<&[(Val, Val)]> for wasmtime_component_valmap_t { + fn from(value: &[(Val, Val)]) -> Self { + value + .iter() + .map(|(k, v)| wasmtime_component_valmap_entry_t { + key: wasmtime_component_val_t::from(k), + value: wasmtime_component_val_t::from(v), + }) + .collect::>() + .into() + } +} + #[derive(Clone)] #[repr(C)] pub struct wasmtime_component_valrecord_entry_t { @@ -70,6 +102,13 @@ pub struct wasmtime_component_valrecord_entry_t { val: wasmtime_component_val_t, } +#[derive(Clone, Default)] +#[repr(C)] +pub struct wasmtime_component_valmap_entry_t { + key: wasmtime_component_val_t, + value: wasmtime_component_val_t, +} + impl Default for wasmtime_component_valrecord_entry_t { fn default() -> Self { Self { @@ -233,6 +272,7 @@ pub enum wasmtime_component_val_t { Option(Option>), Result(wasmtime_component_valresult_t), Flags(wasmtime_component_valflags_t), + Map(wasmtime_component_valmap_t), Resource(Box), } @@ -275,6 +315,7 @@ impl From<&wasmtime_component_val_t> for Val { } wasmtime_component_val_t::Result(x) => Val::Result(x.into()), wasmtime_component_val_t::Flags(x) => Val::Flags(x.into()), + wasmtime_component_val_t::Map(x) => Val::Map(x.into()), wasmtime_component_val_t::Resource(x) => Val::Resource(x.resource), } } @@ -309,6 +350,7 @@ impl From<&Val> for wasmtime_component_val_t { ), Val::Result(x) => wasmtime_component_val_t::Result(x.into()), Val::Flags(x) => wasmtime_component_val_t::Flags(x.as_slice().into()), + Val::Map(x) => wasmtime_component_val_t::Map(x.as_slice().into()), Val::Resource(resource_any) => { wasmtime_component_val_t::Resource(Box::new(wasmtime_component_resource_any_t { resource: *resource_any, diff --git a/crates/c-api/tests/component/types.cc b/crates/c-api/tests/component/types.cc index dcef68dfd8b0..5fc5dc25e578 100644 --- a/crates/c-api/tests/component/types.cc +++ b/crates/c-api/tests/component/types.cc @@ -187,6 +187,15 @@ TEST(types, valtype_list) { EXPECT_TRUE(elem.is_u8()); } +TEST(types, valtype_map) { + auto ty = + result("(component (import \"f\" (func (result (map u32 string)))))"); + EXPECT_TRUE(ty.is_map()); + auto map_ty = ty.map(); + EXPECT_TRUE(map_ty.key().is_u32()); + EXPECT_TRUE(map_ty.value().is_string()); +} + TEST(types, valtype_record) { auto ty = result(R"( (component diff --git a/crates/cli-flags/src/lib.rs b/crates/cli-flags/src/lib.rs index 148676975c70..a966f53f639f 100644 --- a/crates/cli-flags/src/lib.rs +++ b/crates/cli-flags/src/lib.rs @@ -393,6 +393,8 @@ wasmtime_option_group! { /// GC support in the component model: this corresponds to the 🛸 emoji /// in the component model specification. pub component_model_gc: Option, + /// Map support in the component model. + pub component_model_map: Option, /// Configure support for the function-references proposal. pub function_references: Option, /// Configure support for the stack-switching proposal. @@ -1137,6 +1139,7 @@ impl CommonOptions { ("component-model-async", component_model_async_stackful, wasm_component_model_async_stackful) ("component-model-async", component_model_threading, wasm_component_model_threading) ("component-model", component_model_error_context, wasm_component_model_error_context) + ("component-model", component_model_map, wasm_component_model_map) ("component-model", component_model_fixed_length_lists, wasm_component_model_fixed_length_lists) ("threads", threads, wasm_threads) ("gc", gc, wasm_gc) diff --git a/crates/environ/src/component/types.rs b/crates/environ/src/component/types.rs index 0f1a811a0d0f..5e7e056f3bc2 100644 --- a/crates/environ/src/component/types.rs +++ b/crates/environ/src/component/types.rs @@ -89,6 +89,8 @@ indices! { pub struct TypeResultIndex(u32); /// Index pointing to a list type in the component model. pub struct TypeListIndex(u32); + /// Index pointing to a map type in the component model. + pub struct TypeMapIndex(u32); /// Index pointing to a fixed size list type in the component model. pub struct TypeFixedLengthListIndex(u32); /// Index pointing to a future type in the component model. @@ -283,6 +285,7 @@ pub struct ComponentTypes { pub(super) component_instances: PrimaryMap, pub(super) functions: PrimaryMap, pub(super) lists: PrimaryMap, + pub(super) maps: PrimaryMap, pub(super) records: PrimaryMap, pub(super) variants: PrimaryMap, pub(super) tuples: PrimaryMap, @@ -363,7 +366,9 @@ impl ComponentTypes { &CanonicalAbiInfo::SCALAR8 } - InterfaceType::String | InterfaceType::List(_) => &CanonicalAbiInfo::POINTER_PAIR, + InterfaceType::String | InterfaceType::List(_) | InterfaceType::Map(_) => { + &CanonicalAbiInfo::POINTER_PAIR + } InterfaceType::Record(i) => &self[*i].abi, InterfaceType::Variant(i) => &self[*i].abi, @@ -416,6 +421,7 @@ impl_index! { impl Index for ComponentTypes { TypeOption => options } impl Index for ComponentTypes { TypeResult => results } impl Index for ComponentTypes { TypeList => lists } + impl Index for ComponentTypes { TypeMap => maps } impl Index for ComponentTypes { TypeResourceTable => resource_tables } impl Index for ComponentTypes { TypeFuture => futures } impl Index for ComponentTypes { TypeStream => streams } @@ -591,6 +597,7 @@ pub enum InterfaceType { Variant(TypeVariantIndex), List(TypeListIndex), Tuple(TypeTupleIndex), + Map(TypeMapIndex), Flags(TypeFlagsIndex), Enum(TypeEnumIndex), Option(TypeOptionIndex), @@ -1181,6 +1188,15 @@ pub struct TypeList { pub element: InterfaceType, } +/// Shape of a "map" interface type. +#[derive(Serialize, Deserialize, Clone, Hash, Eq, PartialEq, Debug)] +pub struct TypeMap { + /// The key type of the map. + pub key: InterfaceType, + /// The value type of the map. + pub value: InterfaceType, +} + /// Shape of a "fixed size list" interface type. #[derive(Serialize, Deserialize, Clone, Hash, Eq, PartialEq, Debug)] pub struct TypeFixedLengthList { diff --git a/crates/environ/src/component/types_builder.rs b/crates/environ/src/component/types_builder.rs index 97df81cb3a33..d35280856d7a 100644 --- a/crates/environ/src/component/types_builder.rs +++ b/crates/environ/src/component/types_builder.rs @@ -38,6 +38,7 @@ const MAX_TYPE_DEPTH: u32 = 100; pub struct ComponentTypesBuilder { functions: HashMap, lists: HashMap, + maps: HashMap, records: HashMap, variants: HashMap, tuples: HashMap, @@ -103,6 +104,7 @@ impl ComponentTypesBuilder { functions: HashMap::default(), lists: HashMap::default(), + maps: HashMap::default(), records: HashMap::default(), variants: HashMap::default(), tuples: HashMap::default(), @@ -441,6 +443,9 @@ impl ComponentTypesBuilder { InterfaceType::Variant(self.variant_type(types, e)?) } ComponentDefinedType::List(e) => InterfaceType::List(self.list_type(types, e)?), + ComponentDefinedType::Map(key, value) => { + InterfaceType::Map(self.map_type(types, key, value)?) + } ComponentDefinedType::Tuple(e) => InterfaceType::Tuple(self.tuple_type(types, e)?), ComponentDefinedType::Flags(e) => InterfaceType::Flags(self.flags_type(e)), ComponentDefinedType::Enum(e) => InterfaceType::Enum(self.enum_type(e)), @@ -461,9 +466,6 @@ impl ComponentTypesBuilder { ComponentDefinedType::FixedLengthList(ty, size) => { InterfaceType::FixedLengthList(self.fixed_length_list_type(types, ty, *size)?) } - ComponentDefinedType::Map(..) => { - bail!("support not implemented for map type"); - } }; let info = self.type_information(&ret); if info.depth > MAX_TYPE_DEPTH { @@ -690,6 +692,21 @@ impl ComponentTypesBuilder { Ok(self.add_list_type(TypeList { element })) } + fn map_type( + &mut self, + types: TypesRef<'_>, + key: &ComponentValType, + value: &ComponentValType, + ) -> Result { + assert_eq!(types.id(), self.module_types.validator_id()); + let key_ty = self.valtype(types, key)?; + let value_ty = self.valtype(types, value)?; + Ok(self.add_map_type(TypeMap { + key: key_ty, + value: value_ty, + })) + } + /// Converts a wasmparser `id`, which must point to a resource, to its /// corresponding `TypeResourceTableIndex`. pub fn resource_id(&mut self, id: ResourceId) -> TypeResourceTableIndex { @@ -749,6 +766,11 @@ impl ComponentTypesBuilder { intern_and_fill_flat_types!(self, lists, ty) } + /// Interns a new map type within this type information. + pub fn add_map_type(&mut self, ty: TypeMap) -> TypeMapIndex { + intern_and_fill_flat_types!(self, maps, ty) + } + /// Interns a new future type within this type information. pub fn add_future_type(&mut self, ty: TypeFuture) -> TypeFutureIndex { intern(&mut self.futures, &mut self.component_types.futures, ty) @@ -852,6 +874,7 @@ impl ComponentTypesBuilder { } InterfaceType::List(i) => &self.type_info.lists[*i], + InterfaceType::Map(i) => &self.type_info.maps[*i], InterfaceType::Record(i) => &self.type_info.records[*i], InterfaceType::Variant(i) => &self.type_info.variants[*i], InterfaceType::Tuple(i) => &self.type_info.tuples[*i], @@ -969,6 +992,7 @@ struct TypeInformationCache { options: PrimaryMap, results: PrimaryMap, lists: PrimaryMap, + maps: PrimaryMap, fixed_length_lists: PrimaryMap, } @@ -1182,4 +1206,15 @@ impl TypeInformation { self.depth += info.depth; self.has_borrow = info.has_borrow; } + + fn maps(&mut self, types: &ComponentTypesBuilder, ty: &TypeMap) { + // Maps are represented as list> in canonical ABI + // So we use POINTER_PAIR like lists, and calculate depth/borrow from key and value + *self = TypeInformation::string(); + let key_info = types.type_information(&ty.key); + let value_info = types.type_information(&ty.value); + // Depth is max of key/value depths, plus 1 for tuple, plus 1 for list + self.depth = key_info.depth.max(value_info.depth) + 2; + self.has_borrow = key_info.has_borrow || value_info.has_borrow; + } } diff --git a/crates/environ/src/fact/trampoline.rs b/crates/environ/src/fact/trampoline.rs index 8757bcb26aef..950c68102995 100644 --- a/crates/environ/src/fact/trampoline.rs +++ b/crates/environ/src/fact/trampoline.rs @@ -20,9 +20,9 @@ use crate::component::{ InterfaceType, MAX_FLAT_ASYNC_PARAMS, MAX_FLAT_PARAMS, PREPARE_ASYNC_NO_RESULT, PREPARE_ASYNC_WITH_RESULT, START_FLAG_ASYNC_CALLEE, StringEncoding, Transcode, TypeComponentLocalErrorContextTableIndex, TypeEnumIndex, TypeFixedLengthListIndex, - TypeFlagsIndex, TypeFutureTableIndex, TypeListIndex, TypeOptionIndex, TypeRecordIndex, - TypeResourceTableIndex, TypeResultIndex, TypeStreamTableIndex, TypeTupleIndex, - TypeVariantIndex, VariantInfo, + TypeFlagsIndex, TypeFutureTableIndex, TypeListIndex, TypeMapIndex, TypeOptionIndex, + TypeRecordIndex, TypeResourceTableIndex, TypeResultIndex, TypeStreamTableIndex, + TypeTupleIndex, TypeVariantIndex, VariantInfo, }; use crate::fact::signature::Signature; use crate::fact::transcode::Transcoder; @@ -1146,6 +1146,8 @@ impl<'a, 'b> Compiler<'a, 'b> { // Iteration of a loop is along the lines of the cost of a string // so give it the same cost InterfaceType::List(_) => 40, + // Maps are similar to lists in terms of iteration cost + InterfaceType::Map(_) => 40, InterfaceType::Flags(i) => { let count = self.module.types[*i].names.len(); @@ -1196,6 +1198,7 @@ impl<'a, 'b> Compiler<'a, 'b> { InterfaceType::Char => self.translate_char(src, dst_ty, dst), InterfaceType::String => self.translate_string(src, dst_ty, dst), InterfaceType::List(t) => self.translate_list(*t, src, dst_ty, dst), + InterfaceType::Map(t) => self.translate_map(*t, src, dst_ty, dst), InterfaceType::Record(t) => self.translate_record(*t, src, dst_ty, dst), InterfaceType::Flags(f) => self.translate_flags(*f, src, dst_ty, dst), InterfaceType::Tuple(t) => self.translate_tuple(*t, src, dst_ty, dst), @@ -2676,6 +2679,274 @@ impl<'a, 'b> Compiler<'a, 'b> { self.free_temp_local(dst_mem.addr); } + /// Translates a map from one component's memory to another. + /// + /// In the Component Model, a `map` is stored in memory as `list>`. + /// The memory layout is: + /// ```text + /// [pointer to data, length] + /// | + /// v + /// [key1, value1, key2, value2, key3, value3, ...] + /// ``` + /// + /// This function copies each key-value pair from source to destination, + /// potentially converting types along the way. + fn translate_map( + &mut self, + src_ty: TypeMapIndex, + src: &Source<'_>, + dst_ty: &InterfaceType, + dst: &Destination, + ) { + // Extract memory configuration for source and destination + // Get linear memory options (32-bit vs 64-bit pointers, which memory, etc.) + let src_mem_opts = match &src.opts().data_model { + DataModel::Gc {} => todo!("CM+GC"), + DataModel::LinearMemory(opts) => opts, + }; + let dst_mem_opts = match &dst.opts().data_model { + DataModel::Gc {} => todo!("CM+GC"), + DataModel::LinearMemory(opts) => opts, + }; + + // Get type information for source and destination maps + // Look up the TypeMap structs which contain the key and value InterfaceTypes + let src_map_ty = &self.types[src_ty]; + let dst_map_ty = match dst_ty { + InterfaceType::Map(r) => &self.types[*r], + _ => panic!("expected a map"), + }; + + // Load the map's pointer and length into temporary locals + // A map is represented as (ptr, len) - we need both values in locals + // for later use in the translation loop. + match src { + Source::Stack(s) => { + // If map descriptor is passed on the stack (as 2 locals: ptr, len) + assert_eq!(s.locals.len(), 2); + self.stack_get(&s.slice(0..1), src_mem_opts.ptr()); // Push ptr to wasm stack + self.stack_get(&s.slice(1..2), src_mem_opts.ptr()); // Push len to wasm stack + } + Source::Memory(mem) => { + // If map descriptor is stored in memory, load ptr and len from there + self.ptr_load(mem); // Load ptr + self.ptr_load(&mem.bump(src_mem_opts.ptr_size().into())); // Load len (next field) + } + Source::Struct(_) | Source::Array(_) => todo!("CM+GC"), + } + // Pop values from wasm stack into named locals (note: len is on top, then ptr) + let src_len = self.local_set_new_tmp(src_mem_opts.ptr()); + let src_ptr = self.local_set_new_tmp(src_mem_opts.ptr()); + + // Calculate tuple sizes with proper alignment + // Each map entry is a (key, value) tuple. We need to know: + // - Size of key and value in bytes + // - Alignment requirements + // - Total tuple size including any padding + let src_opts = src.opts(); + let dst_opts = dst.opts(); + + // Source tuple layout + let (src_key_size, src_key_align) = self.types.size_align(src_mem_opts, &src_map_ty.key); + let (src_value_size, _) = self.types.size_align(src_mem_opts, &src_map_ty.value); + // Total tuple size = key + value + padding to alignment + // e.g., if key is 4 bytes, value is 8 bytes, align is 4: + // tuple_size = (4 + 8 + 3) & ~3 = 12 bytes + let src_tuple_size = + (src_key_size + src_value_size + src_key_align - 1) & !(src_key_align - 1); + + // Destination tuple layout (may differ if types are converted) + let (dst_key_size, dst_key_align) = self.types.size_align(dst_mem_opts, &dst_map_ty.key); + let (dst_value_size, _) = self.types.size_align(dst_mem_opts, &dst_map_ty.value); + let dst_tuple_size = + (dst_key_size + dst_value_size + dst_key_align - 1) & !(dst_key_align - 1); + + // Create source memory operand and verify alignment + // This creates a Memory operand and verifies the source pointer is properly aligned + let src_mem = self.memory_operand(src_opts, src_ptr, src_key_align); + + // Calculate total byte lengths for source and destination + // total_bytes = count * tuple_size + let src_byte_len = self.local_set_new_tmp(src_mem_opts.ptr()); + self.instruction(LocalGet(src_len.idx)); // Push len + self.ptr_uconst(src_mem_opts, src_tuple_size); // Push tuple_size + self.ptr_mul(src_mem_opts); // len * tuple_size + self.instruction(LocalSet(src_byte_len.idx)); // Save to local + + let dst_byte_len = self.local_set_new_tmp(dst_mem_opts.ptr()); + self.instruction(LocalGet(src_len.idx)); // Push len (same count) + self.ptr_uconst(dst_mem_opts, dst_tuple_size); // Push dst tuple_size + self.ptr_mul(dst_mem_opts); // len * tuple_size + self.instruction(LocalTee(dst_byte_len.idx)); // Save AND keep on stack for malloc + + // Allocate destination buffer + // Call realloc in the destination component to allocate space. + // dst_byte_len is still on the stack from LocalTee above. + let dst_mem = self.malloc(dst_opts, MallocSize::Local(dst_byte_len.idx), dst_key_align); + + // Validate memory bounds + // Verify that ptr + byte_len doesn't overflow or exceed memory bounds. + // Trap if invalid. + self.validate_memory_inbounds( + src_mem_opts, + src_mem.addr.idx, + src_byte_len.idx, + Trap::ListOutOfBounds, + ); + self.validate_memory_inbounds( + dst_mem_opts, + dst_mem.addr.idx, + dst_byte_len.idx, + Trap::ListOutOfBounds, + ); + + // Done with byte length locals + self.free_temp_local(src_byte_len); + self.free_temp_local(dst_byte_len); + + // Main translation loop - copy each (key, value) tuple + // Skip loop entirely if tuples are zero-sized (nothing to copy) + if src_tuple_size > 0 || dst_tuple_size > 0 { + // Loop setup + // Create counter for remaining elements + let remaining = self.local_set_new_tmp(src_mem_opts.ptr()); + self.instruction(LocalGet(src_len.idx)); + self.instruction(LocalSet(remaining.idx)); + + // Create pointer to current position in source + let cur_src_ptr = self.local_set_new_tmp(src_mem_opts.ptr()); + self.instruction(LocalGet(src_mem.addr.idx)); + self.instruction(LocalSet(cur_src_ptr.idx)); + + // Create pointer to current position in destination + let cur_dst_ptr = self.local_set_new_tmp(dst_mem_opts.ptr()); + self.instruction(LocalGet(dst_mem.addr.idx)); + self.instruction(LocalSet(cur_dst_ptr.idx)); + + // WebAssembly loop structure + // Block is the outer container (to break out of loop) + // Loop is what we branch back to for iteration + self.instruction(Block(BlockType::Empty)); + self.instruction(Loop(BlockType::Empty)); + + // Translate the key + // Create Source pointing to current key location + let key_src = Source::Memory(self.memory_operand( + src_opts, + TempLocal { + idx: cur_src_ptr.idx, + ty: src_mem_opts.ptr(), + needs_free: false, + }, + src_key_align, + )); + // Create Destination pointing to where key should go + let key_dst = Destination::Memory(self.memory_operand( + dst_opts, + TempLocal { + idx: cur_dst_ptr.idx, + ty: dst_mem_opts.ptr(), + needs_free: false, + }, + dst_key_align, + )); + // Recursively translate the key (handles any type: primitives, strings, etc.) + self.translate(&src_map_ty.key, &key_src, &dst_map_ty.key, &key_dst); + + // Advance pointers past the key to point at value + if src_key_size > 0 { + self.instruction(LocalGet(cur_src_ptr.idx)); + self.ptr_uconst(src_mem_opts, src_key_size); + self.ptr_add(src_mem_opts); + self.instruction(LocalSet(cur_src_ptr.idx)); + } + if dst_key_size > 0 { + self.instruction(LocalGet(cur_dst_ptr.idx)); + self.ptr_uconst(dst_mem_opts, dst_key_size); + self.ptr_add(dst_mem_opts); + self.instruction(LocalSet(cur_dst_ptr.idx)); + } + + // Translate the value + let value_src = Source::Memory(self.memory_operand( + src_opts, + TempLocal { + idx: cur_src_ptr.idx, + ty: src_mem_opts.ptr(), + needs_free: false, + }, + src_key_align, + )); + let value_dst = Destination::Memory(self.memory_operand( + dst_opts, + TempLocal { + idx: cur_dst_ptr.idx, + ty: dst_mem_opts.ptr(), + needs_free: false, + }, + dst_key_align, + )); + // Recursively translate the value + self.translate(&src_map_ty.value, &value_src, &dst_map_ty.value, &value_dst); + + // Advance pointers past the value (including any alignment padding) + // If tuple_size > key_size + value_size, there's padding we need to skip + if src_tuple_size > src_key_size + src_value_size { + self.instruction(LocalGet(cur_src_ptr.idx)); + self.ptr_uconst(src_mem_opts, src_tuple_size - src_key_size - src_value_size); + self.ptr_add(src_mem_opts); + self.instruction(LocalSet(cur_src_ptr.idx)); + } + if dst_tuple_size > dst_key_size + dst_value_size { + self.instruction(LocalGet(cur_dst_ptr.idx)); + self.ptr_uconst(dst_mem_opts, dst_tuple_size - dst_key_size - dst_value_size); + self.ptr_add(dst_mem_opts); + self.instruction(LocalSet(cur_dst_ptr.idx)); + } + + // Loop continuation: decrement counter and branch if not done + self.instruction(LocalGet(remaining.idx)); + self.ptr_iconst(src_mem_opts, -1); // Push -1 + self.ptr_add(src_mem_opts); // remaining - 1 + self.instruction(LocalTee(remaining.idx)); // Save back AND keep on stack + self.ptr_br_if(src_mem_opts, 0); // If remaining != 0, branch back to Loop + self.instruction(End); // End Loop + self.instruction(End); // End Block + + // Release loop locals + self.free_temp_local(cur_dst_ptr); + self.free_temp_local(cur_src_ptr); + self.free_temp_local(remaining); + } + + // Store the result (ptr, len) to the destination + match dst { + Destination::Stack(s, _) => { + // Put ptr and len on the wasm stack for return + self.instruction(LocalGet(dst_mem.addr.idx)); + self.stack_set(&s[..1], dst_mem_opts.ptr()); + self.convert_src_len_to_dst(src_len.idx, src_mem_opts.ptr(), dst_mem_opts.ptr()); + self.stack_set(&s[1..], dst_mem_opts.ptr()); + } + Destination::Memory(mem) => { + // Store ptr and len to destination memory location + self.instruction(LocalGet(mem.addr.idx)); + self.instruction(LocalGet(dst_mem.addr.idx)); + self.ptr_store(mem); + self.instruction(LocalGet(mem.addr.idx)); + self.convert_src_len_to_dst(src_len.idx, src_mem_opts.ptr(), dst_mem_opts.ptr()); + self.ptr_store(&mem.bump(dst_mem_opts.ptr_size().into())); + } + Destination::Struct(_) | Destination::Array(_) => todo!("CM+GC"), + } + + // Cleanup - release all temporary locals + self.free_temp_local(src_len); + self.free_temp_local(src_mem.addr); + self.free_temp_local(dst_mem.addr); + } + fn calculate_list_byte_len( &mut self, opts: &LinearMemoryOptions, diff --git a/crates/fuzzing/src/generators/config.rs b/crates/fuzzing/src/generators/config.rs index cb3544982e2d..1b442e46c7cb 100644 --- a/crates/fuzzing/src/generators/config.rs +++ b/crates/fuzzing/src/generators/config.rs @@ -140,6 +140,7 @@ impl Config { component_model_threading, component_model_error_context, component_model_gc, + component_model_map, component_model_fixed_length_lists, simd, exceptions, @@ -166,6 +167,7 @@ impl Config { self.module_config.component_model_error_context = component_model_error_context.unwrap_or(false); self.module_config.component_model_gc = component_model_gc.unwrap_or(false); + self.module_config.component_model_map = component_model_map.unwrap_or(false); self.module_config.component_model_fixed_length_lists = component_model_fixed_length_lists.unwrap_or(false); @@ -293,6 +295,7 @@ impl Config { cfg.wasm.component_model_error_context = Some(self.module_config.component_model_error_context); cfg.wasm.component_model_gc = Some(self.module_config.component_model_gc); + cfg.wasm.component_model_map = Some(self.module_config.component_model_map); cfg.wasm.component_model_fixed_length_lists = Some(self.module_config.component_model_fixed_length_lists); cfg.wasm.custom_page_sizes = Some(self.module_config.config.custom_page_sizes_enabled); diff --git a/crates/fuzzing/src/generators/module.rs b/crates/fuzzing/src/generators/module.rs index db89d56b5b82..6d8198649418 100644 --- a/crates/fuzzing/src/generators/module.rs +++ b/crates/fuzzing/src/generators/module.rs @@ -22,6 +22,7 @@ pub struct ModuleConfig { pub component_model_threading: bool, pub component_model_error_context: bool, pub component_model_gc: bool, + pub component_model_map: bool, pub component_model_fixed_length_lists: bool, pub legacy_exceptions: bool, pub shared_memory: bool, @@ -80,6 +81,7 @@ impl<'a> Arbitrary<'a> for ModuleConfig { component_model_threading: false, component_model_error_context: false, component_model_gc: false, + component_model_map: false, component_model_fixed_length_lists: false, legacy_exceptions: false, shared_memory: false, diff --git a/crates/fuzzing/src/oracles/component_api.rs b/crates/fuzzing/src/oracles/component_api.rs index 56b187251a69..417bbdab34bc 100644 --- a/crates/fuzzing/src/oracles/component_api.rs +++ b/crates/fuzzing/src/oracles/component_api.rs @@ -118,8 +118,13 @@ fn arbitrary_val(ty: &component::Type, input: &mut Unstructured) -> arbitrary::R .collect::>()?, ), - // Resources, futures, streams, and error contexts aren't fuzzed at this time. - Type::Own(_) | Type::Borrow(_) | Type::Future(_) | Type::Stream(_) | Type::ErrorContext => { + // Resources, futures, streams, error contexts, and maps aren't fuzzed at this time. + Type::Own(_) + | Type::Borrow(_) + | Type::Future(_) + | Type::Stream(_) + | Type::ErrorContext + | Type::Map(_) => { unreachable!() } }) diff --git a/crates/test-util/src/component.rs b/crates/test-util/src/component.rs index fb31b594a534..f76587f1c935 100644 --- a/crates/test-util/src/component.rs +++ b/crates/test-util/src/component.rs @@ -12,6 +12,7 @@ pub fn config() -> Config { let mut config = Config::new(); config.wasm_component_model(true); + config.wasm_features(wasmtime::WasmFeatures::CM_MAP, true); // When `WASMTIME_TEST_NO_HOG_MEMORY` is set it means we're in qemu. The // component model tests create a disproportionate number of instances so diff --git a/crates/test-util/src/component_fuzz.rs b/crates/test-util/src/component_fuzz.rs index 162b7075fbfe..94b2c2d88e02 100644 --- a/crates/test-util/src/component_fuzz.rs +++ b/crates/test-util/src/component_fuzz.rs @@ -125,6 +125,7 @@ pub enum Type { Char, String, List(Box), + Map(Box, Box), // Give records the ability to generate a generous amount of fields but // don't let the fuzzer go too wild since `wasmparser`'s validator currently @@ -158,6 +159,9 @@ impl Type { fuel: &mut u32, ) -> arbitrary::Result { *fuel = fuel.saturating_sub(1); + // Note: Map types (case 21) are disabled because HashMap doesn't implement + // ComponentType, Lift, or Lower yet. When those implementations are added, + // change this to 21. let max = if depth == 0 || *fuel == 0 { 12 } else { 20 }; Ok(match u.int_in_range(0..=max)? { 0 => Type::Bool, @@ -195,11 +199,44 @@ impl Type { *fuel -= amt; Type::Flags(amt) } + 21 => Type::Map( + Box::new(Type::generate_hashable_key(u, fuel)?), + Box::new(Type::generate(u, depth - 1, fuel)?), + ), // ^-- if you add something here update the `depth != 0` case above _ => unreachable!(), }) } + /// Generate a type that can be used as a HashMap key (implements Hash + Eq). + /// This excludes floats and complex types that might contain floats. + fn generate_hashable_key(u: &mut Unstructured<'_>, fuel: &mut u32) -> arbitrary::Result { + *fuel = fuel.saturating_sub(1); + // Only generate types that implement Hash and Eq: + // - No Float32/Float64 (NaN comparison issues) + // - No complex types (Record, Tuple, Variant, etc.) as they might contain floats + // - String is allowed as it implements Hash + Eq + Ok(match u.int_in_range(0..=11)? { + 0 => Type::Bool, + 1 => Type::S8, + 2 => Type::U8, + 3 => Type::S16, + 4 => Type::U16, + 5 => Type::S32, + 6 => Type::U32, + 7 => Type::S64, + 8 => Type::U64, + 9 => Type::Char, + 10 => Type::String, + 11 => { + let amt = u.int_in_range(1..=(*fuel).max(1).min(257))?; + *fuel -= amt; + Type::Enum(amt) + } + _ => unreachable!(), + }) + } + fn generate_opt( u: &mut Unstructured<'_>, depth: u32, @@ -247,7 +284,7 @@ impl Type { Type::S64 | Type::U64 => Kind::Primitive("i64.store"), Type::Float32 => Kind::Primitive("f32.store"), Type::Float64 => Kind::Primitive("f64.store"), - Type::String | Type::List(_) => Kind::PointerPair, + Type::String | Type::List(_) | Type::Map(_, _) => Kind::PointerPair, Type::Enum(n) if *n <= (1 << 8) => Kind::Primitive("i32.store8"), Type::Enum(n) if *n <= (1 << 16) => Kind::Primitive("i32.store16"), Type::Enum(_) => Kind::Primitive("i32.store"), @@ -374,6 +411,7 @@ impl Type { | Type::Float64 | Type::String | Type::List(_) + | Type::Map(_, _) | Type::Flags(_) | Type::Enum(_) => unreachable!(), @@ -414,7 +452,7 @@ impl Type { Type::U64 | Type::S64 => Kind::Primitive("i64.load"), Type::Float32 => Kind::Primitive("f32.load"), Type::Float64 => Kind::Primitive("f64.load"), - Type::String | Type::List(_) => Kind::PointerPair, + Type::String | Type::List(_) | Type::Map(_, _) => Kind::PointerPair, Type::Enum(n) if *n <= (1 << 8) => Kind::Primitive("i32.load8_u"), Type::Enum(n) if *n <= (1 << 16) => Kind::Primitive("i32.load16_u"), Type::Enum(_) => Kind::Primitive("i32.load"), @@ -551,6 +589,7 @@ impl Type { | Type::Float64 | Type::String | Type::List(_) + | Type::Map(_, _) | Type::Flags(_) | Type::Enum(_) => unreachable!(), @@ -667,7 +706,7 @@ impl Type { Type::S64 | Type::U64 => vec.push(CoreType::I64), Type::Float32 => vec.push(CoreType::F32), Type::Float64 => vec.push(CoreType::F64), - Type::String | Type::List(_) => { + Type::String | Type::List(_) | Type::Map(_, _) => { vec.push(CoreType::I32); vec.push(CoreType::I32); } @@ -706,7 +745,7 @@ impl Type { alignment: 8, }, - Type::String | Type::List(_) => SizeAndAlignment { + Type::String | Type::List(_) | Type::Map(_, _) => SizeAndAlignment { size: 8, alignment: 4, }, @@ -1161,6 +1200,11 @@ pub fn rust_type(ty: &Type, name_counter: &mut u32, declarations: &mut TokenStre let ty = rust_type(ty, name_counter, declarations); quote!(Vec<#ty>) } + Type::Map(key_ty, value_ty) => { + let key_ty = rust_type(key_ty, name_counter, declarations); + let value_ty = rust_type(value_ty, name_counter, declarations); + quote!(std::collections::HashMap<#key_ty, #value_ty>) + } Type::Record(types) => { let fields = types .iter() @@ -1330,6 +1374,7 @@ impl<'a> TypesBuilder<'a> { // Otherwise emit a reference to the type and remember to generate // the corresponding type alias later. Type::List(_) + | Type::Map(_, _) | Type::Record(_) | Type::Tuple(_) | Type::Variant(_) @@ -1367,6 +1412,13 @@ impl<'a> TypesBuilder<'a> { self.write_ref(ty, &mut decl); decl.push_str(")"); } + Type::Map(key_ty, value_ty) => { + decl.push_str("(map "); + self.write_ref(key_ty, &mut decl); + decl.push_str(" "); + self.write_ref(value_ty, &mut decl); + decl.push_str(")"); + } Type::Record(types) => { decl.push_str("(record"); for (index, ty) in types.iter().enumerate() { diff --git a/crates/test-util/src/wasmtime_wast.rs b/crates/test-util/src/wasmtime_wast.rs index d9a22a8684e8..3f038782b2c5 100644 --- a/crates/test-util/src/wasmtime_wast.rs +++ b/crates/test-util/src/wasmtime_wast.rs @@ -43,6 +43,7 @@ pub fn apply_test_config(config: &mut Config, test_config: &wast::TestConfig) { component_model_threading, component_model_error_context, component_model_gc, + component_model_map, component_model_fixed_length_lists, nan_canonicalization, simd, @@ -73,6 +74,7 @@ pub fn apply_test_config(config: &mut Config, test_config: &wast::TestConfig) { let component_model_threading = component_model_threading.unwrap_or(false); let component_model_error_context = component_model_error_context.unwrap_or(false); let component_model_gc = component_model_gc.unwrap_or(false); + let component_model_map = component_model_map.unwrap_or(false); let component_model_fixed_length_lists = component_model_fixed_length_lists.unwrap_or(false); let nan_canonicalization = nan_canonicalization.unwrap_or(false); let relaxed_simd = relaxed_simd.unwrap_or(false); @@ -113,6 +115,7 @@ pub fn apply_test_config(config: &mut Config, test_config: &wast::TestConfig) { .wasm_component_model_threading(component_model_threading) .wasm_component_model_error_context(component_model_error_context) .wasm_component_model_gc(component_model_gc) + .wasm_component_model_map(component_model_map) .wasm_component_model_fixed_length_lists(component_model_fixed_length_lists) .wasm_exceptions(exceptions) .wasm_stack_switching(stack_switching) diff --git a/crates/test-util/src/wast.rs b/crates/test-util/src/wast.rs index 4fdda8111b61..5c76ff3f5d7a 100644 --- a/crates/test-util/src/wast.rs +++ b/crates/test-util/src/wast.rs @@ -268,6 +268,7 @@ macro_rules! foreach_config_option { component_model_threading component_model_error_context component_model_gc + component_model_map component_model_fixed_length_lists simd gc_types diff --git a/crates/wasmtime/src/config.rs b/crates/wasmtime/src/config.rs index 5dd0da61020b..26b5ccd3952e 100644 --- a/crates/wasmtime/src/config.rs +++ b/crates/wasmtime/src/config.rs @@ -1269,6 +1269,16 @@ impl Config { self } + /// Configures whether the component model map type is enabled or not. + /// + /// This is part of the component model specification and enables the + /// `map` type in WIT and the component binary format. + #[cfg(feature = "component-model")] + pub fn wasm_component_model_map(&mut self, enable: bool) -> &mut Self { + self.wasm_features(WasmFeatures::CM_MAP, enable); + self + } + /// This corresponds to the 🔧 emoji in the component model specification. /// /// Please note that Wasmtime's support for this feature is _very_ @@ -2248,6 +2258,7 @@ impl Config { | WasmFeatures::CM_THREADING | WasmFeatures::CM_ERROR_CONTEXT | WasmFeatures::CM_GC + | WasmFeatures::CM_MAP | WasmFeatures::CM_FIXED_LENGTH_LISTS; #[allow(unused_mut, reason = "easier to avoid #[cfg]")] diff --git a/crates/wasmtime/src/runtime/component/func/typed.rs b/crates/wasmtime/src/runtime/component/func/typed.rs index fe2dee93e9ca..e8e2ddd5cc3e 100644 --- a/crates/wasmtime/src/runtime/component/func/typed.rs +++ b/crates/wasmtime/src/runtime/component/func/typed.rs @@ -2909,6 +2909,7 @@ pub fn desc(ty: &InterfaceType) -> &'static str { InterfaceType::Future(_) => "future", InterfaceType::Stream(_) => "stream", InterfaceType::ErrorContext(_) => "error-context", + InterfaceType::Map(_) => "map", InterfaceType::FixedLengthList(_) => "list<_, N>", } } diff --git a/crates/wasmtime/src/runtime/component/types.rs b/crates/wasmtime/src/runtime/component/types.rs index a14cd16764b7..241eb46d196f 100644 --- a/crates/wasmtime/src/runtime/component/types.rs +++ b/crates/wasmtime/src/runtime/component/types.rs @@ -10,9 +10,9 @@ use core::ops::Deref; use wasmtime_environ::component::{ ComponentTypes, Export, InterfaceType, ResourceIndex, TypeComponentIndex, TypeComponentInstanceIndex, TypeDef, TypeEnumIndex, TypeFlagsIndex, TypeFuncIndex, - TypeFutureIndex, TypeFutureTableIndex, TypeListIndex, TypeModuleIndex, TypeOptionIndex, - TypeRecordIndex, TypeResourceTable, TypeResourceTableIndex, TypeResultIndex, TypeStreamIndex, - TypeStreamTableIndex, TypeTupleIndex, TypeVariantIndex, + TypeFutureIndex, TypeFutureTableIndex, TypeListIndex, TypeMapIndex, TypeModuleIndex, + TypeOptionIndex, TypeRecordIndex, TypeResourceTable, TypeResourceTableIndex, TypeResultIndex, + TypeStreamIndex, TypeStreamTableIndex, TypeTupleIndex, TypeVariantIndex, }; use wasmtime_environ::{PanicOnOom as _, PrimaryMap}; @@ -108,6 +108,8 @@ impl TypeChecker<'_> { (InterfaceType::Borrow(_), _) => false, (InterfaceType::List(l1), InterfaceType::List(l2)) => self.lists_equal(l1, l2), (InterfaceType::List(_), _) => false, + (InterfaceType::Map(m1), InterfaceType::Map(m2)) => self.maps_equal(m1, m2), + (InterfaceType::Map(_), _) => false, (InterfaceType::Record(r1), InterfaceType::Record(r2)) => self.records_equal(r1, r2), (InterfaceType::Record(_), _) => false, (InterfaceType::Variant(v1), InterfaceType::Variant(v2)) => self.variants_equal(v1, v2), @@ -168,6 +170,12 @@ impl TypeChecker<'_> { self.interface_types_equal(a.element, b.element) } + fn maps_equal(&self, m1: TypeMapIndex, m2: TypeMapIndex) -> bool { + let a = &self.a_types[m1]; + let b = &self.b_types[m2]; + self.interface_types_equal(a.key, b.key) && self.interface_types_equal(a.value, b.value) + } + fn resources_equal(&self, o1: TypeResourceTableIndex, o2: TypeResourceTableIndex) -> bool { match (&self.a_types[o1], &self.b_types[o2]) { // Concrete resource types are the same if they map back to the @@ -325,6 +333,34 @@ impl List { } } +/// A `map` interface type +#[derive(Clone, Debug)] +pub struct Map(Handle); + +impl PartialEq for Map { + fn eq(&self, other: &Self) -> bool { + self.0.equivalent(&other.0, TypeChecker::maps_equal) + } +} + +impl Eq for Map {} + +impl Map { + pub(crate) fn from(index: TypeMapIndex, ty: &InstanceType<'_>) -> Self { + Map(Handle::new(index, ty)) + } + + /// Retrieve the key type of this `map`. + pub fn key(&self) -> Type { + Type::from(&self.0.types[self.0.index].key, &self.0.instance()) + } + + /// Retrieve the value type of this `map`. + pub fn value(&self) -> Type { + Type::from(&self.0.types[self.0.index].value, &self.0.instance()) + } +} + /// A field declaration belonging to a `record` #[derive(Debug)] pub struct Field<'a> { @@ -684,6 +720,7 @@ pub enum Type { Char, String, List(List), + Map(Map), Record(Record), Tuple(Tuple), Variant(Variant), @@ -844,6 +881,7 @@ impl Type { InterfaceType::Char => Type::Char, InterfaceType::String => Type::String, InterfaceType::List(index) => Type::List(List::from(*index, instance)), + InterfaceType::Map(index) => Type::Map(Map::from(*index, instance)), InterfaceType::Record(index) => Type::Record(Record::from(*index, instance)), InterfaceType::Tuple(index) => Type::Tuple(Tuple::from(*index, instance)), InterfaceType::Variant(index) => Type::Variant(Variant::from(*index, instance)), @@ -876,6 +914,7 @@ impl Type { Type::Char => "char", Type::String => "string", Type::List(_) => "list", + Type::Map(_) => "map", Type::Record(_) => "record", Type::Tuple(_) => "tuple", Type::Variant(_) => "variant", diff --git a/crates/wasmtime/src/runtime/component/values.rs b/crates/wasmtime/src/runtime/component/values.rs index 1049d1c12712..8663be9749f2 100644 --- a/crates/wasmtime/src/runtime/component/values.rs +++ b/crates/wasmtime/src/runtime/component/values.rs @@ -7,8 +7,8 @@ use core::mem::MaybeUninit; use core::slice::{Iter, IterMut}; use wasmtime_component_util::{DiscriminantSize, FlagsSize}; use wasmtime_environ::component::{ - CanonicalAbiInfo, InterfaceType, TypeEnum, TypeFlags, TypeListIndex, TypeOption, TypeResult, - TypeVariant, VariantInfo, + CanonicalAbiInfo, InterfaceType, TypeEnum, TypeFlags, TypeListIndex, TypeMapIndex, TypeOption, + TypeResult, TypeVariant, VariantInfo, }; /// Represents possible runtime values which a component function can either @@ -79,6 +79,9 @@ pub enum Val { Char(char), String(String), List(Vec), + /// A map type represented as a list of key-value pairs. + /// Duplicate keys are allowed and follow "last value wins" semantics. + Map(Vec<(Val, Val)>), Record(Vec<(String, Val)>), Tuple(Vec), Variant(String, Option>), @@ -126,6 +129,13 @@ impl Val { let len = u32::linear_lift_from_flat(cx, InterfaceType::U32, next(src))? as usize; load_list(cx, i, ptr, len)? } + InterfaceType::Map(i) => { + // FIXME(#4311): needs memory64 treatment + // Maps are represented as list> in canonical ABI + let ptr = u32::linear_lift_from_flat(cx, InterfaceType::U32, next(src))? as usize; + let len = u32::linear_lift_from_flat(cx, InterfaceType::U32, next(src))? as usize; + load_map(cx, i, ptr, len)? + } InterfaceType::Record(i) => Val::Record( cx.types[i] .fields @@ -244,6 +254,12 @@ impl Val { let len = u32::from_le_bytes(bytes[4..].try_into().unwrap()) as usize; load_list(cx, i, ptr, len)? } + InterfaceType::Map(i) => { + // FIXME(#4311): needs memory64 treatment + let ptr = u32::from_le_bytes(bytes[..4].try_into().unwrap()) as usize; + let len = u32::from_le_bytes(bytes[4..].try_into().unwrap()) as usize; + load_map(cx, i, ptr, len)? + } InterfaceType::Record(i) => { let mut offset = 0; @@ -425,6 +441,18 @@ impl Val { Ok(()) } (InterfaceType::List(_), _) => unexpected(ty, self), + (InterfaceType::Map(ty), Val::Map(map)) => { + // Maps are stored as list> in canonical ABI + let map_ty = &cx.types[ty]; + // Convert HashMap to Vec<(Val, Val)> for lowering + let pairs: Vec<(Val, Val)> = + map.iter().map(|(k, v)| (k.clone(), v.clone())).collect(); + let (ptr, len) = lower_map(cx, map_ty.key, map_ty.value, &pairs)?; + next_mut(dst).write(ValRaw::i64(ptr as i64)); + next_mut(dst).write(ValRaw::i64(len as i64)); + Ok(()) + } + (InterfaceType::Map(_), _) => unexpected(ty, self), (InterfaceType::Record(ty), Val::Record(values)) => { let ty = &cx.types[ty]; if ty.fields.len() != values.len() { @@ -554,6 +582,15 @@ impl Val { Ok(()) } (InterfaceType::List(_), _) => unexpected(ty, self), + (InterfaceType::Map(ty_idx), Val::Map(values)) => { + let map_ty = &cx.types[ty_idx]; + let (ptr, len) = lower_map(cx, map_ty.key, map_ty.value, values)?; + // FIXME(#4311): needs memory64 handling + *cx.get(offset + 0) = u32::try_from(ptr).unwrap().to_le_bytes(); + *cx.get(offset + 4) = u32::try_from(len).unwrap().to_le_bytes(); + Ok(()) + } + (InterfaceType::Map(_), _) => unexpected(ty, self), (InterfaceType::Record(ty), Val::Record(values)) => { let ty = &cx.types[ty]; if ty.fields.len() != values.len() { @@ -666,6 +703,7 @@ impl Val { Val::Float64(_) => "f64", Val::Char(_) => "char", Val::List(_) => "list", + Val::Map(_) => "map", Val::String(_) => "string", Val::Record(_) => "record", Val::Enum(_) => "enum", @@ -740,6 +778,8 @@ impl PartialEq for Val { (Self::String(_), _) => false, (Self::List(l), Self::List(r)) => l == r, (Self::List(_), _) => false, + (Self::Map(l), Self::Map(r)) => l == r, + (Self::Map(_), _) => false, (Self::Record(l), Self::Record(r)) => l == r, (Self::Record(_), _) => false, (Self::Tuple(l), Self::Tuple(r)) => l == r, @@ -939,6 +979,57 @@ fn load_list(cx: &mut LiftContext<'_>, ty: TypeListIndex, ptr: usize, len: usize )) } +fn load_map(cx: &mut LiftContext<'_>, ty: TypeMapIndex, ptr: usize, len: usize) -> Result { + // Maps are stored as list> in canonical ABI + let map_ty = &cx.types[ty]; + let key_ty = map_ty.key; + let value_ty = map_ty.value; + + // Calculate tuple layout using canonical ABI alignment rules + let key_abi = cx.types.canonical_abi(&key_ty); + let value_abi = cx.types.canonical_abi(&value_ty); + let key_size = usize::try_from(key_abi.size32).unwrap(); + let value_size = usize::try_from(value_abi.size32).unwrap(); + + // Calculate value offset: align key_size to value alignment + let mut offset = u32::try_from(key_size).unwrap(); + let value_offset = value_abi.next_field32(&mut offset); + let value_offset = usize::try_from(value_offset).unwrap(); + + // Tuple size is the final offset aligned to max alignment + let tuple_alignment = key_abi.align32.max(value_abi.align32); + let tuple_size = usize::try_from(offset).unwrap(); + let tuple_size = (tuple_size + usize::try_from(tuple_alignment)? - 1) + & !(usize::try_from(tuple_alignment)? - 1); + + // Bounds check + match len + .checked_mul(tuple_size) + .and_then(|len| ptr.checked_add(len)) + { + Some(n) if n <= cx.memory().len() => {} + _ => bail!("map pointer/length out of bounds of memory"), + } + if ptr % usize::try_from(tuple_alignment)? != 0 { + bail!("map pointer is not aligned") + } + + // Load each tuple (key, value) into a Vec + let mut map = Vec::with_capacity(len); + for index in 0..len { + let tuple_ptr = ptr + (index * tuple_size); + let key = Val::load(cx, key_ty, &cx.memory()[tuple_ptr..][..key_size])?; + let value = Val::load( + cx, + value_ty, + &cx.memory()[tuple_ptr + value_offset..][..value_size], + )?; + map.push((key, value)); + } + + Ok(Val::Map(map)) +} + fn load_variant( cx: &mut LiftContext<'_>, info: &VariantInfo, @@ -1029,6 +1120,47 @@ fn lower_list( Ok((ptr, items.len())) } +/// Lower a map as list> with the specified key and value types. +fn lower_map( + cx: &mut LowerContext<'_, T>, + key_type: InterfaceType, + value_type: InterfaceType, + pairs: &[(Val, Val)], +) -> Result<(usize, usize)> { + // Calculate tuple layout using canonical ABI alignment rules + let key_abi = cx.types.canonical_abi(&key_type); + let value_abi = cx.types.canonical_abi(&value_type); + let key_size = usize::try_from(key_abi.size32)?; + + // Calculate value offset: align key_size to value alignment + let mut offset = u32::try_from(key_size).unwrap(); + let value_offset = value_abi.next_field32(&mut offset); + let value_offset = usize::try_from(value_offset).unwrap(); + + // Tuple size is the final offset aligned to max alignment + let tuple_align = key_abi.align32.max(value_abi.align32); + let tuple_size = usize::try_from(offset).unwrap(); + let tuple_size = + (tuple_size + usize::try_from(tuple_align)? - 1) & !(usize::try_from(tuple_align)? - 1); + + let size = pairs + .len() + .checked_mul(tuple_size) + .ok_or_else(|| crate::format_err!("size overflow copying a map"))?; + let ptr = cx.realloc(0, 0, tuple_align, size)?; + + let mut tuple_ptr = ptr; + for (key, value) in pairs { + // Store key at tuple_ptr + key.store(cx, key_type, tuple_ptr)?; + // Store value at tuple_ptr + value_offset (properly aligned) + value.store(cx, value_type, tuple_ptr + value_offset)?; + tuple_ptr += tuple_size; + } + + Ok((ptr, pairs.len())) +} + fn push_flags(ty: &TypeFlags, flags: &mut Vec, mut offset: u32, mut bits: u32) { while bits > 0 { if bits & 1 != 0 { diff --git a/crates/wasmtime/src/runtime/wave/component.rs b/crates/wasmtime/src/runtime/wave/component.rs index 9849bb455bf6..cc3e8c1b7c2a 100644 --- a/crates/wasmtime/src/runtime/wave/component.rs +++ b/crates/wasmtime/src/runtime/wave/component.rs @@ -45,7 +45,8 @@ impl WasmType for component::Type { | Self::Borrow(_) | Self::Stream(_) | Self::Future(_) - | Self::ErrorContext => WasmTypeKind::Unsupported, + | Self::ErrorContext + | Self::Map(_) => WasmTypeKind::Unsupported, } } @@ -138,9 +139,11 @@ impl WasmValue for component::Val { Self::Option(_) => WasmTypeKind::Option, Self::Result(_) => WasmTypeKind::Result, Self::Flags(_) => WasmTypeKind::Flags, - Self::Resource(_) | Self::Stream(_) | Self::Future(_) | Self::ErrorContext(_) => { - WasmTypeKind::Unsupported - } + Self::Resource(_) + | Self::Stream(_) + | Self::Future(_) + | Self::ErrorContext(_) + | Self::Map(_) => WasmTypeKind::Unsupported, } } diff --git a/crates/wast/src/component.rs b/crates/wast/src/component.rs index 1de0bb1ba3eb..06a8f62a772d 100644 --- a/crates/wast/src/component.rs +++ b/crates/wast/src/component.rs @@ -290,6 +290,7 @@ fn mismatch(expected: &ComponentConst<'_>, actual: &Val) -> Result<()> { Val::Future(..) => "future", Val::Stream(..) => "stream", Val::ErrorContext(..) => "error-context", + Val::Map(..) => "map", }; bail!("expected `{expected}` got `{actual}`") } diff --git a/crates/wit-bindgen/src/lib.rs b/crates/wit-bindgen/src/lib.rs index 8ca36d819e62..609eb7d7eb2c 100644 --- a/crates/wit-bindgen/src/lib.rs +++ b/crates/wit-bindgen/src/lib.rs @@ -1633,9 +1633,9 @@ impl<'a> InterfaceGenerator<'a> { TypeDefKind::Stream(t) => self.type_stream(id, name, t.as_ref(), &ty.docs), TypeDefKind::Handle(handle) => self.type_handle(id, name, handle, &ty.docs), TypeDefKind::Resource => self.type_resource(id, name, ty, &ty.docs), + TypeDefKind::Map(k, v) => self.type_map(id, name, k, v, &ty.docs), TypeDefKind::Unknown => unreachable!(), TypeDefKind::FixedLengthList(..) => todo!(), - TypeDefKind::Map(..) => todo!(), } } @@ -2204,6 +2204,22 @@ impl<'a> InterfaceGenerator<'a> { } } + fn type_map(&mut self, id: TypeId, _name: &str, key: &Type, value: &Type, docs: &Docs) { + let info = self.info(id); + for (name, mode) in self.modes_of(id) { + let lt = self.lifetime_for(&info, mode); + self.rustdoc(docs); + self.push_str(&format!("pub type {name}")); + self.print_generics(lt); + self.push_str(" = "); + let key_ty = self.ty(key, mode); + let value_ty = self.ty(value, mode); + self.push_str(&format!("std::collections::HashMap<{key_ty}, {value_ty}>")); + self.push_str(";\n"); + self.assert_type(id, &name); + } + } + fn type_stream(&mut self, id: TypeId, name: &str, ty: Option<&Type>, docs: &Docs) { self.rustdoc(docs); self.push_str(&format!("pub type {name}")); @@ -3416,8 +3432,10 @@ fn type_contains_lists(ty: Type, resolve: &Resolve) -> bool { .any(|case| option_type_contains_lists(case.ty, resolve)), TypeDefKind::Type(ty) => type_contains_lists(*ty, resolve), TypeDefKind::List(_) => true, + TypeDefKind::Map(k, v) => { + type_contains_lists(*k, resolve) || type_contains_lists(*v, resolve) + } TypeDefKind::FixedLengthList(..) => todo!(), - TypeDefKind::Map(..) => todo!(), }, // Technically strings are lists too, but we ignore that here because diff --git a/crates/wit-bindgen/src/rust.rs b/crates/wit-bindgen/src/rust.rs index 2ba39559ed80..d33987812a82 100644 --- a/crates/wit-bindgen/src/rust.rs +++ b/crates/wit-bindgen/src/rust.rs @@ -121,6 +121,7 @@ pub trait RustGenerator<'a> { | TypeDefKind::Future(_) | TypeDefKind::Stream(_) | TypeDefKind::List(_) + | TypeDefKind::Map(_, _) | TypeDefKind::Flags(_) | TypeDefKind::Enum(_) | TypeDefKind::Tuple(_) @@ -133,7 +134,6 @@ pub trait RustGenerator<'a> { TypeDefKind::Type(_) => false, TypeDefKind::Unknown => unreachable!(), TypeDefKind::FixedLengthList(..) => todo!(), - TypeDefKind::Map(..) => todo!(), } } } @@ -188,9 +188,13 @@ pub trait RustGenerator<'a> { TypeDefKind::Resource => unreachable!(), TypeDefKind::Type(t) => self.ty(t, mode), + TypeDefKind::Map(k, v) => { + let key = self.ty(k, mode); + let value = self.ty(v, mode); + format!("std::collections::HashMap<{key}, {value}>") + } TypeDefKind::Unknown => unreachable!(), TypeDefKind::FixedLengthList(..) => todo!(), - TypeDefKind::Map(..) => todo!(), } } diff --git a/crates/wit-bindgen/src/types.rs b/crates/wit-bindgen/src/types.rs index 200d13a82f93..7dd53a86c1b0 100644 --- a/crates/wit-bindgen/src/types.rs +++ b/crates/wit-bindgen/src/types.rs @@ -148,6 +148,11 @@ impl Types { info = self.type_info(resolve, ty); info.has_list = true; } + TypeDefKind::Map(k, v) => { + info = self.type_info(resolve, k); + info |= self.type_info(resolve, v); + info.has_list = true; + } TypeDefKind::Type(ty) | TypeDefKind::Option(ty) => { info = self.type_info(resolve, ty); } @@ -163,7 +168,6 @@ impl Types { TypeDefKind::Resource => {} TypeDefKind::Unknown => unreachable!(), TypeDefKind::FixedLengthList(..) => todo!(), - TypeDefKind::Map(..) => todo!(), } self.type_info.insert(ty, info); info diff --git a/tests/all/component_model/dynamic.rs b/tests/all/component_model/dynamic.rs index 0a67a320f82b..91b843f7b6aa 100644 --- a/tests/all/component_model/dynamic.rs +++ b/tests/all/component_model/dynamic.rs @@ -136,6 +136,347 @@ fn lists() -> Result<()> { Ok(()) } +#[test] +fn maps() -> Result<()> { + let engine = super::engine(); + let mut store = Store::new(&engine, ()); + + let component = Component::new(&engine, make_echo_component("(map u32 string)", 8))?; + let instance = Linker::new(&engine).instantiate(&mut store, &component)?; + let func = instance.get_func(&mut store, "echo").unwrap(); + + let input_map = vec![ + (Val::U32(1), Val::String("one".into())), + (Val::U32(2), Val::String("two".into())), + (Val::U32(3), Val::String("three".into())), + ]; + let input = Val::Map(input_map); + + let mut output = [Val::Bool(false)]; + func.call_and_post_return(&mut store, &[input.clone()], &mut output)?; + + // Maps should round-trip correctly + match &output[0] { + Val::Map(output_map) => { + assert_eq!(output_map.len(), 3); + assert!( + output_map + .iter() + .any(|(k, v)| *k == Val::U32(1) && *v == Val::String("one".into())) + ); + assert!( + output_map + .iter() + .any(|(k, v)| *k == Val::U32(2) && *v == Val::String("two".into())) + ); + assert!( + output_map + .iter() + .any(|(k, v)| *k == Val::U32(3) && *v == Val::String("three".into())) + ); + } + _ => panic!("expected map"), + } + + // Sad path: type mismatch (wrong key type) + // Need to create a fresh instance because errors can leave the instance in a bad state + let mut store2 = Store::new(&engine, ()); + let instance2 = Linker::new(&engine).instantiate(&mut store2, &component)?; + let func2 = instance2.get_func(&mut store2, "echo").unwrap(); + + let err_map = vec![(Val::String("key".into()), Val::String("value".into()))]; + let err = Val::Map(err_map); + let err = func2 + .call_and_post_return(&mut store2, &[err], &mut output) + .unwrap_err(); + assert!(err.to_string().contains("type mismatch"), "{err}"); + + // Sad path: type mismatch (wrong value type) + let mut store3 = Store::new(&engine, ()); + let instance3 = Linker::new(&engine).instantiate(&mut store3, &component)?; + let func3 = instance3.get_func(&mut store3, "echo").unwrap(); + + let err_map2 = vec![(Val::U32(1), Val::U32(42))]; + let err = Val::Map(err_map2); + let err = func3 + .call_and_post_return(&mut store3, &[err], &mut output) + .unwrap_err(); + assert!(err.to_string().contains("type mismatch"), "{err}"); + + // Test empty map + let empty_map = vec![]; + let input = Val::Map(empty_map); + func.call_and_post_return(&mut store, &[input.clone()], &mut output)?; + match &output[0] { + Val::Map(output_map) => assert_eq!(output_map.len(), 0), + _ => panic!("expected map"), + } + + Ok(()) +} + +#[test] +fn maps_complex_types() -> Result<()> { + let engine = super::engine(); + let mut store = Store::new(&engine, ()); + + // Test map> + let component = Component::new(&engine, make_echo_component("(map string (list u32))", 8))?; + let instance = Linker::new(&engine).instantiate(&mut store, &component)?; + let func = instance.get_func(&mut store, "echo").unwrap(); + + let input_map = vec![ + ( + Val::String("first".into()), + Val::List(vec![Val::U32(1), Val::U32(2), Val::U32(3)]), + ), + ( + Val::String("second".into()), + Val::List(vec![Val::U32(4), Val::U32(5)]), + ), + ]; + let input = Val::Map(input_map); + + let mut output = [Val::Bool(false)]; + func.call_and_post_return(&mut store, &[input.clone()], &mut output)?; + + // Verify round-trip + match &output[0] { + Val::Map(output_map) => { + assert_eq!(output_map.len(), 2); + // Check first entry + let first_entry = output_map + .iter() + .find(|(k, _)| *k == Val::String("first".into())); + match first_entry { + Some((_, Val::List(list))) => { + assert_eq!(list.len(), 3); + assert_eq!(list[0], Val::U32(1)); + assert_eq!(list[1], Val::U32(2)); + assert_eq!(list[2], Val::U32(3)); + } + _ => panic!("expected list"), + } + // Check second entry + let second_entry = output_map + .iter() + .find(|(k, _)| *k == Val::String("second".into())); + match second_entry { + Some((_, Val::List(list))) => { + assert_eq!(list.len(), 2); + assert_eq!(list[0], Val::U32(4)); + assert_eq!(list[1], Val::U32(5)); + } + _ => panic!("expected list"), + } + } + _ => panic!("expected map"), + } + + Ok(()) +} + +#[test] +fn maps_equality() -> Result<()> { + let map1 = vec![ + (Val::U32(1), Val::String("one".into())), + (Val::U32(2), Val::String("two".into())), + ]; + + let map2 = vec![ + (Val::U32(1), Val::String("one".into())), + (Val::U32(2), Val::String("two".into())), + ]; + + // Maps with same content in same order should be equal + assert_eq!(Val::Map(map1.clone()), Val::Map(map2)); + + // Different values should not be equal + let map3 = vec![ + (Val::U32(1), Val::String("different".into())), + (Val::U32(2), Val::String("two".into())), + ]; + assert_ne!(Val::Map(map1.clone()), Val::Map(map3)); + + // Different keys should not be equal + let map4 = vec![ + (Val::U32(3), Val::String("one".into())), + (Val::U32(2), Val::String("two".into())), + ]; + assert_ne!(Val::Map(map1), Val::Map(map4)); + + // Empty maps should be equal + let empty1: Vec<(Val, Val)> = vec![]; + let empty2: Vec<(Val, Val)> = vec![]; + assert_eq!(Val::Map(empty1), Val::Map(empty2)); + + Ok(()) +} + +#[test] +fn maps_duplicate_keys() -> Result<()> { + let engine = super::engine(); + let mut store = Store::new(&engine, ()); + + let component = Component::new(&engine, make_echo_component("(map u32 string)", 8))?; + let instance = Linker::new(&engine).instantiate(&mut store, &component)?; + let func = instance.get_func(&mut store, "echo").unwrap(); + + // Create a map with duplicate keys - Vec preserves all entries + let input_map = vec![ + (Val::U32(1), Val::String("first".into())), + (Val::U32(1), Val::String("last".into())), // Duplicate key + (Val::U32(2), Val::String("two".into())), + ]; + let input = Val::Map(input_map); + + let mut output = [Val::Bool(false)]; + func.call_and_post_return(&mut store, &[input.clone()], &mut output)?; + + // Verify all entries are preserved (Vec doesn't deduplicate) + match &output[0] { + Val::Map(output_map) => { + // Should have 3 entries (Vec preserves duplicates) + assert_eq!(output_map.len(), 3); + } + _ => panic!("expected map"), + } + + Ok(()) +} + +#[test] +fn maps_all_primitive_types() -> Result<()> { + let engine = super::engine(); + let mut store = Store::new(&engine, ()); + + // Test map + let component = Component::new(&engine, make_echo_component("(map u32 u32)", 8))?; + let instance = Linker::new(&engine).instantiate(&mut store, &component)?; + let func = instance.get_func(&mut store, "echo").unwrap(); + + let input_map = vec![(Val::U32(1), Val::U32(100)), (Val::U32(2), Val::U32(200))]; + let input = Val::Map(input_map); + + let mut output = [Val::Bool(false)]; + func.call_and_post_return(&mut store, &[input.clone()], &mut output)?; + assert_eq!(input, output[0]); + + // Test map + let component = Component::new(&engine, make_echo_component("(map string u32)", 8))?; + let instance = Linker::new(&engine).instantiate(&mut store, &component)?; + let func = instance.get_func(&mut store, "echo").unwrap(); + + let input_map = vec![ + (Val::String("one".into()), Val::U32(1)), + (Val::String("two".into()), Val::U32(2)), + ]; + let input = Val::Map(input_map); + + func.call_and_post_return(&mut store, &[input.clone()], &mut output)?; + assert_eq!(input, output[0]); + + Ok(()) +} + +#[test] +fn maps_alignment() -> Result<()> { + let engine = super::engine(); + let mut store = Store::new(&engine, ()); + + // Test map - key_size=1, value_align=8 + // This would fail with the alignment bug because value would be at offset 1 instead of 8 + let component = Component::new(&engine, make_echo_component("(map u8 u64)", 8))?; + let instance = Linker::new(&engine).instantiate(&mut store, &component)?; + let func = instance.get_func(&mut store, "echo").unwrap(); + + let input_map = vec![ + (Val::U8(1), Val::U64(100)), + (Val::U8(2), Val::U64(200)), + (Val::U8(3), Val::U64(300)), + ]; + let input = Val::Map(input_map); + + let mut output = [Val::Bool(false)]; + func.call_and_post_return(&mut store, &[input.clone()], &mut output)?; + assert_eq!(input, output[0]); + + // Test map - key_size=4, value_align=8 + // This would fail with the alignment bug because value would be at offset 4 instead of 8 + let component = Component::new(&engine, make_echo_component("(map u32 u64)", 8))?; + let instance = Linker::new(&engine).instantiate(&mut store, &component)?; + let func = instance.get_func(&mut store, "echo").unwrap(); + + let input_map = vec![(Val::U32(1), Val::U64(1000)), (Val::U32(2), Val::U64(2000))]; + let input = Val::Map(input_map); + + func.call_and_post_return(&mut store, &[input.clone()], &mut output)?; + assert_eq!(input, output[0]); + + // Test map - key_size=1, value_align=4 + // This would fail with the alignment bug because value would be at offset 1 instead of 4 + let component = Component::new(&engine, make_echo_component("(map u8 u32)", 8))?; + let instance = Linker::new(&engine).instantiate(&mut store, &component)?; + let func = instance.get_func(&mut store, "echo").unwrap(); + + let input_map = vec![(Val::U8(10), Val::U32(100)), (Val::U8(20), Val::U32(200))]; + let input = Val::Map(input_map); + + func.call_and_post_return(&mut store, &[input.clone()], &mut output)?; + assert_eq!(input, output[0]); + + // Test map - key_size=2, value_align=8 + // This would fail with the alignment bug because value would be at offset 2 instead of 8 + let component = Component::new(&engine, make_echo_component("(map u16 u64)", 8))?; + let instance = Linker::new(&engine).instantiate(&mut store, &component)?; + let func = instance.get_func(&mut store, "echo").unwrap(); + + let input_map = vec![ + (Val::U16(1), Val::U64(10000)), + (Val::U16(2), Val::U64(20000)), + ]; + let input = Val::Map(input_map); + + func.call_and_post_return(&mut store, &[input.clone()], &mut output)?; + assert_eq!(input, output[0]); + + Ok(()) +} + +#[test] +fn maps_large() -> Result<()> { + let engine = super::engine(); + let mut store = Store::new(&engine, ()); + + let component = Component::new(&engine, make_echo_component("(map u32 string)", 8))?; + let instance = Linker::new(&engine).instantiate(&mut store, &component)?; + let func = instance.get_func(&mut store, "echo").unwrap(); + + // Create a map with many entries + let input_map: Vec<(Val, Val)> = (0..100) + .map(|i| (Val::U32(i), Val::String(format!("value_{i}")))) + .collect(); + let input = Val::Map(input_map); + + let mut output = [Val::Bool(false)]; + func.call_and_post_return(&mut store, &[input.clone()], &mut output)?; + + // Verify all entries are present + match &output[0] { + Val::Map(output_map) => { + assert_eq!(output_map.len(), 100); + for i in 0..100 { + assert!(output_map.iter().any(|(k, v)| { + *k == Val::U32(i) && *v == Val::String(format!("value_{i}")) + })); + } + } + _ => panic!("expected map"), + } + + Ok(()) +} + #[test] fn records() -> Result<()> { let engine = super::engine(); diff --git a/tests/all/component_model/func.rs b/tests/all/component_model/func.rs index b74b1712b11c..43602cf95b70 100644 --- a/tests/all/component_model/func.rs +++ b/tests/all/component_model/func.rs @@ -3846,3 +3846,284 @@ fn host_call_with_concurrency_disabled() -> Result<()> { Ok(()) } + +/// Tests map types with misaligned key/value combinations through the adapter +/// trampoline (component-to-component translation). +/// +/// This specifically tests the alignment bug where the value offset was +/// calculated as `key_size` instead of `align(key_size, value_align)`. +/// For map, the value should be at offset 8 (not 1). +/// +/// NOTE: This test currently demonstrates that the adapter trampoline +/// compilation fails for map types with certain key/value combinations. +/// This is a known issue that needs to be fixed in trampoline.rs. +#[test] +#[ignore] // TODO: Fix trampoline alignment bug first +fn map_trampoline_alignment() -> Result<()> { + // Test map - key_size=1, value_align=8 + // With the alignment bug, value would be read/written at offset 1 instead of 8 + let component = format!( + r#" +(component + (import "host" (func $host (param "m" (map u8 u64)) (result (map u8 u64)))) + + ;; Component A: the "destination" that receives and echoes back + (component $dst + (import "echo" (func $echo (param "m" (map u8 u64)) (result (map u8 u64)))) + (core module $libc + (memory (export "memory") 1) + {REALLOC_AND_FREE} + ) + (core module $echo_mod + (import "" "echo" (func $echo (param i32 i32 i32))) + (import "libc" "memory" (memory 0)) + (import "libc" "realloc" (func $realloc (param i32 i32 i32 i32) (result i32))) + + (func (export "echo") (param i32 i32) (result i32) + (local $retptr i32) + (local.set $retptr + (call $realloc (i32.const 0) (i32.const 0) (i32.const 4) (i32.const 8))) + (call $echo (local.get 0) (local.get 1) (local.get $retptr)) + local.get $retptr + ) + ) + (core instance $libc (instantiate $libc)) + (core func $echo_lower (canon lower (func $echo) + (memory $libc "memory") + (realloc (func $libc "realloc")) + )) + (core instance $echo_inst (instantiate $echo_mod + (with "libc" (instance $libc)) + (with "" (instance (export "echo" (func $echo_lower)))) + )) + (func (export "echo2") (param "m" (map u8 u64)) (result (map u8 u64)) + (canon lift + (core func $echo_inst "echo") + (memory $libc "memory") + (realloc (func $libc "realloc")) + ) + ) + ) + + ;; Component B: the "source" that calls dst + (component $src + (import "echo" (func $echo (param "m" (map u8 u64)) (result (map u8 u64)))) + (core module $libc + (memory (export "memory") 1) + {REALLOC_AND_FREE} + ) + (core module $echo_mod + (import "" "echo" (func $echo (param i32 i32 i32))) + (import "libc" "memory" (memory 0)) + (import "libc" "realloc" (func $realloc (param i32 i32 i32 i32) (result i32))) + + (func (export "echo") (param i32 i32) (result i32) + (local $retptr i32) + (local.set $retptr + (call $realloc (i32.const 0) (i32.const 0) (i32.const 4) (i32.const 8))) + (call $echo (local.get 0) (local.get 1) (local.get $retptr)) + local.get $retptr + ) + ) + (core instance $libc (instantiate $libc)) + (core func $echo_lower (canon lower (func $echo) + (memory $libc "memory") + (realloc (func $libc "realloc")) + )) + (core instance $echo_inst (instantiate $echo_mod + (with "libc" (instance $libc)) + (with "" (instance (export "echo" (func $echo_lower)))) + )) + (func (export "echo2") (param "m" (map u8 u64)) (result (map u8 u64)) + (canon lift + (core func $echo_inst "echo") + (memory $libc "memory") + (realloc (func $libc "realloc")) + ) + ) + ) + + ;; Wire: host -> dst -> src creates adapter trampolines between components + (instance $dst (instantiate $dst (with "echo" (func $host)))) + (instance $src (instantiate $src (with "echo" (func $dst "echo2")))) + (export "echo" (func $src "echo2")) +) +"# + ); + + let mut config = Config::new(); + config.wasm_component_model(true); + config.wasm_component_model_map(true); + let engine = Engine::new(&config)?; + let component = Component::new(&engine, component)?; + + let mut store = Store::new(&engine, ()); + let mut linker = Linker::new(&engine); + + linker.root().func_new("host", |_cx, _ty, args, results| { + results[0] = args[0].clone(); + Ok(()) + })?; + + let instance = linker.instantiate(&mut store, &component)?; + let func = instance.get_func(&mut store, "echo").unwrap(); + + let test_data = vec![ + (Val::U8(1), Val::U64(0x0102030405060708)), + (Val::U8(2), Val::U64(0x1112131415161718)), + (Val::U8(255), Val::U64(0xFFFFFFFFFFFFFFFF)), + ]; + let input = Val::Map(test_data.clone()); + + let mut results = [Val::Bool(false)]; + func.call_and_post_return(&mut store, &[input], &mut results)?; + + match &results[0] { + Val::Map(output) => { + assert_eq!(output.len(), 3); + for (key, value) in &test_data { + assert!( + output.iter().any(|(k, v)| k == key && v == value), + "Missing or corrupted entry" + ); + } + } + _ => panic!("expected map"), + } + + Ok(()) +} + +/// Tests map alignment through trampoline +#[test] +#[ignore] // TODO: Fix trampoline alignment bug first +fn map_trampoline_alignment_u32_u64() -> Result<()> { + // Test map - key_size=4, value_align=8 + // With the alignment bug, value would be read/written at offset 4 instead of 8 + let component = format!( + r#" +(component + (import "host" (func $host (param "m" (map u32 u64)) (result (map u32 u64)))) + + (component $dst + (import "echo" (func $echo (param "m" (map u32 u64)) (result (map u32 u64)))) + (core module $libc + (memory (export "memory") 1) + {REALLOC_AND_FREE} + ) + (core module $echo_mod + (import "" "echo" (func $echo (param i32 i32 i32))) + (import "libc" "memory" (memory 0)) + (import "libc" "realloc" (func $realloc (param i32 i32 i32 i32) (result i32))) + + (func (export "echo") (param i32 i32) (result i32) + (local $retptr i32) + (local.set $retptr + (call $realloc (i32.const 0) (i32.const 0) (i32.const 4) (i32.const 8))) + (call $echo (local.get 0) (local.get 1) (local.get $retptr)) + local.get $retptr + ) + ) + (core instance $libc (instantiate $libc)) + (core func $echo_lower (canon lower (func $echo) + (memory $libc "memory") + (realloc (func $libc "realloc")) + )) + (core instance $echo_inst (instantiate $echo_mod + (with "libc" (instance $libc)) + (with "" (instance (export "echo" (func $echo_lower)))) + )) + (func (export "echo2") (param "m" (map u32 u64)) (result (map u32 u64)) + (canon lift + (core func $echo_inst "echo") + (memory $libc "memory") + (realloc (func $libc "realloc")) + ) + ) + ) + + (component $src + (import "echo" (func $echo (param "m" (map u32 u64)) (result (map u32 u64)))) + (core module $libc + (memory (export "memory") 1) + {REALLOC_AND_FREE} + ) + (core module $echo_mod + (import "" "echo" (func $echo (param i32 i32 i32))) + (import "libc" "memory" (memory 0)) + (import "libc" "realloc" (func $realloc (param i32 i32 i32 i32) (result i32))) + + (func (export "echo") (param i32 i32) (result i32) + (local $retptr i32) + (local.set $retptr + (call $realloc (i32.const 0) (i32.const 0) (i32.const 4) (i32.const 8))) + (call $echo (local.get 0) (local.get 1) (local.get $retptr)) + local.get $retptr + ) + ) + (core instance $libc (instantiate $libc)) + (core func $echo_lower (canon lower (func $echo) + (memory $libc "memory") + (realloc (func $libc "realloc")) + )) + (core instance $echo_inst (instantiate $echo_mod + (with "libc" (instance $libc)) + (with "" (instance (export "echo" (func $echo_lower)))) + )) + (func (export "echo2") (param "m" (map u32 u64)) (result (map u32 u64)) + (canon lift + (core func $echo_inst "echo") + (memory $libc "memory") + (realloc (func $libc "realloc")) + ) + ) + ) + + (instance $dst (instantiate $dst (with "echo" (func $host)))) + (instance $src (instantiate $src (with "echo" (func $dst "echo2")))) + (export "echo" (func $src "echo2")) +) +"# + ); + + let mut config = Config::new(); + config.wasm_component_model(true); + config.wasm_component_model_map(true); + let engine = Engine::new(&config)?; + let component = Component::new(&engine, component)?; + + let mut store = Store::new(&engine, ()); + let mut linker = Linker::new(&engine); + + linker.root().func_new("host", |_cx, _ty, args, results| { + results[0] = args[0].clone(); + Ok(()) + })?; + + let instance = linker.instantiate(&mut store, &component)?; + let func = instance.get_func(&mut store, "echo").unwrap(); + + let test_data = vec![ + (Val::U32(1), Val::U64(0x0102030405060708)), + (Val::U32(2), Val::U64(0x1112131415161718)), + ]; + let input = Val::Map(test_data.clone()); + + let mut results = [Val::Bool(false)]; + func.call_and_post_return(&mut store, &[input], &mut results)?; + + match &results[0] { + Val::Map(output) => { + assert_eq!(output.len(), 2); + for (key, value) in &test_data { + assert!( + output.iter().any(|(k, v)| k == key && v == value), + "Missing or corrupted entry" + ); + } + } + _ => panic!("expected map"), + } + + Ok(()) +} diff --git a/tests/misc_testsuite/component-model/types.wast b/tests/misc_testsuite/component-model/types.wast index 9596ebe6eece..701f13d5a3c8 100644 --- a/tests/misc_testsuite/component-model/types.wast +++ b/tests/misc_testsuite/component-model/types.wast @@ -1,3 +1,5 @@ +;;! component_model_map = true + (component (type string) (type (func (param "a" string))) @@ -19,6 +21,10 @@ (type $errno (enum "a" "b" "e")) (type (list $errno)) + (type (map u32 string)) + (type (map string u32)) + (type (map u32 u32)) + (type (map string (map u32 string))) (type $oflags (flags "read" "write" "exclusive")) (type (tuple $oflags $errno $r)) From bae8fdc9ffa3a1ea02c317eef6c04c18263d93fc Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Thu, 8 Jan 2026 21:46:50 -0500 Subject: [PATCH 02/26] Add Map and MapEntry classes to support key/value pairs in component model This commit introduces the Map and MapEntry classes, enabling the representation of map values in the component model. The Map class allows for the creation and iteration of key/value pairs, enhancing the functionality of the wasmtime component API. Additionally, the .gitignore file is updated to exclude build artifacts from the crates/c-api directory. --- .gitignore | 1 + .../c-api/include/wasmtime/component/val.hh | 84 ++++++++++++++++++- 2 files changed, 84 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 28a72405b61a..9e232c8bbb5d 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ publish vendor examples/build examples/.cache +crates/c-api/build *.coredump *.smt2 cranelift/isle/veri/veri_engine/test_output diff --git a/crates/c-api/include/wasmtime/component/val.hh b/crates/c-api/include/wasmtime/component/val.hh index ffc377ab2ee1..022a31e1fc36 100644 --- a/crates/c-api/include/wasmtime/component/val.hh +++ b/crates/c-api/include/wasmtime/component/val.hh @@ -417,6 +417,63 @@ public: } }; +/// \brief Class representing an entry in a map value. +class MapEntry { + friend class Map; + + wasmtime_component_valmap_entry_t entry; + + // This value can't be constructed or destructed, it's only used in iteration + // of `Map`. + MapEntry() = delete; + ~MapEntry() = delete; + + static const MapEntry * + from_capi(const wasmtime_component_valmap_entry_t *capi) { + return reinterpret_cast(capi); + } + +public: + /// \brief Returns the key of this map entry. + const Val &key() const { return *detail::val_from_capi(&entry.key); } + + /// \brief Returns the value of this map entry. + const Val &value() const { return *detail::val_from_capi(&entry.value); } +}; + +/// \brief Class representing a component model map, a collection of key/value +/// pairs. +class Map { + friend class Val; + + VAL_REPR(Map, wasmtime_component_valmap_t); + + static void transfer(Raw &&from, Raw &to) { + to = from; + from.size = 0; + from.data = nullptr; + } + + void copy(const Raw &other) { wasmtime_component_valmap_copy(&raw, &other); } + + void destroy() { wasmtime_component_valmap_delete(&raw); } + +public: + /// Creates a new map from the key/value pairs provided. + Map(std::vector> entries); + + /// \brief Returns the number of entries in the map. + size_t size() const { return raw.size; } + + /// \brief Returns an iterator to the beginning of the map entries. + const MapEntry *begin() const { return MapEntry::from_capi(raw.data); } + + /// \brief Returns an iterator to the end of the map entries. + const MapEntry *end() const { + return MapEntry::from_capi(raw.data + raw.size); + } +}; + class ResourceHost; /// Class representing a component model `resource` value which is either a @@ -644,6 +701,12 @@ public: Flags::transfer(std::move(f.raw), raw.of.flags); } + /// Creates a new map value. + Val(Map m) { + raw.kind = WASMTIME_COMPONENT_MAP; + Map::transfer(std::move(m.raw), raw.of.map); + } + /// Creates a new resource value. Val(ResourceAny r) { raw.kind = WASMTIME_COMPONENT_RESOURCE; @@ -833,11 +896,20 @@ public: /// \brief Returns whether this value is a resource. bool is_resource() const { return raw.kind == WASMTIME_COMPONENT_RESOURCE; } - /// \brief Returns the flags value, only valid if `is_flags()`. + /// \brief Returns the resource value, only valid if `is_resource()`. const ResourceAny &get_resource() const { assert(is_resource()); return *ResourceAny::from_capi(&raw.of.resource); } + + /// \brief Returns whether this value is a map. + bool is_map() const { return raw.kind == WASMTIME_COMPONENT_MAP; } + + /// \brief Returns the map value, only valid if `is_map()`. + const Map &get_map() const { + assert(is_map()); + return *Map::from_capi(&raw.of.map); + } }; #undef VAL_REPR @@ -852,6 +924,16 @@ inline Record::Record(std::vector> entries) { } } +inline Map::Map(std::vector> entries) { + wasmtime_component_valmap_new_uninit(&raw, entries.size()); + auto dst = raw.data; + for (auto &&[key, val] : entries) { + new (&dst->key) Val(std::move(key)); + new (&dst->value) Val(std::move(val)); + dst++; + } +} + inline List::List(std::vector values) { wasmtime_component_vallist_new_uninit(&raw, values.size()); auto dst = raw.data; From 3aaf4dc85fbc552f6778c2e3f21e8300e739b09f Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Fri, 16 Jan 2026 14:33:58 -0500 Subject: [PATCH 03/26] Add wasm_component_model_map configuration support --- crates/c-api/include/wasmtime/config.h | 9 +++++++++ crates/c-api/include/wasmtime/config.hh | 8 ++++++++ crates/c-api/src/component/component.rs | 5 +++++ crates/c-api/src/component/val.rs | 2 +- crates/c-api/tests/component/types.cc | 11 +++++++++-- 5 files changed, 32 insertions(+), 3 deletions(-) diff --git a/crates/c-api/include/wasmtime/config.h b/crates/c-api/include/wasmtime/config.h index 01f8b7a4b7a8..d7788a694b1d 100644 --- a/crates/c-api/include/wasmtime/config.h +++ b/crates/c-api/include/wasmtime/config.h @@ -833,6 +833,15 @@ WASMTIME_CONFIG_PROP(void, wasm_component_model, bool) */ WASMTIME_CONFIG_PROP(void, concurrency_support, bool) +/** + * \brief Configures whether the WebAssembly component-model map type will be + * enabled for compilation. + * + * For more information see the Rust documentation at + * https://docs.wasmtime.dev/api/wasmtime/struct.Config.html#method.wasm_component_model_map. + */ +WASMTIME_CONFIG_PROP(void, wasm_component_model_map, bool) + #endif // WASMTIME_FEATURE_COMPONENT_MODEL #ifdef __cplusplus diff --git a/crates/c-api/include/wasmtime/config.hh b/crates/c-api/include/wasmtime/config.hh index ff418d1fe427..12d57245fb2f 100644 --- a/crates/c-api/include/wasmtime/config.hh +++ b/crates/c-api/include/wasmtime/config.hh @@ -407,6 +407,14 @@ class Config { void wasm_component_model(bool enable) { wasmtime_config_wasm_component_model_set(ptr.get(), enable); } + + /// \brief Configures whether the WebAssembly component model map type will be + /// enabled + /// + /// https://docs.wasmtime.dev/api/wasmtime/struct.Config.html#method.wasm_component_model_map + void wasm_component_model_map(bool enable) { + wasmtime_config_wasm_component_model_map_set(ptr.get(), enable); + } #endif // WASMTIME_FEATURE_COMPONENT_MODEL #ifdef WASMTIME_FEATURE_PARALLEL_COMPILATION diff --git a/crates/c-api/src/component/component.rs b/crates/c-api/src/component/component.rs index cfededb3dc0b..88e39d7fc190 100644 --- a/crates/c-api/src/component/component.rs +++ b/crates/c-api/src/component/component.rs @@ -10,6 +10,11 @@ pub extern "C" fn wasmtime_config_wasm_component_model_set(c: &mut wasm_config_t c.config.wasm_component_model(enable); } +#[unsafe(no_mangle)] +pub extern "C" fn wasmtime_config_wasm_component_model_map_set(c: &mut wasm_config_t, enable: bool) { + c.config.wasm_component_model_map(enable); +} + #[derive(Clone)] #[repr(transparent)] pub struct wasmtime_component_t { diff --git a/crates/c-api/src/component/val.rs b/crates/c-api/src/component/val.rs index 274de0a8f35d..60177f2d2dc6 100644 --- a/crates/c-api/src/component/val.rs +++ b/crates/c-api/src/component/val.rs @@ -272,8 +272,8 @@ pub enum wasmtime_component_val_t { Option(Option>), Result(wasmtime_component_valresult_t), Flags(wasmtime_component_valflags_t), - Map(wasmtime_component_valmap_t), Resource(Box), + Map(wasmtime_component_valmap_t), } impl Default for wasmtime_component_val_t { diff --git a/crates/c-api/tests/component/types.cc b/crates/c-api/tests/component/types.cc index 5fc5dc25e578..db85cb105ef0 100644 --- a/crates/c-api/tests/component/types.cc +++ b/crates/c-api/tests/component/types.cc @@ -2,6 +2,7 @@ #include using namespace wasmtime::component; +using wasmtime::Config; using wasmtime::Engine; using wasmtime::ExternType; using wasmtime::Result; @@ -188,8 +189,14 @@ TEST(types, valtype_list) { } TEST(types, valtype_map) { - auto ty = - result("(component (import \"f\" (func (result (map u32 string)))))"); + Config config; + config.wasm_component_model_map(true); + Engine engine(std::move(config)); + auto component = + Component::compile(engine, + "(component (import \"f\" (func (result (map u32 string)))))") + .unwrap(); + auto ty = *component.type().import_get(engine, "f")->component_func().result(); EXPECT_TRUE(ty.is_map()); auto map_ty = ty.map(); EXPECT_TRUE(map_ty.key().is_u32()); From ef3df6d1f16bd13d0eddb6b749ced2667ce116f2 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Fri, 16 Jan 2026 15:18:35 -0500 Subject: [PATCH 04/26] Format code --- crates/c-api/src/component/component.rs | 5 ++++- crates/environ/src/fact/trampoline.rs | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/c-api/src/component/component.rs b/crates/c-api/src/component/component.rs index 88e39d7fc190..1db3dbaa55d5 100644 --- a/crates/c-api/src/component/component.rs +++ b/crates/c-api/src/component/component.rs @@ -11,7 +11,10 @@ pub extern "C" fn wasmtime_config_wasm_component_model_set(c: &mut wasm_config_t } #[unsafe(no_mangle)] -pub extern "C" fn wasmtime_config_wasm_component_model_map_set(c: &mut wasm_config_t, enable: bool) { +pub extern "C" fn wasmtime_config_wasm_component_model_map_set( + c: &mut wasm_config_t, + enable: bool, +) { c.config.wasm_component_model_map(enable); } diff --git a/crates/environ/src/fact/trampoline.rs b/crates/environ/src/fact/trampoline.rs index 950c68102995..97214f7614e0 100644 --- a/crates/environ/src/fact/trampoline.rs +++ b/crates/environ/src/fact/trampoline.rs @@ -21,8 +21,8 @@ use crate::component::{ PREPARE_ASYNC_WITH_RESULT, START_FLAG_ASYNC_CALLEE, StringEncoding, Transcode, TypeComponentLocalErrorContextTableIndex, TypeEnumIndex, TypeFixedLengthListIndex, TypeFlagsIndex, TypeFutureTableIndex, TypeListIndex, TypeMapIndex, TypeOptionIndex, - TypeRecordIndex, TypeResourceTableIndex, TypeResultIndex, TypeStreamTableIndex, - TypeTupleIndex, TypeVariantIndex, VariantInfo, + TypeRecordIndex, TypeResourceTableIndex, TypeResultIndex, TypeStreamTableIndex, TypeTupleIndex, + TypeVariantIndex, VariantInfo, }; use crate::fact::signature::Signature; use crate::fact::transcode::Transcoder; From 153c168777453750bd9bad80d45c9bfcfe8a02de Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Fri, 16 Jan 2026 15:28:01 -0500 Subject: [PATCH 05/26] Format C code --- crates/c-api/tests/component/types.cc | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/c-api/tests/component/types.cc b/crates/c-api/tests/component/types.cc index db85cb105ef0..5953366f48e0 100644 --- a/crates/c-api/tests/component/types.cc +++ b/crates/c-api/tests/component/types.cc @@ -193,10 +193,11 @@ TEST(types, valtype_map) { config.wasm_component_model_map(true); Engine engine(std::move(config)); auto component = - Component::compile(engine, - "(component (import \"f\" (func (result (map u32 string)))))") + Component::compile( + engine, "(component (import \"f\" (func (result (map u32 string)))))") .unwrap(); - auto ty = *component.type().import_get(engine, "f")->component_func().result(); + auto ty = + *component.type().import_get(engine, "f")->component_func().result(); EXPECT_TRUE(ty.is_map()); auto map_ty = ty.map(); EXPECT_TRUE(map_ty.key().is_u32()); From f00aedd8e6cf5ad98347e4d1d67609a83107265b Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Fri, 6 Feb 2026 02:00:39 -0500 Subject: [PATCH 06/26] Enhance component model to support HashMap type This commit introduces support for HashMap in the component model, allowing maps to be represented as list> in the canonical ABI. It includes implementations for the ComponentType, Lower, and Lift traits for HashMap, enabling type checking, lowering to flat representations, and lifting from memory. Additionally, the maximum depth for type generation in the fuzzing utility is updated to accommodate the new map type. --- crates/test-util/src/component_fuzz.rs | 5 +- .../src/runtime/component/func/typed.rs | 204 ++++++++++++++++++ 2 files changed, 205 insertions(+), 4 deletions(-) diff --git a/crates/test-util/src/component_fuzz.rs b/crates/test-util/src/component_fuzz.rs index 94b2c2d88e02..1845efc4a3e7 100644 --- a/crates/test-util/src/component_fuzz.rs +++ b/crates/test-util/src/component_fuzz.rs @@ -159,10 +159,7 @@ impl Type { fuel: &mut u32, ) -> arbitrary::Result { *fuel = fuel.saturating_sub(1); - // Note: Map types (case 21) are disabled because HashMap doesn't implement - // ComponentType, Lift, or Lower yet. When those implementations are added, - // change this to 21. - let max = if depth == 0 || *fuel == 0 { 12 } else { 20 }; + let max = if depth == 0 || *fuel == 0 { 12 } else { 21 }; Ok(match u.int_in_range(0..=max)? { 0 => Type::Bool, 1 => Type::S8, diff --git a/crates/wasmtime/src/runtime/component/func/typed.rs b/crates/wasmtime/src/runtime/component/func/typed.rs index e8e2ddd5cc3e..551dfea5af48 100644 --- a/crates/wasmtime/src/runtime/component/func/typed.rs +++ b/crates/wasmtime/src/runtime/component/func/typed.rs @@ -6,10 +6,12 @@ use crate::prelude::*; use crate::{AsContextMut, StoreContext, StoreContextMut, ValRaw}; use alloc::borrow::Cow; use core::fmt; +use core::hash::Hash; use core::iter; use core::marker; use core::mem::{self, MaybeUninit}; use core::str; +use std::collections::HashMap; use wasmtime_environ::component::{ CanonicalAbiInfo, InterfaceType, MAX_FLAT_PARAMS, MAX_FLAT_RESULTS, OptionsIndex, StringEncoding, VariantInfo, @@ -617,6 +619,7 @@ pub unsafe trait ComponentNamedList: ComponentType {} /// | `result` | `Result` | /// | `string` | `String`, `&str`, or [`WasmStr`] | /// | `list` | `Vec`, `&[T]`, or [`WasmList`] | +/// | `map` | `HashMap` | /// | `own`, `borrow` | [`Resource`] or [`ResourceAny`] | /// | `record` | [`#[derive(ComponentType)]`][d-cm] | /// | `variant` | [`#[derive(ComponentType)]`][d-cm] | @@ -2082,6 +2085,207 @@ unsafe impl Lift for WasmList { } } +// ============================================================================= +// HashMap support for component model `map` +// +// Maps are represented as `list>` in the canonical ABI, so the +// lowered form is a (pointer, length) pair just like lists. + +unsafe impl ComponentType for HashMap +where + K: ComponentType, + V: ComponentType, +{ + type Lower = [ValRaw; 2]; + + const ABI: CanonicalAbiInfo = CanonicalAbiInfo::POINTER_PAIR; + + fn typecheck(ty: &InterfaceType, types: &InstanceType<'_>) -> Result<()> { + match ty { + InterfaceType::Map(t) => { + let map_ty = &types.types[*t]; + K::typecheck(&map_ty.key, types)?; + V::typecheck(&map_ty.value, types)?; + Ok(()) + } + other => bail!("expected `map` found `{}`", desc(other)), + } + } +} + +unsafe impl Lower for HashMap +where + K: Lower, + V: Lower, +{ + fn linear_lower_to_flat( + &self, + cx: &mut LowerContext<'_, U>, + ty: InterfaceType, + dst: &mut MaybeUninit<[ValRaw; 2]>, + ) -> Result<()> { + let (key_ty, value_ty) = match ty { + InterfaceType::Map(i) => { + let m = &cx.types[i]; + (m.key, m.value) + } + _ => bad_type_info(), + }; + let (ptr, len) = lower_map(cx, key_ty, value_ty, self)?; + // See "WRITEPTR64" above for why this is always storing a 64-bit + // integer. + map_maybe_uninit!(dst[0]).write(ValRaw::i64(ptr as i64)); + map_maybe_uninit!(dst[1]).write(ValRaw::i64(len as i64)); + Ok(()) + } + + fn linear_lower_to_memory( + &self, + cx: &mut LowerContext<'_, U>, + ty: InterfaceType, + offset: usize, + ) -> Result<()> { + let (key_ty, value_ty) = match ty { + InterfaceType::Map(i) => { + let m = &cx.types[i]; + (m.key, m.value) + } + _ => bad_type_info(), + }; + debug_assert!(offset % (Self::ALIGN32 as usize) == 0); + let (ptr, len) = lower_map(cx, key_ty, value_ty, self)?; + *cx.get(offset + 0) = u32::try_from(ptr).unwrap().to_le_bytes(); + *cx.get(offset + 4) = u32::try_from(len).unwrap().to_le_bytes(); + Ok(()) + } +} + +fn lower_map( + cx: &mut LowerContext<'_, U>, + key_ty: InterfaceType, + value_ty: InterfaceType, + map: &HashMap, +) -> Result<(usize, usize)> +where + K: Lower, + V: Lower, +{ + // Calculate the tuple layout: each entry is a (key, value) record. + let tuple_abi = CanonicalAbiInfo::record_static(&[K::ABI, V::ABI]); + let tuple_size = tuple_abi.size32 as usize; + let tuple_align = tuple_abi.align32; + + let size = map + .len() + .checked_mul(tuple_size) + .ok_or_else(|| format_err!("size overflow copying a map"))?; + let ptr = cx.realloc(0, 0, tuple_align, size)?; + + let mut entry_offset = ptr; + for (key, value) in map.iter() { + // Lower key at the start of the tuple + let mut field_offset = 0usize; + let key_field = K::ABI.next_field32_size(&mut field_offset); + key.linear_lower_to_memory(cx, key_ty, entry_offset + key_field)?; + // Lower value at its aligned offset within the tuple + let value_field = V::ABI.next_field32_size(&mut field_offset); + value.linear_lower_to_memory(cx, value_ty, entry_offset + value_field)?; + entry_offset += tuple_size; + } + + Ok((ptr, map.len())) +} + +unsafe impl Lift for HashMap +where + K: Lift + Eq + Hash, + V: Lift, +{ + fn linear_lift_from_flat( + cx: &mut LiftContext<'_>, + ty: InterfaceType, + src: &Self::Lower, + ) -> Result { + let (key_ty, value_ty) = match ty { + InterfaceType::Map(i) => { + let m = &cx.types[i]; + (m.key, m.value) + } + _ => bad_type_info(), + }; + // FIXME(#4311): needs memory64 treatment + let ptr = src[0].get_u32(); + let len = src[1].get_u32(); + let (ptr, len) = (usize::try_from(ptr)?, usize::try_from(len)?); + lift_map(cx, key_ty, value_ty, ptr, len) + } + + fn linear_lift_from_memory( + cx: &mut LiftContext<'_>, + ty: InterfaceType, + bytes: &[u8], + ) -> Result { + let (key_ty, value_ty) = match ty { + InterfaceType::Map(i) => { + let m = &cx.types[i]; + (m.key, m.value) + } + _ => bad_type_info(), + }; + debug_assert!((bytes.as_ptr() as usize) % (Self::ALIGN32 as usize) == 0); + // FIXME(#4311): needs memory64 treatment + let ptr = u32::from_le_bytes(bytes[..4].try_into().unwrap()); + let len = u32::from_le_bytes(bytes[4..].try_into().unwrap()); + let (ptr, len) = (usize::try_from(ptr)?, usize::try_from(len)?); + lift_map(cx, key_ty, value_ty, ptr, len) + } +} + +fn lift_map( + cx: &mut LiftContext<'_>, + key_ty: InterfaceType, + value_ty: InterfaceType, + ptr: usize, + len: usize, +) -> Result> +where + K: Lift + Eq + Hash, + V: Lift, +{ + let tuple_abi = CanonicalAbiInfo::record_static(&[K::ABI, V::ABI]); + let tuple_size = tuple_abi.size32 as usize; + let tuple_align = tuple_abi.align32 as usize; + + match len + .checked_mul(tuple_size) + .and_then(|total| ptr.checked_add(total)) + { + Some(n) if n <= cx.memory().len() => {} + _ => bail!("map pointer/length out of bounds of memory"), + } + if ptr % tuple_align != 0 { + bail!("map pointer is not aligned"); + } + + let mut result = HashMap::with_capacity(len); + for i in 0..len { + let entry_base = ptr + (i * tuple_size); + + let mut field_offset = 0usize; + let key_field = K::ABI.next_field32_size(&mut field_offset); + let key_bytes = &cx.memory()[entry_base + key_field..][..K::SIZE32]; + let key = K::linear_lift_from_memory(cx, key_ty, key_bytes)?; + + let value_field = V::ABI.next_field32_size(&mut field_offset); + let value_bytes = &cx.memory()[entry_base + value_field..][..V::SIZE32]; + let value = V::linear_lift_from_memory(cx, value_ty, value_bytes)?; + + result.insert(key, value); + } + + Ok(result) +} + /// Verify that the given wasm type is a tuple with the expected fields in the right order. fn typecheck_tuple( ty: &InterfaceType, From 890a2c91b6cd9678873bd7a538ae4cc1ac72ef2c Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Fri, 6 Feb 2026 02:15:34 -0500 Subject: [PATCH 07/26] Refactor component configuration to introduce map support This commit removes the previous wasm features configuration and adds new functions for creating a map-configured engine. The `map_config` and `map_engine` functions are introduced to facilitate the use of the component model with maps in tests, ensuring that the engine is properly configured for map types in the component model. --- crates/test-util/src/component.rs | 11 ++++++++++- tests/all/component_model.rs | 2 +- tests/all/component_model/dynamic.rs | 12 ++++++------ 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/crates/test-util/src/component.rs b/crates/test-util/src/component.rs index f76587f1c935..77ca3d64f5bc 100644 --- a/crates/test-util/src/component.rs +++ b/crates/test-util/src/component.rs @@ -12,7 +12,6 @@ pub fn config() -> Config { let mut config = Config::new(); config.wasm_component_model(true); - config.wasm_features(wasmtime::WasmFeatures::CM_MAP, true); // When `WASMTIME_TEST_NO_HOG_MEMORY` is set it means we're in qemu. The // component model tests create a disproportionate number of instances so @@ -28,6 +27,16 @@ pub fn engine() -> Engine { Engine::new(&config()).unwrap() } +pub fn map_config() -> Config { + let mut config = config(); + config.wasm_component_model_map(true); + config +} + +pub fn map_engine() -> Engine { + Engine::new(&map_config()).unwrap() +} + pub fn async_engine() -> Engine { Engine::default() } diff --git a/tests/all/component_model.rs b/tests/all/component_model.rs index 785bf398fff6..b7e46fcc1377 100644 --- a/tests/all/component_model.rs +++ b/tests/all/component_model.rs @@ -5,7 +5,7 @@ use wasmtime::component::{ }; use wasmtime::{Config, Result, Store}; use wasmtime_component_util::REALLOC_AND_FREE; -use wasmtime_test_util::component::{async_engine, config, engine}; +use wasmtime_test_util::component::{async_engine, config, engine, map_engine}; mod aot; mod r#async; diff --git a/tests/all/component_model/dynamic.rs b/tests/all/component_model/dynamic.rs index 91b843f7b6aa..2e247e424d9b 100644 --- a/tests/all/component_model/dynamic.rs +++ b/tests/all/component_model/dynamic.rs @@ -138,7 +138,7 @@ fn lists() -> Result<()> { #[test] fn maps() -> Result<()> { - let engine = super::engine(); + let engine = super::map_engine(); let mut store = Store::new(&engine, ()); let component = Component::new(&engine, make_echo_component("(map u32 string)", 8))?; @@ -217,7 +217,7 @@ fn maps() -> Result<()> { #[test] fn maps_complex_types() -> Result<()> { - let engine = super::engine(); + let engine = super::map_engine(); let mut store = Store::new(&engine, ()); // Test map> @@ -315,7 +315,7 @@ fn maps_equality() -> Result<()> { #[test] fn maps_duplicate_keys() -> Result<()> { - let engine = super::engine(); + let engine = super::map_engine(); let mut store = Store::new(&engine, ()); let component = Component::new(&engine, make_echo_component("(map u32 string)", 8))?; @@ -347,7 +347,7 @@ fn maps_duplicate_keys() -> Result<()> { #[test] fn maps_all_primitive_types() -> Result<()> { - let engine = super::engine(); + let engine = super::map_engine(); let mut store = Store::new(&engine, ()); // Test map @@ -381,7 +381,7 @@ fn maps_all_primitive_types() -> Result<()> { #[test] fn maps_alignment() -> Result<()> { - let engine = super::engine(); + let engine = super::map_engine(); let mut store = Store::new(&engine, ()); // Test map - key_size=1, value_align=8 @@ -445,7 +445,7 @@ fn maps_alignment() -> Result<()> { #[test] fn maps_large() -> Result<()> { - let engine = super::engine(); + let engine = super::map_engine(); let mut store = Store::new(&engine, ()); let component = Component::new(&engine, make_echo_component("(map u32 string)", 8))?; From c05f4abe41bc642b870c9b8129b54d337d0b6cd5 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Fri, 6 Feb 2026 02:30:16 -0500 Subject: [PATCH 08/26] Add new WAST test for map types and remove map type definitions from existing tests This commit introduces a new WAST test file specifically for testing various map types in the component model. Additionally, it removes the redundant map type definitions from the existing types.wast file to streamline the test suite. --- tests/misc_testsuite/component-model/map-types.wast | 8 ++++++++ tests/misc_testsuite/component-model/types.wast | 6 ------ 2 files changed, 8 insertions(+), 6 deletions(-) create mode 100644 tests/misc_testsuite/component-model/map-types.wast diff --git a/tests/misc_testsuite/component-model/map-types.wast b/tests/misc_testsuite/component-model/map-types.wast new file mode 100644 index 000000000000..74f4f34ac164 --- /dev/null +++ b/tests/misc_testsuite/component-model/map-types.wast @@ -0,0 +1,8 @@ +;;! component_model_map = true + +(component + (type (map u32 string)) + (type (map string u32)) + (type (map u32 u32)) + (type (map string (map u32 string))) +) diff --git a/tests/misc_testsuite/component-model/types.wast b/tests/misc_testsuite/component-model/types.wast index 701f13d5a3c8..9596ebe6eece 100644 --- a/tests/misc_testsuite/component-model/types.wast +++ b/tests/misc_testsuite/component-model/types.wast @@ -1,5 +1,3 @@ -;;! component_model_map = true - (component (type string) (type (func (param "a" string))) @@ -21,10 +19,6 @@ (type $errno (enum "a" "b" "e")) (type (list $errno)) - (type (map u32 string)) - (type (map string u32)) - (type (map u32 u32)) - (type (map string (map u32 string))) (type $oflags (flags "read" "write" "exclusive")) (type (tuple $oflags $errno $r)) From a219108d32d0161430d7ba8dd2ed03ca5747b4c8 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Fri, 6 Feb 2026 03:47:20 -0500 Subject: [PATCH 09/26] Update component fuzzing and dynamic tests to replace call_and_post_return with call --- crates/test-util/src/component_fuzz.rs | 2 +- tests/all/component_model/dynamic.rs | 26 +++++++++++++------------- tests/all/component_model/func.rs | 4 ++-- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/crates/test-util/src/component_fuzz.rs b/crates/test-util/src/component_fuzz.rs index 1845efc4a3e7..e1a121022d81 100644 --- a/crates/test-util/src/component_fuzz.rs +++ b/crates/test-util/src/component_fuzz.rs @@ -1282,7 +1282,7 @@ pub fn rust_type(ty: &Type, name_counter: &mut u32, declarations: &mut TokenStre }; declarations.extend(quote! { - #[derive(ComponentType, Lift, Lower, PartialEq, Debug, Copy, Clone, Arbitrary)] + #[derive(ComponentType, Lift, Lower, PartialEq, Eq, Hash, Debug, Copy, Clone, Arbitrary)] #[component(enum)] #[repr(#repr)] enum #name { diff --git a/tests/all/component_model/dynamic.rs b/tests/all/component_model/dynamic.rs index 2e247e424d9b..460989f04c7f 100644 --- a/tests/all/component_model/dynamic.rs +++ b/tests/all/component_model/dynamic.rs @@ -153,7 +153,7 @@ fn maps() -> Result<()> { let input = Val::Map(input_map); let mut output = [Val::Bool(false)]; - func.call_and_post_return(&mut store, &[input.clone()], &mut output)?; + func.call(&mut store, &[input.clone()], &mut output)?; // Maps should round-trip correctly match &output[0] { @@ -187,7 +187,7 @@ fn maps() -> Result<()> { let err_map = vec![(Val::String("key".into()), Val::String("value".into()))]; let err = Val::Map(err_map); let err = func2 - .call_and_post_return(&mut store2, &[err], &mut output) + .call(&mut store2, &[err], &mut output) .unwrap_err(); assert!(err.to_string().contains("type mismatch"), "{err}"); @@ -199,14 +199,14 @@ fn maps() -> Result<()> { let err_map2 = vec![(Val::U32(1), Val::U32(42))]; let err = Val::Map(err_map2); let err = func3 - .call_and_post_return(&mut store3, &[err], &mut output) + .call(&mut store3, &[err], &mut output) .unwrap_err(); assert!(err.to_string().contains("type mismatch"), "{err}"); // Test empty map let empty_map = vec![]; let input = Val::Map(empty_map); - func.call_and_post_return(&mut store, &[input.clone()], &mut output)?; + func.call(&mut store, &[input.clone()], &mut output)?; match &output[0] { Val::Map(output_map) => assert_eq!(output_map.len(), 0), _ => panic!("expected map"), @@ -238,7 +238,7 @@ fn maps_complex_types() -> Result<()> { let input = Val::Map(input_map); let mut output = [Val::Bool(false)]; - func.call_and_post_return(&mut store, &[input.clone()], &mut output)?; + func.call(&mut store, &[input.clone()], &mut output)?; // Verify round-trip match &output[0] { @@ -331,7 +331,7 @@ fn maps_duplicate_keys() -> Result<()> { let input = Val::Map(input_map); let mut output = [Val::Bool(false)]; - func.call_and_post_return(&mut store, &[input.clone()], &mut output)?; + func.call(&mut store, &[input.clone()], &mut output)?; // Verify all entries are preserved (Vec doesn't deduplicate) match &output[0] { @@ -359,7 +359,7 @@ fn maps_all_primitive_types() -> Result<()> { let input = Val::Map(input_map); let mut output = [Val::Bool(false)]; - func.call_and_post_return(&mut store, &[input.clone()], &mut output)?; + func.call(&mut store, &[input.clone()], &mut output)?; assert_eq!(input, output[0]); // Test map @@ -373,7 +373,7 @@ fn maps_all_primitive_types() -> Result<()> { ]; let input = Val::Map(input_map); - func.call_and_post_return(&mut store, &[input.clone()], &mut output)?; + func.call(&mut store, &[input.clone()], &mut output)?; assert_eq!(input, output[0]); Ok(()) @@ -398,7 +398,7 @@ fn maps_alignment() -> Result<()> { let input = Val::Map(input_map); let mut output = [Val::Bool(false)]; - func.call_and_post_return(&mut store, &[input.clone()], &mut output)?; + func.call(&mut store, &[input.clone()], &mut output)?; assert_eq!(input, output[0]); // Test map - key_size=4, value_align=8 @@ -410,7 +410,7 @@ fn maps_alignment() -> Result<()> { let input_map = vec![(Val::U32(1), Val::U64(1000)), (Val::U32(2), Val::U64(2000))]; let input = Val::Map(input_map); - func.call_and_post_return(&mut store, &[input.clone()], &mut output)?; + func.call(&mut store, &[input.clone()], &mut output)?; assert_eq!(input, output[0]); // Test map - key_size=1, value_align=4 @@ -422,7 +422,7 @@ fn maps_alignment() -> Result<()> { let input_map = vec![(Val::U8(10), Val::U32(100)), (Val::U8(20), Val::U32(200))]; let input = Val::Map(input_map); - func.call_and_post_return(&mut store, &[input.clone()], &mut output)?; + func.call(&mut store, &[input.clone()], &mut output)?; assert_eq!(input, output[0]); // Test map - key_size=2, value_align=8 @@ -437,7 +437,7 @@ fn maps_alignment() -> Result<()> { ]; let input = Val::Map(input_map); - func.call_and_post_return(&mut store, &[input.clone()], &mut output)?; + func.call(&mut store, &[input.clone()], &mut output)?; assert_eq!(input, output[0]); Ok(()) @@ -459,7 +459,7 @@ fn maps_large() -> Result<()> { let input = Val::Map(input_map); let mut output = [Val::Bool(false)]; - func.call_and_post_return(&mut store, &[input.clone()], &mut output)?; + func.call(&mut store, &[input.clone()], &mut output)?; // Verify all entries are present match &output[0] { diff --git a/tests/all/component_model/func.rs b/tests/all/component_model/func.rs index 43602cf95b70..4ba611c3d088 100644 --- a/tests/all/component_model/func.rs +++ b/tests/all/component_model/func.rs @@ -3976,7 +3976,7 @@ fn map_trampoline_alignment() -> Result<()> { let input = Val::Map(test_data.clone()); let mut results = [Val::Bool(false)]; - func.call_and_post_return(&mut store, &[input], &mut results)?; + func.call(&mut store, &[input], &mut results)?; match &results[0] { Val::Map(output) => { @@ -4110,7 +4110,7 @@ fn map_trampoline_alignment_u32_u64() -> Result<()> { let input = Val::Map(test_data.clone()); let mut results = [Val::Bool(false)]; - func.call_and_post_return(&mut store, &[input], &mut results)?; + func.call(&mut store, &[input], &mut results)?; match &results[0] { Val::Map(output) => { From 59f95f398361b0312adb9fcd3b413fc5d494beb3 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Fri, 6 Feb 2026 03:56:18 -0500 Subject: [PATCH 10/26] Format code --- tests/all/component_model/dynamic.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/all/component_model/dynamic.rs b/tests/all/component_model/dynamic.rs index 460989f04c7f..bb63f8020623 100644 --- a/tests/all/component_model/dynamic.rs +++ b/tests/all/component_model/dynamic.rs @@ -186,9 +186,7 @@ fn maps() -> Result<()> { let err_map = vec![(Val::String("key".into()), Val::String("value".into()))]; let err = Val::Map(err_map); - let err = func2 - .call(&mut store2, &[err], &mut output) - .unwrap_err(); + let err = func2.call(&mut store2, &[err], &mut output).unwrap_err(); assert!(err.to_string().contains("type mismatch"), "{err}"); // Sad path: type mismatch (wrong value type) @@ -198,9 +196,7 @@ fn maps() -> Result<()> { let err_map2 = vec![(Val::U32(1), Val::U32(42))]; let err = Val::Map(err_map2); - let err = func3 - .call(&mut store3, &[err], &mut output) - .unwrap_err(); + let err = func3.call(&mut store3, &[err], &mut output).unwrap_err(); assert!(err.to_string().contains("type mismatch"), "{err}"); // Test empty map From 33891f4fce31f5195087ef618b72d9cdb34815a3 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Fri, 6 Feb 2026 04:43:09 -0500 Subject: [PATCH 11/26] Refactor HashMap usage in typed.rs to use wasmtime_environ collections --- crates/wasmtime/src/runtime/component/func/typed.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/func/typed.rs b/crates/wasmtime/src/runtime/component/func/typed.rs index 551dfea5af48..dfa552c0bea8 100644 --- a/crates/wasmtime/src/runtime/component/func/typed.rs +++ b/crates/wasmtime/src/runtime/component/func/typed.rs @@ -11,7 +11,7 @@ use core::iter; use core::marker; use core::mem::{self, MaybeUninit}; use core::str; -use std::collections::HashMap; +use wasmtime_environ::collections::HashMap; use wasmtime_environ::component::{ CanonicalAbiInfo, InterfaceType, MAX_FLAT_PARAMS, MAX_FLAT_RESULTS, OptionsIndex, StringEncoding, VariantInfo, @@ -2186,10 +2186,10 @@ where // Lower key at the start of the tuple let mut field_offset = 0usize; let key_field = K::ABI.next_field32_size(&mut field_offset); - key.linear_lower_to_memory(cx, key_ty, entry_offset + key_field)?; + ::linear_lower_to_memory(key, cx, key_ty, entry_offset + key_field)?; // Lower value at its aligned offset within the tuple let value_field = V::ABI.next_field32_size(&mut field_offset); - value.linear_lower_to_memory(cx, value_ty, entry_offset + value_field)?; + ::linear_lower_to_memory(value, cx, value_ty, entry_offset + value_field)?; entry_offset += tuple_size; } From d02a7f88d7bd8a5b178e453ee360d58a2de6f3b3 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Fri, 6 Feb 2026 06:11:55 -0500 Subject: [PATCH 12/26] Fix HashMap initialization and insertion to handle potential errors in typed.rs --- crates/wasmtime/src/runtime/component/func/typed.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/func/typed.rs b/crates/wasmtime/src/runtime/component/func/typed.rs index dfa552c0bea8..43940e8fef21 100644 --- a/crates/wasmtime/src/runtime/component/func/typed.rs +++ b/crates/wasmtime/src/runtime/component/func/typed.rs @@ -2267,7 +2267,7 @@ where bail!("map pointer is not aligned"); } - let mut result = HashMap::with_capacity(len); + let mut result = HashMap::with_capacity(len)?; for i in 0..len { let entry_base = ptr + (i * tuple_size); @@ -2280,7 +2280,7 @@ where let value_bytes = &cx.memory()[entry_base + value_field..][..V::SIZE32]; let value = V::linear_lift_from_memory(cx, value_ty, value_bytes)?; - result.insert(key, value); + result.insert(key, value)?; } Ok(result) From a40bb649c63a0f6161e154542670f62343a9233e Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Fri, 6 Feb 2026 06:44:33 -0500 Subject: [PATCH 13/26] Refactor HashMap handling in typed.rs to use lower_map_iter for improved iteration and memory management. Introduce new implementations for ComponentType, Lower, and Lift traits for std::collections::HashMap, enhancing support for map types in the component model. --- .../src/runtime/component/func/typed.rs | 190 +++++++++++++++++- 1 file changed, 180 insertions(+), 10 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/func/typed.rs b/crates/wasmtime/src/runtime/component/func/typed.rs index 43940e8fef21..f85236d9172e 100644 --- a/crates/wasmtime/src/runtime/component/func/typed.rs +++ b/crates/wasmtime/src/runtime/component/func/typed.rs @@ -2131,7 +2131,7 @@ where } _ => bad_type_info(), }; - let (ptr, len) = lower_map(cx, key_ty, value_ty, self)?; + let (ptr, len) = lower_map_iter(cx, key_ty, value_ty, self.len(), self.iter())?; // See "WRITEPTR64" above for why this is always storing a 64-bit // integer. map_maybe_uninit!(dst[0]).write(ValRaw::i64(ptr as i64)); @@ -2153,36 +2153,36 @@ where _ => bad_type_info(), }; debug_assert!(offset % (Self::ALIGN32 as usize) == 0); - let (ptr, len) = lower_map(cx, key_ty, value_ty, self)?; + let (ptr, len) = lower_map_iter(cx, key_ty, value_ty, self.len(), self.iter())?; *cx.get(offset + 0) = u32::try_from(ptr).unwrap().to_le_bytes(); *cx.get(offset + 4) = u32::try_from(len).unwrap().to_le_bytes(); Ok(()) } } -fn lower_map( +fn lower_map_iter<'a, K, V, U>( cx: &mut LowerContext<'_, U>, key_ty: InterfaceType, value_ty: InterfaceType, - map: &HashMap, + len: usize, + iter: impl Iterator, ) -> Result<(usize, usize)> where - K: Lower, - V: Lower, + K: Lower + 'a, + V: Lower + 'a, { // Calculate the tuple layout: each entry is a (key, value) record. let tuple_abi = CanonicalAbiInfo::record_static(&[K::ABI, V::ABI]); let tuple_size = tuple_abi.size32 as usize; let tuple_align = tuple_abi.align32; - let size = map - .len() + let size = len .checked_mul(tuple_size) .ok_or_else(|| format_err!("size overflow copying a map"))?; let ptr = cx.realloc(0, 0, tuple_align, size)?; let mut entry_offset = ptr; - for (key, value) in map.iter() { + for (key, value) in iter { // Lower key at the start of the tuple let mut field_offset = 0usize; let key_field = K::ABI.next_field32_size(&mut field_offset); @@ -2193,7 +2193,7 @@ where entry_offset += tuple_size; } - Ok((ptr, map.len())) + Ok((ptr, len)) } unsafe impl Lift for HashMap @@ -2286,6 +2286,176 @@ where Ok(result) } +// ============================================================================= +// std::collections::HashMap support for component model `map` +// +// This mirrors the wasmtime_environ::collections::HashMap implementation above +// but works with the standard library HashMap type, which is what users will +// naturally reach for. + +#[cfg(feature = "std")] +unsafe impl ComponentType for std::collections::HashMap +where + K: ComponentType, + V: ComponentType, +{ + type Lower = [ValRaw; 2]; + + const ABI: CanonicalAbiInfo = CanonicalAbiInfo::POINTER_PAIR; + + fn typecheck(ty: &InterfaceType, types: &InstanceType<'_>) -> Result<()> { + match ty { + InterfaceType::Map(t) => { + let map_ty = &types.types[*t]; + K::typecheck(&map_ty.key, types)?; + V::typecheck(&map_ty.value, types)?; + Ok(()) + } + other => bail!("expected `map` found `{}`", desc(other)), + } + } +} + +#[cfg(feature = "std")] +unsafe impl Lower for std::collections::HashMap +where + K: Lower, + V: Lower, +{ + fn linear_lower_to_flat( + &self, + cx: &mut LowerContext<'_, U>, + ty: InterfaceType, + dst: &mut MaybeUninit<[ValRaw; 2]>, + ) -> Result<()> { + let (key_ty, value_ty) = match ty { + InterfaceType::Map(i) => { + let m = &cx.types[i]; + (m.key, m.value) + } + _ => bad_type_info(), + }; + let (ptr, len) = lower_map_iter(cx, key_ty, value_ty, self.len(), self.iter())?; + // See "WRITEPTR64" above for why this is always storing a 64-bit + // integer. + map_maybe_uninit!(dst[0]).write(ValRaw::i64(ptr as i64)); + map_maybe_uninit!(dst[1]).write(ValRaw::i64(len as i64)); + Ok(()) + } + + fn linear_lower_to_memory( + &self, + cx: &mut LowerContext<'_, U>, + ty: InterfaceType, + offset: usize, + ) -> Result<()> { + let (key_ty, value_ty) = match ty { + InterfaceType::Map(i) => { + let m = &cx.types[i]; + (m.key, m.value) + } + _ => bad_type_info(), + }; + debug_assert!(offset % (Self::ALIGN32 as usize) == 0); + let (ptr, len) = lower_map_iter(cx, key_ty, value_ty, self.len(), self.iter())?; + *cx.get(offset + 0) = u32::try_from(ptr).unwrap().to_le_bytes(); + *cx.get(offset + 4) = u32::try_from(len).unwrap().to_le_bytes(); + Ok(()) + } +} + +#[cfg(feature = "std")] +unsafe impl Lift for std::collections::HashMap +where + K: Lift + Eq + Hash, + V: Lift, +{ + fn linear_lift_from_flat( + cx: &mut LiftContext<'_>, + ty: InterfaceType, + src: &Self::Lower, + ) -> Result { + let (key_ty, value_ty) = match ty { + InterfaceType::Map(i) => { + let m = &cx.types[i]; + (m.key, m.value) + } + _ => bad_type_info(), + }; + // FIXME(#4311): needs memory64 treatment + let ptr = src[0].get_u32(); + let len = src[1].get_u32(); + let (ptr, len) = (usize::try_from(ptr)?, usize::try_from(len)?); + lift_std_map(cx, key_ty, value_ty, ptr, len) + } + + fn linear_lift_from_memory( + cx: &mut LiftContext<'_>, + ty: InterfaceType, + bytes: &[u8], + ) -> Result { + let (key_ty, value_ty) = match ty { + InterfaceType::Map(i) => { + let m = &cx.types[i]; + (m.key, m.value) + } + _ => bad_type_info(), + }; + debug_assert!((bytes.as_ptr() as usize) % (Self::ALIGN32 as usize) == 0); + // FIXME(#4311): needs memory64 treatment + let ptr = u32::from_le_bytes(bytes[..4].try_into().unwrap()); + let len = u32::from_le_bytes(bytes[4..].try_into().unwrap()); + let (ptr, len) = (usize::try_from(ptr)?, usize::try_from(len)?); + lift_std_map(cx, key_ty, value_ty, ptr, len) + } +} + +#[cfg(feature = "std")] +fn lift_std_map( + cx: &mut LiftContext<'_>, + key_ty: InterfaceType, + value_ty: InterfaceType, + ptr: usize, + len: usize, +) -> Result> +where + K: Lift + Eq + Hash, + V: Lift, +{ + let tuple_abi = CanonicalAbiInfo::record_static(&[K::ABI, V::ABI]); + let tuple_size = tuple_abi.size32 as usize; + let tuple_align = tuple_abi.align32 as usize; + + match len + .checked_mul(tuple_size) + .and_then(|total| ptr.checked_add(total)) + { + Some(n) if n <= cx.memory().len() => {} + _ => bail!("map pointer/length out of bounds of memory"), + } + if ptr % tuple_align != 0 { + bail!("map pointer is not aligned"); + } + + let mut result = std::collections::HashMap::with_capacity(len); + for i in 0..len { + let entry_base = ptr + (i * tuple_size); + + let mut field_offset = 0usize; + let key_field = K::ABI.next_field32_size(&mut field_offset); + let key_bytes = &cx.memory()[entry_base + key_field..][..K::SIZE32]; + let key = K::linear_lift_from_memory(cx, key_ty, key_bytes)?; + + let value_field = V::ABI.next_field32_size(&mut field_offset); + let value_bytes = &cx.memory()[entry_base + value_field..][..V::SIZE32]; + let value = V::linear_lift_from_memory(cx, value_ty, value_bytes)?; + + result.insert(key, value); + } + + Ok(result) +} + /// Verify that the given wasm type is a tuple with the expected fields in the right order. fn typecheck_tuple( ty: &InterfaceType, From b80b41adf7d8de2c3622a7092ca8d354b707a845 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Thu, 26 Feb 2026 21:58:20 -0500 Subject: [PATCH 14/26] Fix map adapter trampoline compilation and alignment bugs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The translate_map function had two categories of bugs preventing map adapter trampolines from working: 1. Wasm stack discipline: local_set_new_tmp emits LocalSet which pops from the stack, but was called when the stack was empty (to "pre-allocate" locals). Fixed by computing values first, then calling local_set_new_tmp to consume them—matching translate_list's pattern. Also removed an erroneous LocalTee that left an orphan value on the stack. Affected: src_byte_len, dst_byte_len, cur_src_ptr, cur_dst_ptr. 2. Pointer advancement: after value translation, the pointer still points at the value start. The code only advanced by trailing padding instead of value_size + trailing_padding, causing every loop iteration to re-read the same memory. Also fixes entry layout to use proper record alignment rules (entry align = max(key_align, value_align), value at aligned offset). --- crates/environ/src/fact/trampoline.rs | 159 +++----- tests/all/component_model/func.rs | 529 +++++++++++++++++++++++++- 2 files changed, 584 insertions(+), 104 deletions(-) diff --git a/crates/environ/src/fact/trampoline.rs b/crates/environ/src/fact/trampoline.rs index 97214f7614e0..1bdc96665a13 100644 --- a/crates/environ/src/fact/trampoline.rs +++ b/crates/environ/src/fact/trampoline.rs @@ -2699,8 +2699,6 @@ impl<'a, 'b> Compiler<'a, 'b> { dst_ty: &InterfaceType, dst: &Destination, ) { - // Extract memory configuration for source and destination - // Get linear memory options (32-bit vs 64-bit pointers, which memory, etc.) let src_mem_opts = match &src.opts().data_model { DataModel::Gc {} => todo!("CM+GC"), DataModel::LinearMemory(opts) => opts, @@ -2710,84 +2708,66 @@ impl<'a, 'b> Compiler<'a, 'b> { DataModel::LinearMemory(opts) => opts, }; - // Get type information for source and destination maps - // Look up the TypeMap structs which contain the key and value InterfaceTypes let src_map_ty = &self.types[src_ty]; let dst_map_ty = match dst_ty { InterfaceType::Map(r) => &self.types[*r], _ => panic!("expected a map"), }; - // Load the map's pointer and length into temporary locals - // A map is represented as (ptr, len) - we need both values in locals - // for later use in the translation loop. match src { Source::Stack(s) => { - // If map descriptor is passed on the stack (as 2 locals: ptr, len) assert_eq!(s.locals.len(), 2); - self.stack_get(&s.slice(0..1), src_mem_opts.ptr()); // Push ptr to wasm stack - self.stack_get(&s.slice(1..2), src_mem_opts.ptr()); // Push len to wasm stack + self.stack_get(&s.slice(0..1), src_mem_opts.ptr()); + self.stack_get(&s.slice(1..2), src_mem_opts.ptr()); } Source::Memory(mem) => { - // If map descriptor is stored in memory, load ptr and len from there - self.ptr_load(mem); // Load ptr - self.ptr_load(&mem.bump(src_mem_opts.ptr_size().into())); // Load len (next field) + self.ptr_load(mem); + self.ptr_load(&mem.bump(src_mem_opts.ptr_size().into())); } Source::Struct(_) | Source::Array(_) => todo!("CM+GC"), } - // Pop values from wasm stack into named locals (note: len is on top, then ptr) let src_len = self.local_set_new_tmp(src_mem_opts.ptr()); let src_ptr = self.local_set_new_tmp(src_mem_opts.ptr()); - // Calculate tuple sizes with proper alignment - // Each map entry is a (key, value) tuple. We need to know: - // - Size of key and value in bytes - // - Alignment requirements - // - Total tuple size including any padding let src_opts = src.opts(); let dst_opts = dst.opts(); - // Source tuple layout + // Each map entry is a tuple following record layout rules: + // key at offset 0, padding to value_align, value, trailing padding + // to entry_align. let (src_key_size, src_key_align) = self.types.size_align(src_mem_opts, &src_map_ty.key); - let (src_value_size, _) = self.types.size_align(src_mem_opts, &src_map_ty.value); - // Total tuple size = key + value + padding to alignment - // e.g., if key is 4 bytes, value is 8 bytes, align is 4: - // tuple_size = (4 + 8 + 3) & ~3 = 12 bytes + let (src_value_size, src_value_align) = + self.types.size_align(src_mem_opts, &src_map_ty.value); + let src_entry_align = src_key_align.max(src_value_align); + let src_value_offset = + (src_key_size + src_value_align - 1) & !(src_value_align - 1); let src_tuple_size = - (src_key_size + src_value_size + src_key_align - 1) & !(src_key_align - 1); + (src_value_offset + src_value_size + src_entry_align - 1) & !(src_entry_align - 1); - // Destination tuple layout (may differ if types are converted) let (dst_key_size, dst_key_align) = self.types.size_align(dst_mem_opts, &dst_map_ty.key); - let (dst_value_size, _) = self.types.size_align(dst_mem_opts, &dst_map_ty.value); + let (dst_value_size, dst_value_align) = + self.types.size_align(dst_mem_opts, &dst_map_ty.value); + let dst_entry_align = dst_key_align.max(dst_value_align); + let dst_value_offset = + (dst_key_size + dst_value_align - 1) & !(dst_value_align - 1); let dst_tuple_size = - (dst_key_size + dst_value_size + dst_key_align - 1) & !(dst_key_align - 1); + (dst_value_offset + dst_value_size + dst_entry_align - 1) & !(dst_entry_align - 1); - // Create source memory operand and verify alignment - // This creates a Memory operand and verifies the source pointer is properly aligned - let src_mem = self.memory_operand(src_opts, src_ptr, src_key_align); + let src_mem = self.memory_operand(src_opts, src_ptr, src_entry_align); - // Calculate total byte lengths for source and destination - // total_bytes = count * tuple_size + // Calculate source/destination byte lengths and allocate destination. + self.instruction(LocalGet(src_len.idx)); + self.ptr_uconst(src_mem_opts, src_tuple_size); + self.ptr_mul(src_mem_opts); let src_byte_len = self.local_set_new_tmp(src_mem_opts.ptr()); - self.instruction(LocalGet(src_len.idx)); // Push len - self.ptr_uconst(src_mem_opts, src_tuple_size); // Push tuple_size - self.ptr_mul(src_mem_opts); // len * tuple_size - self.instruction(LocalSet(src_byte_len.idx)); // Save to local + self.instruction(LocalGet(src_len.idx)); + self.ptr_uconst(dst_mem_opts, dst_tuple_size); + self.ptr_mul(dst_mem_opts); let dst_byte_len = self.local_set_new_tmp(dst_mem_opts.ptr()); - self.instruction(LocalGet(src_len.idx)); // Push len (same count) - self.ptr_uconst(dst_mem_opts, dst_tuple_size); // Push dst tuple_size - self.ptr_mul(dst_mem_opts); // len * tuple_size - self.instruction(LocalTee(dst_byte_len.idx)); // Save AND keep on stack for malloc - - // Allocate destination buffer - // Call realloc in the destination component to allocate space. - // dst_byte_len is still on the stack from LocalTee above. - let dst_mem = self.malloc(dst_opts, MallocSize::Local(dst_byte_len.idx), dst_key_align); - - // Validate memory bounds - // Verify that ptr + byte_len doesn't overflow or exceed memory bounds. - // Trap if invalid. + + let dst_mem = self.malloc(dst_opts, MallocSize::Local(dst_byte_len.idx), dst_entry_align); + self.validate_memory_inbounds( src_mem_opts, src_mem.addr.idx, @@ -2801,37 +2781,25 @@ impl<'a, 'b> Compiler<'a, 'b> { Trap::ListOutOfBounds, ); - // Done with byte length locals self.free_temp_local(src_byte_len); self.free_temp_local(dst_byte_len); - // Main translation loop - copy each (key, value) tuple - // Skip loop entirely if tuples are zero-sized (nothing to copy) if src_tuple_size > 0 || dst_tuple_size > 0 { - // Loop setup - // Create counter for remaining elements - let remaining = self.local_set_new_tmp(src_mem_opts.ptr()); + self.instruction(Block(BlockType::Empty)); + self.instruction(LocalGet(src_len.idx)); - self.instruction(LocalSet(remaining.idx)); + let remaining = self.local_tee_new_tmp(src_mem_opts.ptr()); + self.ptr_eqz(src_mem_opts); + self.instruction(BrIf(0)); - // Create pointer to current position in source - let cur_src_ptr = self.local_set_new_tmp(src_mem_opts.ptr()); self.instruction(LocalGet(src_mem.addr.idx)); - self.instruction(LocalSet(cur_src_ptr.idx)); + let cur_src_ptr = self.local_set_new_tmp(src_mem_opts.ptr()); - // Create pointer to current position in destination - let cur_dst_ptr = self.local_set_new_tmp(dst_mem_opts.ptr()); self.instruction(LocalGet(dst_mem.addr.idx)); - self.instruction(LocalSet(cur_dst_ptr.idx)); + let cur_dst_ptr = self.local_set_new_tmp(dst_mem_opts.ptr()); - // WebAssembly loop structure - // Block is the outer container (to break out of loop) - // Loop is what we branch back to for iteration - self.instruction(Block(BlockType::Empty)); self.instruction(Loop(BlockType::Empty)); - // Translate the key - // Create Source pointing to current key location let key_src = Source::Memory(self.memory_operand( src_opts, TempLocal { @@ -2841,7 +2809,6 @@ impl<'a, 'b> Compiler<'a, 'b> { }, src_key_align, )); - // Create Destination pointing to where key should go let key_dst = Destination::Memory(self.memory_operand( dst_opts, TempLocal { @@ -2851,24 +2818,22 @@ impl<'a, 'b> Compiler<'a, 'b> { }, dst_key_align, )); - // Recursively translate the key (handles any type: primitives, strings, etc.) self.translate(&src_map_ty.key, &key_src, &dst_map_ty.key, &key_dst); - // Advance pointers past the key to point at value - if src_key_size > 0 { + // Advance pointers from key start to value start + if src_value_offset > 0 { self.instruction(LocalGet(cur_src_ptr.idx)); - self.ptr_uconst(src_mem_opts, src_key_size); + self.ptr_uconst(src_mem_opts, src_value_offset); self.ptr_add(src_mem_opts); self.instruction(LocalSet(cur_src_ptr.idx)); } - if dst_key_size > 0 { + if dst_value_offset > 0 { self.instruction(LocalGet(cur_dst_ptr.idx)); - self.ptr_uconst(dst_mem_opts, dst_key_size); + self.ptr_uconst(dst_mem_opts, dst_value_offset); self.ptr_add(dst_mem_opts); self.instruction(LocalSet(cur_dst_ptr.idx)); } - // Translate the value let value_src = Source::Memory(self.memory_operand( src_opts, TempLocal { @@ -2876,7 +2841,7 @@ impl<'a, 'b> Compiler<'a, 'b> { ty: src_mem_opts.ptr(), needs_free: false, }, - src_key_align, + src_value_align, )); let value_dst = Destination::Memory(self.memory_operand( dst_opts, @@ -2885,52 +2850,49 @@ impl<'a, 'b> Compiler<'a, 'b> { ty: dst_mem_opts.ptr(), needs_free: false, }, - dst_key_align, + dst_value_align, )); - // Recursively translate the value self.translate(&src_map_ty.value, &value_src, &dst_map_ty.value, &value_dst); - // Advance pointers past the value (including any alignment padding) - // If tuple_size > key_size + value_size, there's padding we need to skip - if src_tuple_size > src_key_size + src_value_size { + // Advance pointers past the value and any trailing padding to + // reach the next entry. After value translation, cur_ptr still + // points at the value start, so advance by value_size + trailing + // padding (i.e. tuple_size - value_offset). + let src_advance_to_next = src_tuple_size - src_value_offset; + if src_advance_to_next > 0 { self.instruction(LocalGet(cur_src_ptr.idx)); - self.ptr_uconst(src_mem_opts, src_tuple_size - src_key_size - src_value_size); + self.ptr_uconst(src_mem_opts, src_advance_to_next); self.ptr_add(src_mem_opts); self.instruction(LocalSet(cur_src_ptr.idx)); } - if dst_tuple_size > dst_key_size + dst_value_size { + let dst_advance_to_next = dst_tuple_size - dst_value_offset; + if dst_advance_to_next > 0 { self.instruction(LocalGet(cur_dst_ptr.idx)); - self.ptr_uconst(dst_mem_opts, dst_tuple_size - dst_key_size - dst_value_size); + self.ptr_uconst(dst_mem_opts, dst_advance_to_next); self.ptr_add(dst_mem_opts); self.instruction(LocalSet(cur_dst_ptr.idx)); } - // Loop continuation: decrement counter and branch if not done self.instruction(LocalGet(remaining.idx)); - self.ptr_iconst(src_mem_opts, -1); // Push -1 - self.ptr_add(src_mem_opts); // remaining - 1 - self.instruction(LocalTee(remaining.idx)); // Save back AND keep on stack - self.ptr_br_if(src_mem_opts, 0); // If remaining != 0, branch back to Loop - self.instruction(End); // End Loop - self.instruction(End); // End Block - - // Release loop locals + self.ptr_iconst(src_mem_opts, -1); + self.ptr_add(src_mem_opts); + self.instruction(LocalTee(remaining.idx)); + self.ptr_br_if(src_mem_opts, 0); + self.instruction(End); // end of loop + self.instruction(End); // end of block self.free_temp_local(cur_dst_ptr); self.free_temp_local(cur_src_ptr); self.free_temp_local(remaining); } - // Store the result (ptr, len) to the destination match dst { Destination::Stack(s, _) => { - // Put ptr and len on the wasm stack for return self.instruction(LocalGet(dst_mem.addr.idx)); self.stack_set(&s[..1], dst_mem_opts.ptr()); self.convert_src_len_to_dst(src_len.idx, src_mem_opts.ptr(), dst_mem_opts.ptr()); self.stack_set(&s[1..], dst_mem_opts.ptr()); } Destination::Memory(mem) => { - // Store ptr and len to destination memory location self.instruction(LocalGet(mem.addr.idx)); self.instruction(LocalGet(dst_mem.addr.idx)); self.ptr_store(mem); @@ -2941,7 +2903,6 @@ impl<'a, 'b> Compiler<'a, 'b> { Destination::Struct(_) | Destination::Array(_) => todo!("CM+GC"), } - // Cleanup - release all temporary locals self.free_temp_local(src_len); self.free_temp_local(src_mem.addr); self.free_temp_local(dst_mem.addr); diff --git a/tests/all/component_model/func.rs b/tests/all/component_model/func.rs index 4ba611c3d088..0cd986cca59f 100644 --- a/tests/all/component_model/func.rs +++ b/tests/all/component_model/func.rs @@ -3854,11 +3854,7 @@ fn host_call_with_concurrency_disabled() -> Result<()> { /// calculated as `key_size` instead of `align(key_size, value_align)`. /// For map, the value should be at offset 8 (not 1). /// -/// NOTE: This test currently demonstrates that the adapter trampoline -/// compilation fails for map types with certain key/value combinations. -/// This is a known issue that needs to be fixed in trampoline.rs. #[test] -#[ignore] // TODO: Fix trampoline alignment bug first fn map_trampoline_alignment() -> Result<()> { // Test map - key_size=1, value_align=8 // With the alignment bug, value would be read/written at offset 1 instead of 8 @@ -3996,7 +3992,6 @@ fn map_trampoline_alignment() -> Result<()> { /// Tests map alignment through trampoline #[test] -#[ignore] // TODO: Fix trampoline alignment bug first fn map_trampoline_alignment_u32_u64() -> Result<()> { // Test map - key_size=4, value_align=8 // With the alignment bug, value would be read/written at offset 4 instead of 8 @@ -4127,3 +4122,527 @@ fn map_trampoline_alignment_u32_u64() -> Result<()> { Ok(()) } + +/// Tests map alignment through trampoline +#[test] +fn map_trampoline_alignment_u8_u32() -> Result<()> { + let component = format!( + r#" +(component + (import "host" (func $host (param "m" (map u8 u32)) (result (map u8 u32)))) + + (component $dst + (import "echo" (func $echo (param "m" (map u8 u32)) (result (map u8 u32)))) + (core module $libc + (memory (export "memory") 1) + {REALLOC_AND_FREE} + ) + (core module $echo_mod + (import "" "echo" (func $echo (param i32 i32 i32))) + (import "libc" "memory" (memory 0)) + (import "libc" "realloc" (func $realloc (param i32 i32 i32 i32) (result i32))) + + (func (export "echo") (param i32 i32) (result i32) + (local $retptr i32) + (local.set $retptr + (call $realloc (i32.const 0) (i32.const 0) (i32.const 4) (i32.const 8))) + (call $echo (local.get 0) (local.get 1) (local.get $retptr)) + local.get $retptr + ) + ) + (core instance $libc (instantiate $libc)) + (core func $echo_lower (canon lower (func $echo) + (memory $libc "memory") + (realloc (func $libc "realloc")) + )) + (core instance $echo_inst (instantiate $echo_mod + (with "libc" (instance $libc)) + (with "" (instance (export "echo" (func $echo_lower)))) + )) + (func (export "echo2") (param "m" (map u8 u32)) (result (map u8 u32)) + (canon lift + (core func $echo_inst "echo") + (memory $libc "memory") + (realloc (func $libc "realloc")) + ) + ) + ) + + (component $src + (import "echo" (func $echo (param "m" (map u8 u32)) (result (map u8 u32)))) + (core module $libc + (memory (export "memory") 1) + {REALLOC_AND_FREE} + ) + (core module $echo_mod + (import "" "echo" (func $echo (param i32 i32 i32))) + (import "libc" "memory" (memory 0)) + (import "libc" "realloc" (func $realloc (param i32 i32 i32 i32) (result i32))) + + (func (export "echo") (param i32 i32) (result i32) + (local $retptr i32) + (local.set $retptr + (call $realloc (i32.const 0) (i32.const 0) (i32.const 4) (i32.const 8))) + (call $echo (local.get 0) (local.get 1) (local.get $retptr)) + local.get $retptr + ) + ) + (core instance $libc (instantiate $libc)) + (core func $echo_lower (canon lower (func $echo) + (memory $libc "memory") + (realloc (func $libc "realloc")) + )) + (core instance $echo_inst (instantiate $echo_mod + (with "libc" (instance $libc)) + (with "" (instance (export "echo" (func $echo_lower)))) + )) + (func (export "echo2") (param "m" (map u8 u32)) (result (map u8 u32)) + (canon lift + (core func $echo_inst "echo") + (memory $libc "memory") + (realloc (func $libc "realloc")) + ) + ) + ) + + (instance $dst (instantiate $dst (with "echo" (func $host)))) + (instance $src (instantiate $src (with "echo" (func $dst "echo2")))) + (export "echo" (func $src "echo2")) +) +"# + ); + + let mut config = Config::new(); + config.wasm_component_model(true); + config.wasm_component_model_map(true); + let engine = Engine::new(&config)?; + let component = Component::new(&engine, component)?; + + let mut store = Store::new(&engine, ()); + let mut linker = Linker::new(&engine); + + linker.root().func_new("host", |_cx, _ty, args, results| { + results[0] = args[0].clone(); + Ok(()) + })?; + + let instance = linker.instantiate(&mut store, &component)?; + let func = instance.get_func(&mut store, "echo").unwrap(); + + let test_data = vec![ + (Val::U8(1), Val::U32(0x01020304)), + (Val::U8(2), Val::U32(0x11121314)), + ]; + let input = Val::Map(test_data.clone()); + + let mut results = [Val::Bool(false)]; + func.call(&mut store, &[input], &mut results)?; + + match &results[0] { + Val::Map(output) => { + assert_eq!(output.len(), 2); + for (key, value) in &test_data { + assert!( + output.iter().any(|(k, v)| k == key && v == value), + "Missing or corrupted entry" + ); + } + } + _ => panic!("expected map"), + } + + Ok(()) +} + +/// Tests map alignment through trampoline +#[test] +fn map_trampoline_alignment_u16_u64() -> Result<()> { + let component = format!( + r#" +(component + (import "host" (func $host (param "m" (map u16 u64)) (result (map u16 u64)))) + + (component $dst + (import "echo" (func $echo (param "m" (map u16 u64)) (result (map u16 u64)))) + (core module $libc + (memory (export "memory") 1) + {REALLOC_AND_FREE} + ) + (core module $echo_mod + (import "" "echo" (func $echo (param i32 i32 i32))) + (import "libc" "memory" (memory 0)) + (import "libc" "realloc" (func $realloc (param i32 i32 i32 i32) (result i32))) + + (func (export "echo") (param i32 i32) (result i32) + (local $retptr i32) + (local.set $retptr + (call $realloc (i32.const 0) (i32.const 0) (i32.const 4) (i32.const 8))) + (call $echo (local.get 0) (local.get 1) (local.get $retptr)) + local.get $retptr + ) + ) + (core instance $libc (instantiate $libc)) + (core func $echo_lower (canon lower (func $echo) + (memory $libc "memory") + (realloc (func $libc "realloc")) + )) + (core instance $echo_inst (instantiate $echo_mod + (with "libc" (instance $libc)) + (with "" (instance (export "echo" (func $echo_lower)))) + )) + (func (export "echo2") (param "m" (map u16 u64)) (result (map u16 u64)) + (canon lift + (core func $echo_inst "echo") + (memory $libc "memory") + (realloc (func $libc "realloc")) + ) + ) + ) + + (component $src + (import "echo" (func $echo (param "m" (map u16 u64)) (result (map u16 u64)))) + (core module $libc + (memory (export "memory") 1) + {REALLOC_AND_FREE} + ) + (core module $echo_mod + (import "" "echo" (func $echo (param i32 i32 i32))) + (import "libc" "memory" (memory 0)) + (import "libc" "realloc" (func $realloc (param i32 i32 i32 i32) (result i32))) + + (func (export "echo") (param i32 i32) (result i32) + (local $retptr i32) + (local.set $retptr + (call $realloc (i32.const 0) (i32.const 0) (i32.const 4) (i32.const 8))) + (call $echo (local.get 0) (local.get 1) (local.get $retptr)) + local.get $retptr + ) + ) + (core instance $libc (instantiate $libc)) + (core func $echo_lower (canon lower (func $echo) + (memory $libc "memory") + (realloc (func $libc "realloc")) + )) + (core instance $echo_inst (instantiate $echo_mod + (with "libc" (instance $libc)) + (with "" (instance (export "echo" (func $echo_lower)))) + )) + (func (export "echo2") (param "m" (map u16 u64)) (result (map u16 u64)) + (canon lift + (core func $echo_inst "echo") + (memory $libc "memory") + (realloc (func $libc "realloc")) + ) + ) + ) + + (instance $dst (instantiate $dst (with "echo" (func $host)))) + (instance $src (instantiate $src (with "echo" (func $dst "echo2")))) + (export "echo" (func $src "echo2")) +) +"# + ); + + let mut config = Config::new(); + config.wasm_component_model(true); + config.wasm_component_model_map(true); + let engine = Engine::new(&config)?; + let component = Component::new(&engine, component)?; + + let mut store = Store::new(&engine, ()); + let mut linker = Linker::new(&engine); + + linker.root().func_new("host", |_cx, _ty, args, results| { + results[0] = args[0].clone(); + Ok(()) + })?; + + let instance = linker.instantiate(&mut store, &component)?; + let func = instance.get_func(&mut store, "echo").unwrap(); + + let test_data = vec![ + (Val::U16(1), Val::U64(0x0102030405060708)), + (Val::U16(2), Val::U64(0x1112131415161718)), + ]; + let input = Val::Map(test_data.clone()); + + let mut results = [Val::Bool(false)]; + func.call(&mut store, &[input], &mut results)?; + + match &results[0] { + Val::Map(output) => { + assert_eq!(output.len(), 2); + for (key, value) in &test_data { + assert!( + output.iter().any(|(k, v)| k == key && v == value), + "Missing or corrupted entry" + ); + } + } + _ => panic!("expected map"), + } + + Ok(()) +} + +/// Tests map alignment through trampoline +#[test] +fn map_trampoline_alignment_u8_u16() -> Result<()> { + let component = format!( + r#" +(component + (import "host" (func $host (param "m" (map u8 u16)) (result (map u8 u16)))) + + (component $dst + (import "echo" (func $echo (param "m" (map u8 u16)) (result (map u8 u16)))) + (core module $libc + (memory (export "memory") 1) + {REALLOC_AND_FREE} + ) + (core module $echo_mod + (import "" "echo" (func $echo (param i32 i32 i32))) + (import "libc" "memory" (memory 0)) + (import "libc" "realloc" (func $realloc (param i32 i32 i32 i32) (result i32))) + + (func (export "echo") (param i32 i32) (result i32) + (local $retptr i32) + (local.set $retptr + (call $realloc (i32.const 0) (i32.const 0) (i32.const 4) (i32.const 8))) + (call $echo (local.get 0) (local.get 1) (local.get $retptr)) + local.get $retptr + ) + ) + (core instance $libc (instantiate $libc)) + (core func $echo_lower (canon lower (func $echo) + (memory $libc "memory") + (realloc (func $libc "realloc")) + )) + (core instance $echo_inst (instantiate $echo_mod + (with "libc" (instance $libc)) + (with "" (instance (export "echo" (func $echo_lower)))) + )) + (func (export "echo2") (param "m" (map u8 u16)) (result (map u8 u16)) + (canon lift + (core func $echo_inst "echo") + (memory $libc "memory") + (realloc (func $libc "realloc")) + ) + ) + ) + + (component $src + (import "echo" (func $echo (param "m" (map u8 u16)) (result (map u8 u16)))) + (core module $libc + (memory (export "memory") 1) + {REALLOC_AND_FREE} + ) + (core module $echo_mod + (import "" "echo" (func $echo (param i32 i32 i32))) + (import "libc" "memory" (memory 0)) + (import "libc" "realloc" (func $realloc (param i32 i32 i32 i32) (result i32))) + + (func (export "echo") (param i32 i32) (result i32) + (local $retptr i32) + (local.set $retptr + (call $realloc (i32.const 0) (i32.const 0) (i32.const 4) (i32.const 8))) + (call $echo (local.get 0) (local.get 1) (local.get $retptr)) + local.get $retptr + ) + ) + (core instance $libc (instantiate $libc)) + (core func $echo_lower (canon lower (func $echo) + (memory $libc "memory") + (realloc (func $libc "realloc")) + )) + (core instance $echo_inst (instantiate $echo_mod + (with "libc" (instance $libc)) + (with "" (instance (export "echo" (func $echo_lower)))) + )) + (func (export "echo2") (param "m" (map u8 u16)) (result (map u8 u16)) + (canon lift + (core func $echo_inst "echo") + (memory $libc "memory") + (realloc (func $libc "realloc")) + ) + ) + ) + + (instance $dst (instantiate $dst (with "echo" (func $host)))) + (instance $src (instantiate $src (with "echo" (func $dst "echo2")))) + (export "echo" (func $src "echo2")) +) +"# + ); + + let mut config = Config::new(); + config.wasm_component_model(true); + config.wasm_component_model_map(true); + let engine = Engine::new(&config)?; + let component = Component::new(&engine, component)?; + + let mut store = Store::new(&engine, ()); + let mut linker = Linker::new(&engine); + + linker.root().func_new("host", |_cx, _ty, args, results| { + results[0] = args[0].clone(); + Ok(()) + })?; + + let instance = linker.instantiate(&mut store, &component)?; + let func = instance.get_func(&mut store, "echo").unwrap(); + + let test_data = vec![ + (Val::U8(1), Val::U16(0x0102)), + (Val::U8(2), Val::U16(0x1112)), + ]; + let input = Val::Map(test_data.clone()); + + let mut results = [Val::Bool(false)]; + func.call(&mut store, &[input], &mut results)?; + + match &results[0] { + Val::Map(output) => { + assert_eq!(output.len(), 2); + for (key, value) in &test_data { + assert!( + output.iter().any(|(k, v)| k == key && v == value), + "Missing or corrupted entry" + ); + } + } + _ => panic!("expected map"), + } + + Ok(()) +} + +/// Tests map alignment through trampoline (reverse case: key larger than value) +#[test] +fn map_trampoline_alignment_u64_u8() -> Result<()> { + let component = format!( + r#" +(component + (import "host" (func $host (param "m" (map u64 u8)) (result (map u64 u8)))) + + (component $dst + (import "echo" (func $echo (param "m" (map u64 u8)) (result (map u64 u8)))) + (core module $libc + (memory (export "memory") 1) + {REALLOC_AND_FREE} + ) + (core module $echo_mod + (import "" "echo" (func $echo (param i32 i32 i32))) + (import "libc" "memory" (memory 0)) + (import "libc" "realloc" (func $realloc (param i32 i32 i32 i32) (result i32))) + + (func (export "echo") (param i32 i32) (result i32) + (local $retptr i32) + (local.set $retptr + (call $realloc (i32.const 0) (i32.const 0) (i32.const 4) (i32.const 8))) + (call $echo (local.get 0) (local.get 1) (local.get $retptr)) + local.get $retptr + ) + ) + (core instance $libc (instantiate $libc)) + (core func $echo_lower (canon lower (func $echo) + (memory $libc "memory") + (realloc (func $libc "realloc")) + )) + (core instance $echo_inst (instantiate $echo_mod + (with "libc" (instance $libc)) + (with "" (instance (export "echo" (func $echo_lower)))) + )) + (func (export "echo2") (param "m" (map u64 u8)) (result (map u64 u8)) + (canon lift + (core func $echo_inst "echo") + (memory $libc "memory") + (realloc (func $libc "realloc")) + ) + ) + ) + + (component $src + (import "echo" (func $echo (param "m" (map u64 u8)) (result (map u64 u8)))) + (core module $libc + (memory (export "memory") 1) + {REALLOC_AND_FREE} + ) + (core module $echo_mod + (import "" "echo" (func $echo (param i32 i32 i32))) + (import "libc" "memory" (memory 0)) + (import "libc" "realloc" (func $realloc (param i32 i32 i32 i32) (result i32))) + + (func (export "echo") (param i32 i32) (result i32) + (local $retptr i32) + (local.set $retptr + (call $realloc (i32.const 0) (i32.const 0) (i32.const 4) (i32.const 8))) + (call $echo (local.get 0) (local.get 1) (local.get $retptr)) + local.get $retptr + ) + ) + (core instance $libc (instantiate $libc)) + (core func $echo_lower (canon lower (func $echo) + (memory $libc "memory") + (realloc (func $libc "realloc")) + )) + (core instance $echo_inst (instantiate $echo_mod + (with "libc" (instance $libc)) + (with "" (instance (export "echo" (func $echo_lower)))) + )) + (func (export "echo2") (param "m" (map u64 u8)) (result (map u64 u8)) + (canon lift + (core func $echo_inst "echo") + (memory $libc "memory") + (realloc (func $libc "realloc")) + ) + ) + ) + + (instance $dst (instantiate $dst (with "echo" (func $host)))) + (instance $src (instantiate $src (with "echo" (func $dst "echo2")))) + (export "echo" (func $src "echo2")) +) +"# + ); + + let mut config = Config::new(); + config.wasm_component_model(true); + config.wasm_component_model_map(true); + let engine = Engine::new(&config)?; + let component = Component::new(&engine, component)?; + + let mut store = Store::new(&engine, ()); + let mut linker = Linker::new(&engine); + + linker.root().func_new("host", |_cx, _ty, args, results| { + results[0] = args[0].clone(); + Ok(()) + })?; + + let instance = linker.instantiate(&mut store, &component)?; + let func = instance.get_func(&mut store, "echo").unwrap(); + + let test_data = vec![ + (Val::U64(0x0102030405060708), Val::U8(42)), + (Val::U64(0x1112131415161718), Val::U8(99)), + ]; + let input = Val::Map(test_data.clone()); + + let mut results = [Val::Bool(false)]; + func.call(&mut store, &[input], &mut results)?; + + match &results[0] { + Val::Map(output) => { + assert_eq!(output.len(), 2); + for (key, value) in &test_data { + assert!( + output.iter().any(|(k, v)| k == key && v == value), + "Missing or corrupted entry" + ); + } + } + _ => panic!("expected map"), + } + + Ok(()) +} \ No newline at end of file From db1f8166d3bbd65f6f36523c846baf43c785056f Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Thu, 26 Feb 2026 23:37:07 -0500 Subject: [PATCH 15/26] Refactor map entry layout calculations to use canonical ABI --- crates/environ/src/fact/trampoline.rs | 64 ++++++++++++++++++--------- 1 file changed, 44 insertions(+), 20 deletions(-) diff --git a/crates/environ/src/fact/trampoline.rs b/crates/environ/src/fact/trampoline.rs index 1bdc96665a13..dc0c683d629c 100644 --- a/crates/environ/src/fact/trampoline.rs +++ b/crates/environ/src/fact/trampoline.rs @@ -2732,26 +2732,50 @@ impl<'a, 'b> Compiler<'a, 'b> { let src_opts = src.opts(); let dst_opts = dst.opts(); - // Each map entry is a tuple following record layout rules: - // key at offset 0, padding to value_align, value, trailing padding - // to entry_align. - let (src_key_size, src_key_align) = self.types.size_align(src_mem_opts, &src_map_ty.key); - let (src_value_size, src_value_align) = - self.types.size_align(src_mem_opts, &src_map_ty.value); - let src_entry_align = src_key_align.max(src_value_align); - let src_value_offset = - (src_key_size + src_value_align - 1) & !(src_value_align - 1); - let src_tuple_size = - (src_value_offset + src_value_size + src_entry_align - 1) & !(src_entry_align - 1); - - let (dst_key_size, dst_key_align) = self.types.size_align(dst_mem_opts, &dst_map_ty.key); - let (dst_value_size, dst_value_align) = - self.types.size_align(dst_mem_opts, &dst_map_ty.value); - let dst_entry_align = dst_key_align.max(dst_value_align); - let dst_value_offset = - (dst_key_size + dst_value_align - 1) & !(dst_value_align - 1); - let dst_tuple_size = - (dst_value_offset + dst_value_size + dst_entry_align - 1) & !(dst_entry_align - 1); + // Each map entry is a tuple following record layout rules. + let src_key_abi = self.types.canonical_abi(&src_map_ty.key); + let src_value_abi = self.types.canonical_abi(&src_map_ty.value); + let src_entry_abi = + CanonicalAbiInfo::record([src_key_abi, src_value_abi].into_iter()); + let (_, src_key_align) = self.types.size_align(src_mem_opts, &src_map_ty.key); + let (_, src_value_align) = self.types.size_align(src_mem_opts, &src_map_ty.value); + let (src_tuple_size, src_entry_align) = if src_mem_opts.memory64 { + (src_entry_abi.size64, src_entry_abi.align64) + } else { + (src_entry_abi.size32, src_entry_abi.align32) + }; + let src_value_offset = { + let mut offset = 0u32; + if src_mem_opts.memory64 { + src_key_abi.next_field64(&mut offset); + src_value_abi.next_field64(&mut offset) + } else { + src_key_abi.next_field32(&mut offset); + src_value_abi.next_field32(&mut offset) + } + }; + + let dst_key_abi = self.types.canonical_abi(&dst_map_ty.key); + let dst_value_abi = self.types.canonical_abi(&dst_map_ty.value); + let dst_entry_abi = + CanonicalAbiInfo::record([dst_key_abi, dst_value_abi].into_iter()); + let (_, dst_key_align) = self.types.size_align(dst_mem_opts, &dst_map_ty.key); + let (_, dst_value_align) = self.types.size_align(dst_mem_opts, &dst_map_ty.value); + let (dst_tuple_size, dst_entry_align) = if dst_mem_opts.memory64 { + (dst_entry_abi.size64, dst_entry_abi.align64) + } else { + (dst_entry_abi.size32, dst_entry_abi.align32) + }; + let dst_value_offset = { + let mut offset = 0u32; + if dst_mem_opts.memory64 { + dst_key_abi.next_field64(&mut offset); + dst_value_abi.next_field64(&mut offset) + } else { + dst_key_abi.next_field32(&mut offset); + dst_value_abi.next_field32(&mut offset) + } + }; let src_mem = self.memory_operand(src_opts, src_ptr, src_entry_align); From 504c7eb0c28dfd916042215b54719342ab1354b5 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Thu, 26 Feb 2026 23:39:41 -0500 Subject: [PATCH 16/26] Remove unnecessary clone of map pairs during lowering Val::Map already holds Vec<(Val, Val)> which derefs to &[(Val, Val)], matching lower_map's signature directly. The intermediate Vec allocation and deep clone of every key/value pair was redundant. --- crates/wasmtime/src/runtime/component/values.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/values.rs b/crates/wasmtime/src/runtime/component/values.rs index 8663be9749f2..ff2df8754fe7 100644 --- a/crates/wasmtime/src/runtime/component/values.rs +++ b/crates/wasmtime/src/runtime/component/values.rs @@ -441,13 +441,9 @@ impl Val { Ok(()) } (InterfaceType::List(_), _) => unexpected(ty, self), - (InterfaceType::Map(ty), Val::Map(map)) => { - // Maps are stored as list> in canonical ABI + (InterfaceType::Map(ty), Val::Map(pairs)) => { let map_ty = &cx.types[ty]; - // Convert HashMap to Vec<(Val, Val)> for lowering - let pairs: Vec<(Val, Val)> = - map.iter().map(|(k, v)| (k.clone(), v.clone())).collect(); - let (ptr, len) = lower_map(cx, map_ty.key, map_ty.value, &pairs)?; + let (ptr, len) = lower_map(cx, map_ty.key, map_ty.value, pairs)?; next_mut(dst).write(ValRaw::i64(ptr as i64)); next_mut(dst).write(ValRaw::i64(len as i64)); Ok(()) From 1097cda6547b6e31d74faf848c5db92aac1249eb Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Fri, 27 Feb 2026 00:04:45 -0500 Subject: [PATCH 17/26] Deduplicate map lift logic between HashMap implementations --- .../src/runtime/component/func/typed.rs | 69 ++++++++----------- 1 file changed, 30 insertions(+), 39 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/func/typed.rs b/crates/wasmtime/src/runtime/component/func/typed.rs index f85236d9172e..643e7214317a 100644 --- a/crates/wasmtime/src/runtime/component/func/typed.rs +++ b/crates/wasmtime/src/runtime/component/func/typed.rs @@ -2241,15 +2241,18 @@ where } } -fn lift_map( +/// Shared helper that validates a map's memory region and lifts each +/// (key, value) pair, forwarding them to `insert`. +fn lift_map_pairs( cx: &mut LiftContext<'_>, key_ty: InterfaceType, value_ty: InterfaceType, ptr: usize, len: usize, -) -> Result> + mut insert: impl FnMut(K, V) -> Result<()>, +) -> Result<()> where - K: Lift + Eq + Hash, + K: Lift, V: Lift, { let tuple_abi = CanonicalAbiInfo::record_static(&[K::ABI, V::ABI]); @@ -2267,7 +2270,6 @@ where bail!("map pointer is not aligned"); } - let mut result = HashMap::with_capacity(len)?; for i in 0..len { let entry_base = ptr + (i * tuple_size); @@ -2280,18 +2282,33 @@ where let value_bytes = &cx.memory()[entry_base + value_field..][..V::SIZE32]; let value = V::linear_lift_from_memory(cx, value_ty, value_bytes)?; - result.insert(key, value)?; + insert(key, value)?; } + Ok(()) +} + +fn lift_map( + cx: &mut LiftContext<'_>, + key_ty: InterfaceType, + value_ty: InterfaceType, + ptr: usize, + len: usize, +) -> Result> +where + K: Lift + Eq + Hash, + V: Lift, +{ + let mut result = HashMap::with_capacity(len)?; + lift_map_pairs(cx, key_ty, value_ty, ptr, len, |k, v| { + result.insert(k, v)?; + Ok(()) + })?; Ok(result) } // ============================================================================= // std::collections::HashMap support for component model `map` -// -// This mirrors the wasmtime_environ::collections::HashMap implementation above -// but works with the standard library HashMap type, which is what users will -// naturally reach for. #[cfg(feature = "std")] unsafe impl ComponentType for std::collections::HashMap @@ -2422,37 +2439,11 @@ where K: Lift + Eq + Hash, V: Lift, { - let tuple_abi = CanonicalAbiInfo::record_static(&[K::ABI, V::ABI]); - let tuple_size = tuple_abi.size32 as usize; - let tuple_align = tuple_abi.align32 as usize; - - match len - .checked_mul(tuple_size) - .and_then(|total| ptr.checked_add(total)) - { - Some(n) if n <= cx.memory().len() => {} - _ => bail!("map pointer/length out of bounds of memory"), - } - if ptr % tuple_align != 0 { - bail!("map pointer is not aligned"); - } - let mut result = std::collections::HashMap::with_capacity(len); - for i in 0..len { - let entry_base = ptr + (i * tuple_size); - - let mut field_offset = 0usize; - let key_field = K::ABI.next_field32_size(&mut field_offset); - let key_bytes = &cx.memory()[entry_base + key_field..][..K::SIZE32]; - let key = K::linear_lift_from_memory(cx, key_ty, key_bytes)?; - - let value_field = V::ABI.next_field32_size(&mut field_offset); - let value_bytes = &cx.memory()[entry_base + value_field..][..V::SIZE32]; - let value = V::linear_lift_from_memory(cx, value_ty, value_bytes)?; - - result.insert(key, value); - } - + lift_map_pairs(cx, key_ty, value_ty, ptr, len, |k, v| { + result.insert(k, v); + Ok(()) + })?; Ok(result) } From cd97d706fb0541430b93ad1d003a6619cdef6cdc Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Fri, 27 Feb 2026 00:28:30 -0500 Subject: [PATCH 18/26] Deduplicate list and map sequence translation scaffolding --- crates/environ/src/fact/trampoline.rs | 407 +++++++++++++------------- 1 file changed, 201 insertions(+), 206 deletions(-) diff --git a/crates/environ/src/fact/trampoline.rs b/crates/environ/src/fact/trampoline.rs index dc0c683d629c..67924e73511d 100644 --- a/crates/environ/src/fact/trampoline.rs +++ b/crates/environ/src/fact/trampoline.rs @@ -2503,13 +2503,23 @@ impl<'a, 'b> Compiler<'a, 'b> { self.instruction(End); } - fn translate_list( + /// Shared preamble for translating list-like sequences (lists and maps). + /// + /// Emits: load ptr/len from source, compute byte lengths, malloc + /// destination, validate bounds, and if element sizes are non-zero opens + /// Block + Loop and initializes iteration locals. + /// + /// Returns a `SequenceTranslation` that the caller uses to emit the + /// loop body before calling `end_translate_sequence`. + fn begin_translate_sequence<'c>( &mut self, - src_ty: TypeListIndex, - src: &Source<'_>, - dst_ty: &InterfaceType, - dst: &Destination, - ) { + src: &Source<'c>, + dst: &Destination<'c>, + src_element_size: u32, + src_element_align: u32, + dst_element_size: u32, + dst_element_align: u32, + ) -> SequenceTranslation<'c> { let src_mem_opts = match &src.opts().data_model { DataModel::Gc {} => todo!("CM+GC"), DataModel::LinearMemory(opts) => opts, @@ -2519,20 +2529,13 @@ impl<'a, 'b> Compiler<'a, 'b> { DataModel::LinearMemory(opts) => opts, }; - let src_element_ty = &self.types[src_ty].element; - let dst_element_ty = match dst_ty { - InterfaceType::List(r) => &self.types[*r].element, - _ => panic!("expected a list"), - }; let src_opts = src.opts(); let dst_opts = dst.opts(); - let (src_size, src_align) = self.types.size_align(src_mem_opts, src_element_ty); - let (dst_size, dst_align) = self.types.size_align(dst_mem_opts, dst_element_ty); - // Load the pointer/length of this list into temporary locals. These - // will be referenced a good deal so this just makes it easier to deal - // with them consistently below rather than trying to reload from memory - // for example. + // Load the pointer/length of this sequence into temporary locals. + // These will be referenced a good deal so this just makes it easier + // to deal with them consistently below rather than trying to reload + // from memory for example. match src { Source::Stack(s) => { assert_eq!(s.locals.len(), 2); @@ -2550,30 +2553,32 @@ impl<'a, 'b> Compiler<'a, 'b> { // Create a `Memory` operand which will internally assert that the // `src_ptr` value is properly aligned. - let src_mem = self.memory_operand(src_opts, src_ptr, src_align); + let src_mem = self.memory_operand(src_opts, src_ptr, src_element_align); // Calculate the source/destination byte lengths into unique locals. - let src_byte_len = self.calculate_list_byte_len(src_mem_opts, src_len.idx, src_size); - let dst_byte_len = if src_size == dst_size { + let src_byte_len = + self.calculate_list_byte_len(src_mem_opts, src_len.idx, src_element_size); + let dst_byte_len = if src_element_size == dst_element_size { self.convert_src_len_to_dst(src_byte_len.idx, src_mem_opts.ptr(), dst_mem_opts.ptr()); self.local_set_new_tmp(dst_mem_opts.ptr()) } else if src_mem_opts.ptr() == dst_mem_opts.ptr() { - self.calculate_list_byte_len(dst_mem_opts, src_len.idx, dst_size) + self.calculate_list_byte_len(dst_mem_opts, src_len.idx, dst_element_size) } else { self.convert_src_len_to_dst(src_byte_len.idx, src_mem_opts.ptr(), dst_mem_opts.ptr()); let tmp = self.local_set_new_tmp(dst_mem_opts.ptr()); - let ret = self.calculate_list_byte_len(dst_mem_opts, tmp.idx, dst_size); + let ret = self.calculate_list_byte_len(dst_mem_opts, tmp.idx, dst_element_size); self.free_temp_local(tmp); ret }; // Here `realloc` is invoked (in a `malloc`-like fashion) to allocate - // space for the list in the destination memory. This will also + // space for the sequence in the destination memory. This will also // internally insert checks that the returned pointer is aligned // correctly for the destination. - let dst_mem = self.malloc(dst_opts, MallocSize::Local(dst_byte_len.idx), dst_align); + let dst_mem = + self.malloc(dst_opts, MallocSize::Local(dst_byte_len.idx), dst_element_align); - // With all the pointers and byte lengths verity that both the source + // With all the pointers and byte lengths verify that both the source // and the destination buffers are in-bounds. self.validate_memory_inbounds( src_mem_opts, @@ -2591,21 +2596,19 @@ impl<'a, 'b> Compiler<'a, 'b> { self.free_temp_local(src_byte_len); self.free_temp_local(dst_byte_len); - // This is the main body of the loop to actually translate list types. - // Note that if both element sizes are 0 then this won't actually do - // anything so the loop is removed entirely. - if src_size > 0 || dst_size > 0 { - // This block encompasses the entire loop and is use to exit before even - // entering the loop if the list size is zero. + // If both element sizes are 0 then there's nothing to copy so the + // loop is skipped entirely. Otherwise open a Block (for early exit + // on zero-length) and a Loop for the per-element iteration. + let loop_state = if src_element_size > 0 || dst_element_size > 0 { self.instruction(Block(BlockType::Empty)); - // Set the `remaining` local and only continue if it's > 0 + // Set the `remaining` local and only continue if it's > 0. self.instruction(LocalGet(src_len.idx)); let remaining = self.local_tee_new_tmp(src_mem_opts.ptr()); self.ptr_eqz(src_mem_opts); self.instruction(BrIf(0)); - // Initialize the two destination pointers to their initial values + // Initialize the two iteration pointers to their starting values. self.instruction(LocalGet(src_mem.addr.idx)); let cur_src_ptr = self.local_set_new_tmp(src_mem_opts.ptr()); self.instruction(LocalGet(dst_mem.addr.idx)); @@ -2613,85 +2616,146 @@ impl<'a, 'b> Compiler<'a, 'b> { self.instruction(Loop(BlockType::Empty)); - // Translate the next element in the list - let element_src = Source::Memory(Memory { - opts: src_opts, - offset: 0, - addr: TempLocal::new(cur_src_ptr.idx, cur_src_ptr.ty), - }); - let element_dst = Destination::Memory(Memory { - opts: dst_opts, - offset: 0, - addr: TempLocal::new(cur_dst_ptr.idx, cur_dst_ptr.ty), - }); - self.translate(src_element_ty, &element_src, dst_element_ty, &element_dst); + Some(SequenceLoopState { + remaining, + cur_src_ptr, + cur_dst_ptr, + }) + } else { + None + }; - // Update the two loop pointers - if src_size > 0 { - self.instruction(LocalGet(cur_src_ptr.idx)); - self.ptr_uconst(src_mem_opts, src_size); - self.ptr_add(src_mem_opts); - self.instruction(LocalSet(cur_src_ptr.idx)); - } - if dst_size > 0 { - self.instruction(LocalGet(cur_dst_ptr.idx)); - self.ptr_uconst(dst_mem_opts, dst_size); - self.ptr_add(dst_mem_opts); - self.instruction(LocalSet(cur_dst_ptr.idx)); - } + SequenceTranslation { + src_len, + src_mem, + dst_mem, + src_opts, + dst_opts, + src_mem_opts, + dst_mem_opts, + loop_state, + } + } - // Update the remaining count, falling through to break out if it's zero - // now. - self.instruction(LocalGet(remaining.idx)); - self.ptr_iconst(src_mem_opts, -1); - self.ptr_add(src_mem_opts); - self.instruction(LocalTee(remaining.idx)); - self.ptr_br_if(src_mem_opts, 0); + /// Shared epilogue for translating list-like sequences. + /// + /// If a loop was opened, emits: decrement remaining, BrIf to loop + /// head, End loop, End block, and frees iteration locals. Then stores + /// the ptr/len pair into the destination and frees all temporaries. + fn end_translate_sequence( + &mut self, + seq: SequenceTranslation<'_>, + dst: &Destination, + ) { + if let Some(loop_state) = seq.loop_state { + // Update the remaining count, falling through to break out if + // it's zero now. + self.instruction(LocalGet(loop_state.remaining.idx)); + self.ptr_iconst(seq.src_mem_opts, -1); + self.ptr_add(seq.src_mem_opts); + self.instruction(LocalTee(loop_state.remaining.idx)); + self.ptr_br_if(seq.src_mem_opts, 0); self.instruction(End); // end of loop self.instruction(End); // end of block - self.free_temp_local(cur_dst_ptr); - self.free_temp_local(cur_src_ptr); - self.free_temp_local(remaining); + self.free_temp_local(loop_state.cur_dst_ptr); + self.free_temp_local(loop_state.cur_src_ptr); + self.free_temp_local(loop_state.remaining); } - // Store the ptr/length in the desired destination + // Store the ptr/length in the desired destination. match dst { Destination::Stack(s, _) => { - self.instruction(LocalGet(dst_mem.addr.idx)); - self.stack_set(&s[..1], dst_mem_opts.ptr()); - self.convert_src_len_to_dst(src_len.idx, src_mem_opts.ptr(), dst_mem_opts.ptr()); - self.stack_set(&s[1..], dst_mem_opts.ptr()); + self.instruction(LocalGet(seq.dst_mem.addr.idx)); + self.stack_set(&s[..1], seq.dst_mem_opts.ptr()); + self.convert_src_len_to_dst( + seq.src_len.idx, + seq.src_mem_opts.ptr(), + seq.dst_mem_opts.ptr(), + ); + self.stack_set(&s[1..], seq.dst_mem_opts.ptr()); } Destination::Memory(mem) => { self.instruction(LocalGet(mem.addr.idx)); - self.instruction(LocalGet(dst_mem.addr.idx)); + self.instruction(LocalGet(seq.dst_mem.addr.idx)); self.ptr_store(mem); self.instruction(LocalGet(mem.addr.idx)); - self.convert_src_len_to_dst(src_len.idx, src_mem_opts.ptr(), dst_mem_opts.ptr()); - self.ptr_store(&mem.bump(dst_mem_opts.ptr_size().into())); + self.convert_src_len_to_dst( + seq.src_len.idx, + seq.src_mem_opts.ptr(), + seq.dst_mem_opts.ptr(), + ); + self.ptr_store(&mem.bump(seq.dst_mem_opts.ptr_size().into())); } Destination::Struct(_) | Destination::Array(_) => todo!("CM+GC"), } - self.free_temp_local(src_len); - self.free_temp_local(src_mem.addr); - self.free_temp_local(dst_mem.addr); + self.free_temp_local(seq.src_len); + self.free_temp_local(seq.src_mem.addr); + self.free_temp_local(seq.dst_mem.addr); + } + + fn translate_list( + &mut self, + src_ty: TypeListIndex, + src: &Source<'_>, + dst_ty: &InterfaceType, + dst: &Destination, + ) { + let src_mem_opts = match &src.opts().data_model { + DataModel::Gc {} => todo!("CM+GC"), + DataModel::LinearMemory(opts) => opts, + }; + let dst_mem_opts = match &dst.opts().data_model { + DataModel::Gc {} => todo!("CM+GC"), + DataModel::LinearMemory(opts) => opts, + }; + + let src_element_ty = &self.types[src_ty].element; + let dst_element_ty = match dst_ty { + InterfaceType::List(r) => &self.types[*r].element, + _ => panic!("expected a list"), + }; + let (src_size, src_align) = self.types.size_align(src_mem_opts, src_element_ty); + let (dst_size, dst_align) = self.types.size_align(dst_mem_opts, dst_element_ty); + + let seq = self.begin_translate_sequence(src, dst, src_size, src_align, dst_size, dst_align); + + if let Some(ref loop_state) = seq.loop_state { + let element_src = Source::Memory(Memory { + opts: seq.src_opts, + offset: 0, + addr: TempLocal::new(loop_state.cur_src_ptr.idx, loop_state.cur_src_ptr.ty), + }); + let element_dst = Destination::Memory(Memory { + opts: seq.dst_opts, + offset: 0, + addr: TempLocal::new(loop_state.cur_dst_ptr.idx, loop_state.cur_dst_ptr.ty), + }); + self.translate(src_element_ty, &element_src, dst_element_ty, &element_dst); + + if src_size > 0 { + self.instruction(LocalGet(loop_state.cur_src_ptr.idx)); + self.ptr_uconst(src_mem_opts, src_size); + self.ptr_add(src_mem_opts); + self.instruction(LocalSet(loop_state.cur_src_ptr.idx)); + } + if dst_size > 0 { + self.instruction(LocalGet(loop_state.cur_dst_ptr.idx)); + self.ptr_uconst(dst_mem_opts, dst_size); + self.ptr_add(dst_mem_opts); + self.instruction(LocalSet(loop_state.cur_dst_ptr.idx)); + } + } + + self.end_translate_sequence(seq, dst); } /// Translates a map from one component's memory to another. /// - /// In the Component Model, a `map` is stored in memory as `list>`. - /// The memory layout is: - /// ```text - /// [pointer to data, length] - /// | - /// v - /// [key1, value1, key2, value2, key3, value3, ...] - /// ``` - /// - /// This function copies each key-value pair from source to destination, - /// potentially converting types along the way. + /// In the Component Model, a `map` is stored in memory as + /// `list>`, so the translation reuses the same sequence + /// scaffolding as lists but with a two-field (key, value) loop body. fn translate_map( &mut self, src_ty: TypeMapIndex, @@ -2714,24 +2778,6 @@ impl<'a, 'b> Compiler<'a, 'b> { _ => panic!("expected a map"), }; - match src { - Source::Stack(s) => { - assert_eq!(s.locals.len(), 2); - self.stack_get(&s.slice(0..1), src_mem_opts.ptr()); - self.stack_get(&s.slice(1..2), src_mem_opts.ptr()); - } - Source::Memory(mem) => { - self.ptr_load(mem); - self.ptr_load(&mem.bump(src_mem_opts.ptr_size().into())); - } - Source::Struct(_) | Source::Array(_) => todo!("CM+GC"), - } - let src_len = self.local_set_new_tmp(src_mem_opts.ptr()); - let src_ptr = self.local_set_new_tmp(src_mem_opts.ptr()); - - let src_opts = src.opts(); - let dst_opts = dst.opts(); - // Each map entry is a tuple following record layout rules. let src_key_abi = self.types.canonical_abi(&src_map_ty.key); let src_value_abi = self.types.canonical_abi(&src_map_ty.value); @@ -2777,66 +2823,29 @@ impl<'a, 'b> Compiler<'a, 'b> { } }; - let src_mem = self.memory_operand(src_opts, src_ptr, src_entry_align); - - // Calculate source/destination byte lengths and allocate destination. - self.instruction(LocalGet(src_len.idx)); - self.ptr_uconst(src_mem_opts, src_tuple_size); - self.ptr_mul(src_mem_opts); - let src_byte_len = self.local_set_new_tmp(src_mem_opts.ptr()); - - self.instruction(LocalGet(src_len.idx)); - self.ptr_uconst(dst_mem_opts, dst_tuple_size); - self.ptr_mul(dst_mem_opts); - let dst_byte_len = self.local_set_new_tmp(dst_mem_opts.ptr()); - - let dst_mem = self.malloc(dst_opts, MallocSize::Local(dst_byte_len.idx), dst_entry_align); - - self.validate_memory_inbounds( - src_mem_opts, - src_mem.addr.idx, - src_byte_len.idx, - Trap::ListOutOfBounds, - ); - self.validate_memory_inbounds( - dst_mem_opts, - dst_mem.addr.idx, - dst_byte_len.idx, - Trap::ListOutOfBounds, + let seq = self.begin_translate_sequence( + src, + dst, + src_tuple_size, + src_entry_align, + dst_tuple_size, + dst_entry_align, ); - self.free_temp_local(src_byte_len); - self.free_temp_local(dst_byte_len); - - if src_tuple_size > 0 || dst_tuple_size > 0 { - self.instruction(Block(BlockType::Empty)); - - self.instruction(LocalGet(src_len.idx)); - let remaining = self.local_tee_new_tmp(src_mem_opts.ptr()); - self.ptr_eqz(src_mem_opts); - self.instruction(BrIf(0)); - - self.instruction(LocalGet(src_mem.addr.idx)); - let cur_src_ptr = self.local_set_new_tmp(src_mem_opts.ptr()); - - self.instruction(LocalGet(dst_mem.addr.idx)); - let cur_dst_ptr = self.local_set_new_tmp(dst_mem_opts.ptr()); - - self.instruction(Loop(BlockType::Empty)); - + if let Some(ref loop_state) = seq.loop_state { let key_src = Source::Memory(self.memory_operand( - src_opts, + seq.src_opts, TempLocal { - idx: cur_src_ptr.idx, + idx: loop_state.cur_src_ptr.idx, ty: src_mem_opts.ptr(), needs_free: false, }, src_key_align, )); let key_dst = Destination::Memory(self.memory_operand( - dst_opts, + seq.dst_opts, TempLocal { - idx: cur_dst_ptr.idx, + idx: loop_state.cur_dst_ptr.idx, ty: dst_mem_opts.ptr(), needs_free: false, }, @@ -2844,33 +2853,32 @@ impl<'a, 'b> Compiler<'a, 'b> { )); self.translate(&src_map_ty.key, &key_src, &dst_map_ty.key, &key_dst); - // Advance pointers from key start to value start if src_value_offset > 0 { - self.instruction(LocalGet(cur_src_ptr.idx)); + self.instruction(LocalGet(loop_state.cur_src_ptr.idx)); self.ptr_uconst(src_mem_opts, src_value_offset); self.ptr_add(src_mem_opts); - self.instruction(LocalSet(cur_src_ptr.idx)); + self.instruction(LocalSet(loop_state.cur_src_ptr.idx)); } if dst_value_offset > 0 { - self.instruction(LocalGet(cur_dst_ptr.idx)); + self.instruction(LocalGet(loop_state.cur_dst_ptr.idx)); self.ptr_uconst(dst_mem_opts, dst_value_offset); self.ptr_add(dst_mem_opts); - self.instruction(LocalSet(cur_dst_ptr.idx)); + self.instruction(LocalSet(loop_state.cur_dst_ptr.idx)); } let value_src = Source::Memory(self.memory_operand( - src_opts, + seq.src_opts, TempLocal { - idx: cur_src_ptr.idx, + idx: loop_state.cur_src_ptr.idx, ty: src_mem_opts.ptr(), needs_free: false, }, src_value_align, )); let value_dst = Destination::Memory(self.memory_operand( - dst_opts, + seq.dst_opts, TempLocal { - idx: cur_dst_ptr.idx, + idx: loop_state.cur_dst_ptr.idx, ty: dst_mem_opts.ptr(), needs_free: false, }, @@ -2878,58 +2886,24 @@ impl<'a, 'b> Compiler<'a, 'b> { )); self.translate(&src_map_ty.value, &value_src, &dst_map_ty.value, &value_dst); - // Advance pointers past the value and any trailing padding to - // reach the next entry. After value translation, cur_ptr still - // points at the value start, so advance by value_size + trailing - // padding (i.e. tuple_size - value_offset). + // Advance past value + trailing padding to the next entry let src_advance_to_next = src_tuple_size - src_value_offset; if src_advance_to_next > 0 { - self.instruction(LocalGet(cur_src_ptr.idx)); + self.instruction(LocalGet(loop_state.cur_src_ptr.idx)); self.ptr_uconst(src_mem_opts, src_advance_to_next); self.ptr_add(src_mem_opts); - self.instruction(LocalSet(cur_src_ptr.idx)); + self.instruction(LocalSet(loop_state.cur_src_ptr.idx)); } let dst_advance_to_next = dst_tuple_size - dst_value_offset; if dst_advance_to_next > 0 { - self.instruction(LocalGet(cur_dst_ptr.idx)); + self.instruction(LocalGet(loop_state.cur_dst_ptr.idx)); self.ptr_uconst(dst_mem_opts, dst_advance_to_next); self.ptr_add(dst_mem_opts); - self.instruction(LocalSet(cur_dst_ptr.idx)); - } - - self.instruction(LocalGet(remaining.idx)); - self.ptr_iconst(src_mem_opts, -1); - self.ptr_add(src_mem_opts); - self.instruction(LocalTee(remaining.idx)); - self.ptr_br_if(src_mem_opts, 0); - self.instruction(End); // end of loop - self.instruction(End); // end of block - self.free_temp_local(cur_dst_ptr); - self.free_temp_local(cur_src_ptr); - self.free_temp_local(remaining); - } - - match dst { - Destination::Stack(s, _) => { - self.instruction(LocalGet(dst_mem.addr.idx)); - self.stack_set(&s[..1], dst_mem_opts.ptr()); - self.convert_src_len_to_dst(src_len.idx, src_mem_opts.ptr(), dst_mem_opts.ptr()); - self.stack_set(&s[1..], dst_mem_opts.ptr()); + self.instruction(LocalSet(loop_state.cur_dst_ptr.idx)); } - Destination::Memory(mem) => { - self.instruction(LocalGet(mem.addr.idx)); - self.instruction(LocalGet(dst_mem.addr.idx)); - self.ptr_store(mem); - self.instruction(LocalGet(mem.addr.idx)); - self.convert_src_len_to_dst(src_len.idx, src_mem_opts.ptr(), dst_mem_opts.ptr()); - self.ptr_store(&mem.bump(dst_mem_opts.ptr_size().into())); - } - Destination::Struct(_) | Destination::Array(_) => todo!("CM+GC"), } - self.free_temp_local(src_len); - self.free_temp_local(src_mem.addr); - self.free_temp_local(dst_mem.addr); + self.end_translate_sequence(seq, dst); } fn calculate_list_byte_len( @@ -4380,6 +4354,27 @@ where .0 } +/// State for the iteration loop inside a sequence translation. +struct SequenceLoopState { + remaining: TempLocal, + cur_src_ptr: TempLocal, + cur_dst_ptr: TempLocal, +} + +/// Holds all temporaries created by `begin_translate_sequence` so the +/// caller can emit a custom loop body before calling +/// `end_translate_sequence`. +struct SequenceTranslation<'a> { + src_len: TempLocal, + src_mem: Memory<'a>, + dst_mem: Memory<'a>, + src_opts: &'a Options, + dst_opts: &'a Options, + src_mem_opts: &'a LinearMemoryOptions, + dst_mem_opts: &'a LinearMemoryOptions, + loop_state: Option, +} + enum MallocSize { Const(u32), Local(u32), From d48daf553e3c2986445a38c7fd35581d10ed9397 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Fri, 27 Feb 2026 00:31:07 -0500 Subject: [PATCH 19/26] Fix cargo fmt formatting issues --- crates/environ/src/fact/trampoline.rs | 19 ++++++++----------- tests/all/component_model/func.rs | 2 +- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/crates/environ/src/fact/trampoline.rs b/crates/environ/src/fact/trampoline.rs index 67924e73511d..0476ea3c9f02 100644 --- a/crates/environ/src/fact/trampoline.rs +++ b/crates/environ/src/fact/trampoline.rs @@ -2575,8 +2575,11 @@ impl<'a, 'b> Compiler<'a, 'b> { // space for the sequence in the destination memory. This will also // internally insert checks that the returned pointer is aligned // correctly for the destination. - let dst_mem = - self.malloc(dst_opts, MallocSize::Local(dst_byte_len.idx), dst_element_align); + let dst_mem = self.malloc( + dst_opts, + MallocSize::Local(dst_byte_len.idx), + dst_element_align, + ); // With all the pointers and byte lengths verify that both the source // and the destination buffers are in-bounds. @@ -2642,11 +2645,7 @@ impl<'a, 'b> Compiler<'a, 'b> { /// If a loop was opened, emits: decrement remaining, BrIf to loop /// head, End loop, End block, and frees iteration locals. Then stores /// the ptr/len pair into the destination and frees all temporaries. - fn end_translate_sequence( - &mut self, - seq: SequenceTranslation<'_>, - dst: &Destination, - ) { + fn end_translate_sequence(&mut self, seq: SequenceTranslation<'_>, dst: &Destination) { if let Some(loop_state) = seq.loop_state { // Update the remaining count, falling through to break out if // it's zero now. @@ -2781,8 +2780,7 @@ impl<'a, 'b> Compiler<'a, 'b> { // Each map entry is a tuple following record layout rules. let src_key_abi = self.types.canonical_abi(&src_map_ty.key); let src_value_abi = self.types.canonical_abi(&src_map_ty.value); - let src_entry_abi = - CanonicalAbiInfo::record([src_key_abi, src_value_abi].into_iter()); + let src_entry_abi = CanonicalAbiInfo::record([src_key_abi, src_value_abi].into_iter()); let (_, src_key_align) = self.types.size_align(src_mem_opts, &src_map_ty.key); let (_, src_value_align) = self.types.size_align(src_mem_opts, &src_map_ty.value); let (src_tuple_size, src_entry_align) = if src_mem_opts.memory64 { @@ -2803,8 +2801,7 @@ impl<'a, 'b> Compiler<'a, 'b> { let dst_key_abi = self.types.canonical_abi(&dst_map_ty.key); let dst_value_abi = self.types.canonical_abi(&dst_map_ty.value); - let dst_entry_abi = - CanonicalAbiInfo::record([dst_key_abi, dst_value_abi].into_iter()); + let dst_entry_abi = CanonicalAbiInfo::record([dst_key_abi, dst_value_abi].into_iter()); let (_, dst_key_align) = self.types.size_align(dst_mem_opts, &dst_map_ty.key); let (_, dst_value_align) = self.types.size_align(dst_mem_opts, &dst_map_ty.value); let (dst_tuple_size, dst_entry_align) = if dst_mem_opts.memory64 { diff --git a/tests/all/component_model/func.rs b/tests/all/component_model/func.rs index 0cd986cca59f..e9dcfc3a589a 100644 --- a/tests/all/component_model/func.rs +++ b/tests/all/component_model/func.rs @@ -4645,4 +4645,4 @@ fn map_trampoline_alignment_u64_u8() -> Result<()> { } Ok(()) -} \ No newline at end of file +} From 24c75e05ad737ee6e0ca57776fbfa8908055dbdf Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Fri, 6 Mar 2026 22:54:36 -0500 Subject: [PATCH 20/26] Deduplicate map typecheck logic --- .../src/runtime/component/func/typed.rs | 43 +++++++++++-------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/func/typed.rs b/crates/wasmtime/src/runtime/component/func/typed.rs index 643e7214317a..2a2c9777b1c9 100644 --- a/crates/wasmtime/src/runtime/component/func/typed.rs +++ b/crates/wasmtime/src/runtime/component/func/typed.rs @@ -11,7 +11,8 @@ use core::iter; use core::marker; use core::mem::{self, MaybeUninit}; use core::str; -use wasmtime_environ::collections::HashMap; +#[cfg(not(feature = "std"))] +use crate::hash_map::HashMap; use wasmtime_environ::component::{ CanonicalAbiInfo, InterfaceType, MAX_FLAT_PARAMS, MAX_FLAT_RESULTS, OptionsIndex, StringEncoding, VariantInfo, @@ -2091,6 +2092,23 @@ unsafe impl Lift for WasmList { // Maps are represented as `list>` in the canonical ABI, so the // lowered form is a (pointer, length) pair just like lists. +fn typecheck_map(ty: &InterfaceType, types: &InstanceType<'_>) -> Result<()> +where + K: ComponentType, + V: ComponentType, +{ + match ty { + InterfaceType::Map(t) => { + let map_ty = &types.types[*t]; + K::typecheck(&map_ty.key, types)?; + V::typecheck(&map_ty.value, types)?; + Ok(()) + } + other => bail!("expected `map` found `{}`", desc(other)), + } +} + +#[cfg(not(feature = "std"))] unsafe impl ComponentType for HashMap where K: ComponentType, @@ -2101,18 +2119,11 @@ where const ABI: CanonicalAbiInfo = CanonicalAbiInfo::POINTER_PAIR; fn typecheck(ty: &InterfaceType, types: &InstanceType<'_>) -> Result<()> { - match ty { - InterfaceType::Map(t) => { - let map_ty = &types.types[*t]; - K::typecheck(&map_ty.key, types)?; - V::typecheck(&map_ty.value, types)?; - Ok(()) - } - other => bail!("expected `map` found `{}`", desc(other)), - } + typecheck_map::(ty, types) } } +#[cfg(not(feature = "std"))] unsafe impl Lower for HashMap where K: Lower, @@ -2196,6 +2207,7 @@ where Ok((ptr, len)) } +#[cfg(not(feature = "std"))] unsafe impl Lift for HashMap where K: Lift + Eq + Hash, @@ -2288,6 +2300,7 @@ where Ok(()) } +#[cfg(not(feature = "std"))] fn lift_map( cx: &mut LiftContext<'_>, key_ty: InterfaceType, @@ -2321,15 +2334,7 @@ where const ABI: CanonicalAbiInfo = CanonicalAbiInfo::POINTER_PAIR; fn typecheck(ty: &InterfaceType, types: &InstanceType<'_>) -> Result<()> { - match ty { - InterfaceType::Map(t) => { - let map_ty = &types.types[*t]; - K::typecheck(&map_ty.key, types)?; - V::typecheck(&map_ty.value, types)?; - Ok(()) - } - other => bail!("expected `map` found `{}`", desc(other)), - } + typecheck_map::(ty, types) } } From 9f860ff7f7b545d5e0d34c423ca890a590403ab8 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Sat, 7 Mar 2026 00:11:31 -0500 Subject: [PATCH 21/26] Deduplicate map lowering with linear_lower_map_to_flat and linear_lower_map_to_memory helpers --- .../src/runtime/component/func/typed.rs | 104 +++++++++--------- 1 file changed, 54 insertions(+), 50 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/func/typed.rs b/crates/wasmtime/src/runtime/component/func/typed.rs index 2a2c9777b1c9..63a2ade593db 100644 --- a/crates/wasmtime/src/runtime/component/func/typed.rs +++ b/crates/wasmtime/src/runtime/component/func/typed.rs @@ -2135,19 +2135,7 @@ where ty: InterfaceType, dst: &mut MaybeUninit<[ValRaw; 2]>, ) -> Result<()> { - let (key_ty, value_ty) = match ty { - InterfaceType::Map(i) => { - let m = &cx.types[i]; - (m.key, m.value) - } - _ => bad_type_info(), - }; - let (ptr, len) = lower_map_iter(cx, key_ty, value_ty, self.len(), self.iter())?; - // See "WRITEPTR64" above for why this is always storing a 64-bit - // integer. - map_maybe_uninit!(dst[0]).write(ValRaw::i64(ptr as i64)); - map_maybe_uninit!(dst[1]).write(ValRaw::i64(len as i64)); - Ok(()) + linear_lower_map_to_flat(cx, ty, self.len(), self.iter(), dst) } fn linear_lower_to_memory( @@ -2156,18 +2144,7 @@ where ty: InterfaceType, offset: usize, ) -> Result<()> { - let (key_ty, value_ty) = match ty { - InterfaceType::Map(i) => { - let m = &cx.types[i]; - (m.key, m.value) - } - _ => bad_type_info(), - }; - debug_assert!(offset % (Self::ALIGN32 as usize) == 0); - let (ptr, len) = lower_map_iter(cx, key_ty, value_ty, self.len(), self.iter())?; - *cx.get(offset + 0) = u32::try_from(ptr).unwrap().to_le_bytes(); - *cx.get(offset + 4) = u32::try_from(len).unwrap().to_le_bytes(); - Ok(()) + linear_lower_map_to_memory(cx, ty, self.len(), self.iter(), offset) } } @@ -2207,6 +2184,56 @@ where Ok((ptr, len)) } +fn linear_lower_map_to_flat<'a, K, V, U>( + cx: &mut LowerContext<'_, U>, + ty: InterfaceType, + len: usize, + iter: impl Iterator, + dst: &mut MaybeUninit<[ValRaw; 2]>, +) -> Result<()> +where + K: Lower + 'a, + V: Lower + 'a, +{ + let (key_ty, value_ty) = match ty { + InterfaceType::Map(i) => { + let m = &cx.types[i]; + (m.key, m.value) + } + _ => bad_type_info(), + }; + let (ptr, len) = lower_map_iter(cx, key_ty, value_ty, len, iter)?; + // See "WRITEPTR64" above for why this is always storing a 64-bit integer. + map_maybe_uninit!(dst[0]).write(ValRaw::i64(ptr as i64)); + map_maybe_uninit!(dst[1]).write(ValRaw::i64(len as i64)); + Ok(()) +} + +fn linear_lower_map_to_memory<'a, K, V, U>( + cx: &mut LowerContext<'_, U>, + ty: InterfaceType, + len: usize, + iter: impl Iterator, + offset: usize, +) -> Result<()> +where + K: Lower + 'a, + V: Lower + 'a, +{ + let (key_ty, value_ty) = match ty { + InterfaceType::Map(i) => { + let m = &cx.types[i]; + (m.key, m.value) + } + _ => bad_type_info(), + }; + debug_assert!(offset % (CanonicalAbiInfo::POINTER_PAIR.align32 as usize) == 0); + let (ptr, len) = lower_map_iter(cx, key_ty, value_ty, len, iter)?; + *cx.get(offset + 0) = u32::try_from(ptr).unwrap().to_le_bytes(); + *cx.get(offset + 4) = u32::try_from(len).unwrap().to_le_bytes(); + Ok(()) +} + #[cfg(not(feature = "std"))] unsafe impl Lift for HashMap where @@ -2350,19 +2377,7 @@ where ty: InterfaceType, dst: &mut MaybeUninit<[ValRaw; 2]>, ) -> Result<()> { - let (key_ty, value_ty) = match ty { - InterfaceType::Map(i) => { - let m = &cx.types[i]; - (m.key, m.value) - } - _ => bad_type_info(), - }; - let (ptr, len) = lower_map_iter(cx, key_ty, value_ty, self.len(), self.iter())?; - // See "WRITEPTR64" above for why this is always storing a 64-bit - // integer. - map_maybe_uninit!(dst[0]).write(ValRaw::i64(ptr as i64)); - map_maybe_uninit!(dst[1]).write(ValRaw::i64(len as i64)); - Ok(()) + linear_lower_map_to_flat(cx, ty, self.len(), self.iter(), dst) } fn linear_lower_to_memory( @@ -2371,18 +2386,7 @@ where ty: InterfaceType, offset: usize, ) -> Result<()> { - let (key_ty, value_ty) = match ty { - InterfaceType::Map(i) => { - let m = &cx.types[i]; - (m.key, m.value) - } - _ => bad_type_info(), - }; - debug_assert!(offset % (Self::ALIGN32 as usize) == 0); - let (ptr, len) = lower_map_iter(cx, key_ty, value_ty, self.len(), self.iter())?; - *cx.get(offset + 0) = u32::try_from(ptr).unwrap().to_le_bytes(); - *cx.get(offset + 4) = u32::try_from(len).unwrap().to_le_bytes(); - Ok(()) + linear_lower_map_to_memory(cx, ty, self.len(), self.iter(), offset) } } From 1d6ca26fdc6442ef04bf2184ea5d3d05a51897d4 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Sat, 7 Mar 2026 01:13:02 -0500 Subject: [PATCH 22/26] Clean up lift_try_map: use drop, move TryHashMap import to module scope --- .../src/runtime/component/func/typed.rs | 91 +++++++++++++++++-- 1 file changed, 82 insertions(+), 9 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/func/typed.rs b/crates/wasmtime/src/runtime/component/func/typed.rs index 63a2ade593db..f56810be461e 100644 --- a/crates/wasmtime/src/runtime/component/func/typed.rs +++ b/crates/wasmtime/src/runtime/component/func/typed.rs @@ -2,6 +2,8 @@ use crate::component::Instance; use crate::component::func::{Func, LiftContext, LowerContext}; use crate::component::matching::InstanceType; use crate::component::storage::{storage_as_slice, storage_as_slice_mut}; +#[cfg(not(feature = "std"))] +use crate::hash_map::HashMap; use crate::prelude::*; use crate::{AsContextMut, StoreContext, StoreContextMut, ValRaw}; use alloc::borrow::Cow; @@ -11,8 +13,6 @@ use core::iter; use core::marker; use core::mem::{self, MaybeUninit}; use core::str; -#[cfg(not(feature = "std"))] -use crate::hash_map::HashMap; use wasmtime_environ::component::{ CanonicalAbiInfo, InterfaceType, MAX_FLAT_PARAMS, MAX_FLAT_RESULTS, OptionsIndex, StringEncoding, VariantInfo, @@ -21,6 +21,9 @@ use wasmtime_environ::component::{ #[cfg(feature = "component-model-async")] use crate::component::concurrent::{self, AsAccessor, PreparedCall}; +#[cfg(feature = "std")] +use wasmtime_environ::collections::TryHashMap; + /// A statically-typed version of [`Func`] which takes `Params` as input and /// returns `Return`. /// @@ -2392,6 +2395,77 @@ where #[cfg(feature = "std")] unsafe impl Lift for std::collections::HashMap +where + K: Lift + Eq + Hash, + V: Lift, +{ + fn linear_lift_from_flat( + cx: &mut LiftContext<'_>, + ty: InterfaceType, + src: &Self::Lower, + ) -> Result { + let try_map = + as Lift>::linear_lift_from_flat( + cx, ty, src, + )?; + Ok(try_map.into_iter().collect()) + } + + fn linear_lift_from_memory( + cx: &mut LiftContext<'_>, + ty: InterfaceType, + bytes: &[u8], + ) -> Result { + let try_map = + as Lift>::linear_lift_from_memory( + cx, ty, bytes, + )?; + Ok(try_map.into_iter().collect()) + } +} + +#[cfg(feature = "std")] +unsafe impl ComponentType for wasmtime_environ::collections::TryHashMap +where + K: ComponentType, + V: ComponentType, +{ + type Lower = [ValRaw; 2]; + + const ABI: CanonicalAbiInfo = CanonicalAbiInfo::POINTER_PAIR; + + fn typecheck(ty: &InterfaceType, types: &InstanceType<'_>) -> Result<()> { + typecheck_map::(ty, types) + } +} + +#[cfg(feature = "std")] +unsafe impl Lower for wasmtime_environ::collections::TryHashMap +where + K: Lower, + V: Lower, +{ + fn linear_lower_to_flat( + &self, + cx: &mut LowerContext<'_, U>, + ty: InterfaceType, + dst: &mut MaybeUninit<[ValRaw; 2]>, + ) -> Result<()> { + linear_lower_map_to_flat(cx, ty, self.len(), self.iter(), dst) + } + + fn linear_lower_to_memory( + &self, + cx: &mut LowerContext<'_, U>, + ty: InterfaceType, + offset: usize, + ) -> Result<()> { + linear_lower_map_to_memory(cx, ty, self.len(), self.iter(), offset) + } +} + +#[cfg(feature = "std")] +unsafe impl Lift for wasmtime_environ::collections::TryHashMap where K: Lift + Eq + Hash, V: Lift, @@ -2412,7 +2486,7 @@ where let ptr = src[0].get_u32(); let len = src[1].get_u32(); let (ptr, len) = (usize::try_from(ptr)?, usize::try_from(len)?); - lift_std_map(cx, key_ty, value_ty, ptr, len) + lift_try_map(cx, key_ty, value_ty, ptr, len) } fn linear_lift_from_memory( @@ -2432,26 +2506,25 @@ where let ptr = u32::from_le_bytes(bytes[..4].try_into().unwrap()); let len = u32::from_le_bytes(bytes[4..].try_into().unwrap()); let (ptr, len) = (usize::try_from(ptr)?, usize::try_from(len)?); - lift_std_map(cx, key_ty, value_ty, ptr, len) + lift_try_map(cx, key_ty, value_ty, ptr, len) } } #[cfg(feature = "std")] -fn lift_std_map( +fn lift_try_map( cx: &mut LiftContext<'_>, key_ty: InterfaceType, value_ty: InterfaceType, ptr: usize, len: usize, -) -> Result> +) -> Result> where K: Lift + Eq + Hash, V: Lift, { - let mut result = std::collections::HashMap::with_capacity(len); + let mut result = TryHashMap::with_capacity(len)?; lift_map_pairs(cx, key_ty, value_ty, ptr, len, |k, v| { - result.insert(k, v); - Ok(()) + result.insert(k, v).map(drop).map_err(Into::into) })?; Ok(result) } From 3c8797713ff0cb1ecf2afb6b5c98252f5925ef51 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Sat, 7 Mar 2026 01:26:35 -0500 Subject: [PATCH 23/26] Fix CI: arbtest overflow and no-std HashMap lift_map - component_fuzz: use saturating_sub in generate_hashable_key to prevent underflow when fuel is 0 and Enum variant is chosen - typed: remove incorrect ? operators in lift_map for hashbrown::HashMap (with_capacity and insert don't return Result) --- crates/test-util/src/component_fuzz.rs | 2 +- crates/wasmtime/src/runtime/component/func/typed.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/test-util/src/component_fuzz.rs b/crates/test-util/src/component_fuzz.rs index e1a121022d81..ca0ed4a89001 100644 --- a/crates/test-util/src/component_fuzz.rs +++ b/crates/test-util/src/component_fuzz.rs @@ -227,7 +227,7 @@ impl Type { 10 => Type::String, 11 => { let amt = u.int_in_range(1..=(*fuel).max(1).min(257))?; - *fuel -= amt; + *fuel = fuel.saturating_sub(amt); Type::Enum(amt) } _ => unreachable!(), diff --git a/crates/wasmtime/src/runtime/component/func/typed.rs b/crates/wasmtime/src/runtime/component/func/typed.rs index f56810be461e..1cf0a07b14b9 100644 --- a/crates/wasmtime/src/runtime/component/func/typed.rs +++ b/crates/wasmtime/src/runtime/component/func/typed.rs @@ -2342,9 +2342,9 @@ where K: Lift + Eq + Hash, V: Lift, { - let mut result = HashMap::with_capacity(len)?; + let mut result = HashMap::with_capacity(len); lift_map_pairs(cx, key_ty, value_ty, ptr, len, |k, v| { - result.insert(k, v)?; + result.insert(k, v); Ok(()) })?; Ok(result) From 12ed2880f42ea9a1d19b719a7ecf052efd675caa Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Sat, 7 Mar 2026 03:31:21 -0500 Subject: [PATCH 24/26] Store map tuple layout in TypeMap Compute map entry ABI and value offsets once during type building, and reuse that metadata in runtime map lift/lower paths instead of recalculating tuple layout at each call site. --- crates/environ/src/component/types.rs | 8 + crates/environ/src/component/types_builder.rs | 15 ++ .../src/runtime/component/func/typed.rs | 197 ++++++++++++++---- .../wasmtime/src/runtime/component/values.rs | 46 ++-- 4 files changed, 187 insertions(+), 79 deletions(-) diff --git a/crates/environ/src/component/types.rs b/crates/environ/src/component/types.rs index 5e7e056f3bc2..5cbfde24c693 100644 --- a/crates/environ/src/component/types.rs +++ b/crates/environ/src/component/types.rs @@ -1195,6 +1195,14 @@ pub struct TypeMap { pub key: InterfaceType, /// The value type of the map. pub value: InterfaceType, + /// Byte information for each map entry represented as `tuple`. + pub entry_abi: CanonicalAbiInfo, + /// Offset in bytes from the start of the entry tuple to the value field in + /// memory32. + pub value_offset32: u32, + /// Offset in bytes from the start of the entry tuple to the value field in + /// memory64. + pub value_offset64: u32, } /// Shape of a "fixed size list" interface type. diff --git a/crates/environ/src/component/types_builder.rs b/crates/environ/src/component/types_builder.rs index d35280856d7a..ae60bda8d1b2 100644 --- a/crates/environ/src/component/types_builder.rs +++ b/crates/environ/src/component/types_builder.rs @@ -701,9 +701,24 @@ impl ComponentTypesBuilder { assert_eq!(types.id(), self.module_types.validator_id()); let key_ty = self.valtype(types, key)?; let value_ty = self.valtype(types, value)?; + let key_abi = self.component_types.canonical_abi(&key_ty); + let value_abi = self.component_types.canonical_abi(&value_ty); + let entry_abi = CanonicalAbiInfo::record([key_abi, value_abi].into_iter()); + + let mut offset32 = 0; + key_abi.next_field32(&mut offset32); + let value_offset32 = value_abi.next_field32(&mut offset32); + + let mut offset64 = 0; + key_abi.next_field64(&mut offset64); + let value_offset64 = value_abi.next_field64(&mut offset64); + Ok(self.add_map_type(TypeMap { key: key_ty, value: value_ty, + entry_abi, + value_offset32, + value_offset64, })) } diff --git a/crates/wasmtime/src/runtime/component/func/typed.rs b/crates/wasmtime/src/runtime/component/func/typed.rs index 1cf0a07b14b9..93b661ed0d33 100644 --- a/crates/wasmtime/src/runtime/component/func/typed.rs +++ b/crates/wasmtime/src/runtime/component/func/typed.rs @@ -2155,6 +2155,9 @@ fn lower_map_iter<'a, K, V, U>( cx: &mut LowerContext<'_, U>, key_ty: InterfaceType, value_ty: InterfaceType, + tuple_size: usize, + tuple_align: u32, + value_offset: usize, len: usize, iter: impl Iterator, ) -> Result<(usize, usize)> @@ -2162,11 +2165,6 @@ where K: Lower + 'a, V: Lower + 'a, { - // Calculate the tuple layout: each entry is a (key, value) record. - let tuple_abi = CanonicalAbiInfo::record_static(&[K::ABI, V::ABI]); - let tuple_size = tuple_abi.size32 as usize; - let tuple_align = tuple_abi.align32; - let size = len .checked_mul(tuple_size) .ok_or_else(|| format_err!("size overflow copying a map"))?; @@ -2174,13 +2172,10 @@ where let mut entry_offset = ptr; for (key, value) in iter { - // Lower key at the start of the tuple - let mut field_offset = 0usize; - let key_field = K::ABI.next_field32_size(&mut field_offset); - ::linear_lower_to_memory(key, cx, key_ty, entry_offset + key_field)?; - // Lower value at its aligned offset within the tuple - let value_field = V::ABI.next_field32_size(&mut field_offset); - ::linear_lower_to_memory(value, cx, value_ty, entry_offset + value_field)?; + // Keys are the first field in each entry tuple. + ::linear_lower_to_memory(key, cx, key_ty, entry_offset)?; + // Values start at the precomputed value offset within the tuple. + ::linear_lower_to_memory(value, cx, value_ty, entry_offset + value_offset)?; entry_offset += tuple_size; } @@ -2198,14 +2193,29 @@ where K: Lower + 'a, V: Lower + 'a, { - let (key_ty, value_ty) = match ty { + let (key_ty, value_ty, tuple_size, tuple_align, value_offset) = match ty { InterfaceType::Map(i) => { let m = &cx.types[i]; - (m.key, m.value) + ( + m.key, + m.value, + usize::try_from(m.entry_abi.size32).unwrap(), + m.entry_abi.align32, + usize::try_from(m.value_offset32).unwrap(), + ) } _ => bad_type_info(), }; - let (ptr, len) = lower_map_iter(cx, key_ty, value_ty, len, iter)?; + let (ptr, len) = lower_map_iter( + cx, + key_ty, + value_ty, + tuple_size, + tuple_align, + value_offset, + len, + iter, + )?; // See "WRITEPTR64" above for why this is always storing a 64-bit integer. map_maybe_uninit!(dst[0]).write(ValRaw::i64(ptr as i64)); map_maybe_uninit!(dst[1]).write(ValRaw::i64(len as i64)); @@ -2223,15 +2233,30 @@ where K: Lower + 'a, V: Lower + 'a, { - let (key_ty, value_ty) = match ty { + let (key_ty, value_ty, tuple_size, tuple_align, value_offset) = match ty { InterfaceType::Map(i) => { let m = &cx.types[i]; - (m.key, m.value) + ( + m.key, + m.value, + usize::try_from(m.entry_abi.size32).unwrap(), + m.entry_abi.align32, + usize::try_from(m.value_offset32).unwrap(), + ) } _ => bad_type_info(), }; debug_assert!(offset % (CanonicalAbiInfo::POINTER_PAIR.align32 as usize) == 0); - let (ptr, len) = lower_map_iter(cx, key_ty, value_ty, len, iter)?; + let (ptr, len) = lower_map_iter( + cx, + key_ty, + value_ty, + tuple_size, + tuple_align, + value_offset, + len, + iter, + )?; *cx.get(offset + 0) = u32::try_from(ptr).unwrap().to_le_bytes(); *cx.get(offset + 4) = u32::try_from(len).unwrap().to_le_bytes(); Ok(()) @@ -2248,10 +2273,16 @@ where ty: InterfaceType, src: &Self::Lower, ) -> Result { - let (key_ty, value_ty) = match ty { + let (key_ty, value_ty, tuple_size, tuple_align, value_offset) = match ty { InterfaceType::Map(i) => { let m = &cx.types[i]; - (m.key, m.value) + ( + m.key, + m.value, + usize::try_from(m.entry_abi.size32).unwrap(), + usize::try_from(m.entry_abi.align32).unwrap(), + usize::try_from(m.value_offset32).unwrap(), + ) } _ => bad_type_info(), }; @@ -2259,7 +2290,16 @@ where let ptr = src[0].get_u32(); let len = src[1].get_u32(); let (ptr, len) = (usize::try_from(ptr)?, usize::try_from(len)?); - lift_map(cx, key_ty, value_ty, ptr, len) + lift_map( + cx, + key_ty, + value_ty, + tuple_size, + tuple_align, + value_offset, + ptr, + len, + ) } fn linear_lift_from_memory( @@ -2267,10 +2307,16 @@ where ty: InterfaceType, bytes: &[u8], ) -> Result { - let (key_ty, value_ty) = match ty { + let (key_ty, value_ty, tuple_size, tuple_align, value_offset) = match ty { InterfaceType::Map(i) => { let m = &cx.types[i]; - (m.key, m.value) + ( + m.key, + m.value, + usize::try_from(m.entry_abi.size32).unwrap(), + usize::try_from(m.entry_abi.align32).unwrap(), + usize::try_from(m.value_offset32).unwrap(), + ) } _ => bad_type_info(), }; @@ -2279,7 +2325,16 @@ where let ptr = u32::from_le_bytes(bytes[..4].try_into().unwrap()); let len = u32::from_le_bytes(bytes[4..].try_into().unwrap()); let (ptr, len) = (usize::try_from(ptr)?, usize::try_from(len)?); - lift_map(cx, key_ty, value_ty, ptr, len) + lift_map( + cx, + key_ty, + value_ty, + tuple_size, + tuple_align, + value_offset, + ptr, + len, + ) } } @@ -2289,6 +2344,9 @@ fn lift_map_pairs( cx: &mut LiftContext<'_>, key_ty: InterfaceType, value_ty: InterfaceType, + tuple_size: usize, + tuple_align: usize, + value_offset: usize, ptr: usize, len: usize, mut insert: impl FnMut(K, V) -> Result<()>, @@ -2297,10 +2355,6 @@ where K: Lift, V: Lift, { - let tuple_abi = CanonicalAbiInfo::record_static(&[K::ABI, V::ABI]); - let tuple_size = tuple_abi.size32 as usize; - let tuple_align = tuple_abi.align32 as usize; - match len .checked_mul(tuple_size) .and_then(|total| ptr.checked_add(total)) @@ -2315,13 +2369,10 @@ where for i in 0..len { let entry_base = ptr + (i * tuple_size); - let mut field_offset = 0usize; - let key_field = K::ABI.next_field32_size(&mut field_offset); - let key_bytes = &cx.memory()[entry_base + key_field..][..K::SIZE32]; + let key_bytes = &cx.memory()[entry_base..][..K::SIZE32]; let key = K::linear_lift_from_memory(cx, key_ty, key_bytes)?; - let value_field = V::ABI.next_field32_size(&mut field_offset); - let value_bytes = &cx.memory()[entry_base + value_field..][..V::SIZE32]; + let value_bytes = &cx.memory()[entry_base + value_offset..][..V::SIZE32]; let value = V::linear_lift_from_memory(cx, value_ty, value_bytes)?; insert(key, value)?; @@ -2335,6 +2386,9 @@ fn lift_map( cx: &mut LiftContext<'_>, key_ty: InterfaceType, value_ty: InterfaceType, + tuple_size: usize, + tuple_align: usize, + value_offset: usize, ptr: usize, len: usize, ) -> Result> @@ -2343,10 +2397,20 @@ where V: Lift, { let mut result = HashMap::with_capacity(len); - lift_map_pairs(cx, key_ty, value_ty, ptr, len, |k, v| { - result.insert(k, v); - Ok(()) - })?; + lift_map_pairs( + cx, + key_ty, + value_ty, + tuple_size, + tuple_align, + value_offset, + ptr, + len, + |k, v| { + result.insert(k, v); + Ok(()) + }, + )?; Ok(result) } @@ -2475,10 +2539,16 @@ where ty: InterfaceType, src: &Self::Lower, ) -> Result { - let (key_ty, value_ty) = match ty { + let (key_ty, value_ty, tuple_size, tuple_align, value_offset) = match ty { InterfaceType::Map(i) => { let m = &cx.types[i]; - (m.key, m.value) + ( + m.key, + m.value, + usize::try_from(m.entry_abi.size32).unwrap(), + usize::try_from(m.entry_abi.align32).unwrap(), + usize::try_from(m.value_offset32).unwrap(), + ) } _ => bad_type_info(), }; @@ -2486,7 +2556,16 @@ where let ptr = src[0].get_u32(); let len = src[1].get_u32(); let (ptr, len) = (usize::try_from(ptr)?, usize::try_from(len)?); - lift_try_map(cx, key_ty, value_ty, ptr, len) + lift_try_map( + cx, + key_ty, + value_ty, + tuple_size, + tuple_align, + value_offset, + ptr, + len, + ) } fn linear_lift_from_memory( @@ -2494,10 +2573,16 @@ where ty: InterfaceType, bytes: &[u8], ) -> Result { - let (key_ty, value_ty) = match ty { + let (key_ty, value_ty, tuple_size, tuple_align, value_offset) = match ty { InterfaceType::Map(i) => { let m = &cx.types[i]; - (m.key, m.value) + ( + m.key, + m.value, + usize::try_from(m.entry_abi.size32).unwrap(), + usize::try_from(m.entry_abi.align32).unwrap(), + usize::try_from(m.value_offset32).unwrap(), + ) } _ => bad_type_info(), }; @@ -2506,7 +2591,16 @@ where let ptr = u32::from_le_bytes(bytes[..4].try_into().unwrap()); let len = u32::from_le_bytes(bytes[4..].try_into().unwrap()); let (ptr, len) = (usize::try_from(ptr)?, usize::try_from(len)?); - lift_try_map(cx, key_ty, value_ty, ptr, len) + lift_try_map( + cx, + key_ty, + value_ty, + tuple_size, + tuple_align, + value_offset, + ptr, + len, + ) } } @@ -2515,6 +2609,9 @@ fn lift_try_map( cx: &mut LiftContext<'_>, key_ty: InterfaceType, value_ty: InterfaceType, + tuple_size: usize, + tuple_align: usize, + value_offset: usize, ptr: usize, len: usize, ) -> Result> @@ -2523,9 +2620,17 @@ where V: Lift, { let mut result = TryHashMap::with_capacity(len)?; - lift_map_pairs(cx, key_ty, value_ty, ptr, len, |k, v| { - result.insert(k, v).map(drop).map_err(Into::into) - })?; + lift_map_pairs( + cx, + key_ty, + value_ty, + tuple_size, + tuple_align, + value_offset, + ptr, + len, + |k, v| result.insert(k, v).map(drop).map_err(Into::into), + )?; Ok(result) } diff --git a/crates/wasmtime/src/runtime/component/values.rs b/crates/wasmtime/src/runtime/component/values.rs index ff2df8754fe7..efab9768cd7f 100644 --- a/crates/wasmtime/src/runtime/component/values.rs +++ b/crates/wasmtime/src/runtime/component/values.rs @@ -7,8 +7,8 @@ use core::mem::MaybeUninit; use core::slice::{Iter, IterMut}; use wasmtime_component_util::{DiscriminantSize, FlagsSize}; use wasmtime_environ::component::{ - CanonicalAbiInfo, InterfaceType, TypeEnum, TypeFlags, TypeListIndex, TypeMapIndex, TypeOption, - TypeResult, TypeVariant, VariantInfo, + CanonicalAbiInfo, InterfaceType, TypeEnum, TypeFlags, TypeListIndex, TypeMap, TypeMapIndex, + TypeOption, TypeResult, TypeVariant, VariantInfo, }; /// Represents possible runtime values which a component function can either @@ -443,7 +443,7 @@ impl Val { (InterfaceType::List(_), _) => unexpected(ty, self), (InterfaceType::Map(ty), Val::Map(pairs)) => { let map_ty = &cx.types[ty]; - let (ptr, len) = lower_map(cx, map_ty.key, map_ty.value, pairs)?; + let (ptr, len) = lower_map(cx, map_ty, pairs)?; next_mut(dst).write(ValRaw::i64(ptr as i64)); next_mut(dst).write(ValRaw::i64(len as i64)); Ok(()) @@ -580,7 +580,7 @@ impl Val { (InterfaceType::List(_), _) => unexpected(ty, self), (InterfaceType::Map(ty_idx), Val::Map(values)) => { let map_ty = &cx.types[ty_idx]; - let (ptr, len) = lower_map(cx, map_ty.key, map_ty.value, values)?; + let (ptr, len) = lower_map(cx, map_ty, values)?; // FIXME(#4311): needs memory64 handling *cx.get(offset + 0) = u32::try_from(ptr).unwrap().to_le_bytes(); *cx.get(offset + 4) = u32::try_from(len).unwrap().to_le_bytes(); @@ -981,22 +981,13 @@ fn load_map(cx: &mut LiftContext<'_>, ty: TypeMapIndex, ptr: usize, len: usize) let key_ty = map_ty.key; let value_ty = map_ty.value; - // Calculate tuple layout using canonical ABI alignment rules let key_abi = cx.types.canonical_abi(&key_ty); let value_abi = cx.types.canonical_abi(&value_ty); let key_size = usize::try_from(key_abi.size32).unwrap(); let value_size = usize::try_from(value_abi.size32).unwrap(); - - // Calculate value offset: align key_size to value alignment - let mut offset = u32::try_from(key_size).unwrap(); - let value_offset = value_abi.next_field32(&mut offset); - let value_offset = usize::try_from(value_offset).unwrap(); - - // Tuple size is the final offset aligned to max alignment - let tuple_alignment = key_abi.align32.max(value_abi.align32); - let tuple_size = usize::try_from(offset).unwrap(); - let tuple_size = (tuple_size + usize::try_from(tuple_alignment)? - 1) - & !(usize::try_from(tuple_alignment)? - 1); + let value_offset = usize::try_from(map_ty.value_offset32).unwrap(); + let tuple_alignment = map_ty.entry_abi.align32; + let tuple_size = usize::try_from(map_ty.entry_abi.size32).unwrap(); // Bounds check match len @@ -1119,25 +1110,14 @@ fn lower_list( /// Lower a map as list> with the specified key and value types. fn lower_map( cx: &mut LowerContext<'_, T>, - key_type: InterfaceType, - value_type: InterfaceType, + map_ty: &TypeMap, pairs: &[(Val, Val)], ) -> Result<(usize, usize)> { - // Calculate tuple layout using canonical ABI alignment rules - let key_abi = cx.types.canonical_abi(&key_type); - let value_abi = cx.types.canonical_abi(&value_type); - let key_size = usize::try_from(key_abi.size32)?; - - // Calculate value offset: align key_size to value alignment - let mut offset = u32::try_from(key_size).unwrap(); - let value_offset = value_abi.next_field32(&mut offset); - let value_offset = usize::try_from(value_offset).unwrap(); - - // Tuple size is the final offset aligned to max alignment - let tuple_align = key_abi.align32.max(value_abi.align32); - let tuple_size = usize::try_from(offset).unwrap(); - let tuple_size = - (tuple_size + usize::try_from(tuple_align)? - 1) & !(usize::try_from(tuple_align)? - 1); + let key_type = map_ty.key; + let value_type = map_ty.value; + let value_offset = usize::try_from(map_ty.value_offset32).unwrap(); + let tuple_align = map_ty.entry_abi.align32; + let tuple_size = usize::try_from(map_ty.entry_abi.size32).unwrap(); let size = pairs .len() From b94eac8c9b2f1816b1696e11fce69fd891f17251 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Sat, 7 Mar 2026 03:37:39 -0500 Subject: [PATCH 25/26] Refactor map ABI argument passing Bundle map lift/lower layout and type metadata into a small MapAbi32 helper so map helper calls stay concise without changing behavior. --- .../src/runtime/component/func/typed.rs | 262 +++++------------- 1 file changed, 66 insertions(+), 196 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/func/typed.rs b/crates/wasmtime/src/runtime/component/func/typed.rs index 93b661ed0d33..960c07624e28 100644 --- a/crates/wasmtime/src/runtime/component/func/typed.rs +++ b/crates/wasmtime/src/runtime/component/func/typed.rs @@ -14,8 +14,8 @@ use core::marker; use core::mem::{self, MaybeUninit}; use core::str; use wasmtime_environ::component::{ - CanonicalAbiInfo, InterfaceType, MAX_FLAT_PARAMS, MAX_FLAT_RESULTS, OptionsIndex, - StringEncoding, VariantInfo, + CanonicalAbiInfo, ComponentTypes, InterfaceType, MAX_FLAT_PARAMS, MAX_FLAT_RESULTS, + OptionsIndex, StringEncoding, VariantInfo, }; #[cfg(feature = "component-model-async")] @@ -2111,6 +2111,31 @@ where } } +#[derive(Copy, Clone)] +struct MapAbi32 { + key_ty: InterfaceType, + value_ty: InterfaceType, + tuple_size: usize, + tuple_align: u32, + value_offset: usize, +} + +fn map_abi32(ty: InterfaceType, types: &ComponentTypes) -> MapAbi32 { + match ty { + InterfaceType::Map(i) => { + let m = &types[i]; + MapAbi32 { + key_ty: m.key, + value_ty: m.value, + tuple_size: usize::try_from(m.entry_abi.size32).unwrap(), + tuple_align: m.entry_abi.align32, + value_offset: usize::try_from(m.value_offset32).unwrap(), + } + } + _ => bad_type_info(), + } +} + #[cfg(not(feature = "std"))] unsafe impl ComponentType for HashMap where @@ -2153,11 +2178,7 @@ where fn lower_map_iter<'a, K, V, U>( cx: &mut LowerContext<'_, U>, - key_ty: InterfaceType, - value_ty: InterfaceType, - tuple_size: usize, - tuple_align: u32, - value_offset: usize, + map: MapAbi32, len: usize, iter: impl Iterator, ) -> Result<(usize, usize)> @@ -2166,17 +2187,22 @@ where V: Lower + 'a, { let size = len - .checked_mul(tuple_size) + .checked_mul(map.tuple_size) .ok_or_else(|| format_err!("size overflow copying a map"))?; - let ptr = cx.realloc(0, 0, tuple_align, size)?; + let ptr = cx.realloc(0, 0, map.tuple_align, size)?; let mut entry_offset = ptr; for (key, value) in iter { // Keys are the first field in each entry tuple. - ::linear_lower_to_memory(key, cx, key_ty, entry_offset)?; + ::linear_lower_to_memory(key, cx, map.key_ty, entry_offset)?; // Values start at the precomputed value offset within the tuple. - ::linear_lower_to_memory(value, cx, value_ty, entry_offset + value_offset)?; - entry_offset += tuple_size; + ::linear_lower_to_memory( + value, + cx, + map.value_ty, + entry_offset + map.value_offset, + )?; + entry_offset += map.tuple_size; } Ok((ptr, len)) @@ -2193,29 +2219,8 @@ where K: Lower + 'a, V: Lower + 'a, { - let (key_ty, value_ty, tuple_size, tuple_align, value_offset) = match ty { - InterfaceType::Map(i) => { - let m = &cx.types[i]; - ( - m.key, - m.value, - usize::try_from(m.entry_abi.size32).unwrap(), - m.entry_abi.align32, - usize::try_from(m.value_offset32).unwrap(), - ) - } - _ => bad_type_info(), - }; - let (ptr, len) = lower_map_iter( - cx, - key_ty, - value_ty, - tuple_size, - tuple_align, - value_offset, - len, - iter, - )?; + let map = map_abi32(ty, &cx.types); + let (ptr, len) = lower_map_iter(cx, map, len, iter)?; // See "WRITEPTR64" above for why this is always storing a 64-bit integer. map_maybe_uninit!(dst[0]).write(ValRaw::i64(ptr as i64)); map_maybe_uninit!(dst[1]).write(ValRaw::i64(len as i64)); @@ -2233,30 +2238,9 @@ where K: Lower + 'a, V: Lower + 'a, { - let (key_ty, value_ty, tuple_size, tuple_align, value_offset) = match ty { - InterfaceType::Map(i) => { - let m = &cx.types[i]; - ( - m.key, - m.value, - usize::try_from(m.entry_abi.size32).unwrap(), - m.entry_abi.align32, - usize::try_from(m.value_offset32).unwrap(), - ) - } - _ => bad_type_info(), - }; + let map = map_abi32(ty, &cx.types); debug_assert!(offset % (CanonicalAbiInfo::POINTER_PAIR.align32 as usize) == 0); - let (ptr, len) = lower_map_iter( - cx, - key_ty, - value_ty, - tuple_size, - tuple_align, - value_offset, - len, - iter, - )?; + let (ptr, len) = lower_map_iter(cx, map, len, iter)?; *cx.get(offset + 0) = u32::try_from(ptr).unwrap().to_le_bytes(); *cx.get(offset + 4) = u32::try_from(len).unwrap().to_le_bytes(); Ok(()) @@ -2273,33 +2257,12 @@ where ty: InterfaceType, src: &Self::Lower, ) -> Result { - let (key_ty, value_ty, tuple_size, tuple_align, value_offset) = match ty { - InterfaceType::Map(i) => { - let m = &cx.types[i]; - ( - m.key, - m.value, - usize::try_from(m.entry_abi.size32).unwrap(), - usize::try_from(m.entry_abi.align32).unwrap(), - usize::try_from(m.value_offset32).unwrap(), - ) - } - _ => bad_type_info(), - }; + let map = map_abi32(ty, &cx.types); // FIXME(#4311): needs memory64 treatment let ptr = src[0].get_u32(); let len = src[1].get_u32(); let (ptr, len) = (usize::try_from(ptr)?, usize::try_from(len)?); - lift_map( - cx, - key_ty, - value_ty, - tuple_size, - tuple_align, - value_offset, - ptr, - len, - ) + lift_map(cx, map, ptr, len) } fn linear_lift_from_memory( @@ -2307,34 +2270,13 @@ where ty: InterfaceType, bytes: &[u8], ) -> Result { - let (key_ty, value_ty, tuple_size, tuple_align, value_offset) = match ty { - InterfaceType::Map(i) => { - let m = &cx.types[i]; - ( - m.key, - m.value, - usize::try_from(m.entry_abi.size32).unwrap(), - usize::try_from(m.entry_abi.align32).unwrap(), - usize::try_from(m.value_offset32).unwrap(), - ) - } - _ => bad_type_info(), - }; + let map = map_abi32(ty, &cx.types); debug_assert!((bytes.as_ptr() as usize) % (Self::ALIGN32 as usize) == 0); // FIXME(#4311): needs memory64 treatment let ptr = u32::from_le_bytes(bytes[..4].try_into().unwrap()); let len = u32::from_le_bytes(bytes[4..].try_into().unwrap()); let (ptr, len) = (usize::try_from(ptr)?, usize::try_from(len)?); - lift_map( - cx, - key_ty, - value_ty, - tuple_size, - tuple_align, - value_offset, - ptr, - len, - ) + lift_map(cx, map, ptr, len) } } @@ -2342,11 +2284,7 @@ where /// (key, value) pair, forwarding them to `insert`. fn lift_map_pairs( cx: &mut LiftContext<'_>, - key_ty: InterfaceType, - value_ty: InterfaceType, - tuple_size: usize, - tuple_align: usize, - value_offset: usize, + map: MapAbi32, ptr: usize, len: usize, mut insert: impl FnMut(K, V) -> Result<()>, @@ -2356,24 +2294,24 @@ where V: Lift, { match len - .checked_mul(tuple_size) + .checked_mul(map.tuple_size) .and_then(|total| ptr.checked_add(total)) { Some(n) if n <= cx.memory().len() => {} _ => bail!("map pointer/length out of bounds of memory"), } - if ptr % tuple_align != 0 { + if ptr % (map.tuple_align as usize) != 0 { bail!("map pointer is not aligned"); } for i in 0..len { - let entry_base = ptr + (i * tuple_size); + let entry_base = ptr + (i * map.tuple_size); let key_bytes = &cx.memory()[entry_base..][..K::SIZE32]; - let key = K::linear_lift_from_memory(cx, key_ty, key_bytes)?; + let key = K::linear_lift_from_memory(cx, map.key_ty, key_bytes)?; - let value_bytes = &cx.memory()[entry_base + value_offset..][..V::SIZE32]; - let value = V::linear_lift_from_memory(cx, value_ty, value_bytes)?; + let value_bytes = &cx.memory()[entry_base + map.value_offset..][..V::SIZE32]; + let value = V::linear_lift_from_memory(cx, map.value_ty, value_bytes)?; insert(key, value)?; } @@ -2384,11 +2322,7 @@ where #[cfg(not(feature = "std"))] fn lift_map( cx: &mut LiftContext<'_>, - key_ty: InterfaceType, - value_ty: InterfaceType, - tuple_size: usize, - tuple_align: usize, - value_offset: usize, + map: MapAbi32, ptr: usize, len: usize, ) -> Result> @@ -2397,20 +2331,10 @@ where V: Lift, { let mut result = HashMap::with_capacity(len); - lift_map_pairs( - cx, - key_ty, - value_ty, - tuple_size, - tuple_align, - value_offset, - ptr, - len, - |k, v| { - result.insert(k, v); - Ok(()) - }, - )?; + lift_map_pairs(cx, map, ptr, len, |k, v| { + result.insert(k, v); + Ok(()) + })?; Ok(result) } @@ -2539,33 +2463,12 @@ where ty: InterfaceType, src: &Self::Lower, ) -> Result { - let (key_ty, value_ty, tuple_size, tuple_align, value_offset) = match ty { - InterfaceType::Map(i) => { - let m = &cx.types[i]; - ( - m.key, - m.value, - usize::try_from(m.entry_abi.size32).unwrap(), - usize::try_from(m.entry_abi.align32).unwrap(), - usize::try_from(m.value_offset32).unwrap(), - ) - } - _ => bad_type_info(), - }; + let map = map_abi32(ty, &cx.types); // FIXME(#4311): needs memory64 treatment let ptr = src[0].get_u32(); let len = src[1].get_u32(); let (ptr, len) = (usize::try_from(ptr)?, usize::try_from(len)?); - lift_try_map( - cx, - key_ty, - value_ty, - tuple_size, - tuple_align, - value_offset, - ptr, - len, - ) + lift_try_map(cx, map, ptr, len) } fn linear_lift_from_memory( @@ -2573,45 +2476,20 @@ where ty: InterfaceType, bytes: &[u8], ) -> Result { - let (key_ty, value_ty, tuple_size, tuple_align, value_offset) = match ty { - InterfaceType::Map(i) => { - let m = &cx.types[i]; - ( - m.key, - m.value, - usize::try_from(m.entry_abi.size32).unwrap(), - usize::try_from(m.entry_abi.align32).unwrap(), - usize::try_from(m.value_offset32).unwrap(), - ) - } - _ => bad_type_info(), - }; + let map = map_abi32(ty, &cx.types); debug_assert!((bytes.as_ptr() as usize) % (Self::ALIGN32 as usize) == 0); // FIXME(#4311): needs memory64 treatment let ptr = u32::from_le_bytes(bytes[..4].try_into().unwrap()); let len = u32::from_le_bytes(bytes[4..].try_into().unwrap()); let (ptr, len) = (usize::try_from(ptr)?, usize::try_from(len)?); - lift_try_map( - cx, - key_ty, - value_ty, - tuple_size, - tuple_align, - value_offset, - ptr, - len, - ) + lift_try_map(cx, map, ptr, len) } } #[cfg(feature = "std")] fn lift_try_map( cx: &mut LiftContext<'_>, - key_ty: InterfaceType, - value_ty: InterfaceType, - tuple_size: usize, - tuple_align: usize, - value_offset: usize, + map: MapAbi32, ptr: usize, len: usize, ) -> Result> @@ -2620,17 +2498,9 @@ where V: Lift, { let mut result = TryHashMap::with_capacity(len)?; - lift_map_pairs( - cx, - key_ty, - value_ty, - tuple_size, - tuple_align, - value_offset, - ptr, - len, - |k, v| result.insert(k, v).map(drop).map_err(Into::into), - )?; + lift_map_pairs(cx, map, ptr, len, |k, v| { + result.insert(k, v).map(drop).map_err(Into::into) + })?; Ok(result) } From 36008b63de2e87a011cd13fac9e16b127b859111 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Mon, 9 Mar 2026 19:13:12 -0400 Subject: [PATCH 26/26] Fix CI: enable component_model_map in fuzzing and handle map in arbitrary_val The fuzzer's component_api oracle was generating map types but the engine didn't have the map feature enabled, and arbitrary_val had no arm for Type::Map. Enable component_model_map in the store helper (matching how component_model_async is forced on) and implement arbitrary value generation for map types. --- crates/fuzzing/src/oracles/component_api.rs | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/crates/fuzzing/src/oracles/component_api.rs b/crates/fuzzing/src/oracles/component_api.rs index 417bbdab34bc..2767d5a2eaab 100644 --- a/crates/fuzzing/src/oracles/component_api.rs +++ b/crates/fuzzing/src/oracles/component_api.rs @@ -118,13 +118,19 @@ fn arbitrary_val(ty: &component::Type, input: &mut Unstructured) -> arbitrary::R .collect::>()?, ), - // Resources, futures, streams, error contexts, and maps aren't fuzzed at this time. - Type::Own(_) - | Type::Borrow(_) - | Type::Future(_) - | Type::Stream(_) - | Type::ErrorContext - | Type::Map(_) => { + Type::Map(map) => { + let mut pairs = Vec::new(); + input.arbitrary_loop(Some(MIN_LIST_LENGTH), Some(MAX_LIST_LENGTH), |input| { + let key = arbitrary_val(&map.key(), input)?; + let value = arbitrary_val(&map.value(), input)?; + pairs.push((key, value)); + Ok(ControlFlow::Continue(())) + })?; + Val::Map(pairs) + } + + // Resources, futures, streams, and error contexts aren't fuzzed at this time. + Type::Own(_) | Type::Borrow(_) | Type::Future(_) | Type::Stream(_) | Type::ErrorContext => { unreachable!() } }) @@ -140,6 +146,7 @@ fn store(input: &mut Unstructured<'_>, val: T) -> arbitrary::Result> config.module_config.config.max_memories = 2; config.module_config.component_model_async = true; config.module_config.component_model_async_stackful = true; + config.module_config.component_model_map = true; if config.wasmtime.compiler_strategy == CompilerStrategy::Winch { config.wasmtime.compiler_strategy = CompilerStrategy::CraneliftNative; }