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,929 @@
1
+ r"""CASL text parsing for classical FOL and many-sorted FOL (MSFOL) — the
2
+ importer half of the export/import pair completed by
3
+ :mod:`unicode_logic_kit.fol.casl_export`.
4
+
5
+ :func:`parse_casl_spec` turns ``spec <NAME> = ... end`` CASL text back into a
6
+ :class:`CaslSpec` (a declared :class:`~unicode_logic_kit.fol.signature.Signature`
7
+ plus the axiom/conjecture ASTs) — a hand-rolled recursive-descent parser, not
8
+ a Lark grammar, since this module owns no ``.lark`` file and is scoped to
9
+ exactly the CASL fragment :func:`~unicode_logic_kit.fol.casl_export.to_casl_spec`
10
+ emits (plus a small, explicitly bounded tolerant superset — see "Scope"
11
+ below), not general CASL. Anything outside that fragment is refused loudly
12
+ with :class:`CaslImportError` naming the offending construct and the line it
13
+ was found on — never silently dropped or mistranslated, the same honesty
14
+ convention :mod:`~unicode_logic_kit.fol.casl_export`'s own docstring states for
15
+ the export direction.
16
+
17
+ Scope: exactly ``to_casl_spec``'s emission, plus a bounded tolerant superset
18
+ --------------------------------------------------------------------------
19
+ SUPPORTED (byte-for-byte what :func:`~unicode_logic_kit.fol.casl_export.to_casl_spec`
20
+ emits, plus the listed tolerances):
21
+
22
+ * One flat ``spec <NAME> = <items> end`` block. ``end`` is OPTIONAL (bare
23
+ end-of-input also terminates the item list — CASL's own basic-spec grammar
24
+ makes the trailing ``end`` optional; this parser follows that rather than
25
+ being stricter than CASL itself).
26
+ * ``sort``/``sorts`` (singular and plural both accepted as synonyms),
27
+ comma-separated sort names.
28
+ * ``op``/``ops`` and ``pred``/``preds`` (singular and plural both accepted),
29
+ semicolon-separated entries. An operation entry is ``name : S1 * … -> R``
30
+ (a function, arity = number of argument sorts) or a bare ``name : S`` (a
31
+ 0-ary operation — see "0-ary operations import as Constant" below). A
32
+ predicate entry is ``name : S1 * … * Sn`` or ``name : ()`` for 0-ary.
33
+ * Formulas: ``not``, ``/\``, ``\/``, ``=>``, ``<=>``, ``=``, ``forall``/
34
+ ``exists`` with ``x : Sort . <body>``, parentheses, nested quantifiers.
35
+ Precedence (tolerant extension: ``to_casl_spec`` itself always emits full
36
+ parenthesisation, so nothing in ``to_casl_spec`` output actually exercises
37
+ precedence, but hand-written or other tools' CASL text might): ``not`` >
38
+ ``/\`` (left-assoc) > ``\/`` (left-assoc) > ``=>`` (right-assoc) > ``<=>``
39
+ (left-assoc chain) — matching ``to_casl_spec``'s own documented reading
40
+ wherever both are defined; verified directly against every golden case in
41
+ ``tests/test_casl_export.py``.
42
+ * Multiple variables per quantifier, one shared sort: ``forall x, y : S .
43
+ phi`` desugars to nested same-type/same-sort quantifiers, innermost
44
+ wrapping the body (``forall x : S . forall y : S . phi``) — a tolerant
45
+ extension beyond ``to_casl_spec``, which only ever emits one variable per
46
+ quantifier, but a direct, unambiguous reading of CASL's own VAR-DECL
47
+ grammar (a comma-separated variable list sharing one sort).
48
+ * ``%%`` line comments and the ``%implied`` conjecture annotation, both
49
+ anywhere whitespace-flexible; arbitrary whitespace/newlines everywhere.
50
+
51
+ * Subsorting (``sort S < T``, and the list form ``sort S1, S2 < T``): a
52
+ DIRECT edge, subset-semantics only (``ext(S) ⊆ ext(T)``) — see
53
+ :mod:`unicode_logic_kit.fol.signature`'s "Subsorting" docstring section for
54
+ the exact semantics and why it is deliberately narrower than full CASL
55
+ order-sorted algebra. Parsed into the returned :class:`CaslSpec`'s
56
+ :attr:`~unicode_logic_kit.fol.signature.Signature.subsorts`; a cycle across
57
+ one or more ``sort ... < ...`` declarations is refused with
58
+ :class:`CaslImportError` naming it (the same check
59
+ :class:`~unicode_logic_kit.fol.signature.Signature`'s own ``__post_init__``
60
+ runs, re-wrapped here so every refusal from this module is a
61
+ ``CaslImportError``).
62
+
63
+ REFUSED with :class:`CaslImportError` (a specific line number and construct
64
+ name in every message — never silently accepted as if it meant something
65
+ narrower): partial function arrows (``->?``), free/generated types (``free
66
+ type``/``generated type``/bare ``type``/``types`` items), structured-
67
+ specification constructs inside the one spec body (``then``, ``view``,
68
+ ``given``, ``arch``, ``unit``, …), and operation/predicate attributes
69
+ (``, assoc``, ``, comm``, …) — these still hold with or without a subsort
70
+ declaration in the same spec, since honoring them would need the full
71
+ order-sorted algebra this module deliberately does not attempt (injections,
72
+ casts, overload resolution across a sort hierarchy — see the "Subsorting"
73
+ bullet above). None of these can ever appear in ``to_casl_spec`` output, so
74
+ refusing them costs nothing against the round-trip contract below and keeps
75
+ this parser from silently accepting CASL it cannot faithfully turn back into
76
+ a kit AST. Operation/predicate OVERLOADING (the same name declared twice
77
+ with a different arity/argument-sort profile, subsort-related sorts or not)
78
+ is likewise still refused — not by a subsort-specific check, but because
79
+ :func:`_declare_function`/:func:`_declare_predicate` already refuse ANY
80
+ redeclaration with a conflicting type, subsorting or not.
81
+
82
+ 0-ary operations import as :class:`~unicode_logic_kit.fol.nodes.Constant`, never
83
+ :class:`~unicode_logic_kit.fol.nodes.SortedConstant` — an inherent, documented
84
+ information loss
85
+ ----------------------------------------------------------------------------
86
+ ``to_casl_spec`` renders a 0-ary operation's ``ops`` entry as a bare
87
+ ``name : Sort`` — INDISTINGUISHABLE, by design, from how it would render if
88
+ that name were instead a 0-ary :class:`~unicode_logic_kit.fol.nodes.Function`
89
+ (see :func:`~unicode_logic_kit.fol.casl_export._render_op_type`'s own
90
+ docstring: "indistinguishable in shape from a constant's own type"). And
91
+ every TERM reference to that name — whether the original kit AST built it as
92
+ a plain (unsorted) :class:`Constant`, an explicitly-sorted
93
+ :class:`SortedConstant`, or (in principle) a 0-ary
94
+ :class:`~unicode_logic_kit.fol.nodes.Function` — renders as the exact same bare
95
+ identifier (:func:`~unicode_logic_kit.fol.casl_export._render_term`). So CASL
96
+ text carries no information distinguishing any of these three kit-AST shapes
97
+ for a 0-ary symbol: the ``ops`` declaration only ever states the RESOLVED
98
+ sort (which may have been inferred purely through union-find propagation
99
+ from some OTHER axiom entirely, with no explicit annotation anywhere near
100
+ this occurrence — see ``casl_export``'s own worked example,
101
+ ``test_golden_person_animal_msfol_worked_example``, where an unsorted
102
+ ``Constant("socrates")`` gets ``ops socrates : Person`` purely from
103
+ appearing in the same predicate argument position as a ``Person``-sorted
104
+ variable in a DIFFERENT axiom).
105
+
106
+ Given that, this parser makes ONE deliberate, uniform choice: every bare
107
+ 0-ary term reference resolves to a plain :class:`Constant` — never
108
+ :class:`SortedConstant`, never a 0-ary :class:`Function`. This is not a
109
+ guess dressed up as a default; it is the only choice that does not invent
110
+ information the text does not carry, and it is exactly what makes the
111
+ round-trip contract below hold for ``to_casl_spec``'s own worked example
112
+ (where the original constant WAS a bare, unsorted ``Constant`` despite its
113
+ inferred CASL sort not equal ``default_sort``). A caller whose original
114
+ formulas used :class:`SortedConstant` throughout will NOT get
115
+ byte-identical structural equality back through
116
+ ``parse_casl_spec(to_casl_spec(f))`` — this is the one place the export/
117
+ import pair is lossy, and it is lossy on the EXPORT side (the CASL text
118
+ itself has already thrown the distinction away by the time this parser ever
119
+ sees it), not something this parser could recover with a cleverer grammar.
120
+
121
+ The round-trip contract
122
+ ------------------------
123
+ For a list ``fs`` of the kit's own classical/many-sorted formulas built with
124
+ plain :class:`~unicode_logic_kit.fol.nodes.Constant` (never
125
+ :class:`SortedConstant`) and without :class:`~unicode_logic_kit.fol.nodes.Xor`
126
+ (see below)::
127
+
128
+ parse_casl_spec(to_casl_spec(fs, conjectures=cjs)).axioms == tuple(fs)
129
+ parse_casl_spec(to_casl_spec(fs, conjectures=cjs)).conjectures == tuple(cjs)
130
+
131
+ holds by structural equality (the kit's node dataclasses are frozen with
132
+ value equality) when no bound variable of ``fs`` or ``cjs`` is spelled like a
133
+ symbol of the spec (a constant, function, predicate or sort). Where one is,
134
+ ``to_casl_spec`` writes that binder under a fresh name (CASL has one name
135
+ space for a variable and an operation, see ``casl_export``), and the formulas
136
+ read back equal up to the names of those renamed binders.
137
+ ``tests/test_casl_import.py`` hand-verifies the contract over a
138
+ representative formula set covering both quantifier kinds, equality, every
139
+ supported connective, function terms, 0-ary predicates/operations, and
140
+ ``%implied``.
141
+
142
+ :class:`~unicode_logic_kit.fol.nodes.Xor` is EXCLUDED from that contract for a
143
+ reason internal to the exporter, not this module: ``to_casl_spec`` has no
144
+ CASL operator for XOR and expands it to the classically-equivalent
145
+ ``not (<a> <=> <b>)`` (see ``casl_export``'s own docstring). Parsing that
146
+ text back necessarily yields ``Not(Iff(a, b))``, not ``Xor(a, b)`` — a
147
+ different (semantically equivalent, structurally distinct) tree. This parser
148
+ is faithful to the TEXT it is given: ``not (P <=> Q)`` always means
149
+ ``Not(Iff(P, Q))``, exactly like every other ``not``/``<=>`` occurrence,
150
+ never a special-cased reconstruction of ``Xor`` that no other tool
151
+ generating CASL text would expect.
152
+
153
+ A THIRD disclosed exception, exactly analogous to the 0-ary
154
+ Constant-vs-SortedConstant case above (adversarial review, Tier 3): a
155
+ :class:`~unicode_logic_kit.fol.nodes.SortedQuantifier` whose explicit ``sort``
156
+ EQUALS ``default_sort`` renders the very same CASL text as a plain
157
+ :class:`~unicode_logic_kit.fol.nodes.Quantifier` (both emit
158
+ ``forall x : <default_sort> . <body>``), so the wire format has already
159
+ thrown the node-type distinction away and this parser's reconstruction rule
160
+ (``sort == default_sort`` → plain ``Quantifier``) necessarily collapses it.
161
+ ``∀x:Thing P(x)`` therefore round-trips to the semantically identical but
162
+ structurally distinct ``∀x P(x)`` under the default ``default_sort="Thing"``
163
+ — pinned by ``test_round_trip_sorted_quantifier_at_default_sort_collapses``.
164
+
165
+ Reserved words
166
+ ---------------
167
+ Every identifier this parser reads into a declaration OR a reference
168
+ (sort/predicate/operation/variable/spec name) is checked against the same
169
+ CASL keyword list :mod:`~unicode_logic_kit.fol.casl_export` refuses on export
170
+ (``_CASL_KEYWORDS`` there). This is an intentional small independent literal
171
+ copy, not an import of that module's private name — the same reasoning
172
+ :mod:`unicode_logic_kit.fol.signature`'s own docstring gives for its identical
173
+ choice about ``_BUILTIN_PREDS``/``_BUILTIN_FUNCS``: importing a leading-
174
+ underscore name across modules would make this parser depend on
175
+ ``casl_export``'s internals staying stable, when the two lists just happen
176
+ to need to agree by CONTENT, not by object identity.
177
+
178
+ The two keywords ``true`` and ``false`` are not refused where a formula is expected:
179
+ they are CASL's own atomic formulas and read as the truth constants, the nullary atoms
180
+ ``$true`` and ``$false`` (which :func:`~unicode_logic_kit.fol.casl_export.to_casl_spec`
181
+ writes as ``true`` and ``false``, so a spec the exporter writes reads back). They
182
+ declare no predicate, and as a NAME (a sort, a predicate, an operation or a variable
183
+ called ``true``) they stay refused.
184
+ """
185
+
186
+ from collections import namedtuple
187
+ from dataclasses import dataclass
188
+ from typing import Dict, List, Optional, Sequence, Tuple
189
+ import re
190
+
191
+ from .nodes import (
192
+ Node, Variable, Constant, Function,
193
+ Atom, Not, And, Or, Implies, Iff,
194
+ Quantifier, SortedQuantifier,
195
+ )
196
+ from .signature import Signature, PredicateDecl, FunctionDecl, ConstantDecl
197
+ from ._truth_constants import is_truth_constant
198
+
199
+ __all__ = ["parse_casl_spec", "CaslSpec", "CaslImportError"]
200
+
201
+
202
+ class CaslImportError(ValueError):
203
+ """A CASL text construct is outside :func:`parse_casl_spec`'s supported
204
+ grammar, or the text is otherwise malformed.
205
+
206
+ Every raise site names the specific construct and where it sits — a
207
+ ``line N:`` prefix for lexical/grammatical refusals, or the axiom/
208
+ conjecture position for the post-parse usage-conformance pass (see
209
+ :func:`_validate_usage` — by then the text positions are gone but the
210
+ formula order is stable). This is the import-side half of the kit's
211
+ "refuse loudly, never silently mistranslate" convention (see
212
+ :mod:`unicode_logic_kit.fol.casl_export`'s module docstring for the same
213
+ principle stated for export).
214
+ """
215
+
216
+
217
+ @dataclass(frozen=True)
218
+ class CaslSpec:
219
+ """One parsed CASL ``spec`` block: its name, declared vocabulary, and
220
+ formulas, split into axioms and ``%implied`` conjectures.
221
+
222
+ ``signature`` is built directly from the ``sorts``/``ops``/``preds``
223
+ declarations actually present in the text (never re-inferred from the
224
+ formula ASTs the way :meth:`~unicode_logic_kit.fol.signature.Signature.from_formulas`
225
+ would) — CASL text already states arities and argument/result sorts
226
+ explicitly, so re-inferring would either duplicate that information or
227
+ risk silently disagreeing with it.
228
+ """
229
+
230
+ name: str
231
+ signature: Signature
232
+ axioms: Tuple[Node, ...]
233
+ conjectures: Tuple[Node, ...]
234
+
235
+ def to_dict(self) -> dict:
236
+ """JSON-compatible dict: ``name``, ``signature`` (via
237
+ :meth:`~unicode_logic_kit.fol.signature.Signature.to_dict`), and
238
+ ``axioms``/``conjectures`` as lists of node dicts (via each node's
239
+ own ``to_dict``)."""
240
+ return {
241
+ "name": self.name,
242
+ "signature": self.signature.to_dict(),
243
+ "axioms": [a.to_dict() for a in self.axioms],
244
+ "conjectures": [c.to_dict() for c in self.conjectures],
245
+ }
246
+
247
+
248
+ # =============================================================================
249
+ # Reserved words (see module docstring's "Reserved words" section)
250
+ # =============================================================================
251
+
252
+ _CASL_KEYWORDS = frozenset({
253
+ "and", "arch", "as", "assoc", "axiom", "axioms", "closed", "comm", "def",
254
+ "else", "end", "exists", "false", "fit", "forall", "free", "from",
255
+ "generated", "get", "given", "hide", "idem", "if", "in", "lambda",
256
+ "library", "local", "logic", "not", "op", "ops", "pred", "preds",
257
+ "result", "reveal", "sort", "sorts", "spec", "then", "to", "true",
258
+ "type", "types", "unit", "units", "var", "vars", "version", "view",
259
+ "when", "with", "within",
260
+ })
261
+
262
+ #: Item-position keywords naming a refused structuring construct: any of
263
+ #: these appearing where a sort/op/pred declaration or an axiom is expected
264
+ #: means the input is a STRUCTURED spec (or a library-level item), which is
265
+ #: out of scope — see the module docstring's "Scope" section.
266
+ _STRUCTURING_KEYWORDS = frozenset({
267
+ "then", "view", "given", "arch", "unit", "units", "hide", "reveal",
268
+ "local", "within", "closed", "library", "logic", "and", "fit",
269
+ })
270
+
271
+ #: Item-position keywords naming a refused free/generated-type construct.
272
+ _FREE_TYPE_KEYWORDS = frozenset({"free", "type", "types", "generated"})
273
+
274
+
275
+ # =============================================================================
276
+ # Tokenizer
277
+ # =============================================================================
278
+
279
+ _Token = namedtuple("_Token", "kind value line")
280
+
281
+ # Order matters: alternation tries each pattern in LISTED order at a given
282
+ # position (Python `re` alternation is first-match, not longest-match), so
283
+ # every prefix relationship below is resolved by listing the LONGER/more
284
+ # specific pattern first: ARROW_PARTIAL before ARROW (both start with '->'),
285
+ # IFF before LT (both start with '<'), IMPLIES before EQ (both start with
286
+ # '='), COMMENT before PERCENT_WORD (both start with '%').
287
+ _TOKEN_SPEC = [
288
+ ("COMMENT", r"%%[^\r\n]*"),
289
+ ("PERCENT_WORD", r"%\S*"),
290
+ ("NEWLINE", r"\r\n?|\n"),
291
+ ("WS", r"[ \t]+"),
292
+ ("ARROW_PARTIAL", r"->\?"),
293
+ ("ARROW", r"->"),
294
+ ("IFF", r"<=>"),
295
+ ("IMPLIES", r"=>"),
296
+ ("AND", r"/\\"),
297
+ ("OR", r"\\/"),
298
+ ("LPAREN", r"\("),
299
+ ("RPAREN", r"\)"),
300
+ ("COMMA", r","),
301
+ ("SEMI", r";"),
302
+ ("COLON", r":"),
303
+ ("DOT", r"\."),
304
+ ("STAR", r"\*"),
305
+ ("LT", r"<"),
306
+ ("EQ", r"="),
307
+ ("IDENT", r"[A-Za-z][A-Za-z0-9_]*"),
308
+ ]
309
+ _MASTER_RE = re.compile("|".join(f"(?P<{name}>{pat})" for name, pat in _TOKEN_SPEC))
310
+
311
+
312
+ class _Lexer:
313
+ """Scans ``text`` into tokens ON DEMAND, buffering only as much
314
+ lookahead as :class:`_Parser` actually asks for (via :meth:`peek`/
315
+ :meth:`next`), rather than tokenizing the whole input up front.
316
+
317
+ This matters for refusal quality, not just laziness for its own sake:
318
+ an out-of-scope construct this parser means to refuse by NAME (e.g. a
319
+ ``free type ... ::= zero | succ(Nat)`` declaration) may contain
320
+ characters this grammar has no token for at all (``|``) FURTHER ALONG
321
+ in the same construct. Eager whole-text tokenization would hit that
322
+ unrecognized character first and raise a generic "unrecognized
323
+ character" error before the parser ever gets a chance to recognize
324
+ ``free`` at item-position and give its specific, named refusal. Lazy
325
+ scanning means the parser sees (and can react to) the ``free`` token
326
+ the moment it asks for the next one, without this lexer ever having
327
+ attempted to scan past it into the unsupported tail of the construct.
328
+
329
+ Comments (``%%...`` to end of line) and whitespace/newlines are
330
+ consumed silently while scanning — callers never see them as tokens. A
331
+ bare ``%word`` other than ``%implied`` is refused immediately (a CASL
332
+ annotation this parser does not support, e.g. ``%(label)%`` or
333
+ ``%def`` — none of these are ever emitted by ``to_casl_spec``).
334
+ """
335
+
336
+ def __init__(self, text: str):
337
+ self._text = text
338
+ self._pos = 0
339
+ self._n = len(text)
340
+ self._line = 1
341
+ self._buffer: List[_Token] = []
342
+
343
+ def _scan_one(self) -> _Token:
344
+ while self._pos < self._n:
345
+ m = _MASTER_RE.match(self._text, self._pos)
346
+ if not m:
347
+ raise CaslImportError(
348
+ f"line {self._line}: unrecognized character "
349
+ f"{self._text[self._pos]!r} in CASL text"
350
+ )
351
+ kind, value = m.lastgroup, m.group()
352
+ if kind == "NEWLINE":
353
+ self._line += 1
354
+ self._pos = m.end()
355
+ continue
356
+ if kind in ("WS", "COMMENT"):
357
+ self._pos = m.end()
358
+ continue
359
+ if kind == "PERCENT_WORD":
360
+ if value != "%implied":
361
+ raise CaslImportError(
362
+ f"line {self._line}: unsupported CASL annotation "
363
+ f"{value!r} — only '%implied' (conjecture marker) "
364
+ "and '%%' line comments are supported"
365
+ )
366
+ kind = "IMPLIED"
367
+ self._pos = m.end()
368
+ return _Token(kind, value, self._line)
369
+ return _Token("EOF", "", self._line)
370
+
371
+ def peek(self, ahead: int = 0) -> _Token:
372
+ """Return the token ``ahead`` positions past the cursor without
373
+ consuming it (``ahead=0`` is the next token to be consumed)."""
374
+ while len(self._buffer) <= ahead:
375
+ self._buffer.append(self._scan_one())
376
+ return self._buffer[ahead]
377
+
378
+ def next(self) -> _Token:
379
+ """Consume and return the next token."""
380
+ self.peek(0)
381
+ return self._buffer.pop(0)
382
+
383
+
384
+ # =============================================================================
385
+ # Recursive-descent parser
386
+ # =============================================================================
387
+
388
+ class _Parser:
389
+ """Consumes a token list once, left to right, building spec-level
390
+ declarations and formula ASTs. See the module docstring's "Scope"
391
+ section for the exact grammar and the refusal classes."""
392
+
393
+ def __init__(self, lexer: "_Lexer", default_sort: str):
394
+ self._lexer = lexer
395
+ self.default_sort = default_sort
396
+ self.sorts: set = set()
397
+ #: child sort name -> the set of DIRECT parent sort names declared
398
+ #: for it (accumulated across every ``sort ... < ...`` decl item —
399
+ #: a child can gain further parents from a LATER decl in the same
400
+ #: spec, e.g. multiple inheritance spread across two lines).
401
+ self.subsorts: Dict[str, set] = {}
402
+ self.predicates: Dict[str, PredicateDecl] = {}
403
+ self.functions: Dict[str, FunctionDecl] = {}
404
+ self.constants: Dict[str, ConstantDecl] = {}
405
+ self.axioms: List[Node] = []
406
+ self.conjectures: List[Node] = []
407
+
408
+ # -- token helpers --------------------------------------------------
409
+
410
+ def _cur(self) -> _Token:
411
+ return self._lexer.peek(0)
412
+
413
+ def _at(self, kind: str) -> bool:
414
+ return self._cur().kind == kind
415
+
416
+ def _at_ident(self, *values: str) -> bool:
417
+ t = self._cur()
418
+ return t.kind == "IDENT" and t.value in values
419
+
420
+ def _advance(self) -> _Token:
421
+ return self._lexer.next()
422
+
423
+ def _expect(self, kind: str, what: str) -> _Token:
424
+ if not self._at(kind):
425
+ self._error(f"expected {what}, found {self._describe(self._cur())}")
426
+ return self._advance()
427
+
428
+ def _expect_ident(self, *values: str) -> _Token:
429
+ if not self._at_ident(*values):
430
+ self._error(
431
+ f"expected one of {sorted(values)!r}, found "
432
+ f"{self._describe(self._cur())}"
433
+ )
434
+ return self._advance()
435
+
436
+ @staticmethod
437
+ def _describe(t: _Token) -> str:
438
+ return f"{t.kind} {t.value!r}" if t.value else t.kind
439
+
440
+ def _error(self, msg: str):
441
+ raise CaslImportError(f"line {self._cur().line}: {msg}")
442
+
443
+ def _check_reserved(self, name: str, kind: str, line: int) -> None:
444
+ if name in _CASL_KEYWORDS:
445
+ raise CaslImportError(
446
+ f"line {line}: {kind} name '{name}' collides with the "
447
+ f"reserved CASL keyword '{name}'"
448
+ )
449
+
450
+ # -- top level --------------------------------------------------------
451
+
452
+ def parse(self) -> str:
453
+ """Parse ``spec NAME = <items> end`` (``end`` optional); return NAME."""
454
+ self._expect_ident("spec")
455
+ name_tok = self._expect("IDENT", "spec name")
456
+ self._check_reserved(name_tok.value, "spec name", name_tok.line)
457
+ self._expect("EQ", "'=' after spec name")
458
+ while True:
459
+ if self._at("EOF"):
460
+ break
461
+ if self._at_ident("end"):
462
+ self._advance()
463
+ break
464
+ if self._at("DOT"):
465
+ self._parse_axiom_line()
466
+ continue
467
+ if self._at_ident("sort", "sorts"):
468
+ self._parse_sort_decl()
469
+ continue
470
+ if self._at_ident("op", "ops"):
471
+ self._parse_op_decl()
472
+ continue
473
+ if self._at_ident("pred", "preds"):
474
+ self._parse_pred_decl()
475
+ continue
476
+ if self._at_ident(*_FREE_TYPE_KEYWORDS):
477
+ self._error(
478
+ f"free/generated type declaration ({self._cur().value!r}) "
479
+ "is outside parse_casl_spec's supported grammar"
480
+ )
481
+ if self._at_ident(*_STRUCTURING_KEYWORDS):
482
+ self._error(
483
+ f"structured-specification construct "
484
+ f"{self._cur().value!r} is outside parse_casl_spec's "
485
+ "supported grammar — only a single flat "
486
+ "'spec NAME = ... end' block is supported"
487
+ )
488
+ if self._at("IDENT"):
489
+ nxt = self._lexer.peek(1)
490
+ if nxt.kind == "IDENT" and nxt.value == "then":
491
+ self._error(
492
+ "structured-specification construct 'then' (spec "
493
+ f"extension, e.g. 'spec X = {self._cur().value} "
494
+ "then ...') is outside parse_casl_spec's supported "
495
+ "grammar — only a single flat 'spec NAME = ... end' "
496
+ "block is supported (see unicode_logic_kit.hets.dol "
497
+ "for DOL library 'extends' emission)"
498
+ )
499
+ self._error(
500
+ f"unexpected {self._describe(self._cur())} where a sort/op/"
501
+ "pred declaration or an axiom ('.') was expected"
502
+ )
503
+ if not self._at("EOF"):
504
+ self._error("unexpected content after 'end'")
505
+ return name_tok.value
506
+
507
+ # -- declarations -------------------------------------------------------
508
+
509
+ def _check_no_attributes(self) -> None:
510
+ """Refuse a trailing ``, assoc`` / ``, comm`` / … attribute list —
511
+ the only thing a ',' can legally introduce after an op/pred type in
512
+ this grammar."""
513
+ if self._at("COMMA"):
514
+ self._error(
515
+ "operation/predicate attributes (e.g. ', assoc', ', comm') "
516
+ "are outside parse_casl_spec's supported grammar"
517
+ )
518
+
519
+ def _parse_sort_decl(self) -> None:
520
+ """Parse one ``sort``/``sorts`` item: a plain comma-separated name
521
+ list (``sort S1, S2``), or a subsort declaration (``sort S1, S2 <
522
+ T``, CASL's ``sort-id-list "<" sort-id`` production — every name in
523
+ the list becomes a DIRECT subsort of ``T``). The ``<`` can only
524
+ follow the WHOLE list, matching CASL's own grammar (not after each
525
+ individual name), so ``sort S < T`` and ``sort S1, S2 < T`` are both
526
+ supported but ``sort S1 < T1, S2 < T2`` (two subsort decls chained
527
+ by comma) is not — write those as two separate ``sort ...`` items.
528
+ """
529
+ self._advance() # 'sort' | 'sorts'
530
+ names = []
531
+ while True:
532
+ tok = self._expect("IDENT", "sort name")
533
+ self._check_reserved(tok.value, "sort", tok.line)
534
+ names.append(tok.value)
535
+ if self._at("COMMA"):
536
+ self._advance()
537
+ continue
538
+ break
539
+ if self._at("LT"):
540
+ self._advance()
541
+ parent_tok = self._expect("IDENT", "supersort name")
542
+ self._check_reserved(parent_tok.value, "sort", parent_tok.line)
543
+ parent = parent_tok.value
544
+ self.sorts.add(parent)
545
+ for child in names:
546
+ self.sorts.add(child)
547
+ self.subsorts.setdefault(child, set()).add(parent)
548
+ return
549
+ for name in names:
550
+ self.sorts.add(name)
551
+
552
+ def _parse_op_decl(self) -> None:
553
+ self._advance() # 'op' | 'ops'
554
+ while True:
555
+ name_tok = self._expect("IDENT", "operation name")
556
+ name = name_tok.value
557
+ self._check_reserved(name, "operation", name_tok.line)
558
+ self._expect("COLON", "':' after operation name")
559
+ arg_sorts, result_sort = self._parse_op_type()
560
+ self._check_no_attributes()
561
+ if arg_sorts:
562
+ self._declare_function(name, arg_sorts, result_sort, name_tok.line)
563
+ else:
564
+ self._declare_constant(name, result_sort, name_tok.line)
565
+ if self._at("SEMI"):
566
+ self._advance()
567
+ continue
568
+ break
569
+
570
+ def _parse_op_type(self) -> Tuple[List[str], str]:
571
+ if self._at("ARROW_PARTIAL"):
572
+ self._error(
573
+ "partial function arrow ('->?') is outside parse_casl_spec's "
574
+ "supported grammar"
575
+ )
576
+ first = self._expect("IDENT", "sort name")
577
+ self._check_reserved(first.value, "sort", first.line)
578
+ sorts = [first.value]
579
+ while self._at("STAR"):
580
+ self._advance()
581
+ tok = self._expect("IDENT", "sort name")
582
+ self._check_reserved(tok.value, "sort", tok.line)
583
+ sorts.append(tok.value)
584
+ if self._at("ARROW_PARTIAL"):
585
+ self._error(
586
+ "partial function arrow ('->?') is outside parse_casl_spec's "
587
+ "supported grammar"
588
+ )
589
+ if self._at("ARROW"):
590
+ self._advance()
591
+ result = self._expect("IDENT", "result sort name")
592
+ self._check_reserved(result.value, "sort", result.line)
593
+ for s in sorts:
594
+ self.sorts.add(s)
595
+ self.sorts.add(result.value)
596
+ return sorts, result.value
597
+ if len(sorts) != 1:
598
+ self._error(
599
+ "expected '->' after a multi-sort argument list in an "
600
+ "operation type"
601
+ )
602
+ self.sorts.add(sorts[0])
603
+ return [], sorts[0]
604
+
605
+ def _parse_pred_decl(self) -> None:
606
+ self._advance() # 'pred' | 'preds'
607
+ while True:
608
+ name_tok = self._expect("IDENT", "predicate name")
609
+ name = name_tok.value
610
+ self._check_reserved(name, "predicate", name_tok.line)
611
+ self._expect("COLON", "':' after predicate name")
612
+ arg_sorts = self._parse_pred_type()
613
+ self._check_no_attributes()
614
+ self._declare_predicate(name, arg_sorts, name_tok.line)
615
+ if self._at("SEMI"):
616
+ self._advance()
617
+ continue
618
+ break
619
+
620
+ def _parse_pred_type(self) -> List[str]:
621
+ if self._at("LPAREN"):
622
+ self._advance()
623
+ self._expect("RPAREN", "')' closing a 0-ary predicate type")
624
+ return []
625
+ tok = self._expect("IDENT", "sort name")
626
+ self._check_reserved(tok.value, "sort", tok.line)
627
+ sorts = [tok.value]
628
+ self.sorts.add(tok.value)
629
+ while self._at("STAR"):
630
+ self._advance()
631
+ tok = self._expect("IDENT", "sort name")
632
+ self._check_reserved(tok.value, "sort", tok.line)
633
+ sorts.append(tok.value)
634
+ self.sorts.add(tok.value)
635
+ return sorts
636
+
637
+ def _declare_function(self, name: str, arg_sorts: List[str],
638
+ result_sort: str, line: int) -> None:
639
+ if name in self.constants:
640
+ self._error(
641
+ f"'{name}' is declared both as a 0-ary operation and as a "
642
+ "function with arguments — a signature declares at most "
643
+ "one meaning per operation name"
644
+ )
645
+ decl = FunctionDecl(name, len(arg_sorts), tuple(arg_sorts), result_sort)
646
+ if name in self.functions and self.functions[name] != decl:
647
+ self._error(f"operation '{name}' redeclared with a conflicting type")
648
+ self.functions[name] = decl
649
+
650
+ def _declare_constant(self, name: str, sort: str, line: int) -> None:
651
+ if name in self.functions:
652
+ self._error(
653
+ f"'{name}' is declared both as a 0-ary operation and as a "
654
+ "function with arguments — a signature declares at most "
655
+ "one meaning per operation name"
656
+ )
657
+ decl = ConstantDecl(name, sort)
658
+ if name in self.constants and self.constants[name] != decl:
659
+ self._error(f"operation '{name}' redeclared with a conflicting sort")
660
+ self.constants[name] = decl
661
+
662
+ def _declare_predicate(self, name: str, arg_sorts: List[str], line: int) -> None:
663
+ decl = PredicateDecl(name, len(arg_sorts), tuple(arg_sorts) if arg_sorts else None)
664
+ if name in self.predicates and self.predicates[name] != decl:
665
+ self._error(f"predicate '{name}' redeclared with a conflicting type")
666
+ self.predicates[name] = decl
667
+
668
+ # -- axioms / formulas ----------------------------------------------
669
+
670
+ def _parse_axiom_line(self) -> None:
671
+ self._advance() # leading '.'
672
+ formula = self._parse_formula(frozenset())
673
+ if self._at("IMPLIED"):
674
+ self._advance()
675
+ self.conjectures.append(formula)
676
+ else:
677
+ self.axioms.append(formula)
678
+
679
+ def _parse_formula(self, env: frozenset) -> Node:
680
+ return self._parse_iff(env)
681
+
682
+ def _parse_iff(self, env: frozenset) -> Node:
683
+ left = self._parse_implies(env)
684
+ while self._at("IFF"):
685
+ self._advance()
686
+ right = self._parse_implies(env)
687
+ left = Iff(left, right)
688
+ return left
689
+
690
+ def _parse_implies(self, env: frozenset) -> Node:
691
+ left = self._parse_or(env)
692
+ if self._at("IMPLIES"):
693
+ self._advance()
694
+ right = self._parse_implies(env) # right-associative
695
+ return Implies(left, right)
696
+ return left
697
+
698
+ def _parse_or(self, env: frozenset) -> Node:
699
+ left = self._parse_and(env)
700
+ while self._at("OR"):
701
+ self._advance()
702
+ right = self._parse_and(env)
703
+ left = Or(left, right)
704
+ return left
705
+
706
+ def _parse_and(self, env: frozenset) -> Node:
707
+ left = self._parse_unary(env)
708
+ while self._at("AND"):
709
+ self._advance()
710
+ right = self._parse_unary(env)
711
+ left = And(left, right)
712
+ return left
713
+
714
+ def _parse_unary(self, env: frozenset) -> Node:
715
+ if self._at_ident("not"):
716
+ self._advance()
717
+ return Not(self._parse_unary(env))
718
+ return self._parse_primary(env)
719
+
720
+ def _parse_primary(self, env: frozenset) -> Node:
721
+ if self._at_ident("true", "false"):
722
+ # CASL's own atomic formulas: the truth constants.
723
+ word = self._advance().value
724
+ if self._at("LPAREN") or self._at("EQ"):
725
+ self._error(f"'{word}' is a CASL formula, not a predicate or a term: it "
726
+ "takes no arguments and is no operand of '='")
727
+ return Atom("$true" if word == "true" else "$false", ())
728
+ if self._at_ident("forall", "exists"):
729
+ return self._parse_quantifier(env)
730
+ if self._at("LPAREN"):
731
+ self._advance()
732
+ f = self._parse_formula(env)
733
+ self._expect("RPAREN", "')'")
734
+ return f
735
+ return self._parse_atom_or_equality(env)
736
+
737
+ def _parse_quantifier(self, env: frozenset) -> Node:
738
+ kw = self._advance() # 'forall' | 'exists'
739
+ qtype = "∀" if kw.value == "forall" else "∃"
740
+ names = []
741
+ tok = self._expect("IDENT", "quantified variable name")
742
+ self._check_reserved(tok.value, "variable", tok.line)
743
+ names.append(tok.value)
744
+ while self._at("COMMA"):
745
+ self._advance()
746
+ tok = self._expect("IDENT", "quantified variable name")
747
+ self._check_reserved(tok.value, "variable", tok.line)
748
+ names.append(tok.value)
749
+ self._expect("COLON", "':' after quantified variable(s)")
750
+ sort_tok = self._expect("IDENT", "sort name")
751
+ sort = sort_tok.value
752
+ self._check_reserved(sort, "sort", sort_tok.line)
753
+ self.sorts.add(sort)
754
+ self._expect("DOT", "'.' after the quantifier's sort")
755
+ new_env = env | frozenset(names)
756
+ body = self._parse_formula(new_env)
757
+ result = body
758
+ for name in reversed(names):
759
+ var = Variable(name)
760
+ if sort == self.default_sort:
761
+ result = Quantifier(qtype, var, result)
762
+ else:
763
+ result = SortedQuantifier(qtype, var, sort, result)
764
+ return result
765
+
766
+ def _parse_atom_or_equality(self, env: frozenset) -> Node:
767
+ name_tok = self._expect(
768
+ "IDENT", "an atom, predicate application, or term")
769
+ name = name_tok.value
770
+ self._check_reserved(name, "predicate/operation", name_tok.line)
771
+ args = self._parse_optional_arg_list(env)
772
+ if self._at("EQ"):
773
+ left_term = self._resolve_term(name, args, env)
774
+ self._advance() # '='
775
+ right_term = self._parse_term(env)
776
+ return Atom("=", (left_term, right_term))
777
+ return Atom(name, tuple(args))
778
+
779
+ def _parse_term(self, env: frozenset) -> Node:
780
+ tok = self._expect("IDENT", "a term (variable, constant, or function)")
781
+ name = tok.value
782
+ self._check_reserved(name, "predicate/operation", tok.line)
783
+ args = self._parse_optional_arg_list(env)
784
+ return self._resolve_term(name, args, env)
785
+
786
+ def _parse_optional_arg_list(self, env: frozenset) -> List[Node]:
787
+ if not self._at("LPAREN"):
788
+ return []
789
+ self._advance()
790
+ args = [self._parse_term(env)]
791
+ while self._at("COMMA"):
792
+ self._advance()
793
+ args.append(self._parse_term(env))
794
+ self._expect("RPAREN", "')' closing an argument list")
795
+ return args
796
+
797
+ @staticmethod
798
+ def _resolve_term(name: str, args: List[Node], env: frozenset) -> Node:
799
+ """A bare designator resolves to a bound :class:`Variable` (if
800
+ ``name`` is in scope), else a :class:`Constant`; an applied one
801
+ (``args`` non-empty) always resolves to a :class:`Function` — see
802
+ the module docstring's "0-ary operations import as Constant"
803
+ section for why a bare reference never becomes a
804
+ :class:`~unicode_logic_kit.fol.nodes.SortedConstant` or a 0-ary
805
+ :class:`Function`."""
806
+ if args:
807
+ return Function(name, tuple(args))
808
+ if name in env:
809
+ return Variable(name)
810
+ return Constant(name)
811
+
812
+
813
+ # =============================================================================
814
+ # Public API
815
+ # =============================================================================
816
+
817
+ def _validate_usage(parser: "_Parser") -> None:
818
+ """Post-parse conformance pass: every symbol the formulas USE must have
819
+ been DECLARED, with matching arity.
820
+
821
+ The recursive-descent parser builds Atom/Function/Constant nodes purely
822
+ from the tokens it encounters; without this pass a symbol used but never
823
+ declared silently produced a :class:`CaslSpec` whose signature does not
824
+ describe its own axioms, and a declared/used arity mismatch was accepted
825
+ outright (adversarial review, Tier 3) — both violating the module's
826
+ "refuse loudly" contract. Runs after the whole spec is parsed (a
827
+ declaration is in scope for every formula regardless of textual order),
828
+ so errors carry the axiom/conjecture position instead of a line number.
829
+ """
830
+ labeled = (
831
+ [(f"axiom {i + 1}", f) for i, f in enumerate(parser.axioms)]
832
+ + [(f"%implied conjecture {i + 1}", f)
833
+ for i, f in enumerate(parser.conjectures)])
834
+ for label, formula in labeled:
835
+ for node in formula.walk():
836
+ if is_truth_constant(node):
837
+ continue # `true` / `false`: CASL's own formulas, declared by no one
838
+ if isinstance(node, Atom) and node.predicate != "=":
839
+ decl = parser.predicates.get(node.predicate)
840
+ if decl is None:
841
+ raise CaslImportError(
842
+ f"{label}: predicate '{node.predicate}' is used but "
843
+ f"never declared (add a 'preds {node.predicate} : "
844
+ f"...' declaration)")
845
+ if decl.arity != len(node.args):
846
+ raise CaslImportError(
847
+ f"{label}: predicate '{node.predicate}' is declared "
848
+ f"with arity {decl.arity} but used with "
849
+ f"{len(node.args)} argument(s)")
850
+ elif isinstance(node, Function):
851
+ decl = parser.functions.get(node.name)
852
+ if decl is None:
853
+ raise CaslImportError(
854
+ f"{label}: operation '{node.name}' is used but never "
855
+ f"declared (add an 'ops {node.name} : ...' "
856
+ f"declaration)")
857
+ if decl.arity != len(node.args):
858
+ raise CaslImportError(
859
+ f"{label}: operation '{node.name}' is declared with "
860
+ f"arity {decl.arity} but used with "
861
+ f"{len(node.args)} argument(s)")
862
+ elif isinstance(node, Constant):
863
+ if node.name not in parser.constants:
864
+ raise CaslImportError(
865
+ f"{label}: constant '{node.name}' is used but never "
866
+ f"declared (add an 'ops {node.name} : <Sort>' "
867
+ f"declaration)")
868
+
869
+
870
+ def parse_casl_spec(text: str, *, default_sort: str = "Thing") -> CaslSpec:
871
+ """Parse one CASL ``spec <NAME> = … end`` block into a :class:`CaslSpec`.
872
+
873
+ See the module docstring for the exact grammar supported (a bounded
874
+ tolerant superset of what
875
+ :func:`unicode_logic_kit.fol.casl_export.to_casl_spec` emits), the round-
876
+ trip contract this establishes with that exporter, and the refusal
877
+ classes below.
878
+
879
+ Args:
880
+ text: CASL spec text.
881
+ default_sort: the sort a PLAIN (unsorted)
882
+ :class:`~unicode_logic_kit.fol.nodes.Quantifier` is reconstructed
883
+ for — a quantifier declared at exactly this sort in the text
884
+ becomes a plain ``Quantifier``; any other sort becomes a
885
+ :class:`~unicode_logic_kit.fol.nodes.SortedQuantifier`. Must match
886
+ whatever ``default_sort`` the text was originally exported with
887
+ for the round-trip contract to hold.
888
+
889
+ Returns:
890
+ A :class:`CaslSpec` carrying the spec's name, its declared
891
+ :class:`~unicode_logic_kit.fol.signature.Signature` (read directly off
892
+ the ``sorts``/``subsorts``/``ops``/``preds`` declarations, not
893
+ re-inferred from the formulas), and its axioms/conjectures as kit
894
+ ASTs.
895
+
896
+ Raises:
897
+ CaslImportError: a construct outside the supported grammar (partial
898
+ functions, a subsort cycle, free/generated types, structured-spec
899
+ constructs, operation/predicate attributes, an unrecognized
900
+ token, a malformed op/pred type, a reserved-word identifier, or
901
+ any other parse failure) — always with a line number (or, for a
902
+ subsort cycle — detected only once the whole spec is assembled —
903
+ the cycle itself) named.
904
+ """
905
+ lexer = _Lexer(text)
906
+ parser = _Parser(lexer, default_sort)
907
+ name = parser.parse()
908
+ _validate_usage(parser)
909
+ try:
910
+ signature = Signature(
911
+ predicates=dict(parser.predicates),
912
+ functions=dict(parser.functions),
913
+ constants=dict(parser.constants),
914
+ sorts=frozenset(parser.sorts),
915
+ subsorts={child: frozenset(parents)
916
+ for child, parents in parser.subsorts.items()},
917
+ )
918
+ except ValueError as e:
919
+ # Signature.__post_init__'s own subsort-cycle check — re-raised as
920
+ # this module's own error type so every refusal from parse_casl_spec
921
+ # is a CaslImportError, never a bare ValueError leaking a different
922
+ # module's exception type.
923
+ raise CaslImportError(str(e)) from e
924
+ return CaslSpec(
925
+ name=name,
926
+ signature=signature,
927
+ axioms=tuple(parser.axioms),
928
+ conjectures=tuple(parser.conjectures),
929
+ )