From 614d96f795e835fd8c4a93f22f90b4873071260c Mon Sep 17 00:00:00 2001 From: igor-casper Date: Wed, 10 Dec 2025 04:56:23 +0100 Subject: [PATCH 1/3] resolve abi convention --- smart_contracts/vm2/macros/src/lib.rs | 84 +++++++++++++++++++++++++-- 1 file changed, 78 insertions(+), 6 deletions(-) diff --git a/smart_contracts/vm2/macros/src/lib.rs b/smart_contracts/vm2/macros/src/lib.rs index 8d44a33933..09746e30dc 100644 --- a/smart_contracts/vm2/macros/src/lib.rs +++ b/smart_contracts/vm2/macros/src/lib.rs @@ -67,6 +67,12 @@ enum ItemFnMeta { Export, } +#[derive(Debug, FromMeta)] +struct FnCommonMeta { + #[darling(default)] + abi_convention: Option, +} + #[derive(Debug, FromMeta)] struct ImplTraitForContractMeta { /// Fully qualified path of the trait. @@ -144,8 +150,9 @@ pub fn casper(attrs: TokenStream, item: TokenStream) -> TokenStream { } } else if let Ok(func) = syn::parse::(item.clone()) { let func_meta = ItemFnMeta::from_list(&attr_args).unwrap(); + let fn_common = FnCommonMeta::from_list(&attr_args).unwrap(); match func_meta { - ItemFnMeta::Export => generate_export_function(&func), + ItemFnMeta::Export => generate_export_function(&func, fn_common.abi_convention), } } else { let err = syn::Error::new( @@ -248,7 +255,7 @@ fn process_casper_message_for_struct( .into() } -fn generate_export_function(func: &ItemFn) -> TokenStream { +fn generate_export_function(func: &ItemFn, abi_convention: Option) -> TokenStream { let func_name = &func.sig.ident; let mut arg_names = Vec::new(); let mut arg_types = Vec::new(); @@ -271,6 +278,42 @@ fn generate_export_function(func: &ItemFn) -> TokenStream { syn::ReturnType::Type(_, ty) => quote! { #ty }, }; + let resolve_abi_convention = match abi_convention { + Some(convention_path) => quote! { #convention_path }, + None => quote! { casper_contract_sdk::serializers::AbiConvention::Positional }, + }; + + // Generate return handling tokens + let handle_ret = match &func.sig.output { + syn::ReturnType::Default => { + quote! { + match resolved_abi_convention { + casper_contract_sdk::serializers::AbiConvention::Positional => { + casper_contract_sdk::casper::ret(flags, None) + } + casper_contract_sdk::serializers::AbiConvention::Named => { + let ret_bytes = casper_contract_sdk::serializers::borsh::to_vec(&casper_contract_sdk::compat::types::CLValue::UNIT).expect("Failed to serialize return CLValue"); + casper_contract_sdk::casper::ret(flags, Some(&ret_bytes)) + } + } + } + } + syn::ReturnType::Type(_, _ty) => { + quote! { + let ret_bytes = match resolved_abi_convention { + casper_contract_sdk::serializers::AbiConvention::Positional => { + casper_contract_sdk::serializers::borsh::to_vec(&_ret).expect("Failed to serialize return value") + } + casper_contract_sdk::serializers::AbiConvention::Named => { + let ret_clvalue = casper_contract_sdk::compat::types::CLValue::from_t(&_ret).expect("Failed to convert return value to CLValue"); + casper_contract_sdk::serializers::borsh::to_vec(&ret_clvalue).expect("Failed to serialize return CLValue") + } + }; + casper_contract_sdk::casper::ret(flags, Some(&ret_bytes)) + } + } + }; + let _ctor_name = format_ident!("{func_name}_ctor"); let exported_func_name = format_ident!("__casper_export_{func_name}"); @@ -288,9 +331,38 @@ fn generate_export_function(func: &ItemFn) -> TokenStream { struct Arguments { #(#arg_names: #arg_types,)* } + + let mut flags = casper_contract_sdk::common::flags::ReturnFlags::empty(); let input = casper_contract_sdk::prelude::casper::copy_input(); - let args: Arguments = casper_contract_sdk::serializers::borsh::from_slice(&input).unwrap(); + let resolved_abi_convention = #resolve_abi_convention; + let args: Arguments = { + match resolved_abi_convention { + casper_contract_sdk::serializers::AbiConvention::Positional => { + casper_contract_sdk::serializers::borsh::from_slice(&input).unwrap() + } + casper_contract_sdk::serializers::AbiConvention::Named => { + let runtime_args: casper_contract_sdk::compat::types::RuntimeArgs = + casper_contract_sdk::serializers::borsh::from_slice(&input).unwrap(); + #( + let #arg_names: #arg_types = { + let cl_value = runtime_args.get(stringify!(#arg_names)).unwrap_or_else(|| panic!(concat!("Failed to get named argument \"", stringify!(#arg_names), "\""))); + cl_value.to_t::<#arg_types>().unwrap_or_else(|error| { + panic!(concat!("Failed to convert named argument \"", stringify!(#arg_names), "\": {}"), error) + }) + }; + )* + Arguments { + #( + #arg_names, + )* + } + } + } + }; + let _ret = #func_name(#(args.#arg_names,)*); + + #handle_ret } #[cfg(not(target_arch = "wasm32"))] @@ -324,7 +396,7 @@ fn generate_export_function(func: &ItemFn) -> TokenStream { }, )* ], - abi_convention: casper_contract_sdk::serializers::AbiConvention::Positional, // todo + abi_convention: #resolve_abi_convention, result_decl: { casper_contract_sdk::abi::collector::AbiType { type_name: core::any::type_name::<#ret>, @@ -511,7 +583,7 @@ fn generate_impl_for_contract(mut entry_points: ItemImpl) -> TokenStream { Some(quote! { match #resolve_abi_convention { casper_contract_sdk::serializers::AbiConvention::Positional => { - // Do nothing as lack of ret is synonymous with returning empty bytes (unit serializes to empty buffer) + casper_contract_sdk::casper::ret(flags, None) } casper_contract_sdk::serializers::AbiConvention::Named => { // For a named ABI convention we'd always ret with the bytes of unit CLValue. @@ -1204,7 +1276,7 @@ fn casper_trait_definition(mut item_trait: ItemTrait, trait_meta: TraitMeta) -> Some(quote! { match #resolve_abi_convention { casper_contract_sdk::serializers::AbiConvention::Positional => { - // Do nothing as lack of ret is synonymous with returning empty bytes (unit serializes to empty buffer) + casper_contract_sdk::casper::ret(flags, None) } casper_contract_sdk::serializers::AbiConvention::Named => { // For a named ABI convention we'd always ret with the bytes of unit CLValue. From 3d1bc6835d728d4f3a9dc92e554f35f7fbe50f3d Mon Sep 17 00:00:00 2001 From: igor-casper Date: Wed, 10 Dec 2025 16:49:37 +0100 Subject: [PATCH 2/3] add tests --- .../contracts/vm2/vm2-named-args/src/lib.rs | 80 +++++++++++++++++++ smart_contracts/vm2/macros/src/lib.rs | 40 +++++++--- 2 files changed, 107 insertions(+), 13 deletions(-) diff --git a/smart_contracts/contracts/vm2/vm2-named-args/src/lib.rs b/smart_contracts/contracts/vm2/vm2-named-args/src/lib.rs index 7e645c52dc..70b5c3a7a2 100644 --- a/smart_contracts/contracts/vm2/vm2-named-args/src/lib.rs +++ b/smart_contracts/contracts/vm2/vm2-named-args/src/lib.rs @@ -39,11 +39,33 @@ impl Contract { pub fn args_with_overriden_abi_convention(a: u32, b: u32, c: u32) -> Vec { vec![a, b, c] } + + // Explicit Positional ABI with unit return + #[casper(abi_convention = AbiConvention::Positional)] + pub fn positional_unit_no_args() { + } } #[casper] impl ContractTrait for Contract {} +// Default (Positional) export with a value return. +#[casper(export)] +pub fn positional_export_inc(x: u32) -> u32 { + x + 1 +} + +// Default (Positional) export with unit return. +#[casper(export)] +pub fn positional_export_no_args_unit() { +} + +// Named export +#[casper(export, abi_convention = AbiConvention::Named)] +pub fn named_export_add(a: u32, b: u32) -> u32 { + a + b +} + #[cfg(test)] mod tests { use std::sync::Arc; @@ -242,4 +264,62 @@ mod tests { .expect("Expected entry point"); assert_eq!(e2.abi_convention, AbiConvention::Named); } + + #[test] + fn test_positional_impl_unit_ret_calls_ret_none() { + let env = Arc::new(EnvironmentMock::new()); + env.add_expectation(ExpectedCall::expect_copy_input(&[])); + env.add_expectation(ExpectedCall::expect_get_info(Some(EnvInfo::default()))); + env.add_expectation(ExpectedCall::expect_return(Some((0, None)), HOST_ERROR_SUCCESS)); + with_env(env.clone(), || { + let _ = run_expecting_panic(|| __casper_export_positional_unit_no_args()); + }); + } + + #[test] + fn test_positional_export_value_ret() { + let env = Arc::new(EnvironmentMock::new()); + + let input_bytes = borsh::to_vec(&(7u32,)).expect("borsh"); + env.add_expectation(ExpectedCall::expect_copy_input(&input_bytes)); + + let expected_ret = borsh::to_vec(&8u32).expect("borsh"); + env.add_expectation(ExpectedCall::expect_return( + Some((0, Some(expected_ret))), + HOST_ERROR_SUCCESS, + )); + with_env(env.clone(), || { + let _ = run_expecting_panic(|| __casper_export_positional_export_inc()); + }); + } + + #[test] + fn test_named_export_value_ret() { + let env = Arc::new(EnvironmentMock::new()); + let mut runtime_args = RuntimeArgs::new(); + runtime_args.insert("a", 2u32).unwrap(); + runtime_args.insert("b", 3u32).unwrap(); + let input_bytes = borsh::to_vec(&runtime_args).expect("borsh"); + env.add_expectation(ExpectedCall::expect_copy_input(&input_bytes)); + // Named returns CLValue-encoded value + let ret_clvalue = CLValue::from_t(&5u32).expect("clvalue"); + let expected_ret = borsh::to_vec(&ret_clvalue).expect("borsh"); + env.add_expectation(ExpectedCall::expect_return( + Some((0, Some(expected_ret))), + HOST_ERROR_SUCCESS, + )); + with_env(env.clone(), || { + let _ = run_expecting_panic(|| __casper_export_named_export_add()); + }); + } + + #[test] + fn test_positional_export_unit_ret_calls_ret_none() { + let env = Arc::new(EnvironmentMock::new()); + env.add_expectation(ExpectedCall::expect_copy_input(&[])); + env.add_expectation(ExpectedCall::expect_return(Some((0, None)), HOST_ERROR_SUCCESS)); + with_env(env.clone(), || { + let _ = run_expecting_panic(|| __casper_export_positional_export_no_args_unit()); + }); + } } diff --git a/smart_contracts/vm2/macros/src/lib.rs b/smart_contracts/vm2/macros/src/lib.rs index 09746e30dc..0d83db561a 100644 --- a/smart_contracts/vm2/macros/src/lib.rs +++ b/smart_contracts/vm2/macros/src/lib.rs @@ -62,16 +62,7 @@ struct TraitMeta { abi_convention: Option, } -#[derive(Debug, FromMeta)] -enum ItemFnMeta { - Export, -} -#[derive(Debug, FromMeta)] -struct FnCommonMeta { - #[darling(default)] - abi_convention: Option, -} #[derive(Debug, FromMeta)] struct ImplTraitForContractMeta { @@ -149,10 +140,33 @@ pub fn casper(attrs: TokenStream, item: TokenStream) -> TokenStream { generate_impl_for_contract(entry_points) } } else if let Ok(func) = syn::parse::(item.clone()) { - let func_meta = ItemFnMeta::from_list(&attr_args).unwrap(); - let fn_common = FnCommonMeta::from_list(&attr_args).unwrap(); - match func_meta { - ItemFnMeta::Export => generate_export_function(&func, fn_common.abi_convention), + let mut is_export = false; + let mut abi_convention: Option = None; + for meta in &attr_args { + match meta { + ast::NestedMeta::Meta(syn::Meta::Path(path)) => { + if path.is_ident("export") { + is_export = true; + } + } + ast::NestedMeta::Meta(syn::Meta::NameValue(nv)) => { + if nv.path.is_ident("abi_convention") { + if let syn::Expr::Path(expr_path) = &nv.value { + abi_convention = Some(expr_path.path.clone()); + } + } + } + _ => {} + } + } + if is_export { + generate_export_function(&func, abi_convention) + } else { + let err = syn::Error::new( + Span::call_site(), + "Unsupported function attribute; expected #[casper(export ...)]", + ); + TokenStream::from(err.to_compile_error()) } } else { let err = syn::Error::new( From ffa9e37fd11351c7174c569fd3d6eb27e41c853d Mon Sep 17 00:00:00 2001 From: igor-casper Date: Wed, 10 Dec 2025 17:09:57 +0100 Subject: [PATCH 3/3] lint, fmt --- .../contracts/vm2/vm2-named-args/src/lib.rs | 16 ++++++++++------ smart_contracts/vm2/macros/src/lib.rs | 2 -- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/smart_contracts/contracts/vm2/vm2-named-args/src/lib.rs b/smart_contracts/contracts/vm2/vm2-named-args/src/lib.rs index 70b5c3a7a2..5ea10859f6 100644 --- a/smart_contracts/contracts/vm2/vm2-named-args/src/lib.rs +++ b/smart_contracts/contracts/vm2/vm2-named-args/src/lib.rs @@ -42,8 +42,7 @@ impl Contract { // Explicit Positional ABI with unit return #[casper(abi_convention = AbiConvention::Positional)] - pub fn positional_unit_no_args() { - } + pub fn positional_unit_no_args() {} } #[casper] @@ -57,8 +56,7 @@ pub fn positional_export_inc(x: u32) -> u32 { // Default (Positional) export with unit return. #[casper(export)] -pub fn positional_export_no_args_unit() { -} +pub fn positional_export_no_args_unit() {} // Named export #[casper(export, abi_convention = AbiConvention::Named)] @@ -270,7 +268,10 @@ mod tests { let env = Arc::new(EnvironmentMock::new()); env.add_expectation(ExpectedCall::expect_copy_input(&[])); env.add_expectation(ExpectedCall::expect_get_info(Some(EnvInfo::default()))); - env.add_expectation(ExpectedCall::expect_return(Some((0, None)), HOST_ERROR_SUCCESS)); + env.add_expectation(ExpectedCall::expect_return( + Some((0, None)), + HOST_ERROR_SUCCESS, + )); with_env(env.clone(), || { let _ = run_expecting_panic(|| __casper_export_positional_unit_no_args()); }); @@ -317,7 +318,10 @@ mod tests { fn test_positional_export_unit_ret_calls_ret_none() { let env = Arc::new(EnvironmentMock::new()); env.add_expectation(ExpectedCall::expect_copy_input(&[])); - env.add_expectation(ExpectedCall::expect_return(Some((0, None)), HOST_ERROR_SUCCESS)); + env.add_expectation(ExpectedCall::expect_return( + Some((0, None)), + HOST_ERROR_SUCCESS, + )); with_env(env.clone(), || { let _ = run_expecting_panic(|| __casper_export_positional_export_no_args_unit()); }); diff --git a/smart_contracts/vm2/macros/src/lib.rs b/smart_contracts/vm2/macros/src/lib.rs index 0d83db561a..e54711cb0b 100644 --- a/smart_contracts/vm2/macros/src/lib.rs +++ b/smart_contracts/vm2/macros/src/lib.rs @@ -62,8 +62,6 @@ struct TraitMeta { abi_convention: Option, } - - #[derive(Debug, FromMeta)] struct ImplTraitForContractMeta { /// Fully qualified path of the trait.