1#![no_std]
22#![warn(missing_docs)]
24
25extern crate alloc;
26
27#[cfg(feature = "std")]
28extern crate std;
29
30pub mod sat;
31
32use alloc::string::String;
33use alloc::vec;
34use alloc::vec::Vec;
35use core::fmt;
36
37use elenchus_compiler::{AtomId, Clause, Compiled, Lit, Origin, Value};
38pub use elenchus_compiler::{CompileError, MemoryResolver, Resolver, compile, compile_source};
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum V3 {
43 True,
45 False,
47 Unknown,
49}
50
51impl V3 {
52 fn not(self) -> V3 {
54 match self {
55 V3::True => V3::False,
56 V3::False => V3::True,
57 V3::Unknown => V3::Unknown,
58 }
59 }
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub enum Status {
65 Consistent,
67 Underdetermined,
70 Warning,
72 Conflict,
74}
75
76#[derive(Debug, Clone, PartialEq, Eq)]
78pub struct Conflict {
79 pub origin: Origin,
81 pub atoms: Vec<String>,
83 pub trace: Vec<TraceStep>,
89}
90
91#[derive(Debug, Clone, PartialEq, Eq)]
94pub struct TraceStep {
95 pub atom: String,
97 pub value: Value,
99 pub reason: TraceReason,
101}
102
103#[derive(Debug, Clone, PartialEq, Eq)]
105pub enum TraceReason {
106 Asserted(Origin),
108 Derived {
110 origin: Origin,
112 from: Vec<String>,
114 },
115}
116
117#[derive(Debug, Clone, PartialEq, Eq)]
119pub struct Warning {
120 pub origin: Origin,
122 pub blocked_by: Vec<String>,
124}
125
126#[derive(Debug, Clone, PartialEq, Eq)]
128pub struct Derived {
129 pub atom: String,
131 pub value: Value,
133 pub origin: Origin,
135}
136
137#[derive(Debug, Clone, PartialEq, Eq)]
139pub struct Report {
140 pub status: Status,
142 pub conflicts: Vec<Conflict>,
144 pub warnings: Vec<Warning>,
146 pub derived: Vec<Derived>,
148 pub underdetermined: Option<String>,
151 pub unsat_core: Vec<CoreItem>,
156}
157
158#[derive(Debug, Clone, PartialEq, Eq)]
160pub struct CoreItem {
161 pub origin: Origin,
163 pub label: String,
165}
166
167impl Report {
168 pub fn exit_code(&self) -> i32 {
170 match self.status {
171 Status::Conflict => 2,
172 Status::Underdetermined | Status::Warning => 1,
173 Status::Consistent => 0,
174 }
175 }
176
177 pub fn to_json(&self) -> String {
180 use core::fmt::Write as _;
181 let mut s = String::new();
182 let _ = write!(s, "{{\"status\":");
183 json_str(status_name(self.status), &mut s);
184 let _ = write!(s, ",\"exit_code\":{}", self.exit_code());
185
186 s.push_str(",\"conflicts\":[");
187 for (i, c) in self.conflicts.iter().enumerate() {
188 if i > 0 {
189 s.push(',');
190 }
191 json_origin(&c.origin, &mut s);
192 s.push_str(",\"atoms\":");
193 json_array(&c.atoms, &mut s);
194 s.push_str(",\"trace\":[");
195 for (j, step) in c.trace.iter().enumerate() {
196 if j > 0 {
197 s.push(',');
198 }
199 json_trace_step(step, &mut s);
200 }
201 s.push_str("]}");
202 }
203 s.push_str("],\"warnings\":[");
204 for (i, w) in self.warnings.iter().enumerate() {
205 if i > 0 {
206 s.push(',');
207 }
208 json_origin(&w.origin, &mut s);
209 s.push_str(",\"blocked_by\":");
210 json_array(&w.blocked_by, &mut s);
211 s.push('}');
212 }
213 s.push_str("],\"derived\":[");
214 for (i, d) in self.derived.iter().enumerate() {
215 if i > 0 {
216 s.push(',');
217 }
218 s.push('{');
219 json_origin_fields(&d.origin, &mut s);
220 s.push_str(",\"atom\":");
221 json_str(&d.atom, &mut s);
222 let _ = write!(s, ",\"value\":{}", matches!(d.value, Value::True));
223 s.push('}');
224 }
225 s.push_str("],\"underdetermined\":");
226 match &self.underdetermined {
227 Some(atom) => json_str(atom, &mut s),
228 None => s.push_str("null"),
229 }
230 s.push_str(",\"unsat_core\":[");
231 for (i, it) in self.unsat_core.iter().enumerate() {
232 if i > 0 {
233 s.push(',');
234 }
235 json_origin(&it.origin, &mut s);
236 s.push_str(",\"label\":");
237 json_str(&it.label, &mut s);
238 s.push('}');
239 }
240 s.push_str("]}");
241 s
242 }
243}
244
245fn json_trace_step(step: &TraceStep, out: &mut String) {
247 use core::fmt::Write as _;
248 out.push_str("{\"atom\":");
249 json_str(&step.atom, out);
250 let _ = write!(out, ",\"value\":{}", matches!(step.value, Value::True));
251 match &step.reason {
252 TraceReason::Asserted(o) => {
253 out.push_str(",\"how\":\"asserted\",");
254 json_origin_fields(o, out);
255 out.push_str(",\"from\":[]");
256 }
257 TraceReason::Derived { origin, from } => {
258 out.push_str(",\"how\":\"derived\",");
259 json_origin_fields(origin, out);
260 out.push_str(",\"from\":");
261 json_array(from, out);
262 }
263 }
264 out.push('}');
265}
266
267fn status_name(s: Status) -> &'static str {
268 match s {
269 Status::Consistent => "CONSISTENT",
270 Status::Underdetermined => "UNDERDETERMINED",
271 Status::Warning => "WARNING",
272 Status::Conflict => "CONFLICT",
273 }
274}
275
276fn json_str(value: &str, out: &mut String) {
278 use core::fmt::Write as _;
279 out.push('"');
280 for ch in value.chars() {
281 match ch {
282 '"' => out.push_str("\\\""),
283 '\\' => out.push_str("\\\\"),
284 '\n' => out.push_str("\\n"),
285 '\r' => out.push_str("\\r"),
286 '\t' => out.push_str("\\t"),
287 c if (c as u32) < 0x20 => {
288 let _ = write!(out, "\\u{:04x}", c as u32);
289 }
290 c => out.push(c),
291 }
292 }
293 out.push('"');
294}
295
296fn json_array(items: &[String], out: &mut String) {
298 out.push('[');
299 for (i, item) in items.iter().enumerate() {
300 if i > 0 {
301 out.push(',');
302 }
303 json_str(item, out);
304 }
305 out.push(']');
306}
307
308fn json_origin_fields(o: &Origin, out: &mut String) {
310 use core::fmt::Write as _;
311 out.push_str("\"premise\":");
312 match &o.premise {
313 Some(name) => json_str(name, out),
314 None => out.push_str("null"),
315 }
316 out.push_str(",\"kind\":");
317 json_str(o.kind, out);
318 out.push_str(",\"source\":");
319 json_str(&o.source, out);
320 let _ = write!(out, ",\"line\":{}", o.line);
321}
322
323fn json_origin(o: &Origin, out: &mut String) {
325 out.push('{');
326 json_origin_fields(o, out);
327}
328
329fn label(c: &Compiled, a: AtomId) -> String {
331 let k = &c.atoms[a as usize];
332 match &k.object {
333 Some(o) => alloc::format!("{} {} {}", k.subject, k.predicate, o),
334 None => alloc::format!("{} {}", k.subject, k.predicate),
335 }
336}
337
338fn lit_value(model: &[V3], l: &Lit) -> V3 {
340 let v = model[l.atom as usize];
341 if l.negated { v.not() } else { v }
342}
343
344fn conjunction(model: &[V3], lits: &[Lit]) -> V3 {
346 let mut result = V3::True;
347 for l in lits {
348 match lit_value(model, l) {
349 V3::False => return V3::False,
350 V3::Unknown => result = V3::Unknown,
351 V3::True => {}
352 }
353 }
354 result
355}
356
357enum ClauseEval {
359 Violated,
361 Satisfied,
363 Blocked(Vec<AtomId>),
365}
366
367fn eval_clause(model: &[V3], clause: &Clause) -> ClauseEval {
370 let mut any_false = false;
371 let mut all_true = true;
372 let mut blocked = Vec::new();
373 for l in &clause.lits {
374 match lit_value(model, l) {
375 V3::False => {
376 any_false = true;
377 all_true = false;
378 }
379 V3::Unknown => {
380 all_true = false;
381 blocked.push(l.atom);
382 }
383 V3::True => {}
384 }
385 }
386 if all_true {
387 ClauseEval::Violated
388 } else if any_false {
389 ClauseEval::Satisfied
390 } else {
391 ClauseEval::Blocked(blocked)
392 }
393}
394
395#[derive(Clone)]
398enum AtomReason {
399 Asserted(Origin),
401 Derived { origin: Origin, from: Vec<AtomId> },
403}
404
405struct RawConflict {
409 origin: Origin,
410 atoms: Vec<String>,
411 cause: Vec<AtomId>,
412}
413
414struct Eval<'a> {
416 c: &'a Compiled,
417 model: Vec<V3>,
418 reason: Vec<Option<AtomReason>>,
421 conflicts: Vec<RawConflict>,
422 warnings: Vec<Warning>,
423 derived: Vec<Derived>,
424 unsat_core: Vec<CoreItem>,
426}
427
428impl<'a> Eval<'a> {
429 fn new(c: &'a Compiled) -> Self {
430 Eval {
431 c,
432 model: vec![V3::Unknown; c.atoms.len()],
433 reason: vec![None; c.atoms.len()],
434 conflicts: Vec::new(),
435 warnings: Vec::new(),
436 derived: Vec::new(),
437 unsat_core: Vec::new(),
438 }
439 }
440
441 fn label(&self, a: AtomId) -> String {
442 label(self.c, a)
443 }
444
445 fn seed_facts(&mut self) {
447 let c = self.c;
448 let n = c.atoms.len();
449 let mut t: Vec<Option<Origin>> = vec![None; n];
450 let mut f: Vec<Option<Origin>> = vec![None; n];
451 for fact in &c.facts {
452 let slot = match fact.value {
453 Value::True => &mut t,
454 Value::False => &mut f,
455 };
456 if slot[fact.atom as usize].is_none() {
457 slot[fact.atom as usize] = Some(fact.origin.clone());
458 }
459 }
460 for a in 0..n {
461 match (&t[a], &f[a]) {
462 (Some(o), Some(_)) => {
463 self.model[a] = V3::True;
464 self.reason[a] = Some(AtomReason::Asserted(o.clone()));
465 self.conflicts.push(RawConflict {
466 origin: o.clone(),
467 atoms: vec![alloc::format!(
468 "{} (asserted both TRUE and FALSE)",
469 self.label(a as AtomId)
470 )],
471 cause: Vec::new(),
472 });
473 }
474 (Some(o), None) => {
475 self.model[a] = V3::True;
476 self.reason[a] = Some(AtomReason::Asserted(o.clone()));
477 }
478 (None, Some(o)) => {
479 self.model[a] = V3::False;
480 self.reason[a] = Some(AtomReason::Asserted(o.clone()));
481 }
482 (None, None) => {}
483 }
484 }
485 }
486
487 fn saturate_rules(&mut self) {
489 let c = self.c;
490 loop {
491 let mut changed = false;
492 for r in &c.rules {
493 if conjunction(&self.model, &r.antecedent) != V3::True {
494 continue; }
496 for cl in &r.consequent {
497 let target = if cl.negated { V3::False } else { V3::True };
498 match self.model[cl.atom as usize] {
499 V3::Unknown => {
500 self.model[cl.atom as usize] = target;
501 self.reason[cl.atom as usize] = Some(AtomReason::Derived {
502 origin: r.origin.clone(),
503 from: r.antecedent.iter().map(|l| l.atom).collect(),
504 });
505 changed = true;
506 self.derived.push(Derived {
507 atom: self.label(cl.atom),
508 value: if cl.negated {
509 Value::False
510 } else {
511 Value::True
512 },
513 origin: r.origin.clone(),
514 });
515 }
516 v if v == target => {}
517 _ => {
518 let mut cause: Vec<AtomId> =
522 r.antecedent.iter().map(|l| l.atom).collect();
523 cause.push(cl.atom);
524 self.conflicts.push(RawConflict {
525 origin: r.origin.clone(),
526 atoms: vec![alloc::format!(
527 "{} (derived value contradicts a known fact)",
528 self.label(cl.atom)
529 )],
530 cause,
531 });
532 }
533 }
534 }
535 }
536 if !changed {
537 break;
538 }
539 }
540 }
541
542 fn check_premises(&mut self) {
544 let c = self.c;
545 for clause in &c.clauses {
546 match eval_clause(&self.model, clause) {
547 ClauseEval::Violated => self.conflicts.push(RawConflict {
548 origin: clause.origin.clone(),
549 atoms: clause.lits.iter().map(|l| self.label(l.atom)).collect(),
550 cause: clause.lits.iter().map(|l| l.atom).collect(),
551 }),
552 ClauseEval::Satisfied => {}
553 ClauseEval::Blocked(unknowns) if clause.origin.kind == "PREMISE" => {
556 self.warnings.push(Warning {
557 origin: clause.origin.clone(),
558 blocked_by: unknowns.iter().map(|a| self.label(*a)).collect(),
559 });
560 }
561 ClauseEval::Blocked(_) => {}
562 }
563 }
564 }
565
566 fn build_trace(&self, causes: &[AtomId]) -> Vec<TraceStep> {
572 let mut visited = vec![false; self.c.atoms.len()];
573 let mut out = Vec::new();
574 for &a in causes {
575 self.trace_dfs(a, &mut visited, &mut out);
576 }
577 out
578 }
579
580 fn trace_dfs(&self, a: AtomId, visited: &mut [bool], out: &mut Vec<TraceStep>) {
581 if visited[a as usize] {
582 return;
583 }
584 visited[a as usize] = true;
585 let value = match v3_to_value(self.model[a as usize]) {
586 Some(v) => v,
587 None => return, };
589 let reason = match &self.reason[a as usize] {
590 Some(AtomReason::Asserted(o)) => TraceReason::Asserted(o.clone()),
591 Some(AtomReason::Derived { origin, from }) => {
592 for &f in from {
593 self.trace_dfs(f, visited, out); }
595 TraceReason::Derived {
596 origin: origin.clone(),
597 from: from.iter().map(|&f| self.label(f)).collect(),
598 }
599 }
600 None => return,
601 };
602 out.push(TraceStep {
603 atom: self.label(a),
604 value,
605 reason,
606 });
607 }
608
609 fn backward_pass(&mut self) -> Option<String> {
616 if !self.c.checks.iter().any(|ch| ch.bidirectional) {
617 return None;
618 }
619 let (cnf, project) = build_cnf(self.c);
620 let found = sat::models(&cnf, &project, 2);
621 match found.len() {
622 0 if self.conflicts.is_empty() => {
623 self.unsat_core = minimal_unsat_core(self.c);
624 self.conflicts.push(RawConflict {
625 origin: Origin {
626 source: String::from("<system>"),
627 line: 0,
628 premise: None,
629 kind: "UNSAT",
630 },
631 atoms: vec![String::from(
632 "the premises and facts are jointly unsatisfiable",
633 )],
634 cause: Vec::new(),
635 });
636 None
637 }
638 n if n >= 2 => {
639 let (m0, m1) = (&found[0], &found[1]);
640 project
641 .iter()
642 .find(|&&v| m0[v as usize] != m1[v as usize])
643 .map(|&v| self.label(v))
644 .or_else(|| Some(String::from("a free atom")))
645 }
646 _ => None,
647 }
648 }
649
650 fn finish(mut self) -> Report {
652 let underdetermined = self.backward_pass();
653 self.conflicts.sort_by_key(|c| key(&c.origin));
654 self.warnings.sort_by_key(|w| key(&w.origin));
655 let status = if !self.conflicts.is_empty() {
656 Status::Conflict
657 } else if underdetermined.is_some() {
658 Status::Underdetermined
659 } else if !self.warnings.is_empty() {
660 Status::Warning
661 } else {
662 Status::Consistent
663 };
664 let conflicts: Vec<Conflict> = self
667 .conflicts
668 .iter()
669 .map(|rc| Conflict {
670 origin: rc.origin.clone(),
671 atoms: rc.atoms.clone(),
672 trace: self.build_trace(&rc.cause),
673 })
674 .collect();
675 Report {
676 status,
677 conflicts,
678 warnings: self.warnings,
679 derived: self.derived,
680 underdetermined,
681 unsat_core: self.unsat_core,
682 }
683 }
684}
685
686pub fn solve(c: &Compiled) -> Report {
689 let mut e = Eval::new(c);
690 e.seed_facts();
691 e.saturate_rules();
692 e.check_premises();
693 e.finish()
694}
695
696fn build_cnf(c: &Compiled) -> (sat::Cnf, Vec<sat::Var>) {
701 use sat::SatLit;
702 let mut cnf = sat::Cnf::new(c.atoms.len());
703 let mut constrained = vec![false; c.atoms.len()];
704 let mark = |a: AtomId, constrained: &mut [bool]| constrained[a as usize] = true;
705
706 for clause in &c.clauses {
708 let lits = clause
709 .lits
710 .iter()
711 .map(|l| {
712 mark(l.atom, &mut constrained);
713 SatLit::new(l.atom, l.negated)
714 })
715 .collect();
716 cnf.add_clause(lits);
717 }
718 for r in &c.rules {
720 for cons in &r.consequent {
721 let mut lits: Vec<SatLit> = r
722 .antecedent
723 .iter()
724 .map(|a| {
725 mark(a.atom, &mut constrained);
726 SatLit::new(a.atom, a.negated)
727 })
728 .collect();
729 mark(cons.atom, &mut constrained);
730 lits.push(SatLit::new(cons.atom, !cons.negated));
731 cnf.add_clause(lits);
732 }
733 }
734 for f in &c.facts {
736 let lit = match f.value {
737 Value::True => SatLit::positive(f.atom),
738 Value::False => SatLit::negative(f.atom),
739 };
740 cnf.add_clause(vec![lit]);
741 }
742
743 let project = (0..c.atoms.len() as AtomId)
744 .filter(|&a| constrained[a as usize])
745 .collect();
746 (cnf, project)
747}
748
749fn v3_to_value(v: V3) -> Option<Value> {
751 match v {
752 V3::True => Some(Value::True),
753 V3::False => Some(Value::False),
754 V3::Unknown => None,
755 }
756}
757
758struct Construct {
761 origin: Origin,
762 label: String,
763 clauses: Vec<Vec<sat::SatLit>>,
764}
765
766fn same_origin(a: &Origin, b: &Origin) -> bool {
768 a.source == b.source && a.line == b.line && a.premise == b.premise && a.kind == b.kind
769}
770
771fn constructs(c: &Compiled) -> Vec<Construct> {
775 use sat::SatLit;
776 let mut out: Vec<Construct> = Vec::new();
777
778 for f in &c.facts {
779 let lit = match f.value {
780 Value::True => SatLit::positive(f.atom),
781 Value::False => SatLit::negative(f.atom),
782 };
783 out.push(Construct {
784 origin: f.origin.clone(),
785 label: label(c, f.atom),
786 clauses: vec![vec![lit]],
787 });
788 }
789
790 let mut premises: Vec<Construct> = Vec::new();
791 for clause in &c.clauses {
792 let lits: Vec<SatLit> = clause
793 .lits
794 .iter()
795 .map(|l| SatLit::new(l.atom, l.negated))
796 .collect();
797 match premises
798 .iter_mut()
799 .find(|k| same_origin(&k.origin, &clause.origin))
800 {
801 Some(k) => k.clauses.push(lits),
802 None => premises.push(Construct {
803 label: clause.origin.premise.clone().unwrap_or_default(),
804 origin: clause.origin.clone(),
805 clauses: vec![lits],
806 }),
807 }
808 }
809 out.extend(premises);
810
811 for r in &c.rules {
812 let clauses = r
813 .consequent
814 .iter()
815 .map(|cons| {
816 let mut lits: Vec<SatLit> = r
817 .antecedent
818 .iter()
819 .map(|a| SatLit::new(a.atom, a.negated))
820 .collect();
821 lits.push(SatLit::new(cons.atom, !cons.negated));
822 lits
823 })
824 .collect();
825 out.push(Construct {
826 label: r.origin.premise.clone().unwrap_or_default(),
827 origin: r.origin.clone(),
828 clauses,
829 });
830 }
831 out
832}
833
834fn subset_is_sat(num_vars: usize, all: &[Construct], active: &[bool]) -> bool {
836 let mut cnf = sat::Cnf::new(num_vars);
837 for (k, &keep) in all.iter().zip(active) {
838 if keep {
839 for cl in &k.clauses {
840 cnf.add_clause(cl.clone());
841 }
842 }
843 }
844 sat::solve(&cnf).is_some()
845}
846
847fn candidate_via_assumptions(c: &Compiled, all: &[Construct]) -> Vec<bool> {
853 let base = c.atoms.len();
854 let mut cnf = sat::Cnf::new(base + all.len());
855 let sel = |i: usize| (base + i) as sat::Var;
856 for (i, k) in all.iter().enumerate() {
857 let s_neg = sat::SatLit::negative(sel(i));
858 for cl in &k.clauses {
859 let mut lits = Vec::with_capacity(cl.len() + 1);
860 lits.push(s_neg);
861 lits.extend_from_slice(cl);
862 cnf.add_clause(lits);
863 }
864 }
865 let assumptions: Vec<sat::SatLit> = (0..all.len())
866 .map(|i| sat::SatLit::positive(sel(i)))
867 .collect();
868 let mut active = vec![false; all.len()];
869 match sat::solve_assuming(&cnf, &assumptions) {
870 sat::Solved::Unsat(core) => {
871 for lit in core {
872 let v = lit.var() as usize;
873 if v >= base {
874 active[v - base] = true;
875 }
876 }
877 }
878 sat::Solved::Sat(_) => active.iter_mut().for_each(|a| *a = true),
882 }
883 active
884}
885
886fn minimal_unsat_core(c: &Compiled) -> Vec<CoreItem> {
892 let all = constructs(c);
893 let mut active = candidate_via_assumptions(c, &all);
894 for i in 0..all.len() {
895 if active[i] {
896 active[i] = false;
897 if subset_is_sat(c.atoms.len(), &all, &active) {
898 active[i] = true; }
900 }
901 }
902 let mut core: Vec<CoreItem> = all
903 .iter()
904 .zip(&active)
905 .filter(|&(_, &keep)| keep)
906 .map(|(k, _)| CoreItem {
907 origin: k.origin.clone(),
908 label: k.label.clone(),
909 })
910 .collect();
911 core.sort_by_key(|it| key(&it.origin));
912 core
913}
914
915fn key(o: &Origin) -> (String, u32) {
917 (o.source.clone(), o.line)
918}
919
920pub fn verify_source(name: &str, src: &str) -> Result<Report, CompileError> {
922 Ok(solve(&compile_source(name, src)?))
923}
924
925pub fn verify<R: Resolver>(root: &str, resolver: &R) -> Result<Report, CompileError> {
927 Ok(solve(&compile(root, resolver)?))
928}
929
930impl fmt::Display for Status {
933 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
934 f.write_str(match self {
935 Status::Consistent => "CONSISTENT",
936 Status::Underdetermined => "UNDERDETERMINED",
937 Status::Warning => "WARNING",
938 Status::Conflict => "CONFLICT",
939 })
940 }
941}
942
943fn premise_tag(o: &Origin) -> String {
945 let name = o.premise.as_deref().unwrap_or("-");
946 alloc::format!("{} ({}) [{}:{}]", name, o.kind, o.source, o.line)
947}
948
949fn trace_line(step: &TraceStep) -> String {
951 let v = match step.value {
952 Value::True => "TRUE",
953 Value::False => "FALSE",
954 };
955 match &step.reason {
956 TraceReason::Asserted(o) => {
957 alloc::format!(
958 "{} = {} [{} {}:{}]",
959 step.atom,
960 v,
961 o.kind,
962 o.source,
963 o.line
964 )
965 }
966 TraceReason::Derived { origin, from } => alloc::format!(
967 "{} = {} from {} ({}) [{}:{}] <= {}",
968 step.atom,
969 v,
970 origin.premise.as_deref().unwrap_or("-"),
971 origin.kind,
972 origin.source,
973 origin.line,
974 from.join(", ")
975 ),
976 }
977}
978
979impl fmt::Display for Report {
980 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
981 writeln!(f, "RESULT: {}", self.status)?;
982 for c in &self.conflicts {
983 writeln!(f, " CONFLICT {}", premise_tag(&c.origin))?;
984 for a in &c.atoms {
985 writeln!(f, " {}", a)?;
986 }
987 if !c.trace.is_empty() {
988 writeln!(f, " why:")?;
989 for step in &c.trace {
990 writeln!(f, " {}", trace_line(step))?;
991 }
992 }
993 }
994 if !self.unsat_core.is_empty() {
995 writeln!(
996 f,
997 " CORE smallest jointly-unsatisfiable set ({}):",
998 self.unsat_core.len()
999 )?;
1000 for it in &self.unsat_core {
1001 let name = if it.label.is_empty() { "-" } else { &it.label };
1002 writeln!(
1003 f,
1004 " {} ({}) [{}:{}]",
1005 name, it.origin.kind, it.origin.source, it.origin.line
1006 )?;
1007 }
1008 }
1009 for w in &self.warnings {
1010 writeln!(f, " WARNING {}", premise_tag(&w.origin))?;
1011 writeln!(f, " blocked by: {}", w.blocked_by.join(", "))?;
1012 }
1013 if let Some(atom) = &self.underdetermined {
1014 writeln!(f, " UNDERDETERMINED an alternative model exists")?;
1015 writeln!(f, " pin it down: add FACT {atom} or NOT {atom}")?;
1016 }
1017 for d in &self.derived {
1018 let v = match d.value {
1019 Value::True => "TRUE",
1020 Value::False => "FALSE",
1021 };
1022 writeln!(
1023 f,
1024 " DERIVED {} = {} from {}",
1025 d.atom,
1026 v,
1027 premise_tag(&d.origin)
1028 )?;
1029 }
1030 let underdetermined = usize::from(self.status == Status::Underdetermined);
1031 writeln!(
1032 f,
1033 "SUMMARY: {} conflicts, {} underdetermined, {} warnings, {} derived",
1034 self.conflicts.len(),
1035 underdetermined,
1036 self.warnings.len(),
1037 self.derived.len()
1038 )?;
1039 write!(f, "EXIT_CODE: {}", self.exit_code())
1040 }
1041}
1042
1043#[cfg(test)]
1044mod tests {
1045 use super::*;
1046
1047 #[test]
1048 fn clean_consistent() {
1049 let r = verify_source("<t>", "FACT x a\nCHECK x\n").unwrap();
1050 assert_eq!(r.status, Status::Consistent);
1051 assert!(r.conflicts.is_empty() && r.warnings.is_empty());
1052 }
1053
1054 #[test]
1055 fn fact_contradiction_is_conflict() {
1056 let r = verify_source("<t>", "FACT x a\nNOT x a\n").unwrap();
1057 assert_eq!(r.status, Status::Conflict);
1058 assert_eq!(r.conflicts.len(), 1);
1059 }
1060
1061 #[test]
1062 fn exclusive_violation_is_conflict() {
1063 let src = include_str!("../../../docs/examples/conflict.vrf");
1064 let r = verify_source("conflict.vrf", src).unwrap();
1065 assert_eq!(r.status, Status::Conflict);
1066 assert_eq!(
1067 r.conflicts[0].origin.premise.as_deref(),
1068 Some("fly_xor_swim")
1069 );
1070 assert_eq!(r.conflicts[0].atoms.len(), 2);
1071 }
1072
1073 #[test]
1074 fn exclusive_with_unknown_is_consistent_not_warning() {
1075 let r = verify_source("<t>", "FACT A has flying\nPREMISE e:\n EXCLUSIVE\n A has flying\n A has swimming\n").unwrap();
1077 assert_eq!(r.status, Status::Consistent);
1078 assert!(r.warnings.is_empty());
1079 }
1080
1081 #[test]
1082 fn implication_missing_consequent_is_warning() {
1083 let r = verify_source(
1085 "<t>",
1086 "FACT A has flying\nPREMISE w:\n WHEN A has flying\n THEN A has wing\n",
1087 )
1088 .unwrap();
1089 assert_eq!(r.status, Status::Warning);
1090 assert_eq!(r.warnings.len(), 1);
1091 assert_eq!(r.warnings[0].blocked_by, vec![String::from("A has wing")]);
1092 }
1093
1094 #[test]
1095 fn implication_satisfied_is_consistent() {
1096 let r = verify_source("<t>", "FACT A has flying\nFACT A has wing\nPREMISE w:\n WHEN A has flying\n THEN A has wing\n").unwrap();
1097 assert_eq!(r.status, Status::Consistent);
1098 }
1099
1100 #[test]
1101 fn implication_violated_is_conflict() {
1102 let r = verify_source("<t>", "FACT A has flying\nNOT A has wing\nPREMISE w:\n WHEN A has flying\n THEN A has wing\n").unwrap();
1104 assert_eq!(r.status, Status::Conflict);
1105 }
1106
1107 #[test]
1108 fn rule_derives_fact() {
1109 let r = verify_source(
1110 "<t>",
1111 "FACT A has flying\nRULE o:\n WHEN A has flying\n THEN A needs oxygen\n",
1112 )
1113 .unwrap();
1114 assert_eq!(r.status, Status::Consistent);
1115 assert_eq!(r.derived.len(), 1);
1116 assert_eq!(r.derived[0].atom, "A needs oxygen");
1117 }
1118
1119 #[test]
1120 fn rule_derivation_contradiction_is_conflict() {
1121 let r = verify_source("<t>", "FACT A has flying\nNOT A needs oxygen\nRULE o:\n WHEN A has flying\n THEN A needs oxygen\n").unwrap();
1123 assert_eq!(r.status, Status::Conflict);
1124 }
1125
1126 #[test]
1127 fn bidirectional_finds_alternative_model_underdetermined() {
1128 let r = verify_source(
1130 "<t>",
1131 "PREMISE e:\n EXCLUSIVE\n x a\n x b\nCHECK x BIDIRECTIONAL\n",
1132 )
1133 .unwrap();
1134 assert_eq!(r.status, Status::Underdetermined);
1135 }
1136
1137 #[test]
1138 fn fact_pins_unique_model_consistent() {
1139 let r = verify_source(
1141 "<t>",
1142 "FACT x a\nPREMISE e:\n EXCLUSIVE\n x a\n x b\nCHECK x BIDIRECTIONAL\n",
1143 )
1144 .unwrap();
1145 assert_eq!(r.status, Status::Consistent);
1146 }
1147
1148 #[test]
1149 fn no_bidirectional_skips_backward_pass() {
1150 let r = verify_source(
1152 "<t>",
1153 "PREMISE e:\n EXCLUSIVE\n x a\n x b\nCHECK x\n",
1154 )
1155 .unwrap();
1156 assert_eq!(r.status, Status::Consistent);
1157 }
1158
1159 #[test]
1160 fn creature_example_forward_pass() {
1161 let src = include_str!("../../../docs/examples/creature.vrf");
1162 let r = verify_source("creature.vrf", src).unwrap();
1163 assert_eq!(r.status, Status::Warning);
1166 assert!(r.conflicts.is_empty());
1167 assert_eq!(r.warnings.len(), 2);
1168 assert_eq!(r.derived.len(), 1);
1169 assert_eq!(r.derived[0].atom, "Creature.A needs oxygen");
1170 }
1171
1172 #[test]
1173 fn roles_puzzle_is_uniquely_solved() {
1174 let src = include_str!("../../../docs/examples/roles-puzzle.vrf");
1178 let r = verify_source("roles-puzzle.vrf", src).unwrap();
1179 assert_eq!(r.status, Status::Consistent);
1180 assert!(r.conflicts.is_empty());
1181 assert!(r.underdetermined.is_none());
1182 }
1183
1184 #[test]
1185 fn roles_puzzle_underdetermined_without_a_clue() {
1186 let src = include_str!("../../../docs/examples/roles-puzzle.vrf")
1191 .replace("\r\n", "\n")
1192 .replace("NOT bob is qa\n", "");
1193 let r = verify_source("roles-puzzle.vrf", &src).unwrap();
1194 assert_eq!(r.status, Status::Underdetermined);
1195 }
1196
1197 #[test]
1198 fn socrates_chain_is_a_conflict() {
1199 let src = include_str!("../../../docs/examples/socrates.vrf");
1202 let r = verify_source("socrates.vrf", src).unwrap();
1203 assert_eq!(r.status, Status::Conflict);
1204 assert_eq!(r.conflicts.len(), 1);
1205 assert_eq!(
1206 r.conflicts[0].origin.premise.as_deref(),
1207 Some("mortal_xor_immortal")
1208 );
1209 assert_eq!(r.derived.len(), 3); }
1211
1212 #[test]
1215 fn forward_conflict_carries_a_trace_of_its_facts() {
1216 let r = verify_source(
1217 "<t>",
1218 "FACT x a\nFACT x b\nPREMISE e:\n EXCLUSIVE\n x a\n x b\nCHECK x\n",
1219 )
1220 .unwrap();
1221 assert_eq!(r.status, Status::Conflict);
1222 let t = &r.conflicts[0].trace;
1223 assert_eq!(t.len(), 2);
1224 assert_eq!(t[0].atom, "x a");
1225 assert_eq!(t[0].value, Value::True);
1226 assert!(matches!(t[0].reason, TraceReason::Asserted(_)));
1227 assert!(r.unsat_core.is_empty());
1228 }
1229
1230 #[test]
1231 fn derivation_chain_is_traced_back_to_the_root_fact() {
1232 let src = include_str!("../../../docs/examples/socrates.vrf");
1234 let r = verify_source("socrates.vrf", src).unwrap();
1235 let t = &r.conflicts[0].trace;
1236 assert_eq!(t.len(), 5);
1239 assert_eq!(t[0].atom, "socrates is human");
1240 assert!(matches!(t[0].reason, TraceReason::Asserted(_)));
1241 let mortal = t.iter().find(|s| s.atom == "socrates is mortal").unwrap();
1242 match &mortal.reason {
1243 TraceReason::Derived { from, .. } => {
1244 assert_eq!(from, &vec![String::from("socrates is living")]);
1245 }
1246 _ => panic!("mortal should be derived, not asserted"),
1247 }
1248 }
1249
1250 #[test]
1251 fn direct_fact_contradiction_has_no_trace() {
1252 let r = verify_source("<t>", "FACT x a\nNOT x a\nCHECK x\n").unwrap();
1253 assert_eq!(r.status, Status::Conflict);
1254 assert!(r.conflicts[0].trace.is_empty());
1255 }
1256
1257 #[test]
1258 fn jointly_unsatisfiable_reports_a_minimal_core() {
1259 let src = "PREMISE one:\n ONEOF\n x a\n x b\nPREMISE ac:\n WHEN x a\n THEN x c\nPREMISE bc:\n WHEN x b\n THEN x c\nNOT x c\nCHECK x BIDIRECTIONAL\n";
1262 let r = verify_source("<t>", src).unwrap();
1263 assert_eq!(r.status, Status::Conflict);
1264 assert_eq!(r.conflicts[0].origin.kind, "UNSAT");
1265 assert_eq!(r.unsat_core.len(), 4);
1266 let labels: Vec<&str> = r.unsat_core.iter().map(|c| c.label.as_str()).collect();
1267 assert!(labels.contains(&"one"));
1268 assert!(labels.contains(&"x c")); }
1270
1271 #[test]
1272 fn unsat_core_excludes_irrelevant_constructs() {
1273 let src = "PREMISE one:\n ONEOF\n x a\n x b\nPREMISE ac:\n WHEN x a\n THEN x c\nPREMISE bc:\n WHEN x b\n THEN x c\nNOT x c\nFACT z here\nPREMISE noise:\n EXCLUSIVE\n z here\n z gone\nCHECK x BIDIRECTIONAL\n";
1276 let r = verify_source("<t>", src).unwrap();
1277 assert_eq!(r.status, Status::Conflict);
1278 assert_eq!(r.unsat_core.len(), 4);
1279 let labels: Vec<&str> = r.unsat_core.iter().map(|c| c.label.as_str()).collect();
1280 assert!(!labels.contains(&"noise"));
1281 assert!(!labels.iter().any(|l| l.contains("here")));
1282 }
1283
1284 #[test]
1285 fn consistent_report_has_empty_core_and_no_trace() {
1286 let r = verify_source("<t>", "FACT x a\nCHECK x BIDIRECTIONAL\n").unwrap();
1287 assert_eq!(r.status, Status::Consistent);
1288 assert!(r.unsat_core.is_empty());
1289 assert!(r.conflicts.is_empty());
1290 }
1291}