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.
- unicode_logic_kit/__init__.py +385 -0
- unicode_logic_kit/__main__.py +520 -0
- unicode_logic_kit/_deadline.py +219 -0
- unicode_logic_kit/ace/__init__.py +126 -0
- unicode_logic_kit/ace/_align.py +135 -0
- unicode_logic_kit/ace/chem_lexicon.py +128 -0
- unicode_logic_kit/ace/drs_reader.py +570 -0
- unicode_logic_kit/ace/mapping.py +666 -0
- unicode_logic_kit/ace/reverse_modal.py +138 -0
- unicode_logic_kit/ace/runner.py +551 -0
- unicode_logic_kit/ace/translate.py +452 -0
- unicode_logic_kit/ace/verbalize.py +1070 -0
- unicode_logic_kit/api.py +1284 -0
- unicode_logic_kit/atp/__init__.py +177 -0
- unicode_logic_kit/atp/_ascii_names.py +113 -0
- unicode_logic_kit/atp/_html.py +72 -0
- unicode_logic_kit/atp/_substructural_input.py +228 -0
- unicode_logic_kit/atp/_tff_problem.py +715 -0
- unicode_logic_kit/atp/_tptp_problem.py +1111 -0
- unicode_logic_kit/atp/_writer_support.py +289 -0
- unicode_logic_kit/atp/clingo_backend.py +1180 -0
- unicode_logic_kit/atp/cvc5_backend.py +1385 -0
- unicode_logic_kit/atp/eprover_backend.py +732 -0
- unicode_logic_kit/atp/finite_domain.py +1055 -0
- unicode_logic_kit/atp/fitch.py +1547 -0
- unicode_logic_kit/atp/fitch_search.py +551 -0
- unicode_logic_kit/atp/hets_backend.py +339 -0
- unicode_logic_kit/atp/hybrid_down.py +120 -0
- unicode_logic_kit/atp/incremental.py +250 -0
- unicode_logic_kit/atp/kripke_enum.py +741 -0
- unicode_logic_kit/atp/lambek.py +436 -0
- unicode_logic_kit/atp/leo3_backend.py +332 -0
- unicode_logic_kit/atp/linear.py +738 -0
- unicode_logic_kit/atp/lj.py +705 -0
- unicode_logic_kit/atp/logic_backends.py +566 -0
- unicode_logic_kit/atp/ltl_tableau.py +1084 -0
- unicode_logic_kit/atp/minizinc_backend.py +1402 -0
- unicode_logic_kit/atp/modal_tableau.py +1382 -0
- unicode_logic_kit/atp/nanocop_backend.py +410 -0
- unicode_logic_kit/atp/portfolio.py +489 -0
- unicode_logic_kit/atp/protocol.py +1803 -0
- unicode_logic_kit/atp/prover9_entailment.py +1153 -0
- unicode_logic_kit/atp/resolution.py +1376 -0
- unicode_logic_kit/atp/resolution_check.py +1114 -0
- unicode_logic_kit/atp/sequent.py +1050 -0
- unicode_logic_kit/atp/tableau.py +921 -0
- unicode_logic_kit/atp/tableau_check.py +543 -0
- unicode_logic_kit/atp/tptp_ncl.py +811 -0
- unicode_logic_kit/atp/tptp_tff.py +1546 -0
- unicode_logic_kit/atp/tstp.py +1333 -0
- unicode_logic_kit/atp/tstp_check.py +1096 -0
- unicode_logic_kit/atp/twee_backend.py +236 -0
- unicode_logic_kit/atp/twee_check.py +711 -0
- unicode_logic_kit/atp/twee_entailment.py +953 -0
- unicode_logic_kit/atp/vampire_entailment.py +540 -0
- unicode_logic_kit/atp/z3_arith.py +470 -0
- unicode_logic_kit/atp/z3_equivalence.py +36 -0
- unicode_logic_kit/atp/z3_fuzzy.py +362 -0
- unicode_logic_kit/atp/z3_input.py +500 -0
- unicode_logic_kit/atp/z3_models.py +208 -0
- unicode_logic_kit/chem/__init__.py +88 -0
- unicode_logic_kit/chem/_naming.py +284 -0
- unicode_logic_kit/chem/cache.py +185 -0
- unicode_logic_kit/chem/interop.py +244 -0
- unicode_logic_kit/chem/mol.py +525 -0
- unicode_logic_kit/chem/signature.py +112 -0
- unicode_logic_kit/comorphism.py +497 -0
- unicode_logic_kit/dl/__init__.py +384 -0
- unicode_logic_kit/dl/classification.py +227 -0
- unicode_logic_kit/dl/concepts.py +632 -0
- unicode_logic_kit/dl/datatypes.py +818 -0
- unicode_logic_kit/dl/owl_functional.py +2433 -0
- unicode_logic_kit/dl/owl_manchester.py +1637 -0
- unicode_logic_kit/dl/owl_reasoner.py +790 -0
- unicode_logic_kit/dl/parser.py +391 -0
- unicode_logic_kit/dl/tableau.py +4048 -0
- unicode_logic_kit/dl/translate.py +2704 -0
- unicode_logic_kit/drt/__init__.py +94 -0
- unicode_logic_kit/drt/export.py +179 -0
- unicode_logic_kit/drt/nodes.py +506 -0
- unicode_logic_kit/drt/parser.py +965 -0
- unicode_logic_kit/drt/resolve.py +195 -0
- unicode_logic_kit/drt/reverse.py +175 -0
- unicode_logic_kit/eval/__init__.py +106 -0
- unicode_logic_kit/eval/batch.py +382 -0
- unicode_logic_kit/eval/canonical.py +663 -0
- unicode_logic_kit/eval/chem_batch.py +606 -0
- unicode_logic_kit/eval/converses.py +200 -0
- unicode_logic_kit/eval/datasets/__init__.py +136 -0
- unicode_logic_kit/eval/datasets/_base.py +263 -0
- unicode_logic_kit/eval/datasets/_proofwriter_proof.py +422 -0
- unicode_logic_kit/eval/datasets/c3po.py +678 -0
- unicode_logic_kit/eval/datasets/folio.py +158 -0
- unicode_logic_kit/eval/datasets/fracas.py +418 -0
- unicode_logic_kit/eval/datasets/groves.py +191 -0
- unicode_logic_kit/eval/datasets/logicbench.py +467 -0
- unicode_logic_kit/eval/datasets/logicnli.py +303 -0
- unicode_logic_kit/eval/datasets/malls.py +133 -0
- unicode_logic_kit/eval/datasets/pfolio.py +594 -0
- unicode_logic_kit/eval/datasets/pmb.py +242 -0
- unicode_logic_kit/eval/datasets/prontoqa.py +611 -0
- unicode_logic_kit/eval/datasets/proofwriter.py +1431 -0
- unicode_logic_kit/eval/datasets/proverqa.py +674 -0
- unicode_logic_kit/eval/datasets/willow.py +478 -0
- unicode_logic_kit/eval/equivalence.py +466 -0
- unicode_logic_kit/eval/exercise_gen.py +533 -0
- unicode_logic_kit/eval/explain.py +791 -0
- unicode_logic_kit/eval/generality.py +750 -0
- unicode_logic_kit/eval/metric_hf.py +458 -0
- unicode_logic_kit/eval/predicate_match.py +343 -0
- unicode_logic_kit/eval/theory_check.py +1170 -0
- unicode_logic_kit/eval/validate.py +306 -0
- unicode_logic_kit/fol/__init__.py +177 -0
- unicode_logic_kit/fol/_atom_keys.py +510 -0
- unicode_logic_kit/fol/_fol_nodes.py +3586 -0
- unicode_logic_kit/fol/_free_parameters.py +105 -0
- unicode_logic_kit/fol/_ho_nodes.py +448 -0
- unicode_logic_kit/fol/_hybrid_nodes.py +308 -0
- unicode_logic_kit/fol/_identifiers.py +1091 -0
- unicode_logic_kit/fol/_lambek_nodes.py +112 -0
- unicode_logic_kit/fol/_linear_nodes.py +352 -0
- unicode_logic_kit/fol/_modal_nodes.py +1467 -0
- unicode_logic_kit/fol/_msfl_nodes.py +2196 -0
- unicode_logic_kit/fol/_numeral_symbols.py +231 -0
- unicode_logic_kit/fol/_so_nodes.py +200 -0
- unicode_logic_kit/fol/_symbol_names.py +81 -0
- unicode_logic_kit/fol/_team_nodes.py +181 -0
- unicode_logic_kit/fol/_tptp_symbols.py +551 -0
- unicode_logic_kit/fol/_truth_constants.py +117 -0
- unicode_logic_kit/fol/casl_export.py +1135 -0
- unicode_logic_kit/fol/casl_import.py +929 -0
- unicode_logic_kit/fol/derivation.py +367 -0
- unicode_logic_kit/fol/dialect_detect.py +70 -0
- unicode_logic_kit/fol/dialect_repair.py +537 -0
- unicode_logic_kit/fol/frames.py +637 -0
- unicode_logic_kit/fol/grammars/terminals.lark +31 -0
- unicode_logic_kit/fol/lambda_tools.py +297 -0
- unicode_logic_kit/fol/latex_input.py +429 -0
- unicode_logic_kit/fol/modal_translation.py +944 -0
- unicode_logic_kit/fol/msflparser.py +1033 -0
- unicode_logic_kit/fol/naming.py +422 -0
- unicode_logic_kit/fol/nodes.py +241 -0
- unicode_logic_kit/fol/normalforms.py +492 -0
- unicode_logic_kit/fol/pal.py +287 -0
- unicode_logic_kit/fol/prolog_export.py +566 -0
- unicode_logic_kit/fol/prolog_input.py +505 -0
- unicode_logic_kit/fol/prover9_input.py +1325 -0
- unicode_logic_kit/fol/qml.py +1760 -0
- unicode_logic_kit/fol/qmltp_input.py +525 -0
- unicode_logic_kit/fol/sanitize.py +221 -0
- unicode_logic_kit/fol/serialize.py +79 -0
- unicode_logic_kit/fol/signature.py +1290 -0
- unicode_logic_kit/fol/simplify_check.py +544 -0
- unicode_logic_kit/fol/spans.py +594 -0
- unicode_logic_kit/fol/tptp_input.py +1503 -0
- unicode_logic_kit/fol/tptp_repair.py +941 -0
- unicode_logic_kit/fol/unification.py +157 -0
- unicode_logic_kit/fol/verbalize.py +263 -0
- unicode_logic_kit/hets/__init__.py +163 -0
- unicode_logic_kit/hets/bridge.py +142 -0
- unicode_logic_kit/hets/client.py +748 -0
- unicode_logic_kit/hets/docker.py +420 -0
- unicode_logic_kit/hets/dol.py +712 -0
- unicode_logic_kit/hets/haskell_json.py +355 -0
- unicode_logic_kit/hets/owl_backend.py +794 -0
- unicode_logic_kit/hets/owl_cli.py +598 -0
- unicode_logic_kit/hets/symbols.py +512 -0
- unicode_logic_kit/hol/__init__.py +140 -0
- unicode_logic_kit/hol/_ho_common.py +323 -0
- unicode_logic_kit/hol/_isabelle_binders.py +125 -0
- unicode_logic_kit/hol/classical.py +812 -0
- unicode_logic_kit/hol/deepshallow/__init__.py +45 -0
- unicode_logic_kit/hol/deepshallow/_common.py +177 -0
- unicode_logic_kit/hol/deepshallow/conditional.py +225 -0
- unicode_logic_kit/hol/deepshallow/intuitionistic.py +181 -0
- unicode_logic_kit/hol/deepshallow/modal.py +217 -0
- unicode_logic_kit/hol/deepshallow/qml.py +406 -0
- unicode_logic_kit/hol/deepshallow/relevant.py +206 -0
- unicode_logic_kit/hol/free.py +753 -0
- unicode_logic_kit/hol/goedel.py +336 -0
- unicode_logic_kit/hol/ho_modal.py +1743 -0
- unicode_logic_kit/hol/intuitionistic.py +403 -0
- unicode_logic_kit/hol/isabelle_conditional.py +593 -0
- unicode_logic_kit/hol/isabelle_modal.py +1908 -0
- unicode_logic_kit/hol/isabelle_relevant.py +412 -0
- unicode_logic_kit/hol/isabelle_runner.py +1147 -0
- unicode_logic_kit/hol/isabelle_substructural.py +884 -0
- unicode_logic_kit/hol/lean.py +1018 -0
- unicode_logic_kit/hol/manyvalued.py +921 -0
- unicode_logic_kit/hol/secondorder.py +687 -0
- unicode_logic_kit/hol/thf_modal.py +941 -0
- unicode_logic_kit/hol/thirdorder.py +397 -0
- unicode_logic_kit/ilp/__init__.py +89 -0
- unicode_logic_kit/ilp/readback.py +389 -0
- unicode_logic_kit/ilp/separation.py +153 -0
- unicode_logic_kit/ilp/task.py +730 -0
- unicode_logic_kit/logic.py +163 -0
- unicode_logic_kit/mcp/__init__.py +28 -0
- unicode_logic_kit/mcp/__main__.py +5 -0
- unicode_logic_kit/mcp/chem_tools.py +1031 -0
- unicode_logic_kit/mcp/server.py +2453 -0
- unicode_logic_kit/mcp/syntax_spec.py +681 -0
- unicode_logic_kit/prob/__init__.py +53 -0
- unicode_logic_kit/prob/_bdd.py +225 -0
- unicode_logic_kit/prob/_column_gen.py +668 -0
- unicode_logic_kit/prob/distribution.py +686 -0
- unicode_logic_kit/prob/nilsson.py +470 -0
- unicode_logic_kit/py.typed +0 -0
- unicode_logic_kit/semantics/__init__.py +137 -0
- unicode_logic_kit/semantics/_modal_reject.py +156 -0
- unicode_logic_kit/semantics/action_models.py +466 -0
- unicode_logic_kit/semantics/asp_models.py +1200 -0
- unicode_logic_kit/semantics/conditional.py +580 -0
- unicode_logic_kit/semantics/dynamic_epistemic.py +95 -0
- unicode_logic_kit/semantics/free_logic.py +913 -0
- unicode_logic_kit/semantics/fuzzy.py +384 -0
- unicode_logic_kit/semantics/fuzzy_kripke.py +442 -0
- unicode_logic_kit/semantics/intuitionistic.py +581 -0
- unicode_logic_kit/semantics/kripke.py +1139 -0
- unicode_logic_kit/semantics/manyvalued.py +580 -0
- unicode_logic_kit/semantics/matrix.py +342 -0
- unicode_logic_kit/semantics/model_eval.py +1135 -0
- unicode_logic_kit/semantics/modelfinder.py +1036 -0
- unicode_logic_kit/semantics/nonmonotonic.py +372 -0
- unicode_logic_kit/semantics/relevant.py +331 -0
- unicode_logic_kit/semantics/secondorder.py +657 -0
- unicode_logic_kit/semantics/structures.py +352 -0
- unicode_logic_kit/semantics/tarski.py +975 -0
- unicode_logic_kit/semantics/team.py +315 -0
- unicode_logic_kit/semantics/team_translation.py +416 -0
- unicode_logic_kit/semantics/thirdorder.py +358 -0
- unicode_logic_kit/semantics/tnorm.py +85 -0
- unicode_logic_kit/semantics/truthtable.py +201 -0
- unicode_logic_kit-0.31.0.dist-info/METADATA +333 -0
- unicode_logic_kit-0.31.0.dist-info/RECORD +237 -0
- unicode_logic_kit-0.31.0.dist-info/WHEEL +4 -0
- 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)"
|