| name | banish |
| description | Reference for writing Rust code using the banish state machine framework. Use when writing, reviewing, or debugging banish! blocks. |
| user-invocable | false |
Banish Framework Reference
Banish is a declarative Rust library for rule-based state machines. You declare states
and rules; the framework generates the fixed-point scheduler, interaction tracking, and
state advancement at compile time.
[dependencies]
banish = "1.4.1"
Core Syntax at a Glance
use banish::banish;
banish! {
#![async]
#![id = "my_machine"]
#![dispatch(entry_state)]
#![trace]
let mut count: u32 = 0;
#[max_entry = 3 => @bail]
@my_state
let mut idx: usize = 0;
name ? condition { body }
name ? condition { body } !? { else_body }
name ? let Pat = expr { body }
name ? let Pat = expr { body } !? { else_body }
name? { body }
=> @other_state if condition;
=> @other_state;
}
Execution Model
Within each state, rules evaluate top to bottom repeatedly until a full pass fires
nothing (fixed point). Then the scheduler advances to the next non-isolated state.
enter state
└─ evaluate rules top-to-bottom
├─ any rule fired? → re-evaluate from top
└─ no rules fired → fixed point → advance to next state
- A rule "fires" when its condition is
true. Firing sets __interaction = true.
!? fallback branches do not set __interaction — they never cause re-evaluation.
- Conditionless rules (
name? { }) fire once per entry (first pass only).
- Pattern conditions (
name ? let Pat = expr { }) use if let semantics. The rule fires when the pattern matches, binding variables into the body.
- Rule order matters: earlier rules can change state that affects later rules.
Variables
| Scope | Where declared | Lifetime | Resets on re-entry? |
|---|
| Block-level | Before first @state | Entire machine | No |
| State-level | After @state, before first rule | One state entry | Yes — every entry |
Transitions
=> @target_state;
=> @target_state if some_condition;
Transitions inside a rule body act immediately. They do not need to be the last
statement. Code after a taken guarded transition is unreachable.
Return Values
return expr; exits the entire banish! block with a value. The block is an
expression and can be assigned or returned from a function.
let result: String = banish! {
@work
done ? finished { return compute_result(); }
};
break exits the current state and lets the scheduler advance normally.
continue restarts rule evaluation from the top of the current state immediately.
State Attributes
#[isolate]
#[max_iter = N]
#[max_iter = N => @state]
#[max_entry = N]
#[max_entry = N => @state]
#[trace]
Multiple attributes are comma-separated on one line:
#[isolate, max_iter = 1 => @finish]
@handler
...
Isolated States
Isolated states are invisible to the scheduler. They are only entered via an
explicit => @state transition. They must have a defined exit path (transition or
return), or use max_iter with a redirect.
#[isolate]
@error
handle? { return Err("failed"); }
Async
banish! {
#![async]
@fetch
load? { let data = some_future().await; }
}
Or use the function attribute to avoid manual wiring:
#[banish::machine]
#[tokio::main]
async fn main() {
banish! { ... }
}
Dispatch (Resumable Machines)
use banish::BanishDispatch;
#[derive(BanishDispatch)]
enum Stage { Validate, Process, Finalize }
fn run(order: Order, resume: Stage) -> Result {
banish! {
#![dispatch(resume)]
@validate ...
@process ...
@finalize done? { return Ok(()); }
#[isolate]
@rejected handle? { return Err("bad"); }
}
}
BanishDispatch maps PascalCase variant names → snake_case state names.
Stage::Validate → "validate" → @validate. Passing a variant with no matching
state panics at runtime.
Tracing
banish = { version = "1.4.0", features = ["trace-logger"] }
fn main() {
banish::init_trace(None);
banish::init_trace(Some("t.log"));
...
}
Or bring your own log-compatible backend. Diagnostics are emitted as log::trace!.
Patterns & Gotchas
Cycling between states — use max_iter with a redirect instead of an explicit
loop, to avoid unbounded recursion:
#[max_iter = 1 => @b]
@a attack? { ... }
#[max_iter = 1 => @a]
@b attack? { ... }
One-shot setup per state entry: conditionless rules run once, then never again
until the state is re-entered:
@load
init? { data = load_data(); }
process ? data.is_some() { ... }
Fallback as else-branch that needs a loop: fallbacks do not re-evaluate; use an
explicit transition if re-evaluation is needed after the else branch:
valid ? x > 0 { use(x); } !? { x = default; => @reset; }
Rule order is evaluation order: if rule A mutates a value rule B checks, put A
before B to make B see the updated value in the same pass.
Pattern conditions bind into the rule body: use name ? let Pat = expr { } to
match and destructure in one step. Combine with !? to handle the non-matching case:
@drain
pop ? let Some(val) = queue.pop() {
process(val);
} !? { return result; }
State-level let resets every entry: don't store cross-entry state there; use
block-level variables instead.
#[banish::machine] must be the outermost attribute: it runs first and transforms
the function before any runtime macro (e.g. #[tokio::main]) sees it.