From c78e6170b00f113afe847144c1facab18ba3e3c6 Mon Sep 17 00:00:00 2001 From: moana Date: Thu, 9 Apr 2026 14:13:35 +0200 Subject: [PATCH 1/5] chore: Add rustfmt.toml and Makefile targets (check, doc, cq) --- Makefile | 17 ++++++++++++++--- rustfmt.toml | 5 +++++ 2 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 rustfmt.toml diff --git a/Makefile b/Makefile index d284736..305a010 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ # Workspace Makefile for dusk-forge -.PHONY: all test test-unit test-integration clippy fmt clean help +.PHONY: all test test-unit test-integration clippy cq fmt check doc clean help all: test @@ -14,8 +14,19 @@ test-unit: ## Run unit tests test-integration: ## Run integration tests (test-contract) @$(MAKE) -C tests/test-contract test -fmt: ## Format all Rust source files - @cargo fmt --all +fmt: ## Format code (requires nightly) + @rustup component add --toolchain nightly rustfmt 2>/dev/null || true + @cargo +nightly fmt --all $(if $(CHECK),-- --check,) + +check: ## Run cargo check on all targets + @cargo check --all-targets + +doc: ## Generate documentation + @cargo doc --no-deps + +cq: ## Run code quality checks (formatting + clippy) + @$(MAKE) fmt CHECK=1 + @$(MAKE) clippy clippy: ## Run clippy on all workspace members @echo "Running clippy..." diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..cd4c7d8 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,5 @@ +unstable_features = true +group_imports = "StdExternalCrate" +imports_granularity = "Module" +use_field_init_shorthand = true +wrap_comments = true From 73662a6e19ebf601bab8559a42f9d025cf55b015 Mon Sep 17 00:00:00 2001 From: moana Date: Thu, 9 Apr 2026 14:13:45 +0200 Subject: [PATCH 2/5] chore: Apply rustfmt formatting --- CLAUDE.md | 41 +++++++++++++++++++++++ cli/src/build_runner/mod.rs | 22 +++++------- cli/src/build_runner/wasm_opt.rs | 3 +- cli/src/commands/build.rs | 11 +++--- cli/src/commands/call.rs | 4 +-- cli/src/commands/check.rs | 10 +++--- cli/src/commands/clean.rs | 10 +++--- cli/src/commands/completions.rs | 6 ++-- cli/src/commands/expand.rs | 13 +++---- cli/src/commands/new.rs | 21 +++++------- cli/src/commands/schema.rs | 4 +-- cli/src/commands/test.rs | 12 +++---- cli/src/commands/verify.rs | 4 +-- cli/src/project/detect.rs | 3 +- cli/src/project/metadata.rs | 6 ++-- cli/src/template/engine.rs | 3 +- cli/src/toolchain.rs | 8 ++--- cli/src/tools.rs | 6 ++-- cli/tests/core_commands.rs | 11 +++--- cli/tests/dx_commands.rs | 3 +- contract-macro/src/data_driver.rs | 18 ++++++---- contract-macro/src/extract.rs | 23 ++++++++----- contract-macro/src/generate.rs | 27 ++++++++++----- contract-macro/src/lib.rs | 32 +++++++++++------- contract-macro/src/resolve.rs | 9 +++-- contract-macro/src/validate.rs | 6 ++-- tests/test-contract/src/lib.rs | 15 +++++---- tests/test-contract/tests/contract.rs | 9 ++--- tests/test-contract/tests/schema.rs | 6 ++-- tests/test-contract/tests/test_session.rs | 5 ++- tests/types/src/lib.rs | 1 - 31 files changed, 200 insertions(+), 152 deletions(-) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..00694de --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,41 @@ +# Dusk Forge Workspace + + +Contract macro and tooling for Dusk Network WASM smart contracts. + +## Key Files + +- `forge/docs/desing.md` - Macro design explanation +- `forge/src/lib.rs` - Main `#[contract]` proc-macro implementation +- `forge/contract-macro/` - Macro internals (parsing, codegen, schema) +- `forge/tests/test-contract/src/lib.rs` - Integration test contract (mirrors general-purpose macro exerciser) +- `forge/contract-template` - Template to start writing your own contract +- `forge/tests/test-contract/tests/` - Contract deployment and schema tests +- `forge/Makefile` - Workspace commands (`make test`, `make clippy`) + +## Reference Implementation + +The test-contract contract is based on the real general-purpose macro exerciser at: +`/Users/esel/dusk/evm/L1Contracts/general-purpose macro exerciser/src/lib.rs` + +Core types come from: +`/Users/esel/dusk/evm/L1Contracts/core/src/` + +## Build Commands + +Use the Makefiles in all projects to build and test + +```bash +make test # Run all tests (unit + integration) +make clippy # Run clippy with pedantic warnings +``` + +## Architecture Notes + +- The `#[contract]` macro generates WASM exports and a `CONTRACT_SCHEMA` +- Trait methods with empty bodies signal the macro to use trait defaults +- Schema extraction uses a separate "data-driver" WASM build target + +## General Notes + +- commit without a `Co-Authored by` line diff --git a/cli/src/build_runner/mod.rs b/cli/src/build_runner/mod.rs index 22d78b2..7d8ef1e 100644 --- a/cli/src/build_runner/mod.rs +++ b/cli/src/build_runner/mod.rs @@ -1,18 +1,14 @@ pub mod wasm_opt; -use std::{ - env, - ffi::OsStr, - path::{Path, PathBuf}, - process::{Command, Stdio}, -}; - -use crate::{ - error::{CliError, Result}, - project::detect, - project::metadata::ProjectMetadata, - toolchain::{self, WASM_TARGET}, -}; +use std::env; +use std::ffi::OsStr; +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; + +use crate::error::{CliError, Result}; +use crate::project::detect; +use crate::project::metadata::ProjectMetadata; +use crate::toolchain::{self, WASM_TARGET}; const CONTRACT_FEATURE: &str = "contract"; const STACK_SIZE: u32 = 65_536; diff --git a/cli/src/build_runner/wasm_opt.rs b/cli/src/build_runner/wasm_opt.rs index 200ba8c..b3b6e1f 100644 --- a/cli/src/build_runner/wasm_opt.rs +++ b/cli/src/build_runner/wasm_opt.rs @@ -1,4 +1,5 @@ -use std::{path::Path, process::Command}; +use std::path::Path; +use std::process::Command; use crate::error::{CliError, Result}; use crate::tools; diff --git a/cli/src/commands/build.rs b/cli/src/commands/build.rs index dae06c6..f4b4fbd 100644 --- a/cli/src/commands/build.rs +++ b/cli/src/commands/build.rs @@ -1,12 +1,9 @@ use std::fs; -use crate::{ - build_runner, - cli::BuildArgs, - error::Result, - project::{detect, metadata}, - toolchain, ui, -}; +use crate::cli::BuildArgs; +use crate::error::Result; +use crate::project::{detect, metadata}; +use crate::{build_runner, toolchain, ui}; pub fn run(args: BuildArgs) -> Result<()> { let project = metadata::load(&args.project.path)?; diff --git a/cli/src/commands/call.rs b/cli/src/commands/call.rs index a596cd4..92331e7 100644 --- a/cli/src/commands/call.rs +++ b/cli/src/commands/call.rs @@ -1,5 +1,5 @@ -use crate::{cli::CallArgs, error::Result}; - +use crate::cli::CallArgs; +use crate::error::Result; #[cfg(feature = "schema")] use crate::{ build_runner::{self, BuildTarget}, diff --git a/cli/src/commands/check.rs b/cli/src/commands/check.rs index 3e09af9..804c54b 100644 --- a/cli/src/commands/check.rs +++ b/cli/src/commands/check.rs @@ -1,9 +1,7 @@ -use crate::{ - cli::ProjectOptions, - error::{CliError, Result}, - project::{detect, metadata}, - toolchain, ui, -}; +use crate::cli::ProjectOptions; +use crate::error::{CliError, Result}; +use crate::project::{detect, metadata}; +use crate::{toolchain, ui}; pub fn run(args: ProjectOptions) -> Result<()> { let project = metadata::load(&args.path)?; diff --git a/cli/src/commands/clean.rs b/cli/src/commands/clean.rs index c15f25e..1df298b 100644 --- a/cli/src/commands/clean.rs +++ b/cli/src/commands/clean.rs @@ -1,11 +1,9 @@ use std::fs; -use crate::{ - cli::ProjectOptions, - error::Result, - project::{detect, metadata}, - ui, -}; +use crate::cli::ProjectOptions; +use crate::error::Result; +use crate::project::{detect, metadata}; +use crate::ui; pub fn run(args: ProjectOptions) -> Result<()> { let project = metadata::load(&args.path)?; diff --git a/cli/src/commands/completions.rs b/cli/src/commands/completions.rs index ac79a55..8bf8a69 100644 --- a/cli/src/commands/completions.rs +++ b/cli/src/commands/completions.rs @@ -3,10 +3,8 @@ use std::io; use clap::CommandFactory; use clap_complete::generate; -use crate::{ - cli::{Cli, CompletionsArgs}, - error::Result, -}; +use crate::cli::{Cli, CompletionsArgs}; +use crate::error::Result; pub fn run(args: CompletionsArgs) -> Result<()> { let mut cmd = Cli::command(); diff --git a/cli/src/commands/expand.rs b/cli/src/commands/expand.rs index 722b5c4..9b5dc8d 100644 --- a/cli/src/commands/expand.rs +++ b/cli/src/commands/expand.rs @@ -1,13 +1,10 @@ use std::process::{Command, Stdio}; -use crate::{ - build_runner, - cli::ExpandArgs, - error::{CliError, Result}, - project::{detect, metadata}, - toolchain::{self, WASM_TARGET}, - tools, ui, -}; +use crate::cli::ExpandArgs; +use crate::error::{CliError, Result}; +use crate::project::{detect, metadata}; +use crate::toolchain::{self, WASM_TARGET}; +use crate::{build_runner, tools, ui}; pub fn run(args: ExpandArgs) -> Result<()> { let project = metadata::load(&args.project.path)?; diff --git a/cli/src/commands/new.rs b/cli/src/commands/new.rs index 6b1c4a6..8febf34 100644 --- a/cli/src/commands/new.rs +++ b/cli/src/commands/new.rs @@ -1,15 +1,12 @@ -use std::{fs, path::Path, process::Command}; - -use crate::{ - build_runner, - cli::{NewArgs, TemplateChoice}, - error::{CliError, Result}, - template::{ - embedded::TemplateKind, - engine::{render_template, validate_contract_name}, - }, - toolchain, ui, -}; +use std::fs; +use std::path::Path; +use std::process::Command; + +use crate::cli::{NewArgs, TemplateChoice}; +use crate::error::{CliError, Result}; +use crate::template::embedded::TemplateKind; +use crate::template::engine::{render_template, validate_contract_name}; +use crate::{build_runner, toolchain, ui}; pub fn run(args: NewArgs) -> Result<()> { let parsed_name = validate_contract_name(&args.name)?; diff --git a/cli/src/commands/schema.rs b/cli/src/commands/schema.rs index fa50c84..0ee8746 100644 --- a/cli/src/commands/schema.rs +++ b/cli/src/commands/schema.rs @@ -1,5 +1,5 @@ -use crate::{cli::SchemaArgs, error::Result}; - +use crate::cli::SchemaArgs; +use crate::error::Result; #[cfg(feature = "schema")] use crate::{ build_runner::{self, BuildTarget}, diff --git a/cli/src/commands/test.rs b/cli/src/commands/test.rs index f9958e8..19eea53 100644 --- a/cli/src/commands/test.rs +++ b/cli/src/commands/test.rs @@ -1,12 +1,10 @@ use std::process::{Command, Stdio}; -use crate::{ - build_runner::{self, BuildTarget}, - cli::TestArgs, - error::{CliError, Result}, - project::{detect, metadata}, - toolchain, ui, -}; +use crate::build_runner::{self, BuildTarget}; +use crate::cli::TestArgs; +use crate::error::{CliError, Result}; +use crate::project::{detect, metadata}; +use crate::{toolchain, ui}; pub fn run(args: TestArgs) -> Result<()> { let project = metadata::load(&args.project.path)?; diff --git a/cli/src/commands/verify.rs b/cli/src/commands/verify.rs index dca0868..096871c 100644 --- a/cli/src/commands/verify.rs +++ b/cli/src/commands/verify.rs @@ -1,8 +1,8 @@ #[cfg(feature = "schema")] use std::fs; -use crate::{cli::VerifyArgs, error::Result}; - +use crate::cli::VerifyArgs; +use crate::error::Result; #[cfg(feature = "schema")] use crate::{ build_runner::{self, BuildTarget}, diff --git a/cli/src/project/detect.rs b/cli/src/project/detect.rs index c86c84a..f4ddcff 100644 --- a/cli/src/project/detect.rs +++ b/cli/src/project/detect.rs @@ -1,4 +1,5 @@ -use std::{fs, path::Path}; +use std::fs; +use std::path::Path; use toml::Value; diff --git a/cli/src/project/metadata.rs b/cli/src/project/metadata.rs index 1669b2b..20eadb7 100644 --- a/cli/src/project/metadata.rs +++ b/cli/src/project/metadata.rs @@ -1,7 +1,5 @@ -use std::{ - fs, - path::{Path, PathBuf}, -}; +use std::fs; +use std::path::{Path, PathBuf}; use cargo_metadata::{MetadataCommand, Package}; diff --git a/cli/src/template/engine.rs b/cli/src/template/engine.rs index cb135c5..8399025 100644 --- a/cli/src/template/engine.rs +++ b/cli/src/template/engine.rs @@ -1,6 +1,5 @@ -use crate::error::{CliError, Result}; - use super::embedded::{TemplateKind, files}; +use crate::error::{CliError, Result}; #[derive(Debug, Clone, PartialEq, Eq)] pub struct ContractName { diff --git a/cli/src/toolchain.rs b/cli/src/toolchain.rs index ba66814..c77ed3f 100644 --- a/cli/src/toolchain.rs +++ b/cli/src/toolchain.rs @@ -1,8 +1,6 @@ -use std::{ - fs, - path::{Path, PathBuf}, - process::Command, -}; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; use crate::error::{CliError, Result}; use crate::tools; diff --git a/cli/src/tools.rs b/cli/src/tools.rs index 631ae0c..8e7a072 100644 --- a/cli/src/tools.rs +++ b/cli/src/tools.rs @@ -1,7 +1,5 @@ -use std::{ - env, - path::{Path, PathBuf}, -}; +use std::env; +use std::path::{Path, PathBuf}; pub fn find_in_path(program: &str) -> Option { let program_path = Path::new(program); diff --git a/cli/tests/core_commands.rs b/cli/tests/core_commands.rs index 0a82945..ad92e5e 100644 --- a/cli/tests/core_commands.rs +++ b/cli/tests/core_commands.rs @@ -1,12 +1,9 @@ #![cfg(unix)] -use std::{ - env, - ffi::OsString, - fs, - os::unix::fs::PermissionsExt, - path::{Path, PathBuf}, -}; +use std::ffi::OsString; +use std::os::unix::fs::PermissionsExt; +use std::path::{Path, PathBuf}; +use std::{env, fs}; use assert_cmd::cargo::cargo_bin_cmd; use predicates::prelude::*; diff --git a/cli/tests/dx_commands.rs b/cli/tests/dx_commands.rs index 7320cd7..3a824af 100644 --- a/cli/tests/dx_commands.rs +++ b/cli/tests/dx_commands.rs @@ -1,4 +1,5 @@ -use std::{fs, path::PathBuf}; +use std::fs; +use std::path::PathBuf; use assert_cmd::cargo::cargo_bin_cmd; use predicates::prelude::*; diff --git a/contract-macro/src/data_driver.rs b/contract-macro/src/data_driver.rs index 024d658..1f584d9 100644 --- a/contract-macro/src/data_driver.rs +++ b/contract-macro/src/data_driver.rs @@ -7,7 +7,8 @@ //! Data driver module generation. //! //! Generates a `data_driver` module at crate root level containing a `Driver` -//! struct that implements the `ConvertibleContract` trait from `dusk-data-driver`. +//! struct that implements the `ConvertibleContract` trait from +//! `dusk-data-driver`. //! //! The module is feature-gated with `#[cfg(feature = "data-driver")]` and uses //! fully-qualified type paths resolved at extraction time. @@ -118,11 +119,13 @@ pub(crate) fn module( } } -/// Get the resolved type path from the `type_map`, or return the original if not found. +/// Get the resolved type path from the `type_map`, or return the original if +/// not found. fn get_resolved_type(ty: &TokenStream2, type_map: &TypeMap) -> TokenStream2 { let key = ty.to_string(); if let Some(resolved) = type_map.get(&key) { - // Parse the resolved string back into tokens as a Type (not Path, since tuples aren't paths) + // Parse the resolved string back into tokens as a Type (not Path, since tuples + // aren't paths) if let Ok(resolved_type) = syn::parse_str::(resolved) { return quote! { #resolved_type }; } @@ -311,12 +314,15 @@ fn generate_decode_event_arms(events: &[EventInfo], type_map: &TypeMap) -> Vec String { tokens .to_string() diff --git a/contract-macro/src/extract.rs b/contract-macro/src/extract.rs index 4da033e..c8e15fa 100644 --- a/contract-macro/src/extract.rs +++ b/contract-macro/src/extract.rs @@ -24,8 +24,10 @@ use crate::{ /// /// Checks that: /// 1. There is at most one `abi::feed()` call site in the function -/// 2. If `abi::feed()` is used, the `#[contract(feeds = "Type")]` attribute is present -/// 3. If present, the feeds type matches the fed expression (tuple vs non-tuple) +/// 2. If `abi::feed()` is used, the `#[contract(feeds = "Type")]` attribute is +/// present +/// 3. If present, the feeds type matches the fed expression (tuple vs +/// non-tuple) /// /// Returns an error if validation fails. fn validate_feeds( @@ -360,7 +362,8 @@ pub(crate) fn emit_calls(impl_block: &ItemImpl) -> Vec { .collect() } -/// Extract the `expose = [method1, method2, ...]` list from a `#[contract(...)]` attribute. +/// Extract the `expose = [method1, method2, ...]` list from a +/// `#[contract(...)]` attribute. /// /// Returns `None` if there's no `#[contract(expose = [...])]` attribute. /// Returns `Some(vec![...])` with the method names if found. @@ -463,8 +466,9 @@ fn imports(items: &[Item]) -> Result, syn::Error> { /// Find the contract struct in the module. /// -/// The module must contain exactly one `pub struct` which serves as the contract state. -/// Returns an error if there are zero or multiple public structs. +/// The module must contain exactly one `pub struct` which serves as the +/// contract state. Returns an error if there are zero or multiple public +/// structs. fn contract_struct<'a>( module: &'a ItemMod, items: &'a [Item], @@ -523,7 +527,8 @@ fn impl_blocks<'a>(items: &'a [Item], contract_name: &str) -> Vec<&'a ItemImpl> /// Find trait impl blocks with `#[contract(expose = [...])]` attributes. /// /// Only trait implementations that have an explicit expose list are returned. -/// The expose list specifies which trait methods should have extern wrappers generated. +/// The expose list specifies which trait methods should have extern wrappers +/// generated. fn trait_impls<'a>(items: &'a [Item], contract_name: &str) -> Vec> { items .iter() @@ -575,7 +580,8 @@ fn custom_data_driver_handlers(items: &[Item]) -> Vec { continue; }; - // Parse: encode_input = "fn_name", decode_input = "fn_name", or decode_output = "fn_name" + // Parse: encode_input = "fn_name", decode_input = "fn_name", or decode_output = + // "fn_name" let tokens = meta.tokens.clone(); let mut iter = tokens.into_iter().peekable(); @@ -625,7 +631,8 @@ fn custom_data_driver_handlers(items: &[Item]) -> Vec { /// Check if an item is a custom data-driver handler function. /// /// Returns true if the item has a `#[contract(encode_input = ...)]`, -/// `#[contract(decode_input = ...)]`, or `#[contract(decode_output = ...)]` attribute. +/// `#[contract(decode_input = ...)]`, or `#[contract(decode_output = ...)]` +/// attribute. pub(crate) fn is_custom_handler(item: &Item) -> bool { let Item::Fn(func) = item else { return false; diff --git a/contract-macro/src/generate.rs b/contract-macro/src/generate.rs index 9b35aca..76b19cb 100644 --- a/contract-macro/src/generate.rs +++ b/contract-macro/src/generate.rs @@ -92,7 +92,9 @@ pub(crate) fn schema( /// Generate the static `STATE` variable declaration. /// -/// This creates a mutable static variable initialized via the contract's `new()` constructor: +/// This creates a mutable static variable initialized via the contract's +/// `new()` constructor: +/// /// ```ignore /// static mut STATE: ContractName = ContractName::new(); /// ``` @@ -106,11 +108,16 @@ pub(crate) fn state_static(contract_ident: &Ident) -> TokenStream2 { /// Generate extern "C" wrapper functions for all public methods. /// -/// Each wrapper deserializes input, calls the method on STATE, and serializes output. -/// - For methods that return references, the wrapper clones the result before serialization. -/// - For parameters that are references, the wrapper receives the owned value and passes a reference. -/// - For trait methods with default implementations, calls the trait method via fully-qualified syntax. -/// - For associated functions (no self), calls the function on the contract type. +/// Each wrapper deserializes input, calls the method on STATE, and serializes +/// output. +/// - For methods that return references, the wrapper clones the result before +/// serialization. +/// - For parameters that are references, the wrapper receives the owned value +/// and passes a reference. +/// - For trait methods with default implementations, calls the trait method via +/// fully-qualified syntax. +/// - For associated functions (no self), calls the function on the contract +/// type. pub(crate) fn extern_wrappers(functions: &[FunctionInfo], contract_ident: &Ident) -> TokenStream2 { let wrappers: Vec<_> = functions .iter() @@ -208,8 +215,9 @@ pub(crate) fn extern_wrappers(functions: &[FunctionInfo], contract_ident: &Ident } /// Strip #[contract(...)] attributes from the impl block and its methods. -/// For trait impl blocks, also removes empty-body methods (they're just signature stubs -/// for wrapper generation and should use the trait's default implementation). +/// For trait impl blocks, also removes empty-body methods (they're just +/// signature stubs for wrapper generation and should use the trait's default +/// implementation). pub(crate) fn strip_contract_attributes(mut impl_block: ItemImpl) -> ItemImpl { let is_trait_impl = impl_block.trait_.is_some(); @@ -227,7 +235,8 @@ pub(crate) fn strip_contract_attributes(mut impl_block: ItemImpl) -> ItemImpl { } } - // For trait impls, remove empty-body methods so they use the default implementation + // For trait impls, remove empty-body methods so they use the default + // implementation if is_trait_impl { impl_block.items.retain(|item| { if let ImplItem::Fn(method) = item { diff --git a/contract-macro/src/lib.rs b/contract-macro/src/lib.rs index daf539d..eaf15bb 100644 --- a/contract-macro/src/lib.rs +++ b/contract-macro/src/lib.rs @@ -47,9 +47,10 @@ mod validate; use proc_macro::TokenStream; use proc_macro2::{Ident, TokenStream as TokenStream2}; use quote::quote; +use syn::visit::Visit; use syn::{ Attribute, Expr, ExprCall, ExprLit, ExprPath, FnArg, ImplItemFn, Item, ItemImpl, ItemMod, Lit, - Type, parse_macro_input, visit::Visit, + Type, parse_macro_input, }; // ============================================================================ @@ -82,7 +83,8 @@ struct ParameterInfo { name: Ident, /// The type (dereferenced if the parameter is a reference). ty: TokenStream2, - /// Whether the parameter is a reference (requires `&` when passing to method). + /// Whether the parameter is a reference (requires `&` when passing to + /// method). is_ref: bool, /// Whether the parameter is a mutable reference. is_mut_ref: bool, @@ -106,10 +108,12 @@ struct FunctionInfo { returns_ref: bool, /// The method's receiver type (`&self`, `&mut self`, or none). receiver: Receiver, - /// For trait methods with empty bodies: the trait name to call the default impl. + /// For trait methods with empty bodies: the trait name to call the default + /// impl. trait_name: Option, - /// The type fed via `abi::feed()` for streaming functions (from `#[contract(feeds = "Type")]`). - /// When present, the data-driver uses this type for `decode_output_fn` instead of `output_type`. + /// The type fed via `abi::feed()` for streaming functions (from + /// `#[contract(feeds = "Type")]`). When present, the data-driver uses + /// this type for `decode_output_fn` instead of `output_type`. feed_type: Option, } @@ -134,7 +138,8 @@ enum DataDriverRole { /// Information about a custom data-driver handler function. struct CustomDataDriverHandler { - /// The data-driver function name this handler is for (e.g., `"extra_data"`). + /// The data-driver function name this handler is for (e.g., + /// `"extra_data"`). fn_name: String, /// Which role this handler plays. role: DataDriverRole, @@ -243,7 +248,8 @@ fn get_feed_exprs(method: &ImplItemFn) -> Vec { visitor.feed_exprs } -/// Check if a type string looks like a tuple (starts with `(` and contains `,`). +/// Check if a type string looks like a tuple (starts with `(` and contains +/// `,`). fn looks_like_tuple(s: &str) -> bool { let trimmed = s.trim(); trimmed.starts_with('(') && trimmed.contains(',') @@ -381,9 +387,9 @@ fn has_custom_attribute(attrs: &[Attribute]) -> bool { /// Extract the `feeds` type from a `#[contract(feeds = "Type")]` attribute. /// -/// This attribute specifies the type fed via `abi::feed()` for streaming functions. -/// When present, the data-driver uses this type for `decode_output_fn` instead of the -/// function's return type. +/// This attribute specifies the type fed via `abi::feed()` for streaming +/// functions. When present, the data-driver uses this type for +/// `decode_output_fn` instead of the function's return type. /// /// Returns `Some(TokenStream2)` with the feed type if found, `None` otherwise. fn extract_feeds_attribute(attrs: &[Attribute]) -> Option { @@ -461,7 +467,8 @@ fn generate_arg_expr(param: &ParameterInfo) -> TokenStream2 { /// This macro will produce compile errors if: /// - The module has no content (just a declaration like `mod foo;`) /// - The module contains glob imports (`use foo::*`) -/// - The module contains relative imports (`use self::`, `use super::`, `use crate::`) +/// - The module contains relative imports (`use self::`, `use super::`, `use +/// crate::`) /// - The module contains multiple `pub struct` declarations /// - The module contains no `pub struct` /// - The module contains no impl block for the contract struct @@ -593,9 +600,10 @@ pub fn contract(_attr: TokenStream, item: TokenStream) -> TokenStream { #[cfg(test)] mod tests { - use super::*; use syn::visit::Visit; + use super::*; + // ========================================================================= // EmitVisitor tests // ========================================================================= diff --git a/contract-macro/src/resolve.rs b/contract-macro/src/resolve.rs index 146a37d..1df4b3e 100644 --- a/contract-macro/src/resolve.rs +++ b/contract-macro/src/resolve.rs @@ -35,7 +35,8 @@ fn build_import_map(imports: &[ImportInfo]) -> HashMap { /// Handles: /// - Simple types: `Deposit` -> `my_crate::Deposit` /// - Aliased types: `DSAddress` -> `my_crate::Address` -/// - Multi-segment paths: `events::PauseToggled` -> `my_crate::events::PauseToggled` +/// - Multi-segment paths: `events::PauseToggled` -> +/// `my_crate::events::PauseToggled` /// - Generic types: `Option` -> `Option` fn resolve_type(ty: &TokenStream2, import_map: &HashMap) -> String { let ty_str = ty.to_string(); @@ -153,7 +154,8 @@ fn format_generic_args(args: &syn::PathArguments, import_map: &HashMap) -> String { @@ -219,9 +221,10 @@ pub(crate) fn build_type_map( #[cfg(test)] mod tests { - use super::*; use quote::quote; + use super::*; + fn make_import(name: &str, path: &str) -> ImportInfo { ImportInfo { name: name.to_string(), diff --git a/contract-macro/src/validate.rs b/contract-macro/src/validate.rs index 5852fdd..9fdc9a2 100644 --- a/contract-macro/src/validate.rs +++ b/contract-macro/src/validate.rs @@ -8,7 +8,8 @@ use syn::{FnArg, ImplItem, ImplItemFn, ItemImpl, ReturnType, Type, Visibility}; -/// Validate that a public method has a supported signature for extern wrapper generation. +/// Validate that a public method has a supported signature for extern wrapper +/// generation. /// /// Returns an error if the method: /// - Has generic type or const parameters @@ -267,7 +268,8 @@ pub(crate) fn init_method( /// Validate a method from a trait impl block. /// /// Similar to `public_method` but with trait-specific error messages. -/// For default implementations (empty body), associated functions (no self) are allowed. +/// For default implementations (empty body), associated functions (no self) are +/// allowed. pub(crate) fn trait_method( method: &ImplItemFn, trait_name: &str, diff --git a/tests/test-contract/src/lib.rs b/tests/test-contract/src/lib.rs index f557ec4..7f7015b 100644 --- a/tests/test-contract/src/lib.rs +++ b/tests/test-contract/src/lib.rs @@ -30,8 +30,7 @@ mod test_contract { use dusk_core::abi; use dusk_core::signatures::bls::PublicKey; - use types::events; - use types::{Item, ItemId, Ownable}; + use types::{Item, ItemId, Ownable, events}; // ========================================================================= // Versioned trait — tests trait-exposed associated function (no self) @@ -171,7 +170,8 @@ mod test_contract { /// Checks whether an item exists in the collection. /// - /// Exercises: reference parameter (macro receives owned value, passes `&item`). + /// Exercises: reference parameter (macro receives owned value, passes + /// `&item`). pub fn contains_item(&self, item: &Item) -> bool { self.items.contains_key(&item.id) } @@ -231,11 +231,13 @@ mod test_contract { } /// Transfers ownership to a new public key. - /// Empty body signals the macro to use the trait's default implementation. + /// Empty body signals the macro to use the trait's default + /// implementation. fn transfer_ownership(&mut self, new_owner: PublicKey) {} /// Renounces ownership of the contract. - /// Empty body signals the macro to use the trait's default implementation. + /// Empty body signals the macro to use the trait's default + /// implementation. fn renounce_ownership(&mut self) {} } @@ -248,7 +250,8 @@ mod test_contract { #[contract(expose = [version])] impl Versioned for TestContract { /// Returns the contract version. - /// Empty body signals the macro to use the trait's default implementation. + /// Empty body signals the macro to use the trait's default + /// implementation. fn version() -> String {} } diff --git a/tests/test-contract/tests/contract.rs b/tests/test-contract/tests/contract.rs index 2e74f35..6ae7727 100644 --- a/tests/test-contract/tests/contract.rs +++ b/tests/test-contract/tests/contract.rs @@ -13,8 +13,7 @@ extern crate alloc; -use std::sync::LazyLock; -use std::sync::mpsc; +use std::sync::{LazyLock, mpsc}; use dusk_core::abi::ContractId; use dusk_core::dusk; @@ -22,12 +21,10 @@ use dusk_core::signatures::bls::{PublicKey as AccountPublicKey, SecretKey as Acc use dusk_vm::CallReceipt; mod test_session; -use test_session::TestSession; - -use types::{Item, ItemId}; - use rand::SeedableRng; use rand::rngs::StdRng; +use test_session::TestSession; +use types::{Item, ItemId}; const DEPLOYER: [u8; 64] = [0u8; 64]; diff --git a/tests/test-contract/tests/schema.rs b/tests/test-contract/tests/schema.rs index a9f81f6..42c2aff 100644 --- a/tests/test-contract/tests/schema.rs +++ b/tests/test-contract/tests/schema.rs @@ -16,7 +16,8 @@ use wasmtime::*; const DATA_DRIVER_WASM: &[u8] = include_bytes!("../../../target/data-driver/wasm32-unknown-unknown/release/test_contract.wasm"); -/// Load the data-driver WASM and call get_schema to retrieve the contract schema. +/// Load the data-driver WASM and call get_schema to retrieve the contract +/// schema. fn get_schema_from_wasm() -> String { let engine = Engine::default(); let module = Module::new(&engine, DATA_DRIVER_WASM).expect("Failed to compile WASM"); @@ -208,7 +209,8 @@ impl DataDriverWasm { Self { store, instance } } - /// Call get_last_error to retrieve the error message after a failed operation. + /// Call get_last_error to retrieve the error message after a failed + /// operation. fn get_last_error(&mut self) -> String { let memory = self .instance diff --git a/tests/test-contract/tests/test_session.rs b/tests/test-contract/tests/test_session.rs index 215ebe2..f7e98d6 100644 --- a/tests/test-contract/tests/test_session.rs +++ b/tests/test-contract/tests/test_session.rs @@ -22,15 +22,14 @@ use dusk_core::{JubJubScalar, LUX}; use dusk_vm::host_queries::{self, HardFork}; use dusk_vm::{CallReceipt, ContractData, Error as VMError, ExecutionConfig, Session, VM, execute}; use ff::Field; +use rand::SeedableRng; +use rand::rngs::StdRng; use rkyv::bytecheck::CheckBytes; use rkyv::ser::Serializer; use rkyv::ser::serializers::{BufferScratch, BufferSerializer, CompositeSerializer}; use rkyv::validation::validators::DefaultValidator; use rkyv::{Archive, Deserialize, Infallible, Serialize, check_archived_root}; -use rand::SeedableRng; -use rand::rngs::StdRng; - const ZERO_ADDRESS: ContractId = ContractId::from_bytes([0; CONTRACT_ID_BYTES]); const GAS_LIMIT: u64 = 0x10_000_000; const CHAIN_ID: u8 = 0x1; diff --git a/tests/types/src/lib.rs b/tests/types/src/lib.rs index 723790e..e883468 100644 --- a/tests/types/src/lib.rs +++ b/tests/types/src/lib.rs @@ -108,7 +108,6 @@ pub trait Ownable { /// Events emitted by the test contract. pub mod events { use dusk_core::signatures::bls::PublicKey; - #[allow(unused_imports)] use rkyv::bytecheck::CheckBytes; use rkyv::{Archive, Deserialize, Serialize}; From 2d52189b0bfa078d6d6711a8a6eb12cb6e15831f Mon Sep 17 00:00:00 2001 From: moana Date: Thu, 9 Apr 2026 14:17:19 +0200 Subject: [PATCH 3/5] chore: Add AGENTS.md and symlink CLAUDE.md --- AGENTS.md | 105 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ CLAUDE.md | 42 +--------------------- 2 files changed, 106 insertions(+), 41 deletions(-) create mode 100644 AGENTS.md mode change 100644 => 120000 CLAUDE.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..b7da589 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,105 @@ +# Forge + +Smart contract development framework for the Dusk network. Provides a `#[contract]` proc-macro that generates WASM exports and contract schemas, a CLI for scaffolding and building contract projects, and integration tests exercising every macro code path. + +## Workspace Layout + +``` +forge/ +├── src/ # dusk-forge — re-exports and schema types +├── contract-macro/ # dusk-forge-contract — proc-macro (#[contract]) +├── cli/ # dusk-forge-cli — CLI binary (new, build, test, schema, call, verify) +├── tests/types/ # types — helper types for integration tests +├── tests/test-contract/ # test-contract — general-purpose macro exerciser +├── contract-template/ # Template for scaffolding new contract projects +├── docs/ # Design documents +├── Makefile # Workspace-level targets +└── rust-toolchain.toml # Stable toolchain with wasm32 target +``` + +| Directory | Crate | Kind | +|-----------|-------|------| +| `/` (root) | `dusk-forge` | Library | +| `contract-macro/` | `dusk-forge-contract` | Proc-macro | +| `cli/` | `dusk-forge-cli` | Binary | +| `tests/types/` | `types` | Library (test helper) | +| `tests/test-contract/` | `test-contract` | Contract (integration test) | + +## Commands + +Run `make help` to see all available targets. + +## Architecture + +### `#[contract]` Macro + +The `#[contract]` attribute macro (in `contract-macro/`) transforms a Rust module into a Dusk WASM smart contract: + +- **Parsing** (`extract.rs`, `resolve.rs`) — extracts method signatures, trait impls, and state types from the annotated module +- **Validation** (`validate.rs`) — enforces contract rules (e.g., mutually exclusive feature gates for `contract` vs `data-driver`) +- **Code generation** (`generate.rs`) — emits `#[no_mangle]` WASM export wrappers with rkyv (de)serialization +- **Schema generation** (`data_driver.rs`) — produces a `CONTRACT_SCHEMA` constant describing the contract's ABI for data-driver builds + +Trait methods with empty bodies signal the macro to use trait defaults. + +### CLI + +The `dusk-forge` CLI (`cli/`) provides project management commands: + +- `new` — scaffold a contract project from the template +- `build` — compile contract and data-driver WASM binaries +- `test` / `check` / `clean` / `expand` — development workflow wrappers +- `schema` — extract and display the contract schema from a data-driver WASM +- `call` / `verify` — invoke contract methods and verify results via wasmtime + +### Test Contract + +`tests/test-contract/` is a general-purpose macro exerciser that covers every `#[contract]` code path: owned methods, borrowed methods, trait implementations, associated functions, and schema generation. + +## Conventions + +- **`no_std`** for contract crates. The proc-macro and CLI are `std`. +- **`--release` for tests**: integration tests in `tests/test-contract/` build WASM binaries, which require release mode. +- **Edition 2024** with MSRV 1.85 (stable toolchain). +- **Serialization**: `rkyv` for contract state and arguments, `serde` for schema JSON. +- **Feature gates**: Contracts use mutually exclusive features (`contract` vs `data-driver` / `data-driver-js`) to produce different WASM binaries from the same source. + +## Change Propagation + +| Changed | Also verify | +|---------|-------------| +| `dusk-forge` / `contract-macro` | `tests/test-contract`, `duskevm-contracts`, downstream contract repos | + +## Git Conventions + +- Default branch: `main` +- License: MPL-2.0 + +### Commit messages + +Format: `: ` — imperative mood, capitalize first word after colon. + +**One commit per scope per concern.** Each commit touches one logical scope and one concern. Don't bundle unrelated changes. + +Canonical scopes: + +| Scope | Directory | +|-------|-----------| +| `forge` | Root crate (`src/`) | +| `macro` | `contract-macro/` | +| `cli` | `cli/` | +| `test-contract` | `tests/test-contract/` | +| `types` | `tests/types/` | +| `workspace` | Root `Cargo.toml`, Makefile, `rust-toolchain.toml` | +| `ci` | `.github/workflows/` | +| `chore` | Formatting, config files, tooling | + +Examples: +- `macro: Add support for generic trait impls` +- `cli: Fix data-driver feature detection` +- `test-contract: Add borrowed-state method test` +- `workspace: Update dusk-core dependency` + +### Changelog + +Maintain `CHANGELOG.md` with entries under `[Unreleased]` using [keep-a-changelog](https://keepachangelog.com/) format. If a change traces to a GitHub issue, reference it as a link: `[#42](https://github.com/dusk-network/forge/issues/42)`. Only link to GitHub issues. diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 00694de..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,41 +0,0 @@ -# Dusk Forge Workspace - - -Contract macro and tooling for Dusk Network WASM smart contracts. - -## Key Files - -- `forge/docs/desing.md` - Macro design explanation -- `forge/src/lib.rs` - Main `#[contract]` proc-macro implementation -- `forge/contract-macro/` - Macro internals (parsing, codegen, schema) -- `forge/tests/test-contract/src/lib.rs` - Integration test contract (mirrors general-purpose macro exerciser) -- `forge/contract-template` - Template to start writing your own contract -- `forge/tests/test-contract/tests/` - Contract deployment and schema tests -- `forge/Makefile` - Workspace commands (`make test`, `make clippy`) - -## Reference Implementation - -The test-contract contract is based on the real general-purpose macro exerciser at: -`/Users/esel/dusk/evm/L1Contracts/general-purpose macro exerciser/src/lib.rs` - -Core types come from: -`/Users/esel/dusk/evm/L1Contracts/core/src/` - -## Build Commands - -Use the Makefiles in all projects to build and test - -```bash -make test # Run all tests (unit + integration) -make clippy # Run clippy with pedantic warnings -``` - -## Architecture Notes - -- The `#[contract]` macro generates WASM exports and a `CONTRACT_SCHEMA` -- Trait methods with empty bodies signal the macro to use trait defaults -- Schema extraction uses a separate "data-driver" WASM build target - -## General Notes - -- commit without a `Co-Authored by` line diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file From 06bdfa9bc6fb2e348da1002270f7d1cf8b2c00bf Mon Sep 17 00:00:00 2001 From: moana Date: Thu, 9 Apr 2026 14:17:33 +0200 Subject: [PATCH 4/5] ci: Migrate to make-based cq and test jobs --- .github/workflows/ci.yaml | 25 ------------------------- .github/workflows/dusk_ci.yml | 20 ++++++++++++++++++++ 2 files changed, 20 insertions(+), 25 deletions(-) delete mode 100644 .github/workflows/ci.yaml create mode 100644 .github/workflows/dusk_ci.yml diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml deleted file mode 100644 index b1cbab6..0000000 --- a/.github/workflows/ci.yaml +++ /dev/null @@ -1,25 +0,0 @@ -on: [pull_request] - -name: Forge CI - -env: - CARGO_TERM_COLOR: always - -jobs: - rust-fmt: - name: Rustfmt & Clippy - runs-on: core - steps: - - uses: actions/checkout@v4 - - uses: dsherret/rust-toolchain-file@v1 - - run: cargo fmt --all -- --check - - run: make clippy - - rust-test: - name: Run tests - needs: rust-fmt - runs-on: core - steps: - - uses: actions/checkout@v4 - - uses: dsherret/rust-toolchain-file@v1 - - run: make test diff --git a/.github/workflows/dusk_ci.yml b/.github/workflows/dusk_ci.yml new file mode 100644 index 0000000..5437ad6 --- /dev/null +++ b/.github/workflows/dusk_ci.yml @@ -0,0 +1,20 @@ +on: + pull_request: + push: + branches: + - main + +name: Continuous integration + +jobs: + code-quality: + name: Code quality + uses: dusk-network/.github/.github/workflows/run-make.yml@main + with: + make_target: cq + + test: + name: Run tests + uses: dusk-network/.github/.github/workflows/run-make.yml@main + with: + make_target: test From 736bcc889a5ea1260323627d772c6a4f543e7070 Mon Sep 17 00:00:00 2001 From: moana Date: Thu, 9 Apr 2026 14:33:33 +0200 Subject: [PATCH 5/5] chore: Add changelog entries for edition 2024 migration and fix template dep --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1038889..3ec25f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Move workspace to Rust edition 2024 on the stable toolchain (MSRV 1.85). Generated contract wrappers now use `#[unsafe(no_mangle)]`. +- Remove `-Z build-std=core,alloc` from contract builds (no longer needed on stable). - Replace EVM-flavored test-bridge with a general-purpose test contract that exercises every `#[contract]` macro code path without domain-specific types. - Make local forge path overrides opt-in for release builds and harden CLI template/path handling across platforms.