macro_machines/
lib.rs

1//! State machine macros with logging and graphviz DOT file generation.
2//!
3//! [Repository](https://github.com/spearman/macro-machines)
4//!
5//! An example that shows a number of features of the macro syntax is a `Door`
6//! state machine with:
7//!
8//! - two *states*: `Closed` (with *state-local variable* `knock_count` and an
9//!   exit action) and a simple state `Open` (with no state variables or
10//!   actions)
11//! - three *events*: one *internal event* `Knock` (with *action* on the
12//!   `Closed` state) and two *external events* `Open` (with associated action)
13//!   and `Close` (without any action)
14//! - an *extended state variable* `open_count` -- this variable is initialized
15//!   once and is independent of the current machine state
16//!
17//! ```text
18//! def_machine_debug! {
19//!   Door (open_count : u64) @ door {
20//!     STATES [
21//!       state Closed (knock_count : u64) {
22//!         exit {
23//!           println!("knock count: {}", knock_count);
24//!           println!("open count: {}", open_count);
25//!         }
26//!       }
27//!       state Opened ()
28//!     ]
29//!     EVENTS [
30//!       event Knock <Closed> () { knock_count } => { *knock_count += 1; }
31//!       event Open  <Closed> => <Opened> ()  {} => { *open_count += 1; }
32//!       event Close <Opened> => <Closed> ()
33//!     ]
34//!     initial_state:  Closed {
35//!       initial_action: {
36//!         println!("hello");
37//!         println!("open_count: {:?}", door.as_ref().open_count);
38//!       }
39//!     }
40//!     terminal_state: Closed {
41//!       terminate_success: {
42//!         println!("open_count: {:?}", door.as_ref().open_count);
43//!         println!("goodbye")
44//!       }
45//!       terminate_failure: {
46//!         panic!("door was left: {:?}", door.state())
47//!       }
48//!     }
49//!   }
50//! }
51//! ```
52//!
53//! Within state entry and exit action blocks all extended state and local state
54//! variables are in scope.
55//!
56//! In event actions, mutable references to extended state variables will
57//! implicitly be brought into scope of the associated action block, however
58//! local state variables need to be explicitly listed in the LHS brace of the
59//! action construct to be accessible (e.g. the `knock_count` local state
60//! variable in the `Knock` event action of the current example).
61//!
62//! When making a universal or external transition, first state exit actions are
63//! performed, followed by event actions, and then after initializing the new
64//! state, state entry actions.
65//!
66//! To make the state machine accessible in initial and terminal action blocks,
67//! the macro implementation requires an identifier be introduced, `door`,
68//! following the `@` symbol. This variable is then brought into scope as an
69//! alias for a mutable self-reference in initial and terminal action blocks.
70//!
71//! Initial and terminal actions are always before and after any state entry and
72//! exit actions, respectively.
73//!
74//! The `Door::dotfile()` function will generate a '.dot' file string that can
75//! be saved and rendered as a PNG with layout generated by graphviz `dot` tool:
76//!
77//! ```text
78//! $ dot -Tpng door.dot > door.png
79//! ```
80//!
81//! ![](https://raw.githubusercontent.com/spearman/macro-machines/master/door.png)
82
83#![cfg_attr(test, allow(dead_code, unreachable_code))]
84
85pub extern crate log;
86extern crate marksman_escape;
87extern crate variant_count;
88
89pub use variant_count::VariantCount;
90
91mod macro_def;
92
93/// Methods for DOT file creation
94// TODO: if we had a proper Machine trait with associated state and event ID
95// types, some of this would be redundant
96pub trait MachineDotfile {
97  // required
98  fn name()                       -> &'static str;
99  fn type_vars()                  -> Vec <String>;
100  fn extended_state_names()       -> Vec <&'static str>;
101  fn extended_state_types()       -> Vec <&'static str>;
102  fn extended_state_defaults()    -> Vec <&'static str>;
103  fn self_reference()             -> &'static str;
104  fn states()                     -> Vec <&'static str>;
105  fn state_data_names()           -> Vec <Vec <&'static str>>;
106  fn state_data_types()           -> Vec <Vec <&'static str>>;
107  fn state_data_defaults()        -> Vec <Vec <&'static str>>;
108  fn state_data_pretty_defaults() -> Vec <Vec <String>>;
109  fn state_initial()              -> &'static str;
110  fn state_terminal()             -> &'static str;
111  fn events()                     -> Vec <&'static str>;
112  fn event_sources()              -> Vec <&'static str>;
113  fn event_targets()              -> Vec <&'static str>;
114  fn event_actions()              -> Vec <&'static str>;
115  // provided: these are intended to be called by the user
116  /// Generate a DOT file for the state machine that hides default expressions
117  /// for state fields and extended state fields, but shows event actions
118  fn dotfile() -> String where Self : Sized {
119    machine_dotfile::<Self> (true, false, false)
120  }
121  /// Generate a DOT file for the state machine that shows default expressions
122  /// for state fields and extended state fields
123  fn dotfile_show_defaults() -> String where Self : Sized {
124    machine_dotfile::<Self> (false, false, false)
125  }
126  /// Generate a DOT file for the state machine that pretty prints the *values*
127  /// of default expressions for state fields and extended state fields.
128  ///
129  /// &#9888; Calling this this function evaluates default expressions and
130  /// pretty prints the resulting values at runtime.
131  fn dotfile_pretty_defaults() -> String where Self : Sized {
132    machine_dotfile::<Self> (false, true, false)
133  }
134  /// Do not show event actions
135  fn dotfile_hide_actions() -> String where Self : Sized {
136    machine_dotfile::<Self> (true, false, true)
137  }
138}
139
140/// Describes an exceptional result when attempting to handle an event.
141///
142/// Currently the only exception is the '`WrongState`' exception.
143#[derive(Debug, PartialEq)]
144pub enum HandleEventException {
145  WrongState
146}
147
148//
149//  private functions
150//
151
152/// Private DOT file creation function
153fn machine_dotfile <M : MachineDotfile>
154  (hide_defaults : bool, pretty_defaults : bool, hide_actions : bool) -> String
155{
156  let mut s = String::new();
157  //
158  // begin graph
159  //
160  // overlap = scale for neato layouts
161  s.push_str (
162    "digraph {\n  \
163       overlap=scale\n  \
164       rankdir=LR\n  \
165       node [shape=record, style=rounded, fontname=\"Sans Bold\"]\n  \
166       edge [fontname=\"Sans\"]\n");
167
168    //
169  { // begin subgraph
170    //
171  s.push_str (format!(
172    "  subgraph cluster_{} {{\n", M::name()).as_str());
173  let title_string = {
174    let mut s = String::new();
175    s.push_str (M::name());
176    if !M::type_vars().is_empty() {
177      s.push_str ("<");
178      let type_vars = M::type_vars();
179      for string in type_vars {
180        s.push_str (string.as_str());
181        s.push_str (",");
182      }
183      assert_eq!(s.pop(), Some (','));
184      s.push_str (">");
185    }
186    s
187  };
188  s.push_str (format!("    label=<{}", escape (title_string)).as_str());
189
190  //  extended state
191  let mut mono_font           = false;
192  let extended_state_names    = M::extended_state_names();
193  let extended_state_types    = M::extended_state_types();
194  let extended_state_defaults = M::extended_state_defaults();
195  debug_assert_eq!(extended_state_names.len(), extended_state_types.len());
196  debug_assert_eq!(extended_state_types.len(), extended_state_defaults.len());
197
198  if !extended_state_names.is_empty() {
199    s.push_str ("<FONT FACE=\"Mono\"><BR/><BR/>\n");
200    mono_font = true;
201    //  for each extended state field, print a line
202    // TODO: we are manually aligning the columns of the field name and field
203    // type, is there a better way ? (record node, html table, format width?)
204    debug_assert!(mono_font);
205
206    let mut extended_string = String::new();
207    let separator = ",<BR ALIGN=\"LEFT\"/>\n";
208
209    let longest_fieldname = extended_state_names.iter().fold (
210      0, |longest, ref fieldname| std::cmp::max (longest, fieldname.len())
211    );
212
213    let longest_typename = extended_state_types.iter().fold (
214      0, |longest, ref typename| std::cmp::max (longest, typename.len())
215    );
216
217    for (i,f) in extended_state_names.iter().enumerate() {
218      let spacer1 : String = std::iter::repeat (' ')
219        .take (longest_fieldname - f.len())
220        .collect();
221      let spacer2 : String = std::iter::repeat (' ')
222        .take (longest_typename - extended_state_types[i].len())
223        .collect();
224
225      if !hide_defaults && !extended_state_defaults[i].is_empty() {
226        extended_string.push_str (escape (format!(
227          "{}{} : {}{} = {}",
228          f, spacer1, extended_state_types[i], spacer2, extended_state_defaults[i]
229        )).as_str());
230      } else {
231        extended_string.push_str (escape (format!(
232          "{}{} : {}", f, spacer1, extended_state_types[i]
233        )).as_str());
234      }
235      extended_string.push_str (format!("{}", separator).as_str());
236    }
237
238    let len = extended_string.len();
239    extended_string.truncate (len - separator.len());
240    s.push_str (format!("{}", extended_string).as_str());
241  } // end extended state
242
243  s.push_str ("<BR ALIGN=\"LEFT\"/>");
244  let self_reference = M::self_reference();
245  if !self_reference.is_empty() && mono_font {
246    s.push_str (format!("@ {}<BR ALIGN=\"CENTER\"/>", self_reference).as_str());
247  }
248  if !extended_state_names.is_empty() {
249    s.push_str ("\n      ");
250  }
251
252  // TODO: extended state transitions
253
254  if mono_font {
255    s.push_str ("</FONT><BR/>");
256  }
257  s.push_str (">\
258    \n    shape=record\
259    \n    style=rounded\
260    \n    fontname=\"Sans Bold Italic\"\n");
261  } // end begin subgraph
262
263  //
264  // nodes (states)
265  //
266  // initial node
267  s.push_str (
268    "    INITIAL [label=\"\", shape=circle, width=0.2, \
269           style=filled, fillcolor=black]\n");
270  // states
271  let state_data_names    = M::state_data_names();
272  let state_data_types    = M::state_data_types();
273  let state_data_defaults : Vec <Vec <String>> = if !pretty_defaults {
274    M::state_data_defaults().into_iter().map (
275      |v| v.into_iter().map (str::to_string).collect()
276    ).collect()
277  } else {
278    let pretty_defaults = M::state_data_pretty_defaults();
279    pretty_defaults.into_iter().map (
280      |v| v.into_iter().map (|pretty_newline| {
281        let mut pretty_br = String::new();
282        let separator = "<BR ALIGN=\"LEFT\"/>\n";
283        for line in pretty_newline.lines() {
284          pretty_br.push_str (escape (line.to_string()).as_str());
285          pretty_br.push_str (separator);
286        }
287        let len = pretty_br.len();
288        pretty_br.truncate (len - separator.len());
289        pretty_br
290      }).collect()
291    ).collect()
292  };
293  debug_assert_eq!(state_data_names.len(), state_data_types.len());
294  debug_assert_eq!(state_data_types.len(), state_data_defaults.len());
295
296  // for each state: node
297  for (i, state) in M::states().iter().enumerate() {
298    let mut mono_font       = false;
299    let state_data_names    = &state_data_names[i];
300    let state_data_types    = &state_data_types[i];
301    let state_data_defaults = &state_data_defaults[i];
302    debug_assert_eq!(state_data_names.len(), state_data_types.len());
303    debug_assert_eq!(state_data_types.len(), state_data_defaults.len());
304    s.push_str (format!("    {} [label=<<B>{}</B>", state, state).as_str());
305    // NOTE: within the mono font block leading whitespace in the source
306    // is counted as part of the layout so we don't indent these lines
307    if !state_data_names.is_empty() {
308      if !mono_font {
309        s.push_str ("|<FONT FACE=\"Mono\"><BR/>\n");
310        mono_font = true;
311      }
312      let mut data_string = String::new();
313      let separator = ",<BR ALIGN=\"LEFT\"/>\n";
314      let longest_fieldname = state_data_names.iter().fold (
315        0, |longest, ref fieldname| std::cmp::max (longest, fieldname.len())
316      );
317      let longest_typename = state_data_types.iter().fold (
318        0, |longest, ref typename| std::cmp::max (longest, typename.len())
319      );
320      for (i,f) in state_data_names.iter().enumerate() {
321        let spacer1 : String = std::iter::repeat (' ')
322          .take(longest_fieldname - f.len())
323          .collect();
324        let spacer2 : String = std::iter::repeat (' ')
325          .take(longest_typename - state_data_types[i].len())
326          .collect();
327        if !hide_defaults && !state_data_defaults[i].is_empty() {
328          data_string.push_str (escape (format!(
329            "{}{} : {}{} = {}",
330            f, spacer1, state_data_types[i], spacer2, state_data_defaults[i]
331          )).as_str());
332        } else {
333          data_string.push_str (escape (format!(
334            "{}{} : {}", f, spacer1, state_data_types[i]
335          )).as_str());
336        }
337        data_string.push_str (format!("{}", separator).as_str());
338      }
339      let len = data_string.len();
340      data_string.truncate (len - separator.len());
341      s.push_str (format!("{}", data_string).as_str());
342    }
343
344    /*
345    if s.chars().last().unwrap() == '>' {
346      let len = s.len();
347      s.truncate (len-5);
348    } else {
349      s.push_str ("</FONT>");
350    }
351    */
352
353    // state guards
354    // TODO
355
356    if mono_font {
357      s.push_str ("<BR ALIGN=\"LEFT\"/></FONT>");
358    }
359    s.push_str (">]\n");
360  } // end for each state: node
361  // end nodes (states)
362
363  //
364  // transitions (events)
365  //
366  // initial transition edge
367  // TODO: show initial action
368  s.push_str (format!(
369    "    INITIAL -> {}\n", M::state_initial()).as_str());
370  let event_sources = M::event_sources();
371  let event_targets = M::event_targets();
372  let event_actions = M::event_actions();
373  let mut universal = false;
374  // for each event: transition edge
375  for (i, event) in M::events().into_iter().enumerate() {
376    let source = event_sources[i];
377    let mut target = event_targets[i];
378    let action = event_actions[i];
379    if target.is_empty() {  // internal transition source == target
380      target = source;
381    }
382
383    if source == "*" {
384      universal = true;
385    }
386    s.push_str (format!(
387      "    \"{}\" -> \"{}\" [label=<<FONT FACE=\"Sans Italic\">{}</FONT>",
388      source, target, event
389    ).as_str());
390
391    let mut mono_font = false;
392    // params
393    // TODO
394    // guards
395    // TODO
396
397    if !hide_actions && !action.is_empty() {
398      match action {
399        // don't render empty actions
400        "{}" | "{ }" => {}
401        _ => {
402          if !mono_font {
403            s.push_str ("<FONT FACE=\"Mono\"><BR/>");
404            mono_font = true;
405          }
406          // replace whitespace with single spaces
407          let action_string = {
408            let mut s : String = action.split_whitespace().map (
409              |s| {
410                let mut s = s.to_string();
411                s.push (' ');
412                s
413              }
414            ).collect();
415            assert_eq!(s.pop(), Some (' '));
416            s
417          };
418          // TODO: different formatting if params or guards were present
419          //action = "  ".to_string() + action.as_str();
420          s.push_str (format!("{}", escape (action_string)).as_str());
421        }
422      }
423    }
424
425    if mono_font {
426      s.push_str ("</FONT>");
427    }
428    s.push_str (">]\n");
429  } // end for each event: transition edge
430
431  if universal {
432    for state in M::states() {
433      s.push_str (format!(
434        "    {} -> \"*\" [style=dashed, color=gray]", state).as_str());
435    }
436  }
437
438  // terminal transition: node + edge
439  // TODO: show terminal action(s)
440  let state_terminal = M::state_terminal();
441  if !state_terminal.is_empty() {
442    s.push_str (
443      "    TERMINAL [label=\"\", shape=doublecircle, width=0.2,\
444     \n      style=filled, fillcolor=black]\n");
445    s.push_str (format!(
446      "    {} -> TERMINAL\n", state_terminal).as_str());
447  }
448  // end transitions
449
450  //
451  //  end graph
452  //
453  s.push_str (
454    "  }\n\
455    }");
456  s
457} // end fn machine_dotfile
458
459/// Escape HTML special characters
460#[inline]
461fn escape (s : String) -> String {
462  use marksman_escape::Escape;
463  String::from_utf8 (Escape::new (s.bytes()).collect()).unwrap()
464}
465
466#[cfg(doc)]
467pub mod example {
468  //! Example generated state machine
469  use crate::def_machine_debug;
470  def_machine_debug! {
471    Door (open_count : u64) @ door {
472      STATES [
473        state Closed (knock_count : u64) {
474          exit { println!("final knock count: {}", knock_count); }
475        }
476        state Opened () {
477          entry { println!("open count: {}", open_count); }
478        }
479      ]
480      EVENTS [
481        event Knock <Closed> () { knock_count } => { *knock_count += 1; }
482        event Open  <Closed> => <Opened> ()  {} => { *open_count += 1; }
483        event Close <Opened> => <Closed> ()
484      ]
485      initial_state:  Closed {
486        initial_action: { println!("hello"); }
487      }
488      terminal_state: Closed {
489        terminate_success: { println!("goodbye") }
490        terminate_failure: {
491          panic!("door was left: {:?}", door.state())
492        }
493      }
494    }
495  }
496}
497
498#[cfg(test)]
499mod tests {
500  use super::*;
501  #[test]
502  fn test_initial() {
503    {
504      def_machine!{
505        Test () {
506          STATES [ state A () ]
507          EVENTS [ ]
508          initial_state: A
509        }
510      }
511      let test = Test::initial();
512      assert_eq!(test.state_id(), StateId::A);
513    } {
514      def_machine_debug!{
515        Test () {
516          STATES [ state A () ]
517          EVENTS [ ]
518          initial_state: A
519        }
520      }
521      let test = Test::initial();
522      assert_eq!(test.state_id(), StateId::A);
523    }
524  }
525  #[test]
526  fn test_new() {
527    {
528      def_machine!{
529        Test () {
530          STATES [ state A () ]
531          EVENTS [ ]
532          initial_state: A
533        }
534      }
535      let test = Test::new (ExtendedState::new());
536      assert_eq!(test.state_id(), StateId::A);
537    } {
538      def_machine_debug!{
539        Test () {
540          STATES [ state A () ]
541          EVENTS [ ]
542          initial_state: A
543        }
544      }
545      let test = Test::new (ExtendedState::new());
546      assert_eq!(test.state_id(), StateId::A);
547    } {
548      def_machine_nodefault!{
549        Test () {
550          STATES [ state A () ]
551          EVENTS [ ]
552          initial_state: A
553        }
554      }
555      let test = Test::new (ExtendedState::new().unwrap());
556      assert_eq!(test.state_id(), StateId::A);
557    } {
558      def_machine_nodefault_debug!{
559        Test () {
560          STATES [ state A () ]
561          EVENTS [ ]
562          initial_state: A
563        }
564      }
565      let test = Test::new (ExtendedState::new().unwrap());
566      assert_eq!(test.state_id(), StateId::A);
567    }
568  }
569  #[test]
570  fn test_event_internal() {
571    {
572      def_machine!{
573        Test () {
574          STATES [ state A () ]
575          EVENTS [ event E <A> () ]
576          initial_state: A
577        }
578      }
579      let mut test = Test::initial();
580      test.handle_event (EventId::E.into()).unwrap();
581    } {
582      def_machine_debug!{
583        Test () {
584          STATES [ state A () ]
585          EVENTS [ event E <A> () ]
586          initial_state: A
587        }
588      }
589      let mut test = Test::initial();
590      test.handle_event (EventId::E.into()).unwrap();
591    } {
592      def_machine_nodefault!{
593        Test () {
594          STATES [ state A () ]
595          EVENTS [ event E <A> () ]
596          initial_state: A
597        }
598      }
599      let mut test = Test::new (ExtendedState::new().unwrap());
600      test.handle_event (EventParams::E{}.into()).unwrap();
601    } {
602      def_machine_nodefault_debug!{
603        Test () {
604          STATES [ state A () ]
605          EVENTS [ event E <A> () ]
606          initial_state: A
607        }
608      }
609      let mut test = Test::new (ExtendedState::new().unwrap());
610      test.handle_event (EventParams::E{}.into()).unwrap();
611    }
612  }
613  #[test]
614  fn test_event_external() {
615    {
616      def_machine!{
617        Test () {
618          STATES [
619            state A ()
620            state B ()
621          ]
622          EVENTS [ event E <A> => <B> () ]
623          initial_state: A
624        }
625      }
626      let mut test = Test::initial();
627      test.handle_event (EventId::E.into()).unwrap();
628      assert_eq!(test.state_id(), StateId::B);
629    } {
630      def_machine_debug!{
631        Test () {
632          STATES [
633            state A ()
634            state B ()
635          ]
636          EVENTS [ event E <A> => <B> () ]
637          initial_state: A
638        }
639      }
640      let mut test = Test::initial();
641      test.handle_event (EventId::E.into()).unwrap();
642      assert_eq!(test.state_id(), StateId::B);
643    } {
644      def_machine_nodefault!{
645        Test () {
646          STATES [
647            state A ()
648            state B ()
649          ]
650          EVENTS [ event E <A> => <B> () ]
651          initial_state: A
652        }
653      }
654      let mut test = Test::new (ExtendedState::new().unwrap());
655      test.handle_event (EventParams::E{}.into()).unwrap();
656      assert_eq!(test.state_id(), StateId::B);
657    } {
658      def_machine_nodefault_debug!{
659        Test () {
660          STATES [
661            state A ()
662            state B ()
663          ]
664          EVENTS [ event E <A> => <B> () ]
665          initial_state: A
666        }
667      }
668      let mut test = Test::new (ExtendedState::new().unwrap());
669      test.handle_event (EventParams::E{}.into()).unwrap();
670      assert_eq!(test.state_id(), StateId::B);
671    }
672  }
673}