# Banish
[](https://crates.io/crates/banish)
[](https://docs.rs/banish)
[](https://github.com/LoganFlaherty/banish/blob/main/LICENSE)
Banish is a declarative DSL for building rule-based state machines in Rust. States evaluate their rules until reaching a fixed point or triggering a transition, reducing control flow boilerplate.
```rust
use banish::banish;
// Will print all light colors twice
fn main() {
let mut ticks: i32 = 0;
banish! {
// Returns on the third entry immediately
#[max_entry = 2]
@red
announce ? {
ticks = 0;
println!("Red light");
}
timer ? ticks < 3 {
ticks += 1;
}
@green
announce ? { println!("Green light"); }
timer ? ticks < 6 {
ticks += 1;
}
@yellow
announce ? { println!("Yellow light"); }
timer ? ticks < 10 {
ticks += 1;
} !? { => @red; }
}
}
```
## Why Banish?
* **Fixed-Point Looping:** States automatically re-evaluate their rules until none of them fire, then advance.
* **Zero Runtime Overhead:** Banish is a procedural macro. It generates standard optimized Rust at compile time. No interpreter, no allocations, no virtual machine.
* **Full Rust Integration:** Rule bodies are plain Rust. Closures, external crates, mutable references. Everything works as you'd expect.
* **Self-Documenting Structure**: Named states and named rules make the shape of your logic readable at a glance, without requiring comments to explain what each block is doing.
* **Flexible Transitions:** States advance implicitly in declaration order by default. Explicit `=> @state` transitions let you jump anywhere when you need to.
## Comparison
Most state machines in Rust end up as a `loop` wrapping a `match` wrapping a pile of `if` chains with careful flag management. The structure of the problem gets lost in the structure of the code. Banish flips this around. You write the *what*, not the *how*.
```rust
// Without banish
let mut state = 0;
loop {
match state {
0 => {
let mut interaction = false;
if !initialized {
initialized = true;
value = compute();
interaction = true;
}
if initialized && value > threshold {
value = clamp(value);
interaction = true;
}
if initialized && value <= threshold {
state = 1;
interaction = true;
}
if !interaction { state = 1; }
}
1 => {
let mut interaction = false;
if !logged {
logged = true;
log(value);
interaction = true;
}
if logged && errors.is_empty() { return Ok(value); }
if logged && !errors.is_empty() { return Err(errors); }
if !interaction { break; }
}
_ => unreachable!()
}
}
// With banish
@normalize
setup ? !initialized {
initialized = true;
value = compute();
}
clamp ? value > threshold { value = clamp(value); }
@report
log ? !logged {
logged = true;
log(value);
}
finish ? errors.is_empty() { return Ok(value); }
fail ? !errors.is_empty() { return Err(errors); }
```
The manual version has three concerns tangled together: state indexing, interaction tracking, and the actual logic. The banish version is just the logic.
## Concepts
**States** (`@name`) group related rules. The machine starts at the first declared state and advances through them in order.
**Rules** (`name ? condition { body }`) fire when their condition is true. After firing, the state re-evaluates from the top. Once a full pass completes with no rules firing, the state has reached its fixed point and the machine advances.
**Conditionless rules** (`name ? { body }`) fire exactly once per state entry, on the first pass.
**Fallback branches** (`!? { body }`) run when a rule's condition is false, every pass.
**Explicit transitions** (`=> @state;`) jump to any named state immediately, bypassing the implicit scheduler.
**Return values** (`return expr;`) work naturally. Exits the entire `banish!` block with a value, just like returning from a closure.
## State Attributes
Attributes go above a state declaration and modify its behavior.
```rust
#[isolate, max_iter = 10 => @fallback, trace]
@my_state
...
```
| `isolate` | Removes the state from implicit scheduling. Only reachable via explicit `=> @state` transition. |
| `max_iter = N` | Caps the fixed-point loop to N iterations, then advances normally. |
| `max_iter = N => @state` | Same, but transitions to `@state` on exhaustion instead of advancing. |
| `max_entry = N` | Limits how many times this state can be entered. Returns on the (N+1)th entry. |
| `trace` | Prints state entry and rule evaluation to stderr. Useful for debugging. |
## Install
```toml
[dependencies]
banish = "1.2.0"
```
Or with cargo:
```
cargo add banish
```
## More Examples
See [`docs/README.md`](https://github.com/LoganFlaherty/banish/blob/main/docs/README.md) for more examples including game logic, search algorithms, and data pipelines.
## Contributing
Contributions are welcome. Before opening a PR, please open a discussion first. This keeps design decisions visible and avoids duplicated effort.
The test suite covers all documented behavior and edge cases. Run it locally before submitting:
```
cargo test
```
New behavior and edge cases should include corresponding tests.