reasonkit/thinktool/
fol.rs

1//! # First-Order Logic Verification
2//!
3//! Implements NL→FOL translation and logical verification for LaserLogic.
4//!
5//! ## Scientific Foundation
6//!
7//! Based on NL2FOL framework research:
8//! - 78-80% F1 on fallacy detection with FOL translation
9//! - Structured logical forms enable mechanical verification
10//! - Bridges natural language reasoning to formal proof
11//!
12//! ## Approach
13//!
14//! 1. Parse natural language into logical structure
15//! 2. Translate to First-Order Logic
16//! 3. Check validity using satisfiability rules
17//! 4. Detect fallacies through logical patterns
18//!
19//! ## Usage
20//!
21//! ```rust,ignore
22//! use reasonkit::thinktool::fol::{FolVerifier, FolConfig};
23//!
24//! let verifier = FolVerifier::new(FolConfig::default());
25//! let result = verifier.verify(argument).await?;
26//! ```
27
28use serde::{Deserialize, Serialize};
29
30/// Configuration for FOL verification
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct FolConfig {
33    /// Whether to generate full logical forms
34    pub generate_logical_forms: bool,
35    /// Enable fallacy detection
36    pub detect_fallacies: bool,
37    /// Enable validity checking
38    pub check_validity: bool,
39    /// Maximum nested quantifier depth
40    pub max_quantifier_depth: usize,
41    /// Enable soundness checking (premises + validity)
42    pub check_soundness: bool,
43    /// Confidence threshold for translation
44    pub translation_confidence_threshold: f32,
45}
46
47impl Default for FolConfig {
48    fn default() -> Self {
49        Self {
50            generate_logical_forms: true,
51            detect_fallacies: true,
52            check_validity: true,
53            max_quantifier_depth: 3,
54            check_soundness: true,
55            translation_confidence_threshold: 0.7,
56        }
57    }
58}
59
60impl FolConfig {
61    /// LaserLogic-optimized configuration
62    pub fn laser_logic() -> Self {
63        Self {
64            generate_logical_forms: true,
65            detect_fallacies: true,
66            check_validity: true,
67            max_quantifier_depth: 4,
68            check_soundness: true,
69            translation_confidence_threshold: 0.75,
70        }
71    }
72
73    /// Quick validity check only
74    pub fn quick_check() -> Self {
75        Self {
76            generate_logical_forms: false,
77            detect_fallacies: true,
78            check_validity: true,
79            max_quantifier_depth: 2,
80            check_soundness: false,
81            translation_confidence_threshold: 0.6,
82        }
83    }
84}
85
86/// Logical connectives
87#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
88pub enum Connective {
89    /// Logical AND (∧)
90    And,
91    /// Logical OR (∨)
92    Or,
93    /// Logical NOT (¬)
94    Not,
95    /// Implication (→)
96    Implies,
97    /// Biconditional (↔)
98    Iff,
99}
100
101impl Connective {
102    pub fn symbol(&self) -> &'static str {
103        match self {
104            Self::And => "∧",
105            Self::Or => "∨",
106            Self::Not => "¬",
107            Self::Implies => "→",
108            Self::Iff => "↔",
109        }
110    }
111}
112
113/// Quantifiers
114#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
115pub enum Quantifier {
116    /// Universal quantifier (∀)
117    ForAll,
118    /// Existential quantifier (∃)
119    Exists,
120}
121
122impl Quantifier {
123    pub fn symbol(&self) -> &'static str {
124        match self {
125            Self::ForAll => "∀",
126            Self::Exists => "∃",
127        }
128    }
129}
130
131/// A term in FOL (variable, constant, or function application)
132#[derive(Debug, Clone, Serialize, Deserialize)]
133pub enum Term {
134    /// A variable (x, y, z)
135    Variable(String),
136    /// A constant (john, 42)
137    Constant(String),
138    /// A function application (f(x), father(john))
139    Function { name: String, args: Vec<Term> },
140}
141
142/// A formula in FOL
143#[derive(Debug, Clone, Serialize, Deserialize)]
144pub enum Formula {
145    /// A predicate application (P(x), Loves(x, y))
146    Predicate { name: String, args: Vec<Term> },
147    /// Negation
148    Not(Box<Formula>),
149    /// Conjunction (AND)
150    And(Box<Formula>, Box<Formula>),
151    /// Disjunction (OR)
152    Or(Box<Formula>, Box<Formula>),
153    /// Implication
154    Implies(Box<Formula>, Box<Formula>),
155    /// Biconditional
156    Iff(Box<Formula>, Box<Formula>),
157    /// Universal quantification
158    ForAll {
159        variable: String,
160        formula: Box<Formula>,
161    },
162    /// Existential quantification
163    Exists {
164        variable: String,
165        formula: Box<Formula>,
166    },
167}
168
169impl Formula {
170    /// Check if formula is a simple predicate
171    pub fn is_atomic(&self) -> bool {
172        matches!(self, Formula::Predicate { .. })
173    }
174
175    /// Get free variables in the formula
176    pub fn free_variables(&self) -> Vec<String> {
177        match self {
178            Formula::Predicate { args, .. } => args
179                .iter()
180                .filter_map(|t| match t {
181                    Term::Variable(v) => Some(v.clone()),
182                    _ => None,
183                })
184                .collect(),
185            Formula::Not(f) => f.free_variables(),
186            Formula::And(l, r)
187            | Formula::Or(l, r)
188            | Formula::Implies(l, r)
189            | Formula::Iff(l, r) => {
190                let mut vars = l.free_variables();
191                vars.extend(r.free_variables());
192                vars.sort();
193                vars.dedup();
194                vars
195            }
196            Formula::ForAll { variable, formula } | Formula::Exists { variable, formula } => {
197                formula
198                    .free_variables()
199                    .into_iter()
200                    .filter(|v| v != variable)
201                    .collect()
202            }
203        }
204    }
205}
206
207/// An argument structure for FOL verification
208#[derive(Debug, Clone, Serialize, Deserialize)]
209pub struct FolArgument {
210    /// Natural language premises
211    pub premises_nl: Vec<String>,
212    /// Natural language conclusion
213    pub conclusion_nl: String,
214    /// Translated premises (FOL)
215    pub premises_fol: Vec<Formula>,
216    /// Translated conclusion (FOL)
217    pub conclusion_fol: Option<Formula>,
218    /// Translation confidence
219    pub translation_confidence: f32,
220    /// Any translation notes/issues
221    pub translation_notes: Vec<String>,
222}
223
224/// Fallacy types detectable through FOL analysis
225#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
226pub enum FolFallacy {
227    /// Affirming the consequent: P→Q, Q ⊢ P
228    AffirmingConsequent,
229    /// Denying the antecedent: P→Q, ¬P ⊢ ¬Q
230    DenyingAntecedent,
231    /// Undistributed middle: All A are B, All C are B ⊢ All A are C
232    UndistributedMiddle,
233    /// Illicit major/minor: Distribution errors in syllogisms
234    IllicitDistribution,
235    /// Four-term fallacy: Using a term with different meanings
236    FourTerms,
237    /// Existential fallacy: Assuming existence from universal
238    ExistentialFallacy,
239    /// Composition: What's true of parts is true of whole
240    Composition,
241    /// Division: What's true of whole is true of parts
242    Division,
243    /// Circular reasoning: Conclusion appears in premises
244    CircularReasoning,
245    /// Non sequitur: Conclusion doesn't follow logically
246    NonSequitur,
247}
248
249impl FolFallacy {
250    /// Get description of the fallacy
251    pub fn description(&self) -> &'static str {
252        match self {
253            Self::AffirmingConsequent => {
254                "Inferring P from P→Q and Q (invalid: other causes possible)"
255            }
256            Self::DenyingAntecedent => {
257                "Inferring ¬Q from P→Q and ¬P (invalid: Q might still be true)"
258            }
259            Self::UndistributedMiddle => "Middle term not distributed in at least one premise",
260            Self::IllicitDistribution => "Term distributed in conclusion but not in premises",
261            Self::FourTerms => "Four distinct terms used where three expected",
262            Self::ExistentialFallacy => "Assuming existence from purely universal premises",
263            Self::Composition => "Assuming whole has properties of its parts",
264            Self::Division => "Assuming parts have properties of the whole",
265            Self::CircularReasoning => "Conclusion is equivalent to or contained in premises",
266            Self::NonSequitur => "Conclusion does not logically follow from premises",
267        }
268    }
269
270    /// Get the logical pattern of the fallacy
271    pub fn pattern(&self) -> &'static str {
272        match self {
273            Self::AffirmingConsequent => "P→Q, Q ⊢ P (INVALID)",
274            Self::DenyingAntecedent => "P→Q, ¬P ⊢ ¬Q (INVALID)",
275            Self::UndistributedMiddle => "∀x(Mx→Px), ∀x(Mx→Sx) ⊢ ∀x(Sx→Px) (INVALID)",
276            Self::IllicitDistribution => "∀x(Px→Mx), ∃x(Mx∧Sx) ⊢ ∀x(Sx→Px) (INVALID)",
277            Self::FourTerms => "Four distinct predicates instead of three",
278            Self::ExistentialFallacy => "∀x(Px→Qx) ⊢ ∃x(Px∧Qx) (INVALID if ¬∃xPx)",
279            Self::Composition => "∀x∈S(Px) ⊢ P(S) (INVALID)",
280            Self::Division => "P(S) ⊢ ∀x∈S(Px) (INVALID)",
281            Self::CircularReasoning => "P ⊢ P (TRIVIALLY VALID but uninformative)",
282            Self::NonSequitur => "No valid derivation path",
283        }
284    }
285}
286
287/// A detected fallacy with evidence
288#[derive(Debug, Clone, Serialize, Deserialize)]
289pub struct DetectedFallacy {
290    pub fallacy: FolFallacy,
291    /// Confidence in detection (0.0 - 1.0)
292    pub confidence: f32,
293    /// Evidence/explanation
294    pub evidence: String,
295    /// Which premises are involved
296    pub involved_premises: Vec<usize>,
297}
298
299/// Validity status
300#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
301pub enum ValidityStatus {
302    /// Argument is logically valid
303    Valid,
304    /// Argument is logically invalid
305    Invalid,
306    /// Validity could not be determined
307    Undetermined,
308}
309
310/// Soundness status (validity + true premises)
311#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
312pub enum SoundnessStatus {
313    /// Argument is sound (valid with true premises)
314    Sound,
315    /// Argument is unsound (invalid or false premises)
316    Unsound,
317    /// Soundness could not be determined
318    Undetermined,
319}
320
321/// Complete FOL verification result
322#[derive(Debug, Clone, Serialize, Deserialize)]
323pub struct FolResult {
324    /// The translated argument
325    pub argument: FolArgument,
326    /// Validity status
327    pub validity: ValidityStatus,
328    /// Validity explanation
329    pub validity_explanation: String,
330    /// Soundness status
331    pub soundness: SoundnessStatus,
332    /// Premise truth assessments
333    pub premise_assessments: Vec<PremiseAssessment>,
334    /// Detected fallacies
335    pub fallacies: Vec<DetectedFallacy>,
336    /// Overall confidence in the analysis
337    pub confidence: f32,
338    /// Suggested improvements
339    pub suggestions: Vec<String>,
340}
341
342/// Assessment of a single premise
343#[derive(Debug, Clone, Serialize, Deserialize)]
344pub struct PremiseAssessment {
345    pub premise_index: usize,
346    pub premise_nl: String,
347    /// Is the premise likely true?
348    pub likely_true: Option<bool>,
349    /// Confidence in truth assessment
350    pub confidence: f32,
351    /// Reasoning for assessment
352    pub reasoning: String,
353}
354
355impl FolResult {
356    /// Overall verdict
357    pub fn verdict(&self) -> &'static str {
358        match (self.validity, self.soundness) {
359            (ValidityStatus::Valid, SoundnessStatus::Sound) => {
360                "SOUND - Argument is valid with true premises"
361            }
362            (ValidityStatus::Valid, SoundnessStatus::Unsound) => {
363                "VALID BUT UNSOUND - Valid logic but questionable premises"
364            }
365            (ValidityStatus::Valid, SoundnessStatus::Undetermined) => {
366                "VALID - Logic is correct, premises unverified"
367            }
368            (ValidityStatus::Invalid, _) => "INVALID - Conclusion does not follow from premises",
369            (ValidityStatus::Undetermined, _) => "UNDETERMINED - Could not fully analyze",
370        }
371    }
372
373    /// Has any fallacies?
374    pub fn has_fallacies(&self) -> bool {
375        !self.fallacies.is_empty()
376    }
377
378    /// Most severe fallacy
379    pub fn most_confident_fallacy(&self) -> Option<&DetectedFallacy> {
380        self.fallacies.iter().max_by(|a, b| {
381            a.confidence
382                .partial_cmp(&b.confidence)
383                .unwrap_or(std::cmp::Ordering::Equal)
384        })
385    }
386}
387
388/// Prompt templates for FOL verification
389pub struct FolPrompts;
390
391impl FolPrompts {
392    /// Translate argument to FOL
393    pub fn translate(premises: &[String], conclusion: &str) -> String {
394        let premises_formatted: String = premises
395            .iter()
396            .enumerate()
397            .map(|(i, p)| format!("P{}: {}", i + 1, p))
398            .collect::<Vec<_>>()
399            .join("\n");
400
401        format!(
402            r#"Translate this argument into First-Order Logic (FOL).
403
404PREMISES:
405{premises_formatted}
406
407CONCLUSION:
408C: {conclusion}
409
410For each statement:
4111. Identify predicates (properties and relations)
4122. Identify quantifiers (all, some, none)
4133. Identify logical connectives (and, or, not, if-then)
4144. Express in FOL notation
415
416Use standard symbols:
417- ∀x (for all x)
418- ∃x (there exists x)
419- ∧ (and)
420- ∨ (or)
421- ¬ (not)
422- → (implies)
423- ↔ (if and only if)
424
425Format:
426P1_FOL: [FOL translation]
427P2_FOL: [FOL translation]
428...
429C_FOL: [FOL translation]
430
431TRANSLATION_CONFIDENCE: [0.0-1.0]
432TRANSLATION_NOTES: [any ambiguities or assumptions]"#,
433            premises_formatted = premises_formatted,
434            conclusion = conclusion
435        )
436    }
437
438    /// Check validity of FOL argument
439    pub fn check_validity(premises_fol: &[String], conclusion_fol: &str) -> String {
440        let premises_formatted: String = premises_fol
441            .iter()
442            .enumerate()
443            .map(|(i, p)| format!("P{}: {}", i + 1, p))
444            .collect::<Vec<_>>()
445            .join("\n");
446
447        format!(
448            r#"Determine the logical validity of this argument.
449
450PREMISES (FOL):
451{premises_formatted}
452
453CONCLUSION (FOL):
454C: {conclusion_fol}
455
456An argument is VALID if: whenever all premises are true, the conclusion MUST be true.
457An argument is INVALID if: it's possible for all premises to be true while the conclusion is false.
458
459Analysis steps:
4601. Identify the logical structure
4612. Look for counterexamples (model where premises true, conclusion false)
4623. If no counterexample possible, the argument is valid
463
464VALIDITY: [VALID | INVALID | UNDETERMINED]
465EXPLANATION: [why valid/invalid]
466COUNTEREXAMPLE: [if invalid, describe a scenario where premises are true but conclusion is false]
467CONFIDENCE: [0.0-1.0]"#,
468            premises_formatted = premises_formatted,
469            conclusion_fol = conclusion_fol
470        )
471    }
472
473    /// Detect fallacies
474    pub fn detect_fallacies(argument_nl: &str, argument_fol: Option<&str>) -> String {
475        let fol_section = argument_fol.map_or(
476            "Not available - analyze from natural language only.".to_string(),
477            |f| format!("FOL:\n{}", f),
478        );
479
480        format!(
481            r#"Analyze this argument for logical fallacies.
482
483ARGUMENT (Natural Language):
484{argument_nl}
485
486{fol_section}
487
488Check for these formal fallacies:
4891. Affirming the Consequent: P→Q, Q ⊢ P
4902. Denying the Antecedent: P→Q, ¬P ⊢ ¬Q
4913. Undistributed Middle: syllogism with undistributed middle term
4924. Illicit Distribution: invalid distribution in syllogism
4935. Four-Term Fallacy: using equivocal term
4946. Existential Fallacy: invalid existential inference
4957. Composition/Division: part-whole errors
4968. Circular Reasoning: conclusion in premises
4979. Non Sequitur: conclusion doesn't follow
498
499For each fallacy found:
500FALLACY: [name]
501PATTERN: [the invalid logical pattern]
502EVIDENCE: [where this appears in the argument]
503CONFIDENCE: [0.0-1.0]
504
505If no fallacies found:
506NO_FALLACIES_DETECTED
507CONFIDENCE: [0.0-1.0]"#,
508            argument_nl = argument_nl,
509            fol_section = fol_section
510        )
511    }
512
513    /// Assess premise truth
514    pub fn assess_premises(premises: &[String]) -> String {
515        let premises_formatted: String = premises
516            .iter()
517            .enumerate()
518            .map(|(i, p)| format!("P{}: {}", i + 1, p))
519            .collect::<Vec<_>>()
520            .join("\n");
521
522        format!(
523            r#"Assess the truth of each premise.
524
525PREMISES:
526{premises_formatted}
527
528For each premise:
5291. Is it empirically verifiable?
5302. Is it a definitional truth?
5313. Is it a widely accepted claim?
5324. What evidence supports or refutes it?
533
534Format:
535P1_ASSESSMENT:
536- LIKELY_TRUE: [true | false | unknown]
537- CONFIDENCE: [0.0-1.0]
538- REASONING: [explanation]
539
540P2_ASSESSMENT:
541..."#,
542            premises_formatted = premises_formatted
543        )
544    }
545}
546
547#[cfg(test)]
548mod tests {
549    use super::*;
550
551    #[test]
552    fn test_config_default() {
553        let config = FolConfig::default();
554        assert!(config.generate_logical_forms);
555        assert!(config.detect_fallacies);
556        assert!(config.check_validity);
557    }
558
559    #[test]
560    fn test_connectives() {
561        assert_eq!(Connective::And.symbol(), "∧");
562        assert_eq!(Connective::Or.symbol(), "∨");
563        assert_eq!(Connective::Implies.symbol(), "→");
564    }
565
566    #[test]
567    fn test_quantifiers() {
568        assert_eq!(Quantifier::ForAll.symbol(), "∀");
569        assert_eq!(Quantifier::Exists.symbol(), "∃");
570    }
571
572    #[test]
573    fn test_formula_free_variables() {
574        // P(x, y)
575        let formula = Formula::Predicate {
576            name: "P".into(),
577            args: vec![Term::Variable("x".into()), Term::Variable("y".into())],
578        };
579        let vars = formula.free_variables();
580        assert!(vars.contains(&"x".to_string()));
581        assert!(vars.contains(&"y".to_string()));
582
583        // ∀x P(x, y) - y is free, x is bound
584        let quantified = Formula::ForAll {
585            variable: "x".into(),
586            formula: Box::new(formula.clone()),
587        };
588        let vars = quantified.free_variables();
589        assert!(!vars.contains(&"x".to_string()));
590        assert!(vars.contains(&"y".to_string()));
591    }
592
593    #[test]
594    fn test_fallacy_descriptions() {
595        let fallacy = FolFallacy::AffirmingConsequent;
596        assert!(fallacy.description().contains("Inferring P"));
597        assert!(fallacy.pattern().contains("INVALID"));
598    }
599
600    #[test]
601    fn test_result_verdict() {
602        let result = FolResult {
603            argument: FolArgument {
604                premises_nl: vec!["All men are mortal".into()],
605                conclusion_nl: "Socrates is mortal".into(),
606                premises_fol: vec![],
607                conclusion_fol: None,
608                translation_confidence: 0.9,
609                translation_notes: vec![],
610            },
611            validity: ValidityStatus::Valid,
612            validity_explanation: "Valid syllogism".into(),
613            soundness: SoundnessStatus::Sound,
614            premise_assessments: vec![],
615            fallacies: vec![],
616            confidence: 0.95,
617            suggestions: vec![],
618        };
619
620        assert!(result.verdict().contains("SOUND"));
621        assert!(!result.has_fallacies());
622    }
623
624    #[test]
625    fn test_invalid_result() {
626        let result = FolResult {
627            argument: FolArgument {
628                premises_nl: vec![
629                    "If it rains, the ground is wet".into(),
630                    "The ground is wet".into(),
631                ],
632                conclusion_nl: "It rained".into(),
633                premises_fol: vec![],
634                conclusion_fol: None,
635                translation_confidence: 0.85,
636                translation_notes: vec![],
637            },
638            validity: ValidityStatus::Invalid,
639            validity_explanation: "Affirming the consequent".into(),
640            soundness: SoundnessStatus::Unsound,
641            premise_assessments: vec![],
642            fallacies: vec![DetectedFallacy {
643                fallacy: FolFallacy::AffirmingConsequent,
644                confidence: 0.95,
645                evidence: "Inferring cause from effect".into(),
646                involved_premises: vec![0, 1],
647            }],
648            confidence: 0.9,
649            suggestions: vec!["Consider other causes".into()],
650        };
651
652        assert!(result.verdict().contains("INVALID"));
653        assert!(result.has_fallacies());
654        assert_eq!(result.fallacies[0].fallacy, FolFallacy::AffirmingConsequent);
655    }
656}