Banish v1.3.0: Becoming a Framework
This release adds the block attribute system, async support, runtime dispatch, a trait boundary for external types, and enough surrounding tooling that I can confidently declare that banish is a declarative framework for rule-based state machines.
This release also represents a lot of personal growth. Banish has been as much a lesson in language design, compiler internals, and managing something at this scale has taught me things about software design that I could not have learned any other way.
Block Attributes
Everything in this release is built on top of block attributes. Written as #![...] at the top of a banish! block, they configure the behavior of the entire machine in one place. Previous releases gave you control at the state level. Now the block itself is configurable.
banish! {
#![async, id = "fetcher", dispatch(entry)]
@fetch
...
}Async
#![async] expands the block to an async move { ... } expression. From there, .await works exactly as you would expect inside any rule body. Banish does not pick a runtime for you. Tokio, async-std, smol, anything that can drive a future works without any integration code.
#[tokio::main]
async fn main() {
banish! {
#![async]
@fetch
load? {
let data = reqwest::get("https://example.com").await?;
return data;
}
}.await;
}Dispatch and BanishDispatch
With states and transitions solid, the next question was how to configure where a machine begins. #![dispatch(expr)] answers it. Pass in an enum variant and the machine enters the matching state directly, making machines resumable and configurable at the call site without any extra wiring.
#[derive(BanishDispatch)]
enum PipelineState {
Normalize,
Validate,
Finalize,
}
let entry = PipelineState::Validate;
banish! {
#![dispatch(entry)]
@normalize
...
@validate
...
@finalize
done? { return; }
}BanishDispatch is the trait that makes this work. #[derive(BanishDispatch)] generates a zero-allocation variant_name implementation that maps each PascalCase variant to its snake_case state name. Manual implementations are supported for cases where custom naming is needed.
#[banish::machine]
Once async and id were both block attributes, async functions needed both every time. #[banish::machine] handles the setup automatically. It injects #![async], appends .await, and sets id to the function name. None of it fires if the corresponding attribute is already written explicitly.
#[banish::machine]
#[tokio::main]
async fn pipeline() {
banish! {
@process
step? { do_work().await; return; }
}
}Tracing
Tracing has been a state-level feature for a while. It is now a block-level one too. #![trace] enables diagnostics for every state at once, and id = "name" labels the output so machines are distinguishable when multiple are running.
[banish:pipeline] entering state `validate`
[banish:pipeline] rule `check`: condition = true
[banish:pipeline] rule `route`: condition = false
banish::init_trace is available behind the trace-logger feature for quick setup. Pass None to print to stderr or Some("path") to write to a file.
Transition Guards
Nested transitions have always been a known gap. => @state if condition; closes the most common case. The jump happens when the condition is true. When it is false, execution continues with the remaining statements in the rule body, no separate rule required.
step? {
do_work();
=> @done if finished;
log_progress(); // runs only when the guard is false
}Variable Declarations
Variables can now be declared at two scopes. Block-level declarations appear before the first state and live for the entire machine lifetime. State-level declarations appear before the first rule and re-initialize on every entry, making them useful for per-pass scratch values that should not persist across entries.
banish! {
let mut total = 0; // lives for the whole machine
@process
let mut dirty = false; // resets on every entry to @process
check ? condition { dirty = true; }
flush ? dirty { write(); return; }
}Bug Fixes
Isolated states are now correctly ignored as the entry state. Previously a machine whose first declared state was isolated would incorrectly enter it on startup instead of skipping to the first non-isolated state.
Fixed nested returns not being recognized in validation.
v1.3.1
A small patch to correct a small bug where acronym enums would not convert as intended.
v1.4.0
This is the most recent minor release which added tighter no_std support and pattern matching in rule condition statements.
https://github.com/LoganFlaherty/banish/releases/tag/v1.4.0
What's Next
The foundation is where it needs to be. If you have been following along and have a use case that does not quite fit, open a discussion. The design conversations are where the interesting decisions happen.
Full documentation is in the [Reference](https://github.com/LoganFlaherty/banish/blob/main/docs/reference.md).