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    let before_links: HashSet<Link> = changed_states.iter().map(|(b, _)| *b).collect();
37    let after_links: HashSet<Link> = changed_states.iter().map(|(_, a)| *a).collect();
38
39    // Identify initial states: appear as Before but never as After
40    let initial_states: Vec<Link> = before_links
41        .iter()
42        .filter(|b| !after_links.contains(b))
43        .copied()
44        .collect();
45
46    // Identify final states: appear as After but never as Before
47    let final_states: HashSet<Link> = after_links
48        .iter()
49        .filter(|a| !before_links.contains(a))
50        .copied()
51        .collect();
52
53    // Build adjacency (Before -> possible list of After links)
54    let mut adjacency: HashMap<Link, Vec<Link>> = HashMap::new();
55    for (before, after) in changed_states.iter() {
56        adjacency.entry(*before).or_default().push(*after);
57    }
58
59    // If we have no identified initial states, treat it as a no-op scenario:
60    // just return original transitions.
61    if initial_states.is_empty() {
62        return changes;
63    }
64
65    let mut results = Vec::new();
66
67    // Add unchanged states first
68    results.extend(unchanged_states);
69
70    // Traverse each initial state with DFS
71    for initial in initial_states.iter() {
72        let mut stack = vec![*initial];
73        let mut visited: HashSet<Link> = HashSet::new();
74
75        while let Some(current) = stack.pop() {
76            // Skip if already visited
77            if !visited.insert(current) {
78                continue;
79            }
80
81            let has_next = adjacency.contains_key(&current);
82            let next_links = adjacency.get(&current);
83            let is_final_or_dead_end = final_states.contains(&current)
84                || !has_next
85                || next_links.is_none_or(|v| v.is_empty());
86
87            // If final or no further transitions, record (initial -> current)
88            if is_final_or_dead_end {
89                results.push((*initial, current));
90            }
91
92            // Otherwise push neighbors
93            if let Some(next_links) = next_links {
94                for next in next_links {
95                    stack.push(*next);
96                }
97            }
98        }
99    }
100
101    // Sort the final results so that items appear in ascending order by their After link.
102    // This ensures tests that expect a specific order pass reliably.
103    results.sort_by(|a, b| {
104        a.1.index
105            .cmp(&b.1.index)
106            .then_with(|| a.1.source.cmp(&b.1.source))
107            .then_with(|| a.1.target.cmp(&b.1.target))
108    });
109
110    results
111}
112
113/// Removes problematic duplicate before states that lead to simplification issues.
114/// This fixes Issue #26 where multiple transformations from the same before state
115/// to conflicting after states (including null states) would cause the simplifier to fail.
116///
117/// The key insight: If we have multiple transitions from the same before state,
118/// and one of them is to a "null" state (0: 0 0), we should prefer the non-null transition
119/// as it represents the actual final transformation.
120fn remove_duplicate_before_states(changes: Vec<(Link, Link)>) -> Vec<(Link, Link)> {
121    // Group changes by their before state
122    let mut grouped: HashMap<Link, Vec<(Link, Link)>> = HashMap::new();
123    for change in changes {
124        grouped.entry(change.0).or_default().push(change);
125    }
126
127    let mut result = Vec::new();
128
129    for (_before, changes_for_this_before) in grouped {
130        if changes_for_this_before.len() == 1 {
131            // No duplicates, keep as is
132            result.extend(changes_for_this_before);
133        } else {
134            // Multiple changes from the same before state
135            // Check if any of them is to a null state (0: 0 0)
136            let null_link = Link::new(0, 0, 0);
137            let has_null_transition = changes_for_this_before
138                .iter()
139                .any(|(_, after)| *after == null_link);
140            let non_null_transitions: Vec<_> = changes_for_this_before
141                .iter()
142                .filter(|(_, after)| *after != null_link)
143                .cloned()
144                .collect();
145
146            if has_null_transition && !non_null_transitions.is_empty() {
147                // Issue #26 scenario: We have both null and non-null transitions
148                // Prefer the non-null transitions as they represent the actual final states
149                result.extend(non_null_transitions);
150            } else {
151                // No null transitions involved, this is a legitimate multiple-branch scenario
152                // Keep all transitions
153                result.extend(changes_for_this_before);
154            }
155        }
156    }
157
158    result
159}