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,632 @@
1
+ """The description logic **ALC** (extended to **ALCQ**): concept expressions, roles,
2
+ and negation normal form.
3
+
4
+ ALC (*Attributive Language with Complements*) is the smallest propositionally closed
5
+ description logic and the notation underlying OWL. Its *concepts* describe sets of
6
+ individuals and its *roles* binary relations between them:
7
+
8
+ - ``Top`` (⊤) — everything; ``Bottom`` (⊥) — nothing;
9
+ - ``Atomic("C")`` — a primitive concept name;
10
+ - ``Not(C)`` (¬C), ``And(C, D)`` (C ⊓ D), ``Or(C, D)`` (C ⊔ D);
11
+ - ``Exists("r", C)`` (∃r.C) — has an ``r``-successor in ``C``;
12
+ - ``ForAll("r", C)`` (∀r.C) — all ``r``-successors are in ``C``;
13
+ - ``AtLeast(n, "r", C)`` (≥n r.C) — at least ``n`` PAIRWISE-DISTINCT ``r``-successors
14
+ are in ``C``;
15
+ - ``AtMost(n, "r", C)`` (≤n r.C) — at most ``n`` pairwise-distinct ``r``-successors
16
+ are in ``C``;
17
+ - ``HasValue("r", "a")`` (∃r.{a}) — has the named individual ``a`` among its
18
+ ``r``-successors (OWL's ``ObjectHasValue``). A nominal in disguise: the
19
+ in-house tableau refuses it by name, and the first-order image
20
+ (:func:`~unicode_logic_kit.dl.translate.kb_to_fol` with ``api.prove``) and the
21
+ ``dl.external_*`` entry points decide it; see :class:`HasValue` for why it is
22
+ a constructor of its own rather than ``Exists("r", Nominal("a"))``.
23
+
24
+ ALC alone is exactly the multi-modal logic **K** (a role ``r`` is a modality, ``∃r``
25
+ its ◇ and ``∀r`` its □), which is why it is decidable; ``AtLeast``/``AtMost`` add
26
+ *qualified number restrictions* (the DL usually written **ALCQ**), decided by the
27
+ tableau's dedicated ≥/≤/choose completion rules — see the "Qualified number
28
+ restrictions" section of :mod:`unicode_logic_kit.dl.tableau`'s module docstring for the
29
+ algorithm, and note the "simple roles" restriction documented there (number
30
+ restrictions may not target a transitive role, or one with a transitive sub-role).
31
+ :func:`nnf` rewrites a concept to *negation normal form* (negation only on atomic
32
+ concepts), the shape the tableau in :mod:`unicode_logic_kit.dl.tableau` consumes.
33
+
34
+ Inverse roles and nominals (I, O) — represented, but not decided here
35
+ ------------------------------------------------------------------------
36
+ Two more constructs are recognised by this AST but sit OUTSIDE ALCHQ and are
37
+ NEVER decided by :mod:`unicode_logic_kit.dl.tableau`'s in-house tableau, which
38
+ refuses both by name (see that module's ``_reject_beyond_alc``):
39
+
40
+ - :class:`InverseRole` (``r⁻``) — a *role expression*, not a concept: it is
41
+ used wherever a plain role NAME (a ``str``) is expected, i.e. as the
42
+ ``role`` field of :class:`Exists`/:class:`ForAll`/:class:`AtLeast`/
43
+ :class:`AtMost` (``Exists(InverseRole("hasChild"), C)`` is ``∃hasChild⁻.C``).
44
+ This is deliberate: that field is never type-checked anywhere in this
45
+ module (see :func:`nnf`'s Exists/ForAll branches), so an
46
+ ``InverseRole``-wrapped role sails through unrecognised rather than
47
+ crashing — which is exactly why :mod:`unicode_logic_kit.dl.tableau` needs its
48
+ own explicit guard rather than relying on this module to reject it "for
49
+ free" (see that module's docstring for the full soundness argument).
50
+ - :class:`Nominal` (``{a}``) — the singleton concept containing exactly the
51
+ named individual ``a`` (OWL's ``ObjectOneOf`` restricted to one
52
+ individual; a multi-individual nominal set ``{a, b}`` is expressed as
53
+ ``Or(Nominal("a"), Nominal("b"))`` — semantically identical, since OWL 2
54
+ gives ``ObjectOneOf`` exactly that union reading, and it avoids a second
55
+ AST shape for "a set of names").
56
+
57
+ :class:`HasValue` (``∃r.{a}``) belongs on that list too. It names an
58
+ individual inside a concept, so it is a NOMINAL in disguise, and the in-house
59
+ tableau refuses it by name exactly as it refuses a bare :class:`Nominal`. An
60
+ in-house value rule was built and removed again, because it is unsound under
61
+ subset blocking; "Value restrictions (ObjectHasValue)" in
62
+ :mod:`unicode_logic_kit.dl.tableau`'s module docstring has the two-axiom
63
+ counterexample. What decides a value restriction is the first-order image
64
+ (:func:`~unicode_logic_kit.dl.translate.kb_to_fol`, then ``api.prove``) and the
65
+ external, HermiT-backed reasoner (the ``dl.external_*`` entry points).
66
+ :func:`nnf` passes it through (it refuses a ``Nominal``).
67
+
68
+ Only :mod:`unicode_logic_kit.dl.owl_reasoner`'s external, HermiT-backed reasoner
69
+ decides concepts built from an :class:`InverseRole` or a :class:`Nominal` — see
70
+ that module's docstring.
71
+ :func:`nnf` refuses EVERY concept containing a ``Nominal`` — bare
72
+ (``nnf(Nominal("a"))``) or nested (``nnf(Not(Nominal("a")))``,
73
+ ``nnf(And(Nominal("a"), C))``, …) — with a :class:`TypeError`, by design (see
74
+ :func:`nnf`'s own docstring); this is deliberately NOT weakened anywhere to
75
+ give ``Nominal`` "free" passage the way ``Top``/``Bottom``/``Atomic`` get.
76
+ ``to_unicode``/:func:`_render`, by contrast, DO handle ``Nominal`` (and an
77
+ ``InverseRole``-valued role), since rendering has no soundness consequence —
78
+ a mixed ALCHQ+I/O concept can still be printed for diagnostics even though
79
+ the in-house tableau will never reason over it.
80
+
81
+ The classes live in the ``dl`` namespace (``import unicode_logic_kit.dl as dl``); the
82
+ names ``And`` / ``Or`` / ``Not`` are the concept constructors, distinct from the FOL
83
+ AST's connectives.
84
+ """
85
+
86
+ from dataclasses import dataclass
87
+ from typing import Iterator
88
+
89
+ from .datatypes import DataRange, Literal, _paren_range
90
+
91
+
92
+ class Concept:
93
+ """Base class for ALC concept expressions (see the module docstring)."""
94
+
95
+ def to_unicode(self) -> str:
96
+ """Render the concept with the standard DL glyphs (⊤ ⊥ ¬ ⊓ ⊔ ∃ ∀).
97
+
98
+ The text reads back as the same concept with
99
+ :func:`~unicode_logic_kit.dl.parser.parse_concept` (a right operand of the
100
+ same connective is parenthesised: ``And(A, And(B, C))`` is
101
+ ``A ⊓ (B ⊓ C)``), except for the constructors the glyph syntax cannot
102
+ read — see that module's docstring.
103
+
104
+ A NAME that holds whitespace or one of the glyphs the syntax reserves
105
+ (``⊓ ⊔ ¬ ⊤ ⊥ ∃ ∀ ≥ ≤ ( ) . ⊑ { }``) has no spelling in the glyph syntax
106
+ (there is no escape), and such a name is written as it is, for display: the
107
+ reader then refuses the text. A name for which it would instead read some
108
+ OTHER concept — ``Atomic("A ⊓ B")`` prints ``A ⊓ B``, which is the
109
+ intersection of two classes — is refused here.
110
+
111
+ Raises:
112
+ ValueError: a class, role or individual name makes the text read back
113
+ as a different concept.
114
+ """
115
+ text = _render(self)
116
+ _reject_misread_names(self, text)
117
+ return text
118
+
119
+ def __str__(self) -> str:
120
+ # Display only, so it never raises: a name the glyph syntax cannot spell
121
+ # is shown as it is (``to_unicode`` is the one that refuses it).
122
+ return _render(self)
123
+
124
+
125
+ @dataclass(frozen=True)
126
+ class Top(Concept):
127
+ """The universal concept ⊤ (every individual)."""
128
+
129
+
130
+ @dataclass(frozen=True)
131
+ class Bottom(Concept):
132
+ """The empty concept ⊥ (no individual)."""
133
+
134
+
135
+ @dataclass(frozen=True)
136
+ class Atomic(Concept):
137
+ """A primitive concept name, e.g. ``Atomic("Person")``."""
138
+
139
+ name: str
140
+
141
+
142
+ @dataclass(frozen=True)
143
+ class Not(Concept):
144
+ """Concept complement ¬C."""
145
+
146
+ concept: Concept
147
+
148
+
149
+ @dataclass(frozen=True)
150
+ class And(Concept):
151
+ """Concept intersection C ⊓ D."""
152
+
153
+ left: Concept
154
+ right: Concept
155
+
156
+
157
+ @dataclass(frozen=True)
158
+ class Or(Concept):
159
+ """Concept union C ⊔ D."""
160
+
161
+ left: Concept
162
+ right: Concept
163
+
164
+
165
+ @dataclass(frozen=True)
166
+ class Exists(Concept):
167
+ """Existential restriction ∃r.C — at least one ``role``-successor is in ``concept``."""
168
+
169
+ role: str
170
+ concept: Concept
171
+
172
+
173
+ @dataclass(frozen=True)
174
+ class ForAll(Concept):
175
+ """Value restriction ∀r.C — every ``role``-successor is in ``concept``."""
176
+
177
+ role: str
178
+ concept: Concept
179
+
180
+
181
+ @dataclass(frozen=True)
182
+ class AtLeast(Concept):
183
+ """Qualified at-least number restriction ≥n r.C — at least ``n`` PAIRWISE-DISTINCT
184
+ ``role``-successors are in ``concept``.
185
+
186
+ No unique name assumption is in force anywhere in this kit's DL reasoner (see
187
+ :mod:`unicode_logic_kit.dl.tableau`), so "pairwise-distinct" is a genuine semantic
188
+ requirement, not just "n role-successors that happen to carry different names" —
189
+ two successors are only known distinct when something *forces* it (see the
190
+ tableau module docstring). ``n = 0`` is a tautology (every individual vacuously
191
+ has at least zero such successors, so it never constrains anything); ``n = 1``
192
+ is semantically ``Exists(role, concept)`` but kept as a separate constructor
193
+ (the tableau still gives it its own, simpler ∃-rule) rather than folded away.
194
+ """
195
+
196
+ n: int
197
+ role: str
198
+ concept: Concept
199
+
200
+ def __post_init__(self):
201
+ """Validate that n is a non-negative integer (mirrors Count's own check)."""
202
+ if not (isinstance(self.n, int) and self.n >= 0):
203
+ raise ValueError(f"AtLeast: n must be a non-negative integer, got {self.n!r}")
204
+
205
+
206
+ @dataclass(frozen=True)
207
+ class AtMost(Concept):
208
+ """Qualified at-most number restriction ≤n r.C — at most ``n`` pairwise-distinct
209
+ ``role``-successors are in ``concept`` (see :class:`AtLeast` for the "no unique
210
+ name assumption" caveat, which applies here identically). ``n = 0`` forbids any
211
+ ``role``-successor in ``concept`` at all.
212
+ """
213
+
214
+ n: int
215
+ role: str
216
+ concept: Concept
217
+
218
+ def __post_init__(self):
219
+ """Validate that n is a non-negative integer (mirrors Count's own check)."""
220
+ if not (isinstance(self.n, int) and self.n >= 0):
221
+ raise ValueError(f"AtMost: n must be a non-negative integer, got {self.n!r}")
222
+
223
+
224
+ @dataclass(frozen=True)
225
+ class InverseRole:
226
+ """A role EXPRESSION ``r⁻`` (OWL's ``ObjectInverseOf``) — the inverse of the
227
+ named role ``role``: ``x —r⁻→ y`` holds exactly when ``y —r→ x`` does.
228
+
229
+ Deliberately NOT a :class:`Concept` subclass: it is used in place of a plain
230
+ role NAME (a ``str``), as the ``role`` field of :class:`Exists`/:class:`ForAll`/
231
+ :class:`AtLeast`/:class:`AtMost` (``Exists(InverseRole("hasChild"), C)`` is
232
+ ``∃hasChild⁻.C``) — see the module docstring's "Inverse roles and nominals"
233
+ section for why that field being untyped is exactly what lets this work
234
+ without any change to :func:`nnf`'s Exists/ForAll/AtLeast/AtMost branches,
235
+ and why that same fact makes it the in-house tableau's job, not this
236
+ module's, to refuse it (:mod:`unicode_logic_kit.dl.tableau`'s
237
+ ``_reject_beyond_alc``).
238
+ """
239
+
240
+ role: str
241
+
242
+
243
+ @dataclass(frozen=True)
244
+ class Nominal(Concept):
245
+ """The nominal concept ``{a}`` (OWL's ``ObjectOneOf`` restricted to one
246
+ individual) — the singleton concept containing exactly the named individual
247
+ ``individual``, and nothing else, in EVERY model (no unique name assumption
248
+ is needed for this: a nominal fixes membership by naming the element
249
+ directly, not by comparison with another name — see
250
+ :mod:`unicode_logic_kit.dl.tableau`'s "Qualified number restrictions" section
251
+ for how the reasoner treats individual identity elsewhere).
252
+
253
+ Outside ALCHQ — see the module docstring's "Inverse roles and nominals"
254
+ section: :func:`nnf` refuses any concept containing one, and only
255
+ :mod:`unicode_logic_kit.dl.owl_reasoner`'s external reasoner decides concepts
256
+ that use it. A multi-individual nominal set ``{a, b}`` is
257
+ ``Or(Nominal("a"), Nominal("b"))`` — see that section for why this needs no
258
+ separate AST shape.
259
+ """
260
+
261
+ individual: str
262
+
263
+
264
+ @dataclass(frozen=True)
265
+ class HasValue(Concept):
266
+ """The value restriction ``∃r.{a}`` (OWL's ``ObjectHasValue(r a)``) — the
267
+ concept of the individuals that have ``individual`` among their
268
+ ``role``-successors.
269
+
270
+ OWL 2 direct semantics: ``(ObjectHasValue(P a))^C = { x | <x, a^I> ∈ P^OP }``.
271
+ The standard translation of the equivalent ``∃P.{a}`` is
272
+ ``∃y (P(x, y) ∧ y = a)``, and one-point elimination of the equality-bounded
273
+ existential (``y`` occurs only in that conjunction, so the equivalence is a
274
+ FOL validity) reduces it to the atom ``P(x, a)`` — which is the image
275
+ :mod:`unicode_logic_kit.dl.translate` produces: same models, no minted
276
+ variable, and the individual in the argument position OWL's own reading
277
+ puts it in.
278
+
279
+ A node of its OWN rather than ``Exists(role, Nominal(individual))``, for
280
+ two reasons:
281
+
282
+ * **Round trip.** ``ObjectHasValue(r a)`` and the (still refused)
283
+ ``ObjectSomeValuesFrom(r ObjectOneOf(a))`` are two distinct OWL 2
284
+ structural objects; folding both into one AST shape would make
285
+ ``parse(render(c)) == c`` stop holding for one of them. Same on the
286
+ Manchester side: ``r value a`` versus the refused ``r some {a}``.
287
+ * **It is the textbook DL reading.** There is no separate glyph for a value
288
+ restriction, so this renders as ``∃r.{a}`` — identical to
289
+ ``Exists(role, Nominal(a))``'s rendering, which is honest, because the
290
+ two say the same thing. The glyph syntax therefore has two AST shapes
291
+ with one rendering, and :mod:`unicode_logic_kit.dl.parser` refuses that
292
+ text by name rather than guessing which was meant.
293
+
294
+ Like :class:`Nominal`, this is OUTSIDE the fragment the in-house tableau
295
+ decides: a value restriction is a nominal in disguise, the tableau refuses
296
+ it by name, and the first-order image
297
+ (:func:`~unicode_logic_kit.dl.translate.kb_to_fol`, then ``api.prove``) and the
298
+ ``dl.external_*`` entry points decide it instead — see "Value restrictions (ObjectHasValue)" in
299
+ :mod:`unicode_logic_kit.dl.tableau`'s module docstring for why a tableau rule
300
+ for it is unsound (a two-axiom counterexample). Unlike :class:`Nominal`,
301
+ :func:`nnf` passes it through, and ``Not(HasValue(...))`` is a LITERAL (like
302
+ ``Not(Atomic(...))``) with no dual node — the dual of "has ``a`` as an
303
+ ``r``-successor" is "does not", which is a condition on an EDGE, not a
304
+ concept :func:`nnf` could push inward.
305
+ """
306
+
307
+ role: str
308
+ individual: str
309
+
310
+
311
+ # --------------------------------------------------------------------------- #
312
+ # Data restrictions: the concept constructors that mention a DATA property.
313
+ # --------------------------------------------------------------------------- #
314
+ #
315
+ # Field named ``prop`` and not ``role`` on purpose: a data property is not an
316
+ # object role, and a generic walker that reads ``.role`` (the simple-role check,
317
+ # the modal translation, the OWL writers) must NOT take it for one.
318
+
319
+ @dataclass(frozen=True)
320
+ class DataExists(Concept):
321
+ """The data restriction ``∃d.DR`` (OWL's ``DataSomeValuesFrom(d DR)``) — has
322
+ at least one ``prop``-value in the data range ``datarange``.
323
+
324
+ OWL 2 direct semantics: ``{ x | ∃v. (x, v) ∈ d^DP ∧ v ∈ DR^DT }``. The
325
+ first-order image is ``∃w (d(x, w) ∧ δ(DR, w))``.
326
+
327
+ Outside the fragment the in-house tableau decides: it has no data domain,
328
+ so it REFUSES a concept containing one by name. The first-order image is
329
+ the route that answers (see :func:`unicode_logic_kit.dl.translate.kb_to_fol`
330
+ and :func:`unicode_logic_kit.dl.translate.data_sort_axioms`).
331
+ """
332
+
333
+ prop: str
334
+ datarange: DataRange
335
+
336
+
337
+ @dataclass(frozen=True)
338
+ class DataForAll(Concept):
339
+ """The data restriction ``∀d.DR`` (OWL's ``DataAllValuesFrom(d DR)``) —
340
+ every ``prop``-value is in the data range ``datarange``. Image:
341
+ ``∀w (d(x, w) → δ(DR, w))``."""
342
+
343
+ prop: str
344
+ datarange: DataRange
345
+
346
+
347
+ @dataclass(frozen=True)
348
+ class DataHasValue(Concept):
349
+ """The data value restriction (OWL's ``DataHasValue(d lt)``) — has the
350
+ literal ``value`` among its ``prop``-values.
351
+
352
+ OWL 2 direct semantics: ``{ x | (x, lt^LT) ∈ d^DP }``. There is no
353
+ quantifier — a literal denotes ONE fixed data value — so the image is the
354
+ ground atom ``d(x, t)`` for the literal's term ``t``: no bound variable and
355
+ no datatype guard (the literal already pins its datatype).
356
+ """
357
+
358
+ prop: str
359
+ value: Literal
360
+
361
+
362
+ @dataclass(frozen=True)
363
+ class DataAtLeast(Concept):
364
+ """The qualified data number restriction ``≥n d.DR`` (OWL's
365
+ ``DataMinCardinality(n d DR)``) — at least ``n`` PAIRWISE-DISTINCT
366
+ ``prop``-values are in ``datarange``. Image: the same ``Count`` node the
367
+ object ``AtLeast`` uses, over the data atom. ``DataExactCardinality`` is
368
+ not a node of its own: the parsers desugar it to ``DataAtLeast ⊓ DataAtMost``,
369
+ as ``ObjectExactCardinality`` is."""
370
+
371
+ n: int
372
+ prop: str
373
+ datarange: DataRange
374
+
375
+ def __post_init__(self):
376
+ """Validate that n is a non-negative integer (mirrors AtLeast's check)."""
377
+ if not (isinstance(self.n, int) and self.n >= 0):
378
+ raise ValueError(f"DataAtLeast: n must be a non-negative integer, got {self.n!r}")
379
+
380
+
381
+ @dataclass(frozen=True)
382
+ class DataAtMost(Concept):
383
+ """The qualified data number restriction ``≤n d.DR`` (OWL's
384
+ ``DataMaxCardinality(n d DR)``) — at most ``n`` pairwise-distinct
385
+ ``prop``-values are in ``datarange``."""
386
+
387
+ n: int
388
+ prop: str
389
+ datarange: DataRange
390
+
391
+ def __post_init__(self):
392
+ """Validate that n is a non-negative integer (mirrors AtMost's check)."""
393
+ if not (isinstance(self.n, int) and self.n >= 0):
394
+ raise ValueError(f"DataAtMost: n must be a non-negative integer, got {self.n!r}")
395
+
396
+
397
+ #: Every data restriction class, for the walkers that must treat them alike.
398
+ DATA_CONCEPTS = (DataExists, DataForAll, DataHasValue, DataAtLeast, DataAtMost)
399
+
400
+
401
+ def _refuse_data_concept(concept: Concept) -> None:
402
+ """Raise the loud ``TypeError`` :func:`nnf` gives a data restriction."""
403
+ raise TypeError(
404
+ f"nnf: the data restriction {type(concept).__name__} ({_render(concept)}) "
405
+ f"has no negation normal form in this kit: the in-house tableau has no "
406
+ f"data domain, so its dual (a restriction over the COMPLEMENT of the "
407
+ f"data range, taken within the data domain) would be a rule nothing "
408
+ f"implements. The route that answers is the first-order image, which is "
409
+ f"sound for the data layer and not complete: kb = dl.kb_to_fol(tbox, abox, "
410
+ f"query=[concept]) and its goal methods (kb.unsatisfiability_goal, "
411
+ f"kb.subsumption_goal, kb.instance_goal) with api.prove.")
412
+
413
+
414
+ def nnf(concept: Concept) -> Concept:
415
+ """Return ``concept`` in negation normal form (negation only on atomic concepts).
416
+
417
+ Pushes ¬ inward with the De Morgan / modal dualities:
418
+ ``¬⊤ = ⊥``, ``¬⊥ = ⊤``, ``¬¬C = C``, ``¬(C⊓D) = ¬C⊔¬D``, ``¬(C⊔D) = ¬C⊓¬D``,
419
+ ``¬∃r.C = ∀r.¬C``, ``¬∀r.C = ∃r.¬C``, and the number-restriction duals
420
+ ``¬(≥n r.C) = ≤(n-1) r.C`` for ``n ≥ 1`` (``¬(≥0 r.C) = ⊥``, since ``≥0`` is a
421
+ tautology), ``¬(≤n r.C) = ≥(n+1) r.C``.
422
+
423
+ Raises ``TypeError`` for any concept outside ALCHQ, in particular a
424
+ :class:`Nominal` anywhere in ``concept`` — bare or nested under ``Not``/
425
+ ``And``/``Or``/a restriction. This is INTENTIONAL and must stay this way:
426
+ ``Nominal`` is deliberately left OUT of the top-level ``(Top, Bottom,
427
+ Atomic)`` pass-through tuple below, even though it is "atomic-shaped" and
428
+ could technically be passed through unchanged like they are. Doing so
429
+ would remove a free, structurally-guaranteed second line of defense that
430
+ :mod:`unicode_logic_kit.dl.tableau` relies on: every call this kit makes
431
+ into ``nnf`` from ``tableau.py`` is already guarded by that module's own
432
+ ``_reject_beyond_alc`` walker, so this raise is never *supposed* to fire
433
+ in normal operation — but if that guard ever has a gap (a future call
434
+ site, a missed nested occurrence), a ``Nominal`` reaching here still fails
435
+ LOUDLY instead of silently being treated as an ordinary opaque
436
+ :class:`Atomic` concept and producing a too-permissive verdict. See
437
+ ``tests/test_dl_alc.py``'s dedicated regression test (which calls ``nnf``
438
+ directly, bypassing the tableau's guard on purpose) and
439
+ :mod:`unicode_logic_kit.dl.tableau`'s module docstring for the full
440
+ soundness argument. An ``InverseRole``-valued ``role`` field needs no
441
+ parallel case here at all: ``Exists``/``ForAll``/``AtLeast``/``AtMost``
442
+ never inspect their ``role`` field's type, so it already sails through
443
+ every branch below untouched, for exactly the same reason it is NOT safe
444
+ on its own — see ``tableau.py`` for why that, too, is refused there.
445
+ """
446
+ if isinstance(concept, (Top, Bottom, Atomic)):
447
+ return concept
448
+ # A LITERAL of its own, like Atomic: a value restriction has no dual
449
+ # concept to push a negation into (see HasValue's class docstring), so both
450
+ # polarities pass through unchanged -- the positive one here, the negative
451
+ # one in the Not(inner) dispatch below. A new literal-shaped concept kind
452
+ # belongs here and there, never in the (Top, Bottom, Atomic) tuple above,
453
+ # whose membership carries the Nominal argument documented in this
454
+ # function's own docstring.
455
+ if isinstance(concept, HasValue):
456
+ return concept
457
+ if isinstance(concept, DATA_CONCEPTS):
458
+ _refuse_data_concept(concept)
459
+ if isinstance(concept, And):
460
+ return And(nnf(concept.left), nnf(concept.right))
461
+ if isinstance(concept, Or):
462
+ return Or(nnf(concept.left), nnf(concept.right))
463
+ if isinstance(concept, Exists):
464
+ return Exists(concept.role, nnf(concept.concept))
465
+ if isinstance(concept, ForAll):
466
+ return ForAll(concept.role, nnf(concept.concept))
467
+ if isinstance(concept, AtLeast):
468
+ return AtLeast(concept.n, concept.role, nnf(concept.concept))
469
+ if isinstance(concept, AtMost):
470
+ return AtMost(concept.n, concept.role, nnf(concept.concept))
471
+ if isinstance(concept, Not):
472
+ inner = concept.concept
473
+ if isinstance(inner, Top):
474
+ return Bottom()
475
+ if isinstance(inner, Bottom):
476
+ return Top()
477
+ if isinstance(inner, Atomic):
478
+ return concept
479
+ if isinstance(inner, HasValue):
480
+ return concept # a literal; see the positive case above
481
+ if isinstance(inner, DATA_CONCEPTS):
482
+ _refuse_data_concept(inner)
483
+ if isinstance(inner, Not):
484
+ return nnf(inner.concept)
485
+ if isinstance(inner, And):
486
+ return Or(nnf(Not(inner.left)), nnf(Not(inner.right)))
487
+ if isinstance(inner, Or):
488
+ return And(nnf(Not(inner.left)), nnf(Not(inner.right)))
489
+ if isinstance(inner, Exists):
490
+ return ForAll(inner.role, nnf(Not(inner.concept)))
491
+ if isinstance(inner, ForAll):
492
+ return Exists(inner.role, nnf(Not(inner.concept)))
493
+ if isinstance(inner, AtLeast):
494
+ if inner.n == 0:
495
+ return Bottom() # ¬(≥0 r.C): ≥0 is a tautology
496
+ return AtMost(inner.n - 1, inner.role, nnf(inner.concept))
497
+ if isinstance(inner, AtMost):
498
+ return AtLeast(inner.n + 1, inner.role, nnf(inner.concept))
499
+ raise TypeError(f"nnf: unsupported concept {type(concept).__name__}")
500
+
501
+
502
+ _PREC = {Or: 1, And: 2, Not: 3, Exists: 3, ForAll: 3, AtLeast: 3, AtMost: 3,
503
+ HasValue: 3, DataExists: 3, DataForAll: 3, DataHasValue: 3,
504
+ DataAtLeast: 3, DataAtMost: 3,
505
+ Atomic: 4, Top: 4, Bottom: 4, Nominal: 4}
506
+
507
+
508
+ def _render_role(role) -> str:
509
+ """Render a ``role`` field (a plain ``str``, or an :class:`InverseRole` —
510
+ see the module docstring's "Inverse roles and nominals" section): ``r⁻``
511
+ for the latter, matching :func:`_render`'s other glyph choices. This is
512
+ display-only, unlike :mod:`unicode_logic_kit.dl.tableau`'s refusal of
513
+ ``InverseRole``: rendering carries no soundness consequence.
514
+ """
515
+ if isinstance(role, InverseRole):
516
+ return f"{role.role}⁻"
517
+ return role
518
+
519
+
520
+ def _render(c: Concept) -> str:
521
+ """Render a concept with precedence-aware parenthesisation."""
522
+ if isinstance(c, Top):
523
+ return "⊤"
524
+ if isinstance(c, Bottom):
525
+ return "⊥"
526
+ if isinstance(c, Atomic):
527
+ return c.name
528
+ if isinstance(c, Nominal):
529
+ return "{" + c.individual + "}"
530
+ if isinstance(c, HasValue):
531
+ # The textbook DL spelling of a value restriction -- there is no glyph
532
+ # of its own, so this is ∃r.{a}, the same text Exists(r, Nominal(a))
533
+ # renders to. Honest (the two concepts have the same models) and the
534
+ # reason dl.parser refuses to read it back: see HasValue's docstring.
535
+ return f"∃{_render_role(c.role)}.{{{c.individual}}}"
536
+ if isinstance(c, Not):
537
+ return "¬" + _paren(c.concept, 3)
538
+ # The reader folds a flat chain of one connective to the LEFT (``A ⊓ B ⊓ C``
539
+ # is ``(A ⊓ B) ⊓ C``), so only a LEFT operand of the same connective may go
540
+ # unparenthesised: the right operand is written one level tighter, and an
541
+ # ``⊓`` inside an ``⊓`` (an ``⊔`` inside an ``⊔``) on the right keeps its
542
+ # parentheses.
543
+ if isinstance(c, And):
544
+ return f"{_paren(c.left, 2)} ⊓ {_paren(c.right, 3)}"
545
+ if isinstance(c, Or):
546
+ return f"{_paren(c.left, 1)} ⊔ {_paren(c.right, 2)}"
547
+ if isinstance(c, Exists):
548
+ return f"∃{_render_role(c.role)}.{_paren(c.concept, 3)}"
549
+ if isinstance(c, ForAll):
550
+ return f"∀{_render_role(c.role)}.{_paren(c.concept, 3)}"
551
+ if isinstance(c, AtLeast):
552
+ return f"≥{c.n} {_render_role(c.role)}.{_paren(c.concept, 3)}"
553
+ if isinstance(c, AtMost):
554
+ return f"≤{c.n} {_render_role(c.role)}.{_paren(c.concept, 3)}"
555
+ # Display-only, like everything here: the glyph parser has no data syntax,
556
+ # so these are read with dl.parse_manchester or dl.parse_owl_functional. A
557
+ # COMPOUND data range is parenthesised, so `∃d.(xsd:integer ⊔ xsd:string)`
558
+ # is not mistaken for `(∃d.xsd:integer) ⊔ xsd:string`.
559
+ if isinstance(c, DataExists):
560
+ return f"∃{c.prop}.{_paren_range(c.datarange)}"
561
+ if isinstance(c, DataForAll):
562
+ return f"∀{c.prop}.{_paren_range(c.datarange)}"
563
+ if isinstance(c, DataHasValue):
564
+ return f"∃{c.prop}.{{{c.value.to_unicode()}}}"
565
+ if isinstance(c, DataAtLeast):
566
+ return f"≥{c.n} {c.prop}.{_paren_range(c.datarange)}"
567
+ if isinstance(c, DataAtMost):
568
+ return f"≤{c.n} {c.prop}.{_paren_range(c.datarange)}"
569
+ raise TypeError(f"render: unsupported concept {type(c).__name__}")
570
+
571
+
572
+ def _paren(c: Concept, parent_prec: int) -> str:
573
+ """Parenthesise ``c`` when its precedence is below the parent's."""
574
+ inner = _render(c)
575
+ return f"({inner})" if _PREC.get(type(c), 4) < parent_prec else inner
576
+
577
+
578
+ def _role_name(role) -> str:
579
+ """The name inside a ``role`` field: the string, or an :class:`InverseRole`'s."""
580
+ return role.role if isinstance(role, InverseRole) else role
581
+
582
+
583
+ def _names_of(c: Concept) -> Iterator[str]:
584
+ """Every class, role, individual and data-property name inside ``c``."""
585
+ if isinstance(c, Atomic):
586
+ yield c.name
587
+ elif isinstance(c, Nominal):
588
+ yield c.individual
589
+ elif isinstance(c, Not):
590
+ yield from _names_of(c.concept)
591
+ elif isinstance(c, (And, Or)):
592
+ yield from _names_of(c.left)
593
+ yield from _names_of(c.right)
594
+ elif isinstance(c, (Exists, ForAll, AtLeast, AtMost)):
595
+ yield _role_name(c.role)
596
+ yield from _names_of(c.concept)
597
+ elif isinstance(c, HasValue):
598
+ yield _role_name(c.role)
599
+ yield c.individual
600
+ elif isinstance(c, DATA_CONCEPTS):
601
+ yield c.prop
602
+
603
+
604
+ def _reject_misread_names(concept: Concept, text: str) -> None:
605
+ """Raise :class:`ValueError` unless ``text``, the glyph rendering of ``concept``,
606
+ is either read back as ``concept`` or refused by the reader.
607
+
608
+ A name made of ordinary characters is one NAME token and is read back as
609
+ itself; the only way the text can read as ANOTHER concept is a name that holds
610
+ whitespace or one of the reader's reserved glyphs. So the reader is asked only
611
+ when such a name occurs (the glyph syntax has no escape, and an IRI with a dot
612
+ in it is a common name that the reader refuses, which is fine: that is
613
+ loud). Imported here because the parser module imports this one.
614
+ """
615
+ from .parser import _GLYPH_TOKENS, parse_concept
616
+
617
+ odd = sorted({name for name in _names_of(concept)
618
+ if isinstance(name, str)
619
+ and (name == "" or any(ch.isspace() or ch in _GLYPH_TOKENS for ch in name))})
620
+ if not odd:
621
+ return
622
+ try:
623
+ back = parse_concept(text)
624
+ except (ValueError, RecursionError):
625
+ return # the reader refuses this text: nothing reads it as another concept
626
+ if back != concept:
627
+ raise ValueError(
628
+ f"to_unicode: {text!r} reads back as {back!r}, not as the concept it "
629
+ f"was written from: the name(s) {odd!r} hold whitespace or a glyph the glyph syntax "
630
+ f"reserves, and that syntax has no escape for them. Rename the class or role, or "
631
+ f"write the concept with dl.to_owl_functional_class_expression, whose <...> carries "
632
+ f"any name that has no '>' in it.")