1use std::collections::BTreeSet;
8
9use crate::schema::Contract;
10
11#[derive(Debug, Clone)]
13pub struct ContractDiff {
14 pub old_version: String,
16 pub new_version: String,
18 pub sections: Vec<SectionDiff>,
20 pub suggested_bump: SemverBump,
22}
23
24#[derive(Debug, Clone)]
26pub struct SectionDiff {
27 pub section: String,
29 pub added: Vec<String>,
31 pub removed: Vec<String>,
33 pub changed: Vec<String>,
35}
36
37impl SectionDiff {
39 pub fn is_empty(&self) -> bool {
41 self.added.is_empty() && self.removed.is_empty() && self.changed.is_empty()
42 }
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum SemverBump {
48 None,
50 Patch,
52 Minor,
54 Major,
56}
57
58impl std::fmt::Display for SemverBump {
60 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61 match self {
62 Self::None => write!(f, "none"),
63 Self::Patch => write!(f, "patch"),
64 Self::Minor => write!(f, "minor"),
65 Self::Major => write!(f, "major"),
66 }
67 }
68}
69
70pub fn diff_contracts(old: &Contract, new: &Contract) -> ContractDiff {
72 let mut sections = Vec::new();
73 let mut max_bump = SemverBump::None;
74
75 let eq_diff = diff_keys(
77 "equations",
78 &old.equations.keys().cloned().collect(),
79 &new.equations.keys().cloned().collect(),
80 );
81 let eq_changed = diff_equation_changes(old, new);
82 let eq_section = SectionDiff {
83 section: "equations".to_string(),
84 added: eq_diff.0,
85 removed: eq_diff.1,
86 changed: eq_changed,
87 };
88 max_bump = bump_max(
89 max_bump,
90 section_bump(&eq_section, SemverBump::Major, SemverBump::Minor),
91 );
92 sections.push(eq_section);
93
94 let old_obs: BTreeSet<String> = old
96 .proof_obligations
97 .iter()
98 .map(|o| format!("{}:{}", o.obligation_type, o.property))
99 .collect();
100 let new_obs: BTreeSet<String> = new
101 .proof_obligations
102 .iter()
103 .map(|o| format!("{}:{}", o.obligation_type, o.property))
104 .collect();
105 let ob_diff = diff_sets("proof_obligations", &old_obs, &new_obs);
106 max_bump = bump_max(
107 max_bump,
108 section_bump(&ob_diff, SemverBump::Major, SemverBump::Minor),
109 );
110 sections.push(ob_diff);
111
112 let old_ft: BTreeSet<String> = old
114 .falsification_tests
115 .iter()
116 .map(|t| t.id.clone())
117 .collect();
118 let new_ft: BTreeSet<String> = new
119 .falsification_tests
120 .iter()
121 .map(|t| t.id.clone())
122 .collect();
123 let ft_diff = diff_sets("falsification_tests", &old_ft, &new_ft);
124 max_bump = bump_max(
125 max_bump,
126 section_bump(&ft_diff, SemverBump::Minor, SemverBump::Minor),
127 );
128 sections.push(ft_diff);
129
130 let old_kh: BTreeSet<String> = old.kani_harnesses.iter().map(|h| h.id.clone()).collect();
132 let new_kh: BTreeSet<String> = new.kani_harnesses.iter().map(|h| h.id.clone()).collect();
133 let kh_diff = diff_sets("kani_harnesses", &old_kh, &new_kh);
134 max_bump = bump_max(
135 max_bump,
136 section_bump(&kh_diff, SemverBump::Minor, SemverBump::Minor),
137 );
138 sections.push(kh_diff);
139
140 let old_enf: BTreeSet<String> = old.enforcement.keys().cloned().collect();
142 let new_enf: BTreeSet<String> = new.enforcement.keys().cloned().collect();
143 let enf_diff = diff_sets("enforcement", &old_enf, &new_enf);
144 max_bump = bump_max(
145 max_bump,
146 section_bump(&enf_diff, SemverBump::Minor, SemverBump::Patch),
147 );
148 sections.push(enf_diff);
149
150 if old.metadata.version != new.metadata.version {
152 max_bump = bump_max(max_bump, SemverBump::Patch);
153 }
154
155 ContractDiff {
156 old_version: old.metadata.version.clone(),
157 new_version: new.metadata.version.clone(),
158 sections,
159 suggested_bump: max_bump,
160 }
161}
162
163fn diff_keys(
165 _section: &str,
166 old_keys: &BTreeSet<String>,
167 new_keys: &BTreeSet<String>,
168) -> (Vec<String>, Vec<String>) {
169 let added: Vec<String> = new_keys.difference(old_keys).cloned().collect();
170 let removed: Vec<String> = old_keys.difference(new_keys).cloned().collect();
171 (added, removed)
172}
173
174fn diff_sets(section: &str, old: &BTreeSet<String>, new: &BTreeSet<String>) -> SectionDiff {
176 let added: Vec<String> = new.difference(old).cloned().collect();
177 let removed: Vec<String> = old.difference(new).cloned().collect();
178 SectionDiff {
179 section: section.to_string(),
180 added,
181 removed,
182 changed: Vec::new(),
183 }
184}
185
186fn diff_equation_changes(old: &Contract, new: &Contract) -> Vec<String> {
188 let mut changed = Vec::new();
189 for (name, old_eq) in &old.equations {
190 if let Some(new_eq) = new.equations.get(name) {
191 if old_eq.formula != new_eq.formula {
192 changed.push(format!("{name}: formula changed"));
193 }
194 if old_eq.domain != new_eq.domain {
195 changed.push(format!("{name}: domain changed"));
196 }
197 if old_eq.codomain != new_eq.codomain {
198 changed.push(format!("{name}: codomain changed"));
199 }
200 if old_eq.invariants != new_eq.invariants {
201 changed.push(format!("{name}: invariants changed"));
202 }
203 }
204 }
205 changed
206}
207
208fn section_bump(section: &SectionDiff, on_break: SemverBump, on_add: SemverBump) -> SemverBump {
210 if !section.removed.is_empty() || !section.changed.is_empty() {
211 on_break
212 } else if !section.added.is_empty() {
213 on_add
214 } else {
215 SemverBump::None
216 }
217}
218
219fn bump_max(current: SemverBump, candidate: SemverBump) -> SemverBump {
221 let rank = |b: SemverBump| match b {
222 SemverBump::None => 0,
223 SemverBump::Patch => 1,
224 SemverBump::Minor => 2,
225 SemverBump::Major => 3,
226 };
227 if rank(candidate) > rank(current) {
228 candidate
229 } else {
230 current
231 }
232}
233
234pub fn is_identical(diff: &ContractDiff) -> bool {
236 diff.sections.iter().all(SectionDiff::is_empty) && diff.suggested_bump == SemverBump::None
237}
238
239#[cfg(test)]
240mod tests {
241 include!("diff_tests.rs");
242}