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: Option<String>,
93 pub object: Option<String>,
96}
97
98struct DomainCtx {
103 current: String,
105 aliases: BTreeMap<String, String>,
108}
109
110impl DomainCtx {
111 fn resolve(&self, prefix: Option<&str>) -> Result<String, CompileError> {
114 match prefix {
115 None => Ok(self.current.clone()),
116 Some(p) => self
117 .aliases
118 .get(p)
119 .cloned()
120 .ok_or_else(|| CompileError::UnknownDomain {
121 domain: p.to_string(),
122 }),
123 }
124 }
125
126 fn key(&self, a: &Atom) -> Result<AtomKey, CompileError> {
129 Ok(AtomKey {
130 domain: self.resolve(a.domain)?,
131 subject: a.subject.to_string(),
132 predicate: a.predicate.map(|p| p.to_string()),
133 object: a.object.map(|o| o.to_string()),
134 })
135 }
136}
137
138#[derive(Debug, Clone, Copy, PartialEq, Eq)]
141pub struct Lit {
142 pub atom: AtomId,
144 pub negated: bool,
146}
147
148#[derive(Debug, Clone, Copy, PartialEq, Eq)]
150pub enum Value {
151 True,
153 False,
155}
156
157#[derive(Debug, Clone, PartialEq, Eq)]
159pub struct Origin {
160 pub source: String,
162 pub line: u32,
164 pub premise: Option<String>,
166 pub kind: &'static str,
170}
171
172pub const KIND_UNSAT: &str = "UNSAT";
177
178#[derive(Debug, Clone, PartialEq, Eq)]
181pub struct Fact {
182 pub atom: AtomId,
184 pub value: Value,
186 pub origin: Origin,
188 pub soft: bool,
193}
194
195#[derive(Debug, Clone, PartialEq, Eq)]
197pub struct Clause {
198 pub lits: Vec<Lit>,
200 pub origin: Origin,
202}
203
204#[derive(Debug, Clone, PartialEq, Eq)]
207pub struct Rule {
208 pub antecedent: Vec<Lit>,
210 pub consequent: Vec<Lit>,
212 pub origin: Origin,
214}
215
216#[derive(Debug, Clone, PartialEq, Eq)]
218pub struct Check {
219 pub subject: Option<String>,
221 pub bidirectional: bool,
223}
224
225#[derive(Debug, Clone, PartialEq, Eq)]
227pub struct Compiled {
228 pub atoms: Vec<AtomKey>,
230 pub facts: Vec<Fact>,
232 pub clauses: Vec<Clause>,
234 pub rules: Vec<Rule>,
236 pub checks: Vec<Check>,
238 pub pending_imports: Vec<String>,
241 pub unused_imports: Vec<UnusedImport>,
246 pub consumed: Vec<AtomId>,
250 pub placeholders: Vec<PlaceholderInfo>,
255}
256
257#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
262pub struct UnusedImport {
263 pub file: String,
265 pub domain: String,
267 pub alias: Option<String>,
269 pub line: u32,
271}
272
273#[derive(Debug, Clone, PartialEq, Eq)]
277pub struct PortBinding {
278 pub value: bool,
280 pub origin: String,
282}
283
284#[derive(Debug, Clone, Copy, PartialEq, Eq)]
288pub enum PlaceholderStatus {
289 Supplied,
291 DefaultUsed,
293 Unset,
295}
296
297#[derive(Debug, Clone, PartialEq, Eq)]
300pub struct PlaceholderInfo {
301 pub key: String,
303 pub status: PlaceholderStatus,
305 pub value: Option<bool>,
307 pub origin: Option<String>,
309}
310
311#[derive(Debug, Error, PartialEq, Eq)]
313pub enum CompileError {
314 #[error("{0}")]
318 Parse(elenchus_parser::Diagnostics),
319 #[error("'{name}' redefined with a different body")]
321 PremiseRedefinition {
322 name: String,
324 },
325 #[error("{file}: missing a DOMAIN declaration (every file must start with `DOMAIN <name>`)")]
328 MissingDomain {
329 file: String,
331 },
332 #[error("{file}: more than one DOMAIN declaration (a file has exactly one domain)")]
334 DuplicateDomain {
335 file: String,
337 },
338 #[error("unknown domain '{domain}' — declare it with DOMAIN, or IMPORT it in this file")]
341 UnknownDomain {
342 domain: String,
344 },
345 #[error("domain name '{alias}' is bound to two different imports (disambiguate with AS)")]
348 DomainAliasClash {
349 alias: String,
351 },
352 #[error("import not found: {0}")]
354 ImportNotFound(String),
355 #[error("circular import: {0}")]
357 CircularImport(String),
358 #[error("rule '{name}' cannot derive a disjunction (OR in THEN); use a PREMISE instead")]
362 RuleDisjunctiveConsequent {
363 name: String,
365 },
366 #[error(transparent)]
372 UnknownValue(Box<UnknownValue>),
373 #[error("{file}:{line}: FOR EACH ranges over '{set}', which is not a declared SET{suggestion}")]
377 UnknownSet {
378 file: String,
380 line: u32,
382 set: String,
384 suggestion: String,
386 },
387 #[error(
390 "{file}:{line}: relation '{relation}' has a cycle (`{node}` reaches itself) \
391 — CLOSE … TRANSITIVE requires a DAG"
392 )]
393 CyclicRelation {
394 file: String,
396 line: u32,
398 relation: String,
400 node: String,
402 },
403 #[error(
407 "{file}:{line}: '{name}' is a bare proposition but no VAR declares it \
408 — add `VAR {name}`{suggestion}"
409 )]
410 UndeclaredPort {
411 file: String,
413 line: u32,
415 name: String,
417 suggestion: String,
419 },
420 #[error("no VAR declares the port '{name}' that an external value sets{suggestion}")]
424 UnknownPort {
425 name: String,
427 suggestion: String,
429 },
430 #[error(
434 "'{name}' is declared in multiple domains ({domains}); qualify which one as `<domain>.{name}`"
435 )]
436 AmbiguousPort {
437 name: String,
439 domains: String,
441 },
442 #[error(
447 "no atom '{name}' is used in the program, so an external value cannot set it \
448 (use it in a FACT/PREMISE/RULE, or fix a typo)"
449 )]
450 UnknownExternalAtom {
451 name: String,
453 },
454 #[error(
457 "the port '{name}' is set to two different values: {a_value} (from {a_origin}) \
458 and {b_value} (from {b_origin})"
459 )]
460 PortConflict {
461 name: String,
463 a_value: bool,
465 a_origin: String,
467 b_value: bool,
469 b_origin: String,
471 },
472 #[error("{file}:{line}: a data file may only contain PROVIDE (and DOMAIN), not this statement")]
475 DataFileStatement {
476 file: String,
478 line: u32,
480 },
481}
482
483#[derive(Debug, Error, PartialEq, Eq)]
486#[error(
487 "{file}:{line}: '{value}' is not a declared value of '{subject} {predicate}' \
488 — ONEOF declares {{ {declared} }}{suggestion}"
489)]
490pub struct UnknownValue {
491 pub file: String,
493 pub line: u32,
495 pub subject: String,
497 pub predicate: String,
499 pub value: String,
501 pub declared: String,
503 pub suggestion: String,
505}
506
507#[derive(Clone)]
515struct RawLit {
516 key: AtomKey,
517 negated: bool,
518}
519
520struct RawFact {
522 key: AtomKey,
523 value: Value,
524 origin: Origin,
525 soft: bool,
526}
527
528struct RawClause {
530 lits: Vec<RawLit>,
531 origin: Origin,
532}
533
534struct RawRule {
536 antecedent: Vec<RawLit>,
537 consequent: Vec<RawLit>,
538 origin: Origin,
539}
540
541struct PortDecl {
545 default: Option<bool>,
546 source: String,
547 line: u32,
548}
549
550#[derive(Debug, Clone, PartialEq, Eq)]
557struct PortRef {
558 domain: Option<String>,
559 subject: String,
560 predicate: Option<String>,
561 object: Option<String>,
562}
563
564impl PortRef {
565 fn label(&self) -> String {
568 let mut s = String::new();
569 if let Some(d) = &self.domain {
570 s.push_str(d);
571 s.push('.');
572 }
573 s.push_str(&self.subject);
574 if let Some(p) = &self.predicate {
575 s.push(' ');
576 s.push_str(p);
577 }
578 if let Some(o) = &self.object {
579 s.push(' ');
580 s.push_str(o);
581 }
582 s
583 }
584}
585
586fn parse_port_ref(key: &str) -> PortRef {
592 let mut words = key.split_whitespace();
593 let first = words.next().unwrap_or("");
594 let (domain, subject) = match first.split_once('.') {
595 Some((d, s)) => (Some(d.to_string()), s.to_string()),
596 None => (None, first.to_string()),
597 };
598 PortRef {
599 domain,
600 subject,
601 predicate: words.next().map(str::to_string),
602 object: words.next().map(str::to_string),
603 }
604}
605
606fn atomkey_label(k: &AtomKey) -> String {
609 let mut s = alloc::format!("{}.{}", k.domain, k.subject);
610 if let Some(p) = &k.predicate {
611 s.push(' ');
612 s.push_str(p);
613 }
614 if let Some(o) = &k.object {
615 s.push(' ');
616 s.push_str(o);
617 }
618 s
619}
620
621#[derive(Default)]
625pub struct Compiler {
626 keys: BTreeSet<AtomKey>,
627 facts: Vec<RawFact>,
628 clauses: Vec<RawClause>,
629 rules: Vec<RawRule>,
630 checks: Vec<Check>,
631 pending_imports: Vec<String>,
632 defined: BTreeMap<(String, String), String>,
637 clause_sigs: BTreeSet<String>,
639 fact_sigs: BTreeSet<String>,
641 oneof_values: BTreeMap<(String, String, String), BTreeSet<String>>,
648 sets: BTreeMap<String, Vec<String>>,
653 relations: BTreeMap<String, Vec<(String, String)>>,
657 relation_consumed: BTreeSet<AtomKey>,
662 ports: BTreeMap<(String, String), PortDecl>,
667 provides: Vec<(PortRef, PortBinding)>,
672}
673
674impl Compiler {
675 pub fn new() -> Self {
677 Self::default()
678 }
679
680 pub fn add_source(&mut self, source: &str, src: &str) -> Result<(), CompileError> {
686 let program = parse_tagged(source, src)?;
687 let domain = extract_domain(&program, source)?;
688 let mut aliases = BTreeMap::new();
689 aliases.insert(domain.clone(), domain.clone());
690 let ctx = DomainCtx {
691 current: domain,
692 aliases,
693 };
694 self.collect_decls(&program);
695 self.apply_closures(&program, source)?;
696 for stmt in &program.statements {
697 match stmt {
698 Statement::Domain(_) => {}
699 Statement::Import { path, .. } => {
700 self.pending_imports.push(path.data.to_string());
701 }
702 other => self.add_statement(source, other, &ctx)?,
703 }
704 }
705 Ok(())
706 }
707
708 fn apply_closures(
714 &mut self,
715 program: &elenchus_parser::Program,
716 source: &str,
717 ) -> Result<(), CompileError> {
718 for stmt in &program.statements {
719 if let Statement::Close { relation, kind } = stmt {
720 let CloseKind::Transitive = kind;
721 let pairs = self
722 .relations
723 .get(relation.data)
724 .cloned()
725 .unwrap_or_default();
726 let closed = transitive_closure(pairs);
727 if let Some((node, _)) = closed.iter().find(|(a, b)| a == b) {
728 return Err(CompileError::CyclicRelation {
729 file: source.to_string(),
730 line: relation.span.location_line(),
731 relation: relation.data.to_string(),
732 node: node.clone(),
733 });
734 }
735 self.relations.insert(relation.data.to_string(), closed);
736 }
737 }
738 Ok(())
739 }
740
741 fn collect_decls(&mut self, program: &elenchus_parser::Program) {
745 for stmt in &program.statements {
746 match stmt {
747 Statement::Set { name, elements } => {
748 self.sets.insert(
749 name.data.to_string(),
750 elements.iter().map(|e| e.data.to_string()).collect(),
751 );
752 }
753 Statement::Fact(a) => {
754 if let (Some(pred), Some(obj)) = (a.data.predicate, a.data.object) {
758 self.relations
759 .entry(pred.to_string())
760 .or_default()
761 .push((a.data.subject.to_string(), obj.to_string()));
762 }
763 }
764 _ => {}
765 }
766 }
767 }
768
769 fn add_resolved(&mut self, file: &ResolvedFile) -> Result<(), CompileError> {
771 let program = parse_tagged(&file.path, &file.content)?;
772 self.collect_decls(&program);
773 self.apply_closures(&program, &file.path)?;
774 for stmt in &program.statements {
775 match stmt {
776 Statement::Import { .. } | Statement::Domain(_) => {}
777 other => self.add_statement(&file.path, other, &file.ctx)?,
778 }
779 }
780 Ok(())
781 }
782
783 fn add_statement(
786 &mut self,
787 source: &str,
788 stmt: &Statement,
789 ctx: &DomainCtx,
790 ) -> Result<(), CompileError> {
791 match stmt {
792 Statement::Import { .. } | Statement::Domain(_) => {}
794 Statement::Fact(a) => self.add_fact(source, a, Value::True, kw::FACT, false, ctx)?,
795 Statement::Negation(a) => {
796 self.add_fact(source, a, Value::False, kw::NOT, false, ctx)?
797 }
798 Statement::Assume(l) => {
799 let value = if l.data.negated {
800 Value::False
801 } else {
802 Value::True
803 };
804 let located = elenchus_parser::Located {
808 data: l.data.atom.clone(),
809 span: l.span,
810 };
811 self.add_fact(source, &located, value, kw::ASSUME, true, ctx)?;
812 }
813 Statement::Check {
814 subject,
815 bidirectional,
816 } => self.checks.push(Check {
817 subject: subject.as_ref().map(|s| s.data.to_string()),
818 bidirectional: *bidirectional,
819 }),
820 Statement::Set { .. } | Statement::Close { .. } => {}
823 Statement::Var { name, default } => {
827 self.ports
828 .entry((ctx.current.clone(), name.data.to_string()))
829 .or_insert(PortDecl {
830 default: *default,
831 source: source.to_string(),
832 line: name.span.location_line(),
833 });
834 }
835 Statement::Provide { atom, value } => {
840 let a = &atom.data;
841 let domain = match a.domain {
842 None => None,
843 Some(p) => Some(ctx.resolve(Some(p))?),
844 };
845 self.provides.push((
846 PortRef {
847 domain,
848 subject: a.subject.to_string(),
849 predicate: a.predicate.map(|p| p.to_string()),
850 object: a.object.map(|o| o.to_string()),
851 },
852 PortBinding {
853 value: *value,
854 origin: alloc::format!("PROVIDE {source}"),
855 },
856 ));
857 }
858 Statement::Premise { name, quant, body } => {
859 self.add_named(source, name, quant.as_ref(), body, false, ctx)?;
860 }
861 Statement::Rule { name, quant, body } => {
862 self.add_named(source, name, quant.as_ref(), body, true, ctx)?;
863 }
864 }
865 Ok(())
866 }
867
868 fn intern(&mut self, key: &AtomKey) {
870 if !self.keys.contains(key) {
871 self.keys.insert(key.clone());
872 }
873 }
874
875 fn add_fact(
879 &mut self,
880 source: &str,
881 a: &elenchus_parser::Located<Atom>,
882 value: Value,
883 kind: &'static str,
884 soft: bool,
885 ctx: &DomainCtx,
886 ) -> Result<(), CompileError> {
887 let key = ctx.key(&a.data)?;
888 self.intern(&key);
889 let sig = alloc::format!(
890 "{}|{}|{}|{}",
891 key_sig(&key),
892 matches!(value, Value::True) as u8,
893 kind,
894 "" );
896 if !self.fact_sigs.insert(sig) {
897 return Ok(()); }
899 self.facts.push(RawFact {
900 key,
901 value,
902 origin: Origin {
903 source: source.to_string(),
904 line: a.span.location_line(),
905 premise: None,
906 kind,
907 },
908 soft,
909 });
910 Ok(())
911 }
912
913 fn add_named(
916 &mut self,
917 source: &str,
918 name: &Located<&str>,
919 quant: Option<&Quant>,
920 body: &Body,
921 is_rule: bool,
922 ctx: &DomainCtx,
923 ) -> Result<(), CompileError> {
924 let line = name.span.location_line();
925 let name = name.data;
926 let mut canon = canonical_body(name, body, is_rule, ctx)?;
929 if let Some(q) = quant {
930 canon.push_str(&quant_sig(q));
931 }
932 let body_hash = hash_hex(canon.as_bytes());
933 let key = (source.to_string(), name.to_string());
934 match self.defined.get(&key) {
935 Some(prev) if *prev == body_hash => return Ok(()), Some(_) => {
937 return Err(CompileError::PremiseRedefinition {
939 name: name.to_string(),
940 });
941 }
942 None => {
943 self.defined.insert(key, body_hash);
944 }
945 }
946
947 match quant {
948 None => self.emit_named(source, name, line, body, is_rule, ctx),
950 Some(Quant::InSet { binder, set }) => {
955 let elements = match self.sets.get(set.data) {
956 Some(els) => els.clone(),
957 None => {
958 return Err(CompileError::UnknownSet {
959 file: source.to_string(),
960 line: set.span.location_line(),
961 set: set.data.to_string(),
962 suggestion: nearest_set_suggestion(set.data, &self.sets),
963 });
964 }
965 };
966 for el in &elements {
967 let grounded = subst_body(body, &[(binder.data, el)]);
968 self.emit_named(source, name, line, &grounded, is_rule, ctx)?;
969 }
970 Ok(())
971 }
972 Some(Quant::Relation {
977 left,
978 predicate,
979 right,
980 }) => {
981 let pairs = self
982 .relations
983 .get(predicate.data)
984 .cloned()
985 .unwrap_or_default();
986 for (subj, obj) in &pairs {
987 let grounded = subst_body(body, &[(left.data, subj), (right.data, obj)]);
988 self.emit_named(source, name, line, &grounded, is_rule, ctx)?;
989 if let Ok(k) = ctx.key(&Atom {
992 domain: None,
993 subject: subj,
994 predicate: Some(predicate.data),
995 object: Some(obj),
996 }) {
997 self.relation_consumed.insert(k);
998 }
999 }
1000 Ok(())
1001 }
1002 }
1003 }
1004
1005 fn emit_named(
1009 &mut self,
1010 source: &str,
1011 name: &str,
1012 line: u32,
1013 body: &Body,
1014 is_rule: bool,
1015 ctx: &DomainCtx,
1016 ) -> Result<(), CompileError> {
1017 if is_rule {
1018 if let Body::Impl {
1020 antecedent,
1021 ante_conn,
1022 consequent,
1023 cons_conn,
1024 } = body
1025 {
1026 if *cons_conn == Conn::Or {
1029 return Err(CompileError::RuleDisjunctiveConsequent {
1030 name: name.to_string(),
1031 });
1032 }
1033 let (ante, cons) = (raw_lits(antecedent, ctx)?, raw_lits(consequent, ctx)?);
1034 for l in ante.iter().chain(cons.iter()) {
1035 self.intern(&l.key);
1036 }
1037 let origin = self.origin(source, line, Some(name), kw::RULE);
1038 match ante_conn {
1039 Conn::And => self.rules.push(RawRule {
1041 antecedent: ante,
1042 consequent: cons,
1043 origin,
1044 }),
1045 Conn::Or => {
1047 for a in &ante {
1048 self.rules.push(RawRule {
1049 antecedent: vec![a.clone()],
1050 consequent: cons.clone(),
1051 origin: origin.clone(),
1052 });
1053 }
1054 }
1055 }
1056 }
1057 return Ok(());
1058 }
1059
1060 match body {
1061 Body::List { op, atoms } => {
1062 let keys: Vec<AtomKey> = atoms
1063 .iter()
1064 .map(|a| ctx.key(&a.data))
1065 .collect::<Result<_, _>>()?;
1066 for k in &keys {
1067 self.intern(k);
1068 }
1069 let kind = list_kind(*op);
1070 let origin = self.origin(source, line, Some(name), kind);
1071 match op {
1072 ListOp::Exclusive | ListOp::Forbids => {
1074 self.emit_pairwise(&keys, &origin);
1075 }
1076 ListOp::OneOf => {
1081 self.emit_pairwise(&keys, &origin);
1082 self.emit_at_least_one(&keys, &origin);
1083 for k in &keys {
1084 if let (Some(pred), Some(obj)) = (&k.predicate, &k.object) {
1085 self.oneof_values
1086 .entry((k.domain.clone(), k.subject.clone(), pred.clone()))
1087 .or_default()
1088 .insert(obj.clone());
1089 }
1090 }
1091 }
1092 ListOp::AtLeast => {
1094 self.emit_at_least_one(&keys, &origin);
1095 }
1096 }
1097 }
1098 Body::Impl {
1099 antecedent,
1100 ante_conn,
1101 consequent,
1102 cons_conn,
1103 } => {
1104 let ante = raw_lits(antecedent, ctx)?;
1112 let cons = raw_lits(consequent, ctx)?;
1113 for l in ante.iter().chain(cons.iter()) {
1114 self.intern(&l.key);
1115 }
1116 let origin = self.origin(source, line, Some(name), kw::PREMISE);
1117
1118 let ante_groups: Vec<Vec<RawLit>> = match ante_conn {
1119 Conn::And => vec![ante.clone()],
1120 Conn::Or => ante.iter().map(|l| vec![l.clone()]).collect(),
1121 };
1122 let cons_groups: Vec<Vec<RawLit>> = match cons_conn {
1123 Conn::And => cons.iter().map(|l| vec![l.clone()]).collect(),
1124 Conn::Or => vec![cons.clone()],
1125 };
1126 for ag in &ante_groups {
1127 for cg in &cons_groups {
1128 let mut lits = ag.clone();
1129 for c in cg {
1130 lits.push(RawLit {
1131 key: c.key.clone(),
1132 negated: !c.negated,
1133 });
1134 }
1135 self.push_clause(lits, origin.clone());
1136 }
1137 }
1138 }
1139 }
1140 Ok(())
1141 }
1142
1143 fn emit_pairwise(&mut self, keys: &[AtomKey], origin: &Origin) {
1147 for i in 0..keys.len() {
1148 for j in (i + 1)..keys.len() {
1149 let lits = vec![
1150 RawLit {
1151 key: keys[i].clone(),
1152 negated: false,
1153 },
1154 RawLit {
1155 key: keys[j].clone(),
1156 negated: false,
1157 },
1158 ];
1159 self.push_clause(lits, origin.clone());
1160 }
1161 }
1162 }
1163
1164 fn emit_at_least_one(&mut self, keys: &[AtomKey], origin: &Origin) {
1167 let lits = keys
1168 .iter()
1169 .map(|k| RawLit {
1170 key: k.clone(),
1171 negated: true,
1172 })
1173 .collect();
1174 self.push_clause(lits, origin.clone());
1175 }
1176
1177 fn push_clause(&mut self, lits: Vec<RawLit>, origin: Origin) {
1180 let sig = clause_sig(&lits);
1181 if self.clause_sigs.insert(sig) {
1182 self.clauses.push(RawClause { lits, origin });
1183 }
1184 }
1186
1187 fn origin(&self, source: &str, line: u32, premise: Option<&str>, kind: &'static str) -> Origin {
1189 Origin {
1190 source: source.to_string(),
1191 line,
1192 premise: premise.map(|s| s.to_string()),
1193 kind,
1194 }
1195 }
1196
1197 fn validate_closed_world(&self) -> Result<(), CompileError> {
1205 if self.oneof_values.is_empty() {
1206 return Ok(());
1207 }
1208 let out_of_set = |key: &AtomKey| -> bool {
1213 match (key.predicate.as_ref(), key.object.as_ref()) {
1216 (Some(pred), Some(obj)) => self
1217 .oneof_values
1218 .get(&(key.domain.clone(), key.subject.clone(), pred.clone()))
1219 .is_some_and(|set| !set.contains(obj)),
1220 _ => false,
1221 }
1222 };
1223 let mut offenders: Vec<(&str, u32, &AtomKey)> = Vec::new();
1224 for f in &self.facts {
1225 if out_of_set(&f.key) {
1226 offenders.push((&f.origin.source, f.origin.line, &f.key));
1227 }
1228 }
1229 for c in &self.clauses {
1230 for l in &c.lits {
1231 if out_of_set(&l.key) {
1232 offenders.push((&c.origin.source, c.origin.line, &l.key));
1233 }
1234 }
1235 }
1236 for r in &self.rules {
1237 for l in r.antecedent.iter().chain(r.consequent.iter()) {
1238 if out_of_set(&l.key) {
1239 offenders.push((&r.origin.source, r.origin.line, &l.key));
1240 }
1241 }
1242 }
1243 let Some(&(source, line, key)) = offenders.iter().min_by(|a, b| {
1245 (a.0, a.1, &a.2.subject, &a.2.object).cmp(&(b.0, b.1, &b.2.subject, &b.2.object))
1246 }) else {
1247 return Ok(());
1248 };
1249 let pred = key.predicate.clone().unwrap_or_default();
1251 let set = &self.oneof_values[&(key.domain.clone(), key.subject.clone(), pred.clone())];
1252 let declared: Vec<&str> = set.iter().map(|s| s.as_str()).collect(); let value = key.object.clone().unwrap_or_default();
1254 let suggestion = did_you_mean(&value, &declared);
1255 Err(CompileError::UnknownValue(Box::new(UnknownValue {
1256 file: source.to_string(),
1257 line,
1258 subject: key.subject.clone(),
1259 predicate: pred,
1260 value,
1261 declared: declared.join(", "),
1262 suggestion,
1263 })))
1264 }
1265
1266 fn resolve_ports(
1281 &mut self,
1282 inputs: &[(String, PortBinding)],
1283 ) -> Result<Vec<PlaceholderInfo>, CompileError> {
1284 {
1287 let bad = |k: &AtomKey| {
1288 k.predicate.is_none()
1289 && !self
1290 .ports
1291 .contains_key(&(k.domain.clone(), k.subject.clone()))
1292 };
1293 let mut offenders: Vec<(&str, u32, &str)> = Vec::new();
1294 for f in &self.facts {
1295 if bad(&f.key) {
1296 offenders.push((&f.origin.source, f.origin.line, &f.key.subject));
1297 }
1298 }
1299 for c in &self.clauses {
1300 for l in &c.lits {
1301 if bad(&l.key) {
1302 offenders.push((&c.origin.source, c.origin.line, &l.key.subject));
1303 }
1304 }
1305 }
1306 for r in &self.rules {
1307 for l in r.antecedent.iter().chain(r.consequent.iter()) {
1308 if bad(&l.key) {
1309 offenders.push((&r.origin.source, r.origin.line, &l.key.subject));
1310 }
1311 }
1312 }
1313 if let Some(&(source, line, name)) = offenders
1314 .iter()
1315 .min_by(|a, b| (a.0, a.1, a.2).cmp(&(b.0, b.1, b.2)))
1316 {
1317 let names: Vec<&str> = self.ports.keys().map(|(_, n)| n.as_str()).collect();
1318 return Err(CompileError::UndeclaredPort {
1319 file: source.to_string(),
1320 line,
1321 name: name.to_string(),
1322 suggestion: did_you_mean(name, &names),
1323 });
1324 }
1325 }
1326
1327 let external = inputs.iter().map(|(k, b)| (parse_port_ref(k), b.clone()));
1332 let bindings: Vec<(PortRef, PortBinding)> =
1333 self.provides.iter().cloned().chain(external).collect();
1334 let mut merged: BTreeMap<AtomKey, PortBinding> = BTreeMap::new();
1335 for (rf, b) in bindings {
1336 let key = self.resolve_port_ref(&rf)?;
1337 match merged.get(&key) {
1338 Some(prev) if prev.value != b.value => {
1339 return Err(CompileError::PortConflict {
1340 name: atomkey_label(&key),
1341 a_value: prev.value,
1342 a_origin: prev.origin.clone(),
1343 b_value: b.value,
1344 b_origin: b.origin.clone(),
1345 });
1346 }
1347 _ => {
1348 merged.entry(key).or_insert(b);
1349 }
1350 }
1351 }
1352
1353 let mut name_counts: BTreeMap<&str, usize> = BTreeMap::new();
1357 for (_, n) in self.ports.keys() {
1358 *name_counts.entry(n.as_str()).or_default() += 1;
1359 }
1360 let mut placeholders: Vec<PlaceholderInfo> = Vec::new();
1361 let mut interns: Vec<AtomKey> = Vec::new();
1362 let mut synth: Vec<(AtomKey, bool, String, u32)> = Vec::new();
1363 for ((domain, name), decl) in &self.ports {
1364 let key = AtomKey {
1365 domain: domain.clone(),
1366 subject: name.clone(),
1367 predicate: None,
1368 object: None,
1369 };
1370 interns.push(key.clone());
1371 let (status, value, origin) = match merged.get(&key) {
1372 Some(b) => (
1373 PlaceholderStatus::Supplied,
1374 Some(b.value),
1375 Some(b.origin.clone()),
1376 ),
1377 None => match decl.default {
1378 Some(v) => (PlaceholderStatus::DefaultUsed, Some(v), None),
1379 None => (PlaceholderStatus::Unset, None, None),
1380 },
1381 };
1382 if let Some(v) = value {
1383 synth.push((key, v, decl.source.clone(), decl.line));
1384 }
1385 let label = if name_counts.get(name.as_str()).copied().unwrap_or(0) > 1 {
1386 alloc::format!("{domain}.{name}")
1387 } else {
1388 name.clone()
1389 };
1390 placeholders.push(PlaceholderInfo {
1391 key: label,
1392 status,
1393 value,
1394 origin,
1395 });
1396 }
1397
1398 for key in interns {
1401 self.intern(&key);
1402 self.relation_consumed.insert(key);
1403 }
1404 for (key, value, source, line) in synth {
1405 self.facts.push(RawFact {
1406 key,
1407 value: if value { Value::True } else { Value::False },
1408 origin: Origin {
1409 source,
1410 line,
1411 premise: None,
1412 kind: kw::VAR,
1413 },
1414 soft: false,
1415 });
1416 }
1417
1418 for (key, b) in &merged {
1422 if key.predicate.is_some() {
1423 self.facts.push(RawFact {
1424 key: key.clone(),
1425 value: if b.value { Value::True } else { Value::False },
1426 origin: Origin {
1427 source: b.origin.clone(),
1428 line: 0,
1429 premise: None,
1430 kind: kw::PROVIDE,
1431 },
1432 soft: false,
1433 });
1434 }
1435 }
1436
1437 Ok(placeholders)
1438 }
1439
1440 fn resolve_port_ref(&self, rf: &PortRef) -> Result<AtomKey, CompileError> {
1446 if rf.predicate.is_none() {
1447 let domains: Vec<&str> = self
1448 .ports
1449 .keys()
1450 .filter(|k| k.1 == rf.subject && rf.domain.as_deref().is_none_or(|d| k.0 == d))
1451 .map(|k| k.0.as_str())
1452 .collect();
1453 match domains.as_slice() {
1454 [] => {
1455 let names: Vec<&str> = self.ports.keys().map(|(_, n)| n.as_str()).collect();
1456 Err(CompileError::UnknownPort {
1457 name: rf.label(),
1458 suggestion: did_you_mean(&rf.subject, &names),
1459 })
1460 }
1461 [d] => Ok(AtomKey {
1462 domain: (*d).to_string(),
1463 subject: rf.subject.clone(),
1464 predicate: None,
1465 object: None,
1466 }),
1467 many => Err(CompileError::AmbiguousPort {
1468 name: rf.subject.clone(),
1469 domains: many.join(", "),
1470 }),
1471 }
1472 } else {
1473 let cands: Vec<&AtomKey> = self
1474 .keys
1475 .iter()
1476 .filter(|k| {
1477 k.subject == rf.subject
1478 && k.predicate.as_deref() == rf.predicate.as_deref()
1479 && k.object.as_deref() == rf.object.as_deref()
1480 && rf.domain.as_deref().is_none_or(|d| k.domain == d)
1481 })
1482 .collect();
1483 match cands.as_slice() {
1484 [] => Err(CompileError::UnknownExternalAtom { name: rf.label() }),
1485 [k] => Ok((*k).clone()),
1486 many => Err(CompileError::AmbiguousPort {
1487 name: rf.label(),
1488 domains: many
1489 .iter()
1490 .map(|k| k.domain.as_str())
1491 .collect::<Vec<_>>()
1492 .join(", "),
1493 }),
1494 }
1495 }
1496 }
1497
1498 pub fn finalize(self) -> Compiled {
1500 let atoms: Vec<AtomKey> = self.keys.into_iter().collect(); let mut id_of: BTreeMap<AtomKey, AtomId> = BTreeMap::new();
1502 for (i, k) in atoms.iter().enumerate() {
1503 id_of.insert(k.clone(), i as AtomId);
1504 }
1505 let lower = |l: &RawLit| Lit {
1506 atom: id_of[&l.key],
1507 negated: l.negated,
1508 };
1509
1510 let facts = self
1511 .facts
1512 .into_iter()
1513 .map(|f| Fact {
1514 atom: id_of[&f.key],
1515 value: f.value,
1516 origin: f.origin,
1517 soft: f.soft,
1518 })
1519 .collect();
1520 let clauses = self
1521 .clauses
1522 .into_iter()
1523 .map(|c| Clause {
1524 lits: c.lits.iter().map(lower).collect(),
1525 origin: c.origin,
1526 })
1527 .collect();
1528 let rules = self
1529 .rules
1530 .into_iter()
1531 .map(|r| Rule {
1532 antecedent: r.antecedent.iter().map(lower).collect(),
1533 consequent: r.consequent.iter().map(lower).collect(),
1534 origin: r.origin,
1535 })
1536 .collect();
1537
1538 let consumed = self
1539 .relation_consumed
1540 .iter()
1541 .filter_map(|k| id_of.get(k).copied())
1542 .collect();
1543
1544 Compiled {
1545 atoms,
1546 facts,
1547 clauses,
1548 rules,
1549 checks: self.checks,
1550 pending_imports: self.pending_imports,
1551 unused_imports: Vec::new(), consumed,
1553 placeholders: Vec::new(), }
1555 }
1556}
1557
1558pub fn read_data_source(file: &str, src: &str) -> Result<Vec<(String, bool)>, CompileError> {
1563 let program = parse_tagged(file, src)?;
1564 let mut out = Vec::new();
1565 for stmt in &program.statements {
1566 match stmt {
1567 Statement::Provide { atom, value } => {
1568 let a = &atom.data;
1572 let mut key = String::new();
1573 if let Some(d) = a.domain {
1574 key.push_str(d);
1575 key.push('.');
1576 }
1577 key.push_str(a.subject);
1578 if let Some(p) = a.predicate {
1579 key.push(' ');
1580 key.push_str(p);
1581 }
1582 if let Some(o) = a.object {
1583 key.push(' ');
1584 key.push_str(o);
1585 }
1586 out.push((key, *value));
1587 }
1588 Statement::Domain(_) => {}
1589 other => {
1590 return Err(CompileError::DataFileStatement {
1591 file: file.to_string(),
1592 line: statement_line(other),
1593 });
1594 }
1595 }
1596 }
1597 Ok(out)
1598}
1599
1600pub fn read_data_bindings(
1605 file: &str,
1606 src: &str,
1607) -> Result<Vec<(String, PortBinding)>, CompileError> {
1608 Ok(read_data_source(file, src)?
1609 .into_iter()
1610 .map(|(name, value)| {
1611 (
1612 name,
1613 PortBinding {
1614 value,
1615 origin: alloc::format!("data:{file}"),
1616 },
1617 )
1618 })
1619 .collect())
1620}
1621
1622fn statement_line(s: &Statement) -> u32 {
1624 match s {
1625 Statement::Domain(n) => n.span.location_line(),
1626 Statement::Import { path, .. } => path.span.location_line(),
1627 Statement::Fact(a) | Statement::Negation(a) => a.span.location_line(),
1628 Statement::Assume(l) => l.span.location_line(),
1629 Statement::Set { name, .. } => name.span.location_line(),
1630 Statement::Close { relation, .. } => relation.span.location_line(),
1631 Statement::Var { name, .. } => name.span.location_line(),
1632 Statement::Provide { atom, .. } => atom.span.location_line(),
1633 Statement::Premise { name, .. } | Statement::Rule { name, .. } => name.span.location_line(),
1634 Statement::Check { subject, .. } => subject
1635 .as_ref()
1636 .map(|s| s.span.location_line())
1637 .unwrap_or(0),
1638 }
1639}
1640
1641pub fn compile_source(source: &str, src: &str) -> Result<Compiled, CompileError> {
1644 compile_source_with(source, src, &[])
1645}
1646
1647pub fn compile_source_with(
1651 source: &str,
1652 src: &str,
1653 inputs: &[(String, PortBinding)],
1654) -> Result<Compiled, CompileError> {
1655 let mut c = Compiler::new();
1656 c.add_source(source, src)?;
1657 c.validate_closed_world()?;
1658 let placeholders = c.resolve_ports(inputs)?;
1659 let mut compiled = c.finalize();
1660 compiled.placeholders = placeholders;
1661 Ok(compiled)
1662}
1663
1664pub trait Resolver {
1670 fn load(&self, path: &str) -> Result<String, CompileError>;
1672
1673 fn resolve(&self, _base: &str, relative: &str) -> String {
1676 relative.to_string()
1677 }
1678}
1679
1680#[derive(Default)]
1683pub struct MemoryResolver {
1684 sources: BTreeMap<String, String>,
1685}
1686
1687impl MemoryResolver {
1688 pub fn new() -> Self {
1690 Self::default()
1691 }
1692
1693 pub fn add(&mut self, path: &str, content: &str) -> &mut Self {
1695 self.sources.insert(path.to_string(), content.to_string());
1696 self
1697 }
1698}
1699
1700pub fn normalize_import_path(base: &str, relative: &str) -> String {
1708 fn is_sep(c: char) -> bool {
1709 c == '/' || c == '\\'
1710 }
1711 fn push<'a>(parts: &mut Vec<&'a str>, seg: &'a str) {
1712 match seg {
1713 "" | "." => {}
1714 ".." => {
1715 parts.pop();
1716 }
1717 _ => parts.push(seg),
1718 }
1719 }
1720 let mut absolute = base.starts_with(is_sep);
1721 let mut parts: Vec<&str> = Vec::new();
1722 let base_segs: Vec<&str> = base.split(is_sep).collect();
1724 for seg in base_segs.iter().take(base_segs.len().saturating_sub(1)) {
1725 push(&mut parts, seg);
1726 }
1727 if relative.starts_with(is_sep) {
1729 parts.clear();
1730 absolute = true;
1731 }
1732 for seg in relative.split(is_sep) {
1733 push(&mut parts, seg);
1734 }
1735 let joined = parts.join("/");
1736 if absolute {
1737 alloc::format!("/{joined}")
1738 } else {
1739 joined
1740 }
1741}
1742
1743impl Resolver for MemoryResolver {
1744 fn load(&self, path: &str) -> Result<String, CompileError> {
1745 self.sources
1746 .get(path)
1747 .cloned()
1748 .ok_or_else(|| CompileError::ImportNotFound(path.to_string()))
1749 }
1750
1751 fn resolve(&self, base: &str, relative: &str) -> String {
1752 normalize_import_path(base, relative)
1753 }
1754}
1755
1756#[cfg(feature = "std")]
1760pub struct FileResolver;
1761
1762#[cfg(feature = "std")]
1763impl Resolver for FileResolver {
1764 fn load(&self, path: &str) -> Result<String, CompileError> {
1765 std::fs::read_to_string(path)
1766 .map_err(|e| CompileError::ImportNotFound(alloc::format!("{}: {}", path, e)))
1767 }
1768
1769 fn resolve(&self, base: &str, relative: &str) -> String {
1770 normalize_import_path(base, relative)
1775 }
1776}
1777
1778struct ResolvedFile {
1781 path: String,
1782 content: String,
1783 ctx: DomainCtx,
1784}
1785
1786pub fn compile<R: Resolver>(root: &str, resolver: &R) -> Result<Compiled, CompileError> {
1802 compile_with(root, resolver, &[])
1803}
1804
1805pub fn compile_with<R: Resolver>(
1809 root: &str,
1810 resolver: &R,
1811 inputs: &[(String, PortBinding)],
1812) -> Result<Compiled, CompileError> {
1813 let (files, unused_imports) = resolve_graph(root, resolver)?;
1814 let mut c = Compiler::new();
1815 for file in &files {
1816 c.add_resolved(file)?;
1817 }
1818 c.validate_closed_world()?;
1819 let placeholders = c.resolve_ports(inputs)?;
1820 let mut compiled = c.finalize();
1821 compiled.unused_imports = unused_imports;
1822 compiled.placeholders = placeholders;
1823 Ok(compiled)
1824}
1825
1826struct ImportEdge {
1829 alias: Option<String>,
1830 child_path: String,
1831 line: u32,
1832}
1833
1834struct DiscoveredFile {
1839 path: String,
1840 content: String,
1841 domain: String,
1842 edges: Vec<ImportEdge>,
1843 used_prefixes: BTreeSet<Option<String>>,
1844}
1845
1846fn resolve_graph<R: Resolver>(
1854 root: &str,
1855 resolver: &R,
1856) -> Result<(Vec<ResolvedFile>, Vec<UnusedImport>), CompileError> {
1857 enum Step {
1859 Enter(String),
1861 Exit(String),
1863 }
1864
1865 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())];
1870
1871 while let Some(step) = work.pop() {
1872 match step {
1873 Step::Exit(hash) => {
1874 active.remove(&hash);
1875 order.push(hash);
1876 }
1877 Step::Enter(path) => {
1878 let content = resolver.load(&path)?;
1879 let hash = hash_hex(content.as_bytes());
1880 path_hash.insert(path.clone(), hash.clone());
1881 if active.contains(&hash) {
1882 return Err(CompileError::CircularImport(path)); }
1884 if discovered.contains_key(&hash) {
1885 continue; }
1887 let program = parse_tagged(&path, &content)?;
1888 let domain = extract_domain(&program, &path)?;
1889 let mut edges = Vec::new();
1890 let mut used_prefixes = BTreeSet::new();
1891 for stmt in &program.statements {
1892 if let Statement::Import { path: p, alias } = stmt {
1893 edges.push(ImportEdge {
1894 alias: alias.as_ref().map(|a| a.data.to_string()),
1895 child_path: resolver.resolve(&path, p.data),
1896 line: p.span.location_line(),
1897 });
1898 } else {
1899 collect_prefixes(stmt, &mut used_prefixes);
1900 }
1901 }
1902 drop(program); active.insert(hash.clone());
1904 work.push(Step::Exit(hash.clone()));
1905 for e in edges.iter().rev() {
1906 work.push(Step::Enter(e.child_path.clone()));
1907 }
1908 discovered.insert(
1909 hash,
1910 DiscoveredFile {
1911 path,
1912 content,
1913 domain,
1914 edges,
1915 used_prefixes,
1916 },
1917 );
1918 }
1919 }
1920 }
1921
1922 let domain_of: BTreeMap<&str, &str> = discovered
1926 .iter()
1927 .map(|(h, f)| (h.as_str(), f.domain.as_str()))
1928 .collect();
1929
1930 let mut out = Vec::with_capacity(order.len());
1931 let mut unused: Vec<UnusedImport> = Vec::new();
1932 for hash in &order {
1933 let file = &discovered[hash];
1934 let mut aliases = BTreeMap::new();
1935 aliases.insert(file.domain.clone(), file.domain.clone());
1936 for edge in &file.edges {
1937 let child_domain = domain_of[path_hash[&edge.child_path].as_str()];
1938 let bind = edge
1939 .alias
1940 .clone()
1941 .unwrap_or_else(|| child_domain.to_string());
1942 match aliases.get(&bind) {
1943 Some(existing) if existing != child_domain => {
1944 return Err(CompileError::DomainAliasClash { alias: bind });
1945 }
1946 _ => {
1947 aliases.insert(bind, child_domain.to_string());
1948 }
1949 }
1950 }
1951
1952 let referenced: BTreeSet<&str> = file
1956 .used_prefixes
1957 .iter()
1958 .filter_map(|p| match p {
1959 None => Some(file.domain.as_str()),
1960 Some(name) => aliases.get(name).map(|d| d.as_str()),
1961 })
1962 .collect();
1963 for edge in &file.edges {
1964 let child_domain = domain_of[path_hash[&edge.child_path].as_str()];
1965 if !referenced.contains(child_domain) {
1966 unused.push(UnusedImport {
1967 file: file.path.clone(),
1968 domain: child_domain.to_string(),
1969 alias: edge.alias.clone(),
1970 line: edge.line,
1971 });
1972 }
1973 }
1974
1975 let ctx = DomainCtx {
1976 current: file.domain.clone(),
1977 aliases,
1978 };
1979 out.push((hash.clone(), ctx));
1980 }
1981 unused.sort();
1982
1983 let files = out
1986 .into_iter()
1987 .map(|(hash, ctx)| {
1988 let file = discovered.remove(&hash).expect("hash was discovered");
1989 ResolvedFile {
1990 path: file.path,
1991 content: file.content,
1992 ctx,
1993 }
1994 })
1995 .collect();
1996 Ok((files, unused))
1997}
1998
1999fn quant_sig(q: &Quant) -> String {
2003 match q {
2004 Quant::InSet { binder, set } => alloc::format!("|FOREACH {} IN {}", binder.data, set.data),
2005 Quant::Relation {
2006 left,
2007 predicate,
2008 right,
2009 } => alloc::format!("|FOREACH {} {} {}", left.data, predicate.data, right.data),
2010 }
2011}
2012
2013fn parse_tagged<'a>(
2017 file: &str,
2018 content: &'a str,
2019) -> Result<elenchus_parser::Program<'a>, CompileError> {
2020 elenchus_parser::parse(content).map_err(|mut diag| {
2021 diag.set_file(file);
2022 CompileError::Parse(diag)
2023 })
2024}
2025
2026fn nearest_set_suggestion(set: &str, sets: &BTreeMap<String, Vec<String>>) -> String {
2029 let names: Vec<&str> = sets.keys().map(String::as_str).collect();
2030 did_you_mean(set, &names)
2031}
2032
2033type Subs<'s> = [(&'s str, &'s str)];
2036
2037fn subst_ident<'s>(s: &'s str, subs: &Subs<'s>) -> &'s str {
2039 subs.iter()
2040 .find_map(|&(b, v)| (s == b).then_some(v))
2041 .unwrap_or(s)
2042}
2043
2044fn subst_atom<'s>(a: &Atom<'s>, subs: &Subs<'s>) -> Atom<'s> {
2046 Atom {
2047 domain: a.domain,
2048 subject: subst_ident(a.subject, subs),
2049 predicate: a.predicate.map(|p| subst_ident(p, subs)),
2050 object: a.object.map(|o| subst_ident(o, subs)),
2051 }
2052}
2053
2054fn subst_lit<'s>(ll: &Located<'s, Literal<'s>>, subs: &Subs<'s>) -> Located<'s, Literal<'s>> {
2056 Located {
2057 data: Literal {
2058 negated: ll.data.negated,
2059 atom: subst_atom(&ll.data.atom, subs),
2060 },
2061 span: ll.span,
2062 }
2063}
2064
2065fn subst_body<'s>(body: &Body<'s>, subs: &Subs<'s>) -> Body<'s> {
2070 match body {
2071 Body::List { op, atoms } => Body::List {
2072 op: *op,
2073 atoms: atoms
2074 .iter()
2075 .map(|la| Located {
2076 data: subst_atom(&la.data, subs),
2077 span: la.span,
2078 })
2079 .collect(),
2080 },
2081 Body::Impl {
2082 antecedent,
2083 ante_conn,
2084 consequent,
2085 cons_conn,
2086 } => Body::Impl {
2087 antecedent: antecedent.iter().map(|l| subst_lit(l, subs)).collect(),
2088 ante_conn: *ante_conn,
2089 consequent: consequent.iter().map(|l| subst_lit(l, subs)).collect(),
2090 cons_conn: *cons_conn,
2091 },
2092 }
2093}
2094
2095fn collect_prefixes(stmt: &Statement, out: &mut BTreeSet<Option<String>>) {
2098 let mut add = |a: &Atom| {
2099 out.insert(a.domain.map(|d| d.to_string()));
2100 };
2101 match stmt {
2102 Statement::Fact(a) | Statement::Negation(a) => add(&a.data),
2103 Statement::Assume(l) => add(&l.data.atom),
2104 Statement::Premise { body, .. } | Statement::Rule { body, .. } => match body {
2105 Body::List { atoms, .. } => atoms.iter().for_each(|a| add(&a.data)),
2106 Body::Impl {
2107 antecedent,
2108 consequent,
2109 ..
2110 } => antecedent
2111 .iter()
2112 .chain(consequent)
2113 .for_each(|l| add(&l.data.atom)),
2114 },
2115 Statement::Domain(_)
2116 | Statement::Import { .. }
2117 | Statement::Check { .. }
2118 | Statement::Set { .. }
2119 | Statement::Close { .. }
2120 | Statement::Var { .. }
2121 | Statement::Provide { .. } => {}
2122 }
2123}
2124
2125fn transitive_closure(pairs: Vec<(String, String)>) -> Vec<(String, String)> {
2129 let mut set: BTreeSet<(String, String)> = pairs.into_iter().collect();
2130 loop {
2131 let mut added: Vec<(String, String)> = Vec::new();
2132 for (a, b) in &set {
2133 for (c, d) in &set {
2134 if b == c {
2135 let p = (a.clone(), d.clone());
2136 if !set.contains(&p) {
2137 added.push(p);
2138 }
2139 }
2140 }
2141 }
2142 if added.is_empty() {
2143 break;
2144 }
2145 set.extend(added);
2146 }
2147 set.into_iter().collect()
2148}
2149
2150fn extract_domain(
2152 program: &elenchus_parser::Program,
2153 source: &str,
2154) -> Result<String, CompileError> {
2155 let mut found: Option<String> = None;
2156 for stmt in &program.statements {
2157 if let Statement::Domain(name) = stmt {
2158 if found.is_some() {
2159 return Err(CompileError::DuplicateDomain {
2160 file: source.to_string(),
2161 });
2162 }
2163 found = Some(name.data.to_string());
2164 }
2165 }
2166 found.ok_or_else(|| CompileError::MissingDomain {
2167 file: source.to_string(),
2168 })
2169}
2170
2171pub fn levenshtein(a: &[char], b: &[char]) -> usize {
2178 let mut prev: Vec<usize> = (0..=b.len()).collect();
2179 let mut cur = vec![0usize; b.len() + 1];
2180 for (i, &ca) in a.iter().enumerate() {
2181 cur[0] = i + 1;
2182 for (j, &cb) in b.iter().enumerate() {
2183 let cost = usize::from(ca != cb);
2184 cur[j + 1] = (prev[j + 1] + 1).min(cur[j] + 1).min(prev[j] + cost);
2185 }
2186 core::mem::swap(&mut prev, &mut cur);
2187 }
2188 prev[b.len()]
2189}
2190
2191fn nearest<'a>(word: &str, candidates: &[&'a str]) -> Option<&'a str> {
2203 let budget = word.chars().count() / 3;
2204 if budget == 0 {
2205 return None;
2206 }
2207 let w: Vec<char> = word.chars().collect();
2208 candidates
2209 .iter()
2210 .map(|&c| (levenshtein(&w, &c.chars().collect::<Vec<char>>()), c))
2211 .filter(|&(d, _)| d <= budget)
2212 .min_by_key(|&(d, _)| d)
2213 .map(|(_, c)| c)
2214}
2215
2216fn did_you_mean(word: &str, candidates: &[&str]) -> String {
2220 match nearest(word, candidates) {
2221 Some(s) => alloc::format!(" — did you mean `{s}`?"),
2222 None => String::new(),
2223 }
2224}
2225
2226fn raw_lits(
2229 lits: &[elenchus_parser::Located<Literal>],
2230 ctx: &DomainCtx,
2231) -> Result<Vec<RawLit>, CompileError> {
2232 lits.iter()
2233 .map(|l| {
2234 Ok(RawLit {
2235 key: ctx.key(&l.data.atom)?,
2236 negated: l.data.negated,
2237 })
2238 })
2239 .collect()
2240}
2241
2242fn list_kind(op: ListOp) -> &'static str {
2244 match op {
2245 ListOp::Exclusive => kw::EXCLUSIVE,
2246 ListOp::Forbids => kw::FORBIDS,
2247 ListOp::OneOf => kw::ONEOF,
2248 ListOp::AtLeast => kw::ATLEAST,
2249 }
2250}
2251
2252fn key_sig(k: &AtomKey) -> String {
2256 alloc::format!(
2257 "{}|{}|{}|{}",
2258 k.domain,
2259 k.subject,
2260 k.predicate.as_deref().unwrap_or(""),
2261 k.object.as_deref().unwrap_or("")
2262 )
2263}
2264
2265fn clause_sig(lits: &[RawLit]) -> String {
2267 let mut parts: Vec<String> = lits
2268 .iter()
2269 .map(|l| alloc::format!("{}|{}", key_sig(&l.key), l.negated as u8))
2270 .collect();
2271 parts.sort();
2272 parts.dedup();
2273 parts.join(";")
2274}
2275
2276fn canonical_body(
2279 name: &str,
2280 body: &Body,
2281 is_rule: bool,
2282 ctx: &DomainCtx,
2283) -> Result<String, CompileError> {
2284 let mut s = String::new();
2285 let _ = write!(
2286 s,
2287 "{}|{}|",
2288 if is_rule { kw::RULE } else { kw::PREMISE },
2289 name
2290 );
2291 match body {
2292 Body::List { op, atoms } => {
2293 let _ = write!(s, "LIST|{}|", list_kind(*op));
2294 let mut keys: Vec<String> = atoms
2295 .iter()
2296 .map(|a| Ok(key_sig(&ctx.key(&a.data)?)))
2297 .collect::<Result<_, CompileError>>()?;
2298 keys.sort();
2299 s.push_str(&keys.join(";"));
2300 }
2301 Body::Impl {
2302 antecedent,
2303 ante_conn,
2304 consequent,
2305 cons_conn,
2306 } => {
2307 let conn = |c: &Conn| if *c == Conn::Or { "OR" } else { "AND" };
2308 s.push_str("IMPL|ANTE|");
2309 s.push_str(conn(ante_conn));
2310 s.push('|');
2311 s.push_str(&lit_sigs(antecedent, ctx)?);
2312 s.push_str("|CONS|");
2313 s.push_str(conn(cons_conn));
2314 s.push('|');
2315 s.push_str(&lit_sigs(consequent, ctx)?);
2316 }
2317 }
2318 Ok(s)
2319}
2320
2321fn lit_sigs(
2324 lits: &[elenchus_parser::Located<Literal>],
2325 ctx: &DomainCtx,
2326) -> Result<String, CompileError> {
2327 let mut parts: Vec<String> = lits
2328 .iter()
2329 .map(|l| {
2330 Ok(alloc::format!(
2331 "{}|{}",
2332 key_sig(&ctx.key(&l.data.atom)?),
2333 l.data.negated as u8
2334 ))
2335 })
2336 .collect::<Result<_, CompileError>>()?;
2337 parts.sort();
2338 Ok(parts.join(";"))
2339}
2340
2341#[cfg(test)]
2342mod tests {
2343 use super::*;
2344
2345 fn cs(src: &str) -> Result<Compiled, CompileError> {
2348 compile_source("<t>", &alloc::format!("DOMAIN t\n{src}"))
2349 }
2350
2351 fn key(subject: &str, predicate: &str, object: Option<&str>) -> AtomKey {
2353 key_in("t", subject, predicate, object)
2354 }
2355
2356 fn key_in(domain: &str, subject: &str, predicate: &str, object: Option<&str>) -> AtomKey {
2358 AtomKey {
2359 domain: domain.to_string(),
2360 subject: subject.to_string(),
2361 predicate: Some(predicate.to_string()),
2362 object: object.map(|o| o.to_string()),
2363 }
2364 }
2365
2366 fn id(c: &Compiled, k: &AtomKey) -> AtomId {
2367 c.atoms.iter().position(|a| a == k).unwrap() as AtomId
2368 }
2369
2370 #[test]
2371 fn exclusive_unfolds_pairwise() {
2372 let src = r#"
2373 PREMISE e:
2374 EXCLUSIVE
2375 x a
2376 x b
2377 x c
2378 "#;
2379 let c = cs(src).unwrap();
2380 assert_eq!(c.clauses.len(), 3);
2382 for cl in &c.clauses {
2383 assert_eq!(cl.lits.len(), 2);
2384 assert!(cl.lits.iter().all(|l| !l.negated));
2385 }
2386 }
2387
2388 #[test]
2389 fn implication_negates_consequent() {
2390 let src = r#"
2392 PREMISE r:
2393 WHEN x a
2394 THEN x b
2395 "#;
2396 let c = cs(src).unwrap();
2397 assert_eq!(c.clauses.len(), 1);
2398 let cl = &c.clauses[0];
2399 assert_eq!(cl.lits.len(), 2);
2400 let a = id(&c, &key("x", "a", None));
2401 let b = id(&c, &key("x", "b", None));
2402 assert!(cl.lits.contains(&Lit {
2403 atom: a,
2404 negated: false
2405 }));
2406 assert!(cl.lits.contains(&Lit {
2407 atom: b,
2408 negated: true
2409 }));
2410 }
2411
2412 #[test]
2413 fn negated_consequent_flips_to_positive() {
2414 let src = r#"
2416 PREMISE r:
2417 WHEN x a
2418 THEN NOT x b
2419 "#;
2420 let c = cs(src).unwrap();
2421 let b = id(&c, &key("x", "b", None));
2422 assert!(c.clauses[0].lits.contains(&Lit {
2423 atom: b,
2424 negated: false
2425 }));
2426 }
2427
2428 #[test]
2429 fn consequent_or_is_one_clause_with_all_negated() {
2430 let src = r#"
2432 PREMISE r:
2433 WHEN x p
2434 THEN x a
2435 OR x b
2436 "#;
2437 let c = cs(src).unwrap();
2438 assert_eq!(c.clauses.len(), 1);
2439 let cl = &c.clauses[0];
2440 assert_eq!(cl.lits.len(), 3);
2441 let p = id(&c, &key("x", "p", None));
2442 let a = id(&c, &key("x", "a", None));
2443 let b = id(&c, &key("x", "b", None));
2444 assert!(cl.lits.contains(&Lit {
2445 atom: p,
2446 negated: false
2447 }));
2448 assert!(cl.lits.contains(&Lit {
2449 atom: a,
2450 negated: true
2451 }));
2452 assert!(cl.lits.contains(&Lit {
2453 atom: b,
2454 negated: true
2455 }));
2456 }
2457
2458 #[test]
2459 fn antecedent_or_is_one_clause_per_disjunct() {
2460 let src = r#"
2463 PREMISE r:
2464 WHEN x a
2465 OR x b
2466 THEN x c
2467 "#;
2468 let c = cs(src).unwrap();
2469 assert_eq!(c.clauses.len(), 2);
2470 let a = id(&c, &key("x", "a", None));
2471 let b = id(&c, &key("x", "b", None));
2472 let cc = id(&c, &key("x", "c", None));
2473 for cl in &c.clauses {
2475 assert_eq!(cl.lits.len(), 2);
2476 assert!(cl.lits.contains(&Lit {
2477 atom: cc,
2478 negated: true
2479 }));
2480 }
2481 let has = |atom| {
2482 c.clauses.iter().any(|cl| {
2483 cl.lits.contains(&Lit {
2484 atom,
2485 negated: false,
2486 })
2487 })
2488 };
2489 assert!(has(a) && has(b));
2490 }
2491
2492 #[test]
2493 fn antecedent_or_with_consequent_or_distributes() {
2494 let src = r#"
2496 PREMISE r:
2497 WHEN x a
2498 OR x b
2499 THEN x c
2500 OR x d
2501 "#;
2502 let c = cs(src).unwrap();
2503 assert_eq!(c.clauses.len(), 2);
2504 for cl in &c.clauses {
2505 assert_eq!(cl.lits.len(), 3);
2506 }
2507 }
2508
2509 #[test]
2510 fn rule_with_or_antecedent_splits_into_two_rules() {
2511 let src = r#"
2513 RULE r:
2514 WHEN x a
2515 OR x b
2516 THEN x c
2517 "#;
2518 let c = cs(src).unwrap();
2519 assert_eq!(c.rules.len(), 2);
2520 assert!(
2521 c.rules
2522 .iter()
2523 .all(|r| r.antecedent.len() == 1 && r.consequent.len() == 1)
2524 );
2525 }
2526
2527 #[test]
2528 fn rule_with_or_consequent_is_rejected() {
2529 let src = r#"
2531 RULE r:
2532 WHEN x a
2533 THEN x b
2534 OR x c
2535 "#;
2536 let err = cs(src).unwrap_err();
2537 assert!(matches!(
2538 err,
2539 CompileError::RuleDisjunctiveConsequent { .. }
2540 ));
2541 }
2542
2543 #[test]
2544 fn oneof_is_pairwise_plus_at_least_one() {
2545 let src = r#"
2546 PREMISE o:
2547 ONEOF
2548 x a
2549 x b
2550 "#;
2551 let c = cs(src).unwrap();
2552 assert_eq!(c.clauses.len(), 2);
2554 assert!(c.clauses.iter().any(|cl| cl.lits.iter().all(|l| l.negated)));
2556 }
2557
2558 #[test]
2559 fn atleast_is_one_negated_clause() {
2560 let src = r#"
2561 PREMISE a:
2562 ATLEAST
2563 x a
2564 x b
2565 x c
2566 "#;
2567 let c = cs(src).unwrap();
2568 assert_eq!(c.clauses.len(), 1);
2569 assert_eq!(c.clauses[0].lits.len(), 3);
2570 assert!(c.clauses[0].lits.iter().all(|l| l.negated));
2571 }
2572
2573 #[test]
2574 fn rules_are_separate_from_clauses() {
2575 let src = r#"
2576 RULE needs:
2577 WHEN x a
2578 THEN x b
2579 "#;
2580 let c = cs(src).unwrap();
2581 assert_eq!(c.clauses.len(), 0);
2582 assert_eq!(c.rules.len(), 1);
2583 assert_eq!(c.rules[0].antecedent.len(), 1);
2584 assert_eq!(c.rules[0].consequent.len(), 1);
2585 }
2586
2587 #[test]
2588 fn atoms_are_canonically_sorted() {
2589 let src = r#"
2590 FACT z z
2591 FACT a a
2592 FACT m m
2593 "#;
2594 let c = cs(src).unwrap();
2595 let mut sorted = c.atoms.clone();
2596 sorted.sort();
2597 assert_eq!(c.atoms, sorted);
2598 }
2599
2600 #[test]
2601 fn duplicate_premise_is_idempotent() {
2602 let src = r#"
2603 PREMISE e:
2604 EXCLUSIVE
2605 x a
2606 x b
2607 PREMISE e:
2608 EXCLUSIVE
2609 x a
2610 x b
2611 "#;
2612 let c = cs(src).unwrap();
2613 assert_eq!(c.clauses.len(), 1);
2614 }
2615
2616 #[test]
2617 fn redefinition_with_different_body_errors() {
2618 let src = r#"
2619 PREMISE e:
2620 EXCLUSIVE
2621 x a
2622 x b
2623 PREMISE e:
2624 EXCLUSIVE
2625 x a
2626 x c
2627 "#;
2628 let err = cs(src).unwrap_err();
2629 assert_eq!(
2630 err,
2631 CompileError::PremiseRedefinition {
2632 name: "e".to_string()
2633 }
2634 );
2635 }
2636
2637 #[test]
2638 fn duplicate_fact_is_idempotent() {
2639 let c = cs("FACT x a\nFACT x a\n").unwrap();
2640 assert_eq!(c.facts.len(), 1);
2641 }
2642
2643 #[test]
2644 fn conflicting_facts_are_both_kept() {
2645 let c = cs("FACT x a\nNOT x a\n").unwrap();
2647 assert_eq!(c.facts.len(), 2);
2648 }
2649
2650 #[test]
2651 fn import_is_recorded_pending() {
2652 let c = cs("IMPORT \"physics.vrf\"\nFACT x a\n").unwrap();
2653 assert_eq!(c.pending_imports, vec!["physics.vrf".to_string()]);
2654 }
2655
2656 #[test]
2657 fn qualified_fact_lands_in_the_imported_domain() {
2658 let mut r = MemoryResolver::new();
2661 r.add(
2662 "lib.vrf",
2663 r#"
2664 DOMAIN physics
2665 PREMISE needs_fuel:
2666 WHEN Engine_X has engine
2667 THEN Engine_X has fuel
2668 "#,
2669 );
2670 r.add(
2671 "main.vrf",
2672 r#"
2673 DOMAIN main
2674 IMPORT "lib.vrf"
2675 FACT physics.Engine_X has engine
2676 FACT physics.Engine_X has fuel
2677 "#,
2678 );
2679 let c = compile("main.vrf", &r).unwrap();
2680 assert!(c.pending_imports.is_empty());
2681 assert_eq!(c.clauses.len(), 1); assert_eq!(c.facts.len(), 2);
2683
2684 let fuel = key_in("physics", "Engine_X", "has", Some("fuel"));
2686 let fuel_id = id(&c, &fuel);
2687 assert!(c.facts.iter().any(|f| f.atom == fuel_id));
2688 assert!(c.clauses[0].lits.iter().any(|l| l.atom == fuel_id));
2689 }
2690
2691 #[test]
2692 fn same_triple_in_different_domains_does_not_unify() {
2693 let mut r = MemoryResolver::new();
2696 r.add("lib.vrf", "DOMAIN physics\nFACT Engine_X has fuel\n");
2697 r.add(
2698 "main.vrf",
2699 "DOMAIN main\nIMPORT \"lib.vrf\"\nFACT Engine_X has fuel\n",
2700 );
2701 let c = compile("main.vrf", &r).unwrap();
2702 assert!(c.atoms.iter().any(|a| a.domain == "physics"));
2704 assert!(c.atoms.iter().any(|a| a.domain == "main"));
2705 assert_eq!(
2706 c.atoms
2707 .iter()
2708 .filter(|a| a.subject == "Engine_X" && a.predicate.as_deref() == Some("has"))
2709 .count(),
2710 2
2711 );
2712 }
2713
2714 #[test]
2715 fn import_alias_binds_a_local_domain_name() {
2716 let mut r = MemoryResolver::new();
2718 r.add("lib.vrf", "DOMAIN physics\nFACT Motor over_200\n");
2719 r.add(
2720 "main.vrf",
2721 "DOMAIN main\nIMPORT \"lib.vrf\" AS phys\nFACT phys.Motor over_100\n",
2722 );
2723 let c = compile("main.vrf", &r).unwrap();
2724 assert_eq!(c.atoms.iter().filter(|a| a.domain == "physics").count(), 2);
2726 }
2727
2728 #[test]
2729 fn unknown_domain_reference_errors() {
2730 let err = cs("FACT ghost.x a\n").unwrap_err();
2732 assert!(matches!(err, CompileError::UnknownDomain { .. }));
2733 }
2734
2735 #[test]
2736 fn imports_are_not_transitive_for_naming() {
2737 let mut r = MemoryResolver::new();
2739 r.add("math.vrf", "DOMAIN math\nFACT foo bar\n");
2740 r.add(
2741 "physics.vrf",
2742 "DOMAIN physics\nIMPORT \"math.vrf\"\nFACT Motor over_100\n",
2743 );
2744 r.add(
2745 "main.vrf",
2746 "DOMAIN main\nIMPORT \"physics.vrf\"\nFACT math.foo bar\n",
2747 );
2748 let err = compile("main.vrf", &r).unwrap_err();
2749 assert!(matches!(err, CompileError::UnknownDomain { .. }));
2750 }
2751
2752 #[test]
2753 fn transitive_dependency_clauses_still_load() {
2754 let mut r = MemoryResolver::new();
2756 r.add(
2757 "math.vrf",
2758 r"
2759 DOMAIN math
2760 PREMISE e:
2761 EXCLUSIVE
2762 x a
2763 x b
2764 ",
2765 );
2766 r.add("physics.vrf", "DOMAIN physics\nIMPORT \"math.vrf\"\n");
2767 r.add("main.vrf", "DOMAIN main\nIMPORT \"physics.vrf\"\n");
2768 let c = compile("main.vrf", &r).unwrap();
2769 assert_eq!(c.clauses.len(), 1); assert!(c.clauses.iter().all(|cl| cl.origin.source == "math.vrf"));
2771 }
2772
2773 #[test]
2774 fn missing_domain_errors() {
2775 let err = compile_source("nodomain.vrf", "FACT x a\n").unwrap_err();
2776 assert!(matches!(err, CompileError::MissingDomain { .. }));
2777 }
2778
2779 #[test]
2780 fn duplicate_domain_errors() {
2781 let err = compile_source("dup.vrf", "DOMAIN a\nDOMAIN b\nFACT x a\n").unwrap_err();
2782 assert!(matches!(err, CompileError::DuplicateDomain { .. }));
2783 }
2784
2785 #[test]
2786 fn alias_clash_when_one_local_name_binds_two_domains() {
2787 let mut r = MemoryResolver::new();
2790 r.add("a.vrf", "DOMAIN physics\nFACT Motor over_100\n");
2791 r.add("b.vrf", "DOMAIN chemistry\nFACT atom reacts\n");
2792 r.add(
2793 "main.vrf",
2794 "DOMAIN main\nIMPORT \"a.vrf\" AS x\nIMPORT \"b.vrf\" AS x\n",
2795 );
2796 let err = compile("main.vrf", &r).unwrap_err();
2797 assert!(matches!(err, CompileError::DomainAliasClash { .. }));
2798 }
2799
2800 #[test]
2801 fn two_files_with_the_same_domain_name_merge() {
2802 let mut r = MemoryResolver::new();
2805 r.add("a.vrf", "DOMAIN physics\nFACT Motor over_100\n");
2806 r.add(
2807 "main.vrf",
2808 "DOMAIN physics\nIMPORT \"a.vrf\"\nFACT Motor over_200\n",
2809 );
2810 let c = compile("main.vrf", &r).unwrap();
2811 assert!(c.atoms.iter().all(|a| a.domain == "physics"));
2813 assert_eq!(c.atoms.len(), 2);
2814 }
2815
2816 #[test]
2817 fn diamond_import_is_deduped() {
2818 let mut r = MemoryResolver::new();
2820 r.add(
2821 "base.vrf",
2822 r#"
2823 DOMAIN base
2824 PREMISE b:
2825 EXCLUSIVE
2826 x a
2827 x b
2828 "#,
2829 );
2830 r.add("a.vrf", "DOMAIN a\nIMPORT \"base.vrf\"\n");
2831 r.add("c.vrf", "DOMAIN c\nIMPORT \"base.vrf\"\n");
2832 r.add(
2833 "main.vrf",
2834 "DOMAIN main\nIMPORT \"a.vrf\"\nIMPORT \"c.vrf\"\n",
2835 );
2836 let c = compile("main.vrf", &r).unwrap();
2837 assert_eq!(c.clauses.len(), 1); }
2839
2840 #[test]
2841 fn circular_import_errors() {
2842 let mut r = MemoryResolver::new();
2843 r.add("a.vrf", "DOMAIN a\nIMPORT \"b.vrf\"\n");
2844 r.add("b.vrf", "DOMAIN b\nIMPORT \"a.vrf\"\n");
2845 let err = compile("a.vrf", &r).unwrap_err();
2846 assert!(matches!(err, CompileError::CircularImport(_)));
2847 }
2848
2849 #[test]
2850 fn three_node_cycle_errors() {
2851 let mut r = MemoryResolver::new();
2853 r.add("a.vrf", "DOMAIN a\nIMPORT \"b.vrf\"\n");
2854 r.add("b.vrf", "DOMAIN b\nIMPORT \"c.vrf\"\n");
2855 r.add("c.vrf", "DOMAIN c\nIMPORT \"a.vrf\"\n");
2856 let err = compile("a.vrf", &r).unwrap_err();
2857 assert!(matches!(err, CompileError::CircularImport(_)));
2858 }
2859
2860 #[test]
2861 fn shared_grandchild_diamond_loads_once() {
2862 let mut r = MemoryResolver::new();
2865 r.add(
2866 "b.vrf",
2867 r"
2868 DOMAIN b
2869 PREMISE e:
2870 EXCLUSIVE
2871 x a
2872 x b
2873 ",
2874 );
2875 r.add("c.vrf", "DOMAIN c\nIMPORT \"b.vrf\"\n");
2876 r.add("a.vrf", "DOMAIN a\nIMPORT \"b.vrf\"\nIMPORT \"c.vrf\"\n");
2877 let c = compile("a.vrf", &r).unwrap();
2878 assert_eq!(
2879 c.clauses.len(),
2880 1,
2881 "b.vrf's clause must appear exactly once"
2882 );
2883 }
2884
2885 #[test]
2886 fn exponential_fan_out_is_memoized_not_blown_up() {
2887 let mut r = MemoryResolver::new();
2891 r.add("f0.vrf", "DOMAIN d0\nFACT x a\n");
2892 let n = 40;
2893 for k in 1..=n {
2894 r.add(
2895 &alloc::format!("f{k}.vrf"),
2896 &alloc::format!(
2897 "DOMAIN d{k}\nIMPORT \"f{}.vrf\"\nIMPORT \"f{}.vrf\"\n",
2898 k - 1,
2899 k - 1
2900 ),
2901 );
2902 }
2903 let c = compile(&alloc::format!("f{n}.vrf"), &r).unwrap();
2904 assert_eq!(c.facts.len(), 1); }
2906
2907 #[test]
2908 fn very_deep_linear_chain_does_not_overflow() {
2909 let mut r = MemoryResolver::new();
2912 r.add("f0.vrf", "DOMAIN d0\nFACT x a\n");
2913 let n = 10_000;
2914 for k in 1..=n {
2915 r.add(
2916 &alloc::format!("f{k}.vrf"),
2917 &alloc::format!("DOMAIN d{k}\nIMPORT \"f{}.vrf\"\n", k - 1),
2918 );
2919 }
2920 let c = compile(&alloc::format!("f{n}.vrf"), &r).unwrap();
2921 assert_eq!(c.facts.len(), 1);
2922 }
2923
2924 #[test]
2925 fn missing_import_errors() {
2926 let mut r = MemoryResolver::new();
2927 r.add("main.vrf", "DOMAIN main\nIMPORT \"ghost.vrf\"\n");
2928 let err = compile("main.vrf", &r).unwrap_err();
2929 assert!(matches!(err, CompileError::ImportNotFound(_)));
2930 }
2931
2932 #[test]
2933 fn unused_import_is_flagged() {
2934 let mut r = MemoryResolver::new();
2936 r.add("physics.vrf", "DOMAIN physics\nFACT Motor over_100\n");
2937 r.add(
2938 "main.vrf",
2939 "DOMAIN main\nIMPORT \"physics.vrf\"\nFACT x a\n",
2940 );
2941 let c = compile("main.vrf", &r).unwrap();
2942 assert_eq!(c.unused_imports.len(), 1);
2943 assert_eq!(c.unused_imports[0].domain, "physics");
2944 assert_eq!(c.unused_imports[0].file, "main.vrf");
2945 assert_eq!(c.unused_imports[0].alias, None);
2946 }
2947
2948 #[test]
2949 fn referenced_import_is_not_unused() {
2950 let mut r = MemoryResolver::new();
2952 r.add("physics.vrf", "DOMAIN physics\nFACT Motor over_100\n");
2953 r.add(
2954 "main.vrf",
2955 "DOMAIN main\nIMPORT \"physics.vrf\"\nFACT physics.Motor over_200\n",
2956 );
2957 let c = compile("main.vrf", &r).unwrap();
2958 assert!(c.unused_imports.is_empty(), "{:?}", c.unused_imports);
2959 }
2960
2961 #[test]
2962 fn unused_import_records_its_alias() {
2963 let mut r = MemoryResolver::new();
2964 r.add("physics.vrf", "DOMAIN physics\nFACT Motor over_100\n");
2965 r.add(
2966 "main.vrf",
2967 "DOMAIN main\nIMPORT \"physics.vrf\" AS phys\nFACT x a\n",
2968 );
2969 let c = compile("main.vrf", &r).unwrap();
2970 assert_eq!(c.unused_imports.len(), 1);
2971 assert_eq!(c.unused_imports[0].alias.as_deref(), Some("phys"));
2972 }
2973
2974 #[test]
2975 fn import_referenced_only_inside_a_premise_is_used() {
2976 let mut r = MemoryResolver::new();
2978 r.add("physics.vrf", "DOMAIN physics\nFACT Motor over_100\n");
2979 r.add(
2980 "main.vrf",
2981 r#"
2982 DOMAIN main
2983 IMPORT "physics.vrf"
2984 PREMISE p:
2985 WHEN physics.Motor over_100
2986 THEN x ok
2987 "#,
2988 );
2989 let c = compile("main.vrf", &r).unwrap();
2990 assert!(c.unused_imports.is_empty(), "{:?}", c.unused_imports);
2991 }
2992
2993 #[test]
2994 fn same_premise_name_across_files_coexists() {
2995 let mut r = MemoryResolver::new();
2999 r.add(
3000 "physics.vrf",
3001 r#"
3002 DOMAIN physics
3003 PREMISE safety:
3004 EXCLUSIVE
3005 x a
3006 x b
3007 "#,
3008 );
3009 r.add(
3010 "main.vrf",
3011 r#"
3012 DOMAIN main
3013 IMPORT "physics.vrf"
3014 PREMISE safety:
3015 EXCLUSIVE
3016 x a
3017 x c
3018 "#,
3019 );
3020 let c = compile("main.vrf", &r).unwrap();
3021 assert_eq!(c.clauses.len(), 2); assert!(c.clauses.iter().any(|cl| cl.origin.source == "physics.vrf"));
3023 assert!(c.clauses.iter().any(|cl| cl.origin.source == "main.vrf"));
3024 }
3025
3026 #[test]
3027 fn redefinition_within_one_source_still_errors() {
3028 let src = r#"
3030 DOMAIN m
3031 PREMISE e:
3032 EXCLUSIVE
3033 x a
3034 x b
3035 PREMISE e:
3036 EXCLUSIVE
3037 x a
3038 x c
3039 "#;
3040 let err = compile_source("main.vrf", src).unwrap_err();
3041 assert_eq!(
3042 err,
3043 CompileError::PremiseRedefinition {
3044 name: "e".to_string()
3045 }
3046 );
3047 }
3048
3049 #[test]
3050 fn import_demo_examples_resolve() {
3051 let mut r = MemoryResolver::new();
3052 r.add(
3053 "physics.vrf",
3054 include_str!("../../../docs/examples/physics.vrf"),
3055 );
3056 r.add(
3057 "import-demo.vrf",
3058 include_str!("../../../docs/examples/import-demo.vrf"),
3059 );
3060 let c = compile("import-demo.vrf", &r).unwrap();
3061 assert!(c.pending_imports.is_empty());
3062 assert_eq!(c.clauses.len(), 2);
3064 let over_100 = id(&c, &key_in("physics", "Motor", "over_100", None));
3066 assert!(c.facts.iter().any(|f| f.atom == over_100));
3067 assert!(
3068 c.clauses
3069 .iter()
3070 .any(|cl| cl.lits.iter().any(|l| l.atom == over_100))
3071 );
3072 }
3073
3074 #[test]
3075 fn creature_example_compiles() {
3076 let src = include_str!("../../../docs/examples/creature.vrf");
3077 let c = compile_source("creature.vrf", src).unwrap();
3078 assert_eq!(c.facts.len(), 2); assert_eq!(c.rules.len(), 1); assert_eq!(c.checks.len(), 1);
3081 assert_eq!(c.clauses.len(), 4);
3083 assert_eq!(c.atoms.len(), 7);
3084 }
3085
3086 #[test]
3087 fn forbids_unfolds_pairwise() {
3088 let src = r#"
3089 PREMISE f:
3090 FORBIDS
3091 x a
3092 x b
3093 x c
3094 "#;
3095 let c = cs(src).unwrap();
3096 assert_eq!(c.clauses.len(), 3); assert!(
3098 c.clauses
3099 .iter()
3100 .all(|cl| cl.lits.len() == 2 && cl.lits.iter().all(|l| !l.negated))
3101 );
3102 }
3103
3104 #[test]
3105 fn rule_with_multiple_consequents() {
3106 let src = r#"
3107 RULE r:
3108 WHEN x a
3109 THEN x b
3110 AND x c
3111 "#;
3112 let c = cs(src).unwrap();
3113 assert_eq!(c.rules.len(), 1);
3114 assert_eq!(c.rules[0].consequent.len(), 2);
3115 }
3116
3117 #[test]
3118 fn negated_antecedent_literal_keeps_polarity() {
3119 let src = r#"
3121 PREMISE a:
3122 WHEN NOT x a
3123 THEN x b
3124 "#;
3125 let c = cs(src).unwrap();
3126 let xa = id(&c, &key("x", "a", None));
3127 assert!(c.clauses[0].lits.contains(&Lit {
3128 atom: xa,
3129 negated: true
3130 }));
3131 }
3132
3133 #[test]
3134 fn rule_keeps_consequent_negation() {
3135 let src = r#"
3136 RULE r:
3137 WHEN x a
3138 THEN NOT x b
3139 "#;
3140 let c = cs(src).unwrap();
3141 assert!(c.rules[0].consequent[0].negated);
3142 }
3143
3144 #[test]
3145 fn compilation_is_deterministic() {
3146 let src = r#"
3147 PREMISE e:
3148 EXCLUSIVE
3149 z z
3150 a a
3151 m m
3152 FACT q q
3153 "#;
3154 assert_eq!(cs(src).unwrap(), cs(src).unwrap());
3155 }
3156
3157 #[test]
3158 fn empty_program_compiles_to_empty_ir() {
3159 let c = cs("// nothing here\n").unwrap();
3160 assert!(c.atoms.is_empty() && c.clauses.is_empty() && c.facts.is_empty());
3161 }
3162
3163 #[test]
3164 fn same_clause_from_two_named_premises_is_deduped() {
3165 let src = r#"
3167 PREMISE e1:
3168 EXCLUSIVE
3169 x a
3170 x b
3171 PREMISE e2:
3172 EXCLUSIVE
3173 x a
3174 x b
3175 "#;
3176 let c = cs(src).unwrap();
3177 assert_eq!(c.clauses.len(), 1);
3178 }
3179
3180 #[test]
3181 fn object_distinguishes_atom_identity() {
3182 let c = cs("FACT x p a\nFACT x p b\n").unwrap();
3184 assert_eq!(c.atoms.len(), 2);
3185 }
3186
3187 const ONEOF_RESOLVED: &str = r"PREMISE pick:
3192 ONEOF
3193 resolved is censored
3194 resolved is censored_mtp
3195 resolved is uncensored
3196";
3197
3198 #[test]
3199 fn value_outside_oneof_is_rejected() {
3200 let src = alloc::format!("{ONEOF_RESOLVED}FACT resolved is censoredmtp\n");
3201 let err = cs(&src).unwrap_err();
3202 let CompileError::UnknownValue(e) = err else {
3203 panic!("expected UnknownValue, got {err:?}");
3204 };
3205 assert_eq!(e.value, "censoredmtp");
3206 assert_eq!(e.subject, "resolved");
3207 assert_eq!(e.predicate, "is");
3208 assert_eq!(e.declared, "censored, censored_mtp, uncensored");
3209 }
3210
3211 #[test]
3212 fn near_miss_value_suggests_the_intended_one() {
3213 let src = alloc::format!("{ONEOF_RESOLVED}FACT resolved is censoredmtp\n");
3214 let CompileError::UnknownValue(e) = cs(&src).unwrap_err() else {
3215 panic!("expected UnknownValue");
3216 };
3217 assert_eq!(e.suggestion, " — did you mean `censored_mtp`?");
3218 }
3219
3220 #[test]
3221 fn far_off_value_offers_no_suggestion() {
3222 let src = alloc::format!("{ONEOF_RESOLVED}FACT resolved is wildly_different\n");
3225 let CompileError::UnknownValue(e) = cs(&src).unwrap_err() else {
3226 panic!("expected UnknownValue");
3227 };
3228 assert_eq!(e.suggestion, "");
3229 }
3230
3231 #[test]
3232 fn declared_value_compiles_cleanly() {
3233 let src = alloc::format!("{ONEOF_RESOLVED}FACT resolved is censored_mtp\n");
3234 assert!(cs(&src).is_ok());
3235 }
3236
3237 #[test]
3238 fn oneof_declared_after_the_reference_still_catches_it() {
3239 let src = alloc::format!("FACT resolved is censoredmtp\n{ONEOF_RESOLVED}");
3241 assert!(matches!(
3242 cs(&src).unwrap_err(),
3243 CompileError::UnknownValue(_)
3244 ));
3245 }
3246
3247 #[test]
3248 fn out_of_set_value_inside_a_premise_is_caught() {
3249 let src = alloc::format!(
3251 r"{ONEOF_RESOLVED}
3252 PREMISE p:
3253 WHEN resolved is censoredmtp
3254 THEN x done
3255 "
3256 );
3257 assert!(matches!(
3258 cs(&src).unwrap_err(),
3259 CompileError::UnknownValue(_)
3260 ));
3261 }
3262
3263 #[test]
3264 fn out_of_set_value_inside_a_rule_is_caught() {
3265 let src = alloc::format!(
3266 r"{ONEOF_RESOLVED}
3267 RULE r:
3268 WHEN x go
3269 THEN resolved is censoredmtp
3270 "
3271 );
3272 assert!(matches!(
3273 cs(&src).unwrap_err(),
3274 CompileError::UnknownValue(_)
3275 ));
3276 }
3277
3278 #[test]
3279 fn binary_atoms_in_a_oneof_do_not_close_anything() {
3280 let src = r"
3283 PREMISE chores:
3284 ONEOF
3285 alice cooks
3286 alice cleans
3287 FACT alice bakes
3288 ";
3289 assert!(cs(src).is_ok());
3290 }
3291
3292 #[test]
3293 fn a_subject_without_a_oneof_stays_open() {
3294 let src = alloc::format!("{ONEOF_RESOLVED}FACT mood is anything_goes\n");
3296 assert!(cs(&src).is_ok());
3297 }
3298
3299 #[test]
3300 fn two_oneofs_union_their_declared_values() {
3301 let src = r"
3303 PREMISE a:
3304 ONEOF
3305 v is one
3306 v is two
3307 PREMISE b:
3308 ONEOF
3309 v is two
3310 v is three
3311 FACT v is three
3312 ";
3313 assert!(cs(src).is_ok());
3314 }
3315
3316 #[test]
3317 fn earliest_offender_is_reported() {
3318 let src = alloc::format!(
3320 "{ONEOF_RESOLVED}FACT resolved is firstbad\nFACT resolved is secondbad\n"
3321 );
3322 let CompileError::UnknownValue(e) = cs(&src).unwrap_err() else {
3323 panic!("expected UnknownValue");
3324 };
3325 assert_eq!(e.value, "firstbad");
3326 }
3327
3328 #[test]
3329 fn closed_world_spans_imported_domains() {
3330 let mut r = MemoryResolver::new();
3333 r.add(
3334 "physics.vrf",
3335 r"
3336 DOMAIN physics
3337 PREMISE g:
3338 ONEOF
3339 Motor speed slow
3340 Motor speed fast
3341 ",
3342 );
3343 r.add(
3344 "main.vrf",
3345 "DOMAIN main\nIMPORT \"physics.vrf\"\nFACT physics.Motor speed faast\n",
3346 );
3347 let CompileError::UnknownValue(e) = compile("main.vrf", &r).unwrap_err() else {
3348 panic!("expected UnknownValue");
3349 };
3350 assert_eq!(e.value, "faast");
3351 assert_eq!(e.suggestion, " — did you mean `fast`?");
3352 }
3353
3354 #[test]
3355 fn same_value_in_a_different_domain_does_not_clash() {
3356 let mut r = MemoryResolver::new();
3359 r.add(
3360 "a.vrf",
3361 r"
3362 DOMAIN a
3363 PREMISE s:
3364 ONEOF
3365 state is open
3366 state is closed
3367 ",
3368 );
3369 r.add("b.vrf", "DOMAIN b\nIMPORT \"a.vrf\"\nFACT state is shut\n");
3370 assert!(compile("b.vrf", &r).is_ok());
3372 }
3373
3374 #[test]
3375 fn levenshtein_basics() {
3376 fn lev(a: &str, b: &str) -> usize {
3379 levenshtein(
3380 &a.chars().collect::<Vec<char>>(),
3381 &b.chars().collect::<Vec<char>>(),
3382 )
3383 }
3384 assert_eq!(lev("", ""), 0);
3385 assert_eq!(lev("abc", "abc"), 0);
3386 assert_eq!(lev("censoredmtp", "censored_mtp"), 1);
3387 assert_eq!(lev("norml", "normal"), 1);
3388 assert_eq!(lev("kitten", "sitting"), 3);
3389 }
3390
3391 #[test]
3392 fn nearest_respects_the_length_budget() {
3393 let cands = ["censored", "censored_mtp", "uncensored"];
3394 assert_eq!(nearest("censoredmtp", &cands), Some("censored_mtp"));
3395 assert_eq!(nearest("zzz", &cands), None);
3397 }
3398
3399 #[test]
3400 fn nearest_offers_nothing_for_very_short_values() {
3401 assert_eq!(nearest("七", &["一", "二", "三"]), None);
3405 assert_eq!(nearest("us", &["uk", "eu", "jp"]), None);
3406 assert_eq!(nearest("中文字", &["中文学", "日本語"]), Some("中文学"));
3409 }
3410
3411 #[test]
3412 fn short_value_is_still_rejected_just_without_a_guess() {
3413 let src = r"
3417 PREMISE pick:
3418 ONEOF
3419 roll is 一
3420 roll is 二
3421 FACT roll is 七
3422 ";
3423 let CompileError::UnknownValue(e) = cs(src).unwrap_err() else {
3424 panic!("expected UnknownValue");
3425 };
3426 assert_eq!(e.value, "七");
3427 assert_eq!(e.suggestion, "");
3428 }
3429
3430 #[test]
3433 fn for_each_grounds_once_per_element() {
3434 let src = r"
3438 SET xs
3439 a
3440 b
3441 PREMISE slot FOR EACH t IN xs:
3442 ONEOF
3443 t slot m
3444 t slot n
3445 ";
3446 let c = cs(src).unwrap();
3447 assert_eq!(c.clauses.len(), 4);
3448 for s in ["a", "b"] {
3449 for o in ["m", "n"] {
3450 assert!(c.atoms.contains(&key(s, "slot", Some(o))));
3451 }
3452 }
3453 }
3454
3455 #[test]
3456 fn for_each_in_a_rule_derives_per_element() {
3457 let src = r"
3459 SET xs
3460 a
3461 b
3462 RULE r FOR EACH t IN xs:
3463 WHEN t on
3464 THEN t hot
3465 ";
3466 let c = cs(src).unwrap();
3467 assert_eq!(c.rules.len(), 2);
3468 }
3469
3470 #[test]
3471 fn for_each_over_an_undeclared_set_is_rejected() {
3472 let src = r"
3473 SET tasks
3474 a
3475 PREMISE p FOR EACH t IN taske:
3476 ONEOF
3477 t s x
3478 t s y
3479 ";
3480 let CompileError::UnknownSet {
3481 set, suggestion, ..
3482 } = cs(src).unwrap_err()
3483 else {
3484 panic!("expected UnknownSet");
3485 };
3486 assert_eq!(set, "taske");
3487 assert_eq!(suggestion, " — did you mean `tasks`?");
3488 }
3489
3490 #[test]
3491 fn for_each_closes_each_grounded_variable() {
3492 let src = r"
3495 SET xs
3496 a
3497 b
3498 PREMISE c FOR EACH t IN xs:
3499 ONEOF
3500 t color red
3501 t color blue
3502 FACT a color gren
3503 ";
3504 let CompileError::UnknownValue(e) = cs(src).unwrap_err() else {
3505 panic!("expected UnknownValue from the grounded ONEOF");
3506 };
3507 assert_eq!(e.value, "gren");
3508 assert_eq!(e.subject, "a");
3509 }
3510
3511 #[test]
3512 fn nested_for_each_is_a_parse_error() {
3513 let src = r"
3517 SET xs
3518 a
3519 PREMISE p FOR EACH x IN xs FOR EACH y IN xs:
3520 ONEOF
3521 x r y
3522 x s y
3523 ";
3524 assert!(matches!(cs(src), Err(CompileError::Parse(_))));
3525 }
3526
3527 #[test]
3528 fn relation_for_each_grounds_per_fact_pair() {
3529 let src = r"
3533 FACT a linked b
3534 FACT b linked c
3535 PREMISE p FOR EACH x linked y:
3536 FORBIDS
3537 x hot on
3538 y hot on
3539 ";
3540 let c = cs(src).unwrap();
3541 assert_eq!(c.clauses.len(), 2);
3542 assert_eq!(c.consumed.len(), 2);
3543 assert!(c.consumed.contains(&id(&c, &key("a", "linked", Some("b")))));
3544 }
3545
3546 #[test]
3547 fn relation_for_each_over_no_edges_is_inert() {
3548 let src = r"
3551 PREMISE p FOR EACH x linked y:
3552 FORBIDS
3553 x hot on
3554 y hot on
3555 ";
3556 let c = cs(src).unwrap();
3557 assert_eq!(c.clauses.len(), 0);
3558 assert!(c.consumed.is_empty());
3559 }
3560
3561 #[test]
3562 fn close_transitive_extends_the_relation() {
3563 let src = r"
3566 FACT a r b
3567 FACT b r c
3568 CLOSE r TRANSITIVE
3569 PREMISE p FOR EACH x r y:
3570 FORBIDS
3571 x hot on
3572 y hot on
3573 ";
3574 let c = cs(src).unwrap();
3575 assert_eq!(c.clauses.len(), 3);
3576 }
3577
3578 #[test]
3579 fn close_transitive_rejects_a_cycle() {
3580 let src = r"
3581 FACT a r b
3582 FACT b r a
3583 CLOSE r TRANSITIVE
3584 ";
3585 let CompileError::CyclicRelation { relation, .. } = cs(src).unwrap_err() else {
3586 panic!("expected CyclicRelation");
3587 };
3588 assert_eq!(relation, "r");
3589 }
3590
3591 #[test]
3592 fn grounding_count_is_linear_in_the_set() {
3593 let elems: alloc::string::String = (0..20).map(|i| alloc::format!(" e{i}\n")).collect();
3596 let src = alloc::format!(
3597 "SET xs\n{elems}PREMISE p FOR EACH t IN xs:\n ATLEAST\n t a\n t b\n"
3598 );
3599 let c = cs(&src).unwrap();
3600 assert_eq!(c.clauses.len(), 20);
3601 }
3602}