From 52427245b13c6d8d0ac96edc59f977bc1acf2b54 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Tue, 23 Dec 2025 02:49:23 -0500 Subject: [PATCH 01/19] 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 11404e2775c97e6d6d27a11fe051d44ffea0b117 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Thu, 8 Jan 2026 21:46:50 -0500 Subject: [PATCH 02/19] 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 48bde2c6cb82a16e985d397abab2f3806db6d89c Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Fri, 16 Jan 2026 14:33:58 -0500 Subject: [PATCH 03/19] 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 31834395ddb717e7be634c7e7f5be4359acfc54d Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Fri, 16 Jan 2026 15:18:35 -0500 Subject: [PATCH 04/19] 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 be7f323ea76eb1a9863782d0dc5c2e2156c4947f Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Fri, 16 Jan 2026 15:28:01 -0500 Subject: [PATCH 05/19] 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 35c9154bc0e9e2d0d15b46ee68b532a567c2e132 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Fri, 6 Feb 2026 02:00:39 -0500 Subject: [PATCH 06/19] 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 d69657fab5ae73af357a8e1d8964079b30c74bd2 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Fri, 6 Feb 2026 02:15:34 -0500 Subject: [PATCH 07/19] 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 67bab6a12604282308db231c9ae8b5caeb49f194 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Fri, 6 Feb 2026 02:30:16 -0500 Subject: [PATCH 08/19] 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 f29a9b92be11f84390d166fe9fc5de22c986b355 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Fri, 6 Feb 2026 03:47:20 -0500 Subject: [PATCH 09/19] 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 b90576f5f421cb151021e19ed40c84091485fac1 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Fri, 6 Feb 2026 03:56:18 -0500 Subject: [PATCH 10/19] 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 000c800a901aa0dc56df0cc6db41d7782aaa7a0c Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Fri, 6 Feb 2026 04:43:09 -0500 Subject: [PATCH 11/19] 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 8d3f90ed499ceb1bd145d520a2a3977410716f37 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Fri, 6 Feb 2026 06:11:55 -0500 Subject: [PATCH 12/19] 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 9eb8966e8d89443ec03f60335ce7da8c92de96d8 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Fri, 6 Feb 2026 06:44:33 -0500 Subject: [PATCH 13/19] 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 b80288cac3ea1a9ee6827e4328507b107e14d1dd Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Thu, 26 Feb 2026 21:58:20 -0500 Subject: [PATCH 14/19] 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 9f6c10a5745fa4702564c7e2c7abcf59bb49f1c2 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Thu, 26 Feb 2026 23:37:07 -0500 Subject: [PATCH 15/19] 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 64f38cf9796148f4c394b1bd90fc33ef2b756228 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Thu, 26 Feb 2026 23:39:41 -0500 Subject: [PATCH 16/19] 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 deacce6c2233605ba6715256c33f62cefb5b0476 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Fri, 27 Feb 2026 00:04:45 -0500 Subject: [PATCH 17/19] 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 bbd3e25662b35fa4e0d13a50e1f6d5ace84ad8d8 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Fri, 27 Feb 2026 00:28:30 -0500 Subject: [PATCH 18/19] 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 78b933c0bb95927c5b4bd9c06181a9380b5af77a Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Fri, 27 Feb 2026 00:31:07 -0500 Subject: [PATCH 19/19] 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 +}