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,520 @@
1
+ """Command-line interface for unicode-logic-kit.
2
+
3
+ Two ways to run this module, dispatched purely on ``argv[0]``:
4
+
5
+ **Legacy mode** (unchanged since Tier 0) — parse a single formula and render
6
+ it in one output format::
7
+
8
+ python -m unicode_logic_kit "∀x P(x)" --to latex
9
+
10
+ The ``--mode`` flag selects the parser dialect (which maps onto the
11
+ constructor flags of :class:`MSFLParser`, table-driven via ``_MODE_KWARGS``);
12
+ ``--to`` selects the rendering applied to the parsed AST. A formula that does
13
+ not parse prints its message to stderr and exits 1; a formula that parses but
14
+ that the chosen rendering REFUSES (``NotImplementedError`` / ``ValueError`` from
15
+ the renderer: a modal operator has no TPTP form, two distinct names would be
16
+ written as one TPTP word, a constant Prover9 would read as a variable, a
17
+ non-finite number) is reported exactly like the same failure of a subcommand:
18
+ one message on stderr, exit 3, no traceback.
19
+
20
+ **Subcommand mode** — a 1:1 mirror of :mod:`unicode_logic_kit.api`'s verbs onto
21
+ the command line, for every ``argv[0]`` in
22
+ ``{check, equiv, prove, countermodel, repair, translate}``::
23
+
24
+ python -m unicode_logic_kit check "∀x P(x)"
25
+ python -m unicode_logic_kit equiv "P → Q" "¬P ∨ Q"
26
+ python -m unicode_logic_kit prove "Q" --premise "P" --premise "P → Q"
27
+ python -m unicode_logic_kit countermodel "∀x P(x)"
28
+ python -m unicode_logic_kit repair "∀x (P(x)"
29
+ python -m unicode_logic_kit translate "□P" --from modal --to-logic fol
30
+
31
+ Dispatch rule: if ``argv`` is non-empty and ``argv[0]`` names one of the six
32
+ subcommands above, the rest of ``argv`` is parsed by that subcommand's own
33
+ parser; otherwise ``argv`` goes to the legacy single-formula parser
34
+ unchanged. Because the legacy grammar's first positional is the formula
35
+ itself, a formula that happens to be spelled exactly ``"check"`` (etc.) would
36
+ be misrouted — an acceptable, documented edge case shared with any CLI that
37
+ grows subcommands after a positional-argument v1.
38
+
39
+ Every subcommand accepts three common flags: ``--dialect`` (the
40
+ :func:`~unicode_logic_kit.api.parse_any` dialect hint; omitted means
41
+ auto-detect), ``--json`` (machine-readable ``to_dict()`` JSON instead of the
42
+ default human-readable summary), and ``--timeout`` (milliseconds, forwarded
43
+ to whichever backend/solver call the subcommand makes; accepted uniformly
44
+ even by subcommands with nothing to forward it to).
45
+
46
+ Error culture: a malformed FORMULA argument to ``check``/``repair`` is not an
47
+ error — that is the diagnosis those two verbs exist to report (``ok=False``,
48
+ exit 1). For every other subcommand, a formula that :func:`api.parse_any`
49
+ cannot parse, an unknown/unavailable backend
50
+ (:class:`~unicode_logic_kit.atp.protocol.BackendUnavailable`), an unsupported
51
+ translation fragment (``NotImplementedError``), or a bad ``--signature``
52
+ JSON file all print one clean message to stderr and exit 3 — never a Python
53
+ traceback. The legacy path treats a rendering that refuses its formula the same
54
+ way (see above).
55
+ """
56
+
57
+ import argparse
58
+ import json
59
+ import sys
60
+
61
+ from .fol.msflparser import MSFLParser
62
+ from .fol.naming import NamingError, ParsingError
63
+ from . import api
64
+ from .atp.protocol import BackendUnavailable
65
+
66
+
67
+ # Map each --mode value to the MSFLParser constructor kwargs it selects. Every
68
+ # entry here must be a legal (non-conflicting) combination on its own — see
69
+ # MSFLParser's own mutual-exclusivity rules (module docstring / __init__).
70
+ # Reused (read-only) by the subcommand dispatcher below: its keys double as
71
+ # the set of dialect-hint names that :func:`~unicode_logic_kit.api.parse_any`
72
+ # also accepts verbatim (see ``_dialect_hint_for_source``).
73
+ _MODE_KWARGS = {
74
+ "fol": {},
75
+ "msfol": {"many_sorted": True},
76
+ "msfl": {"many_sorted": True, "fuzzy": True},
77
+ "fl": {"fuzzy": True},
78
+ "modal": {"modal": True},
79
+ "second_order": {"second_order": True},
80
+ "dependence": {"dependence": True},
81
+ "linear": {"linear": True},
82
+ "lambek": {"lambek": True},
83
+ }
84
+
85
+
86
+ def _render(node, fmt: str) -> str:
87
+ """Render a parsed AST node in the requested output format.
88
+
89
+ The mapping mirrors the rendering methods defined on the Node base class;
90
+ ``json`` emits the versioned envelope of
91
+ :func:`unicode_logic_kit.fol.serialize.serialize` (``schema_version`` + the
92
+ ``to_dict()`` tree under ``root``) via :func:`json.dumps`.
93
+ """
94
+ if fmt == "tree":
95
+ return node.tree_str()
96
+ if fmt == "unicode":
97
+ return node.to_unicode_str()
98
+ if fmt == "latex":
99
+ return node.to_latex()
100
+ if fmt == "tptp":
101
+ return node.to_tptp()
102
+ if fmt == "prover9":
103
+ return node.to_prover9()
104
+ if fmt == "smtlib":
105
+ return node.to_smtlib()
106
+ if fmt == "json":
107
+ from .fol.serialize import serialize
108
+ return json.dumps(serialize(node))
109
+ if fmt == "dot":
110
+ return node.to_dot()
111
+ # argparse restricts choices, so this is unreachable in normal use.
112
+ raise ValueError(f"Unknown output format: {fmt}")
113
+
114
+
115
+ def _build_parser() -> argparse.ArgumentParser:
116
+ """Construct the argparse parser for the legacy single-formula CLI."""
117
+ parser = argparse.ArgumentParser(
118
+ prog="unicode_logic_kit",
119
+ description="Parse and render a Unicode first-order logic formula.",
120
+ )
121
+ parser.add_argument(
122
+ "formula",
123
+ metavar="FORMULA",
124
+ help="the formula string to parse (e.g. \"∀x P(x)\")",
125
+ )
126
+ parser.add_argument(
127
+ "--mode",
128
+ choices=sorted(_MODE_KWARGS),
129
+ default="fol",
130
+ help=(
131
+ "parser dialect: fol (default), msfol, msfl, fl, modal, "
132
+ "second_order, dependence, linear, or lambek"
133
+ ),
134
+ )
135
+ parser.add_argument(
136
+ "--to",
137
+ dest="to",
138
+ choices=["tree", "unicode", "latex", "tptp", "prover9", "smtlib", "json", "dot"],
139
+ default="tree",
140
+ help="output rendering (default: tree)",
141
+ )
142
+ return parser
143
+
144
+
145
+ def _run_legacy(argv) -> int:
146
+ """The original (Tier 0) single-formula parse-and-render behaviour."""
147
+ parser = _build_parser()
148
+ args = parser.parse_args(argv)
149
+
150
+ formula_parser = MSFLParser(**_MODE_KWARGS[args.mode])
151
+
152
+ try:
153
+ ast = formula_parser.parse(args.formula)
154
+ except (NamingError, ParsingError) as exc:
155
+ print(str(exc), file=sys.stderr)
156
+ return 1
157
+
158
+ try:
159
+ text = _render(ast, args.to)
160
+ except (NotImplementedError, ValueError) as exc:
161
+ # A renderer that refuses the formula is an unsupported fragment, the
162
+ # same failure the subcommand path reports (message on stderr, exit 3).
163
+ print(f"--to {args.to}: {exc}", file=sys.stderr)
164
+ return 3
165
+ print(text)
166
+ return 0
167
+
168
+
169
+ # ---------------------------------------------------------------------------
170
+ # Subcommands: a 1:1 mirror of unicode_logic_kit.api onto the command line.
171
+ # ---------------------------------------------------------------------------
172
+
173
+ _SUBCOMMANDS = ("check", "equiv", "prove", "countermodel", "repair", "translate")
174
+
175
+ # api.translate's logic labels that do not coincide with a parse_any dialect
176
+ # hint of the same name (see _dialect_hint_for_source).
177
+ _LOGIC_TO_DIALECT_HINT = {"team": "dependence"}
178
+
179
+
180
+ def _add_common(parser: argparse.ArgumentParser) -> None:
181
+ """Add the three flags shared by every subcommand (see module docstring)."""
182
+ parser.add_argument(
183
+ "--dialect", default=None, metavar="DIALECT",
184
+ help="parse_any dialect hint (default: auto-detect)")
185
+ parser.add_argument(
186
+ "--json", dest="as_json", action="store_true",
187
+ help="machine-readable JSON output (default: human-readable summary)")
188
+ parser.add_argument(
189
+ "--timeout", type=int, default=10000, metavar="MS",
190
+ help="backend/solver timeout in milliseconds (default: 10000)")
191
+
192
+
193
+ def _parse_or_raise(text: str, dialect):
194
+ """Parse ``text`` via :func:`api.parse_any`, raising ``ValueError`` on failure.
195
+
196
+ Used by every subcommand that needs an actual :class:`Node` to hand to a
197
+ decision API (``equiv``/``prove``/``countermodel``/``translate``) —
198
+ unlike ``check``/``repair``, which treat a parse failure as their own
199
+ diagnosis rather than a hard error (see module docstring).
200
+ """
201
+ result = api.parse_any(text, hint=dialect)
202
+ if not result.ok:
203
+ message = (result.errors[-1]["message"] if result.errors
204
+ else f"no parser accepted {text!r}")
205
+ raise ValueError(f"could not parse {text!r}: {message}")
206
+ return result.formula
207
+
208
+
209
+ def _load_json_file(path: str) -> dict:
210
+ """Load a JSON object from ``path`` (used for ``--signature`` files)."""
211
+ with open(path, "r", encoding="utf-8") as fh:
212
+ return json.load(fh)
213
+
214
+
215
+ def _print_kv(pairs) -> None:
216
+ """Print ``key: value`` lines — the shared shape of every human summary."""
217
+ for key, value in pairs:
218
+ print(f"{key}: {value}")
219
+
220
+
221
+ # -- check -------------------------------------------------------------- #
222
+
223
+ def _cmd_check(argv) -> int:
224
+ parser = argparse.ArgumentParser(
225
+ prog="unicode_logic_kit check",
226
+ description="Well-formedness report for a formula, optionally against a signature.")
227
+ parser.add_argument("formula", metavar="FORMULA")
228
+ parser.add_argument(
229
+ "--signature", default=None, metavar="SIG_JSON",
230
+ help="path to a JSON signature spec (see api.check / api._signature_errors)")
231
+ _add_common(parser)
232
+ args = parser.parse_args(argv)
233
+
234
+ signature = _load_json_file(args.signature) if args.signature else None
235
+ result = api.check(args.formula, signature=signature, dialect=args.dialect)
236
+
237
+ if args.as_json:
238
+ print(json.dumps(result.to_dict()))
239
+ else:
240
+ _print_kv([
241
+ ("ok", result.ok),
242
+ ("parseable", result.parseable),
243
+ ("is_closed", result.is_closed),
244
+ ("arity_consistent", result.arity_consistent),
245
+ ("has_lambdas", result.has_lambdas),
246
+ ])
247
+ if result.free_variables:
248
+ print("free_variables: " + ", ".join(result.free_variables))
249
+ if result.arity_conflicts:
250
+ print("arity_conflicts: " + json.dumps(list(result.arity_conflicts)))
251
+ if result.signature_errors:
252
+ print("signature_errors: " + json.dumps(list(result.signature_errors)))
253
+ if result.error:
254
+ print(f"error: {result.error}")
255
+
256
+ return 0 if result.ok else 1
257
+
258
+
259
+ # -- equiv -------------------------------------------------------------- #
260
+
261
+ def _cmd_equiv(argv) -> int:
262
+ parser = argparse.ArgumentParser(
263
+ prog="unicode_logic_kit equiv",
264
+ description="Graded equivalence between two formulas (api.equivalent).")
265
+ parser.add_argument("formula1", metavar="FORMULA1")
266
+ parser.add_argument("formula2", metavar="FORMULA2")
267
+ parser.add_argument(
268
+ "--method", choices=["exact", "canonical", "predicate_align", "solver", "auto"],
269
+ default="auto", help="equivalence level (default: auto, cheapest-first ladder)")
270
+ _add_common(parser)
271
+ args = parser.parse_args(argv)
272
+
273
+ f1 = _parse_or_raise(args.formula1, args.dialect)
274
+ f2 = _parse_or_raise(args.formula2, args.dialect)
275
+
276
+ result = api.equivalent(f1, f2, method=args.method, timeout=args.timeout)
277
+
278
+ if args.as_json:
279
+ print(json.dumps(result.to_dict()))
280
+ else:
281
+ _print_kv([("equivalent", result.equivalent), ("method_used", result.method_used)])
282
+ if result.counterexample is not None:
283
+ print("counterexample: " + json.dumps(result.counterexample))
284
+
285
+ if result.equivalent is True:
286
+ return 0
287
+ if result.equivalent is False:
288
+ return 1
289
+ return 2
290
+
291
+
292
+ # -- prove -------------------------------------------------------------- #
293
+
294
+ def _cmd_prove(argv) -> int:
295
+ parser = argparse.ArgumentParser(
296
+ prog="unicode_logic_kit prove",
297
+ description="Decide premises |= formula over a chain of backends (api.prove).")
298
+ parser.add_argument("formula", metavar="FORMULA")
299
+ parser.add_argument(
300
+ "--premise", action="append", default=[], metavar="P",
301
+ help="a premise formula (repeatable)")
302
+ parser.add_argument(
303
+ "--backends", default=None, metavar="a,b,c",
304
+ help="comma-separated backend names (default: the logic's default chain)")
305
+ parser.add_argument(
306
+ "--logic", default="auto", metavar="LOGIC",
307
+ help="'auto' (default, routes on modal operators), 'fol', or 'modal'")
308
+ _add_common(parser)
309
+ args = parser.parse_args(argv)
310
+
311
+ formula = _parse_or_raise(args.formula, args.dialect)
312
+ premises = [_parse_or_raise(p, args.dialect) for p in args.premise]
313
+ backends = ([b.strip() for b in args.backends.split(",") if b.strip()]
314
+ if args.backends else None)
315
+
316
+ verdict = api.prove(formula, premises, logic=args.logic, backends=backends,
317
+ timeout=args.timeout)
318
+
319
+ if args.as_json:
320
+ print(json.dumps(verdict.to_dict()))
321
+ else:
322
+ _print_kv([
323
+ ("status", verdict.status),
324
+ ("backend", verdict.backend),
325
+ ("logic", verdict.logic),
326
+ ("szs", verdict.szs_status),
327
+ ("wall_time", verdict.wall_time),
328
+ ])
329
+ if verdict.reason:
330
+ print(f"reason: {verdict.reason}")
331
+ if verdict.detail:
332
+ print(f"detail: {verdict.detail}")
333
+
334
+ return {"proved": 0, "refuted": 1, "unknown": 2, "error": 3}[verdict.status]
335
+
336
+
337
+ # -- countermodel --------------------------------------------------------- #
338
+
339
+ def _cmd_countermodel(argv) -> int:
340
+ parser = argparse.ArgumentParser(
341
+ prog="unicode_logic_kit countermodel",
342
+ description="Search for a countermodel to premises |= formula (api.countermodel).")
343
+ parser.add_argument("formula", metavar="FORMULA")
344
+ parser.add_argument(
345
+ "--premise", action="append", default=[], metavar="P",
346
+ help="a premise formula (repeatable)")
347
+ _add_common(parser)
348
+ args = parser.parse_args(argv)
349
+
350
+ formula = _parse_or_raise(args.formula, args.dialect)
351
+ premises = [_parse_or_raise(p, args.dialect) for p in args.premise]
352
+
353
+ result = api.countermodel(formula, premises, timeout=args.timeout)
354
+
355
+ if args.as_json:
356
+ print(json.dumps(result.to_dict()))
357
+ else:
358
+ _print_kv([("found", result.found), ("backend", result.backend)])
359
+ if result.explanation_nl:
360
+ print(f"explanation: {result.explanation_nl}")
361
+ if result.model is not None:
362
+ print("model: " + json.dumps(result.model))
363
+
364
+ return 0 if result.found else 1
365
+
366
+
367
+ # -- repair --------------------------------------------------------------- #
368
+
369
+ def _cmd_repair(argv) -> int:
370
+ parser = argparse.ArgumentParser(
371
+ prog="unicode_logic_kit repair",
372
+ description="One diagnose round over raw formula text, no fixer (api.repair).")
373
+ parser.add_argument("text", metavar="TEXT")
374
+ parser.add_argument(
375
+ "--signature", default=None, metavar="SIG_JSON",
376
+ help="path to a JSON signature spec (see api.check / api._signature_errors)")
377
+ _add_common(parser)
378
+ args = parser.parse_args(argv)
379
+
380
+ signature = _load_json_file(args.signature) if args.signature else None
381
+ step = next(api.repair(args.text, dialect=args.dialect, signature=signature,
382
+ fixer=None, max_attempts=1))
383
+
384
+ if args.as_json:
385
+ print(json.dumps(step.to_dict()))
386
+ else:
387
+ _print_kv([("ok", step.ok), ("converged", step.converged)])
388
+ if step.suggestion:
389
+ print(f"suggestion: {step.suggestion}")
390
+ print("diagnostics: " + json.dumps(step.diagnostics))
391
+
392
+ return 0 if step.ok else 1
393
+
394
+
395
+ # -- translate -------------------------------------------------------------- #
396
+
397
+ def _dialect_hint_for_source(from_logic: str, explicit):
398
+ """Resolve the parse_any hint for translate's source term.
399
+
400
+ An explicit ``--dialect`` always wins. Otherwise: a ``from_logic`` that
401
+ names one of the kit's own unicode modes IS its own parse_any hint
402
+ (``"modal"``, ``"fol"``, …); ``"team"`` (the comorphism registry's label
403
+ for dependence-logic sentences) maps to the ``"dependence"`` mode, since
404
+ the two use different names for the same surface grammar and, unlike
405
+ ``modal``/``fol``, auto-detection cannot disambiguate a dependence atom
406
+ ``=(x,y)`` from a plain FOL equality atom of the same shape. Any other
407
+ label (``"alc"`` is handled separately before this is called; ``"eso"``
408
+ has no surface syntax of its own) falls back to auto-detection.
409
+ """
410
+ if explicit is not None:
411
+ return explicit
412
+ if from_logic in _MODE_KWARGS:
413
+ return from_logic
414
+ return _LOGIC_TO_DIALECT_HINT.get(from_logic)
415
+
416
+
417
+ def _render_translated(obj) -> str:
418
+ """Render a translation result for the human-readable summary.
419
+
420
+ Every default-registry target (``fol``, ``modal``, ``eso``) is a
421
+ :class:`~unicode_logic_kit.fol.nodes.Node` (``to_unicode_str``); ALC
422
+ :class:`~unicode_logic_kit.dl.concepts.Concept` only ever appears as a
423
+ SOURCE in the default registry, but ``to_unicode``/``str`` cover it too
424
+ should a future edge target ``alc``.
425
+ """
426
+ if hasattr(obj, "to_unicode_str"):
427
+ return obj.to_unicode_str()
428
+ if hasattr(obj, "to_unicode"):
429
+ return obj.to_unicode()
430
+ return str(obj)
431
+
432
+
433
+ def _cmd_translate(argv) -> int:
434
+ parser = argparse.ArgumentParser(
435
+ prog="unicode_logic_kit translate",
436
+ description="Translate a term between logics via the comorphism registry (api.translate).")
437
+ parser.add_argument("formula", metavar="FORMULA")
438
+ parser.add_argument(
439
+ "--from", dest="from_logic", required=True, metavar="L1",
440
+ help="source logic label (e.g. 'fol', 'modal', 'alc', 'team')")
441
+ parser.add_argument(
442
+ "--to-logic", dest="to_logic", required=True, metavar="L2",
443
+ help="target logic label (e.g. 'fol', 'modal', 'eso') — NOT --to, "
444
+ "which the legacy parser already owns")
445
+ _add_common(parser)
446
+ args = parser.parse_args(argv)
447
+
448
+ if args.from_logic == "alc":
449
+ from .dl.parser import parse_concept
450
+ term = parse_concept(args.formula)
451
+ else:
452
+ dialect = _dialect_hint_for_source(args.from_logic, args.dialect)
453
+ term = _parse_or_raise(args.formula, dialect)
454
+
455
+ result = api.translate(term, args.from_logic, args.to_logic)
456
+
457
+ if args.as_json:
458
+ print(json.dumps(result.to_dict()))
459
+ else:
460
+ _print_kv([
461
+ ("source", result.source),
462
+ ("target", result.target),
463
+ ("path", " -> ".join(result.path) if result.path else "(identity)"),
464
+ ("lossy", result.lossy),
465
+ ])
466
+ if result.note:
467
+ print(f"note: {result.note}")
468
+ print("result: " + _render_translated(result.result))
469
+
470
+ return 0
471
+
472
+
473
+ _SUBCOMMAND_HANDLERS = {
474
+ "check": _cmd_check,
475
+ "equiv": _cmd_equiv,
476
+ "prove": _cmd_prove,
477
+ "countermodel": _cmd_countermodel,
478
+ "repair": _cmd_repair,
479
+ "translate": _cmd_translate,
480
+ }
481
+
482
+
483
+ def _run_subcommand(name: str, rest_argv) -> int:
484
+ """Dispatch to one subcommand handler, enforcing the error culture.
485
+
486
+ argparse usage errors (missing/unknown flags) still raise ``SystemExit``
487
+ straight through, exactly like the legacy parser — those are the user
488
+ misusing the CLI, not a runtime failure to report cleanly. Everything
489
+ else that a handler can raise on a bad-but-well-formed request (an
490
+ unparseable formula from ``_parse_or_raise``, an unknown/unavailable
491
+ backend, an unsupported translation fragment, a malformed
492
+ ``--signature`` file) is caught here, printed as one clean message, and
493
+ turned into exit code 3.
494
+ """
495
+ handler = _SUBCOMMAND_HANDLERS[name]
496
+ try:
497
+ return handler(rest_argv)
498
+ except (ValueError, BackendUnavailable, NotImplementedError, OSError) as exc:
499
+ print(f"{name}: {exc}", file=sys.stderr)
500
+ return 3
501
+
502
+
503
+ def main(argv=None) -> int:
504
+ """Run the CLI: dispatch to a subcommand, or fall back to the legacy parser.
505
+
506
+ Parses ``argv`` (defaults to ``sys.argv[1:]``). If ``argv`` is non-empty
507
+ and ``argv[0]`` is one of ``check``/``equiv``/``prove``/``countermodel``/
508
+ ``repair``/``translate``, the rest of ``argv`` is handed to that
509
+ subcommand; otherwise the whole of ``argv`` goes to the legacy
510
+ single-formula parse-and-render path (see module docstring for both).
511
+ """
512
+ if argv is None:
513
+ argv = sys.argv[1:]
514
+ if argv and argv[0] in _SUBCOMMANDS:
515
+ return _run_subcommand(argv[0], argv[1:])
516
+ return _run_legacy(argv)
517
+
518
+
519
+ if __name__ == "__main__":
520
+ sys.exit(main())