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,1055 @@
1
+ """Solver-independent core for the finite-domain (ASP / CP) refutation backends.
2
+
3
+ Two backends (``ClingoBackend``, ``MinizincBackend`` — built alongside this
4
+ module, not in it) decide FOL questions by searching for a finite
5
+ countermodel of ``premises ∧ ¬φ``: they GROUND that goal into ASP or CP and
6
+ let clingo / MiniZinc search. Everything the two backends would otherwise
7
+ duplicate lives here instead, so ``unsupported`` and a re-verified
8
+ countermodel read identically whichever solver was asked:
9
+
10
+ * :func:`lower_msfol` — the many-sorted-to-classical FRONT DOOR both
11
+ backends call before :func:`fragment_check` ever runs: many-sorted input
12
+ (``SortedQuantifier``/``SortedConstant``/``SortedCount``/
13
+ ``SortedCardinality``) is relativised to plain classical FOL with the
14
+ kit's own :func:`~unicode_logic_kit.fol.nodes.to_fol` — exactly the pattern
15
+ :mod:`~unicode_logic_kit.atp.z3_arith` already uses ahead of its own
16
+ (unrelated) arithmetic translation — so every OTHER piece of this module
17
+ sees ordinary unary predicates where a sort guard used to be, and needs no
18
+ per-sort concept of its own. See "Many-sorted input" below.
19
+ * :class:`FiniteDomainProblem` — the one problem shape both backends encode:
20
+ a batch of sentences to satisfy simultaneously, a domain size, a
21
+ :class:`~unicode_logic_kit.fol.signature.Signature`, and the
22
+ distinct-constants convention.
23
+ * :func:`fragment_check` — the single gate: is a sentence even IN the
24
+ fragment these backends can ground? Unsorted classical FOL plus the
25
+ counting fragment (``Count``, ``Cardinality``) AND function symbols
26
+ (``Function``) is encodable; everything without a reading over one
27
+ finite, bivalent, first-order structure is refused BY NAME (see "Fragment
28
+ boundary" below). A ``Function`` with a SORTED declaration (a
29
+ :class:`~unicode_logic_kit.fol.signature.FunctionDecl` carrying
30
+ ``arg_sorts``/``result_sort``) is refused separately, by
31
+ :class:`FiniteDomainProblem` itself — see "Sorted function symbols" below.
32
+ * :func:`structure_from_solution` — turn a solver's true ground atoms back
33
+ into a :class:`~unicode_logic_kit.semantics.structures.FiniteStructure`,
34
+ including the shared "function = total relation + functionality
35
+ constraint" reconstruction so neither backend has to invent it twice.
36
+ * :func:`verify_model` — the safety net: re-check a reconstructed structure
37
+ against the ORIGINAL sentences with the kit's own independent evaluator
38
+ before a backend is allowed to call anything REFUTED. See "Known
39
+ verification gap" below — a narrow residual collision between this
40
+ module's own scope and an existing module it does not own, down to ONE
41
+ node type (``Contrast``) now that the counting fragment AND function
42
+ symbols are both independently verifiable.
43
+
44
+ This module never grounds anything itself. clingo and MiniZinc both ground
45
+ better than a hand-rolled loop here would (see the design note), so the job
46
+ here stops at emitting a solver-agnostic PROBLEM and re-checking a
47
+ solver-agnostic SOLUTION; the ASP/CP text itself is each backend's own.
48
+
49
+ THE ONE RULE THIS MODULE EXISTS TO PROTECT
50
+ -------------------------------------------
51
+ Both backends are refutation-only: a finite model of ``premises ∧ ¬φ``
52
+ proves ``φ`` REFUTED, but the absence of one up to a size bound proves
53
+ NOTHING (first-order logic has no finite model property). Neither backend
54
+ may ever report ``PROVED`` from this search. :func:`verify_model` is the
55
+ second half of that discipline: a model-finder that hands back a structure
56
+ that does not actually satisfy the goal is worse than one that finds
57
+ nothing, so every witness is re-checked here before a backend is allowed to
58
+ call it REFUTED.
59
+
60
+ Fragment boundary (what :func:`fragment_check` accepts and refuses)
61
+ ---------------------------------------------------------------------
62
+ IN: ``Atom``, ``Not``, ``And``, ``Or``, ``Xor``, ``Implies``, ``Iff``,
63
+ ``Contrast`` (truth-functionally ``And`` — see its own docstring),
64
+ ``Quantifier`` (unsorted ``∀``/``∃`` only), ``Count`` (``∃≥n``/``∃≤n``/
65
+ ``∃=n``), ``Cardinality`` (``|S|`` as a term), and the term vocabulary
66
+ ``Variable``/``Constant``/``Number``/``Function``. This is "unsorted
67
+ classical FOL plus the counting fragment plus function symbols" — the
68
+ scope the design this module implements calls for, closed in two steps: the
69
+ counting fragment first, ``Function`` here (see "Known verification gap"
70
+ below for how each became independently verifiable, not merely
71
+ groundable). ``Function`` is admitted by NODE TYPE only, and two narrower refusals sit
72
+ downstream of this gate rather than in it: a *sorted* function DECLARATION
73
+ is refused at :class:`FiniteDomainProblem` itself (see "Sorted function
74
+ symbols" below), because ``fragment_check`` walks ``sentences`` and has no
75
+ ``signature`` to consult; and the four arithmetic operator NAMES (``+``,
76
+ ``-``, ``*``, ``/``) stay refused too, but by
77
+ :meth:`~unicode_logic_kit.fol.signature.Signature.from_formulas` never
78
+ declaring them as user functions in the first place (the ``_BUILTIN_FUNCS``
79
+ carve-out) — a ``Function`` node using one of these names passes THIS gate
80
+ (it is a ``Function`` node like any other) and is refused downstream instead,
81
+ by each encoder's own loud "symbol not declared in signature" error.
82
+
83
+ OUT, each refused by name with the reason that node type has no reading
84
+ over a SINGLE finite bivalent structure:
85
+
86
+ * ``Measure`` — a degree on an uninterpreted ORDERED codomain; nothing for
87
+ ``#count``/``sum`` to range over.
88
+ * the sorted family (``SortedQuantifier``, ``SortedConstant``,
89
+ ``SortedCount``, ``SortedCardinality``) — refused HERE because this gate
90
+ has no per-sort concept and :class:`FiniteDomainProblem` deliberately
91
+ stays sort-blind. Neither backend actually hands this gate a sorted node,
92
+ though: :func:`lower_msfol` relativises many-sorted input to plain
93
+ classical FOL FIRST (see "Many-sorted input" below), so this refusal only
94
+ fires for a caller that builds a :class:`FiniteDomainProblem` directly,
95
+ bypassing that front door — defense in depth, not the live path.
96
+ * the modal/temporal/epistemic/hybrid family (``Box``, ``Diamond``,
97
+ ``Knows``, ``Believes``, ``Says``, ``Wants``, ``Always``, ``Eventually``,
98
+ ``Next``, ``Until``, ``Historically``, ``Once``, ``Previous``, ``Since``,
99
+ ``Obligatory``, ``Permitted``, ``Would``, ``Might``, ``Announce``,
100
+ ``AnnounceDiamond``, ``Nominal``, ``At``) — these quantify over POSSIBLE
101
+ WORLDS, not domain individuals; the modal family already has its own
102
+ finite-model backend (``kripke-enum``).
103
+ * ``SecondOrderQuantifier`` — ranges over relations, not individuals.
104
+ * the substructural family (linear logic's ``Tensor``, ``With``, ``OPlus``,
105
+ ``LinearImplies``, ``OfCourse``, ``One``, ``Top``, ``Zero``; Lambek's
106
+ ``Product``, ``Under``, ``Over``) — these track RESOURCE USE, not truth in
107
+ one structure.
108
+ * the lambda family (``LambdaVar``, ``Lambda``, ``Application``) — functions
109
+ FROM formulas TO formulas; higher-order, not first-order individuals.
110
+ * the team-semantic family (``Dependence``, ``SlashedExists``) — evaluated
111
+ against a TEAM (a set of assignments), not a single structure; not named
112
+ in the originating design's exclusion table but excluded for the
113
+ identical reason, so it gets its own family here rather than being
114
+ silently swept into "modal" or left unclassified.
115
+ * the Łukasiewicz fuzzy family (``WeakConjunction``, ``WeakDisjunction``,
116
+ ``StrongConjunction``, ``StrongDisjunction``, ``LukNegation``,
117
+ ``LukImplication``, ``LukEquivalence``) — many-valued; a finite
118
+ countermodel search assumes classical, two-valued truth. Also not named
119
+ in the design's own table, excluded for the same reason as team
120
+ semantics.
121
+
122
+ A node type this module has never heard of (a future addition to the kit's
123
+ AST) is refused too, with a generic reason, rather than silently accepted —
124
+ default-deny, matching :mod:`~unicode_logic_kit.fol.signature`'s own
125
+ loud-refusal convention.
126
+
127
+ Sorted function symbols — admitted by node type, refused by DECLARATION
128
+ -------------------------------------------------------------------------
129
+ ``Function`` is IN the fragment above, but only in its plain, UNSORTED
130
+ reading — a :class:`~unicode_logic_kit.fol.signature.FunctionDecl` whose
131
+ ``arg_sorts`` or ``result_sort`` is not ``None`` is refused, by
132
+ ``ValueError``, in :meth:`FiniteDomainProblem.__post_init__` itself. This is
133
+ an explicit decision, not an oversight: the two backends' function encoders
134
+ (``_AspEncoder._function_rule``'s total-relation choice rule;
135
+ :class:`~unicode_logic_kit.atp.minizinc_backend.MinizincBackend`'s
136
+ ``array[DOM, …] of var DOM`` declaration) both ground a function's arguments
137
+ and result over the WHOLE domain regardless of any declared sort — neither
138
+ consults ``arg_sorts``/``result_sort`` at all — so admitting a sorted
139
+ :class:`~unicode_logic_kit.fol.signature.FunctionDecl` without refusing it
140
+ would silently IGNORE the caller's own stated constraint rather than enforce
141
+ or verify it: exactly the kind of silent semantic substitution this kit
142
+ refuses elsewhere (see e.g. the ``all_different`` footnote on
143
+ :class:`FiniteDomainProblem`). Even setting the encoders aside,
144
+ :class:`~unicode_logic_kit.semantics.structures.FiniteStructure` — what
145
+ :func:`verify_model` checks a countermodel back against — has NO ``sorts``
146
+ concept at all (see
147
+ :mod:`~unicode_logic_kit.semantics.model_eval`'s own docstring, which weighs
148
+ and declines exactly this for ``SortedCount``), so a sort-respecting
149
+ function graph could not even be VERIFIED if the encoders somehow ground it
150
+ correctly. Refusing loudly, by name, at construction time is therefore the
151
+ same choice this module already made for the sorted QUANTIFIER family (see
152
+ the bullet above) — narrowed here to the one sorted DECLARATION shape that
153
+ family's own four node types do not cover, since there is no
154
+ ``SortedFunction`` AST node.
155
+
156
+ This refusal can only fire for a caller that hand-builds ``signature`` and
157
+ passes it to :class:`FiniteDomainProblem` directly:
158
+ :meth:`~unicode_logic_kit.fol.signature.Signature.from_formulas` — what both
159
+ :meth:`~unicode_logic_kit.atp.clingo_backend.ClingoBackend.decide` and
160
+ :meth:`~unicode_logic_kit.atp.minizinc_backend.MinizincBackend.decide` always
161
+ use to build their OWN signature — never sets ``arg_sorts``/``result_sort``
162
+ for a function (it infers only arity; see that method's own "scope" section),
163
+ and a many-sorted ``SortedQuantifier``/``SortedConstant`` has already been
164
+ relativised away by :func:`lower_msfol` before ``Signature.from_formulas``
165
+ ever runs (see "Many-sorted input" below) — so it is defense in depth, not a
166
+ live path through either backend today, exactly like the sorted-quantifier
167
+ refusal above.
168
+
169
+ Many-sorted input (:func:`lower_msfol`)
170
+ -----------------------------------------
171
+ Many-sorted FOL (MSFOL) — ``SortedQuantifier`` (``∀x:S φ`` / ``∃x:S φ``),
172
+ ``SortedConstant`` (``alice:Human``), ``SortedCount`` (``∃≥n x:S φ``), and
173
+ ``SortedCardinality`` (``|{x:S : φ}|``) — never reaches :func:`fragment_check`
174
+ directly when a caller goes through :func:`lower_msfol` first, which both
175
+ :class:`~unicode_logic_kit.atp.clingo_backend.ClingoBackend` and
176
+ :class:`~unicode_logic_kit.atp.minizinc_backend.MinizincBackend` do, right
177
+ after building their refutation-goal sentences and before either
178
+ :func:`fragment_check` or ``Signature.from_formulas`` sees them. It is
179
+ RELATIVISED to plain classical FOL with the kit's existing
180
+ :func:`~unicode_logic_kit.fol.nodes.to_fol`: ``∀x:S φ`` becomes
181
+ ``∀x (S(x) → φ)``, ``∃x:S φ`` becomes ``∃x (S(x) ∧ φ)``, ``alice:Human``
182
+ becomes the plain constant ``alice`` plus the fact ``Human(alice)``, and
183
+ ``SortedCount``/``SortedCardinality`` guard their matrix with the sort atom
184
+ and fall back to the already-encodable ``Count``/``Cardinality``. Once
185
+ lowered, a sort name is nothing more than an ordinary unary predicate:
186
+ :meth:`~unicode_logic_kit.fol.signature.Signature.from_formulas`,
187
+ :func:`fragment_check`, both backends' own encoders, and :func:`verify_model`
188
+ all need NO sort-specific handling at all.
189
+
190
+ One soundness subtlety ``to_fol``'s relativisation alone does not cover:
191
+ :func:`~unicode_logic_kit.semantics.modelfinder.find_model` /
192
+ :func:`~unicode_logic_kit.semantics.modelfinder.find_countermodel` — the
193
+ independent oracle this lowering is checked against — enumerate ONLY
194
+ NON-EMPTY subsets of the domain as a sort's universe
195
+ (``modelfinder._nonempty_subsets``), because a many-sorted-logic sort is, by
196
+ convention, never empty. A bare relativisation carries no such guarantee:
197
+ ``∀x:S φ`` becomes vacuously TRUE the moment a countermodel search makes
198
+ ``S`` empty, which would let this module's ASP/CP route accept a
199
+ "countermodel" :mod:`~unicode_logic_kit.semantics.modelfinder` would never
200
+ even consider a legal structure — the two routes would silently disagree.
201
+ :func:`lower_msfol` closes this by asserting one EXTRA sentence per distinct
202
+ sort name referenced anywhere in the input, ``∃x (S(x))``, alongside the
203
+ relativised originals.
204
+
205
+ :func:`lower_msfol` is a no-op — returns its input, ``tuple``-coerced,
206
+ completely untouched, never even passed through ``to_fol`` — when NO
207
+ sentence contains any of the four sorted node types (one
208
+ :meth:`~unicode_logic_kit.fol.nodes.Node.walk` pass over the batch to confirm
209
+ this). So the entire pre-existing unsorted-only test suite, in this module
210
+ and both backends', sees byte-identical sentences and pays zero overhead;
211
+ this many-sorted path is purely additive. This no-op check is PER SENTENCE,
212
+ not merely per batch: even once one sentence in a batch IS sorted, every
213
+ OTHER sentence that is not — an unrelated ``Count``/``Contrast`` premise
214
+ sharing a ``decide()`` call with a sorted one, say — is left completely
215
+ untouched too, rather than being routed through ``to_fol`` as collateral
216
+ damage. This matters because ``to_fol``'s own ``_reduce_nl_nodes`` phase
217
+ unconditionally expands ``Count`` via its bounded (``n<=500``) O(n²)
218
+ distinct-witnesses encoding; only a sentence that is ITSELF sorted may pay
219
+ that cost, and every other sentence keeps reaching its backend's own native
220
+ (unbounded, cheaper) counting encoding exactly as before.
221
+
222
+ Known verification gap (an objection, not a silent deviation)
223
+ -----------------------------------------------------------------
224
+ :func:`verify_model` is specified to re-check a reconstructed structure by
225
+ running the kit's OWN independent evaluator,
226
+ :func:`~unicode_logic_kit.semantics.model_eval.evaluate` (re-exported as
227
+ ``evaluate_in_structure``), over every sentence — and that is exactly what
228
+ it does here. Three of the four node types that used to defeat that
229
+ evaluator no longer do — this is the SECOND time this exact narrative
230
+ pattern needed updating, and it follows the identical playbook the first
231
+ time (the counting fragment) did:
232
+
233
+ ``Cardinality`` and ``Number`` (as a comparison operand) are read
234
+ ARITHMETICALLY by ``evaluate_in_structure`` itself: its comparison branch
235
+ (``_atom_value``, via ``_numeric_value``) counts ``|{v : φ}|`` and compares
236
+ it against another count or a bare numeral whenever the comparison has at
237
+ least one numeric operand (see that module's own docstring, "Two kinds of
238
+ term value, kept apart"). The counting fragment this whole design exists to
239
+ decide (see the design's §1) is therefore DECIDED AND VERIFIED end to
240
+ end: ``|{x : P(x)}| > |{y : Q(y)}|`` can be correctly REFUTED by
241
+ clingo/MiniZinc, reconstructed by :func:`structure_from_solution`, AND
242
+ independently confirmed here.
243
+
244
+ ``Function`` is now read the SAME way ``structure_from_solution`` already
245
+ built it: ``evaluate_in_structure``'s term evaluator (``_term_value``, via
246
+ its ``Function`` case) resolves ``f(t1,...,tk)`` off the ``(name, arity+1)``
247
+ total-relation extension the reconstruction produces — the unique row whose
248
+ leading ``k`` components match the (recursively evaluated) arguments — and
249
+ is refused loudly, never silently mismatched, if that row is missing or not
250
+ unique (see :mod:`~unicode_logic_kit.semantics.model_eval`'s own docstring for
251
+ the full account). A function-bearing countermodel is therefore also
252
+ DECIDED AND VERIFIED end to end now: ``∃x (op(x,x) ≠ x)`` (idempotence
253
+ refuted) can be correctly REFUTED by clingo/MiniZinc, reconstructed, AND
254
+ independently confirmed here — the identical closure the counting fragment
255
+ already got, now for functions.
256
+
257
+ That leaves ONE node type this evaluator still cannot touch:
258
+
259
+ * ``Contrast`` IS in the encodable fragment (:func:`fragment_check` admits
260
+ it — truth-functionally ``And``, see its own docstring) but is genuinely
261
+ NOT in ``evaluate_in_structure``'s supported-node list (that module's own
262
+ "Supported nodes" section stops at ``Atom``/``Not``/``And``/``Or``/
263
+ ``Xor``/``Implies``/``Iff``/``Quantifier``/``Count``) — it raises
264
+ ``UnsupportedNode`` there like any node type the evaluator has never
265
+ heard of. A sentence built with ``Contrast`` can therefore still be
266
+ correctly REFUTED by a backend and correctly reconstructed, and still
267
+ fail :func:`verify_model` — not because the countermodel is wrong, but
268
+ because the independent checker this module is told to call cannot
269
+ evaluate ``Contrast`` AT ALL. Per this module's OWN rule above (never
270
+ hand back an unverified countermodel), that failure is treated as "could
271
+ not verify" and a calling backend must downgrade to ``ERROR``/``"infra"``
272
+ rather than ever return ``REFUTED`` for such a sentence — SOUND, but,
273
+ unlike the counting fragment and ``Function`` above, this one IS still
274
+ live: closing it needs
275
+ either :mod:`~unicode_logic_kit.semantics.model_eval` to grow a
276
+ ``Contrast`` case (out of this file's assignment — that module is owned
277
+ elsewhere) or :func:`verify_model` to be redesigned with its own
278
+ evaluator for this one connective (a decision this module deliberately
279
+ does not make unilaterally; see the instructions this file was written
280
+ under).
281
+
282
+ Separately, and NOT a gap: comparing a domain INDIVIDUAL with a numeral
283
+ remains refused, deliberately. ``∀x (x = 1)`` reaches the same comparison
284
+ branch as a genuine counting comparison, but ``_numeric_value`` is only
285
+ ever handed the operand syntactically marked numeric (``Cardinality``/
286
+ ``Number``); called on the OTHER, individual-denoting operand it raises
287
+ ``UnsupportedNode`` ("does not denote a number, so it cannot be compared
288
+ with one"). Admitting numeric terms into comparisons between two numeric
289
+ operands is the whole of what the counting fragment calls for; admitting
290
+ them into a comparison against a domain individual was never in scope, and
291
+ staying refused there is correct, not incomplete — a structure whose
292
+ individuals happen to be named ``"0"``/``"1"``/… must not tempt anyone into
293
+ reading those names as integers.
294
+
295
+ This is reported here, in the code, rather than silently worked around, and
296
+ again in :func:`verify_model`'s own docstring.
297
+ """
298
+
299
+ import itertools
300
+ from dataclasses import dataclass
301
+ from typing import Dict, Iterable, List, Optional, Sequence, Tuple, Type
302
+
303
+ from ..fol.nodes import (
304
+ Node,
305
+ Variable, Constant, Number, Function,
306
+ Atom, Not, And, Or, Xor, Implies, Iff, Quantifier,
307
+ Count, Cardinality, Contrast, Measure,
308
+ SortedQuantifier, SortedConstant, SortedCount, SortedCardinality,
309
+ to_fol,
310
+ Box, Diamond, Knows, Believes, Says, Wants,
311
+ Always, Eventually, Next, Until,
312
+ Historically, Once, Previous, Since,
313
+ Obligatory, Permitted, Would, Might,
314
+ Announce, AnnounceDiamond,
315
+ SecondOrderQuantifier,
316
+ Nominal, At,
317
+ Dependence, SlashedExists,
318
+ Tensor, With, OPlus, LinearImplies, OfCourse, One, Top, Zero,
319
+ Product, Under, Over,
320
+ WeakConjunction, WeakDisjunction, StrongConjunction, StrongDisjunction,
321
+ LukNegation, LukImplication, LukEquivalence,
322
+ LambdaVar, Lambda, Application,
323
+ )
324
+ from ..fol._msfl_nodes import nonempty_sort_axioms
325
+ from ..fol._tptp_symbols import is_tptp_boolean_atom as _is_tptp_boolean_atom
326
+ from ..fol.signature import Signature
327
+ from ..semantics.structures import FiniteStructure
328
+ from ..semantics.model_eval import (
329
+ evaluate as _evaluate_in_structure,
330
+ UninterpretedSymbol, UnsupportedNode,
331
+ )
332
+
333
+ __all__ = [
334
+ "lower_msfol",
335
+ "FiniteDomainProblem", "fragment_check", "free_variable_reason", "structure_from_solution",
336
+ "verify_model",
337
+ ]
338
+
339
+
340
+ # =============================================================================
341
+ # lower_msfol — many-sorted FOL to classical FOL, ahead of fragment_check
342
+ # =============================================================================
343
+
344
+ # The four many-sorted node types lower_msfol looks for; every one of them
345
+ # carries a `.sort: str` field, which is all the sort-name collection below
346
+ # needs.
347
+ _SORTED_NODE_TYPES = (SortedQuantifier, SortedConstant, SortedCount, SortedCardinality)
348
+
349
+
350
+ def lower_msfol(sentences: Sequence[Node]) -> Tuple[Node, ...]:
351
+ """Relativise many-sorted sentences to classical FOL, or pass them through.
352
+
353
+ Both :meth:`~unicode_logic_kit.atp.clingo_backend.ClingoBackend.decide` and
354
+ :meth:`~unicode_logic_kit.atp.minizinc_backend.MinizincBackend.decide` call
355
+ this on their refutation-goal ``sentences`` BEFORE :func:`fragment_check`
356
+ (or, for the MiniZinc backend, ``Signature.from_formulas``) ever sees
357
+ them — see the module docstring's "Many-sorted input" section for the
358
+ full design: why :func:`~unicode_logic_kit.fol.nodes.to_fol` alone is not
359
+ enough for soundness, and why the fast path below guarantees zero
360
+ behaviour change for every unsorted-only caller (i.e. the entire
361
+ pre-existing test suite).
362
+
363
+ Args:
364
+ sentences: the sentences to lower — typically a backend's own
365
+ ``premises + (goal,)``, already universally closed.
366
+
367
+ Returns:
368
+ ``sentences``, coerced to a ``tuple`` and otherwise BYTE-IDENTICAL
369
+ (never even passed through ``to_fol``), when none of them contains a
370
+ ``SortedQuantifier``/``SortedConstant``/``SortedCount``/
371
+ ``SortedCardinality`` node anywhere. Otherwise: each sentence that
372
+ itself contains a sorted node is run through
373
+ ``to_fol(s, include_sort_facts=True)`` (so a sorted constant's own
374
+ sort membership is asserted, not merely used as a guard elsewhere);
375
+ every OTHER sentence in the same batch — one with no sorted node of
376
+ its own, e.g. an unrelated ``Count``/``Contrast`` premise — is left
377
+ completely untouched, so it keeps reaching the calling backend's own
378
+ native encoding for that construct instead of being routed through
379
+ ``to_fol``'s unrelated ``Count``-expansion phase (which is both
380
+ bounded at ``n<=500`` and, well under that bound, asymptotically
381
+ worse than a backend's native counting encoding). PLUS one extra
382
+ sentence ``∃x (S(x))`` per distinct sort name referenced anywhere in
383
+ the input — in first-occurrence order — asserting that ``S``'s
384
+ universe is non-empty, the soundness guarantee ``to_fol``'s
385
+ relativisation alone does not supply (see the module docstring).
386
+
387
+ Raises:
388
+ TypeError: a member of ``sentences`` is not a
389
+ :class:`~unicode_logic_kit.fol.nodes.Node`.
390
+ """
391
+ sentences = tuple(sentences)
392
+ for s in sentences:
393
+ if not isinstance(s, Node):
394
+ raise TypeError(
395
+ f"lower_msfol: every sentence must be a Node, got {type(s).__name__}."
396
+ )
397
+
398
+ # One pass over every sentence: collect every distinct sort name in
399
+ # first-occurrence order, AND which individual sentences contain a
400
+ # sorted node at all. An empty sort_names IS the fast-path signal (no
401
+ # sorted node anywhere), so this single walk answers "is there anything
402
+ # to do", "which sorts", and "which sentences actually need lowering"
403
+ # all at once -- the last of these matters because to_fol's third phase
404
+ # (_reduce_nl_nodes) unconditionally expands a plain Count/SortedCount
405
+ # node via its O(n^2) distinct-witnesses encoding (bounded at n<=500),
406
+ # so an unrelated large-n Count sentence must never be routed through
407
+ # to_fol merely because SOME OTHER sentence in the same batch is sorted.
408
+ sort_names: List[str] = []
409
+ sentence_is_sorted: List[bool] = []
410
+ for s in sentences:
411
+ is_sorted = False
412
+ for node in s.walk():
413
+ if isinstance(node, _SORTED_NODE_TYPES):
414
+ is_sorted = True
415
+ if node.sort not in sort_names:
416
+ sort_names.append(node.sort)
417
+ sentence_is_sorted.append(is_sorted)
418
+
419
+ if not sort_names:
420
+ # Fast path: nothing sorted anywhere. Returned untouched -- not even
421
+ # round-tripped through to_fol -- so a plain-FOL caller's sentences
422
+ # stay byte-identical (the no-op guarantee the module docstring
423
+ # promises).
424
+ return sentences
425
+
426
+ # Slow path, but still per-sentence: only a sentence that itself
427
+ # contains a sorted node is run through to_fol. A sentence with nothing
428
+ # sorted in it (e.g. an unrelated Count/Contrast sentence sharing this
429
+ # batch with a sorted one) is left completely untouched, so it keeps
430
+ # reaching each backend's own native encoding for that construct
431
+ # (clingo's #count aggregate, MiniZinc's counting encoding) instead of
432
+ # being silently pre-expanded/collapsed by to_fol's unrelated third
433
+ # phase.
434
+ lowered = tuple(
435
+ to_fol(s, include_sort_facts=True) if is_sorted else s
436
+ for s, is_sorted in zip(sentences, sentence_is_sorted)
437
+ )
438
+ # The non-emptiness sentences themselves are built by the shared helper
439
+ # (unicode_logic_kit.fol._msfl_nodes.nonempty_sort_axioms) — every OTHER
440
+ # classical decision route with the same soundness obligation (Z3,
441
+ # cvc5, Prover9, the TPTP fof export, eval.equivalence's solver level)
442
+ # reuses that exact construction too, so this module and every one of
443
+ # them can never drift apart on what "S is non-empty" means as a
444
+ # sentence. This re-walks ``sentences`` once more (a second, cheap pass
445
+ # on top of the one above that decided ``sentence_is_sorted``) rather
446
+ # than threading ``sort_names`` through by hand, so the two collection
447
+ # sites cannot silently diverge either.
448
+ nonempty = nonempty_sort_axioms(*sentences)
449
+ return lowered + nonempty
450
+
451
+
452
+ # =============================================================================
453
+ # FiniteDomainProblem
454
+ # =============================================================================
455
+
456
+ @dataclass(frozen=True)
457
+ class FiniteDomainProblem:
458
+ """A finite-domain search problem: sentences to satisfy over a bounded domain.
459
+
460
+ Both backends encode the SAME shape into their own solver language, so a
461
+ problem built once is what ``ClingoBackend`` and ``MinizincBackend`` both
462
+ read — this is what makes the two backends' answers comparable at all.
463
+
464
+ ``sentences`` are conjoined (every one must hold simultaneously); for a
465
+ refutation search a caller passes ``premises + (¬φ,)``, but this class
466
+ has no opinion about where its sentences came from — it is the shared
467
+ CONTAINER, not the refutation-goal-building logic (that lives in each
468
+ backend's own ``decide()``).
469
+
470
+ Args:
471
+ sentences: the formulas to satisfy together. Must be non-empty —
472
+ an empty problem has no goal to search for.
473
+ size: the domain size ``n``; individuals are ``0 … n-1``. Must be
474
+ ``>= 1`` — an empty domain satisfies no ``∃`` and is never a
475
+ useful search.
476
+ signature: the declared vocabulary. When ``None`` (the default) it
477
+ is inferred from ``sentences`` via
478
+ :meth:`~unicode_logic_kit.fol.signature.Signature.from_formulas`
479
+ — the canonical way to get one, so most callers never need to
480
+ build it by hand. Inference NEVER produces a sorted
481
+ :class:`~unicode_logic_kit.fol.signature.FunctionDecl` (see
482
+ ``Raises`` below and the module docstring's "Sorted function
483
+ symbols" section), so this refusal is inert for every caller
484
+ that leaves ``signature=None`` — it only fires for a caller that
485
+ hand-builds a sorted one and passes it in directly.
486
+ all_different: whether every declared CONSTANT must denote a
487
+ pairwise-distinct individual (the unique-names convention some
488
+ encodings rely on). This is a property of the SOLUTION a
489
+ backend is allowed to return, not of ``sentences`` themselves —
490
+ it is deliberately a separate flag rather than baked into the
491
+ sentences as explicit ``≠`` atoms, and it is UNRELATED to
492
+ :func:`~unicode_logic_kit.semantics.model_eval.evaluate`'s own
493
+ ``all_different`` parameter (that one governs whether separately
494
+ quantified EXISTENTIAL VARIABLES must denote distinct
495
+ individuals — a different symbol class, a different convention;
496
+ conflating the two would be exactly the kind of silent semantic
497
+ substitution this kit refuses elsewhere). Defaults to ``False``.
498
+
499
+ Raises:
500
+ TypeError: a sentence is not a
501
+ :class:`~unicode_logic_kit.fol.nodes.Node`, ``signature`` is
502
+ neither ``None`` nor a
503
+ :class:`~unicode_logic_kit.fol.signature.Signature`, or
504
+ ``all_different`` is not a ``bool``.
505
+ ValueError: ``sentences`` is empty, ``size < 1``, or (this
506
+ module's explicit "sorted function symbols" decision — see the
507
+ module docstring) the resolved ``signature`` declares a
508
+ :class:`~unicode_logic_kit.fol.signature.FunctionDecl` with a
509
+ non-``None`` ``arg_sorts`` or ``result_sort``.
510
+ """
511
+
512
+ sentences: Tuple[Node, ...]
513
+ size: int
514
+ signature: Optional[Signature] = None
515
+ all_different: bool = False
516
+
517
+ def __post_init__(self):
518
+ object.__setattr__(self, "sentences", tuple(self.sentences))
519
+ if not self.sentences:
520
+ raise ValueError(
521
+ "FiniteDomainProblem: sentences must be non-empty — there is "
522
+ "no goal to search for otherwise."
523
+ )
524
+ for s in self.sentences:
525
+ if not isinstance(s, Node):
526
+ raise TypeError(
527
+ f"FiniteDomainProblem: every sentence must be a Node, got "
528
+ f"{type(s).__name__}."
529
+ )
530
+ if isinstance(self.size, bool) or not isinstance(self.size, int) or self.size < 1:
531
+ raise ValueError(
532
+ f"FiniteDomainProblem: size must be an int >= 1, got {self.size!r}."
533
+ )
534
+ if self.signature is None:
535
+ object.__setattr__(self, "signature", Signature.from_formulas(self.sentences))
536
+ elif not isinstance(self.signature, Signature):
537
+ raise TypeError(
538
+ f"FiniteDomainProblem: signature must be a Signature or None, "
539
+ f"got {type(self.signature).__name__}."
540
+ )
541
+ # Sorted function symbols: refused HERE, by name, rather than
542
+ # admitted and silently mishandled — see the module docstring's
543
+ # "Sorted function symbols" section for the full argument. This can
544
+ # only fire for a caller that hand-builds `signature` (inference via
545
+ # Signature.from_formulas never sets arg_sorts/result_sort for a
546
+ # function), so it is defense in depth, not a live path through
547
+ # either backend's own decide().
548
+ sorted_functions = sorted(
549
+ name for name, decl in self.signature.functions.items()
550
+ if decl.arg_sorts is not None or decl.result_sort is not None
551
+ )
552
+ if sorted_functions:
553
+ raise ValueError(
554
+ f"FiniteDomainProblem: sorted function symbol(s) "
555
+ f"{sorted_functions} declare a non-None arg_sorts/"
556
+ "result_sort, but this backend's function encoding "
557
+ "(_AspEncoder._function_rule's total-relation choice rule; "
558
+ "MinizincBackend's `array[DOM, ...] of var DOM` declaration) "
559
+ "treats every function as fully UNSORTED — its arguments and "
560
+ "result range over the WHOLE domain — so a declared sort "
561
+ "constraint would be silently ignored rather than enforced "
562
+ "or verified. FiniteStructure (what verify_model checks a "
563
+ "countermodel back against) has no sorts concept either "
564
+ "(see semantics.model_eval's own docstring on SortedCount), "
565
+ "so a sort-respecting function graph could not even be "
566
+ "checked back if the encoders grounded it. Declare an "
567
+ "unsorted FunctionDecl (arg_sorts=None, result_sort=None) "
568
+ "instead, or express the sort constraint as an explicit "
569
+ "guard atom in the sentences themselves (the reading "
570
+ "lower_msfol already gives a SortedQuantifier)."
571
+ )
572
+ # Subsort edges: the encoders read only the vocabulary, so a declared
573
+ # S < T would be silently ignored and a countermodel could put an S
574
+ # outside T. Refused by name; the edges are plain unsorted sentences
575
+ # over the same guard predicates lower_msfol emits, so the caller can
576
+ # state them as sentences instead.
577
+ if getattr(self.signature, "subsorts", None):
578
+ raise ValueError(
579
+ f"FiniteDomainProblem: the signature declares subsort edges "
580
+ f"{sorted((c, p) for c, ps in self.signature.subsorts.items() for p in ps)}, "
581
+ "which this backend's encoders do not read — they would be "
582
+ "silently ignored. Add fol.subsort_axioms(signature) to the "
583
+ "sentences instead (plain ∀x (S(x) → T(x)) implications over "
584
+ "the same guard predicates lower_msfol relativises to), and "
585
+ "pass a signature without subsorts."
586
+ )
587
+ if not isinstance(self.all_different, bool):
588
+ raise TypeError(
589
+ f"FiniteDomainProblem: all_different must be a bool, got "
590
+ f"{type(self.all_different).__name__}."
591
+ )
592
+
593
+
594
+ # =============================================================================
595
+ # fragment_check — the single gate
596
+ # =============================================================================
597
+
598
+ # The counting-fragment-plus-unsorted-classical-FOL node types both backends
599
+ # may encode. Deliberately an ALLOW-list, not a deny-list: a future AST
600
+ # addition this module has never seen is refused by default (see
601
+ # _GENERIC_REASON below), matching the kit's loud-refusal convention rather
602
+ # than silently letting an unrecognised node type through.
603
+ _ALLOWED_NODE_TYPES: frozenset = frozenset({
604
+ Variable, Constant, Number, Function,
605
+ Atom, Not, And, Or, Xor, Implies, Iff, Contrast,
606
+ Quantifier, Count, Cardinality,
607
+ })
608
+
609
+
610
+ def _family(reason: str, *classes: Type[Node]) -> Dict[Type[Node], str]:
611
+ """Map every class in ``classes`` to the SAME rejection ``reason``.
612
+
613
+ A small helper so each family below states its reason exactly once
614
+ rather than repeating the string per node type — a family that grows a
615
+ new node type later needs one new entry in a tuple, not a new copy of
616
+ the prose.
617
+ """
618
+ return {cls: reason for cls in classes}
619
+
620
+
621
+ _SORTED_REASON = (
622
+ "the sorted/many-sorted family needs per-sort subdomains that "
623
+ "FiniteDomainProblem does not carry — a real extension, not a detail, "
624
+ "and out of scope for this backend"
625
+ )
626
+ _MODAL_REASON = (
627
+ "modal/temporal/epistemic/hybrid operators quantify over POSSIBLE "
628
+ "WORLDS, not domain individuals — no finite-domain reading here; the "
629
+ "modal family already has its own finite-model backend (kripke-enum)"
630
+ )
631
+ _SECOND_ORDER_REASON = (
632
+ "second-order quantification ranges over relations, not individuals — "
633
+ "no finite-DOMAIN (single-structure) reading"
634
+ )
635
+ _SUBSTRUCTURAL_REASON = (
636
+ "linear/Lambek connectives track RESOURCE USE (how many times a "
637
+ "formula is consumed), not truth in one structure — no finite-domain "
638
+ "reading; see the dedicated linear/Lambek provers for this fragment"
639
+ )
640
+ _LAMBDA_REASON = (
641
+ "lambda terms are functions FROM formulas TO formulas — higher-order, "
642
+ "not first-order individuals — no finite-domain reading"
643
+ )
644
+ _TEAM_REASON = (
645
+ "team-semantic connectives are evaluated against a TEAM (a set of "
646
+ "assignments), not a single structure — no finite-domain reading"
647
+ )
648
+ _FUZZY_REASON = (
649
+ "Łukasiewicz connectives are many-valued — a finite-domain "
650
+ "countermodel search assumes classical, two-valued truth"
651
+ )
652
+ _MEASURE_REASON = (
653
+ "Measure denotes a degree on an uninterpreted ORDERED codomain, not a "
654
+ "set to count — nothing for #count/sum to range over"
655
+ )
656
+ _GENERIC_REASON = (
657
+ "not part of the unsorted-classical-FOL-plus-Count/Cardinality "
658
+ "fragment these backends encode"
659
+ )
660
+
661
+ _REJECTED_NODE_REASONS: Dict[Type[Node], str] = {}
662
+ _REJECTED_NODE_REASONS.update(_family(
663
+ _SORTED_REASON,
664
+ SortedQuantifier, SortedConstant, SortedCount, SortedCardinality,
665
+ ))
666
+ _REJECTED_NODE_REASONS.update(_family(
667
+ _MODAL_REASON,
668
+ Box, Diamond, Knows, Believes, Says, Wants,
669
+ Always, Eventually, Next, Until,
670
+ Historically, Once, Previous, Since,
671
+ Obligatory, Permitted, Would, Might,
672
+ Announce, AnnounceDiamond, Nominal, At,
673
+ ))
674
+ _REJECTED_NODE_REASONS.update(_family(_SECOND_ORDER_REASON, SecondOrderQuantifier))
675
+ _REJECTED_NODE_REASONS.update(_family(
676
+ _SUBSTRUCTURAL_REASON,
677
+ Tensor, With, OPlus, LinearImplies, OfCourse, One, Top, Zero,
678
+ Product, Under, Over,
679
+ ))
680
+ _REJECTED_NODE_REASONS.update(_family(_LAMBDA_REASON, LambdaVar, Lambda, Application))
681
+ _REJECTED_NODE_REASONS.update(_family(_TEAM_REASON, Dependence, SlashedExists))
682
+ _REJECTED_NODE_REASONS.update(_family(
683
+ _FUZZY_REASON,
684
+ WeakConjunction, WeakDisjunction, StrongConjunction, StrongDisjunction,
685
+ LukNegation, LukImplication, LukEquivalence,
686
+ ))
687
+ _REJECTED_NODE_REASONS.update(_family(_MEASURE_REASON, Measure))
688
+
689
+
690
+ def free_variable_reason(sentences: Iterable[Node]) -> Optional[str]:
691
+ """Return why ``sentences`` cannot be WRITTEN as a search problem as they stand, or ``None``.
692
+
693
+ The reason is a free variable. A solver's program has no place for a variable that
694
+ nothing binds, and the readings a writer could pick on its own (every element, one
695
+ sentence at a time; or some element of each sentence) both differ from what a free
696
+ variable means on the kit's routes: a parameter, ONE unknown element that all sentences
697
+ of the problem share, so ``P(x)`` together with ``¬P(x)`` has no model while ``P(x)``
698
+ together with ``¬P(alpha)`` has one. The two backends replace every free variable by
699
+ such a parameter before they write a problem
700
+ (:func:`~unicode_logic_kit.fol._free_parameters.parameterize`); the writers
701
+ (``to_asp``, ``to_minizinc``) are handed sentences and refuse an open one with this
702
+ reason, so that a program is never written under another reading.
703
+ """
704
+ from ..fol._free_parameters import free_parameter_names
705
+
706
+ names = free_parameter_names(sentences)
707
+ if not names:
708
+ return None
709
+ return (
710
+ f"a sentence has the free variable{'s' if len(names) > 1 else ''} "
711
+ f"{', '.join(repr(name) for name in names)}. A free variable is a parameter of the "
712
+ "whole problem (one unknown element, the same in every sentence), which a program "
713
+ "writer cannot state for sentences it is handed one by one: replace it by a constant "
714
+ "in all sentences together (fol._free_parameters.parameterize) or bind it with a "
715
+ "quantifier."
716
+ )
717
+
718
+
719
+ def fragment_check(sentences: Iterable[Node]) -> Optional[str]:
720
+ """Return why ``sentences`` cannot be finite-domain-encoded, or ``None``.
721
+
722
+ The single gate both :class:`ClingoBackend
723
+ <unicode_logic_kit.atp.clingo_backend.ClingoBackend>` and
724
+ :class:`MinizincBackend
725
+ <unicode_logic_kit.atp.minizinc_backend.MinizincBackend>` consult before
726
+ attempting to ground anything, so an ``UNKNOWN``/``"unsupported"``
727
+ verdict (see :mod:`unicode_logic_kit.atp.protocol`) names the exact same
728
+ offending node type and reason regardless of which solver was asked.
729
+ Walks every sentence with
730
+ :meth:`~unicode_logic_kit.fol.nodes.Node.walk` (pre-order, every
731
+ descendant), so a disallowed node buried under an allowed one — e.g. a
732
+ ``Box`` nested inside an otherwise-plain ``And`` — is still caught; see
733
+ the module docstring's "Fragment boundary" section for the exact
734
+ allow/refuse lists and the reason given for each refused family.
735
+
736
+ Args:
737
+ sentences: the formulas to check, e.g. a
738
+ :class:`FiniteDomainProblem`'s ``sentences``.
739
+
740
+ Returns:
741
+ ``None`` if every sentence stays inside the encodable fragment;
742
+ otherwise a message of the form ``"<NodeType> is not encodable:
743
+ <reason>"`` naming the FIRST offending node type encountered (in
744
+ sentence order, then pre-order within a sentence).
745
+
746
+ Raises:
747
+ TypeError: a member of ``sentences`` is not a
748
+ :class:`~unicode_logic_kit.fol.nodes.Node`.
749
+ """
750
+ for sentence in sentences:
751
+ if not isinstance(sentence, Node):
752
+ raise TypeError(
753
+ f"fragment_check: every sentence must be a Node, got "
754
+ f"{type(sentence).__name__}."
755
+ )
756
+ for node in sentence.walk():
757
+ node_type = type(node)
758
+ if node_type is Atom and _is_tptp_boolean_atom(node):
759
+ # TPTP's defined propositions are the truth constants on every
760
+ # route that reads them (to_z3, the model finder, the TPTP
761
+ # writers). The grounding here would treat them as a relation
762
+ # the solver may choose, and report a 'countermodel' of `$true`.
763
+ return (f"Atom is not encodable: {node.predicate} is TPTP's defined "
764
+ f"proposition, a truth constant, and this finite-domain "
765
+ f"encoding has no constant for it (it would become a "
766
+ f"relation the solver may choose). Decide the formula "
767
+ f"with a route that reads it: api.prove(..., "
768
+ f"backends=['z3']) or the finite model finder.")
769
+ if node_type in _ALLOWED_NODE_TYPES:
770
+ continue
771
+ reason = _REJECTED_NODE_REASONS.get(node_type, _GENERIC_REASON)
772
+ return f"{node_type.__name__} is not encodable: {reason}"
773
+ return None
774
+
775
+
776
+ # =============================================================================
777
+ # structure_from_solution — the shared reconstruction
778
+ # =============================================================================
779
+
780
+ def structure_from_solution(
781
+ signature: Signature,
782
+ atoms: Iterable[Tuple[str, Sequence[int]]],
783
+ size: int,
784
+ *,
785
+ all_different: bool = False,
786
+ ) -> FiniteStructure:
787
+ """Rebuild a :class:`~unicode_logic_kit.semantics.structures.FiniteStructure`
788
+ from a solver's true ground atoms.
789
+
790
+ ``atoms`` is the one solver-agnostic shape both backends must translate
791
+ their native output INTO before calling this function: pairs of
792
+ ``(symbol_name, args)`` where ``args`` is a tuple of individual
793
+ indices in ``0 … size-1``. A backend's OWN auxiliary/helper atoms (a
794
+ ``dom/1`` domain fact, an aggregate's internal bookkeeping atom, …) must
795
+ already be filtered out by the caller — every symbol name reaching this
796
+ function is checked against ``signature`` and an unrecognised one is
797
+ refused (see Raises), so a leaked helper atom surfaces as a loud error
798
+ here rather than silently becoming a phantom predicate.
799
+
800
+ Three namespaces, told apart by which section of ``signature`` the
801
+ symbol name is declared in:
802
+
803
+ * a PREDICATE ``p`` of arity ``k`` — each atom supplies one ``k``-tuple
804
+ of the relation's extension. A predicate with zero true atoms is a
805
+ perfectly valid (empty) extension, not an error — a countermodel is
806
+ free to make a relation entirely false.
807
+ * a FUNCTION ``f`` of arity ``k`` — the standard finite-model-finding
808
+ "total relation" reading: each atom is a ``(k+1)``-tuple, the first
809
+ ``k`` entries the arguments and the last the result. This is where
810
+ the "functionality constraint (exactly one value per argument
811
+ tuple)" the design calls for is actually enforced — see Raises — so
812
+ neither backend has to re-derive that check from its own solver
813
+ output.
814
+ * a CONSTANT ``c`` — a 1-tuple naming the single individual it denotes;
815
+ the arity-0 special case of the same "exactly one value" reading.
816
+
817
+ Every declared predicate/function/constant gets an extension entry even
818
+ when no atom mentions it (an all-false relation still needs a ``set()``
819
+ in ``extensions``, or
820
+ :meth:`~unicode_logic_kit.semantics.structures.FiniteStructure.holds`
821
+ would read "no atoms" as "uninterpreted" and raise, rather than as the
822
+ correct answer "false everywhere").
823
+
824
+ Args:
825
+ signature: the problem's declared vocabulary (typically
826
+ ``problem.signature`` for some :class:`FiniteDomainProblem`).
827
+ atoms: the solver's true ground atoms, translated into the shared
828
+ ``(name, args)`` shape described above.
829
+ size: the domain size; individuals are named ``"0" … "<size-1>"``.
830
+ all_different: when ``True``, additionally check that every
831
+ declared constant denotes a DISTINCT individual (see
832
+ :class:`FiniteDomainProblem`'s ``all_different``) — a solver
833
+ whose encoding was supposed to enforce this but did not is a
834
+ solver/encoding bug, and this catches it here rather than
835
+ handing back a structure that quietly violates the convention
836
+ it was asked to honour.
837
+
838
+ Returns:
839
+ The reconstructed structure: ``domain`` is ``("0", …,
840
+ "<size-1>")``; every predicate AND every function (stored as an
841
+ arity-``(k+1)`` relation) has an extension; every constant is set.
842
+
843
+ Raises:
844
+ TypeError: ``signature`` is not a
845
+ :class:`~unicode_logic_kit.fol.signature.Signature`, or an
846
+ atom's individual index is not a plain ``int``.
847
+ ValueError: ``size < 1``; an atom names an individual index outside
848
+ ``0 … size-1``; an atom's arity does not match its symbol's
849
+ declared arity; an atom names a symbol ``signature`` does not
850
+ declare as a predicate, function, or constant; a function or
851
+ constant is given two DIFFERENT results for the same inputs
852
+ (functionality violated); a function is missing a result for
853
+ some input tuple (totality violated); a constant is never
854
+ assigned an individual; or ``all_different=True`` and two
855
+ constants denote the same individual.
856
+ """
857
+ if not isinstance(signature, Signature):
858
+ raise TypeError(
859
+ f"structure_from_solution: signature must be a Signature, got "
860
+ f"{type(signature).__name__}."
861
+ )
862
+ if isinstance(size, bool) or not isinstance(size, int) or size < 1:
863
+ raise ValueError(f"structure_from_solution: size must be an int >= 1, got {size!r}.")
864
+
865
+ domain_range = range(size)
866
+ pred_rows: Dict[Tuple[str, int], set] = {}
867
+ func_graph: Dict[str, Dict[Tuple[int, ...], int]] = {}
868
+ const_val: Dict[str, int] = {}
869
+
870
+ for name, raw_args in atoms:
871
+ args = tuple(raw_args)
872
+ for a in args:
873
+ if isinstance(a, bool) or not isinstance(a, int):
874
+ raise TypeError(
875
+ f"structure_from_solution: atom {name}{args} has a "
876
+ f"non-int individual index {a!r}."
877
+ )
878
+ bad = [a for a in args if a not in domain_range]
879
+ if bad:
880
+ raise ValueError(
881
+ f"structure_from_solution: atom {name}{args} names "
882
+ f"individual index(es) {bad} outside the domain "
883
+ f"0..{size - 1}."
884
+ )
885
+ if name in signature.predicates:
886
+ decl = signature.predicates[name]
887
+ if len(args) != decl.arity:
888
+ raise ValueError(
889
+ f"structure_from_solution: predicate {name!r} is "
890
+ f"declared arity {decl.arity}, but the solver returned "
891
+ f"a {len(args)}-tuple {args}."
892
+ )
893
+ pred_rows.setdefault((name, decl.arity), set()).add(args)
894
+ elif name in signature.functions:
895
+ decl = signature.functions[name]
896
+ expected = decl.arity + 1 # k inputs + 1 result
897
+ if len(args) != expected:
898
+ raise ValueError(
899
+ f"structure_from_solution: function {name!r} is "
900
+ f"declared arity {decl.arity}, so its total-relation "
901
+ f"encoding needs {expected} args (inputs + result), "
902
+ f"but the solver returned a {len(args)}-tuple {args}."
903
+ )
904
+ inputs, result = args[:-1], args[-1]
905
+ graph = func_graph.setdefault(name, {})
906
+ if inputs in graph and graph[inputs] != result:
907
+ raise ValueError(
908
+ f"structure_from_solution: functionality violated for "
909
+ f"{name}{inputs} — both {graph[inputs]} and {result} "
910
+ f"are claimed as the result (a solver/encoding bug: a "
911
+ f"function must have EXACTLY one value per argument "
912
+ f"tuple)."
913
+ )
914
+ graph[inputs] = result
915
+ elif name in signature.constants:
916
+ if len(args) != 1:
917
+ raise ValueError(
918
+ f"structure_from_solution: constant {name!r} names "
919
+ f"exactly one individual, but the solver returned a "
920
+ f"{len(args)}-tuple {args}."
921
+ )
922
+ (val,) = args
923
+ if name in const_val and const_val[name] != val:
924
+ raise ValueError(
925
+ f"structure_from_solution: functionality violated for "
926
+ f"constant {name!r} — both {const_val[name]} and {val} "
927
+ f"are claimed as its denotation."
928
+ )
929
+ const_val[name] = val
930
+ else:
931
+ raise ValueError(
932
+ f"structure_from_solution: the solver returned an atom for "
933
+ f"{name!r}, which the signature does not declare as a "
934
+ f"predicate, function, or constant."
935
+ )
936
+
937
+ # Totality: every function symbol needs a result for EVERY input tuple,
938
+ # not just the ones an atom happened to mention — functionality (checked
939
+ # above, one value at most) is only half of "total relation"; this is
940
+ # the other half. Bounded by size ** arity, the same finite domain the
941
+ # search already covers, so this is cheap validation of an
942
+ # already-finite solution, not the grounding this module otherwise
943
+ # deliberately avoids (see the module docstring).
944
+ for name, decl in signature.functions.items():
945
+ graph = func_graph.get(name, {})
946
+ for inputs in itertools.product(domain_range, repeat=decl.arity):
947
+ if inputs not in graph:
948
+ raise ValueError(
949
+ f"structure_from_solution: totality violated for "
950
+ f"{name}{inputs} — the total-relation encoding must "
951
+ f"give it SOME result, but the solver returned none "
952
+ f"(a solver/encoding bug)."
953
+ )
954
+
955
+ for name in signature.constants:
956
+ if name not in const_val:
957
+ raise ValueError(
958
+ f"structure_from_solution: constant {name!r} was never "
959
+ f"assigned an individual by the solver."
960
+ )
961
+
962
+ if all_different:
963
+ seen: Dict[int, str] = {}
964
+ for name, val in const_val.items():
965
+ if val in seen:
966
+ raise ValueError(
967
+ f"structure_from_solution: all_different=True requires "
968
+ f"every constant to denote a distinct individual, but "
969
+ f"{name!r} and {seen[val]!r} both denote {val}."
970
+ )
971
+ seen[val] = name
972
+
973
+ domain = tuple(str(i) for i in domain_range)
974
+ extensions: Dict[Tuple[str, int], set] = {
975
+ key: {tuple(str(i) for i in row) for row in rows}
976
+ for key, rows in pred_rows.items()
977
+ }
978
+ # Every declared predicate gets an entry even with zero true atoms — see
979
+ # the "empty extension, not uninterpreted" note in the docstring above.
980
+ for name, decl in signature.predicates.items():
981
+ extensions.setdefault((name, decl.arity), set())
982
+ for name, decl in signature.functions.items():
983
+ graph = func_graph.get(name, {})
984
+ extensions[(name, decl.arity + 1)] = {
985
+ tuple(str(i) for i in (*inputs, result))
986
+ for inputs, result in graph.items()
987
+ }
988
+ constants = {name: str(val) for name, val in const_val.items()}
989
+
990
+ return FiniteStructure(domain=domain, extensions=extensions, constants=constants)
991
+
992
+
993
+ # =============================================================================
994
+ # verify_model — the safety net
995
+ # =============================================================================
996
+
997
+ def verify_model(structure: FiniteStructure, sentences: Iterable[Node]) -> Optional[str]:
998
+ """Re-check ``structure`` against ``sentences`` with the kit's OWN evaluator.
999
+
1000
+ The §3 safety net: a backend must call this on every reconstructed
1001
+ structure before reporting ``REFUTED`` and, if it returns anything but
1002
+ ``None``, report ``ERROR``/``"infra"`` instead — never hand back a
1003
+ countermodel this function could not confirm. Delegates every sentence,
1004
+ whole, to
1005
+ :func:`~unicode_logic_kit.semantics.model_eval.evaluate` (re-exported as
1006
+ ``evaluate_in_structure``) — the kit's independent, hand-checked
1007
+ structural evaluator, so a mistake in a backend's OWN ASP/CP encoder
1008
+ cannot also be the thing that validates its output.
1009
+
1010
+ .. warning::
1011
+ ``evaluate_in_structure`` evaluates ``Cardinality`` and ``Number``
1012
+ (as a comparison operand) ARITHMETICALLY, and ``Function`` off the
1013
+ ``(name, arity+1)`` total-relation extension
1014
+ :func:`structure_from_solution` builds — see
1015
+ :mod:`~unicode_logic_kit.semantics.model_eval`'s own docstring — so
1016
+ both the counting fragment AND function-bearing sentences are fully
1017
+ re-verified here, not merely decided. ONE gap remains: ``Contrast``
1018
+ genuinely still raises ``UnsupportedNode`` here even though
1019
+ :func:`fragment_check` admits it into the encodable fragment — a
1020
+ live collision, not a moot one. This function then reports "could
1021
+ not verify", and the caller still MUST treat that as a reason not to
1022
+ report ``REFUTED`` (per this module's one rule: never hand back an
1023
+ unverified countermodel). Comparing a domain individual with a bare
1024
+ numeral (``∀x (x = 1)``) is ALSO reported as "could not verify" —
1025
+ but that is the deliberate edge of the counting fragment, not a gap.
1026
+ See the module docstring's "Known verification gap" section for the
1027
+ full account.
1028
+
1029
+ Args:
1030
+ structure: the candidate countermodel, typically fresh out of
1031
+ :func:`structure_from_solution`.
1032
+ sentences: the sentences ``structure`` is claimed to satisfy —
1033
+ typically a :class:`FiniteDomainProblem`'s ``sentences``.
1034
+
1035
+ Returns:
1036
+ ``None`` if every sentence evaluates ``True`` in ``structure``;
1037
+ otherwise a message naming the FIRST sentence (in order) that
1038
+ either evaluates ``False`` or could not be evaluated at all, and
1039
+ why.
1040
+ """
1041
+ for sentence in sentences:
1042
+ try:
1043
+ holds = _evaluate_in_structure(sentence, structure)
1044
+ except (UninterpretedSymbol, UnsupportedNode, ValueError) as exc:
1045
+ return (
1046
+ f"could not verify {sentence.to_unicode_str()!r}: the "
1047
+ f"independent checker (evaluate_in_structure) could not "
1048
+ f"evaluate it — {exc}"
1049
+ )
1050
+ if not holds:
1051
+ return (
1052
+ f"{sentence.to_unicode_str()!r} does not hold in the "
1053
+ f"reconstructed structure."
1054
+ )
1055
+ return None