From 2042862fcb6155757170421059a69179e7da090e Mon Sep 17 00:00:00 2001 From: tritao Date: Fri, 7 Nov 2025 08:54:47 +0000 Subject: [PATCH] Add support for ABI alias types --- Cargo.toml | 3 +- e2e/sway/abi/contract_with_alias/Forc.toml | 5 ++ e2e/sway/abi/contract_with_alias/src/main.sw | 26 +++++++ e2e/sway/contracts/alias/Forc.toml | 5 ++ e2e/sway/contracts/alias/src/main.sw | 20 ++++++ e2e/tests/alias.rs | 30 ++++++++ examples/codegen/Cargo.toml | 15 ++++ examples/codegen/src/lib.rs | 22 ++++++ .../src/program_bindings/abigen.rs | 23 ++++++ .../src/program_bindings/custom_types.rs | 72 +++++++++++++++++-- .../program_bindings/custom_types/utils.rs | 4 +- .../src/program_bindings/resolved_type.rs | 48 ++++++++++++- .../param_types/from_type_application.rs | 4 +- packages/fuels-macros/src/lib.rs | 34 ++++++++- packages/fuels/Cargo.toml | 1 + packages/fuels/src/lib.rs | 4 ++ 16 files changed, 298 insertions(+), 18 deletions(-) create mode 100644 e2e/sway/abi/contract_with_alias/Forc.toml create mode 100644 e2e/sway/abi/contract_with_alias/src/main.sw create mode 100644 e2e/sway/contracts/alias/Forc.toml create mode 100644 e2e/sway/contracts/alias/src/main.sw create mode 100644 e2e/tests/alias.rs create mode 100644 examples/codegen/Cargo.toml create mode 100644 examples/codegen/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index c863b419b8..bda3ff7928 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ resolver = "2" members = [ "e2e", "examples/codec", + "examples/codegen", "examples/contracts", "examples/cookbook", "examples/debugging", @@ -52,7 +53,7 @@ cynic = { version = "3.1.0", default-features = false } test-case = { version = "3.3", default-features = false } eth-keystore = "0.5.0" flate2 = { version = "1.0", default-features = false } -fuel-abi-types = "0.15.3" +fuel-abi-types = "0.16.0" futures = "0.3.29" hex = { version = "0.4.3", default-features = false } itertools = "0.12.0" diff --git a/e2e/sway/abi/contract_with_alias/Forc.toml b/e2e/sway/abi/contract_with_alias/Forc.toml new file mode 100644 index 0000000000..9a686002fe --- /dev/null +++ b/e2e/sway/abi/contract_with_alias/Forc.toml @@ -0,0 +1,5 @@ +[project] +authors = ["Fuel Labs "] +entry = "main.sw" +license = "Apache-2.0" +name = "contract_with_alias" diff --git a/e2e/sway/abi/contract_with_alias/src/main.sw b/e2e/sway/abi/contract_with_alias/src/main.sw new file mode 100644 index 0000000000..fdf5f1ba66 --- /dev/null +++ b/e2e/sway/abi/contract_with_alias/src/main.sw @@ -0,0 +1,26 @@ +contract; + +pub type MyAlias = Vec; +pub type MyU64 = u64; +pub type MyTuple = (MyU64, MyU64); +pub type MyArray = [MyTuple; 2]; + +abi MyContract { + fn with_b256(b: b256) -> b256; + fn with_myalias_vec() -> MyAlias; + fn with_mytuple() -> MyTuple; +} + +impl MyContract for Contract { + fn with_b256(b: b256) -> b256 { + b256::zero() + } + + fn with_myalias_vec() -> MyAlias { + MyAlias::new() + } + + fn with_mytuple() -> MyTuple { + (32, 64) + } +} diff --git a/e2e/sway/contracts/alias/Forc.toml b/e2e/sway/contracts/alias/Forc.toml new file mode 100644 index 0000000000..24cb4a6fd8 --- /dev/null +++ b/e2e/sway/contracts/alias/Forc.toml @@ -0,0 +1,5 @@ +[project] +authors = ["Fuel Labs "] +entry = "main.sw" +license = "Apache-2.0" +name = "alias" diff --git a/e2e/sway/contracts/alias/src/main.sw b/e2e/sway/contracts/alias/src/main.sw new file mode 100644 index 0000000000..52d11d2286 --- /dev/null +++ b/e2e/sway/contracts/alias/src/main.sw @@ -0,0 +1,20 @@ +contract; + +pub type MyAlias = b256; +pub type MyU64 = u64; +pub type MyTuple = (MyU64, MyU64); + +abi MyContract { + fn with_myalias(b: MyAlias) -> MyAlias; + fn with_mytuple() -> MyTuple; +} + +impl MyContract for Contract { + fn with_myalias(b: MyAlias) -> MyAlias { + b256::zero() + } + + fn with_mytuple() -> MyTuple { + (32, 64) + } +} diff --git a/e2e/tests/alias.rs b/e2e/tests/alias.rs new file mode 100644 index 0000000000..3b51244a9c --- /dev/null +++ b/e2e/tests/alias.rs @@ -0,0 +1,30 @@ +use fuels::prelude::*; + +#[tokio::test] +async fn test_alias() -> Result<()> { + setup_program_test!( + Wallets("wallet"), + Abigen(Contract( + name = "MyContract", + project = "e2e/sway/contracts/alias" + )), + Deploy( + name = "contract_instance", + contract = "MyContract", + wallet = "wallet", + random_salt = false, + ), + ); + + // Make sure we can call the contract with multiple arguments + let contract_methods = contract_instance.methods(); + use abigen_bindings::my_contract_mod::MyAlias; + let response = contract_methods + .with_myalias(MyAlias::zeroed()) + .call() + .await?; + + assert_eq!(response.value, MyAlias::zeroed()); + + Ok(()) +} diff --git a/examples/codegen/Cargo.toml b/examples/codegen/Cargo.toml new file mode 100644 index 0000000000..9aaa60734e --- /dev/null +++ b/examples/codegen/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "fuels-example-codegen" +version = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +publish = false +repository = { workspace = true } +description = "Fuel Rust SDK codegen examples." + +[dev-dependencies] +fuels = { workspace = true, features = ["default"] } +fuels-code-gen = { workspace = true } +tokio = { workspace = true, features = ["full"] } diff --git a/examples/codegen/src/lib.rs b/examples/codegen/src/lib.rs new file mode 100644 index 0000000000..738c8c049a --- /dev/null +++ b/examples/codegen/src/lib.rs @@ -0,0 +1,22 @@ +extern crate alloc; + +#[cfg(test)] +mod tests { + #[test] + fn example_alias() { + use fuels::code_gen::*; + + let target = AbigenTarget::new( + "MyContract".into(), + Abi::load_from( + "/home/joao/dev/fuels-rs/e2e/sway/contracts/alias/out/release/alias-abi.json", + ) + // Abi::load_from("/home/joao/dev/sway/_test_aliases_abi/out/debug/test-case-abi.json") + .unwrap(), + ProgramType::Contract, + ); + let targets = vec![target]; + + let _abigen = Abigen::generate(targets, false).expect("abigen generation failed"); + } +} diff --git a/packages/fuels-code-gen/src/program_bindings/abigen.rs b/packages/fuels-code-gen/src/program_bindings/abigen.rs index e4fccbdbda..8972839ab8 100644 --- a/packages/fuels-code-gen/src/program_bindings/abigen.rs +++ b/packages/fuels-code-gen/src/program_bindings/abigen.rs @@ -34,8 +34,15 @@ impl Abigen { /// for, and of what nature (Contract, Script or Predicate). /// * `no_std`: don't use the Rust std library. pub fn generate(targets: Vec, no_std: bool) -> Result { + // eprintln!("{:#?}", targets); + let generated_code = Self::generate_code(no_std, targets)?; + // eprintln!( + // "========================== CODE: \n {:#}", + // generated_code.code() + // ); + let use_statements = generated_code.use_statements_for_uniquely_named_types(); let code = if no_std { @@ -73,9 +80,12 @@ impl Abigen { .iter() .flat_map(|abi| abi.source.abi.logged_types.clone()) .collect_vec(); + let bindings = Self::generate_all_bindings(parsed_targets, no_std, &shared_types)?; + // eprintln!("bindings {:#}", bindings.code().to_string()); let shared_types = Self::generate_shared_types(shared_types, &logged_types, no_std)?; + eprintln!("shared_types {:#}", shared_types.code().to_string()); let mod_name = ident("abigen_bindings"); Ok(shared_types.merge(bindings).wrap_in_mod(mod_name)) @@ -99,16 +109,20 @@ impl Abigen { no_std: bool, shared_types: &HashSet, ) -> Result { + //eprintln!("generate_bindings shared_types {:#?}", shared_types); let mod_name = ident(&format!("{}_mod", &target.name.to_snake_case())); let recompile_trigger = Self::generate_macro_recompile_trigger(target.source.path.as_ref(), no_std); + let types = generate_types( &target.source.abi.types, shared_types, &target.source.abi.logged_types, no_std, )?; + //eprintln!("generate_bindings types {:#?}", types); + let bindings = generate_bindings(target, no_std)?; Ok(recompile_trigger .merge(types) @@ -156,6 +170,15 @@ impl Abigen { .filter(|ttype| ttype.is_custom_type()) } + // fn filter_alias_types( + // all_types: &[AbigenTarget], + // ) -> impl Iterator { + // all_types + // .iter() + // .flat_map(|target| &target.source.abi.types) + // .filter(|ttype| ttype.is_alias_type()) + // } + /// A type is considered "shared" if it appears at least twice in /// `all_custom_types`. /// diff --git a/packages/fuels-code-gen/src/program_bindings/custom_types.rs b/packages/fuels-code-gen/src/program_bindings/custom_types.rs index 48157f74af..f724c978db 100644 --- a/packages/fuels-code-gen/src/program_bindings/custom_types.rs +++ b/packages/fuels-code-gen/src/program_bindings/custom_types.rs @@ -2,13 +2,14 @@ use std::collections::{HashMap, HashSet}; use fuel_abi_types::abi::full_program::{FullLoggedType, FullTypeDeclaration}; use itertools::Itertools; -use quote::quote; +use quote::{ToTokens, quote}; use crate::{ error::Result, program_bindings::{ custom_types::{enums::expand_custom_enum, structs::expand_custom_struct}, generated_code::GeneratedCode, + resolved_type::TypeResolver, utils::sdk_provided_custom_types_lookup, }, utils::TypePath, @@ -21,7 +22,7 @@ pub(crate) mod utils; /// Generates Rust code for each type inside `types` if: /// * the type is not present inside `shared_types`, and /// * if it should be generated (see: [`should_skip_codegen`], and -/// * if it is a struct or an enum. +/// * if it is a struct or an enum or an alias. /// /// /// # Arguments @@ -47,6 +48,8 @@ pub(crate) fn generate_types<'a>( let log_id = log_ids.get(&ttype.type_field); if shared_types.contains(ttype) { reexport_the_shared_type(ttype, no_std) + } else if ttype.is_alias_type() { + expand_alias_type(ttype, no_std) } else if ttype.is_struct_type() { expand_custom_struct(ttype, no_std, log_id) } else { @@ -58,13 +61,62 @@ pub(crate) fn generate_types<'a>( }) } +fn expand_alias_type(ttype: &FullTypeDeclaration, no_std: bool) -> Result { + let type_path = ttype.alias_type_path().expect("This must be an alias type"); + let type_path_ident = syn::Ident::new( + type_path.ident().unwrap().to_string().as_str(), + proc_macro2::Span::call_site(), + ); + + let alias_of = ttype.alias_of.as_ref().unwrap(); + eprintln!("alias_of: {:?}", alias_of); + + // let mut raw_type_str = alias_of.name.as_str(); + + // if let Some(stripped) = raw_type_str.strip_prefix("struct ") { + // raw_type_str = stripped; + // } else if let Some(stripped) = raw_type_str.strip_prefix("enum ") { + // raw_type_str = stripped; + // } + + let resolver = TypeResolver::default(); + let resolved_alias = resolver.resolve(alias_of.as_ref())?; + eprintln!("resolved_alias: {:?}", resolved_alias); + // panic!(); + + // let alias_of_path: syn::Type = + // syn::parse_str(raw_type_str).expect("Failed to parse type"); + + let alias_of_path = resolved_alias.to_token_stream(); + + // eprintln!("type_path: {:?}", type_path); + // panic!(); + + let type_mod = type_path.parent(); + + let top_lvl_mod = TypePath::default(); + let from_current_mod_to_top_level = top_lvl_mod.relative_path_from(&type_mod); + + let _path = from_current_mod_to_top_level.append(type_path); + + let the_reexport = quote! {pub type #type_path_ident = #alias_of_path;}; + + Ok(GeneratedCode::new(the_reexport, Default::default(), no_std).wrap_in_mod(type_mod)) +} + /// Instead of generating bindings for `ttype` this fn will just generate a `pub use` pointing to /// the already generated equivalent shared type. fn reexport_the_shared_type(ttype: &FullTypeDeclaration, no_std: bool) -> Result { // e.g. some_library::another_mod::SomeStruct - let type_path = ttype - .custom_type_path() - .expect("This must be a custom type due to the previous filter step"); + let type_path = if ttype.is_custom_type() { + ttype + .custom_type_path() + .expect("This must be a custom type due to the previous filter step") + } else if ttype.is_alias_type() { + ttype.alias_type_path().expect("This must be an alias type") + } else { + unreachable!() + }; let type_mod = type_path.parent(); @@ -92,11 +144,17 @@ fn reexport_the_shared_type(ttype: &FullTypeDeclaration, no_std: bool) -> Result // implementation details of the contract's Vec type and are not directly // used in the SDK. pub fn should_skip_codegen(type_decl: &FullTypeDeclaration) -> bool { - if !type_decl.is_custom_type() { + if !type_decl.is_custom_type() && !type_decl.is_alias_type() { return true; } - let type_path = type_decl.custom_type_path().unwrap(); + let type_path = if type_decl.is_custom_type() { + type_decl.custom_type_path().unwrap() + } else if type_decl.is_alias_type() { + type_decl.alias_type_path().unwrap() + } else { + unreachable!() + }; is_type_sdk_provided(&type_path) || is_type_unused(&type_path) } diff --git a/packages/fuels-code-gen/src/program_bindings/custom_types/utils.rs b/packages/fuels-code-gen/src/program_bindings/custom_types/utils.rs index f777e8e588..c8e9fe37f3 100644 --- a/packages/fuels-code-gen/src/program_bindings/custom_types/utils.rs +++ b/packages/fuels-code-gen/src/program_bindings/custom_types/utils.rs @@ -1,6 +1,6 @@ use fuel_abi_types::{ abi::full_program::FullTypeDeclaration, - utils::{self, extract_generic_name}, + utils::{self}, }; use proc_macro2::Ident; @@ -11,7 +11,7 @@ pub(crate) fn extract_generic_parameters(type_decl: &FullTypeDeclaration) -> Vec .type_parameters .iter() .map(|decl| { - let name = extract_generic_name(&decl.type_field).unwrap_or_else(|| { + let name = decl.generic_name().unwrap_or_else(|| { panic!("Type parameters should only contain ids of generic types!") }); utils::ident(&name) diff --git a/packages/fuels-code-gen/src/program_bindings/resolved_type.rs b/packages/fuels-code-gen/src/program_bindings/resolved_type.rs index 06891fd2d7..4e729fa639 100644 --- a/packages/fuels-code-gen/src/program_bindings/resolved_type.rs +++ b/packages/fuels-code-gen/src/program_bindings/resolved_type.rs @@ -1,8 +1,11 @@ -use std::fmt::{Display, Formatter}; +use std::{ + fmt::{Display, Formatter}, + rc::Rc, +}; use fuel_abi_types::{ abi::full_program::FullTypeApplication, - utils::{self, extract_array_len, extract_generic_name, extract_str_len, has_tuple_format}, + utils::{self, extract_array_len, extract_str_len, has_tuple_format}, }; use proc_macro2::{Ident, TokenStream}; use quote::{ToTokens, quote}; @@ -44,6 +47,10 @@ pub enum ResolvedType { Array(Box, usize), Tuple(Vec), Generic(GenericType), + Alias { + path: TypePath, + target: Rc, + }, } impl ResolvedType { @@ -57,6 +64,7 @@ impl ResolvedType { } ResolvedType::Array(el, _) => el.generics(), ResolvedType::Generic(inner) => vec![inner.clone()], + ResolvedType::Alias { target, .. } => target.generics(), _ => vec![], } } @@ -82,6 +90,10 @@ impl ToTokens for ResolvedType { quote! { (#(#elements,)*) } } ResolvedType::Generic(generic_type) => generic_type.into_token_stream(), + ResolvedType::Alias { path, .. } => { + path.to_token_stream() + // target.as_ref().into_token_stream() + } }; tokens.extend(tokenized) @@ -122,6 +134,7 @@ impl TypeResolver { Self::try_as_tuple, Self::try_as_raw_slice, Self::try_as_custom_type, + Self::try_as_alias_type, ]; for resolver in resolvers { @@ -148,7 +161,9 @@ impl TypeResolver { &self, type_application: &FullTypeApplication, ) -> Result> { - let Some(name) = extract_generic_name(&type_application.type_decl.type_field) else { + let type_decl = &type_application.type_decl; + + let Some(name) = type_decl.generic_name() else { return Ok(None); }; @@ -302,6 +317,33 @@ impl TypeResolver { generics, })) } + + fn try_as_alias_type( + &self, + type_application: &FullTypeApplication, + ) -> Result> { + let type_decl = &type_application.type_decl; + + if !type_decl.is_alias_type() { + return Ok(None); + } + + let Some(alias_of) = &type_decl.alias_of else { + return Ok(None); + }; + + let target = Rc::new(self.resolve(alias_of.as_ref())?); + + let path = if let Ok(custom_type_path) = type_decl.custom_type_path() { + custom_type_path + } else { + type_decl.alias_type_path()? + }; + + let path = path.relative_path_from(&self.current_mod); + + Ok(Some(ResolvedType::Alias { path, target })) + } } #[cfg(test)] diff --git a/packages/fuels-core/src/types/param_types/from_type_application.rs b/packages/fuels-core/src/types/param_types/from_type_application.rs index 3da8d43038..29c337bfe7 100644 --- a/packages/fuels-core/src/types/param_types/from_type_application.rs +++ b/packages/fuels-core/src/types/param_types/from_type_application.rs @@ -2,7 +2,7 @@ use std::{collections::HashMap, iter::zip}; use fuel_abi_types::{ abi::unified_program::{UnifiedTypeApplication, UnifiedTypeDeclaration}, - utils::{extract_array_len, extract_generic_name, extract_str_len, has_tuple_format}, + utils::{extract_array_len, extract_str_len, has_tuple_format}, }; use crate::types::{ @@ -61,7 +61,7 @@ impl Type { ) })?; - if extract_generic_name(&type_declaration.type_field).is_some() { + if type_declaration.generic_name().is_some() { let (_, generic_type) = parent_generic_params .iter() .find(|(id, _)| *id == type_application.type_id) diff --git a/packages/fuels-macros/src/lib.rs b/packages/fuels-macros/src/lib.rs index 7804de7699..3102f8374f 100644 --- a/packages/fuels-macros/src/lib.rs +++ b/packages/fuels-macros/src/lib.rs @@ -1,3 +1,5 @@ +use std::{env, fs, path::Path}; + use fuels_code_gen::Abigen; use proc_macro::TokenStream; use syn::{DeriveInput, parse_macro_input}; @@ -57,9 +59,35 @@ pub fn wasm_abigen(input: TokenStream) -> TokenStream { pub fn setup_program_test(input: TokenStream) -> TokenStream { let test_program_commands = parse_macro_input!(input as TestProgramCommands); - generate_setup_program_test_code(test_program_commands) - .unwrap_or_else(|e| e.to_compile_error()) - .into() + // Generate the TokenStream + let tokens = match generate_setup_program_test_code(test_program_commands) { + Ok(toks) => toks, + Err(err) => return err.to_compile_error().into(), + }; + + // Only dump when DEBUG_SETUP=1 is set in the build environment + if env::var_os("DEBUG_SETUP").is_some() { + let target_dir = env::var("CARGO_TARGET_DIR").unwrap_or_else(|_| "target".into()); + let profile = env::var("PROFILE").unwrap_or_else(|_| "debug".into()); + let out_dir = Path::new(&target_dir) + .join(&profile) + .join("generated_tests"); + + if let Err(e) = fs::create_dir_all(&out_dir) { + panic!("failed to create debug dir {:?}: {}", out_dir, e); + } + let file_path = out_dir.join("setup_program_test.rs"); + if let Err(e) = fs::write(&file_path, tokens.to_string()) { + panic!("failed to write debug file {:?}: {}", file_path, e); + } + + eprintln!( + "[setup_program_test] Wrote generated code to {:?}", + file_path + ); + } + + tokens.into() } #[proc_macro_derive(Parameterize, attributes(FuelsTypesPath, FuelsCorePath, NoStd, Ignore))] diff --git a/packages/fuels/Cargo.toml b/packages/fuels/Cargo.toml index ee8fe674fa..0dc559731d 100644 --- a/packages/fuels/Cargo.toml +++ b/packages/fuels/Cargo.toml @@ -19,6 +19,7 @@ fuel-crypto = { workspace = true } fuel-tx = { workspace = true } fuels-accounts = { workspace = true, default-features = false } fuels-core = { workspace = true } +fuels-code-gen = { workspace = true } fuels-macros = { workspace = true } fuels-programs = { workspace = true } fuels-test-helpers = { workspace = true, optional = true } diff --git a/packages/fuels/src/lib.rs b/packages/fuels/src/lib.rs index d02496d321..1c605b5dca 100644 --- a/packages/fuels/src/lib.rs +++ b/packages/fuels/src/lib.rs @@ -41,6 +41,10 @@ pub mod core { pub use fuels_core::{Configurable, Configurables, codec, constants, offsets, traits}; } +pub mod code_gen { + pub use fuels_code_gen::*; +} + pub mod crypto { pub use fuel_crypto::{Hasher, Message, PublicKey, SecretKey, Signature}; }