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,581 @@
1
+ """Intuitionistic propositional logic — Kripke semantics and a validity/countermodel search.
2
+
3
+ Intuitionistic logic drops the law of excluded middle and double-negation elimination;
4
+ its models are **Kripke models**: a partial order of *worlds* (stages of knowledge)
5
+ with a **monotone** forcing relation (once an atom is forced at a world it is forced at
6
+ every later world). The connectives ``→`` and ``¬`` quantify over future worlds, which
7
+ is exactly what makes ``P ∨ ¬P`` and ``¬¬P → P`` fail.
8
+
9
+ - :class:`IntKripkeModel` — build a model and ask ``model.forces(world, φ)``.
10
+ - :func:`int_valid` — is ``φ`` intuitionistically valid? A propositional formula is
11
+ decided exactly: the search over small Kripke models is tried first, and what it
12
+ does not settle is handed to the G4ip calculus of :func:`~unicode_logic_kit.atp.lj.int_prove`.
13
+ - :func:`int_countermodel` — a model + world where ``φ`` fails (a witness that it is
14
+ *not* intuitionistically valid), or None. ``None`` says that no such model was found
15
+ among the models of at most ``max_worlds`` worlds; it is NOT a proof of validity.
16
+ Intuitionistic propositional logic has the finite-model property, but the size of the
17
+ smallest countermodel grows with the formula: ``(P → Q) ∨ (Q → R) ∨ (R → P)`` is not
18
+ valid and has no countermodel with fewer than four worlds, so at the default bound of
19
+ three worlds the search comes up empty on it. Only :func:`int_valid` (which goes on to
20
+ the calculus) and :func:`~unicode_logic_kit.atp.lj.int_decide` answer for validity.
21
+
22
+ The **propositional** fragment is decided exactly by :func:`int_valid`. The
23
+ **first-order** fragment (``∀x`` / ``∃x``) is handled over *increasing-domain* Kripke
24
+ models — ``w ⊩ ∀x φ`` quantifies over every later world and every individual existing
25
+ there — by a *bounded* search: a counter-model it finds genuinely refutes validity,
26
+ but a clean search does **not** prove validity (first-order intuitionistic logic is
27
+ undecidable, and some non-theorems such as the double-negation shift have only infinite
28
+ counter-models, being valid in every finite model). Every intuitionistic validity is
29
+ also classically valid, which the test-suite cross-checks.
30
+
31
+ **A free variable is a parameter.** ``Γ ⊨ φ`` is the consequence relation of the
32
+ textbooks, over every model, every world and every assignment: a variable that is free
33
+ in a formula names ONE unknown individual, the same in every part of the formula, and an
34
+ assignment gives it an individual that exists at the world of evaluation. In an
35
+ increasing-domain model that individual then exists at every later world too, so the
36
+ search reads the parameter as a CONSTANT named like the variable: rigid, and a member of
37
+ every world's domain. Nothing is lost by it: a formula is checked at every world of
38
+ every model, and the submodel generated by a world has the individual in all of its
39
+ domains. ``P(x) → P(a)`` is not valid (one world, domain ``{x, a}``, ``P`` of ``x``
40
+ only), ``P(x) → P(x)``, ``P(x) → ∃y P(y)`` and ``∀y P(y) → P(x)`` are, ``(P(x) ∧ Q(y)) →
41
+ ∀z (P(z) ∧ Q(z))`` is not (``P`` of ``x``, ``Q`` of ``y``, two elements); for a formula
42
+ without a premise the reading coincides with the universal closure. The model that
43
+ :func:`int_countermodel` returns has the individual under the variable's own name; a
44
+ variable and a constant spelled alike would be one individual there, so the pair is
45
+ refused by name. A formula with no quantifier is propositional whatever its terms are:
46
+ an atom is a letter, a variable in it stands for itself, and two atoms whose terms differ
47
+ are two letters, which is what a parameter allows (no quantifier-free formula can tie
48
+ two terms together, and the identity atom is refused).
49
+
50
+ A **numeral** in an atom of the first-order search is the constant of its value
51
+ (``1`` and ``1.0`` are one; a constant spelled like a numeral of the same formula is
52
+ refused by name), and a predicate used at two arities is two predicates.
53
+
54
+ **Many-sorted formulas** (``SortedQuantifier``, ``∀x:S φ`` / ``∃x:S φ``) are decided by
55
+ delegating ONCE, at the top of :func:`int_valid` / :func:`int_countermodel`, to the same
56
+ ``_relativize`` reduction :mod:`unicode_logic_kit.fol.to_fol` uses (``∀x:S φ`` → ``∀x (S(x)
57
+ → φ)``, ``∃x:S φ`` → ``∃x (S(x) ∧ φ)``) — done ONCE for the WHOLE formula, rather than
58
+ node-by-node inside the search, so a ``SortedConstant`` anywhere in the formula (not only
59
+ one directly under a ``SortedQuantifier``) is also turned into a plain ``Constant`` before
60
+ :func:`_fo_countermodel` scans for constants/predicates. The sort guard ``S(x)`` becomes an
61
+ ordinary atom, forced exactly like any other predicate — and every atom's valuation in this
62
+ module is already required to be MONOTONE (up-closed along the order, see
63
+ :func:`_upclosed_subsets` / :func:`_monotone_valuations`), so a sort is "stage-stable" (once
64
+ forced, stays forced at every later stage) but not fully rigid (an individual need not
65
+ already be of sort ``S`` at the initial stage) — the natural reading given the module's
66
+ existing atom semantics, and the same "world/stage-relative, not rigid" choice
67
+ :func:`~unicode_logic_kit.semantics.kripke.satisfies_modal` makes for the modal routes.
68
+
69
+ Non-emptiness and membership: the classical many-sorted routes (``api.prove`` et al.)
70
+ always assume every sort is non-empty and every sorted constant ``c:S`` is an element of
71
+ ``S``, by adding :func:`~unicode_logic_kit.fol._msfl_nodes.sort_axioms` as extra premises —
72
+ without the matching assumption here, a schema like ``∀x:S P(x) → ∃x:S P(x)`` would come
73
+ out intuitionistically INVALID (a countermodel with ``S`` empty everywhere) while being
74
+ classically valid, and ``∀x:Human Mortal(x) → Mortal(socrates:Human)`` would have the
75
+ countermodel in which ``socrates`` is no ``Human`` — the "bare relativisation is
76
+ inconsistent with the classical routes" trap the sort-guard-relativisation approach
77
+ warns about. So :func:`int_valid` / :func:`int_countermodel` fold those same sentences
78
+ in as EXTRA PREMISES — checking ``⋀sort_axioms → relativised-formula`` — the
79
+ standard "no separate premise list" trick, sound both classically and intuitionistically
80
+ for a FINITE premise set (the deduction theorem holds in both). A many-sorted formula's
81
+ :func:`int_valid` / :func:`int_countermodel` verdict therefore agrees with
82
+ ``api.prove``/Z3's on any MODAL-FREE sorted formula the propositional-or-bounded-first-order
83
+ fragments both decide — see ``tests/test_sorted_modal.py``.
84
+
85
+ Public API: :class:`IntKripkeModel`, :func:`int_valid`, :func:`int_countermodel`.
86
+ """
87
+
88
+ from dataclasses import dataclass
89
+ from itertools import combinations, product
90
+ from typing import Dict, FrozenSet, List, Optional, Tuple
91
+
92
+ from ..fol.nodes import (
93
+ Node, Atom, Not, And, Or, Xor, Implies, Iff, Quantifier, SortedQuantifier,
94
+ Constant, Variable, Function, substitute,
95
+ )
96
+ from ..fol._so_nodes import SecondOrderQuantifier
97
+ from ..fol._atom_keys import find_key
98
+ from ..fol._msfl_nodes import key_text, sort_axioms
99
+ from ..fol._free_parameters import parameterize
100
+ from ..fol._numeral_symbols import numerals_as_constants, numeral_name
101
+ from ..fol._truth_constants import truth_value as _truth_value
102
+ from ._modal_reject import reject_equality_in
103
+
104
+
105
+ def _is_propositional(formula: Node) -> bool:
106
+ """True iff ``formula`` has no quantifier (the decidable propositional fragment).
107
+
108
+ Checks for ``SortedQuantifier`` too, even though ``int_valid``/
109
+ ``int_countermodel`` always relativize a many-sorted formula away (see
110
+ :func:`_prepare_many_sorted`) before this is ever called on it from those
111
+ two entry points -- so in practice this branch never fires there. It is
112
+ kept anyway: a caller may run this directly on a formula that still has
113
+ its ``SortedQuantifier`` nodes, and it must answer correctly there too.
114
+ """
115
+ return not any(
116
+ isinstance(node, (Quantifier, SortedQuantifier, SecondOrderQuantifier))
117
+ for node in formula.walk())
118
+
119
+
120
+ def _prepare_many_sorted(formula: Node) -> Node:
121
+ """Relativize a many-sorted formula ONCE, for the search entry points.
122
+
123
+ ``int_valid``/``int_countermodel`` call this before anything else. Two
124
+ things happen together, both needed for the search to agree with the
125
+ classical many-sorted routes on a many-sorted formula (see the module
126
+ docstring's "Many-sorted formulas" section):
127
+
128
+ 1. The WHOLE formula is relativized in one pass (``formula._relativize([])``
129
+ — the same reduction :func:`~unicode_logic_kit.fol.to_fol` uses), not
130
+ node-by-node during the search. Doing it once, up front, also turns a
131
+ ``SortedConstant`` occurring ANYWHERE in the formula (not only directly
132
+ under a ``SortedQuantifier``) into a plain ``Constant`` before
133
+ :func:`_fo_countermodel` scans the formula for its constants — a
134
+ per-node fix inside the search would miss that case.
135
+ 2. The two facts every classical route assumes of a sort
136
+ (:func:`~unicode_logic_kit.fol._msfl_nodes.sort_axioms`: no sort is empty,
137
+ and a sorted constant ``c:S`` is an element of ``S``) are folded in
138
+ here as an extra premise too — checking
139
+ ``⋀sort-axioms → relativized-formula`` in place of the bare relativized
140
+ formula. Sound both classically and intuitionistically for a finite
141
+ premise set (the deduction theorem holds in both), and it is what makes
142
+ e.g. ``∀x:S P(x) → ∃x:S P(x)`` agree with the classical Z3 verdict here
143
+ instead of finding an empty-``S`` countermodel, and
144
+ ``∀x:Human Mortal(x) → Mortal(socrates:Human)`` valid (``socrates`` is a
145
+ ``Human``) while the same formula with a plain ``socrates`` is not.
146
+
147
+ The membership fact is an ANTECEDENT, not a conjunct of the formula. A
148
+ valuation is up-closed, so ``S(c)`` forced at a stage is forced at every
149
+ later one, and the clause for ``→`` only looks upward: the wrapped
150
+ formula is forced everywhere in every model iff the relativized formula
151
+ is forced in every model in which ``S(c)`` is forced at every stage
152
+ (restrict a refuting model to the up-set of the stage where the
153
+ antecedent holds). That is exactly "the constant is in its sort", with
154
+ the sort a stage-relative atom like every other one here.
155
+
156
+ Returns ``formula`` UNCHANGED (same object, not even copied) when it has
157
+ no many-sorted node at all — ``sort_axioms`` returning nothing IS
158
+ exactly "no ``SortedQuantifier``/``SortedConstant``/``SortedCount``/
159
+ ``SortedCardinality`` anywhere in it" (the same four types
160
+ ``_relativize`` would otherwise eliminate), so every call site that never
161
+ used many_sorted sees byte-identical behaviour to before this existed.
162
+ """
163
+ axioms = sort_axioms(formula)
164
+ if not axioms:
165
+ return formula
166
+ relativized = formula._relativize([])
167
+ hyp = axioms[0]
168
+ for ax in axioms[1:]:
169
+ hyp = And(hyp, ax)
170
+ return Implies(hyp, relativized)
171
+
172
+
173
+ #: Identity is not one of the atoms this search can read. A world's valuation is
174
+ #: a set of atom KEYS (the text of the atom with every constant written by its name),
175
+ #: monotone along the order, so
176
+ #: ``a = b`` would be an unconstrained letter: ``int_valid(a = a)`` came back False
177
+ #: and ``int_countermodel(a = a)`` handed back a one-world model as if it refuted
178
+ #: reflexivity (both measured on 0.28.1). The Gödel–McKinsey–Tarski embedding
179
+ #: (:mod:`unicode_logic_kit.hol.intuitionistic`) already refuses the atom by name, and
180
+ #: THIS module is the oracle it is checked against, so the two have to agree about
181
+ #: what they will not answer. Intuitionistic logic WITH equality needs a term
182
+ #: semantics neither this search nor G4ip has, and it is not part of the GMT theorem.
183
+ _EQUALITY_ROUTE = "the intuitionistic Kripke search"
184
+ _EQUALITY_ATOM_READING = ("a world's valuation is a monotone set of atom keys, "
185
+ "and no term is interpreted")
186
+ _EQUALITY_INSTEAD = (
187
+ "Decide identity classically with unicode_logic_kit.api.prove, or on the "
188
+ "quantified modal route with unicode_logic_kit.fol.qml.qml_is_valid (rigid "
189
+ "identity over the object domain); intuitionistic logic with equality is not "
190
+ "what this search or atp.lj.int_prove decides."
191
+ )
192
+
193
+
194
+ def _reject_equality_everywhere(formula: Node, caller: str) -> None:
195
+ """Refuse an identity atom anywhere in ``formula``, before any search.
196
+
197
+ A whole-tree scan at the entry point, not a check inside the evaluator: the
198
+ search stops at the first refuting model, so a formula whose countermodel is
199
+ found before the identity atom is reached would come back with a verdict that
200
+ never read it — ``(p ∧ ¬p) → (a = b)`` is the clean case, intuitionistically
201
+ valid without the atom mattering at all.
202
+ """
203
+ reject_equality_in(formula, caller, _EQUALITY_ROUTE,
204
+ atom_reading=_EQUALITY_ATOM_READING,
205
+ instead=_EQUALITY_INSTEAD)
206
+
207
+
208
+ def _reject_second_order(formula: Node) -> None:
209
+ """Reject a second-order quantifier or a function term in the FO search.
210
+
211
+ A sorted quantifier/constant needs no rejection here any more: by the
212
+ time :func:`_fo_countermodel` calls this, ``int_valid``/
213
+ ``int_countermodel`` have already relativized it away (see
214
+ :func:`_prepare_many_sorted`), so it is an ordinary (unsorted)
215
+ first-order formula.
216
+ """
217
+ for node in formula.walk():
218
+ if isinstance(node, SecondOrderQuantifier):
219
+ raise ValueError("intuitionistic: second-order quantifiers are not supported.")
220
+ if isinstance(node, Function):
221
+ raise ValueError(
222
+ "intuitionistic: function terms are not supported in the first-order "
223
+ "Kripke search; use predicates over variables and constants.")
224
+
225
+
226
+ @dataclass(frozen=True)
227
+ class IntKripkeModel:
228
+ """A finite intuitionistic Kripke model over the worlds ``0`` to ``n-1``.
229
+
230
+ ``upset[w]`` is the set of worlds accessible from ``w`` (its up-set in the partial
231
+ order, reflexive and transitive — including ``w`` itself). ``valuation[key]`` is the
232
+ up-closed set of worlds forcing the atom ``key`` (its text with every constant written by
233
+ its name, ``P(a)`` for the atom ``P`` of the constant ``a``; the text of the atom as a
234
+ formula, ``P('a')``, is read as the same key, and a table that holds both spellings with
235
+ different sets of worlds is refused). Build directly, or let :func:`int_countermodel`
236
+ produce one, keyed by the names.
237
+ """
238
+
239
+ upset: Dict[int, FrozenSet[int]]
240
+ valuation: Dict[str, FrozenSet[int]]
241
+ domains: Optional[Dict[int, FrozenSet]] = None
242
+
243
+ def forces(self, world: int, formula: Node) -> bool:
244
+ """Return whether ``world`` forces ``formula`` (the intuitionistic clauses).
245
+
246
+ First-order clauses (when the model carries per-world ``domains``, which must
247
+ be **increasing** along the order): ``w ⊩ ∀x φ`` iff for every later ``w' ≥ w``
248
+ and every ``d ∈ D_{w'}``, ``w' ⊩ φ[x:=d]``; ``w ⊩ ∃x φ`` iff some ``d ∈ D_w``
249
+ has ``w ⊩ φ[x:=d]``.
250
+ """
251
+ if isinstance(formula, Quantifier):
252
+ if self.domains is None:
253
+ raise ValueError(
254
+ "intuitionistic forcing: a quantifier needs per-world domains "
255
+ "(build the model with domains={world: [...]}).")
256
+ var = formula.variable
257
+ if formula.type in ("∀", "forall"):
258
+ return all(
259
+ self.forces(w2, substitute(formula.formula, var, Constant(d)))
260
+ for w2 in self.upset[world] for d in self.domains[w2])
261
+ if formula.type in ("∃", "exists"):
262
+ return any(
263
+ self.forces(world, substitute(formula.formula, var, Constant(d)))
264
+ for d in self.domains[world])
265
+ raise ValueError(f"intuitionistic forcing: unknown quantifier {formula.type!r}")
266
+ if isinstance(formula, Atom):
267
+ constant = _truth_value(formula)
268
+ if constant is not None:
269
+ return constant # `$true` is forced at every world, `$false` at none
270
+ # The key first, then the formula text: the elements of the domain are substituted
271
+ # as constants (``Constant("a")`` is ``'a'`` in the text of a formula), the
272
+ # valuation of a model is keyed ``P(a)``, and a caller who keyed it by the text of
273
+ # a hand-built atom, ``P('a')``, is read the same.
274
+ found = find_key(self.valuation, formula)
275
+ return found is not None and world in self.valuation[found]
276
+ if isinstance(formula, And):
277
+ return self.forces(world, formula.left) and self.forces(world, formula.right)
278
+ if isinstance(formula, Or):
279
+ return self.forces(world, formula.left) or self.forces(world, formula.right)
280
+ if isinstance(formula, Implies):
281
+ # w ⊩ A→B iff for every w' ≥ w, w' ⊩ A implies w' ⊩ B.
282
+ return all(self.forces(w2, formula.right)
283
+ for w2 in self.upset[world] if self.forces(w2, formula.left))
284
+ if isinstance(formula, Not):
285
+ # w ⊩ ¬A iff no w' ≥ w forces A.
286
+ return not any(self.forces(w2, formula.formula) for w2 in self.upset[world])
287
+ if isinstance(formula, Iff):
288
+ return (self.forces(world, Implies(formula.left, formula.right))
289
+ and self.forces(world, Implies(formula.right, formula.left)))
290
+ if isinstance(formula, Xor):
291
+ # Defined intuitionistically as (A ∨ B) ∧ ¬(A ∧ B).
292
+ a, b = formula.left, formula.right
293
+ return self.forces(world, And(Or(a, b), Not(And(a, b))))
294
+ raise ValueError(f"intuitionistic forcing: unsupported node "
295
+ f"{type(formula).__name__}")
296
+
297
+
298
+ def _atom_keys(formula: Node) -> List[str]:
299
+ """Distinct atom keys (propositional variables) in ``formula``.
300
+
301
+ A letter is named by the text its atom prints as, so two different atoms that print
302
+ alike (the numeral ``1`` and a constant named ``1``, a free variable ``x`` and a constant
303
+ named ``x``) would be one letter and the formula another problem: such a pair is refused
304
+ by name (``NotImplementedError``). ``$true`` / ``$false`` are constants, not letters.
305
+ """
306
+ from ..fol._atom_keys import AtomKeys
307
+ return AtomKeys("int_countermodel").letters([formula])
308
+
309
+
310
+ def _partial_orders(n: int):
311
+ """Yield each partial order on ``{0..n-1}`` as a dict ``world -> up-set frozenset``.
312
+
313
+ Enumerates reflexive relations and keeps the transitive (and antisymmetric) ones;
314
+ the up-set ``upset[w] = {w' : w ≤ w'}`` is what the forcing clauses consult.
315
+ """
316
+ worlds = list(range(n))
317
+ off_diagonal = [(i, j) for i in worlds for j in worlds if i != j]
318
+ for mask in product((False, True), repeat=len(off_diagonal)):
319
+ leq = {(i, i) for i in worlds}
320
+ leq |= {pair for pair, inc in zip(off_diagonal, mask) if inc}
321
+ # transitivity
322
+ if any((a, b) in leq and (b, c) in leq and (a, c) not in leq
323
+ for a in worlds for b in worlds for c in worlds):
324
+ continue
325
+ # antisymmetry (a genuine partial order; preorders collapse to these for forcing)
326
+ if any(a != b and (a, b) in leq and (b, a) in leq for a in worlds for b in worlds):
327
+ continue
328
+ yield {w: frozenset(j for j in worlds if (w, j) in leq) for w in worlds}
329
+
330
+
331
+ def _monotone_valuations(upset: Dict[int, FrozenSet[int]], keys: List[str]):
332
+ """Yield every monotone (up-closed) valuation of ``keys`` over the order ``upset``."""
333
+ worlds = list(upset)
334
+ # Up-closed subsets: a set S with w∈S ⇒ upset[w] ⊆ S.
335
+ upclosed = []
336
+ for r in range(len(worlds) + 1):
337
+ for combo in combinations(worlds, r):
338
+ s = frozenset(combo)
339
+ if all(upset[w] <= s for w in s):
340
+ upclosed.append(s)
341
+ for choice in product(upclosed, repeat=len(keys)):
342
+ yield dict(zip(keys, choice))
343
+
344
+
345
+ def _upclosed_subsets(upset: Dict[int, FrozenSet[int]], allowed) -> List[FrozenSet[int]]:
346
+ """Every up-closed subset of the (itself up-closed) world set ``allowed``."""
347
+ worlds = sorted(allowed)
348
+ out: List[FrozenSet[int]] = []
349
+ for r in range(len(worlds) + 1):
350
+ for combo in combinations(worlds, r):
351
+ s = frozenset(combo)
352
+ if all(upset[w] <= s for w in s):
353
+ out.append(s)
354
+ return out
355
+
356
+
357
+ def _increasing_domains(upset: Dict[int, FrozenSet[int]], consts: List[str], n_fresh: int):
358
+ """Yield each increasing per-world domain over ``consts`` + up to ``n_fresh`` elements.
359
+
360
+ Constants exist at every world (rigid). A base fresh element exists everywhere
361
+ so no world is empty; each further fresh element appears on an up-closed set of
362
+ worlds (so the domains grow along the order). The fresh elements are named
363
+ ``_e0``, ``_e1``, … and skip every name of ``consts``, so a constant or a parameter
364
+ spelled like one of them is never merged with it.
365
+ """
366
+ worlds = sorted(upset)
367
+ base = set(consts)
368
+ fresh: List[str] = []
369
+ index = 0
370
+ while len(fresh) < n_fresh:
371
+ candidate = f"_e{index}"
372
+ index += 1
373
+ if candidate not in base:
374
+ fresh.append(candidate)
375
+ if not fresh:
376
+ if base:
377
+ yield {w: frozenset(base) for w in worlds}
378
+ return
379
+ always = frozenset(worlds)
380
+ var_fresh = fresh[1:]
381
+ options = _upclosed_subsets(upset, worlds)
382
+ for appearances in product(options, repeat=len(var_fresh)):
383
+ appear = {fresh[0]: always}
384
+ appear.update(zip(var_fresh, appearances))
385
+ domains = {}
386
+ for w in worlds:
387
+ elems = set(base) | {fresh[0]}
388
+ elems.update(e for e in var_fresh if w in appear[e])
389
+ domains[w] = frozenset(elems)
390
+ yield domains
391
+
392
+
393
+ def _read_first_order_terms(formula: Node) -> Node:
394
+ """``formula`` with every term of an atom a constant, a function term or a bound variable.
395
+
396
+ The search gives a valuation to the ground atoms over the individuals of the model
397
+ and to nothing else, so a term of any other kind would leave its atom forced at no
398
+ world at all: ``P(y)`` with ``y`` free would be false in every model, as the
399
+ antecedent of ``P(y) → ∃x Q(x)`` and as the conclusion of ``∀x P(x) → P(y)``. Three
400
+ readings put every such term in the search's reach:
401
+
402
+ * a numeral is the constant of its value (``1`` and ``1.0`` are one constant), and a
403
+ constant spelled like a numeral of the formula is refused by name;
404
+ * a variable that is free is a parameter, read as a constant named like the variable
405
+ (see the module docstring), and a free variable spelled like a constant of the
406
+ formula is refused by name;
407
+ * any other kind of term is refused by name.
408
+
409
+ Raises:
410
+ NotImplementedError: a numeral and a constant are spelled alike, or a free
411
+ variable and a constant are.
412
+ ValueError: an atom holds a term of a kind the search does not read.
413
+ """
414
+ [formula], _ = numerals_as_constants([formula], where="int_countermodel",
415
+ spell=numeral_name)
416
+ [formula], _ = parameterize([formula], after_variables=True)
417
+ for node in formula.walk():
418
+ if isinstance(node, Atom):
419
+ for term in node.args:
420
+ if not isinstance(term, (Variable, Constant, Function)):
421
+ raise ValueError(
422
+ f"intuitionistic: the term {term.to_unicode_str()!r} of "
423
+ f"{node.to_unicode_str()!r} is not a variable, a constant or a "
424
+ "numeral, and the first-order Kripke search gives no atom over "
425
+ "such a term a valuation; use a constant or a variable.")
426
+ return formula
427
+
428
+
429
+ def _fo_countermodel(formula: Node, max_worlds: int, domain_elements: int,
430
+ max_steps: int) -> Optional[Tuple[IntKripkeModel, int]]:
431
+ """Bounded first-order intuitionistic counter-model search (sound; not a decision).
432
+
433
+ Enumerates Kripke models up to ``max_worlds`` worlds with increasing domains drawn
434
+ from the formula's constants plus ``domain_elements`` fresh individuals, and
435
+ monotone predicate valuations respecting element existence. A returned model
436
+ genuinely refutes intuitionistic validity; ``None`` means "no counter-model within
437
+ the bounds" — NOT a proof of validity (first-order intuitionistic logic is
438
+ undecidable). The formula's terms are constants and bound variables only (see
439
+ :func:`_read_first_order_terms`).
440
+ """
441
+ _reject_second_order(formula)
442
+ consts = sorted({n.name for n in formula.walk() if isinstance(n, Constant)})
443
+ preds: Dict[Tuple[str, int], None] = {} # one predicate per (name, arity)
444
+ for node in formula.walk():
445
+ if isinstance(node, Atom) and _truth_value(node) is None:
446
+ preds[(node.predicate, len(node.args))] = None
447
+ steps = 0
448
+ for n in range(1, max_worlds + 1):
449
+ for upset in _partial_orders(n):
450
+ for domains in _increasing_domains(upset, consts, domain_elements):
451
+ pool = sorted(set().union(*domains.values()))
452
+ ground, seen = [], set()
453
+ for name, ar in preds:
454
+ for tup in product(pool, repeat=ar):
455
+ key = key_text(Atom(name, [Constant(t) for t in tup]))
456
+ if key not in seen:
457
+ seen.add(key)
458
+ allowed = frozenset(w for w in upset if set(tup) <= domains[w])
459
+ ground.append((key, _upclosed_subsets(upset, allowed)))
460
+ keys = [k for k, _ in ground]
461
+ option_lists = [opts for _, opts in ground]
462
+ for choice in (product(*option_lists) if ground else [()]):
463
+ steps += 1
464
+ if steps > max_steps:
465
+ return None
466
+ model = IntKripkeModel(upset, dict(zip(keys, choice)), domains)
467
+ for w in range(n):
468
+ if not model.forces(w, formula):
469
+ return model, w
470
+ return None
471
+
472
+
473
+ def int_countermodel(formula: Node, max_worlds: int = 3,
474
+ domain_elements: int = 2,
475
+ max_steps: int = 300000) -> Optional[Tuple[IntKripkeModel, int]]:
476
+ """Return ``(model, world)`` where ``formula`` fails intuitionistically, or None.
477
+
478
+ A returned model genuinely refutes validity: it is a witness that ``formula`` is
479
+ NOT intuitionistically valid. ``None`` never proves validity, for either fragment.
480
+ It says that the bounded search found no countermodel, and the bound is a real one.
481
+ For a **propositional** formula the search runs over every Kripke model of at most
482
+ ``max_worlds`` worlds (intuitionistic propositional logic has the finite-model
483
+ property, but the smallest countermodel of a formula can have more worlds than the
484
+ default allows: ``(P → Q) ∨ (Q → R) ∨ (R → P)`` is not valid, yet ``None`` comes
485
+ back at ``max_worlds=3`` and a model at ``max_worlds=4``). Ask
486
+ :func:`int_valid`, or :func:`~unicode_logic_kit.atp.lj.int_decide`, for a verdict on
487
+ validity: both are exact on a propositional formula. For a **first-order** formula
488
+ (containing ∀/∃) it is a *bounded* search over increasing-domain Kripke models (the
489
+ formula's constants plus ``domain_elements`` fresh individuals, up to ``max_steps``
490
+ valuations), and first-order intuitionistic logic is undecidable, so ``None`` only
491
+ means "no counter-model within the bounds".
492
+
493
+ A variable that is free in ``formula`` is a PARAMETER: one unknown individual,
494
+ the same everywhere in the formula, which exists in every world of the model (the
495
+ module docstring has the derivation). The search reads it as a constant named like
496
+ the variable, so ``∀x P(x) → P(y)`` has no countermodel, ``P(y) → ∃x Q(x)`` has one
497
+ (the individual ``y`` with ``P`` of it and ``Q`` of nothing) and ``y`` is a member of
498
+ every domain of the model it returns; evaluate it with
499
+ ``model.forces(world, substitute(formula, Variable('y'), Constant('y')))``. A
500
+ numeral in an atom is the constant of its value. Both are read in the first-order
501
+ search only: a formula with no quantifier is propositional and keeps its atoms as
502
+ letters. Every parameter and every constant is one individual of each model, so a
503
+ formula with many of them searches larger models within the same ``max_steps``, and
504
+ a countermodel that needs a fresh individual beyond ``domain_elements`` (or more
505
+ valuations than ``max_steps``) is not found.
506
+
507
+ Many-sorted (``∀x:S`` / ``∃x:S`` / ``c:S``): relativized and given the classical
508
+ sort assumptions (no sort is empty, a sorted constant is in its sort) before the
509
+ search runs — see
510
+ :func:`_prepare_many_sorted` and the module docstring.
511
+
512
+ Raises:
513
+ NotImplementedError: a free variable is spelled like a constant of the formula
514
+ (the two would be one individual of the model), or a numeral like a
515
+ constant, or an identity atom occurs.
516
+ ValueError: second-order quantifiers or function terms occur, or an atom holds a
517
+ term that is neither a variable, a constant nor a numeral.
518
+ """
519
+ _reject_equality_everywhere(formula, "int_countermodel")
520
+ formula = _prepare_many_sorted(formula)
521
+ if _is_propositional(formula):
522
+ keys = _atom_keys(formula)
523
+ for n in range(1, max_worlds + 1):
524
+ for upset in _partial_orders(n):
525
+ for valuation in _monotone_valuations(upset, keys):
526
+ model = IntKripkeModel(upset, valuation)
527
+ for w in range(n):
528
+ if not model.forces(w, formula):
529
+ return model, w
530
+ return None
531
+ formula = _read_first_order_terms(formula)
532
+ return _fo_countermodel(formula, max_worlds, domain_elements, max_steps)
533
+
534
+
535
+ def int_valid(formula: Node, max_worlds: int = 3,
536
+ domain_elements: int = 2, max_steps: int = 300000) -> bool:
537
+ """Return True iff ``formula`` is intuitionistically valid.
538
+
539
+ **Split contract** — the two fragments are decided differently, and honestly:
540
+
541
+ - **Propositional**: a genuine decision procedure, full stop, REGARDLESS of
542
+ ``max_worlds``. The bounded Kripke search (:func:`int_countermodel`) is tried
543
+ first as a fast path — a countermodel it finds within ``max_worlds`` worlds is a
544
+ real witness, so a quick ``False`` is returned without going further. But
545
+ propositional IPL's finite-model-property bound *grows with the formula* (e.g.
546
+ ``(p→q)∨(q→r)∨(r→p)`` needs 4 worlds — 3, the default, finds nothing), so
547
+ "no countermodel within the bound" is NOT by itself proof of validity. When the
548
+ bounded search comes up empty, the verdict is instead handed to
549
+ :func:`~unicode_logic_kit.atp.lj.int_prove` (Dyckhoff's contraction-free G4ip, a
550
+ terminating decision procedure with no bound to exhaust) for a DEFINITIVE
551
+ True-or-False answer. Net effect: both the ``True`` and the ``False`` verdicts on
552
+ a propositional formula are exact, at every ``max_worlds`` — including the
553
+ default. (Before this delegation, ``int_valid`` of the formula above wrongly
554
+ returned ``True`` at ``max_worlds=3``; see ``tests/test_lj_search.py`` for the
555
+ pinned regression.)
556
+ - **First-order**: a sound but *incomplete* check. ``True`` means "no
557
+ counter-model within the bounds" (not a proof, since first-order intuitionistic
558
+ validity is undecidable), while ``False`` is always backed by a real counter-model
559
+ from :func:`int_countermodel`. A free variable is a parameter, an individual that
560
+ exists in every world of the model, so ``∀x P(x) → P(y)`` is valid and
561
+ ``P(y) → ∃x Q(x)`` is not (see :func:`int_countermodel` and the module docstring).
562
+
563
+ Many-sorted (``∀x:S`` / ``∃x:S`` / ``c:S``): relativized and given the classical
564
+ sort assumptions (no sort is empty, a sorted constant is in its sort) before
565
+ either fragment's check runs (so a
566
+ formula that is many-sorted always takes the first-order branch above,
567
+ even if it would otherwise look propositional — see
568
+ :func:`_prepare_many_sorted` and the module docstring).
569
+
570
+ Raises:
571
+ NotImplementedError: as :func:`int_countermodel`.
572
+ ValueError: as :func:`int_countermodel`.
573
+ """
574
+ _reject_equality_everywhere(formula, "int_valid")
575
+ formula = _prepare_many_sorted(formula)
576
+ if not _is_propositional(formula):
577
+ return int_countermodel(formula, max_worlds, domain_elements, max_steps) is None
578
+ if int_countermodel(formula, max_worlds, domain_elements, max_steps) is not None:
579
+ return False
580
+ from ..atp.lj import int_prove # lazy: avoids a semantics<->atp import cycle
581
+ return int_prove([], formula)