1use std::collections::HashSet;
2
3use libpetri_core::output::enumerate_branches;
4use libpetri_core::petri_net::PetriNet;
5
6use crate::environment::EnvironmentAnalysisMode;
7use crate::marking_state::MarkingState;
8use crate::scc::{compute_sccs, find_terminal_sccs};
9use crate::state_class_graph::StateClassGraph;
10
11#[derive(Debug)]
13pub struct LivenessResult {
14 pub state_class_graph: StateClassGraph,
15 pub all_sccs: Vec<Vec<usize>>,
16 pub terminal_sccs: Vec<Vec<usize>>,
17 pub goal_classes: HashSet<usize>,
18 pub can_reach_goal: HashSet<usize>,
19 pub is_goal_live: bool,
20 pub is_l4_live: bool,
21 pub is_complete: bool,
22 pub report: String,
23}
24
25#[derive(Debug)]
27pub struct XorBranchInfo {
28 pub transition_name: String,
29 pub total_branches: usize,
30 pub taken_branches: HashSet<usize>,
31 pub untaken_branches: HashSet<usize>,
32}
33
34#[derive(Debug)]
36pub struct XorBranchAnalysis {
37 pub branches: Vec<XorBranchInfo>,
38}
39
40impl XorBranchAnalysis {
41 pub fn unreachable_branches(&self) -> Vec<(&str, &HashSet<usize>)> {
42 self.branches
43 .iter()
44 .filter(|b| !b.untaken_branches.is_empty())
45 .map(|b| (b.transition_name.as_str(), &b.untaken_branches))
46 .collect()
47 }
48
49 pub fn is_xor_complete(&self) -> bool {
50 self.branches.iter().all(|b| b.untaken_branches.is_empty())
51 }
52
53 pub fn report(&self) -> String {
54 if self.branches.is_empty() {
55 return "No XOR transitions in net.".to_string();
56 }
57
58 let mut lines = Vec::new();
59 lines.push("XOR Branch Coverage Analysis".to_string());
60 lines.push("============================\n".to_string());
61
62 for info in &self.branches {
63 lines.push(format!("Transition: {}", info.transition_name));
64 lines.push(format!(" Branches: {}", info.total_branches));
65 lines.push(format!(
66 " Taken: [{:?}]",
67 info.taken_branches.iter().collect::<Vec<_>>()
68 ));
69
70 if info.untaken_branches.is_empty() {
71 lines.push(" All branches reachable".to_string());
72 } else {
73 lines.push(format!(
74 " UNREACHABLE: [{:?}]",
75 info.untaken_branches.iter().collect::<Vec<_>>()
76 ));
77 }
78 lines.push(String::new());
79 }
80
81 if self.is_xor_complete() {
82 lines.push("RESULT: All XOR branches are reachable.".to_string());
83 } else {
84 lines.push("RESULT: Some XOR branches are unreachable!".to_string());
85 }
86
87 lines.join("\n")
88 }
89}
90
91pub struct TimePetriNetAnalyzer<'a> {
93 net: &'a PetriNet,
94 initial_marking: MarkingState,
95 goal_places: Vec<String>,
96 max_classes: usize,
97 env_places: Vec<String>,
98 env_mode: EnvironmentAnalysisMode,
99}
100
101impl<'a> TimePetriNetAnalyzer<'a> {
102 pub fn for_net(net: &'a PetriNet) -> TimePetriNetAnalyzerBuilder<'a> {
103 TimePetriNetAnalyzerBuilder {
104 net,
105 initial_marking: MarkingState::new(),
106 goal_places: Vec::new(),
107 max_classes: 100_000,
108 env_places: Vec::new(),
109 env_mode: EnvironmentAnalysisMode::Ignore,
110 }
111 }
112
113 pub fn analyze(&self) -> LivenessResult {
115 let mut report = Vec::new();
116 report.push("=== TIME PETRI NET FORMAL ANALYSIS ===\n".to_string());
117 report.push("Method: State Class Graph (Berthomieu-Diaz 1991)".to_string());
118 report.push(format!("Net: {}", self.net.name()));
119 report.push(format!("Places: {}", self.net.places().len()));
120 report.push(format!("Transitions: {}", self.net.transitions().len()));
121 report.push(format!("Goal places: [{}]\n", self.goal_places.join(", ")));
122
123 report.push("Phase 1: Building State Class Graph...".to_string());
125 if !self.env_places.is_empty() {
126 report.push(format!(" Environment places: {}", self.env_places.len()));
127 report.push(format!(" Environment mode: {:?}", self.env_mode));
128 }
129 let env_refs: Vec<&str> = self.env_places.iter().map(|s| s.as_str()).collect();
130 let scg = StateClassGraph::build_with_env(
131 self.net,
132 &self.initial_marking,
133 self.max_classes,
134 &env_refs,
135 &self.env_mode,
136 );
137 report.push(format!(" State classes: {}", scg.class_count()));
138 report.push(format!(" Edges: {}", scg.edge_count()));
139 report.push(format!(
140 " Complete: {}",
141 if scg.is_complete() {
142 "YES"
143 } else {
144 "NO (truncated)"
145 }
146 ));
147
148 if !scg.is_complete() {
149 report.push(format!(
150 " WARNING: State class graph truncated at {} classes. Analysis may be incomplete.",
151 self.max_classes
152 ));
153 }
154 report.push(String::new());
155
156 report.push("Phase 2: Identifying goal state classes...".to_string());
158 let goal_place_refs: Vec<&str> = self.goal_places.iter().map(|s| s.as_str()).collect();
159 let mut goal_classes = HashSet::new();
160 for (idx, sc) in scg.classes().iter().enumerate() {
161 if sc.marking.has_tokens_in_any(&goal_place_refs) {
162 goal_classes.insert(idx);
163 }
164 }
165 report.push(format!(" Goal state classes: {}\n", goal_classes.len()));
166
167 report.push("Phase 3: Computing Strongly Connected Components...".to_string());
169 let successor_fn = |idx: usize| scg.successors(idx).to_vec();
170 let all_sccs = compute_sccs(scg.class_count(), successor_fn);
171 let terminal_sccs = find_terminal_sccs(scg.class_count(), successor_fn);
172
173 report.push(format!(" Total SCCs: {}", all_sccs.len()));
174 report.push(format!(" Terminal SCCs: {}\n", terminal_sccs.len()));
175
176 report.push("Phase 4: Verifying Goal Liveness...".to_string());
178 report
179 .push(" Property: From every reachable state, a goal state is reachable".to_string());
180
181 let mut terminal_without_goal = 0;
182 for scc in &terminal_sccs {
183 if !scc.iter().any(|idx| goal_classes.contains(idx)) {
184 terminal_without_goal += 1;
185 }
186 }
187
188 let can_reach_goal = compute_backward_reachability(&scg, &goal_classes);
189 let states_not_reaching_goal = scg.class_count() - can_reach_goal.len();
190
191 report.push(format!(
192 " Terminal SCCs with goal: {}",
193 terminal_sccs.len() - terminal_without_goal
194 ));
195 report.push(format!(
196 " Terminal SCCs without goal: {}",
197 terminal_without_goal
198 ));
199 report.push(format!(
200 " States that can reach goal: {}/{}\n",
201 can_reach_goal.len(),
202 scg.class_count()
203 ));
204
205 let is_goal_live = terminal_without_goal == 0 && states_not_reaching_goal == 0;
206
207 report.push("Phase 5: Verifying Classical Liveness (L4)...".to_string());
209 report
210 .push(" Property: Every transition can fire from every reachable marking".to_string());
211
212 let all_transition_names: HashSet<&str> =
213 self.net.transitions().iter().map(|t| t.name()).collect();
214
215 let mut terminal_missing_transitions = 0;
216 for scc in &terminal_sccs {
217 let scc_set: HashSet<usize> = scc.iter().copied().collect();
218 let mut transitions_in_scc: HashSet<String> = HashSet::new();
219
220 for &class_idx in scc {
221 for t_name in scg.enabled_transitions(class_idx) {
222 let edges = scg.branch_edges(class_idx, t_name);
223 for edge in edges {
224 if scc_set.contains(&edge.to) {
225 transitions_in_scc.insert(t_name.clone());
226 }
227 }
228 }
229 }
230
231 let mut missing_any = false;
232 for &t_name in &all_transition_names {
233 if !transitions_in_scc.contains(t_name) {
234 missing_any = true;
235 break;
236 }
237 }
238 if missing_any {
239 terminal_missing_transitions += 1;
240 let missing: Vec<&str> = all_transition_names
241 .iter()
242 .filter(|&&t| !transitions_in_scc.contains(t))
243 .copied()
244 .collect();
245 report.push(format!(
246 " Terminal SCC missing transitions: [{}]",
247 missing.join(", ")
248 ));
249 }
250 }
251
252 let is_l4_live = terminal_missing_transitions == 0 && scg.is_complete();
253
254 report.push("\n=== ANALYSIS RESULT ===\n".to_string());
256
257 if is_goal_live && scg.is_complete() {
258 report.push("GOAL LIVENESS VERIFIED".to_string());
259 report.push(
260 " From every reachable state class, a goal marking is reachable.".to_string(),
261 );
262 } else if is_goal_live {
263 report.push("GOAL LIVENESS LIKELY (incomplete proof)".to_string());
264 } else {
265 report.push("GOAL LIVENESS VIOLATION".to_string());
266 if terminal_without_goal > 0 {
267 report.push(format!(
268 " {} terminal SCC(s) have no goal state.",
269 terminal_without_goal
270 ));
271 }
272 if states_not_reaching_goal > 0 {
273 report.push(format!(
274 " {} state class(es) cannot reach goal.",
275 states_not_reaching_goal
276 ));
277 }
278 }
279
280 report.push(String::new());
281
282 if is_l4_live {
283 report.push("CLASSICAL LIVENESS (L4) VERIFIED".to_string());
284 } else {
285 report.push("CLASSICAL LIVENESS (L4) NOT VERIFIED".to_string());
286 if terminal_missing_transitions > 0 {
287 report.push(" Some terminal SCCs don't contain all transitions.".to_string());
288 }
289 if !scg.is_complete() {
290 report.push(" (State class graph incomplete - cannot prove L4)".to_string());
291 }
292 }
293
294 let is_complete = scg.is_complete();
295
296 LivenessResult {
297 state_class_graph: scg,
298 all_sccs,
299 terminal_sccs,
300 goal_classes,
301 can_reach_goal,
302 is_goal_live,
303 is_l4_live,
304 is_complete,
305 report: report.join("\n"),
306 }
307 }
308
309 pub fn analyze_xor_branches(net: &PetriNet, scg: &StateClassGraph) -> XorBranchAnalysis {
311 let mut branches = Vec::new();
312
313 for transition in net.transitions() {
314 let out_spec = match transition.output_spec() {
315 Some(spec) => spec,
316 None => continue,
317 };
318
319 let all_branches = enumerate_branches(out_spec);
320 if all_branches.len() <= 1 {
321 continue;
322 }
323
324 let mut taken = HashSet::new();
325 for class_idx in 0..scg.class_count() {
326 for edge in scg.branch_edges(class_idx, transition.name()) {
327 taken.insert(edge.branch_index);
328 }
329 }
330
331 let untaken: HashSet<usize> = (0..all_branches.len())
332 .filter(|i| !taken.contains(i))
333 .collect();
334
335 branches.push(XorBranchInfo {
336 transition_name: transition.name().to_string(),
337 total_branches: all_branches.len(),
338 taken_branches: taken,
339 untaken_branches: untaken,
340 });
341 }
342
343 XorBranchAnalysis { branches }
344 }
345}
346
347fn compute_backward_reachability(scg: &StateClassGraph, goals: &HashSet<usize>) -> HashSet<usize> {
348 let mut reachable: HashSet<usize> = goals.clone();
349 let mut queue: Vec<usize> = goals.iter().copied().collect();
350
351 while let Some(current) = queue.pop() {
352 for &pred in scg.predecessors(current) {
353 if reachable.insert(pred) {
354 queue.push(pred);
355 }
356 }
357 }
358
359 reachable
360}
361
362pub struct TimePetriNetAnalyzerBuilder<'a> {
364 net: &'a PetriNet,
365 initial_marking: MarkingState,
366 goal_places: Vec<String>,
367 max_classes: usize,
368 env_places: Vec<String>,
369 env_mode: EnvironmentAnalysisMode,
370}
371
372impl<'a> TimePetriNetAnalyzerBuilder<'a> {
373 pub fn initial_marking(mut self, marking: MarkingState) -> Self {
374 self.initial_marking = marking;
375 self
376 }
377
378 pub fn goal_place(mut self, place_name: &str) -> Self {
379 self.goal_places.push(place_name.to_string());
380 self
381 }
382
383 pub fn goal_places(mut self, names: &[&str]) -> Self {
384 for name in names {
385 self.goal_places.push(name.to_string());
386 }
387 self
388 }
389
390 pub fn max_classes(mut self, max: usize) -> Self {
391 self.max_classes = max;
392 self
393 }
394
395 pub fn env_place(mut self, place_name: &str) -> Self {
396 self.env_places.push(place_name.to_string());
397 self
398 }
399
400 pub fn env_places(mut self, names: &[&str]) -> Self {
401 for name in names {
402 self.env_places.push(name.to_string());
403 }
404 self
405 }
406
407 pub fn env_mode(mut self, mode: EnvironmentAnalysisMode) -> Self {
408 self.env_mode = mode;
409 self
410 }
411
412 pub fn build(self) -> TimePetriNetAnalyzer<'a> {
413 assert!(
414 !self.goal_places.is_empty(),
415 "At least one goal place must be specified"
416 );
417 TimePetriNetAnalyzer {
418 net: self.net,
419 initial_marking: self.initial_marking,
420 goal_places: self.goal_places,
421 max_classes: self.max_classes,
422 env_places: self.env_places,
423 env_mode: self.env_mode,
424 }
425 }
426}
427
428#[cfg(test)]
429mod tests {
430 use super::*;
431 use crate::marking_state::MarkingStateBuilder;
432 use libpetri_core::input::one;
433 use libpetri_core::output::{out_place, xor};
434 use libpetri_core::place::Place;
435 use libpetri_core::transition::Transition;
436
437 #[test]
438 fn goal_liveness_circular_net() {
439 let p_a = Place::<i32>::new("A");
440 let p_b = Place::<i32>::new("B");
441
442 let t1 = Transition::builder("t1")
443 .input(one(&p_a))
444 .output(out_place(&p_b))
445 .build();
446 let t2 = Transition::builder("t2")
447 .input(one(&p_b))
448 .output(out_place(&p_a))
449 .build();
450
451 let net = PetriNet::builder("circular").transitions([t1, t2]).build();
452
453 let result = TimePetriNetAnalyzer::for_net(&net)
454 .initial_marking(MarkingStateBuilder::new().tokens("A", 1).build())
455 .goal_place("B")
456 .max_classes(100)
457 .build()
458 .analyze();
459
460 assert!(result.is_goal_live);
461 assert!(result.is_complete);
462 assert!(result.state_class_graph.class_count() >= 2);
463 assert!(result.report.contains("GOAL LIVENESS VERIFIED"));
464 }
465
466 #[test]
467 fn goal_liveness_violation_dead_end() {
468 let p_a = Place::<i32>::new("A");
469 let p_b = Place::<i32>::new("B");
470 let p_goal = Place::<i32>::new("Goal");
471
472 let t1 = Transition::builder("t1")
473 .input(one(&p_a))
474 .output(out_place(&p_b))
475 .build();
476
477 let net = PetriNet::builder("deadend").transition(t1).build();
478 let _ = &p_goal; let result = TimePetriNetAnalyzer::for_net(&net)
481 .initial_marking(MarkingStateBuilder::new().tokens("A", 1).build())
482 .goal_place("Goal")
483 .max_classes(100)
484 .build()
485 .analyze();
486
487 assert!(!result.is_goal_live);
488 assert!(result.report.contains("GOAL LIVENESS VIOLATION"));
489 }
490
491 #[test]
492 fn l4_liveness_circular_net() {
493 let p_a = Place::<i32>::new("A");
494 let p_b = Place::<i32>::new("B");
495
496 let t1 = Transition::builder("t1")
497 .input(one(&p_a))
498 .output(out_place(&p_b))
499 .build();
500 let t2 = Transition::builder("t2")
501 .input(one(&p_b))
502 .output(out_place(&p_a))
503 .build();
504
505 let net = PetriNet::builder("circular").transitions([t1, t2]).build();
506
507 let result = TimePetriNetAnalyzer::for_net(&net)
508 .initial_marking(MarkingStateBuilder::new().tokens("A", 1).build())
509 .goal_place("A")
510 .max_classes(100)
511 .build()
512 .analyze();
513
514 assert!(result.is_l4_live);
515 assert!(result.report.contains("CLASSICAL LIVENESS (L4) VERIFIED"));
516 }
517
518 #[test]
519 fn l4_violation_dead_end() {
520 let p_a = Place::<i32>::new("A");
521 let p_b = Place::<i32>::new("B");
522
523 let t1 = Transition::builder("t1")
524 .input(one(&p_a))
525 .output(out_place(&p_b))
526 .build();
527
528 let net = PetriNet::builder("deadend").transition(t1).build();
529
530 let result = TimePetriNetAnalyzer::for_net(&net)
531 .initial_marking(MarkingStateBuilder::new().tokens("A", 1).build())
532 .goal_place("B")
533 .max_classes(100)
534 .build()
535 .analyze();
536
537 assert!(!result.is_l4_live);
538 }
539
540 #[test]
541 fn xor_branch_analysis() {
542 let p0 = Place::<i32>::new("start");
543 let p_a = Place::<i32>::new("branchA");
544 let p_b = Place::<i32>::new("branchB");
545 let p_end = Place::<i32>::new("end");
546
547 let t_choice = Transition::builder("choice")
548 .input(one(&p0))
549 .output(xor(vec![out_place(&p_a), out_place(&p_b)]))
550 .build();
551 let t_a = Transition::builder("fromA")
552 .input(one(&p_a))
553 .output(out_place(&p_end))
554 .build();
555 let t_b = Transition::builder("fromB")
556 .input(one(&p_b))
557 .output(out_place(&p_end))
558 .build();
559
560 let net = PetriNet::builder("xor")
561 .transitions([t_choice, t_a, t_b])
562 .build();
563
564 let marking = MarkingStateBuilder::new().tokens("start", 1).build();
565 let scg = StateClassGraph::build(&net, &marking, 100);
566 let analysis = TimePetriNetAnalyzer::analyze_xor_branches(&net, &scg);
567
568 assert_eq!(analysis.branches.len(), 1);
569 assert_eq!(analysis.branches[0].transition_name, "choice");
570 assert_eq!(analysis.branches[0].total_branches, 2);
571 assert_eq!(analysis.branches[0].taken_branches.len(), 2);
572 assert!(analysis.branches[0].untaken_branches.is_empty());
573 assert!(analysis.is_xor_complete());
574 assert!(analysis.unreachable_branches().is_empty());
575 }
576
577 #[test]
578 fn xor_report_generation() {
579 let p0 = Place::<i32>::new("start");
580 let p_a = Place::<i32>::new("branchA");
581 let p_b = Place::<i32>::new("branchB");
582
583 let t_choice = Transition::builder("choice")
584 .input(one(&p0))
585 .output(xor(vec![out_place(&p_a), out_place(&p_b)]))
586 .build();
587
588 let net = PetriNet::builder("xor").transition(t_choice).build();
589
590 let marking = MarkingStateBuilder::new().tokens("start", 1).build();
591 let scg = StateClassGraph::build(&net, &marking, 100);
592 let analysis = TimePetriNetAnalyzer::analyze_xor_branches(&net, &scg);
593
594 let report = analysis.report();
595 assert!(report.contains("XOR Branch Coverage"));
596 assert!(report.contains("choice"));
597 }
598
599 #[test]
600 fn no_xor_report() {
601 let p_a = Place::<i32>::new("A");
602 let p_b = Place::<i32>::new("B");
603
604 let t1 = Transition::builder("t1")
605 .input(one(&p_a))
606 .output(out_place(&p_b))
607 .build();
608
609 let net = PetriNet::builder("no-xor").transition(t1).build();
610
611 let marking = MarkingStateBuilder::new().tokens("A", 1).build();
612 let scg = StateClassGraph::build(&net, &marking, 100);
613 let analysis = TimePetriNetAnalyzer::analyze_xor_branches(&net, &scg);
614
615 assert_eq!(analysis.report(), "No XOR transitions in net.");
616 }
617
618 #[test]
619 #[should_panic(expected = "At least one goal place")]
620 fn builder_requires_goal_places() {
621 let p_a = Place::<i32>::new("A");
622 let t1 = Transition::builder("t1").input(one(&p_a)).build();
623 let net = PetriNet::builder("test").transition(t1).build();
624
625 TimePetriNetAnalyzer::for_net(&net)
626 .initial_marking(MarkingState::new())
627 .build();
628 }
629
630 #[test]
631 fn always_available_env_enables_transitions() {
632 let p_ready = Place::<i32>::new("ready");
633 let p_env = Place::<i32>::new("input");
634 let p_out = Place::<i32>::new("output");
635
636 let t1 = Transition::builder("process")
637 .input(one(&p_ready))
638 .input(one(&p_env))
639 .output(out_place(&p_out))
640 .build();
641 let t2 = Transition::builder("reset")
642 .input(one(&p_out))
643 .output(out_place(&p_ready))
644 .build();
645
646 let net = PetriNet::builder("env-net").transitions([t1, t2]).build();
647
648 let result = TimePetriNetAnalyzer::for_net(&net)
649 .initial_marking(MarkingStateBuilder::new().tokens("ready", 1).build())
650 .goal_place("output")
651 .env_place("input")
652 .env_mode(EnvironmentAnalysisMode::AlwaysAvailable)
653 .max_classes(100)
654 .build()
655 .analyze();
656
657 assert!(result.is_goal_live);
658 assert!(result.report.contains("Environment places"));
659 }
660
661 #[test]
662 fn ignore_env_treats_as_regular() {
663 let p_env = Place::<i32>::new("input");
664 let p_out = Place::<i32>::new("output");
665
666 let t1 = Transition::builder("process")
667 .input(one(&p_env))
668 .output(out_place(&p_out))
669 .build();
670
671 let net = PetriNet::builder("env-net").transition(t1).build();
672
673 let result = TimePetriNetAnalyzer::for_net(&net)
674 .initial_marking(MarkingState::new())
675 .goal_place("output")
676 .env_place("input")
677 .env_mode(EnvironmentAnalysisMode::Ignore)
678 .max_classes(100)
679 .build()
680 .analyze();
681
682 assert!(!result.is_goal_live);
684 }
685
686 #[test]
687 fn bounded_env_limits_tokens() {
688 let p_ready = Place::<i32>::new("ready");
689 let p_env = Place::<i32>::new("input");
690 let p_out = Place::<i32>::new("output");
691
692 let t1 = Transition::builder("process")
693 .input(one(&p_ready))
694 .input(one(&p_env))
695 .output(out_place(&p_out))
696 .build();
697 let t2 = Transition::builder("reset")
698 .input(one(&p_out))
699 .output(out_place(&p_ready))
700 .build();
701
702 let net = PetriNet::builder("env-net").transitions([t1, t2]).build();
703
704 let result = TimePetriNetAnalyzer::for_net(&net)
705 .initial_marking(MarkingStateBuilder::new().tokens("ready", 1).build())
706 .goal_place("output")
707 .env_place("input")
708 .env_mode(EnvironmentAnalysisMode::Bounded { max_tokens: 1 })
709 .max_classes(100)
710 .build()
711 .analyze();
712
713 assert!(result.is_complete);
714 }
715
716 #[test]
717 fn comprehensive_report() {
718 let p_a = Place::<i32>::new("A");
719 let p_b = Place::<i32>::new("B");
720
721 let t1 = Transition::builder("t1")
722 .input(one(&p_a))
723 .output(out_place(&p_b))
724 .build();
725 let t2 = Transition::builder("t2")
726 .input(one(&p_b))
727 .output(out_place(&p_a))
728 .build();
729
730 let net = PetriNet::builder("circular").transitions([t1, t2]).build();
731
732 let result = TimePetriNetAnalyzer::for_net(&net)
733 .initial_marking(MarkingStateBuilder::new().tokens("A", 1).build())
734 .goal_place("A")
735 .max_classes(100)
736 .build()
737 .analyze();
738
739 assert!(result.report.contains("TIME PETRI NET FORMAL ANALYSIS"));
740 assert!(result.report.contains("Berthomieu-Diaz"));
741 assert!(result.report.contains("State classes:"));
742 assert!(result.report.contains("Terminal SCCs:"));
743 assert!(result.report.contains("ANALYSIS RESULT"));
744 }
745}