banish 1.2.0

A declarative DSL for building rule-based state machines.
Documentation

Banish

Crates.io Docs.rs 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.

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.

// 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.

#[isolate, max_iter = 10 => @fallback, trace]
@my_state
    ...
Attribute Description
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

[dependencies]

banish = "1.2.0"

Or with cargo:

cargo add banish

More Examples

See 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.