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}
84
85#[derive(Debug, Clone, PartialEq, Eq)]
87pub struct Warning {
88 pub origin: Origin,
90 pub blocked_by: Vec<String>,
92}
93
94#[derive(Debug, Clone, PartialEq, Eq)]
96pub struct Derived {
97 pub atom: String,
99 pub value: Value,
101 pub origin: Origin,
103}
104
105#[derive(Debug, Clone, PartialEq, Eq)]
107pub struct Report {
108 pub status: Status,
110 pub conflicts: Vec<Conflict>,
112 pub warnings: Vec<Warning>,
114 pub derived: Vec<Derived>,
116 pub underdetermined: Option<String>,
119}
120
121impl Report {
122 pub fn exit_code(&self) -> i32 {
124 match self.status {
125 Status::Conflict => 2,
126 Status::Underdetermined | Status::Warning => 1,
127 Status::Consistent => 0,
128 }
129 }
130
131 pub fn to_json(&self) -> String {
134 use core::fmt::Write as _;
135 let mut s = String::new();
136 let _ = write!(s, "{{\"status\":");
137 json_str(status_name(self.status), &mut s);
138 let _ = write!(s, ",\"exit_code\":{}", self.exit_code());
139
140 s.push_str(",\"conflicts\":[");
141 for (i, c) in self.conflicts.iter().enumerate() {
142 if i > 0 {
143 s.push(',');
144 }
145 json_origin(&c.origin, &mut s);
146 s.push_str(",\"atoms\":");
147 json_array(&c.atoms, &mut s);
148 s.push('}');
149 }
150 s.push_str("],\"warnings\":[");
151 for (i, w) in self.warnings.iter().enumerate() {
152 if i > 0 {
153 s.push(',');
154 }
155 json_origin(&w.origin, &mut s);
156 s.push_str(",\"blocked_by\":");
157 json_array(&w.blocked_by, &mut s);
158 s.push('}');
159 }
160 s.push_str("],\"derived\":[");
161 for (i, d) in self.derived.iter().enumerate() {
162 if i > 0 {
163 s.push(',');
164 }
165 s.push('{');
166 json_origin_fields(&d.origin, &mut s);
167 s.push_str(",\"atom\":");
168 json_str(&d.atom, &mut s);
169 let _ = write!(s, ",\"value\":{}", matches!(d.value, Value::True));
170 s.push('}');
171 }
172 s.push_str("],\"underdetermined\":");
173 match &self.underdetermined {
174 Some(atom) => json_str(atom, &mut s),
175 None => s.push_str("null"),
176 }
177 s.push('}');
178 s
179 }
180}
181
182fn status_name(s: Status) -> &'static str {
183 match s {
184 Status::Consistent => "CONSISTENT",
185 Status::Underdetermined => "UNDERDETERMINED",
186 Status::Warning => "WARNING",
187 Status::Conflict => "CONFLICT",
188 }
189}
190
191fn json_str(value: &str, out: &mut String) {
193 use core::fmt::Write as _;
194 out.push('"');
195 for ch in value.chars() {
196 match ch {
197 '"' => out.push_str("\\\""),
198 '\\' => out.push_str("\\\\"),
199 '\n' => out.push_str("\\n"),
200 '\r' => out.push_str("\\r"),
201 '\t' => out.push_str("\\t"),
202 c if (c as u32) < 0x20 => {
203 let _ = write!(out, "\\u{:04x}", c as u32);
204 }
205 c => out.push(c),
206 }
207 }
208 out.push('"');
209}
210
211fn json_array(items: &[String], out: &mut String) {
213 out.push('[');
214 for (i, item) in items.iter().enumerate() {
215 if i > 0 {
216 out.push(',');
217 }
218 json_str(item, out);
219 }
220 out.push(']');
221}
222
223fn json_origin_fields(o: &Origin, out: &mut String) {
225 use core::fmt::Write as _;
226 out.push_str("\"premise\":");
227 match &o.premise {
228 Some(name) => json_str(name, out),
229 None => out.push_str("null"),
230 }
231 out.push_str(",\"kind\":");
232 json_str(o.kind, out);
233 out.push_str(",\"source\":");
234 json_str(&o.source, out);
235 let _ = write!(out, ",\"line\":{}", o.line);
236}
237
238fn json_origin(o: &Origin, out: &mut String) {
240 out.push('{');
241 json_origin_fields(o, out);
242}
243
244fn label(c: &Compiled, a: AtomId) -> String {
246 let k = &c.atoms[a as usize];
247 match &k.object {
248 Some(o) => alloc::format!("{} {} {}", k.subject, k.predicate, o),
249 None => alloc::format!("{} {}", k.subject, k.predicate),
250 }
251}
252
253fn lit_value(model: &[V3], l: &Lit) -> V3 {
255 let v = model[l.atom as usize];
256 if l.negated { v.not() } else { v }
257}
258
259fn conjunction(model: &[V3], lits: &[Lit]) -> V3 {
261 let mut result = V3::True;
262 for l in lits {
263 match lit_value(model, l) {
264 V3::False => return V3::False,
265 V3::Unknown => result = V3::Unknown,
266 V3::True => {}
267 }
268 }
269 result
270}
271
272enum ClauseEval {
274 Violated,
276 Satisfied,
278 Blocked(Vec<AtomId>),
280}
281
282fn eval_clause(model: &[V3], clause: &Clause) -> ClauseEval {
285 let mut any_false = false;
286 let mut all_true = true;
287 let mut blocked = Vec::new();
288 for l in &clause.lits {
289 match lit_value(model, l) {
290 V3::False => {
291 any_false = true;
292 all_true = false;
293 }
294 V3::Unknown => {
295 all_true = false;
296 blocked.push(l.atom);
297 }
298 V3::True => {}
299 }
300 }
301 if all_true {
302 ClauseEval::Violated
303 } else if any_false {
304 ClauseEval::Satisfied
305 } else {
306 ClauseEval::Blocked(blocked)
307 }
308}
309
310struct Eval<'a> {
312 c: &'a Compiled,
313 model: Vec<V3>,
314 conflicts: Vec<Conflict>,
315 warnings: Vec<Warning>,
316 derived: Vec<Derived>,
317}
318
319impl<'a> Eval<'a> {
320 fn new(c: &'a Compiled) -> Self {
321 Eval {
322 c,
323 model: vec![V3::Unknown; c.atoms.len()],
324 conflicts: Vec::new(),
325 warnings: Vec::new(),
326 derived: Vec::new(),
327 }
328 }
329
330 fn label(&self, a: AtomId) -> String {
331 label(self.c, a)
332 }
333
334 fn seed_facts(&mut self) {
336 let c = self.c;
337 let n = c.atoms.len();
338 let mut t: Vec<Option<Origin>> = vec![None; n];
339 let mut f: Vec<Option<Origin>> = vec![None; n];
340 for fact in &c.facts {
341 let slot = match fact.value {
342 Value::True => &mut t,
343 Value::False => &mut f,
344 };
345 if slot[fact.atom as usize].is_none() {
346 slot[fact.atom as usize] = Some(fact.origin.clone());
347 }
348 }
349 for a in 0..n {
350 match (&t[a], &f[a]) {
351 (Some(o), Some(_)) => {
352 self.model[a] = V3::True;
353 self.conflicts.push(Conflict {
354 origin: o.clone(),
355 atoms: vec![alloc::format!(
356 "{} (asserted both TRUE and FALSE)",
357 self.label(a as AtomId)
358 )],
359 });
360 }
361 (Some(_), None) => self.model[a] = V3::True,
362 (None, Some(_)) => self.model[a] = V3::False,
363 (None, None) => {}
364 }
365 }
366 }
367
368 fn saturate_rules(&mut self) {
370 let c = self.c;
371 loop {
372 let mut changed = false;
373 for r in &c.rules {
374 if conjunction(&self.model, &r.antecedent) != V3::True {
375 continue; }
377 for cl in &r.consequent {
378 let target = if cl.negated { V3::False } else { V3::True };
379 match self.model[cl.atom as usize] {
380 V3::Unknown => {
381 self.model[cl.atom as usize] = target;
382 changed = true;
383 self.derived.push(Derived {
384 atom: self.label(cl.atom),
385 value: if cl.negated {
386 Value::False
387 } else {
388 Value::True
389 },
390 origin: r.origin.clone(),
391 });
392 }
393 v if v == target => {}
394 _ => self.conflicts.push(Conflict {
395 origin: r.origin.clone(),
396 atoms: vec![alloc::format!(
397 "{} (derived value contradicts a known fact)",
398 self.label(cl.atom)
399 )],
400 }),
401 }
402 }
403 }
404 if !changed {
405 break;
406 }
407 }
408 }
409
410 fn check_premises(&mut self) {
412 let c = self.c;
413 for clause in &c.clauses {
414 match eval_clause(&self.model, clause) {
415 ClauseEval::Violated => self.conflicts.push(Conflict {
416 origin: clause.origin.clone(),
417 atoms: clause.lits.iter().map(|l| self.label(l.atom)).collect(),
418 }),
419 ClauseEval::Satisfied => {}
420 ClauseEval::Blocked(unknowns) if clause.origin.kind == "PREMISE" => {
423 self.warnings.push(Warning {
424 origin: clause.origin.clone(),
425 blocked_by: unknowns.iter().map(|a| self.label(*a)).collect(),
426 });
427 }
428 ClauseEval::Blocked(_) => {}
429 }
430 }
431 }
432
433 fn backward_pass(&mut self) -> Option<String> {
440 if !self.c.checks.iter().any(|ch| ch.bidirectional) {
441 return None;
442 }
443 let (cnf, project) = build_cnf(self.c);
444 let found = sat::models(&cnf, &project, 2);
445 match found.len() {
446 0 if self.conflicts.is_empty() => {
447 self.conflicts.push(Conflict {
448 origin: Origin {
449 source: String::from("<system>"),
450 line: 0,
451 premise: None,
452 kind: "UNSAT",
453 },
454 atoms: vec![String::from(
455 "the premises and facts are jointly unsatisfiable",
456 )],
457 });
458 None
459 }
460 n if n >= 2 => {
461 let (m0, m1) = (&found[0], &found[1]);
462 project
463 .iter()
464 .find(|&&v| m0[v as usize] != m1[v as usize])
465 .map(|&v| self.label(v))
466 .or_else(|| Some(String::from("a free atom")))
467 }
468 _ => None,
469 }
470 }
471
472 fn finish(mut self) -> Report {
474 let underdetermined = self.backward_pass();
475 self.conflicts.sort_by_key(|c| key(&c.origin));
476 self.warnings.sort_by_key(|w| key(&w.origin));
477 let status = if !self.conflicts.is_empty() {
478 Status::Conflict
479 } else if underdetermined.is_some() {
480 Status::Underdetermined
481 } else if !self.warnings.is_empty() {
482 Status::Warning
483 } else {
484 Status::Consistent
485 };
486 Report {
487 status,
488 conflicts: self.conflicts,
489 warnings: self.warnings,
490 derived: self.derived,
491 underdetermined,
492 }
493 }
494}
495
496pub fn solve(c: &Compiled) -> Report {
499 let mut e = Eval::new(c);
500 e.seed_facts();
501 e.saturate_rules();
502 e.check_premises();
503 e.finish()
504}
505
506fn build_cnf(c: &Compiled) -> (sat::Cnf, Vec<sat::Var>) {
511 use sat::SatLit;
512 let mut cnf = sat::Cnf::new(c.atoms.len());
513 let mut constrained = vec![false; c.atoms.len()];
514 let mark = |a: AtomId, constrained: &mut [bool]| constrained[a as usize] = true;
515
516 for clause in &c.clauses {
518 let lits = clause
519 .lits
520 .iter()
521 .map(|l| {
522 mark(l.atom, &mut constrained);
523 SatLit::new(l.atom, l.negated)
524 })
525 .collect();
526 cnf.add_clause(lits);
527 }
528 for r in &c.rules {
530 for cons in &r.consequent {
531 let mut lits: Vec<SatLit> = r
532 .antecedent
533 .iter()
534 .map(|a| {
535 mark(a.atom, &mut constrained);
536 SatLit::new(a.atom, a.negated)
537 })
538 .collect();
539 mark(cons.atom, &mut constrained);
540 lits.push(SatLit::new(cons.atom, !cons.negated));
541 cnf.add_clause(lits);
542 }
543 }
544 for f in &c.facts {
546 let lit = match f.value {
547 Value::True => SatLit::positive(f.atom),
548 Value::False => SatLit::negative(f.atom),
549 };
550 cnf.add_clause(vec![lit]);
551 }
552
553 let project = (0..c.atoms.len() as AtomId)
554 .filter(|&a| constrained[a as usize])
555 .collect();
556 (cnf, project)
557}
558
559fn key(o: &Origin) -> (String, u32) {
561 (o.source.clone(), o.line)
562}
563
564pub fn verify_source(name: &str, src: &str) -> Result<Report, CompileError> {
566 Ok(solve(&compile_source(name, src)?))
567}
568
569pub fn verify<R: Resolver>(root: &str, resolver: &R) -> Result<Report, CompileError> {
571 Ok(solve(&compile(root, resolver)?))
572}
573
574impl fmt::Display for Status {
577 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
578 f.write_str(match self {
579 Status::Consistent => "CONSISTENT",
580 Status::Underdetermined => "UNDERDETERMINED",
581 Status::Warning => "WARNING",
582 Status::Conflict => "CONFLICT",
583 })
584 }
585}
586
587fn premise_tag(o: &Origin) -> String {
589 let name = o.premise.as_deref().unwrap_or("-");
590 alloc::format!("{} ({}) [{}:{}]", name, o.kind, o.source, o.line)
591}
592
593impl fmt::Display for Report {
594 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
595 writeln!(f, "RESULT: {}", self.status)?;
596 for c in &self.conflicts {
597 writeln!(f, " CONFLICT {}", premise_tag(&c.origin))?;
598 for a in &c.atoms {
599 writeln!(f, " {}", a)?;
600 }
601 }
602 for w in &self.warnings {
603 writeln!(f, " WARNING {}", premise_tag(&w.origin))?;
604 writeln!(f, " blocked by: {}", w.blocked_by.join(", "))?;
605 }
606 if let Some(atom) = &self.underdetermined {
607 writeln!(f, " UNDERDETERMINED an alternative model exists")?;
608 writeln!(f, " pin it down: add FACT {atom} or NOT {atom}")?;
609 }
610 for d in &self.derived {
611 let v = match d.value {
612 Value::True => "TRUE",
613 Value::False => "FALSE",
614 };
615 writeln!(
616 f,
617 " DERIVED {} = {} from {}",
618 d.atom,
619 v,
620 premise_tag(&d.origin)
621 )?;
622 }
623 let underdetermined = usize::from(self.status == Status::Underdetermined);
624 writeln!(
625 f,
626 "SUMMARY: {} conflicts, {} underdetermined, {} warnings, {} derived",
627 self.conflicts.len(),
628 underdetermined,
629 self.warnings.len(),
630 self.derived.len()
631 )?;
632 write!(f, "EXIT_CODE: {}", self.exit_code())
633 }
634}
635
636#[cfg(test)]
637mod tests {
638 use super::*;
639
640 #[test]
641 fn clean_consistent() {
642 let r = verify_source("<t>", "FACT x a\nCHECK x\n").unwrap();
643 assert_eq!(r.status, Status::Consistent);
644 assert!(r.conflicts.is_empty() && r.warnings.is_empty());
645 }
646
647 #[test]
648 fn fact_contradiction_is_conflict() {
649 let r = verify_source("<t>", "FACT x a\nNOT x a\n").unwrap();
650 assert_eq!(r.status, Status::Conflict);
651 assert_eq!(r.conflicts.len(), 1);
652 }
653
654 #[test]
655 fn exclusive_violation_is_conflict() {
656 let src = include_str!("../../../docs/examples/conflict.vrf");
657 let r = verify_source("conflict.vrf", src).unwrap();
658 assert_eq!(r.status, Status::Conflict);
659 assert_eq!(
660 r.conflicts[0].origin.premise.as_deref(),
661 Some("fly_xor_swim")
662 );
663 assert_eq!(r.conflicts[0].atoms.len(), 2);
664 }
665
666 #[test]
667 fn exclusive_with_unknown_is_consistent_not_warning() {
668 let r = verify_source("<t>", "FACT A has flying\nPREMISE e:\n EXCLUSIVE\n A has flying\n A has swimming\n").unwrap();
670 assert_eq!(r.status, Status::Consistent);
671 assert!(r.warnings.is_empty());
672 }
673
674 #[test]
675 fn implication_missing_consequent_is_warning() {
676 let r = verify_source(
678 "<t>",
679 "FACT A has flying\nPREMISE w:\n WHEN A has flying\n THEN A has wing\n",
680 )
681 .unwrap();
682 assert_eq!(r.status, Status::Warning);
683 assert_eq!(r.warnings.len(), 1);
684 assert_eq!(r.warnings[0].blocked_by, vec![String::from("A has wing")]);
685 }
686
687 #[test]
688 fn implication_satisfied_is_consistent() {
689 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();
690 assert_eq!(r.status, Status::Consistent);
691 }
692
693 #[test]
694 fn implication_violated_is_conflict() {
695 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();
697 assert_eq!(r.status, Status::Conflict);
698 }
699
700 #[test]
701 fn rule_derives_fact() {
702 let r = verify_source(
703 "<t>",
704 "FACT A has flying\nRULE o:\n WHEN A has flying\n THEN A needs oxygen\n",
705 )
706 .unwrap();
707 assert_eq!(r.status, Status::Consistent);
708 assert_eq!(r.derived.len(), 1);
709 assert_eq!(r.derived[0].atom, "A needs oxygen");
710 }
711
712 #[test]
713 fn rule_derivation_contradiction_is_conflict() {
714 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();
716 assert_eq!(r.status, Status::Conflict);
717 }
718
719 #[test]
720 fn bidirectional_finds_alternative_model_underdetermined() {
721 let r = verify_source(
723 "<t>",
724 "PREMISE e:\n EXCLUSIVE\n x a\n x b\nCHECK x BIDIRECTIONAL\n",
725 )
726 .unwrap();
727 assert_eq!(r.status, Status::Underdetermined);
728 }
729
730 #[test]
731 fn fact_pins_unique_model_consistent() {
732 let r = verify_source(
734 "<t>",
735 "FACT x a\nPREMISE e:\n EXCLUSIVE\n x a\n x b\nCHECK x BIDIRECTIONAL\n",
736 )
737 .unwrap();
738 assert_eq!(r.status, Status::Consistent);
739 }
740
741 #[test]
742 fn no_bidirectional_skips_backward_pass() {
743 let r = verify_source(
745 "<t>",
746 "PREMISE e:\n EXCLUSIVE\n x a\n x b\nCHECK x\n",
747 )
748 .unwrap();
749 assert_eq!(r.status, Status::Consistent);
750 }
751
752 #[test]
753 fn creature_example_forward_pass() {
754 let src = include_str!("../../../docs/examples/creature.vrf");
755 let r = verify_source("creature.vrf", src).unwrap();
756 assert_eq!(r.status, Status::Warning);
759 assert!(r.conflicts.is_empty());
760 assert_eq!(r.warnings.len(), 2);
761 assert_eq!(r.derived.len(), 1);
762 assert_eq!(r.derived[0].atom, "Creature.A needs oxygen");
763 }
764
765 #[test]
766 fn roles_puzzle_is_uniquely_solved() {
767 let src = include_str!("../../../docs/examples/roles-puzzle.vrf");
771 let r = verify_source("roles-puzzle.vrf", src).unwrap();
772 assert_eq!(r.status, Status::Consistent);
773 assert!(r.conflicts.is_empty());
774 assert!(r.underdetermined.is_none());
775 }
776
777 #[test]
778 fn roles_puzzle_underdetermined_without_a_clue() {
779 let src = include_str!("../../../docs/examples/roles-puzzle.vrf")
784 .replace("\r\n", "\n")
785 .replace("NOT bob is qa\n", "");
786 let r = verify_source("roles-puzzle.vrf", &src).unwrap();
787 assert_eq!(r.status, Status::Underdetermined);
788 }
789
790 #[test]
791 fn socrates_chain_is_a_conflict() {
792 let src = include_str!("../../../docs/examples/socrates.vrf");
795 let r = verify_source("socrates.vrf", src).unwrap();
796 assert_eq!(r.status, Status::Conflict);
797 assert_eq!(r.conflicts.len(), 1);
798 assert_eq!(
799 r.conflicts[0].origin.premise.as_deref(),
800 Some("mortal_xor_immortal")
801 );
802 assert_eq!(r.derived.len(), 3); }
804}