unicode-fol-kit 0.5.2__tar.gz → 0.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. {unicode_fol_kit-0.5.2 → unicode_fol_kit-0.6.0}/CHANGELOG.md +123 -0
  2. {unicode_fol_kit-0.5.2 → unicode_fol_kit-0.6.0}/PKG-INFO +231 -1
  3. {unicode_fol_kit-0.5.2 → unicode_fol_kit-0.6.0}/README.md +230 -0
  4. {unicode_fol_kit-0.5.2 → unicode_fol_kit-0.6.0}/pyproject.toml +1 -1
  5. {unicode_fol_kit-0.5.2 → unicode_fol_kit-0.6.0}/unicode_fol_kit/__init__.py +35 -1
  6. unicode_fol_kit-0.6.0/unicode_fol_kit/atp/__init__.py +47 -0
  7. unicode_fol_kit-0.6.0/unicode_fol_kit/atp/fitch.py +1422 -0
  8. unicode_fol_kit-0.6.0/unicode_fol_kit/atp/fitch_search.py +486 -0
  9. unicode_fol_kit-0.6.0/unicode_fol_kit/atp/lj.py +352 -0
  10. unicode_fol_kit-0.6.0/unicode_fol_kit/atp/sequent.py +915 -0
  11. unicode_fol_kit-0.6.0/unicode_fol_kit/atp/tableau.py +255 -0
  12. unicode_fol_kit-0.6.0/unicode_fol_kit/atp/z3_input.py +179 -0
  13. {unicode_fol_kit-0.5.2 → unicode_fol_kit-0.6.0}/unicode_fol_kit/fol/__init__.py +6 -0
  14. unicode_fol_kit-0.6.0/unicode_fol_kit/fol/prover9_input.py +210 -0
  15. unicode_fol_kit-0.6.0/unicode_fol_kit/fol/tptp_input.py +314 -0
  16. unicode_fol_kit-0.6.0/unicode_fol_kit/fol/verbalize.py +106 -0
  17. {unicode_fol_kit-0.5.2 → unicode_fol_kit-0.6.0}/unicode_fol_kit/semantics/__init__.py +13 -0
  18. unicode_fol_kit-0.6.0/unicode_fol_kit/semantics/intuitionistic.py +151 -0
  19. unicode_fol_kit-0.6.0/unicode_fol_kit/semantics/modelfinder.py +189 -0
  20. unicode_fol_kit-0.6.0/unicode_fol_kit/semantics/truthtable.py +158 -0
  21. unicode_fol_kit-0.5.2/unicode_fol_kit/atp/__init__.py +0 -21
  22. {unicode_fol_kit-0.5.2 → unicode_fol_kit-0.6.0}/.gitignore +0 -0
  23. {unicode_fol_kit-0.5.2 → unicode_fol_kit-0.6.0}/LICENSE +0 -0
  24. {unicode_fol_kit-0.5.2 → unicode_fol_kit-0.6.0}/unicode_fol_kit/__main__.py +0 -0
  25. {unicode_fol_kit-0.5.2 → unicode_fol_kit-0.6.0}/unicode_fol_kit/atp/prover9_entailment.py +0 -0
  26. {unicode_fol_kit-0.5.2 → unicode_fol_kit-0.6.0}/unicode_fol_kit/atp/resolution.py +0 -0
  27. {unicode_fol_kit-0.5.2 → unicode_fol_kit-0.6.0}/unicode_fol_kit/atp/vampire_entailment.py +0 -0
  28. {unicode_fol_kit-0.5.2 → unicode_fol_kit-0.6.0}/unicode_fol_kit/atp/z3_arith.py +0 -0
  29. {unicode_fol_kit-0.5.2 → unicode_fol_kit-0.6.0}/unicode_fol_kit/atp/z3_equivalence.py +0 -0
  30. {unicode_fol_kit-0.5.2 → unicode_fol_kit-0.6.0}/unicode_fol_kit/atp/z3_fuzzy.py +0 -0
  31. {unicode_fol_kit-0.5.2 → unicode_fol_kit-0.6.0}/unicode_fol_kit/atp/z3_models.py +0 -0
  32. {unicode_fol_kit-0.5.2 → unicode_fol_kit-0.6.0}/unicode_fol_kit/eval/__init__.py +0 -0
  33. {unicode_fol_kit-0.5.2 → unicode_fol_kit-0.6.0}/unicode_fol_kit/eval/canonical.py +0 -0
  34. {unicode_fol_kit-0.5.2 → unicode_fol_kit-0.6.0}/unicode_fol_kit/eval/predicate_match.py +0 -0
  35. {unicode_fol_kit-0.5.2 → unicode_fol_kit-0.6.0}/unicode_fol_kit/eval/validate.py +0 -0
  36. {unicode_fol_kit-0.5.2 → unicode_fol_kit-0.6.0}/unicode_fol_kit/fol/_fol_nodes.py +0 -0
  37. {unicode_fol_kit-0.5.2 → unicode_fol_kit-0.6.0}/unicode_fol_kit/fol/_modal_nodes.py +0 -0
  38. {unicode_fol_kit-0.5.2 → unicode_fol_kit-0.6.0}/unicode_fol_kit/fol/_msfl_nodes.py +0 -0
  39. {unicode_fol_kit-0.5.2 → unicode_fol_kit-0.6.0}/unicode_fol_kit/fol/_so_nodes.py +0 -0
  40. {unicode_fol_kit-0.5.2 → unicode_fol_kit-0.6.0}/unicode_fol_kit/fol/grammars/fl.lark +0 -0
  41. {unicode_fol_kit-0.5.2 → unicode_fol_kit-0.6.0}/unicode_fol_kit/fol/grammars/fol.lark +0 -0
  42. {unicode_fol_kit-0.5.2 → unicode_fol_kit-0.6.0}/unicode_fol_kit/fol/grammars/modal.lark +0 -0
  43. {unicode_fol_kit-0.5.2 → unicode_fol_kit-0.6.0}/unicode_fol_kit/fol/grammars/msfl.lark +0 -0
  44. {unicode_fol_kit-0.5.2 → unicode_fol_kit-0.6.0}/unicode_fol_kit/fol/grammars/msfol.lark +0 -0
  45. {unicode_fol_kit-0.5.2 → unicode_fol_kit-0.6.0}/unicode_fol_kit/fol/grammars/so.lark +0 -0
  46. {unicode_fol_kit-0.5.2 → unicode_fol_kit-0.6.0}/unicode_fol_kit/fol/grammars/terminals.lark +0 -0
  47. {unicode_fol_kit-0.5.2 → unicode_fol_kit-0.6.0}/unicode_fol_kit/fol/lambda_tools.py +0 -0
  48. {unicode_fol_kit-0.5.2 → unicode_fol_kit-0.6.0}/unicode_fol_kit/fol/latex_input.py +0 -0
  49. {unicode_fol_kit-0.5.2 → unicode_fol_kit-0.6.0}/unicode_fol_kit/fol/modal_translation.py +0 -0
  50. {unicode_fol_kit-0.5.2 → unicode_fol_kit-0.6.0}/unicode_fol_kit/fol/msflparser.py +0 -0
  51. {unicode_fol_kit-0.5.2 → unicode_fol_kit-0.6.0}/unicode_fol_kit/fol/naming.py +0 -0
  52. {unicode_fol_kit-0.5.2 → unicode_fol_kit-0.6.0}/unicode_fol_kit/fol/nodes.py +0 -0
  53. {unicode_fol_kit-0.5.2 → unicode_fol_kit-0.6.0}/unicode_fol_kit/fol/normalforms.py +0 -0
  54. {unicode_fol_kit-0.5.2 → unicode_fol_kit-0.6.0}/unicode_fol_kit/fol/unification.py +0 -0
  55. {unicode_fol_kit-0.5.2 → unicode_fol_kit-0.6.0}/unicode_fol_kit/py.typed +0 -0
  56. {unicode_fol_kit-0.5.2 → unicode_fol_kit-0.6.0}/unicode_fol_kit/semantics/_modal_reject.py +0 -0
  57. {unicode_fol_kit-0.5.2 → unicode_fol_kit-0.6.0}/unicode_fol_kit/semantics/fuzzy.py +0 -0
  58. {unicode_fol_kit-0.5.2 → unicode_fol_kit-0.6.0}/unicode_fol_kit/semantics/kripke.py +0 -0
  59. {unicode_fol_kit-0.5.2 → unicode_fol_kit-0.6.0}/unicode_fol_kit/semantics/manyvalued.py +0 -0
  60. {unicode_fol_kit-0.5.2 → unicode_fol_kit-0.6.0}/unicode_fol_kit/semantics/secondorder.py +0 -0
  61. {unicode_fol_kit-0.5.2 → unicode_fol_kit-0.6.0}/unicode_fol_kit/semantics/tarski.py +0 -0
@@ -5,6 +5,129 @@ loosely based on [Keep a Changelog](https://keepachangelog.com/). Versioning is
5
5
  semantic, but the project is pre-1.0 (alpha): a **minor** release may contain
6
6
  breaking changes.
7
7
 
8
+ ## [0.6.0] - 2026-06-27
9
+
10
+ A large reasoning-and-interoperability release: a Fitch natural-deduction checker and
11
+ backtracking prover, a Gentzen **LK** sequent calculus (with second-order rules) and an
12
+ intuitionistic **LJ** calculus, analytic tableaux, a finite model finder, truth tables,
13
+ reverse importers for TPTP / Prover9 / Z3-SMT-LIB, intuitionistic Kripke semantics, and
14
+ formula verbalization. All additive.
15
+
16
+ ### Added
17
+
18
+ - **Fitch-style natural-deduction proof checker** (`unicode_fol_kit.atp.fitch`) —
19
+ `Proof` / `Subproof` / `Line` / `Justification` proof objects (frozen, hashable,
20
+ JSON-serialisable) plus `check_proof` / `verify_proof`, all re-exported at the
21
+ package top level. The checker is *sound*: it returns `True` only when every
22
+ line genuinely follows by the cited rule and the proof's premises entail its
23
+ conclusion; `verify_proof` reports the certified sequent and the first failing
24
+ line with a reason.
25
+ - **Classical FOL / MSFOL** (`logic="fol"`/`"msfol"`) is checked by a syntactic
26
+ rule table: the connective rules (`∧I`/`∧E`, `∨I`/`∨E`, `→I`/`→E`, `↔I`/`↔E`,
27
+ `¬I`, `⊥I`/`⊥E`, `¬E` double-negation, `RAA`, `Reit`), the quantifier rules
28
+ (`∀I`/`∀E`, `∃I`/`∃E`) with the eigenvariable side-conditions enforced via a
29
+ capture-avoiding substitution, and equality (`=I`/`=E`, certified against Z3).
30
+ Citation accessibility is enforced (no reaching into a closed sibling
31
+ subproof) and discharge rules are checked against the proof's *open
32
+ assumptions*. `⊥` is the reserved logical constant `FALSUM`.
33
+ - **Three-valued K3 / LP** (`logic="K3"`/`"LP"`) certify each step against the
34
+ many-valued decision procedure (`semantics.manyvalued.entails`), so the
35
+ paraconsistency facts hold: LP rejects modus ponens, the disjunctive
36
+ syllogism, and explosion; K3 has no zero-premise theorems. Propositional
37
+ fragment.
38
+ - **Modal family** (`logic="K"`/`"T"`/`"S4"`/`"S5"`) certifies each step by the
39
+ standard translation to FOL plus the frame axioms, decided by Z3. Knowledge
40
+ (`Knows`, S5) is factive; belief (`Believes`, KD45) and obligation
41
+ (`Obligatory`, KD) are not. Propositional fragment; temporal and quantified
42
+ modal input are rejected.
43
+ - **Rendering** — `render_fitch` (Unicode/ASCII scope bars, line-number gutter,
44
+ justification column; also `proof.to_fitch()`) and `render_latex_fitch`
45
+ (self-contained LaTeX `array`; also `proof.to_latex_fitch()`).
46
+ - Tested with hand-derived proofs per rule, soundness guards for the broken
47
+ cases, and a randomised audit that checks every accepted proof line-by-line
48
+ against the Z3 / resolution oracles.
49
+ - **Gentzen sequent-calculus checker** (`unicode_fol_kit.atp.sequent`) — a
50
+ two-sided **LK** derivation checker re-exported at the package top level:
51
+ `Sequent` / `Derivation` / `Comprehension` / `SequentResult`, the helpers
52
+ `sequent` / `derive` / `axiom`, and `check_sequent_proof` / `verify_sequent_proof`
53
+ / `render_sequent_proof`. A sequent `Γ ⊢ Δ` (multisets, read `⋀Γ → ⋁Δ`) is
54
+ derived by a tree of rules; the checker verifies each step.
55
+ - Rules: `Ax`, structural `WL`/`WR`/`CL`/`CR`/`Cut`, the connective rules
56
+ (`¬`, `∧`, `∨`, `→`, `↔`, each L and R), the first-order quantifier rules
57
+ (`∀L`/`∀R`, `∃L`/`∃R`, with the eigenvariable condition on `∀R`/`∃L`), and the
58
+ **second-order** rules `∀²L`/`∀²R`, `∃²L`/`∃²R`. `∀²L`/`∃²R` instantiate a bound
59
+ predicate variable with a comprehension term `λx̄.ψ` (a `Comprehension`,
60
+ arity-checked, capture-avoiding); `∀²R`/`∃²L` use a fresh predicate
61
+ eigenvariable. This reaches the second-order fragment (`second_order=True`),
62
+ which has no first-order / SMT encoding.
63
+ - Sound but, for full second-order logic, necessarily **not a complete prover**
64
+ (second-order validity is not r.e.). Tested with hand derivations per rule,
65
+ soundness guards, a randomised mutation audit that re-checks every accepted
66
+ derivation node-by-node against Z3 (first-order fragment), and `satisfies_so`
67
+ spot-checks over small finite models (second-order fragment).
68
+ - **Analytic tableaux** (`unicode_fol_kit.atp.tableau`) — `is_valid_tableau`,
69
+ `prove_tableau`, `tableau_closed`, and `tableau_model`, re-exported at the top
70
+ level. A fourth proof method (beside resolution, Fitch, and the sequent calculus):
71
+ the signed-free α/β/γ/δ rules, a branch closing on `φ`/`¬φ`. Decidable and complete
72
+ for the propositional fragment; first-order γ-instantiation is bounded (`max_terms`
73
+ / `max_steps`). An *open* branch is returned as a countermodel by `tableau_model`.
74
+ - **Finite model finder** (`unicode_fol_kit.semantics.modelfinder`) — `find_model`,
75
+ `find_countermodel`, `is_satisfiable_finite`, and `is_valid_finite`. Brute-force
76
+ enumeration of finite `Structure`s (domain `1..max_size`) checked with the Tarskian
77
+ evaluator — the Mace4-style partner of the provers (a valid entailment has no
78
+ countermodel; an invalid one usually a small finite one). Bounded by
79
+ `max_candidates`.
80
+ - **Truth tables** (`unicode_fol_kit.semantics.truthtable`) — `truth_table` returning
81
+ a `TruthTable` (Markdown `render`, `is_tautology`/`is_contradiction`/`is_satisfiable`),
82
+ plus `is_tautology` / `is_contradiction` / `is_satisfiable_tt`, over **classical**,
83
+ Kleene **K3**, and Priest **LP** value sets (cross-checked against Z3 for classical).
84
+ - **Intuitionistic propositional logic** (`unicode_fol_kit.semantics.intuitionistic`) —
85
+ `IntKripkeModel` with monotone Kripke `forces`, and `int_valid` / `int_countermodel`
86
+ that decide intuitionistic validity by Kripke-model search (the logic has the
87
+ finite-model property). Excluded middle, double-negation elimination, and Peirce's
88
+ law are reported invalid with explicit countermodels; every intuitionistic validity
89
+ is also classically valid (cross-checked).
90
+ - **Intuitionistic sequent calculus LJ** (`unicode_fol_kit.atp.lj`) — `check_lj_proof`
91
+ / `verify_lj_proof`, re-exported at the top level. Gentzen **LJ** is the LK calculus
92
+ (it reuses the same `Sequent` / `Derivation` data model) restricted to **at most one
93
+ succedent formula** — the change that makes excluded middle / double-negation
94
+ elimination / Peirce's law underivable. Rules: `Ax`, structural `WL`/`WR`/`CL`/`Cut`,
95
+ `¬`/`∧`/`→`/`↔` (L and R), the split disjunction-right `∨R1`/`∨R2` and `∨L`, and the
96
+ quantifier rules `∀L`/`∀R`, `∃L`/`∃R`. Accepted derivations are cross-checked against
97
+ the intuitionistic Kripke decision procedure and classical Z3 validity.
98
+ - **Verbalization** (`unicode_fol_kit.fol.verbalize`) — `to_english`, an English
99
+ paraphrase of a formula (a readability aid, not a parse inverse).
100
+ - **Fitch proof *searcher*** (`unicode_fol_kit.atp.fitch_search`) — `find_fitch_proof`,
101
+ `fitch_prove`, and `is_valid_fitch`, re-exported at the package top level. A
102
+ goal-directed, **iterative-deepening backtracking** search over the classical
103
+ propositional + first-order natural-deduction rules (introduction rules, ∨/∃
104
+ elimination by case split, backward chaining, ex falso, and reductio/RAA — which
105
+ makes it complete for the propositional fragment). It builds an actual `Proof`
106
+ that is re-validated by `check_proof` before being returned, so it is **sound by
107
+ construction**: a search/assembly bug can only make it fail to find a proof, never
108
+ return an unsound one. Like the resolution prover it is sound but, under its depth
109
+ bound, incomplete (`None`/`False` = "not found within `max_depth`"). Tested with
110
+ curated theorems/non-theorems and a randomised cross-check that every found proof
111
+ is Z3-valid.
112
+ - **Reverse importers for TPTP, Prover9, and Z3/SMT-LIB** — the inverses of
113
+ `to_tptp` / `to_prover9` / `to_z3`, all re-exported at the package top level:
114
+ - **TPTP** (`unicode_fol_kit.fol.tptp_input`): `parse_tptp_formula` (one bare
115
+ FOF/CNF formula → `Node`), `parse_tptp` (a whole problem → a list of
116
+ `TptpFormula(name, role, formula)`), and `load_tptp` (a `.p`/`.tptp` file), via
117
+ a dedicated Lark grammar. Round-trips `to_tptp`; `%` and `/* */` comments are
118
+ ignored; predicates are re-capitalised (TPTP lowercases them); typed
119
+ `tff`/`thf` and `include` are out of scope.
120
+ - **Prover9/LADR** (`unicode_fol_kit.fol.prover9_input`): `parse_prover9`,
121
+ following `set(prolog_style_variables)` to match `to_prover9`'s output (a
122
+ trailing `.` is accepted). `Xor` round-trips to its `(a|b) & -(a&b)` desugaring.
123
+ - **Z3** (`unicode_fol_kit.atp.z3_input`): `from_z3` (a `z3.ExprRef` → `Node`)
124
+ and `parse_smtlib` / `load_smtlib` (SMT-LIB2 via Z3's own parser). Conversion is
125
+ meaning-preserving (Z3 collapses variables/constants/numbers onto one
126
+ uninterpreted sort, so a free variable returns as a `Constant`).
127
+ - Tested by round-trip over random formulas (`parse(node.to_X()) == node`) for
128
+ TPTP/Prover9 and by logical equivalence (`is_valid(Iff(node, from_z3(node.to_z3())))`)
129
+ for Z3, plus curated problem-file and SMT-LIB cases.
130
+
8
131
  ## [0.5.2] - 2026-06-26
9
132
 
10
133
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: unicode-fol-kit
3
- Version: 0.5.2
3
+ Version: 0.6.0
4
4
  Summary: Parser and toolkit for first-order logic formulas using Unicode operators
5
5
  Project-URL: Repository, https://github.com/fvossel/unicode-fol-kit
6
6
  Project-URL: Issues, https://github.com/fvossel/unicode-fol-kit/issues
@@ -39,6 +39,7 @@ A Python toolkit for parsing and working with first-order logic (FOL) formulas w
39
39
  - **Unicode round-trip** — `to_unicode_str()` renders any node back to a parseable Unicode formula; re-parsing in the matching mode yields a structurally equal AST
40
40
  - **LaTeX export** — `to_latex()` renders any node as LaTeX math-mode markup, with the same precedence-aware parenthesisation as the Unicode renderer
41
41
  - **LaTeX import** — `parse_latex()` reads a LaTeX-math formula (the inverse of `to_latex()`); `latex_to_unicode()` does the LaTeX→Unicode translation on its own
42
+ - **TPTP / Prover9 / SMT-LIB import** — read formulas back from the standard tools: `parse_tptp()` / `parse_tptp_formula()` / `load_tptp()` (FOF & CNF), `parse_prover9()`, and `from_z3()` / `parse_smtlib()` — the inverses of `to_tptp()` / `to_prover9()` / `to_z3()`
42
43
  - **Normal forms** — `to_nnf()`, `to_pnf()`, `to_cnf()`, `to_dnf()` (equivalence-preserving), `to_tseitin_cnf()` (equisatisfiable, no blow-up), and `skolemize()` (satisfiability-preserving) for classical FOL
43
44
  - **Horn check** — `is_horn()` reports whether a formula's clausal form consists of Horn clauses
44
45
  - **Traversal API** — `walk()`, `subformulas()`, `atoms()`, `variables()`, `count()`, `depth()` on every node
@@ -50,6 +51,14 @@ A Python toolkit for parsing and working with first-order logic (FOL) formulas w
50
51
  - **Equivalence checking** — check if two formulas are logically equivalent via Z3
51
52
  - **Entailment checking** — check if a conclusion follows from premises via Prover9 (`check_logical_entailment`) or Vampire (`check_logical_entailment_vampire`), each taking the prover's executable path as an argument
52
53
  - **Built-in resolution prover** — `prove()` and `is_valid_resolution()` decide entailment/validity in-process (sound first-order resolution, no external solver needed)
54
+ - **Natural-deduction proof checker** — `check_proof()` / `verify_proof()` check Fitch-style proofs with nested subproofs and discharge rules (classical FOL/MSFOL, plus K3/LP and the modal family via `logic=`); `render_fitch()` lays them out with scope bars and a justification column
55
+ - **Sequent-calculus proof checker** — `check_sequent_proof()` checks Gentzen **LK** derivations `Γ ⊢ Δ`, including the **second-order** rules (`∀²`/`∃²` with comprehension and predicate eigenvariables) that reach the `second_order=True` fragment; `render_sequent_proof()` prints the derivation tree
56
+ - **Fitch proof search** — `find_fitch_proof()` / `fitch_prove()` / `is_valid_fitch()` automatically *find* a Fitch natural-deduction proof (iterative-deepening backtracking, classical FOL); the result is re-validated by `check_proof`, so it is sound by construction
57
+ - **Analytic tableaux** — `is_valid_tableau()` / `prove_tableau()` / `tableau_closed()` and `tableau_model()` (an open branch is a countermodel); a fourth proof method, decidable propositionally
58
+ - **Finite model finder** — `find_model()` / `find_countermodel()` / `is_satisfiable_finite()` / `is_valid_finite()` search finite `Structure`s up to size N (Mace4-style: a valid entailment has no countermodel, an invalid one usually a small one)
59
+ - **Truth tables** — `truth_table()` (and `is_tautology()` / `is_contradiction()` / `is_satisfiable_tt()`) over classical, Kleene **K3**, and Priest **LP** values, renderable to Markdown
60
+ - **Intuitionistic logic** — `int_valid()` / `int_countermodel()` decide intuitionistic propositional validity by Kripke-model search; the **LJ sequent calculus** checker `check_lj_proof()` / `verify_lj_proof()` verifies intuitionistic derivations; LEM, DNE, and Peirce fail with explicit countermodels (`IntKripkeModel.forces()`)
61
+ - **Verbalization** — `to_english()` paraphrases a formula as an English sentence
53
62
  - **Canonical form & exact match** — `canonicalize()` normalises bound-variable renaming, commutativity/associativity, operand duplication, and double negation; `exact_match()` gives a fair NL→FOL comparison stricter than logical equivalence but more forgiving than raw equality
54
63
  - **Formula validation** — `validate()` / `is_wellformed()` / `validate_text()` report free variables, inconsistent predicate/function arity, leftover lambda nodes, and parseability of raw model output
55
64
  - **Modal, temporal, epistemic & deontic logic** — `MSFLParser(modal=True)` parses `□`/`◇` (alethic), `K_a`/`B_a` (epistemic/doxastic), `Ⓖ`/`Ⓕ`/`Ⓝ`/`Ⓤ` (temporal), `Ⓞ`/`Ⓟ` (obligation/permission); Kripke-model semantics via `satisfies_modal()`; `standard_translation()` to classical FOL so Z3/resolution can decide modal validity
@@ -233,6 +242,33 @@ parse_latex(ast.to_latex(), many_sorted=True) == ast # True
233
242
 
234
243
  `parse_latex` takes the same mode flags as `MSFLParser` (`many_sorted`, `fuzzy`, `modal`, `second_order`); the LaTeX must translate to syntax valid for that mode. Hand-written `c_`-constants need an escaped underscore (`c\_zero` or `c_{zero}`), since a bare `_` is LaTeX subscript.
235
244
 
245
+ ### Reading TPTP, Prover9, and SMT-LIB
246
+
247
+ The exporters `to_tptp()` / `to_prover9()` / `to_z3()` have inverses, so formulas written for the standard automated-reasoning tools can be read back into the AST:
248
+
249
+ ```python
250
+ from unicode_fol_kit import parse_tptp, parse_tptp_formula, parse_prover9, from_z3, parse_smtlib
251
+
252
+ # TPTP: a single formula, or a whole problem file (fof/cnf statements)
253
+ parse_tptp_formula("![X]: (man(X) => mortal(X))") # AST for: forall x (Man(x) -> Mortal(x))
254
+ problem = parse_tptp("""
255
+ fof(ax, axiom, ![X]: (man(X) => mortal(X))).
256
+ fof(hyp, hypothesis, man(socrates)).
257
+ fof(g, conjecture, mortal(socrates)).
258
+ """)
259
+ roles = [f.role for f in problem] # ['axiom', 'hypothesis', 'conjecture']
260
+
261
+ # Prover9 / LADR
262
+ parse_prover9("(all X (man(X) -> mortal(X)))") # AST for: forall x (man(x) -> mortal(x))
263
+
264
+ # Z3: a z3 expression (from_z3), or SMT-LIB2 text (via Z3's own parser)
265
+ parse_smtlib("(declare-fun x () Int) (assert (< x (+ x 1)))") # [x < x + 1]
266
+ ```
267
+
268
+ - **TPTP** — `parse_tptp_formula(s)` reads one bare FOF/CNF formula; `parse_tptp(text)` reads a whole problem into a list of `TptpFormula(name, role, formula)` records; `load_tptp(path)` reads a `.p`/`.tptp` file. `%` and `/* */` comments are ignored. Round-trips `to_tptp()`. TPTP lowercases predicates, so a predicate is capitalised on import (`loves` → `Loves`) to match the toolkit's uppercase-predicate convention; `$true`/`$false` import as opaque atoms; the typed `tff`/`thf` dialects and `include` directives are out of scope.
269
+ - **Prover9** — `parse_prover9(s)` reads a Prover9/LADR formula (a trailing `.` is accepted). It follows `set(prolog_style_variables)` (uppercase/underscore-initial names are variables), matching `to_prover9()`'s output, and is case-preserving. `to_prover9()` desugars exclusive-or, so an `Xor` round-trips to `(a | b) & -(a & b)`.
270
+ - **Z3** — `from_z3(expr)` turns a `z3.ExprRef` back into the AST; `parse_smtlib(text)` / `load_smtlib(path)` parse SMT-LIB2 (via Z3's own parser) and convert every assertion. The conversion is **meaning-preserving, not structure-preserving** — Z3 maps variables/constants/numbers onto one uninterpreted sort, so a *free* variable comes back as a `Constant` (only bound variables survive as `Variable`); `A == B` reads as `Iff` on Booleans and `=` on individuals.
271
+
236
272
  ### Traversal and inspection
237
273
 
238
274
  Every node exposes a small traversal API:
@@ -520,6 +556,162 @@ is_valid_resolution(parser.parse("∃x ∀y L(x, y) → ∀y ∃x L(x, y)")) #
520
556
  - **Equality is uninterpreted.** `=` is treated as an ordinary predicate (no built-in reflexivity/congruence). Entailments that rely on the theory of equality (e.g. `a = b, P(a) ⊨ P(b)`) need those axioms supplied as explicit premises, or use the Z3 backend instead.
521
557
  - `to_clauses(formula)` exposes the clausal form, and `refute(clauses)` runs the saturation directly.
522
558
 
559
+ ### Natural deduction (Fitch proofs)
560
+
561
+ The provers above decide *whether* an entailment holds. The package can also **check a Fitch-style natural-deduction proof** — a derivation with nested subproofs (hypothetical reasoning), per-line justifications, and discharge rules. `check_proof` is *sound*: it returns `True` only when every line genuinely follows by the cited rule and the proof's premises really do entail its conclusion. `verify_proof` additionally returns the certified sequent and the first failing line with a reason.
562
+
563
+ ```python
564
+ from unicode_fol_kit import (
565
+ MSFLParser, Proof, Subproof, premise, assume, line,
566
+ check_proof, verify_proof, render_fitch,
567
+ )
568
+
569
+ parse = MSFLParser().parse
570
+
571
+ # Hypothetical syllogism: P→Q, Q→R ⊢ P→R
572
+ proof = Proof(
573
+ premises=[premise(1, parse("P → Q")), premise(2, parse("Q → R"))],
574
+ steps=[
575
+ Subproof(
576
+ assumption=assume(3, parse("P")),
577
+ body=[line(4, parse("Q"), "→E", 1, 3),
578
+ line(5, parse("R"), "→E", 2, 4)],
579
+ ),
580
+ line(6, parse("P → R"), "→I", (3, 5)),
581
+ ],
582
+ )
583
+
584
+ check_proof(proof) # True
585
+ print(render_fitch(proof))
586
+ ```
587
+
588
+ `render_fitch` lays the proof out in classic Fitch notation — a line-number gutter, one vertical scope bar per open subproof, a rule under each assumption, and a justification column:
589
+
590
+ ```text
591
+ 1 │ P → Q Premise
592
+ 2 │ Q → R Premise
593
+ ├──────
594
+ 3 │ │ P Assume
595
+ │ ├──────
596
+ 4 │ │ Q →E 1, 3
597
+ 5 │ │ R →E 2, 4
598
+ 6 │ P → R →I 3–5
599
+ ```
600
+
601
+ The classical rule set covers the connectives (`∧I`/`∧E`, `∨I`/`∨E`, `→I`/`→E`, `↔I`/`↔E`, `¬I`, `⊥I`/`⊥E`, `¬E` double-negation, `RAA`, `Reit`), the first-order quantifiers (`∀I`/`∀E`, `∃I`/`∃E`, with the eigenvariable side-conditions enforced via the kit's capture-avoiding substitution), and equality (`=I`/`=E`, certified against Z3 since `=` is otherwise uninterpreted). A subproof is cited by its line span, e.g. `(3, 5)`; the instantiation/witness term of `∀E`/`∃I` is passed as `extra=[term]`. `⊥` is the reserved logical constant `FALSUM`.
602
+
603
+ **Non-classical logics.** Pass `logic=` to check a proof under a different consequence relation. For the three-valued **K3**/**LP** logics each step is certified against the many-valued decision procedure, so the paraconsistency facts come out correctly — in **LP** modus ponens, the disjunctive syllogism, and explosion are *not* valid, and the checker rejects a proof that uses them:
604
+
605
+ ```python
606
+ # In LP (paraconsistent) modus ponens is NOT valid — the checker rejects it:
607
+ mp = Proof(premises=[premise(1, parse("P")), premise(2, parse("P → Q"))],
608
+ steps=[line(3, parse("Q"), "→E", 2, 1)], logic="LP")
609
+ check_proof(mp) # False
610
+
611
+ # The very same proof is fine classically:
612
+ check_proof(Proof(premises=mp.premises, steps=mp.steps, logic="fol")) # True
613
+ ```
614
+
615
+ For the **modal family** (`logic="K"`/`"T"`/`"S4"`/`"S5"`) each step is certified by the standard translation to FOL plus the frame axioms, decided by Z3. Knowledge (`Knows`) is factive, but belief (`Believes`) and obligation (`Obligatory`) are not:
616
+
617
+ ```python
618
+ from unicode_fol_kit import Atom, Knows, Believes
619
+
620
+ p = Atom("P", [])
621
+
622
+ # Knowledge is factive (S5): K_a P ⊢ P
623
+ knows = Proof(premises=[premise(1, Knows("a", p))],
624
+ steps=[line(2, p, "T", 1)], logic="S5")
625
+ check_proof(knows) # True
626
+
627
+ # Belief is NOT factive: B_a P ⊬ P → rejected
628
+ believes = Proof(premises=[premise(1, Believes("a", p))],
629
+ steps=[line(2, p, "T", 1)], logic="S5")
630
+ check_proof(believes) # False
631
+ ```
632
+
633
+ - **Sound by construction.** Citation accessibility is enforced (a line may not reach into a closed sibling subproof), discharge rules are checked against the proof's *open assumptions* (not merely the cited lines), and every classical rule application is cross-checked in the test-suite against Z3 and the resolution prover — randomised proofs are audited line-by-line so an accepted step is never one the oracle reports invalid.
634
+ - **Scope.** Classical FOL/MSFOL is checked by the syntactic rule table; K3/LP and the modal family are checked over their propositional fragment. Temporal logic (`Ⓖ`/`Ⓕ`/`Ⓝ`/`Ⓤ`), quantified modal logic, second-order quantification, and the substructural Łukasiewicz connectives are out of scope (no sound finitary Fitch calculus / first-order rendering) and are rejected with a clear message.
635
+ - **LaTeX.** `render_latex_fitch(proof)` (also `proof.to_latex_fitch()`) emits the derivation as a self-contained LaTeX `array`; `proof.to_dict()` / `Proof.from_dict(...)` round-trips a whole proof to JSON.
636
+
637
+ ### Finding Fitch proofs (backtracking search)
638
+
639
+ The checker above *verifies* a proof; `find_fitch_proof` *finds* one. It is a goal-directed, iterative-deepening backtracking searcher over the classical propositional and first-order rules — introduction rules, ∨/∃ elimination by case split, backward chaining, and reductio (RAA), which makes it complete for the propositional fragment.
640
+
641
+ ```python
642
+ from unicode_fol_kit import find_fitch_proof, fitch_prove, is_valid_fitch
643
+ from unicode_fol_kit.fol.nodes import Atom, Or, Not, Implies
644
+
645
+ P, Q = Atom("P", ()), Atom("Q", ())
646
+
647
+ fitch_prove([], Or(P, Not(P))) # True — a proof of P ∨ ¬P was found
648
+ is_valid_fitch(Implies(Implies(Implies(P, Q), P), P)) # True — Peirce's law
649
+
650
+ proof = find_fitch_proof([P, Implies(P, Q)], Q) # returns a Proof (or None)
651
+ print(proof.to_fitch())
652
+ ```
653
+
654
+ ```text
655
+ 1 │ P Premise
656
+ 2 │ P → Q Premise
657
+ ├──────
658
+ 3 │ Q →E 2, 1
659
+ ```
660
+
661
+ - **Sound by construction.** Whatever the search assembles is re-validated by `check_proof` before it is returned, so a bug in the search can only make it *fail to find* a proof — never return an invalid one. Like the resolution prover it is sound and, under its depth bound, *incomplete*: `find_fitch_proof` returning `None` means "no proof found within `max_depth`", never "not a theorem".
662
+ - `fitch_prove(premises, conclusion)` returns a bool; `is_valid_fitch(formula)` proves from no premises; `find_fitch_proof` returns the actual `Proof` (render it, serialise it, or re-check it). Classical FOL only (the non-classical checkers above are verification-only).
663
+
664
+ ### Sequent calculus (Gentzen LK, incl. second-order)
665
+
666
+ A second proof system: a **two-sided Gentzen sequent calculus**. A sequent `Γ ⊢ Δ` (multisets of formulas, read as `⋀Γ → ⋁Δ`) is derived by a tree of inference rules, and `check_sequent_proof` verifies the tree. This is the classical system **LK** with the first-order quantifier rules *and* the **second-order** rules (`∀²`/`∃²` over predicate variables), so it reaches the second-order fragment (the `MSFLParser(second_order=True)` syntax) that natural deduction / resolution / Z3 cannot.
667
+
668
+ ```python
669
+ from unicode_fol_kit import (
670
+ MSFLParser, sequent, derive, axiom, check_sequent_proof, render_sequent_proof,
671
+ )
672
+ from unicode_fol_kit.fol.nodes import Atom, Implies, Quantifier, Variable, Constant
673
+
674
+ x, c = Variable("x"), Constant("c")
675
+ def Px(t): return Atom("P", [t])
676
+
677
+ # ∀x P(x) ⊢ P(c) via the ∀L rule (instantiating the bound x with the term c)
678
+ d = derive(sequent([Quantifier("∀", x, Px(x))], [Px(c)]), "∀L",
679
+ axiom(sequent([Px(c)], [Px(c)])),
680
+ extra=[c])
681
+
682
+ check_sequent_proof(d) # True
683
+ print(render_sequent_proof(d))
684
+ ```
685
+
686
+ `render_sequent_proof` prints the derivation as an indented tree (conclusion first, premises below, each annotated with its rule):
687
+
688
+ ```text
689
+ ∀x P(x) ⊢ P(c) [∀L c]
690
+ P(c) ⊢ P(c) [Ax]
691
+ ```
692
+
693
+ The second-order rules `∀²L` / `∃²R` instantiate a bound predicate variable `X` with a **comprehension term** `λx̄.ψ` (a `Comprehension`), replacing each `X(t̄)` by `ψ[x̄ := t̄]`; `∀²R` / `∃²L` use a fresh **predicate eigenvariable**:
694
+
695
+ ```python
696
+ from unicode_fol_kit import Comprehension
697
+ from unicode_fol_kit.fol.nodes import SecondOrderQuantifier
698
+
699
+ z = Variable("z")
700
+ Xc = Atom("X", [c]) # the bound predicate variable X applied to c
701
+ Pc = Px(c)
702
+
703
+ # ∀X X(c) ⊢ P(c) via ∀²L, instantiating X with the comprehension λz. P(z)
704
+ so = derive(sequent([SecondOrderQuantifier("∀", "X", 1, Xc)], [Pc]), "∀²L",
705
+ axiom(sequent([Pc], [Pc])),
706
+ extra=[Comprehension((z,), Px(z))])
707
+
708
+ check_sequent_proof(so) # True
709
+ ```
710
+
711
+ - **Rule set.** `Ax`, the structural rules `WL`/`WR` (weakening), `CL`/`CR` (contraction), `Cut`; the connective rules `¬L`/`¬R`, `∧L`/`∧R`, `∨L`/`∨R`, `→L`/`→R`, `↔L`/`↔R`; the quantifier rules `∀L`/`∀R`, `∃L`/`∃R` (with the eigenvariable condition on `∀R`/`∃L`); and the second-order rules `∀²L`/`∀²R`, `∃²L`/`∃²R`. Contexts are shared additively and compared as multisets; the instantiation term / eigenvariable / comprehension goes in `extra=[…]`.
712
+ - **Checker, not prover.** Full second-order validity is not recursively enumerable (Gödel), so no calculus can be a *complete* prover — `check_sequent_proof` verifies a *given* derivation. It is sound by the LK metatheory, cross-checked in the test-suite against Z3 (for the first-order-expressible sequents) and `satisfies_so` over small finite models (for the genuinely second-order ones, where a randomised audit confirms every accepted derivation is valid node-by-node).
713
+ - `derivation.to_dict()` / `Derivation.from_dict(...)` round-trips a whole derivation (including comprehension terms) to JSON.
714
+
523
715
  ### Tarskian semantics (FOL / MSFOL)
524
716
 
525
717
  Instead of calling an external solver, you can build a concrete **world** (a first-order `Structure`) and compute the truth value of a formula directly, following Tarski's recursive definition of satisfaction. A structure is a non-empty domain of individuals together with interpretations of the constants, functions, and predicates (and, for MSFOL, the named sorts).
@@ -856,6 +1048,44 @@ holds(parser.parse("∀x ∀y (∀P (P(x) ↔ P(y)) → x = y)"), universe) #
856
1048
 
857
1049
  This is second-order **predicate** quantification with full (standard) semantics over finite models. Quantification over functions, third-order and up, and a complete higher-order type system are out of scope; the lambda layer already provides higher-order *terms* (`λP. P(x)`), which you beta-reduce/eliminate before evaluation. Second-order formulas reject `to_z3`/`to_prover9`/`to_tptp` (second-order validity is not first-order / not SMT-expressible) — use `satisfies_so` on finite models instead.
858
1050
 
1051
+ ## Truth tables, model finding, tableaux, and intuitionistic logic
1052
+
1053
+ Five further analysis tools round out the kit. They all reuse the existing AST and evaluators.
1054
+
1055
+ ```python
1056
+ from unicode_fol_kit import (
1057
+ MSFLParser, truth_table, is_tautology, find_countermodel, is_valid_finite,
1058
+ is_valid_tableau, tableau_model, int_valid, int_countermodel, to_english,
1059
+ )
1060
+ p = MSFLParser().parse
1061
+
1062
+ # Truth tables (classical / K3 / LP)
1063
+ print(truth_table(p("P → Q")).render())
1064
+ is_tautology(p("P ∨ ¬P")) # True (classical) …
1065
+ is_tautology(p("P ∨ ¬P"), "K3") # … False in K3 (no three-valued tautologies)
1066
+
1067
+ # Finite model finder — a countermodel witnesses a non-entailment
1068
+ find_countermodel([p("P(tom)")], p("∀x P(x)"), max_size=3) # a Structure (or None)
1069
+ is_valid_finite(p("∀x P(x) → P(tom)")) # True (no finite countermodel)
1070
+
1071
+ # Analytic tableaux — and an open branch is a model
1072
+ is_valid_tableau(p("((P → Q) → P) → P")) # True (Peirce, classically)
1073
+ tableau_model([p("P → Q"), p("P")]) # {'P': True, 'Q': True}
1074
+
1075
+ # Intuitionistic logic — LEM and Peirce fail, with a Kripke countermodel
1076
+ int_valid(p("P ∨ ¬P")) # False (int_countermodel returns the model)
1077
+ int_valid(p("¬¬(P ∨ ¬P)")) # True
1078
+
1079
+ # Verbalize a formula in English
1080
+ to_english(p("∀x (Human(x) → Mortal(x))")) # 'for every x, if x is human, then x is mortal'
1081
+ ```
1082
+
1083
+ - **Truth tables** — `truth_table(formula, logic)` (`"classical"` / `"K3"` / `"LP"`) returns a `TruthTable` (`.render()` to Markdown, `.is_tautology` / `.is_contradiction` / `.is_satisfiable`); the convenience predicates `is_tautology` / `is_contradiction` / `is_satisfiable_tt` wrap it. Each distinct atom is a propositional variable; quantified formulas are rejected.
1084
+ - **Finite model finder** — `find_model(theory, max_size)` brute-force searches finite `Structure`s (domain `1..N`) satisfying a theory; `find_countermodel(premises, conclusion)` finds one refuting an entailment; `is_satisfiable_finite` / `is_valid_finite` are the booleans. The Mace4-style partner of the provers: a valid entailment has no countermodel, an invalid one usually has a small one. Bounded (a domain whose interpretation space is too large is skipped).
1085
+ - **Analytic tableaux** — `is_valid_tableau`, `prove_tableau`, `tableau_closed`, and `tableau_model` (an open branch is a satisfying assignment / countermodel). A fourth proof method beside resolution, Fitch, and the sequent calculus; decidable and complete propositionally, bounded first-order.
1086
+ - **Intuitionistic logic** — `int_valid` and `int_countermodel` decide intuitionistic *propositional* validity by Kripke-model search (the logic has the finite-model property); `IntKripkeModel.forces(world, φ)` evaluates a model directly. The **LJ sequent calculus** checker `check_lj_proof` / `verify_lj_proof` verifies intuitionistic derivations — Gentzen **LJ** is the LK calculus restricted to *at most one succedent formula*, which is exactly what blocks the classical theorems. Excluded middle, double-negation elimination, and Peirce's law fail, each with an explicit Kripke countermodel; every intuitionistic validity is also classically valid.
1087
+ - **Verbalization** — `to_english(formula)` renders a formula as an English sentence (a readability aid, not a parse inverse).
1088
+
859
1089
  ## Syntax reference
860
1090
 
861
1091
  This section describes the full surface syntax accepted by the parser. Because the four modes share the same term and atom layer, most of the syntax is identical across modes; differences are called out explicitly.