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,2433 @@
1
+ """A parser/renderer for the **OWL 2 Functional-Style Syntax**, restricted to ALCHQ.
2
+
3
+ `OWL 2 Functional-Style Syntax <https://www.w3.org/TR/owl2-syntax/#Functional-Style_Syntax>`_
4
+ is the W3C-standardised, S-expression-shaped ``Keyword(arg arg ...)`` concrete
5
+ syntax for a whole OWL 2 *ontology document* — Declarations, an ``Ontology(...)``
6
+ wrapper, and a flat list of axioms — as opposed to
7
+ :mod:`unicode_logic_kit.dl.owl_manchester`'s frame-based, keyword-infix Manchester
8
+ Syntax, which this module otherwise mirrors closely: same recursive-descent
9
+ style, same "refuse loudly, name the construct" convention, and the same
10
+ treatment of prefixed/full-IRI names as opaque :class:`~unicode_logic_kit.dl.concepts.Atomic`/
11
+ role/individual strings, with no RDF-graph or IRI-resolution machinery. This is
12
+ the *OWL bridge, part 2*: it reads/writes exactly the ALCHQ-expressible fragment
13
+ of a Functional-Style ontology document into/from :class:`~unicode_logic_kit.dl.tableau.TBox`/
14
+ :class:`~unicode_logic_kit.dl.tableau.ABox`, plus a standalone class-expression
15
+ entry point analogous to :func:`~unicode_logic_kit.dl.owl_manchester.parse_manchester`.
16
+
17
+ Supported fragment (ALCHQ — this kit's DL fragment, see
18
+ :mod:`unicode_logic_kit.dl.tableau`'s module docstring for the RBox/counting
19
+ machinery this reduces to):
20
+
21
+ - **Document shell**: zero or more ``Prefix(pfx:=<iri>)`` lines (parsed and
22
+ discarded — see "IRIs and names" below), then one
23
+ ``Ontology(<iri>? <version-iri>? axiom*)`` wrapper (both IRIs optional and,
24
+ like the prefixes, discarded: this module carries no ontology-metadata
25
+ state, only the TBox/ABox content);
26
+ - **Class expressions**: ``owl:Thing``/``owl:Nothing`` (⊤/⊥, either the
27
+ abbreviated spelling or the resolved full IRI —
28
+ ``<http://www.w3.org/2002/07/owl#Thing>``/``...#Nothing``);
29
+ ``ObjectIntersectionOf(CE CE+)``, ``ObjectUnionOf(CE CE+)`` (n-ary,
30
+ folding **left** into the binary :class:`~unicode_logic_kit.dl.concepts.And`/
31
+ :class:`~unicode_logic_kit.dl.concepts.Or` AST, matching
32
+ :mod:`unicode_logic_kit.dl.owl_manchester`'s documented left-fold for
33
+ ``and``/``or`` chains); ``ObjectComplementOf(CE)``;
34
+ ``ObjectSomeValuesFrom(OPE CE)``, ``ObjectAllValuesFrom(OPE CE)``,
35
+ ``ObjectHasValue(OPE a)`` (a value restriction ->
36
+ :class:`~unicode_logic_kit.dl.concepts.HasValue`, a concept kind of its own
37
+ and NOT ``ObjectSomeValuesFrom(OPE ObjectOneOf(a))``, which stays
38
+ refused -- see that class's docstring);
39
+ ``ObjectMinCardinality(n OPE CE?)``, ``ObjectMaxCardinality(n OPE CE?)``,
40
+ ``ObjectExactCardinality(n OPE CE?)`` (the qualifying class-expression
41
+ argument is OPTIONAL per the W3C grammar's ``'(' n ObjectPropertyExpression
42
+ ClassExpression? ')'`` production, defaulting to ``owl:Thing`` when
43
+ omitted — both the 2-arg "unqualified" and 3-arg "qualified" spellings are
44
+ accepted on read; see "Qualified number restrictions" below); atomic names
45
+ (IRI or prefixed name, taken verbatim — see "IRIs and names").
46
+ - **Axioms**: ``SubClassOf(CE CE)`` -> ``TBox.add``; ``EquivalentClasses(CE
47
+ CE+)`` -> a chain of pairwise ``TBox.add_equivalence`` calls (sound: OWL's
48
+ n-ary ``EquivalentClasses`` is defined as pairwise equivalence — see
49
+ "EquivalentClasses and DisjointClasses" below); ``DisjointClasses(CE CE+)``
50
+ -> reduced to one ``SubClassOf(ObjectIntersectionOf(Ci Cj) owl:Nothing)``
51
+ per unordered pair, since ALCHQ has no primitive "disjoint" TBox axiom
52
+ (see the same section); ``SubObjectPropertyOf(OPE OPE)`` ->
53
+ ``TBox.add_role_inclusion``; ``TransitiveObjectProperty(OPE)`` ->
54
+ ``TBox.add_transitive_role``; ``ObjectPropertyDomain(OPE CE)`` /
55
+ ``ObjectPropertyRange(OPE CE)`` -> ``TBox.add_role_domain`` /
56
+ ``TBox.add_role_range`` (stored NATIVELY in the role box, not desugared
57
+ into the equivalent GCI -- see ``TBox.add_role_domain``);
58
+ ``SameIndividual(a1 a2+)`` -> a CONSECUTIVE chain of
59
+ ``ABox.assert_same`` calls (sound because equality is transitive, so the
60
+ chain entails every pair -- the same argument ``EquivalentClasses`` uses,
61
+ and deliberately NOT ``DifferentIndividuals``' all-pairs expansion);
62
+ ``NegativeObjectPropertyAssertion(OPE a b)`` ->
63
+ ``ABox.assert_negative_role``; ``ClassAssertion(CE a)`` ->
64
+ ``ABox.assert_concept``; ``ObjectPropertyAssertion(OPE a b)`` ->
65
+ ``ABox.assert_role``; the DATA layer -- ``DataSomeValuesFrom(DPE DR)``,
66
+ ``DataAllValuesFrom(DPE DR)``, ``DataHasValue(DPE lt)``,
67
+ ``DataMin/Max/ExactCardinality(n DPE DR?)`` as class expressions (the exact
68
+ form desugared to ``DataAtLeast ⊓ DataAtMost``), the data ranges
69
+ ``DataIntersectionOf``/``DataUnionOf``/``DataComplementOf``/``DataOneOf``/
70
+ ``DatatypeRestriction`` with typed literals (``"400"^^xsd:integer``,
71
+ ``"abc"``, ``"abc"@en``), and ``SubDataPropertyOf``,
72
+ ``EquivalentDataProperties``, ``DisjointDataProperties``,
73
+ ``FunctionalDataProperty``, ``DataPropertyDomain``, ``DataPropertyRange``,
74
+ ``DatatypeDefinition``, ``DataPropertyAssertion`` and
75
+ ``NegativeDataPropertyAssertion`` (see "The data layer" below);
76
+ ``DifferentIndividuals(a1 a2+)`` -> **every**
77
+ pairwise ``ABox.assert_distinct`` (all C(k, 2) pairs, not just a
78
+ consecutive chain — see "DifferentIndividuals" below, this is a
79
+ completeness requirement, not a style choice); ``Declaration(Class(...)
80
+ / ObjectProperty(...) / NamedIndividual(...))`` parsed and discarded
81
+ (declarations carry no ALCHQ-relevant content, since
82
+ :mod:`unicode_logic_kit.dl.concepts`'s ``Atomic``/role/individual names are
83
+ already untyped strings — this is not a silent weakening, just a no-op
84
+ the W3C spec itself defines as non-restrictive); axiom-level and
85
+ standalone ``Annotation(...)``/``AnnotationAssertion(...)`` parsed and
86
+ discarded (annotations are explicitly non-logical in OWL 2, so dropping
87
+ them is not a "weaker than requested" translation of truth-bearing
88
+ content) — an axiom's own leading ``axiomAnnotations`` (e.g.
89
+ ``SubClassOf(Annotation(rdfs:label "x") A B)``) are skipped the same way,
90
+ before its real arguments are parsed.
91
+
92
+ Rejected loudly, one :class:`OwlFunctionalUnsupportedError` per construct,
93
+ naming it by its OWL keyword (mirrors
94
+ :mod:`unicode_logic_kit.dl.owl_manchester`'s messages):
95
+ ``ObjectInverseOf`` (inverse roles — ALCHQ has none; the ONE place it is read is
96
+ either side of a ``SubObjectPropertyOf``, where ``r ⊑ s⁻`` is a genuinely
97
+ different axiom from ``r ⊑ s`` that a ``TBox`` holds, ``rbox_to_fol`` renders and
98
+ the tableau then refuses BY NAME at query time), ``ObjectHasSelf``, ``ObjectOneOf`` (nominals), ``HasKey``, ``DisjointUnion``,
99
+ an OWL 2 BUILT-IN property name in any non-tautological position, anonymous
100
+ individuals (``_:nodeID``), and inside the data layer an out-of-scope facet
101
+ (``xsd:pattern``, the length facets, an ordering facet on a non-numeric base)
102
+ or an ill-typed literal -- each by name, from
103
+ :class:`~unicode_logic_kit.dl.datatypes.UnsupportedDatatypeError`. Also refused, by
104
+ name and with the wording :mod:`unicode_logic_kit.dl.owl_manchester` uses: a
105
+ BUILT-IN datatype name (``xsd:integer``, either namespace spelling) where a CLASS
106
+ is wanted, and ``owl:Thing``/``owl:Nothing`` where a DATA RANGE is wanted --
107
+ OWL 2 does not let one name be both a class and a datatype -- and a literal with
108
+ no first-order term (``xsd:float``/``xsd:double``, a decimal no ``Number`` holds
109
+ exactly), so that an axiom this reader ACCEPTS is one the FOL image can render.
110
+
111
+ CONSUMED without logical content (not refused: nothing is lost) -- reported by
112
+ :func:`parse_owl_functional_axioms` in ``OwlFunctionalResult.consumed``, one
113
+ :class:`ConsumedAxiom` each: ``SubAnnotationPropertyOf``,
114
+ ``AnnotationPropertyDomain`` and ``AnnotationPropertyRange`` (annotation
115
+ properties carry no content under the OWL 2 direct semantics, so these are
116
+ exactly what ``AnnotationAssertion`` already was), and a TAUTOLOGICAL property
117
+ inclusion into ``owl:topObjectProperty``/``owl:topDataProperty`` or out of
118
+ ``owl:bottomObjectProperty``/``owl:bottomDataProperty``.
119
+
120
+ The data layer
121
+ ---------------
122
+ The data half of OWL 2 -- see :mod:`unicode_logic_kit.dl.datatypes` -- is stored
123
+ (``TBox.add_data_property_*``, ``ABox.assert_data``) and translated by the FOL
124
+ image, and REFUSED by the in-house tableau, which has no data domain. A parser
125
+ never refuses an axiom KIND, so all of it is read here. The reading of a
126
+ literal is OWL's own: ``"lexical"^^datatype`` (a full-IRI datatype is
127
+ canonicalised, so ``<http://www.w3.org/2001/XMLSchema#integer>`` and
128
+ ``xsd:integer`` are one datatype), a plain ``"text"`` is an ``xsd:string``, and
129
+ ``"text"@en`` a language-tagged string. A BARE numeral such as ``42`` is not a
130
+ literal in this syntax (it is in Manchester Syntax) and is a syntax error.
131
+
132
+ A whole ontology, with the refusals as data
133
+ --------------------------------------------
134
+ :func:`parse_owl_functional` is STRICT: it raises on the FIRST construct
135
+ outside the fragment, so a real ontology carrying one ``DataPropertyRange``
136
+ yields no TBox at all. :func:`parse_owl_functional_axioms` is the per-axiom
137
+ reader — one tokenization, one pass, recovering at AXIOM boundaries — and
138
+ returns an :class:`OwlFunctionalResult` holding the TBox/ABox it could build
139
+ plus one :class:`RefusedAxiom` per axiom it could not, each naming the
140
+ construct, its offset and its own source text. It recovers from
141
+ :class:`OwlFunctionalUnsupportedError` and from NOTHING else: malformed input
142
+ still raises, because recovering from an unbalanced paren could drop arbitrary
143
+ content. See that function for the contract.
144
+
145
+ IRIs and names
146
+ ---------------
147
+ Exactly :mod:`unicode_logic_kit.dl.owl_manchester`'s convention (both readers store
148
+ one IRI as ONE Python string, the IRI WITHOUT its angle brackets, in every
149
+ position), adapted to two
150
+ concrete-syntax spellings instead of one: a **full IRI** token, ``<...>``, is
151
+ stored as the kit name with the angle brackets stripped (``<http://example.org/Person>``
152
+ -> ``Atomic("http://example.org/Person")``); a **prefixed (or otherwise bare)
153
+ name** token, e.g. ``ex:Person`` or a plain ``Person``, is stored VERBATIM, with
154
+ no prefix resolution at all — this module carries no ``Prefix(...)`` -> IRI
155
+ table, since (like Manchester's ``owl:Thing``/``owl:Nothing`` special case) the
156
+ kit's ``Atomic``/role/individual names are opaque strings, not resolved IRIs.
157
+ Rendering back (:func:`to_owl_functional_class_expression`, :func:`to_owl_functional`)
158
+ picks one of the two spellings DETERMINISTICALLY, by a single rule: a name
159
+ containing ``"://"`` (i.e., shaped like an absolute IRI with a network scheme) is
160
+ wrapped back in ``<...>``; every other name is written bare when the tokenizer
161
+ reads it as ONE plain word, and in ``<...>`` otherwise. The brackets are the
162
+ syntax's own escape and carry ANY name that has no ``>`` in it — whitespace, the
163
+ structural characters ``()=<>"``, the empty name, a name spelled like a keyword
164
+ the reader dispatches on (``ObjectUnionOf``, ``Annotation``, ...) and an
165
+ anonymous-individual label (``_:x``) alike — because the reader takes everything
166
+ up to the first ``>`` for one name. So
167
+ ``parse_owl_functional_class_expression(to_owl_functional_class_expression(c))
168
+ == c`` holds for every concept ``c`` built from any name string that has a spelling
169
+ (see "Round-trip guarantee" below); the RENDERED spelling need not match the
170
+ ORIGINALLY PARSED spelling (a full-IRI ``owl:Thing`` reads as
171
+ :class:`~unicode_logic_kit.dl.concepts.Top` and always renders back as the bare
172
+ ``"owl:Thing"``, for instance), only the resulting
173
+ :class:`~unicode_logic_kit.dl.concepts.Concept`/``TBox``/``ABox`` structure must
174
+ match, which is exactly the oracle the tests check.
175
+
176
+ A name with NO spelling is refused, by name, with a :class:`ValueError`: a name
177
+ holding ``>`` (which ends a full IRI, and a bare ``>`` is no part of a word); a
178
+ name that ALREADY carries its brackets (a hand-built ``"<http://x.org/A>"``),
179
+ since ``<...>`` reads back as the text between the brackets, which is another
180
+ name, and ``<<...>>`` is refused by the reader; and ``owl:Thing`` /
181
+ ``owl:Nothing`` (in either spelling) as a CLASS, which the reader makes the top
182
+ and the bottom class however they are written (as a role or an individual they
183
+ are ordinary names). The same rule, over a different escape, is
184
+ :mod:`unicode_logic_kit.dl.owl_manchester`'s "Names with no spelling".
185
+
186
+ EquivalentClasses and DisjointClasses
187
+ ---------------------------------------
188
+ Neither is a primitive of :class:`~unicode_logic_kit.dl.tableau.TBox` (which only
189
+ has ``add`` (⊑) and ``add_equivalence`` (⊑ both ways) — see its module
190
+ docstring), so both n-ary OWL axioms are DECOMPOSED at parse time:
191
+ ``EquivalentClasses(C1 C2 ... Ck)`` becomes the chain ``add_equivalence(C1, C2)``,
192
+ ``add_equivalence(C2, C3)``, ..., ``add_equivalence(C{k-1}, Ck)`` — sound
193
+ because pairwise-consecutive equivalence already entails the full mutual
194
+ equivalence of every pair by transitivity of ⊑, and it is what
195
+ :func:`to_owl_functional` can deterministically detect and re-fold on the way
196
+ out (see below). ``DisjointClasses(C1 ... Ck)`` becomes one
197
+ ``add(C_i ⊓ C_j, ⊥)`` GCI per UNORDERED pair ``i < j`` (all C(k, 2) of them,
198
+ not a chain — disjointness has no transitive shortcut the way equivalence
199
+ does: knowing C1⊓C2⊑⊥ and C2⊓C3⊑⊥ says nothing about C1⊓C3), which is exactly
200
+ what "``DisjointClasses`` pairwise" means model-theoretically and is always
201
+ expressible in ALCHQ (conjunction and ⊥ are both primitive), hence
202
+ unconditionally reducible — there is no case where this module rejects
203
+ ``DisjointClasses`` outright, unlike the several other axiom kinds it does
204
+ reject by name. :func:`to_owl_functional` does not attempt to re-detect and
205
+ re-fold a set of GCIs back into a single ``DisjointClasses(...)`` axiom (that
206
+ would require solving a covering problem over ``TBox.inclusions`` with no
207
+ guarantee of a unique answer) — it always renders each ``TBox.inclusions``
208
+ entry as its own axiom, using the ``EquivalentClasses`` re-fold ONLY where the
209
+ adjacent-pair shape ``add_equivalence`` itself produces is recognised (see
210
+ :func:`_render_axioms`); every other inclusion, including one that
211
+ happens to look like ``C ⊓ D ⊑ ⊥``, is rendered as a plain ``SubClassOf``.
212
+ This asymmetry (read supports both compact forms, write only re-folds one)
213
+ is deliberate and harmless: :class:`~unicode_logic_kit.dl.tableau.TBox` equality
214
+ (a plain ``@dataclass``) only ever compares the underlying ``inclusions``
215
+ list, never which OWL keyword produced it, so the round-trip oracle
216
+ ``parse_owl_functional(to_owl_functional(tbox, abox)) == (tbox, abox)`` holds
217
+ regardless of which axiom spelling is chosen on the way out.
218
+
219
+ DifferentIndividuals
220
+ ----------------------
221
+ :class:`~unicode_logic_kit.dl.tableau.ABox` has no unique name assumption (see
222
+ "Qualified number restrictions" in :mod:`unicode_logic_kit.dl.tableau`'s module
223
+ docstring), so ``a ≠ b`` is a genuine semantic fact that must be asserted, not
224
+ inferred from two individuals merely having different names. OWL's
225
+ ``DifferentIndividuals(a1 ... ak)`` asserts ALL of them pairwise distinct, so
226
+ reading it as anything less than every one of the C(k, 2) pairs — e.g. only a
227
+ consecutive chain, the way ``EquivalentClasses`` is decomposed above — would
228
+ be a silent WEAKENING of what the axiom actually states (equivalence has a
229
+ free transitive closure through ⊑; distinctness has no such closure: knowing
230
+ ``a ≠ b`` and ``b ≠ c`` says nothing about whether ``a`` and ``c`` might still
231
+ be forced equal), which the kit's honesty convention forbids. So every pair is
232
+ asserted explicitly. :func:`to_owl_functional`, symmetrically, does not try to
233
+ re-cluster ``ABox.distinct_assertions`` back into one k-ary
234
+ ``DifferentIndividuals(...)`` per clique (an unnecessary complication — see
235
+ the DisjointClasses note above for the identical reasoning): it renders each
236
+ ``distinct_assertions`` entry as its own binary ``DifferentIndividuals(a b)``,
237
+ which is itself perfectly legal OWL 2 syntax (the W3C grammar's minimum arity
238
+ is two, not three) and round-trips to the identical single pair.
239
+
240
+ ``SameIndividual(a1 ... ak)`` is the CONTRAST that makes the argument above
241
+ concrete: equality IS transitive, so the consecutive chain ``a1 = a2``,
242
+ ``a2 = a3``, … already entails every pair, and reading it that way is not a
243
+ weakening. Its renderer is the same one-binary-line-per-pair shape, for the
244
+ same reason.
245
+
246
+ Qualified number restrictions
247
+ ------------------------------
248
+ Identical semantics to :mod:`unicode_logic_kit.dl.owl_manchester`'s ``min``/
249
+ ``max``/``exactly``: ``ObjectMinCardinality``/``ObjectMaxCardinality`` parse
250
+ into :class:`~unicode_logic_kit.dl.concepts.AtLeast`/:class:`~unicode_logic_kit.dl.concepts.AtMost`,
251
+ and ``ObjectExactCardinality(n OPE CE?)`` desugars AT PARSE TIME into
252
+ ``AtLeast(n, role, CE) ⊓ AtMost(n, role, CE)`` (there is no separate "exactly"
253
+ AST node — see :mod:`unicode_logic_kit.dl.concepts`). Unlike
254
+ :func:`~unicode_logic_kit.dl.owl_manchester.to_manchester` (which omits the
255
+ qualifying-class text when it is ⊤, for terseness),
256
+ :func:`to_owl_functional_class_expression` always renders the explicit 3-arg
257
+ qualified form — S-expression syntax has no terseness pressure the way
258
+ Manchester's inline keyword notation does, and always emitting one shape
259
+ keeps the renderer simpler with no loss of round-trip fidelity (the 2-arg
260
+ unqualified form is still accepted on READ, defaulting to ``owl:Thing``,
261
+ exactly like Manchester's bare ``role min n``). See ``dl.tableau``'s
262
+ "Qualified number restrictions" section for the reasoning side, in particular
263
+ the SIMPLE ROLES restriction (a number restriction on a transitive role, or
264
+ one with a transitive sub-role, is rejected there — by
265
+ :class:`~unicode_logic_kit.dl.tableau.NonSimpleRoleError` — not by this module,
266
+ which only builds the AST).
267
+
268
+ No ambiguity, no precedence
269
+ -----------------------------
270
+ Unlike :mod:`unicode_logic_kit.dl.owl_manchester` (whose ``_PREC`` lattice and
271
+ ``_paren`` helper resolve a genuinely ambiguous infix grammar), OWL 2
272
+ Functional-Style Syntax has NO precedence to resolve: every compound
273
+ expression is ``Keyword(arg arg ...)``, fully parenthesised by the keyword
274
+ itself. :func:`_render_ce` therefore needs no precedence table and never
275
+ omits or inserts a parenthesis conditionally — the keyword IS the
276
+ parenthesisation.
277
+
278
+ Round-trip guarantee
279
+ ---------------------
280
+ ``parse_owl_functional_class_expression(to_owl_functional_class_expression(c))
281
+ == c`` for every ALCHQ concept ``c``, and
282
+ ``parse_owl_functional(to_owl_functional(tbox, abox)) == (tbox, abox)`` for
283
+ every ALCHQ ``(tbox, abox)`` pair — see ``tests/test_owl_functional.py`` for
284
+ the hand-checked cases, including one per constructor, nested expressions, and
285
+ a full document with every supported axiom kind.
286
+
287
+ The one exception is a knowledge base the READER refuses on principle: a
288
+ hand-built ``Atomic`` named like a built-in datatype (``xsd:integer`` is a
289
+ datatype, never a class), a datatype named ``owl:Thing``/``owl:Nothing``, or a
290
+ literal with no first-order term (``xsd:float``/``xsd:double``, a decimal no
291
+ ``Number`` holds exactly). :func:`to_owl_functional` writes such a knowledge base
292
+ as it was given, and :func:`parse_owl_functional` refuses the text by name on the
293
+ way back in — loudly, never as a different knowledge base.
294
+ """
295
+
296
+ import functools
297
+ from dataclasses import dataclass
298
+ from dataclasses import fields as dataclass_fields
299
+ from typing import Dict, List, Optional, Sequence, Set, Tuple, Union
300
+
301
+ from .concepts import (
302
+ Concept, Top, Bottom, Atomic, Not, And, Or, Exists, ForAll, AtLeast, AtMost,
303
+ InverseRole, Nominal, HasValue, DataExists, DataForAll, DataHasValue,
304
+ DataAtLeast, DataAtMost, DATA_CONCEPTS,
305
+ )
306
+ from .datatypes import (
307
+ DataRange, Datatype, DatatypeRestriction, DataOneOf, DataComplementOf,
308
+ DataIntersectionOf, DataUnionOf, Literal, UnsupportedDatatypeError,
309
+ is_builtin_datatype, render_datarange_fs, render_literal_fs,
310
+ )
311
+ from .tableau import (
312
+ TBox, ABox, RESERVED_TOP_ROLES, is_tautological_role_inclusion,
313
+ reserved_role, _reject_abox_roles, _reject_concept_roles_deep,
314
+ _tbox_class_expressions, _validate_data_box, _validate_role_box,
315
+ )
316
+ from .translate import _collect_vocabulary
317
+
318
+ __all__ = [
319
+ "parse_owl_functional", "parse_owl_functional_axioms", "to_owl_functional",
320
+ "parse_owl_functional_class_expression", "to_owl_functional_class_expression",
321
+ "OwlFunctionalResult", "RefusedAxiom", "ConsumedAxiom",
322
+ "OwlFunctionalSyntaxError", "OwlFunctionalUnsupportedError",
323
+ ]
324
+
325
+
326
+ class OwlFunctionalSyntaxError(ValueError):
327
+ """Raised by this module's parser on malformed input *and* on syntax that is
328
+ valid OWL 2 Functional-Style Syntax but falls outside ALCHQ (see the module
329
+ docstring's "Rejected"). Always a :class:`ValueError` subclass with a
330
+ message naming the specific offending construct and its position in the
331
+ input, per the kit's honesty convention.
332
+
333
+ :class:`OwlFunctionalUnsupportedError` is the SUBCLASS raised for the
334
+ second case alone — "valid OWL 2, outside this fragment" — so a caller can
335
+ tell it from genuinely malformed input. Catching this class still catches
336
+ both, which is why adding the subclass broke no existing call site.
337
+ """
338
+
339
+
340
+ class OwlFunctionalUnsupportedError(OwlFunctionalSyntaxError):
341
+ """Raised for input that is valid OWL 2 Functional-Style Syntax but falls
342
+ outside ALCHQ — as opposed to input that is malformed.
343
+
344
+ A subclass of :class:`OwlFunctionalSyntaxError`, deliberately: every
345
+ existing ``except OwlFunctionalSyntaxError`` and every ``pytest.raises`` in
346
+ the suite keeps working unchanged.
347
+
348
+ The distinction is what makes :func:`parse_owl_functional_axioms` possible
349
+ and SAFE. That reader recovers from this class and from nothing else: a
350
+ construct outside the fragment is a question about the FRAGMENT, and
351
+ skipping it loses exactly the axiom it names, which the result reports. A
352
+ tokenizer error, an unbalanced paren, a missing ``Ontology(`` or a bad name
353
+ is a question about the DOCUMENT, and recovering from one of those could
354
+ silently drop arbitrary content — so those still raise.
355
+
356
+ Attributes:
357
+ keyword: The offending construct's own OWL keyword (or name), e.g.
358
+ ``"ObjectHasSelf"``. For a construct nested inside an axiom this is
359
+ the INNER keyword, not the axiom's.
360
+ position: Its 0-based character offset in the parsed text, or ``-1``
361
+ when the raising site has no token to point at.
362
+ """
363
+
364
+ def __init__(self, message: str, keyword: str = "", position: int = -1):
365
+ super().__init__(message)
366
+ self.keyword = keyword
367
+ self.position = position
368
+
369
+
370
+ # --------------------------------------------------------------------------- #
371
+ # Tokenizer: '(', ')', '=', '<full IRI>', '"string literal"', and WORD (a
372
+ # maximal run of everything else -- prefixed names, plain names, and every
373
+ # axiom/class-expression keyword alike; keywords are matched by exact string
374
+ # comparison in the parser, not pre-classified here, since the grammar is a
375
+ # flat prefix S-expression with no infix operators to disambiguate).
376
+ # --------------------------------------------------------------------------- #
377
+
378
+ _STRUCT_CHARS = set("()=<>\"")
379
+
380
+ # A Token is (type: str, value: str, pos: int).
381
+ _Token = Tuple[str, str, int]
382
+
383
+
384
+ def _tokenize(text: str) -> List[_Token]:
385
+ """Split ``text`` into structural/IRI/string/word tokens plus a trailing
386
+ EOF sentinel (whose ``pos`` is ``len(text)``, for error messages).
387
+ """
388
+ tokens: List[_Token] = []
389
+ i, n = 0, len(text)
390
+ while i < n:
391
+ ch = text[i]
392
+ if ch.isspace():
393
+ i += 1
394
+ continue
395
+ if ch == "(":
396
+ tokens.append(("LPAREN", ch, i))
397
+ i += 1
398
+ continue
399
+ if ch == ")":
400
+ tokens.append(("RPAREN", ch, i))
401
+ i += 1
402
+ continue
403
+ if ch == "=":
404
+ tokens.append(("EQUALS", ch, i))
405
+ i += 1
406
+ continue
407
+ if ch == "<":
408
+ start = i
409
+ end = text.find(">", i + 1)
410
+ if end == -1:
411
+ raise OwlFunctionalSyntaxError(
412
+ f"parse_owl_functional: unterminated IRI ('<...>' never "
413
+ f"closed) starting at position {start} in {text!r}")
414
+ tokens.append(("IRI", text[i + 1:end], start))
415
+ i = end + 1
416
+ continue
417
+ if ch == '"':
418
+ start = i
419
+ j = i + 1
420
+ buf = []
421
+ while j < n and text[j] != '"':
422
+ if text[j] == "\\" and j + 1 < n:
423
+ buf.append(text[j:j + 2])
424
+ j += 2
425
+ else:
426
+ buf.append(text[j])
427
+ j += 1
428
+ if j >= n:
429
+ raise OwlFunctionalSyntaxError(
430
+ f"parse_owl_functional: unterminated string literal "
431
+ f"starting at position {start} in {text!r}")
432
+ tokens.append(("STRING", "".join(buf), start))
433
+ i = j + 1
434
+ continue
435
+ if ch == ">":
436
+ raise OwlFunctionalSyntaxError(
437
+ f"parse_owl_functional: unexpected '>' at position {i} in {text!r}")
438
+ start = i
439
+ while i < n and not text[i].isspace() and text[i] not in _STRUCT_CHARS:
440
+ i += 1
441
+ tokens.append(("WORD", text[start:i], start))
442
+ tokens.append(("EOF", "", n))
443
+ return tokens
444
+
445
+
446
+ # --------------------------------------------------------------------------- #
447
+ # Recursive-descent parser.
448
+ # --------------------------------------------------------------------------- #
449
+
450
+ _TOP_SPELLINGS = {"owl:Thing", "http://www.w3.org/2002/07/owl#Thing"}
451
+ _BOTTOM_SPELLINGS = {"owl:Nothing", "http://www.w3.org/2002/07/owl#Nothing"}
452
+
453
+ # Class-expression keywords rejected outright (outside ALCHQ), keyed by their
454
+ # OWL name -> a human-readable phrase naming the construct for the error message.
455
+ _REJECTED_CE = {
456
+ "ObjectHasSelf": "Self restrictions (ObjectHasSelf)",
457
+ "ObjectOneOf": "nominal concepts (ObjectOneOf)",
458
+ "ObjectInverseOf": "inverse object properties (ObjectInverseOf)",
459
+ }
460
+
461
+ # The seven OWL 2 object-property CHARACTERISTIC axioms, each a one-role
462
+ # keyword, mapped to the TBox builder that stores it. One table rather than
463
+ # seven parse branches: they differ only in the method name, and the tableau's
464
+ # own decision about each (decide / internalise / refuse by name) is recorded
465
+ # in dl.tableau._AXIOM_KINDS, not here -- a PARSER never refuses a KIND.
466
+ _CHARACTERISTIC_AXIOMS = {
467
+ "TransitiveObjectProperty": "add_transitive_role",
468
+ "SymmetricObjectProperty": "add_symmetric_role",
469
+ "AsymmetricObjectProperty": "add_asymmetric_role",
470
+ "ReflexiveObjectProperty": "add_reflexive_role",
471
+ "IrreflexiveObjectProperty": "add_irreflexive_role",
472
+ "FunctionalObjectProperty": "add_functional_role",
473
+ "InverseFunctionalObjectProperty": "add_inverse_functional_role",
474
+ }
475
+
476
+ # Axiom keywords rejected outright (outside ALCHQ), same convention.
477
+ _REJECTED_AXIOMS = {
478
+ "HasKey": "keys (HasKey)",
479
+ "DisjointUnion": "disjoint union class axioms (DisjointUnion)",
480
+ }
481
+
482
+ # The annotation-property axioms. Annotation properties carry no content under
483
+ # the OWL 2 direct semantics (annotations are explicitly non-logical), so these
484
+ # are exactly what AnnotationAssertion already was: read, consumed, and --
485
+ # unlike a Declaration -- REPORTED, as a ConsumedAxiom, so they do not vanish.
486
+ _ANNOTATION_PROPERTY_AXIOMS = frozenset({
487
+ "SubAnnotationPropertyOf", "AnnotationPropertyDomain", "AnnotationPropertyRange",
488
+ })
489
+
490
+
491
+ #: The keywords that start a data range: a word like these in a position that
492
+ #: wants a datatype NAME is a mistake worth naming, not a datatype called that.
493
+ _DATA_RANGE_KEYWORDS = frozenset({
494
+ "DataIntersectionOf", "DataUnionOf", "DataComplementOf", "DataOneOf",
495
+ "DatatypeRestriction",
496
+ })
497
+
498
+ #: Every bare word the reader takes for a KEYWORD where a name could stand: the
499
+ #: class-expression keywords, the data-range keywords, ``ObjectInverseOf`` (an
500
+ #: object property expression) and ``Annotation`` (skipped in front of every
501
+ #: axiom's first argument). A name spelled like one of them is written in angle
502
+ #: brackets, which the reader takes for a name whatever it says.
503
+ _NAME_KEYWORDS = frozenset({
504
+ "ObjectIntersectionOf", "ObjectUnionOf", "ObjectComplementOf",
505
+ "ObjectSomeValuesFrom", "ObjectAllValuesFrom", "ObjectHasValue",
506
+ "ObjectMinCardinality", "ObjectMaxCardinality", "ObjectExactCardinality",
507
+ "DataSomeValuesFrom", "DataAllValuesFrom", "DataHasValue",
508
+ "DataMinCardinality", "DataMaxCardinality", "DataExactCardinality",
509
+ "Annotation",
510
+ }) | frozenset(_REJECTED_CE) | _DATA_RANGE_KEYWORDS
511
+
512
+
513
+ def _unescape_string(raw: str) -> str:
514
+ """The text of a string-literal token with its two OWL 2 escapes undone
515
+ (``\\"`` and ``\\\\``). The tokenizer keeps the escapes in the token's value so
516
+ that the token's SOURCE length is recoverable."""
517
+ out: List[str] = []
518
+ i = 0
519
+ while i < len(raw):
520
+ if raw[i] == "\\" and i + 1 < len(raw) and raw[i + 1] in ('"', "\\"):
521
+ out.append(raw[i + 1])
522
+ i += 2
523
+ else:
524
+ out.append(raw[i])
525
+ i += 1
526
+ return "".join(out)
527
+
528
+
529
+ def _is_tautology_of(sub: str, sup: str, family: str) -> bool:
530
+ """:func:`~unicode_logic_kit.dl.tableau.is_tautological_role_inclusion`,
531
+ restricted to built-ins of ONE family (``"Object"`` or ``"Data"``).
532
+
533
+ ``SubDataPropertyOf(P owl:topObjectProperty)`` mixes the families: it is
534
+ ill-typed OWL 2, not a tautology, so it must be refused and not consumed.
535
+ """
536
+ if not is_tautological_role_inclusion(sub, sup):
537
+ return False
538
+ for name in (sub, sup):
539
+ builtin = reserved_role(name)
540
+ if builtin is not None and family not in builtin:
541
+ return False
542
+ return True
543
+
544
+
545
+ def _owl_role_text(role) -> str:
546
+ """``role`` as OWL's functional syntax spells it: the name, or
547
+ ``ObjectInverseOf(name)`` for an :class:`InverseRole`."""
548
+ return f"ObjectInverseOf({role.role})" if isinstance(role, InverseRole) else role
549
+
550
+
551
+ def _tautology_reason(axiom: str, sub, sup) -> str:
552
+ builtin = reserved_role(sup) if reserved_role(sup) in RESERVED_TOP_ROLES else reserved_role(sub)
553
+ sub, sup = _owl_role_text(sub), _owl_role_text(sup)
554
+ return (f"{axiom}({sub} {sup}) is a tautology -- it holds in every "
555
+ f"interpretation because of the built-in property {builtin} -- so "
556
+ f"it constrains nothing (consumed as a documented no-op, and "
557
+ f"reported)")
558
+
559
+
560
+ class _Parser:
561
+ """A single parse of one tokenized input; not re-used across calls."""
562
+
563
+ def __init__(self, tokens: List[_Token], text: str):
564
+ self._tokens = tokens
565
+ self._text = text
566
+ self._i = 0
567
+ # The ``DatatypeDefinition`` entries of the axioms ALREADY read, when
568
+ # each axiom is parsed into a scratch ``TBox`` of its own (the lenient
569
+ # reader): a definition is checked against these as well as against the
570
+ # scratch ``TBox``, which holds nothing earlier. Empty for the strict
571
+ # reader, whose one ``TBox`` already holds everything read so far.
572
+ self._earlier_definitions: Sequence[Tuple[str, DataRange]] = ()
573
+
574
+ def _peek(self) -> _Token:
575
+ return self._tokens[self._i]
576
+
577
+ def _advance(self) -> _Token:
578
+ tok = self._tokens[self._i]
579
+ self._i += 1
580
+ return tok
581
+
582
+ def _error(self, message: str) -> OwlFunctionalSyntaxError:
583
+ return OwlFunctionalSyntaxError(f"parse_owl_functional: {message} in {self._text!r}")
584
+
585
+ def _expect(self, ttype: str, what: str) -> _Token:
586
+ tok = self._peek()
587
+ if tok[0] != ttype:
588
+ found = "end of input" if tok[0] == "EOF" else f"{tok[1]!r}"
589
+ raise self._error(f"expected {what} but found {found} at position {tok[2]}")
590
+ return self._advance()
591
+
592
+ def _expect_word(self, word: str) -> _Token:
593
+ tok = self._peek()
594
+ if tok[0] != "WORD" or tok[1] != word:
595
+ found = "end of input" if tok[0] == "EOF" else f"{tok[1]!r}"
596
+ raise self._error(f"expected {word!r} but found {found} at position {tok[2]}")
597
+ return self._advance()
598
+
599
+ def _expect_eof(self) -> None:
600
+ tok = self._peek()
601
+ if tok[0] != "EOF":
602
+ raise self._error(f"unexpected trailing input {tok[1]!r} at position {tok[2]}")
603
+
604
+ def _unsupported(self, message: str, keyword: str,
605
+ position: int) -> OwlFunctionalUnsupportedError:
606
+ """The "valid OWL 2, outside this fragment" error, carrying the
607
+ offending construct and its offset as DATA — see
608
+ :class:`OwlFunctionalUnsupportedError` and
609
+ :func:`parse_owl_functional_axioms`.
610
+ """
611
+ return OwlFunctionalUnsupportedError(
612
+ f"parse_owl_functional: {message} in {self._text!r}",
613
+ keyword=keyword, position=position)
614
+
615
+ def _reject(self, what: str, tok: _Token) -> None:
616
+ raise self._unsupported(
617
+ f"{what} is not supported — outside ALCHQ (this kit's DL fragment), "
618
+ f"found {tok[1]!r} at position {tok[2]}", tok[1], tok[2])
619
+
620
+ # -- names: full IRIs ('<...>') and prefixed/plain names, taken verbatim -- #
621
+
622
+ def _parse_name(self) -> str:
623
+ tok = self._peek()
624
+ if tok[0] == "IRI":
625
+ self._advance()
626
+ return tok[1]
627
+ if tok[0] == "WORD":
628
+ if tok[1].startswith("_:"):
629
+ self._reject("anonymous individuals ('_:nodeID')", tok)
630
+ self._advance()
631
+ return tok[1]
632
+ found = "end of input" if tok[0] == "EOF" else f"{tok[1]!r}"
633
+ raise self._error(
634
+ f"expected a name (a full IRI '<...>' or a prefixed/plain name) "
635
+ f"but found {found} at position {tok[2]}")
636
+
637
+ def _parse_inclusion_role(self) -> Union[str, InverseRole]:
638
+ """One side of a ``SubObjectPropertyOf``: a role name, or
639
+ ``ObjectInverseOf(name)`` as an :class:`InverseRole`.
640
+
641
+ The one position this grammar reads an inverse role in. ``TBox``
642
+ accepts one on either side of a role inclusion (``r ⊑ s⁻`` is a
643
+ different axiom from ``r ⊑ s``) and the writers print it —
644
+ ``dl.to_owl_functional``, the Hets renderer — so a reader that refused
645
+ what the writer wrote would break the round trip. Everywhere else it is
646
+ still refused by name (a characteristic of ``r⁻`` is not, in general,
647
+ the characteristic of ``r``; see ``dl.tableau``'s ``_inverse_advice``).
648
+ The name INSIDE an inverse is an ordinary role name: a built-in is
649
+ refused there, and so is a nested ``ObjectInverseOf``.
650
+ """
651
+ tok = self._peek()
652
+ if tok[0] == "WORD" and tok[1] == "ObjectInverseOf":
653
+ self._advance()
654
+ self._expect("LPAREN", "'('")
655
+ inner = self._parse_object_property_expression()
656
+ self._expect("RPAREN", "')'")
657
+ return InverseRole(inner)
658
+ return self._parse_object_property_expression(allow_reserved=True)
659
+
660
+ def _parse_object_property_expression(self, *,
661
+ allow_reserved: bool = False) -> str:
662
+ """One ``ObjectPropertyExpression``: a plain role name.
663
+
664
+ An OWL 2 BUILT-IN property name is refused here, so EVERY role position
665
+ in the grammar — a restriction's role, an ``ObjectPropertyAssertion``,
666
+ a characteristic's subject — is covered by this one guard rather than
667
+ by each branch remembering to ask. ``allow_reserved=True`` is passed by
668
+ the ``SubObjectPropertyOf`` branch alone, which must read both sides
669
+ before it can tell whether the inclusion is the TAUTOLOGY it consumes
670
+ as a no-op.
671
+ """
672
+ tok = self._peek()
673
+ if tok[0] == "WORD" and tok[1] == "ObjectInverseOf":
674
+ self._reject(_REJECTED_CE["ObjectInverseOf"], tok)
675
+ name = self._parse_name()
676
+ if not allow_reserved:
677
+ self._check_role_name(name, "a role position", tok[2])
678
+ return name
679
+
680
+ def _check_role_name(self, role: str, axiom: str, position: int = -1) -> None:
681
+ """Refuse an OWL 2 BUILT-IN property name by name.
682
+
683
+ ALCHQ has neither the universal nor the empty role, so a built-in
684
+ cannot be carried as an ordinary role (which is what this parser did
685
+ until 0.30.0: 31 of the OEO axioms translated with a bogus
686
+ uninterpreted ``TopObjectProperty`` predicate). The ONE exception is a
687
+ TAUTOLOGICAL inclusion, consumed as a no-op by the
688
+ ``SubObjectPropertyOf`` branch before this is reached — see
689
+ :func:`~unicode_logic_kit.dl.tableau.is_tautological_role_inclusion` and
690
+ "The OWL 2 built-in roles" in ``dl.tableau``'s module docstring.
691
+
692
+ Raises :class:`OwlFunctionalUnsupportedError`, not the plain error: a
693
+ built-in property name is valid OWL 2 that this fragment has no reading
694
+ for, which is exactly the class
695
+ :func:`parse_owl_functional_axioms` recovers from. Without that, one
696
+ such axiom would abort a whole-document read.
697
+ """
698
+ builtin = reserved_role(role)
699
+ if builtin is None:
700
+ return
701
+ builtin_is_data = "Data" in builtin
702
+ axiom_is_data = "data" in axiom.lower()
703
+ if builtin_is_data != axiom_is_data:
704
+ # A DATA built-in in an OBJECT position (or the other way round):
705
+ # ill-typed OWL 2, not the tautology the same inclusion would be
706
+ # between two properties of ONE family -- so it is refused by name
707
+ # and not consumed.
708
+ what = ("the universal property (it relates every pair)"
709
+ if builtin in RESERVED_TOP_ROLES else
710
+ "the empty property (it relates no pair at all)")
711
+ remedy = (f"It is also ILL-TYPED OWL 2: {builtin} is "
712
+ f"{'a data' if builtin_is_data else 'an object'} property "
713
+ f"and {axiom} takes "
714
+ f"{'a data' if axiom_is_data else 'an object'} property, "
715
+ f"so this is refused rather than consumed as the "
716
+ f"tautology the same inclusion between two "
717
+ f"{'data' if axiom_is_data else 'object'} properties "
718
+ f"would be")
719
+ elif builtin in RESERVED_TOP_ROLES:
720
+ what = "the universal property (it relates every pair)"
721
+ remedy = ("An inclusion INTO it is a tautology and IS accepted, as "
722
+ "a no-op; 'P is universal' is the universal role of "
723
+ "SROIQ, which breaks the tree-model property and this "
724
+ "kit does not have it")
725
+ else:
726
+ what = "the empty property (it relates no pair at all)"
727
+ remedy = ("An inclusion OUT OF it is a tautology and IS accepted, "
728
+ "as a no-op; 'P is empty' is the concept inclusion "
729
+ "SubClassOf(owl:Thing ObjectAllValuesFrom(P owl:Nothing))")
730
+ raise self._unsupported(
731
+ f"the OWL 2 BUILT-IN property {role!r} ({builtin} — {what}) in "
732
+ f"{axiom} is not supported — outside ALCHQ (this kit's DL "
733
+ f"fragment), which has neither the universal nor the empty role. "
734
+ f"{remedy}", role, position)
735
+
736
+ # -- the data layer: data properties, literals, data ranges -------------- #
737
+
738
+ def _parse_data_property_expression(self, *,
739
+ allow_reserved: bool = False) -> str:
740
+ """One ``DataPropertyExpression``: a plain data property name.
741
+
742
+ A data property has no inverse and no expression syntax, so this is a
743
+ name; the OWL 2 BUILT-IN property names are refused in every position
744
+ but the tautological ``SubDataPropertyOf`` one (``allow_reserved``), by
745
+ the same guard the object side uses.
746
+ """
747
+ tok = self._peek()
748
+ name = self._parse_name()
749
+ if not allow_reserved:
750
+ self._check_role_name(name, "a data property position", tok[2])
751
+ return name
752
+
753
+ def _parse_data_property_name(self, axiom: str) -> str:
754
+ """One data property that must be an ordinary name, ``axiom`` naming the
755
+ construct in the refusal message."""
756
+ position = self._peek()[2]
757
+ prop = self._parse_data_property_expression(allow_reserved=True)
758
+ self._check_role_name(prop, axiom, position)
759
+ return prop
760
+
761
+ def _parse_datatype_name(self) -> str:
762
+ tok = self._peek()
763
+ if tok[0] == "WORD" and tok[1] in _DATA_RANGE_KEYWORDS:
764
+ raise self._error(
765
+ f"expected a datatype name but found the data range "
766
+ f"{tok[1]!r} at position {tok[2]}")
767
+ return self._check_not_a_class_name(self._parse_name(), tok[2])
768
+
769
+ def _datatype_construct(self, build, keyword: str, position: int):
770
+ """``build()``, with a data-layer refusal turned into the RECOVERABLE
771
+ :class:`OwlFunctionalUnsupportedError` carrying the offending construct
772
+ as data (an out-of-scope facet is valid OWL 2 this fragment cannot
773
+ translate, not malformed input)."""
774
+ try:
775
+ return build()
776
+ except UnsupportedDatatypeError as exc:
777
+ raise self._unsupported(str(exc), keyword, position) from exc
778
+
779
+ def _add_datatype_definition(self, tbox: TBox, name: str,
780
+ datarange: DataRange) -> None:
781
+ """Store ``DatatypeDefinition(name datarange)`` in ``tbox``, after the
782
+ three rules of :meth:`~unicode_logic_kit.dl.tableau.TBox.add_datatype_definition`
783
+ (not a built-in name, one definition per name, no cycle) have been run
784
+ against the definitions read EARLIER in the document as well.
785
+
786
+ ``tbox`` is the scratch ``TBox`` of one axiom when the lenient reader
787
+ drives this, so it knows no earlier definition: a definition that closes
788
+ a cycle with one, or gives a name a second and different definition,
789
+ would be stored and refused only when the knowledge base is rendered.
790
+ The probe below holds the earlier ones, so the refusal is the one the
791
+ strict reader raises, at the axiom that causes it.
792
+ """
793
+ if self._earlier_definitions:
794
+ probe = TBox(datatype_definitions=list(self._earlier_definitions))
795
+ probe.add_datatype_definition(name, datarange)
796
+ tbox.add_datatype_definition(name, datarange)
797
+
798
+ def _parse_literal(self) -> Literal:
799
+ """``"lexical"``, ``"lexical"^^datatype`` or ``"lexical"@lang``.
800
+
801
+ The ``^^`` and ``@`` suffixes must be ADJACENT to the closing quote, as
802
+ in the W3C grammar's one token; a datatype may be a prefixed name or a
803
+ full ``<IRI>``. A plain string is an ``xsd:string``.
804
+ """
805
+ tok = self._peek()
806
+ if tok[0] != "STRING":
807
+ found = "end of input" if tok[0] == "EOF" else f"{tok[1]!r}"
808
+ raise self._error(
809
+ f"expected a literal (\"lexical\"^^datatype) but found {found} "
810
+ f"at position {tok[2]}")
811
+ self._advance()
812
+ lexical = _unescape_string(tok[1])
813
+ end = tok[2] + len(tok[1]) + 2 # past the closing quote
814
+ datatype, language = "xsd:string", None
815
+ nxt = self._peek()
816
+ if nxt[0] == "WORD" and nxt[2] == end and nxt[1].startswith("^^"):
817
+ self._advance()
818
+ rest = nxt[1][2:]
819
+ if rest:
820
+ datatype = rest
821
+ else:
822
+ iri = self._peek()
823
+ if iri[0] != "IRI" or iri[2] != nxt[2] + 2:
824
+ raise self._error(
825
+ f"expected a datatype after '^^' at position {nxt[2]}")
826
+ self._advance()
827
+ datatype = iri[1]
828
+ elif nxt[0] == "WORD" and nxt[2] == end and nxt[1].startswith("@"):
829
+ self._advance()
830
+ language = nxt[1][1:]
831
+ if not language:
832
+ raise self._error(f"empty language tag at position {nxt[2]}")
833
+ if language is None:
834
+ # owl:Thing / owl:Nothing are CLASSES: as the datatype of a literal
835
+ # they are refused exactly as they are in a data range, in either
836
+ # spelling of the name (the same check, so one policy)
837
+ self._check_not_a_class_name(datatype, tok[2])
838
+ literal = self._datatype_construct(
839
+ lambda: Literal(lexical, datatype, language), "IllTypedLiteral", tok[2])
840
+ # A literal the first-order image cannot hold (xsd:double/xsd:float, a
841
+ # decimal no Number holds exactly) is refused HERE, by its datatype, so
842
+ # that an axiom reported as ACCEPTED is one result.to_kb() can
843
+ # translate -- not one that reads fine and then fails at the image.
844
+ self._datatype_construct(literal.to_term, literal.datatype, tok[2])
845
+ return literal
846
+
847
+ def _parse_data_range(self) -> DataRange:
848
+ """One ``DataRange``: a datatype name, ``DataIntersectionOf(DR DR+)``,
849
+ ``DataUnionOf(DR DR+)``, ``DataComplementOf(DR)``, ``DataOneOf(lt+)`` or
850
+ ``DatatypeRestriction(Datatype (facet lt)+)``."""
851
+ tok = self._peek()
852
+ if tok[0] == "IRI":
853
+ self._advance()
854
+ return Datatype(self._check_not_a_class_name(tok[1], tok[2]))
855
+ if tok[0] != "WORD":
856
+ found = "end of input" if tok[0] == "EOF" else f"{tok[1]!r}"
857
+ raise self._error(
858
+ f"expected a data range but found {found} at position {tok[2]}")
859
+ word = tok[1]
860
+ if word in ("DataIntersectionOf", "DataUnionOf"):
861
+ self._advance()
862
+ self._expect("LPAREN", "'('")
863
+ ranges: List[DataRange] = []
864
+ while self._peek()[0] != "RPAREN":
865
+ ranges.append(self._parse_data_range())
866
+ self._expect("RPAREN", "')'")
867
+ if len(ranges) < 2:
868
+ raise self._error(f"{word} expects at least 2 data ranges, "
869
+ f"found {len(ranges)}")
870
+ return (DataIntersectionOf if word == "DataIntersectionOf"
871
+ else DataUnionOf)(tuple(ranges))
872
+ if word == "DataComplementOf":
873
+ self._advance()
874
+ self._expect("LPAREN", "'('")
875
+ inner = self._parse_data_range()
876
+ self._expect("RPAREN", "')'")
877
+ return DataComplementOf(inner)
878
+ if word == "DataOneOf":
879
+ self._advance()
880
+ self._expect("LPAREN", "'('")
881
+ values: List[Literal] = []
882
+ while self._peek()[0] != "RPAREN":
883
+ values.append(self._parse_literal())
884
+ self._expect("RPAREN", "')'")
885
+ if not values:
886
+ raise self._error("DataOneOf expects at least 1 literal, found 0")
887
+ return DataOneOf(tuple(values))
888
+ if word == "DatatypeRestriction":
889
+ self._advance()
890
+ self._expect("LPAREN", "'('")
891
+ base = self._parse_datatype_name()
892
+ facets: List[Tuple[str, Literal]] = []
893
+ while self._peek()[0] != "RPAREN":
894
+ facet = self._parse_name()
895
+ facets.append((facet, self._parse_literal()))
896
+ self._expect("RPAREN", "')'")
897
+ if not facets:
898
+ raise self._error("DatatypeRestriction expects at least one "
899
+ "(facet literal) pair, found none")
900
+ first_facet = facets[0][0]
901
+ return self._datatype_construct(
902
+ lambda: DatatypeRestriction(Datatype(base), tuple(facets)),
903
+ first_facet, tok[2])
904
+ self._advance()
905
+ if self._peek()[0] == "LPAREN":
906
+ raise self._unsupported(
907
+ f"unsupported or unknown OWL 2 data range {word!r}", word, tok[2])
908
+ return Datatype(self._check_not_a_class_name(word, tok[2]))
909
+
910
+ def _parse_role_name(self, axiom: str) -> str:
911
+ """One object property expression that must be an ordinary role name,
912
+ with ``axiom`` naming the construct in the refusal message."""
913
+ position = self._peek()[2]
914
+ role = self._parse_object_property_expression(allow_reserved=True)
915
+ self._check_role_name(role, axiom, position)
916
+ return role
917
+
918
+ def _parse_role_list(self, axiom: str, minimum: int = 2) -> List[str]:
919
+ """Parse ``OPE+ ')'`` (the ``'('`` already consumed) as ordinary role
920
+ names, consume the ``')'``, and require at least ``minimum`` of them.
921
+ """
922
+ roles: List[str] = []
923
+ while self._peek()[0] != "RPAREN":
924
+ roles.append(self._parse_role_name(axiom))
925
+ self._expect("RPAREN", "')'")
926
+ if len(roles) < minimum:
927
+ raise self._error(
928
+ f"{axiom} expects at least {minimum} object properties, "
929
+ f"found {len(roles)}")
930
+ return roles
931
+
932
+ def _parse_data_property_list(self, axiom: str, minimum: int = 2) -> List[str]:
933
+ """Parse ``DPE+ ')'`` (the ``'('`` already consumed) as ordinary data
934
+ property names, consume the ``')'``, and require at least ``minimum``."""
935
+ props: List[str] = []
936
+ while self._peek()[0] != "RPAREN":
937
+ props.append(self._parse_data_property_name(axiom))
938
+ self._expect("RPAREN", "')'")
939
+ if len(props) < minimum:
940
+ raise self._error(
941
+ f"{axiom} expects at least {minimum} data properties, "
942
+ f"found {len(props)}")
943
+ return props
944
+
945
+ def _parse_individual_list(self, axiom: str, minimum: int = 2) -> List[str]:
946
+ """Parse ``Individual+ ')'`` (the ``'('`` already consumed), consume the
947
+ ``')'``, and require at least ``minimum`` of them.
948
+
949
+ One function for ``SameIndividual`` and ``DifferentIndividuals``: the
950
+ two differ only in how the list is EXPANDED (a consecutive chain versus
951
+ all pairs — see each branch), never in how it is read, and the arity
952
+ message should read the same for both.
953
+ """
954
+ individuals: List[str] = []
955
+ while self._peek()[0] != "RPAREN":
956
+ individuals.append(self._parse_name())
957
+ self._expect("RPAREN", "')'")
958
+ if len(individuals) < minimum:
959
+ raise self._error(
960
+ f"{axiom} expects at least {minimum} individuals, "
961
+ f"found {len(individuals)}")
962
+ return individuals
963
+
964
+ def _name_to_class_expression(self, name: str, position: int = -1) -> Concept:
965
+ if name in _TOP_SPELLINGS:
966
+ return Top()
967
+ if name in _BOTTOM_SPELLINGS:
968
+ return Bottom()
969
+ if is_builtin_datatype(name):
970
+ # One reading policy for both OWL readers: Manchester refuses this
971
+ # by name ("is a DATATYPE, not a class"); reading it here as an
972
+ # ordinary class called xsd:integer put the datatype's own
973
+ # predicate in a class position, and the verdict changed.
974
+ raise self._unsupported(
975
+ f"{name!r} (at position {position}) is a DATATYPE, not a class "
976
+ f"— OWL 2 does not let one name be both. Use it as the "
977
+ f"filler of a data restriction (DataSomeValuesFrom(d {name})) "
978
+ f"or as a data range", name, position)
979
+ return Atomic(name)
980
+
981
+ def _check_not_a_class_name(self, name: str, position: int) -> str:
982
+ """``name``, unless it is one of the two CLASS names ``owl:Thing`` /
983
+ ``owl:Nothing`` (either spelling) -- the mirror of the refusal above: a
984
+ class is not a data range, and a datatype of that name would be the
985
+ class's own predicate."""
986
+ if name in _TOP_SPELLINGS or name in _BOTTOM_SPELLINGS:
987
+ raise self._unsupported(
988
+ f"{name!r} (at position {position}) is a CLASS, not a data range "
989
+ f"— OWL 2 does not let one name be both. The data domain's own "
990
+ f"'everything' is rdfs:Literal; use a datatype name here",
991
+ name, position)
992
+ return name
993
+
994
+ # -- balanced-parenthesis skipping, for Declaration/Annotation bodies and
995
+ # an axiom's own leading axiomAnnotations (their content is discarded, so
996
+ # it is skipped structurally rather than parsed field-by-field) -------- #
997
+
998
+ def _skip_balanced_parens(self) -> None:
999
+ self._expect("LPAREN", "'('")
1000
+ depth = 1
1001
+ while depth > 0:
1002
+ tok = self._advance()
1003
+ if tok[0] == "LPAREN":
1004
+ depth += 1
1005
+ elif tok[0] == "RPAREN":
1006
+ depth -= 1
1007
+ elif tok[0] == "EOF":
1008
+ raise self._error(
1009
+ "unexpected end of input while skipping a parenthesised group")
1010
+
1011
+ def _skip_leading_annotations(self) -> None:
1012
+ while self._peek()[0] == "WORD" and self._peek()[1] == "Annotation":
1013
+ self._advance()
1014
+ self._skip_balanced_parens()
1015
+
1016
+ # -- class expressions -------------------------------------------------- #
1017
+
1018
+ def _parse_ce_list(self, minimum: int, what: str) -> List[Concept]:
1019
+ """Parse ``'(' CE+ ')'`` (the '(' already consumed by the caller)."""
1020
+ ces: List[Concept] = []
1021
+ while self._peek()[0] != "RPAREN":
1022
+ ces.append(self.parse_class_expression())
1023
+ self._expect("RPAREN", "')'")
1024
+ if len(ces) < minimum:
1025
+ raise self._error(f"{what} expects at least {minimum} arguments, found {len(ces)}")
1026
+ return ces
1027
+
1028
+ def parse_class_expression(self) -> Concept:
1029
+ tok = self._peek()
1030
+ if tok[0] == "IRI":
1031
+ self._advance()
1032
+ return self._name_to_class_expression(tok[1], tok[2])
1033
+ if tok[0] == "WORD":
1034
+ word = tok[1]
1035
+ if word in _REJECTED_CE:
1036
+ self._reject(_REJECTED_CE[word], tok)
1037
+ if word == "ObjectIntersectionOf":
1038
+ self._advance()
1039
+ self._expect("LPAREN", "'('")
1040
+ ces = self._parse_ce_list(2, "ObjectIntersectionOf")
1041
+ return _fold_left(And, ces)
1042
+ if word == "ObjectUnionOf":
1043
+ self._advance()
1044
+ self._expect("LPAREN", "'('")
1045
+ ces = self._parse_ce_list(2, "ObjectUnionOf")
1046
+ return _fold_left(Or, ces)
1047
+ if word == "ObjectComplementOf":
1048
+ self._advance()
1049
+ self._expect("LPAREN", "'('")
1050
+ ce = self.parse_class_expression()
1051
+ self._expect("RPAREN", "')'")
1052
+ return Not(ce)
1053
+ if word == "ObjectSomeValuesFrom":
1054
+ self._advance()
1055
+ self._expect("LPAREN", "'('")
1056
+ role = self._parse_object_property_expression()
1057
+ ce = self.parse_class_expression()
1058
+ self._expect("RPAREN", "')'")
1059
+ return Exists(role, ce)
1060
+ if word == "ObjectAllValuesFrom":
1061
+ self._advance()
1062
+ self._expect("LPAREN", "'('")
1063
+ role = self._parse_object_property_expression()
1064
+ ce = self.parse_class_expression()
1065
+ self._expect("RPAREN", "')'")
1066
+ return ForAll(role, ce)
1067
+ if word == "ObjectHasValue":
1068
+ # ObjectHasValue(OPE Individual) -- its own concept kind, NOT
1069
+ # Exists(role, Nominal(individual)): the two are distinct OWL 2
1070
+ # structural objects (ObjectSomeValuesFrom(r ObjectOneOf(a)) is
1071
+ # the other one, and still refused), and folding them together
1072
+ # would break the round-trip guarantee for whichever lost. See
1073
+ # dl.concepts.HasValue.
1074
+ self._advance()
1075
+ self._expect("LPAREN", "'('")
1076
+ role = self._parse_object_property_expression()
1077
+ individual = self._parse_name()
1078
+ self._expect("RPAREN", "')'")
1079
+ return HasValue(role, individual)
1080
+ if word in ("DataSomeValuesFrom", "DataAllValuesFrom"):
1081
+ self._advance()
1082
+ self._expect("LPAREN", "'('")
1083
+ prop = self._parse_data_property_expression()
1084
+ datarange = self._parse_data_range()
1085
+ self._expect("RPAREN", "')'")
1086
+ return (DataExists if word == "DataSomeValuesFrom"
1087
+ else DataForAll)(prop, datarange)
1088
+ if word == "DataHasValue":
1089
+ self._advance()
1090
+ self._expect("LPAREN", "'('")
1091
+ prop = self._parse_data_property_expression()
1092
+ value = self._parse_literal()
1093
+ self._expect("RPAREN", "')'")
1094
+ return DataHasValue(prop, value)
1095
+ if word in ("DataMinCardinality", "DataMaxCardinality", "DataExactCardinality"):
1096
+ self._advance()
1097
+ self._expect("LPAREN", "'('")
1098
+ n = self._expect_nonneg_int()
1099
+ prop = self._parse_data_property_expression()
1100
+ # the data range is OPTIONAL, defaulting to rdfs:Literal -- the
1101
+ # data twin of the object cardinalities' owl:Thing default
1102
+ datarange = (self._parse_data_range() if self._peek()[0] != "RPAREN"
1103
+ else Datatype("rdfs:Literal"))
1104
+ self._expect("RPAREN", "')'")
1105
+ if word == "DataMinCardinality":
1106
+ return DataAtLeast(n, prop, datarange)
1107
+ if word == "DataMaxCardinality":
1108
+ return DataAtMost(n, prop, datarange)
1109
+ return And(DataAtLeast(n, prop, datarange),
1110
+ DataAtMost(n, prop, datarange)) # ExactCardinality
1111
+ if word in ("ObjectMinCardinality", "ObjectMaxCardinality", "ObjectExactCardinality"):
1112
+ self._advance()
1113
+ self._expect("LPAREN", "'('")
1114
+ n = self._expect_nonneg_int()
1115
+ role = self._parse_object_property_expression()
1116
+ filler = self.parse_class_expression() if self._peek()[0] != "RPAREN" else Top()
1117
+ self._expect("RPAREN", "')'")
1118
+ if word == "ObjectMinCardinality":
1119
+ return AtLeast(n, role, filler)
1120
+ if word == "ObjectMaxCardinality":
1121
+ return AtMost(n, role, filler)
1122
+ return And(AtLeast(n, role, filler), AtMost(n, role, filler)) # ExactCardinality
1123
+ # Not a recognised class-expression keyword: an atomic name, UNLESS
1124
+ # it is immediately followed by '(' -- an unknown/unsupported
1125
+ # construct, not a bare name that merely happens to precede one.
1126
+ self._advance()
1127
+ if self._peek()[0] == "LPAREN":
1128
+ raise self._unsupported(
1129
+ f"unsupported or unknown OWL 2 class expression {word!r}",
1130
+ word, tok[2])
1131
+ return self._name_to_class_expression(word, tok[2])
1132
+ found = "end of input" if tok[0] == "EOF" else f"{tok[1]!r}"
1133
+ raise self._error(
1134
+ f"expected a class expression but found {found} at position {tok[2]}; "
1135
+ "expected a class name, 'owl:Thing', 'owl:Nothing', or an "
1136
+ "ObjectIntersectionOf/ObjectUnionOf/ObjectComplementOf/"
1137
+ "ObjectSomeValuesFrom/ObjectAllValuesFrom/ObjectHasValue/"
1138
+ "ObjectMinCardinality/"
1139
+ "ObjectMaxCardinality/ObjectExactCardinality or "
1140
+ "DataSomeValuesFrom/DataAllValuesFrom/DataHasValue/"
1141
+ "DataMin/Max/ExactCardinality expression")
1142
+
1143
+ def _expect_nonneg_int(self) -> int:
1144
+ tok = self._peek()
1145
+ if tok[0] == "WORD" and tok[1].isdigit():
1146
+ self._advance()
1147
+ return int(tok[1])
1148
+ found = "end of input" if tok[0] == "EOF" else f"{tok[1]!r}"
1149
+ raise self._error(
1150
+ f"expected a non-negative integer but found {found} at position {tok[2]}")
1151
+
1152
+ # -- axioms --------------------------------------------------------------- #
1153
+
1154
+ def _parse_axiom(self, tbox: TBox, abox: ABox) -> Optional[str]:
1155
+ """Parse ONE axiom into ``tbox``/``abox``.
1156
+
1157
+ Returns ``None`` for an axiom that was read (or a Declaration/
1158
+ Annotation/AnnotationAssertion, which :meth:`parse_document_lenient`
1159
+ counts as skipped), and a REASON string for an axiom that was CONSUMED
1160
+ without logical content -- a tautological property inclusion or an
1161
+ annotation-property axiom -- so the caller can report it instead of
1162
+ letting it vanish.
1163
+ """
1164
+ tok = self._peek()
1165
+ if tok[0] != "WORD":
1166
+ found = "end of input" if tok[0] == "EOF" else f"{tok[1]!r}"
1167
+ raise self._error(f"expected an axiom keyword but found {found} at position {tok[2]}")
1168
+ word = tok[1]
1169
+
1170
+ if word == "Declaration" or word in ("Annotation", "AnnotationAssertion"):
1171
+ self._advance()
1172
+ self._skip_balanced_parens()
1173
+ return None
1174
+
1175
+ if word in _ANNOTATION_PROPERTY_AXIOMS:
1176
+ self._advance()
1177
+ self._skip_balanced_parens()
1178
+ return (f"{word} is an annotation-property axiom: annotation "
1179
+ f"properties carry no content under the OWL 2 direct "
1180
+ f"semantics, so it constrains nothing (consumed, like "
1181
+ f"AnnotationAssertion, but reported)")
1182
+
1183
+ if word in _REJECTED_AXIOMS:
1184
+ self._advance()
1185
+ self._reject(_REJECTED_AXIOMS[word], tok)
1186
+ return None
1187
+
1188
+ if word == "SubClassOf":
1189
+ self._advance()
1190
+ self._expect("LPAREN", "'('")
1191
+ self._skip_leading_annotations()
1192
+ sub = self.parse_class_expression()
1193
+ sup = self.parse_class_expression()
1194
+ self._expect("RPAREN", "')'")
1195
+ tbox.add(sub, sup)
1196
+ return None
1197
+
1198
+ if word == "EquivalentClasses":
1199
+ self._advance()
1200
+ self._expect("LPAREN", "'('")
1201
+ self._skip_leading_annotations()
1202
+ ces = self._parse_ce_list(2, "EquivalentClasses")
1203
+ for left, right in zip(ces, ces[1:]):
1204
+ tbox.add_equivalence(left, right)
1205
+ return None
1206
+
1207
+ if word == "DisjointClasses":
1208
+ self._advance()
1209
+ self._expect("LPAREN", "'('")
1210
+ self._skip_leading_annotations()
1211
+ ces = self._parse_ce_list(2, "DisjointClasses")
1212
+ for i in range(len(ces)):
1213
+ for j in range(i + 1, len(ces)):
1214
+ tbox.add(And(ces[i], ces[j]), Bottom())
1215
+ return None
1216
+
1217
+ if word == "SubObjectPropertyOf":
1218
+ self._advance()
1219
+ self._expect("LPAREN", "'('")
1220
+ self._skip_leading_annotations()
1221
+ chain_tok = self._peek()
1222
+ if chain_tok[0] == "WORD" and chain_tok[1] == "ObjectPropertyChain":
1223
+ self._advance()
1224
+ self._expect("LPAREN", "'('")
1225
+ chain: List[str] = []
1226
+ while self._peek()[0] != "RPAREN":
1227
+ chain.append(self._parse_role_name("SubObjectPropertyOf"))
1228
+ self._expect("RPAREN", "')'")
1229
+ if len(chain) < 2:
1230
+ raise self._error(
1231
+ f"ObjectPropertyChain expects at least 2 object "
1232
+ f"properties, found {len(chain)} (OWL 2's grammar is "
1233
+ f"ObjectPropertyChain(OPE OPE+); a single role on the "
1234
+ f"left is an ordinary SubObjectPropertyOf axiom)")
1235
+ chain_super_role = self._parse_role_name("SubObjectPropertyOf")
1236
+ self._expect("RPAREN", "')'")
1237
+ tbox.add_role_chain(tuple(chain), chain_super_role)
1238
+ return None
1239
+ sub_position = self._peek()[2]
1240
+ sub_role = self._parse_inclusion_role()
1241
+ sup_position = self._peek()[2]
1242
+ sup_role = self._parse_inclusion_role()
1243
+ self._expect("RPAREN", "')'")
1244
+ # A9: an inclusion that is a TAUTOLOGY because of a built-in role
1245
+ # name is consumed as a no-op. Not the "silent dropping" this kit
1246
+ # forbids: a tautology carries no truth to lose, and it is the same
1247
+ # precedent Declaration(...)/Annotation(...) already set above.
1248
+ # EVERY other use of a built-in name is refused by name, below.
1249
+ if _is_tautology_of(sub_role, sup_role, "Object"):
1250
+ return _tautology_reason("SubObjectPropertyOf", sub_role, sup_role)
1251
+ # A side that is an inverse role was already read through the
1252
+ # plain-name reader (built-ins refused there); only a PLAIN side
1253
+ # can still be a built-in that is not part of a tautology.
1254
+ if isinstance(sub_role, str):
1255
+ self._check_role_name(sub_role, "SubObjectPropertyOf", sub_position)
1256
+ if isinstance(sup_role, str):
1257
+ self._check_role_name(sup_role, "SubObjectPropertyOf", sup_position)
1258
+ tbox.add_role_inclusion(sub_role, sup_role)
1259
+ return None
1260
+
1261
+ if word == "ObjectPropertyChain":
1262
+ # Legal OWL 2 only INSIDE SubObjectPropertyOf (handled above);
1263
+ # on its own it is not an axiom at all.
1264
+ self._advance()
1265
+ raise self._error(
1266
+ f"ObjectPropertyChain is not an axiom on its own — it is the "
1267
+ f"left-hand side of SubObjectPropertyOf(ObjectPropertyChain(P1 "
1268
+ f"… Pn) Q), found {tok[1]!r} at position {tok[2]}")
1269
+
1270
+ if word == "EquivalentObjectProperties":
1271
+ self._advance()
1272
+ self._expect("LPAREN", "'('")
1273
+ self._skip_leading_annotations()
1274
+ roles = self._parse_role_list("EquivalentObjectProperties")
1275
+ # Stored as the role inclusions it abbreviates, CONSECUTIVE pairs
1276
+ # only: ⊑ is transitive, so P1 ≡ P2 ≡ P3 already entails P1 ≡ P3.
1277
+ # Contrast DisjointObjectProperties below, where disjointness has
1278
+ # no such closure and ALL pairs are needed -- the same argument
1279
+ # this module's docstring makes for EquivalentClasses.
1280
+ tbox.add_equivalent_roles(*roles)
1281
+ return None
1282
+
1283
+ if word == "DisjointObjectProperties":
1284
+ self._advance()
1285
+ self._expect("LPAREN", "'('")
1286
+ self._skip_leading_annotations()
1287
+ roles = self._parse_role_list("DisjointObjectProperties")
1288
+ tbox.add_disjoint_roles(*roles)
1289
+ return None
1290
+
1291
+ if word == "InverseObjectProperties":
1292
+ self._advance()
1293
+ self._expect("LPAREN", "'('")
1294
+ self._skip_leading_annotations()
1295
+ roles = self._parse_role_list("InverseObjectProperties")
1296
+ if len(roles) != 2:
1297
+ raise self._error(
1298
+ f"InverseObjectProperties expects exactly 2 object "
1299
+ f"properties, found {len(roles)}")
1300
+ tbox.add_inverse_roles(roles[0], roles[1])
1301
+ return None
1302
+
1303
+ if word in _CHARACTERISTIC_AXIOMS:
1304
+ self._advance()
1305
+ self._expect("LPAREN", "'('")
1306
+ self._skip_leading_annotations()
1307
+ role = self._parse_role_name(word)
1308
+ self._expect("RPAREN", "')'")
1309
+ getattr(tbox, _CHARACTERISTIC_AXIOMS[word])(role)
1310
+ return None
1311
+
1312
+ if word in ("ObjectPropertyDomain", "ObjectPropertyRange"):
1313
+ # Stored NATIVELY in the role box, not desugared into the
1314
+ # equivalent GCI -- see dl.TBox.add_role_domain for the three
1315
+ # reasons, the FOL image being the first of them.
1316
+ self._advance()
1317
+ self._expect("LPAREN", "'('")
1318
+ self._skip_leading_annotations()
1319
+ role = self._parse_role_name(word)
1320
+ filler = self.parse_class_expression()
1321
+ self._expect("RPAREN", "')'")
1322
+ builder = ("add_role_domain" if word == "ObjectPropertyDomain"
1323
+ else "add_role_range")
1324
+ getattr(tbox, builder)(role, filler)
1325
+ return None
1326
+
1327
+ if word == "SameIndividual":
1328
+ self._advance()
1329
+ self._expect("LPAREN", "'('")
1330
+ self._skip_leading_annotations()
1331
+ individuals = self._parse_individual_list("SameIndividual")
1332
+ # CONSECUTIVE pairs only: equality is transitive, so a1 = a2 and
1333
+ # a2 = a3 already entail a1 = a3. Deliberately NOT the all-pairs
1334
+ # expansion DifferentIndividuals needs below -- distinctness has no
1335
+ # transitive closure. (Same argument this module's docstring makes
1336
+ # for EquivalentClasses versus DisjointClasses.)
1337
+ for first, second in zip(individuals, individuals[1:]):
1338
+ abox.assert_same(first, second)
1339
+ return None
1340
+
1341
+ if word == "NegativeObjectPropertyAssertion":
1342
+ self._advance()
1343
+ self._expect("LPAREN", "'('")
1344
+ self._skip_leading_annotations()
1345
+ role = self._parse_object_property_expression()
1346
+ a = self._parse_name()
1347
+ b = self._parse_name()
1348
+ self._expect("RPAREN", "')'")
1349
+ abox.assert_negative_role(a, b, role)
1350
+ return None
1351
+
1352
+ if word == "ClassAssertion":
1353
+ self._advance()
1354
+ self._expect("LPAREN", "'('")
1355
+ self._skip_leading_annotations()
1356
+ ce = self.parse_class_expression()
1357
+ individual = self._parse_name()
1358
+ self._expect("RPAREN", "')'")
1359
+ abox.assert_concept(individual, ce)
1360
+ return None
1361
+
1362
+ if word == "ObjectPropertyAssertion":
1363
+ self._advance()
1364
+ self._expect("LPAREN", "'('")
1365
+ self._skip_leading_annotations()
1366
+ role = self._parse_object_property_expression()
1367
+ a = self._parse_name()
1368
+ b = self._parse_name()
1369
+ self._expect("RPAREN", "')'")
1370
+ abox.assert_role(a, b, role)
1371
+ return None
1372
+
1373
+ if word == "DifferentIndividuals":
1374
+ self._advance()
1375
+ self._expect("LPAREN", "'('")
1376
+ self._skip_leading_annotations()
1377
+ individuals = self._parse_individual_list("DifferentIndividuals")
1378
+ for i in range(len(individuals)):
1379
+ for j in range(i + 1, len(individuals)):
1380
+ abox.assert_distinct(individuals[i], individuals[j])
1381
+ return None
1382
+
1383
+ if word == "SubDataPropertyOf":
1384
+ self._advance()
1385
+ self._expect("LPAREN", "'('")
1386
+ self._skip_leading_annotations()
1387
+ sub_position = self._peek()[2]
1388
+ sub_prop = self._parse_data_property_expression(allow_reserved=True)
1389
+ sup_position = self._peek()[2]
1390
+ sup_prop = self._parse_data_property_expression(allow_reserved=True)
1391
+ self._expect("RPAREN", "')'")
1392
+ # The data twin of the tautological object inclusion: P ⊑ topDataProperty
1393
+ # and bottomDataProperty ⊑ P hold in every interpretation.
1394
+ if _is_tautology_of(sub_prop, sup_prop, "Data"):
1395
+ return _tautology_reason("SubDataPropertyOf", sub_prop, sup_prop)
1396
+ self._check_role_name(sub_prop, "SubDataPropertyOf", sub_position)
1397
+ self._check_role_name(sup_prop, "SubDataPropertyOf", sup_position)
1398
+ tbox.add_data_property_inclusion(sub_prop, sup_prop)
1399
+ return None
1400
+
1401
+ if word == "EquivalentDataProperties":
1402
+ self._advance()
1403
+ self._expect("LPAREN", "'('")
1404
+ self._skip_leading_annotations()
1405
+ props = self._parse_data_property_list(word)
1406
+ tbox.add_equivalent_data_properties(*props)
1407
+ return None
1408
+
1409
+ if word == "DisjointDataProperties":
1410
+ self._advance()
1411
+ self._expect("LPAREN", "'('")
1412
+ self._skip_leading_annotations()
1413
+ props = self._parse_data_property_list(word)
1414
+ tbox.add_disjoint_data_properties(*props)
1415
+ return None
1416
+
1417
+ if word == "FunctionalDataProperty":
1418
+ self._advance()
1419
+ self._expect("LPAREN", "'('")
1420
+ self._skip_leading_annotations()
1421
+ prop = self._parse_data_property_name(word)
1422
+ self._expect("RPAREN", "')'")
1423
+ tbox.add_functional_data_property(prop)
1424
+ return None
1425
+
1426
+ if word == "DataPropertyDomain":
1427
+ self._advance()
1428
+ self._expect("LPAREN", "'('")
1429
+ self._skip_leading_annotations()
1430
+ prop = self._parse_data_property_name(word)
1431
+ filler = self.parse_class_expression()
1432
+ self._expect("RPAREN", "')'")
1433
+ tbox.add_data_property_domain(prop, filler)
1434
+ return None
1435
+
1436
+ if word == "DataPropertyRange":
1437
+ self._advance()
1438
+ self._expect("LPAREN", "'('")
1439
+ self._skip_leading_annotations()
1440
+ prop = self._parse_data_property_name(word)
1441
+ datarange = self._parse_data_range()
1442
+ self._expect("RPAREN", "')'")
1443
+ tbox.add_data_property_range(prop, datarange)
1444
+ return None
1445
+
1446
+ if word == "DatatypeDefinition":
1447
+ self._advance()
1448
+ self._expect("LPAREN", "'('")
1449
+ self._skip_leading_annotations()
1450
+ name_position = self._peek()[2]
1451
+ name = self._parse_datatype_name()
1452
+ datarange = self._parse_data_range()
1453
+ self._expect("RPAREN", "')'")
1454
+ self._datatype_construct(
1455
+ lambda: self._add_datatype_definition(tbox, name, datarange),
1456
+ "DatatypeDefinition", name_position)
1457
+ return None
1458
+
1459
+ if word in ("DataPropertyAssertion", "NegativeDataPropertyAssertion"):
1460
+ self._advance()
1461
+ self._expect("LPAREN", "'('")
1462
+ self._skip_leading_annotations()
1463
+ prop = self._parse_data_property_name(word)
1464
+ individual = self._parse_name()
1465
+ value = self._parse_literal()
1466
+ self._expect("RPAREN", "')'")
1467
+ (abox.assert_data if word == "DataPropertyAssertion"
1468
+ else abox.assert_negative_data)(individual, prop, value)
1469
+ return None
1470
+
1471
+ raise self._unsupported(
1472
+ f"unknown or unsupported OWL 2 axiom {word!r}", word, tok[2])
1473
+
1474
+ # -- document shell: Prefix(...)* Ontology(iri? version-iri? axiom*) ---- #
1475
+
1476
+ def _parse_prefix(self) -> None:
1477
+ self._advance() # 'Prefix'
1478
+ self._expect("LPAREN", "'('")
1479
+ self._expect("WORD", "a prefix name (e.g. 'owl:' or ':')")
1480
+ self._expect("EQUALS", "'='")
1481
+ self._expect("IRI", "a full IRI ('<...>')")
1482
+ self._expect("RPAREN", "')'")
1483
+
1484
+ def parse_document(self) -> Tuple[TBox, ABox]:
1485
+ self._parse_document_shell()
1486
+ tbox, abox = TBox(), ABox()
1487
+ while self._peek()[0] != "RPAREN":
1488
+ self._parse_axiom(tbox, abox)
1489
+ self._expect("RPAREN", "')'")
1490
+ self._expect_eof()
1491
+ return tbox, abox
1492
+
1493
+ def _parse_document_shell(self) -> None:
1494
+ """Consume ``Prefix(...)* Ontology( <iri>? <version-iri>?`` — the part
1495
+ of the document shell both :meth:`parse_document` and
1496
+ :meth:`parse_document_lenient` read identically.
1497
+ """
1498
+ while self._peek()[0] == "WORD" and self._peek()[1] == "Prefix":
1499
+ self._parse_prefix()
1500
+ self._expect_word("Ontology")
1501
+ self._expect("LPAREN", "'('")
1502
+ if self._peek()[0] == "IRI":
1503
+ self._advance() # ontology IRI, discarded
1504
+ if self._peek()[0] == "IRI":
1505
+ self._advance() # version IRI, discarded
1506
+
1507
+ def _consume_axiom_text(self) -> str:
1508
+ """Consume one whole ``Keyword(...)`` group from the axiom keyword the
1509
+ cursor is on, and return that axiom's OWN source slice.
1510
+
1511
+ The recovery step of :meth:`parse_document_lenient`: it skips the
1512
+ keyword WORD plus its balanced parens with the existing
1513
+ :meth:`_skip_balanced_parens`, so a refused axiom costs exactly its own
1514
+ text and the next axiom starts where it should. An unbalanced paren
1515
+ inside it still raises, which is the right answer — that is malformed
1516
+ input, not a fragment question.
1517
+ """
1518
+ keyword = self._advance()
1519
+ if self._peek()[0] == "LPAREN":
1520
+ self._skip_balanced_parens()
1521
+ last = self._tokens[self._i - 1]
1522
+ return self._text[keyword[2]:last[2] + len(last[1])]
1523
+
1524
+ def parse_document_lenient(self) -> "OwlFunctionalResult":
1525
+ """:meth:`parse_document`, but recovering at AXIOM boundaries from every
1526
+ construct that is valid OWL 2 outside this fragment — see
1527
+ :func:`parse_owl_functional_axioms`, which is the public entry point and
1528
+ carries the contract.
1529
+
1530
+ Each axiom is parsed into a FRESH scratch ``TBox``/``ABox`` pair and
1531
+ merged on success. That makes "a refused axiom leaves nothing behind"
1532
+ STRUCTURAL rather than a reading of :meth:`_parse_axiom`'s code: if a
1533
+ future axiom branch ever mutated its TBox before finishing its
1534
+ arguments, recovery would otherwise leave half an axiom in the result.
1535
+ """
1536
+ self._parse_document_shell()
1537
+ tbox, abox = TBox(), ABox()
1538
+ # The live list: ``_merge_axiom_holder`` extends it in place, so what
1539
+ # the parser checks a definition against is what has been merged so far.
1540
+ self._earlier_definitions = tbox.datatype_definitions
1541
+ refused: List[RefusedAxiom] = []
1542
+ consumed: List[ConsumedAxiom] = []
1543
+ accepted = 0
1544
+ skipped = 0
1545
+ while self._peek()[0] != "RPAREN":
1546
+ start = self._i
1547
+ head = self._peek()
1548
+ is_noop = (head[0] == "WORD"
1549
+ and head[1] in ("Declaration", "Annotation",
1550
+ "AnnotationAssertion"))
1551
+ scratch_tbox, scratch_abox = TBox(), ABox()
1552
+ try:
1553
+ reason = self._parse_axiom(scratch_tbox, scratch_abox)
1554
+ except OwlFunctionalUnsupportedError as exc:
1555
+ self._i = start
1556
+ refused.append(RefusedAxiom(
1557
+ keyword=exc.keyword or head[1],
1558
+ position=exc.position if exc.position >= 0 else head[2],
1559
+ text=self._consume_axiom_text(),
1560
+ reason=str(exc)))
1561
+ continue
1562
+ _merge_axiom_holder(tbox, scratch_tbox)
1563
+ _merge_axiom_holder(abox, scratch_abox)
1564
+ if is_noop:
1565
+ skipped += 1
1566
+ elif reason is not None:
1567
+ last = self._tokens[self._i - 1]
1568
+ consumed.append(ConsumedAxiom(
1569
+ keyword=head[1], position=head[2],
1570
+ text=self._text[head[2]:last[2] + len(last[1])],
1571
+ reason=reason))
1572
+ else:
1573
+ accepted += 1
1574
+ self._expect("RPAREN", "')'")
1575
+ self._expect_eof()
1576
+ return OwlFunctionalResult(tbox=tbox, abox=abox, refused=tuple(refused),
1577
+ accepted=accepted, skipped=skipped,
1578
+ consumed=tuple(consumed))
1579
+
1580
+
1581
+ def _merge_axiom_holder(target, source) -> None:
1582
+ """Merge every axiom ``source`` holds into ``target`` (both a ``TBox`` or
1583
+ both an ``ABox``), generically over the dataclass fields.
1584
+
1585
+ Generic on purpose: a field added to ``TBox``/``ABox`` is merged here with
1586
+ no edit, and :data:`~unicode_logic_kit.dl.tableau._AXIOM_KINDS`' own meta-test
1587
+ is what stops a field existing without anyone having thought about it.
1588
+ """
1589
+ for declared in dataclass_fields(type(source)):
1590
+ value = getattr(source, declared.name)
1591
+ current = getattr(target, declared.name)
1592
+ if isinstance(current, set):
1593
+ current.update(value)
1594
+ else:
1595
+ current.extend(value)
1596
+
1597
+
1598
+ @dataclass(frozen=True)
1599
+ class RefusedAxiom:
1600
+ """One axiom :func:`parse_owl_functional_axioms` did not read, as DATA.
1601
+
1602
+ Structured rather than an exception message, because the useful questions
1603
+ about a real ontology are "how many, of which kinds, and where" — and the
1604
+ requesting OEO project answered them by regexing 4041 caught exception
1605
+ strings.
1606
+
1607
+ Fields:
1608
+
1609
+ * ``keyword`` — the OWL keyword that was refused. For a construct nested
1610
+ inside an otherwise-supported axiom this is the INNER one
1611
+ (``SubClassOf(A DataHasValue(d "1"))`` refuses as ``"DataHasValue"``),
1612
+ because that is the construct a caller would have to add support for.
1613
+ * ``position`` — its 0-based character offset in the input text.
1614
+ * ``text`` — the refused AXIOM's own source slice, keyword through its
1615
+ closing paren. The axiom, not the inner construct: that is the unit that
1616
+ was skipped.
1617
+ * ``reason`` — the full refusal message, which names the construct and says
1618
+ what to use instead.
1619
+ """
1620
+
1621
+ keyword: str
1622
+ position: int
1623
+ text: str
1624
+ reason: str
1625
+
1626
+
1627
+ @dataclass(frozen=True)
1628
+ class ConsumedAxiom:
1629
+ """One axiom :func:`parse_owl_functional_axioms` READ and found to carry no
1630
+ logical content -- as DATA, so it is reported and does not vanish.
1631
+
1632
+ Not a refusal: nothing is lost by consuming it. Two kinds exist, both
1633
+ valid OWL 2: an ANNOTATION-PROPERTY axiom (``SubAnnotationPropertyOf``,
1634
+ ``AnnotationPropertyDomain``, ``AnnotationPropertyRange`` -- annotation
1635
+ properties carry no content under the OWL 2 direct semantics), and a
1636
+ TAUTOLOGICAL property inclusion (``SubObjectPropertyOf(P
1637
+ owl:topObjectProperty)`` and its data and bottom-property twins, which hold
1638
+ in every interpretation).
1639
+
1640
+ Fields: ``keyword`` (the axiom's own OWL keyword), ``position`` (its
1641
+ 0-based offset), ``text`` (its source slice) and ``reason`` (why it carries
1642
+ no content).
1643
+ """
1644
+
1645
+ keyword: str
1646
+ position: int
1647
+ text: str
1648
+ reason: str
1649
+
1650
+
1651
+ @dataclass(frozen=True)
1652
+ class OwlFunctionalResult:
1653
+ """What :func:`parse_owl_functional_axioms` read from a document, and what
1654
+ it did not.
1655
+
1656
+ Fields:
1657
+
1658
+ * ``tbox`` / ``abox`` — built from every axiom that WAS read. Ordinary
1659
+ :class:`~unicode_logic_kit.dl.tableau.TBox`/
1660
+ :class:`~unicode_logic_kit.dl.tableau.ABox` objects, so every ``dl``
1661
+ function takes them unchanged.
1662
+ * ``refused`` — one :class:`RefusedAxiom` per axiom skipped, in document
1663
+ order.
1664
+ * ``accepted`` — how many axioms were read into ``tbox``/``abox``.
1665
+ * ``skipped`` — how many ``Declaration``/``Annotation``/
1666
+ ``AnnotationAssertion`` no-ops were consumed. Counted separately from
1667
+ ``accepted`` because they carry no logical content.
1668
+ * ``consumed`` — one :class:`ConsumedAxiom` per axiom that WAS read but
1669
+ carries no logical content (an annotation-property axiom, a tautological
1670
+ property inclusion), in document order. Reported rather than counted
1671
+ with ``skipped``, because these LOOK logical: a caller auditing a
1672
+ census against the document needs to be told. ``accepted + skipped +
1673
+ len(consumed) + len(refused)`` is the document's axiom count.
1674
+
1675
+ ``accepted`` counts AXIOMS, not stored entries: one
1676
+ ``EquivalentClasses(C D)`` is one accepted axiom and two
1677
+ ``tbox.inclusions``, and one ``DisjointObjectProperties(P Q R)`` is one
1678
+ accepted axiom and three stored pairs.
1679
+ """
1680
+
1681
+ tbox: TBox
1682
+ abox: ABox
1683
+ refused: Tuple[RefusedAxiom, ...] = ()
1684
+ accepted: int = 0
1685
+ skipped: int = 0
1686
+ consumed: Tuple[ConsumedAxiom, ...] = ()
1687
+
1688
+ @property
1689
+ def ok(self) -> bool:
1690
+ """True iff nothing was refused — the same convention
1691
+ :class:`unicode_logic_kit.api.ParseResult` uses.
1692
+
1693
+ ``False`` does NOT mean the result is unusable: ``tbox``/``abox`` hold
1694
+ every axiom that WAS read. It means the knowledge base they describe is
1695
+ WEAKER than the document, by exactly the axioms ``refused`` names.
1696
+ """
1697
+ return not self.refused
1698
+
1699
+ @property
1700
+ def refused_keywords(self) -> Dict[str, int]:
1701
+ """``keyword -> how many axioms were refused for it``, so a census of an
1702
+ ontology's unsupported fragment needs no regex over messages.
1703
+ """
1704
+ counts: Dict[str, int] = {}
1705
+ for axiom in self.refused:
1706
+ counts[axiom.keyword] = counts.get(axiom.keyword, 0) + 1
1707
+ return counts
1708
+
1709
+ def to_kb(self) -> "object":
1710
+ """``dl.kb_to_fol(self.tbox, self.abox)`` — the FOL image, with the role
1711
+ box as SEPARATE premises.
1712
+
1713
+ The obvious next call, and the reason it is a method: handing the TBox
1714
+ to :func:`~unicode_logic_kit.dl.translate.tbox_to_fol` instead would drop
1715
+ every role-box axiom the document carried (218 of them on the OEO
1716
+ ontology), which is the trap
1717
+ :class:`~unicode_logic_kit.dl.translate.RoleBoxOmittedError` exists for
1718
+ and the one the requesting project's own translator fell into.
1719
+
1720
+ The import is local: ``dl.translate`` imports THIS module's siblings
1721
+ (``tableau``, ``concepts``) but not this module, so a module-level
1722
+ import here would add an edge to the package's layering for one
1723
+ convenience method.
1724
+ """
1725
+ from .translate import kb_to_fol
1726
+
1727
+ return kb_to_fol(self.tbox, self.abox)
1728
+
1729
+ def to_dict(self) -> dict:
1730
+ """JSON-compatible form: the counts and the refusals.
1731
+
1732
+ The TBox/ABox are NOT in it — they are object graphs, and the FOL image
1733
+ (``to_kb().to_dict()``) is the serialisable form of their content.
1734
+ """
1735
+ return {
1736
+ "ok": self.ok,
1737
+ "accepted": self.accepted,
1738
+ "skipped": self.skipped,
1739
+ "consumed": [{"keyword": a.keyword, "position": a.position,
1740
+ "text": a.text, "reason": a.reason}
1741
+ for a in self.consumed],
1742
+ "refused": [{"keyword": a.keyword, "position": a.position,
1743
+ "text": a.text, "reason": a.reason}
1744
+ for a in self.refused],
1745
+ "refused_keywords": self.refused_keywords,
1746
+ }
1747
+
1748
+
1749
+ def _fold_left(ctor, items: List[Concept]) -> Concept:
1750
+ """Fold ``items`` into ``ctor`` left-associatively (see the module
1751
+ docstring's "Supported fragment" note on ``ObjectIntersectionOf``/``ObjectUnionOf``).
1752
+ """
1753
+ acc = items[0]
1754
+ for item in items[1:]:
1755
+ acc = ctor(acc, item)
1756
+ return acc
1757
+
1758
+
1759
+ # --------------------------------------------------------------------------- #
1760
+ # Public parsing API.
1761
+ # --------------------------------------------------------------------------- #
1762
+
1763
+ def parse_owl_functional(text: str) -> Tuple[TBox, ABox]:
1764
+ """Parse a whole OWL 2 Functional-Style Syntax ontology document.
1765
+
1766
+ Round-trips against :func:`to_owl_functional`: ``parse_owl_functional(
1767
+ to_owl_functional(tbox, abox)) == (tbox, abox)`` for every ALCHQ ``(tbox,
1768
+ abox)`` pair (see the module docstring's "Round-trip guarantee").
1769
+
1770
+ Args:
1771
+ text: A document of the shape ``Prefix(...)* Ontology(<iri>? axiom*)``
1772
+ (see the module docstring's "Supported fragment").
1773
+
1774
+ Returns:
1775
+ ``(tbox, abox)``, the :class:`~unicode_logic_kit.dl.tableau.TBox`/
1776
+ :class:`~unicode_logic_kit.dl.tableau.ABox` built from every supported
1777
+ axiom in the document (``Declaration``/``Annotation``/
1778
+ ``AnnotationAssertion`` axioms are no-ops — see the module docstring).
1779
+
1780
+ Raises:
1781
+ OwlFunctionalSyntaxError: On malformed input, or on syntax that is
1782
+ valid OWL 2 Functional-Style Syntax but falls outside ALCHQ (see
1783
+ the module docstring's "Rejected").
1784
+
1785
+ STRICT, and deliberately kept so: it raises on the FIRST axiom outside the
1786
+ fragment and returns nothing, which for a real ontology means no TBox at
1787
+ all. :func:`parse_owl_functional_axioms` is the per-axiom reader that
1788
+ reports the refusals instead.
1789
+
1790
+ Consumed without a report: besides the ``Declaration``/``Annotation``/
1791
+ ``AnnotationAssertion`` no-ops, this function reads and DROPS the
1792
+ annotation-property axioms (``SubAnnotationPropertyOf``,
1793
+ ``AnnotationPropertyDomain``, ``AnnotationPropertyRange``) and a
1794
+ TAUTOLOGICAL property inclusion (into ``owl:topObjectProperty``/
1795
+ ``owl:topDataProperty``, or out of ``owl:bottomObjectProperty``/
1796
+ ``owl:bottomDataProperty``). They carry no logical content, so nothing is
1797
+ lost -- but they vanish without a report here. Use
1798
+ :func:`parse_owl_functional_axioms`, whose ``consumed`` lists each one, when
1799
+ the document's axiom count has to add up.
1800
+
1801
+ A literal with no first-order term (``xsd:float``/``xsd:double``, a decimal
1802
+ beyond float precision) and a built-in datatype name in class position are
1803
+ refused here as well (see the module docstring). A class expression nested
1804
+ about a thousand levels deep raises :class:`RecursionError` (the reader is
1805
+ recursive descent; the nesting is not rewritten).
1806
+ """
1807
+ return _Parser(_tokenize(text), text).parse_document()
1808
+
1809
+
1810
+ def parse_owl_functional_axioms(text: str) -> OwlFunctionalResult:
1811
+ """Read a whole OWL 2 Functional-Style Syntax document, REPORTING the axioms
1812
+ outside this fragment instead of raising on the first one.
1813
+
1814
+ The entry point for a real ontology. :func:`parse_owl_functional` raises as
1815
+ soon as it meets a construct it has no reading for, so a document with one
1816
+ ``DataPropertyRange`` yields nothing at all; this returns everything it
1817
+ could read plus a :class:`RefusedAxiom` per axiom it could not, each naming
1818
+ the construct, its offset and its own source text. House rule: an
1819
+ unsupported fragment is refused LOUDLY, by name — never approximated and
1820
+ never silently dropped — so the refusals are part of the RESULT, and a
1821
+ caller who ignores them is choosing to, in writing.
1822
+
1823
+ One tokenization and ONE pass over the document, recovering at axiom
1824
+ boundaries. Each axiom is parsed into a scratch ``TBox``/``ABox`` and
1825
+ merged only on success, so a refused axiom leaves nothing behind
1826
+ structurally rather than by anyone's reading of the parser.
1827
+
1828
+ Args:
1829
+ text: A document of the shape ``Prefix(...)* Ontology(<iri>? axiom*)``
1830
+ (see the module docstring's "Supported fragment").
1831
+
1832
+ Returns:
1833
+ An :class:`OwlFunctionalResult`. ``doc.to_kb()`` is the FOL image (==
1834
+ ``dl.kb_to_fol(doc.tbox, doc.abox)``), whose ``premises`` /
1835
+ ``tbox_premises`` are what ``api.prove`` wants;
1836
+ ``dl.abox_consistent(doc.tbox and doc.abox)`` is the in-house tableau
1837
+ over the SAME object, and the two must agree.
1838
+
1839
+ Raises:
1840
+ OwlFunctionalSyntaxError: on MALFORMED input only — an unterminated
1841
+ IRI or string literal, an unbalanced paren, a missing
1842
+ ``Ontology(``, a bad name, trailing input. Those are questions
1843
+ about the document rather than about the fragment, and recovering
1844
+ from one could silently drop arbitrary content. Never
1845
+ :class:`OwlFunctionalUnsupportedError`: that class is precisely
1846
+ what this function recovers from, and it is a SUBCLASS, so a
1847
+ caller catching ``OwlFunctionalSyntaxError`` around this call is
1848
+ catching malformed input alone.
1849
+ RecursionError: a class expression nested about a thousand levels deep
1850
+ (Python's default recursion limit). The reader is recursive descent
1851
+ and the recursion is not rewritten; the call fails, it never
1852
+ truncates. Nothing short of a machine-generated document nests that
1853
+ deeply.
1854
+
1855
+ ``accepted`` counts axioms READ. Each literal an accepted axiom carries has
1856
+ a first-order term (``xsd:float``/``xsd:double`` literals and decimals no
1857
+ ``Number`` holds exactly are refused HERE, by their datatype, instead of
1858
+ failing later), so ``to_kb()`` can render every accepted axiom. So is a
1859
+ ``DatatypeDefinition`` that OWL 2 (Structural Specification §9.4) does not
1860
+ allow given the definitions read before it: one that closes a cycle with
1861
+ them, or gives a datatype a second and different definition, is a
1862
+ :class:`RefusedAxiom` with ``keyword == "DatatypeDefinition"`` and the same
1863
+ reason :func:`parse_owl_functional` raises with, and it is not stored (the
1864
+ definitions before it are). An identical repeat of a definition is the same
1865
+ axiom and is accepted. ``to_kb()`` may still refuse the knowledge base as a
1866
+ WHOLE, for what no single axiom shows: one name used both as an object
1867
+ property and a data property or both as a class and a datatype (OWL 2 DL
1868
+ forbids it, and the image has one predicate per name), or
1869
+ ``OwlThing``/``OwlData`` used as a name.
1870
+ """
1871
+ return _Parser(_tokenize(text), text).parse_document_lenient()
1872
+
1873
+
1874
+ def parse_owl_functional_class_expression(text: str) -> Concept:
1875
+ """Parse a single OWL 2 Functional-Style Syntax class expression.
1876
+
1877
+ Round-trips against :func:`to_owl_functional_class_expression` (see the
1878
+ module docstring's "Round-trip guarantee").
1879
+
1880
+ Args:
1881
+ text: A class expression, e.g. ``"ObjectIntersectionOf(Person
1882
+ ObjectSomeValuesFrom(hasChild Doctor))"``.
1883
+
1884
+ Returns:
1885
+ The parsed :class:`~unicode_logic_kit.dl.concepts.Concept`.
1886
+
1887
+ Raises:
1888
+ OwlFunctionalSyntaxError: On malformed input, or on syntax outside
1889
+ ALCHQ (see the module docstring's "Rejected").
1890
+ """
1891
+ parser = _Parser(_tokenize(text), text)
1892
+ ce = parser.parse_class_expression()
1893
+ parser._expect_eof()
1894
+ return ce
1895
+
1896
+
1897
+ # --------------------------------------------------------------------------- #
1898
+ # Renderer: no precedence table needed (see the module docstring's "No
1899
+ # ambiguity, no precedence") -- every compound is 'Keyword(args)'.
1900
+ # --------------------------------------------------------------------------- #
1901
+
1902
+ def _is_one_word(name: str) -> bool:
1903
+ """True iff the tokenizer reads ``name`` as ONE bare WORD that is no keyword
1904
+ and no anonymous-individual label."""
1905
+ try:
1906
+ tokens = _tokenize(name)
1907
+ except OwlFunctionalSyntaxError:
1908
+ return False
1909
+ return (len(tokens) == 2 and tokens[0][0] == "WORD" and tokens[0][1] == name
1910
+ and name not in _NAME_KEYWORDS and not name.startswith("_:"))
1911
+
1912
+
1913
+ @functools.lru_cache(maxsize=8192)
1914
+ def _name_spelling(name: str, kind: str) -> Tuple[Optional[str], str]:
1915
+ """``(spelling, "")`` for the one-token spelling of ``name`` that the reader
1916
+ reads back as exactly that name, or ``(None, reason)`` when there is none.
1917
+
1918
+ A name holding ``://`` is written in angle brackets, as it always was. Any
1919
+ other name is written bare when the tokenizer reads it as one plain word, and
1920
+ in angle brackets otherwise, which carries whitespace, structural characters,
1921
+ keywords and the empty name alike: the reader takes everything up to the
1922
+ first ``>`` for one IRI. That leaves a name with a ``>`` in it with no
1923
+ spelling, and a name that already holds the brackets of a full IRI, since
1924
+ ``<...>`` reads back as the text between them. For a ``kind`` of ``"class"``
1925
+ ``owl:Thing`` and ``owl:Nothing`` read as the top and the bottom class in
1926
+ every spelling, so they have none either. (A class named like a built-in
1927
+ datatype is a different case: the reader REFUSES it, loudly, and it is
1928
+ written as given — see the module docstring's "Round-trip guarantee".)
1929
+ """
1930
+ if kind == "class":
1931
+ try:
1932
+ back = _Parser([], "")._name_to_class_expression(name)
1933
+ except OwlFunctionalSyntaxError:
1934
+ back = Atomic(name) # refused on reading, not another reading
1935
+ if back != Atomic(name):
1936
+ return None, (f"read as a class it is {back!r} in every spelling "
1937
+ f"(owl:Thing and owl:Nothing are the top and the bottom class)")
1938
+ if name[:1] == "<" and name[-1:] == ">" and ">" not in name[1:-1]:
1939
+ return None, ("it already holds the angle brackets of a full IRI, and the "
1940
+ "reader reads <...> as the text between them, which is "
1941
+ "another name")
1942
+ bracketable = ">" not in name
1943
+ if "://" in name and bracketable:
1944
+ return f"<{name}>", ""
1945
+ if _is_one_word(name):
1946
+ return name, ""
1947
+ if bracketable:
1948
+ return f"<{name}>", ""
1949
+ return None, "it holds '>', which ends a full IRI, and a bare name cannot carry it"
1950
+
1951
+
1952
+ def _render_name(name: str, kind: str = "name") -> str:
1953
+ """Render a kit name back as a Functional-Syntax token that reads back as that
1954
+ name: ``<...>`` when it looks like an absolute IRI (contains ``"://"``) or
1955
+ when a bare name would not read back as ONE name (whitespace, a structural
1956
+ character, a keyword, the empty name), bare otherwise -- see the module
1957
+ docstring's "IRIs and names". ``kind`` is ``"class"`` where the reader gives
1958
+ ``owl:Thing`` and ``owl:Nothing`` a meaning of their own, ``"name"`` elsewhere.
1959
+
1960
+ Raises:
1961
+ ValueError: no spelling reads back as ``name`` (it holds ``>``, already
1962
+ holds the brackets of a full IRI, or is ``owl:Thing`` /
1963
+ ``owl:Nothing`` as a class name). The text is never written as
1964
+ something that reads back as another name or expression.
1965
+ """
1966
+ spelling, reason = _name_spelling(name, kind)
1967
+ if spelling is None:
1968
+ raise ValueError(
1969
+ f"to_owl_functional: the name {name!r} cannot be written so that "
1970
+ f"parse_owl_functional reads it back as that name: {reason}. Rename it.")
1971
+ return spelling
1972
+
1973
+
1974
+ # The message shared by both I/O rejections below: unlike parse_owl_functional's
1975
+ # _reject (which points at a byte position in some INPUT TEXT), there is no text
1976
+ # position here -- we are rendering a Concept object tree that was built directly
1977
+ # via dl.InverseRole/dl.Nominal (never round-tripped through this module's own
1978
+ # parser, which already refuses ObjectInverseOf/ObjectOneOf on the way in -- see
1979
+ # _REJECTED_CE). Mirrors the wording of dl.translate.concept_to_modal's own I/O
1980
+ # refusal (_IO_MODAL_MSG) and points the caller at the two things that DO support
1981
+ # I/O: dl.to_manchester (rendering only) and dl.owl_reasoner (deciding).
1982
+ _IO_RENDER_MSG = (
1983
+ "{fn}: {what} is outside ALCHQ (this kit's DL fragment) -- OWL 2 "
1984
+ "Functional-Style Syntax rendering only covers ALCHQ, the same fragment "
1985
+ "this module's parser accepts. Use dl.to_manchester for a diagnostic-only "
1986
+ "rendering that does support inverse roles (I) and nominals (O), or "
1987
+ "dl.owl_reasoner to actually decide a concept that needs them."
1988
+ )
1989
+
1990
+
1991
+ def _plain_role_name(role: Union[str, InverseRole], fn: str) -> str:
1992
+ """``role`` as a plain role NAME, or a loud ``TypeError`` naming ``fn`` if it
1993
+ is an :class:`InverseRole` -- outside ALCHQ, refused by name (see
1994
+ :data:`_IO_RENDER_MSG`).
1995
+
1996
+ The one place the ``str | InverseRole`` union a role field may carry is
1997
+ narrowed for the renderer, so no call site hands :func:`_render_name` (which
1998
+ expects a ``str``) an object it would turn into a low-level, unnamed
1999
+ ``TypeError`` (``InverseRole`` is not iterable) or, worse, a set member
2000
+ ``sorted()`` then chokes on.
2001
+ """
2002
+ if isinstance(role, InverseRole):
2003
+ raise TypeError(_IO_RENDER_MSG.format(
2004
+ fn=fn, what=f"the inverse role {role.role}⁻ (InverseRole)"))
2005
+ return role
2006
+
2007
+
2008
+ def _render_role(role: Union[str, InverseRole],
2009
+ fn: str = "to_owl_functional_class_expression") -> str:
2010
+ """Render a ``role`` field (the ``role`` of :class:`Exists`/:class:`ForAll`/
2011
+ :class:`AtLeast`/:class:`AtMost`, or either side of a role inclusion), which
2012
+ may be a plain role name (``str``) or an :class:`InverseRole` -- outside
2013
+ ALCHQ, refused by name (see :func:`_plain_role_name`).
2014
+ """
2015
+ return _render_name(_plain_role_name(role, fn))
2016
+
2017
+
2018
+ def _render_ce(c: Concept) -> str:
2019
+ if isinstance(c, Top):
2020
+ return "owl:Thing"
2021
+ if isinstance(c, Bottom):
2022
+ return "owl:Nothing"
2023
+ if isinstance(c, Atomic):
2024
+ return _render_name(c.name, "class")
2025
+ if isinstance(c, Nominal):
2026
+ raise TypeError(_IO_RENDER_MSG.format(
2027
+ fn="to_owl_functional_class_expression",
2028
+ what=f"the nominal {{{c.individual}}} (Nominal)"))
2029
+ if isinstance(c, Not):
2030
+ return f"ObjectComplementOf({_render_ce(c.concept)})"
2031
+ if isinstance(c, And):
2032
+ return f"ObjectIntersectionOf({_render_ce(c.left)} {_render_ce(c.right)})"
2033
+ if isinstance(c, Or):
2034
+ return f"ObjectUnionOf({_render_ce(c.left)} {_render_ce(c.right)})"
2035
+ if isinstance(c, Exists):
2036
+ return f"ObjectSomeValuesFrom({_render_role(c.role)} {_render_ce(c.concept)})"
2037
+ if isinstance(c, ForAll):
2038
+ return f"ObjectAllValuesFrom({_render_role(c.role)} {_render_ce(c.concept)})"
2039
+ if isinstance(c, HasValue):
2040
+ return f"ObjectHasValue({_render_role(c.role)} {_render_name(c.individual)})"
2041
+ if isinstance(c, AtLeast):
2042
+ return f"ObjectMinCardinality({c.n} {_render_role(c.role)} {_render_ce(c.concept)})"
2043
+ if isinstance(c, AtMost):
2044
+ return f"ObjectMaxCardinality({c.n} {_render_role(c.role)} {_render_ce(c.concept)})"
2045
+ if isinstance(c, DataExists):
2046
+ return (f"DataSomeValuesFrom({_render_name(c.prop)} "
2047
+ f"{render_datarange_fs(c.datarange, _render_name)})")
2048
+ if isinstance(c, DataForAll):
2049
+ return (f"DataAllValuesFrom({_render_name(c.prop)} "
2050
+ f"{render_datarange_fs(c.datarange, _render_name)})")
2051
+ if isinstance(c, DataHasValue):
2052
+ return (f"DataHasValue({_render_name(c.prop)} "
2053
+ f"{render_literal_fs(c.value, _render_name)})")
2054
+ if isinstance(c, DataAtLeast):
2055
+ return (f"DataMinCardinality({c.n} {_render_name(c.prop)} "
2056
+ f"{render_datarange_fs(c.datarange, _render_name)})")
2057
+ if isinstance(c, DataAtMost):
2058
+ return (f"DataMaxCardinality({c.n} {_render_name(c.prop)} "
2059
+ f"{render_datarange_fs(c.datarange, _render_name)})")
2060
+ raise TypeError(f"to_owl_functional_class_expression: unsupported concept {type(c).__name__}")
2061
+
2062
+
2063
+ def to_owl_functional_class_expression(concept: Concept) -> str:
2064
+ """Render ``concept`` in OWL 2 Functional-Style Syntax, dual to
2065
+ :func:`parse_owl_functional_class_expression`.
2066
+
2067
+ Args:
2068
+ concept: Any ALCHQ :class:`~unicode_logic_kit.dl.concepts.Concept`.
2069
+
2070
+ Returns:
2071
+ The Functional-Syntax rendering, e.g.
2072
+ ``"ObjectSomeValuesFrom(r ObjectIntersectionOf(A B))"``.
2073
+
2074
+ Raises:
2075
+ ~unicode_logic_kit.dl.tableau.RoleExpressionError:
2076
+ a restriction's role is an OWL 2 built-in
2077
+ property name or ``=``/``≠`` — see :func:`to_owl_functional`.
2078
+ ValueError: a name has no spelling that the reader reads back as that
2079
+ name (see "IRIs and names" in the module docstring).
2080
+ """
2081
+ _reject_concept_roles_deep(concept, where="to_owl_functional_class_expression")
2082
+ return _render_ce(concept)
2083
+
2084
+
2085
+ def _render_inclusion_role(role: Union[str, InverseRole]) -> str:
2086
+ """One side of a ``SubObjectPropertyOf`` line: the name, or
2087
+ ``ObjectInverseOf(name)`` — the only position the grammar reads an inverse
2088
+ role in (see ``_Parser._parse_inclusion_role``)."""
2089
+ if isinstance(role, InverseRole):
2090
+ return f"ObjectInverseOf({_render_name(role.role)})"
2091
+ return _render_name(role)
2092
+
2093
+
2094
+ def _render_role_box(tbox: TBox) -> List[str]:
2095
+ """Render ``tbox``'s role box as Functional-Syntax axiom lines.
2096
+
2097
+ Every field of the role box, in the order
2098
+ :data:`~unicode_logic_kit.dl.tableau._AXIOM_KINDS` lists them: a kind the
2099
+ parser reads and this drops is a round-trip bug (see "The axiom-kind table"
2100
+ in :mod:`unicode_logic_kit.dl.tableau`), which is why
2101
+ ``tests/test_dl_route_agreement.py`` checks the round trip per kind.
2102
+
2103
+ Two deliberate read-both/write-one asymmetries, both of which still
2104
+ round-trip to the identical ``TBox``:
2105
+
2106
+ * ``EquivalentObjectProperties`` is NOT re-folded out of the role
2107
+ inclusions it was stored as — two ``SubObjectPropertyOf`` lines come
2108
+ back out, exactly as ``DisjointClasses`` comes back out as ``SubClassOf``
2109
+ lines;
2110
+ * ``DisjointObjectProperties`` is written as one BINARY line per stored
2111
+ pair (legal OWL 2 — the grammar's minimum is two) rather than re-folded
2112
+ into one k-ary line, because the pairs are what the ``TBox`` holds and
2113
+ re-folding could only reconstruct a different-but-equivalent grouping.
2114
+ """
2115
+ lines: List[str] = []
2116
+ for sub_role, super_role in tbox.role_inclusions:
2117
+ # An inverse role is written as OWL 2 spells it, ObjectInverseOf(P) --
2118
+ # the TBox holds `r ⊑ s⁻`, rbox_to_fol renders it, the Hets renderer
2119
+ # writes it, and parse_owl_functional reads it on exactly this
2120
+ # position, so refusing it here broke the round trip with a TypeError.
2121
+ lines.append(f"SubObjectPropertyOf({_render_inclusion_role(sub_role)} "
2122
+ f"{_render_inclusion_role(super_role)})")
2123
+ for role in sorted(tbox.transitive_roles):
2124
+ lines.append(f"TransitiveObjectProperty({_render_name(role)})")
2125
+ for left, right in tbox.disjoint_role_pairs:
2126
+ lines.append(f"DisjointObjectProperties({_render_name(left)} "
2127
+ f"{_render_name(right)})")
2128
+ for role in sorted(tbox.asymmetric_roles):
2129
+ lines.append(f"AsymmetricObjectProperty({_render_name(role)})")
2130
+ for role in sorted(tbox.irreflexive_roles):
2131
+ lines.append(f"IrreflexiveObjectProperty({_render_name(role)})")
2132
+ for role in sorted(tbox.functional_roles):
2133
+ lines.append(f"FunctionalObjectProperty({_render_name(role)})")
2134
+ for p, q in tbox.inverse_role_pairs:
2135
+ lines.append(f"InverseObjectProperties({_render_name(p)} {_render_name(q)})")
2136
+ for role in sorted(tbox.symmetric_roles):
2137
+ lines.append(f"SymmetricObjectProperty({_render_name(role)})")
2138
+ for role in sorted(tbox.reflexive_roles):
2139
+ lines.append(f"ReflexiveObjectProperty({_render_name(role)})")
2140
+ for role in sorted(tbox.inverse_functional_roles):
2141
+ lines.append(f"InverseFunctionalObjectProperty({_render_name(role)})")
2142
+ for chain, super_role in tbox.role_chains:
2143
+ body = " ".join(_render_name(role) for role in chain)
2144
+ lines.append(f"SubObjectPropertyOf(ObjectPropertyChain({body}) "
2145
+ f"{_render_name(super_role)})")
2146
+ for role, filler in tbox.role_domains:
2147
+ lines.append(f"ObjectPropertyDomain({_render_name(role)} "
2148
+ f"{_render_ce(filler)})")
2149
+ for role, filler in tbox.role_ranges:
2150
+ lines.append(f"ObjectPropertyRange({_render_name(role)} "
2151
+ f"{_render_ce(filler)})")
2152
+ return lines
2153
+
2154
+
2155
+ def _render_data_box(tbox: TBox) -> List[str]:
2156
+ """Render ``tbox``'s data box as Functional-Syntax axiom lines, in the order
2157
+ :data:`~unicode_logic_kit.dl.tableau._AXIOM_KINDS` lists the kinds. Same two
2158
+ read-both/write-one asymmetries as :func:`_render_role_box`:
2159
+ ``EquivalentDataProperties`` comes back out as the two inclusions it was
2160
+ stored as, and ``DisjointDataProperties`` as one binary line per stored pair.
2161
+ """
2162
+ lines: List[str] = []
2163
+ for sub_prop, super_prop in tbox.data_property_inclusions:
2164
+ lines.append(f"SubDataPropertyOf({_render_name(sub_prop)} "
2165
+ f"{_render_name(super_prop)})")
2166
+ for left, right in tbox.disjoint_data_property_pairs:
2167
+ lines.append(f"DisjointDataProperties({_render_name(left)} "
2168
+ f"{_render_name(right)})")
2169
+ for prop in sorted(tbox.functional_data_properties):
2170
+ lines.append(f"FunctionalDataProperty({_render_name(prop)})")
2171
+ for prop, filler in tbox.data_property_domains:
2172
+ lines.append(f"DataPropertyDomain({_render_name(prop)} {_render_ce(filler)})")
2173
+ for prop, datarange in tbox.data_property_ranges:
2174
+ lines.append(f"DataPropertyRange({_render_name(prop)} "
2175
+ f"{render_datarange_fs(datarange, _render_name)})")
2176
+ for name, datarange in tbox.datatype_definitions:
2177
+ lines.append(f"DatatypeDefinition({_render_name(name)} "
2178
+ f"{render_datarange_fs(datarange, _render_name)})")
2179
+ return lines
2180
+
2181
+
2182
+ def _role_box_role_names(tbox: TBox) -> Set[str]:
2183
+ """Every role name ``tbox``'s role box mentions (for the
2184
+ ``Declaration(...)`` block — see :func:`_collect_names`).
2185
+
2186
+ Driven field by field over the SAME fields :func:`_render_role_box` writes,
2187
+ so a role that appears only in, say, a property chain is still declared.
2188
+ """
2189
+ roles: Set[str] = set()
2190
+ for sub_role, super_role in tbox.role_inclusions:
2191
+ for role in (sub_role, super_role):
2192
+ roles.add(role.role if isinstance(role, InverseRole) else role)
2193
+ for pair in tbox.inverse_role_pairs + tbox.disjoint_role_pairs:
2194
+ roles.update(pair)
2195
+ for chain, super_role in tbox.role_chains:
2196
+ roles.update(chain)
2197
+ roles.add(super_role)
2198
+ for role, _filler in tbox.role_domains + tbox.role_ranges:
2199
+ roles.add(role) # the FILLER's names are walked by
2200
+ # _collect_names, which has the
2201
+ # classes/individuals sets too
2202
+ for names in (tbox.transitive_roles, tbox.symmetric_roles,
2203
+ tbox.asymmetric_roles, tbox.reflexive_roles,
2204
+ tbox.irreflexive_roles, tbox.functional_roles,
2205
+ tbox.inverse_functional_roles):
2206
+ roles.update(names)
2207
+ return roles
2208
+
2209
+
2210
+ def _render_axioms(tbox: TBox) -> List[str]:
2211
+ """Render ``tbox``'s concept-level axioms as ``SubClassOf``/
2212
+ ``EquivalentClasses`` lines.
2213
+
2214
+ Named for the TBox, not for ``tbox.inclusions``: every axiom kind the
2215
+ parser reads, the writer must write, or the round-trip guarantee is a lie
2216
+ (see "The axiom-kind table" in :mod:`unicode_logic_kit.dl.tableau`), so this
2217
+ is where a TBox axiom kind other than the two below is rendered as well.
2218
+
2219
+ Recognises the exact adjacent-pair shape ``TBox.add_equivalence`` produces
2220
+ (``(c, d)`` immediately followed by ``(d, c)``) and re-folds it into one
2221
+ ``EquivalentClasses(c d)`` line; every other inclusion is a plain
2222
+ ``SubClassOf`` (see the module docstring's "EquivalentClasses and
2223
+ DisjointClasses").
2224
+ """
2225
+ lines: List[str] = []
2226
+ incs = tbox.inclusions
2227
+ i, n = 0, len(incs)
2228
+ while i < n:
2229
+ c, d = incs[i]
2230
+ if i + 1 < n and incs[i + 1] == (d, c) and c != d:
2231
+ lines.append(f"EquivalentClasses({_render_ce(c)} {_render_ce(d)})")
2232
+ i += 2
2233
+ else:
2234
+ lines.append(f"SubClassOf({_render_ce(c)} {_render_ce(d)})")
2235
+ i += 1
2236
+ return lines
2237
+
2238
+
2239
+ def _walk_class_expression_names(c: Concept, classes: Set[str], roles: Set[str],
2240
+ individuals: Set[str]) -> None:
2241
+ """Collect every ``Atomic`` name into ``classes``, every role name into
2242
+ ``roles`` and every INDIVIDUAL name into ``individuals``, recursively (used
2243
+ by :func:`_collect_names` for the ``Declaration(...)`` block -- see
2244
+ :func:`to_owl_functional`).
2245
+
2246
+ ``individuals`` is an out-parameter because a class expression CAN name one:
2247
+ a HasValue's filler. Without it, an individual that occurs ONLY inside a
2248
+ TBox class expression -- which is every ObjectHasValue filler, 95 axioms'
2249
+ worth on the OEO ontology -- would get no
2250
+ ``Declaration(NamedIndividual(...))`` line.
2251
+ """
2252
+ if isinstance(c, Atomic):
2253
+ classes.add(c.name)
2254
+ elif isinstance(c, (Top, Bottom)):
2255
+ pass
2256
+ elif isinstance(c, Nominal):
2257
+ raise TypeError(_IO_RENDER_MSG.format(
2258
+ fn="to_owl_functional", what=f"the nominal {{{c.individual}}} (Nominal)"))
2259
+ elif isinstance(c, HasValue):
2260
+ if isinstance(c.role, InverseRole):
2261
+ raise TypeError(_IO_RENDER_MSG.format(
2262
+ fn="to_owl_functional",
2263
+ what=f"the inverse role {c.role.role}⁻ (InverseRole)"))
2264
+ roles.add(c.role)
2265
+ individuals.add(c.individual)
2266
+ elif isinstance(c, Not):
2267
+ _walk_class_expression_names(c.concept, classes, roles, individuals)
2268
+ elif isinstance(c, (And, Or)):
2269
+ _walk_class_expression_names(c.left, classes, roles, individuals)
2270
+ _walk_class_expression_names(c.right, classes, roles, individuals)
2271
+ elif isinstance(c, DATA_CONCEPTS):
2272
+ # A data restriction names a DATA property and a data range, neither of
2273
+ # which is a class, an object role or an individual; they are collected
2274
+ # from the knowledge base's whole vocabulary by to_owl_functional.
2275
+ pass
2276
+ elif isinstance(c, (Exists, ForAll, AtLeast, AtMost)):
2277
+ # c.role may be an InverseRole (see _render_role's docstring) -- caught
2278
+ # HERE, before it is ever added to the `roles: Set[str]` collected for
2279
+ # the Declaration(...) block, so a mixed/singleton set of roles never
2280
+ # reaches _collect_names's sorted(roles) with a non-str member (which
2281
+ # would otherwise raise its own low-level, unnamed TypeError there).
2282
+ if isinstance(c.role, InverseRole):
2283
+ raise TypeError(_IO_RENDER_MSG.format(
2284
+ fn="to_owl_functional",
2285
+ what=f"the inverse role {c.role.role}⁻ (InverseRole)"))
2286
+ roles.add(c.role)
2287
+ _walk_class_expression_names(c.concept, classes, roles, individuals)
2288
+ else:
2289
+ raise TypeError(f"to_owl_functional: unsupported concept {type(c).__name__}")
2290
+
2291
+
2292
+ def _collect_names(tbox: TBox, abox: ABox) -> Tuple[List[str], List[str], List[str]]:
2293
+ """Every atomic-class, role, and individual name referenced anywhere in
2294
+ ``tbox``/``abox``, each as a sorted, de-duplicated list (sorted for
2295
+ deterministic output -- see :func:`to_owl_functional`).
2296
+ """
2297
+ classes: Set[str] = set()
2298
+ roles: Set[str] = set()
2299
+ individuals: Set[str] = set()
2300
+
2301
+ for sub, sup in tbox.inclusions:
2302
+ _walk_class_expression_names(sub, classes, roles, individuals)
2303
+ _walk_class_expression_names(sup, classes, roles, individuals)
2304
+ roles.update(_role_box_role_names(tbox))
2305
+ for _role, filler in tbox.role_domains + tbox.role_ranges:
2306
+ _walk_class_expression_names(filler, classes, roles, individuals)
2307
+ for _prop, filler in tbox.data_property_domains:
2308
+ _walk_class_expression_names(filler, classes, roles, individuals)
2309
+ for individual, concept in abox.concept_assertions:
2310
+ individuals.add(individual)
2311
+ _walk_class_expression_names(concept, classes, roles, individuals)
2312
+ for a, b, role in abox.role_assertions:
2313
+ individuals.add(a)
2314
+ individuals.add(b)
2315
+ roles.add(role)
2316
+ for a, b in abox.distinct_assertions + abox.same_assertions:
2317
+ individuals.add(a)
2318
+ individuals.add(b)
2319
+ for a, b, role in abox.negative_role_assertions:
2320
+ individuals.add(a)
2321
+ individuals.add(b)
2322
+ roles.add(role)
2323
+ for individual, _prop, _value in abox.data_assertions + abox.negative_data_assertions:
2324
+ individuals.add(individual)
2325
+
2326
+ return sorted(classes), sorted(roles), sorted(individuals)
2327
+
2328
+
2329
+ def _reject_unwritable_roles(tbox: TBox, abox: ABox, fn: str) -> None:
2330
+ """The roles ``fn`` could not write back to a document the reader accepts:
2331
+ the stored role box and data box must be well-formed (the validation the
2332
+ tableau and the FOL image run), and no assertion or class expression may
2333
+ carry an OWL 2 built-in property name, ``=`` or ``≠``."""
2334
+ _validate_role_box(tbox, where=fn)
2335
+ _validate_data_box(tbox, where=fn)
2336
+ _reject_abox_roles(abox, where=fn)
2337
+ fillers = _tbox_class_expressions(tbox) + [c for _, c in tbox.data_property_domains]
2338
+ for _individual, concept in abox.concept_assertions:
2339
+ fillers.append(concept)
2340
+ for concept in fillers:
2341
+ _reject_concept_roles_deep(concept, where=fn)
2342
+
2343
+
2344
+ def to_owl_functional(tbox: TBox, abox: ABox, ontology_iri: str = "") -> str:
2345
+ """Render ``(tbox, abox)`` as one OWL 2 Functional-Style Syntax ``Ontology(...)``
2346
+ document, dual to :func:`parse_owl_functional`.
2347
+
2348
+ Writes a ``Declaration(Class(...)/ObjectProperty(.../NamedIndividual(...))``
2349
+ block for every atomic-class/role/individual name referenced anywhere in
2350
+ ``tbox``/``abox`` (see :func:`_collect_names`), then the whole role box
2351
+ (see :func:`_render_role_box`), then the TBox's concept inclusions (as
2352
+ ``SubClassOf``/``EquivalentClasses`` — see :func:`_render_axioms`), then
2353
+ the ABox's assertions (``ClassAssertion``, ``ObjectPropertyAssertion``,
2354
+ ``DifferentIndividuals``).
2355
+
2356
+ Args:
2357
+ tbox: The TBox to render.
2358
+ abox: The ABox to render.
2359
+ ontology_iri: An optional ontology IRI for the ``Ontology(<iri> ...)``
2360
+ header; omitted entirely (``Ontology(...)``, no IRI argument) when
2361
+ empty (the default). Discarded on the way back in by
2362
+ :func:`parse_owl_functional`, exactly like a ``Prefix(...)`` line
2363
+ — this module carries no ontology-metadata state (see the module
2364
+ docstring's "Document shell").
2365
+
2366
+ Returns:
2367
+ The Functional-Syntax document text.
2368
+
2369
+ Raises:
2370
+ ~unicode_logic_kit.dl.tableau.RoleExpressionError:
2371
+ a stored role is no usable name, or an OWL 2
2372
+ built-in property name (``owl:topObjectProperty`` …) or ``=``/``≠``
2373
+ is the role of a restriction or an assertion — written as an
2374
+ ordinary role it would read back as a refusal
2375
+ (:func:`parse_owl_functional` refuses the name in every such
2376
+ position), and a document this module wrote must be one it reads.
2377
+ TypeError: an inverse role anywhere BUT either side of a role inclusion,
2378
+ or a nominal (outside ALCHQ).
2379
+ ValueError: a class, role, property, datatype or individual name has no
2380
+ spelling that :func:`parse_owl_functional` reads back as that name
2381
+ (see "IRIs and names" in the module docstring).
2382
+ """
2383
+ _reject_unwritable_roles(tbox, abox, "to_owl_functional")
2384
+ class_names, role_names, individual_names = _collect_names(tbox, abox)
2385
+ vocabulary = _collect_vocabulary(tbox, abox)
2386
+ lines: List[str] = []
2387
+ for name in class_names:
2388
+ lines.append(f"Declaration(Class({_render_name(name, 'class')}))")
2389
+ for name in role_names:
2390
+ lines.append(f"Declaration(ObjectProperty({_render_name(name)}))")
2391
+ for name in sorted(vocabulary.data_properties):
2392
+ lines.append(f"Declaration(DataProperty({_render_name(name)}))")
2393
+ # Only the datatypes OWL 2 does not already know: declaring xsd:integer is
2394
+ # not wrong, but a declaration for every built-in a document mentions would
2395
+ # bury the ones that matter (the user-defined datatypes).
2396
+ for name in sorted(vocabulary.datatypes):
2397
+ if not is_builtin_datatype(name):
2398
+ lines.append(f"Declaration(Datatype({_render_name(name)}))")
2399
+ for name in individual_names:
2400
+ lines.append(f"Declaration(NamedIndividual({_render_name(name)}))")
2401
+ lines.extend(_render_role_box(tbox))
2402
+ lines.extend(_render_data_box(tbox))
2403
+ lines.extend(_render_axioms(tbox))
2404
+ for individual, concept in abox.concept_assertions:
2405
+ lines.append(f"ClassAssertion({_render_ce(concept)} {_render_name(individual)})")
2406
+ for a, b, role in abox.role_assertions:
2407
+ lines.append(f"ObjectPropertyAssertion({_render_name(role)} {_render_name(a)} {_render_name(b)})")
2408
+ for a, b in abox.distinct_assertions:
2409
+ lines.append(f"DifferentIndividuals({_render_name(a)} {_render_name(b)})")
2410
+ # One BINARY line per stored pair, for both identity kinds: binary is legal
2411
+ # OWL 2 (the grammar's minimum arity is two), the pairs are what the ABox
2412
+ # holds, and re-folding them into a k-ary line could only reconstruct a
2413
+ # different-but-equivalent grouping -- the same read-many/write-binary
2414
+ # asymmetry DisjointObjectProperties has in _render_role_box.
2415
+ for a, b in abox.same_assertions:
2416
+ lines.append(f"SameIndividual({_render_name(a)} {_render_name(b)})")
2417
+ for a, b, role in abox.negative_role_assertions:
2418
+ lines.append(f"NegativeObjectPropertyAssertion({_render_name(role)} "
2419
+ f"{_render_name(a)} {_render_name(b)})")
2420
+ for individual, prop, value in abox.data_assertions:
2421
+ lines.append(f"DataPropertyAssertion({_render_name(prop)} "
2422
+ f"{_render_name(individual)} "
2423
+ f"{render_literal_fs(value, _render_name)})")
2424
+ for individual, prop, value in abox.negative_data_assertions:
2425
+ lines.append(f"NegativeDataPropertyAssertion({_render_name(prop)} "
2426
+ f"{_render_name(individual)} "
2427
+ f"{render_literal_fs(value, _render_name)})")
2428
+
2429
+ head =f"Ontology(<{ontology_iri}>" if ontology_iri else "Ontology("
2430
+ if not lines:
2431
+ return head + ")"
2432
+ body = "\n".join(f" {line}" for line in lines)
2433
+ return f"{head}\n{body}\n)"