//! Helpers for generating [GraphViz //! Dot](https://graphviz.gitlab.io/_pages/pdf/dotguide.pdf) files to visually //! render automata. //! //! **This module only exists when the `"dot"` cargo feature is enabled.** use crate::{Automaton, Output, State}; use std::fmt::{Debug, Display}; use std::fs; use std::hash::Hash; use std::io::{self, Write}; use std::path::Path; /// Format the user-provided bits of an `Automaton` for Graphviz Dot output. /// /// There are two provided implementations of `DotFmt`: /// /// * [`DebugDotFmt`][crate::dot::DebugDotFmt] -- format each type parameter /// with its `std::fmt::Debug` implementation. /// /// * [`DisplayDotFmt`][crate::dot::DisplayDotFmt] -- format each type parameter /// with its `std::fmt::Display` implementation. /// /// You can also implement this trait yourself if your type parameters don't /// implement `Debug` or `Display`, or if you want to format them in some other /// way. pub trait DotFmt { /// Format a transition edge: `from ---input---> to`. /// /// This will be inside an [HTML /// label](https://www.graphviz.org/doc/info/shapes.html#html), so you may /// use balanced HTML tags. fn fmt_transition( &self, w: &mut impl Write, from: Option<&TState>, input: &TAlphabet, to: Option<&TState>, ) -> io::Result<()>; /// Format the custom data associated with a state. /// /// This will be inside an [HTML /// label](https://www.graphviz.org/doc/info/shapes.html#html), so you may /// use balanced HTML tags. fn fmt_state(&self, w: &mut impl Write, state: &TState) -> io::Result<()>; /// Format a transition's output or the final output of a final state. /// /// This will be inside an [HTML /// label](https://www.graphviz.org/doc/info/shapes.html#html), so you may /// use balanced HTML tags. fn fmt_output(&self, w: &mut impl Write, output: &TOutput) -> io::Result<()>; } impl Automaton where TAlphabet: Clone + Eq + Hash + Ord, TState: Clone + Eq + Hash, TOutput: Output, { /// Write this `Automaton` out as a [GraphViz /// Dot](https://graphviz.gitlab.io/_pages/pdf/dotguide.pdf) file at the /// given path. /// /// The `formatter` parameter controls how `TAlphabet`, `TState`, and /// `TOutput` are rendered. See the [`DotFmt`][crate::dot::DotFmt] trait for /// details. /// /// **This method only exists when the `"dot"` cargo feature is enabled.** pub fn write_dot_file( &self, formatter: &impl DotFmt, path: impl AsRef, ) -> io::Result<()> { let mut file = fs::File::create(path)?; self.write_dot(formatter, &mut file)?; Ok(()) } /// Write this `Automaton` out to the given write-able as a [GraphViz /// Dot](https://graphviz.gitlab.io/_pages/pdf/dotguide.pdf) file. /// /// The `formatter` parameter controls how `TAlphabet`, `TState`, and /// `TOutput` are rendered. See the [`DotFmt`][crate::dot::DotFmt] trait for /// details. /// /// **This method only exists when the `"dot"` cargo feature is enabled.** pub fn write_dot( &self, formatter: &impl DotFmt, w: &mut impl Write, ) -> io::Result<()> { writeln!(w, "digraph {{")?; writeln!(w, " rankdir = \"LR\";")?; writeln!(w, " nodesep = 2;")?; // Fake state for the incoming arrow to the start state. writeln!(w, " \"\" [shape = none];")?; // Each state, its associated custom data, and its final output. for (i, state_data) in self.state_data.iter().enumerate() { write!( w, r#" state_{i} [shape = {shape}, label = <")?; if let Some(final_output) = self.final_states.get(&State(i as u32)) { write!(w, r#"")?; } writeln!(w, "
{i}
"#, i = i, shape = if self.final_states.contains_key(&State(i as u32)) { "doublecircle" } else { "circle" } )?; if let Some(state_data) = state_data { formatter.fmt_state(w, state_data)?; } else { write!(w, "(no state data)")?; } write!(w, "
"#)?; formatter.fmt_output(w, final_output)?; write!(w, "
>];")?; } // Fake transition to the start state. writeln!(w, r#" "" -> state_{};"#, self.start_state.0)?; // Transitions between states and their outputs. for (from, transitions) in self.transitions.iter().enumerate() { for (input, (to, output)) in transitions { write!( w, r#" state_{from} -> state_{to} [label = <
Input:"#, from = from, to = to.0, )?; formatter.fmt_transition( w, self.state_data[from].as_ref(), input, self.state_data[to.0 as usize].as_ref(), )?; write!( w, r#"
Output:"#, )?; formatter.fmt_output(w, output)?; writeln!(w, "
>];")?; } } writeln!(w, "}}")?; Ok(()) } } /// Format an `Automaton`'s `TAlphabet`, `TState`, and `TOutput` with their /// `std::fmt::Debug` implementations. #[derive(Debug)] pub struct DebugDotFmt; impl DotFmt for DebugDotFmt where TAlphabet: Debug, TState: Debug, TOutput: Debug, { fn fmt_transition( &self, w: &mut impl Write, _from: Option<&TState>, input: &TAlphabet, _to: Option<&TState>, ) -> io::Result<()> { write!(w, r#"{:?}"#, input) } fn fmt_state(&self, w: &mut impl Write, state: &TState) -> io::Result<()> { write!(w, r#"{:?}"#, state) } fn fmt_output(&self, w: &mut impl Write, output: &TOutput) -> io::Result<()> { write!(w, r#"{:?}"#, output) } } /// Format an `Automaton`'s `TAlphabet`, `TState`, and `TOutput` with their /// `std::fmt::Display` implementations. #[derive(Debug)] pub struct DisplayDotFmt; impl DotFmt for DisplayDotFmt where TAlphabet: Display, TState: Display, TOutput: Display, { fn fmt_transition( &self, w: &mut impl Write, _from: Option<&TState>, input: &TAlphabet, _to: Option<&TState>, ) -> io::Result<()> { write!(w, "{}", input) } fn fmt_state(&self, w: &mut impl Write, state: &TState) -> io::Result<()> { write!(w, "{}", state) } fn fmt_output(&self, w: &mut impl Write, output: &TOutput) -> io::Result<()> { write!(w, "{}", output) } } #[cfg(test)] mod tests { use super::*; use crate::Builder; #[test] fn test_write_dot() { let mut builder = Builder::::new(); // Insert "mon" -> 1 let mut insertion = builder.insert(); insertion.next('m', 1).next('o', 0).next('n', 0); insertion.finish(); // Insert "sat" -> 6 let mut insertion = builder.insert(); insertion.next('s', 6).next('a', 0).next('t', 0); insertion.finish(); // Insert "sun" -> 0 let mut insertion = builder.insert(); insertion.next('s', 0).next('u', 0).next('n', 0); insertion.finish(); let automata = builder.finish(); let expected = r#" digraph { rankdir = "LR"; nodesep = 2; "" [shape = none]; state_0 [shape = doublecircle, label = <
0
(no state data)
0
>]; state_1 [shape = circle, label = <
1
(no state data)
>]; state_2 [shape = circle, label = <
2
(no state data)
>]; state_3 [shape = circle, label = <
3
(no state data)
>]; state_4 [shape = circle, label = <
4
(no state data)
>]; state_5 [shape = circle, label = <
5
(no state data)
>]; "" -> state_5; state_1 -> state_0 [label = <
Input:'n'
Output:0
>]; state_2 -> state_1 [label = <
Input:'o'
Output:0
>]; state_3 -> state_0 [label = <
Input:'t'
Output:0
>]; state_4 -> state_3 [label = <
Input:'a'
Output:6
>]; state_4 -> state_1 [label = <
Input:'u'
Output:0
>]; state_5 -> state_2 [label = <
Input:'m'
Output:1
>]; state_5 -> state_4 [label = <
Input:'s'
Output:0
>]; } "#; let mut buf = vec![]; automata.write_dot(&DebugDotFmt, &mut buf).unwrap(); let actual = String::from_utf8(buf).unwrap(); eprintln!("{}", actual); assert_eq!(expected.trim(), actual.trim()); } }