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,884 @@
1
+ r"""Isabelle/HOL export for ILL and Lambek-calculus derivations — proof by REPLAY.
2
+
3
+ Unlike the *shallow* semantical embeddings elsewhere in :mod:`unicode_logic_kit.hol`
4
+ (e.g. :mod:`~unicode_logic_kit.hol.isabelle_modal`), this module needs no semantics at
5
+ all. :mod:`unicode_logic_kit.atp.linear` (ILL) and :mod:`unicode_logic_kit.atp.lambek` (the
6
+ Lambek calculus L) are already **complete, sound, cut-free sequent-calculus decision
7
+ procedures**: a formula's :class:`~unicode_logic_kit.atp.linear.ILLDerivation` /
8
+ :class:`~unicode_logic_kit.atp.lambek.LambekDerivation` tree, once found, already *is* a
9
+ complete proof — Isabelle only needs to be shown, at every node, which rule fired and
10
+ why its premises license its conclusion. So this module:
11
+
12
+ 1. **Deep-embeds** the formula grammar as an Isabelle ``datatype`` (``ill`` / ``lam``,
13
+ one constructor per connective, prefixed ``I``/``L`` to dodge any clash with an
14
+ existing HOL identifier — mirroring the ``tv = tT | tB | tF`` prefixing convention
15
+ in :mod:`~unicode_logic_kit.hol.manyvalued`);
16
+ 2. Defines the sequent-calculus derivability judgement **1:1** as an
17
+ ``inductive derivable`` predicate, one intro rule per rule of the Python calculus,
18
+ each named after and transcribed verbatim from the corresponding ``_r_*`` checker
19
+ in :mod:`~unicode_logic_kit.atp.linear` / :mod:`~unicode_logic_kit.atp.lambek`;
20
+ 3. States the queried sequent as a ``lemma`` and **replays** the Python-found
21
+ derivation tree as an explicit, fully mechanical Isar proof — a ``have`` step per
22
+ tree node, closed by exactly the rule that node used (``by (rule <IntroName>)``,
23
+ ``using`` the already-established premise facts). No search, no automation beyond
24
+ the purely computational bookkeeping described next: the *replay itself* is the
25
+ proof.
26
+
27
+ ILL's antecedent: ``list``, not ``multiset`` — and why
28
+ --------------------------------------------------------
29
+ ILL's antecedent Γ is mathematically a **multiset** (:mod:`atp.linear`'s own
30
+ docstring says so, and the natural Isabelle rendering is
31
+ ``inductive derivable :: "ill multiset ⇒ ill ⇒ bool"`` after
32
+ ``imports "HOL-Library.Multiset"``). This module does **not** do that, for a concrete,
33
+ checkable reason: the theories it emits are built and checked with
34
+ :func:`unicode_logic_kit.hol.isabelle_runner.check_theory`, whose scratch session ROOT is
35
+ fixed as ``session S = HOL + ... theories <name>`` — it extends only the base ``HOL``
36
+ session, with no ``sessions "HOL-Library"`` dependency declaration, so
37
+ ``imports "HOL-Library.Multiset"`` fails to resolve (confirmed empirically: Isabelle
38
+ reports *"Bad import of theory 'HOL-Library.Multiset': need to include sessions
39
+ 'HOL-Library' in ROOT"*). Since ``check_theory`` is used exactly as instructed and
40
+ lives outside this module, this module instead represents Γ as an ``ill list`` and
41
+ makes the ONE structural fact a multiset gets "for free" — exchange, i.e. every
42
+ reordering of Γ is derivability-preserving — an **explicit extra intro rule**,
43
+ ``Exch``. This is mathematically exact: "multiset" and "list quotiented by the
44
+ transitive closure of adjacent transpositions" are the same equivalence, so
45
+ ``derivable`` here decides *exactly* the same relation as the Python ``Counter``-based
46
+ :func:`~unicode_logic_kit.atp.linear.ill_prove` — it is simply not literally typed as
47
+ ``multiset``. Every place the replay needs two lists that are the same multiset in a
48
+ different order (which happens often — Python's own antecedent tuples are sorted by
49
+ render text, not by proof structure) it emits an explicit, finite chain of ``Exch``
50
+ applications computed by an adjacent-transposition sort (see ``_reorder_swaps``),
51
+ never asking ``simp``/``auto`` to guess a permutation. The Lambek side needs no such
52
+ device: L's antecedent genuinely IS an ordered list (no exchange), so
53
+ ``lam list`` is the literally correct type, ``imports Main`` alone suffices, and the
54
+ replay never reorders anything — only re-associates ``@``, always by a trivial,
55
+ fully mechanical ``simp`` step (pure ground computation, since every list involved is,
56
+ by construction, fully concrete).
57
+
58
+ Public API
59
+ ----------
60
+ :func:`to_isabelle_ill` / :func:`to_isabelle_lambek` (run the Python prover
61
+ internally, raise ``ValueError`` if the sequent is not derivable) and
62
+ :func:`ill_derivation_theory` / :func:`lambek_derivation_theory` (take an
63
+ already-found — and checker-validated — derivation object directly).
64
+ """
65
+
66
+ from collections import Counter
67
+ from typing import Iterable, List, Optional, Sequence, Tuple
68
+
69
+ from ..fol.nodes import (
70
+ Node, Atom, Tensor, With, OPlus, LinearImplies, OfCourse, One,
71
+ Product, Under, Over,
72
+ )
73
+ from ..fol._linear_nodes import Top, Zero
74
+ from ..fol._msfl_nodes import key_text
75
+ from ..fol._truth_constants import refuse_truth_constants
76
+ from ..atp.linear import (
77
+ ILLDerivation, ILLSequent, ill_prove, check_ill_proof,
78
+ _NO_TRUTH_CONSTANT_WHY as _ILL_NO_CONSTANT_WHY,
79
+ )
80
+ from ..atp.lambek import (
81
+ LambekDerivation, LambekSequent, lambek_prove, check_lambek_proof,
82
+ _NO_TRUTH_CONSTANT_WHY as _LAMBEK_NO_CONSTANT_WHY,
83
+ )
84
+
85
+ __all__ = [
86
+ "to_isabelle_ill", "ill_derivation_theory",
87
+ "to_isabelle_lambek", "lambek_derivation_theory",
88
+ ]
89
+
90
+
91
+ # =============================================================================
92
+ # Shared low-level helpers: Isabelle string literals, name bookkeeping.
93
+ # =============================================================================
94
+
95
+ def _isa_str(s: str) -> str:
96
+ """Render a Python string as an Isabelle ``string`` literal ``''...''``.
97
+
98
+ ILL/Lambek atoms in this kit are propositional letters / atomic categories
99
+ (``A``, ``NP``, ...); their text never legitimately contains a
100
+ single quote, so — rather than silently mis-escaping — an embedded ``'``
101
+ is rejected outright with a message pointing at why. The text of an atom
102
+ is its key (``key_text``: every constant by its bare name), not the text of
103
+ the formula: that writes a constant such as ``k2`` in quotes, which would
104
+ turn an export that works into this refusal.
105
+ """
106
+ if "'" in s:
107
+ raise NotImplementedError(
108
+ f"to_isabelle_ill/to_isabelle_lambek: atom text {s!r} contains a "
109
+ "single quote, which the Isabelle string-literal encoding used "
110
+ "here does not escape; rename the atom.")
111
+ return "''" + s + "''"
112
+
113
+
114
+ class _Emitter:
115
+ """Accumulates the Isar proof-script lines of one lemma and hands out
116
+ fresh, collision-free fact names (``h1``, ``h2``, ...)."""
117
+
118
+ def __init__(self):
119
+ self.lines: List[str] = []
120
+ self._n = 0
121
+
122
+ def fresh(self) -> str:
123
+ self._n += 1
124
+ return f"h{self._n}"
125
+
126
+ def have(self, stmt: str, using: Sequence[str], method: str) -> str:
127
+ """Emit ``have hN: "stmt" using f1 f2 ... by (method)`` and return hN."""
128
+ name = self.fresh()
129
+ using_clause = f"using {' '.join(using)} " if using else ""
130
+ self.lines.append(f' have {name}: "{stmt}"\n {using_clause}by {method}')
131
+ return name
132
+
133
+
134
+ # =============================================================================
135
+ # The general list-reordering primitive (ILL only — see the module docstring).
136
+ # =============================================================================
137
+
138
+ def _reorder_swaps(src: Sequence[Node], dst: Sequence[Node]) -> List[int]:
139
+ """Adjacent-transposition sequence turning ``src`` into ``dst`` (same multiset).
140
+
141
+ Returns a list of positions ``k`` (swap ``cur[k]``/``cur[k+1]``), to be applied
142
+ to ``src`` **in order**, ending at ``dst``. Standard selection-style algorithm:
143
+ for each target position ``i`` (left to right), find the first not-yet-placed
144
+ occurrence of ``dst[i]`` at or after ``i`` and bubble it left into place —
145
+ O(n^2) swaps worst case, ample for the antecedents this module handles.
146
+ """
147
+ cur = list(src)
148
+ n = len(cur)
149
+ swaps: List[int] = []
150
+ for i in range(n):
151
+ if cur[i] == dst[i]:
152
+ continue
153
+ j = i + 1
154
+ while cur[j] != dst[i]:
155
+ j += 1
156
+ while j > i:
157
+ swaps.append(j - 1)
158
+ cur[j - 1], cur[j] = cur[j], cur[j - 1]
159
+ j -= 1
160
+ assert cur == list(dst), "internal error: _reorder_swaps did not reach dst"
161
+ return swaps
162
+
163
+
164
+ def _remove_one(lst: List[Node], value: Node) -> List[Node]:
165
+ """Return ``lst`` with the first element equal to ``value`` deleted."""
166
+ out = list(lst)
167
+ for i, x in enumerate(out):
168
+ if x == value:
169
+ del out[i]
170
+ return out
171
+ raise AssertionError(f"internal error: {value!r} not found in {lst!r}")
172
+
173
+
174
+ def _counter_diff(a: Sequence[Node], b: Sequence[Node]) -> Counter:
175
+ """``Counter(a) - Counter(b)``, positive entries only (what ``a`` has extra)."""
176
+ c = Counter(a)
177
+ c.subtract(Counter(b))
178
+ return Counter({k: v for k, v in c.items() if v > 0})
179
+
180
+
181
+ def _the_one(c: Counter) -> Node:
182
+ """Return the single key of a one-entry-count-1 Counter (asserts the shape)."""
183
+ assert len(c) == 1 and next(iter(c.values())) == 1, f"internal error: not a singleton diff: {c!r}"
184
+ return next(iter(c))
185
+
186
+
187
+ # =============================================================================
188
+ # ILL: deep embedding.
189
+ # =============================================================================
190
+
191
+ ILL_DATATYPE = (
192
+ "datatype ill = IAtom string | ITensor ill ill | IWith ill ill | IOPlus ill ill "
193
+ "| ILImp ill ill | IBang ill | IOne | ITop | IZero"
194
+ )
195
+
196
+ # One entry per Isabelle intro rule, in emission order. 19 mirror the 19 rules of
197
+ # atp.linear._ILL_RULES one-for-one (named identically up to Isabelle-legal
198
+ # spelling — see _ILL_RULE_TO_ISA); "Exch" is the one addition, the explicit
199
+ # structural rule that stands in for "antecedent is a multiset" (see the module
200
+ # docstring). 20 rules total.
201
+ _ILL_INTROS: List[Tuple[str, str]] = [
202
+ ("Ax", 'derivable [A] A'),
203
+ ("Exch", 'derivable (G1 @ [B, A] @ G2) C \\<Longrightarrow> derivable (G1 @ [A, B] @ G2) C'),
204
+ ("OneR", 'derivable [] IOne'),
205
+ ("OneL", 'derivable G C \\<Longrightarrow> derivable (G @ [IOne]) C'),
206
+ ("TensorL", 'derivable (G @ [A, B]) C \\<Longrightarrow> derivable (G @ [ITensor A B]) C'),
207
+ ("TensorR", 'derivable G A \\<Longrightarrow> derivable D B \\<Longrightarrow> derivable (G @ D) (ITensor A B)'),
208
+ ("LImpL", 'derivable G A \\<Longrightarrow> derivable (D @ [B]) C \\<Longrightarrow> '
209
+ 'derivable (G @ D @ [ILImp A B]) C'),
210
+ ("LImpR", 'derivable (G @ [A]) B \\<Longrightarrow> derivable G (ILImp A B)'),
211
+ ("WithL1", 'derivable (G @ [A]) C \\<Longrightarrow> derivable (G @ [IWith A B]) C'),
212
+ ("WithL2", 'derivable (G @ [B]) C \\<Longrightarrow> derivable (G @ [IWith A B]) C'),
213
+ ("WithR", 'derivable G A \\<Longrightarrow> derivable G B \\<Longrightarrow> derivable G (IWith A B)'),
214
+ ("OPlusL", 'derivable (G @ [A]) C \\<Longrightarrow> derivable (G @ [B]) C \\<Longrightarrow> '
215
+ 'derivable (G @ [IOPlus A B]) C'),
216
+ ("OPlusR1", 'derivable G A \\<Longrightarrow> derivable G (IOPlus A B)'),
217
+ ("OPlusR2", 'derivable G B \\<Longrightarrow> derivable G (IOPlus A B)'),
218
+ ("BangW", 'derivable G C \\<Longrightarrow> derivable (G @ [IBang A]) C'),
219
+ ("BangC", 'derivable (G @ [IBang A, IBang A]) C \\<Longrightarrow> derivable (G @ [IBang A]) C'),
220
+ ("BangD", 'derivable (G @ [A]) C \\<Longrightarrow> derivable (G @ [IBang A]) C'),
221
+ ("BangP", 'derivable (map IBang G0) A \\<Longrightarrow> derivable (map IBang G0) (IBang A)'),
222
+ ("TopR", 'derivable G ITop'),
223
+ ("ZeroL", 'derivable (G @ [IZero]) C'),
224
+ ]
225
+
226
+ # Python atp.linear rule name -> Isabelle intro name (Exch has no Python
227
+ # counterpart — see the module docstring).
228
+ _ILL_RULE_TO_ISA = {
229
+ "Ax": "Ax", "1R": "OneR", "1L": "OneL",
230
+ "⊗L": "TensorL", "⊗R": "TensorR",
231
+ "⊸L": "LImpL", "⊸R": "LImpR",
232
+ "&L1": "WithL1", "&L2": "WithL2", "&R": "WithR",
233
+ "⊕L": "OPlusL", "⊕R1": "OPlusR1", "⊕R2": "OPlusR2",
234
+ "!W": "BangW", "!C": "BangC", "!D": "BangD", "!P": "BangP",
235
+ "⊤R": "TopR", "0L": "ZeroL",
236
+ }
237
+
238
+
239
+ def _lift_ill(node: Node) -> str:
240
+ """Lift an ILL formula to its ``ill``-datatype Isabelle term."""
241
+ if isinstance(node, Atom):
242
+ refuse_truth_constants([node], "to_isabelle_ill", _ILL_NO_CONSTANT_WHY)
243
+ return f"(IAtom {_isa_str(key_text(node))})"
244
+ if isinstance(node, Tensor):
245
+ return f"(ITensor {_lift_ill(node.left)} {_lift_ill(node.right)})"
246
+ if isinstance(node, With):
247
+ return f"(IWith {_lift_ill(node.left)} {_lift_ill(node.right)})"
248
+ if isinstance(node, OPlus):
249
+ return f"(IOPlus {_lift_ill(node.left)} {_lift_ill(node.right)})"
250
+ if isinstance(node, LinearImplies):
251
+ return f"(ILImp {_lift_ill(node.left)} {_lift_ill(node.right)})"
252
+ if isinstance(node, OfCourse):
253
+ return f"(IBang {_lift_ill(node.formula)})"
254
+ if isinstance(node, One):
255
+ return "IOne"
256
+ if isinstance(node, Top):
257
+ return "ITop"
258
+ if isinstance(node, Zero):
259
+ return "IZero"
260
+ raise NotImplementedError(f"to_isabelle_ill: unsupported node {type(node).__name__}.")
261
+
262
+
263
+ def _flat_ill(nodes: Sequence[Node]) -> str:
264
+ """Bracket-list Isabelle text for a list of ILL formulas, e.g. ``[A, B]``."""
265
+ if not nodes:
266
+ return "[]"
267
+ return "[" + ", ".join(_lift_ill(n) for n in nodes) + "]"
268
+
269
+
270
+ # ---------------------------------------------------------------------------
271
+ # ILL replay engine.
272
+ # ---------------------------------------------------------------------------
273
+ #
274
+ # Contract of _emit_ill(d, emitter) -> (fact_name, antecedent_list): fact_name
275
+ # names a fact, already emitted into emitter.lines, whose statement is EXACTLY
276
+ # "derivable <_flat_ill(antecedent_list)> <_lift_ill(d.conclusion.succedent)>".
277
+ # antecedent_list is SOME list of Node with that multiset (not necessarily
278
+ # d.conclusion.antecedent's own order — this module never needs to match that
279
+ # specific order, only to be internally consistent; see the module docstring).
280
+
281
+ def _reorder_bridge(fact: str, src: List[Node], dst: List[Node],
282
+ succ_isa: str, emitter: _Emitter) -> str:
283
+ """Reshape a flat fact for ``src`` into one for ``dst`` (same multiset), via an
284
+ explicit ``Exch`` chain. Returns ``fact`` unchanged if ``src == dst`` already."""
285
+ if src == dst:
286
+ return fact
287
+ cur = list(src)
288
+ cur_fact = fact
289
+ for k in _reorder_swaps(src, dst):
290
+ x, y = cur[k], cur[k + 1]
291
+ g1, g2 = cur[:k], cur[k + 2:]
292
+ decomp_before = f"{_flat_ill(g1)} @ {_flat_ill([x, y])} @ {_flat_ill(g2)}"
293
+ h_shaped = emitter.have(
294
+ f'derivable ({decomp_before}) {succ_isa}', [cur_fact], "simp")
295
+ decomp_after = f"{_flat_ill(g1)} @ {_flat_ill([y, x])} @ {_flat_ill(g2)}"
296
+ h_swapped = emitter.have(
297
+ f'derivable ({decomp_after}) {succ_isa}', [h_shaped], "(rule Exch)")
298
+ cur[k], cur[k + 1] = y, x
299
+ cur_fact = emitter.have(
300
+ f'derivable {_flat_ill(cur)} {succ_isa}', [h_swapped], "simp")
301
+ assert cur == dst
302
+ return cur_fact
303
+
304
+
305
+ def _decompose(fact: str, full: List[Node], pieces: List[List[Node]],
306
+ succ_isa: str, emitter: _Emitter) -> str:
307
+ """Reshape a flat fact for ``full`` (== the concatenation of ``pieces``) into
308
+ an explicit ``@``-joined form matching ``pieces`` — a pure re-association
309
+ (never a reorder), always closed by a trivial ground ``simp``."""
310
+ text = " @ ".join(_flat_ill(p) for p in pieces) if pieces else "[]"
311
+ flat_text = _flat_ill(full)
312
+ if text == flat_text:
313
+ return fact
314
+ return emitter.have(f'derivable ({text}) {succ_isa}', [fact], "simp")
315
+
316
+
317
+ def _emit_ill(d: ILLDerivation, emitter: _Emitter) -> Tuple[str, List[Node]]:
318
+ """Recursively replay one ILL derivation node; see the contract above."""
319
+ succ_isa = _lift_ill(d.conclusion.succedent)
320
+ rule = d.rule
321
+
322
+ if rule == "Ax":
323
+ lst = [d.conclusion.antecedent[0]]
324
+ fact = emitter.have(f'derivable {_flat_ill(lst)} {succ_isa}', [], "(rule Ax)")
325
+ return fact, lst
326
+
327
+ if rule == "1R":
328
+ fact = emitter.have(f'derivable [] {succ_isa}', [], "(rule OneR)")
329
+ return fact, []
330
+
331
+ if rule == "⊤R":
332
+ lst = list(d.conclusion.antecedent)
333
+ fact = emitter.have(f'derivable {_flat_ill(lst)} {succ_isa}', [], "(rule TopR)")
334
+ return fact, lst
335
+
336
+ if rule == "0L":
337
+ lst_all = list(d.conclusion.antecedent)
338
+ idx = next(i for i, x in enumerate(lst_all) if isinstance(x, Zero))
339
+ rest = lst_all[:idx] + lst_all[idx + 1:]
340
+ target = rest + [lst_all[idx]]
341
+ stmt = f'({_flat_ill(rest)} @ {_flat_ill([lst_all[idx]])}) {succ_isa}'
342
+ fact = emitter.have(f'derivable {stmt}', [], "(rule ZeroL)")
343
+ return fact, target
344
+
345
+ # --- single-premise rules -------------------------------------------------
346
+ if rule in ("1L", "⊗L", "⊸R", "&L1", "&L2", "!W", "!C", "!D"):
347
+ child = d.premises[0]
348
+ cfact, clist = _emit_ill(child, emitter)
349
+ csucc = _lift_ill(child.conclusion.succedent)
350
+
351
+ if rule == "1L":
352
+ extract, tail = [], [One()]
353
+ elif rule == "⊗L":
354
+ compound = _the_one(_counter_diff(d.conclusion.antecedent, clist))
355
+ extract, tail = [compound.left, compound.right], [compound]
356
+ elif rule == "⊸R":
357
+ a = _the_one(_counter_diff(clist, d.conclusion.antecedent))
358
+ extract, tail = [a], []
359
+ elif rule in ("&L1", "&L2"):
360
+ compound = _the_one(_counter_diff(d.conclusion.antecedent, clist))
361
+ extract = [compound.left if rule == "&L1" else compound.right]
362
+ tail = [compound]
363
+ elif rule == "!W":
364
+ compound = _the_one(_counter_diff(d.conclusion.antecedent, clist))
365
+ extract, tail = [], [compound]
366
+ elif rule == "!C":
367
+ compound = _the_one(_counter_diff(clist, d.conclusion.antecedent))
368
+ extract, tail = [compound, compound], [compound]
369
+ else: # "!D"
370
+ compound = _the_one(_counter_diff(d.conclusion.antecedent, clist))
371
+ extract, tail = [compound.formula], [compound]
372
+
373
+ rest = list(clist)
374
+ for v in extract:
375
+ rest = _remove_one(rest, v)
376
+ target = rest + extract
377
+ flat_fact = _reorder_bridge(cfact, clist, target, csucc, emitter)
378
+ if extract:
379
+ # This rule's Isabelle premise pattern needs "G @ [extracted...]" —
380
+ # reshape into that explicit decomposition.
381
+ premise_fact = _decompose(flat_fact, target, [rest, extract], csucc, emitter)
382
+ else:
383
+ # 1L / !W: the premise pattern is bare "G" (no tail at all — the new
384
+ # formula is added only in the CONCLUSION) — flat_fact (== rest,
385
+ # nothing was extracted) is already exactly that; no reshape needed.
386
+ premise_fact = flat_fact
387
+
388
+ concl_list = rest + tail
389
+ concl_stmt = f'({_flat_ill(rest)} @ {_flat_ill(tail)}) {succ_isa}'
390
+ isa_rule = _ILL_RULE_TO_ISA[rule]
391
+ hraw = emitter.have(f'derivable {concl_stmt}', [premise_fact], f"(rule {isa_rule})")
392
+ # hraw's antecedent is the DECOMPOSED "rest @ tail" (or, for LImpR/⊸R
393
+ # where tail == [], "rest @ []"); flatten to the plain bracket-list form
394
+ # every _emit_ill caller expects (a pure ground computation, always
395
+ # simp-provable — never a reorder).
396
+ hflat = emitter.have(f'derivable {_flat_ill(concl_list)} {succ_isa}', [hraw], "simp")
397
+ return hflat, concl_list
398
+
399
+ # --- BangP (its own shape: image over the SAME antecedent) --------------
400
+ if rule == "!P":
401
+ child = d.premises[0]
402
+ cfact, clist = _emit_ill(child, emitter)
403
+ csucc = _lift_ill(child.conclusion.succedent)
404
+ g0 = [f.formula for f in clist] # every element of clist is OfCourse(...)
405
+ map_text = f"map IBang {_flat_ill(g0)}"
406
+ h_mapshape = emitter.have(f'derivable ({map_text}) {csucc}', [cfact], "simp")
407
+ hraw = emitter.have(f'derivable ({map_text}) {succ_isa}', [h_mapshape],
408
+ "(rule BangP)")
409
+ hflat = emitter.have(f'derivable {_flat_ill(clist)} {succ_isa}', [hraw], "simp")
410
+ return hflat, list(clist)
411
+
412
+ # --- two-premise rules ----------------------------------------------------
413
+ if rule == "⊗R":
414
+ c0, c1 = d.premises
415
+ f0, l0 = _emit_ill(c0, emitter)
416
+ f1, l1 = _emit_ill(c1, emitter)
417
+ target = l0 + l1
418
+ stmt = f'({_flat_ill(l0)} @ {_flat_ill(l1)}) {succ_isa}'
419
+ hraw = emitter.have(f'derivable {stmt}', [f0, f1], "(rule TensorR)")
420
+ hflat = emitter.have(f'derivable {_flat_ill(target)} {succ_isa}', [hraw], "simp")
421
+ return hflat, target
422
+
423
+ if rule == "&R":
424
+ c0, c1 = d.premises
425
+ f0, l0 = _emit_ill(c0, emitter)
426
+ f1, l1 = _emit_ill(c1, emitter)
427
+ csucc1 = _lift_ill(c1.conclusion.succedent)
428
+ f1m = _reorder_bridge(f1, l1, l0, csucc1, emitter)
429
+ stmt = f'derivable {_flat_ill(l0)} {succ_isa}'
430
+ hraw = emitter.have(stmt, [f0, f1m], "(rule WithR)")
431
+ return hraw, l0
432
+
433
+ if rule in ("⊕R1", "⊕R2"):
434
+ child = d.premises[0]
435
+ cfact, clist = _emit_ill(child, emitter)
436
+ stmt = f'derivable {_flat_ill(clist)} {succ_isa}'
437
+ isa_rule = "OPlusR1" if rule == "⊕R1" else "OPlusR2"
438
+ hraw = emitter.have(stmt, [cfact], f"(rule {isa_rule})")
439
+ return hraw, list(clist)
440
+
441
+ if rule == "⊸L":
442
+ c0, c1 = d.premises # c0: Γ⊢A ; c1: Δ,B⊢C
443
+ f0, l0 = _emit_ill(c0, emitter)
444
+ f1, l1 = _emit_ill(c1, emitter)
445
+ csucc1 = _lift_ill(c1.conclusion.succedent)
446
+ b = _the_one(_counter_diff(l1, d.conclusion.antecedent))
447
+ d1 = list(l1)
448
+ d1 = _remove_one(d1, b)
449
+ target1 = d1 + [b]
450
+ flat1 = _reorder_bridge(f1, l1, target1, csucc1, emitter)
451
+ premise1 = _decompose(flat1, target1, [d1, [b]], csucc1, emitter)
452
+ compound = _the_one(_counter_diff(d.conclusion.antecedent, l0 + d1))
453
+ concl_list = l0 + d1 + [compound]
454
+ stmt = (f'derivable ({_flat_ill(l0)} @ {_flat_ill(d1)} @ {_flat_ill([compound])}) '
455
+ f'{succ_isa}')
456
+ hraw = emitter.have(stmt, [f0, premise1], "(rule LImpL)")
457
+ hflat = emitter.have(f'derivable {_flat_ill(concl_list)} {succ_isa}', [hraw], "simp")
458
+ return hflat, concl_list
459
+
460
+ if rule == "⊕L":
461
+ c0, c1 = d.premises # c0: Γ,A⊢C ; c1: Γ,B⊢C
462
+ f0, l0 = _emit_ill(c0, emitter)
463
+ f1, l1 = _emit_ill(c1, emitter)
464
+ csucc0 = _lift_ill(c0.conclusion.succedent)
465
+ csucc1 = _lift_ill(c1.conclusion.succedent)
466
+ a = _the_one(_counter_diff(l0, d.conclusion.antecedent))
467
+ b = _the_one(_counter_diff(l1, d.conclusion.antecedent))
468
+ rest0 = _remove_one(list(l0), a)
469
+ rest1 = _remove_one(list(l1), b)
470
+ target0 = rest0 + [a]
471
+ flat0 = _reorder_bridge(f0, l0, target0, csucc0, emitter)
472
+ premise0 = _decompose(flat0, target0, [rest0, [a]], csucc0, emitter)
473
+ # Reconcile rest1 (proving Γ,B⊢C) to rest0's chosen order for Γ.
474
+ target1 = rest0 + [b]
475
+ flat1_native = rest1 + [b]
476
+ flat1 = _reorder_bridge(f1, l1, flat1_native, csucc1, emitter)
477
+ flat1 = _reorder_bridge(flat1, flat1_native, target1, csucc1, emitter)
478
+ premise1 = _decompose(flat1, target1, [rest0, [b]], csucc1, emitter)
479
+ compound = _the_one(_counter_diff(d.conclusion.antecedent, rest0))
480
+ concl_list = rest0 + [compound]
481
+ stmt = f'derivable ({_flat_ill(rest0)} @ {_flat_ill([compound])}) {succ_isa}'
482
+ hraw = emitter.have(stmt, [premise0, premise1], "(rule OPlusL)")
483
+ hflat = emitter.have(f'derivable {_flat_ill(concl_list)} {succ_isa}', [hraw], "simp")
484
+ return hflat, concl_list
485
+
486
+ raise NotImplementedError(f"to_isabelle_ill: unsupported derivation rule {rule!r}.")
487
+
488
+
489
+ # ---------------------------------------------------------------------------
490
+ # ILL public API.
491
+ # ---------------------------------------------------------------------------
492
+
493
+ def ill_derivation_theory(derivation: ILLDerivation,
494
+ theory_name: str = "ILLDerivation") -> str:
495
+ """Emit a complete, loadable Isabelle/HOL theory replaying an already-found
496
+ (and checker-validated) :class:`~unicode_logic_kit.atp.linear.ILLDerivation`.
497
+
498
+ The returned theory: declares the deep-embedded ``ill`` datatype, defines
499
+ ``derivable`` as an ``inductive`` predicate with one intro rule per rule of
500
+ the Python calculus PLUS ``Exch`` (see the module docstring), states the
501
+ derivation's end-sequent as ``lemma ill_goal`` with its antecedent in the
502
+ ORIGINAL order recorded on ``derivation.conclusion`` (a final ``Exch``
503
+ chain re-orders the replay's own working order to match it), and proves it
504
+ by replaying ``derivation`` node-for-node — one ``have`` per tree node,
505
+ closed by exactly the rule that node used.
506
+
507
+ Raises:
508
+ ValueError: if ``derivation`` does not check
509
+ (:func:`~unicode_logic_kit.atp.linear.check_ill_proof`) — there is
510
+ nothing sound to replay.
511
+ """
512
+ if not check_ill_proof(derivation):
513
+ raise ValueError(
514
+ "ill_derivation_theory: the given ILLDerivation does not check "
515
+ "(unicode_logic_kit.check_ill_proof / verify_ill_proof rejects it) "
516
+ "— use ill_prove/ill_derivable to find a valid derivation first.")
517
+
518
+ emitter = _Emitter()
519
+ root_fact, root_list = _emit_ill(derivation, emitter)
520
+ wanted = list(derivation.conclusion.antecedent)
521
+ succ_isa = _lift_ill(derivation.conclusion.succedent)
522
+ final_fact = _reorder_bridge(root_fact, root_list, wanted, succ_isa, emitter)
523
+
524
+ lines: List[str] = []
525
+ lines.append(f"theory {theory_name}")
526
+ lines.append(" imports Main")
527
+ lines.append("begin")
528
+ lines.append("")
529
+ lines.append("(* Deep embedding + derivation REPLAY of an ILL sequent. The toolkit's own")
530
+ lines.append(" cut-free search (unicode_logic_kit.ill_prove) already found this proof; this")
531
+ lines.append(" theory transcribes it into Isabelle, one intro rule per tree node -- no")
532
+ lines.append(" automation searches for the proof. See the module docstring of")
533
+ lines.append(" unicode_logic_kit.hol.isabelle_substructural for why the antecedent is an")
534
+ lines.append(" \"ill list\" with an explicit Exch rule rather than \"ill multiset\". *)")
535
+ lines.append(f"(* Sequent: {derivation.conclusion} *)")
536
+ lines.append("")
537
+ lines.append(ILL_DATATYPE)
538
+ lines.append("")
539
+ lines.append("inductive derivable :: \"ill list \\<Rightarrow> ill \\<Rightarrow> bool\" where")
540
+ for i, (name, body) in enumerate(_ILL_INTROS):
541
+ prefix = " " if i == 0 else "| "
542
+ lines.append(f"{prefix}{name}: \"{body}\"")
543
+ lines.append("")
544
+ lines.append(f'lemma ill_goal: "derivable {_flat_ill(wanted)} {succ_isa}"')
545
+ lines.append("proof -")
546
+ lines.extend(emitter.lines)
547
+ lines.append(f" show ?thesis using {final_fact} by simp")
548
+ lines.append("qed")
549
+ lines.append("")
550
+ lines.append("end")
551
+ return "\n".join(lines) + "\n"
552
+
553
+
554
+ def to_isabelle_ill(premises: Iterable[Node], goal: Node,
555
+ theory_name: str = "ILLDerivation",
556
+ max_depth: Optional[int] = None,
557
+ max_steps: int = 200000) -> str:
558
+ """Run :func:`~unicode_logic_kit.atp.linear.ill_prove` on ``premises ⊢ goal`` and,
559
+ if derivable, emit a complete Isabelle/HOL theory that replays the proof
560
+ (see :func:`ill_derivation_theory`).
561
+
562
+ Raises:
563
+ ValueError: if the sequent is not (found) derivable — an underivable
564
+ sequent has no derivation to replay; use ``ill_prove`` /
565
+ ``ill_derivable`` directly first to investigate why (for a
566
+ ``!``-sequent, ``None`` may mean only "not found within the
567
+ default search bound" — see :func:`~unicode_logic_kit.atp.linear.ill_prove`).
568
+ """
569
+ ants = list(premises)
570
+ derivation = ill_prove(ants, goal, max_depth=max_depth, max_steps=max_steps)
571
+ if derivation is None:
572
+ raise ValueError(
573
+ f"to_isabelle_ill: {ILLSequent(tuple(ants), goal)} was not found "
574
+ "derivable by ill_prove — there is no derivation to replay. Call "
575
+ "unicode_logic_kit.ill_prove / ill_derivable first (with an explicit "
576
+ "max_depth for a !-sequent if needed) to confirm derivability.")
577
+ return ill_derivation_theory(derivation, theory_name=theory_name)
578
+
579
+
580
+ # =============================================================================
581
+ # Lambek calculus L: deep embedding.
582
+ # =============================================================================
583
+ #
584
+ # L's antecedent is genuinely ORDERED (no exchange), so "lam list" is the
585
+ # literally correct type — no Exch-style device is needed here, and the
586
+ # replay never reorders anything: every list constructed below is either the
587
+ # direct concatenation of already-known sub-lists, or a known list split at a
588
+ # known index (found by comparing a conclusion's antecedent tuple against its
589
+ # premises' — see _find_split_left / _find_split_right), so the only "bridge"
590
+ # ever needed is a pure "@"-re-association, always closed by a trivial,
591
+ # fully-ground `simp` (never a permutation).
592
+
593
+ LAMBEK_DATATYPE = "datatype lam = LAtom string | LProduct lam lam | LUnder lam lam | LOver lam lam"
594
+
595
+ # One entry per Isabelle intro rule; 7 total, 1:1 with atp.lambek's 7 rules
596
+ # (Ax, •L, •R, \L, \R, /L, /R — see _LAMBEK_RULE_TO_ISA). Unlike ILL, nothing
597
+ # extra is added: L's list type needs no structural-rule stand-in.
598
+ _LAMBEK_INTROS: List[Tuple[str, str]] = [
599
+ ("Ax", 'derivable [A] A'),
600
+ ("ProdL", 'derivable (G1 @ [A, B] @ G2) C \\<Longrightarrow> derivable (G1 @ [LProduct A B] @ G2) C'),
601
+ ("ProdR", 'derivable G A \\<Longrightarrow> derivable D B \\<Longrightarrow> derivable (G @ D) (LProduct A B)'),
602
+ ("UnderL", 'G \\<noteq> [] \\<Longrightarrow> derivable G A \\<Longrightarrow> derivable (D1 @ [B] @ D2) C \\<Longrightarrow> '
603
+ 'derivable (D1 @ G @ [LUnder A B] @ D2) C'),
604
+ ("UnderR", 'G \\<noteq> [] \\<Longrightarrow> derivable (A # G) B \\<Longrightarrow> derivable G (LUnder A B)'),
605
+ ("OverL", 'G \\<noteq> [] \\<Longrightarrow> derivable G A \\<Longrightarrow> derivable (D1 @ [B] @ D2) C \\<Longrightarrow> '
606
+ 'derivable (D1 @ [LOver B A] @ G @ D2) C'),
607
+ ("OverR", 'G \\<noteq> [] \\<Longrightarrow> derivable (G @ [A]) B \\<Longrightarrow> derivable G (LOver B A)'),
608
+ ]
609
+
610
+ _LAMBEK_RULE_TO_ISA = {
611
+ "Ax": "Ax", "•L": "ProdL", "•R": "ProdR",
612
+ "\\L": "UnderL", "\\R": "UnderR", "/L": "OverL", "/R": "OverR",
613
+ }
614
+
615
+
616
+ def _lift_lambek(node: Node) -> str:
617
+ """Lift a Lambek type/formula to its ``lam``-datatype Isabelle term."""
618
+ if isinstance(node, Atom):
619
+ refuse_truth_constants([node], "to_isabelle_lambek", _LAMBEK_NO_CONSTANT_WHY)
620
+ return f"(LAtom {_isa_str(key_text(node))})"
621
+ if isinstance(node, Product):
622
+ return f"(LProduct {_lift_lambek(node.left)} {_lift_lambek(node.right)})"
623
+ if isinstance(node, Under):
624
+ return f"(LUnder {_lift_lambek(node.left)} {_lift_lambek(node.right)})"
625
+ if isinstance(node, Over):
626
+ return f"(LOver {_lift_lambek(node.left)} {_lift_lambek(node.right)})"
627
+ raise NotImplementedError(f"to_isabelle_lambek: unsupported node {type(node).__name__}.")
628
+
629
+
630
+ def _flat_lambek(nodes: Sequence[Node]) -> str:
631
+ """Bracket-list Isabelle text for a list of Lambek types, e.g. ``[A, B]``."""
632
+ if not nodes:
633
+ return "[]"
634
+ return "[" + ", ".join(_lift_lambek(n) for n in nodes) + "]"
635
+
636
+
637
+ # ---------------------------------------------------------------------------
638
+ # Lambek replay engine.
639
+ # ---------------------------------------------------------------------------
640
+ #
641
+ # Contract of _emit_lambek(d, emitter) -> fact_name: fact_name names a fact,
642
+ # already emitted into emitter.lines, whose statement is EXACTLY
643
+ # "derivable <_flat_lambek(d.conclusion.antecedent)> <_lift_lambek(d.conclusion.succedent)>"
644
+ # — i.e. (unlike ILL) always in EXACTLY d.conclusion.antecedent's own order,
645
+ # since that order is already the semantically correct one and there is
646
+ # nothing to reorder to.
647
+
648
+ def _nonempty(flat_text: str, emitter: _Emitter) -> str:
649
+ """Emit (and return the fact name for) ``<flat_text> \\<noteq> []``.
650
+
651
+ Used for the ``Γ ≠ []`` side condition of UnderL/UnderR/OverL/OverR
652
+ (Lambek's restriction). ``flat_text`` is always a concrete, nonempty
653
+ bracket-list literal here (guaranteed by
654
+ :func:`~unicode_logic_kit.atp.lambek.check_lambek_proof`, which every
655
+ derivation this module replays has already passed), so this is always a
656
+ trivial, purely computational ``simp``.
657
+ """
658
+ return emitter.have(f'{flat_text} \\<noteq> []', [], "simp")
659
+
660
+
661
+ def _lcp_len(a: Sequence, b: Sequence) -> int:
662
+ """Length of the longest common prefix of ``a`` and ``b``."""
663
+ n = 0
664
+ while n < len(a) and n < len(b) and a[n] == b[n]:
665
+ n += 1
666
+ return n
667
+
668
+
669
+ def _emit_lambek(d: LambekDerivation, emitter: _Emitter) -> str:
670
+ """Recursively replay one Lambek derivation node; see the contract above."""
671
+ concl_list = list(d.conclusion.antecedent)
672
+ succ_isa = _lift_lambek(d.conclusion.succedent)
673
+ rule = d.rule
674
+
675
+ if rule == "Ax":
676
+ return emitter.have(f'derivable {_flat_lambek(concl_list)} {succ_isa}', [], "(rule Ax)")
677
+
678
+ if rule == "•L":
679
+ child = d.premises[0]
680
+ cfact = _emit_lambek(child, emitter)
681
+ clist = list(child.conclusion.antecedent)
682
+ i = _lcp_len(clist, concl_list) # concl_list[i] is the Product; clist[i:i+2] = (A, B)
683
+ g1, g2 = concl_list[:i], concl_list[i + 1:]
684
+ a, b = clist[i], clist[i + 1]
685
+ decomp = f"{_flat_lambek(g1)} @ {_flat_lambek([a, b])} @ {_flat_lambek(g2)}"
686
+ premise = emitter.have(f'derivable ({decomp}) {succ_isa}', [cfact], "simp")
687
+ concl_stmt = (f"derivable ({_flat_lambek(g1)} @ {_flat_lambek([concl_list[i]])} @ "
688
+ f"{_flat_lambek(g2)}) {succ_isa}")
689
+ hraw = emitter.have(concl_stmt, [premise], "(rule ProdL)")
690
+ return emitter.have(f'derivable {_flat_lambek(concl_list)} {succ_isa}', [hraw], "simp")
691
+
692
+ if rule == "•R":
693
+ c0, c1 = d.premises
694
+ f0 = _emit_lambek(c0, emitter)
695
+ f1 = _emit_lambek(c1, emitter)
696
+ l0, l1 = list(c0.conclusion.antecedent), list(c1.conclusion.antecedent)
697
+ stmt = f'derivable ({_flat_lambek(l0)} @ {_flat_lambek(l1)}) {succ_isa}'
698
+ hraw = emitter.have(stmt, [f0, f1], "(rule ProdR)")
699
+ return emitter.have(f'derivable {_flat_lambek(concl_list)} {succ_isa}', [hraw], "simp")
700
+
701
+ if rule == "\\R":
702
+ child = d.premises[0]
703
+ cfact = _emit_lambek(child, emitter)
704
+ csucc = _lift_lambek(child.conclusion.succedent) # the INNER succedent B, not A\B
705
+ clist = list(child.conclusion.antecedent) # clist == [A] + concl_list
706
+ a = clist[0]
707
+ stmt = f'derivable ({_flat_lambek([a])} @ {_flat_lambek(concl_list)}) {csucc}'
708
+ premise = emitter.have(stmt, [cfact], "simp")
709
+ nonempty = _nonempty(_flat_lambek(concl_list), emitter)
710
+ concl_stmt = f'derivable {_flat_lambek(concl_list)} {succ_isa}'
711
+ return emitter.have(concl_stmt, [nonempty, premise], "(rule UnderR)")
712
+
713
+ if rule == "/R":
714
+ child = d.premises[0]
715
+ cfact = _emit_lambek(child, emitter)
716
+ csucc = _lift_lambek(child.conclusion.succedent) # the INNER succedent B, not B/A
717
+ clist = list(child.conclusion.antecedent) # clist == concl_list + [A]
718
+ a = clist[-1]
719
+ stmt = f'derivable ({_flat_lambek(concl_list)} @ {_flat_lambek([a])}) {csucc}'
720
+ premise = emitter.have(stmt, [cfact], "simp")
721
+ nonempty = _nonempty(_flat_lambek(concl_list), emitter)
722
+ concl_stmt = f'derivable {_flat_lambek(concl_list)} {succ_isa}'
723
+ return emitter.have(concl_stmt, [nonempty, premise], "(rule OverR)")
724
+
725
+ if rule == "\\L":
726
+ p_arg, p_main = d.premises
727
+ f_arg = _emit_lambek(p_arg, emitter)
728
+ f_main = _emit_lambek(p_main, emitter)
729
+ gamma = list(p_arg.conclusion.antecedent)
730
+ main_list = list(p_main.conclusion.antecedent) # D1 + [B] + D2
731
+ d1, d2, compound = _find_split_under(concl_list, main_list, gamma,
732
+ p_arg.conclusion.succedent)
733
+ main_stmt = (f'derivable ({_flat_lambek(d1)} @ {_flat_lambek([compound.right])} @ '
734
+ f'{_flat_lambek(d2)}) {succ_isa}')
735
+ main_shaped = emitter.have(main_stmt, [f_main], "simp")
736
+ nonempty = _nonempty(_flat_lambek(gamma), emitter)
737
+ concl_stmt = (f'derivable ({_flat_lambek(d1)} @ {_flat_lambek(gamma)} @ '
738
+ f'{_flat_lambek([compound])} @ {_flat_lambek(d2)}) {succ_isa}')
739
+ hraw = emitter.have(concl_stmt, [nonempty, f_arg, main_shaped], "(rule UnderL)")
740
+ return emitter.have(f'derivable {_flat_lambek(concl_list)} {succ_isa}', [hraw], "simp")
741
+
742
+ if rule == "/L":
743
+ p_arg, p_main = d.premises
744
+ f_arg = _emit_lambek(p_arg, emitter)
745
+ f_main = _emit_lambek(p_main, emitter)
746
+ gamma = list(p_arg.conclusion.antecedent)
747
+ main_list = list(p_main.conclusion.antecedent) # D1 + [B] + D2
748
+ d1, d2, compound = _find_split_over(concl_list, main_list, gamma,
749
+ p_arg.conclusion.succedent)
750
+ main_stmt = (f'derivable ({_flat_lambek(d1)} @ {_flat_lambek([compound.left])} @ '
751
+ f'{_flat_lambek(d2)}) {succ_isa}')
752
+ main_shaped = emitter.have(main_stmt, [f_main], "simp")
753
+ nonempty = _nonempty(_flat_lambek(gamma), emitter)
754
+ concl_stmt = (f'derivable ({_flat_lambek(d1)} @ {_flat_lambek([compound])} @ '
755
+ f'{_flat_lambek(gamma)} @ {_flat_lambek(d2)}) {succ_isa}')
756
+ hraw = emitter.have(concl_stmt, [nonempty, f_arg, main_shaped], "(rule OverL)")
757
+ return emitter.have(f'derivable {_flat_lambek(concl_list)} {succ_isa}', [hraw], "simp")
758
+
759
+ raise NotImplementedError(f"to_isabelle_lambek: unsupported derivation rule {rule!r}.")
760
+
761
+
762
+ def _find_split_under(concl_list: List[Node], main_list: List[Node], gamma: List[Node],
763
+ a_formula: Node) -> Tuple[List[Node], List[Node], Under]:
764
+ """``\\L``: find ``(D1, D2, compound)`` with ``concl_list == D1+gamma+[compound]+D2``,
765
+ ``main_list == D1+[compound.right]+D2``, ``compound`` an ``Under`` with
766
+ ``compound.left == a_formula``. Exists and is unique for a valid derivation."""
767
+ n = len(gamma)
768
+ for k in range(0, len(concl_list) - n):
769
+ if concl_list[k:k + n] != gamma:
770
+ continue
771
+ pos = k + n
772
+ if pos >= len(concl_list):
773
+ continue
774
+ compound = concl_list[pos]
775
+ if not isinstance(compound, Under) or compound.left != a_formula:
776
+ continue
777
+ d1, d2 = concl_list[:k], concl_list[pos + 1:]
778
+ if main_list == d1 + [compound.right] + d2:
779
+ return list(d1), list(d2), compound
780
+ raise AssertionError("internal error: no consistent \\L split found")
781
+
782
+
783
+ def _find_split_over(concl_list: List[Node], main_list: List[Node], gamma: List[Node],
784
+ a_formula: Node) -> Tuple[List[Node], List[Node], Over]:
785
+ """``/L``: find ``(D1, D2, compound)`` with ``concl_list == D1+[compound]+gamma+D2``,
786
+ ``main_list == D1+[compound.left]+D2``, ``compound`` an ``Over`` with
787
+ ``compound.right == a_formula``. Exists and is unique for a valid derivation."""
788
+ n = len(gamma)
789
+ for k in range(0, len(concl_list)):
790
+ compound = concl_list[k]
791
+ if not isinstance(compound, Over) or compound.right != a_formula:
792
+ continue
793
+ gstart = k + 1
794
+ if concl_list[gstart:gstart + n] != gamma:
795
+ continue
796
+ d1, d2 = concl_list[:k], concl_list[gstart + n:]
797
+ if main_list == d1 + [compound.left] + d2:
798
+ return list(d1), list(d2), compound
799
+ raise AssertionError("internal error: no consistent /L split found")
800
+
801
+
802
+ # ---------------------------------------------------------------------------
803
+ # Lambek public API.
804
+ # ---------------------------------------------------------------------------
805
+
806
+ def lambek_derivation_theory(derivation: LambekDerivation,
807
+ theory_name: str = "LambekDerivation") -> str:
808
+ """Emit a complete, loadable Isabelle/HOL theory replaying an already-found
809
+ (and checker-validated) :class:`~unicode_logic_kit.atp.lambek.LambekDerivation`.
810
+
811
+ Same shape as :func:`ill_derivation_theory`: deep-embeds the Lambek type
812
+ grammar as ``datatype lam``, defines ``derivable :: "lam list ⇒ lam ⇒ bool"``
813
+ inductively — the **ordered** antecedent (a plain ``list``, no exchange
814
+ rule) is the entire point of the Lambek calculus, unlike ILL's list+Exch
815
+ workaround (see the module docstring) — and replays ``derivation`` node by
816
+ node.
817
+
818
+ Raises:
819
+ ValueError: if ``derivation`` does not check
820
+ (:func:`~unicode_logic_kit.atp.lambek.check_lambek_proof`).
821
+ """
822
+ if not check_lambek_proof(derivation):
823
+ raise ValueError(
824
+ "lambek_derivation_theory: the given LambekDerivation does not "
825
+ "check (unicode_logic_kit.check_lambek_proof / verify_lambek_proof "
826
+ "rejects it) — use lambek_prove/lambek_derivable to find a valid "
827
+ "derivation first.")
828
+
829
+ emitter = _Emitter()
830
+ root_fact = _emit_lambek(derivation, emitter)
831
+ wanted = list(derivation.conclusion.antecedent)
832
+ succ_isa = _lift_lambek(derivation.conclusion.succedent)
833
+
834
+ lines: List[str] = []
835
+ lines.append(f"theory {theory_name}")
836
+ lines.append(" imports Main")
837
+ lines.append("begin")
838
+ lines.append("")
839
+ lines.append("(* Deep embedding + derivation REPLAY of a Lambek-calculus (L) sequent. The")
840
+ lines.append(" toolkit's own decision procedure (unicode_logic_kit.lambek_prove) already")
841
+ lines.append(" found this proof; this theory transcribes it into Isabelle, one intro rule")
842
+ lines.append(" per tree node -- no automation searches for the proof. The antecedent is a")
843
+ lines.append(" plain \"lam list\": order is the whole point of L, so unlike the ILL export")
844
+ lines.append(" there is no exchange rule at all. *)")
845
+ lines.append(f"(* Sequent: {derivation.conclusion} *)")
846
+ lines.append("")
847
+ lines.append(LAMBEK_DATATYPE)
848
+ lines.append("")
849
+ lines.append("inductive derivable :: \"lam list \\<Rightarrow> lam \\<Rightarrow> bool\" where")
850
+ for i, (name, body) in enumerate(_LAMBEK_INTROS):
851
+ prefix = " " if i == 0 else "| "
852
+ lines.append(f"{prefix}{name}: \"{body}\"")
853
+ lines.append("")
854
+ lines.append(f'lemma lambek_goal: "derivable {_flat_lambek(wanted)} {succ_isa}"')
855
+ lines.append("proof -")
856
+ lines.extend(emitter.lines)
857
+ lines.append(f" show ?thesis using {root_fact} by simp")
858
+ lines.append("qed")
859
+ lines.append("")
860
+ lines.append("end")
861
+ return "\n".join(lines) + "\n"
862
+
863
+
864
+ def to_isabelle_lambek(sequence: Iterable[Node], goal: Node,
865
+ theory_name: str = "LambekDerivation") -> str:
866
+ """Run :func:`~unicode_logic_kit.atp.lambek.lambek_prove` on ``sequence ⊢ goal`` and,
867
+ if derivable, emit a complete Isabelle/HOL theory that replays the proof
868
+ (see :func:`lambek_derivation_theory`).
869
+
870
+ Raises:
871
+ ValueError: if the sequent is not derivable in L (a decision
872
+ procedure, so this is a genuine refutation — see
873
+ :func:`~unicode_logic_kit.atp.lambek.lambek_prove`).
874
+ ValueError: (propagated from ``lambek_prove``) if ``sequence`` is empty
875
+ — L requires a nonempty antecedent.
876
+ """
877
+ seq = list(sequence)
878
+ derivation = lambek_prove(seq, goal)
879
+ if derivation is None:
880
+ raise ValueError(
881
+ f"to_isabelle_lambek: {LambekSequent(tuple(seq), goal)} is not "
882
+ "derivable in L (lambek_prove is a decision procedure — this is "
883
+ "a genuine refutation) — there is no derivation to replay.")
884
+ return lambek_derivation_theory(derivation, theory_name=theory_name)