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/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/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; diff --git a/crates/c-api/include/wasmtime/config.h b/crates/c-api/include/wasmtime/config.h index af5f3e0f06a2..c7c94ecaa6e3 100644 --- a/crates/c-api/include/wasmtime/config.h +++ b/crates/c-api/include/wasmtime/config.h @@ -816,6 +816,15 @@ WASM_API_EXTERN void wasmtime_pooling_allocation_strategy_set( */ WASMTIME_CONFIG_PROP(void, wasm_component_model, 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 59d549899de8..b340fa38a20f 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..1db3dbaa55d5 100644 --- a/crates/c-api/src/component/component.rs +++ b/crates/c-api/src/component/component.rs @@ -10,6 +10,14 @@ 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/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..60177f2d2dc6 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 { @@ -234,6 +273,7 @@ pub enum wasmtime_component_val_t { Result(wasmtime_component_valresult_t), Flags(wasmtime_component_valflags_t), Resource(Box), + Map(wasmtime_component_valmap_t), } impl Default for wasmtime_component_val_t { @@ -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..5953366f48e0 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; @@ -187,6 +188,22 @@ TEST(types, valtype_list) { EXPECT_TRUE(elem.is_u8()); } +TEST(types, valtype_map) { + 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()); + 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 49437ace7203..0f85554cb235 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. @@ -1125,6 +1127,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 f680d2849462..a4d5442c7654 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 e729b1a03265..f96a5c3fb9ef 100644 --- a/crates/environ/src/fact/trampoline.rs +++ b/crates/environ/src/fact/trampoline.rs @@ -20,8 +20,8 @@ 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, + TypeFlagsIndex, TypeFutureTableIndex, TypeListIndex, TypeMapIndex, TypeOptionIndex, + TypeRecordIndex, TypeResourceTableIndex, TypeResultIndex, TypeStreamTableIndex, TypeTupleIndex, TypeVariantIndex, VariantInfo, }; use crate::fact::signature::Signature; @@ -1135,6 +1135,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(); @@ -1185,6 +1187,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), @@ -2665,6 +2668,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 1663e963c58a..09aef0048e3d 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 131f92d6e704..e8471af81793 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..77ca3d64f5bc 100644 --- a/crates/test-util/src/component.rs +++ b/crates/test-util/src/component.rs @@ -27,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/crates/test-util/src/component_fuzz.rs b/crates/test-util/src/component_fuzz.rs index 162b7075fbfe..e1a121022d81 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,7 +159,7 @@ impl Type { fuel: &mut u32, ) -> arbitrary::Result { *fuel = fuel.saturating_sub(1); - 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, @@ -195,11 +196,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 +281,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 +408,7 @@ impl Type { | Type::Float64 | Type::String | Type::List(_) + | Type::Map(_, _) | Type::Flags(_) | Type::Enum(_) => unreachable!(), @@ -414,7 +449,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 +586,7 @@ impl Type { | Type::Float64 | Type::String | Type::List(_) + | Type::Map(_, _) | Type::Flags(_) | Type::Enum(_) => unreachable!(), @@ -667,7 +703,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 +742,7 @@ impl Type { alignment: 8, }, - Type::String | Type::List(_) => SizeAndAlignment { + Type::String | Type::List(_) | Type::Map(_, _) => SizeAndAlignment { size: 8, alignment: 4, }, @@ -1161,6 +1197,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() @@ -1241,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 { @@ -1330,6 +1371,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 +1409,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 89902a7af408..3becc09ed501 100644 --- a/crates/wasmtime/src/config.rs +++ b/crates/wasmtime/src/config.rs @@ -1231,6 +1231,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_ @@ -2210,6 +2220,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 a8dd6607f7c5..6d3d56fff6a5 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 wasmtime_environ::collections::HashMap; use wasmtime_environ::component::{ CanonicalAbiInfo, InterfaceType, MAX_FLAT_PARAMS, MAX_FLAT_RESULTS, OptionsIndex, StringEncoding, VariantInfo, @@ -624,6 +626,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] | @@ -2089,6 +2092,377 @@ 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_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(()) + } +} + +fn lower_map_iter<'a, K, V, U>( + cx: &mut LowerContext<'_, U>, + key_ty: InterfaceType, + value_ty: InterfaceType, + len: usize, + iter: impl Iterator, +) -> Result<(usize, usize)> +where + K: Lower + 'a, + V: Lower + 'a, +{ + // Calculate the tuple layout: each entry is a (key, value) record. + let tuple_abi = CanonicalAbiInfo::record_static(&[K::ABI, V::ABI]); + let tuple_size = tuple_abi.size32 as usize; + let tuple_align = tuple_abi.align32; + + let size = len + .checked_mul(tuple_size) + .ok_or_else(|| format_err!("size overflow copying a map"))?; + let ptr = cx.realloc(0, 0, tuple_align, size)?; + + let mut entry_offset = ptr; + for (key, value) in iter { + // Lower key at the start of the tuple + let mut field_offset = 0usize; + let key_field = K::ABI.next_field32_size(&mut field_offset); + ::linear_lower_to_memory(key, cx, key_ty, entry_offset + key_field)?; + // Lower value at its aligned offset within the tuple + let value_field = V::ABI.next_field32_size(&mut field_offset); + ::linear_lower_to_memory(value, cx, value_ty, entry_offset + value_field)?; + entry_offset += tuple_size; + } + + Ok((ptr, 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) +} + +// ============================================================================= +// 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, @@ -2916,6 +3290,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 8e4875aadc09..167895598065 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, 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 695cb140a864..e0e34d2a1bd5 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 f55d9a860d25..9d9dca679f5d 100644 --- a/crates/wit-bindgen/src/lib.rs +++ b/crates/wit-bindgen/src/lib.rs @@ -1627,9 +1627,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!(), } } @@ -2198,6 +2198,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}")); @@ -3436,8 +3452,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.rs b/tests/all/component_model.rs index d2efd3e70412..d40441f6ed61 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 0a67a320f82b..bb63f8020623 100644 --- a/tests/all/component_model/dynamic.rs +++ b/tests/all/component_model/dynamic.rs @@ -136,6 +136,343 @@ fn lists() -> Result<()> { Ok(()) } +#[test] +fn maps() -> Result<()> { + let engine = super::map_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(&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(&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(&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(&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::map_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(&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::map_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(&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::map_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(&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(&mut store, &[input.clone()], &mut output)?; + assert_eq!(input, output[0]); + + Ok(()) +} + +#[test] +fn maps_alignment() -> Result<()> { + let engine = super::map_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(&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(&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(&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(&mut store, &[input.clone()], &mut output)?; + assert_eq!(input, output[0]); + + Ok(()) +} + +#[test] +fn maps_large() -> Result<()> { + let engine = super::map_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(&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 65954213683e..837bf10bb86b 100644 --- a/tests/all/component_model/func.rs +++ b/tests/all/component_model/func.rs @@ -3730,3 +3730,288 @@ fn with_new_instance( let instance = Linker::new(engine).instantiate(&mut store, component)?; fun(&mut store, instance) } + +/// 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); + + // Use dynamic API since typed API doesn't support map types yet + linker.root().func_new("host", |_cx, _ty, args, results| { + // Echo the map back + results[0] = args[0].clone(); + Ok(()) + })?; + + let instance = linker.instantiate(&mut store, &component)?; + let func = instance.get_func(&mut store, "echo").unwrap(); + + // Test with distinctive values that would be corrupted by misaligned reads + 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(&mut store, &[input], &mut results)?; + + // Verify the data round-tripped correctly + 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(&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/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))) +)