1use std::collections::HashMap;
8use std::fmt;
9
10use crate::audit::{AuditEntry, AuditReport, CoverageSummary, SpecItemKind, TracePart};
11
12#[derive(Debug, Clone)]
14pub struct DiffReport {
15 pub module_name: String,
16 pub changes: Vec<DiffEntry>,
17 pub summary: DiffSummary,
18}
19
20#[derive(Debug, Clone)]
22pub struct DiffEntry {
23 pub kind: SpecItemKind,
24 pub name: String,
25 pub change: ChangeKind,
26 pub details: Vec<DiffDetail>,
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum ChangeKind {
33 Added,
34 Removed,
35 Modified,
36}
37
38#[derive(Debug, Clone)]
40pub struct DiffDetail {
41 pub label: String,
42 pub change: ChangeKind,
43 pub old_desc: Option<String>,
44 pub new_desc: Option<String>,
45}
46
47#[derive(Debug, Clone)]
49pub struct DiffSummary {
50 pub added: usize,
51 pub removed: usize,
52 pub modified: usize,
53 pub old_coverage: CoverageSummary,
54 pub new_coverage: CoverageSummary,
55 pub old_verify_errors: usize,
56 pub new_verify_errors: usize,
57 pub old_obligations: usize,
58 pub new_obligations: usize,
59}
60
61pub fn diff_reports(old: &AuditReport, new: &AuditReport) -> DiffReport {
63 let old_map: HashMap<(SpecItemKind, &str), &AuditEntry> = old
65 .entries
66 .iter()
67 .map(|e| ((e.kind, e.name.as_str()), e))
68 .collect();
69
70 let new_map: HashMap<(SpecItemKind, &str), &AuditEntry> = new
71 .entries
72 .iter()
73 .map(|e| ((e.kind, e.name.as_str()), e))
74 .collect();
75
76 let mut changes = Vec::new();
77
78 for entry in &old.entries {
80 let key = (entry.kind, entry.name.as_str());
81 if !new_map.contains_key(&key) {
82 changes.push(DiffEntry {
83 kind: entry.kind,
84 name: entry.name.clone(),
85 change: ChangeKind::Removed,
86 details: vec![],
87 });
88 }
89 }
90
91 for entry in &new.entries {
93 let key = (entry.kind, entry.name.as_str());
94 match old_map.get(&key) {
95 None => {
96 changes.push(DiffEntry {
97 kind: entry.kind,
98 name: entry.name.clone(),
99 change: ChangeKind::Added,
100 details: vec![],
101 });
102 }
103 Some(old_entry) => {
104 let details = diff_parts(&old_entry.parts, &entry.parts);
105 if !details.is_empty() {
106 changes.push(DiffEntry {
107 kind: entry.kind,
108 name: entry.name.clone(),
109 change: ChangeKind::Modified,
110 details,
111 });
112 }
113 }
114 }
115 }
116
117 let added = changes
118 .iter()
119 .filter(|c| c.change == ChangeKind::Added)
120 .count();
121 let removed = changes
122 .iter()
123 .filter(|c| c.change == ChangeKind::Removed)
124 .count();
125 let modified = changes
126 .iter()
127 .filter(|c| c.change == ChangeKind::Modified)
128 .count();
129
130 let summary = DiffSummary {
131 added,
132 removed,
133 modified,
134 old_coverage: old.summary.clone(),
135 new_coverage: new.summary.clone(),
136 old_verify_errors: old.verify_error_count,
137 new_verify_errors: new.verify_error_count,
138 old_obligations: old.obligations.len(),
139 new_obligations: new.obligations.len(),
140 };
141
142 DiffReport {
143 module_name: new.module_name.clone(),
144 changes,
145 summary,
146 }
147}
148
149fn diff_parts(old_parts: &[TracePart], new_parts: &[TracePart]) -> Vec<DiffDetail> {
151 let old_map: HashMap<&str, &TracePart> =
152 old_parts.iter().map(|p| (p.label.as_str(), p)).collect();
153 let new_map: HashMap<&str, &TracePart> =
154 new_parts.iter().map(|p| (p.label.as_str(), p)).collect();
155
156 let mut details = Vec::new();
157
158 for part in old_parts {
160 if !new_map.contains_key(part.label.as_str()) {
161 details.push(DiffDetail {
162 label: part.label.clone(),
163 change: ChangeKind::Removed,
164 old_desc: Some(part.ir_desc.clone()),
165 new_desc: None,
166 });
167 }
168 }
169
170 for part in new_parts {
172 match old_map.get(part.label.as_str()) {
173 None => {
174 details.push(DiffDetail {
175 label: part.label.clone(),
176 change: ChangeKind::Added,
177 old_desc: None,
178 new_desc: Some(part.ir_desc.clone()),
179 });
180 }
181 Some(old_part) => {
182 if old_part.ir_desc != part.ir_desc {
183 details.push(DiffDetail {
184 label: part.label.clone(),
185 change: ChangeKind::Modified,
186 old_desc: Some(old_part.ir_desc.clone()),
187 new_desc: Some(part.ir_desc.clone()),
188 });
189 }
190 }
191 }
192 }
193
194 details
195}
196
197impl fmt::Display for ChangeKind {
200 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
201 match self {
202 ChangeKind::Added => write!(f, "+"),
203 ChangeKind::Removed => write!(f, "-"),
204 ChangeKind::Modified => write!(f, "~"),
205 }
206 }
207}
208
209impl DiffReport {
210 pub fn format(&self) -> String {
212 let mut out = format!("{} — Spec Diff Report\n", self.module_name);
213
214 if self.changes.is_empty() {
215 out.push_str("\n No spec-level changes detected.\n");
216 return out;
217 }
218
219 for entry in &self.changes {
220 out.push('\n');
221 let symbol = entry.change;
222 out.push_str(&format!(" [{symbol}] {} {}\n", entry.kind, entry.name,));
223
224 for detail in &entry.details {
225 let dsym = detail.change;
226 match detail.change {
227 ChangeKind::Added => {
228 out.push_str(&format!(
229 " [{dsym}] {}: {}\n",
230 detail.label,
231 detail.new_desc.as_deref().unwrap_or(""),
232 ));
233 }
234 ChangeKind::Removed => {
235 out.push_str(&format!(
236 " [{dsym}] {}: {}\n",
237 detail.label,
238 detail.old_desc.as_deref().unwrap_or(""),
239 ));
240 }
241 ChangeKind::Modified => {
242 out.push_str(&format!(
243 " [{dsym}] {}: {} -> {}\n",
244 detail.label,
245 detail.old_desc.as_deref().unwrap_or(""),
246 detail.new_desc.as_deref().unwrap_or(""),
247 ));
248 }
249 }
250 }
251 }
252
253 let s = &self.summary;
255 out.push_str(&format!(
256 "\nSummary: {} added, {} removed, {} modified\n",
257 s.added, s.removed, s.modified,
258 ));
259
260 let old_total = s.old_coverage.total();
262 let new_total = s.new_coverage.total();
263 if new_total != old_total {
264 let delta = new_total as isize - old_total as isize;
265 let sign = if delta > 0 { "+" } else { "" };
266 out.push_str(&format!(
267 "Coverage: {} -> {} spec items ({sign}{})\n",
268 old_total, new_total, delta,
269 ));
270 } else {
271 out.push_str(&format!("Coverage: {} spec items (unchanged)\n", new_total));
272 }
273
274 match (s.old_verify_errors, s.new_verify_errors) {
276 (0, 0) => out.push_str("Verification: clean -> clean\n"),
277 (0, n) => out.push_str(&format!("Verification: clean -> {} error(s)\n", n)),
278 (o, 0) => out.push_str(&format!("Verification: {} error(s) -> clean\n", o)),
279 (o, n) if o == n => {
280 out.push_str(&format!("Verification: {} error(s) (unchanged)\n", n))
281 }
282 (o, n) => out.push_str(&format!("Verification: {} -> {} error(s)\n", o, n)),
283 }
284
285 if s.new_obligations != s.old_obligations {
287 out.push_str(&format!(
288 "Obligations: {} -> {}\n",
289 s.old_obligations, s.new_obligations,
290 ));
291 }
292
293 out
294 }
295}