1use serde::{Deserialize, Serialize};
12
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15pub enum ChoiceGraphNode {
16 Start,
18 End,
20 Activity(String),
23 SubModel(u32),
25}
26
27#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
29pub struct ChoiceGraph {
30 pub nodes: Vec<ChoiceGraphNode>,
31 pub edges: Vec<(usize, usize)>,
32 pub start_idx: usize,
33 pub end_idx: usize,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
38pub enum ChoiceGraphError {
39 NoStart,
40 NoEnd,
41 MultipleStarts,
42 MultipleEnds,
43 StartHasIncoming,
44 EndHasOutgoing,
45 EdgeOutOfBounds,
46 Cyclic,
47 NodeNotOnStartEndPath,
48}
49
50impl core::fmt::Display for ChoiceGraphError {
51 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
52 let s = match self {
53 ChoiceGraphError::NoStart => "no Start node",
54 ChoiceGraphError::NoEnd => "no End node",
55 ChoiceGraphError::MultipleStarts => "multiple Start nodes",
56 ChoiceGraphError::MultipleEnds => "multiple End nodes",
57 ChoiceGraphError::StartHasIncoming => "Start node has incoming edges",
58 ChoiceGraphError::EndHasOutgoing => "End node has outgoing edges",
59 ChoiceGraphError::EdgeOutOfBounds => "edge endpoint out of bounds",
60 ChoiceGraphError::Cyclic => "graph is cyclic",
61 ChoiceGraphError::NodeNotOnStartEndPath => {
62 "node not on any Start→End path"
63 }
64 };
65 f.write_str(s)
66 }
67}
68
69impl std::error::Error for ChoiceGraphError {}
70
71impl ChoiceGraph {
72 pub fn new(
76 nodes: Vec<ChoiceGraphNode>,
77 edges: Vec<(usize, usize)>,
78 ) -> Result<Self, ChoiceGraphError> {
79 let mut start_idx: Option<usize> = None;
81 let mut end_idx: Option<usize> = None;
82 for (i, n) in nodes.iter().enumerate() {
83 match n {
84 ChoiceGraphNode::Start => {
85 if start_idx.is_some() {
86 return Err(ChoiceGraphError::MultipleStarts);
87 }
88 start_idx = Some(i);
89 }
90 ChoiceGraphNode::End => {
91 if end_idx.is_some() {
92 return Err(ChoiceGraphError::MultipleEnds);
93 }
94 end_idx = Some(i);
95 }
96 _ => {}
97 }
98 }
99 let start_idx = start_idx.ok_or(ChoiceGraphError::NoStart)?;
100 let end_idx = end_idx.ok_or(ChoiceGraphError::NoEnd)?;
101
102 let n = nodes.len();
104 for &(a, b) in &edges {
105 if a >= n || b >= n {
106 return Err(ChoiceGraphError::EdgeOutOfBounds);
107 }
108 }
109
110 for &(a, b) in &edges {
112 if b == start_idx {
113 return Err(ChoiceGraphError::StartHasIncoming);
114 }
115 if a == end_idx {
116 return Err(ChoiceGraphError::EndHasOutgoing);
117 }
118 }
119
120 let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n];
122 for &(a, b) in &edges {
123 adj[a].push(b);
124 }
125 let mut color = vec![0u8; n];
127 for s in 0..n {
128 if color[s] != 0 {
129 continue;
130 }
131 let mut stack: Vec<(usize, usize)> = vec![(s, 0)];
132 color[s] = 1;
133 while let Some(&(v, i)) = stack.last() {
134 if i < adj[v].len() {
135 let w = adj[v][i];
136 stack.last_mut().unwrap().1 += 1;
137 match color[w] {
138 0 => {
139 color[w] = 1;
140 stack.push((w, 0));
141 }
142 1 => return Err(ChoiceGraphError::Cyclic),
143 _ => {}
144 }
145 } else {
146 color[v] = 2;
147 stack.pop();
148 }
149 }
150 }
151
152 let mut radj: Vec<Vec<usize>> = vec![Vec::new(); n];
155 for &(a, b) in &edges {
156 radj[b].push(a);
157 }
158 let reach_from_start = bfs_reach(&adj, start_idx, n);
159 let reach_to_end = bfs_reach(&radj, end_idx, n);
160 for i in 0..n {
161 if !(reach_from_start[i] && reach_to_end[i]) {
162 return Err(ChoiceGraphError::NodeNotOnStartEndPath);
163 }
164 }
165
166 Ok(ChoiceGraph {
167 nodes,
168 edges,
169 start_idx,
170 end_idx,
171 })
172 }
173
174 pub fn successors(&self, node_idx: usize) -> Vec<usize> {
175 self.edges
176 .iter()
177 .filter_map(|&(a, b)| if a == node_idx { Some(b) } else { None })
178 .collect()
179 }
180
181 pub fn predecessors(&self, node_idx: usize) -> Vec<usize> {
182 self.edges
183 .iter()
184 .filter_map(|&(a, b)| if b == node_idx { Some(a) } else { None })
185 .collect()
186 }
187
188 pub fn has_empty_path(&self) -> bool {
190 self.edges
191 .iter()
192 .any(|&(a, b)| a == self.start_idx && b == self.end_idx)
193 }
194}
195
196fn bfs_reach(adj: &[Vec<usize>], src: usize, n: usize) -> Vec<bool> {
197 let mut seen = vec![false; n];
198 if src >= n {
199 return seen;
200 }
201 let mut q: Vec<usize> = vec![src];
202 seen[src] = true;
203 while let Some(v) = q.pop() {
204 for &w in &adj[v] {
205 if !seen[w] {
206 seen[w] = true;
207 q.push(w);
208 }
209 }
210 }
211 seen
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217
218 #[test]
219 fn minimal_valid() {
220 let cg = ChoiceGraph::new(
221 vec![ChoiceGraphNode::Start, ChoiceGraphNode::End],
222 vec![(0, 1)],
223 )
224 .unwrap();
225 assert_eq!(cg.start_idx, 0);
226 assert_eq!(cg.end_idx, 1);
227 assert!(cg.has_empty_path());
228 }
229
230 #[test]
231 fn no_start() {
232 let err = ChoiceGraph::new(vec![ChoiceGraphNode::End], vec![]).unwrap_err();
233 assert_eq!(err, ChoiceGraphError::NoStart);
234 }
235
236 #[test]
237 fn no_end() {
238 let err = ChoiceGraph::new(vec![ChoiceGraphNode::Start], vec![]).unwrap_err();
239 assert_eq!(err, ChoiceGraphError::NoEnd);
240 }
241
242 #[test]
243 fn multiple_starts() {
244 let err = ChoiceGraph::new(
245 vec![
246 ChoiceGraphNode::Start,
247 ChoiceGraphNode::Start,
248 ChoiceGraphNode::End,
249 ],
250 vec![(0, 2), (1, 2)],
251 )
252 .unwrap_err();
253 assert_eq!(err, ChoiceGraphError::MultipleStarts);
254 }
255
256 #[test]
257 fn start_has_incoming() {
258 let err = ChoiceGraph::new(
259 vec![
260 ChoiceGraphNode::Start,
261 ChoiceGraphNode::Activity("a".into()),
262 ChoiceGraphNode::End,
263 ],
264 vec![(1, 0), (0, 2)],
265 )
266 .unwrap_err();
267 assert_eq!(err, ChoiceGraphError::StartHasIncoming);
268 }
269
270 #[test]
271 fn end_has_outgoing() {
272 let err = ChoiceGraph::new(
273 vec![
274 ChoiceGraphNode::Start,
275 ChoiceGraphNode::Activity("a".into()),
276 ChoiceGraphNode::End,
277 ],
278 vec![(0, 2), (2, 1)],
279 )
280 .unwrap_err();
281 assert_eq!(err, ChoiceGraphError::EndHasOutgoing);
282 }
283
284 #[test]
285 fn edge_oob() {
286 let err = ChoiceGraph::new(
287 vec![ChoiceGraphNode::Start, ChoiceGraphNode::End],
288 vec![(0, 99)],
289 )
290 .unwrap_err();
291 assert_eq!(err, ChoiceGraphError::EdgeOutOfBounds);
292 }
293
294 #[test]
295 fn successors_predecessors() {
296 let cg = ChoiceGraph::new(
297 vec![
298 ChoiceGraphNode::Start,
299 ChoiceGraphNode::Activity("a".into()),
300 ChoiceGraphNode::End,
301 ],
302 vec![(0, 1), (1, 2)],
303 )
304 .unwrap();
305 assert_eq!(cg.successors(0), vec![1]);
306 assert_eq!(cg.predecessors(2), vec![1]);
307 }
308}