From ab790eedf5f705ae963ed9e564e32a1e4a346b92 Mon Sep 17 00:00:00 2001 From: Janito Vaqueiro Ferreira Filho Date: Tue, 8 Aug 2023 23:57:38 +0000 Subject: [PATCH 01/22] Add missing lift and lowering round-trip tests Missing from some of the unit tests. --- linera-witty/src/type_traits/implementations/tests.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/linera-witty/src/type_traits/implementations/tests.rs b/linera-witty/src/type_traits/implementations/tests.rs index abbd144af7b..7f14a54d2ef 100644 --- a/linera-witty/src/type_traits/implementations/tests.rs +++ b/linera-witty/src/type_traits/implementations/tests.rs @@ -102,6 +102,7 @@ fn ok_two_bytes_but_large_err() { 0x00, 0, 0, 0, 0, 0, 0, 0, 0x34, 0x12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ], ); + test_flattening_roundtrip(input, hlist![0_i32, 0x0000_1234_i64, 0_i64]); } /// Test roundtrip of `Err::`. @@ -116,6 +117,10 @@ fn large_err() { 0x05, 0x04, 0x03, 0x02, 0x01, 0x00, ], ); + test_flattening_roundtrip( + input, + hlist![1_i32, 0x0809_0a0b_0c0d_0e0f_i64, 0x0001_0203_0405_0607_i64], + ); } /// Test storing an instance of `T` to memory, checking that the `memory_data` bytes are correctly From cc105c733775cd7783e43681cc16e1ab416676b5 Mon Sep 17 00:00:00 2001 From: Janito Vaqueiro Ferreira Filho Date: Sat, 5 Aug 2023 00:14:36 +0000 Subject: [PATCH 02/22] Refactor `repeat_macro!` to support lone elements Don't require a name and type pair, allowing macros to be applied on lists of either just names or just types as well. --- linera-witty/src/macro_utils.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/linera-witty/src/macro_utils.rs b/linera-witty/src/macro_utils.rs index dd3f182fdad..10c43a509fe 100644 --- a/linera-witty/src/macro_utils.rs +++ b/linera-witty/src/macro_utils.rs @@ -3,32 +3,32 @@ //! Common macros used by other macros. -/// Repeats a `macro` call with a growing list of `name: Type` elements. +/// Repeats a `macro` call with a growing list of `name: Type` or just `name` elements. /// /// Calls the `macro` repeatedly, starting with list of the elements until the first `|`, then /// adding the next element for each successive call. macro_rules! repeat_macro { ( $macro:tt => - $( $current_names:ident : $current_types:ident ),* - | $next_name:ident : $next_type:ident - $(, $queued_names:ident : $queued_types:ident )* $(,)? + $( $current_names:ident $( : $current_types:ident )? ),* + | $next_name:ident $( : $next_type:ident )? + $(, $queued_names:ident $( : $queued_types:ident )? )* $(,)? ) => { - $macro!($( $current_names: $current_types ),*); + $macro!($( $current_names $(: $current_types)? ),*); repeat_macro!( $macro => - $( $current_names: $current_types, )* - $next_name: $next_type - | $($queued_names: $queued_types ),* + $( $current_names $(: $current_types)?, )* + $next_name $(: $next_type)? + | $($queued_names $(: $queued_types)? ),* ); }; - ( $macro:tt => $( $current_names:ident : $current_types:ident ),* |) => { - $macro!($( $current_names: $current_types ),*); + ( $macro:tt => $( $current_names:ident $( : $current_types:ident )? ),* |) => { + $macro!($( $current_names $(: $current_types)? ),*); }; - ( $macro:tt => $( $current_names:ident : $current_types:ident ),* $(,)*) => { - repeat_macro!($macro => | $( $current_names: $current_types ),*); + ( $macro:tt => $( $current_names:ident $( : $current_types:ident )? ),* $(,)*) => { + repeat_macro!($macro => | $( $current_names $(: $current_types)? ),*); } } From 766014c7690ac217a68e0c63fab5d54dc822359d Mon Sep 17 00:00:00 2001 From: Janito Vaqueiro Ferreira Filho Date: Sat, 5 Aug 2023 02:02:11 +0000 Subject: [PATCH 03/22] Create a `FlatHostParameters` helper trait Make it easier to convert a host parameters type into the flat type parameters for an imported guest function. --- .../src/imported_function_interface/mod.rs | 13 ++ .../imported_function_interface/parameters.rs | 116 ++++++++++++++++++ linera-witty/src/lib.rs | 1 + 3 files changed, 130 insertions(+) create mode 100644 linera-witty/src/imported_function_interface/mod.rs create mode 100644 linera-witty/src/imported_function_interface/parameters.rs diff --git a/linera-witty/src/imported_function_interface/mod.rs b/linera-witty/src/imported_function_interface/mod.rs new file mode 100644 index 00000000000..deddcc435d9 --- /dev/null +++ b/linera-witty/src/imported_function_interface/mod.rs @@ -0,0 +1,13 @@ +// Copyright (c) Zefchain Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +//! Generic representation of a function the host imports from a guest Wasm instance. +//! +//! This helps determining the actual signature of the imported guest Wasm function based on host +//! types for the parameters and the results. +//! +//! The signature depends on the number of flat types used to represent the parameters and the +//! results, as specified in the [canonical +//! ABI](https://github.com/WebAssembly/component-model/blob/main/design/mvp/CanonicalABI.md#flattening). + +mod parameters; diff --git a/linera-witty/src/imported_function_interface/parameters.rs b/linera-witty/src/imported_function_interface/parameters.rs new file mode 100644 index 00000000000..321ae3704b3 --- /dev/null +++ b/linera-witty/src/imported_function_interface/parameters.rs @@ -0,0 +1,116 @@ +// Copyright (c) Zefchain Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +//! Representation of the parameters of an imported function. +//! +//! The host parameters type is flattened and if it's made up of 16 or less flat types, they are +//! sent directly as the guest's function parameters. If there are more than 16 flat types, then +//! the host type is instead stored in a heap allocated region of memory, and the address to that +//! region is sent as a parameter instead. +//! +//! See the [canonical ABI's section on +//! flattening](https://github.com/WebAssembly/component-model/blob/main/design/mvp/CanonicalABI.md#flattening) +//! for more details. + +use crate::{ + memory_layout::FlatLayout, primitive_types::FlatType, InstanceWithMemory, Layout, Memory, + Runtime, RuntimeError, RuntimeMemory, WitStore, WitType, +}; +use frunk::HList; + +/// Helper trait for converting from the host parameters to the guest parameters. +/// +/// This trait is implemented for the intermediate flat layout of the host parameters type. This +/// allows converting from the host parameters type into this flat layout and finally into the guest +/// parameters type. +pub trait FlatHostParameters: FlatLayout { + /// The actual parameters sent to the guest function. + type GuestParameters: FlatLayout; + + /// Converts the `HostParameters` into the guest parameters that can be sent to the guest + /// function. + fn lower_parameters( + parameters: HostParameters, + memory: &mut Memory<'_, Instance>, + ) -> Result + where + HostParameters: WitStore, + ::Layout: Layout, + Instance: InstanceWithMemory, + ::Memory: RuntimeMemory; +} + +/// Macro to implement [`FlatHostParameters`] for a parameters that are sent directly to the guest. +/// +/// Each element in the flat layout type is sent as a separate parameter to the function. +macro_rules! direct_parameters { + ($( $types:ident ),*) => { + impl<$( $types ),*> FlatHostParameters for HList![$( $types ),*] + where + $( $types: FlatType, )* + { + type GuestParameters = HList![$( $types ),*]; + + fn lower_parameters( + parameters: Parameters, + memory: &mut Memory<'_, Instance>, + ) -> Result + where + Parameters: WitStore, + ::Layout: Layout, + Instance: InstanceWithMemory, + ::Memory: RuntimeMemory, + { + parameters.lower(memory) + } + } + }; +} + +repeat_macro!(direct_parameters => A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P); + +/// Implementation of [`FlatHostParameters`] for parameters that are sent to the guest through the +/// heap. +/// +/// When more than 16 function parameters are needed, they are spilled over into a heap memory +/// allocation and the only parameter sent as a function parameter is the address to that +/// allocation. +impl FlatHostParameters for HList![A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, ...Tail] +where + A: FlatType, + B: FlatType, + C: FlatType, + D: FlatType, + E: FlatType, + F: FlatType, + G: FlatType, + H: FlatType, + I: FlatType, + J: FlatType, + K: FlatType, + L: FlatType, + M: FlatType, + N: FlatType, + O: FlatType, + P: FlatType, + Q: FlatType, + Tail: FlatLayout, +{ + type GuestParameters = HList![i32]; + + fn lower_parameters( + parameters: Parameters, + memory: &mut Memory<'_, Instance>, + ) -> Result + where + Parameters: WitStore, + ::Layout: Layout, + Instance: InstanceWithMemory, + ::Memory: RuntimeMemory, + { + let location = memory.allocate(Parameters::SIZE)?; + + parameters.store(memory, location)?; + location.lower(memory) + } +} diff --git a/linera-witty/src/lib.rs b/linera-witty/src/lib.rs index c3c0ceeeaa7..475594cb7a9 100644 --- a/linera-witty/src/lib.rs +++ b/linera-witty/src/lib.rs @@ -14,6 +14,7 @@ #[macro_use] mod macro_utils; +mod imported_function_interface; mod memory_layout; mod primitive_types; mod runtime; From 390dd635e35472784c12b23275ab8dfdafcc7e23 Mon Sep 17 00:00:00 2001 From: Janito Vaqueiro Ferreira Filho Date: Sat, 5 Aug 2023 02:48:05 +0000 Subject: [PATCH 04/22] Create a `FlatHostResults` helper trait Allow converting from the results received from an imported guest Wasm function into the host results type. --- .../src/imported_function_interface/mod.rs | 1 + .../imported_function_interface/results.rs | 110 ++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 linera-witty/src/imported_function_interface/results.rs diff --git a/linera-witty/src/imported_function_interface/mod.rs b/linera-witty/src/imported_function_interface/mod.rs index deddcc435d9..461354703c3 100644 --- a/linera-witty/src/imported_function_interface/mod.rs +++ b/linera-witty/src/imported_function_interface/mod.rs @@ -11,3 +11,4 @@ //! ABI](https://github.com/WebAssembly/component-model/blob/main/design/mvp/CanonicalABI.md#flattening). mod parameters; +mod results; diff --git a/linera-witty/src/imported_function_interface/results.rs b/linera-witty/src/imported_function_interface/results.rs new file mode 100644 index 00000000000..787908bae94 --- /dev/null +++ b/linera-witty/src/imported_function_interface/results.rs @@ -0,0 +1,110 @@ +// Copyright (c) Zefchain Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +//! Representation of the results of an imported function. +//! +//! The results returned from an imported guest function may be empty, a single flat value or a +//! more complex type that's stored in the heap. That depends on the flat layout of the type +//! representing the host results, or more specifically, how many elements that flat layout has. If +//! the flat layout is empty, nothing should be returned from the guest. If there's only one +//! element, it should be returned as a value from the function. If there is more than one element, +//! then the results type is stored in a heap allocated memory region instead, and the address to +//! that region is returned as a value from the function. +//! +//! See the [canonical ABI's section on +//! flattening](https://github.com/WebAssembly/component-model/blob/main/design/mvp/CanonicalABI.md#flattening) +//! for more details. + +use crate::{ + memory_layout::FlatLayout, primitive_types::FlatType, GuestPointer, InstanceWithMemory, Layout, + Memory, Runtime, RuntimeError, RuntimeMemory, WitLoad, WitType, +}; +use frunk::HList; + +/// Helper trait for converting from the guest results to the host results. +/// +/// This trait is implemented for the intermediate flat layout of the host results type. This +/// allows converting from the guest results type into this flat layout and finally into the host +/// results type. +pub trait FlatHostResults: FlatLayout { + /// The results received from the guest function. + type GuestResults: FlatLayout; + + /// Converts the received guest `results` into the `HostResults` type. + fn lift_results( + results: Self::GuestResults, + memory: &Memory<'_, Instance>, + ) -> Result + where + HostResults: WitLoad, + ::Layout: Layout, + Instance: InstanceWithMemory, + ::Memory: RuntimeMemory; +} + +/// Implementation for host results that are zero-sized types. +impl FlatHostResults for HList![] { + type GuestResults = HList![]; + + fn lift_results( + results: Self::GuestResults, + memory: &Memory<'_, Instance>, + ) -> Result + where + Results: WitLoad, + ::Layout: Layout, + Instance: InstanceWithMemory, + ::Memory: RuntimeMemory, + { + Results::lift_from(results, memory) + } +} + +/// Implementation for host results that are flattened into a single value. +impl FlatHostResults for HList![FlatResult] +where + FlatResult: FlatType, +{ + type GuestResults = HList![FlatResult]; + + fn lift_results( + results: Self::GuestResults, + memory: &Memory<'_, Instance>, + ) -> Result + where + Results: WitLoad, + ::Layout: Layout, + Instance: InstanceWithMemory, + ::Memory: RuntimeMemory, + { + Results::lift_from(results, memory) + } +} + +/// Implementation for host results that are flattened into multiple values. +/// +/// The value received from the guest is an address to a heap allocated region of memory containing +/// the results. The host results type is loaded from that region. +impl FlatHostResults for HList![FirstResult, SecondResult, ...Tail] +where + FirstResult: FlatType, + SecondResult: FlatType, + Tail: FlatLayout, +{ + type GuestResults = HList![i32]; + + fn lift_results( + results: Self::GuestResults, + memory: &Memory<'_, Instance>, + ) -> Result + where + Results: WitLoad, + ::Layout: Layout, + Instance: InstanceWithMemory, + ::Memory: RuntimeMemory, + { + let location = GuestPointer::lift_from(results, memory)?; + + Results::load(memory, location) + } +} From 8bc1217f55d6df246f3b40e60e3117bd6d666d19 Mon Sep 17 00:00:00 2001 From: Janito Vaqueiro Ferreira Filho Date: Sat, 5 Aug 2023 02:49:45 +0000 Subject: [PATCH 05/22] Create an `ImportedFunctionInterface` helper trait Use the `FlatHostParameters` and `FlatHostResults` traits in a single trait that can be used to determine the interface to an imported function from a guest based on the host inteface types. --- .../src/imported_function_interface/mod.rs | 80 +++++++++++++++++++ linera-witty/src/lib.rs | 1 + 2 files changed, 81 insertions(+) diff --git a/linera-witty/src/imported_function_interface/mod.rs b/linera-witty/src/imported_function_interface/mod.rs index 461354703c3..cb4cb756878 100644 --- a/linera-witty/src/imported_function_interface/mod.rs +++ b/linera-witty/src/imported_function_interface/mod.rs @@ -12,3 +12,83 @@ mod parameters; mod results; + +use self::{parameters::FlatHostParameters, results::FlatHostResults}; +use crate::{ + memory_layout::FlatLayout, InstanceWithMemory, Layout, Memory, Runtime, RuntimeError, + RuntimeMemory, WitLoad, WitStore, +}; + +/// Representation of an imported function's interface. +/// +/// Implemented for a tuple pair of the host parameters type and the host results type, and then +/// allows converting to the guest function's signature. +pub trait ImportedFunctionInterface { + /// The type representing the host-side parameters. + type HostParameters: WitStore; + + /// The type representing the host-side results. + type HostResults: WitLoad; + + /// The flat layout representing the guest-side parameters. + type GuestParameters: FlatLayout; + + /// The flat layout representing the guest-side results. + type GuestResults: FlatLayout; + + /// Converts the host-side parameters into the guest-side parameters. + fn lower_parameters( + parameters: Self::HostParameters, + memory: &mut Memory<'_, Instance>, + ) -> Result + where + Instance: InstanceWithMemory, + ::Memory: RuntimeMemory; + + /// Converts the guest-side results into the host-side results. + fn lift_results( + results: Self::GuestResults, + memory: &Memory<'_, Instance>, + ) -> Result + where + Instance: InstanceWithMemory, + ::Memory: RuntimeMemory; +} + +impl ImportedFunctionInterface for (Parameters, Results) +where + Parameters: WitStore, + Results: WitLoad, + ::Flat: FlatHostParameters, + ::Flat: FlatHostResults, +{ + type HostParameters = Parameters; + type HostResults = Results; + type GuestParameters = + <::Flat as FlatHostParameters>::GuestParameters; + type GuestResults = <::Flat as FlatHostResults>::GuestResults; + + fn lower_parameters( + parameters: Self::HostParameters, + memory: &mut Memory<'_, Instance>, + ) -> Result + where + Instance: InstanceWithMemory, + ::Memory: RuntimeMemory, + { + <::Flat as FlatHostParameters>::lower_parameters( + parameters, memory, + ) + } + + fn lift_results( + results: Self::GuestResults, + memory: &Memory<'_, Instance>, + ) -> Result + where + Instance: InstanceWithMemory, + ::Memory: RuntimeMemory, + { + <::Flat as FlatHostResults>::lift_results(results, memory) + } +} diff --git a/linera-witty/src/lib.rs b/linera-witty/src/lib.rs index 475594cb7a9..4486c090efe 100644 --- a/linera-witty/src/lib.rs +++ b/linera-witty/src/lib.rs @@ -24,6 +24,7 @@ mod util; #[cfg(any(test, feature = "test"))] pub use self::runtime::{FakeInstance, FakeRuntime}; pub use self::{ + imported_function_interface::ImportedFunctionInterface, memory_layout::{JoinFlatLayouts, Layout}, runtime::{GuestPointer, InstanceWithMemory, Memory, Runtime, RuntimeError, RuntimeMemory}, type_traits::{WitLoad, WitStore, WitType}, From 24cae02ac6abf1569f6849dfd626ad99889071c7 Mon Sep 17 00:00:00 2001 From: Janito Vaqueiro Ferreira Filho Date: Fri, 4 Aug 2023 15:28:17 +0000 Subject: [PATCH 06/22] Extract namespace from attribute parameters Prepare to add support for a new `wit_import` attribute procedural macro. The attribute requires receiving an extra parameter to determine the package of the imported functions, and the namespace can also be specified as a parameter. Implement extraction of the namespace by parsing the attribute parameters. It will expect the package to be specified, and will use the namespace if provided as a parameter or fallback to the trait name if it's not. --- Cargo.lock | 1 + linera-witty-macros/Cargo.toml | 1 + linera-witty-macros/src/util.rs | 73 +++++++++++++++++++++++++++++++-- 3 files changed, 72 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 221ea81b5a8..e0c761a75f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3172,6 +3172,7 @@ dependencies = [ name = "linera-witty-macros" version = "0.3.0" dependencies = [ + "heck 0.4.1", "proc-macro-error", "proc-macro2", "quote", diff --git a/linera-witty-macros/Cargo.toml b/linera-witty-macros/Cargo.toml index b38c66a3e44..55c2959ad68 100644 --- a/linera-witty-macros/Cargo.toml +++ b/linera-witty-macros/Cargo.toml @@ -14,6 +14,7 @@ edition = "2021" proc-macro = true [dependencies] +heck = { workspace = true } proc-macro-error = { workspace = true } proc-macro2 = { workspace = true } quote = { workspace = true } diff --git a/linera-witty-macros/src/util.rs b/linera-witty-macros/src/util.rs index 58bef55d4c6..88e26e4ecdb 100644 --- a/linera-witty-macros/src/util.rs +++ b/linera-witty-macros/src/util.rs @@ -1,11 +1,17 @@ // Copyright (c) Zefchain Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -//! Helper functions shared between different macro implementations. +//! Helper types and functions shared between different macro implementations. -use proc_macro2::TokenStream; +use heck::ToKebabCase; +use proc_macro2::{Span, TokenStream}; +use proc_macro_error::abort; use quote::quote; -use syn::{Fields, Ident}; +use syn::{ + parse::{self, Parse, ParseStream}, + punctuated::Punctuated, + Fields, Ident, Lit, LitStr, MetaNameValue, Token, +}; /// Returns the code with a pattern to match a heterogenous list using the `field_names` as /// bindings. @@ -22,3 +28,64 @@ pub fn hlist_type_for(fields: &Fields) -> TokenStream { let field_types = fields.iter().map(|field| &field.ty); quote! { linera_witty::HList![#( #field_types ),*] } } + +/// Returns the package and namespace for the WIT interface generated by an attribute macro. +/// +/// Requires a `package` to be specified in `attribute_parameters` and can use a specified +/// `namespace` or infer it from the `type_name`. +pub fn extract_namespace( + attribute_parameters: proc_macro::TokenStream, + type_name: &Ident, +) -> LitStr { + let span = Span::call_site(); + let parameters = syn::parse::(attribute_parameters).unwrap_or_else(|_| { + abort!( + span, + r#"Failed to parse attribute parameters, expected either `root = true` \ + or `package = "namespace:package""# + ) + }); + + let package_name = parameters.parameter("package").unwrap_or_else(|| { + abort!( + span, + r#"Missing package name specifier in attribute parameters \ + (package = "namespace:package")"# + ) + }); + + let interface_name = parameters + .parameter("interface") + .unwrap_or_else(|| type_name.to_string().to_kebab_case()); + + LitStr::new(&format!("{package_name}/{interface_name}"), span) +} + +/// A type representing the parameters for an attribute procedural macro. +struct AttributeParameters { + metadata: Punctuated, +} + +impl Parse for AttributeParameters { + fn parse(input: ParseStream) -> parse::Result { + Ok(AttributeParameters { + metadata: Punctuated::parse_terminated(input)?, + }) + } +} + +impl AttributeParameters { + /// Returns the string value of a parameter named `name`, if it exists. + pub fn parameter(&self, name: &str) -> Option { + self.metadata + .iter() + .find(|pair| pair.path.is_ident(name)) + .map(|pair| { + let Lit::Str(lit_str) = &pair.lit else { + abort!(&pair.lit, "Expected a string literal"); + }; + + lit_str.value() + }) + } +} From cfde95c88fe5e1d8ccc75992fc004d3da2bf78ca Mon Sep 17 00:00:00 2001 From: Janito Vaqueiro Ferreira Filho Date: Sat, 5 Aug 2023 13:09:42 +0000 Subject: [PATCH 07/22] Collect information for functions to import Parse the functions from a trait and extract the information to later be used to generate code to import functions. --- linera-witty-macros/src/lib.rs | 1 + linera-witty-macros/src/wit_import.rs | 116 ++++++++++++++++++++++++++ 2 files changed, 117 insertions(+) create mode 100644 linera-witty-macros/src/wit_import.rs diff --git a/linera-witty-macros/src/lib.rs b/linera-witty-macros/src/lib.rs index 9bc898265ef..57b6d0827cf 100644 --- a/linera-witty-macros/src/lib.rs +++ b/linera-witty-macros/src/lib.rs @@ -8,6 +8,7 @@ #![deny(missing_docs)] mod util; +mod wit_import; mod wit_load; mod wit_store; mod wit_type; diff --git a/linera-witty-macros/src/wit_import.rs b/linera-witty-macros/src/wit_import.rs new file mode 100644 index 00000000000..0b9db730aa4 --- /dev/null +++ b/linera-witty-macros/src/wit_import.rs @@ -0,0 +1,116 @@ +// Copyright (c) Zefchain Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +//! Generation of code to import functions from a Wasm guest module. + +use proc_macro2::{Span, TokenStream}; +use proc_macro_error::abort; +use quote::{quote, quote_spanned, ToTokens}; +use syn::{spanned::Spanned, FnArg, Ident, ReturnType, TraitItem, TraitItemMethod}; + +/// Pieces of information extracted from a function's definition. +struct FunctionInformation<'input> { + function: &'input TraitItemMethod, + parameter_definitions: TokenStream, + parameter_bindings: TokenStream, + return_type: TokenStream, + interface: TokenStream, + instance_constraint: TokenStream, +} + +impl<'input> FunctionInformation<'input> { + /// Extracts the necessary information from the `function` and stores it in a new + /// [`FunctionInformation`] instance. + pub fn new(function: &'input TraitItemMethod) -> Self { + let (parameter_definitions, parameter_bindings, parameter_types) = + Self::parse_parameters(function.sig.inputs.iter()); + + let return_type = match &function.sig.output { + ReturnType::Default => quote_spanned! { function.sig.output.span() => () }, + ReturnType::Type(_, return_type) => return_type.to_token_stream(), + }; + + let interface = quote_spanned! { function.sig.span() => + <(linera_witty::HList![#parameter_types], #return_type) + as linera_witty::ImportedFunctionInterface> + }; + + let instance_constraint = quote_spanned! { function.sig.span() => + linera_witty::InstanceWithFunction< + #interface::GuestParameters, + #interface::GuestResults, + > + }; + + FunctionInformation { + function, + parameter_definitions, + parameter_bindings, + return_type, + interface, + instance_constraint, + } + } + + /// Parses a function's parameters and returns the pieces constructed from the parameters. + /// + /// Returns the parameter definitions (the name and type pairs), the parameter bindings (the + /// names) and the parameter types. + fn parse_parameters( + function_inputs: impl Iterator, + ) -> (TokenStream, TokenStream, TokenStream) { + let parameters = function_inputs.map(|input| match input { + FnArg::Typed(parameter) => parameter, + FnArg::Receiver(receiver) => abort!( + receiver.self_token, + "Imported interfaces can not have `self` parameters" + ), + }); + + let mut parameter_definitions = quote! {}; + let mut parameter_bindings = quote! {}; + let mut parameter_types = quote! {}; + + for parameter in parameters { + let parameter_binding = ¶meter.pat; + let parameter_type = ¶meter.ty; + + parameter_definitions.extend(quote! { #parameter, }); + parameter_bindings.extend(quote! { #parameter_binding, }); + parameter_types.extend(quote! { #parameter_type, }); + } + + (parameter_definitions, parameter_bindings, parameter_types) + } + + /// Returns the name of the function. + pub fn name(&self) -> &Ident { + &self.function.sig.ident + } + + /// Returns the code span of the function. + pub fn span(&self) -> Span { + self.function.span() + } +} + +impl<'input> From<&'input TraitItem> for FunctionInformation<'input> { + fn from(item: &'input TraitItem) -> Self { + match item { + TraitItem::Method(function) => FunctionInformation::new(function), + TraitItem::Const(const_item) => abort!( + const_item.ident, + "Const items are not supported in imported traits" + ), + TraitItem::Type(type_item) => abort!( + type_item.ident, + "Type items are not supported in imported traits" + ), + TraitItem::Macro(macro_item) => abort!( + macro_item.mac.path, + "Macro items are not supported in imported traits" + ), + _ => abort!(item, "Only function items are supported in imported traits"), + } + } +} From c8798ec22fd3c19b4acc20f2df2c64a92e4887f3 Mon Sep 17 00:00:00 2001 From: Janito Vaqueiro Ferreira Filho Date: Sat, 5 Aug 2023 14:13:56 +0000 Subject: [PATCH 08/22] Create a `TokensSetItem` helper type A type to allow `TokenStream`s to be used as an element in a `HashSet`. --- linera-witty-macros/src/util.rs | 42 ++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/linera-witty-macros/src/util.rs b/linera-witty-macros/src/util.rs index 88e26e4ecdb..b74963454a0 100644 --- a/linera-witty-macros/src/util.rs +++ b/linera-witty-macros/src/util.rs @@ -6,7 +6,8 @@ use heck::ToKebabCase; use proc_macro2::{Span, TokenStream}; use proc_macro_error::abort; -use quote::quote; +use quote::{quote, ToTokens}; +use std::hash::{Hash, Hasher}; use syn::{ parse::{self, Parse, ParseStream}, punctuated::Punctuated, @@ -89,3 +90,42 @@ impl AttributeParameters { }) } } + +/// A helper type to allow comparing [`TokenStream`] instances, allowing it to be used in a +/// [`HashSet`]. +pub struct TokensSetItem<'input> { + string: String, + tokens: &'input TokenStream, +} + +impl<'input> From<&'input TokenStream> for TokensSetItem<'input> { + fn from(tokens: &'input TokenStream) -> Self { + TokensSetItem { + string: tokens.to_string(), + tokens, + } + } +} + +impl PartialEq for TokensSetItem<'_> { + fn eq(&self, other: &Self) -> bool { + self.string.eq(&other.string) + } +} + +impl Eq for TokensSetItem<'_> {} + +impl Hash for TokensSetItem<'_> { + fn hash(&self, state: &mut H) + where + H: Hasher, + { + self.string.hash(state) + } +} + +impl ToTokens for TokensSetItem<'_> { + fn to_tokens(&self, stream: &mut TokenStream) { + self.tokens.to_tokens(stream) + } +} From 0cfa8813404e133900de2633a14403cc769c9110 Mon Sep 17 00:00:00 2001 From: Janito Vaqueiro Ferreira Filho Date: Sat, 5 Aug 2023 14:50:10 +0000 Subject: [PATCH 09/22] Generate code to import functions from guests Given a trait interface, generate the code for a type with equivalent methods that load a function from the guest and calls it. --- linera-witty-macros/src/lib.rs | 16 ++- linera-witty-macros/src/wit_import.rs | 176 +++++++++++++++++++++++++- 2 files changed, 190 insertions(+), 2 deletions(-) diff --git a/linera-witty-macros/src/lib.rs b/linera-witty-macros/src/lib.rs index 57b6d0827cf..60d4c6d5612 100644 --- a/linera-witty-macros/src/lib.rs +++ b/linera-witty-macros/src/lib.rs @@ -13,11 +13,12 @@ mod wit_load; mod wit_store; mod wit_type; +use self::util::extract_namespace; use proc_macro::TokenStream; use proc_macro2::Span; use proc_macro_error::{abort, proc_macro_error}; use quote::{quote, ToTokens}; -use syn::{parse_macro_input, Data, DeriveInput, Ident}; +use syn::{parse_macro_input, Data, DeriveInput, Ident, ItemTrait}; /// Derives `WitType` for a Rust type. /// @@ -92,3 +93,16 @@ fn derive_trait(input: DeriveInput, body: impl ToTokens, trait_name: Ident) -> T } .into() } + +/// Generates a generic type from a trait. +/// +/// The generic type has a type parameter for the Wasm guest instance to use, and allows calling +/// functions that the instance exports through the trait's methods. +#[proc_macro_error] +#[proc_macro_attribute] +pub fn wit_import(attribute: TokenStream, input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as ItemTrait); + let namespace = extract_namespace(attribute, &input.ident); + + wit_import::generate(input, &namespace).into() +} diff --git a/linera-witty-macros/src/wit_import.rs b/linera-witty-macros/src/wit_import.rs index 0b9db730aa4..2625b4ff3ac 100644 --- a/linera-witty-macros/src/wit_import.rs +++ b/linera-witty-macros/src/wit_import.rs @@ -3,10 +3,34 @@ //! Generation of code to import functions from a Wasm guest module. +use crate::util::TokensSetItem; +use heck::ToKebabCase; use proc_macro2::{Span, TokenStream}; use proc_macro_error::abort; use quote::{quote, quote_spanned, ToTokens}; -use syn::{spanned::Spanned, FnArg, Ident, ReturnType, TraitItem, TraitItemMethod}; +use std::collections::HashSet; +use syn::{ + spanned::Spanned, FnArg, Ident, ItemTrait, LitStr, ReturnType, TraitItem, TraitItemMethod, +}; + +/// Returns the code generated for calling imported Wasm functions. +/// +/// The generated code contains a new generic type with the `trait_definition`'s name that allows +/// calling into the functions imported from a guest Wasm instance represented by a generic +/// parameter. +pub fn generate(trait_definition: ItemTrait, namespace: &LitStr) -> TokenStream { + WitImportGenerator::new(&trait_definition, namespace).generate() +} + +/// A helper type for generation of the importing of Wasm functions. +/// +/// Code generating is done in two phases. First the necessary pieces are collected and stored in +/// this type. Then, they are used to generate the final code. +pub struct WitImportGenerator<'input> { + trait_name: &'input Ident, + namespace: &'input LitStr, + functions: Vec>, +} /// Pieces of information extracted from a function's definition. struct FunctionInformation<'input> { @@ -18,6 +42,156 @@ struct FunctionInformation<'input> { instance_constraint: TokenStream, } +impl<'input> WitImportGenerator<'input> { + /// Collects the pieces necessary for code generation from the inputs. + fn new(trait_definition: &'input ItemTrait, namespace: &'input LitStr) -> Self { + let functions: Vec<_> = trait_definition + .items + .iter() + .map(FunctionInformation::from) + .collect(); + + WitImportGenerator { + trait_name: &trait_definition.ident, + namespace, + functions, + } + } + + /// Consumes the collected pieces to generate the final code. + fn generate(self) -> TokenStream { + let function_slots = self.function_slots(); + let slot_initializations = self.slot_initializations(); + let imported_functions = self.imported_functions(); + let instance_constraints = self.instance_constraints(); + + let trait_name = self.trait_name; + + quote! { + #[allow(clippy::type_complexity)] + pub struct #trait_name + #instance_constraints + { + instance: Instance, + #( #function_slots ),* + } + + impl #trait_name + #instance_constraints + { + pub fn new(instance: Instance) -> Self { + #trait_name { + instance, + #( #slot_initializations ),* + } + } + + #( #imported_functions )* + } + } + } + + /// Returns the function slots definitions. + /// + /// The function slots are `Option` types used to lazily store handles to the functions + /// obtained from a Wasm guest instance. + fn function_slots(&self) -> impl Iterator + '_ { + self.functions.iter().map(|function| { + let function_name = function.name(); + let instance_constraint = &function.instance_constraint; + + quote_spanned! { function.span() => + #function_name: Option<::Function> + } + }) + } + + /// Returns the expressions to initialize the function slots. + fn slot_initializations(&self) -> impl Iterator + '_ { + self.functions.iter().map(|function| { + let function_name = function.name(); + + quote_spanned! { function.span() => + #function_name: None + } + }) + } + + /// Returns the code to import and call each function. + fn imported_functions(&self) -> impl Iterator + '_ { + self.functions.iter().map(|function| { + let namespace = self.namespace; + + let function_name = function.name(); + let function_wit_name = function_name.to_string().to_kebab_case(); + + let instance = &function.instance_constraint; + let parameters = &function.parameter_definitions; + let parameter_bindings = &function.parameter_bindings; + let return_type = &function.return_type; + let interface = &function.interface; + + quote_spanned! { function.span() => + pub fn #function_name( + &mut self, + #parameters + ) -> Result<#return_type, linera_witty::RuntimeError> { + let function = match &self.#function_name { + Some(function) => function, + None => { + self.#function_name = Some(::load_function( + &mut self.instance, + &format!("{}#{}", #namespace, #function_wit_name), + )?); + + self.#function_name + .as_ref() + .expect("Function loaded into slot, but the slot remains empty") + } + }; + + let flat_parameters = #interface::lower_parameters( + linera_witty::hlist![#parameter_bindings], + &mut self.instance.memory()?, + )?; + + let flat_results = self.instance.call(function, flat_parameters)?; + + #[allow(clippy::let_unit_value)] + let result = #interface::lift_results(flat_results, &self.instance.memory()?)?; + + Ok(result) + } + } + }) + } + + /// Returns the instance constraints necessary for the generated type. + fn instance_constraints(&self) -> TokenStream { + let constraint_set: HashSet<_> = self + .functions + .iter() + .map(|function| TokensSetItem::from(&function.instance_constraint)) + .collect(); + + if constraint_set.is_empty() { + quote! {} + } else { + let constraints = constraint_set.into_iter().fold( + quote! { linera_witty::InstanceWithMemory }, + |list, item| quote! { #list + #item }, + ); + + quote! { + where + Instance: #constraints, + ::Memory: + linera_witty::RuntimeMemory, + } + } + } +} + impl<'input> FunctionInformation<'input> { /// Extracts the necessary information from the `function` and stores it in a new /// [`FunctionInformation`] instance. From 7afecc52322b38009b64533d63374e936730491f Mon Sep 17 00:00:00 2001 From: Janito Vaqueiro Ferreira Filho Date: Sat, 5 Aug 2023 15:04:47 +0000 Subject: [PATCH 10/22] Export `InstanceWithFunction` trait Allow generated code to use it. --- linera-witty/src/lib.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/linera-witty/src/lib.rs b/linera-witty/src/lib.rs index 4486c090efe..eddb0d46f41 100644 --- a/linera-witty/src/lib.rs +++ b/linera-witty/src/lib.rs @@ -26,7 +26,10 @@ pub use self::runtime::{FakeInstance, FakeRuntime}; pub use self::{ imported_function_interface::ImportedFunctionInterface, memory_layout::{JoinFlatLayouts, Layout}, - runtime::{GuestPointer, InstanceWithMemory, Memory, Runtime, RuntimeError, RuntimeMemory}, + runtime::{ + GuestPointer, InstanceWithFunction, InstanceWithMemory, Memory, Runtime, RuntimeError, + RuntimeMemory, + }, type_traits::{WitLoad, WitStore, WitType}, util::{Merge, Split}, }; From 99d0c83877b9c4e2a95e6757cf3ff3f47367fc78 Mon Sep 17 00:00:00 2001 From: Janito Vaqueiro Ferreira Filho Date: Wed, 9 Aug 2023 01:03:36 +0000 Subject: [PATCH 11/22] Export `Instance` trait Allow generated code to use it. --- linera-witty/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/linera-witty/src/lib.rs b/linera-witty/src/lib.rs index eddb0d46f41..e8c23ab6957 100644 --- a/linera-witty/src/lib.rs +++ b/linera-witty/src/lib.rs @@ -27,8 +27,8 @@ pub use self::{ imported_function_interface::ImportedFunctionInterface, memory_layout::{JoinFlatLayouts, Layout}, runtime::{ - GuestPointer, InstanceWithFunction, InstanceWithMemory, Memory, Runtime, RuntimeError, - RuntimeMemory, + GuestPointer, Instance, InstanceWithFunction, InstanceWithMemory, Memory, Runtime, + RuntimeError, RuntimeMemory, }, type_traits::{WitLoad, WitStore, WitType}, util::{Merge, Split}, From b48cb96802482c397ce8961ba0b14022a3c4a791 Mon Sep 17 00:00:00 2001 From: Janito Vaqueiro Ferreira Filho Date: Wed, 9 Aug 2023 01:00:52 +0000 Subject: [PATCH 12/22] Refactor to use a trait alias for instances Generate a trait alias for the instance constraints for the implementation of the imported functions. Use this trait alias in the generated code as well instead of using the constraints directly. --- linera-witty-macros/src/wit_import.rs | 58 ++++++++++++++++++--------- 1 file changed, 39 insertions(+), 19 deletions(-) diff --git a/linera-witty-macros/src/wit_import.rs b/linera-witty-macros/src/wit_import.rs index 2625b4ff3ac..624b85816ed 100644 --- a/linera-witty-macros/src/wit_import.rs +++ b/linera-witty-macros/src/wit_import.rs @@ -7,7 +7,7 @@ use crate::util::TokensSetItem; use heck::ToKebabCase; use proc_macro2::{Span, TokenStream}; use proc_macro_error::abort; -use quote::{quote, quote_spanned, ToTokens}; +use quote::{format_ident, quote, quote_spanned, ToTokens}; use std::collections::HashSet; use syn::{ spanned::Spanned, FnArg, Ident, ItemTrait, LitStr, ReturnType, TraitItem, TraitItemMethod, @@ -63,21 +63,27 @@ impl<'input> WitImportGenerator<'input> { let function_slots = self.function_slots(); let slot_initializations = self.slot_initializations(); let imported_functions = self.imported_functions(); - let instance_constraints = self.instance_constraints(); + let (instance_trait_alias_name, instance_trait_alias) = self.instance_trait_alias(); let trait_name = self.trait_name; quote! { #[allow(clippy::type_complexity)] pub struct #trait_name - #instance_constraints + where + Instance: #instance_trait_alias_name, + ::Memory: + linera_witty::RuntimeMemory, { instance: Instance, #( #function_slots ),* } impl #trait_name - #instance_constraints + where + Instance: #instance_trait_alias_name, + ::Memory: + linera_witty::RuntimeMemory, { pub fn new(instance: Instance) -> Self { #trait_name { @@ -88,6 +94,8 @@ impl<'input> WitImportGenerator<'input> { #( #imported_functions )* } + + #instance_trait_alias } } @@ -166,6 +174,29 @@ impl<'input> WitImportGenerator<'input> { }) } + /// Returns a trait alias for all the instance constraints necessary for the generated type. + fn instance_trait_alias(&self) -> (Ident, TokenStream) { + let name = format_ident!("InstanceFor{}", self.trait_name); + let constraints = self.instance_constraints(); + + let definition = quote! { + pub trait #name : #constraints + where + <::Runtime as linera_witty::Runtime>::Memory: + linera_witty::RuntimeMemory, + {} + + impl #name for AnyInstance + where + AnyInstance: #constraints, + ::Memory: + linera_witty::RuntimeMemory, + {} + }; + + (name, definition) + } + /// Returns the instance constraints necessary for the generated type. fn instance_constraints(&self) -> TokenStream { let constraint_set: HashSet<_> = self @@ -174,21 +205,10 @@ impl<'input> WitImportGenerator<'input> { .map(|function| TokensSetItem::from(&function.instance_constraint)) .collect(); - if constraint_set.is_empty() { - quote! {} - } else { - let constraints = constraint_set.into_iter().fold( - quote! { linera_witty::InstanceWithMemory }, - |list, item| quote! { #list + #item }, - ); - - quote! { - where - Instance: #constraints, - ::Memory: - linera_witty::RuntimeMemory, - } - } + constraint_set.into_iter().fold( + quote! { linera_witty::InstanceWithMemory }, + |list, item| quote! { #list + #item }, + ) } } From 9f4b5a355eaf5a4a903478dcf1305455663334f6 Mon Sep 17 00:00:00 2001 From: Janito Vaqueiro Ferreira Filho Date: Wed, 9 Aug 2023 01:22:57 +0000 Subject: [PATCH 13/22] Rename `FakeInstance` to `MockInstance` And `FakeRuntime` to `MockRuntime`. Prepare to allow mocking exported functions in the mocked instance. --- linera-witty/src/lib.rs | 2 +- linera-witty/src/runtime/mod.rs | 2 +- linera-witty/src/runtime/test.rs | 28 +++++++++---------- .../src/type_traits/implementations/tests.rs | 6 ++-- linera-witty/tests/wit_load.rs | 6 ++-- linera-witty/tests/wit_store.rs | 6 ++-- 6 files changed, 25 insertions(+), 25 deletions(-) diff --git a/linera-witty/src/lib.rs b/linera-witty/src/lib.rs index e8c23ab6957..9686161a6f0 100644 --- a/linera-witty/src/lib.rs +++ b/linera-witty/src/lib.rs @@ -22,7 +22,7 @@ mod type_traits; mod util; #[cfg(any(test, feature = "test"))] -pub use self::runtime::{FakeInstance, FakeRuntime}; +pub use self::runtime::{MockInstance, MockRuntime}; pub use self::{ imported_function_interface::ImportedFunctionInterface, memory_layout::{JoinFlatLayouts, Layout}, diff --git a/linera-witty/src/runtime/mod.rs b/linera-witty/src/runtime/mod.rs index aadedb7ea8d..f8f0ef5a454 100644 --- a/linera-witty/src/runtime/mod.rs +++ b/linera-witty/src/runtime/mod.rs @@ -10,7 +10,7 @@ mod test; mod traits; #[cfg(any(test, feature = "test"))] -pub use self::test::{FakeInstance, FakeRuntime}; +pub use self::test::{MockInstance, MockRuntime}; pub use self::{ error::RuntimeError, memory::{GuestPointer, Memory, RuntimeMemory}, diff --git a/linera-witty/src/runtime/test.rs b/linera-witty/src/runtime/test.rs index 88526cb3cac..c693b83a955 100644 --- a/linera-witty/src/runtime/test.rs +++ b/linera-witty/src/runtime/test.rs @@ -17,9 +17,9 @@ use std::{ }; /// A fake Wasm runtime. -pub struct FakeRuntime; +pub struct MockRuntime; -impl Runtime for FakeRuntime { +impl Runtime for MockRuntime { type Export = (); type Memory = Arc>>; } @@ -28,12 +28,12 @@ impl Runtime for FakeRuntime { /// /// Only contains exports for the memory and the canonical ABI allocation functions. #[derive(Default)] -pub struct FakeInstance { +pub struct MockInstance { memory: Arc>>, } -impl Instance for FakeInstance { - type Runtime = FakeRuntime; +impl Instance for MockInstance { + type Runtime = MockRuntime; fn load_export(&mut self, name: &str) -> Option<()> { match name { @@ -44,7 +44,7 @@ impl Instance for FakeInstance { } // Support for `cabi_free`. -impl InstanceWithFunction for FakeInstance { +impl InstanceWithFunction for MockInstance { type Function = (); fn function_from_export( @@ -64,7 +64,7 @@ impl InstanceWithFunction for FakeInstance { } // Support for `cabi_realloc`. -impl InstanceWithFunction for FakeInstance { +impl InstanceWithFunction for MockInstance { type Function = (); fn function_from_export( @@ -85,7 +85,7 @@ impl InstanceWithFunction for FakeInsta let mut memory = self .memory .lock() - .expect("Panic while holding a lock to a `FakeInstance`'s memory"); + .expect("Panic while holding a lock to a `MockInstance`'s memory"); let address = GuestPointer(memory.len().try_into()?).aligned_at(alignment as u32); @@ -100,17 +100,17 @@ impl InstanceWithFunction for FakeInsta } } -impl RuntimeMemory for Arc>> { +impl RuntimeMemory for Arc>> { fn read<'instance>( &self, - instance: &'instance FakeInstance, + instance: &'instance MockInstance, location: GuestPointer, length: u32, ) -> Result, RuntimeError> { let memory = instance .memory .lock() - .expect("Panic while holding a lock to a `FakeInstance`'s memory"); + .expect("Panic while holding a lock to a `MockInstance`'s memory"); let start = location.0 as usize; let end = start + length as usize; @@ -120,14 +120,14 @@ impl RuntimeMemory for Arc>> { fn write( &mut self, - instance: &mut FakeInstance, + instance: &mut MockInstance, location: GuestPointer, bytes: &[u8], ) -> Result<(), RuntimeError> { let mut memory = instance .memory .lock() - .expect("Panic while holding a lock to a `FakeInstance`'s memory"); + .expect("Panic while holding a lock to a `MockInstance`'s memory"); let start = location.0 as usize; let end = start + bytes.len(); @@ -138,7 +138,7 @@ impl RuntimeMemory for Arc>> { } } -impl InstanceWithMemory for FakeInstance { +impl InstanceWithMemory for MockInstance { fn memory_from_export(&self, _export: ()) -> Result>>>, RuntimeError> { Ok(Some(self.memory.clone())) } diff --git a/linera-witty/src/type_traits/implementations/tests.rs b/linera-witty/src/type_traits/implementations/tests.rs index 7f14a54d2ef..0992e4bb10e 100644 --- a/linera-witty/src/type_traits/implementations/tests.rs +++ b/linera-witty/src/type_traits/implementations/tests.rs @@ -3,7 +3,7 @@ //! Unit tests for implementations of the custom traits for existing types. -use crate::{FakeInstance, InstanceWithMemory, Layout, WitLoad, WitStore}; +use crate::{InstanceWithMemory, Layout, MockInstance, WitLoad, WitStore}; use frunk::hlist; use std::fmt::Debug; @@ -129,7 +129,7 @@ fn test_memory_roundtrip(input: T, memory_data: &[u8]) where T: Debug + Eq + WitLoad + WitStore, { - let mut instance = FakeInstance::default(); + let mut instance = MockInstance::default(); let mut memory = instance.memory().unwrap(); let length = memory_data.len() as u32; @@ -150,7 +150,7 @@ where T: Debug + Eq + WitLoad + WitStore, ::Flat: Debug + Eq, { - let mut instance = FakeInstance::default(); + let mut instance = MockInstance::default(); let mut memory = instance.memory().unwrap(); let lowered_layout = input.lower(&mut memory).unwrap(); diff --git a/linera-witty/tests/wit_load.rs b/linera-witty/tests/wit_load.rs index 811c550d436..67726be87f2 100644 --- a/linera-witty/tests/wit_load.rs +++ b/linera-witty/tests/wit_load.rs @@ -10,7 +10,7 @@ use self::types::{ Branch, Enum, Leaf, RecordWithDoublePadding, SimpleWrapper, TupleWithPadding, TupleWithoutPadding, }; -use linera_witty::{hlist, FakeInstance, InstanceWithMemory, Layout, WitLoad}; +use linera_witty::{hlist, InstanceWithMemory, Layout, MockInstance, WitLoad}; use std::fmt::Debug; /// Check that a wrapper type is properly loaded from memory and lifted from its flat layout. @@ -176,7 +176,7 @@ fn test_load_from_memory(input: &[u8], expected: T) where T: Debug + Eq + WitLoad, { - let mut instance = FakeInstance::default(); + let mut instance = MockInstance::default(); let mut memory = instance.memory().unwrap(); let address = memory.allocate(input.len() as u32).unwrap(); @@ -192,7 +192,7 @@ fn test_lift_from_flat_layout(input: ::Flat, expected: T where T: Debug + Eq + WitLoad, { - let mut instance = FakeInstance::default(); + let mut instance = MockInstance::default(); let memory = instance.memory().unwrap(); assert_eq!(T::lift_from(input, &memory).unwrap(), expected); diff --git a/linera-witty/tests/wit_store.rs b/linera-witty/tests/wit_store.rs index 2fcb5d5f79c..5bff82ff6c1 100644 --- a/linera-witty/tests/wit_store.rs +++ b/linera-witty/tests/wit_store.rs @@ -10,7 +10,7 @@ use self::types::{ Branch, Enum, Leaf, RecordWithDoublePadding, SimpleWrapper, TupleWithPadding, TupleWithoutPadding, }; -use linera_witty::{hlist, FakeInstance, InstanceWithMemory, Layout, WitStore}; +use linera_witty::{hlist, InstanceWithMemory, Layout, MockInstance, WitStore}; use std::fmt::Debug; /// Check that a wrapper type is properly stored in memory and lowered into its flat layout. @@ -218,7 +218,7 @@ fn test_store_in_memory(data: &T, expected: &[u8]) where T: WitStore, { - let mut instance = FakeInstance::default(); + let mut instance = MockInstance::default(); let mut memory = instance.memory().unwrap(); let length = expected.len() as u32; @@ -236,7 +236,7 @@ where T: WitStore, ::Flat: Debug + Eq, { - let mut instance = FakeInstance::default(); + let mut instance = MockInstance::default(); let mut memory = instance.memory().unwrap(); assert_eq!(data.lower(&mut memory).unwrap(), expected); From 74f0458601c11c743d0fc86a3bf29e708c6df39b Mon Sep 17 00:00:00 2001 From: Janito Vaqueiro Ferreira Filho Date: Wed, 9 Aug 2023 02:03:14 +0000 Subject: [PATCH 14/22] Allow adding mocked exported functions to instance Keep track of the mock exported functions and allow handlers to be called when the exported functions are called. --- linera-witty/src/runtime/test.rs | 51 ++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/linera-witty/src/runtime/test.rs b/linera-witty/src/runtime/test.rs index c693b83a955..fd267aa6afc 100644 --- a/linera-witty/src/runtime/test.rs +++ b/linera-witty/src/runtime/test.rs @@ -12,7 +12,9 @@ use super::{ }; use frunk::{hlist, hlist_pat, HList}; use std::{ + any::Any, borrow::Cow, + collections::HashMap, sync::{Arc, Mutex}, }; @@ -24,12 +26,61 @@ impl Runtime for MockRuntime { type Memory = Arc>>; } +/// A closure for handling calls to mocked exported guest functions. +pub type ExportedFunctionHandler = Box) -> Result, RuntimeError>>; + /// A fake Wasm instance. /// /// Only contains exports for the memory and the canonical ABI allocation functions. #[derive(Default)] pub struct MockInstance { memory: Arc>>, + exported_functions: HashMap, +} + +impl MockInstance { + /// Adds a mock exported function to this [`MockInstance`]. + /// + /// The `handler` will be called whenever the exported function is called. + pub fn with_exported_function( + mut self, + name: impl Into, + handler: Handler, + ) -> Self + where + Parameters: 'static, + Results: 'static, + Handler: Fn(Parameters) -> Result + 'static, + { + self.add_exported_function(name, handler); + self + } + + /// Adds a mock exported function to this [`MockInstance`]. + /// + /// The `handler` will be called whenever the exported function is called. + pub fn add_exported_function( + &mut self, + name: impl Into, + handler: Handler, + ) -> &mut Self + where + Parameters: 'static, + Results: 'static, + Handler: Fn(Parameters) -> Result + 'static, + { + self.exported_functions.insert( + name.into(), + Box::new(move |boxed_parameters| { + let parameters = boxed_parameters + .downcast() + .expect("Incorrect parameters used to call handler for exported function"); + + handler(*parameters).map(|results| Box::new(results) as Box) + }), + ); + self + } } impl Instance for MockInstance { From 8405d3ec5862845992452f984f1c1bab4513f3e1 Mon Sep 17 00:00:00 2001 From: Janito Vaqueiro Ferreira Filho Date: Wed, 9 Aug 2023 02:25:50 +0000 Subject: [PATCH 15/22] Use `String` as the export type Prepare to keep track of which handler should be called based on the exported function name. --- linera-witty/src/runtime/test.rs | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/linera-witty/src/runtime/test.rs b/linera-witty/src/runtime/test.rs index fd267aa6afc..cfd7d36aec7 100644 --- a/linera-witty/src/runtime/test.rs +++ b/linera-witty/src/runtime/test.rs @@ -22,7 +22,7 @@ use std::{ pub struct MockRuntime; impl Runtime for MockRuntime { - type Export = (); + type Export = String; type Memory = Arc>>; } @@ -86,9 +86,9 @@ impl MockInstance { impl Instance for MockInstance { type Runtime = MockRuntime; - fn load_export(&mut self, name: &str) -> Option<()> { + fn load_export(&mut self, name: &str) -> Option { match name { - "memory" | "cabi_realloc" | "cabi_free" => Some(()), + "memory" | "cabi_realloc" | "cabi_free" => Some(name.to_owned()), _ => None, } } @@ -100,7 +100,7 @@ impl InstanceWithFunction for MockInstance { fn function_from_export( &mut self, - (): ::Export, + _: ::Export, ) -> Result, RuntimeError> { Ok(Some(())) } @@ -120,7 +120,7 @@ impl InstanceWithFunction for MockInsta fn function_from_export( &mut self, - (): ::Export, + _: ::Export, ) -> Result, RuntimeError> { Ok(Some(())) } @@ -190,7 +190,14 @@ impl RuntimeMemory for Arc>> { } impl InstanceWithMemory for MockInstance { - fn memory_from_export(&self, _export: ()) -> Result>>>, RuntimeError> { - Ok(Some(self.memory.clone())) + fn memory_from_export( + &self, + export: String, + ) -> Result>>>, RuntimeError> { + if export == "memory" { + Ok(Some(self.memory.clone())) + } else { + Err(RuntimeError::NotMemory) + } } } From 23a047a42051f52328b65a3fed93be43f39e869d Mon Sep 17 00:00:00 2001 From: Janito Vaqueiro Ferreira Filho Date: Wed, 9 Aug 2023 02:28:06 +0000 Subject: [PATCH 16/22] Register canonical ABI functions an mocks Prepare to use the same mechanism for the canonical ABI memory functions. --- linera-witty/src/runtime/test.rs | 44 +++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/linera-witty/src/runtime/test.rs b/linera-witty/src/runtime/test.rs index cfd7d36aec7..fd3b9547289 100644 --- a/linera-witty/src/runtime/test.rs +++ b/linera-witty/src/runtime/test.rs @@ -32,12 +32,47 @@ pub type ExportedFunctionHandler = Box) -> Result>>, exported_functions: HashMap, } +impl Default for MockInstance { + fn default() -> Self { + let memory = Arc::new(Mutex::new(Vec::new())); + + MockInstance { + memory: memory.clone(), + exported_functions: HashMap::new(), + } + .with_exported_function("cabi_free", |_: HList![i32]| Ok(hlist![])) + .with_exported_function( + "cabi_realloc", + move |hlist_pat![_old_address, _old_size, alignment, new_size]: HList![ + i32, i32, i32, i32 + ]| { + let allocation_size = usize::try_from(new_size) + .expect("Failed to allocate a negative amount of memory"); + + let mut memory = memory + .lock() + .expect("Panic while holding a lock to a `MockInstance`'s memory"); + + let address = GuestPointer(memory.len().try_into()?).aligned_at(alignment as u32); + + memory.resize(address.0 as usize + allocation_size, 0); + + assert!( + memory.len() <= i32::MAX as usize, + "No more memory for allocations" + ); + + Ok(hlist![address.0 as i32]) + }, + ) + } +} + impl MockInstance { /// Adds a mock exported function to this [`MockInstance`]. /// @@ -87,9 +122,10 @@ impl Instance for MockInstance { type Runtime = MockRuntime; fn load_export(&mut self, name: &str) -> Option { - match name { - "memory" | "cabi_realloc" | "cabi_free" => Some(name.to_owned()), - _ => None, + if name == "memory" || self.exported_functions.contains_key(name) { + Some(name.to_owned()) + } else { + None } } } From 1a4d9806044c9461d01231633efe46b04ff34fb2 Mon Sep 17 00:00:00 2001 From: Janito Vaqueiro Ferreira Filho Date: Wed, 9 Aug 2023 02:30:39 +0000 Subject: [PATCH 17/22] Allow mock exported functions to be called Implement `InstanceWithFunction` for all valid parameters and results types. --- linera-witty/src/runtime/test.rs | 68 +++++++++++--------------------- 1 file changed, 22 insertions(+), 46 deletions(-) diff --git a/linera-witty/src/runtime/test.rs b/linera-witty/src/runtime/test.rs index fd3b9547289..687d92657e4 100644 --- a/linera-witty/src/runtime/test.rs +++ b/linera-witty/src/runtime/test.rs @@ -10,6 +10,7 @@ use super::{ GuestPointer, Instance, InstanceWithFunction, InstanceWithMemory, Runtime, RuntimeError, RuntimeMemory, }; +use crate::memory_layout::FlatLayout; use frunk::{hlist, hlist_pat, HList}; use std::{ any::Any, @@ -130,60 +131,35 @@ impl Instance for MockInstance { } } -// Support for `cabi_free`. -impl InstanceWithFunction for MockInstance { - type Function = (); +impl InstanceWithFunction for MockInstance +where + Parameters: FlatLayout + 'static, + Results: FlatLayout + 'static, +{ + type Function = String; fn function_from_export( &mut self, - _: ::Export, + name: ::Export, ) -> Result, RuntimeError> { - Ok(Some(())) + Ok(Some(name)) } fn call( &mut self, - _function: &Self::Function, - _: HList![i32], - ) -> Result { - Ok(hlist![]) - } -} - -// Support for `cabi_realloc`. -impl InstanceWithFunction for MockInstance { - type Function = (); - - fn function_from_export( - &mut self, - _: ::Export, - ) -> Result, RuntimeError> { - Ok(Some(())) - } - - fn call( - &mut self, - _function: &Self::Function, - hlist_pat![_old_address, _old_size, alignment, new_size]: HList![i32, i32, i32, i32], - ) -> Result { - let allocation_size = - usize::try_from(new_size).expect("Failed to allocate a negative amount of memory"); - - let mut memory = self - .memory - .lock() - .expect("Panic while holding a lock to a `MockInstance`'s memory"); - - let address = GuestPointer(memory.len().try_into()?).aligned_at(alignment as u32); - - memory.resize(address.0 as usize + allocation_size, 0); - - assert!( - memory.len() <= i32::MAX as usize, - "No more memory for allocations" - ); - - Ok(hlist![address.0 as i32]) + function: &Self::Function, + parameters: Parameters, + ) -> Result { + let handler = self + .exported_functions + .get(function) + .ok_or_else(|| RuntimeError::FunctionNotFound(function.clone()))?; + + let results = handler(Box::new(parameters))?; + + Ok(*results + .downcast() + .expect("Incorrect results type expected from handler of expected function")) } } From 40770381889ded7e51b52cfef1cbf52273220636 Mon Sep 17 00:00:00 2001 From: Janito Vaqueiro Ferreira Filho Date: Wed, 9 Aug 2023 14:13:39 +0000 Subject: [PATCH 18/22] Create a `MockExportedFunction` helper type Improve the ergonomics of mocking an exported function and checking that it's called correctly. --- linera-witty/src/lib.rs | 2 +- linera-witty/src/runtime/mod.rs | 2 +- linera-witty/src/runtime/test.rs | 60 +++++++++++++++++++++++++++++++- 3 files changed, 61 insertions(+), 3 deletions(-) diff --git a/linera-witty/src/lib.rs b/linera-witty/src/lib.rs index 9686161a6f0..27418a2e998 100644 --- a/linera-witty/src/lib.rs +++ b/linera-witty/src/lib.rs @@ -22,7 +22,7 @@ mod type_traits; mod util; #[cfg(any(test, feature = "test"))] -pub use self::runtime::{MockInstance, MockRuntime}; +pub use self::runtime::{MockExportedFunction, MockInstance, MockRuntime}; pub use self::{ imported_function_interface::ImportedFunctionInterface, memory_layout::{JoinFlatLayouts, Layout}, diff --git a/linera-witty/src/runtime/mod.rs b/linera-witty/src/runtime/mod.rs index f8f0ef5a454..fe1b74ec69b 100644 --- a/linera-witty/src/runtime/mod.rs +++ b/linera-witty/src/runtime/mod.rs @@ -10,7 +10,7 @@ mod test; mod traits; #[cfg(any(test, feature = "test"))] -pub use self::test::{MockInstance, MockRuntime}; +pub use self::test::{MockExportedFunction, MockInstance, MockRuntime}; pub use self::{ error::RuntimeError, memory::{GuestPointer, Memory, RuntimeMemory}, diff --git a/linera-witty/src/runtime/test.rs b/linera-witty/src/runtime/test.rs index 687d92657e4..6dcc386997f 100644 --- a/linera-witty/src/runtime/test.rs +++ b/linera-witty/src/runtime/test.rs @@ -16,7 +16,10 @@ use std::{ any::Any, borrow::Cow, collections::HashMap, - sync::{Arc, Mutex}, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, Mutex, + }, }; /// A fake Wasm runtime. @@ -213,3 +216,58 @@ impl InstanceWithMemory for MockInstance { } } } + +/// A helper type to verify how many times an exported function is called. +pub struct MockExportedFunction { + name: String, + call_counter: Arc, + expected_calls: usize, + handler: fn(Parameters) -> Result, +} + +impl MockExportedFunction +where + Parameters: 'static, + Results: 'static, +{ + /// Creates a new [`MockExportedFunction`] for the exported function with the provided `name`. + /// + /// Every call to the exported function is called is forwarded to the `handler` and an internal + /// counter is incremented. When the [`MockExportedFunction`] instance is dropped (which should + /// be done at the end of the test}, it asserts that the function was called `expected_calls` + /// times. + pub fn new( + name: impl Into, + handler: fn(Parameters) -> Result, + expected_calls: usize, + ) -> Self { + MockExportedFunction { + name: name.into(), + call_counter: Arc::default(), + expected_calls, + handler, + } + } + + /// Registers this [`MockExportedFunction`] with the mock `instance`. + pub fn register(&self, instance: &mut MockInstance) { + let call_counter = self.call_counter.clone(); + let handler = self.handler; + + instance.add_exported_function(self.name.clone(), move |parameters: Parameters| { + call_counter.fetch_add(1, Ordering::AcqRel); + handler(parameters) + }); + } +} + +impl Drop for MockExportedFunction { + fn drop(&mut self) { + assert_eq!( + self.call_counter.load(Ordering::Acquire), + self.expected_calls, + "Unexpected number of calls to `{}`", + self.name + ); + } +} From 8af3b5c18757954b4586b5125b7bf7923ff65b00 Mon Sep 17 00:00:00 2001 From: Janito Vaqueiro Ferreira Filho Date: Wed, 9 Aug 2023 17:48:19 +0000 Subject: [PATCH 19/22] Test importing a simple function Check that a function without parameters or return types can be imported from a mock instance. --- Cargo.lock | 1 + linera-witty/Cargo.toml | 1 + linera-witty/tests/common/test_instance.rs | 73 ++++++++++++++++++++++ linera-witty/tests/wit_import.rs | 36 +++++++++++ 4 files changed, 111 insertions(+) create mode 100644 linera-witty/tests/common/test_instance.rs create mode 100644 linera-witty/tests/wit_import.rs diff --git a/Cargo.lock b/Cargo.lock index e0c761a75f8..5ab0656d3dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3165,6 +3165,7 @@ dependencies = [ "frunk", "linera-witty", "linera-witty-macros", + "test-case", "thiserror", ] diff --git a/linera-witty/Cargo.toml b/linera-witty/Cargo.toml index b5aaba5b27e..56556fa6b01 100644 --- a/linera-witty/Cargo.toml +++ b/linera-witty/Cargo.toml @@ -23,3 +23,4 @@ thiserror = { workspace = true } [dev-dependencies] linera-witty = { workspace = true, features = ["macros", "test"] } +test-case = { workspace = true } diff --git a/linera-witty/tests/common/test_instance.rs b/linera-witty/tests/common/test_instance.rs new file mode 100644 index 00000000000..7ea84d3daaa --- /dev/null +++ b/linera-witty/tests/common/test_instance.rs @@ -0,0 +1,73 @@ +// Copyright (c) Zefchain Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +//! Helper code for testing using different runtimes. + +use frunk::{hlist, HList}; +use linera_witty::{InstanceWithMemory, MockExportedFunction, MockInstance, RuntimeError}; +use std::any::Any; + +/// Trait representing a type that can create instances for tests. +pub trait TestInstanceFactory { + type Instance: InstanceWithMemory; + + fn load_test_module(&mut self, module_name: &str) -> Self::Instance; +} + +/// A factory of [`MockInstance`]s. +#[derive(Default)] +pub struct MockInstanceFactory { + deferred_assertions: Vec>, +} + +impl TestInstanceFactory for MockInstanceFactory { + type Instance = MockInstance; + + fn load_test_module(&mut self, module: &str) -> Self::Instance { + let mut instance = MockInstance::default(); + + match module { + "simple-function" => self.simple_function(&mut instance), + _ => panic!("Attempt to load module {module:?} which has no mocked exported methods"), + } + + instance + } +} + +impl MockInstanceFactory { + /// Mock the exported functions for the "simple-function" module. + fn simple_function(&mut self, instance: &mut MockInstance) { + self.mock_exported_function( + instance, + "witty-macros:test-modules/simple-function#simple", + |_: HList![]| Ok(hlist![]), + 1, + ); + } + + /// Mocks an exported function with the provided `name`. + /// + /// The `handler` is used when the exported function is called, which expected to happen + /// `expected_calls` times. + /// + /// The created [`MockExportedFunction`] is automatically registered in the `instance` and + /// added to the current list of deferred assertions, to be checked when the test finishes. + fn mock_exported_function( + &mut self, + instance: &mut MockInstance, + name: &str, + handler: fn(Parameters) -> Result, + expected_calls: usize, + ) where + Parameters: 'static, + Results: 'static, + { + let mock_exported_function = MockExportedFunction::new(name, handler, expected_calls); + + mock_exported_function.register(instance); + + self.deferred_assertions + .push(Box::new(mock_exported_function)); + } +} diff --git a/linera-witty/tests/wit_import.rs b/linera-witty/tests/wit_import.rs new file mode 100644 index 00000000000..77a47c15cbc --- /dev/null +++ b/linera-witty/tests/wit_import.rs @@ -0,0 +1,36 @@ +// Copyright (c) Zefchain Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +//! Tests for the `wit_import` attribute macro. + +#![allow(clippy::bool_assert_comparison)] + +#[path = "common/test_instance.rs"] +mod test_instance; + +use self::test_instance::{MockInstanceFactory, TestInstanceFactory}; +use linera_witty::{Instance, Runtime, RuntimeMemory}; +use linera_witty_macros::wit_import; +use test_case::test_case; + +/// An interface to import a single function without parameters or return values. +#[wit_import(package = "witty-macros:test-modules")] +trait SimpleFunction { + fn simple(); +} + +/// Test importing a simple function without parameters or return values. +#[test_case(MockInstanceFactory::default(); "with a mock instance")] +fn simple_function(mut factory: InstanceFactory) +where + InstanceFactory: TestInstanceFactory, + InstanceFactory::Instance: InstanceForSimpleFunction, + <::Runtime as Runtime>::Memory: + RuntimeMemory, +{ + let instance = factory.load_test_module("simple-function"); + + SimpleFunction::new(instance) + .simple() + .expect("Failed to call guest's `simple` function"); +} From 521a6706ad0df66ff73f2b15246aa8cfc75b49b0 Mon Sep 17 00:00:00 2001 From: Janito Vaqueiro Ferreira Filho Date: Wed, 9 Aug 2023 18:12:25 +0000 Subject: [PATCH 20/22] Test importing functions that return values Check that functions that have return types can be imported from a mock instance. --- linera-witty/tests/common/test_instance.rs | 77 +++++++++++++++ linera-witty/tests/wit_import.rs | 104 +++++++++++++++++++++ 2 files changed, 181 insertions(+) diff --git a/linera-witty/tests/common/test_instance.rs b/linera-witty/tests/common/test_instance.rs index 7ea84d3daaa..3ea3ceb3e00 100644 --- a/linera-witty/tests/common/test_instance.rs +++ b/linera-witty/tests/common/test_instance.rs @@ -28,6 +28,7 @@ impl TestInstanceFactory for MockInstanceFactory { match module { "simple-function" => self.simple_function(&mut instance), + "getters" => self.getters(&mut instance), _ => panic!("Attempt to load module {module:?} which has no mocked exported methods"), } @@ -46,6 +47,82 @@ impl MockInstanceFactory { ); } + /// Mock the exported functions for the "getters" module. + fn getters(&mut self, instance: &mut MockInstance) { + self.mock_exported_function( + instance, + "witty-macros:test-modules/getters#get-true", + |_: HList![]| Ok(hlist![1_i32]), + 1, + ); + self.mock_exported_function( + instance, + "witty-macros:test-modules/getters#get-false", + |_: HList![]| Ok(hlist![0_i32]), + 1, + ); + self.mock_exported_function( + instance, + "witty-macros:test-modules/getters#get-s8", + |_: HList![]| Ok(hlist![-125_i8 as u8 as i32]), + 1, + ); + self.mock_exported_function( + instance, + "witty-macros:test-modules/getters#get-u8", + |_: HList![]| Ok(hlist![200_u8 as i32]), + 1, + ); + self.mock_exported_function( + instance, + "witty-macros:test-modules/getters#get-s16", + |_: HList![]| Ok(hlist![-410_i16 as u16 as i32]), + 1, + ); + self.mock_exported_function( + instance, + "witty-macros:test-modules/getters#get-u16", + |_: HList![]| Ok(hlist![60_000_u16 as i32]), + 1, + ); + self.mock_exported_function( + instance, + "witty-macros:test-modules/getters#get-s32", + |_: HList![]| Ok(hlist![-100_000_i32]), + 1, + ); + self.mock_exported_function( + instance, + "witty-macros:test-modules/getters#get-u32", + |_: HList![]| Ok(hlist![3_000_111_i32]), + 1, + ); + self.mock_exported_function( + instance, + "witty-macros:test-modules/getters#get-s64", + |_: HList![]| Ok(hlist![-5_000_000_i64]), + 1, + ); + self.mock_exported_function( + instance, + "witty-macros:test-modules/getters#get-u64", + |_: HList![]| Ok(hlist![10_000_000_000_i64]), + 1, + ); + self.mock_exported_function( + instance, + "witty-macros:test-modules/getters#get-float32", + |_: HList![]| Ok(hlist![-0.125_f32]), + 1, + ); + self.mock_exported_function( + instance, + "witty-macros:test-modules/getters#get-float64", + |_: HList![]| Ok(hlist![128.25_f64]), + 1, + ); + } + /// Mocks an exported function with the provided `name`. /// /// The `handler` is used when the exported function is called, which expected to happen diff --git a/linera-witty/tests/wit_import.rs b/linera-witty/tests/wit_import.rs index 77a47c15cbc..5c1d514d5e7 100644 --- a/linera-witty/tests/wit_import.rs +++ b/linera-witty/tests/wit_import.rs @@ -34,3 +34,107 @@ where .simple() .expect("Failed to call guest's `simple` function"); } + +/// An interface to import functions with return values. +#[wit_import(package = "witty-macros:test-modules")] +trait Getters { + fn get_true() -> bool; + fn get_false() -> bool; + fn get_s8() -> i8; + fn get_u8() -> u8; + fn get_s16() -> i16; + fn get_u16() -> u16; + fn get_s32() -> i32; + fn get_u32() -> u32; + fn get_s64() -> i64; + fn get_u64() -> u64; + fn get_float32() -> f32; + fn get_float64() -> f64; +} + +/// Test importing functions with return values. +#[test_case(MockInstanceFactory::default(); "with a mock instance")] +fn getters(mut factory: InstanceFactory) +where + InstanceFactory: TestInstanceFactory, + InstanceFactory::Instance: InstanceForGetters, + <::Runtime as Runtime>::Memory: + RuntimeMemory, +{ + let instance = factory.load_test_module("getters"); + + let mut getters = Getters::new(instance); + + assert_eq!( + getters + .get_true() + .expect("Failed to run guest's `get-true` function"), + true + ); + assert_eq!( + getters + .get_false() + .expect("Failed to run guest's `get-false` function"), + false + ); + assert_eq!( + getters + .get_s8() + .expect("Failed to run guest's `get-s8` function"), + -125 + ); + assert_eq!( + getters + .get_u8() + .expect("Failed to run guest's `get-u8` function"), + 200 + ); + assert_eq!( + getters + .get_s16() + .expect("Failed to run guest's `get-s16` function"), + -410 + ); + assert_eq!( + getters + .get_u16() + .expect("Failed to run guest's `get-u16` function"), + 60_000 + ); + assert_eq!( + getters + .get_s32() + .expect("Failed to run guest's `get-s32` function"), + -100_000 + ); + assert_eq!( + getters + .get_u32() + .expect("Failed to run guest's `get-u32` function"), + 3_000_111 + ); + assert_eq!( + getters + .get_s64() + .expect("Failed to run guest's `get-s64` function"), + -5_000_000 + ); + assert_eq!( + getters + .get_u64() + .expect("Failed to run guest's `get-u64` function"), + 10_000_000_000 + ); + assert_eq!( + getters + .get_float32() + .expect("Failed to run guest's `get-f32` function"), + -0.125 + ); + assert_eq!( + getters + .get_float64() + .expect("Failed to run guest's `get-f64` function"), + 128.25 + ); +} From 80820a7b6fe4e441e9b59079fb396f0ebfb15130 Mon Sep 17 00:00:00 2001 From: Janito Vaqueiro Ferreira Filho Date: Wed, 9 Aug 2023 18:18:48 +0000 Subject: [PATCH 21/22] Test importing functions with single parameters Check that functions that have single parameters can be imported from a mock instance. --- linera-witty/tests/common/test_instance.rs | 106 ++++++++++++++++++++- linera-witty/tests/wit_import.rs | 64 +++++++++++++ 2 files changed, 169 insertions(+), 1 deletion(-) diff --git a/linera-witty/tests/common/test_instance.rs b/linera-witty/tests/common/test_instance.rs index 3ea3ceb3e00..1d46d870ce6 100644 --- a/linera-witty/tests/common/test_instance.rs +++ b/linera-witty/tests/common/test_instance.rs @@ -3,7 +3,7 @@ //! Helper code for testing using different runtimes. -use frunk::{hlist, HList}; +use frunk::{hlist, hlist_pat, HList}; use linera_witty::{InstanceWithMemory, MockExportedFunction, MockInstance, RuntimeError}; use std::any::Any; @@ -29,6 +29,7 @@ impl TestInstanceFactory for MockInstanceFactory { match module { "simple-function" => self.simple_function(&mut instance), "getters" => self.getters(&mut instance), + "setters" => self.setters(&mut instance), _ => panic!("Attempt to load module {module:?} which has no mocked exported methods"), } @@ -123,6 +124,109 @@ impl MockInstanceFactory { ); } + /// Mock the exported functions for the "setters" module. + fn setters(&mut self, instance: &mut MockInstance) { + self.mock_exported_function( + instance, + "witty-macros:test-modules/setters#set-bool", + |hlist_pat![parameter]: HList![i32]| { + assert_eq!(parameter, 0); + Ok(hlist![]) + }, + 1, + ); + self.mock_exported_function( + instance, + "witty-macros:test-modules/setters#set-s8", + |hlist_pat![parameter]: HList![i32]| { + assert_eq!(parameter, -100_i8 as i32); + Ok(hlist![]) + }, + 1, + ); + self.mock_exported_function( + instance, + "witty-macros:test-modules/setters#set-u8", + |hlist_pat![parameter]: HList![i32]| { + assert_eq!(parameter, 201); + Ok(hlist![]) + }, + 1, + ); + self.mock_exported_function( + instance, + "witty-macros:test-modules/setters#set-s16", + |hlist_pat![parameter]: HList![i32]| { + assert_eq!(parameter, -20_000_i16 as i32); + Ok(hlist![]) + }, + 1, + ); + self.mock_exported_function( + instance, + "witty-macros:test-modules/setters#set-u16", + |hlist_pat![parameter]: HList![i32]| { + assert_eq!(parameter, 50_000); + Ok(hlist![]) + }, + 1, + ); + self.mock_exported_function( + instance, + "witty-macros:test-modules/setters#set-s32", + |hlist_pat![parameter]: HList![i32]| { + assert_eq!(parameter, -2_000_000); + Ok(hlist![]) + }, + 1, + ); + self.mock_exported_function( + instance, + "witty-macros:test-modules/setters#set-u32", + |hlist_pat![parameter]: HList![i32]| { + assert_eq!(parameter, 4_000_000_u32 as i32); + Ok(hlist![]) + }, + 1, + ); + self.mock_exported_function( + instance, + "witty-macros:test-modules/setters#set-s64", + |hlist_pat![parameter]: HList![i64]| { + assert_eq!(parameter, -25_000_000_000); + Ok(hlist![]) + }, + 1, + ); + self.mock_exported_function( + instance, + "witty-macros:test-modules/setters#set-u64", + |hlist_pat![parameter]: HList![i64]| { + assert_eq!(parameter, 7_000_000_000); + Ok(hlist![]) + }, + 1, + ); + self.mock_exported_function( + instance, + "witty-macros:test-modules/setters#set-float32", + |hlist_pat![parameter]: HList![f32]| { + assert_eq!(parameter, 10.5); + Ok(hlist![]) + }, + 1, + ); + self.mock_exported_function( + instance, + "witty-macros:test-modules/setters#set-float64", + |hlist_pat![parameter]: HList![f64]| { + assert_eq!(parameter, -0.000_08); + Ok(hlist![]) + }, + 1, + ); + } + /// Mocks an exported function with the provided `name`. /// /// The `handler` is used when the exported function is called, which expected to happen diff --git a/linera-witty/tests/wit_import.rs b/linera-witty/tests/wit_import.rs index 5c1d514d5e7..ef4b1e958e6 100644 --- a/linera-witty/tests/wit_import.rs +++ b/linera-witty/tests/wit_import.rs @@ -138,3 +138,67 @@ where 128.25 ); } + +/// An interface to import functions with parameters. +#[wit_import(package = "witty-macros:test-modules")] +trait Setters { + fn set_bool(value: bool); + fn set_s8(value: i8); + fn set_u8(value: u8); + fn set_s16(value: i16); + fn set_u16(value: u16); + fn set_s32(value: i32); + fn set_u32(value: u32); + fn set_s64(value: i64); + fn set_u64(value: u64); + fn set_float32(value: f32); + fn set_float64(value: f64); +} + +/// Test importing functions with parameters. +#[test_case(MockInstanceFactory::default(); "with a mock instance")] +fn setters(mut factory: InstanceFactory) +where + InstanceFactory: TestInstanceFactory, + InstanceFactory::Instance: InstanceForSetters, + <::Runtime as Runtime>::Memory: + RuntimeMemory, +{ + let instance = factory.load_test_module("setters"); + + let mut setters = Setters::new(instance); + + setters + .set_bool(false) + .expect("Failed to run guest's `set-bool` function"); + setters + .set_s8(-100) + .expect("Failed to run guest's `set-s8` function"); + setters + .set_u8(201) + .expect("Failed to run guest's `set-u8` function"); + setters + .set_s16(-20_000) + .expect("Failed to run guest's `set-s16` function"); + setters + .set_u16(50_000) + .expect("Failed to run guest's `set-u16` function"); + setters + .set_s32(-2_000_000) + .expect("Failed to run guest's `set-s32` function"); + setters + .set_u32(4_000_000) + .expect("Failed to run guest's `set-u32` function"); + setters + .set_s64(-25_000_000_000) + .expect("Failed to run guest's `set-s64` function"); + setters + .set_u64(7_000_000_000) + .expect("Failed to run guest's `set-u64` function"); + setters + .set_float32(10.5) + .expect("Failed to run guest's `set-f32` function"); + setters + .set_float64(-0.000_08) + .expect("Failed to run guest's `set-f64` function"); +} From 253cce5c45c12ed649e3a51ca981f0812eaaf971 Mon Sep 17 00:00:00 2001 From: Janito Vaqueiro Ferreira Filho Date: Wed, 9 Aug 2023 18:19:20 +0000 Subject: [PATCH 22/22] Test importing input/output functions Check that functions that have multiple parameters and return values can be imported from a mock instance. --- linera-witty/tests/common/test_instance.rs | 73 +++++++++++++++ linera-witty/tests/wit_import.rs | 103 +++++++++++++++++++++ 2 files changed, 176 insertions(+) diff --git a/linera-witty/tests/common/test_instance.rs b/linera-witty/tests/common/test_instance.rs index 1d46d870ce6..049fb3155c3 100644 --- a/linera-witty/tests/common/test_instance.rs +++ b/linera-witty/tests/common/test_instance.rs @@ -30,6 +30,7 @@ impl TestInstanceFactory for MockInstanceFactory { "simple-function" => self.simple_function(&mut instance), "getters" => self.getters(&mut instance), "setters" => self.setters(&mut instance), + "operations" => self.operations(&mut instance), _ => panic!("Attempt to load module {module:?} which has no mocked exported methods"), } @@ -227,6 +228,78 @@ impl MockInstanceFactory { ); } + /// Mock the exported functions for the "operations" module. + fn operations(&mut self, instance: &mut MockInstance) { + self.mock_exported_function( + instance, + "witty-macros:test-modules/operations#and-bool", + |hlist_pat![first, second]: HList![i32, i32]| { + Ok(hlist![if first != 0 && second != 0 { 1 } else { 0 }]) + }, + 2, + ); + self.mock_exported_function( + instance, + "witty-macros:test-modules/operations#add-s8", + |hlist_pat![first, second]: HList![i32, i32]| Ok(hlist![first + second]), + 1, + ); + self.mock_exported_function( + instance, + "witty-macros:test-modules/operations#add-u8", + |hlist_pat![first, second]: HList![i32, i32]| Ok(hlist![first + second]), + 1, + ); + self.mock_exported_function( + instance, + "witty-macros:test-modules/operations#add-s16", + |hlist_pat![first, second]: HList![i32, i32]| Ok(hlist![first + second]), + 1, + ); + self.mock_exported_function( + instance, + "witty-macros:test-modules/operations#add-u16", + |hlist_pat![first, second]: HList![i32, i32]| Ok(hlist![first + second]), + 1, + ); + self.mock_exported_function( + instance, + "witty-macros:test-modules/operations#add-s32", + |hlist_pat![first, second]: HList![i32, i32]| Ok(hlist![first + second]), + 1, + ); + self.mock_exported_function( + instance, + "witty-macros:test-modules/operations#add-u32", + |hlist_pat![first, second]: HList![i32, i32]| Ok(hlist![first + second]), + 1, + ); + self.mock_exported_function( + instance, + "witty-macros:test-modules/operations#add-s64", + |hlist_pat![first, second]: HList![i64, i64]| Ok(hlist![first + second]), + 1, + ); + self.mock_exported_function( + instance, + "witty-macros:test-modules/operations#add-u64", + |hlist_pat![first, second]: HList![i64, i64]| Ok(hlist![first + second]), + 1, + ); + self.mock_exported_function( + instance, + "witty-macros:test-modules/operations#add-float32", + |hlist_pat![first, second]: HList![f32, f32]| Ok(hlist![first + second]), + 1, + ); + self.mock_exported_function( + instance, + "witty-macros:test-modules/operations#add-float64", + |hlist_pat![first, second]: HList![f64, f64]| Ok(hlist![first + second]), + 1, + ); + } + /// Mocks an exported function with the provided `name`. /// /// The `handler` is used when the exported function is called, which expected to happen diff --git a/linera-witty/tests/wit_import.rs b/linera-witty/tests/wit_import.rs index ef4b1e958e6..9ee37713ddb 100644 --- a/linera-witty/tests/wit_import.rs +++ b/linera-witty/tests/wit_import.rs @@ -202,3 +202,106 @@ where .set_float64(-0.000_08) .expect("Failed to run guest's `set-f64` function"); } + +/// An interface to import functions with multiple parameters and return values. +#[wit_import(package = "witty-macros:test-modules")] +trait Operations { + fn and_bool(first: bool, second: bool) -> bool; + fn add_s8(first: i8, second: i8) -> i8; + fn add_u8(first: u8, second: u8) -> u8; + fn add_s16(first: i16, second: i16) -> i16; + fn add_u16(first: u16, second: u16) -> u16; + fn add_s32(first: i32, second: i32) -> i32; + fn add_u32(first: u32, second: u32) -> u32; + fn add_s64(first: i64, second: i64) -> i64; + fn add_u64(first: u64, second: u64) -> u64; + fn add_float32(first: f32, second: f32) -> f32; + fn add_float64(first: f64, second: f64) -> f64; +} + +/// Test importing functions with multiple parameters and return values. +#[test_case(MockInstanceFactory::default(); "with a mock instance")] +fn operations(mut factory: InstanceFactory) +where + InstanceFactory: TestInstanceFactory, + InstanceFactory::Instance: InstanceForOperations, + <::Runtime as Runtime>::Memory: + RuntimeMemory, +{ + let instance = factory.load_test_module("operations"); + + let mut operations = Operations::new(instance); + + assert_eq!( + operations + .and_bool(false, true) + .expect("Failed to run guest's `and-bool` function"), + false + ); + assert_eq!( + operations + .and_bool(true, true) + .expect("Failed to run guest's `and-bool` function"), + true + ); + assert_eq!( + operations + .add_s8(-126, 1) + .expect("Failed to run guest's `add-s8` function"), + -125 + ); + assert_eq!( + operations + .add_u8(189, 11) + .expect("Failed to run guest's `add-u8` function"), + 200 + ); + assert_eq!( + operations + .add_s16(-400, -10) + .expect("Failed to run guest's `add-s16` function"), + -410 + ); + assert_eq!( + operations + .add_u16(32_000, 28_000) + .expect("Failed to run guest's `add-u16` function"), + 60_000 + ); + assert_eq!( + operations + .add_s32(-2_000_000, 1_900_000) + .expect("Failed to run guest's `add-s32` function"), + -100_000 + ); + assert_eq!( + operations + .add_u32(3_000_000, 111) + .expect("Failed to run guest's `add-u32` function"), + 3_000_111 + ); + assert_eq!( + operations + .add_s64(-2_000_000_001, 5_000_000_000) + .expect("Failed to run guest's `add-s64` function"), + 2_999_999_999 + ); + assert_eq!( + operations + .add_u64(1_000_000_000, 1_000_000_000_000) + .expect("Failed to run guest's `add-u64` function"), + 1_001_000_000_000 + ); + assert_eq!( + operations + .add_float32(0.0, -0.125) + .expect("Failed to run guest's `add-f32` function"), + -0.125 + ); + assert_eq!( + operations + .add_float64(128.0, 0.25) + .expect("Failed to run guest's `add-f64` function"), + 128.25 + ); +}