Skip to main content

logicaffeine_proof/
tactic.rs

1//! A tactic framework with a first-class goal state — the interactive proof
2//! interface (ROOT R5 of the Lean-competitive architecture).
3//!
4//! A [`ProofState`] holds the open [`Goal`]s; a *tactic* transforms the focused
5//! goal, replacing it with zero or more subgoals and recording the inference rule
6//! used. When no goals remain, [`ProofState::qed`] assembles the recorded steps
7//! into a single [`DerivationTree`] and runs it through the SAME trust door as
8//! every other proof ([`check_derivation`]): the certifier turns it into a kernel
9//! term and the kernel re-checks that term against the goal. So a tactic proof is
10//! exactly as trusted as an automated one — the tactics only *build* the
11//! derivation the kernel validates, they are never themselves trusted.
12//!
13//! The backward chainer is hosted here as the [`ProofState::auto`] tactic, so the
14//! existing automation is one tactic among many rather than the whole story.
15
16use std::collections::VecDeque;
17
18use crate::engine::BackwardChainer;
19use crate::unify::{apply_subst_to_expr, unify_exprs, Substitution};
20use crate::verify::{check_derivation, VerifiedProof};
21use crate::{
22    DerivationTree, InductionArg, InductionCase, InferenceRule, ProofExpr, ProofTerm,
23};
24
25/// A local hypothesis: its `name`, its proposition `prop`, and `proof` — a
26/// derivation that proves `prop` in the current local context. For a hypothesis
27/// that is directly in scope (a premise, an `intro`-discharged antecedent, or a
28/// branch hypothesis bound by `cases`) the proof is a `PremiseMatch` leaf the
29/// certifier resolves against the bound/registered hypothesis. For a conjunct
30/// extracted by `cases` from `A ∧ B`, the proof is the `ConjunctionElim`
31/// PROJECTION of the parent — so using the conjunct emits the projection, not a
32/// dangling reference. This is what lets elimination compose with the rest.
33#[derive(Debug, Clone)]
34pub struct Hyp {
35    pub name: String,
36    pub prop: ProofExpr,
37    proof: DerivationTree,
38}
39
40/// One constructor of an inductive, as given to [`ProofState::induction_over`]: its
41/// name and, per positional argument, whether that argument is itself of the
42/// inductive type — a recursive position that carries an induction hypothesis.
43/// `Nil` is `CtorSpec { constructor: "Nil", recursive: vec![] }`; `Cons` (head then
44/// tail) is `CtorSpec { constructor: "Cons", recursive: vec![false, true] }`.
45#[derive(Debug, Clone)]
46pub struct CtorSpec {
47    /// The constructor's name, as registered in the kernel (e.g. `"Cons"`).
48    pub constructor: String,
49    /// One flag per argument, in order: `true` iff that argument is recursive.
50    pub recursive: Vec<bool>,
51}
52
53/// A single proof obligation: prove `target` under the local `hyps`.
54#[derive(Debug, Clone)]
55pub struct Goal {
56    /// Named local hypotheses in scope (introduced by `intro`/`cases`, or premises).
57    pub hyps: Vec<Hyp>,
58    /// The proposition to prove.
59    pub target: ProofExpr,
60}
61
62/// What went wrong applying a tactic.
63#[derive(Debug, Clone, PartialEq)]
64pub enum TacticError {
65    /// No focused goal — the proof state is already complete.
66    NoOpenGoals,
67    /// The tactic does not apply to the focused goal's shape (e.g. `intro` on an atom).
68    DoesNotApply(String),
69    /// No hypothesis (or premise) matches the goal for `assumption`/`exact`.
70    NoSuchHypothesis(String),
71    /// The backward chainer could not close the goal.
72    AutoFailed,
73    /// `qed` was called with goals still open.
74    GoalsRemain(usize),
75}
76
77/// A child of a refinement: either an already-complete proof or a fresh subgoal.
78enum Child {
79    Closed(DerivationTree),
80    Sub(Goal),
81}
82
83/// A node in the proof under construction.
84#[derive(Clone)]
85enum Node {
86    /// An unproved goal (a metavariable / hole).
87    Hole(Goal),
88    /// A complete sub-derivation (closed by `exact`/`assumption`/`auto`).
89    Done(DerivationTree),
90    /// A refinement: `rule` applied with the given children (indices into `nodes`).
91    Filled {
92        conclusion: ProofExpr,
93        rule: InferenceRule,
94        children: Vec<usize>,
95    },
96}
97
98/// The interactive proof state: the partial derivation plus the queue of open
99/// goals (front = focused). Tactics consume the focused goal and push any subgoals
100/// they create back to the front, so the proof is built depth-first, first-subgoal
101/// first — the order a reader expects. `Clone` is cheap relative to a proof and is
102/// what lets the backtracking combinators (`first`/`try`/`repeat`) speculate.
103#[derive(Clone)]
104pub struct ProofState {
105    premises: Vec<ProofExpr>,
106    goal: ProofExpr,
107    nodes: Vec<Node>,
108    root: usize,
109    open: VecDeque<usize>,
110    fresh: usize,
111}
112
113/// Replace every occurrence of the term `from` with `to` inside a term.
114fn replace_in_term(t: &ProofTerm, from: &ProofTerm, to: &ProofTerm) -> ProofTerm {
115    if t == from {
116        return to.clone();
117    }
118    match t {
119        ProofTerm::Function(n, args) => ProofTerm::Function(
120            n.clone(),
121            args.iter().map(|a| replace_in_term(a, from, to)).collect(),
122        ),
123        ProofTerm::Group(args) => {
124            ProofTerm::Group(args.iter().map(|a| replace_in_term(a, from, to)).collect())
125        }
126        other => other.clone(),
127    }
128}
129
130/// Replace every occurrence of the term `from` with `to` throughout an expression —
131/// the Leibniz substitution `rewrite` performs on the goal.
132fn replace_in_expr(e: &ProofExpr, from: &ProofTerm, to: &ProofTerm) -> ProofExpr {
133    let re = |x: &ProofExpr| Box::new(replace_in_expr(x, from, to));
134    let rt = |x: &ProofTerm| replace_in_term(x, from, to);
135    match e {
136        ProofExpr::Predicate { name, args, world } => ProofExpr::Predicate {
137            name: name.clone(),
138            args: args.iter().map(rt).collect(),
139            world: world.clone(),
140        },
141        ProofExpr::Identity(l, r) => ProofExpr::Identity(rt(l), rt(r)),
142        ProofExpr::Atom(s) => ProofExpr::Atom(s.clone()),
143        ProofExpr::And(l, r) => ProofExpr::And(re(l), re(r)),
144        ProofExpr::Or(l, r) => ProofExpr::Or(re(l), re(r)),
145        ProofExpr::Implies(l, r) => ProofExpr::Implies(re(l), re(r)),
146        ProofExpr::Iff(l, r) => ProofExpr::Iff(re(l), re(r)),
147        ProofExpr::Not(p) => ProofExpr::Not(re(p)),
148        ProofExpr::ForAll { variable, body } => {
149            ProofExpr::ForAll { variable: variable.clone(), body: re(body) }
150        }
151        ProofExpr::Exists { variable, body } => {
152            ProofExpr::Exists { variable: variable.clone(), body: re(body) }
153        }
154        other => other.clone(),
155    }
156}
157
158/// Rewrite the eigen-`Constant` `name` back to `Variable(name)` throughout a
159/// derivation — the generalization step for `induction`'s step case, turning the
160/// rigid search variable into the one the certifier's `Match` arm binds.
161fn eigen_to_var_tree(tree: &DerivationTree, name: &str) -> DerivationTree {
162    let from = ProofTerm::Constant(name.to_string());
163    let to = ProofTerm::Variable(name.to_string());
164    let rule = match &tree.rule {
165        InferenceRule::Rewrite { from: rf, to: rt } => InferenceRule::Rewrite {
166            from: replace_in_term(rf, &from, &to),
167            to: replace_in_term(rt, &from, &to),
168        },
169        other => other.clone(),
170    };
171    DerivationTree {
172        conclusion: replace_in_expr(&tree.conclusion, &from, &to),
173        rule,
174        premises: tree.premises.iter().map(|p| eigen_to_var_tree(p, name)).collect(),
175        depth: tree.depth,
176        substitution: tree.substitution.clone(),
177    }
178}
179
180/// A hypothesis directly in scope (premise / `intro` / `cases`-branch): its proof
181/// is a `PremiseMatch` leaf the certifier resolves against the bound or registered
182/// hypothesis of the same proposition.
183fn direct_hyp(name: String, prop: ProofExpr) -> Hyp {
184    let proof = DerivationTree::leaf(prop.clone(), InferenceRule::PremiseMatch);
185    Hyp { name, prop, proof }
186}
187
188impl ProofState {
189    /// Begin proving `goal` from `premises`, naming the hypotheses `hp0`, `hp1`, ….
190    pub fn start(premises: Vec<ProofExpr>, goal: ProofExpr) -> Self {
191        Self::start_with_names(premises, &[], goal)
192    }
193
194    /// Begin proving `goal` from `premises`, using the given `names` for the
195    /// hypotheses where present (parallel to `premises`), falling back to the
196    /// positional `hp{i}` otherwise. Lets a surface `Given (h): …` name a premise so a
197    /// `Proof:` script can say `cases h` instead of `cases hp0`.
198    pub fn start_with_names(
199        premises: Vec<ProofExpr>,
200        names: &[Option<String>],
201        goal: ProofExpr,
202    ) -> Self {
203        let hyps = premises
204            .iter()
205            .enumerate()
206            .map(|(i, p)| {
207                let name = names
208                    .get(i)
209                    .and_then(|n| n.as_ref())
210                    .cloned()
211                    .unwrap_or_else(|| format!("hp{i}"));
212                direct_hyp(name, p.clone())
213            })
214            .collect();
215        let root_goal = Goal { hyps, target: goal.clone() };
216        ProofState {
217            premises,
218            goal,
219            nodes: vec![Node::Hole(root_goal)],
220            root: 0,
221            open: VecDeque::from([0]),
222            fresh: 0,
223        }
224    }
225
226    /// A fresh entity-constant name for an `∃`-elimination witness. Uppercase-leading
227    /// so the verifier reads it as a constant, not a variable.
228    fn fresh_witness(&mut self) -> String {
229        self.fresh += 1;
230        format!("W{}", self.fresh)
231    }
232
233    /// Replace the focused hole's goal in place (used by `cases` on `∧`, which
234    /// enriches the hypotheses without splitting the goal).
235    fn set_focused_goal(&mut self, goal: Goal) {
236        if let Some(&idx) = self.open.front() {
237            self.nodes[idx] = Node::Hole(goal);
238        }
239    }
240
241    /// The focused goal, or `None` if the proof is complete.
242    pub fn focus(&self) -> Option<&Goal> {
243        let idx = *self.open.front()?;
244        match &self.nodes[idx] {
245            Node::Hole(g) => Some(g),
246            _ => None,
247        }
248    }
249
250    /// Number of still-open goals.
251    pub fn open_goals(&self) -> usize {
252        self.open.len()
253    }
254
255    /// The focused goal's target, if any goal is open — the tactic-script and
256    /// IDE surface for inspecting where a proof stands.
257    pub fn focused_target(&self) -> Option<&ProofExpr> {
258        self.focus().map(|g| &g.target)
259    }
260
261    /// A [`crate::simp::SimpSet`] compiled from every in-scope hypothesis that
262    /// orients as a rewrite rule — the default rule set for the script-level
263    /// `simp` (a premise or cited lemma of rule shape is automatically a rule).
264    pub fn scope_simp_set(&self) -> crate::simp::SimpSet {
265        let mut set = crate::simp::SimpSet::new();
266        if let Some(g) = self.focus() {
267            for h in &g.hyps {
268                set.register_lemma(&h.name, &h.prop);
269            }
270        }
271        set
272    }
273
274    fn focused_goal(&self) -> Result<Goal, TacticError> {
275        self.focus().cloned().ok_or(TacticError::NoOpenGoals)
276    }
277
278    /// Replace the focused hole with a refinement node whose children are the given
279    /// closed proofs / subgoals (in order). New subgoals are focused next, in order.
280    fn refine(&mut self, conclusion: ProofExpr, rule: InferenceRule, children: Vec<Child>) {
281        let idx = self.open.pop_front().expect("a focused goal");
282        let mut child_indices = Vec::with_capacity(children.len());
283        let mut new_holes = Vec::new();
284        for child in children {
285            let cidx = self.nodes.len();
286            match child {
287                Child::Closed(tree) => self.nodes.push(Node::Done(tree)),
288                Child::Sub(goal) => {
289                    self.nodes.push(Node::Hole(goal));
290                    new_holes.push(cidx);
291                }
292            }
293            child_indices.push(cidx);
294        }
295        // Focus the new subgoals next, first one at the front.
296        for &h in new_holes.iter().rev() {
297            self.open.push_front(h);
298        }
299        self.nodes[idx] = Node::Filled { conclusion, rule, children: child_indices };
300    }
301
302    /// Close the focused goal with an already-complete derivation.
303    fn close(&mut self, tree: DerivationTree) {
304        let idx = self.open.pop_front().expect("a focused goal");
305        self.nodes[idx] = Node::Done(tree);
306    }
307
308    // === Tactics ===
309
310    /// `intro name`: for a goal `P → Q`, assume `P` as hypothesis `name` and reduce
311    /// to `Q`; for `∀x. φ(x)`, introduce the bound variable under `name` and reduce
312    /// to `φ(name)`. Mirrors Lean's `intro`.
313    pub fn intro(&mut self, name: &str) -> Result<&mut Self, TacticError> {
314        let g = self.focused_goal()?;
315        match &g.target {
316            ProofExpr::Implies(p, q) => {
317                let mut hyps = g.hyps.clone();
318                hyps.push(direct_hyp(name.to_string(), (**p).clone()));
319                let sub = Goal { hyps, target: (**q).clone() };
320                self.refine(g.target.clone(), InferenceRule::ImpliesIntro, vec![Child::Sub(sub)]);
321                Ok(self)
322            }
323            ProofExpr::ForAll { variable, body } => {
324                // Introduce the bound variable under `name`. When `name` already IS the
325                // binder, skip the rename — substituting `{z ↦ z}` would chase its own
326                // chain forever in `apply_subst_to_term`. The eigenvariable soundness
327                // worry of search does not apply here: the user constructs the proof,
328                // they never *instantiate* the introduced variable.
329                let (conclusion, renamed) = if name == variable {
330                    (g.target.clone(), (**body).clone())
331                } else {
332                    let mut subst = Substitution::new();
333                    subst.insert(variable.clone(), ProofTerm::Variable(name.to_string()));
334                    let renamed = apply_subst_to_expr(body, &subst);
335                    let conclusion = ProofExpr::ForAll {
336                        variable: name.to_string(),
337                        body: Box::new(renamed.clone()),
338                    };
339                    (conclusion, renamed)
340                };
341                let sub = Goal { hyps: g.hyps.clone(), target: renamed };
342                self.refine(
343                    conclusion,
344                    InferenceRule::UniversalIntro {
345                        variable: name.to_string(),
346                        var_type: "Entity".to_string(),
347                    },
348                    vec![Child::Sub(sub)],
349                );
350                Ok(self)
351            }
352            other => Err(TacticError::DoesNotApply(format!(
353                "intro expects → or ∀, got {other:?}"
354            ))),
355        }
356    }
357
358    /// `assumption`: close the goal with any hypothesis whose proposition is exactly
359    /// the target, emitting THAT hypothesis's proof (a direct reference, or a
360    /// `ConjunctionElim` projection for a conjunct extracted by `cases`).
361    pub fn assumption(&mut self) -> Result<&mut Self, TacticError> {
362        let g = self.focused_goal()?;
363        if let Some(h) = g.hyps.iter().find(|h| h.prop == g.target) {
364            let proof = h.proof.clone();
365            self.close(proof);
366            Ok(self)
367        } else {
368            Err(TacticError::NoSuchHypothesis(format!(
369                "no hypothesis proves {:?}",
370                g.target
371            )))
372        }
373    }
374
375    /// `exact name`: close the goal with the specifically-named hypothesis, which
376    /// must prove the target.
377    pub fn exact(&mut self, name: &str) -> Result<&mut Self, TacticError> {
378        let g = self.focused_goal()?;
379        match g.hyps.iter().find(|h| h.name == name) {
380            Some(h) if h.prop == g.target => {
381                let proof = h.proof.clone();
382                self.close(proof);
383                Ok(self)
384            }
385            Some(h) => Err(TacticError::DoesNotApply(format!(
386                "hypothesis {name} : {:?} does not prove {:?}",
387                h.prop, g.target
388            ))),
389            None => Err(TacticError::NoSuchHypothesis(name.to_string())),
390        }
391    }
392
393    /// `constructor` / `split`: for a goal `A ∧ B`, reduce to the two subgoals `A`
394    /// and `B`.
395    pub fn split(&mut self) -> Result<&mut Self, TacticError> {
396        let g = self.focused_goal()?;
397        match &g.target {
398            ProofExpr::And(a, b) => {
399                let ga = Goal { hyps: g.hyps.clone(), target: (**a).clone() };
400                let gb = Goal { hyps: g.hyps.clone(), target: (**b).clone() };
401                self.refine(
402                    g.target.clone(),
403                    InferenceRule::ConjunctionIntro,
404                    vec![Child::Sub(ga), Child::Sub(gb)],
405                );
406                Ok(self)
407            }
408            other => Err(TacticError::DoesNotApply(format!("split expects ∧, got {other:?}"))),
409        }
410    }
411
412    /// `left`: prove `A ∨ B` by proving `A`.
413    pub fn left(&mut self) -> Result<&mut Self, TacticError> {
414        self.disjunct(true)
415    }
416
417    /// `right`: prove `A ∨ B` by proving `B`.
418    pub fn right(&mut self) -> Result<&mut Self, TacticError> {
419        self.disjunct(false)
420    }
421
422    fn disjunct(&mut self, take_left: bool) -> Result<&mut Self, TacticError> {
423        let g = self.focused_goal()?;
424        match &g.target {
425            ProofExpr::Or(a, b) => {
426                let chosen = if take_left { (**a).clone() } else { (**b).clone() };
427                let sub = Goal { hyps: g.hyps.clone(), target: chosen };
428                self.refine(
429                    g.target.clone(),
430                    InferenceRule::DisjunctionIntro,
431                    vec![Child::Sub(sub)],
432                );
433                Ok(self)
434            }
435            other => Err(TacticError::DoesNotApply(format!(
436                "left/right expects ∨, got {other:?}"
437            ))),
438        }
439    }
440
441    /// `exists w`: prove `∃x. φ(x)` by exhibiting the witness `w` and reducing to
442    /// `φ(w)`.
443    pub fn exists(&mut self, witness: ProofTerm) -> Result<&mut Self, TacticError> {
444        let g = self.focused_goal()?;
445        match &g.target {
446            ProofExpr::Exists { variable, body } => {
447                let mut subst = Substitution::new();
448                subst.insert(variable.clone(), witness.clone());
449                let instantiated = apply_subst_to_expr(body, &subst);
450                let sub = Goal { hyps: g.hyps.clone(), target: instantiated };
451                self.refine(
452                    g.target.clone(),
453                    InferenceRule::ExistentialIntro {
454                        witness: format!("{witness}"),
455                        witness_type: "Entity".to_string(),
456                    },
457                    vec![Child::Sub(sub)],
458                );
459                Ok(self)
460            }
461            other => Err(TacticError::DoesNotApply(format!("exists expects ∃, got {other:?}"))),
462        }
463    }
464
465    /// `apply rule`: `rule` is `P → Goal` (a hypothesis or premise). Reduce the goal
466    /// to its antecedent `P` by modus ponens.
467    pub fn apply(&mut self, rule: &ProofExpr) -> Result<&mut Self, TacticError> {
468        let g = self.focused_goal()?;
469        if let ProofExpr::Implies(ant, con) = rule {
470            if unify_exprs(&g.target, con).is_ok() {
471                let rule_leaf = DerivationTree::leaf(rule.clone(), InferenceRule::PremiseMatch);
472                let sub = Goal { hyps: g.hyps.clone(), target: (**ant).clone() };
473                self.refine(
474                    g.target.clone(),
475                    InferenceRule::ModusPonens,
476                    vec![Child::Closed(rule_leaf), Child::Sub(sub)],
477                );
478                return Ok(self);
479            }
480        }
481        Err(TacticError::DoesNotApply(format!(
482            "apply: {rule:?} is not an implication whose conclusion is the goal"
483        )))
484    }
485
486    /// `cases h` / `destruct h`: eliminate a hypothesis by its shape.
487    /// - `A ∧ B`: add `h_1 : A` and `h_2 : B` to the goal, each backed by the
488    ///   `ConjunctionElim` projection of `h` — so using a conjunct emits the
489    ///   projection, never a dangling reference.
490    /// - `A ∨ B`: split into two goals, one assuming `A`, one assuming `B`
491    ///   (`DisjunctionCases`).
492    /// - `∃x. φ`: introduce a fresh witness `c` and the hypothesis `φ(c)`
493    ///   (`ExistentialElim`); the goal must not mention `c`, which holds since it is
494    ///   fresh.
495    pub fn cases(&mut self, name: &str) -> Result<&mut Self, TacticError> {
496        let g = self.focused_goal()?;
497        let hyp = g
498            .hyps
499            .iter()
500            .find(|h| h.name == name)
501            .cloned()
502            .ok_or_else(|| TacticError::NoSuchHypothesis(name.to_string()))?;
503        match &hyp.prop {
504            ProofExpr::And(a, b) => {
505                let proj_a = DerivationTree::new(
506                    (**a).clone(),
507                    InferenceRule::ConjunctionElim,
508                    vec![hyp.proof.clone()],
509                );
510                let proj_b = DerivationTree::new(
511                    (**b).clone(),
512                    InferenceRule::ConjunctionElim,
513                    vec![hyp.proof.clone()],
514                );
515                let mut hyps = g.hyps.clone();
516                hyps.push(Hyp { name: format!("{name}_1"), prop: (**a).clone(), proof: proj_a });
517                hyps.push(Hyp { name: format!("{name}_2"), prop: (**b).clone(), proof: proj_b });
518                self.set_focused_goal(Goal { hyps, target: g.target.clone() });
519                Ok(self)
520            }
521            ProofExpr::Or(a, b) => {
522                let mut ha = g.hyps.clone();
523                ha.push(direct_hyp(format!("{name}_l"), (**a).clone()));
524                let ga = Goal { hyps: ha, target: g.target.clone() };
525                let mut hb = g.hyps.clone();
526                hb.push(direct_hyp(format!("{name}_r"), (**b).clone()));
527                let gb = Goal { hyps: hb, target: g.target.clone() };
528                self.refine(
529                    g.target.clone(),
530                    InferenceRule::DisjunctionCases,
531                    vec![Child::Closed(hyp.proof.clone()), Child::Sub(ga), Child::Sub(gb)],
532                );
533                Ok(self)
534            }
535            ProofExpr::Exists { variable, body } => {
536                let witness = self.fresh_witness();
537                let mut subst = Substitution::new();
538                subst.insert(variable.clone(), ProofTerm::Constant(witness.clone()));
539                let phi_c = apply_subst_to_expr(body, &subst);
540                let mut hyps = g.hyps.clone();
541                hyps.push(direct_hyp(format!("{name}_w"), phi_c));
542                let gbody = Goal { hyps, target: g.target.clone() };
543                self.refine(
544                    g.target.clone(),
545                    InferenceRule::ExistentialElim { witness },
546                    vec![Child::Closed(hyp.proof.clone()), Child::Sub(gbody)],
547                );
548                Ok(self)
549            }
550            other => Err(TacticError::DoesNotApply(format!(
551                "cases expects ∧/∨/∃ hypothesis, got {other:?}"
552            ))),
553        }
554    }
555
556    /// `induction`: on a goal `∀n. P(n)` over `Nat`, split into the base case `P(Zero)`
557    /// and the step case `P(Succ k)` with the induction hypothesis `ih : P(k)` in scope.
558    /// Certified as a `Fix`/`Match` over the kernel's `Nat` recursor; the IH resolves to
559    /// the recursive call. (Nat only for now — `List`/general inductives need the
560    /// dependent `InductionScheme`.)
561    pub fn induction(&mut self) -> Result<&mut Self, TacticError> {
562        let g = self.focused_goal()?;
563        let (variable, body) = match &g.target {
564            ProofExpr::ForAll { variable, body } => (variable.clone(), (**body).clone()),
565            other => {
566                return Err(TacticError::DoesNotApply(format!(
567                    "induction expects a ∀ goal, got {other:?}"
568                )))
569            }
570        };
571        self.fresh += 1;
572        // Uppercase so the search treats the step variable as a rigid eigen-CONSTANT
573        // (never a unifiable metavariable, which the backward chainer would either
574        // ground to `Zero` or expand into an infinite `P(Succ^n k)` chain). It is
575        // remapped back to a bound `Variable` when the proof is assembled (see
576        // `assemble`), which is what the certifier's `Match` arm binds.
577        let step_var = format!("K{}", self.fresh);
578        let subst_to = |term: ProofTerm| {
579            let mut s = Substitution::new();
580            s.insert(variable.clone(), term);
581            apply_subst_to_expr(&body, &s)
582        };
583        let base_target = subst_to(ProofTerm::Constant("Zero".to_string()));
584        let succ_k =
585            ProofTerm::Function("Succ".to_string(), vec![ProofTerm::Constant(step_var.clone())]);
586        let step_target = subst_to(succ_k);
587        let ih_prop = subst_to(ProofTerm::Constant(step_var.clone()));
588
589        let base_goal = Goal { hyps: g.hyps.clone(), target: base_target };
590        // Put the induction hypothesis FIRST so `auto`/`assumption` discharge the step's
591        // recursive obligation `P(k)` against the IH (keeping `k` symbolic) rather than
592        // against a base fact like `P(Zero)` (which would unsoundly ground `k := Zero`).
593        let mut step_hyps = vec![direct_hyp("ih".to_string(), ih_prop)];
594        step_hyps.extend(g.hyps.clone());
595        let step_goal = Goal { hyps: step_hyps, target: step_target };
596
597        self.refine(
598            g.target.clone(),
599            InferenceRule::StructuralInduction {
600                variable,
601                ind_type: "Nat".to_string(),
602                step_var,
603            },
604            vec![Child::Sub(base_goal), Child::Sub(step_goal)],
605        );
606        Ok(self)
607    }
608
609    /// `induction_over`: generic structural induction over ANY inductive `ind_type`,
610    /// one subgoal per constructor. On a goal `∀x. P(x)`, each constructor `C a₀…aₙ`
611    /// yields the subgoal `P(C a₀ … aₙ)`; every recursive argument `aᵢ` (one of the
612    /// inductive type) contributes an induction hypothesis `P(aᵢ)`, placed FIRST so
613    /// `auto`/`assumption` discharge the recursive obligation against the IH rather
614    /// than a base fact. Generalizes [`induction`](Self::induction) (fixed to the
615    /// nullary-base + unary-step Nat shape) to constructors with several — or zero —
616    /// recursive positions (`Nil`/`Cons`, `Leaf`/`Node`, an N-way enum). Assembled as
617    /// the dependent `InductionScheme` eliminator (`fix rec. λx. match x { … }`), which
618    /// the kernel re-checks for coverage, case types, and termination.
619    ///
620    /// The constructor arguments run the search as rigid eigen-CONSTANTS (so the
621    /// backward chainer neither grounds nor unrolls them); [`assemble`](Self::assemble)
622    /// remaps each back to the bound `Variable` the certifier's `match` arm binds.
623    pub fn induction_over(
624        &mut self,
625        ind_type: &str,
626        ctors: Vec<CtorSpec>,
627    ) -> Result<&mut Self, TacticError> {
628        let g = self.focused_goal()?;
629        let (variable, body) = match &g.target {
630            ProofExpr::ForAll { variable, body } => (variable.clone(), (**body).clone()),
631            other => {
632                return Err(TacticError::DoesNotApply(format!(
633                    "induction expects a ∀ goal, got {other:?}"
634                )))
635            }
636        };
637        let subst_to = |term: ProofTerm| {
638            let mut s = Substitution::new();
639            s.insert(variable.clone(), term);
640            apply_subst_to_expr(&body, &s)
641        };
642
643        let mut cases = Vec::with_capacity(ctors.len());
644        let mut children = Vec::with_capacity(ctors.len());
645        for ctor in &ctors {
646            // A fresh rigid eigenconstant per constructor argument.
647            let arg_names: Vec<String> = ctor
648                .recursive
649                .iter()
650                .map(|_| {
651                    self.fresh += 1;
652                    format!("K{}", self.fresh)
653                })
654                .collect();
655
656            // The constructor applied to its eigenconstant arguments — `C` (a bare
657            // `Constant`) when nullary, else `C(a₀, …, aₙ)`.
658            let ctor_term = if arg_names.is_empty() {
659                ProofTerm::Constant(ctor.constructor.clone())
660            } else {
661                ProofTerm::Function(
662                    ctor.constructor.clone(),
663                    arg_names.iter().map(|n| ProofTerm::Constant(n.clone())).collect(),
664                )
665            };
666            let case_target = subst_to(ctor_term);
667
668            // IHs for the recursive arguments, FIRST (see the Nat induction rationale).
669            let mut hyps = Vec::new();
670            for (arg, &is_rec) in arg_names.iter().zip(ctor.recursive.iter()) {
671                if is_rec {
672                    let ih_prop = subst_to(ProofTerm::Constant(arg.clone()));
673                    hyps.push(direct_hyp(format!("ih_{arg}"), ih_prop));
674                }
675            }
676            hyps.extend(g.hyps.clone());
677            children.push(Child::Sub(Goal { hyps, target: case_target }));
678
679            cases.push(InductionCase {
680                constructor: ctor.constructor.clone(),
681                args: arg_names
682                    .iter()
683                    .zip(ctor.recursive.iter())
684                    .map(|(name, &recursive)| InductionArg { name: name.clone(), recursive })
685                    .collect(),
686            });
687        }
688
689        self.refine(
690            g.target.clone(),
691            InferenceRule::InductionScheme { variable, ind_type: ind_type.to_string(), cases },
692            children,
693        );
694        Ok(self)
695    }
696
697    /// `rewrite h`: `h : lhs = rhs` rewrites every occurrence of `lhs` to `rhs` in the
698    /// goal, reducing it to `goal[lhs := rhs]`. Certified by `Eq_rec` (Leibniz): the
699    /// node concludes the original goal (which contains `lhs = to`), its equality
700    /// premise proves `rhs = lhs` (the symmetric of `h`, so `from = rhs`, `to = lhs`),
701    /// and its source premise is the rewritten subgoal. Fails if `lhs` does not occur.
702    pub fn rewrite(&mut self, name: &str) -> Result<&mut Self, TacticError> {
703        let g = self.focused_goal()?;
704        let hyp = g
705            .hyps
706            .iter()
707            .find(|h| h.name == name)
708            .cloned()
709            .ok_or_else(|| TacticError::NoSuchHypothesis(name.to_string()))?;
710        let (lhs, rhs) = match &hyp.prop {
711            ProofExpr::Identity(l, r) => (l.clone(), r.clone()),
712            other => {
713                return Err(TacticError::DoesNotApply(format!(
714                    "rewrite expects an equality hypothesis, got {other:?}"
715                )))
716            }
717        };
718        let subgoal_target = replace_in_expr(&g.target, &lhs, &rhs);
719        if subgoal_target == g.target {
720            return Err(TacticError::DoesNotApply(format!(
721                "rewrite: {lhs} does not occur in the goal"
722            )));
723        }
724        // The equality premise must conclude `rhs = lhs` — the symmetric of `h`.
725        let eq_sym = DerivationTree::new(
726            ProofExpr::Identity(rhs.clone(), lhs.clone()),
727            InferenceRule::EqualitySymmetry,
728            vec![hyp.proof.clone()],
729        );
730        let sub = Goal { hyps: g.hyps.clone(), target: subgoal_target };
731        self.refine(
732            g.target.clone(),
733            InferenceRule::Rewrite { from: rhs, to: lhs },
734            vec![Child::Closed(eq_sym), Child::Sub(sub)],
735        );
736        Ok(self)
737    }
738
739    /// `simp`: normalize the focused goal by the rule set, left-to-right to a
740    /// fixpoint, then close it if it became trivial (reflexivity or an exact
741    /// hypothesis). Mirrors Lean's `simp`.
742    ///
743    /// Each rewrite is one certified `Rewrite` refinement — the instantiated
744    /// rule equality (a `UniversalInstTerm` chain over the lemma, conditions
745    /// discharged by `ModusPonens`) is the closed premise, the rewritten goal
746    /// the open one — so `qed` re-checks every step in the kernel. Loop
747    /// protection is a step budget plus a seen-goal set (commuting rules like
748    /// `a = b, b = a` terminate instead of ping-ponging). Fails only if it
749    /// neither rewrote nor closed anything.
750    pub fn simp(&mut self, set: &crate::simp::SimpSet) -> Result<&mut Self, TacticError> {
751        const BUDGET: usize = 1000;
752        let mut progress = false;
753        let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
754        seen.insert(self.focused_goal()?.target.to_string());
755
756        for _ in 0..BUDGET {
757            let g = self.focused_goal()?;
758            let hyp_view: Vec<(&ProofExpr, &DerivationTree)> =
759                g.hyps.iter().map(|h| (&h.prop, &h.proof)).collect();
760
761            // One term rewrite (rule step or ground arithmetic fold).
762            let step = set
763                .find_term_step(&g.target, &hyp_view)
764                .or_else(|| crate::simp::find_ground_fold(&g.target));
765            if let Some(step) = step {
766                let new_target = replace_in_expr(&g.target, &step.from, &step.to);
767                if new_target == g.target || !seen.insert(new_target.to_string()) {
768                    break;
769                }
770                // Same shape as `rewrite`: the equality premise proves
771                // `to = from` (the symmetric of the step equality).
772                let eq_sym = DerivationTree::new(
773                    ProofExpr::Identity(step.to.clone(), step.from.clone()),
774                    InferenceRule::EqualitySymmetry,
775                    vec![step.eq],
776                );
777                let sub = Goal { hyps: g.hyps.clone(), target: new_target };
778                self.refine(
779                    g.target.clone(),
780                    InferenceRule::Rewrite { from: step.to, to: step.from },
781                    vec![Child::Closed(eq_sym), Child::Sub(sub)],
782                );
783                progress = true;
784                continue;
785            }
786
787            // A top-level propositional step: an iff rule matching the whole
788            // goal reduces it to the rule's right-hand side via ModusPonens.
789            if let Some(iff) = set.find_iff_step(&g.target, &hyp_view) {
790                if !seen.insert(iff.rhs.to_string()) {
791                    break;
792                }
793                let sub = Goal { hyps: g.hyps.clone(), target: iff.rhs };
794                self.refine(
795                    g.target.clone(),
796                    InferenceRule::ModusPonens,
797                    vec![Child::Closed(iff.imp), Child::Sub(sub)],
798                );
799                progress = true;
800                continue;
801            }
802
803            break;
804        }
805
806        // Closing attempts on the normalized goal.
807        let g = self.focused_goal()?;
808        if let ProofExpr::Identity(l, r) = &g.target {
809            if l == r {
810                self.close(DerivationTree::leaf(
811                    g.target.clone(),
812                    InferenceRule::Reflexivity,
813                ));
814                return Ok(self);
815            }
816        }
817        if self.assumption().is_ok() {
818            return Ok(self);
819        }
820        if progress {
821            Ok(self)
822        } else {
823            Err(TacticError::DoesNotApply(
824                "simp: no rule rewrites the goal and it is not closable".to_string(),
825            ))
826        }
827    }
828
829    /// `decide`: close the focused goal by evaluation, if it is a closed
830    /// decidable proposition that evaluates TRUE (ground arithmetic,
831    /// comparisons, Bool equalities, and ∧/∨/→ over them). Mirrors Lean's
832    /// `decide`. A false or open goal is declined.
833    pub fn decide(&mut self) -> Result<&mut Self, TacticError> {
834        let g = self.focused_goal()?;
835        match crate::decide::decide_expr(&g.target) {
836            Some(tree) => {
837                self.close(tree);
838                Ok(self)
839            }
840            None => Err(TacticError::DoesNotApply(
841                "decide: not a closed decidable goal (or it is false)".to_string(),
842            )),
843        }
844    }
845
846    /// `omega`: linear INTEGER arithmetic. When the in-scope `≤`/`<` hypotheses
847    /// are jointly unsatisfiable over ℤ — using discreteness (`a < b ⟹ a+1 ≤ b`),
848    /// the fact rational solvers lack — refute them and discharge ANY goal by
849    /// ex-falso. Proves `x < y ∧ y < x+1 ⊢ Q`, which `linarith` cannot (that
850    /// system is rationally satisfiable). Mirrors the refutation half of Lean's
851    /// `omega`. Declines when the hypotheses have an integer model.
852    pub fn omega(&mut self) -> Result<&mut Self, TacticError> {
853        let g = self.focused_goal()?;
854        if !crate::omega_solve::has_arith_facts(
855            &g.hyps.iter().map(|h| (h.prop.clone(), h.proof.clone())).collect::<Vec<_>>(),
856        ) {
857            return Err(TacticError::DoesNotApply(
858                "omega: no arithmetic (≤ / <) hypotheses in scope".to_string(),
859            ));
860        }
861        let known: Vec<(ProofExpr, DerivationTree)> =
862            g.hyps.iter().map(|h| (h.prop.clone(), h.proof.clone())).collect();
863        match crate::omega_solve::omega_close(&known) {
864            Some(bot) => {
865                // ⊥ ⊢ goal, by ex-falso.
866                let tree = DerivationTree::new(
867                    g.target.clone(),
868                    InferenceRule::ExFalso,
869                    vec![bot],
870                );
871                self.close(tree);
872                Ok(self)
873            }
874            None => Err(TacticError::DoesNotApply(
875                "omega: the integer hypotheses are satisfiable (no refutation)".to_string(),
876            )),
877        }
878    }
879
880    /// `crush`: the grind-style closer. E-matches the in-scope `∀`-equality
881    /// lemmas at the goal's ground terms, then discharges the goal by certified
882    /// congruence closure. Proves goals plain `auto` cannot — e.g.
883    /// `∀x. f(x)=g(x), a=b, P(g(b)) ⊢ P(f(a))` — and every step is kernel-checked.
884    pub fn crush(&mut self) -> Result<&mut Self, TacticError> {
885        let g = self.focused_goal()?;
886        let premises: Vec<ProofExpr> = g.hyps.iter().map(|h| h.prop.clone()).collect();
887        match crate::crush::crush_prove(&premises, &g.target) {
888            Some(tree) => {
889                self.close(tree);
890                Ok(self)
891            }
892            None => Err(TacticError::DoesNotApply(
893                "crush: could not close the goal by e-matching + congruence".to_string(),
894            )),
895        }
896    }
897
898    /// `auto`: discharge the focused goal with the backward chainer (the existing
899    /// automation, hosted as one tactic). Its hypotheses and the premises are the
900    /// available knowledge base.
901    pub fn auto(&mut self) -> Result<&mut Self, TacticError> {
902        let g = self.focused_goal()?;
903        let mut engine = BackwardChainer::new();
904        for h in &g.hyps {
905            engine.add_axiom(h.prop.clone());
906        }
907        match engine.prove(g.target.clone()) {
908            Ok(tree) => {
909                self.close(tree);
910                Ok(self)
911            }
912            Err(_) => Err(TacticError::AutoFailed),
913        }
914    }
915
916    fn assemble(&self, idx: usize) -> Result<DerivationTree, TacticError> {
917        match &self.nodes[idx] {
918            Node::Done(tree) => Ok(tree.clone()),
919            Node::Filled { conclusion, rule, children } => {
920                let mut kids = children
921                    .iter()
922                    .map(|&c| self.assemble(c))
923                    .collect::<Result<Vec<_>, _>>()?;
924                // Generalize the step case: the eigenconstant the search ran under
925                // becomes the `Variable` the certifier's `Match` binds.
926                if let InferenceRule::StructuralInduction { step_var, .. } = rule {
927                    if kids.len() == 2 {
928                        kids[1] = eigen_to_var_tree(&kids[1], step_var);
929                    }
930                }
931                // Generic induction: each case ran under its own rigid eigenconstants
932                // (one per constructor argument); remap each back to the bound
933                // `Variable` the certifier's `match` arm binds and the IH cites.
934                if let InferenceRule::InductionScheme { cases, .. } = rule {
935                    for (kid, case) in kids.iter_mut().zip(cases.iter()) {
936                        for arg in &case.args {
937                            *kid = eigen_to_var_tree(kid, &arg.name);
938                        }
939                    }
940                }
941                Ok(DerivationTree::new(conclusion.clone(), rule.clone(), kids))
942            }
943            Node::Hole(_) => Err(TacticError::GoalsRemain(1)),
944        }
945    }
946
947    /// Assemble the current proof tree, if every goal is closed (for inspection/debug).
948    pub fn assembled(&self) -> Option<DerivationTree> {
949        if self.open.is_empty() {
950            self.assemble(self.root).ok()
951        } else {
952            None
953        }
954    }
955
956    /// Run a composed [`Tactic`] against this state, for fluent chaining with the
957    /// primitive methods.
958    pub fn run(&mut self, t: &Tactic) -> Result<&mut Self, TacticError> {
959        t(self)?;
960        Ok(self)
961    }
962
963    /// Finish the proof: every goal must be closed. Assembles the recorded tactic
964    /// steps into one derivation and runs it through the kernel trust door — the
965    /// returned [`VerifiedProof`] is `verified` only if the kernel accepts the term.
966    pub fn qed(&self) -> Result<VerifiedProof, TacticError> {
967        if !self.open.is_empty() {
968            return Err(TacticError::GoalsRemain(self.open.len()));
969        }
970        let tree = self.assemble(self.root)?;
971        Ok(check_derivation(&self.premises, &self.goal, tree))
972    }
973}
974
975/// A first-class tactic: a transformation of the proof state that either makes
976/// progress (`Ok`) or does not apply (`Err`). Tactics are values, so they compose
977/// with the [`combinators`].
978pub type Tactic = Box<dyn Fn(&mut ProofState) -> Result<(), TacticError>>;
979
980/// Tactic *values* and the combinators that compose them — the language layer over
981/// the primitive [`ProofState`] methods. The backtracking combinators (`first`,
982/// `try_`, `repeat`) speculate on a clone of the state and commit only on success,
983/// so a partial failure never corrupts the proof.
984pub mod combinators {
985    use super::{ProofState, ProofTerm, Tactic, TacticError};
986
987    /// `intro name` as a tactic value.
988    pub fn intro(name: &str) -> Tactic {
989        let name = name.to_string();
990        Box::new(move |st: &mut ProofState| st.intro(&name).map(|_| ()))
991    }
992    /// `assumption` as a tactic value.
993    pub fn assumption() -> Tactic {
994        Box::new(|st: &mut ProofState| st.assumption().map(|_| ()))
995    }
996    /// `simp` as a tactic value, its rule set drawn from everything in scope
997    /// (premises, intro'd hypotheses, cited lemmas) — the script-level default.
998    pub fn simp() -> Tactic {
999        Box::new(|st: &mut ProofState| {
1000            let set = st.scope_simp_set();
1001            st.simp(&set).map(|_| ())
1002        })
1003    }
1004    /// `decide` as a tactic value.
1005    pub fn decide() -> Tactic {
1006        Box::new(|st: &mut ProofState| st.decide().map(|_| ()))
1007    }
1008    /// `omega` as a tactic value.
1009    pub fn omega() -> Tactic {
1010        Box::new(|st: &mut ProofState| st.omega().map(|_| ()))
1011    }
1012    /// `crush` as a tactic value.
1013    pub fn crush() -> Tactic {
1014        Box::new(|st: &mut ProofState| st.crush().map(|_| ()))
1015    }
1016    /// `exact name` as a tactic value.
1017    pub fn exact(name: &str) -> Tactic {
1018        let name = name.to_string();
1019        Box::new(move |st: &mut ProofState| st.exact(&name).map(|_| ()))
1020    }
1021    /// `split` (∧I) as a tactic value.
1022    pub fn split() -> Tactic {
1023        Box::new(|st: &mut ProofState| st.split().map(|_| ()))
1024    }
1025    /// `left` (∨I) as a tactic value.
1026    pub fn left() -> Tactic {
1027        Box::new(|st: &mut ProofState| st.left().map(|_| ()))
1028    }
1029    /// `right` (∨I) as a tactic value.
1030    pub fn right() -> Tactic {
1031        Box::new(|st: &mut ProofState| st.right().map(|_| ()))
1032    }
1033    /// `exists witness` (∃I) as a tactic value.
1034    pub fn exists(witness: ProofTerm) -> Tactic {
1035        Box::new(move |st: &mut ProofState| st.exists(witness.clone()).map(|_| ()))
1036    }
1037    /// `cases name` (∧/∨/∃ elimination) as a tactic value.
1038    pub fn cases(name: &str) -> Tactic {
1039        let name = name.to_string();
1040        Box::new(move |st: &mut ProofState| st.cases(&name).map(|_| ()))
1041    }
1042    /// `rewrite name` (Leibniz substitution by an equality) as a tactic value.
1043    pub fn rewrite(name: &str) -> Tactic {
1044        let name = name.to_string();
1045        Box::new(move |st: &mut ProofState| st.rewrite(&name).map(|_| ()))
1046    }
1047    /// `induction` (structural induction over `Nat`) as a tactic value.
1048    pub fn induction() -> Tactic {
1049        Box::new(|st: &mut ProofState| st.induction().map(|_| ()))
1050    }
1051    /// `induction_over` (generic structural induction over `ind_type`) as a tactic value.
1052    pub fn induction_over(ind_type: &str, ctors: Vec<super::CtorSpec>) -> Tactic {
1053        let ind_type = ind_type.to_string();
1054        Box::new(move |st: &mut ProofState| {
1055            st.induction_over(&ind_type, ctors.clone()).map(|_| ())
1056        })
1057    }
1058    /// `auto` (the backward chainer) as a tactic value.
1059    pub fn auto() -> Tactic {
1060        Box::new(|st: &mut ProofState| st.auto().map(|_| ()))
1061    }
1062
1063    /// `t1; t2; …`: run each tactic in turn; fail (without committing the rest) at
1064    /// the first that does not apply.
1065    pub fn seq(tactics: Vec<Tactic>) -> Tactic {
1066        Box::new(move |st: &mut ProofState| {
1067            for t in &tactics {
1068                t(st)?;
1069            }
1070            Ok(())
1071        })
1072    }
1073
1074    /// `first [t1, t2, …]`: try each tactic on a speculative copy and commit the
1075    /// first that succeeds; fail only if none apply.
1076    pub fn first(tactics: Vec<Tactic>) -> Tactic {
1077        Box::new(move |st: &mut ProofState| {
1078            for t in &tactics {
1079                let mut trial = st.clone();
1080                if t(&mut trial).is_ok() {
1081                    *st = trial;
1082                    return Ok(());
1083                }
1084            }
1085            Err(TacticError::DoesNotApply("first: no alternative applied".to_string()))
1086        })
1087    }
1088
1089    /// `try t`: run `t` if it applies, otherwise leave the state unchanged. Always
1090    /// succeeds.
1091    pub fn try_(t: Tactic) -> Tactic {
1092        Box::new(move |st: &mut ProofState| {
1093            let mut trial = st.clone();
1094            if t(&mut trial).is_ok() {
1095                *st = trial;
1096            }
1097            Ok(())
1098        })
1099    }
1100
1101    /// `repeat t`: apply `t` as long as it keeps applying (committing each success),
1102    /// then stop. Always succeeds. Bounded to avoid a non-progressing tactic looping
1103    /// forever.
1104    pub fn repeat(t: Tactic) -> Tactic {
1105        Box::new(move |st: &mut ProofState| {
1106            for _ in 0..100_000 {
1107                let mut trial = st.clone();
1108                if t(&mut trial).is_ok() {
1109                    *st = trial;
1110                } else {
1111                    break;
1112                }
1113                if st.open.is_empty() {
1114                    break;
1115                }
1116            }
1117            Ok(())
1118        })
1119    }
1120
1121    /// `t1 <;> t2`-style: apply `t` to EVERY goal currently open (not just the
1122    /// focused one). Each goal is focused in turn; the subgoals `t` produces for it
1123    /// become the new open set, in original goal order.
1124    pub fn all_goals(t: Tactic) -> Tactic {
1125        Box::new(move |st: &mut ProofState| {
1126            let current: Vec<usize> = st.open.iter().copied().collect();
1127            let mut new_open = std::collections::VecDeque::new();
1128            for goal_idx in current {
1129                st.open = std::collections::VecDeque::from([goal_idx]);
1130                t(st)?;
1131                new_open.extend(st.open.iter().copied());
1132            }
1133            st.open = new_open;
1134            Ok(())
1135        })
1136    }
1137
1138    /// `t1 <;> t2`: run `t1` on the focused goal, then `t2` on every goal it produces.
1139    pub fn then_all(t1: Tactic, t2: Tactic) -> Tactic {
1140        let t2 = std::rc::Rc::new(t2);
1141        Box::new(move |st: &mut ProofState| {
1142            t1(st)?;
1143            let t2 = t2.clone();
1144            all_goals(Box::new(move |s| t2(s)))(st)
1145        })
1146    }
1147}