Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow mutable parameters in messages #2004

Merged
merged 6 commits into from
Nov 28, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .config/cargo_spellcheck.dic
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ fuzzer
getter
growable
ident
idents
interoperate
invariants
kB
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Added
- Allow mutable parameters in messages - [#2004](https://github.com/paritytech/ink/pull/2004)
- [E2E] Allow testing with live-chain state - [#1949](https://github.com/paritytech/ink/pull/1949)
- [E2E] Call builders and extra gas margin option - [#1917](https://github.com/paritytech/ink/pull/1917)
- Linter: `storage_never_freed` lint - [#1932](https://github.com/paritytech/ink/pull/1932)
Expand Down
19 changes: 18 additions & 1 deletion crates/ink/codegen/src/generator/arg_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ pub fn output_ident(message_name: &syn::Ident) -> syn::Ident {
format_ident!("{}Output", message_name.to_string().to_lower_camel_case())
}

/// Returns the sequence of artificial input parameter bindings for the message.
/// Returns the sequence of artificial input parameter bindings
/// for the message or constructor.
///
/// # Note
///
Expand All @@ -46,6 +47,22 @@ pub fn input_types(inputs: ir::InputsIter) -> Vec<&syn::Type> {
inputs.map(|pat_type| &*pat_type.ty).collect::<Vec<_>>()
}

/// Returns the sequence of input idents for the message.
pub fn input_message_idents(inputs: ir::InputsIter) -> Vec<&syn::Ident> {
inputs
.map(|input| {
match &*input.pat {
syn::Pat::Ident(ident) => &ident.ident,
_ => {
unreachable!(
"encountered ink! dispatch input with missing identifier"
)
}
}
})
.collect::<Vec<_>>()
}

/// Returns a tuple type representing the types yielded by the input types.
pub fn input_types_tuple(inputs: ir::InputsIter) -> TokenStream2 {
let input_types = input_types(inputs);
Expand Down
16 changes: 8 additions & 8 deletions crates/ink/codegen/src/generator/as_dependency/contract_ref.rs
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ impl ContractRef<'_> {
ir::Receiver::RefMut => quote! { forward_mut },
};
let mut_token = message.receiver().is_ref_mut().then(|| quote! { mut });
let input_bindings = message.inputs().map(|input| &input.pat).collect::<Vec<_>>();
let input_idents = generator::input_message_idents(message.inputs());
let input_types = message.inputs().map(|input| &input.ty).collect::<Vec<_>>();
let cfg_attrs = message.get_cfg_attrs(span);
quote_spanned!(span=>
Expand All @@ -301,13 +301,13 @@ impl ContractRef<'_> {
#( #cfg_attrs )*
fn #message_ident(
& #mut_token self
#( , #input_bindings : #input_types )*
#( , #input_idents : #input_types )*
) -> Self::#output_ident {
<_ as #trait_path>::#message_ident(
<_ as ::ink::codegen::TraitCallForwarderFor<{#trait_info_id}>>::#forward_operator(
<Self as ::ink::codegen::TraitCallBuilder>::#call_operator(self),
)
#( , #input_bindings )*
#( , #input_idents )*
)
}
)
Expand Down Expand Up @@ -384,7 +384,7 @@ impl ContractRef<'_> {
ir::Receiver::RefMut => quote! { call_mut },
};
let mut_token = message.receiver().is_ref_mut().then(|| quote! { mut });
let input_bindings = message.inputs().map(|input| &input.pat).collect::<Vec<_>>();
let input_idents = generator::input_message_idents(message.inputs());
let input_types = message.inputs().map(|input| &input.ty).collect::<Vec<_>>();
let output_type = message.output().map(|ty| quote! { -> #ty });
let wrapped_output_type = message.wrapped_output();
Expand All @@ -393,9 +393,9 @@ impl ContractRef<'_> {
#[inline]
pub fn #message_ident(
& #mut_token self
#( , #input_bindings : #input_types )*
#( , #input_idents : #input_types )*
) #output_type {
self.#try_message_ident( #( #input_bindings, )* )
self.#try_message_ident( #( #input_idents, )* )
.unwrap_or_else(|error| ::core::panic!(
"encountered error while calling {}::{}: {:?}",
::core::stringify!(#storage_ident),
Expand All @@ -408,10 +408,10 @@ impl ContractRef<'_> {
#[inline]
pub fn #try_message_ident(
& #mut_token self
#( , #input_bindings : #input_types )*
#( , #input_idents : #input_types )*
) -> #wrapped_output_type {
<Self as ::ink::codegen::TraitCallBuilder>::#call_operator(self)
.#message_ident( #( #input_bindings ),* )
.#message_ident( #( #input_idents ),* )
.try_invoke()
.unwrap_or_else(|error| ::core::panic!(
"encountered error while calling {}::{}: {:?}",
Expand Down
1 change: 1 addition & 0 deletions crates/ink/codegen/src/generator/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ pub use self::{
generate_reference_to_trait_info,
input_bindings,
input_bindings_tuple,
input_message_idents,
input_types,
input_types_tuple,
output_ident,
Expand Down
30 changes: 24 additions & 6 deletions crates/ink/ir/src/ir/item_impl/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -382,13 +382,23 @@ mod tests {

#[test]
fn inputs_works() {
macro_rules! expected_input {
( mut $name:ident: $ty:ty ) => {{
syn::parse_quote! {
mut $name: $ty
}
}};
( $name:ident: $ty:ty ) => {{
syn::parse_quote! {
$name: $ty
}
}};
}
macro_rules! expected_inputs {
( $( $name:ident: $ty:ty ),* ) => {{
( $( $($ts:ident)+: $ty:ty ),* ) => {{
vec![
$(
syn::parse_quote! {
$name: $ty
}
expected_input!($($ts)+: $ty)
),*
]
}};
Expand All @@ -410,12 +420,20 @@ mod tests {
fn my_message(&self, a: i32) {}
},
),
(
// Single mutable input:
expected_inputs!(mut a: i32),
syn::parse_quote! {
#[ink(message)]
fn my_message(&self, mut a: i32) {}
},
),
(
// Some inputs:
expected_inputs!(a: i32, b: u64, c: [u8; 32]),
expected_inputs!(a: i32, b: u64, mut c: [u8; 32]),
syn::parse_quote! {
#[ink(message)]
fn my_message(&self, a: i32, b: u64, c: [u8; 32]) {}
fn my_message(&self, a: i32, b: u64, mut c: [u8; 32]) {}
},
),
];
Expand Down
9 changes: 9 additions & 0 deletions integration-tests/incrementer-mut/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Ignore build artifacts from the local tests sub-crate.
/target/

# Ignore backup files creates by cargo fmt.
**/*.rs.bk

# Remove Cargo.lock when creating an executable, leave it for libraries
# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock
Cargo.lock
23 changes: 23 additions & 0 deletions integration-tests/incrementer-mut/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[package]
name = "incrementer_mut"
version = "5.0.0-alpha"
authors = ["Parity Technologies <[email protected]>"]
edition = "2021"
publish = false

[dependencies]
ink = { path = "../../crates/ink", default-features = false }

[dev-dependencies]
ink_e2e = { path = "../../crates/e2e" }

[lib]
path = "lib.rs"

[features]
default = ["std"]
std = [
"ink/std",
]
ink-as-dependency = []
e2e-tests = []
136 changes: 136 additions & 0 deletions integration-tests/incrementer-mut/lib.rs
ascjones marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
#![cfg_attr(not(feature = "std"), no_std, no_main)]

//! A simple incrementer contract
//! demonstrating the internal mutability of message parameters.

pub use self::incrementer_mut::{
Incrementer,
IncrementerRef,
};

#[ink::contract]
mod incrementer_mut {
#[ink(storage)]
pub struct Incrementer {
value: i32,
}

impl Incrementer {
/// Create a new contract with the specified counter value.
/// If it is below 0, it is set to 0
#[ink(constructor)]
pub fn new(mut init_value: i32) -> Self {
if init_value < 0 {
init_value = 0;
}
Self { value: init_value }
}

#[ink(constructor)]
pub fn new_default() -> Self {
Self::new(Default::default())
}

#[ink(message)]
pub fn inc(&mut self, by: i32) {
self.value = self.value.checked_add(by).unwrap();
}

/// Update the counter with the specified value.
/// If it is above 100, we set it to 0.
#[ink(message)]
pub fn update(&mut self, mut value: i32) {
if value > 100 {
value = 0;
}
self.value = value;
}

#[ink(message)]
pub fn get(&self) -> i32 {
self.value
}
}

#[cfg(test)]
mod tests {
use super::*;

#[ink::test]
fn default_works() {
let contract = Incrementer::new_default();
assert_eq!(contract.get(), 0);
}

#[ink::test]
fn it_works() {
let mut contract = Incrementer::new(42);
assert_eq!(contract.get(), 42);
contract.inc(5);
assert_eq!(contract.get(), 47);
contract.inc(-50);
assert_eq!(contract.get(), -3);
}
#[ink::test]
fn mutability_works() {
let mut contract = Incrementer::new(-5);
assert_eq!(contract.get(), 0);
contract.update(80);
assert_eq!(contract.get(), 80);
contract.update(120);
assert_eq!(contract.get(), 0);
}
}

#[cfg(all(test, feature = "e2e-tests"))]
mod e2e_tests {
use super::*;
use ink_e2e::ContractsBackend;

type E2EResult<T> = std::result::Result<T, Box<dyn std::error::Error>>;

#[ink_e2e::test]
async fn it_works<Client: E2EBackend>(mut client: Client) -> E2EResult<()> {
// given
let mut constructor = IncrementerRef::new(-2);
let contract = client
.instantiate("incrementer_mut", &ink_e2e::alice(), &mut constructor)
.submit()
.await
.expect("instantiate failed");
let mut call = contract.call::<Incrementer>();

let get = call.get();
let get_res = client.call(&ink_e2e::bob(), &get).dry_run().await?;
assert!(matches!(get_res.return_value(), 0));

// when
let flip = call.update(50);
let _flip_res = client
.call(&ink_e2e::bob(), &flip)
.submit()
.await
.expect("update failed");

// then
let get = call.get();
let get_res = client.call(&ink_e2e::bob(), &get).dry_run().await?;
assert!(matches!(get_res.return_value(), 50));

// when
let flip = call.update(150);
let _flip_res = client
.call(&ink_e2e::bob(), &flip)
.submit()
.await
.expect("update failed");

// then
let get = call.get();
let get_res = client.call(&ink_e2e::bob(), &get).dry_run().await?;
assert!(matches!(get_res.return_value(), 0));

Ok(())
}
}
}