Skip to main content

elenchus_compiler/
lib.rs

1//! elenchus-compiler — compiles parsed elenchus DSL into a canonical clause IR.
2//!
3//! This crate is **preparation, not solving**. It takes the AST (from
4//! `elenchus-parser`) and produces a deterministic, solver-ready intermediate
5//! representation:
6//!
7//! - **atom interner**: `(subject, predicate, object?)` → dense `u32` id,
8//!   canonically sorted so ids (and any later enumeration) are deterministic;
9//! - **desugaring**: surface CAPS sugar → `Impossible` clauses
10//!   (`EXCLUSIVE` pairwise, `WHEN…THEN` → `Impossible([A, …, NOT C])`, etc.);
11//! - **content-addressing** (sha256, mirroring vsm-guard's CAS): identical
12//!   clauses are deduped (idempotent — `P ∧ P ≡ P`), and a named construct
13//!   redefined with a different body is a `PremiseRedefinition` error.
14//!
15//! The actual reasoning (3-valued forward chaining, SAT, all-SAT, the WARNING
16//! pool, the four results) lives in `elenchus-solver`. `IMPORT` resolution is a
17//! source-agnostic [`Resolver`] that flat-merges another source into the shared
18//! atom universe ([`compile`] resolves imports; [`compile_source`] leaves them
19//! pending).
20//!
21//! # Example
22//!
23//! ```
24//! use elenchus_compiler::compile_source;
25//!
26//! // `ASSUME` lowers to a *soft* fact: the same atom universe as a `FACT`, but
27//! // one the solver may retract. Here `x a` is asserted both ways (hard + soft).
28//! let ir = compile_source("demo.vrf", "DOMAIN d\nFACT x a\nASSUME NOT x a\nCHECK x\n").unwrap();
29//! assert_eq!(ir.facts.len(), 2);
30//! assert!(ir.facts.iter().any(|f| f.soft)); // the ASSUME is the soft one
31//! ```
32#![no_std]
33// Every public item is documented; CI (`clippy -D warnings`) keeps it that way.
34#![warn(missing_docs)]
35
36extern crate alloc;
37
38#[cfg(feature = "std")]
39extern crate std;
40
41mod closure;
42mod compiler;
43mod data;
44mod domain;
45mod error;
46mod ir;
47mod ports;
48mod resolver;
49mod sig;
50mod subst;
51
52use alloc::string::String;
53use core::fmt::Write as _;
54use sha2::{Digest, Sha256};
55
56use resolver::resolve_graph;
57
58pub use compiler::Compiler;
59pub use data::{read_data_bindings, read_data_source};
60/// Re-exported so downstream crates name one source of truth: [`Diagnostics`] for
61/// the syntax errors carried by [`CompileError::Parse`], and [`kw`] for the
62/// keyword spellings an [`Origin::kind`] is built from (so the solver matches a
63/// `kind` against `kw::PREMISE`, not a re-typed `"PREMISE"`).
64pub use elenchus_parser::{Diagnostics, kw};
65pub use error::{CompileError, UnknownValue, levenshtein};
66pub use ir::{
67    AtomId, AtomKey, Check, Clause, Compiled, Fact, KIND_UNSAT, Lit, Origin, PlaceholderInfo,
68    PlaceholderStatus, PortBinding, Rule, UnusedImport, Value,
69};
70#[cfg(feature = "std")]
71pub use resolver::FileResolver;
72pub use resolver::{MemoryResolver, Resolver, normalize_import_path};
73
74// --- content-addressing (mirrors vsm-guard::hashing) -----------------------
75
76/// SHA-256 content addressing. Used only for dedup / redefinition / provenance,
77/// never for namespacing atoms.
78pub fn hash_hex(data: &[u8]) -> String {
79    let mut hasher = Sha256::new();
80    hasher.update(data);
81    let out = hasher.finalize();
82    let mut s = String::with_capacity(out.len() * 2);
83    for b in out {
84        let _ = write!(s, "{:02x}", b);
85    }
86    s
87}
88
89/// Convenience: compile a single source into the IR. `IMPORT`s are recorded as
90/// pending, not resolved (use [`compile`] with a [`Resolver`] to resolve them).
91pub fn compile_source(source: &str, src: &str) -> Result<Compiled, CompileError> {
92    compile_source_with(source, src, &[])
93}
94
95/// Like [`compile_source`], but resolving declared `VAR` ports against external
96/// `inputs` (`(name, binding)` pairs from CLI / API / data). The `Compiled` carries
97/// a `placeholders` record per declared port.
98pub fn compile_source_with(
99    source: &str,
100    src: &str,
101    inputs: &[(String, PortBinding)],
102) -> Result<Compiled, CompileError> {
103    let mut c = Compiler::new();
104    c.add_source(source, src)?;
105    c.validate_closed_world()?;
106    let placeholders = c.resolve_ports(inputs)?;
107    let mut compiled = c.finalize();
108    compiled.placeholders = placeholders;
109    Ok(compiled)
110}
111
112/// Compile a root source and all its transitive `IMPORT`s into one IR.
113///
114/// Each file is keyed by `DOMAIN`; atoms unify only within a domain. Imports are
115/// referenced by `<domain>.<atom>` and visibility is file-local (naming is not
116/// transitive, though a dependency's clauses still participate). Sources are
117/// content-addressed (sha256): a source reached by several paths is compiled once
118/// (so a diamond — or an exponential fan-out — stays linear, never blowing up),
119/// and an import cycle is an error.
120///
121/// Resolution is **iterative** (an explicit work stack, not native recursion), so
122/// an arbitrarily deep import chain cannot overflow the call stack.
123///
124/// Premise/rule names are per-source labels, not global identifiers: different
125/// files may reuse a name, and the report qualifies them by source. A name reused
126/// with a different body is an error only *within the same source*.
127pub fn compile<R: Resolver>(root: &str, resolver: &R) -> Result<Compiled, CompileError> {
128    compile_with(root, resolver, &[])
129}
130
131/// Like [`compile`], but resolving declared `VAR` ports against external `inputs`.
132/// Ports declared in any file of the import graph are aggregated, then resolved
133/// once after every file is added.
134pub fn compile_with<R: Resolver>(
135    root: &str,
136    resolver: &R,
137    inputs: &[(String, PortBinding)],
138) -> Result<Compiled, CompileError> {
139    let (files, unused_imports) = resolve_graph(root, resolver)?;
140    let mut c = Compiler::new();
141    for file in &files {
142        c.add_resolved(file)?;
143    }
144    c.validate_closed_world()?;
145    let placeholders = c.resolve_ports(inputs)?;
146    let mut compiled = c.finalize();
147    compiled.unused_imports = unused_imports;
148    compiled.placeholders = placeholders;
149    Ok(compiled)
150}