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, 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}
198
199#[derive(Clone)]
207struct RawLit {
208 key: AtomKey,
209 negated: bool,
210}
211
212struct RawFact {
214 key: AtomKey,
215 value: Value,
216 origin: Origin,
217}
218
219struct RawClause {
221 lits: Vec<RawLit>,
222 origin: Origin,
223}
224
225struct RawRule {
227 antecedent: Vec<RawLit>,
228 consequent: Vec<RawLit>,
229 origin: Origin,
230}
231
232#[derive(Default)]
236pub struct Compiler {
237 keys: BTreeSet<AtomKey>,
238 facts: Vec<RawFact>,
239 clauses: Vec<RawClause>,
240 rules: Vec<RawRule>,
241 checks: Vec<Check>,
242 pending_imports: Vec<String>,
243 defined: BTreeMap<(String, String), String>,
248 clause_sigs: BTreeSet<String>,
250 fact_sigs: BTreeSet<String>,
252}
253
254impl Compiler {
255 pub fn new() -> Self {
257 Self::default()
258 }
259
260 pub fn add_source(&mut self, source: &str, src: &str) -> Result<(), CompileError> {
263 let program = elenchus_parser::parse(src).map_err(|e| CompileError::Parse {
264 file: source.to_string(),
265 message: e.message,
266 })?;
267 for stmt in &program.statements {
268 self.add_statement(source, stmt)?;
269 }
270 Ok(())
271 }
272
273 fn load_recursive<R: Resolver>(
277 &mut self,
278 path: &str,
279 resolver: &R,
280 visited: &mut BTreeSet<String>,
281 stack: &mut Vec<String>,
282 ) -> Result<(), CompileError> {
283 let content = resolver.load(path)?;
284 let hash = hash_hex(content.as_bytes());
285 if visited.contains(&hash) {
286 return Ok(()); }
288 if stack.contains(&hash) {
289 return Err(CompileError::CircularImport(path.to_string()));
290 }
291 stack.push(hash.clone());
292
293 let program = elenchus_parser::parse(&content).map_err(|e| CompileError::Parse {
294 file: path.to_string(),
295 message: e.message,
296 })?;
297 for stmt in &program.statements {
298 if let Statement::Import(p) = stmt {
299 let resolved = resolver.resolve(path, p.data);
300 self.load_recursive(&resolved, resolver, visited, stack)?;
301 } else {
302 self.add_statement(path, stmt)?;
303 }
304 }
305
306 stack.pop();
307 visited.insert(hash);
308 Ok(())
309 }
310
311 fn add_statement(&mut self, source: &str, stmt: &Statement) -> Result<(), CompileError> {
315 match stmt {
316 Statement::Import(path) => {
317 self.pending_imports.push(path.data.to_string());
318 }
319 Statement::Fact(a) => self.add_fact(source, a, Value::True, "FACT"),
320 Statement::Negation(a) => self.add_fact(source, a, Value::False, "NOT"),
321 Statement::Check {
322 subject,
323 bidirectional,
324 } => self.checks.push(Check {
325 subject: subject.as_ref().map(|s| s.data.to_string()),
326 bidirectional: *bidirectional,
327 }),
328 Statement::Premise { name, body } => {
329 let line = name.span.location_line();
330 self.add_named(source, name.data, line, body, false)?;
331 }
332 Statement::Rule { name, body } => {
333 let line = name.span.location_line();
334 self.add_named(source, name.data, line, body, true)?;
335 }
336 }
337 Ok(())
338 }
339
340 fn intern(&mut self, key: &AtomKey) {
342 if !self.keys.contains(key) {
343 self.keys.insert(key.clone());
344 }
345 }
346
347 fn add_fact(
351 &mut self,
352 source: &str,
353 a: &elenchus_parser::Located<Atom>,
354 value: Value,
355 kind: &'static str,
356 ) {
357 let key = AtomKey::from_atom(&a.data);
358 self.intern(&key);
359 let sig = alloc::format!(
360 "{}|{}|{}|{}",
361 key_sig(&key),
362 matches!(value, Value::True) as u8,
363 kind,
364 "" );
366 if !self.fact_sigs.insert(sig) {
367 return; }
369 self.facts.push(RawFact {
370 key,
371 value,
372 origin: Origin {
373 source: source.to_string(),
374 line: a.span.location_line(),
375 premise: None,
376 kind,
377 },
378 });
379 }
380
381 fn add_named(
384 &mut self,
385 source: &str,
386 name: &str,
387 line: u32,
388 body: &Body,
389 is_rule: bool,
390 ) -> Result<(), CompileError> {
391 let body_hash = hash_hex(canonical_body(name, body, is_rule).as_bytes());
392 let key = (source.to_string(), name.to_string());
393 match self.defined.get(&key) {
394 Some(prev) if *prev == body_hash => return Ok(()), Some(_) => {
396 return Err(CompileError::PremiseRedefinition {
398 name: name.to_string(),
399 });
400 }
401 None => {
402 self.defined.insert(key, body_hash);
403 }
404 }
405
406 if is_rule {
407 if let Body::Impl {
409 antecedent,
410 consequent,
411 } = body
412 {
413 let (ante, cons) = (raw_lits(antecedent), raw_lits(consequent));
414 for l in ante.iter().chain(cons.iter()) {
415 self.intern(&l.key);
416 }
417 self.rules.push(RawRule {
418 antecedent: ante,
419 consequent: cons,
420 origin: self.origin(source, line, Some(name), "RULE"),
421 });
422 }
423 return Ok(());
424 }
425
426 match body {
427 Body::List { op, atoms } => {
428 let keys: Vec<AtomKey> =
429 atoms.iter().map(|a| AtomKey::from_atom(&a.data)).collect();
430 for k in &keys {
431 self.intern(k);
432 }
433 let kind = list_kind(*op);
434 let origin = self.origin(source, line, Some(name), kind);
435 match op {
436 ListOp::Exclusive | ListOp::Forbids => {
438 self.emit_pairwise(&keys, &origin);
439 }
440 ListOp::OneOf => {
442 self.emit_pairwise(&keys, &origin);
443 self.emit_at_least_one(&keys, &origin);
444 }
445 ListOp::AtLeast => {
447 self.emit_at_least_one(&keys, &origin);
448 }
449 }
450 }
451 Body::Impl {
452 antecedent,
453 consequent,
454 } => {
455 let ante = raw_lits(antecedent);
457 for l in &ante {
458 self.intern(&l.key);
459 }
460 let origin = self.origin(source, line, Some(name), "PREMISE");
461 for c in consequent {
462 let neg_c = RawLit {
463 key: AtomKey::from_atom(&c.data.atom),
464 negated: !c.data.negated,
465 };
466 self.intern(&neg_c.key);
467 let mut lits = ante.clone();
468 lits.push(neg_c);
469 self.push_clause(lits, origin.clone());
470 }
471 }
472 }
473 Ok(())
474 }
475
476 fn emit_pairwise(&mut self, keys: &[AtomKey], origin: &Origin) {
480 for i in 0..keys.len() {
481 for j in (i + 1)..keys.len() {
482 let lits = vec![
483 RawLit {
484 key: keys[i].clone(),
485 negated: false,
486 },
487 RawLit {
488 key: keys[j].clone(),
489 negated: false,
490 },
491 ];
492 self.push_clause(lits, origin.clone());
493 }
494 }
495 }
496
497 fn emit_at_least_one(&mut self, keys: &[AtomKey], origin: &Origin) {
500 let lits = keys
501 .iter()
502 .map(|k| RawLit {
503 key: k.clone(),
504 negated: true,
505 })
506 .collect();
507 self.push_clause(lits, origin.clone());
508 }
509
510 fn push_clause(&mut self, lits: Vec<RawLit>, origin: Origin) {
513 let sig = clause_sig(&lits);
514 if self.clause_sigs.insert(sig) {
515 self.clauses.push(RawClause { lits, origin });
516 }
517 }
519
520 fn origin(&self, source: &str, line: u32, premise: Option<&str>, kind: &'static str) -> Origin {
522 Origin {
523 source: source.to_string(),
524 line,
525 premise: premise.map(|s| s.to_string()),
526 kind,
527 }
528 }
529
530 pub fn finalize(self) -> Compiled {
532 let atoms: Vec<AtomKey> = self.keys.into_iter().collect(); let mut id_of: BTreeMap<AtomKey, AtomId> = BTreeMap::new();
534 for (i, k) in atoms.iter().enumerate() {
535 id_of.insert(k.clone(), i as AtomId);
536 }
537 let lower = |l: &RawLit| Lit {
538 atom: id_of[&l.key],
539 negated: l.negated,
540 };
541
542 let facts = self
543 .facts
544 .into_iter()
545 .map(|f| Fact {
546 atom: id_of[&f.key],
547 value: f.value,
548 origin: f.origin,
549 })
550 .collect();
551 let clauses = self
552 .clauses
553 .into_iter()
554 .map(|c| Clause {
555 lits: c.lits.iter().map(lower).collect(),
556 origin: c.origin,
557 })
558 .collect();
559 let rules = self
560 .rules
561 .into_iter()
562 .map(|r| Rule {
563 antecedent: r.antecedent.iter().map(lower).collect(),
564 consequent: r.consequent.iter().map(lower).collect(),
565 origin: r.origin,
566 })
567 .collect();
568
569 Compiled {
570 atoms,
571 facts,
572 clauses,
573 rules,
574 checks: self.checks,
575 pending_imports: self.pending_imports,
576 }
577 }
578}
579
580pub fn compile_source(source: &str, src: &str) -> Result<Compiled, CompileError> {
583 let mut c = Compiler::new();
584 c.add_source(source, src)?;
585 Ok(c.finalize())
586}
587
588pub trait Resolver {
594 fn load(&self, path: &str) -> Result<String, CompileError>;
596
597 fn resolve(&self, _base: &str, relative: &str) -> String {
600 relative.to_string()
601 }
602}
603
604#[derive(Default)]
607pub struct MemoryResolver {
608 sources: BTreeMap<String, String>,
609}
610
611impl MemoryResolver {
612 pub fn new() -> Self {
614 Self::default()
615 }
616
617 pub fn add(&mut self, path: &str, content: &str) -> &mut Self {
619 self.sources.insert(path.to_string(), content.to_string());
620 self
621 }
622}
623
624impl Resolver for MemoryResolver {
625 fn load(&self, path: &str) -> Result<String, CompileError> {
626 self.sources
627 .get(path)
628 .cloned()
629 .ok_or_else(|| CompileError::ImportNotFound(path.to_string()))
630 }
631}
632
633#[cfg(feature = "std")]
637pub struct FileResolver;
638
639#[cfg(feature = "std")]
640impl Resolver for FileResolver {
641 fn load(&self, path: &str) -> Result<String, CompileError> {
642 std::fs::read_to_string(path)
643 .map_err(|e| CompileError::ImportNotFound(alloc::format!("{}: {}", path, e)))
644 }
645
646 fn resolve(&self, base: &str, relative: &str) -> String {
647 use std::path::{Component, Path, PathBuf};
648 let parent = Path::new(base).parent().unwrap_or_else(|| Path::new("."));
649 let joined = parent.join(relative);
650 let mut out = PathBuf::new();
651 for component in joined.components() {
652 match component {
653 Component::ParentDir => {
654 out.pop();
655 }
656 Component::CurDir => {}
657 c => out.push(c),
658 }
659 }
660 out.to_string_lossy().replace('\\', "/")
664 }
665}
666
667pub fn compile<R: Resolver>(root: &str, resolver: &R) -> Result<Compiled, CompileError> {
677 let mut c = Compiler::new();
678 let mut visited = BTreeSet::new();
679 let mut stack = Vec::new();
680 c.load_recursive(root, resolver, &mut visited, &mut stack)?;
681 Ok(c.finalize())
682}
683
684fn raw_lits(lits: &[elenchus_parser::Located<Literal>]) -> Vec<RawLit> {
688 lits.iter()
689 .map(|l| RawLit {
690 key: AtomKey::from_atom(&l.data.atom),
691 negated: l.data.negated,
692 })
693 .collect()
694}
695
696fn list_kind(op: ListOp) -> &'static str {
698 match op {
699 ListOp::Exclusive => "EXCLUSIVE",
700 ListOp::Forbids => "FORBIDS",
701 ListOp::OneOf => "ONEOF",
702 ListOp::AtLeast => "ATLEAST",
703 }
704}
705
706fn key_sig(k: &AtomKey) -> String {
709 alloc::format!(
710 "{}|{}|{}",
711 k.subject,
712 k.predicate,
713 k.object.as_deref().unwrap_or("")
714 )
715}
716
717fn clause_sig(lits: &[RawLit]) -> String {
719 let mut parts: Vec<String> = lits
720 .iter()
721 .map(|l| alloc::format!("{}|{}", key_sig(&l.key), l.negated as u8))
722 .collect();
723 parts.sort();
724 parts.dedup();
725 parts.join(";")
726}
727
728fn canonical_body(name: &str, body: &Body, is_rule: bool) -> String {
730 let mut s = String::new();
731 let _ = write!(s, "{}|{}|", if is_rule { "RULE" } else { "PREMISE" }, name);
732 match body {
733 Body::List { op, atoms } => {
734 let _ = write!(s, "LIST|{}|", list_kind(*op));
735 let mut keys: Vec<String> = atoms
736 .iter()
737 .map(|a| key_sig(&AtomKey::from_atom(&a.data)))
738 .collect();
739 keys.sort();
740 s.push_str(&keys.join(";"));
741 }
742 Body::Impl {
743 antecedent,
744 consequent,
745 } => {
746 s.push_str("IMPL|ANTE|");
747 s.push_str(&lit_sigs(antecedent));
748 s.push_str("|CONS|");
749 s.push_str(&lit_sigs(consequent));
750 }
751 }
752 s
753}
754
755fn lit_sigs(lits: &[elenchus_parser::Located<Literal>]) -> String {
758 let mut parts: Vec<String> = lits
759 .iter()
760 .map(|l| {
761 alloc::format!(
762 "{}|{}",
763 key_sig(&AtomKey::from_atom(&l.data.atom)),
764 l.data.negated as u8
765 )
766 })
767 .collect();
768 parts.sort();
769 parts.join(";")
770}
771
772#[cfg(test)]
773mod tests {
774 use super::*;
775
776 fn key(subject: &str, predicate: &str, object: Option<&str>) -> AtomKey {
777 AtomKey {
778 subject: subject.to_string(),
779 predicate: predicate.to_string(),
780 object: object.map(|o| o.to_string()),
781 }
782 }
783
784 fn id(c: &Compiled, k: &AtomKey) -> AtomId {
785 c.atoms.iter().position(|a| a == k).unwrap() as AtomId
786 }
787
788 #[test]
789 fn exclusive_unfolds_pairwise() {
790 let src = "PREMISE e:\n EXCLUSIVE\n x a\n x b\n x c\n";
791 let c = compile_source("<t>", src).unwrap();
792 assert_eq!(c.clauses.len(), 3);
794 for cl in &c.clauses {
795 assert_eq!(cl.lits.len(), 2);
796 assert!(cl.lits.iter().all(|l| !l.negated));
797 }
798 }
799
800 #[test]
801 fn implication_negates_consequent() {
802 let src = "PREMISE r:\n WHEN x a\n THEN x b\n";
804 let c = compile_source("<t>", src).unwrap();
805 assert_eq!(c.clauses.len(), 1);
806 let cl = &c.clauses[0];
807 assert_eq!(cl.lits.len(), 2);
808 let a = id(&c, &key("x", "a", None));
809 let b = id(&c, &key("x", "b", None));
810 assert!(cl.lits.contains(&Lit {
811 atom: a,
812 negated: false
813 }));
814 assert!(cl.lits.contains(&Lit {
815 atom: b,
816 negated: true
817 }));
818 }
819
820 #[test]
821 fn negated_consequent_flips_to_positive() {
822 let src = "PREMISE r:\n WHEN x a\n THEN NOT x b\n";
824 let c = compile_source("<t>", src).unwrap();
825 let b = id(&c, &key("x", "b", None));
826 assert!(c.clauses[0].lits.contains(&Lit {
827 atom: b,
828 negated: false
829 }));
830 }
831
832 #[test]
833 fn oneof_is_pairwise_plus_at_least_one() {
834 let src = "PREMISE o:\n ONEOF\n x a\n x b\n";
835 let c = compile_source("<t>", src).unwrap();
836 assert_eq!(c.clauses.len(), 2);
838 assert!(c.clauses.iter().any(|cl| cl.lits.iter().all(|l| l.negated)));
840 }
841
842 #[test]
843 fn atleast_is_one_negated_clause() {
844 let src = "PREMISE a:\n ATLEAST\n x a\n x b\n x c\n";
845 let c = compile_source("<t>", src).unwrap();
846 assert_eq!(c.clauses.len(), 1);
847 assert_eq!(c.clauses[0].lits.len(), 3);
848 assert!(c.clauses[0].lits.iter().all(|l| l.negated));
849 }
850
851 #[test]
852 fn rules_are_separate_from_clauses() {
853 let src = "RULE needs:\n WHEN x a\n THEN x b\n";
854 let c = compile_source("<t>", src).unwrap();
855 assert_eq!(c.clauses.len(), 0);
856 assert_eq!(c.rules.len(), 1);
857 assert_eq!(c.rules[0].antecedent.len(), 1);
858 assert_eq!(c.rules[0].consequent.len(), 1);
859 }
860
861 #[test]
862 fn atoms_are_canonically_sorted() {
863 let src = "FACT z z\nFACT a a\nFACT m m\n";
864 let c = compile_source("<t>", src).unwrap();
865 let mut sorted = c.atoms.clone();
866 sorted.sort();
867 assert_eq!(c.atoms, sorted);
868 }
869
870 #[test]
871 fn duplicate_premise_is_idempotent() {
872 let src = "PREMISE e:\n EXCLUSIVE\n x a\n x b\nPREMISE e:\n EXCLUSIVE\n x a\n x b\n";
873 let c = compile_source("<t>", src).unwrap();
874 assert_eq!(c.clauses.len(), 1);
875 }
876
877 #[test]
878 fn redefinition_with_different_body_errors() {
879 let src = "PREMISE e:\n EXCLUSIVE\n x a\n x b\nPREMISE e:\n EXCLUSIVE\n x a\n x c\n";
880 let err = compile_source("<t>", src).unwrap_err();
881 assert_eq!(
882 err,
883 CompileError::PremiseRedefinition {
884 name: "e".to_string()
885 }
886 );
887 }
888
889 #[test]
890 fn duplicate_fact_is_idempotent() {
891 let c = compile_source("<t>", "FACT x a\nFACT x a\n").unwrap();
892 assert_eq!(c.facts.len(), 1);
893 }
894
895 #[test]
896 fn conflicting_facts_are_both_kept() {
897 let c = compile_source("<t>", "FACT x a\nNOT x a\n").unwrap();
899 assert_eq!(c.facts.len(), 2);
900 }
901
902 #[test]
903 fn import_is_recorded_pending() {
904 let c = compile_source("<t>", "IMPORT \"physics.vrf\"\nFACT x a\n").unwrap();
905 assert_eq!(c.pending_imports, vec!["physics.vrf".to_string()]);
906 }
907
908 #[test]
909 fn import_flat_merges_and_atoms_unify() {
910 let mut r = MemoryResolver::new();
913 r.add(
914 "lib.vrf",
915 "PREMISE needs_fuel:\n WHEN Engine.X has engine\n THEN Engine.X has fuel\n",
916 );
917 r.add(
918 "main.vrf",
919 "IMPORT \"lib.vrf\"\nFACT Engine.X has engine\nFACT Engine.X has fuel\n",
920 );
921 let c = compile("main.vrf", &r).unwrap();
922 assert!(c.pending_imports.is_empty());
923 assert_eq!(c.clauses.len(), 1); assert_eq!(c.facts.len(), 2);
925
926 let fuel = key("Engine.X", "has", Some("fuel"));
928 let fuel_id = id(&c, &fuel);
929 assert!(c.facts.iter().any(|f| f.atom == fuel_id));
930 assert!(c.clauses[0].lits.iter().any(|l| l.atom == fuel_id));
931 }
932
933 #[test]
934 fn diamond_import_is_deduped() {
935 let mut r = MemoryResolver::new();
937 r.add(
938 "base.vrf",
939 "PREMISE b:\n EXCLUSIVE\n x a\n x b\n",
940 );
941 r.add("a.vrf", "IMPORT \"base.vrf\"\n");
942 r.add("c.vrf", "IMPORT \"base.vrf\"\n");
943 r.add("main.vrf", "IMPORT \"a.vrf\"\nIMPORT \"c.vrf\"\n");
944 let c = compile("main.vrf", &r).unwrap();
945 assert_eq!(c.clauses.len(), 1); }
947
948 #[test]
949 fn circular_import_errors() {
950 let mut r = MemoryResolver::new();
951 r.add("a.vrf", "IMPORT \"b.vrf\"\n");
952 r.add("b.vrf", "IMPORT \"a.vrf\"\n");
953 let err = compile("a.vrf", &r).unwrap_err();
954 assert!(matches!(err, CompileError::CircularImport(_)));
955 }
956
957 #[test]
958 fn missing_import_errors() {
959 let mut r = MemoryResolver::new();
960 r.add("main.vrf", "IMPORT \"ghost.vrf\"\n");
961 let err = compile("main.vrf", &r).unwrap_err();
962 assert!(matches!(err, CompileError::ImportNotFound(_)));
963 }
964
965 #[test]
966 fn same_name_across_domains_coexists() {
967 let mut r = MemoryResolver::new();
971 r.add(
972 "physics.vrf",
973 "PREMISE safety:\n EXCLUSIVE\n x a\n x b\n",
974 );
975 r.add(
976 "main.vrf",
977 "IMPORT \"physics.vrf\"\nPREMISE safety:\n EXCLUSIVE\n x a\n x c\n",
978 );
979 let c = compile("main.vrf", &r).unwrap();
980 assert_eq!(c.clauses.len(), 2); assert!(c.clauses.iter().any(|cl| cl.origin.source == "physics.vrf"));
982 assert!(c.clauses.iter().any(|cl| cl.origin.source == "main.vrf"));
983 }
984
985 #[test]
986 fn two_libs_same_name_into_one_consumer() {
987 let mut r = MemoryResolver::new();
991 r.add(
992 "A.vrf",
993 "PREMISE x:\n EXCLUSIVE\n S has a\n S has b\n",
994 );
995 r.add(
996 "B.vrf",
997 "PREMISE x:\n EXCLUSIVE\n S has a\n S has c\n",
998 );
999 r.add("C.vrf", "IMPORT \"A.vrf\"\nIMPORT \"B.vrf\"\n");
1000 let c = compile("C.vrf", &r).unwrap();
1001
1002 assert_eq!(c.clauses.len(), 2);
1004 assert!(
1005 c.clauses
1006 .iter()
1007 .any(|cl| cl.origin.source == "A.vrf" && cl.origin.premise.as_deref() == Some("x"))
1008 );
1009 assert!(
1010 c.clauses
1011 .iter()
1012 .any(|cl| cl.origin.source == "B.vrf" && cl.origin.premise.as_deref() == Some("x"))
1013 );
1014
1015 let s_a = id(&c, &key("S", "has", Some("a")));
1017 assert!(
1018 c.clauses
1019 .iter()
1020 .filter(|cl| cl.lits.iter().any(|l| l.atom == s_a))
1021 .count()
1022 == 2,
1023 "both clauses must reference the same unified `S has a` atom"
1024 );
1025 }
1026
1027 #[test]
1028 fn redefinition_within_one_source_still_errors() {
1029 let src = "PREMISE e:\n EXCLUSIVE\n x a\n x b\nPREMISE e:\n EXCLUSIVE\n x a\n x c\n";
1031 let err = compile_source("main.vrf", src).unwrap_err();
1032 assert_eq!(
1033 err,
1034 CompileError::PremiseRedefinition {
1035 name: "e".to_string()
1036 }
1037 );
1038 }
1039
1040 #[test]
1041 fn import_demo_examples_resolve() {
1042 let mut r = MemoryResolver::new();
1043 r.add(
1044 "physics.vrf",
1045 include_str!("../../../docs/examples/physics.vrf"),
1046 );
1047 r.add(
1048 "import-demo.vrf",
1049 include_str!("../../../docs/examples/import-demo.vrf"),
1050 );
1051 let c = compile("import-demo.vrf", &r).unwrap();
1052 assert!(c.pending_imports.is_empty());
1053 assert_eq!(c.clauses.len(), 2);
1055 let over_100 = id(&c, &key("Motor", "over_100", None));
1057 assert!(c.facts.iter().any(|f| f.atom == over_100));
1058 assert!(
1059 c.clauses
1060 .iter()
1061 .any(|cl| cl.lits.iter().any(|l| l.atom == over_100))
1062 );
1063 }
1064
1065 #[test]
1066 fn creature_example_compiles() {
1067 let src = include_str!("../../../docs/examples/creature.vrf");
1068 let c = compile_source("creature.vrf", src).unwrap();
1069 assert_eq!(c.facts.len(), 2); assert_eq!(c.rules.len(), 1); assert_eq!(c.checks.len(), 1);
1072 assert_eq!(c.clauses.len(), 4);
1074 assert_eq!(c.atoms.len(), 7);
1075 }
1076
1077 #[test]
1078 fn forbids_unfolds_pairwise() {
1079 let src = "PREMISE f:\n FORBIDS\n x a\n x b\n x c\n";
1080 let c = compile_source("<t>", src).unwrap();
1081 assert_eq!(c.clauses.len(), 3); assert!(
1083 c.clauses
1084 .iter()
1085 .all(|cl| cl.lits.len() == 2 && cl.lits.iter().all(|l| !l.negated))
1086 );
1087 }
1088
1089 #[test]
1090 fn rule_with_multiple_consequents() {
1091 let src = "RULE r:\n WHEN x a\n THEN x b\n AND x c\n";
1092 let c = compile_source("<t>", src).unwrap();
1093 assert_eq!(c.rules.len(), 1);
1094 assert_eq!(c.rules[0].consequent.len(), 2);
1095 }
1096
1097 #[test]
1098 fn negated_antecedent_literal_keeps_polarity() {
1099 let src = "PREMISE a:\n WHEN NOT x a\n THEN x b\n";
1101 let c = compile_source("<t>", src).unwrap();
1102 let xa = id(&c, &key("x", "a", None));
1103 assert!(c.clauses[0].lits.contains(&Lit {
1104 atom: xa,
1105 negated: true
1106 }));
1107 }
1108
1109 #[test]
1110 fn rule_keeps_consequent_negation() {
1111 let src = "RULE r:\n WHEN x a\n THEN NOT x b\n";
1112 let c = compile_source("<t>", src).unwrap();
1113 assert!(c.rules[0].consequent[0].negated);
1114 }
1115
1116 #[test]
1117 fn compilation_is_deterministic() {
1118 let src = "PREMISE e:\n EXCLUSIVE\n z z\n a a\n m m\nFACT q q\n";
1119 assert_eq!(
1120 compile_source("<t>", src).unwrap(),
1121 compile_source("<t>", src).unwrap()
1122 );
1123 }
1124
1125 #[test]
1126 fn empty_program_compiles_to_empty_ir() {
1127 let c = compile_source("<t>", "// nothing here\n").unwrap();
1128 assert!(c.atoms.is_empty() && c.clauses.is_empty() && c.facts.is_empty());
1129 }
1130
1131 #[test]
1132 fn same_clause_from_two_named_premises_is_deduped() {
1133 let src = "PREMISE e1:\n EXCLUSIVE\n x a\n x b\nPREMISE e2:\n EXCLUSIVE\n x a\n x b\n";
1135 let c = compile_source("<t>", src).unwrap();
1136 assert_eq!(c.clauses.len(), 1);
1137 }
1138
1139 #[test]
1140 fn object_distinguishes_atom_identity() {
1141 let c = compile_source("<t>", "FACT x p a\nFACT x p b\n").unwrap();
1143 assert_eq!(c.atoms.len(), 2);
1144 }
1145}