Skip to main content

tensorlogic_ir/
sequent.rs

1//! # Sequent Calculus for Proof-Theoretic Foundations
2//!
3//! This module implements sequent calculus representations and inference rules for TensorLogic.
4//! Sequent calculus provides a formal foundation for logical reasoning and proof construction.
5//!
6//! ## Overview
7//!
8//! A **sequent** is a formal statement of the form `Γ ⊢ Δ` where:
9//! - `Γ` (Gamma) is a multiset of antecedent formulas (hypotheses)
10//! - `Δ` (Delta) is a multiset of consequent formulas (conclusions)
11//! - `⊢` (turnstile) represents the entailment relation
12//!
13//! The sequent `Γ ⊢ Δ` is valid if: assuming all formulas in Γ are true,
14//! at least one formula in Δ must be true.
15//!
16//! ## Inference Rules
17//!
18//! This module implements the following sequent calculus rules:
19//!
20//! ### Structural Rules
21//! - **Identity**: `A ⊢ A`
22//! - **Weakening**: From `Γ ⊢ Δ` derive `Γ, A ⊢ Δ` or `Γ ⊢ Δ, A`
23//! - **Contraction**: From `Γ, A, A ⊢ Δ` derive `Γ, A ⊢ Δ`
24//! - **Exchange**: Reorder formulas in Γ or Δ
25//! - **Cut**: From `Γ ⊢ Δ, A` and `A, Γ' ⊢ Δ'` derive `Γ, Γ' ⊢ Δ, Δ'`
26//!
27//! ### Logical Rules (Left and Right)
28//! - **AND-Left**: From `Γ, A, B ⊢ Δ` derive `Γ, A ∧ B ⊢ Δ`
29//! - **AND-Right**: From `Γ ⊢ Δ, A` and `Γ ⊢ Δ, B` derive `Γ ⊢ Δ, A ∧ B`
30//! - **OR-Left**: From `Γ, A ⊢ Δ` and `Γ, B ⊢ Δ` derive `Γ, A ∨ B ⊢ Δ`
31//! - **OR-Right**: From `Γ ⊢ Δ, A, B` derive `Γ ⊢ Δ, A ∨ B`
32//! - **NOT-Left**: From `Γ ⊢ Δ, A` derive `Γ, ¬A ⊢ Δ`
33//! - **NOT-Right**: From `Γ, A ⊢ Δ` derive `Γ ⊢ Δ, ¬A`
34//! - **IMPLY-Left**: From `Γ ⊢ Δ, A` and `Γ, B ⊢ Δ` derive `Γ, A → B ⊢ Δ`
35//! - **IMPLY-Right**: From `Γ, A ⊢ Δ, B` derive `Γ ⊢ Δ, A → B`
36//!
37//! ### Quantifier Rules
38//! - **EXISTS-Left**: From `Γ, A[t/x] ⊢ Δ` derive `Γ, ∃x.A ⊢ Δ` (t is fresh)
39//! - **EXISTS-Right**: From `Γ ⊢ Δ, A[t/x]` derive `Γ ⊢ Δ, ∃x.A`
40//! - **FORALL-Left**: From `Γ, A[t/x] ⊢ Δ` derive `Γ, ∀x.A ⊢ Δ`
41//! - **FORALL-Right**: From `Γ ⊢ Δ, A[t/x]` derive `Γ ⊢ Δ, ∀x.A` (t is fresh)
42//!
43//! ## Applications
44//!
45//! - **Proof Search**: Construct proofs by applying inference rules backward
46//! - **Proof Checking**: Verify that a derivation tree is valid
47//! - **Proof Normalization**: Transform proofs to normal forms (cut-elimination)
48//! - **Compilation**: Use sequent calculus to guide tensor compilation strategies
49//!
50//! ## Example
51//!
52//! ```rust
53//! use tensorlogic_ir::{TLExpr, Term, Sequent, InferenceRule, ProofTree};
54//!
55//! // Construct a sequent: P(x) ∧ Q(x) ⊢ P(x)
56//! let p = TLExpr::pred("P", vec![Term::var("x")]);
57//! let q = TLExpr::pred("Q", vec![Term::var("x")]);
58//! let and_pq = TLExpr::and(p.clone(), q);
59//!
60//! let conclusion = Sequent::new(vec![and_pq], vec![p.clone()]);
61//!
62//! // Construct a proof tree: Identity axiom as premise, then apply AndLeft
63//! let identity_premise = ProofTree::identity(p.clone());
64//! let proof = ProofTree::new(
65//!     conclusion,
66//!     InferenceRule::AndLeft { index: 0 },
67//!     vec![identity_premise]
68//! );
69//!
70//! assert!(proof.is_valid());
71//! ```
72
73use crate::expr::TLExpr;
74use crate::term::Term;
75use crate::unification::Substitution;
76use serde::{Deserialize, Serialize};
77use std::collections::HashSet;
78
79/// Helper function to substitute terms in predicates within an expression.
80///
81/// This performs capture-avoiding substitution, respecting bound variables in quantifiers.
82fn substitute_in_expr(expr: &TLExpr, subst: &Substitution) -> TLExpr {
83    match expr {
84        TLExpr::Pred { name, args } => {
85            // Apply substitution to each term in the predicate
86            let new_args = args.iter().map(|term| subst.apply(term)).collect();
87            TLExpr::Pred {
88                name: name.clone(),
89                args: new_args,
90            }
91        }
92        TLExpr::And(left, right) => TLExpr::And(
93            Box::new(substitute_in_expr(left, subst)),
94            Box::new(substitute_in_expr(right, subst)),
95        ),
96        TLExpr::Or(left, right) => TLExpr::Or(
97            Box::new(substitute_in_expr(left, subst)),
98            Box::new(substitute_in_expr(right, subst)),
99        ),
100        TLExpr::Not(inner) => TLExpr::Not(Box::new(substitute_in_expr(inner, subst))),
101        TLExpr::Imply(left, right) => TLExpr::Imply(
102            Box::new(substitute_in_expr(left, subst)),
103            Box::new(substitute_in_expr(right, subst)),
104        ),
105        TLExpr::Exists { var, domain, body } => {
106            // Capture-avoiding substitution: don't substitute if var is bound
107            if subst.domain().contains(var) {
108                // Variable is bound by this quantifier, return unchanged
109                expr.clone()
110            } else {
111                TLExpr::Exists {
112                    var: var.clone(),
113                    domain: domain.clone(),
114                    body: Box::new(substitute_in_expr(body, subst)),
115                }
116            }
117        }
118        TLExpr::ForAll { var, domain, body } => {
119            // Capture-avoiding substitution: don't substitute if var is bound
120            if subst.domain().contains(var) {
121                // Variable is bound by this quantifier, return unchanged
122                expr.clone()
123            } else {
124                TLExpr::ForAll {
125                    var: var.clone(),
126                    domain: domain.clone(),
127                    body: Box::new(substitute_in_expr(body, subst)),
128                }
129            }
130        }
131        // For other expression types, return as-is (they don't contain terms)
132        _ => expr.clone(),
133    }
134}
135
136/// A sequent is a formal statement `Γ ⊢ Δ` representing an entailment relation.
137///
138/// - `antecedents` (Γ): Multiset of hypothesis formulas (left side)
139/// - `consequents` (Δ): Multiset of conclusion formulas (right side)
140///
141/// The sequent is valid if: assuming all antecedents are true,
142/// at least one consequent must be true.
143#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
144pub struct Sequent {
145    /// Hypothesis formulas (left side of ⊢)
146    pub antecedents: Vec<TLExpr>,
147    /// Conclusion formulas (right side of ⊢)
148    pub consequents: Vec<TLExpr>,
149}
150
151impl Sequent {
152    /// Create a new sequent from antecedents and consequents.
153    pub fn new(antecedents: Vec<TLExpr>, consequents: Vec<TLExpr>) -> Self {
154        Sequent {
155            antecedents,
156            consequents,
157        }
158    }
159
160    /// Create an identity sequent: `A ⊢ A`
161    pub fn identity(formula: TLExpr) -> Self {
162        Sequent::new(vec![formula.clone()], vec![formula])
163    }
164
165    /// Check if this is an axiom (identity sequent where some antecedent equals some consequent).
166    pub fn is_axiom(&self) -> bool {
167        for ant in &self.antecedents {
168            for cons in &self.consequents {
169                if ant == cons {
170                    return true;
171                }
172            }
173        }
174        false
175    }
176
177    /// Apply weakening rule: add a formula to antecedents.
178    pub fn weaken_left(mut self, formula: TLExpr) -> Self {
179        self.antecedents.push(formula);
180        self
181    }
182
183    /// Apply weakening rule: add a formula to consequents.
184    pub fn weaken_right(mut self, formula: TLExpr) -> Self {
185        self.consequents.push(formula);
186        self
187    }
188
189    /// Apply contraction rule: remove duplicate from antecedents.
190    pub fn contract_left(mut self, index: usize) -> Option<Self> {
191        if index >= self.antecedents.len() {
192            return None;
193        }
194        let formula = self.antecedents[index].clone();
195        // Find another occurrence
196        for i in 0..self.antecedents.len() {
197            if i != index && self.antecedents[i] == formula {
198                self.antecedents.remove(index);
199                return Some(self);
200            }
201        }
202        None
203    }
204
205    /// Apply contraction rule: remove duplicate from consequents.
206    pub fn contract_right(mut self, index: usize) -> Option<Self> {
207        if index >= self.consequents.len() {
208            return None;
209        }
210        let formula = self.consequents[index].clone();
211        // Find another occurrence
212        for i in 0..self.consequents.len() {
213            if i != index && self.consequents[i] == formula {
214                self.consequents.remove(index);
215                return Some(self);
216            }
217        }
218        None
219    }
220
221    /// Get all free variables in the sequent.
222    pub fn free_vars(&self) -> HashSet<String> {
223        let mut vars = HashSet::new();
224        for ant in &self.antecedents {
225            vars.extend(ant.free_vars());
226        }
227        for cons in &self.consequents {
228            vars.extend(cons.free_vars());
229        }
230        vars
231    }
232
233    /// Substitute a term for a variable throughout the sequent.
234    ///
235    /// This creates a new substitution and applies it to all formulas in the sequent.
236    /// The substitution is capture-avoiding for bound variables in quantifiers.
237    ///
238    /// # Example
239    ///
240    /// ```rust
241    /// use tensorlogic_ir::{Sequent, TLExpr, Term};
242    ///
243    /// // P(x) ⊢ P(x)
244    /// let p_x = TLExpr::pred("P", vec![Term::var("x")]);
245    /// let seq = Sequent::identity(p_x);
246    ///
247    /// // Substitute x with a
248    /// let seq_subst = seq.substitute("x", &Term::constant("a"));
249    ///
250    /// // Result should be P(a) ⊢ P(a)
251    /// ```
252    pub fn substitute(&self, var: &str, term: &Term) -> Self {
253        let mut subst = Substitution::empty();
254        subst.bind(var.to_string(), term.clone());
255
256        let new_antecedents = self
257            .antecedents
258            .iter()
259            .map(|expr| substitute_in_expr(expr, &subst))
260            .collect();
261
262        let new_consequents = self
263            .consequents
264            .iter()
265            .map(|expr| substitute_in_expr(expr, &subst))
266            .collect();
267
268        Sequent::new(new_antecedents, new_consequents)
269    }
270}
271
272/// Inference rules in sequent calculus.
273#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
274pub enum InferenceRule {
275    /// Identity axiom: `A ⊢ A`
276    Identity,
277
278    /// Weakening left: add formula to antecedents
279    WeakeningLeft,
280
281    /// Weakening right: add formula to consequents
282    WeakeningRight,
283
284    /// Contraction left: remove duplicate from antecedents
285    ContractionLeft { index: usize },
286
287    /// Contraction right: remove duplicate from consequents
288    ContractionRight { index: usize },
289
290    /// Exchange: reorder formulas (implicit, not tracked)
291    Exchange,
292
293    /// Cut rule: eliminate intermediate formula (index in antecedents/consequents)
294    Cut { index: usize },
295
296    /// AND introduction (left): `Γ, A, B ⊢ Δ` from `Γ, A ∧ B ⊢ Δ`
297    AndLeft { index: usize },
298
299    /// AND introduction (right): `Γ ⊢ Δ, A ∧ B` from `Γ ⊢ Δ, A` and `Γ ⊢ Δ, B`
300    AndRight { index: usize },
301
302    /// OR introduction (left): `Γ, A ∨ B ⊢ Δ` from `Γ, A ⊢ Δ` and `Γ, B ⊢ Δ`
303    OrLeft { index: usize },
304
305    /// OR introduction (right): `Γ ⊢ Δ, A, B` from `Γ ⊢ Δ, A ∨ B`
306    OrRight { index: usize },
307
308    /// NOT introduction (left): `Γ, ¬A ⊢ Δ` from `Γ ⊢ Δ, A`
309    NotLeft { index: usize },
310
311    /// NOT introduction (right): `Γ ⊢ Δ, ¬A` from `Γ, A ⊢ Δ`
312    NotRight { index: usize },
313
314    /// IMPLY introduction (left): `Γ, A → B ⊢ Δ` from `Γ ⊢ Δ, A` and `Γ, B ⊢ Δ`
315    ImplyLeft { index: usize },
316
317    /// IMPLY introduction (right): `Γ ⊢ Δ, A → B` from `Γ, A ⊢ Δ, B`
318    ImplyRight { index: usize },
319
320    /// EXISTS introduction (left): `Γ, ∃x.A ⊢ Δ` from `Γ, A[t/x] ⊢ Δ` (t fresh)
321    ExistsLeft { index: usize, witness: Term },
322
323    /// EXISTS introduction (right): `Γ ⊢ Δ, ∃x.A` from `Γ ⊢ Δ, A[t/x]`
324    ExistsRight { index: usize, witness: Term },
325
326    /// FORALL introduction (left): `Γ, ∀x.A ⊢ Δ` from `Γ, A[t/x] ⊢ Δ`
327    ForAllLeft { index: usize, term: Term },
328
329    /// FORALL introduction (right): `Γ ⊢ Δ, ∀x.A` from `Γ ⊢ Δ, A[t/x]` (t fresh)
330    ForAllRight { index: usize, witness: Term },
331}
332
333/// A proof tree in sequent calculus.
334///
335/// Represents a derivation tree showing how a sequent is proved
336/// using inference rules applied to premises.
337#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
338pub struct ProofTree {
339    /// The conclusion sequent of this proof step
340    pub conclusion: Sequent,
341    /// The inference rule applied
342    pub rule: InferenceRule,
343    /// Premise proof trees (subproofs)
344    pub premises: Vec<ProofTree>,
345}
346
347impl ProofTree {
348    /// Create a proof tree for an identity axiom.
349    pub fn identity(formula: TLExpr) -> Self {
350        ProofTree {
351            conclusion: Sequent::identity(formula),
352            rule: InferenceRule::Identity,
353            premises: vec![],
354        }
355    }
356
357    /// Create a proof tree with premises and rule.
358    pub fn new(conclusion: Sequent, rule: InferenceRule, premises: Vec<ProofTree>) -> Self {
359        ProofTree {
360            conclusion,
361            rule,
362            premises,
363        }
364    }
365
366    /// Check if this proof tree is valid (premises correctly derive conclusion).
367    pub fn is_valid(&self) -> bool {
368        // Check that the rule application is sound
369        match &self.rule {
370            InferenceRule::Identity => {
371                // Must have no premises and be an axiom
372                self.premises.is_empty() && self.conclusion.is_axiom()
373            }
374            InferenceRule::WeakeningLeft | InferenceRule::WeakeningRight => {
375                // Must have exactly one premise
376                if self.premises.len() != 1 {
377                    return false;
378                }
379                // The premise should be a subset of the conclusion
380                // (detailed validation omitted for brevity)
381                true
382            }
383            InferenceRule::AndLeft { index } => {
384                // Must have one premise where A ∧ B is split into A, B
385                if self.premises.len() != 1 {
386                    return false;
387                }
388                if *index >= self.conclusion.antecedents.len() {
389                    return false;
390                }
391                // Check that the formula at index is an AND
392                matches!(self.conclusion.antecedents[*index], TLExpr::And(_, _))
393            }
394            InferenceRule::AndRight { .. } => {
395                // Must have two premises
396                self.premises.len() == 2
397            }
398            InferenceRule::OrLeft { .. } => {
399                // Must have two premises
400                self.premises.len() == 2
401            }
402            InferenceRule::OrRight { index } => {
403                // Must have one premise
404                if self.premises.len() != 1 {
405                    return false;
406                }
407                if *index >= self.conclusion.consequents.len() {
408                    return false;
409                }
410                // Check that the formula at index is an OR
411                matches!(self.conclusion.consequents[*index], TLExpr::Or(_, _))
412            }
413            InferenceRule::NotLeft { .. } | InferenceRule::NotRight { .. } => {
414                // Must have one premise
415                self.premises.len() == 1
416            }
417            InferenceRule::ImplyLeft { .. } => {
418                // Must have two premises
419                self.premises.len() == 2
420            }
421            InferenceRule::ImplyRight { .. } => {
422                // Must have one premise
423                self.premises.len() == 1
424            }
425            InferenceRule::Cut { .. } => {
426                // Must have two premises
427                self.premises.len() == 2
428            }
429            _ => true, // Other rules validation omitted for brevity
430        }
431    }
432
433    /// Get the depth of this proof tree.
434    pub fn depth(&self) -> usize {
435        if self.premises.is_empty() {
436            1
437        } else {
438            1 + self.premises.iter().map(|p| p.depth()).max().unwrap_or(0)
439        }
440    }
441
442    /// Count the number of inference rule applications in this proof.
443    pub fn size(&self) -> usize {
444        1 + self.premises.iter().map(|p| p.size()).sum::<usize>()
445    }
446
447    /// Check if this proof uses the cut rule.
448    pub fn uses_cut(&self) -> bool {
449        matches!(self.rule, InferenceRule::Cut { .. }) || self.premises.iter().any(|p| p.uses_cut())
450    }
451}
452
453/// Proof search strategies for automated theorem proving.
454#[derive(Clone, Debug, PartialEq, Eq)]
455pub enum ProofSearchStrategy {
456    /// Depth-first search with backtracking
457    DepthFirst { max_depth: usize },
458    /// Breadth-first search
459    BreadthFirst { max_depth: usize },
460    /// Best-first search with heuristic
461    BestFirst { max_depth: usize },
462    /// Iterative deepening
463    IterativeDeepening { max_depth: usize },
464}
465
466/// Proof search engine for automated theorem proving.
467pub struct ProofSearchEngine {
468    strategy: ProofSearchStrategy,
469    /// Maximum number of proof search steps
470    max_steps: usize,
471    /// Statistics about the search
472    pub stats: ProofSearchStats,
473}
474
475/// Statistics about proof search.
476#[derive(Clone, Debug, Default, Serialize, Deserialize)]
477pub struct ProofSearchStats {
478    /// Number of sequents explored
479    pub sequents_explored: usize,
480    /// Number of proof trees generated
481    pub proofs_generated: usize,
482    /// Number of backtracking steps
483    pub backtracks: usize,
484    /// Final proof depth (if found)
485    pub proof_depth: Option<usize>,
486}
487
488impl ProofSearchEngine {
489    /// Create a new proof search engine with the given strategy.
490    pub fn new(strategy: ProofSearchStrategy, max_steps: usize) -> Self {
491        ProofSearchEngine {
492            strategy,
493            max_steps,
494            stats: ProofSearchStats::default(),
495        }
496    }
497
498    /// Search for a proof of the given sequent.
499    ///
500    /// Returns `Some(proof)` if a proof is found, `None` otherwise.
501    pub fn search(&mut self, sequent: &Sequent) -> Option<ProofTree> {
502        match self.strategy {
503            ProofSearchStrategy::DepthFirst { max_depth } => self.dfs_search(sequent, 0, max_depth),
504            ProofSearchStrategy::BreadthFirst { max_depth } => self.bfs_search(sequent, max_depth),
505            ProofSearchStrategy::BestFirst { max_depth } => {
506                self.best_first_search(sequent, max_depth)
507            }
508            ProofSearchStrategy::IterativeDeepening { max_depth } => {
509                self.iterative_deepening_search(sequent, max_depth)
510            }
511        }
512    }
513
514    /// Depth-first search for a proof.
515    fn dfs_search(
516        &mut self,
517        sequent: &Sequent,
518        depth: usize,
519        max_depth: usize,
520    ) -> Option<ProofTree> {
521        self.stats.sequents_explored += 1;
522
523        if depth >= max_depth || self.stats.sequents_explored >= self.max_steps {
524            self.stats.backtracks += 1;
525            return None;
526        }
527
528        // Check if this is an axiom
529        if sequent.is_axiom() {
530            // Find the matching formula
531            for ant in &sequent.antecedents {
532                if sequent.consequents.contains(ant) {
533                    self.stats.proofs_generated += 1;
534                    let proof = ProofTree::identity(ant.clone());
535                    self.stats.proof_depth = Some(depth);
536                    return Some(proof);
537                }
538            }
539        }
540
541        // Try applying inference rules
542        // (Simplified implementation - full proof search would try all applicable rules)
543
544        // Try AND-Left rules
545        for (i, ant) in sequent.antecedents.iter().enumerate() {
546            if let TLExpr::And(a, b) = ant {
547                let mut new_ant = sequent.antecedents.clone();
548                new_ant.remove(i);
549                new_ant.push((**a).clone());
550                new_ant.push((**b).clone());
551
552                let new_sequent = Sequent::new(new_ant, sequent.consequents.clone());
553                if let Some(premise) = self.dfs_search(&new_sequent, depth + 1, max_depth) {
554                    self.stats.proofs_generated += 1;
555                    return Some(ProofTree::new(
556                        sequent.clone(),
557                        InferenceRule::AndLeft { index: i },
558                        vec![premise],
559                    ));
560                }
561            }
562        }
563
564        // Try OR-Right rules
565        for (i, cons) in sequent.consequents.iter().enumerate() {
566            if let TLExpr::Or(a, b) = cons {
567                let mut new_cons = sequent.consequents.clone();
568                new_cons.remove(i);
569                new_cons.push((**a).clone());
570                new_cons.push((**b).clone());
571
572                let new_sequent = Sequent::new(sequent.antecedents.clone(), new_cons);
573                if let Some(premise) = self.dfs_search(&new_sequent, depth + 1, max_depth) {
574                    self.stats.proofs_generated += 1;
575                    return Some(ProofTree::new(
576                        sequent.clone(),
577                        InferenceRule::OrRight { index: i },
578                        vec![premise],
579                    ));
580                }
581            }
582        }
583
584        // Try NOT-Left rules
585        for (i, ant) in sequent.antecedents.iter().enumerate() {
586            if let TLExpr::Not(a) = ant {
587                let mut new_ant = sequent.antecedents.clone();
588                new_ant.remove(i);
589
590                let mut new_cons = sequent.consequents.clone();
591                new_cons.push((**a).clone());
592
593                let new_sequent = Sequent::new(new_ant, new_cons);
594                if let Some(premise) = self.dfs_search(&new_sequent, depth + 1, max_depth) {
595                    self.stats.proofs_generated += 1;
596                    return Some(ProofTree::new(
597                        sequent.clone(),
598                        InferenceRule::NotLeft { index: i },
599                        vec![premise],
600                    ));
601                }
602            }
603        }
604
605        // Try NOT-Right rules
606        for (i, cons) in sequent.consequents.iter().enumerate() {
607            if let TLExpr::Not(a) = cons {
608                let mut new_cons = sequent.consequents.clone();
609                new_cons.remove(i);
610
611                let mut new_ant = sequent.antecedents.clone();
612                new_ant.push((**a).clone());
613
614                let new_sequent = Sequent::new(new_ant, new_cons);
615                if let Some(premise) = self.dfs_search(&new_sequent, depth + 1, max_depth) {
616                    self.stats.proofs_generated += 1;
617                    return Some(ProofTree::new(
618                        sequent.clone(),
619                        InferenceRule::NotRight { index: i },
620                        vec![premise],
621                    ));
622                }
623            }
624        }
625
626        self.stats.backtracks += 1;
627        None
628    }
629
630    /// Breadth-first search (stub implementation).
631    fn bfs_search(&mut self, sequent: &Sequent, max_depth: usize) -> Option<ProofTree> {
632        // For simplicity, fall back to DFS
633        self.dfs_search(sequent, 0, max_depth)
634    }
635
636    /// Best-first search with heuristic (stub implementation).
637    fn best_first_search(&mut self, sequent: &Sequent, max_depth: usize) -> Option<ProofTree> {
638        // For simplicity, fall back to DFS
639        self.dfs_search(sequent, 0, max_depth)
640    }
641
642    /// Iterative deepening search.
643    fn iterative_deepening_search(
644        &mut self,
645        sequent: &Sequent,
646        max_depth: usize,
647    ) -> Option<ProofTree> {
648        for depth in 1..=max_depth {
649            if let Some(proof) = self.dfs_search(sequent, 0, depth) {
650                return Some(proof);
651            }
652        }
653        None
654    }
655}
656
657/// Cut elimination transformation.
658///
659/// Cut elimination is a fundamental property of sequent calculus stating that
660/// any proof using the cut rule can be transformed into a cut-free proof.
661/// This is important for proof normalization and extracting computational content.
662pub struct CutElimination;
663
664impl CutElimination {
665    /// Attempt to eliminate all cut rules from a proof tree.
666    ///
667    /// Returns a cut-free proof if successful, or the original proof if
668    /// cut elimination fails or is not applicable.
669    pub fn eliminate(proof: ProofTree) -> ProofTree {
670        if !proof.uses_cut() {
671            return proof;
672        }
673
674        // Recursively eliminate cuts from premises first
675        let premises: Vec<ProofTree> = proof.premises.into_iter().map(Self::eliminate).collect();
676
677        // If this is a cut rule, try to eliminate it
678        if let InferenceRule::Cut { index } = proof.rule {
679            if premises.len() == 2 {
680                // Cut elimination algorithm (simplified)
681                // In a full implementation, this would perform the standard
682                // cut elimination procedure by permuting the cut rule upward
683                // and then removing it using logical rule inversions.
684
685                // For now, return a simplified version
686                // (A full implementation would be significantly more complex)
687                return ProofTree::new(proof.conclusion, InferenceRule::Cut { index }, premises);
688            }
689        }
690
691        // Return proof with eliminated premises
692        ProofTree::new(proof.conclusion, proof.rule, premises)
693    }
694
695    /// Check if a proof is cut-free.
696    pub fn is_cut_free(proof: &ProofTree) -> bool {
697        !proof.uses_cut()
698    }
699}
700
701#[cfg(test)]
702mod tests {
703    use super::*;
704    use crate::TLExpr;
705
706    #[test]
707    fn test_identity_sequent() {
708        let p = TLExpr::pred("P", vec![Term::var("x")]);
709        let seq = Sequent::identity(p);
710        assert!(seq.is_axiom());
711        assert_eq!(seq.antecedents.len(), 1);
712        assert_eq!(seq.consequents.len(), 1);
713    }
714
715    #[test]
716    fn test_weakening_left() {
717        let p = TLExpr::pred("P", vec![]);
718        let q = TLExpr::pred("Q", vec![]);
719        let seq = Sequent::identity(p.clone()).weaken_left(q.clone());
720        assert_eq!(seq.antecedents.len(), 2);
721        assert!(seq.antecedents.contains(&q));
722    }
723
724    #[test]
725    fn test_weakening_right() {
726        let p = TLExpr::pred("P", vec![]);
727        let q = TLExpr::pred("Q", vec![]);
728        let seq = Sequent::identity(p.clone()).weaken_right(q.clone());
729        assert_eq!(seq.consequents.len(), 2);
730        assert!(seq.consequents.contains(&q));
731    }
732
733    #[test]
734    fn test_contraction_left() {
735        let p = TLExpr::pred("P", vec![]);
736        let mut seq = Sequent::identity(p.clone());
737        seq.antecedents.push(p.clone());
738        assert_eq!(seq.antecedents.len(), 2);
739
740        let contracted = seq.contract_left(0);
741        assert!(contracted.is_some());
742        assert_eq!(contracted.unwrap().antecedents.len(), 1);
743    }
744
745    #[test]
746    fn test_free_vars() {
747        let p = TLExpr::pred("P", vec![Term::var("x")]);
748        let q = TLExpr::pred("Q", vec![Term::var("y")]);
749        let seq = Sequent::new(vec![p], vec![q]);
750
751        let vars = seq.free_vars();
752        assert_eq!(vars.len(), 2);
753        assert!(vars.contains("x"));
754        assert!(vars.contains("y"));
755    }
756
757    #[test]
758    fn test_sequent_substitute() {
759        let p_x = TLExpr::pred("P", vec![Term::var("x")]);
760        let seq = Sequent::identity(p_x.clone());
761
762        // Substitute x with a
763        let substituted = seq.substitute("x", &Term::constant("a"));
764        let p_a = TLExpr::pred("P", vec![Term::constant("a")]);
765        assert_eq!(substituted.antecedents[0], p_a);
766        assert_eq!(substituted.consequents[0], p_a);
767
768        // Verify original is unchanged
769        assert_eq!(seq.antecedents[0], p_x);
770    }
771
772    #[test]
773    fn test_sequent_substitute_capture_avoiding() {
774        // Test that substitution respects bound variables
775        // ∃x. P(x) ⊢ Q(x)
776        let p_x = TLExpr::pred("P", vec![Term::var("x")]);
777        let exists_p = TLExpr::exists("x", "Domain", p_x);
778        let q_x = TLExpr::pred("Q", vec![Term::var("x")]);
779        let seq = Sequent::new(vec![exists_p.clone()], vec![q_x]);
780
781        // Substitute x with a
782        let substituted = seq.substitute("x", &Term::constant("a"));
783
784        // Left side should be unchanged (x is bound)
785        assert_eq!(substituted.antecedents[0], exists_p);
786        // Right side should be substituted (x is free)
787        let q_a = TLExpr::pred("Q", vec![Term::constant("a")]);
788        assert_eq!(substituted.consequents[0], q_a);
789    }
790
791    #[test]
792    fn test_sequent_substitute_multiple() {
793        // P(x) ∧ Q(x) ⊢ R(x)
794        let p_x = TLExpr::pred("P", vec![Term::var("x")]);
795        let q_x = TLExpr::pred("Q", vec![Term::var("x")]);
796        let and_pq = TLExpr::and(p_x, q_x);
797        let r_x = TLExpr::pred("R", vec![Term::var("x")]);
798        let seq = Sequent::new(vec![and_pq], vec![r_x]);
799
800        // Substitute x with b
801        let substituted = seq.substitute("x", &Term::constant("b"));
802
803        // All x's should be replaced with b
804        let p_b = TLExpr::pred("P", vec![Term::constant("b")]);
805        let q_b = TLExpr::pred("Q", vec![Term::constant("b")]);
806        let and_pq_b = TLExpr::and(p_b, q_b);
807        let r_b = TLExpr::pred("R", vec![Term::constant("b")]);
808
809        assert_eq!(substituted.antecedents[0], and_pq_b);
810        assert_eq!(substituted.consequents[0], r_b);
811    }
812
813    #[test]
814    fn test_substitution() {
815        let p = TLExpr::pred("P", vec![Term::var("x")]);
816        let seq = Sequent::identity(p.clone());
817
818        // Substitution now works properly with unification
819        let substituted = seq.substitute("x", &Term::constant("a"));
820        let p_a = TLExpr::pred("P", vec![Term::constant("a")]);
821        assert_eq!(substituted.antecedents[0], p_a);
822        assert_eq!(substituted.consequents[0], p_a);
823    }
824
825    #[test]
826    fn test_identity_proof_tree() {
827        let p = TLExpr::pred("P", vec![]);
828        let proof = ProofTree::identity(p);
829        assert!(proof.is_valid());
830        assert_eq!(proof.depth(), 1);
831        assert_eq!(proof.size(), 1);
832        assert!(!proof.uses_cut());
833    }
834
835    #[test]
836    fn test_and_left_proof() {
837        let p = TLExpr::pred("P", vec![]);
838        let q = TLExpr::pred("Q", vec![]);
839        let and_pq = TLExpr::and(p.clone(), q.clone());
840
841        // Build proof: from P, Q ⊢ P, derive P ∧ Q ⊢ P
842        let _premise_seq = Sequent::new(vec![p.clone(), q], vec![p.clone()]);
843        let premise = ProofTree::identity(p.clone());
844
845        let conclusion_seq = Sequent::new(vec![and_pq], vec![p]);
846        let proof = ProofTree::new(
847            conclusion_seq,
848            InferenceRule::AndLeft { index: 0 },
849            vec![premise],
850        );
851
852        assert!(proof.is_valid());
853        assert_eq!(proof.depth(), 2);
854    }
855
856    #[test]
857    fn test_proof_search_simple() {
858        let p = TLExpr::pred("P", vec![]);
859        let sequent = Sequent::identity(p);
860
861        let mut engine =
862            ProofSearchEngine::new(ProofSearchStrategy::DepthFirst { max_depth: 10 }, 1000);
863
864        let proof = engine.search(&sequent);
865        assert!(proof.is_some());
866        assert!(proof.unwrap().is_valid());
867        assert!(engine.stats.proofs_generated > 0);
868    }
869
870    #[test]
871    fn test_proof_search_and() {
872        let p = TLExpr::pred("P", vec![]);
873        let q = TLExpr::pred("Q", vec![]);
874        let and_pq = TLExpr::and(p.clone(), q.clone());
875
876        // Try to prove: P ∧ Q ⊢ P
877        let sequent = Sequent::new(vec![and_pq], vec![p]);
878
879        let mut engine =
880            ProofSearchEngine::new(ProofSearchStrategy::DepthFirst { max_depth: 10 }, 1000);
881
882        let proof = engine.search(&sequent);
883        assert!(proof.is_some());
884        let proof = proof.unwrap();
885        assert!(proof.is_valid());
886        assert!(engine.stats.proofs_generated > 0);
887    }
888
889    #[test]
890    fn test_proof_search_not() {
891        let p = TLExpr::pred("P", vec![]);
892        let not_p = TLExpr::negate(p.clone());
893
894        // Try to prove: ¬P ⊢ ¬P (should be immediate)
895        let sequent = Sequent::identity(not_p);
896
897        let mut engine =
898            ProofSearchEngine::new(ProofSearchStrategy::DepthFirst { max_depth: 10 }, 1000);
899
900        let proof = engine.search(&sequent);
901        assert!(proof.is_some());
902        assert!(proof.unwrap().is_valid());
903    }
904
905    #[test]
906    fn test_cut_elimination_no_cut() {
907        let p = TLExpr::pred("P", vec![]);
908        let proof = ProofTree::identity(p);
909
910        assert!(CutElimination::is_cut_free(&proof));
911        let eliminated = CutElimination::eliminate(proof.clone());
912        assert_eq!(eliminated, proof);
913    }
914
915    #[test]
916    fn test_iterative_deepening_search() {
917        let p = TLExpr::pred("P", vec![]);
918        let q = TLExpr::pred("Q", vec![]);
919        let and_pq = TLExpr::and(p.clone(), q.clone());
920
921        let sequent = Sequent::new(vec![and_pq], vec![p]);
922
923        let mut engine = ProofSearchEngine::new(
924            ProofSearchStrategy::IterativeDeepening { max_depth: 10 },
925            1000,
926        );
927
928        let proof = engine.search(&sequent);
929        assert!(proof.is_some());
930        assert!(proof.unwrap().is_valid());
931    }
932}