172 lines
6.3 KiB
Rust
172 lines
6.3 KiB
Rust
//! RFC 017 — the **same** machine as `examples/statem_fused.rs`, written through
|
|
//! the `gen_statem!` macro. Diff this file against that one to see exactly what
|
|
//! the macro buys: every `// ===` section there that was boilerplate (the `Ev`
|
|
//! enum, the `DoorSm` struct, `start`, the whole `Machine` impl, the `enter`
|
|
//! dispatch, the stay/transition apply-tail) collapses into the invocation
|
|
//! below. What stays hand-written is what carries meaning: the four types, the
|
|
//! per-state successor enum, and the handler fns.
|
|
//!
|
|
//! The point of the exercise is that the macro is *pure sugar*: the four
|
|
//! compile-time guarantees the fused spike demonstrates are properties of the
|
|
//! emitted code, not of the macro, so they survive expansion unchanged. The
|
|
//! BREAK-CASE MENU at the bottom is the same four cases, re-expressed against
|
|
//! the macro surface — flip any one on and the compiler fires identically.
|
|
//!
|
|
//! Run: `cargo run --example statem_macro`
|
|
|
|
#![deny(dead_code)] // guarantee #3 (orphan handlers); the macro denies the
|
|
// dispatch's own unreachable_patterns internally.
|
|
|
|
use smarm::gen_statem;
|
|
use smarm::run;
|
|
use smarm::gen_statem::Reply;
|
|
|
|
// === user types (identical to statem_fused.rs) =============================
|
|
|
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
|
enum Door {
|
|
Open,
|
|
Closed,
|
|
Locked,
|
|
}
|
|
|
|
struct Data {
|
|
enters: u32, // total state entries (incl. initial)
|
|
pushes: u32, // times a push closed the door
|
|
}
|
|
|
|
enum Cast {
|
|
Push,
|
|
Pull,
|
|
Lock,
|
|
Unlock(u32), // carries a key
|
|
}
|
|
|
|
enum Call {
|
|
GetState(Reply<Door>),
|
|
GetEnters(Reply<u32>),
|
|
GetPushes(Reply<u32>),
|
|
}
|
|
|
|
const CODE: u32 = 1234;
|
|
|
|
// === per-row successor enum for the one branching row ======================
|
|
// `Locked + Unlock` may end in Closed (right key) or Locked (wrong key) — and
|
|
// nothing else. This type IS that declared set; `on_unlock` cannot name Open.
|
|
enum UnlockOutcome {
|
|
Closed,
|
|
Locked,
|
|
}
|
|
|
|
impl From<UnlockOutcome> for Door {
|
|
fn from(o: UnlockOutcome) -> Door {
|
|
match o {
|
|
UnlockOutcome::Closed => Door::Closed,
|
|
UnlockOutcome::Locked => Door::Locked,
|
|
}
|
|
}
|
|
}
|
|
|
|
// === module-private handlers (side effects / branch choice only) ===========
|
|
// Reachable solely through the table below. Orphan one and it is dead_code.
|
|
|
|
fn on_push(data: &mut Data) {
|
|
data.pushes += 1;
|
|
}
|
|
|
|
fn on_unlock(key: u32) -> UnlockOutcome {
|
|
if key == CODE {
|
|
UnlockOutcome::Closed
|
|
} else {
|
|
UnlockOutcome::Locked // wrong key: caller will see this == current -> stay
|
|
}
|
|
}
|
|
|
|
// === the machine ===========================================================
|
|
// Everything below — Ev, DoorSm, start, Machine, enter — is generated.
|
|
|
|
gen_statem! {
|
|
machine: DoorSm { state: Door, data: Data };
|
|
event: Ev { cast: Cast, call: Call };
|
|
|
|
// You name the bindings the bodies use; the macro can't lend you its own
|
|
// `self`/`cx` across macro hygiene. `data` = &mut Data, `prev` = current
|
|
// state tag, `cx` = context handle (unused in chunk 1).
|
|
context(data, prev, cx);
|
|
|
|
enter {
|
|
_ => data.enters += 1,
|
|
}
|
|
|
|
on Door::Open => {
|
|
cast Cast::Push => { on_push(data); Door::Closed },
|
|
cast Cast::Pull | Cast::Lock | Cast::Unlock(_) => unhandled,
|
|
}
|
|
on Door::Closed => {
|
|
cast Cast::Pull => Door::Open,
|
|
cast Cast::Lock => Door::Locked,
|
|
cast Cast::Push | Cast::Unlock(_) => unhandled,
|
|
}
|
|
on Door::Locked => {
|
|
cast Cast::Unlock(key) => on_unlock(key), // branch -> UnlockOutcome
|
|
cast Cast::Push | Cast::Pull | Cast::Lock => unhandled,
|
|
}
|
|
|
|
// state-independent queries (reply, then stay via `prev`)
|
|
on _ => {
|
|
call Call::GetState(r) => { r.reply(prev); prev },
|
|
call Call::GetEnters(r) => { r.reply(data.enters); prev },
|
|
call Call::GetPushes(r) => { r.reply(data.pushes); prev },
|
|
}
|
|
}
|
|
|
|
fn main() {
|
|
run(|| {
|
|
let door = DoorSm::start(Door::Closed, Data { enters: 0, pushes: 0 });
|
|
|
|
door.send(Ev::Cast(Cast::Lock)).unwrap(); // Closed -> Locked
|
|
door.send(Ev::Cast(Cast::Push)).unwrap(); // Locked: Push invalid -> Unhandled
|
|
door.send(Ev::Cast(Cast::Unlock(0))).unwrap(); // Locked: wrong key -> stay
|
|
door.send(Ev::Cast(Cast::Unlock(CODE))).unwrap(); // Locked -> Closed
|
|
door.send(Ev::Cast(Cast::Pull)).unwrap(); // Closed -> Open
|
|
door.send(Ev::Cast(Cast::Push)).unwrap(); // Open -> Closed (pushes=1)
|
|
|
|
let st = door.call(|r| Ev::Call(Call::GetState(r))).unwrap();
|
|
let enters = door.call(|r| Ev::Call(Call::GetEnters(r))).unwrap();
|
|
let pushes = door.call(|r| Ev::Call(Call::GetPushes(r))).unwrap();
|
|
|
|
println!("state={st:?} enters={enters} pushes={pushes}");
|
|
assert_eq!(st, Door::Closed);
|
|
assert_eq!(enters, 5); // Closed(start) + Locked + Closed + Open + Closed
|
|
assert_eq!(pushes, 1);
|
|
println!("ok");
|
|
});
|
|
}
|
|
|
|
// ===========================================================================
|
|
// BREAK-CASE MENU — the four guarantees, through the macro. Each fires exactly
|
|
// as it does in the hand-written statem_fused.rs.
|
|
//
|
|
// 1. ORPHAN HANDLER (dead_code -> error):
|
|
// add `fn on_slam(_d: &mut Data) {}` and don't reference it.
|
|
// => error: function `on_slam` is never used
|
|
//
|
|
// 2. CONFLICTING ROW (unreachable_patterns):
|
|
// duplicate a row, e.g. add a second
|
|
// `cast Cast::Push => unhandled,` under `on Door::Open`.
|
|
// NOTE: this example is a separate crate from `smarm`, so rustc's
|
|
// in_external_macro rule SILENCES this lint here even though the macro
|
|
// denies it — the duplicate compiles. The guarantee is real only for
|
|
// machines defined inside the smarm crate (see the unit test in
|
|
// src/statem.rs). This is the documented macro_rules! limitation.
|
|
//
|
|
// 3. MISSING PAIR (non-exhaustive match, E0004):
|
|
// delete the `cast Cast::Push | Cast::Pull | Cast::Lock => unhandled,`
|
|
// row under `on Door::Locked`.
|
|
// => error[E0004]: non-exhaustive patterns: ... not covered
|
|
//
|
|
// 4. OUT-OF-SET TARGET (E0599):
|
|
// in `on_unlock`, return `UnlockOutcome::Open`.
|
|
// => error[E0599]: no variant ... named `Open` found for enum `UnlockOutcome`
|
|
// ===========================================================================
|