1#![no_std]
21#![warn(missing_docs)]
23
24extern crate alloc;
25
26#[cfg(feature = "std")]
27extern crate std;
28
29use alloc::collections::{BTreeMap, BTreeSet};
30use alloc::string::{String, ToString};
31use alloc::vec;
32use alloc::vec::Vec;
33use core::fmt::Write as _;
34
35use elenchus_parser::{Atom, Body, Conn, ListOp, Literal, Statement};
36use sha2::{Digest, Sha256};
37use thiserror::Error;
38
39pub fn hash_hex(data: &[u8]) -> String {
44 let mut hasher = Sha256::new();
45 hasher.update(data);
46 let out = hasher.finalize();
47 let mut s = String::with_capacity(out.len() * 2);
48 for b in out {
49 let _ = write!(s, "{:02x}", b);
50 }
51 s
52}
53
54pub type AtomId = u32;
58
59#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
62pub struct AtomKey {
63 pub subject: String,
65 pub predicate: String,
67 pub object: Option<String>,
69}
70
71impl AtomKey {
72 fn from_atom(a: &Atom) -> Self {
74 AtomKey {
75 subject: a.subject.to_string(),
76 predicate: a.predicate.to_string(),
77 object: a.object.map(|o| o.to_string()),
78 }
79 }
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub struct Lit {
86 pub atom: AtomId,
88 pub negated: bool,
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum Value {
95 True,
97 False,
99}
100
101#[derive(Debug, Clone, PartialEq, Eq)]
103pub struct Origin {
104 pub source: String,
106 pub line: u32,
108 pub premise: Option<String>,
110 pub kind: &'static str,
112}
113
114#[derive(Debug, Clone, PartialEq, Eq)]
117pub struct Fact {
118 pub atom: AtomId,
120 pub value: Value,
122 pub origin: Origin,
124}
125
126#[derive(Debug, Clone, PartialEq, Eq)]
128pub struct Clause {
129 pub lits: Vec<Lit>,
131 pub origin: Origin,
133}
134
135#[derive(Debug, Clone, PartialEq, Eq)]
138pub struct Rule {
139 pub antecedent: Vec<Lit>,
141 pub consequent: Vec<Lit>,
143 pub origin: Origin,
145}
146
147#[derive(Debug, Clone, PartialEq, Eq)]
149pub struct Check {
150 pub subject: Option<String>,
152 pub bidirectional: bool,
154}
155
156#[derive(Debug, Clone, PartialEq, Eq)]
158pub struct Compiled {
159 pub atoms: Vec<AtomKey>,
161 pub facts: Vec<Fact>,
163 pub clauses: Vec<Clause>,
165 pub rules: Vec<Rule>,
167 pub checks: Vec<Check>,
169 pub pending_imports: Vec<String>,
172}
173
174#[derive(Debug, Error, PartialEq, Eq)]
176pub enum CompileError {
177 #[error("parse error in {file}: {message}")]
179 Parse {
180 file: String,
182 message: String,
184 },
185 #[error("'{name}' redefined with a different body")]
187 PremiseRedefinition {
188 name: String,
190 },
191 #[error("import not found: {0}")]
193 ImportNotFound(String),
194 #[error("circular import: {0}")]
196 CircularImport(String),
197 #[error("rule '{name}' cannot derive a disjunction (OR in THEN); use a PREMISE instead")]
201 RuleDisjunctiveConsequent {
202 name: String,
204 },
205}
206
207#[derive(Clone)]
215struct RawLit {
216 key: AtomKey,
217 negated: bool,
218}
219
220struct RawFact {
222 key: AtomKey,
223 value: Value,
224 origin: Origin,
225}
226
227struct RawClause {
229 lits: Vec<RawLit>,
230 origin: Origin,
231}
232
233struct RawRule {
235 antecedent: Vec<RawLit>,
236 consequent: Vec<RawLit>,
237 origin: Origin,
238}
239
240#[derive(Default)]
244pub struct Compiler {
245 keys: BTreeSet<AtomKey>,
246 facts: Vec<RawFact>,
247 clauses: Vec<RawClause>,
248 rules: Vec<RawRule>,
249 checks: Vec<Check>,
250 pending_imports: Vec<String>,
251 defined: BTreeMap<(String, String), String>,
256 clause_sigs: BTreeSet<String>,
258 fact_sigs: BTreeSet<String>,
260}
261
262impl Compiler {
263 pub fn new() -> Self {
265 Self::default()
266 }
267
268 pub fn add_source(&mut self, source: &str, src: &str) -> Result<(), CompileError> {
271 let program = elenchus_parser::parse(src).map_err(|e| CompileError::Parse {
272 file: source.to_string(),
273 message: e.message,
274 })?;
275 for stmt in &program.statements {
276 self.add_statement(source, stmt)?;
277 }
278 Ok(())
279 }
280
281 fn load_recursive<R: Resolver>(
285 &mut self,
286 path: &str,
287 resolver: &R,
288 visited: &mut BTreeSet<String>,
289 stack: &mut Vec<String>,
290 ) -> Result<(), CompileError> {
291 let content = resolver.load(path)?;
292 let hash = hash_hex(content.as_bytes());
293 if visited.contains(&hash) {
294 return Ok(()); }
296 if stack.contains(&hash) {
297 return Err(CompileError::CircularImport(path.to_string()));
298 }
299 stack.push(hash.clone());
300
301 let program = elenchus_parser::parse(&content).map_err(|e| CompileError::Parse {
302 file: path.to_string(),
303 message: e.message,
304 })?;
305 for stmt in &program.statements {
306 if let Statement::Import(p) = stmt {
307 let resolved = resolver.resolve(path, p.data);
308 self.load_recursive(&resolved, resolver, visited, stack)?;
309 } else {
310 self.add_statement(path, stmt)?;
311 }
312 }
313
314 stack.pop();
315 visited.insert(hash);
316 Ok(())
317 }
318
319 fn add_statement(&mut self, source: &str, stmt: &Statement) -> Result<(), CompileError> {
323 match stmt {
324 Statement::Import(path) => {
325 self.pending_imports.push(path.data.to_string());
326 }
327 Statement::Fact(a) => self.add_fact(source, a, Value::True, "FACT"),
328 Statement::Negation(a) => self.add_fact(source, a, Value::False, "NOT"),
329 Statement::Check {
330 subject,
331 bidirectional,
332 } => self.checks.push(Check {
333 subject: subject.as_ref().map(|s| s.data.to_string()),
334 bidirectional: *bidirectional,
335 }),
336 Statement::Premise { name, body } => {
337 let line = name.span.location_line();
338 self.add_named(source, name.data, line, body, false)?;
339 }
340 Statement::Rule { name, body } => {
341 let line = name.span.location_line();
342 self.add_named(source, name.data, line, body, true)?;
343 }
344 }
345 Ok(())
346 }
347
348 fn intern(&mut self, key: &AtomKey) {
350 if !self.keys.contains(key) {
351 self.keys.insert(key.clone());
352 }
353 }
354
355 fn add_fact(
359 &mut self,
360 source: &str,
361 a: &elenchus_parser::Located<Atom>,
362 value: Value,
363 kind: &'static str,
364 ) {
365 let key = AtomKey::from_atom(&a.data);
366 self.intern(&key);
367 let sig = alloc::format!(
368 "{}|{}|{}|{}",
369 key_sig(&key),
370 matches!(value, Value::True) as u8,
371 kind,
372 "" );
374 if !self.fact_sigs.insert(sig) {
375 return; }
377 self.facts.push(RawFact {
378 key,
379 value,
380 origin: Origin {
381 source: source.to_string(),
382 line: a.span.location_line(),
383 premise: None,
384 kind,
385 },
386 });
387 }
388
389 fn add_named(
392 &mut self,
393 source: &str,
394 name: &str,
395 line: u32,
396 body: &Body,
397 is_rule: bool,
398 ) -> Result<(), CompileError> {
399 let body_hash = hash_hex(canonical_body(name, body, is_rule).as_bytes());
400 let key = (source.to_string(), name.to_string());
401 match self.defined.get(&key) {
402 Some(prev) if *prev == body_hash => return Ok(()), Some(_) => {
404 return Err(CompileError::PremiseRedefinition {
406 name: name.to_string(),
407 });
408 }
409 None => {
410 self.defined.insert(key, body_hash);
411 }
412 }
413
414 if is_rule {
415 if let Body::Impl {
417 antecedent,
418 ante_conn,
419 consequent,
420 cons_conn,
421 } = body
422 {
423 if *cons_conn == Conn::Or {
426 return Err(CompileError::RuleDisjunctiveConsequent {
427 name: name.to_string(),
428 });
429 }
430 let (ante, cons) = (raw_lits(antecedent), raw_lits(consequent));
431 for l in ante.iter().chain(cons.iter()) {
432 self.intern(&l.key);
433 }
434 let origin = self.origin(source, line, Some(name), "RULE");
435 match ante_conn {
436 Conn::And => self.rules.push(RawRule {
438 antecedent: ante,
439 consequent: cons,
440 origin,
441 }),
442 Conn::Or => {
444 for a in &ante {
445 self.rules.push(RawRule {
446 antecedent: vec![a.clone()],
447 consequent: cons.clone(),
448 origin: origin.clone(),
449 });
450 }
451 }
452 }
453 }
454 return Ok(());
455 }
456
457 match body {
458 Body::List { op, atoms } => {
459 let keys: Vec<AtomKey> =
460 atoms.iter().map(|a| AtomKey::from_atom(&a.data)).collect();
461 for k in &keys {
462 self.intern(k);
463 }
464 let kind = list_kind(*op);
465 let origin = self.origin(source, line, Some(name), kind);
466 match op {
467 ListOp::Exclusive | ListOp::Forbids => {
469 self.emit_pairwise(&keys, &origin);
470 }
471 ListOp::OneOf => {
473 self.emit_pairwise(&keys, &origin);
474 self.emit_at_least_one(&keys, &origin);
475 }
476 ListOp::AtLeast => {
478 self.emit_at_least_one(&keys, &origin);
479 }
480 }
481 }
482 Body::Impl {
483 antecedent,
484 ante_conn,
485 consequent,
486 cons_conn,
487 } => {
488 let ante = raw_lits(antecedent);
496 let cons = raw_lits(consequent);
497 for l in ante.iter().chain(cons.iter()) {
498 self.intern(&l.key);
499 }
500 let origin = self.origin(source, line, Some(name), "PREMISE");
501
502 let ante_groups: Vec<Vec<RawLit>> = match ante_conn {
503 Conn::And => vec![ante.clone()],
504 Conn::Or => ante.iter().map(|l| vec![l.clone()]).collect(),
505 };
506 let cons_groups: Vec<Vec<RawLit>> = match cons_conn {
507 Conn::And => cons.iter().map(|l| vec![l.clone()]).collect(),
508 Conn::Or => vec![cons.clone()],
509 };
510 for ag in &ante_groups {
511 for cg in &cons_groups {
512 let mut lits = ag.clone();
513 for c in cg {
514 lits.push(RawLit {
515 key: c.key.clone(),
516 negated: !c.negated,
517 });
518 }
519 self.push_clause(lits, origin.clone());
520 }
521 }
522 }
523 }
524 Ok(())
525 }
526
527 fn emit_pairwise(&mut self, keys: &[AtomKey], origin: &Origin) {
531 for i in 0..keys.len() {
532 for j in (i + 1)..keys.len() {
533 let lits = vec![
534 RawLit {
535 key: keys[i].clone(),
536 negated: false,
537 },
538 RawLit {
539 key: keys[j].clone(),
540 negated: false,
541 },
542 ];
543 self.push_clause(lits, origin.clone());
544 }
545 }
546 }
547
548 fn emit_at_least_one(&mut self, keys: &[AtomKey], origin: &Origin) {
551 let lits = keys
552 .iter()
553 .map(|k| RawLit {
554 key: k.clone(),
555 negated: true,
556 })
557 .collect();
558 self.push_clause(lits, origin.clone());
559 }
560
561 fn push_clause(&mut self, lits: Vec<RawLit>, origin: Origin) {
564 let sig = clause_sig(&lits);
565 if self.clause_sigs.insert(sig) {
566 self.clauses.push(RawClause { lits, origin });
567 }
568 }
570
571 fn origin(&self, source: &str, line: u32, premise: Option<&str>, kind: &'static str) -> Origin {
573 Origin {
574 source: source.to_string(),
575 line,
576 premise: premise.map(|s| s.to_string()),
577 kind,
578 }
579 }
580
581 pub fn finalize(self) -> Compiled {
583 let atoms: Vec<AtomKey> = self.keys.into_iter().collect(); let mut id_of: BTreeMap<AtomKey, AtomId> = BTreeMap::new();
585 for (i, k) in atoms.iter().enumerate() {
586 id_of.insert(k.clone(), i as AtomId);
587 }
588 let lower = |l: &RawLit| Lit {
589 atom: id_of[&l.key],
590 negated: l.negated,
591 };
592
593 let facts = self
594 .facts
595 .into_iter()
596 .map(|f| Fact {
597 atom: id_of[&f.key],
598 value: f.value,
599 origin: f.origin,
600 })
601 .collect();
602 let clauses = self
603 .clauses
604 .into_iter()
605 .map(|c| Clause {
606 lits: c.lits.iter().map(lower).collect(),
607 origin: c.origin,
608 })
609 .collect();
610 let rules = self
611 .rules
612 .into_iter()
613 .map(|r| Rule {
614 antecedent: r.antecedent.iter().map(lower).collect(),
615 consequent: r.consequent.iter().map(lower).collect(),
616 origin: r.origin,
617 })
618 .collect();
619
620 Compiled {
621 atoms,
622 facts,
623 clauses,
624 rules,
625 checks: self.checks,
626 pending_imports: self.pending_imports,
627 }
628 }
629}
630
631pub fn compile_source(source: &str, src: &str) -> Result<Compiled, CompileError> {
634 let mut c = Compiler::new();
635 c.add_source(source, src)?;
636 Ok(c.finalize())
637}
638
639pub trait Resolver {
645 fn load(&self, path: &str) -> Result<String, CompileError>;
647
648 fn resolve(&self, _base: &str, relative: &str) -> String {
651 relative.to_string()
652 }
653}
654
655#[derive(Default)]
658pub struct MemoryResolver {
659 sources: BTreeMap<String, String>,
660}
661
662impl MemoryResolver {
663 pub fn new() -> Self {
665 Self::default()
666 }
667
668 pub fn add(&mut self, path: &str, content: &str) -> &mut Self {
670 self.sources.insert(path.to_string(), content.to_string());
671 self
672 }
673}
674
675impl Resolver for MemoryResolver {
676 fn load(&self, path: &str) -> Result<String, CompileError> {
677 self.sources
678 .get(path)
679 .cloned()
680 .ok_or_else(|| CompileError::ImportNotFound(path.to_string()))
681 }
682}
683
684#[cfg(feature = "std")]
688pub struct FileResolver;
689
690#[cfg(feature = "std")]
691impl Resolver for FileResolver {
692 fn load(&self, path: &str) -> Result<String, CompileError> {
693 std::fs::read_to_string(path)
694 .map_err(|e| CompileError::ImportNotFound(alloc::format!("{}: {}", path, e)))
695 }
696
697 fn resolve(&self, base: &str, relative: &str) -> String {
698 use std::path::{Component, Path, PathBuf};
699 let parent = Path::new(base).parent().unwrap_or_else(|| Path::new("."));
700 let joined = parent.join(relative);
701 let mut out = PathBuf::new();
702 for component in joined.components() {
703 match component {
704 Component::ParentDir => {
705 out.pop();
706 }
707 Component::CurDir => {}
708 c => out.push(c),
709 }
710 }
711 out.to_string_lossy().replace('\\', "/")
715 }
716}
717
718pub fn compile<R: Resolver>(root: &str, resolver: &R) -> Result<Compiled, CompileError> {
728 let mut c = Compiler::new();
729 let mut visited = BTreeSet::new();
730 let mut stack = Vec::new();
731 c.load_recursive(root, resolver, &mut visited, &mut stack)?;
732 Ok(c.finalize())
733}
734
735fn raw_lits(lits: &[elenchus_parser::Located<Literal>]) -> Vec<RawLit> {
739 lits.iter()
740 .map(|l| RawLit {
741 key: AtomKey::from_atom(&l.data.atom),
742 negated: l.data.negated,
743 })
744 .collect()
745}
746
747fn list_kind(op: ListOp) -> &'static str {
749 match op {
750 ListOp::Exclusive => "EXCLUSIVE",
751 ListOp::Forbids => "FORBIDS",
752 ListOp::OneOf => "ONEOF",
753 ListOp::AtLeast => "ATLEAST",
754 }
755}
756
757fn key_sig(k: &AtomKey) -> String {
760 alloc::format!(
761 "{}|{}|{}",
762 k.subject,
763 k.predicate,
764 k.object.as_deref().unwrap_or("")
765 )
766}
767
768fn clause_sig(lits: &[RawLit]) -> String {
770 let mut parts: Vec<String> = lits
771 .iter()
772 .map(|l| alloc::format!("{}|{}", key_sig(&l.key), l.negated as u8))
773 .collect();
774 parts.sort();
775 parts.dedup();
776 parts.join(";")
777}
778
779fn canonical_body(name: &str, body: &Body, is_rule: bool) -> String {
781 let mut s = String::new();
782 let _ = write!(s, "{}|{}|", if is_rule { "RULE" } else { "PREMISE" }, name);
783 match body {
784 Body::List { op, atoms } => {
785 let _ = write!(s, "LIST|{}|", list_kind(*op));
786 let mut keys: Vec<String> = atoms
787 .iter()
788 .map(|a| key_sig(&AtomKey::from_atom(&a.data)))
789 .collect();
790 keys.sort();
791 s.push_str(&keys.join(";"));
792 }
793 Body::Impl {
794 antecedent,
795 ante_conn,
796 consequent,
797 cons_conn,
798 } => {
799 let conn = |c: &Conn| if *c == Conn::Or { "OR" } else { "AND" };
800 s.push_str("IMPL|ANTE|");
801 s.push_str(conn(ante_conn));
802 s.push('|');
803 s.push_str(&lit_sigs(antecedent));
804 s.push_str("|CONS|");
805 s.push_str(conn(cons_conn));
806 s.push('|');
807 s.push_str(&lit_sigs(consequent));
808 }
809 }
810 s
811}
812
813fn lit_sigs(lits: &[elenchus_parser::Located<Literal>]) -> String {
816 let mut parts: Vec<String> = lits
817 .iter()
818 .map(|l| {
819 alloc::format!(
820 "{}|{}",
821 key_sig(&AtomKey::from_atom(&l.data.atom)),
822 l.data.negated as u8
823 )
824 })
825 .collect();
826 parts.sort();
827 parts.join(";")
828}
829
830#[cfg(test)]
831mod tests {
832 use super::*;
833
834 fn key(subject: &str, predicate: &str, object: Option<&str>) -> AtomKey {
835 AtomKey {
836 subject: subject.to_string(),
837 predicate: predicate.to_string(),
838 object: object.map(|o| o.to_string()),
839 }
840 }
841
842 fn id(c: &Compiled, k: &AtomKey) -> AtomId {
843 c.atoms.iter().position(|a| a == k).unwrap() as AtomId
844 }
845
846 #[test]
847 fn exclusive_unfolds_pairwise() {
848 let src = r#"
849 PREMISE e:
850 EXCLUSIVE
851 x a
852 x b
853 x c
854 "#;
855 let c = compile_source("<t>", src).unwrap();
856 assert_eq!(c.clauses.len(), 3);
858 for cl in &c.clauses {
859 assert_eq!(cl.lits.len(), 2);
860 assert!(cl.lits.iter().all(|l| !l.negated));
861 }
862 }
863
864 #[test]
865 fn implication_negates_consequent() {
866 let src = r#"
868 PREMISE r:
869 WHEN x a
870 THEN x b
871 "#;
872 let c = compile_source("<t>", src).unwrap();
873 assert_eq!(c.clauses.len(), 1);
874 let cl = &c.clauses[0];
875 assert_eq!(cl.lits.len(), 2);
876 let a = id(&c, &key("x", "a", None));
877 let b = id(&c, &key("x", "b", None));
878 assert!(cl.lits.contains(&Lit {
879 atom: a,
880 negated: false
881 }));
882 assert!(cl.lits.contains(&Lit {
883 atom: b,
884 negated: true
885 }));
886 }
887
888 #[test]
889 fn negated_consequent_flips_to_positive() {
890 let src = r#"
892 PREMISE r:
893 WHEN x a
894 THEN NOT x b
895 "#;
896 let c = compile_source("<t>", src).unwrap();
897 let b = id(&c, &key("x", "b", None));
898 assert!(c.clauses[0].lits.contains(&Lit {
899 atom: b,
900 negated: false
901 }));
902 }
903
904 #[test]
905 fn consequent_or_is_one_clause_with_all_negated() {
906 let src = r#"
908 PREMISE r:
909 WHEN x p
910 THEN x a
911 OR x b
912 "#;
913 let c = compile_source("<t>", src).unwrap();
914 assert_eq!(c.clauses.len(), 1);
915 let cl = &c.clauses[0];
916 assert_eq!(cl.lits.len(), 3);
917 let p = id(&c, &key("x", "p", None));
918 let a = id(&c, &key("x", "a", None));
919 let b = id(&c, &key("x", "b", None));
920 assert!(cl.lits.contains(&Lit {
921 atom: p,
922 negated: false
923 }));
924 assert!(cl.lits.contains(&Lit {
925 atom: a,
926 negated: true
927 }));
928 assert!(cl.lits.contains(&Lit {
929 atom: b,
930 negated: true
931 }));
932 }
933
934 #[test]
935 fn antecedent_or_is_one_clause_per_disjunct() {
936 let src = r#"
939 PREMISE r:
940 WHEN x a
941 OR x b
942 THEN x c
943 "#;
944 let c = compile_source("<t>", src).unwrap();
945 assert_eq!(c.clauses.len(), 2);
946 let a = id(&c, &key("x", "a", None));
947 let b = id(&c, &key("x", "b", None));
948 let cc = id(&c, &key("x", "c", None));
949 for cl in &c.clauses {
951 assert_eq!(cl.lits.len(), 2);
952 assert!(cl.lits.contains(&Lit {
953 atom: cc,
954 negated: true
955 }));
956 }
957 let has = |atom| {
958 c.clauses.iter().any(|cl| {
959 cl.lits.contains(&Lit {
960 atom,
961 negated: false,
962 })
963 })
964 };
965 assert!(has(a) && has(b));
966 }
967
968 #[test]
969 fn antecedent_or_with_consequent_or_distributes() {
970 let src = r#"
972 PREMISE r:
973 WHEN x a
974 OR x b
975 THEN x c
976 OR x d
977 "#;
978 let c = compile_source("<t>", src).unwrap();
979 assert_eq!(c.clauses.len(), 2);
980 for cl in &c.clauses {
981 assert_eq!(cl.lits.len(), 3);
982 }
983 }
984
985 #[test]
986 fn rule_with_or_antecedent_splits_into_two_rules() {
987 let src = r#"
989 RULE r:
990 WHEN x a
991 OR x b
992 THEN x c
993 "#;
994 let c = compile_source("<t>", src).unwrap();
995 assert_eq!(c.rules.len(), 2);
996 assert!(
997 c.rules
998 .iter()
999 .all(|r| r.antecedent.len() == 1 && r.consequent.len() == 1)
1000 );
1001 }
1002
1003 #[test]
1004 fn rule_with_or_consequent_is_rejected() {
1005 let src = r#"
1007 RULE r:
1008 WHEN x a
1009 THEN x b
1010 OR x c
1011 "#;
1012 let err = compile_source("<t>", src).unwrap_err();
1013 assert!(matches!(
1014 err,
1015 CompileError::RuleDisjunctiveConsequent { .. }
1016 ));
1017 }
1018
1019 #[test]
1020 fn oneof_is_pairwise_plus_at_least_one() {
1021 let src = r#"
1022 PREMISE o:
1023 ONEOF
1024 x a
1025 x b
1026 "#;
1027 let c = compile_source("<t>", src).unwrap();
1028 assert_eq!(c.clauses.len(), 2);
1030 assert!(c.clauses.iter().any(|cl| cl.lits.iter().all(|l| l.negated)));
1032 }
1033
1034 #[test]
1035 fn atleast_is_one_negated_clause() {
1036 let src = r#"
1037 PREMISE a:
1038 ATLEAST
1039 x a
1040 x b
1041 x c
1042 "#;
1043 let c = compile_source("<t>", src).unwrap();
1044 assert_eq!(c.clauses.len(), 1);
1045 assert_eq!(c.clauses[0].lits.len(), 3);
1046 assert!(c.clauses[0].lits.iter().all(|l| l.negated));
1047 }
1048
1049 #[test]
1050 fn rules_are_separate_from_clauses() {
1051 let src = r#"
1052 RULE needs:
1053 WHEN x a
1054 THEN x b
1055 "#;
1056 let c = compile_source("<t>", src).unwrap();
1057 assert_eq!(c.clauses.len(), 0);
1058 assert_eq!(c.rules.len(), 1);
1059 assert_eq!(c.rules[0].antecedent.len(), 1);
1060 assert_eq!(c.rules[0].consequent.len(), 1);
1061 }
1062
1063 #[test]
1064 fn atoms_are_canonically_sorted() {
1065 let src = r#"
1066 FACT z z
1067 FACT a a
1068 FACT m m
1069 "#;
1070 let c = compile_source("<t>", src).unwrap();
1071 let mut sorted = c.atoms.clone();
1072 sorted.sort();
1073 assert_eq!(c.atoms, sorted);
1074 }
1075
1076 #[test]
1077 fn duplicate_premise_is_idempotent() {
1078 let src = r#"
1079 PREMISE e:
1080 EXCLUSIVE
1081 x a
1082 x b
1083 PREMISE e:
1084 EXCLUSIVE
1085 x a
1086 x b
1087 "#;
1088 let c = compile_source("<t>", src).unwrap();
1089 assert_eq!(c.clauses.len(), 1);
1090 }
1091
1092 #[test]
1093 fn redefinition_with_different_body_errors() {
1094 let src = r#"
1095 PREMISE e:
1096 EXCLUSIVE
1097 x a
1098 x b
1099 PREMISE e:
1100 EXCLUSIVE
1101 x a
1102 x c
1103 "#;
1104 let err = compile_source("<t>", src).unwrap_err();
1105 assert_eq!(
1106 err,
1107 CompileError::PremiseRedefinition {
1108 name: "e".to_string()
1109 }
1110 );
1111 }
1112
1113 #[test]
1114 fn duplicate_fact_is_idempotent() {
1115 let c = compile_source("<t>", "FACT x a\nFACT x a\n").unwrap();
1116 assert_eq!(c.facts.len(), 1);
1117 }
1118
1119 #[test]
1120 fn conflicting_facts_are_both_kept() {
1121 let c = compile_source("<t>", "FACT x a\nNOT x a\n").unwrap();
1123 assert_eq!(c.facts.len(), 2);
1124 }
1125
1126 #[test]
1127 fn import_is_recorded_pending() {
1128 let c = compile_source("<t>", "IMPORT \"physics.vrf\"\nFACT x a\n").unwrap();
1129 assert_eq!(c.pending_imports, vec!["physics.vrf".to_string()]);
1130 }
1131
1132 #[test]
1133 fn import_flat_merges_and_atoms_unify() {
1134 let mut r = MemoryResolver::new();
1137 r.add(
1138 "lib.vrf",
1139 r#"
1140 PREMISE needs_fuel:
1141 WHEN Engine.X has engine
1142 THEN Engine.X has fuel
1143 "#,
1144 );
1145 r.add(
1146 "main.vrf",
1147 r#"
1148 IMPORT "lib.vrf"
1149 FACT Engine.X has engine
1150 FACT Engine.X has fuel
1151 "#,
1152 );
1153 let c = compile("main.vrf", &r).unwrap();
1154 assert!(c.pending_imports.is_empty());
1155 assert_eq!(c.clauses.len(), 1); assert_eq!(c.facts.len(), 2);
1157
1158 let fuel = key("Engine.X", "has", Some("fuel"));
1160 let fuel_id = id(&c, &fuel);
1161 assert!(c.facts.iter().any(|f| f.atom == fuel_id));
1162 assert!(c.clauses[0].lits.iter().any(|l| l.atom == fuel_id));
1163 }
1164
1165 #[test]
1166 fn diamond_import_is_deduped() {
1167 let mut r = MemoryResolver::new();
1169 r.add(
1170 "base.vrf",
1171 r#"
1172 PREMISE b:
1173 EXCLUSIVE
1174 x a
1175 x b
1176 "#,
1177 );
1178 r.add("a.vrf", "IMPORT \"base.vrf\"\n");
1179 r.add("c.vrf", "IMPORT \"base.vrf\"\n");
1180 r.add("main.vrf", "IMPORT \"a.vrf\"\nIMPORT \"c.vrf\"\n");
1181 let c = compile("main.vrf", &r).unwrap();
1182 assert_eq!(c.clauses.len(), 1); }
1184
1185 #[test]
1186 fn circular_import_errors() {
1187 let mut r = MemoryResolver::new();
1188 r.add("a.vrf", "IMPORT \"b.vrf\"\n");
1189 r.add("b.vrf", "IMPORT \"a.vrf\"\n");
1190 let err = compile("a.vrf", &r).unwrap_err();
1191 assert!(matches!(err, CompileError::CircularImport(_)));
1192 }
1193
1194 #[test]
1195 fn missing_import_errors() {
1196 let mut r = MemoryResolver::new();
1197 r.add("main.vrf", "IMPORT \"ghost.vrf\"\n");
1198 let err = compile("main.vrf", &r).unwrap_err();
1199 assert!(matches!(err, CompileError::ImportNotFound(_)));
1200 }
1201
1202 #[test]
1203 fn same_name_across_domains_coexists() {
1204 let mut r = MemoryResolver::new();
1208 r.add(
1209 "physics.vrf",
1210 r#"
1211 PREMISE safety:
1212 EXCLUSIVE
1213 x a
1214 x b
1215 "#,
1216 );
1217 r.add(
1218 "main.vrf",
1219 r#"
1220 IMPORT "physics.vrf"
1221 PREMISE safety:
1222 EXCLUSIVE
1223 x a
1224 x c
1225 "#,
1226 );
1227 let c = compile("main.vrf", &r).unwrap();
1228 assert_eq!(c.clauses.len(), 2); assert!(c.clauses.iter().any(|cl| cl.origin.source == "physics.vrf"));
1230 assert!(c.clauses.iter().any(|cl| cl.origin.source == "main.vrf"));
1231 }
1232
1233 #[test]
1234 fn two_libs_same_name_into_one_consumer() {
1235 let mut r = MemoryResolver::new();
1239 r.add(
1240 "A.vrf",
1241 r#"
1242 PREMISE x:
1243 EXCLUSIVE
1244 S has a
1245 S has b
1246 "#,
1247 );
1248 r.add(
1249 "B.vrf",
1250 r#"
1251 PREMISE x:
1252 EXCLUSIVE
1253 S has a
1254 S has c
1255 "#,
1256 );
1257 r.add("C.vrf", "IMPORT \"A.vrf\"\nIMPORT \"B.vrf\"\n");
1258 let c = compile("C.vrf", &r).unwrap();
1259
1260 assert_eq!(c.clauses.len(), 2);
1262 assert!(
1263 c.clauses
1264 .iter()
1265 .any(|cl| cl.origin.source == "A.vrf" && cl.origin.premise.as_deref() == Some("x"))
1266 );
1267 assert!(
1268 c.clauses
1269 .iter()
1270 .any(|cl| cl.origin.source == "B.vrf" && cl.origin.premise.as_deref() == Some("x"))
1271 );
1272
1273 let s_a = id(&c, &key("S", "has", Some("a")));
1275 assert!(
1276 c.clauses
1277 .iter()
1278 .filter(|cl| cl.lits.iter().any(|l| l.atom == s_a))
1279 .count()
1280 == 2,
1281 "both clauses must reference the same unified `S has a` atom"
1282 );
1283 }
1284
1285 #[test]
1286 fn redefinition_within_one_source_still_errors() {
1287 let src = r#"
1289 PREMISE e:
1290 EXCLUSIVE
1291 x a
1292 x b
1293 PREMISE e:
1294 EXCLUSIVE
1295 x a
1296 x c
1297 "#;
1298 let err = compile_source("main.vrf", src).unwrap_err();
1299 assert_eq!(
1300 err,
1301 CompileError::PremiseRedefinition {
1302 name: "e".to_string()
1303 }
1304 );
1305 }
1306
1307 #[test]
1308 fn import_demo_examples_resolve() {
1309 let mut r = MemoryResolver::new();
1310 r.add(
1311 "physics.vrf",
1312 include_str!("../../../docs/examples/physics.vrf"),
1313 );
1314 r.add(
1315 "import-demo.vrf",
1316 include_str!("../../../docs/examples/import-demo.vrf"),
1317 );
1318 let c = compile("import-demo.vrf", &r).unwrap();
1319 assert!(c.pending_imports.is_empty());
1320 assert_eq!(c.clauses.len(), 2);
1322 let over_100 = id(&c, &key("Motor", "over_100", None));
1324 assert!(c.facts.iter().any(|f| f.atom == over_100));
1325 assert!(
1326 c.clauses
1327 .iter()
1328 .any(|cl| cl.lits.iter().any(|l| l.atom == over_100))
1329 );
1330 }
1331
1332 #[test]
1333 fn creature_example_compiles() {
1334 let src = include_str!("../../../docs/examples/creature.vrf");
1335 let c = compile_source("creature.vrf", src).unwrap();
1336 assert_eq!(c.facts.len(), 2); assert_eq!(c.rules.len(), 1); assert_eq!(c.checks.len(), 1);
1339 assert_eq!(c.clauses.len(), 4);
1341 assert_eq!(c.atoms.len(), 7);
1342 }
1343
1344 #[test]
1345 fn forbids_unfolds_pairwise() {
1346 let src = r#"
1347 PREMISE f:
1348 FORBIDS
1349 x a
1350 x b
1351 x c
1352 "#;
1353 let c = compile_source("<t>", src).unwrap();
1354 assert_eq!(c.clauses.len(), 3); assert!(
1356 c.clauses
1357 .iter()
1358 .all(|cl| cl.lits.len() == 2 && cl.lits.iter().all(|l| !l.negated))
1359 );
1360 }
1361
1362 #[test]
1363 fn rule_with_multiple_consequents() {
1364 let src = r#"
1365 RULE r:
1366 WHEN x a
1367 THEN x b
1368 AND x c
1369 "#;
1370 let c = compile_source("<t>", src).unwrap();
1371 assert_eq!(c.rules.len(), 1);
1372 assert_eq!(c.rules[0].consequent.len(), 2);
1373 }
1374
1375 #[test]
1376 fn negated_antecedent_literal_keeps_polarity() {
1377 let src = r#"
1379 PREMISE a:
1380 WHEN NOT x a
1381 THEN x b
1382 "#;
1383 let c = compile_source("<t>", src).unwrap();
1384 let xa = id(&c, &key("x", "a", None));
1385 assert!(c.clauses[0].lits.contains(&Lit {
1386 atom: xa,
1387 negated: true
1388 }));
1389 }
1390
1391 #[test]
1392 fn rule_keeps_consequent_negation() {
1393 let src = r#"
1394 RULE r:
1395 WHEN x a
1396 THEN NOT x b
1397 "#;
1398 let c = compile_source("<t>", src).unwrap();
1399 assert!(c.rules[0].consequent[0].negated);
1400 }
1401
1402 #[test]
1403 fn compilation_is_deterministic() {
1404 let src = r#"
1405 PREMISE e:
1406 EXCLUSIVE
1407 z z
1408 a a
1409 m m
1410 FACT q q
1411 "#;
1412 assert_eq!(
1413 compile_source("<t>", src).unwrap(),
1414 compile_source("<t>", src).unwrap()
1415 );
1416 }
1417
1418 #[test]
1419 fn empty_program_compiles_to_empty_ir() {
1420 let c = compile_source("<t>", "// nothing here\n").unwrap();
1421 assert!(c.atoms.is_empty() && c.clauses.is_empty() && c.facts.is_empty());
1422 }
1423
1424 #[test]
1425 fn same_clause_from_two_named_premises_is_deduped() {
1426 let src = r#"
1428 PREMISE e1:
1429 EXCLUSIVE
1430 x a
1431 x b
1432 PREMISE e2:
1433 EXCLUSIVE
1434 x a
1435 x b
1436 "#;
1437 let c = compile_source("<t>", src).unwrap();
1438 assert_eq!(c.clauses.len(), 1);
1439 }
1440
1441 #[test]
1442 fn object_distinguishes_atom_identity() {
1443 let c = compile_source("<t>", "FACT x p a\nFACT x p b\n").unwrap();
1445 assert_eq!(c.atoms.len(), 2);
1446 }
1447}