Skip to main content

timed_fsm/
parser.rs

1//! Shift-reduce parser framework with timer support.
2//!
3//! A streaming parser that receives tokens one at a time and produces
4//! output actions. Tokens can be buffered (Shift), recognized as patterns
5//! (Reduce), or passed through. Timers enable "timeout = forced reduce"
6//! semantics.
7//!
8//! # What is a shift-reduce parser?
9//!
10//! A shift-reduce parser builds up a stack (or buffer) of tokens and
11//! only decides how to interpret them once it has seen enough context.
12//! This is necessary whenever the correct interpretation of a token
13//! depends on tokens that have **not arrived yet**.
14//!
15//! Classic example: the first key of a two-key chord. When you see key A,
16//! you do not know yet whether:
17//! - The user intends a chord `A+B` (requires waiting for B), or
18//! - The user intends a single-key stroke `A` (requires a timeout).
19//!
20//! A plain [`TimedStateMachine`](crate::TimedStateMachine) handles one
21//! token per transition. This module's [`ShiftReduceParser`] lets the
22//! grammar say "buffer this token and wait" (`Shift`) or "I have
23//! recognized a complete pattern" (`Reduce`), with a timer forcing a
24//! `Reduce` when no further token arrives in time.
25//!
26//! # When to use `ShiftReduceParser` vs `TimedStateMachine`
27//!
28//! | Scenario | Use |
29//! |----------|-----|
30//! | Each event is self-contained (debounce, key repeat) | [`TimedStateMachine`](crate::TimedStateMachine) |
31//! | A pattern spans multiple tokens (chord, double-click) | [`ShiftReduceParser`] |
32//! | You need to re-process a token after a partial reduce | [`ShiftReduceParser`] with [`ReduceAndContinue`](ParseAction::ReduceAndContinue) |
33//!
34//! # Example: two-key chord detection
35//!
36//! The following sketch detects a simultaneous press of keys `A` and `B`
37//! within a 50 ms window and emits a `Chord` action; a single `A` or `B`
38//! press outside the window passes through.
39//!
40//! ```
41//! use std::time::Duration;
42//! use timed_fsm::parser::{ShiftReduceParser, ParseAction};
43//! use timed_fsm::TimerCommand;
44//!
45//! #[derive(Clone, Copy, Debug, PartialEq)]
46//! enum Key { A, B, Other(u8) }
47//!
48//! #[derive(Debug, PartialEq)]
49//! enum Out { Chord, Key(Key) }
50//!
51//! struct ChordParser { pending: Option<Key> }
52//! impl ChordParser {
53//!     fn new() -> Self { Self { pending: None } }
54//! }
55//!
56//! impl ShiftReduceParser for ChordParser {
57//!     type Action      = Out;
58//!     type Token       = Key;
59//!     type TimerId     = ();
60//!     type ReduceRecord = ();
61//!
62//!     fn decide(&mut self, token: &Key)
63//!         -> ParseAction<Out, Key, (), ()>
64//!     {
65//!         match (self.pending, token) {
66//!             // No pending key yet — shift and open a 50 ms chord window.
67//!             (None, &k @ (Key::A | Key::B)) => {
68//!                 self.pending = Some(k);
69//!                 ParseAction::Shift {
70//!                     timers: vec![TimerCommand::Set {
71//!                         id: (),
72//!                         duration: Duration::from_millis(50),
73//!                     }],
74//!                 }
75//!             }
76//!             // Second key arrived inside the window — chord recognized.
77//!             (Some(Key::A), Key::B) | (Some(Key::B), Key::A) => {
78//!                 self.pending = None;
79//!                 ParseAction::Reduce {
80//!                     actions: vec![Out::Chord],
81//!                     record: (),
82//!                     timers: vec![TimerCommand::Kill { id: () }],
83//!                 }
84//!             }
85//!             // Unrelated key — pass through.
86//!             _ => ParseAction::PassThrough { timers: vec![] },
87//!         }
88//!     }
89//!
90//!     fn on_reduce(&mut self, (): ()) {}
91//! }
92//!
93//! // Both keys pressed in sequence → chord.
94//! let mut p = ChordParser::new();
95//! let _ = timed_fsm::parse(&mut p, Key::A);  // Shift
96//! let r = timed_fsm::parse(&mut p, Key::B);  // Reduce → Chord
97//! assert_eq!(r.actions, vec![Out::Chord]);
98//! ```
99
100use crate::response::{Response, TimerCommand};
101
102/// Parser action: the result of examining a token in the current state.
103///
104/// Returned by [`ShiftReduceParser::decide`] and consumed by the [`parse`]
105/// driver loop.
106///
107/// | Variant | Loop continues? | Actions accumulated? |
108/// |---------|----------------|----------------------|
109/// | `Shift` | No (terminal) | No — wait for more input |
110/// | `Reduce` | No (terminal) | Yes — emit and finish |
111/// | `ReduceAndContinue` | Yes — re-process `remaining` | Yes — partial emit, then loop |
112/// | `PassThrough` | No (terminal) | No (or yes if prior reduces accumulated some) |
113#[derive(Debug)]
114pub enum ParseAction<A, Token, T, R = ()> {
115    /// Buffer the token and wait for more input.
116    ///
117    /// The parser absorbs the token into its internal buffer (if any) and
118    /// sets a timer to force a decision if no further token arrives in time.
119    /// The [`parse`] loop returns immediately — the token is consumed.
120    Shift {
121        /// Timer commands to execute (typically a `Set` to open the chord window).
122        timers: Vec<TimerCommand<T>>,
123    },
124
125    /// Recognize a complete pattern and emit output. Terminal action.
126    ///
127    /// [`parse`] calls [`ShiftReduceParser::on_reduce`] with `record`, then
128    /// returns a consumed [`Response`] with all accumulated
129    /// actions and the given timer commands.
130    Reduce {
131        /// Output actions to emit.
132        actions: Vec<A>,
133        /// Metadata for bookkeeping (passed to [`ShiftReduceParser::on_reduce`]).
134        record: R,
135        /// Timer commands to execute (e.g., `Kill` the chord window timer).
136        timers: Vec<TimerCommand<T>>,
137    },
138
139    /// Recognize a partial pattern, emit output, and re-process the remaining token.
140    ///
141    /// Use this when two logically independent patterns are packed into a
142    /// single token stream and the second pattern starts immediately after
143    /// the first ends. For example:
144    ///
145    /// - A "forced reduce" on timeout that needs to flush buffered tokens and
146    ///   then re-process the triggering event through the same grammar.
147    /// - A two-byte sequence where the first byte is a complete symbol and the
148    ///   second byte starts a new one.
149    ///
150    /// [`parse`] calls [`on_reduce`](ShiftReduceParser::on_reduce) with `record`,
151    /// accumulates `actions`, and loops by calling `decide` on `remaining`.
152    /// Because the loop continues, the final `consumed` flag is `true` even if
153    /// the eventual terminal action is `PassThrough`.
154    ReduceAndContinue {
155        /// Output actions to emit for the recognized partial pattern.
156        actions: Vec<A>,
157        /// Metadata for bookkeeping (passed to [`ShiftReduceParser::on_reduce`]).
158        record: R,
159        /// The remaining token to re-process in the next loop iteration.
160        remaining: Token,
161    },
162
163    /// This parser does not handle the current token.
164    ///
165    /// Pass the event to the next handler in the chain. If no actions have
166    /// been accumulated by prior [`ReduceAndContinue`](Self::ReduceAndContinue)
167    /// steps, `consumed` will be `false`; otherwise `true` (the partial results
168    /// are still returned).
169    PassThrough {
170        /// Timer commands to execute (usually empty for pass-through).
171        timers: Vec<TimerCommand<T>>,
172    },
173}
174
175/// A shift-reduce parser that processes tokens with timer support.
176///
177/// Implement this trait to define your parser's grammar (action table).
178/// The framework provides the main loop via [`parse()`].
179///
180/// # Implementing `decide`
181///
182/// `decide` is the **action table** of the parser. It receives a reference
183/// to the current token together with whatever state is stored in `self`,
184/// and returns a [`ParseAction`] describing what to do next.
185///
186/// Typical skeleton:
187///
188/// ```
189/// use timed_fsm::parser::{ShiftReduceParser, ParseAction};
190/// use timed_fsm::TimerCommand;
191/// use std::time::Duration;
192///
193/// #[derive(Clone, Copy, Debug, PartialEq)]
194/// enum Key { Shift, A, Other }
195///
196/// struct MyParser { pending_shift: bool }
197///
198/// impl ShiftReduceParser for MyParser {
199///     type Action      = String;
200///     type Token       = Key;
201///     type TimerId     = ();
202///     type ReduceRecord = ();
203///
204///     fn decide(&mut self, token: &Key)
205///         -> ParseAction<String, Key, (), ()>
206///     {
207///         match (self.pending_shift, token) {
208///             // Shift key down — buffer and open chord window.
209///             (false, Key::Shift) => {
210///                 self.pending_shift = true;
211///                 ParseAction::Shift {
212///                     timers: vec![TimerCommand::Set {
213///                         id: (),
214///                         duration: Duration::from_millis(50),
215///                     }],
216///                 }
217///             }
218///             // Shift + A chord recognized.
219///             (true, Key::A) => {
220///                 self.pending_shift = false;
221///                 ParseAction::Reduce {
222///                     actions: vec!["ShiftA".to_string()],
223///                     record: (),
224///                     timers: vec![TimerCommand::Kill { id: () }],
225///                 }
226///             }
227///             // Unrecognized — pass through.
228///             _ => ParseAction::PassThrough { timers: vec![] },
229///         }
230///     }
231///
232///     fn on_reduce(&mut self, (): ()) { /* update history / stats */ }
233/// }
234///
235/// let mut p = MyParser { pending_shift: false };
236/// let _ = timed_fsm::parse(&mut p, Key::Shift);   // Shift
237/// let r = timed_fsm::parse(&mut p, Key::A);       // Reduce
238/// assert_eq!(r.actions, vec!["ShiftA".to_string()]);
239/// ```
240pub trait ShiftReduceParser {
241    /// Output action type.
242    type Action;
243    /// Input token type.
244    type Token;
245    /// Timer ID type.
246    type TimerId;
247    /// Metadata attached to each `Reduce` step (e.g., history tracking).
248    ///
249    /// Use `()` if no per-reduce bookkeeping is needed.
250    type ReduceRecord;
251
252    /// The action table: given the current state and input token, decide what to do.
253    ///
254    /// This method may mutate internal state (e.g., enter a pending state,
255    /// push a token onto a buffer). It is called once per token (or once per
256    /// `remaining` token in a `ReduceAndContinue` loop iteration) by [`parse`].
257    fn decide(
258        &mut self,
259        token: &Self::Token,
260    ) -> ParseAction<Self::Action, Self::Token, Self::TimerId, Self::ReduceRecord>;
261
262    /// Called after each `Reduce` or `ReduceAndContinue` to update internal bookkeeping.
263    ///
264    /// Receives the `record` from the matching [`ParseAction`]. Use this to
265    /// maintain reduce-count statistics, history logs, or any per-reduce state
266    /// that should not live inside `decide`.
267    fn on_reduce(&mut self, record: Self::ReduceRecord);
268}
269
270/// Process a token through a shift-reduce parser, producing a [`Response`].
271///
272/// # Loop semantics
273///
274/// The driver loop calls [`ShiftReduceParser::decide`] on the current token.
275/// Depending on the result:
276///
277/// | Result | Actions accumulated | `on_reduce` called | Next iteration |
278/// |--------|--------------------|--------------------|----------------|
279/// | `Shift` | — | No | Return immediately |
280/// | `Reduce` | Yes | Yes | Return immediately |
281/// | `ReduceAndContinue` | Yes | Yes | Loop with `remaining` |
282/// | `PassThrough` | — | No | Return immediately |
283///
284/// # `consumed` semantics
285///
286/// The returned `Response::consumed` flag is `true` if any of the
287/// following is true:
288///
289/// - The terminal action was `Shift` or `Reduce` (event was handled), **or**
290/// - At least one [`ReduceAndContinue`](ParseAction::ReduceAndContinue) step
291///   accumulated actions before a `PassThrough` was reached.
292///
293/// The second case means that even if the final `PassThrough` would normally
294/// mean "not consumed," the event is still considered consumed because the
295/// parser did useful work in earlier iterations of the loop.
296///
297/// # Termination
298///
299/// The loop always terminates because every path through `decide` either
300/// returns a terminal variant (`Shift`, `Reduce`, `PassThrough`) — which
301/// causes an immediate `return` — or returns `ReduceAndContinue` with a
302/// new token. The new token is always a value that already existed in the
303/// caller's token stream; the grammar must ensure no cycle exists (e.g.,
304/// a token that always `ReduceAndContinue`s into itself). In practice,
305/// the `remaining` token is typically processed via a different match arm
306/// that does not produce another `ReduceAndContinue`.
307pub fn parse<P>(parser: &mut P, initial: P::Token) -> Response<P::Action, P::TimerId>
308where
309    P: ShiftReduceParser,
310{
311    let mut actions: Vec<P::Action> = Vec::new();
312    let mut current = Some(initial);
313
314    while let Some(token) = current.take() {
315        match parser.decide(&token) {
316            ParseAction::Shift { timers } => {
317                return build_response(actions, true, timers);
318            }
319            ParseAction::Reduce {
320                actions: output,
321                record,
322                timers,
323            } => {
324                actions.extend(output);
325                parser.on_reduce(record);
326                return build_response(actions, true, timers);
327            }
328            ParseAction::ReduceAndContinue {
329                actions: output,
330                record,
331                remaining,
332            } => {
333                actions.extend(output);
334                parser.on_reduce(record);
335                current = Some(remaining);
336            }
337            ParseAction::PassThrough { timers } => {
338                let consumed = !actions.is_empty();
339                return build_response(actions, consumed, timers);
340            }
341        }
342    }
343
344    unreachable!("parse loop must terminate via Shift, Reduce, or PassThrough")
345}
346
347fn build_response<A, T>(
348    actions: Vec<A>,
349    consumed: bool,
350    timers: Vec<TimerCommand<T>>,
351) -> Response<A, T> {
352    Response {
353        consumed: consumed || !actions.is_empty(),
354        actions,
355        timers,
356    }
357}
358
359#[cfg(test)]
360mod tests {
361    use super::*;
362    use std::time::Duration;
363
364    /// A simple calculator parser for testing.
365    /// Tokens are i32 values. When it sees 0, it reduces and emits the sum.
366    /// Negative values pass through.
367    struct SumParser {
368        buffer: Vec<i32>,
369        reduce_count: usize,
370    }
371
372    impl SumParser {
373        fn new() -> Self {
374            Self {
375                buffer: Vec::new(),
376                reduce_count: 0,
377            }
378        }
379    }
380
381    impl ShiftReduceParser for SumParser {
382        type Action = i32;
383        type Token = i32;
384        type TimerId = u8;
385        type ReduceRecord = usize; // number of items reduced
386
387        fn decide(&mut self, token: &i32) -> ParseAction<i32, i32, u8, usize> {
388            match *token {
389                t if t < 0 => ParseAction::PassThrough { timers: vec![] },
390                0 => {
391                    // Reduce: emit sum of buffer
392                    let sum: i32 = self.buffer.drain(..).sum();
393                    let count = usize::from(sum != 0);
394                    ParseAction::Reduce {
395                        actions: if sum == 0 { vec![] } else { vec![sum] },
396                        record: count,
397                        timers: vec![TimerCommand::Kill { id: 0 }],
398                    }
399                }
400                t => {
401                    // Shift: buffer the value
402                    self.buffer.push(t);
403                    ParseAction::Shift {
404                        timers: vec![TimerCommand::Set {
405                            id: 0,
406                            duration: Duration::from_millis(100),
407                        }],
408                    }
409                }
410            }
411        }
412
413        fn on_reduce(&mut self, record: usize) {
414            self.reduce_count += record;
415        }
416    }
417
418    #[test]
419    fn shift_buffers_and_returns_consumed() {
420        let mut p = SumParser::new();
421        let r = parse(&mut p, 5);
422        assert!(r.consumed);
423        assert!(r.actions.is_empty());
424        assert_eq!(r.timers.len(), 1);
425        r.assert_timer_set(0);
426        assert_eq!(p.buffer, vec![5]);
427    }
428
429    #[test]
430    fn reduce_emits_sum() {
431        let mut p = SumParser::new();
432        p.buffer = vec![3, 7];
433        let r = parse(&mut p, 0);
434        assert!(r.consumed);
435        assert_eq!(r.actions, vec![10]);
436        r.assert_timer_kill(0);
437        assert_eq!(p.reduce_count, 1);
438    }
439
440    #[test]
441    fn pass_through_not_consumed() {
442        let mut p = SumParser::new();
443        let r = parse(&mut p, -1);
444        assert!(!r.consumed);
445        assert!(r.actions.is_empty());
446    }
447
448    /// A parser that uses `ReduceAndContinue`.
449    /// On token 99, it reduces current buffer and re-processes token 0 (which triggers final reduce).
450    struct SplitParser {
451        buffer: Vec<i32>,
452        reduce_count: usize,
453    }
454
455    impl SplitParser {
456        fn new() -> Self {
457            Self {
458                buffer: Vec::new(),
459                reduce_count: 0,
460            }
461        }
462    }
463
464    impl ShiftReduceParser for SplitParser {
465        type Action = String;
466        type Token = i32;
467        type TimerId = u8;
468        type ReduceRecord = ();
469
470        fn decide(&mut self, token: &i32) -> ParseAction<String, i32, u8, ()> {
471            match *token {
472                99 => {
473                    let msg = format!("split:{}", self.buffer.len());
474                    self.buffer.clear();
475                    ParseAction::ReduceAndContinue {
476                        actions: vec![msg],
477                        record: (),
478                        remaining: 0,
479                    }
480                }
481                0 => ParseAction::Reduce {
482                    actions: vec!["done".to_string()],
483                    record: (),
484                    timers: vec![],
485                },
486                t => {
487                    self.buffer.push(t);
488                    ParseAction::Shift { timers: vec![] }
489                }
490            }
491        }
492
493        fn on_reduce(&mut self, _record: ()) {
494            self.reduce_count += 1;
495        }
496    }
497
498    #[test]
499    fn reduce_and_continue_chains() {
500        let mut p = SplitParser::new();
501        p.buffer = vec![1, 2, 3];
502
503        let r = parse(&mut p, 99);
504        assert!(r.consumed);
505        assert_eq!(r.actions, vec!["split:3".to_string(), "done".to_string()]);
506        // on_reduce called twice (once for ReduceAndContinue, once for Reduce)
507        assert_eq!(p.reduce_count, 2);
508    }
509
510    #[test]
511    fn pass_through_after_reduce_and_continue_is_consumed() {
512        /// Parser that does `ReduceAndContinue` then the remaining token passes through.
513        struct RacParser;
514
515        impl ShiftReduceParser for RacParser {
516            type Action = &'static str;
517            type Token = i32;
518            type TimerId = u8;
519            type ReduceRecord = ();
520
521            fn decide(&mut self, token: &i32) -> ParseAction<&'static str, i32, u8, ()> {
522                match *token {
523                    1 => ParseAction::ReduceAndContinue {
524                        actions: vec!["first"],
525                        record: (),
526                        remaining: -1,
527                    },
528                    _ => ParseAction::PassThrough { timers: vec![] },
529                }
530            }
531
532            fn on_reduce(&mut self, (): ()) {}
533        }
534
535        let mut p = RacParser;
536        let r = parse(&mut p, 1);
537        // Has accumulated actions from the ReduceAndContinue, so consumed is true
538        assert!(r.consumed);
539        assert_eq!(r.actions, vec!["first"]);
540    }
541}