unicode-logic-kit 0.31.0__py3-none-any.whl

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 (237) hide show
  1. unicode_logic_kit/__init__.py +385 -0
  2. unicode_logic_kit/__main__.py +520 -0
  3. unicode_logic_kit/_deadline.py +219 -0
  4. unicode_logic_kit/ace/__init__.py +126 -0
  5. unicode_logic_kit/ace/_align.py +135 -0
  6. unicode_logic_kit/ace/chem_lexicon.py +128 -0
  7. unicode_logic_kit/ace/drs_reader.py +570 -0
  8. unicode_logic_kit/ace/mapping.py +666 -0
  9. unicode_logic_kit/ace/reverse_modal.py +138 -0
  10. unicode_logic_kit/ace/runner.py +551 -0
  11. unicode_logic_kit/ace/translate.py +452 -0
  12. unicode_logic_kit/ace/verbalize.py +1070 -0
  13. unicode_logic_kit/api.py +1284 -0
  14. unicode_logic_kit/atp/__init__.py +177 -0
  15. unicode_logic_kit/atp/_ascii_names.py +113 -0
  16. unicode_logic_kit/atp/_html.py +72 -0
  17. unicode_logic_kit/atp/_substructural_input.py +228 -0
  18. unicode_logic_kit/atp/_tff_problem.py +715 -0
  19. unicode_logic_kit/atp/_tptp_problem.py +1111 -0
  20. unicode_logic_kit/atp/_writer_support.py +289 -0
  21. unicode_logic_kit/atp/clingo_backend.py +1180 -0
  22. unicode_logic_kit/atp/cvc5_backend.py +1385 -0
  23. unicode_logic_kit/atp/eprover_backend.py +732 -0
  24. unicode_logic_kit/atp/finite_domain.py +1055 -0
  25. unicode_logic_kit/atp/fitch.py +1547 -0
  26. unicode_logic_kit/atp/fitch_search.py +551 -0
  27. unicode_logic_kit/atp/hets_backend.py +339 -0
  28. unicode_logic_kit/atp/hybrid_down.py +120 -0
  29. unicode_logic_kit/atp/incremental.py +250 -0
  30. unicode_logic_kit/atp/kripke_enum.py +741 -0
  31. unicode_logic_kit/atp/lambek.py +436 -0
  32. unicode_logic_kit/atp/leo3_backend.py +332 -0
  33. unicode_logic_kit/atp/linear.py +738 -0
  34. unicode_logic_kit/atp/lj.py +705 -0
  35. unicode_logic_kit/atp/logic_backends.py +566 -0
  36. unicode_logic_kit/atp/ltl_tableau.py +1084 -0
  37. unicode_logic_kit/atp/minizinc_backend.py +1402 -0
  38. unicode_logic_kit/atp/modal_tableau.py +1382 -0
  39. unicode_logic_kit/atp/nanocop_backend.py +410 -0
  40. unicode_logic_kit/atp/portfolio.py +489 -0
  41. unicode_logic_kit/atp/protocol.py +1803 -0
  42. unicode_logic_kit/atp/prover9_entailment.py +1153 -0
  43. unicode_logic_kit/atp/resolution.py +1376 -0
  44. unicode_logic_kit/atp/resolution_check.py +1114 -0
  45. unicode_logic_kit/atp/sequent.py +1050 -0
  46. unicode_logic_kit/atp/tableau.py +921 -0
  47. unicode_logic_kit/atp/tableau_check.py +543 -0
  48. unicode_logic_kit/atp/tptp_ncl.py +811 -0
  49. unicode_logic_kit/atp/tptp_tff.py +1546 -0
  50. unicode_logic_kit/atp/tstp.py +1333 -0
  51. unicode_logic_kit/atp/tstp_check.py +1096 -0
  52. unicode_logic_kit/atp/twee_backend.py +236 -0
  53. unicode_logic_kit/atp/twee_check.py +711 -0
  54. unicode_logic_kit/atp/twee_entailment.py +953 -0
  55. unicode_logic_kit/atp/vampire_entailment.py +540 -0
  56. unicode_logic_kit/atp/z3_arith.py +470 -0
  57. unicode_logic_kit/atp/z3_equivalence.py +36 -0
  58. unicode_logic_kit/atp/z3_fuzzy.py +362 -0
  59. unicode_logic_kit/atp/z3_input.py +500 -0
  60. unicode_logic_kit/atp/z3_models.py +208 -0
  61. unicode_logic_kit/chem/__init__.py +88 -0
  62. unicode_logic_kit/chem/_naming.py +284 -0
  63. unicode_logic_kit/chem/cache.py +185 -0
  64. unicode_logic_kit/chem/interop.py +244 -0
  65. unicode_logic_kit/chem/mol.py +525 -0
  66. unicode_logic_kit/chem/signature.py +112 -0
  67. unicode_logic_kit/comorphism.py +497 -0
  68. unicode_logic_kit/dl/__init__.py +384 -0
  69. unicode_logic_kit/dl/classification.py +227 -0
  70. unicode_logic_kit/dl/concepts.py +632 -0
  71. unicode_logic_kit/dl/datatypes.py +818 -0
  72. unicode_logic_kit/dl/owl_functional.py +2433 -0
  73. unicode_logic_kit/dl/owl_manchester.py +1637 -0
  74. unicode_logic_kit/dl/owl_reasoner.py +790 -0
  75. unicode_logic_kit/dl/parser.py +391 -0
  76. unicode_logic_kit/dl/tableau.py +4048 -0
  77. unicode_logic_kit/dl/translate.py +2704 -0
  78. unicode_logic_kit/drt/__init__.py +94 -0
  79. unicode_logic_kit/drt/export.py +179 -0
  80. unicode_logic_kit/drt/nodes.py +506 -0
  81. unicode_logic_kit/drt/parser.py +965 -0
  82. unicode_logic_kit/drt/resolve.py +195 -0
  83. unicode_logic_kit/drt/reverse.py +175 -0
  84. unicode_logic_kit/eval/__init__.py +106 -0
  85. unicode_logic_kit/eval/batch.py +382 -0
  86. unicode_logic_kit/eval/canonical.py +663 -0
  87. unicode_logic_kit/eval/chem_batch.py +606 -0
  88. unicode_logic_kit/eval/converses.py +200 -0
  89. unicode_logic_kit/eval/datasets/__init__.py +136 -0
  90. unicode_logic_kit/eval/datasets/_base.py +263 -0
  91. unicode_logic_kit/eval/datasets/_proofwriter_proof.py +422 -0
  92. unicode_logic_kit/eval/datasets/c3po.py +678 -0
  93. unicode_logic_kit/eval/datasets/folio.py +158 -0
  94. unicode_logic_kit/eval/datasets/fracas.py +418 -0
  95. unicode_logic_kit/eval/datasets/groves.py +191 -0
  96. unicode_logic_kit/eval/datasets/logicbench.py +467 -0
  97. unicode_logic_kit/eval/datasets/logicnli.py +303 -0
  98. unicode_logic_kit/eval/datasets/malls.py +133 -0
  99. unicode_logic_kit/eval/datasets/pfolio.py +594 -0
  100. unicode_logic_kit/eval/datasets/pmb.py +242 -0
  101. unicode_logic_kit/eval/datasets/prontoqa.py +611 -0
  102. unicode_logic_kit/eval/datasets/proofwriter.py +1431 -0
  103. unicode_logic_kit/eval/datasets/proverqa.py +674 -0
  104. unicode_logic_kit/eval/datasets/willow.py +478 -0
  105. unicode_logic_kit/eval/equivalence.py +466 -0
  106. unicode_logic_kit/eval/exercise_gen.py +533 -0
  107. unicode_logic_kit/eval/explain.py +791 -0
  108. unicode_logic_kit/eval/generality.py +750 -0
  109. unicode_logic_kit/eval/metric_hf.py +458 -0
  110. unicode_logic_kit/eval/predicate_match.py +343 -0
  111. unicode_logic_kit/eval/theory_check.py +1170 -0
  112. unicode_logic_kit/eval/validate.py +306 -0
  113. unicode_logic_kit/fol/__init__.py +177 -0
  114. unicode_logic_kit/fol/_atom_keys.py +510 -0
  115. unicode_logic_kit/fol/_fol_nodes.py +3586 -0
  116. unicode_logic_kit/fol/_free_parameters.py +105 -0
  117. unicode_logic_kit/fol/_ho_nodes.py +448 -0
  118. unicode_logic_kit/fol/_hybrid_nodes.py +308 -0
  119. unicode_logic_kit/fol/_identifiers.py +1091 -0
  120. unicode_logic_kit/fol/_lambek_nodes.py +112 -0
  121. unicode_logic_kit/fol/_linear_nodes.py +352 -0
  122. unicode_logic_kit/fol/_modal_nodes.py +1467 -0
  123. unicode_logic_kit/fol/_msfl_nodes.py +2196 -0
  124. unicode_logic_kit/fol/_numeral_symbols.py +231 -0
  125. unicode_logic_kit/fol/_so_nodes.py +200 -0
  126. unicode_logic_kit/fol/_symbol_names.py +81 -0
  127. unicode_logic_kit/fol/_team_nodes.py +181 -0
  128. unicode_logic_kit/fol/_tptp_symbols.py +551 -0
  129. unicode_logic_kit/fol/_truth_constants.py +117 -0
  130. unicode_logic_kit/fol/casl_export.py +1135 -0
  131. unicode_logic_kit/fol/casl_import.py +929 -0
  132. unicode_logic_kit/fol/derivation.py +367 -0
  133. unicode_logic_kit/fol/dialect_detect.py +70 -0
  134. unicode_logic_kit/fol/dialect_repair.py +537 -0
  135. unicode_logic_kit/fol/frames.py +637 -0
  136. unicode_logic_kit/fol/grammars/terminals.lark +31 -0
  137. unicode_logic_kit/fol/lambda_tools.py +297 -0
  138. unicode_logic_kit/fol/latex_input.py +429 -0
  139. unicode_logic_kit/fol/modal_translation.py +944 -0
  140. unicode_logic_kit/fol/msflparser.py +1033 -0
  141. unicode_logic_kit/fol/naming.py +422 -0
  142. unicode_logic_kit/fol/nodes.py +241 -0
  143. unicode_logic_kit/fol/normalforms.py +492 -0
  144. unicode_logic_kit/fol/pal.py +287 -0
  145. unicode_logic_kit/fol/prolog_export.py +566 -0
  146. unicode_logic_kit/fol/prolog_input.py +505 -0
  147. unicode_logic_kit/fol/prover9_input.py +1325 -0
  148. unicode_logic_kit/fol/qml.py +1760 -0
  149. unicode_logic_kit/fol/qmltp_input.py +525 -0
  150. unicode_logic_kit/fol/sanitize.py +221 -0
  151. unicode_logic_kit/fol/serialize.py +79 -0
  152. unicode_logic_kit/fol/signature.py +1290 -0
  153. unicode_logic_kit/fol/simplify_check.py +544 -0
  154. unicode_logic_kit/fol/spans.py +594 -0
  155. unicode_logic_kit/fol/tptp_input.py +1503 -0
  156. unicode_logic_kit/fol/tptp_repair.py +941 -0
  157. unicode_logic_kit/fol/unification.py +157 -0
  158. unicode_logic_kit/fol/verbalize.py +263 -0
  159. unicode_logic_kit/hets/__init__.py +163 -0
  160. unicode_logic_kit/hets/bridge.py +142 -0
  161. unicode_logic_kit/hets/client.py +748 -0
  162. unicode_logic_kit/hets/docker.py +420 -0
  163. unicode_logic_kit/hets/dol.py +712 -0
  164. unicode_logic_kit/hets/haskell_json.py +355 -0
  165. unicode_logic_kit/hets/owl_backend.py +794 -0
  166. unicode_logic_kit/hets/owl_cli.py +598 -0
  167. unicode_logic_kit/hets/symbols.py +512 -0
  168. unicode_logic_kit/hol/__init__.py +140 -0
  169. unicode_logic_kit/hol/_ho_common.py +323 -0
  170. unicode_logic_kit/hol/_isabelle_binders.py +125 -0
  171. unicode_logic_kit/hol/classical.py +812 -0
  172. unicode_logic_kit/hol/deepshallow/__init__.py +45 -0
  173. unicode_logic_kit/hol/deepshallow/_common.py +177 -0
  174. unicode_logic_kit/hol/deepshallow/conditional.py +225 -0
  175. unicode_logic_kit/hol/deepshallow/intuitionistic.py +181 -0
  176. unicode_logic_kit/hol/deepshallow/modal.py +217 -0
  177. unicode_logic_kit/hol/deepshallow/qml.py +406 -0
  178. unicode_logic_kit/hol/deepshallow/relevant.py +206 -0
  179. unicode_logic_kit/hol/free.py +753 -0
  180. unicode_logic_kit/hol/goedel.py +336 -0
  181. unicode_logic_kit/hol/ho_modal.py +1743 -0
  182. unicode_logic_kit/hol/intuitionistic.py +403 -0
  183. unicode_logic_kit/hol/isabelle_conditional.py +593 -0
  184. unicode_logic_kit/hol/isabelle_modal.py +1908 -0
  185. unicode_logic_kit/hol/isabelle_relevant.py +412 -0
  186. unicode_logic_kit/hol/isabelle_runner.py +1147 -0
  187. unicode_logic_kit/hol/isabelle_substructural.py +884 -0
  188. unicode_logic_kit/hol/lean.py +1018 -0
  189. unicode_logic_kit/hol/manyvalued.py +921 -0
  190. unicode_logic_kit/hol/secondorder.py +687 -0
  191. unicode_logic_kit/hol/thf_modal.py +941 -0
  192. unicode_logic_kit/hol/thirdorder.py +397 -0
  193. unicode_logic_kit/ilp/__init__.py +89 -0
  194. unicode_logic_kit/ilp/readback.py +389 -0
  195. unicode_logic_kit/ilp/separation.py +153 -0
  196. unicode_logic_kit/ilp/task.py +730 -0
  197. unicode_logic_kit/logic.py +163 -0
  198. unicode_logic_kit/mcp/__init__.py +28 -0
  199. unicode_logic_kit/mcp/__main__.py +5 -0
  200. unicode_logic_kit/mcp/chem_tools.py +1031 -0
  201. unicode_logic_kit/mcp/server.py +2453 -0
  202. unicode_logic_kit/mcp/syntax_spec.py +681 -0
  203. unicode_logic_kit/prob/__init__.py +53 -0
  204. unicode_logic_kit/prob/_bdd.py +225 -0
  205. unicode_logic_kit/prob/_column_gen.py +668 -0
  206. unicode_logic_kit/prob/distribution.py +686 -0
  207. unicode_logic_kit/prob/nilsson.py +470 -0
  208. unicode_logic_kit/py.typed +0 -0
  209. unicode_logic_kit/semantics/__init__.py +137 -0
  210. unicode_logic_kit/semantics/_modal_reject.py +156 -0
  211. unicode_logic_kit/semantics/action_models.py +466 -0
  212. unicode_logic_kit/semantics/asp_models.py +1200 -0
  213. unicode_logic_kit/semantics/conditional.py +580 -0
  214. unicode_logic_kit/semantics/dynamic_epistemic.py +95 -0
  215. unicode_logic_kit/semantics/free_logic.py +913 -0
  216. unicode_logic_kit/semantics/fuzzy.py +384 -0
  217. unicode_logic_kit/semantics/fuzzy_kripke.py +442 -0
  218. unicode_logic_kit/semantics/intuitionistic.py +581 -0
  219. unicode_logic_kit/semantics/kripke.py +1139 -0
  220. unicode_logic_kit/semantics/manyvalued.py +580 -0
  221. unicode_logic_kit/semantics/matrix.py +342 -0
  222. unicode_logic_kit/semantics/model_eval.py +1135 -0
  223. unicode_logic_kit/semantics/modelfinder.py +1036 -0
  224. unicode_logic_kit/semantics/nonmonotonic.py +372 -0
  225. unicode_logic_kit/semantics/relevant.py +331 -0
  226. unicode_logic_kit/semantics/secondorder.py +657 -0
  227. unicode_logic_kit/semantics/structures.py +352 -0
  228. unicode_logic_kit/semantics/tarski.py +975 -0
  229. unicode_logic_kit/semantics/team.py +315 -0
  230. unicode_logic_kit/semantics/team_translation.py +416 -0
  231. unicode_logic_kit/semantics/thirdorder.py +358 -0
  232. unicode_logic_kit/semantics/tnorm.py +85 -0
  233. unicode_logic_kit/semantics/truthtable.py +201 -0
  234. unicode_logic_kit-0.31.0.dist-info/METADATA +333 -0
  235. unicode_logic_kit-0.31.0.dist-info/RECORD +237 -0
  236. unicode_logic_kit-0.31.0.dist-info/WHEEL +4 -0
  237. unicode_logic_kit-0.31.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,537 @@
1
+ """Repair layer for LLM-generated formulas in the kit's OWN surface syntax.
2
+
3
+ Sibling of :mod:`unicode_logic_kit.fol.tptp_repair`, which repairs in TPTP the
4
+ three shapes LLM-generated formulas fail on for pure SYNTAX — worth repairing
5
+ rather than retrying, since a syntax rejection costs a whole generation
6
+ attempt. Carrying those three over to the kit's unicode dialect gives three
7
+ DIFFERENT answers, and which is which is this module's entire content:
8
+
9
+ 1. **Biimplication structure error** — **does not exist here.** A strict
10
+ TPTP parser (gavel, for one) rejects ``p(X) <=> a(X) & b(X)`` for lacking
11
+ brackets around the right side. The kit's unicode grammar accepts it, and
12
+ ``Node.to_unicode_str`` renders it back in exactly that form, so there is
13
+ nothing to normalise in either direction. It is unambiguous because the
14
+ grammar's ladder puts ``↔``/``→`` below ``∧``/``∨`` — and because the one
15
+ shape that WOULD be ambiguous, mixing ``∧`` and ``∨`` at the same level,
16
+ is refused outright rather than silently resolved by precedence.
17
+
18
+ That refusal is why this module never inserts a bracket. ``A ∧ B ∨ C`` has
19
+ two readings, they are different formulas, and picking one would throw
20
+ away the exact property that makes this dialect worth generating in: the
21
+ parser says "cannot mix" *before* a wrong reading reaches a model checker.
22
+ Reported as a ``"mixed_connectives"`` issue with the grammar's own hint;
23
+ never repaired.
24
+
25
+ 2. **Invalid predicate name** — **repaired differently.** A chemical name
26
+ used as a predicate (``1,2-diacyl-sn-glycero-3-phospho‐
27
+ choline``, ``(2S)Flavan4One``) is not a legal token in any FOL surface
28
+ syntax. TPTP has an escape hatch, single quoting, which preserves the
29
+ name exactly; the unicode grammar quotes a CONSTANT (``'1,2-diacyl'`` as
30
+ a term) but has no quoted form for a PREDICATE or a FUNCTION name
31
+ (``'1,2-diacyl'(x)`` fails at the bracket after the closing quote), so the
32
+ fix here is a sanitising RENAME through
33
+ :class:`~unicode_logic_kit.fol.sanitize.NameMapping`. What stands between a
34
+ pair of single quotes is a constant's name, exactly as written, and is
35
+ never a candidate for this rename (see the limits below).
36
+
37
+ Renaming such a name to a plain camelCase identifier is the obvious
38
+ fallback, and it is a loss: the mapping back to the original chemical
39
+ class name is gone. Here the mapping IS the return value (``names``, and
40
+ the caller's own :class:`NameMapping`), so the original spelling is
41
+ recoverable and the same original renames identically in every formula of
42
+ a run when the caller passes one mapping through. Nothing is lost; it just
43
+ lives beside the formula instead of inside it.
44
+
45
+ 3. **Unbound variable error** — **repaired identically.** Free variables are
46
+ read off the AST, which no longer knows what dialect it was written in, so
47
+ this module calls tptp_repair's own two fixes
48
+ (:func:`~unicode_logic_kit.fol.tptp_repair._drop_argument_fix`,
49
+ :func:`~unicode_logic_kit.fol.tptp_repair._close_universally`) rather than
50
+ reimplementing them — the two paths cannot drift apart if there is only
51
+ one implementation.
52
+
53
+ 4. **A truth glyph beside another dialect's connectives** — **repaired,
54
+ and reported.** ``P -> ⊥`` is the Prover9 arrow with the unicode glyph for
55
+ falsity, and no grammar reads it: the glyph is not ASCII, and the
56
+ detection of the two ASCII-only dialects (Prover9, TPTP) keys on text that
57
+ is ASCII, while the unicode ladder does not read ``->``. ``P -> Q`` parses
58
+ (as Prover9) and ``P → ⊥`` parses (as the unicode ladder), so the mix is
59
+ repaired the way ``P -> Q`` is: the glyphs ``⊤`` / ``⊥`` are read as the
60
+ truth constants of the dialect the rest of the text is written in, first
61
+ as Prover9's ``$T`` / ``$F``, then as TPTP's ``$true`` / ``$false``, and
62
+ the first rewrite that parses wins. Either way the result holds the truth
63
+ constants ``$true`` / ``$false``, and an ``"truth_glyph"`` issue says what
64
+ was read as what. A rewrite that does not parse is discarded whole.
65
+
66
+ Public API: :func:`repair_formula`, returning a :class:`DialectRepairResult`
67
+ (a :class:`~unicode_logic_kit.fol.tptp_repair.RepairResult` plus the dialect
68
+ that parsed and the renamings applied), so a caller that already handles TPTP
69
+ repairs handles these with the same code.
70
+
71
+ Hard limits (refuse rather than silently reinterpret)
72
+ -------------------------------------------------------
73
+ * Mixed same-level connectives are reported, never bracketed (above).
74
+ * A rename is only ever attempted on text that FAILED to parse, and only for
75
+ a name-shaped span in functor position that is legal in no symbol class —
76
+ and it is kept only if the rewritten text then parses. A rewrite that does
77
+ not make the input parseable is discarded whole: ``ok=False``, the original
78
+ text returned untouched, and the parser's own farthest diagnosis reported.
79
+ * Every text between a pair of single quotes is left byte for byte as it was,
80
+ in every step of this module (the scan for names, the rename, the reading of
81
+ a truth glyph): a quoted constant such as ``'Foo-bar(x)'`` or ``'⊤'`` is a
82
+ name, not a functor-position span and not a glyph. A pair is what the
83
+ quoted-name terminal reads, and the escapes ``\\'`` and ``\\\\`` inside it
84
+ do not end it; a quote without a partner is no pair and is left to the
85
+ parser's message.
86
+ * The rename always picks the PREDICATE spelling (``[A-Z][a-zA-Z0-9]*``).
87
+ A functor-position name is a predicate or a function depending only on its
88
+ spelling, so this is a choice, not a reading — stated in the issue message
89
+ each time. A case-2 name of this shape is a class predicate in practice; a
90
+ name meant as a function symbol must be written legally by hand.
91
+ * A legalised name never collides with a symbol already in the text: the
92
+ mapping's reserved set is preseeded with every identifier the text
93
+ contains, so ``1,2-diacyl`` beside an existing ``P12diacyl`` yields
94
+ ``P12diacyl2`` instead of merging the two predicates.
95
+ """
96
+
97
+ import re
98
+ from dataclasses import dataclass
99
+ from functools import lru_cache
100
+ from typing import List, Optional, Tuple
101
+
102
+ from .nodes import Node, free_variables
103
+ # The three symbol-class patterns come from fol._identifiers — the SAME
104
+ # generated character classes the grammar itself lexes PREDICATE/NAME/
105
+ # VARIABLE with — rather than from sanitize.py. sanitize.py's patterns are a
106
+ # DIFFERENT, deliberately narrower thing: the strictly-ASCII subset safe to
107
+ # emit into TPTP/Prover9/SMT-LIB/Isabelle/CASL (see its module docstring), not
108
+ # "every name this dialect's parser accepts". Using sanitize's patterns here
109
+ # used to be harmless because they happened to coincide with the grammar's
110
+ # own (then-ASCII) terminals; once the grammar widened to Unicode letters,
111
+ # underscores, and digit-leading names, that coincidence broke — a legal name
112
+ # like `family_History` would fail sanitize's ASCII-only _NAME_RE and get
113
+ # needlessly (and destructively) renamed, even though it already parses fine.
114
+ # _is_legal_name has to ask what THIS dialect's grammar accepts, which is
115
+ # _identifiers, not what sanitize.py is willing to export.
116
+ from ._identifiers import (
117
+ _UNSPELLABLE_CLASS,
118
+ predicate_pattern as _predicate_pattern,
119
+ name_pattern as _name_pattern,
120
+ variable_pattern as _variable_pattern,
121
+ uppercase_class as _uppercase_class,
122
+ lowercase_class as _lowercase_class,
123
+ combining_class as _combining_class,
124
+ )
125
+ from .sanitize import NameMapping
126
+ from .tptp_repair import (
127
+ Issue, RepairResult, _close_universally, _drop_argument_fix, _squash,
128
+ )
129
+
130
+ __all__ = ["DialectRepairResult", "repair_formula"]
131
+
132
+
133
+ @dataclass(frozen=True)
134
+ class DialectRepairResult(RepairResult):
135
+ """A :class:`~unicode_logic_kit.fol.tptp_repair.RepairResult` for
136
+ :func:`repair_formula`.
137
+
138
+ ``repaired_text`` is the kit's unicode rendering of the repaired AST
139
+ (``Node.to_unicode_str``), not TPTP — it re-parses to ``formula``.
140
+
141
+ ``dialect`` is what actually parsed (``parse_any``'s report), ``None``
142
+ when nothing did. ``names`` lists the ``(original, legal)`` renamings
143
+ case 2 applied, in application order — empty when nothing was renamed.
144
+ The originals are recoverable from this alone; pass a shared
145
+ :class:`~unicode_logic_kit.fol.sanitize.NameMapping` to
146
+ :func:`repair_formula` to keep them consistent across a whole run.
147
+ """
148
+
149
+ dialect: Optional[str] = None
150
+ names: Tuple[Tuple[str, str], ...] = ()
151
+
152
+ def to_dict(self) -> dict:
153
+ out = super().to_dict()
154
+ out["dialect"] = self.dialect
155
+ out["names"] = [{"original": o, "legal": l} for o, l in self.names]
156
+ return out
157
+
158
+
159
+ # ---------------------------------------------------------------------------
160
+ # Quoted constants: what the scans below must not look into
161
+ # ---------------------------------------------------------------------------
162
+ #
163
+ # A constant may be written in single quotes, and the text between the quotes
164
+ # is its name exactly (``'1,2-diacyl'``, ``'Foo_bar(x)'``, ``'⊤'``). The scans
165
+ # of this module are regular expressions over raw text and know nothing of
166
+ # that, so each of them runs only on the stretches OUTSIDE the quoted spans.
167
+ #
168
+ # A quoted span is read the way the QUOTED_NAME terminal reads one — a quote,
169
+ # characters that are no quote, backslash or unspellable character, the escapes
170
+ # of a quote and of a backslash, a quote — with one deliberate widening: a
171
+ # backslash in front of ANY spellable character is taken as an escape. The
172
+ # grammar refuses ``'a\b'``, but a text that is being repaired may hold one, and
173
+ # its span still has to end at the quote the writer meant, not at the quote
174
+ # before it. The empty ``''`` is a span too (it has nothing to protect, and
175
+ # counting it keeps the quotes after it paired as the writer paired them).
176
+ # A quote with no partner is no span.
177
+ _QUOTED_SPAN = re.compile(
178
+ rf"'(?:[^'\\{_UNSPELLABLE_CLASS}]|\\[^{_UNSPELLABLE_CLASS}])*'")
179
+
180
+
181
+ def _unquoted_stretches(text: str) -> List[str]:
182
+ """The pieces of ``text`` between its quoted spans, in order.
183
+
184
+ ``len(result)`` is one more than the number of spans; an empty piece stands
185
+ where a span begins or ends the text. A piece never holds a quote that has a
186
+ partner.
187
+ """
188
+ pieces: List[str] = []
189
+ last = 0
190
+ for span in _QUOTED_SPAN.finditer(text):
191
+ pieces.append(text[last:span.start()])
192
+ last = span.end()
193
+ pieces.append(text[last:])
194
+ return pieces
195
+
196
+
197
+ def _map_unquoted(text: str, rewrite) -> str:
198
+ """``text`` with ``rewrite`` applied to each piece outside its quoted spans;
199
+ every quoted span stays exactly as it was."""
200
+ out: List[str] = []
201
+ last = 0
202
+ for span in _QUOTED_SPAN.finditer(text):
203
+ out.append(rewrite(text[last:span.start()]))
204
+ out.append(span.group(0))
205
+ last = span.end()
206
+ out.append(rewrite(text[last:]))
207
+ return "".join(out)
208
+
209
+
210
+ # ---------------------------------------------------------------------------
211
+ # Case 2 — invalid names: raw-text detection
212
+ # ---------------------------------------------------------------------------
213
+ #
214
+ # Mirrors tptp_repair._CANDIDATE_RE (a name-shaped span immediately before an
215
+ # argument list's "(", optionally preceded by one non-nested parenthesised
216
+ # fragment, with a left-boundary lookbehind so scanning is non-overlapping)
217
+ # with one deliberate difference: "_" is a candidate character here, so a
218
+ # name-shaped span that holds an underscore is found whole and not from the
219
+ # underscore on. An underscore is legal inside a predicate of this dialect
220
+ # (`Foo_bar(x)` reads as it is), so it does not by itself make a name a case-2
221
+ # name; a hyphen does (`Foo-bar(x)`).
222
+ _CANDIDATE_RE = re.compile(
223
+ r"(?<![A-Za-z0-9_,\-)])"
224
+ r"((?:\([^()\n]{1,40}\))?[A-Za-z0-9][A-Za-z0-9_,\-]*)"
225
+ r"(?=\s*\()"
226
+ )
227
+
228
+ # The three symbol-class patterns (and the broader IDENT scan below) are
229
+ # built from fol._identifiers lazily, behind lru_cache, rather than at this
230
+ # module's import time: fol/__init__.py imports this module eagerly, so a
231
+ # module-level ``re.compile(_predicate_pattern())`` here would force
232
+ # _identifiers' once-per-process codepoint scan (~0.1-1s) onto every
233
+ # ``import unicode_logic_kit`` — even one that never calls repair_formula. The
234
+ # lru_cache still means the scan runs at most once per process; it just runs
235
+ # on first actual USE instead of on import.
236
+
237
+ @lru_cache(maxsize=1)
238
+ def _legal_name_patterns():
239
+ return (
240
+ re.compile(_predicate_pattern()),
241
+ re.compile(_name_pattern()),
242
+ re.compile(_variable_pattern()),
243
+ )
244
+
245
+
246
+ @lru_cache(maxsize=1)
247
+ def _ident_re():
248
+ """Every identifier-shaped span in the dialect's widened alphabet: a
249
+ letter or digit, then letters/digits/underscores/combining marks — a
250
+ superset of PREDICATE/NAME/VARIABLE/CONSTANT (permissive on purpose,
251
+ since this only feeds the collision-avoidance preseed below, not a
252
+ legality check)."""
253
+ letters = f"{_uppercase_class()}{_lowercase_class()}"
254
+ cont = f"{letters}0-9_{_combining_class()}"
255
+ return re.compile(f"[{letters}0-9][{cont}]*")
256
+
257
+
258
+ #: Substring of a parse-error message that means the grammar refused a mix of
259
+ #: same-level connectives. Matching on the message text is the same small,
260
+ #: documented heuristic ``unicode_logic_kit.mcp.server._SPEC_HINTS`` already
261
+ #: uses to route a diagnosis; the hint itself is built in
262
+ #: ``unicode_logic_kit.fol.naming._MIXING_INFO``.
263
+ _MIXING_MARKER = "cannot mix"
264
+
265
+
266
+ def _is_legal_name(name: str) -> bool:
267
+ """Is ``name`` already a legal token in SOME symbol class of THIS
268
+ dialect's grammar?
269
+
270
+ Predicate, function/bare constant (NAME, including its underscored and
271
+ digit-leading forms), or variable — see :mod:`unicode_logic_kit.fol.
272
+ _identifiers` for the exact shapes. The variable class matters even
273
+ though a variable is never a functor: ``∀x (…)`` puts ``x`` immediately
274
+ before a "(" and so matches :data:`_CANDIDATE_RE` — renaming a bound
275
+ variable to a predicate would destroy the formula, and the one thing
276
+ keeping it out of the candidate list is that ``x`` is legal here.
277
+ """
278
+ pred_re, name_re, var_re = _legal_name_patterns()
279
+ return bool(pred_re.fullmatch(name) or name_re.fullmatch(name)
280
+ or var_re.fullmatch(name))
281
+
282
+
283
+ def _find_name_candidates(text: str) -> List[str]:
284
+ """Distinct, order-preserving functor-position names in ``text`` that are
285
+ legal in no symbol class. Nothing between a pair of single quotes is a
286
+ candidate: that is a constant's name, not a name in functor position."""
287
+ seen = set()
288
+ out: List[str] = []
289
+ for stretch in _unquoted_stretches(text):
290
+ for match in _CANDIDATE_RE.finditer(stretch):
291
+ name = match.group(1)
292
+ if _is_legal_name(name) or name in seen:
293
+ continue
294
+ seen.add(name)
295
+ out.append(name)
296
+ return out
297
+
298
+
299
+ def _apply_renames(text: str, renames: Tuple[Tuple[str, str], ...]) -> str:
300
+ """Replace every functor-position occurrence of each original name.
301
+
302
+ By name rather than by span (the same mis-written predicate legitimately
303
+ occurs more than once, and substituting by name catches every occurrence
304
+ without tracking shifting offsets), LONGEST FIRST: one invalid name can
305
+ end with another (``3-oxo-steroid`` and ``oxo-steroid``), and rewriting
306
+ the shorter one first would eat the tail of the longer one's occurrences
307
+ and leave a half-renamed name behind.
308
+
309
+ Only the text outside single quotes is rewritten: the spelling of a quoted
310
+ constant is the constant, so a quoted span that holds a candidate's
311
+ spelling (``'1,2-diacyl(x)'``) stays as it is.
312
+ """
313
+ def rewrite(stretch: str) -> str:
314
+ for name, legal in sorted(renames, key=lambda pair: -len(pair[0])):
315
+ stretch = re.sub(re.escape(name) + r"(?=\s*\()", legal, stretch)
316
+ return stretch
317
+
318
+ return _map_unquoted(text, rewrite)
319
+
320
+
321
+ def _farthest(errors) -> str:
322
+ """The most informative of ``parse_any``'s per-dialect messages.
323
+
324
+ The FARTHEST failure, not the last listed — the dialects at the end of
325
+ the detection order are the specialised ones that give up earliest on
326
+ ordinary input. Same rule, same implementation, as
327
+ :func:`unicode_logic_kit.api._suggest`'s.
328
+ """
329
+ from ..api import _message_progress # deferred: api imports fol
330
+
331
+ if not errors:
332
+ return "no parser accepted the text"
333
+ best = max(errors, key=lambda e: _message_progress(e.get("message", "")))
334
+ return best.get("message", "")
335
+
336
+
337
+ def _failed(text: str, kind: str, message: str,
338
+ suggestion: Optional[str] = None) -> DialectRepairResult:
339
+ """An honest failure: nothing rewritten, the diagnosis handed back."""
340
+ return DialectRepairResult(
341
+ ok=False, formula=None, repaired_text=text,
342
+ issues=(Issue(kind, message, suggestion=suggestion),), changed=False)
343
+
344
+
345
+ # ---------------------------------------------------------------------------
346
+ # Case 4 — a truth glyph beside another dialect's connectives
347
+ # ---------------------------------------------------------------------------
348
+
349
+ #: The unicode glyph of each truth constant and, for the two ASCII dialects, the
350
+ #: constant that dialect spells it with: ``(Prover9, TPTP)``.
351
+ _GLYPH_SPELLINGS = {"⊤": ("$T", "$true"), "⊥": ("$F", "$false")}
352
+ _GLYPH_RE = re.compile("[⊤⊥]")
353
+
354
+
355
+ def _glyph_rewrites(text: str):
356
+ """The texts that spell each truth glyph of ``text`` as an ASCII dialect's own
357
+ constant, Prover9's first and TPTP's second; nothing when ``text`` has no glyph.
358
+
359
+ A glyph between single quotes is part of a constant's name (``'⊤'``) and is
360
+ neither found nor rewritten."""
361
+ if not any(_GLYPH_RE.search(stretch) for stretch in _unquoted_stretches(text)):
362
+ return
363
+ for column in (0, 1):
364
+ yield _map_unquoted(
365
+ text,
366
+ lambda stretch, column=column: _GLYPH_RE.sub(
367
+ lambda m: _GLYPH_SPELLINGS[m.group(0)][column], stretch)), column
368
+
369
+
370
+ def _parse_reading_glyphs(text: str, dialect: Optional[str]):
371
+ """``(parse result, rewritten text)``: ``text`` parsed as it stands, else, when it
372
+ does not parse and holds a truth glyph, parsed with the glyphs spelled as an ASCII
373
+ dialect's constants (see the module docstring, case 4). The rewritten text is
374
+ ``None`` when the text parsed as it stands or no rewrite parsed."""
375
+ from ..api import parse_any # deferred: api imports fol
376
+
377
+ parsed = parse_any(text, hint=dialect)
378
+ if parsed.ok:
379
+ return parsed, None
380
+ for rewritten, _column in _glyph_rewrites(text):
381
+ retried = parse_any(rewritten, hint=dialect)
382
+ if retried.ok:
383
+ return retried, rewritten
384
+ return parsed, None
385
+
386
+
387
+ def _glyph_issue(rewritten: str, dialect_name: Optional[str]) -> Issue:
388
+ return Issue(
389
+ "truth_glyph",
390
+ "the text writes the truth glyph ⊤ / ⊥ beside the connectives of another "
391
+ f"dialect, which no single grammar reads; the glyphs were read as that "
392
+ f"dialect's own truth constants ({rewritten!r}), and the formula parsed "
393
+ f"as {dialect_name!r}. The result holds the truth constants $true / $false.",
394
+ suggestion=rewritten)
395
+
396
+
397
+ # ---------------------------------------------------------------------------
398
+ # Public API
399
+ # ---------------------------------------------------------------------------
400
+
401
+ def repair_formula(text: str, *, dialect: Optional[str] = None,
402
+ sanitize_invalid_names: bool = True,
403
+ close_free_variables: bool = False,
404
+ mapping: Optional[NameMapping] = None
405
+ ) -> DialectRepairResult:
406
+ """Repair one formula written in the kit's own surface syntax.
407
+
408
+ Pipeline:
409
+
410
+ 1. Parse via :func:`unicode_logic_kit.api.parse_any`. A text that does not
411
+ parse but holds the truth glyph ``⊤`` / ``⊥`` beside another dialect's
412
+ connectives (``P -> ⊥``) is read with the glyph spelled as that
413
+ dialect's constant (case 4), and says so in an issue. On failure, if the
414
+ grammar refused a mix of same-level connectives, report it and stop
415
+ (case 1' — never bracketed, see the module docstring). Otherwise, if
416
+ ``sanitize_invalid_names``, rename every functor-position name that is
417
+ legal in no symbol class (case 2) and re-parse; a rewrite that still
418
+ does not parse is discarded entirely.
419
+ 2. Free variables (case 3) are read off the AST: always reported, and
420
+ additionally closed — argument-drop where that exact shape holds, else
421
+ universal closure — only when ``close_free_variables=True``.
422
+ 3. ``repaired_text`` is the unicode rendering of the resulting AST.
423
+
424
+ Args:
425
+ text: one formula in the kit's unicode syntax.
426
+ dialect: pin the dialect (``parse_any``'s ``hint``) instead of
427
+ detecting it. ``"unicode"`` for the whole mode ladder, a mode
428
+ name (``"fol"``, ``"modal"``, …) for one mode.
429
+ sanitize_invalid_names: attempt case 2's rename when parsing fails.
430
+ ``False`` reports the syntax error unchanged.
431
+ close_free_variables: apply case 3's fix instead of only reporting
432
+ it. Default ``False``: report, never touch — an unclosed free
433
+ variable never blocks ``ok=True``, because the AST is valid; the
434
+ kit just will not guess whether you meant universal closure or an
435
+ argument that should be dropped.
436
+ mapping: an existing :class:`NameMapping` to extend, so the same
437
+ original name renames to the same token across a whole run. A
438
+ fresh one is used when omitted.
439
+
440
+ Returns:
441
+ A :class:`DialectRepairResult`.
442
+ """
443
+ issues: List[Issue] = []
444
+ renames: Tuple[Tuple[str, str], ...] = ()
445
+
446
+ parsed, glyph_text = _parse_reading_glyphs(text, dialect)
447
+ if glyph_text is not None:
448
+ issues.append(_glyph_issue(glyph_text, parsed.dialect))
449
+ if not parsed.ok:
450
+ message = _farthest(parsed.errors)
451
+ if _MIXING_MARKER in message.lower():
452
+ return _failed(
453
+ text, "mixed_connectives",
454
+ f"{message} The kit does not bracket this for you: the two "
455
+ "readings are different formulas and choosing one would be a "
456
+ "guess. Write the brackets you mean.")
457
+ if not sanitize_invalid_names:
458
+ return _failed(text, "syntax_error", message)
459
+
460
+ candidates = _find_name_candidates(text)
461
+ if not candidates:
462
+ return _failed(text, "syntax_error", message)
463
+
464
+ if mapping is None:
465
+ mapping = NameMapping()
466
+ # Preseed with the identifiers already in the text so a legalised
467
+ # name can never collide with a symbol that is already there (see
468
+ # the module docstring's collision limit). The words of a quoted
469
+ # constant are in the preseed too, on purpose: that reads the text and
470
+ # rewrites nothing, and a legal name that a constant already spells is
471
+ # one more name to keep the renamed predicate away from.
472
+ mapping.used.update(match.group(0) for match in _ident_re().finditer(text))
473
+ renames = tuple((name, mapping.for_predicate(name)) for name in candidates)
474
+
475
+ retried = _apply_renames(text, renames)
476
+ parsed, glyph_text = _parse_reading_glyphs(retried, dialect)
477
+ if glyph_text is not None:
478
+ issues.append(_glyph_issue(glyph_text, parsed.dialect))
479
+ if not parsed.ok:
480
+ names = sorted(name for name, _legal in renames)
481
+ return _failed(
482
+ text, "syntax_error",
483
+ f"renaming {names!r} did not make the formula parseable: "
484
+ f"{_farthest(parsed.errors)}")
485
+ for name, legal in renames:
486
+ issues.append(Issue(
487
+ "invalid_predicate_name",
488
+ f"{name!r} is a legal token in no symbol class of this "
489
+ f"dialect; renamed to the predicate {legal!r}. The original "
490
+ "spelling is preserved in this result's `names` (and in the "
491
+ "NameMapping, if you passed one) — nothing about the name is "
492
+ "lost, but it now lives beside the formula rather than in it.",
493
+ suggestion=legal))
494
+
495
+ formula: Node = parsed.formula
496
+ final_formula = formula
497
+ free = free_variables(formula)
498
+ if free:
499
+ names_text = ", ".join(sorted(v.name for v in free))
500
+ if close_free_variables:
501
+ dropped = (_drop_argument_fix(formula, next(iter(free)))
502
+ if len(free) == 1 else None)
503
+ if dropped is not None:
504
+ final_formula = dropped
505
+ issues.append(Issue(
506
+ "free_variable",
507
+ f"unbound variable {names_text!r} is the sole argument of "
508
+ "the left-hand definiendum and does not recur on the "
509
+ "right — dropped it: the definition is, in substance, a "
510
+ "0-ary property that only accidentally carries an "
511
+ "argument.",
512
+ suggestion=dropped.to_unicode_str()))
513
+ else:
514
+ final_formula = _close_universally(formula, free)
515
+ issues.append(Issue(
516
+ "free_variable",
517
+ f"unbound variable(s) {names_text} closed via universal "
518
+ "quantification (standard universal-closure convention; "
519
+ "the argument-drop fix does not apply — the formula is "
520
+ "not a `P(x) ↔ …`/`P(x) → …` definition with x absent "
521
+ "from the right-hand side).",
522
+ suggestion=final_formula.to_unicode_str()))
523
+ else:
524
+ issues.append(Issue(
525
+ "free_variable",
526
+ f"unbound variable(s) {names_text} — not closed (default "
527
+ "close_free_variables=False). The kit will not guess whether "
528
+ "you meant universal closure or an argument that should be "
529
+ "dropped; pass close_free_variables=True to apply the "
530
+ "documented conservative fix, or rewrite by hand."))
531
+
532
+ repaired_text = final_formula.to_unicode_str()
533
+ return DialectRepairResult(
534
+ ok=True, formula=final_formula, repaired_text=repaired_text,
535
+ issues=tuple(issues),
536
+ changed=_squash(text) != _squash(repaired_text),
537
+ dialect=parsed.dialect, names=renames)