link_cli/changes_simplifier.rs
1//! ChangesSimplifier - Simplifies a list of changes
2//!
3//! This module provides functionality to simplify a list of changes by
4//! identifying chains of transformations.
5//! Corresponds to ChangesSimplifier.cs in C#
6
7use crate::link::Link;
8use std::collections::{HashMap, HashSet};
9
10/// Simplifies a list of changes by identifying chains of transformations.
11///
12/// If multiple final states are reachable from the same initial state, returns multiple simplified changes.
13/// If a scenario arises where no initial or final states can be identified (no-ops), returns the original transitions as-is.
14pub fn simplify_changes(changes: Vec<(Link, Link)>) -> Vec<(Link, Link)> {
15 if changes.is_empty() {
16 return vec![];
17 }
18
19 // **FIX for Issue #26**: Remove duplicate before states by keeping the non-null transitions
20 // This handles cases where the same link is reported with multiple different transformations
21 let changes = remove_duplicate_before_states(changes);
22
23 // First, handle unchanged states directly
24 let mut unchanged_states = Vec::new();
25 let mut changed_states = Vec::new();
26
27 for (before, after) in changes.iter() {
28 if before == after {
29 unchanged_states.push((*before, *after));
30 } else {
31 changed_states.push((*before, *after));
32 }
33 }
34
35 // Gather all 'Before' links and all 'After' links from changed states.
36 //
37 // C# builds these with `new HashSet<Link<uint>>(...)`, which enumerates in
38 // insertion order; a Rust `HashSet` enumerates in an order derived from a
39 // per-process random seed. Since the order of `initialStates` decides the
40 // order of the simplified results whenever the final sort ties, that
41 // difference would make `--changes` non-reproducible. Keeping the first
42 // occurrences in a `Vec` (and using the set only for lookups) restores the
43 // C# ordering.
44 let before_links = distinct(changed_states.iter().map(|(b, _)| *b));
45 let after_links = distinct(changed_states.iter().map(|(_, a)| *a));
46 let before_set: HashSet<Link> = before_links.iter().copied().collect();
47 let after_set: HashSet<Link> = after_links.iter().copied().collect();
48
49 // Identify initial states: appear as Before but never as After
50 let initial_states: Vec<Link> = before_links
51 .iter()
52 .filter(|b| !after_set.contains(b))
53 .copied()
54 .collect();
55
56 // Identify final states: appear as After but never as Before
57 let final_states: HashSet<Link> = after_links
58 .iter()
59 .filter(|a| !before_set.contains(a))
60 .copied()
61 .collect();
62
63 // Build adjacency (Before -> possible list of After links)
64 let mut adjacency: HashMap<Link, Vec<Link>> = HashMap::new();
65 for (before, after) in changed_states.iter() {
66 adjacency.entry(*before).or_default().push(*after);
67 }
68
69 // If we have no identified initial states, treat it as a no-op scenario:
70 // just return original transitions.
71 if initial_states.is_empty() {
72 return changes;
73 }
74
75 let mut results = Vec::new();
76
77 // Add unchanged states first
78 results.extend(unchanged_states);
79
80 // Traverse each initial state with DFS
81 for initial in distinct(initial_states.iter().copied()) {
82 let initial = &initial;
83 let mut stack = vec![*initial];
84 let mut visited: HashSet<Link> = HashSet::new();
85
86 while let Some(current) = stack.pop() {
87 // Skip if already visited
88 if !visited.insert(current) {
89 continue;
90 }
91
92 let has_next = adjacency.contains_key(¤t);
93 let next_links = adjacency.get(¤t);
94 let is_final_or_dead_end = final_states.contains(¤t)
95 || !has_next
96 || next_links.is_none_or(|v| v.is_empty());
97
98 // If final or no further transitions, record (initial -> current)
99 if is_final_or_dead_end {
100 results.push((*initial, current));
101 }
102
103 // Otherwise push neighbors
104 if let Some(next_links) = next_links {
105 for next in next_links {
106 stack.push(*next);
107 }
108 }
109 }
110 }
111
112 // Sort the final results so that items appear in ascending order by their After link.
113 // This ensures tests that expect a specific order pass reliably.
114 results.sort_by(|a, b| {
115 a.1.index
116 .cmp(&b.1.index)
117 .then_with(|| a.1.source.cmp(&b.1.source))
118 .then_with(|| a.1.target.cmp(&b.1.target))
119 });
120
121 results
122}
123
124/// Removes problematic duplicate before states that lead to simplification issues.
125/// This fixes Issue #26 where multiple transformations from the same before state
126/// to conflicting after states (including null states) would cause the simplifier to fail.
127///
128/// The key insight: If we have multiple transitions from the same before state,
129/// and one of them is to a "null" state (0: 0 0), we should prefer the non-null transition
130/// as it represents the actual final transformation.
131fn remove_duplicate_before_states(changes: Vec<(Link, Link)>) -> Vec<(Link, Link)> {
132 // Group changes by their before state, keeping the groups in the order
133 // their first member appeared. C# uses `GroupBy`, which is documented to
134 // preserve that order; iterating a `HashMap` instead would shuffle the
135 // reported changes differently on every process start.
136 let mut order: Vec<Link> = Vec::new();
137 let mut grouped: HashMap<Link, Vec<(Link, Link)>> = HashMap::new();
138 for change in changes {
139 let group = grouped.entry(change.0).or_insert_with(|| {
140 order.push(change.0);
141 Vec::new()
142 });
143 group.push(change);
144 }
145
146 let mut result = Vec::new();
147
148 for before in &order {
149 let changes_for_this_before = &grouped[before];
150 if changes_for_this_before.len() == 1 {
151 // No duplicates, keep as is
152 result.extend(changes_for_this_before.iter().copied());
153 } else {
154 // Multiple changes from the same before state
155 // Check if any of them is to a null state (0: 0 0)
156 let null_link = Link::new(0, 0, 0);
157 let has_null_transition = changes_for_this_before
158 .iter()
159 .any(|(_, after)| *after == null_link);
160 let non_null_transitions: Vec<_> = changes_for_this_before
161 .iter()
162 .filter(|(_, after)| *after != null_link)
163 .cloned()
164 .collect();
165
166 if has_null_transition && !non_null_transitions.is_empty() {
167 // Issue #26 scenario: We have both null and non-null transitions
168 // Prefer the non-null transitions as they represent the actual final states
169 result.extend(non_null_transitions);
170 } else {
171 // No null transitions involved, this is a legitimate multiple-branch scenario
172 // Keep all transitions
173 result.extend(changes_for_this_before.iter().copied());
174 }
175 }
176 }
177
178 result
179}
180
181/// The distinct elements of `items`, in the order they first appear.
182///
183/// Stands in for enumerating a C# `HashSet`, which yields its elements in
184/// insertion order.
185fn distinct(items: impl IntoIterator<Item = Link>) -> Vec<Link> {
186 let mut seen = HashSet::new();
187 items
188 .into_iter()
189 .filter(|item| seen.insert(*item))
190 .collect()
191}