elenchus_solver/lib.rs
1//! elenchus-solver — the inference interpreter (forward pass).
2//!
3//! Consumes the [`Compiled`] IR from `elenchus-compiler` and evaluates it under
4//! three-valued Kleene logic (TRUE / FALSE / UNKNOWN, where UNKNOWN ≠ FALSE):
5//!
6//! 1. seed a model from confident `FACT`/`NOT` facts (and report `FACT X` + `NOT X`);
7//! 2. forward-chain `RULE`s to a fixpoint, deriving facts (a derived value that
8//! contradicts an existing one is a CONFLICT);
9//! 3. evaluate every `Impossible` clause (the desugared `PREMISE`s):
10//! - all literals forced TRUE → **CONFLICT** (the constraint is violated);
11//! - some literal FALSE → satisfied → CONSISTENT;
12//! - otherwise (no FALSE, an UNKNOWN remains): for implication premises this is a
13//! **WARNING** (blocked by missing data), for list premises (EXCLUSIVE/FORBIDS/
14//! ONEOF/ATLEAST) it is CONSISTENT (UNKNOWN means "no conflict yet").
15//!
16//! On `CHECK ... BIDIRECTIONAL` a **backward pass** also runs: the premises, rules
17//! and confident facts are encoded as CNF and handed to a small in-crate CDCL SAT
18//! core ([`sat`], replicating varisat's algorithm) to count models — 0 means the
19//! system is jointly unsatisfiable (a CONFLICT the forward pass may miss), ≥2
20//! means an alternative model exists (`UNDERDETERMINED`).
21//!
22//! # Example
23//!
24//! ```
25//! use elenchus_solver::{Status, verify_source};
26//!
27//! // `A has flying` fires the premise, but `A has wing` was never stated — so
28//! // the engine cannot confirm the rule and reports WARNING (not CONFLICT).
29//! let report = verify_source(
30//! "demo.vrf",
31//! "DOMAIN d\nFACT A has flying\nPREMISE w:\n WHEN A has flying\n THEN A has wing\n",
32//! )
33//! .unwrap();
34//! assert_eq!(report.status, Status::Warning); // `A has wing` is UNKNOWN
35//! println!("{report}"); // the full human report, ready to show a model
36//! ```
37#![no_std]
38// Every public item is documented; CI (`clippy -D warnings`) keeps it that way.
39#![warn(missing_docs)]
40
41extern crate alloc;
42
43#[cfg(feature = "std")]
44extern crate std;
45
46pub mod sat;
47
48mod analysis;
49mod cnf;
50mod eval;
51mod report;
52mod unsat;
53mod v3;
54
55use alloc::string::String;
56use alloc::vec::Vec;
57
58use elenchus_compiler::Compiled;
59
60use crate::analysis::{orphan_facts, similar_atom_pairs};
61use crate::eval::Eval;
62use crate::unsat::retract_assumptions;
63
64/// Re-exported so library users handling a [`CompileError::Parse`] can render the
65/// syntax diagnostics with their own error limit (e.g. CLI `--max-errors`).
66pub use elenchus_compiler::Diagnostics;
67/// The filesystem-backed resolver (reads `IMPORT`s from disk). Only with `std`.
68#[cfg(feature = "std")]
69pub use elenchus_compiler::FileResolver;
70pub use elenchus_compiler::{
71 CompileError, MemoryResolver, PlaceholderInfo, PlaceholderStatus, PortBinding, Resolver,
72 UnusedImport, compile, compile_source, compile_source_with, compile_with,
73 normalize_import_path, read_data_bindings, read_data_source,
74};
75pub use report::{
76 Conflict, CoreItem, Derived, OrphanFact, Report, SimilarAtoms, Status, TraceReason, TraceStep,
77 Warning,
78};
79pub use v3::V3;
80
81/// The engine version (this crate's package version). Exposed so a wrapper —
82/// e.g. the wasm/npm build, which carries its own, independent package version —
83/// can report the *engine* version (and compare it to a skill's
84/// `<!-- skill-version -->` marker) rather than its own.
85pub const VERSION: &str = env!("CARGO_PKG_VERSION");
86
87/// Evaluate a compiled program: the three-valued forward pass, then the backward
88/// pass on `BIDIRECTIONAL`.
89pub fn solve(c: &Compiled) -> Report {
90 let mut e = Eval::new(c);
91 e.seed_facts();
92 e.saturate_rules();
93 e.check_premises();
94 let mut report = e.finish();
95 // If the program is a CONFLICT but the facts/premises are consistent on their
96 // own, the `ASSUME` hypotheses are what break it: name which to retract. The
97 // verdict stays CONFLICT — this only adds the "drop one of these" hint and,
98 // when it applies, supersedes the raw conflict/unsat-core pools (which would
99 // otherwise point at the assumption clause itself).
100 if report.status == Status::Conflict {
101 let retract = retract_assumptions(c);
102 if !retract.is_empty() {
103 report.unsat_core = Vec::new();
104 report.retract = retract;
105 }
106 }
107 // Advisory only: surface likely atom-name typos. Computed after the verdict
108 // so it can never influence status/exit code.
109 report.hints = similar_atom_pairs(c);
110 // Advisory only: surface logically-inert assertions (orphan facts). Also
111 // post-verdict, so it can never influence status/exit code.
112 report.orphans = orphan_facts(c);
113 // Advisory only: imports a file never references (computed at compile time,
114 // carried through the IR). Never influences status/exit code.
115 report.unused_imports = c.unused_imports.clone();
116 // Advisory only: the per-port placeholders record (computed at compile time).
117 // Never influences status/exit code.
118 report.placeholders = c.placeholders.clone();
119 report
120}
121
122/// Parse → compile → solve a single source.
123pub fn verify_source(name: &str, src: &str) -> Result<Report, CompileError> {
124 verify_source_with(name, src, &[])
125}
126
127/// Like [`verify_source`], but resolving declared `VAR` ports against external
128/// `inputs` (`(name, binding)` pairs from CLI / API / data).
129pub fn verify_source_with(
130 name: &str,
131 src: &str,
132 inputs: &[(String, PortBinding)],
133) -> Result<Report, CompileError> {
134 Ok(solve(&compile_source_with(name, src, inputs)?))
135}
136
137/// Parse → compile (resolving imports) → solve, given a [`Resolver`].
138pub fn verify<R: Resolver>(root: &str, resolver: &R) -> Result<Report, CompileError> {
139 verify_with(root, resolver, &[])
140}
141
142/// Like [`verify`], but resolving declared `VAR` ports against external `inputs`.
143pub fn verify_with<R: Resolver>(
144 root: &str,
145 resolver: &R,
146 inputs: &[(String, PortBinding)],
147) -> Result<Report, CompileError> {
148 Ok(solve(&compile_with(root, resolver, inputs)?))
149}