Skip to main content

rust_lstar/automata/
dot_parser.rs

1use super::state::State;
2use super::transition::Transition;
3use super::Automata;
4use crate::letter::Letter;
5use uuid::Uuid;
6
7/// Lightweight DOT parser assuming the format produced by `Automata::build_dot_code()`
8pub fn parse_dot(input: &str) -> Result<Automata, String> {
9    let input = input.trim();
10    if input.is_empty() {
11        return Err("Input DOT is empty".to_string());
12    }
13
14    if !input.starts_with("digraph ") {
15        return Err("DOT should start with 'digraph'".to_string());
16    }
17
18    let i_start_graph_def = input
19        .find('{')
20        .ok_or("Missing opening '{' for graph definition")?;
21
22    let automata_name = input[8..i_start_graph_def].trim().trim_matches('"');
23
24    if automata_name.is_empty() {
25        return Err("Automata name is empty".to_string());
26    }
27
28    let graph_def = input[i_start_graph_def + 1..].trim();
29    let graph_entries: Vec<&str> = graph_def.split(';').collect();
30
31    let mut states: Vec<State> = Vec::new();
32    for graph_entry in graph_entries {
33        if let Err(e) = parse_graph_entry(graph_entry, &mut states) {
34            return Err(format!(
35                "Error parsing graph entry '{}': {}",
36                graph_entry, e
37            ));
38        }
39    }
40    Ok(Automata::new(
41        State::new("S0".to_string()),
42        automata_name.to_string(),
43    ))
44}
45
46pub fn parse_graph_entry(graph_entry: &str, states: &mut Vec<State>) -> Result<(), String> {
47    let graph_entry = graph_entry.trim();
48    if graph_entry.is_empty() {
49        return Ok(());
50    }
51
52    if graph_entry.contains("[shape=") {
53        // Parse state definition
54        if let Some(start) = graph_entry.find('"') {
55            if let Some(end) = graph_entry[start + 1..].find('"') {
56                let state_name = &graph_entry[start + 1..start + 1 + end];
57                states.push(State::new(state_name.to_string()));
58            }
59        }
60    } else if graph_entry.contains("->") && graph_entry.contains("label=") {
61        // Parse transition
62        let parts: Vec<&str> = graph_entry.split("->").collect();
63        if parts.len() >= 2 {
64            let src = extract_quoted_value(parts[0])?;
65            let dest = extract_quoted_value(parts[1])?;
66
67            let label = extract_label_value(graph_entry)?;
68            let label_parts: Vec<&str> = label.split('/').map(|s| s.trim()).collect();
69            let (input, output) = if label_parts.len() >= 2 {
70                (label_parts[0].to_string(), label_parts[1].to_string())
71            } else {
72                (label, "".to_string())
73            };
74
75            let t_name =
76                extract_url_value(graph_entry).unwrap_or_else(|| Uuid::new_v4().to_string());
77
78            // Note: transitions cannot be added without access to source state
79            if let Some(state) = states.iter_mut().find(|s| s.name == src) {
80                state.add_transition(Transition::new_with_source(
81                    t_name,
82                    src.clone(),
83                    State::new(dest),
84                    Letter::new(input),
85                    Letter::new(output),
86                ));
87            } else {
88                return Err(format!("Source state '{}' not found for transition", src));
89            }
90        }
91    }
92
93    Ok(())
94}
95
96fn extract_quoted_value(s: &str) -> Result<String, String> {
97    if let Some(start) = s.find('"') {
98        if let Some(end) = s[start + 1..].find('"') {
99            return Ok(s[start + 1..start + 1 + end].to_string());
100        }
101    }
102    Err("Quoted value not found".to_string())
103}
104
105fn extract_label_value(s: &str) -> Result<String, String> {
106    if let Some(label_pos) = s.find("label=\"") {
107        let label_start = label_pos + 7; // after label="
108        if let Some(label_end_rel) = s[label_start..].find('"') {
109            return Ok(s[label_start..label_start + label_end_rel].to_string());
110        }
111    }
112    Err("Label value not found".to_string())
113}
114
115fn extract_url_value(s: &str) -> Option<String> {
116    if let Some(url_pos) = s.find("URL=") {
117        let url_start = url_pos + 4; // after URL=
118        if let Some(open_q) = s[url_start..].find('"') {
119            let real_start = url_start + open_q + 1;
120            if let Some(end_q) = s[real_start..].find('"') {
121                return Some(s[real_start..real_start + end_q].to_string());
122            }
123        }
124    }
125    None
126}
127
128/// Build DOT code representing the provided automata (reverse of `parse_dot`).
129pub fn build_dot_code(automata: &Automata) -> String {
130    let mut lines: Vec<String> = Vec::new();
131
132    lines.push(format!("digraph \"{}\" {{", automata.name));
133
134    let can_use_flat_transitions = !automata.transitions.is_empty()
135        && automata
136            .transitions
137            .iter()
138            .all(|transition| !transition.source_state.is_empty());
139
140    if can_use_flat_transitions {
141        let mut state_names: Vec<String> = vec![automata.initial_state.name.clone()];
142        for transition in &automata.transitions {
143            if !state_names.contains(&transition.source_state) {
144                state_names.push(transition.source_state.clone());
145            }
146            if !state_names.contains(&transition.output_state.name) {
147                state_names.push(transition.output_state.name.clone());
148            }
149        }
150
151        for state_name in &state_names {
152            let shape = if state_name == &automata.initial_state.name {
153                "doubleoctagon"
154            } else {
155                "ellipse"
156            };
157            lines.push(format!(
158                "    \"{}\" [shape={}, style=filled, fillcolor=white, URL=\"{}\"];",
159                state_name, shape, state_name
160            ));
161        }
162
163        for transition in &automata.transitions {
164            let label = transition.label();
165            lines.push(format!(
166                "    \"{}\" -> \"{}\" [fontsize=5, label=\"{}\", URL=\"{}\"];",
167                transition.source_state, transition.output_state.name, label, transition.name
168            ));
169        }
170    } else {
171        // include all states discovered from nested state transitions
172        let states = automata.get_states();
173        for state in &states {
174            let shape = if state.name == automata.initial_state.name {
175                "doubleoctagon"
176            } else {
177                "ellipse"
178            };
179            lines.push(format!(
180                "    \"{}\" [shape={}, style=filled, fillcolor=white, URL=\"{}\"];",
181                state.name, shape, state.name
182            ));
183        }
184
185        for current_state in &states {
186            for transition in &current_state.transitions {
187                let output_state = &transition.output_state;
188                let input = current_state.name.clone();
189                let output = output_state.name.clone();
190                let label = &transition.label();
191                lines.push(format!(
192                    "    \"{}\" -> \"{}\" [fontsize=5, label=\"{}\", URL=\"{}\"];",
193                    input, output, label, transition.name
194                ));
195            }
196        }
197    }
198
199    lines.push("}".to_string());
200    lines.join("\n")
201}