1use std::collections::BTreeSet;
38
39use serde::{Deserialize, Serialize};
40
41use crate::provenance_poly::ProvenancePoly;
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
49#[serde(rename_all = "snake_case")]
50pub enum BelnapStatus {
51 None,
53 True,
55 False,
57 Both,
59}
60
61impl BelnapStatus {
62 #[must_use]
64 pub fn letter(&self) -> char {
65 match self {
66 Self::None => 'N',
67 Self::True => 'T',
68 Self::False => 'F',
69 Self::Both => 'B',
70 }
71 }
72
73 #[must_use]
75 pub fn has_support(&self) -> bool {
76 matches!(self, Self::True | Self::Both)
77 }
78
79 #[must_use]
81 pub fn has_refute(&self) -> bool {
82 matches!(self, Self::False | Self::Both)
83 }
84}
85
86impl std::fmt::Display for BelnapStatus {
87 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88 write!(f, "{}", self.letter())
89 }
90}
91
92#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
100pub struct StatusProvenance {
101 #[serde(default)]
104 pub support: ProvenancePoly,
105 #[serde(default)]
108 pub refute: ProvenancePoly,
109}
110
111impl StatusProvenance {
112 #[must_use]
115 pub fn empty() -> Self {
116 Self::default()
117 }
118
119 #[must_use]
121 pub fn new(support: ProvenancePoly, refute: ProvenancePoly) -> Self {
122 Self { support, refute }
123 }
124
125 pub fn add_support(&mut self, derivation: &ProvenancePoly) {
127 self.support += derivation;
128 }
129
130 pub fn add_refute(&mut self, derivation: &ProvenancePoly) {
132 self.refute += derivation;
133 }
134
135 pub fn derive_status(&self) -> BelnapStatus {
142 let has_support = !self.support.is_zero();
143 let has_refute = !self.refute.is_zero();
144 match (has_support, has_refute) {
145 (false, false) => BelnapStatus::None,
146 (true, false) => BelnapStatus::True,
147 (false, true) => BelnapStatus::False,
148 (true, true) => BelnapStatus::Both,
149 }
150 }
151
152 pub fn retract<S: AsRef<str>>(&self, retracted: &BTreeSet<S>) -> Self {
159 Self {
160 support: self.support.retract(retracted),
161 refute: self.refute.retract(retracted),
162 }
163 }
164
165 pub fn support_contains(&self, var: &str) -> bool {
167 self.support.support().contains(var)
168 }
169
170 pub fn refute_contains(&self, var: &str) -> bool {
172 self.refute.support().contains(var)
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179
180 fn vars(names: &[&str]) -> BTreeSet<String> {
181 names.iter().map(|s| (*s).to_string()).collect()
182 }
183
184 #[test]
185 fn empty_derives_to_none() {
186 assert_eq!(
187 StatusProvenance::empty().derive_status(),
188 BelnapStatus::None
189 );
190 }
191
192 #[test]
193 fn support_only_derives_to_t() {
194 let sp = StatusProvenance::new(ProvenancePoly::singleton("p1"), ProvenancePoly::zero());
195 assert_eq!(sp.derive_status(), BelnapStatus::True);
196 }
197
198 #[test]
199 fn refute_only_derives_to_f() {
200 let sp = StatusProvenance::new(ProvenancePoly::zero(), ProvenancePoly::singleton("r1"));
201 assert_eq!(sp.derive_status(), BelnapStatus::False);
202 }
203
204 #[test]
205 fn both_derives_to_b() {
206 let sp = StatusProvenance::new(
207 ProvenancePoly::singleton("p1"),
208 ProvenancePoly::singleton("r1"),
209 );
210 assert_eq!(sp.derive_status(), BelnapStatus::Both);
211 }
212
213 #[test]
214 fn theorem_3_t_with_full_retract_cannot_stay_t() {
215 let support = &(&ProvenancePoly::singleton("p1") * &ProvenancePoly::singleton("d3"))
217 + &ProvenancePoly::singleton("r7");
218 let sp = StatusProvenance::new(support, ProvenancePoly::zero());
219 assert_eq!(sp.derive_status(), BelnapStatus::True);
220
221 let retracted = sp.retract(&vars(&["d3", "p1", "r7"]));
223 assert_ne!(retracted.derive_status(), BelnapStatus::True);
225 assert_eq!(retracted.derive_status(), BelnapStatus::None);
227 }
228
229 #[test]
230 fn theorem_3_t_with_partial_retract_keeps_t_if_alternate_path() {
231 let support = &(&ProvenancePoly::singleton("p1") * &ProvenancePoly::singleton("d3"))
233 + &(&ProvenancePoly::singleton("r7") * &ProvenancePoly::singleton("e2"));
234 let sp = StatusProvenance::new(support, ProvenancePoly::zero());
235 assert_eq!(sp.derive_status(), BelnapStatus::True);
236
237 let retracted = sp.retract(&vars(&["p1"]));
239 assert_eq!(retracted.derive_status(), BelnapStatus::True);
240 assert_eq!(retracted.support.term_count(), 1);
241 }
242
243 #[test]
244 fn theorem_3_t_to_f_when_refute_remains() {
245 let mut sp = StatusProvenance::new(ProvenancePoly::singleton("p1"), ProvenancePoly::zero());
247 assert_eq!(sp.derive_status(), BelnapStatus::True);
248 sp.add_refute(&ProvenancePoly::singleton("r1"));
250 assert_eq!(sp.derive_status(), BelnapStatus::Both);
251 let retracted = sp.retract(&vars(&["p1"]));
253 assert_eq!(retracted.derive_status(), BelnapStatus::False);
254 }
255
256 #[test]
257 fn b_to_n_when_both_polynomials_retracted_to_zero() {
258 let sp = StatusProvenance::new(
259 ProvenancePoly::singleton("p1"),
260 ProvenancePoly::singleton("r1"),
261 );
262 assert_eq!(sp.derive_status(), BelnapStatus::Both);
263 let retracted = sp.retract(&vars(&["p1", "r1"]));
264 assert_eq!(retracted.derive_status(), BelnapStatus::None);
265 }
266
267 #[test]
268 fn add_support_accumulates() {
269 let mut sp = StatusProvenance::empty();
270 sp.add_support(&ProvenancePoly::singleton("p1"));
271 sp.add_support(&ProvenancePoly::singleton("d3"));
272 assert_eq!(sp.derive_status(), BelnapStatus::True);
274 assert_eq!(sp.support.term_count(), 2);
275 }
276
277 #[test]
278 fn add_refute_accumulates() {
279 let mut sp = StatusProvenance::empty();
280 sp.add_refute(&ProvenancePoly::singleton("r1"));
281 sp.add_refute(&ProvenancePoly::singleton("r2"));
282 assert_eq!(sp.derive_status(), BelnapStatus::False);
283 assert_eq!(sp.refute.term_count(), 2);
284 }
285
286 #[test]
287 fn belnap_status_predicates() {
288 assert!(BelnapStatus::True.has_support());
289 assert!(BelnapStatus::Both.has_support());
290 assert!(!BelnapStatus::False.has_support());
291 assert!(!BelnapStatus::None.has_support());
292
293 assert!(BelnapStatus::False.has_refute());
294 assert!(BelnapStatus::Both.has_refute());
295 assert!(!BelnapStatus::True.has_refute());
296 assert!(!BelnapStatus::None.has_refute());
297 }
298
299 #[test]
300 fn belnap_status_letters() {
301 assert_eq!(BelnapStatus::None.letter(), 'N');
302 assert_eq!(BelnapStatus::True.letter(), 'T');
303 assert_eq!(BelnapStatus::False.letter(), 'F');
304 assert_eq!(BelnapStatus::Both.letter(), 'B');
305 }
306
307 #[test]
308 fn serde_round_trip() {
309 let sp = StatusProvenance::new(
310 ProvenancePoly::singleton("p1"),
311 ProvenancePoly::singleton("r1"),
312 );
313 let json = serde_json::to_string(&sp).expect("serialize");
314 let restored: StatusProvenance = serde_json::from_str(&json).expect("deserialize");
315 assert_eq!(restored, sp);
316 assert_eq!(restored.derive_status(), BelnapStatus::Both);
317 }
318
319 #[test]
320 fn status_is_pure_function_of_polynomials() {
321 let sp1 = StatusProvenance::new(ProvenancePoly::singleton("p1"), ProvenancePoly::zero());
326 let sp2 = StatusProvenance::new(ProvenancePoly::singleton("p1"), ProvenancePoly::zero());
327 assert_eq!(sp1.derive_status(), sp2.derive_status());
328 }
329
330 #[test]
331 fn retract_does_not_invent_support() {
332 let sp = StatusProvenance::new(ProvenancePoly::zero(), ProvenancePoly::singleton("r1"));
335 assert_eq!(sp.derive_status(), BelnapStatus::False);
336 let retracted = sp.retract(&vars(&["r1"]));
337 assert_ne!(retracted.derive_status(), BelnapStatus::True);
339 assert_eq!(retracted.derive_status(), BelnapStatus::None);
340 }
341}