timed_fsm/lib.rs
1//! A timed finite state machine framework.
2//!
3//! `timed-fsm` extends the classic finite state machine model with
4//! **declarative timer commands**. Transitions return a [`Response`]
5//! that includes not only output actions but also instructions to
6//! set or kill timers. This allows the state machine to express
7//! "if no event arrives within X ms, take action Y" without any
8//! side effects or platform dependencies.
9//!
10//! # Why not a regular FSM?
11//!
12//! A regular FSM transitions on `(State, Event) → (State, Action)`.
13//! It cannot express "the absence of an event" — there is no input
14//! for "nothing happened for 100ms". You need a timer for that, and
15//! the question is: who manages the timer?
16//!
17//! | Approach | Problem |
18//! |----------|---------|
19//! | FSM calls `set_timer()` directly | Side effects in the FSM; untestable without a platform |
20//! | Caller manages timers based on output | Timer logic leaks outside the FSM; grammar is split |
21//! | **FSM returns timer commands in `Response`** | **Timer logic stays inside the FSM; caller just executes** |
22//!
23//! `timed-fsm` takes the third approach.
24//!
25//! # Core types
26//!
27//! | Type | Role |
28//! |------|------|
29//! | [`TimedStateMachine`] | Trait your state machine implements |
30//! | [`Response<A, T>`] | Transition result: actions + timer commands + consumed flag |
31//! | [`TimerCommand<T>`] | Declarative instruction to set or kill a named timer |
32//! | [`dispatch`] | Connects a pure `Response` to runtime side effects |
33//! | [`TimerRuntime`] | Trait for platform timer integration (Windows/Linux/macOS/test) |
34//! | [`ActionExecutor`] | Trait for executing output actions in order |
35//! | [`ShiftReduceParser`] | Extension for token-buffering grammars with timer support |
36//! | [`parse`] | Main loop for a [`ShiftReduceParser`] |
37//!
38//! # Quick start
39//!
40//! The following example shows a **debounce filter**: it absorbs rapid
41//! signal changes and only emits a confirmed level after a 20 ms quiet
42//! period.
43//!
44//! ```
45//! use std::time::Duration;
46//! use timed_fsm::{TimedStateMachine, Response};
47//!
48//! /// A debounce filter that waits 20ms before confirming a level change.
49//! struct Debounce {
50//! pending: Option<bool>,
51//! }
52//!
53//! impl Debounce {
54//! fn new() -> Self { Self { pending: None } }
55//! }
56//!
57//! impl TimedStateMachine for Debounce {
58//! type Event = bool; // GPIO level
59//! type Action = bool; // Confirmed level
60//! type TimerId = (); // Only one timer needed
61//!
62//! fn on_event(&mut self, level: bool) -> Response<bool, ()> {
63//! // Buffer the level and (re)start the debounce timer.
64//! self.pending = Some(level);
65//! Response::consume()
66//! .with_timer((), Duration::from_millis(20))
67//! }
68//!
69//! fn on_timeout(&mut self, _: ()) -> Response<bool, ()> {
70//! // Quiet period elapsed — emit the last buffered level.
71//! match self.pending.take() {
72//! Some(level) => Response::emit_one(level),
73//! None => Response::pass_through(),
74//! }
75//! }
76//! }
77//! ```
78//!
79//! # Testing without platform dependencies
80//!
81//! Because the state machine never calls platform APIs directly, you
82//! can test all transitions by calling [`TimedStateMachine::on_event`]
83//! and [`TimedStateMachine::on_timeout`] directly — no OS timer
84//! infrastructure required.
85//!
86//! ```
87//! # use std::time::Duration;
88//! # use timed_fsm::{TimedStateMachine, Response};
89//! # struct Debounce { pending: Option<bool> }
90//! # impl Debounce { fn new() -> Self { Self { pending: None } } }
91//! # impl TimedStateMachine for Debounce {
92//! # type Event = bool;
93//! # type Action = bool;
94//! # type TimerId = ();
95//! # fn on_event(&mut self, level: bool) -> Response<bool, ()> {
96//! # self.pending = Some(level);
97//! # Response::consume().with_timer((), Duration::from_millis(20))
98//! # }
99//! # fn on_timeout(&mut self, _: ()) -> Response<bool, ()> {
100//! # match self.pending.take() {
101//! # Some(level) => Response::emit_one(level),
102//! # None => Response::pass_through(),
103//! # }
104//! # }
105//! # }
106//! let mut d = Debounce::new();
107//!
108//! // Noisy signal: high, low, high in quick succession
109//! let r = d.on_event(true);
110//! r.assert_consumed();
111//! r.assert_timer_set(());
112//!
113//! let r = d.on_event(false); // overwrites pending
114//! let r = d.on_event(true); // overwrites again
115//!
116//! // Simulate timeout firing — confirmed as true
117//! let r = d.on_timeout(());
118//! assert_eq!(r.actions, vec![true]);
119//! ```
120//!
121//! # Connecting to a runtime
122//!
123//! At the boundary with the OS, implement [`TimerRuntime`] and
124//! [`ActionExecutor`], then call [`dispatch`] after every transition.
125//!
126//! ```
127//! use std::time::Duration;
128//! use timed_fsm::{Response, TimerRuntime, ActionExecutor, dispatch};
129//!
130//! // Minimal in-memory timer stub for illustration.
131//! struct MyTimers;
132//! impl TimerRuntime for MyTimers {
133//! type TimerId = ();
134//! fn set_timer(&mut self, _id: (), _dur: Duration) {
135//! // e.g. SetTimer() on Windows, timerfd on Linux
136//! }
137//! fn kill_timer(&mut self, _id: ()) {
138//! // e.g. KillTimer() on Windows
139//! }
140//! }
141//!
142//! struct MyExecutor;
143//! impl ActionExecutor for MyExecutor {
144//! type Action = bool;
145//! fn execute(&mut self, actions: &[bool]) {
146//! // e.g. SendInput() on Windows, uinput write on Linux
147//! for &a in actions { let _ = a; }
148//! }
149//! }
150//!
151//! // In your event loop:
152//! let response = Response::<bool, ()>::emit_one(true);
153//! let consumed = dispatch(&response, &mut MyTimers, &mut MyExecutor);
154//! assert!(consumed);
155//! ```
156//!
157//! # Multiple timers
158//!
159//! When a state machine needs more than one concurrent timer, use an
160//! enum (or any `Copy + Eq + Debug` type) as `TimerId`.
161//!
162//! ```
163//! use std::time::Duration;
164//! use timed_fsm::{TimedStateMachine, Response};
165//!
166//! #[derive(Clone, Copy, Debug, PartialEq, Eq)]
167//! enum Timer {
168//! Debounce,
169//! Repeat,
170//! }
171//!
172//! struct KeyFilter {
173//! key: Option<u8>,
174//! }
175//!
176//! impl TimedStateMachine for KeyFilter {
177//! type Event = u8;
178//! type Action = u8;
179//! type TimerId = Timer;
180//!
181//! fn on_event(&mut self, key: u8) -> Response<u8, Timer> {
182//! self.key = Some(key);
183//! Response::consume()
184//! .with_timer(Timer::Debounce, Duration::from_millis(10))
185//! .with_kill_timer(Timer::Repeat)
186//! }
187//!
188//! fn on_timeout(&mut self, id: Timer) -> Response<u8, Timer> {
189//! match id {
190//! Timer::Debounce => match self.key {
191//! Some(k) => Response::emit_one(k)
192//! .with_timer(Timer::Repeat, Duration::from_millis(500)),
193//! None => Response::pass_through(),
194//! },
195//! Timer::Repeat => match self.key {
196//! Some(k) => Response::emit_one(k)
197//! .with_timer(Timer::Repeat, Duration::from_millis(100)),
198//! None => Response::pass_through(),
199//! },
200//! }
201//! }
202//! }
203//! ```
204//!
205//! # Shift-reduce parser extension
206//!
207//! When the decision about a token depends on tokens that arrive
208//! *after* it (e.g., distinguishing a single key press from a chord),
209//! a plain `TimedStateMachine` is not enough. The [`parser`] module
210//! provides a [`ShiftReduceParser`] trait and a [`parse`] driver that
211//! buffer tokens until a pattern is recognized or a timer forces a
212//! decision. See the module documentation for details and examples.
213//!
214//! # Use cases
215//!
216//! | Domain | Event | Timer role |
217//! |--------|-------|------------|
218//! | Keyboard firmware | Key press / release | Chord disambiguation timeout |
219//! | GPIO debounce | Signal edge | Quiet-period confirmation |
220//! | UI input | Mouse click | Double-click detection window |
221//! | Protocol framing | Byte received | Inter-frame gap detection |
222//! | IME / input method | Composition key | Commit-after-idle timeout |
223//!
224//! # No dependencies
225//!
226//! `timed-fsm` has no runtime dependencies beyond `std`.
227
228mod dispatch;
229mod machine;
230pub mod parser;
231mod response;
232
233pub use dispatch::{dispatch, ActionExecutor, TimerRuntime};
234pub use machine::TimedStateMachine;
235pub use parser::{parse, ParseAction, ShiftReduceParser};
236pub use response::{Response, TimerCommand};