scnr/internal/
dot.rs

1//! The `dot` module contains the conversion from an finite automata to a graphviz dot format.
2//! The functions in this module are used for testing and debugging purposes.
3
4use std::io::Write;
5
6use dot_writer::{Attributes, DotWriter, RankDirection, Scope};
7
8use crate::internal::compiled_dfa::CompiledDfa;
9
10use super::{nfa::Nfa, CharClassID, CharacterClassRegistry, MultiPatternNfa, StateID};
11
12/// Render the NFA to a graphviz dot format.
13#[allow(dead_code)]
14pub(crate) fn nfa_render<W: Write>(nfa: &Nfa, label: &str, output: &mut W) {
15    let mut writer = DotWriter::from(output);
16    writer.set_pretty_print(true);
17    let mut digraph = writer.digraph();
18    digraph
19        .set_label(format!("{}: {}", label, nfa.pattern()).as_str())
20        .set_rank_direction(RankDirection::LeftRight);
21    for state in nfa.states() {
22        let source_id = {
23            let mut source_node = digraph.node_auto();
24            source_node.set_label(&state.id().as_usize().to_string());
25            if state.id() == nfa.start_state() {
26                source_node
27                    .set_shape(dot_writer::Shape::Circle)
28                    .set_color(dot_writer::Color::Blue)
29                    .set_pen_width(3.0);
30            }
31            if state.id() == nfa.end_state() {
32                source_node
33                    .set_shape(dot_writer::Shape::Circle)
34                    .set_color(dot_writer::Color::Red)
35                    .set_pen_width(3.0);
36            }
37            source_node.id()
38        };
39        for transition in state.transitions() {
40            let target_state = transition.target_state();
41            digraph
42                .edge(
43                    source_id.clone(),
44                    format!("node_{}", target_state.as_usize()),
45                )
46                .attributes()
47                .set_label(
48                    &format!("#{}:'{}'", transition.char_class(), transition.ast())
49                        .escape_default()
50                        .to_string(),
51                );
52        }
53        for epsilon_transition in state.epsilon_transitions() {
54            let target_state = epsilon_transition.target_state();
55            digraph
56                .edge(
57                    source_id.clone(),
58                    format!("node_{}", target_state.as_usize()),
59                )
60                .attributes()
61                .set_label("ε");
62        }
63    }
64}
65
66fn render_compiled_dfa(
67    compiled_dfa: &CompiledDfa,
68    node_prefix: &str,
69    character_class_registry: &CharacterClassRegistry,
70    graph: &mut Scope,
71) {
72    // Render the states of the NFA
73    for id in 0..compiled_dfa.states.len() {
74        let node_name = format!("\"{}{}\"", node_prefix, id);
75        let mut source_node = graph.node_named(&node_name);
76        if id == 0 {
77            // Start state of the compiled NFA
78            source_node
79                .set_shape(dot_writer::Shape::Circle)
80                .set_color(dot_writer::Color::Blue)
81                .set_pen_width(3.0);
82            source_node.set_label(&id.to_string());
83        } else if compiled_dfa.end_states[id].0 {
84            source_node
85                .set_shape(dot_writer::Shape::Circle)
86                .set_color(dot_writer::Color::Red)
87                .set_pen_width(3.0);
88            source_node.set_label(&format!("{} T{}", id, compiled_dfa.end_states[id].1));
89        } else {
90            source_node.set_label(&id.to_string());
91        }
92    }
93    // Render the transitions of the NFA
94    for (id, state) in compiled_dfa.states.iter().enumerate() {
95        for (cc, next) in state.transitions.iter() {
96            // Label the edge with the character class used to transition to the target state.
97            graph
98                .edge(
99                    format!("\"{}{}\"", node_prefix, id),
100                    format!("\"{}{}\"", node_prefix, next.as_usize()),
101                )
102                .attributes()
103                .set_label(&format!(
104                    "{} (C#{})",
105                    character_class_registry.get_character_class(*cc).map_or(
106                        "-".to_string(),
107                        |cc| cc.ast().to_string().escape_debug().to_string()
108                    ),
109                    cc.id()
110                ));
111        }
112    }
113}
114
115// Render a compiled DFA
116pub(crate) fn compiled_dfa_render<W: Write>(
117    compiled_dfa: &CompiledDfa,
118    label: &str,
119    character_class_registry: &CharacterClassRegistry,
120    output: &mut W,
121) {
122    let mut writer = DotWriter::from(output);
123    writer.set_pretty_print(true);
124    let mut digraph = writer.digraph();
125    digraph
126        .set_label(
127            format!(
128                "{}: {}...",
129                label,
130                compiled_dfa.pattern(0.into()).escape_default()
131            )
132            .as_str(),
133        )
134        .set_rank_direction(RankDirection::LeftRight);
135
136    render_compiled_dfa(compiled_dfa, "", character_class_registry, &mut digraph);
137
138    // Render the lookaheads of the DFA each into a separate cluster
139    for (terminal_id, lookahead) in compiled_dfa.lookaheads.iter() {
140        let mut cluster = digraph.cluster();
141        cluster.set_label(&format!(
142            "LA for T{}({})",
143            terminal_id,
144            if lookahead.is_positive { "Pos" } else { "Neg" }
145        ));
146        let node_prefix = format!("{}_", terminal_id);
147        render_compiled_dfa(
148            &lookahead.nfa,
149            &node_prefix,
150            character_class_registry,
151            &mut cluster,
152        );
153    }
154}
155
156/// Render a MultiPatternNfa to a graphviz dot format.
157#[allow(dead_code)]
158pub(crate) fn multi_pattern_nfa_render<W: Write>(
159    multi_pattern_nfa: &MultiPatternNfa,
160    label: &str,
161    character_class_registry: &CharacterClassRegistry,
162    output: &mut W,
163) {
164    let mut writer = DotWriter::from(output);
165    writer.set_pretty_print(true);
166    let mut digraph = writer.digraph();
167    digraph
168        .set_label(label)
169        .set_rank_direction(RankDirection::LeftRight);
170
171    let node_ids = multi_pattern_nfa.nfas.iter().fold(vec![0], |mut acc, nfa| {
172        nfa.states()
173            .iter()
174            .for_each(|state| acc.push(state.id().id()));
175        acc
176    });
177
178    // Add the start transitions of the MultiPatternNfa to the epsilon transitions
179    let mut epsilon_transitions = multi_pattern_nfa.start_transitions().iter().fold(
180        Vec::<(usize, usize)>::new(),
181        |mut acc, t| {
182            acc.push((0, t.target_state().as_usize()));
183            acc
184        },
185    );
186    // Add the epsilon transitions of the NFAs to the epsilon transitions
187    multi_pattern_nfa.nfas.iter().for_each(|nfa| {
188        nfa.states().iter().for_each(|state| {
189            state.epsilon_transitions().iter().for_each(|t| {
190                epsilon_transitions.push((state.id().as_usize(), t.target_state().as_usize()));
191            });
192        });
193    });
194
195    // Add the transitions of the NFAs to the transitions
196    let transitions = multi_pattern_nfa.nfas.iter().fold(
197        Vec::<(StateID, StateID, CharClassID)>::new(),
198        |mut acc, nfa| {
199            nfa.states().iter().for_each(|state| {
200                state.transitions().iter().for_each(|t| {
201                    acc.push((state.id(), t.target_state(), t.char_class()));
202                });
203            });
204            acc
205        },
206    );
207
208    for node_id in node_ids {
209        let mut source_node = digraph.node_auto();
210        source_node.set_label(&node_id.to_string());
211        if node_id == 0 {
212            source_node
213                .set_shape(dot_writer::Shape::Circle)
214                .set_color(dot_writer::Color::Blue)
215                .set_pen_width(3.0);
216        }
217        if multi_pattern_nfa.is_accepting_state(node_id.into()) {
218            source_node
219                .set_shape(dot_writer::Shape::Circle)
220                .set_color(dot_writer::Color::Red)
221                .set_pen_width(3.0);
222        }
223    }
224
225    for (source_id, target_id) in epsilon_transitions {
226        digraph
227            .edge(format!("node_{}", source_id), format!("node_{}", target_id))
228            .attributes()
229            .set_label("ε");
230    }
231
232    for (source_id, target_id, char_class_id) in transitions {
233        digraph
234            .edge(
235                format!("node_{}", source_id.as_usize()),
236                format!("node_{}", target_id.as_usize()),
237            )
238            .attributes()
239            .set_label(&format!(
240                "{}:{}",
241                character_class_registry
242                    .get_character_class(char_class_id)
243                    .map_or("-".to_string(), |cc| cc
244                        .ast()
245                        .to_string()
246                        .escape_default()
247                        .to_string()),
248                char_class_id.id()
249            ));
250    }
251}