Skip to main content

logicaffeine_proof/
development.rs

1//! A formal-development parser: the body of a `## Theory` block — a sequence of `Axiom`
2//! and `Theorem` declarations in formal notation — into the axioms and
3//! [`LibraryTheorem`](crate::verify::LibraryTheorem)s the multi-theorem driver consumes.
4//!
5//! This is the surface for an axiomatic development like Tarski geometry: a shared axiom
6//! base plus dependency-ordered theorems, each citing earlier ones, all discharged and
7//! kernel-certified by [`prove_library_with_axioms`](crate::verify::prove_library_with_axioms).
8//!
9//! ```text
10//! Axiom flip: for all a b, Cong(a, b, b, a).
11//! Axiom inner_trans: for all a b c d e f,
12//!     if Cong(a,b,c,d) and Cong(a,b,e,f) then Cong(c,d,e,f).
13//! Theorem reflexivity: prove for all a b, Cong(a, b, a, b).
14//! Theorem symmetry cites reflexivity:
15//!     prove for all a b c d, if Cong(a,b,c,d) then Cong(c,d,a,b).
16//! Theorem null_segment: given Cong(P, Q, R, R); prove P = Q.
17//! ```
18//!
19//! Grammar (statements terminated by `.`):
20//! * `Axiom <name> : <formula>`
21//! * `Theorem <name> [cites <n1>, <n2>, …] : [given <formula> ;]* prove <formula>`
22//!
23//! Clauses within a theorem are separated by `;`; cited lemma names by `,`. Formulas are
24//! handed to [`parse_formula`](crate::formula::parse_formula).
25
26use crate::formula::{parse_formula, FormulaError};
27use crate::verify::{prove_library_with_axioms, LibraryResult, LibraryTheorem};
28use crate::ProofExpr;
29
30/// A parsed formal development: a shared axiom base and the theorems built on it.
31#[derive(Debug, Clone, PartialEq)]
32pub struct Development {
33    /// Named axioms (the name is for diagnostics/citation; the prover uses the formula).
34    pub axioms: Vec<(String, ProofExpr)>,
35    /// Theorems, in source order, each with its premises, goal, and cited lemma names.
36    pub theorems: Vec<LibraryTheorem>,
37    /// Names of `[simp]`-tagged declarations (axioms and theorems), in source
38    /// order — the development's default rewrite-rule set.
39    pub simp_tagged: Vec<String>,
40}
41
42impl Development {
43    /// The axiom formulas, in declaration order — the shared base for the driver.
44    pub fn axiom_exprs(&self) -> Vec<ProofExpr> {
45        self.axioms.iter().map(|(_, e)| e.clone()).collect()
46    }
47
48    /// The names tagged `[simp]`, in source order.
49    pub fn simp_lemmas(&self) -> &[String] {
50        &self.simp_tagged
51    }
52}
53
54/// Parse a `## Theory` body into a [`Development`].
55pub fn parse_development(body: &str) -> Result<Development, FormulaError> {
56    let mut axioms = Vec::new();
57    let mut theorems = Vec::new();
58
59    let mut simp_tagged = Vec::new();
60
61    for stmt in split_on_top_level(body, '.') {
62        let stmt = stmt.trim();
63        if stmt.is_empty() {
64            continue;
65        }
66        let words: Vec<&str> = stmt.split_whitespace().collect();
67        match words.first().map(|w| w.to_lowercase()).as_deref() {
68            Some("axiom") => {
69                let (decl, simp) = parse_axiom_decl(stmt)?;
70                if simp {
71                    simp_tagged.push(decl.0.clone());
72                }
73                axioms.push(decl);
74            }
75            Some("theorem") => {
76                let (thm, simp) = parse_theorem_decl(stmt)?;
77                if simp {
78                    simp_tagged.push(thm.name.clone());
79                }
80                theorems.push(thm);
81            }
82            other => {
83                return Err(FormulaError::new(format!(
84                    "expected a declaration starting with 'Axiom' or 'Theorem', found {other:?}"
85                )))
86            }
87        }
88    }
89
90    Ok(Development { axioms, theorems, simp_tagged })
91}
92
93/// Parse a `## Theory` body and discharge every theorem against the development's axioms
94/// (and cited lemmas), in citation order — each kernel-certified. Results are in source
95/// order, paired with the theorem name.
96pub fn prove_development(body: &str) -> Result<Vec<(String, LibraryResult)>, FormulaError> {
97    let dev = parse_development(body)?;
98    let axioms = dev.axiom_exprs();
99    let results = prove_library_with_axioms(&axioms, &dev.theorems);
100    Ok(dev
101        .theorems
102        .iter()
103        .map(|t| t.name.clone())
104        .zip(results)
105        .collect())
106}
107
108// ---------------------------------------------------------------------------
109// Declaration parsing
110// ---------------------------------------------------------------------------
111
112/// The `[simp]` attribute token, written between a declaration's name and the
113/// rest of its header. Returns the header words with the attribute removed and
114/// whether it was present.
115fn strip_simp_attr<'a>(words: &[&'a str]) -> (Vec<&'a str>, bool) {
116    let mut simp = false;
117    let kept = words
118        .iter()
119        .filter(|w| {
120            if w.eq_ignore_ascii_case("[simp]") {
121                simp = true;
122                false
123            } else {
124                true
125            }
126        })
127        .copied()
128        .collect();
129    (kept, simp)
130}
131
132/// `Axiom <name> [simp] : <formula>`
133fn parse_axiom_decl(stmt: &str) -> Result<((String, ProofExpr), bool), FormulaError> {
134    let (header, formula_src) = split_once_top_level(stmt, ':').ok_or_else(|| {
135        FormulaError::new("an 'Axiom' declaration needs a ':' before its formula")
136    })?;
137    let hwords: Vec<&str> = header.split_whitespace().collect();
138    let (hwords, simp) = strip_simp_attr(&hwords);
139    let name = hwords
140        .get(1)
141        .ok_or_else(|| FormulaError::new("'Axiom' declaration is missing a name"))?
142        .to_string();
143    let formula = parse_formula(&formula_src)?;
144    Ok(((name, formula), simp))
145}
146
147/// `Theorem <name> [simp] [cites <n1>, <n2>, …] : [given <formula> ;]* prove <formula>`
148fn parse_theorem_decl(stmt: &str) -> Result<(LibraryTheorem, bool), FormulaError> {
149    let (header, body) = split_once_top_level(stmt, ':').ok_or_else(|| {
150        FormulaError::new("a 'Theorem' declaration needs a ':' before its clauses")
151    })?;
152
153    // Header: `Theorem <name> [simp] [cites/using/from <n1>, <n2>, …]`.
154    let hwords: Vec<&str> = header.split_whitespace().collect();
155    let (hwords, simp) = strip_simp_attr(&hwords);
156    let name = hwords
157        .get(1)
158        .ok_or_else(|| FormulaError::new("'Theorem' declaration is missing a name"))?
159        .to_string();
160    let mut cites = Vec::new();
161    if let Some(kw) = hwords.get(2) {
162        if matches!(kw.to_lowercase().as_str(), "cites" | "using" | "from") {
163            for w in &hwords[3..] {
164                for part in w.split(',') {
165                    let part = part.trim();
166                    if !part.is_empty() {
167                        cites.push(part.to_string());
168                    }
169                }
170            }
171        }
172    }
173
174    // Clauses: `given <formula>` (zero or more) then `prove <formula>`. A new clause
175    // begins at each `given`/`assume`/`prove`/`show` KEYWORD — no separator punctuation is
176    // required, so this is robust to the surface NL lexer dropping a `;` (and any stray
177    // `;`/`.` left in a formula is absorbed by the formula tokenizer). Formal formulas never
178    // contain these keywords, so keyword-delimited splitting is unambiguous.
179    let is_clause_kw = |w: &str| matches!(w, "given" | "assume" | "prove" | "show");
180    let words: Vec<&str> = body.split_whitespace().collect();
181    let mut premises = Vec::new();
182    let mut goal: Option<ProofExpr> = None;
183    let mut i = 0;
184    while i < words.len() {
185        let kw = words[i].to_lowercase();
186        if !is_clause_kw(&kw) {
187            // A stray token before the first clause keyword (e.g. a lone `;`): skip it.
188            i += 1;
189            continue;
190        }
191        i += 1;
192        let start = i;
193        while i < words.len() && !is_clause_kw(&words[i].to_lowercase()) {
194            i += 1;
195        }
196        let formula_src = words[start..i].join(" ");
197        match kw.as_str() {
198            "given" | "assume" => premises.push(parse_formula(&formula_src)?),
199            _ /* prove | show */ => {
200                if goal.is_some() {
201                    return Err(FormulaError::new(format!(
202                        "theorem '{name}' has more than one 'prove' clause"
203                    )));
204                }
205                goal = Some(parse_formula(&formula_src)?);
206            }
207        }
208    }
209
210    let goal = goal.ok_or_else(|| {
211        FormulaError::new(format!("theorem '{name}' has no 'prove' clause"))
212    })?;
213    Ok((LibraryTheorem { name, premises, goal, cites }, simp))
214}
215
216// ---------------------------------------------------------------------------
217// Delimiter splitting (paren-depth aware, so a delimiter inside `(…)` is ignored)
218// ---------------------------------------------------------------------------
219
220/// Split `s` on every top-level (paren-depth 0) occurrence of `delim`.
221fn split_on_top_level(s: &str, delim: char) -> Vec<String> {
222    let mut out = Vec::new();
223    let mut depth = 0i32;
224    let mut cur = String::new();
225    for c in s.chars() {
226        match c {
227            '(' => {
228                depth += 1;
229                cur.push(c);
230            }
231            ')' => {
232                depth -= 1;
233                cur.push(c);
234            }
235            c if c == delim && depth == 0 => {
236                out.push(std::mem::take(&mut cur));
237            }
238            c => cur.push(c),
239        }
240    }
241    out.push(cur);
242    out
243}
244
245/// Split `s` at the FIRST top-level occurrence of `delim` into `(before, after)`.
246fn split_once_top_level(s: &str, delim: char) -> Option<(String, String)> {
247    let mut depth = 0i32;
248    for (i, c) in s.char_indices() {
249        match c {
250            '(' => depth += 1,
251            ')' => depth -= 1,
252            c if c == delim && depth == 0 => {
253                return Some((s[..i].to_string(), s[i + c.len_utf8()..].to_string()));
254            }
255            _ => {}
256        }
257    }
258    None
259}