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,105 @@
1
+ """A free variable is a parameter of the problem: one unknown element, the same everywhere.
2
+
3
+ The consequence relation the kit reads is the assignment-wise one of the textbooks:
4
+ ``Γ ⊨ φ`` iff every structure AND assignment that satisfies ``Γ`` satisfies ``φ``. A
5
+ variable that is free in a premise or in the conclusion therefore names ONE element,
6
+ the same in every formula of the problem. It is not a universally quantified variable
7
+ of its own formula: ``P(x) ⊢ P(alpha)`` is not valid (universe ``{0, 1}``, ``x`` ↦ 1,
8
+ ``alpha`` ↦ 0, ``P`` = ``{1}``), while ``P(x) ⊢ ∃y P(y)`` and ``∀y P(y) ⊢ P(x)`` are.
9
+
10
+ A route that has no free variables of its own (a resolution prover, a model finder, a
11
+ writer for a target that closes every formula) reads this by replacing every free
12
+ variable of the whole problem by a constant, before it does anything else:
13
+ ``Γ(x) ⊨ φ(x)`` assignment-wise iff ``Γ(c) ⊨ φ(c)`` for a constant ``c`` that no
14
+ symbol of the problem has. :func:`parameterize` does that, once, for every route that
15
+ needs it. For a problem with no premise the reading coincides with the universal
16
+ closure of the conclusion.
17
+ """
18
+
19
+ from typing import Dict, Iterable, List, Sequence, Tuple
20
+
21
+ from ._identifiers import symbol_names
22
+ from .nodes import Node, Constant, SortedConstant, Variable, free_variables, substitute
23
+
24
+ __all__ = ["free_parameter_names", "parameterize"]
25
+
26
+
27
+ def _free_names(formula: Node) -> frozenset:
28
+ """The names of the object variables that are free in ``formula``."""
29
+ return frozenset(v.name for v in free_variables(formula) if isinstance(v, Variable))
30
+
31
+
32
+ def free_parameter_names(formulas: Iterable[Node]) -> Tuple[str, ...]:
33
+ """The names of the variables that are free in at least one of ``formulas``, sorted.
34
+
35
+ A variable is free in a formula when no quantifier, counting quantifier,
36
+ cardinality term or IF-logic binder of that formula binds it; a name that is bound
37
+ in one formula and free in another is in the result. A lambda-bound variable is not
38
+ an object variable and is never in it.
39
+ """
40
+ names: set = set()
41
+ for formula in formulas:
42
+ names |= _free_names(formula)
43
+ return tuple(sorted(names))
44
+
45
+
46
+ def parameterize(formulas: Sequence[Node], *, avoid: Iterable[str] = (),
47
+ after_variables: bool = False) -> Tuple[List[Node], Dict[str, Constant]]:
48
+ """Replace every free variable of ``formulas`` by a constant, the same one in all of them.
49
+
50
+ Returns ``(closed, parameters)``: the formulas with every free occurrence of a
51
+ variable replaced, and a mapping from each variable's name to the constant that
52
+ stands for it. A variable that is bound inside a formula is left alone there (the
53
+ substitution is capture-avoiding), and a name that is free in one formula and bound
54
+ in another is replaced only where it is free. Formulas without a free variable
55
+ come back as they are.
56
+
57
+ By default the constants are minted (``_p0``, ``_p1``, …) and none of them has the
58
+ spelling of any name that ``formulas`` carry -- a predicate, function, constant,
59
+ sort, variable or nominal -- nor of any name in ``avoid``, so no constant of the
60
+ problem is captured by a parameter. Pass ``avoid`` for names of a vocabulary the
61
+ problem will be read together with.
62
+
63
+ With ``after_variables=True`` the constant carries the variable's own name, so that
64
+ a structure found for the closed formulas reports the parameter under the name the
65
+ caller wrote. A parameter that would then have the name of a constant of the
66
+ problem (``Constant('x')`` next to a free variable ``x``: the Unicode text tells the
67
+ two apart, ``P(x) ∧ Q('x')``, but a structure holds one entry per name) cannot be
68
+ told from that constant in a table keyed by name, so it is refused.
69
+
70
+ Raises:
71
+ NotImplementedError: ``after_variables`` is true and a free variable has the
72
+ name of a constant (plain or sorted) of ``formulas``.
73
+ """
74
+ formulas = list(formulas)
75
+ free_per_formula = [_free_names(formula) for formula in formulas]
76
+ names = sorted(set().union(*free_per_formula)) if free_per_formula else []
77
+ if not names:
78
+ return formulas, {}
79
+ parameters: Dict[str, Constant] = {}
80
+ if after_variables:
81
+ constants = {node.name for formula in formulas for node in formula.walk()
82
+ if isinstance(node, (Constant, SortedConstant))}
83
+ for name in names:
84
+ if name in constants:
85
+ raise NotImplementedError(
86
+ f"a free variable {name!r} and a constant {name!r} are in one problem: "
87
+ "a structure holds one entry per name, so the parameter that stands "
88
+ "for the variable could not be told from the constant. Rename one of "
89
+ "them.")
90
+ parameters[name] = Constant(name)
91
+ else:
92
+ taken = set(symbol_names(*formulas)) | set(avoid)
93
+ index = 0
94
+ for name in names:
95
+ while f"_p{index}" in taken:
96
+ index += 1
97
+ parameters[name] = Constant(f"_p{index}")
98
+ taken.add(f"_p{index}")
99
+ index += 1
100
+ closed = []
101
+ for formula, free in zip(formulas, free_per_formula):
102
+ for name in sorted(free):
103
+ formula = substitute(formula, Variable(name), parameters[name])
104
+ closed.append(formula)
105
+ return closed, parameters
@@ -0,0 +1,448 @@
1
+ """Third-order AST: a predicate standing in ARGUMENT position, and its typing.
2
+
3
+ Second-order syntax lets a predicate variable be *bound* (``∀P φ``); it still
4
+ only ever appears as the HEAD of an application, ``P(x)``. Third-order syntax
5
+ adds the one thing that is genuinely a level up: a predicate whose ARGUMENT is
6
+ itself a predicate — ``Positive(G)``, ``Essence(G, x)``,
7
+ ``Positive(λx. ¬G(x))``. No amount of extra quantification reaches that; it is a
8
+ change to the argument layer, which is why it needs its own node
9
+ (:class:`PredicateTerm`), its own grammar modes (``third_order`` /
10
+ ``third_order_modal``, see ``_MODE_ATOM_ARGS`` in ``_fol_nodes.py``) and its own
11
+ typing pass (:func:`analyse_signatures`).
12
+
13
+ **The node.** ``PredicateTerm(name)`` is a leaf that occupies an argument slot
14
+ and denotes the property/relation the name stands for — not a truth value. It is
15
+ deliberately *not* an ``Atom`` with zero arguments: ``P`` as a nullary atom is a
16
+ proposition, ``PredicateTerm("P")`` is the predicate itself, and conflating them
17
+ is exactly the type error the third order exists to make visible. Its ``name``
18
+ field is spelled like ``Variable.name`` / ``Constant.name`` / ``LambdaVar.name``
19
+ so the shared term renderers reach it by the same path.
20
+
21
+ Every first-order back-end refuses it, for the same reason
22
+ ``SecondOrderQuantifier`` does: a property argument has no first-order or
23
+ SMT-LIB reading. The HOL exporters are where it goes
24
+ (:mod:`unicode_logic_kit.hol.thirdorder`,
25
+ :mod:`unicode_logic_kit.hol.ho_modal`).
26
+
27
+ **The typing pass.** A predicate's argument slots are not annotated in the
28
+ surface syntax, so what each slot holds — an individual, or a property of arity
29
+ k — is *inferred*, and the inference has to run over a whole theory rather than
30
+ one formula: ``Positive(Phi)`` alone says nothing about Phi's arity, while
31
+ ``Positive(G)`` together with ``G(x)`` elsewhere in the same axiom set fixes it
32
+ at 1. :func:`analyse_signatures` takes any number of formulas and returns one
33
+ :class:`Signatures` for all of them: it collects the constraints (an application
34
+ fixes its head's arity; a λ-argument fixes its slot's arity by binder depth; a
35
+ PredicateTerm links its own arity to its slot's) and closes them under
36
+ propagation until nothing moves.
37
+
38
+ Three ways that can fail, all raised rather than papered over: a predicate applied
39
+ at two different arities (``ConflictingArityError``, shared with the second-order
40
+ inference), a slot used once for an individual and once for a property
41
+ (:class:`MixedSlotError`), and a slot that would have to hold a property of
42
+ PROPERTIES (:class:`NestedPropertySlotError`): ``Meta(Pos) ∧ Pos(G)`` makes ``Pos`` a
43
+ predicate of properties and puts it in a slot of ``Meta``, a fourth-order typing that
44
+ a slot type ``("p", k)`` (a relation on individuals) cannot state.
45
+
46
+ One way it can be *underdetermined*: a slot no evidence ever reaches — e.g.
47
+ ``∀Phi (Positive(Phi) → □Positive(Phi))`` read entirely on its own, where Phi is
48
+ never applied and shares its slot with nothing that is. Such a slot defaults to
49
+ a property of arity **1**, because argument position is what makes it a property
50
+ slot at all and 1 is the only reading on which the formula says what it is
51
+ plainly meant to say; arity 0 would silently retype it as a predicate over
52
+ PROPOSITIONS. The default is recorded in :attr:`Signatures.defaulted` so a
53
+ caller that would rather refuse than guess can see exactly which slots were
54
+ guessed — the HOL exporters print them as a comment in the emitted theory.
55
+ """
56
+
57
+ from dataclasses import dataclass
58
+ from typing import Dict, FrozenSet, List, Sequence, Set, Tuple
59
+
60
+ from ._fol_nodes import (
61
+ Node, Z3Env, NODE_CLASSES, Atom, PARSER_OPS, ParserOp, parser_ops_for_mode,
62
+ )
63
+ from ._so_nodes import (
64
+ SecondOrderQuantifier, ConflictingArityError, _infer_so_arity,
65
+ )
66
+ from ._truth_constants import truth_value
67
+ from .naming import ParsingError
68
+
69
+
70
+ _NO_EXPORT = (
71
+ "A predicate in argument position (third order) has no first-order or "
72
+ "SMT-LIB reading: its argument denotes a PROPERTY, not an individual. Use "
73
+ "the HOL exporters (hol.thirdorder / hol.ho_modal) instead."
74
+ )
75
+
76
+
77
+ @dataclass(frozen=True)
78
+ class PredicateTerm(Node):
79
+ """A predicate name occupying an argument slot — the third-order term.
80
+
81
+ ``name`` is the predicate's name as written. The node denotes the property
82
+ (or relation) itself, so it is a TERM, not a formula: ``Positive(G)``
83
+ parses to ``Atom("Positive", [PredicateTerm("G")])``, whereas ``Positive``
84
+ applied to the *proposition* G would be ``Atom("Positive", [Atom("G", [])])``.
85
+
86
+ The node carries no arity of its own. What arity the property has is a
87
+ property of the whole theory, not of this occurrence, and is answered by
88
+ :func:`analyse_signatures`.
89
+ """
90
+
91
+ name: str
92
+
93
+ def _tree_parts(self):
94
+ """Return the ``PredicateTerm: G`` label and no children (it is a leaf)."""
95
+ return f"PredicateTerm: {self.name}", []
96
+
97
+ def to_dict(self):
98
+ """Serialise to dict with type tag and predicate name."""
99
+ return {"_type": "PredicateTerm", "name": self.name}
100
+
101
+ @staticmethod
102
+ def from_dict(d):
103
+ """Deserialise a PredicateTerm from a dict produced by to_dict."""
104
+ return PredicateTerm(d["name"])
105
+
106
+ def to_z3(self, env: Z3Env = None):
107
+ """Reject Z3 export: a property argument is not SMT-expressible."""
108
+ raise NotImplementedError(_NO_EXPORT)
109
+
110
+ def to_prover9(self) -> str:
111
+ """Reject Prover9 export: a property argument is not first-order."""
112
+ raise NotImplementedError(_NO_EXPORT)
113
+
114
+ def to_tptp(self) -> str:
115
+ """Reject TPTP (FOF) export: a property argument is not first-order."""
116
+ raise NotImplementedError(_NO_EXPORT)
117
+
118
+
119
+ NODE_CLASSES.update({"PredicateTerm": PredicateTerm})
120
+
121
+
122
+ # =========================
123
+ # Signature inference
124
+ # =========================
125
+
126
+
127
+ class MixedSlotError(ParsingError):
128
+ """Raised when one argument slot is used for both an individual and a property.
129
+
130
+ ``Loves(x, y)`` and ``Loves(x, G)`` cannot both be right: the second slot
131
+ would have to hold an individual and a property at once. Subclasses
132
+ ParsingError so a caller catching the parser's error type also catches this.
133
+ """
134
+
135
+ def __init__(self, predicate: str, position: int):
136
+ message = (
137
+ f"TYPE_ERROR: argument slot {position} of '{predicate}' is used "
138
+ f"both for an individual and for a predicate; a slot holds one or "
139
+ f"the other, not both."
140
+ )
141
+ self.args = (message,)
142
+
143
+ def __str__(self):
144
+ return self.args[0]
145
+
146
+
147
+ class NestedPropertySlotError(ParsingError):
148
+ """Raised when a property slot would hold a predicate that itself takes a property.
149
+
150
+ A slot that holds a predicate is typed as a property of INDIVIDUALS, of the arity of that
151
+ predicate. ``Meta(Pos)`` together with ``Pos(G)`` breaks that: ``Pos`` takes a property,
152
+ so it is a predicate of properties, and what ``Meta`` takes is then a property of
153
+ properties (a fourth-order typing). The typing cannot state it, so it is refused instead of
154
+ being typed as if ``Pos`` were a property of individuals. Subclasses ParsingError, like
155
+ :class:`MixedSlotError`.
156
+ """
157
+
158
+ def __init__(self, host: str, host_position: int, occupant: str, occupant_position: int):
159
+ message = (
160
+ f"TYPE_ERROR: '{host}' takes the predicate '{occupant}' as a property in "
161
+ f"argument slot {host_position}, but '{occupant}' itself takes a property in its "
162
+ f"argument slot {occupant_position}: '{host}' would be a predicate of predicates "
163
+ f"of properties (fourth order or higher). A property slot holds a relation on "
164
+ f"individuals only, so this is refused rather than typed as if '{occupant}' were "
165
+ f"a property of individuals."
166
+ )
167
+ self.args = (message,)
168
+
169
+ def __str__(self):
170
+ return self.args[0]
171
+
172
+
173
+ #: What one argument slot holds. ``"i"`` is an individual; ``("p", k)`` is a
174
+ #: property/relation of arity ``k``.
175
+ INDIVIDUAL = "i"
176
+
177
+
178
+ @dataclass(frozen=True)
179
+ class Signatures:
180
+ """The result of :func:`analyse_signatures` over one or more formulas.
181
+
182
+ ``arity`` maps every predicate NAME that is applied (or reached by
183
+ propagation) to its arity. ``slots`` maps a predicate name to its argument
184
+ signature — a tuple with one entry per position, each either
185
+ :data:`INDIVIDUAL` or ``("p", k)`` for a property of arity ``k``.
186
+ ``defaulted`` names the ``(predicate, position)`` pairs whose property arity
187
+ no evidence fixed and which therefore defaulted to 1.
188
+ """
189
+
190
+ arity: Dict[str, int]
191
+ slots: Dict[str, Tuple[object, ...]]
192
+ defaulted: FrozenSet[Tuple[str, int]]
193
+
194
+ def is_third_order(self) -> bool:
195
+ """True iff some predicate takes a property argument (i.e. the theory really is third order)."""
196
+ return any(s != INDIVIDUAL for sig in self.slots.values() for s in sig)
197
+
198
+ def property_slots(self) -> List[Tuple[str, int, int]]:
199
+ """Return ``(predicate, position, arity)`` for every property-holding slot, sorted."""
200
+ out = []
201
+ for pred in sorted(self.slots):
202
+ for pos, kind in enumerate(self.slots[pred]):
203
+ if kind != INDIVIDUAL:
204
+ out.append((pred, pos, kind[1]))
205
+ return out
206
+
207
+
208
+ def _lambda_depth(node: Node) -> int:
209
+ """Return how many leading λ binders ``node`` has (0 if it is not a Lambda).
210
+
211
+ ``λx. λy. φ`` is a binary relation, so its depth IS the arity of the
212
+ property it denotes. Imported lazily: Lambda lives in _msfl_nodes, which
213
+ imports this module's dependencies rather than the other way round.
214
+ """
215
+ from ._msfl_nodes import Lambda
216
+ depth = 0
217
+ while isinstance(node, Lambda):
218
+ depth += 1
219
+ node = node.body
220
+ return depth
221
+
222
+
223
+ def analyse_signatures(formulas: Sequence[Node]) -> Signatures:
224
+ """Infer every predicate's arity and argument signature across ``formulas``.
225
+
226
+ Runs over the formulas TOGETHER, because that is the scope on which the
227
+ answer is determined: ``Positive(Phi)`` in one axiom and ``Phi(x)`` in
228
+ another jointly fix Phi's arity at 1, and neither does so alone.
229
+
230
+ Raises ConflictingArityError if a predicate is applied at two arities,
231
+ :class:`MixedSlotError` if one slot holds an individual in one place and a
232
+ property in another, and :class:`NestedPropertySlotError` if a predicate that
233
+ stands in a property slot itself takes a property (a property of properties,
234
+ which the typing cannot state). A property slot that no evidence reaches defaults to
235
+ arity 1 and is listed in the result's ``defaulted`` — see this module's
236
+ docstring for why 1 and not 0.
237
+ """
238
+ app_arity: Dict[str, Set[int]] = {}
239
+ # slot_kind[(pred, pos)] is INDIVIDUAL, or ("p", None) until its arity is known.
240
+ slot_kind: Dict[Tuple[str, int], object] = {}
241
+ # Which predicate NAMES occupy which slots (a slot may be occupied by several).
242
+ slot_occupants: Dict[Tuple[str, int], Set[str]] = {}
243
+ # Property arities pinned directly by a λ-argument's binder depth.
244
+ slot_lambda_arity: Dict[Tuple[str, int], Set[int]] = {}
245
+
246
+ def note_slot(pred: str, pos: int, kind: object) -> None:
247
+ prev = slot_kind.get((pred, pos))
248
+ if prev is None:
249
+ slot_kind[(pred, pos)] = kind
250
+ return
251
+ prev_is_prop = prev != INDIVIDUAL
252
+ now_is_prop = kind != INDIVIDUAL
253
+ if prev_is_prop != now_is_prop:
254
+ raise MixedSlotError(pred, pos)
255
+
256
+ def visit(node: Node) -> None:
257
+ if isinstance(node, Atom) and truth_value(node) is None:
258
+ # (`$true` / `$false` are constants, not predicates: nothing to type.)
259
+ app_arity.setdefault(node.predicate, set()).add(len(node.args))
260
+ for pos, arg in enumerate(node.args):
261
+ if isinstance(arg, PredicateTerm):
262
+ note_slot(node.predicate, pos, ("p", None))
263
+ slot_occupants.setdefault((node.predicate, pos), set()).add(arg.name)
264
+ elif _lambda_depth(arg) > 0:
265
+ note_slot(node.predicate, pos, ("p", None))
266
+ slot_lambda_arity.setdefault((node.predicate, pos), set()).add(
267
+ _lambda_depth(arg))
268
+ else:
269
+ note_slot(node.predicate, pos, INDIVIDUAL)
270
+ for child in node._child_nodes():
271
+ visit(child)
272
+
273
+ for formula in formulas:
274
+ visit(formula)
275
+
276
+ # An application fixes its head's arity outright; two different arities for
277
+ # one name is the same error the second-order inference already raises.
278
+ arity: Dict[str, int] = {}
279
+ for pred, arities in app_arity.items():
280
+ if len(arities) > 1:
281
+ raise ConflictingArityError(pred, sorted(arities))
282
+ arity[pred] = next(iter(arities))
283
+
284
+ # A λ-argument pins its slot's arity directly.
285
+ slot_arity: Dict[Tuple[str, int], int] = {}
286
+ for slot, depths in slot_lambda_arity.items():
287
+ if len(depths) > 1:
288
+ raise ConflictingArityError(f"{slot[0]}[{slot[1]}]", sorted(depths))
289
+ slot_arity[slot] = next(iter(depths))
290
+
291
+ # A predicate that sits in a property slot is typed as a property of INDIVIDUALS (a slot
292
+ # type is ("p", k), a relation on individuals). One that takes a property itself is a
293
+ # predicate of properties, and the slot would have to hold a property of properties.
294
+ property_position: Dict[str, int] = {}
295
+ for (name, position), kind in sorted(slot_kind.items()):
296
+ if kind != INDIVIDUAL:
297
+ property_position.setdefault(name, position)
298
+ for (host, host_position), occupants in sorted(slot_occupants.items()):
299
+ for occupant in sorted(occupants):
300
+ if occupant in property_position:
301
+ raise NestedPropertySlotError(host, host_position, occupant,
302
+ property_position[occupant])
303
+
304
+ # Close under propagation: a known predicate arity fixes every slot it sits
305
+ # in, and a known slot arity fixes every predicate sitting in it.
306
+ changed = True
307
+ while changed:
308
+ changed = False
309
+ for slot, occupants in slot_occupants.items():
310
+ known = {arity[p] for p in occupants if p in arity}
311
+ if slot in slot_arity:
312
+ known.add(slot_arity[slot])
313
+ if len(known) > 1:
314
+ raise ConflictingArityError(f"{slot[0]}[{slot[1]}]", sorted(known))
315
+ if not known:
316
+ continue
317
+ value = next(iter(known))
318
+ if slot_arity.get(slot) != value:
319
+ slot_arity[slot] = value
320
+ changed = True
321
+ for p in occupants:
322
+ if arity.get(p) != value:
323
+ if p in arity:
324
+ raise ConflictingArityError(p, sorted({arity[p], value}))
325
+ arity[p] = value
326
+ changed = True
327
+
328
+ # Whatever propagation never reached is a property slot with no evidence.
329
+ defaulted: Set[Tuple[str, int]] = set()
330
+ for slot, kind in slot_kind.items():
331
+ if kind == INDIVIDUAL:
332
+ continue
333
+ if slot not in slot_arity:
334
+ slot_arity[slot] = 1
335
+ defaulted.add(slot)
336
+ for p in slot_occupants.get(slot, ()):
337
+ arity.setdefault(p, 1)
338
+
339
+ slots: Dict[str, Tuple[object, ...]] = {}
340
+ for pred, n in arity.items():
341
+ sig = []
342
+ for pos in range(n):
343
+ kind = slot_kind.get((pred, pos), INDIVIDUAL)
344
+ sig.append(INDIVIDUAL if kind == INDIVIDUAL
345
+ else ("p", slot_arity[(pred, pos)]))
346
+ slots[pred] = tuple(sig)
347
+
348
+ return Signatures(arity, slots, frozenset(defaulted))
349
+
350
+
351
+ def has_property_argument(node: Node) -> bool:
352
+ """True iff some predicate application in ``node`` has a property argument.
353
+
354
+ That is the syntactic marker of third order — a PredicateTerm or a
355
+ λ-abstraction sitting in an argument slot. Used to keep the second-order
356
+ path exactly as it was: a formula without one is answered by the cheaper
357
+ application-scan, unchanged, and pays nothing for the richer inference.
358
+ """
359
+ if isinstance(node, Atom):
360
+ for arg in node.args:
361
+ if isinstance(arg, PredicateTerm) or _lambda_depth(arg) > 0:
362
+ return True
363
+ return any(has_property_argument(child) for child in node._child_nodes())
364
+
365
+
366
+ def infer_bound_arity(body: Node, predname: str) -> int:
367
+ """Infer a third-order-bound predicate variable's arity from ``body``.
368
+
369
+ The third-order counterpart of ``_so_nodes._infer_so_arity``: it answers the
370
+ same question, but has one more source of evidence to consult, because in a
371
+ third-order formula ``predname`` may never be applied at all and still be a
372
+ property — ``∀Phi (Positive(Phi) → □Positive(Phi))``. Delegates to
373
+ :func:`analyse_signatures`, whose propagation covers both cases; falls back
374
+ to 0 for a name that genuinely occurs nowhere, matching the second-order
375
+ reading of an unapplied predicate variable as propositional.
376
+
377
+ A body with no property argument at all IS a second-order body, and is sent
378
+ straight to ``_infer_so_arity`` — same answer, same cost, and the
379
+ second-order mode's behaviour is then unchanged by construction rather than
380
+ by inspection.
381
+
382
+ Shadowing follows ``_infer_so_arity``: an inner binder over the SAME name
383
+ starts a fresh binding, so its scope is cut out before the analysis.
384
+ """
385
+ if not has_property_argument(body):
386
+ return _infer_so_arity(body, predname)
387
+ pruned = _prune_shadowed(body, predname)
388
+ signatures = analyse_signatures([pruned])
389
+ return signatures.arity.get(predname, 0)
390
+
391
+
392
+ def _prune_shadowed(node: Node, predname: str) -> Node:
393
+ """Replace every subformula that rebinds ``predname`` with a constant-true stand-in.
394
+
395
+ The inner binder's scope must not contribute evidence about the OUTER
396
+ binding, so it is cut away before the signature analysis runs. The stand-in
397
+ is a zero-argument atom under a reserved name that no source formula can
398
+ spell (it contains a space), so it cannot collide with a real predicate.
399
+ """
400
+ if isinstance(node, SecondOrderQuantifier) and node.predicate == predname:
401
+ return Atom("shadowed scope", [])
402
+ return node.map_children(lambda child: _prune_shadowed(child, predname))
403
+
404
+
405
+ # =========================
406
+ # Grammar-mode assembly
407
+ # =========================
408
+ #
409
+ # The two third-order modes are not a new operator set — they are an EXISTING
410
+ # operator set over the widened argument layer. ``third_order`` is the
411
+ # second-order mode's operators; ``third_order_modal`` is those plus the whole
412
+ # modal family. Rather than re-registering ~40 ParserOps (which would drift the
413
+ # moment an operator is added to one mode and not the other), each is CLONED
414
+ # from its sources, so the third-order modes accept exactly what their base
415
+ # modes accept, by construction. ``tests/test_third_order.py`` pins that
416
+ # correspondence.
417
+
418
+
419
+ def _clone_parser_ops(target: str, sources: Sequence[str]) -> List[ParserOp]:
420
+ """Register into ``target`` a copy of every ParserOp of the ``sources``, deduped.
421
+
422
+ Two source modes overlap (the classical connectives and the first-order
423
+ quantifier are registered for both ``modal`` and ``second_order``); an
424
+ operator is taken from whichever source names it first and skipped
425
+ afterwards, keyed on the parse-level identity that matters —
426
+ ``(level, rule_alias, grammar, only_name)``. Appends to PARSER_OPS and
427
+ returns what was appended.
428
+ """
429
+ seen = set()
430
+ added: List[ParserOp] = []
431
+ for source in sources:
432
+ for op in parser_ops_for_mode(source):
433
+ key = (op.level, op.rule_alias, op.grammar, op.only_name)
434
+ if key in seen:
435
+ continue
436
+ seen.add(key)
437
+ clone = ParserOp(target, op.level, op.terminal_name, op.terminal_def,
438
+ op.grammar, op.rule_alias, op.transform,
439
+ op.node_class, op.only_name)
440
+ PARSER_OPS.append(clone)
441
+ added.append(clone)
442
+ return added
443
+
444
+
445
+ # NOT called here. Cloning has to happen after EVERY module that registers an
446
+ # operator for a source mode has run -- _hybrid_nodes registers @i / nominals
447
+ # for "modal" and is imported after this module's own dependencies -- so the
448
+ # call site is nodes.py, which is the one place that controls that order.