diff --git a/Cargo.lock b/Cargo.lock index 27255ecf..2537e32c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -217,10 +217,10 @@ version = "0.4.15" dependencies = [ "Inflector", "anyhow", - "arbiter-bindings", - "arbiter-core", - "arbiter-engine", - "arbiter-macros", + "arbiter-bindings 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "arbiter-core 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)", + "arbiter-engine 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "arbiter-macros 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "assert_cmd", "async-trait", "clap", @@ -250,11 +250,21 @@ dependencies = [ "serde", ] +[[package]] +name = "arbiter-bindings" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d0333ea5e6f32bef018cd4a035ff0241899947d8e0c87346c868ec5d4bb51fc" +dependencies = [ + "ethers", + "serde", +] + [[package]] name = "arbiter-core" version = "0.10.2" dependencies = [ - "arbiter-bindings", + "arbiter-bindings 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", "assert_matches", "async-stream", "async-trait", @@ -282,14 +292,43 @@ dependencies = [ "uint", ] +[[package]] +name = "arbiter-core" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6168baef0eb9e7c22d4b02e1edf526a4588edc97551b0cde96ec296f9b801b" +dependencies = [ + "arbiter-bindings 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "async-stream", + "async-trait", + "bytes", + "crossbeam-channel", + "ethers", + "futures-locks", + "futures-timer", + "futures-util", + "hashbrown 0.14.3", + "hex", + "polars", + "rand", + "revm", + "revm-primitives 2.0.0", + "serde", + "serde_json", + "thiserror", + "tokio", + "tracing", + "uint", +] + [[package]] name = "arbiter-engine" version = "0.2.0" dependencies = [ "anyhow", - "arbiter-bindings", - "arbiter-core", - "arbiter-macros", + "arbiter-bindings 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "arbiter-core 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)", + "arbiter-macros 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "async-stream", "async-trait", "crossbeam-channel", @@ -307,9 +346,44 @@ dependencies = [ "tracing-test", ] +[[package]] +name = "arbiter-engine" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fe11da508518e823bf75dd0e4c20fed24214c09f2458e7db933c9f6dbfd885e" +dependencies = [ + "anyhow", + "arbiter-bindings 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "arbiter-core 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)", + "arbiter-macros 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "async-stream", + "async-trait", + "crossbeam-channel", + "ethers", + "futures", + "futures-util", + "serde", + "serde_json", + "thiserror", + "tokio", + "tokio-stream", + "toml 0.8.10", + "tracing", +] + +[[package]] +name = "arbiter-macros" +version = "0.1.1" +dependencies = [ + "quote", + "syn 2.0.48", +] + [[package]] name = "arbiter-macros" version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a685a9ee2090c7113919cb78d9e87e294b4f65e22f43c5938c301b853b78e411" dependencies = [ "quote", "syn 2.0.48", @@ -1406,8 +1480,8 @@ checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" name = "documentation" version = "0.0.1" dependencies = [ - "arbiter-bindings", - "arbiter-core", + "arbiter-bindings 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "arbiter-core 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)", "skeptic", "tokio", ] diff --git a/Cargo.toml b/Cargo.toml index e4f5cf35..37208e11 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,11 +28,17 @@ name = "project" path = "examples/template/src/main.rs" [workspace.dependencies] -# Local -arbiter-bindings = { path = "bindings" } -arbiter-core = { path = "core" } -arbiter-engine = { path = "engine" } -arbiter-macros = { path = "macros" } +# Arbiter local for development +# arbiter-bindings = { path = "arbiter-bindings" } +# arbiter-core = { path = "arbiter-core" } +# arbiter-engine = { path = "arbiter-engine" } +# arbiter-macros = { path = "arbiter-macros" } + +# Arbiter crates.io for release, these need to be used to do crate releases! +arbiter-bindings = "0.1.4" +arbiter-core = "0.10.2" +arbiter-engine = "0.2.0" +arbiter-macros = "0.1.1" revm = { version = "5.0.0", features = ["ethersdb", "std", "serde"] } revm-primitives = "=2.0.0" diff --git a/README.md b/README.md index 73ff2fc4..1f395702 100644 --- a/README.md +++ b/README.md @@ -8,126 +8,109 @@ [![Twitter Badge](https://badgen.net/badge/icon/twitter?icon=twitter&label)](https://twitter.com/primitivefi) ## Overview -**Arbiter** is a framework for stateful Ethereum smart-contract simulation. -The framework features an [`ethers-rs`](https://github.com/gakonst/ethers-rs) middleware built on top of [revm](https://github.com/bluealloy/revm) which allows the end user to interact with a sandboxed `revm` instance as if it were an Ethereum node. -This provides a familiar interface for interacting with the Ethereum Virtual Machine (EVM), but with unrivaled speed. -Furthermore, Arbiter provides containment and management for simulations. For a running list of vulnerabilities found with Arbiter, please see the [Vulnerability Corpus](https://primitivefinance.github.io/arbiter/vulnerability_corpus.html). +**Arbiter** is a blazing-fast Ethereum sandbox that lets developers orchestrate event-driven simulations. +The framework allows for fine-grained control over a (Rust) Ethereum Virtual Machine (EVM) to provide stateful Ethereum smart-contract interactions and the creation of behaviors that can be coalesced into complex scenarios or automation. +We use [`ethers-rs`](https://github.com/gakonst/ethers-rs) middleware on top of [revm](https://github.com/bluealloy/revm), which is used in ETH clients such as [`reth`](https://github.com/paradigmxyz/reth) as well as [`foundry`](https://github.com/foundry-rs/foundry). +This gives us speed, configurability, and modularity that feels like a lightweight custom Ethereum node. -The Arbiter workspace has five crates: -- `arbiter`: The binary crate that exposes a command line interface for initializing simulations via a templated repository and generating contract bindings needed for the simulation. -- `arbiter-core`: The lib crate that contains the core logic for the Arbiter framework including the `ArbiterMiddleware` discussed before, and the `Environment` which envelopes simulations. -- `arbiter-engine`: The lib crate that provides abstractions for building simulations, agents, and behaviors. -- `arbiter-bindings`: The lib crate that contains the bindings for utility smart contracts that are used for testing and development. -- `arbiter-macros`: The lib crate that contains the macros used in the `arbiter-engine` crate. - -The purpose of Arbiter is to provide a toolset to construct arbitrary agents that interact with an Ethereum-like environment of your design. -All contract bytecode is run directly using a blazing-fast EVM instance `revm` (which is used in live RPC nodes such as [`reth`](https://github.com/paradigmxyz/reth)) so that your contracts are tested in the exact same type of environment that they are deployed in. +The primary use of Arbiter is to probe the mechanism security of smart contracts. +If this interests you, please see the [Vulnerability Corpus](https://primitivefinance.github.io/arbiter/vulnerability_corpus.html). -## Docs -Here you can find the [Arbiter docs](https://primitivefinance.github.io/arbiter/) - -## Motivation +--- -Smart contract engineers need to test their contracts against a wide array of potentially adversarial environments and contract parameters. -The static stateless testing of contracts can only take you so far. -To truly test the security of a contract, you need to test it against a wide array of dynamic environments that encompass the externalities of Ethereum mainnet. -We wanted to do just that with Arbiter. - -Both smart contract and financial engineers come together in Decentralized Finance (DeFi) to build and deploy a wide array of complex decentralized applications as well as financial strategies respectively. -For the latter, a financial engineer may want to test their strategies against thousands of market conditions, contract settings, shocks, and autonomous or random or even AI agents all while making sure their strategy isn't vulnerable to bytecode-level exploits. - -To configure such a rich simulation environment on a test or local network is also possible with Arbiter by a change in choice of middleware. -The most efficient choice for getting robust, yet quick, simulations would bypass any networking and use a low level language's implementation of the EVM. -Furthermore, we can gain control over the EVM worldstate by working directly on `revm`. -We would like the user to have a choice in how they want to simulate their contracts and Arbiter provides that choice. +The Arbiter workspace has five crates: +- `arbiter`: The bin that exposes a command line interface for forking and binding contracts. +- `arbiter-core`: A lib containing the core logic for the Arbiter framework, including the `ArbiterMiddleware` discussed before, and the `Environment`, our sandbox. +- `arbiter-engine`: A lib that provides abstractions for building simulations, agents, and behaviors. +- `arbiter-macros`: A lib crate that contains the macros used to simplify development with Arbiter. +- `arbiter-bindings`: A lib crate containing bindings for utility smart contracts used for testing and development. -### Sim Driven Development and Strategization -Test driven development is a popular engineering practice to write tests first, which fail, and implement logic to get the test to eventually pass. -With simulation driven development, it's possible to build "tests" that can only pass if the *incentives* actually work. For example, a sim driven test might be `is_loan_liquidated`, and a simulation must be made for a liquidator agent to do the liquidation. -This approach significantly improves the testing of economic systems and other mechanism designs, which is important in the world of networks that are mostly incentive driven. +## Book +Here you can find the [Arbiter Documentation](https://primitivefinance.github.io/arbiter/). +This is an mdbook that provides higher level understanding of how to use the entirety of the Arbiter framework. -The same goes with developing strategies that one would like to deploy on a live Ethereum network. -One can use Arbiter to simulate their strategy with an intended goal and see if it actually works. -This is especially important in the world of DeFi where strategies are often a mix of on and offchain and are susceptible to exploits. +## Motivation +Arbiter was built to allow you to work with smart contracts in a stateful sandbox and design agents that can be used alongside the contracts. +This gives you many capabilities. +For instance, smart contract engineers must test their contracts against various potentially adversarial environments and parameters and not rely on static stateless testing. -## Installation +In Decentralized Finance (DeFi), a wide array of complex decentralized applications can use the testing described above. Still, implicit financial strategies also encompass many agents and parameterizations. +A financial engineer may want to test their strategies against thousands of market conditions, contract settings, shocks, and autonomous or random AI agents while ensuring their approach isn't vulnerable to bytecode-level exploits. +Likewise, the same engineer may want to develop searcher agents, solver agents, or other autonomous agents that can be run on the blockchain. -To install Arbiter, you will need to have Rust installed on your machine. +## Working with the Arbiter Framework +To work with Arbiter, you must have Rust installed on your machine. You can install Rust by following the instructions [here](https://www.rust-lang.org/tools/install). -Once you have Rust installed, you can install Arbiter by running the following commands: - +It will also be helpful to get the `cargo-generate` package, which you can install by doing: ```bash -cargo install arbiter +cargo install cargo-generate ``` -This will install the Arbiter binary on your machine. You can then run `arbiter --help` to see that Arbiter was installed properly as well as see the help menu. - -## Command Line Interface - -The Arbiter binary provides a CLI for creating new project much like [Foundry](https://github.com/foundry-rs/foundry), which Arbiter aims to work alongside with. -It also gives you the abilities to fork a state of an EVM network and store it to disk so that you can use this fork in a simulation. - -### Initialization -To create a new project, you should have Foundry installed. -You can find the installation [here](https://getfoundry.sh/). -To create a new Arbiter project, you can run: +### Examples +We have an example that will run what we have set up in a template. +To run this, you can clone the repository and update the submodules: ```bash -arbiter init your-project-name -cd your-project-name +git clone https://github.com/primitivefinance/arbiter.git +cd arbiter +git submodule update --init --recursive ``` - -This initializes a new Arbiter project with a template. You can generate the bindings again by running: - +From here, you can now try running the following from the clone's root directory: ```bash -arbiter bind +cargo run --example template ``` +This command will enter the template CLI and show you the commands and flags. -The template is executable at this point and you can run it by running: +To run the `ModifiedCounter.sol` example and see some logging, try: ```bash -cargo run +cargo run --example template simulate examples/template/configs/example.toml -vvv ``` +This sets the log level to `debug` so you can see what's happening internally. -**Optional Arguments** - -You can run `arbiter init --no-git` to remove the `.git` directory from the template upon initialization. +### Initialization +To create your own Arbiter project off of our template [arbiter-template](https://github.com/primitivefinance/arbiter-template), you can run the following: +```bash +cd +cargo generate https://github.com/primitivefinance/arbiter-template.git +``` +You'll be prompted to provide a project name, and the rest will be set up for you! +### Binary +To install the Arbiter binary, run: +```bash +cargo install Arbiter +``` +This will install the Arbiter binary on your machine. You can then run `arbiter --help` to see that Arbiter was correctly installed and see the help menu. ### Bindings - -You can load or write your own smart contracts in the `arbiter-bindings/contracts/` directory and begin writing your own simulations. -Arbiter treats Rust smart-contract bindings as first-class citizens. The contract bindings are generated via Foundry's `forge` command. -`arbiter bind` wraps `forge` with some convenience features that will generate all your bindings to src/bindings as a rust module. -[Foundry](https://github.com/foundry-rs/foundry) power-users are welcome to use `forge` directly. - +You can load or write your own smart contracts in the `contracts/` directory of your templated project and begin writing your own simulations. +Arbiter treats Rust smart-contract bindings as first-class citizens. +The contract bindings are generated via Foundry's `forge` command. +`arbiter bind` wraps `forge` with convenience features that generate all your bindings to `src/bindings` as a Rust module. +[Foundry](https://github.com/foundry-rs/foundry) power-users can use `forge` directly. ### Forking - To fork a state of an EVM network, you must first create a fork config file. -An example is provided in the `example_fork` directory. -Essentially, you provide your storage location for the data, the network you want the block number you want, and metadata about the contracts you want to fork. +An example is provided in the `examples/fork` directory. +Essentially, you provide your storage location for the data, the network you want, the block number you want, and metadata about the contracts you want to fork. ```bash arbiter fork ``` +This will create a fork of the network you specified in the config file and store it in your specified location. +It can then be loaded into an `arbiter-core` `Environment` using the `Fork::from_disk()` method. -This will create a fork of the network you specified in the config file and store it in the location you specified. -It can then be loaded into an `arbiter-core` `Environment` by using the `Fork::from_disk()` method. - -Forking is done this way to make sure that all emulation done does not require a constant connection to an RPC-endpoint. +Forking is done this way to ensure that all emulation does not require a constant connection to an RPC endpoint. +You may find that [Anvil](https://book.getfoundry.sh/anvil/) has a more accessible forking interface. However, an online forking mechanism makes RPC calls to update the state as necessary. +Arbiter `Environment` forking is for creating a state, storing it locally, and being able to initialize a simulation from that state when desired. +We plan to allow `arbiter-engine` to integrate with other network types, such as Anvil, in the future! **Optional Arguments** You can run `arbiter fork --overwrite` to overwrite the fork if it already exists. +## Cargo Docs -### Optional Arguments - -You can run `arbiter init --no-git` to remove the `.git` directory from the template upon initialization. - - -## Documentation - -To see the documentation for the Arbiter crates, please visit the following: +To see the Cargo docs for the Arbiter crates, please visit the following: - [`arbiter`](https://docs.rs/crate/arbiter/) - [`arbiter-bindings`](https://docs.rs/crate/arbiter-bindings/) - [`arbiter-core`](https://docs.rs/arbiter-core/) @@ -137,18 +120,16 @@ To see the documentation for the Arbiter crates, please visit the following: You will find each of these on crates.io. ## Benchmarks - In `arbiter-core`, we have a a small benchmarking suite that compares the `ArbiterMiddleware` implementation over the `Environment` to the [Anvil](https://github.com/foundry-rs/foundry/tree/master/crates/anvil) local testnet chain implementation. -The biggest reasons why we chose to build Arbiter was to gain more control over the EVM environment and to have a more robust simulation framework, but we also wanted to gain in speed which is why we chose to build our own interface over `revm` as opposed to using Anvil (which does use `revm` under the hood). -For the following, Anvil was set to mine blocks for each transaction as opposed to setting an enforced block time and the `Environment` was set with a block rate of 10.0 (this was chosen somewhat arbitrarily as we will add in more block control in the future). +The biggest reasons we chose to build Arbiter was to gain more control over the EVM environment and to have a more robust simulation framework. Still, we also wanted to gain speed, so we chose to build our own interface over `revm` instead of using Anvil (which uses `revm` under the hood). +For the following, Anvil was set to mine blocks for each transaction instead of setting an enforced block time. The `Environment` was configured with a block rate of 10.0. Preliminary benchmarks of the `ArbiterMiddleware` interface over `revm` against Anvil are given in the following table. -to run the benchmarking code yourself, you can run: +To run the benchmarking code yourself, you can run: ```bash cargo bench --package arbiter-core ``` -bench from 10/24/23 arbiter-core v0.6.3 | Operation | ArbiterMiddleware | Anvil | Relative Difference | |-----------------|-----------------|--------------|---------------------| | Deploy | 238.975µs | 7712.436µs | ~32.2729x | @@ -159,34 +140,33 @@ bench from 10/24/23 arbiter-core v0.6.3 The above can be described by: - Deploy: Deploying a contract to the EVM. -We deployed both `ArbiterToken` and `ArbiterMath` in this method, so you can divide the time by two to get an estimate for the time it takes to deploy a single contract. +In this method, we deployed both `ArbiterToken` and `ArbiterMath`, so you can divide the time by two to estimate the time it takes to deploy a single contract. -- Lookup: Looking up a the `balanceOf` for a client's address for `ArbiterToken`. -We called `ArbiterToken`'s `balanceOf` function 100 times in this method. -Divide by 100 to get the time it takes to lookup a single balance. +- Lookup: Look up the `balanceOf` for a client's address for `ArbiterToken`. +In this method, we called `ArbiterToken`'s `balanceOf` function 100 times. +Divide by 100 to get the time to look up a single balance. - Stateless Call: Calling a contract that does not mutate state. -We called `ArbiterMath`'s `cdf` function 100 times in this method. -Divide by 100 to get the time it takes to call a single stateless function. +In this method, we called `ArbiterMath`'s `cdf` function 100 times. +Divide by 100 to get the time to call a single stateless function. - Stateful Call: Calling a contract that mutates state. -We called `ArbiterToken`'s `mint` function 100 times in this call. -Divide by 100 to get the time it takes to call a single stateful function. +In this call, we called `ArbiterToken`'s `mint` function 100 times. +Divide by 100 to get the time to call a single stateful function. -The benchmarking code can be found in the `arbiter-core/benches/` directory and these specific times were achieved over a 1000 run average. -The above was achieved running `cargo bench --package arbiter-core` which will automatically run with the release profile. -Times were achieved on an Apple Macbook Pro M1 Max with 8 performance and 2 efficiency cores, and with 32GB of RAM. +The benchmarking code can be found in the `arbiter-core/benches/` directory, and these specific times were achieved over a 1000 run average. +The above was achieved by running `cargo bench --package arbiter-core`, which will automatically run with the release profile. +Times were achieved on an Apple Macbook Pro M1 Max with 8 performance and 2 efficiency cores and 32GB of RAM. Of course, the use cases of Anvil and the `ArbiterMiddleware` can be different. -Anvil represents a more realistic environment with networking and mining, while the `ArbiterMiddleware` is simpler environment with the bare essentials to running stateful simulations. +Anvil represents a more realistic environment with networking and mining. At the same time, the `ArbiterMiddleware` is a simpler environment with the bare essentials to running stateful simulations. Anvil also mines blocks for each transaction, while the `ArbiterMiddleware` does not. -We hope to improve our API to allow the end user to be able to interface with their own choice of EVM environment to suit what ever their needs may be! -Please let us know if you find any issues with these benchmarks or if you have any suggestions on how to improve them! +Please let us know if you need any help with these benchmarks or suggestions for improving them! ## Testing -If you contribute please write tests for any new code you write, To run the tests, you can run: +If you contribute, please write tests for any new code you write. To run the tests, you can run the following: ```bash cargo test --all --all-features diff --git a/docs/src/usage/arbiter_macros.md b/docs/src/usage/arbiter_macros.md index ee85cd6b..63ce8d19 100644 --- a/docs/src/usage/arbiter_macros.md +++ b/docs/src/usage/arbiter_macros.md @@ -1,5 +1,62 @@ # Arbiter macros `arbiter_macros` provides a set of macros to help with the use of `arbiter-engine` and `arbiter-core`. -At the moment, we only have one proc macro: `#[derive(Behaviors)]`. -This macro will generate an implementation of a `CreateStateMachine` trait for the `Behaviors` enum and ultimately save you from having to write a lot of boilerplate code. -See the [Configuration](./arbiter_engine/configuration.md) page for more information on how to use this macro. \ No newline at end of file +Macros allow for code generation which enables developers to write code that writes code. +We use them here to reduce boilerplate by abstracting repetitive patterns. +Macros can be used for tasks like deriving traits automatically or for generating code based on custom attributes. + +## Procedural Macros + +> **`#[derive(Behaviors)]`** +This Rust procedural macro automatically implements the [CreateStateMachine](https://github.com/primitivefinance/arbiter/blob/ffbbd146dc05f3e1088a9df5cf78452a1bef2212/macros/src/lib.rs#L68) trait for an enum, generating a [create_state_machine](https://github.com/primitivefinance/arbiter/blob/ffbbd146dc05f3e1088a9df5cf78452a1bef2212/macros/src/lib.rs#L26) method that matches each enum variant to a new state machine instance. +It's designed for enums where each variant contains a single unnamed field representing state data. +This macro simplifies the creation of state machines from enums, eliminating repetitive boilerplate code and enhancing code maintainability. + +### Example +You can use this macro like so: +```rust, ignore +use arbiter_macros::Behaviors; +use arbiter_engine::machine::Behavior; + +struct MyBehavior1 {} +impl Behavior for MyBehavior1 { + // ... +} +struct MyBehavior2 {} + +} +impl Behavior for MyBehavior2 { + // ... +} + +#[derive(Behaviors)] +enum Behaviors { + MyBehavior1(MyBehavior1), + MyBehavior2(MyBehavior2), +} +``` + +> **`#[main]`**. +The [`#[arbiter_macros::main]`](https://github.com/primitivefinance/arbiter/blob/ffbbd146dc05f3e1088a9df5cf78452a1bef2212/macros/src/lib.rs#L161) macro in `arbiter-macros/src/lib.rs` is designed to simplify the creation of a CLI that will let you run your simulations by automatically generating a `main` function that sets up command-line parsing, logging, async execution, and world creation. +It takes custom attributes to configure the application's metadata such as the project's name, description, and the set of behaviors you want to use. +Under the hood, it uses the [clap](https://crates.io/crates/clap) crate for parsing CLI arguments and [tracing](https://crates.io/crates/tracing) for logging based on verbosity level. +The macro needs to have have an object that has the `CreateStateMachine` trait implemented which can be done using the `#[derive(Behaviors)]` macro. + + +## Usage +You can find an example that uses both of these macros in the [arbiter-template repository](https://github.com/primitivefinance/arbiter-template). +Similarly, in the Arbiter repo itself, this exact same collection of code is found in the `examples/template/` directory. + +If you wanted to use the `#[main]` macro alongside the `#[derive(Behaviors)]` macro, you would do so like this: +```rust, ignore +use arbiter_macros::main; + +use Behaviors; // From the Behaviors example above + + +#[main( + name = "ExampleArbiterProject", + about = "Our example to get you started.", + behaviors = Behaviors +)] +pub async fn main() {} +``` \ No newline at end of file diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 5cd2354b..d9eb301a 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -10,20 +10,49 @@ use syn::{ Result as ParseResult, Type, }; +/// A procedural macro to derive the `Behaviors` trait for enums. +/// +/// This macro generates an implementation of the `CreateStateMachine` trait for +/// the specified enum, facilitating the creation of state machines from enum +/// variants. It is designed to work exclusively with enums where each variant +/// contains unnamed fields, ideally a single field that represents the +/// state data for that variant. +/// +/// # Panics +/// The macro will panic if it is applied to anything other than an enum, or if +/// any of the enum's variants do not contain exactly one unnamed field. +/// +/// # Usage +/// Attach this macro to an enum definition to automatically implement the +/// `CreateStateMachine` trait for it. Each variant of the enum must contain a +/// single unnamed field that implements the `StateMachine` trait. +/// +/// ```ignore +/// #[derive(Behaviors)] +/// enum MyBehavior { +/// StateOne(StateDataOne), +/// StateTwo(StateDataTwo), +/// } +/// ``` #[proc_macro_derive(Behaviors)] pub fn create_behavior_from_enum(input: TokenStream) -> TokenStream { + // Parse the input TokenStream into a DeriveInput object. let input = parse_macro_input!(input as DeriveInput); - let name = input.ident; // The name of the enum + // Extract the identifier (name) of the enum. + let name = input.ident; + // Attempt to extract enum data, panicking if the input is not an enum. let enum_data = if let Data::Enum(DataEnum { variants, .. }) = input.data { variants } else { - // Not an enum, so panic or handle as needed panic!("CreateBehaviorFromEnum is only defined for enums"); }; + // Generate match arms for the `create_state_machine` function, one for each + // enum variant. let match_arms = enum_data.into_iter().map(|variant| { + // Extract the variant name and the type of its single unnamed field. let variant_name = variant.ident; let _inner_type = if let Fields::Unnamed(fields) = variant.fields { fields.unnamed.first().unwrap().ty.clone() @@ -31,6 +60,8 @@ pub fn create_behavior_from_enum(input: TokenStream) -> TokenStream { panic!("Expected unnamed fields in enum variant"); }; + // Generate a match arm that constructs a new state machine instance for the + // variant. quote! { #name::#variant_name(inner) => { Box::new(Engine::new(inner)) @@ -38,6 +69,8 @@ pub fn create_behavior_from_enum(input: TokenStream) -> TokenStream { } }); + // Generate the full implementation of the `CreateStateMachine` trait for the + // enum. let expanded = quote! { impl CreateStateMachine for #name { fn create_state_machine(self) -> Box { @@ -48,27 +81,57 @@ pub fn create_behavior_from_enum(input: TokenStream) -> TokenStream { } }; + // Convert the generated code back into a TokenStream to be returned from the + // macro. TokenStream::from(expanded) } -// Define a custom struct to parse our specific macro attributes +/// `MacroArgs` is a struct designed to capture and store the attributes +/// provided to our custom macro. It specifically targets the parsing of `name`, +/// `about`, and `behaviors` attributes, which are essential for configuring the +/// behavior of the macro in a more dynamic and descriptive manner. +/// +/// # Fields +/// - `name`: A `String` representing the name attribute of the macro. +/// - `about`: A `String` providing a brief description about the macro's +/// purpose or usage. +/// - `behaviors`: A `Type` indicating the type of behaviors that the macro will +/// generate or manipulate. struct MacroArgs { name: String, about: String, behaviors: Type, } +/// Implements the `Parse` trait for `MacroArgs`. +/// +/// This implementation is responsible for parsing the input `TokenStream` to +/// extract macro arguments into a `MacroArgs` struct. It specifically looks for +/// `name`, `about`, and `behaviors` fields within the input stream, parsing and +/// assigning them appropriately. +/// +/// # Arguments +/// * `input` - A `ParseStream` containing the input tokens to be parsed. +/// +/// # Returns +/// * `ParseResult` - A result containing `MacroArgs` if parsing succeeds, +/// or an error if it fails. impl Parse for MacroArgs { fn parse(input: ParseStream) -> ParseResult { + // Initialize variables to store parsed values. let mut name = String::new(); let mut about = String::new(); let mut behaviors: Option = None; + // Iterate through the input stream until it's empty. while !input.is_empty() { + // Look ahead in the input stream to determine the next token type. let lookahead = input.lookahead1(); if lookahead.peek(Ident) { + // Parse an identifier and an equals token. let ident: Ident = input.parse()?; let _eq_token: syn::token::Eq = input.parse()?; + // Match the identifier to known fields and parse their values. if ident == "name" { if let Lit::Str(lit_str) = input.parse()? { name = lit_str.value(); @@ -80,20 +143,24 @@ impl Parse for MacroArgs { } else if ident == "behaviors" { behaviors = Some(input.parse()?); } else { + // Return an error if the identifier is not recognized. return Err(lookahead.error()); } } else { + // Return an error if the lookahead does not match an identifier. return Err(lookahead.error()); } - // Parse `,` + // Parse a comma separator if the input stream is not empty. if !input.is_empty() { let _: syn::token::Comma = input.parse()?; } } + // Ensure `behaviors` is not None, returning an error if it is missing. let behaviors = behaviors.ok_or_else(|| input.error("missing behaviors"))?; + // Return the parsed `MacroArgs`. Ok(MacroArgs { name, about, @@ -102,6 +169,36 @@ impl Parse for MacroArgs { } } +/// A procedural macro attribute to generate a main function with async support +/// and CLI parsing. +/// +/// This macro parses the provided attributes to configure the CLI application, +/// including its name, version, and about information. It also sets up logging +/// based on the verbosity level specified through CLI arguments. +/// +/// The macro expects a specific structure of the input TokenStream, which +/// should define the behavior of the application, particularly how it handles +/// different commands specified through the CLI. +/// +/// # Parameters +/// - `attr`: TokenStream containing the macro attributes for configuring the +/// CLI application. +/// - `item`: TokenStream representing the input function, which contains the +/// logic for the application's behavior based on the parsed CLI arguments. +/// +/// # Returns +/// A TokenStream that, when executed, will act as the main function of a CLI +/// application. This includes setting up the CLI with `clap`, initializing +/// logging with `tracing`, and executing the application logic based on the +/// provided CLI arguments. +/// +/// # Example +/// ```ignore +/// #[main(name = "my_app", about = "An example application")] +/// fn app() { +/// // Application logic here +/// } +/// ``` #[proc_macro_attribute] pub fn main(attr: TokenStream, item: TokenStream) -> TokenStream { // Parse the macro attributes