Skip to main content

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(&current);
93            let next_links = adjacency.get(&current);
94            let is_final_or_dead_end = final_states.contains(&current)
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}