Skip to content

Returning error codes from init and receive functions #22

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

Merged
merged 22 commits into from
Mar 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
fb68a97
Adding support for returning custom error codes from init and receive…
amaurremi Mar 1, 2021
5984c38
Remove todo.
amaurremi Mar 1, 2021
6596b58
Formatting.
amaurremi Mar 1, 2021
53804fd
Putting error-code limit handling in the right place.
amaurremi Mar 1, 2021
1e5aad6
Adding auction example to linger. Using concordium_std instead of std.
amaurremi Mar 2, 2021
117627c
Spacing.
amaurremi Mar 2, 2021
b0fde92
Only derive Reject for enums.
amaurremi Mar 2, 2021
01c9f4f
Partial progress on adding field support for enums
amaurremi Mar 2, 2021
2a22782
Revert "Partial progress on adding field support for enums"
amaurremi Mar 2, 2021
a288d60
Wrapping existing errors into enum variants for enums that are return…
amaurremi Mar 4, 2021
3983e30
Update dependency
amaurremi Mar 4, 2021
604774f
Bump concordium-contracts-common version
amaurremi Mar 4, 2021
4ea625b
Adding 1 to error codes.
amaurremi Mar 4, 2021
6753c7b
Undoing making reserved-error-codes constant public.
amaurremi Mar 4, 2021
471e82b
Minor changes.
amaurremi Mar 4, 2021
8e551ae
Moving reserved error codes to lowest i32 numbers.
amaurremi Mar 10, 2021
6681adc
Don't require deriving Copy when deriving Reject.
amaurremi Mar 10, 2021
5a7a632
Formatting, inline, and clippy.
amaurremi Mar 10, 2021
467aa3b
Adjusting reject codes for built-in error types.
amaurremi Mar 10, 2021
20dffe7
Fix some minor issues.
abizjak Mar 10, 2021
f430d1c
Error codes are always negative.
abizjak Mar 11, 2021
9fce72f
Bump submodule version.
abizjak Mar 11, 2021
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
2 changes: 2 additions & 0 deletions .github/workflows/linter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ jobs:
- examples/two-step-transfer/Cargo.toml
- examples/piggy-bank/part1/Cargo.toml
- examples/piggy-bank/part2/Cargo.toml
- examples/auction/Cargo.toml

steps:
- name: Checkout sources
Expand Down Expand Up @@ -202,6 +203,7 @@ jobs:
- examples/two-step-transfer/Cargo.toml
- examples/piggy-bank/part1/Cargo.toml
- examples/piggy-bank/part2/Cargo.toml
- examples/auction/Cargo.toml

steps:
- name: Checkout sources
Expand Down
2 changes: 1 addition & 1 deletion concordium-contracts-common
221 changes: 215 additions & 6 deletions concordium-std-derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ extern crate quote;
use proc_macro::TokenStream;
use proc_macro2::Span;
use quote::ToTokens;
use syn::{parse::Parser, parse_macro_input, punctuated::*, spanned::Spanned, Ident, Meta, Token};
use std::{convert::TryFrom, ops::Neg};
use syn::{
parse::Parser, parse_macro_input, punctuated::*, spanned::Spanned, DataEnum, Ident, Meta, Token,
};

/// A helper to report meaningful compilation errors
/// - If applied to an Ok value they simply return the underlying value.
Expand Down Expand Up @@ -166,7 +169,7 @@ fn init_worker(attr: TokenStream, item: TokenStream) -> syn::Result<TokenStream>
let contract_name = get_attribute_value(attrs.iter(), "contract")?.ok_or_else(|| {
syn::Error::new(
attrs.span(),
"A name for the contract must be provided, using the contract attribute.For example, \
"A name for the contract must be provided, using the contract attribute. For example, \
#[init(contract = \"my-contract\")]",
)
})?;
Expand Down Expand Up @@ -198,7 +201,14 @@ fn init_worker(attr: TokenStream, item: TokenStream) -> syn::Result<TokenStream>
let mut state = ContractState::open(());
match #fn_name(&ctx, #(#fn_optional_args, )* &mut state) {
Ok(()) => 0,
Err(_) => -1,
Err(reject) => {
let code = Reject::from(reject).error_code.get();
if code < 0 {
code
} else {
trap() // precondition violation
}
}
}
}
}
Expand All @@ -217,7 +227,14 @@ fn init_worker(attr: TokenStream, item: TokenStream) -> syn::Result<TokenStream>
};
0
}
Err(_) => -1
Err(reject) => {
let code = Reject::from(reject).error_code.get();
if code < 0 {
code
} else {
trap() // precondition violation
}
}
}
}
}
Expand Down Expand Up @@ -384,7 +401,14 @@ fn receive_worker(attr: TokenStream, item: TokenStream) -> syn::Result<TokenStre
Ok(act) => {
act.tag() as i32
}
Err(_) => -1,
Err(reject) => {
let code = Reject::from(reject).error_code.get();
if code < 0 {
code
} else {
trap() // precondition violation
}
}
}
}
}
Expand All @@ -411,7 +435,14 @@ fn receive_worker(attr: TokenStream, item: TokenStream) -> syn::Result<TokenStre
act.tag() as i32
}
}
Err(_) => -1,
Err(reject) => {
let code = Reject::from(reject).error_code.get();
if code < 0 {
code
} else {
trap() // precondition violation
}
}
}
} else {
trap() // Could not fully read state.
Expand Down Expand Up @@ -1202,6 +1233,184 @@ fn schema_type_fields(fields: &syn::Fields) -> syn::Result<proc_macro2::TokenStr
}
}

/// We reserve a number of error codes for custom errors, such as ParseError,
/// that are provided by concordium-std. These reserved error codes can have
/// indices i32::MIN, i32::MIN + 1, ..., RESERVED_ERROR_CODES
const RESERVED_ERROR_CODES: i32 = i32::MIN + 100;

/// Derive the conversion of enums that represent error types into the Reject
/// struct which can be used as the error type of init and receive functions.
/// Creating custom enums for error types can provide meaningful error messages
/// to the user of the smart contract.
///
/// Note that at the moment, we can only derive fieldless enums.
///
/// The conversion will map the first variant to error code -1, second to -2,
/// etc.
///
/// ### Example
/// ```ignore
/// #[derive(Clone, Copy, Reject)]
/// enum MyError {
/// IllegalState, // receives error code -1
/// WrongSender, // receives error code -2
/// // TimeExpired(time: Timestamp), /* currently not supported */
/// ...
/// }
/// ```
/// ```ignore
/// #[receive(contract = "my_contract", name = "some_receive")]
/// fn receive<A: HasActions>(ctx: &impl HasReceiveContext, state: &mut MyState)
/// -> Result<A, MyError> {...}
/// ```
#[proc_macro_derive(Reject, attributes(from))]
pub fn reject_derive(input: TokenStream) -> TokenStream {
unwrap_or_report(reject_derive_worker(input))
}

fn reject_derive_worker(input: TokenStream) -> syn::Result<TokenStream> {
let ast: syn::DeriveInput = syn::parse(input)?;
let enum_data = match &ast.data {
syn::Data::Enum(data) => Ok(data),
_ => Err(syn::Error::new(ast.span(), "Reject can only be derived for enums.")),
}?;
let enum_ident = &ast.ident;

// Ensure that the number of enum variants fits into the number of error codes
// we can generate.
let too_many_variants = format!(
"Error enum {} cannot have more than {} variants.",
enum_ident,
RESERVED_ERROR_CODES.neg()
);
match i32::try_from(enum_data.variants.len()) {
Ok(n) if n <= RESERVED_ERROR_CODES.neg() => (),
_ => {
return Err(syn::Error::new(ast.span(), &too_many_variants));
}
};

let variant_error_conversions = generate_variant_error_conversions(&enum_data, &enum_ident)?;

let gen = quote! {
/// The from implementation maps the first variant to -1, second to -2, etc.
/// NB: This differs from the cast `variant as i32` since we cannot easily modify
/// the variant tags in the derive macro itself.
#[automatically_derived]
impl From<#enum_ident> for Reject {
#[inline(always)]
fn from(e: #enum_ident) -> Self {
Reject { error_code: unsafe { concordium_std::num::NonZeroI32::new_unchecked(-(e as i32) - 1) } }
}
}

#(#variant_error_conversions)*
};
Ok(gen.into())
}

/// Generate error conversions for enum variants e.g. for converting
/// `ParseError` to `MyParseErrorWrapper` in
///
/// ```ignore
/// enum MyErrorType {
/// #[from(ParseError)]
/// MyParseErrorWrapper,
/// ...
/// }
/// ```
fn generate_variant_error_conversions(
enum_data: &DataEnum,
enum_name: &syn::Ident,
) -> syn::Result<Vec<proc_macro2::TokenStream>> {
Ok(enum_data
.variants
.iter()
.map(|variant| {
// in the future we might incorporate explicit discriminants,
// but the general case of this requires evaluating constant expressions,
// which is not easily supported at the moment.
if let Some((_, discriminant)) = variant.discriminant.as_ref() {
return Err(syn::Error::new(
discriminant.span(),
"Explicit discriminants are not yet supported.",
));
}
let variant_attributes = variant.attrs.iter();
variant_attributes
.map(move |attr| {
parse_attr_and_gen_error_conversions(attr, enum_name, &variant.ident)
})
.collect::<syn::Result<Vec<_>>>()
})
.collect::<syn::Result<Vec<_>>>()?
.into_iter()
.flatten()
.flatten()
.collect())
}

/// Generate error conversion for a given enum variant.
fn parse_attr_and_gen_error_conversions(
attr: &syn::Attribute,
enum_name: &syn::Ident,
variant_name: &syn::Ident,
) -> syn::Result<Vec<proc_macro2::TokenStream>> {
let wrong_from_usage = |x: &dyn Spanned| {
syn::Error::new(
x.span(),
"The `from` attribute expects a list of error types, e.g.: #[from(ParseError)].",
)
};
match attr.parse_meta() {
Ok(syn::Meta::List(list)) if list.path.is_ident("from") => {
let mut from_error_names = vec![];
for nested in list.nested.iter() {
// check that all items in the list are paths
match nested {
syn::NestedMeta::Meta(meta) => match meta {
Meta::Path(from_error) => {
let ident = from_error
.get_ident()
.ok_or_else(|| wrong_from_usage(from_error))?;
from_error_names.push(ident);
}
other => return Err(wrong_from_usage(&other)),
},
syn::NestedMeta::Lit(l) => return Err(wrong_from_usage(&l)),
}
}
Ok(from_error_token_stream(&from_error_names, &enum_name, variant_name).collect())
}
Ok(syn::Meta::NameValue(mnv)) if mnv.path.is_ident("from") => Err(wrong_from_usage(&mnv)),
_ => Ok(vec![]),
}
}

/// Generating the conversion code a la
/// ```ignore
/// impl From<ParseError> for MyErrorType {
/// fn from(x: ParseError) -> Self {
/// MyError::MyParseErrorWrapper
/// }
/// }
/// ```
fn from_error_token_stream<'a>(
paths: &'a [&'a syn::Ident],
enum_name: &'a syn::Ident,
variant_name: &'a syn::Ident,
) -> impl Iterator<Item = proc_macro2::TokenStream> + 'a {
paths.iter().map(move |from_error| {
quote! {
impl From<#from_error> for #enum_name {
#[inline]
fn from(fe: #from_error) -> Self {
#enum_name::#variant_name
}
}}
})
}

#[proc_macro_attribute]
/// Derive the appropriate export for an annotated test function, when feature
/// "wasm-test" is enabled, otherwise behaves like `#[test]`.
Expand Down
3 changes: 3 additions & 0 deletions concordium-std/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
to be consistent with the Write implementation for ContractState.
- Use little-endian encoding for sender contract addresses in receive contexts. This
reverts the change in concordium-std 0.4.1.
- Allow init and receive methods to return custom error codes that will be displayed to the user
if a smart-contract invocation fails.
- Add i128 and u128 support to serialization and schema.

## concordium-std 0.4.1 (2021-02-22)

Expand Down
2 changes: 1 addition & 1 deletion concordium-std/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ version = "=0.5"

[dependencies.concordium-contracts-common]
path = "../concordium-contracts-common"
version = "=0.3.1"
version = "=0.3.2"
default-features = false

[features]
Expand Down
14 changes: 11 additions & 3 deletions concordium-std/src/impls.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
use crate::{convert, mem, prims::*, traits::*, types::*};
use crate::{convert, mem, num, prims::*, traits::*, types::*};
use concordium_contracts_common::*;

use mem::MaybeUninit;

impl convert::From<()> for Reject {
#[inline(always)]
fn from(_: ()) -> Self { Reject {} }
fn from(_: ()) -> Self {
Reject {
error_code: unsafe { num::NonZeroI32::new_unchecked(i32::MIN + 1) },
}
}
}

impl convert::From<ParseError> for Reject {
#[inline(always)]
fn from(_: ParseError) -> Self { Reject {} }
fn from(_: ParseError) -> Self {
Reject {
error_code: unsafe { num::NonZeroI32::new_unchecked(i32::MIN + 2) },
}
}
}

/// # Contract state trait implementations.
Expand Down
30 changes: 3 additions & 27 deletions concordium-std/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,42 +150,18 @@ fn abort_panic(_info: &core::panic::PanicInfo) -> ! {

// Provide some re-exports to make it easier to use the library.
// This should be expanded in the future.
#[cfg(not(feature = "std"))]
pub use core::result::*;

/// Re-export.
#[cfg(not(feature = "std"))]
pub use alloc::collections;
/// Re-export.
#[cfg(not(feature = "std"))]
pub use alloc::{string, string::String, string::ToString, vec, vec::Vec};
/// Re-export.
#[cfg(not(feature = "std"))]
pub use core::convert;
/// Re-export.
#[cfg(not(feature = "std"))]
pub use core::marker;
pub use alloc::{collections, string, string::String, string::ToString, vec, vec::Vec};
/// Re-export.
#[cfg(not(feature = "std"))]
pub use core::mem;
pub use core::{convert, marker, mem, num, result::*};
#[cfg(feature = "std")]
pub(crate) use std::vec;
#[cfg(feature = "std")]
pub use std::vec::Vec;

/// Re-export.
#[cfg(feature = "std")]
pub use std::collections;
/// Re-export.
#[cfg(feature = "std")]
pub use std::convert;
#[cfg(feature = "std")]
pub use std::marker;
/// Re-export.
#[cfg(feature = "std")]
pub use std::mem;
#[cfg(feature = "std")]
pub use std::string::String;
pub use std::{collections, convert, marker, mem, num, string::String, vec::Vec};

/// Chain constants that impose limits on various aspects of smart contract
/// execution.
Expand Down
Loading