1#![no_std]
33#![warn(missing_docs)]
35
36extern crate alloc;
37
38#[cfg(feature = "std")]
39extern crate std;
40
41use alloc::boxed::Box;
42use alloc::collections::{BTreeMap, BTreeSet};
43use alloc::string::{String, ToString};
44use alloc::vec;
45use alloc::vec::Vec;
46use core::fmt::Write as _;
47
48use elenchus_parser::{Atom, Body, CloseKind, Conn, ListOp, Literal, Located, Quant, Statement};
49pub use elenchus_parser::{Diagnostics, kw};
54use sha2::{Digest, Sha256};
55use thiserror::Error;
56
57pub fn hash_hex(data: &[u8]) -> String {
62 let mut hasher = Sha256::new();
63 hasher.update(data);
64 let out = hasher.finalize();
65 let mut s = String::with_capacity(out.len() * 2);
66 for b in out {
67 let _ = write!(s, "{:02x}", b);
68 }
69 s
70}
71
72pub type AtomId = u32;
76
77#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
83pub struct AtomKey {
84 pub domain: String,
87 pub subject: String,
89 pub predicate: String,
91 pub object: Option<String>,
93}
94
95struct DomainCtx {
100 current: String,
102 aliases: BTreeMap<String, String>,
105}
106
107impl DomainCtx {
108 fn resolve(&self, prefix: Option<&str>) -> Result<String, CompileError> {
111 match prefix {
112 None => Ok(self.current.clone()),
113 Some(p) => self
114 .aliases
115 .get(p)
116 .cloned()
117 .ok_or_else(|| CompileError::UnknownDomain {
118 domain: p.to_string(),
119 }),
120 }
121 }
122
123 fn key(&self, a: &Atom) -> Result<AtomKey, CompileError> {
126 Ok(AtomKey {
127 domain: self.resolve(a.domain)?,
128 subject: a.subject.to_string(),
129 predicate: a.predicate.to_string(),
130 object: a.object.map(|o| o.to_string()),
131 })
132 }
133}
134
135#[derive(Debug, Clone, Copy, PartialEq, Eq)]
138pub struct Lit {
139 pub atom: AtomId,
141 pub negated: bool,
143}
144
145#[derive(Debug, Clone, Copy, PartialEq, Eq)]
147pub enum Value {
148 True,
150 False,
152}
153
154#[derive(Debug, Clone, PartialEq, Eq)]
156pub struct Origin {
157 pub source: String,
159 pub line: u32,
161 pub premise: Option<String>,
163 pub kind: &'static str,
167}
168
169pub const KIND_UNSAT: &str = "UNSAT";
174
175#[derive(Debug, Clone, PartialEq, Eq)]
178pub struct Fact {
179 pub atom: AtomId,
181 pub value: Value,
183 pub origin: Origin,
185 pub soft: bool,
190}
191
192#[derive(Debug, Clone, PartialEq, Eq)]
194pub struct Clause {
195 pub lits: Vec<Lit>,
197 pub origin: Origin,
199}
200
201#[derive(Debug, Clone, PartialEq, Eq)]
204pub struct Rule {
205 pub antecedent: Vec<Lit>,
207 pub consequent: Vec<Lit>,
209 pub origin: Origin,
211}
212
213#[derive(Debug, Clone, PartialEq, Eq)]
215pub struct Check {
216 pub subject: Option<String>,
218 pub bidirectional: bool,
220}
221
222#[derive(Debug, Clone, PartialEq, Eq)]
224pub struct Compiled {
225 pub atoms: Vec<AtomKey>,
227 pub facts: Vec<Fact>,
229 pub clauses: Vec<Clause>,
231 pub rules: Vec<Rule>,
233 pub checks: Vec<Check>,
235 pub pending_imports: Vec<String>,
238 pub unused_imports: Vec<UnusedImport>,
243 pub consumed: Vec<AtomId>,
247}
248
249#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
254pub struct UnusedImport {
255 pub file: String,
257 pub domain: String,
259 pub alias: Option<String>,
261 pub line: u32,
263}
264
265#[derive(Debug, Error, PartialEq, Eq)]
267pub enum CompileError {
268 #[error("{0}")]
272 Parse(elenchus_parser::Diagnostics),
273 #[error("'{name}' redefined with a different body")]
275 PremiseRedefinition {
276 name: String,
278 },
279 #[error("{file}: missing a DOMAIN declaration (every file must start with `DOMAIN <name>`)")]
282 MissingDomain {
283 file: String,
285 },
286 #[error("{file}: more than one DOMAIN declaration (a file has exactly one domain)")]
288 DuplicateDomain {
289 file: String,
291 },
292 #[error("unknown domain '{domain}' — declare it with DOMAIN, or IMPORT it in this file")]
295 UnknownDomain {
296 domain: String,
298 },
299 #[error("domain name '{alias}' is bound to two different imports (disambiguate with AS)")]
302 DomainAliasClash {
303 alias: String,
305 },
306 #[error("import not found: {0}")]
308 ImportNotFound(String),
309 #[error("circular import: {0}")]
311 CircularImport(String),
312 #[error("rule '{name}' cannot derive a disjunction (OR in THEN); use a PREMISE instead")]
316 RuleDisjunctiveConsequent {
317 name: String,
319 },
320 #[error(transparent)]
326 UnknownValue(Box<UnknownValue>),
327 #[error("{file}:{line}: FOR EACH ranges over '{set}', which is not a declared SET{suggestion}")]
331 UnknownSet {
332 file: String,
334 line: u32,
336 set: String,
338 suggestion: String,
340 },
341 #[error(
344 "{file}:{line}: relation '{relation}' has a cycle (`{node}` reaches itself) \
345 — CLOSE … TRANSITIVE requires a DAG"
346 )]
347 CyclicRelation {
348 file: String,
350 line: u32,
352 relation: String,
354 node: String,
356 },
357}
358
359#[derive(Debug, Error, PartialEq, Eq)]
362#[error(
363 "{file}:{line}: '{value}' is not a declared value of '{subject} {predicate}' \
364 — ONEOF declares {{ {declared} }}{suggestion}"
365)]
366pub struct UnknownValue {
367 pub file: String,
369 pub line: u32,
371 pub subject: String,
373 pub predicate: String,
375 pub value: String,
377 pub declared: String,
379 pub suggestion: String,
381}
382
383#[derive(Clone)]
391struct RawLit {
392 key: AtomKey,
393 negated: bool,
394}
395
396struct RawFact {
398 key: AtomKey,
399 value: Value,
400 origin: Origin,
401 soft: bool,
402}
403
404struct RawClause {
406 lits: Vec<RawLit>,
407 origin: Origin,
408}
409
410struct RawRule {
412 antecedent: Vec<RawLit>,
413 consequent: Vec<RawLit>,
414 origin: Origin,
415}
416
417#[derive(Default)]
421pub struct Compiler {
422 keys: BTreeSet<AtomKey>,
423 facts: Vec<RawFact>,
424 clauses: Vec<RawClause>,
425 rules: Vec<RawRule>,
426 checks: Vec<Check>,
427 pending_imports: Vec<String>,
428 defined: BTreeMap<(String, String), String>,
433 clause_sigs: BTreeSet<String>,
435 fact_sigs: BTreeSet<String>,
437 oneof_values: BTreeMap<(String, String, String), BTreeSet<String>>,
444 sets: BTreeMap<String, Vec<String>>,
449 relations: BTreeMap<String, Vec<(String, String)>>,
453 relation_consumed: BTreeSet<AtomKey>,
458}
459
460impl Compiler {
461 pub fn new() -> Self {
463 Self::default()
464 }
465
466 pub fn add_source(&mut self, source: &str, src: &str) -> Result<(), CompileError> {
472 let program = parse_tagged(source, src)?;
473 let domain = extract_domain(&program, source)?;
474 let mut aliases = BTreeMap::new();
475 aliases.insert(domain.clone(), domain.clone());
476 let ctx = DomainCtx {
477 current: domain,
478 aliases,
479 };
480 self.collect_decls(&program);
481 self.apply_closures(&program, source)?;
482 for stmt in &program.statements {
483 match stmt {
484 Statement::Domain(_) => {}
485 Statement::Import { path, .. } => {
486 self.pending_imports.push(path.data.to_string());
487 }
488 other => self.add_statement(source, other, &ctx)?,
489 }
490 }
491 Ok(())
492 }
493
494 fn apply_closures(
500 &mut self,
501 program: &elenchus_parser::Program,
502 source: &str,
503 ) -> Result<(), CompileError> {
504 for stmt in &program.statements {
505 if let Statement::Close { relation, kind } = stmt {
506 let CloseKind::Transitive = kind;
507 let pairs = self
508 .relations
509 .get(relation.data)
510 .cloned()
511 .unwrap_or_default();
512 let closed = transitive_closure(pairs);
513 if let Some((node, _)) = closed.iter().find(|(a, b)| a == b) {
514 return Err(CompileError::CyclicRelation {
515 file: source.to_string(),
516 line: relation.span.location_line(),
517 relation: relation.data.to_string(),
518 node: node.clone(),
519 });
520 }
521 self.relations.insert(relation.data.to_string(), closed);
522 }
523 }
524 Ok(())
525 }
526
527 fn collect_decls(&mut self, program: &elenchus_parser::Program) {
531 for stmt in &program.statements {
532 match stmt {
533 Statement::Set { name, elements } => {
534 self.sets.insert(
535 name.data.to_string(),
536 elements.iter().map(|e| e.data.to_string()).collect(),
537 );
538 }
539 Statement::Fact(a) => {
540 if let Some(obj) = a.data.object {
541 self.relations
542 .entry(a.data.predicate.to_string())
543 .or_default()
544 .push((a.data.subject.to_string(), obj.to_string()));
545 }
546 }
547 _ => {}
548 }
549 }
550 }
551
552 fn add_resolved(&mut self, file: &ResolvedFile) -> Result<(), CompileError> {
554 let program = parse_tagged(&file.path, &file.content)?;
555 self.collect_decls(&program);
556 self.apply_closures(&program, &file.path)?;
557 for stmt in &program.statements {
558 match stmt {
559 Statement::Import { .. } | Statement::Domain(_) => {}
560 other => self.add_statement(&file.path, other, &file.ctx)?,
561 }
562 }
563 Ok(())
564 }
565
566 fn add_statement(
569 &mut self,
570 source: &str,
571 stmt: &Statement,
572 ctx: &DomainCtx,
573 ) -> Result<(), CompileError> {
574 match stmt {
575 Statement::Import { .. } | Statement::Domain(_) => {}
577 Statement::Fact(a) => self.add_fact(source, a, Value::True, kw::FACT, false, ctx)?,
578 Statement::Negation(a) => {
579 self.add_fact(source, a, Value::False, kw::NOT, false, ctx)?
580 }
581 Statement::Assume(l) => {
582 let value = if l.data.negated {
583 Value::False
584 } else {
585 Value::True
586 };
587 let located = elenchus_parser::Located {
591 data: l.data.atom.clone(),
592 span: l.span,
593 };
594 self.add_fact(source, &located, value, kw::ASSUME, true, ctx)?;
595 }
596 Statement::Check {
597 subject,
598 bidirectional,
599 } => self.checks.push(Check {
600 subject: subject.as_ref().map(|s| s.data.to_string()),
601 bidirectional: *bidirectional,
602 }),
603 Statement::Set { .. } | Statement::Close { .. } => {}
606 Statement::Premise { name, quant, body } => {
607 self.add_named(source, name, quant.as_ref(), body, false, ctx)?;
608 }
609 Statement::Rule { name, quant, body } => {
610 self.add_named(source, name, quant.as_ref(), body, true, ctx)?;
611 }
612 }
613 Ok(())
614 }
615
616 fn intern(&mut self, key: &AtomKey) {
618 if !self.keys.contains(key) {
619 self.keys.insert(key.clone());
620 }
621 }
622
623 fn add_fact(
627 &mut self,
628 source: &str,
629 a: &elenchus_parser::Located<Atom>,
630 value: Value,
631 kind: &'static str,
632 soft: bool,
633 ctx: &DomainCtx,
634 ) -> Result<(), CompileError> {
635 let key = ctx.key(&a.data)?;
636 self.intern(&key);
637 let sig = alloc::format!(
638 "{}|{}|{}|{}",
639 key_sig(&key),
640 matches!(value, Value::True) as u8,
641 kind,
642 "" );
644 if !self.fact_sigs.insert(sig) {
645 return Ok(()); }
647 self.facts.push(RawFact {
648 key,
649 value,
650 origin: Origin {
651 source: source.to_string(),
652 line: a.span.location_line(),
653 premise: None,
654 kind,
655 },
656 soft,
657 });
658 Ok(())
659 }
660
661 fn add_named(
664 &mut self,
665 source: &str,
666 name: &Located<&str>,
667 quant: Option<&Quant>,
668 body: &Body,
669 is_rule: bool,
670 ctx: &DomainCtx,
671 ) -> Result<(), CompileError> {
672 let line = name.span.location_line();
673 let name = name.data;
674 let mut canon = canonical_body(name, body, is_rule, ctx)?;
677 if let Some(q) = quant {
678 canon.push_str(&quant_sig(q));
679 }
680 let body_hash = hash_hex(canon.as_bytes());
681 let key = (source.to_string(), name.to_string());
682 match self.defined.get(&key) {
683 Some(prev) if *prev == body_hash => return Ok(()), Some(_) => {
685 return Err(CompileError::PremiseRedefinition {
687 name: name.to_string(),
688 });
689 }
690 None => {
691 self.defined.insert(key, body_hash);
692 }
693 }
694
695 match quant {
696 None => self.emit_named(source, name, line, body, is_rule, ctx),
698 Some(Quant::InSet { binder, set }) => {
703 let elements = match self.sets.get(set.data) {
704 Some(els) => els.clone(),
705 None => {
706 return Err(CompileError::UnknownSet {
707 file: source.to_string(),
708 line: set.span.location_line(),
709 set: set.data.to_string(),
710 suggestion: nearest_set_suggestion(set.data, &self.sets),
711 });
712 }
713 };
714 for el in &elements {
715 let grounded = subst_body(body, &[(binder.data, el)]);
716 self.emit_named(source, name, line, &grounded, is_rule, ctx)?;
717 }
718 Ok(())
719 }
720 Some(Quant::Relation {
725 left,
726 predicate,
727 right,
728 }) => {
729 let pairs = self
730 .relations
731 .get(predicate.data)
732 .cloned()
733 .unwrap_or_default();
734 for (subj, obj) in &pairs {
735 let grounded = subst_body(body, &[(left.data, subj), (right.data, obj)]);
736 self.emit_named(source, name, line, &grounded, is_rule, ctx)?;
737 if let Ok(k) = ctx.key(&Atom {
740 domain: None,
741 subject: subj,
742 predicate: predicate.data,
743 object: Some(obj),
744 }) {
745 self.relation_consumed.insert(k);
746 }
747 }
748 Ok(())
749 }
750 }
751 }
752
753 fn emit_named(
757 &mut self,
758 source: &str,
759 name: &str,
760 line: u32,
761 body: &Body,
762 is_rule: bool,
763 ctx: &DomainCtx,
764 ) -> Result<(), CompileError> {
765 if is_rule {
766 if let Body::Impl {
768 antecedent,
769 ante_conn,
770 consequent,
771 cons_conn,
772 } = body
773 {
774 if *cons_conn == Conn::Or {
777 return Err(CompileError::RuleDisjunctiveConsequent {
778 name: name.to_string(),
779 });
780 }
781 let (ante, cons) = (raw_lits(antecedent, ctx)?, raw_lits(consequent, ctx)?);
782 for l in ante.iter().chain(cons.iter()) {
783 self.intern(&l.key);
784 }
785 let origin = self.origin(source, line, Some(name), kw::RULE);
786 match ante_conn {
787 Conn::And => self.rules.push(RawRule {
789 antecedent: ante,
790 consequent: cons,
791 origin,
792 }),
793 Conn::Or => {
795 for a in &ante {
796 self.rules.push(RawRule {
797 antecedent: vec![a.clone()],
798 consequent: cons.clone(),
799 origin: origin.clone(),
800 });
801 }
802 }
803 }
804 }
805 return Ok(());
806 }
807
808 match body {
809 Body::List { op, atoms } => {
810 let keys: Vec<AtomKey> = atoms
811 .iter()
812 .map(|a| ctx.key(&a.data))
813 .collect::<Result<_, _>>()?;
814 for k in &keys {
815 self.intern(k);
816 }
817 let kind = list_kind(*op);
818 let origin = self.origin(source, line, Some(name), kind);
819 match op {
820 ListOp::Exclusive | ListOp::Forbids => {
822 self.emit_pairwise(&keys, &origin);
823 }
824 ListOp::OneOf => {
829 self.emit_pairwise(&keys, &origin);
830 self.emit_at_least_one(&keys, &origin);
831 for k in &keys {
832 if let Some(obj) = &k.object {
833 self.oneof_values
834 .entry((
835 k.domain.clone(),
836 k.subject.clone(),
837 k.predicate.clone(),
838 ))
839 .or_default()
840 .insert(obj.clone());
841 }
842 }
843 }
844 ListOp::AtLeast => {
846 self.emit_at_least_one(&keys, &origin);
847 }
848 }
849 }
850 Body::Impl {
851 antecedent,
852 ante_conn,
853 consequent,
854 cons_conn,
855 } => {
856 let ante = raw_lits(antecedent, ctx)?;
864 let cons = raw_lits(consequent, ctx)?;
865 for l in ante.iter().chain(cons.iter()) {
866 self.intern(&l.key);
867 }
868 let origin = self.origin(source, line, Some(name), kw::PREMISE);
869
870 let ante_groups: Vec<Vec<RawLit>> = match ante_conn {
871 Conn::And => vec![ante.clone()],
872 Conn::Or => ante.iter().map(|l| vec![l.clone()]).collect(),
873 };
874 let cons_groups: Vec<Vec<RawLit>> = match cons_conn {
875 Conn::And => cons.iter().map(|l| vec![l.clone()]).collect(),
876 Conn::Or => vec![cons.clone()],
877 };
878 for ag in &ante_groups {
879 for cg in &cons_groups {
880 let mut lits = ag.clone();
881 for c in cg {
882 lits.push(RawLit {
883 key: c.key.clone(),
884 negated: !c.negated,
885 });
886 }
887 self.push_clause(lits, origin.clone());
888 }
889 }
890 }
891 }
892 Ok(())
893 }
894
895 fn emit_pairwise(&mut self, keys: &[AtomKey], origin: &Origin) {
899 for i in 0..keys.len() {
900 for j in (i + 1)..keys.len() {
901 let lits = vec![
902 RawLit {
903 key: keys[i].clone(),
904 negated: false,
905 },
906 RawLit {
907 key: keys[j].clone(),
908 negated: false,
909 },
910 ];
911 self.push_clause(lits, origin.clone());
912 }
913 }
914 }
915
916 fn emit_at_least_one(&mut self, keys: &[AtomKey], origin: &Origin) {
919 let lits = keys
920 .iter()
921 .map(|k| RawLit {
922 key: k.clone(),
923 negated: true,
924 })
925 .collect();
926 self.push_clause(lits, origin.clone());
927 }
928
929 fn push_clause(&mut self, lits: Vec<RawLit>, origin: Origin) {
932 let sig = clause_sig(&lits);
933 if self.clause_sigs.insert(sig) {
934 self.clauses.push(RawClause { lits, origin });
935 }
936 }
938
939 fn origin(&self, source: &str, line: u32, premise: Option<&str>, kind: &'static str) -> Origin {
941 Origin {
942 source: source.to_string(),
943 line,
944 premise: premise.map(|s| s.to_string()),
945 kind,
946 }
947 }
948
949 fn validate_closed_world(&self) -> Result<(), CompileError> {
957 if self.oneof_values.is_empty() {
958 return Ok(());
959 }
960 let out_of_set = |key: &AtomKey| -> bool {
965 key.object.as_ref().is_some_and(|obj| {
966 self.oneof_values
967 .get(&(
968 key.domain.clone(),
969 key.subject.clone(),
970 key.predicate.clone(),
971 ))
972 .is_some_and(|set| !set.contains(obj))
973 })
974 };
975 let mut offenders: Vec<(&str, u32, &AtomKey)> = Vec::new();
976 for f in &self.facts {
977 if out_of_set(&f.key) {
978 offenders.push((&f.origin.source, f.origin.line, &f.key));
979 }
980 }
981 for c in &self.clauses {
982 for l in &c.lits {
983 if out_of_set(&l.key) {
984 offenders.push((&c.origin.source, c.origin.line, &l.key));
985 }
986 }
987 }
988 for r in &self.rules {
989 for l in r.antecedent.iter().chain(r.consequent.iter()) {
990 if out_of_set(&l.key) {
991 offenders.push((&r.origin.source, r.origin.line, &l.key));
992 }
993 }
994 }
995 let Some(&(source, line, key)) = offenders.iter().min_by(|a, b| {
997 (a.0, a.1, &a.2.subject, &a.2.object).cmp(&(b.0, b.1, &b.2.subject, &b.2.object))
998 }) else {
999 return Ok(());
1000 };
1001 let set = &self.oneof_values[&(
1002 key.domain.clone(),
1003 key.subject.clone(),
1004 key.predicate.clone(),
1005 )];
1006 let declared: Vec<&str> = set.iter().map(|s| s.as_str()).collect(); let value = key.object.clone().unwrap_or_default();
1008 let suggestion = did_you_mean(&value, &declared);
1009 Err(CompileError::UnknownValue(Box::new(UnknownValue {
1010 file: source.to_string(),
1011 line,
1012 subject: key.subject.clone(),
1013 predicate: key.predicate.clone(),
1014 value,
1015 declared: declared.join(", "),
1016 suggestion,
1017 })))
1018 }
1019
1020 pub fn finalize(self) -> Compiled {
1022 let atoms: Vec<AtomKey> = self.keys.into_iter().collect(); let mut id_of: BTreeMap<AtomKey, AtomId> = BTreeMap::new();
1024 for (i, k) in atoms.iter().enumerate() {
1025 id_of.insert(k.clone(), i as AtomId);
1026 }
1027 let lower = |l: &RawLit| Lit {
1028 atom: id_of[&l.key],
1029 negated: l.negated,
1030 };
1031
1032 let facts = self
1033 .facts
1034 .into_iter()
1035 .map(|f| Fact {
1036 atom: id_of[&f.key],
1037 value: f.value,
1038 origin: f.origin,
1039 soft: f.soft,
1040 })
1041 .collect();
1042 let clauses = self
1043 .clauses
1044 .into_iter()
1045 .map(|c| Clause {
1046 lits: c.lits.iter().map(lower).collect(),
1047 origin: c.origin,
1048 })
1049 .collect();
1050 let rules = self
1051 .rules
1052 .into_iter()
1053 .map(|r| Rule {
1054 antecedent: r.antecedent.iter().map(lower).collect(),
1055 consequent: r.consequent.iter().map(lower).collect(),
1056 origin: r.origin,
1057 })
1058 .collect();
1059
1060 let consumed = self
1061 .relation_consumed
1062 .iter()
1063 .filter_map(|k| id_of.get(k).copied())
1064 .collect();
1065
1066 Compiled {
1067 atoms,
1068 facts,
1069 clauses,
1070 rules,
1071 checks: self.checks,
1072 pending_imports: self.pending_imports,
1073 unused_imports: Vec::new(), consumed,
1075 }
1076 }
1077}
1078
1079pub fn compile_source(source: &str, src: &str) -> Result<Compiled, CompileError> {
1082 let mut c = Compiler::new();
1083 c.add_source(source, src)?;
1084 c.validate_closed_world()?;
1085 Ok(c.finalize())
1086}
1087
1088pub trait Resolver {
1094 fn load(&self, path: &str) -> Result<String, CompileError>;
1096
1097 fn resolve(&self, _base: &str, relative: &str) -> String {
1100 relative.to_string()
1101 }
1102}
1103
1104#[derive(Default)]
1107pub struct MemoryResolver {
1108 sources: BTreeMap<String, String>,
1109}
1110
1111impl MemoryResolver {
1112 pub fn new() -> Self {
1114 Self::default()
1115 }
1116
1117 pub fn add(&mut self, path: &str, content: &str) -> &mut Self {
1119 self.sources.insert(path.to_string(), content.to_string());
1120 self
1121 }
1122}
1123
1124impl Resolver for MemoryResolver {
1125 fn load(&self, path: &str) -> Result<String, CompileError> {
1126 self.sources
1127 .get(path)
1128 .cloned()
1129 .ok_or_else(|| CompileError::ImportNotFound(path.to_string()))
1130 }
1131}
1132
1133#[cfg(feature = "std")]
1137pub struct FileResolver;
1138
1139#[cfg(feature = "std")]
1140impl Resolver for FileResolver {
1141 fn load(&self, path: &str) -> Result<String, CompileError> {
1142 std::fs::read_to_string(path)
1143 .map_err(|e| CompileError::ImportNotFound(alloc::format!("{}: {}", path, e)))
1144 }
1145
1146 fn resolve(&self, base: &str, relative: &str) -> String {
1147 use std::path::{Component, Path, PathBuf};
1148 let parent = Path::new(base).parent().unwrap_or_else(|| Path::new("."));
1149 let joined = parent.join(relative);
1150 let mut out = PathBuf::new();
1151 for component in joined.components() {
1152 match component {
1153 Component::ParentDir => {
1154 out.pop();
1155 }
1156 Component::CurDir => {}
1157 c => out.push(c),
1158 }
1159 }
1160 out.to_string_lossy().replace('\\', "/")
1164 }
1165}
1166
1167struct ResolvedFile {
1170 path: String,
1171 content: String,
1172 ctx: DomainCtx,
1173}
1174
1175pub fn compile<R: Resolver>(root: &str, resolver: &R) -> Result<Compiled, CompileError> {
1191 let (files, unused_imports) = resolve_graph(root, resolver)?;
1192 let mut c = Compiler::new();
1193 for file in &files {
1194 c.add_resolved(file)?;
1195 }
1196 c.validate_closed_world()?;
1197 let mut compiled = c.finalize();
1198 compiled.unused_imports = unused_imports;
1199 Ok(compiled)
1200}
1201
1202struct ImportEdge {
1205 alias: Option<String>,
1206 child_path: String,
1207 line: u32,
1208}
1209
1210struct DiscoveredFile {
1215 path: String,
1216 content: String,
1217 domain: String,
1218 edges: Vec<ImportEdge>,
1219 used_prefixes: BTreeSet<Option<String>>,
1220}
1221
1222fn resolve_graph<R: Resolver>(
1230 root: &str,
1231 resolver: &R,
1232) -> Result<(Vec<ResolvedFile>, Vec<UnusedImport>), CompileError> {
1233 enum Step {
1235 Enter(String),
1237 Exit(String),
1239 }
1240
1241 let mut discovered: BTreeMap<String, DiscoveredFile> = BTreeMap::new(); let mut path_hash: BTreeMap<String, String> = BTreeMap::new(); let mut order: Vec<String> = Vec::new(); let mut active: BTreeSet<String> = BTreeSet::new(); let mut work: Vec<Step> = vec![Step::Enter(root.to_string())];
1246
1247 while let Some(step) = work.pop() {
1248 match step {
1249 Step::Exit(hash) => {
1250 active.remove(&hash);
1251 order.push(hash);
1252 }
1253 Step::Enter(path) => {
1254 let content = resolver.load(&path)?;
1255 let hash = hash_hex(content.as_bytes());
1256 path_hash.insert(path.clone(), hash.clone());
1257 if active.contains(&hash) {
1258 return Err(CompileError::CircularImport(path)); }
1260 if discovered.contains_key(&hash) {
1261 continue; }
1263 let program = parse_tagged(&path, &content)?;
1264 let domain = extract_domain(&program, &path)?;
1265 let mut edges = Vec::new();
1266 let mut used_prefixes = BTreeSet::new();
1267 for stmt in &program.statements {
1268 if let Statement::Import { path: p, alias } = stmt {
1269 edges.push(ImportEdge {
1270 alias: alias.as_ref().map(|a| a.data.to_string()),
1271 child_path: resolver.resolve(&path, p.data),
1272 line: p.span.location_line(),
1273 });
1274 } else {
1275 collect_prefixes(stmt, &mut used_prefixes);
1276 }
1277 }
1278 drop(program); active.insert(hash.clone());
1280 work.push(Step::Exit(hash.clone()));
1281 for e in edges.iter().rev() {
1282 work.push(Step::Enter(e.child_path.clone()));
1283 }
1284 discovered.insert(
1285 hash,
1286 DiscoveredFile {
1287 path,
1288 content,
1289 domain,
1290 edges,
1291 used_prefixes,
1292 },
1293 );
1294 }
1295 }
1296 }
1297
1298 let domain_of: BTreeMap<&str, &str> = discovered
1302 .iter()
1303 .map(|(h, f)| (h.as_str(), f.domain.as_str()))
1304 .collect();
1305
1306 let mut out = Vec::with_capacity(order.len());
1307 let mut unused: Vec<UnusedImport> = Vec::new();
1308 for hash in &order {
1309 let file = &discovered[hash];
1310 let mut aliases = BTreeMap::new();
1311 aliases.insert(file.domain.clone(), file.domain.clone());
1312 for edge in &file.edges {
1313 let child_domain = domain_of[path_hash[&edge.child_path].as_str()];
1314 let bind = edge
1315 .alias
1316 .clone()
1317 .unwrap_or_else(|| child_domain.to_string());
1318 match aliases.get(&bind) {
1319 Some(existing) if existing != child_domain => {
1320 return Err(CompileError::DomainAliasClash { alias: bind });
1321 }
1322 _ => {
1323 aliases.insert(bind, child_domain.to_string());
1324 }
1325 }
1326 }
1327
1328 let referenced: BTreeSet<&str> = file
1332 .used_prefixes
1333 .iter()
1334 .filter_map(|p| match p {
1335 None => Some(file.domain.as_str()),
1336 Some(name) => aliases.get(name).map(|d| d.as_str()),
1337 })
1338 .collect();
1339 for edge in &file.edges {
1340 let child_domain = domain_of[path_hash[&edge.child_path].as_str()];
1341 if !referenced.contains(child_domain) {
1342 unused.push(UnusedImport {
1343 file: file.path.clone(),
1344 domain: child_domain.to_string(),
1345 alias: edge.alias.clone(),
1346 line: edge.line,
1347 });
1348 }
1349 }
1350
1351 let ctx = DomainCtx {
1352 current: file.domain.clone(),
1353 aliases,
1354 };
1355 out.push((hash.clone(), ctx));
1356 }
1357 unused.sort();
1358
1359 let files = out
1362 .into_iter()
1363 .map(|(hash, ctx)| {
1364 let file = discovered.remove(&hash).expect("hash was discovered");
1365 ResolvedFile {
1366 path: file.path,
1367 content: file.content,
1368 ctx,
1369 }
1370 })
1371 .collect();
1372 Ok((files, unused))
1373}
1374
1375fn quant_sig(q: &Quant) -> String {
1379 match q {
1380 Quant::InSet { binder, set } => alloc::format!("|FOREACH {} IN {}", binder.data, set.data),
1381 Quant::Relation {
1382 left,
1383 predicate,
1384 right,
1385 } => alloc::format!("|FOREACH {} {} {}", left.data, predicate.data, right.data),
1386 }
1387}
1388
1389fn parse_tagged<'a>(
1393 file: &str,
1394 content: &'a str,
1395) -> Result<elenchus_parser::Program<'a>, CompileError> {
1396 elenchus_parser::parse(content).map_err(|mut diag| {
1397 diag.set_file(file);
1398 CompileError::Parse(diag)
1399 })
1400}
1401
1402fn nearest_set_suggestion(set: &str, sets: &BTreeMap<String, Vec<String>>) -> String {
1405 let names: Vec<&str> = sets.keys().map(String::as_str).collect();
1406 did_you_mean(set, &names)
1407}
1408
1409type Subs<'s> = [(&'s str, &'s str)];
1412
1413fn subst_ident<'s>(s: &'s str, subs: &Subs<'s>) -> &'s str {
1415 subs.iter()
1416 .find_map(|&(b, v)| (s == b).then_some(v))
1417 .unwrap_or(s)
1418}
1419
1420fn subst_atom<'s>(a: &Atom<'s>, subs: &Subs<'s>) -> Atom<'s> {
1422 Atom {
1423 domain: a.domain,
1424 subject: subst_ident(a.subject, subs),
1425 predicate: subst_ident(a.predicate, subs),
1426 object: a.object.map(|o| subst_ident(o, subs)),
1427 }
1428}
1429
1430fn subst_lit<'s>(ll: &Located<'s, Literal<'s>>, subs: &Subs<'s>) -> Located<'s, Literal<'s>> {
1432 Located {
1433 data: Literal {
1434 negated: ll.data.negated,
1435 atom: subst_atom(&ll.data.atom, subs),
1436 },
1437 span: ll.span,
1438 }
1439}
1440
1441fn subst_body<'s>(body: &Body<'s>, subs: &Subs<'s>) -> Body<'s> {
1446 match body {
1447 Body::List { op, atoms } => Body::List {
1448 op: *op,
1449 atoms: atoms
1450 .iter()
1451 .map(|la| Located {
1452 data: subst_atom(&la.data, subs),
1453 span: la.span,
1454 })
1455 .collect(),
1456 },
1457 Body::Impl {
1458 antecedent,
1459 ante_conn,
1460 consequent,
1461 cons_conn,
1462 } => Body::Impl {
1463 antecedent: antecedent.iter().map(|l| subst_lit(l, subs)).collect(),
1464 ante_conn: *ante_conn,
1465 consequent: consequent.iter().map(|l| subst_lit(l, subs)).collect(),
1466 cons_conn: *cons_conn,
1467 },
1468 }
1469}
1470
1471fn collect_prefixes(stmt: &Statement, out: &mut BTreeSet<Option<String>>) {
1474 let mut add = |a: &Atom| {
1475 out.insert(a.domain.map(|d| d.to_string()));
1476 };
1477 match stmt {
1478 Statement::Fact(a) | Statement::Negation(a) => add(&a.data),
1479 Statement::Assume(l) => add(&l.data.atom),
1480 Statement::Premise { body, .. } | Statement::Rule { body, .. } => match body {
1481 Body::List { atoms, .. } => atoms.iter().for_each(|a| add(&a.data)),
1482 Body::Impl {
1483 antecedent,
1484 consequent,
1485 ..
1486 } => antecedent
1487 .iter()
1488 .chain(consequent)
1489 .for_each(|l| add(&l.data.atom)),
1490 },
1491 Statement::Domain(_)
1492 | Statement::Import { .. }
1493 | Statement::Check { .. }
1494 | Statement::Set { .. }
1495 | Statement::Close { .. } => {}
1496 }
1497}
1498
1499fn transitive_closure(pairs: Vec<(String, String)>) -> Vec<(String, String)> {
1503 let mut set: BTreeSet<(String, String)> = pairs.into_iter().collect();
1504 loop {
1505 let mut added: Vec<(String, String)> = Vec::new();
1506 for (a, b) in &set {
1507 for (c, d) in &set {
1508 if b == c {
1509 let p = (a.clone(), d.clone());
1510 if !set.contains(&p) {
1511 added.push(p);
1512 }
1513 }
1514 }
1515 }
1516 if added.is_empty() {
1517 break;
1518 }
1519 set.extend(added);
1520 }
1521 set.into_iter().collect()
1522}
1523
1524fn extract_domain(
1526 program: &elenchus_parser::Program,
1527 source: &str,
1528) -> Result<String, CompileError> {
1529 let mut found: Option<String> = None;
1530 for stmt in &program.statements {
1531 if let Statement::Domain(name) = stmt {
1532 if found.is_some() {
1533 return Err(CompileError::DuplicateDomain {
1534 file: source.to_string(),
1535 });
1536 }
1537 found = Some(name.data.to_string());
1538 }
1539 }
1540 found.ok_or_else(|| CompileError::MissingDomain {
1541 file: source.to_string(),
1542 })
1543}
1544
1545pub fn levenshtein(a: &[char], b: &[char]) -> usize {
1552 let mut prev: Vec<usize> = (0..=b.len()).collect();
1553 let mut cur = vec![0usize; b.len() + 1];
1554 for (i, &ca) in a.iter().enumerate() {
1555 cur[0] = i + 1;
1556 for (j, &cb) in b.iter().enumerate() {
1557 let cost = usize::from(ca != cb);
1558 cur[j + 1] = (prev[j + 1] + 1).min(cur[j] + 1).min(prev[j] + cost);
1559 }
1560 core::mem::swap(&mut prev, &mut cur);
1561 }
1562 prev[b.len()]
1563}
1564
1565fn nearest<'a>(word: &str, candidates: &[&'a str]) -> Option<&'a str> {
1577 let budget = word.chars().count() / 3;
1578 if budget == 0 {
1579 return None;
1580 }
1581 let w: Vec<char> = word.chars().collect();
1582 candidates
1583 .iter()
1584 .map(|&c| (levenshtein(&w, &c.chars().collect::<Vec<char>>()), c))
1585 .filter(|&(d, _)| d <= budget)
1586 .min_by_key(|&(d, _)| d)
1587 .map(|(_, c)| c)
1588}
1589
1590fn did_you_mean(word: &str, candidates: &[&str]) -> String {
1594 match nearest(word, candidates) {
1595 Some(s) => alloc::format!(" — did you mean `{s}`?"),
1596 None => String::new(),
1597 }
1598}
1599
1600fn raw_lits(
1603 lits: &[elenchus_parser::Located<Literal>],
1604 ctx: &DomainCtx,
1605) -> Result<Vec<RawLit>, CompileError> {
1606 lits.iter()
1607 .map(|l| {
1608 Ok(RawLit {
1609 key: ctx.key(&l.data.atom)?,
1610 negated: l.data.negated,
1611 })
1612 })
1613 .collect()
1614}
1615
1616fn list_kind(op: ListOp) -> &'static str {
1618 match op {
1619 ListOp::Exclusive => kw::EXCLUSIVE,
1620 ListOp::Forbids => kw::FORBIDS,
1621 ListOp::OneOf => kw::ONEOF,
1622 ListOp::AtLeast => kw::ATLEAST,
1623 }
1624}
1625
1626fn key_sig(k: &AtomKey) -> String {
1630 alloc::format!(
1631 "{}|{}|{}|{}",
1632 k.domain,
1633 k.subject,
1634 k.predicate,
1635 k.object.as_deref().unwrap_or("")
1636 )
1637}
1638
1639fn clause_sig(lits: &[RawLit]) -> String {
1641 let mut parts: Vec<String> = lits
1642 .iter()
1643 .map(|l| alloc::format!("{}|{}", key_sig(&l.key), l.negated as u8))
1644 .collect();
1645 parts.sort();
1646 parts.dedup();
1647 parts.join(";")
1648}
1649
1650fn canonical_body(
1653 name: &str,
1654 body: &Body,
1655 is_rule: bool,
1656 ctx: &DomainCtx,
1657) -> Result<String, CompileError> {
1658 let mut s = String::new();
1659 let _ = write!(
1660 s,
1661 "{}|{}|",
1662 if is_rule { kw::RULE } else { kw::PREMISE },
1663 name
1664 );
1665 match body {
1666 Body::List { op, atoms } => {
1667 let _ = write!(s, "LIST|{}|", list_kind(*op));
1668 let mut keys: Vec<String> = atoms
1669 .iter()
1670 .map(|a| Ok(key_sig(&ctx.key(&a.data)?)))
1671 .collect::<Result<_, CompileError>>()?;
1672 keys.sort();
1673 s.push_str(&keys.join(";"));
1674 }
1675 Body::Impl {
1676 antecedent,
1677 ante_conn,
1678 consequent,
1679 cons_conn,
1680 } => {
1681 let conn = |c: &Conn| if *c == Conn::Or { "OR" } else { "AND" };
1682 s.push_str("IMPL|ANTE|");
1683 s.push_str(conn(ante_conn));
1684 s.push('|');
1685 s.push_str(&lit_sigs(antecedent, ctx)?);
1686 s.push_str("|CONS|");
1687 s.push_str(conn(cons_conn));
1688 s.push('|');
1689 s.push_str(&lit_sigs(consequent, ctx)?);
1690 }
1691 }
1692 Ok(s)
1693}
1694
1695fn lit_sigs(
1698 lits: &[elenchus_parser::Located<Literal>],
1699 ctx: &DomainCtx,
1700) -> Result<String, CompileError> {
1701 let mut parts: Vec<String> = lits
1702 .iter()
1703 .map(|l| {
1704 Ok(alloc::format!(
1705 "{}|{}",
1706 key_sig(&ctx.key(&l.data.atom)?),
1707 l.data.negated as u8
1708 ))
1709 })
1710 .collect::<Result<_, CompileError>>()?;
1711 parts.sort();
1712 Ok(parts.join(";"))
1713}
1714
1715#[cfg(test)]
1716mod tests {
1717 use super::*;
1718
1719 fn cs(src: &str) -> Result<Compiled, CompileError> {
1722 compile_source("<t>", &alloc::format!("DOMAIN t\n{src}"))
1723 }
1724
1725 fn key(subject: &str, predicate: &str, object: Option<&str>) -> AtomKey {
1727 key_in("t", subject, predicate, object)
1728 }
1729
1730 fn key_in(domain: &str, subject: &str, predicate: &str, object: Option<&str>) -> AtomKey {
1732 AtomKey {
1733 domain: domain.to_string(),
1734 subject: subject.to_string(),
1735 predicate: predicate.to_string(),
1736 object: object.map(|o| o.to_string()),
1737 }
1738 }
1739
1740 fn id(c: &Compiled, k: &AtomKey) -> AtomId {
1741 c.atoms.iter().position(|a| a == k).unwrap() as AtomId
1742 }
1743
1744 #[test]
1745 fn exclusive_unfolds_pairwise() {
1746 let src = r#"
1747 PREMISE e:
1748 EXCLUSIVE
1749 x a
1750 x b
1751 x c
1752 "#;
1753 let c = cs(src).unwrap();
1754 assert_eq!(c.clauses.len(), 3);
1756 for cl in &c.clauses {
1757 assert_eq!(cl.lits.len(), 2);
1758 assert!(cl.lits.iter().all(|l| !l.negated));
1759 }
1760 }
1761
1762 #[test]
1763 fn implication_negates_consequent() {
1764 let src = r#"
1766 PREMISE r:
1767 WHEN x a
1768 THEN x b
1769 "#;
1770 let c = cs(src).unwrap();
1771 assert_eq!(c.clauses.len(), 1);
1772 let cl = &c.clauses[0];
1773 assert_eq!(cl.lits.len(), 2);
1774 let a = id(&c, &key("x", "a", None));
1775 let b = id(&c, &key("x", "b", None));
1776 assert!(cl.lits.contains(&Lit {
1777 atom: a,
1778 negated: false
1779 }));
1780 assert!(cl.lits.contains(&Lit {
1781 atom: b,
1782 negated: true
1783 }));
1784 }
1785
1786 #[test]
1787 fn negated_consequent_flips_to_positive() {
1788 let src = r#"
1790 PREMISE r:
1791 WHEN x a
1792 THEN NOT x b
1793 "#;
1794 let c = cs(src).unwrap();
1795 let b = id(&c, &key("x", "b", None));
1796 assert!(c.clauses[0].lits.contains(&Lit {
1797 atom: b,
1798 negated: false
1799 }));
1800 }
1801
1802 #[test]
1803 fn consequent_or_is_one_clause_with_all_negated() {
1804 let src = r#"
1806 PREMISE r:
1807 WHEN x p
1808 THEN x a
1809 OR x b
1810 "#;
1811 let c = cs(src).unwrap();
1812 assert_eq!(c.clauses.len(), 1);
1813 let cl = &c.clauses[0];
1814 assert_eq!(cl.lits.len(), 3);
1815 let p = id(&c, &key("x", "p", None));
1816 let a = id(&c, &key("x", "a", None));
1817 let b = id(&c, &key("x", "b", None));
1818 assert!(cl.lits.contains(&Lit {
1819 atom: p,
1820 negated: false
1821 }));
1822 assert!(cl.lits.contains(&Lit {
1823 atom: a,
1824 negated: true
1825 }));
1826 assert!(cl.lits.contains(&Lit {
1827 atom: b,
1828 negated: true
1829 }));
1830 }
1831
1832 #[test]
1833 fn antecedent_or_is_one_clause_per_disjunct() {
1834 let src = r#"
1837 PREMISE r:
1838 WHEN x a
1839 OR x b
1840 THEN x c
1841 "#;
1842 let c = cs(src).unwrap();
1843 assert_eq!(c.clauses.len(), 2);
1844 let a = id(&c, &key("x", "a", None));
1845 let b = id(&c, &key("x", "b", None));
1846 let cc = id(&c, &key("x", "c", None));
1847 for cl in &c.clauses {
1849 assert_eq!(cl.lits.len(), 2);
1850 assert!(cl.lits.contains(&Lit {
1851 atom: cc,
1852 negated: true
1853 }));
1854 }
1855 let has = |atom| {
1856 c.clauses.iter().any(|cl| {
1857 cl.lits.contains(&Lit {
1858 atom,
1859 negated: false,
1860 })
1861 })
1862 };
1863 assert!(has(a) && has(b));
1864 }
1865
1866 #[test]
1867 fn antecedent_or_with_consequent_or_distributes() {
1868 let src = r#"
1870 PREMISE r:
1871 WHEN x a
1872 OR x b
1873 THEN x c
1874 OR x d
1875 "#;
1876 let c = cs(src).unwrap();
1877 assert_eq!(c.clauses.len(), 2);
1878 for cl in &c.clauses {
1879 assert_eq!(cl.lits.len(), 3);
1880 }
1881 }
1882
1883 #[test]
1884 fn rule_with_or_antecedent_splits_into_two_rules() {
1885 let src = r#"
1887 RULE r:
1888 WHEN x a
1889 OR x b
1890 THEN x c
1891 "#;
1892 let c = cs(src).unwrap();
1893 assert_eq!(c.rules.len(), 2);
1894 assert!(
1895 c.rules
1896 .iter()
1897 .all(|r| r.antecedent.len() == 1 && r.consequent.len() == 1)
1898 );
1899 }
1900
1901 #[test]
1902 fn rule_with_or_consequent_is_rejected() {
1903 let src = r#"
1905 RULE r:
1906 WHEN x a
1907 THEN x b
1908 OR x c
1909 "#;
1910 let err = cs(src).unwrap_err();
1911 assert!(matches!(
1912 err,
1913 CompileError::RuleDisjunctiveConsequent { .. }
1914 ));
1915 }
1916
1917 #[test]
1918 fn oneof_is_pairwise_plus_at_least_one() {
1919 let src = r#"
1920 PREMISE o:
1921 ONEOF
1922 x a
1923 x b
1924 "#;
1925 let c = cs(src).unwrap();
1926 assert_eq!(c.clauses.len(), 2);
1928 assert!(c.clauses.iter().any(|cl| cl.lits.iter().all(|l| l.negated)));
1930 }
1931
1932 #[test]
1933 fn atleast_is_one_negated_clause() {
1934 let src = r#"
1935 PREMISE a:
1936 ATLEAST
1937 x a
1938 x b
1939 x c
1940 "#;
1941 let c = cs(src).unwrap();
1942 assert_eq!(c.clauses.len(), 1);
1943 assert_eq!(c.clauses[0].lits.len(), 3);
1944 assert!(c.clauses[0].lits.iter().all(|l| l.negated));
1945 }
1946
1947 #[test]
1948 fn rules_are_separate_from_clauses() {
1949 let src = r#"
1950 RULE needs:
1951 WHEN x a
1952 THEN x b
1953 "#;
1954 let c = cs(src).unwrap();
1955 assert_eq!(c.clauses.len(), 0);
1956 assert_eq!(c.rules.len(), 1);
1957 assert_eq!(c.rules[0].antecedent.len(), 1);
1958 assert_eq!(c.rules[0].consequent.len(), 1);
1959 }
1960
1961 #[test]
1962 fn atoms_are_canonically_sorted() {
1963 let src = r#"
1964 FACT z z
1965 FACT a a
1966 FACT m m
1967 "#;
1968 let c = cs(src).unwrap();
1969 let mut sorted = c.atoms.clone();
1970 sorted.sort();
1971 assert_eq!(c.atoms, sorted);
1972 }
1973
1974 #[test]
1975 fn duplicate_premise_is_idempotent() {
1976 let src = r#"
1977 PREMISE e:
1978 EXCLUSIVE
1979 x a
1980 x b
1981 PREMISE e:
1982 EXCLUSIVE
1983 x a
1984 x b
1985 "#;
1986 let c = cs(src).unwrap();
1987 assert_eq!(c.clauses.len(), 1);
1988 }
1989
1990 #[test]
1991 fn redefinition_with_different_body_errors() {
1992 let src = r#"
1993 PREMISE e:
1994 EXCLUSIVE
1995 x a
1996 x b
1997 PREMISE e:
1998 EXCLUSIVE
1999 x a
2000 x c
2001 "#;
2002 let err = cs(src).unwrap_err();
2003 assert_eq!(
2004 err,
2005 CompileError::PremiseRedefinition {
2006 name: "e".to_string()
2007 }
2008 );
2009 }
2010
2011 #[test]
2012 fn duplicate_fact_is_idempotent() {
2013 let c = cs("FACT x a\nFACT x a\n").unwrap();
2014 assert_eq!(c.facts.len(), 1);
2015 }
2016
2017 #[test]
2018 fn conflicting_facts_are_both_kept() {
2019 let c = cs("FACT x a\nNOT x a\n").unwrap();
2021 assert_eq!(c.facts.len(), 2);
2022 }
2023
2024 #[test]
2025 fn import_is_recorded_pending() {
2026 let c = cs("IMPORT \"physics.vrf\"\nFACT x a\n").unwrap();
2027 assert_eq!(c.pending_imports, vec!["physics.vrf".to_string()]);
2028 }
2029
2030 #[test]
2031 fn qualified_fact_lands_in_the_imported_domain() {
2032 let mut r = MemoryResolver::new();
2035 r.add(
2036 "lib.vrf",
2037 r#"
2038 DOMAIN physics
2039 PREMISE needs_fuel:
2040 WHEN Engine_X has engine
2041 THEN Engine_X has fuel
2042 "#,
2043 );
2044 r.add(
2045 "main.vrf",
2046 r#"
2047 DOMAIN main
2048 IMPORT "lib.vrf"
2049 FACT physics.Engine_X has engine
2050 FACT physics.Engine_X has fuel
2051 "#,
2052 );
2053 let c = compile("main.vrf", &r).unwrap();
2054 assert!(c.pending_imports.is_empty());
2055 assert_eq!(c.clauses.len(), 1); assert_eq!(c.facts.len(), 2);
2057
2058 let fuel = key_in("physics", "Engine_X", "has", Some("fuel"));
2060 let fuel_id = id(&c, &fuel);
2061 assert!(c.facts.iter().any(|f| f.atom == fuel_id));
2062 assert!(c.clauses[0].lits.iter().any(|l| l.atom == fuel_id));
2063 }
2064
2065 #[test]
2066 fn same_triple_in_different_domains_does_not_unify() {
2067 let mut r = MemoryResolver::new();
2070 r.add("lib.vrf", "DOMAIN physics\nFACT Engine_X has fuel\n");
2071 r.add(
2072 "main.vrf",
2073 "DOMAIN main\nIMPORT \"lib.vrf\"\nFACT Engine_X has fuel\n",
2074 );
2075 let c = compile("main.vrf", &r).unwrap();
2076 assert!(c.atoms.iter().any(|a| a.domain == "physics"));
2078 assert!(c.atoms.iter().any(|a| a.domain == "main"));
2079 assert_eq!(
2080 c.atoms
2081 .iter()
2082 .filter(|a| a.subject == "Engine_X" && a.predicate == "has")
2083 .count(),
2084 2
2085 );
2086 }
2087
2088 #[test]
2089 fn import_alias_binds_a_local_domain_name() {
2090 let mut r = MemoryResolver::new();
2092 r.add("lib.vrf", "DOMAIN physics\nFACT Motor over_200\n");
2093 r.add(
2094 "main.vrf",
2095 "DOMAIN main\nIMPORT \"lib.vrf\" AS phys\nFACT phys.Motor over_100\n",
2096 );
2097 let c = compile("main.vrf", &r).unwrap();
2098 assert_eq!(c.atoms.iter().filter(|a| a.domain == "physics").count(), 2);
2100 }
2101
2102 #[test]
2103 fn unknown_domain_reference_errors() {
2104 let err = cs("FACT ghost.x a\n").unwrap_err();
2106 assert!(matches!(err, CompileError::UnknownDomain { .. }));
2107 }
2108
2109 #[test]
2110 fn imports_are_not_transitive_for_naming() {
2111 let mut r = MemoryResolver::new();
2113 r.add("math.vrf", "DOMAIN math\nFACT foo bar\n");
2114 r.add(
2115 "physics.vrf",
2116 "DOMAIN physics\nIMPORT \"math.vrf\"\nFACT Motor over_100\n",
2117 );
2118 r.add(
2119 "main.vrf",
2120 "DOMAIN main\nIMPORT \"physics.vrf\"\nFACT math.foo bar\n",
2121 );
2122 let err = compile("main.vrf", &r).unwrap_err();
2123 assert!(matches!(err, CompileError::UnknownDomain { .. }));
2124 }
2125
2126 #[test]
2127 fn transitive_dependency_clauses_still_load() {
2128 let mut r = MemoryResolver::new();
2130 r.add(
2131 "math.vrf",
2132 r"
2133 DOMAIN math
2134 PREMISE e:
2135 EXCLUSIVE
2136 x a
2137 x b
2138 ",
2139 );
2140 r.add("physics.vrf", "DOMAIN physics\nIMPORT \"math.vrf\"\n");
2141 r.add("main.vrf", "DOMAIN main\nIMPORT \"physics.vrf\"\n");
2142 let c = compile("main.vrf", &r).unwrap();
2143 assert_eq!(c.clauses.len(), 1); assert!(c.clauses.iter().all(|cl| cl.origin.source == "math.vrf"));
2145 }
2146
2147 #[test]
2148 fn missing_domain_errors() {
2149 let err = compile_source("nodomain.vrf", "FACT x a\n").unwrap_err();
2150 assert!(matches!(err, CompileError::MissingDomain { .. }));
2151 }
2152
2153 #[test]
2154 fn duplicate_domain_errors() {
2155 let err = compile_source("dup.vrf", "DOMAIN a\nDOMAIN b\nFACT x a\n").unwrap_err();
2156 assert!(matches!(err, CompileError::DuplicateDomain { .. }));
2157 }
2158
2159 #[test]
2160 fn alias_clash_when_one_local_name_binds_two_domains() {
2161 let mut r = MemoryResolver::new();
2164 r.add("a.vrf", "DOMAIN physics\nFACT Motor over_100\n");
2165 r.add("b.vrf", "DOMAIN chemistry\nFACT atom reacts\n");
2166 r.add(
2167 "main.vrf",
2168 "DOMAIN main\nIMPORT \"a.vrf\" AS x\nIMPORT \"b.vrf\" AS x\n",
2169 );
2170 let err = compile("main.vrf", &r).unwrap_err();
2171 assert!(matches!(err, CompileError::DomainAliasClash { .. }));
2172 }
2173
2174 #[test]
2175 fn two_files_with_the_same_domain_name_merge() {
2176 let mut r = MemoryResolver::new();
2179 r.add("a.vrf", "DOMAIN physics\nFACT Motor over_100\n");
2180 r.add(
2181 "main.vrf",
2182 "DOMAIN physics\nIMPORT \"a.vrf\"\nFACT Motor over_200\n",
2183 );
2184 let c = compile("main.vrf", &r).unwrap();
2185 assert!(c.atoms.iter().all(|a| a.domain == "physics"));
2187 assert_eq!(c.atoms.len(), 2);
2188 }
2189
2190 #[test]
2191 fn diamond_import_is_deduped() {
2192 let mut r = MemoryResolver::new();
2194 r.add(
2195 "base.vrf",
2196 r#"
2197 DOMAIN base
2198 PREMISE b:
2199 EXCLUSIVE
2200 x a
2201 x b
2202 "#,
2203 );
2204 r.add("a.vrf", "DOMAIN a\nIMPORT \"base.vrf\"\n");
2205 r.add("c.vrf", "DOMAIN c\nIMPORT \"base.vrf\"\n");
2206 r.add(
2207 "main.vrf",
2208 "DOMAIN main\nIMPORT \"a.vrf\"\nIMPORT \"c.vrf\"\n",
2209 );
2210 let c = compile("main.vrf", &r).unwrap();
2211 assert_eq!(c.clauses.len(), 1); }
2213
2214 #[test]
2215 fn circular_import_errors() {
2216 let mut r = MemoryResolver::new();
2217 r.add("a.vrf", "DOMAIN a\nIMPORT \"b.vrf\"\n");
2218 r.add("b.vrf", "DOMAIN b\nIMPORT \"a.vrf\"\n");
2219 let err = compile("a.vrf", &r).unwrap_err();
2220 assert!(matches!(err, CompileError::CircularImport(_)));
2221 }
2222
2223 #[test]
2224 fn three_node_cycle_errors() {
2225 let mut r = MemoryResolver::new();
2227 r.add("a.vrf", "DOMAIN a\nIMPORT \"b.vrf\"\n");
2228 r.add("b.vrf", "DOMAIN b\nIMPORT \"c.vrf\"\n");
2229 r.add("c.vrf", "DOMAIN c\nIMPORT \"a.vrf\"\n");
2230 let err = compile("a.vrf", &r).unwrap_err();
2231 assert!(matches!(err, CompileError::CircularImport(_)));
2232 }
2233
2234 #[test]
2235 fn shared_grandchild_diamond_loads_once() {
2236 let mut r = MemoryResolver::new();
2239 r.add(
2240 "b.vrf",
2241 r"
2242 DOMAIN b
2243 PREMISE e:
2244 EXCLUSIVE
2245 x a
2246 x b
2247 ",
2248 );
2249 r.add("c.vrf", "DOMAIN c\nIMPORT \"b.vrf\"\n");
2250 r.add("a.vrf", "DOMAIN a\nIMPORT \"b.vrf\"\nIMPORT \"c.vrf\"\n");
2251 let c = compile("a.vrf", &r).unwrap();
2252 assert_eq!(
2253 c.clauses.len(),
2254 1,
2255 "b.vrf's clause must appear exactly once"
2256 );
2257 }
2258
2259 #[test]
2260 fn exponential_fan_out_is_memoized_not_blown_up() {
2261 let mut r = MemoryResolver::new();
2265 r.add("f0.vrf", "DOMAIN d0\nFACT x a\n");
2266 let n = 40;
2267 for k in 1..=n {
2268 r.add(
2269 &alloc::format!("f{k}.vrf"),
2270 &alloc::format!(
2271 "DOMAIN d{k}\nIMPORT \"f{}.vrf\"\nIMPORT \"f{}.vrf\"\n",
2272 k - 1,
2273 k - 1
2274 ),
2275 );
2276 }
2277 let c = compile(&alloc::format!("f{n}.vrf"), &r).unwrap();
2278 assert_eq!(c.facts.len(), 1); }
2280
2281 #[test]
2282 fn very_deep_linear_chain_does_not_overflow() {
2283 let mut r = MemoryResolver::new();
2286 r.add("f0.vrf", "DOMAIN d0\nFACT x a\n");
2287 let n = 10_000;
2288 for k in 1..=n {
2289 r.add(
2290 &alloc::format!("f{k}.vrf"),
2291 &alloc::format!("DOMAIN d{k}\nIMPORT \"f{}.vrf\"\n", k - 1),
2292 );
2293 }
2294 let c = compile(&alloc::format!("f{n}.vrf"), &r).unwrap();
2295 assert_eq!(c.facts.len(), 1);
2296 }
2297
2298 #[test]
2299 fn missing_import_errors() {
2300 let mut r = MemoryResolver::new();
2301 r.add("main.vrf", "DOMAIN main\nIMPORT \"ghost.vrf\"\n");
2302 let err = compile("main.vrf", &r).unwrap_err();
2303 assert!(matches!(err, CompileError::ImportNotFound(_)));
2304 }
2305
2306 #[test]
2307 fn unused_import_is_flagged() {
2308 let mut r = MemoryResolver::new();
2310 r.add("physics.vrf", "DOMAIN physics\nFACT Motor over_100\n");
2311 r.add(
2312 "main.vrf",
2313 "DOMAIN main\nIMPORT \"physics.vrf\"\nFACT x a\n",
2314 );
2315 let c = compile("main.vrf", &r).unwrap();
2316 assert_eq!(c.unused_imports.len(), 1);
2317 assert_eq!(c.unused_imports[0].domain, "physics");
2318 assert_eq!(c.unused_imports[0].file, "main.vrf");
2319 assert_eq!(c.unused_imports[0].alias, None);
2320 }
2321
2322 #[test]
2323 fn referenced_import_is_not_unused() {
2324 let mut r = MemoryResolver::new();
2326 r.add("physics.vrf", "DOMAIN physics\nFACT Motor over_100\n");
2327 r.add(
2328 "main.vrf",
2329 "DOMAIN main\nIMPORT \"physics.vrf\"\nFACT physics.Motor over_200\n",
2330 );
2331 let c = compile("main.vrf", &r).unwrap();
2332 assert!(c.unused_imports.is_empty(), "{:?}", c.unused_imports);
2333 }
2334
2335 #[test]
2336 fn unused_import_records_its_alias() {
2337 let mut r = MemoryResolver::new();
2338 r.add("physics.vrf", "DOMAIN physics\nFACT Motor over_100\n");
2339 r.add(
2340 "main.vrf",
2341 "DOMAIN main\nIMPORT \"physics.vrf\" AS phys\nFACT x a\n",
2342 );
2343 let c = compile("main.vrf", &r).unwrap();
2344 assert_eq!(c.unused_imports.len(), 1);
2345 assert_eq!(c.unused_imports[0].alias.as_deref(), Some("phys"));
2346 }
2347
2348 #[test]
2349 fn import_referenced_only_inside_a_premise_is_used() {
2350 let mut r = MemoryResolver::new();
2352 r.add("physics.vrf", "DOMAIN physics\nFACT Motor over_100\n");
2353 r.add(
2354 "main.vrf",
2355 r#"
2356 DOMAIN main
2357 IMPORT "physics.vrf"
2358 PREMISE p:
2359 WHEN physics.Motor over_100
2360 THEN x ok
2361 "#,
2362 );
2363 let c = compile("main.vrf", &r).unwrap();
2364 assert!(c.unused_imports.is_empty(), "{:?}", c.unused_imports);
2365 }
2366
2367 #[test]
2368 fn same_premise_name_across_files_coexists() {
2369 let mut r = MemoryResolver::new();
2373 r.add(
2374 "physics.vrf",
2375 r#"
2376 DOMAIN physics
2377 PREMISE safety:
2378 EXCLUSIVE
2379 x a
2380 x b
2381 "#,
2382 );
2383 r.add(
2384 "main.vrf",
2385 r#"
2386 DOMAIN main
2387 IMPORT "physics.vrf"
2388 PREMISE safety:
2389 EXCLUSIVE
2390 x a
2391 x c
2392 "#,
2393 );
2394 let c = compile("main.vrf", &r).unwrap();
2395 assert_eq!(c.clauses.len(), 2); assert!(c.clauses.iter().any(|cl| cl.origin.source == "physics.vrf"));
2397 assert!(c.clauses.iter().any(|cl| cl.origin.source == "main.vrf"));
2398 }
2399
2400 #[test]
2401 fn redefinition_within_one_source_still_errors() {
2402 let src = r#"
2404 DOMAIN m
2405 PREMISE e:
2406 EXCLUSIVE
2407 x a
2408 x b
2409 PREMISE e:
2410 EXCLUSIVE
2411 x a
2412 x c
2413 "#;
2414 let err = compile_source("main.vrf", src).unwrap_err();
2415 assert_eq!(
2416 err,
2417 CompileError::PremiseRedefinition {
2418 name: "e".to_string()
2419 }
2420 );
2421 }
2422
2423 #[test]
2424 fn import_demo_examples_resolve() {
2425 let mut r = MemoryResolver::new();
2426 r.add(
2427 "physics.vrf",
2428 include_str!("../../../docs/examples/physics.vrf"),
2429 );
2430 r.add(
2431 "import-demo.vrf",
2432 include_str!("../../../docs/examples/import-demo.vrf"),
2433 );
2434 let c = compile("import-demo.vrf", &r).unwrap();
2435 assert!(c.pending_imports.is_empty());
2436 assert_eq!(c.clauses.len(), 2);
2438 let over_100 = id(&c, &key_in("physics", "Motor", "over_100", None));
2440 assert!(c.facts.iter().any(|f| f.atom == over_100));
2441 assert!(
2442 c.clauses
2443 .iter()
2444 .any(|cl| cl.lits.iter().any(|l| l.atom == over_100))
2445 );
2446 }
2447
2448 #[test]
2449 fn creature_example_compiles() {
2450 let src = include_str!("../../../docs/examples/creature.vrf");
2451 let c = compile_source("creature.vrf", src).unwrap();
2452 assert_eq!(c.facts.len(), 2); assert_eq!(c.rules.len(), 1); assert_eq!(c.checks.len(), 1);
2455 assert_eq!(c.clauses.len(), 4);
2457 assert_eq!(c.atoms.len(), 7);
2458 }
2459
2460 #[test]
2461 fn forbids_unfolds_pairwise() {
2462 let src = r#"
2463 PREMISE f:
2464 FORBIDS
2465 x a
2466 x b
2467 x c
2468 "#;
2469 let c = cs(src).unwrap();
2470 assert_eq!(c.clauses.len(), 3); assert!(
2472 c.clauses
2473 .iter()
2474 .all(|cl| cl.lits.len() == 2 && cl.lits.iter().all(|l| !l.negated))
2475 );
2476 }
2477
2478 #[test]
2479 fn rule_with_multiple_consequents() {
2480 let src = r#"
2481 RULE r:
2482 WHEN x a
2483 THEN x b
2484 AND x c
2485 "#;
2486 let c = cs(src).unwrap();
2487 assert_eq!(c.rules.len(), 1);
2488 assert_eq!(c.rules[0].consequent.len(), 2);
2489 }
2490
2491 #[test]
2492 fn negated_antecedent_literal_keeps_polarity() {
2493 let src = r#"
2495 PREMISE a:
2496 WHEN NOT x a
2497 THEN x b
2498 "#;
2499 let c = cs(src).unwrap();
2500 let xa = id(&c, &key("x", "a", None));
2501 assert!(c.clauses[0].lits.contains(&Lit {
2502 atom: xa,
2503 negated: true
2504 }));
2505 }
2506
2507 #[test]
2508 fn rule_keeps_consequent_negation() {
2509 let src = r#"
2510 RULE r:
2511 WHEN x a
2512 THEN NOT x b
2513 "#;
2514 let c = cs(src).unwrap();
2515 assert!(c.rules[0].consequent[0].negated);
2516 }
2517
2518 #[test]
2519 fn compilation_is_deterministic() {
2520 let src = r#"
2521 PREMISE e:
2522 EXCLUSIVE
2523 z z
2524 a a
2525 m m
2526 FACT q q
2527 "#;
2528 assert_eq!(cs(src).unwrap(), cs(src).unwrap());
2529 }
2530
2531 #[test]
2532 fn empty_program_compiles_to_empty_ir() {
2533 let c = cs("// nothing here\n").unwrap();
2534 assert!(c.atoms.is_empty() && c.clauses.is_empty() && c.facts.is_empty());
2535 }
2536
2537 #[test]
2538 fn same_clause_from_two_named_premises_is_deduped() {
2539 let src = r#"
2541 PREMISE e1:
2542 EXCLUSIVE
2543 x a
2544 x b
2545 PREMISE e2:
2546 EXCLUSIVE
2547 x a
2548 x b
2549 "#;
2550 let c = cs(src).unwrap();
2551 assert_eq!(c.clauses.len(), 1);
2552 }
2553
2554 #[test]
2555 fn object_distinguishes_atom_identity() {
2556 let c = cs("FACT x p a\nFACT x p b\n").unwrap();
2558 assert_eq!(c.atoms.len(), 2);
2559 }
2560
2561 const ONEOF_RESOLVED: &str = r"PREMISE pick:
2566 ONEOF
2567 resolved is censored
2568 resolved is censored_mtp
2569 resolved is uncensored
2570";
2571
2572 #[test]
2573 fn value_outside_oneof_is_rejected() {
2574 let src = alloc::format!("{ONEOF_RESOLVED}FACT resolved is censoredmtp\n");
2575 let err = cs(&src).unwrap_err();
2576 let CompileError::UnknownValue(e) = err else {
2577 panic!("expected UnknownValue, got {err:?}");
2578 };
2579 assert_eq!(e.value, "censoredmtp");
2580 assert_eq!(e.subject, "resolved");
2581 assert_eq!(e.predicate, "is");
2582 assert_eq!(e.declared, "censored, censored_mtp, uncensored");
2583 }
2584
2585 #[test]
2586 fn near_miss_value_suggests_the_intended_one() {
2587 let src = alloc::format!("{ONEOF_RESOLVED}FACT resolved is censoredmtp\n");
2588 let CompileError::UnknownValue(e) = cs(&src).unwrap_err() else {
2589 panic!("expected UnknownValue");
2590 };
2591 assert_eq!(e.suggestion, " — did you mean `censored_mtp`?");
2592 }
2593
2594 #[test]
2595 fn far_off_value_offers_no_suggestion() {
2596 let src = alloc::format!("{ONEOF_RESOLVED}FACT resolved is wildly_different\n");
2599 let CompileError::UnknownValue(e) = cs(&src).unwrap_err() else {
2600 panic!("expected UnknownValue");
2601 };
2602 assert_eq!(e.suggestion, "");
2603 }
2604
2605 #[test]
2606 fn declared_value_compiles_cleanly() {
2607 let src = alloc::format!("{ONEOF_RESOLVED}FACT resolved is censored_mtp\n");
2608 assert!(cs(&src).is_ok());
2609 }
2610
2611 #[test]
2612 fn oneof_declared_after_the_reference_still_catches_it() {
2613 let src = alloc::format!("FACT resolved is censoredmtp\n{ONEOF_RESOLVED}");
2615 assert!(matches!(
2616 cs(&src).unwrap_err(),
2617 CompileError::UnknownValue(_)
2618 ));
2619 }
2620
2621 #[test]
2622 fn out_of_set_value_inside_a_premise_is_caught() {
2623 let src = alloc::format!(
2625 r"{ONEOF_RESOLVED}
2626 PREMISE p:
2627 WHEN resolved is censoredmtp
2628 THEN x done
2629 "
2630 );
2631 assert!(matches!(
2632 cs(&src).unwrap_err(),
2633 CompileError::UnknownValue(_)
2634 ));
2635 }
2636
2637 #[test]
2638 fn out_of_set_value_inside_a_rule_is_caught() {
2639 let src = alloc::format!(
2640 r"{ONEOF_RESOLVED}
2641 RULE r:
2642 WHEN x go
2643 THEN resolved is censoredmtp
2644 "
2645 );
2646 assert!(matches!(
2647 cs(&src).unwrap_err(),
2648 CompileError::UnknownValue(_)
2649 ));
2650 }
2651
2652 #[test]
2653 fn binary_atoms_in_a_oneof_do_not_close_anything() {
2654 let src = r"
2657 PREMISE chores:
2658 ONEOF
2659 alice cooks
2660 alice cleans
2661 FACT alice bakes
2662 ";
2663 assert!(cs(src).is_ok());
2664 }
2665
2666 #[test]
2667 fn a_subject_without_a_oneof_stays_open() {
2668 let src = alloc::format!("{ONEOF_RESOLVED}FACT mood is anything_goes\n");
2670 assert!(cs(&src).is_ok());
2671 }
2672
2673 #[test]
2674 fn two_oneofs_union_their_declared_values() {
2675 let src = r"
2677 PREMISE a:
2678 ONEOF
2679 v is one
2680 v is two
2681 PREMISE b:
2682 ONEOF
2683 v is two
2684 v is three
2685 FACT v is three
2686 ";
2687 assert!(cs(src).is_ok());
2688 }
2689
2690 #[test]
2691 fn earliest_offender_is_reported() {
2692 let src = alloc::format!(
2694 "{ONEOF_RESOLVED}FACT resolved is firstbad\nFACT resolved is secondbad\n"
2695 );
2696 let CompileError::UnknownValue(e) = cs(&src).unwrap_err() else {
2697 panic!("expected UnknownValue");
2698 };
2699 assert_eq!(e.value, "firstbad");
2700 }
2701
2702 #[test]
2703 fn closed_world_spans_imported_domains() {
2704 let mut r = MemoryResolver::new();
2707 r.add(
2708 "physics.vrf",
2709 r"
2710 DOMAIN physics
2711 PREMISE g:
2712 ONEOF
2713 Motor speed slow
2714 Motor speed fast
2715 ",
2716 );
2717 r.add(
2718 "main.vrf",
2719 "DOMAIN main\nIMPORT \"physics.vrf\"\nFACT physics.Motor speed faast\n",
2720 );
2721 let CompileError::UnknownValue(e) = compile("main.vrf", &r).unwrap_err() else {
2722 panic!("expected UnknownValue");
2723 };
2724 assert_eq!(e.value, "faast");
2725 assert_eq!(e.suggestion, " — did you mean `fast`?");
2726 }
2727
2728 #[test]
2729 fn same_value_in_a_different_domain_does_not_clash() {
2730 let mut r = MemoryResolver::new();
2733 r.add(
2734 "a.vrf",
2735 r"
2736 DOMAIN a
2737 PREMISE s:
2738 ONEOF
2739 state is open
2740 state is closed
2741 ",
2742 );
2743 r.add("b.vrf", "DOMAIN b\nIMPORT \"a.vrf\"\nFACT state is shut\n");
2744 assert!(compile("b.vrf", &r).is_ok());
2746 }
2747
2748 #[test]
2749 fn levenshtein_basics() {
2750 fn lev(a: &str, b: &str) -> usize {
2753 levenshtein(
2754 &a.chars().collect::<Vec<char>>(),
2755 &b.chars().collect::<Vec<char>>(),
2756 )
2757 }
2758 assert_eq!(lev("", ""), 0);
2759 assert_eq!(lev("abc", "abc"), 0);
2760 assert_eq!(lev("censoredmtp", "censored_mtp"), 1);
2761 assert_eq!(lev("norml", "normal"), 1);
2762 assert_eq!(lev("kitten", "sitting"), 3);
2763 }
2764
2765 #[test]
2766 fn nearest_respects_the_length_budget() {
2767 let cands = ["censored", "censored_mtp", "uncensored"];
2768 assert_eq!(nearest("censoredmtp", &cands), Some("censored_mtp"));
2769 assert_eq!(nearest("zzz", &cands), None);
2771 }
2772
2773 #[test]
2774 fn nearest_offers_nothing_for_very_short_values() {
2775 assert_eq!(nearest("七", &["一", "二", "三"]), None);
2779 assert_eq!(nearest("us", &["uk", "eu", "jp"]), None);
2780 assert_eq!(nearest("中文字", &["中文学", "日本語"]), Some("中文学"));
2783 }
2784
2785 #[test]
2786 fn short_value_is_still_rejected_just_without_a_guess() {
2787 let src = r"
2791 PREMISE pick:
2792 ONEOF
2793 roll is 一
2794 roll is 二
2795 FACT roll is 七
2796 ";
2797 let CompileError::UnknownValue(e) = cs(src).unwrap_err() else {
2798 panic!("expected UnknownValue");
2799 };
2800 assert_eq!(e.value, "七");
2801 assert_eq!(e.suggestion, "");
2802 }
2803
2804 #[test]
2807 fn for_each_grounds_once_per_element() {
2808 let src = r"
2812 SET xs
2813 a
2814 b
2815 PREMISE slot FOR EACH t IN xs:
2816 ONEOF
2817 t slot m
2818 t slot n
2819 ";
2820 let c = cs(src).unwrap();
2821 assert_eq!(c.clauses.len(), 4);
2822 for s in ["a", "b"] {
2823 for o in ["m", "n"] {
2824 assert!(c.atoms.contains(&key(s, "slot", Some(o))));
2825 }
2826 }
2827 }
2828
2829 #[test]
2830 fn for_each_in_a_rule_derives_per_element() {
2831 let src = r"
2833 SET xs
2834 a
2835 b
2836 RULE r FOR EACH t IN xs:
2837 WHEN t on
2838 THEN t hot
2839 ";
2840 let c = cs(src).unwrap();
2841 assert_eq!(c.rules.len(), 2);
2842 }
2843
2844 #[test]
2845 fn for_each_over_an_undeclared_set_is_rejected() {
2846 let src = r"
2847 SET tasks
2848 a
2849 PREMISE p FOR EACH t IN taske:
2850 ONEOF
2851 t s x
2852 t s y
2853 ";
2854 let CompileError::UnknownSet {
2855 set, suggestion, ..
2856 } = cs(src).unwrap_err()
2857 else {
2858 panic!("expected UnknownSet");
2859 };
2860 assert_eq!(set, "taske");
2861 assert_eq!(suggestion, " — did you mean `tasks`?");
2862 }
2863
2864 #[test]
2865 fn for_each_closes_each_grounded_variable() {
2866 let src = r"
2869 SET xs
2870 a
2871 b
2872 PREMISE c FOR EACH t IN xs:
2873 ONEOF
2874 t color red
2875 t color blue
2876 FACT a color gren
2877 ";
2878 let CompileError::UnknownValue(e) = cs(src).unwrap_err() else {
2879 panic!("expected UnknownValue from the grounded ONEOF");
2880 };
2881 assert_eq!(e.value, "gren");
2882 assert_eq!(e.subject, "a");
2883 }
2884
2885 #[test]
2886 fn nested_for_each_is_a_parse_error() {
2887 let src = r"
2891 SET xs
2892 a
2893 PREMISE p FOR EACH x IN xs FOR EACH y IN xs:
2894 ONEOF
2895 x r y
2896 x s y
2897 ";
2898 assert!(matches!(cs(src), Err(CompileError::Parse(_))));
2899 }
2900
2901 #[test]
2902 fn relation_for_each_grounds_per_fact_pair() {
2903 let src = r"
2907 FACT a linked b
2908 FACT b linked c
2909 PREMISE p FOR EACH x linked y:
2910 FORBIDS
2911 x hot on
2912 y hot on
2913 ";
2914 let c = cs(src).unwrap();
2915 assert_eq!(c.clauses.len(), 2);
2916 assert_eq!(c.consumed.len(), 2);
2917 assert!(c.consumed.contains(&id(&c, &key("a", "linked", Some("b")))));
2918 }
2919
2920 #[test]
2921 fn relation_for_each_over_no_edges_is_inert() {
2922 let src = r"
2925 PREMISE p FOR EACH x linked y:
2926 FORBIDS
2927 x hot on
2928 y hot on
2929 ";
2930 let c = cs(src).unwrap();
2931 assert_eq!(c.clauses.len(), 0);
2932 assert!(c.consumed.is_empty());
2933 }
2934
2935 #[test]
2936 fn close_transitive_extends_the_relation() {
2937 let src = r"
2940 FACT a r b
2941 FACT b r c
2942 CLOSE r TRANSITIVE
2943 PREMISE p FOR EACH x r y:
2944 FORBIDS
2945 x hot on
2946 y hot on
2947 ";
2948 let c = cs(src).unwrap();
2949 assert_eq!(c.clauses.len(), 3);
2950 }
2951
2952 #[test]
2953 fn close_transitive_rejects_a_cycle() {
2954 let src = r"
2955 FACT a r b
2956 FACT b r a
2957 CLOSE r TRANSITIVE
2958 ";
2959 let CompileError::CyclicRelation { relation, .. } = cs(src).unwrap_err() else {
2960 panic!("expected CyclicRelation");
2961 };
2962 assert_eq!(relation, "r");
2963 }
2964
2965 #[test]
2966 fn grounding_count_is_linear_in_the_set() {
2967 let elems: alloc::string::String = (0..20).map(|i| alloc::format!(" e{i}\n")).collect();
2970 let src = alloc::format!(
2971 "SET xs\n{elems}PREMISE p FOR EACH t IN xs:\n ATLEAST\n t a\n t b\n"
2972 );
2973 let c = cs(&src).unwrap();
2974 assert_eq!(c.clauses.len(), 20);
2975 }
2976}