ワンクリックで
banish
Reference for writing Rust code using the banish state machine framework. Use when writing, reviewing, or debugging banish! blocks.
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
メニュー
Reference for writing Rust code using the banish state machine framework. Use when writing, reviewing, or debugging banish! blocks.
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
SOC 職業分類に基づく
| 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 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"
use banish::banish;
banish! {
// Block attributes (optional, must come first)
#![async]
#![id = "my_machine"]
#![dispatch(entry_state)]
#![trace]
// Block-level variables (live entire machine lifetime)
let mut count: u32 = 0;
// State declaration
#[max_entry = 3 => @bail]
@my_state
// State-level variables (reset on every entry)
let mut idx: usize = 0;
// Conditional rule
name ? condition { body }
// Conditional rule with fallback (fallback does NOT re-evaluate)
name ? condition { body } !? { else_body }
// Pattern condition fires when the pattern matches (if let semantics)
name ? let Pat = expr { body }
name ? let Pat = expr { body } !? { else_body }
// Conditionless rule (fires exactly once per entry, on first pass)
name? { body }
// Guarded transition (inside a rule body)
=> @other_state if condition;
// Unconditional transition (inside a rule body)
=> @other_state;
}
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
true. Firing sets __interaction = true.!? fallback branches do not set __interaction — they never cause re-evaluation.name? { }) fire once per entry (first pass only).name ? let Pat = expr { }) use if let semantics. The rule fires when the pattern matches, binding variables into the body.| 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 |
// Implicit — scheduler advances to next non-isolated state after fixed point
// (no code needed)
// Explicit — jump immediately, bypasses scheduler
=> @target_state;
// Guarded — conditional jump; execution continues in rule body if false
=> @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 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.
#[isolate] // removed from scheduler; explicit transition only
#[max_iter = N] // cap fixed-point loop; advance normally on exhaustion
#[max_iter = N => @state] // cap fixed-point loop; transition on exhaustion
#[max_entry = N] // return on (N+1)th entry
#[max_entry = N => @state] // transition on (N+1)th entry
#[trace] // emit log::trace! diagnostics (needs log backend)
Multiple attributes are comma-separated on one line:
#[isolate, max_iter = 1 => @finish]
@handler
...
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"); }
banish! {
#![async]
@fetch
load? { let data = some_future().await; }
}
// The banish! block is now an async expression — must be .awaited
Or use the function attribute to avoid manual wiring:
#[banish::machine] // must be outermost — before #[tokio::main]
#[tokio::main]
async fn main() {
banish! { ... } // async + .await injected automatically; id = "main"
}
use banish::BanishDispatch;
#[derive(BanishDispatch)]
enum Stage { Validate, Process, Finalize }
fn run(order: Order, resume: Stage) -> Result {
banish! {
#![dispatch(resume)] // entry state determined at runtime
@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.
banish = { version = "1.4.0", features = ["trace-logger"] }
fn main() {
banish::init_trace(None); // stderr
banish::init_trace(Some("t.log")); // file
...
}
Or bring your own log-compatible backend. Diagnostics are emitted as log::trace!.
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(); } // fires once
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.