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,505 @@
|
|
|
1
|
+
"""Prolog / Datalog clauses -> kit AST.
|
|
2
|
+
|
|
3
|
+
The missing leg of the importer family (TPTP, Prover9, SMT-LIB, LaTeX, CASL).
|
|
4
|
+
Its immediate use is reading back what a rule learner (an ILP system such as
|
|
5
|
+
Popper or Aleph, or a hand-written Prolog program) produced, so the result can
|
|
6
|
+
be checked against a structure, proved with, exported to TPTP, or compared
|
|
7
|
+
against a reference formula — instead of being eyeballed.
|
|
8
|
+
|
|
9
|
+
**Two readings, and the caller must choose.** A definite clause
|
|
10
|
+
|
|
11
|
+
amide(A) :- carbon(C), nitrogen(N), single_bond(C, N), in(A, C).
|
|
12
|
+
|
|
13
|
+
says two different things depending on what you want from it:
|
|
14
|
+
|
|
15
|
+
``mode="clause"`` (default) — the standard logical reading of the clause:
|
|
16
|
+
every variable universally quantified over the whole implication,
|
|
17
|
+
|
|
18
|
+
∀a ∀c ∀n (Carbon(c) ∧ Nitrogen(n) ∧ Single_bond(c, n) ∧ In(a, c) → Amide(a))
|
|
19
|
+
|
|
20
|
+
``mode="body"`` — only the CONDITION, with the variables that occur solely in
|
|
21
|
+
the body existentially closed and the head's own variables left free:
|
|
22
|
+
|
|
23
|
+
∃c ∃n (Carbon(c) ∧ Nitrogen(n) ∧ Single_bond(c, n) ∧ In(a, c))
|
|
24
|
+
|
|
25
|
+
That second form is what a *class definition* is: the property a thing must
|
|
26
|
+
have, ready to model-check against one structure. The two are not the same
|
|
27
|
+
formula and this module will not guess which you meant — the same discipline
|
|
28
|
+
:func:`unicode_logic_kit.fol.tptp_repair.repair_tptp_formula` applies to a free
|
|
29
|
+
variable it could close two ways.
|
|
30
|
+
|
|
31
|
+
(Body-only variables are existentially closed in ``"body"`` mode and
|
|
32
|
+
universally closed in ``"clause"`` mode, and both are correct: for a variable
|
|
33
|
+
occurring only in the antecedent, ``∀x (P(x) → Q)`` and ``(∃x P(x)) → Q`` are
|
|
34
|
+
logically equivalent. ``"clause"`` mode emits the prenex form because that is
|
|
35
|
+
the shape every prover expects.)
|
|
36
|
+
|
|
37
|
+
**Negation as failure is not classical negation.** ``\\+ G`` succeeds when
|
|
38
|
+
Prolog fails to prove ``G``, which coincides with ``¬G`` only under the closed
|
|
39
|
+
world assumption on a stratified program. Reading it as ``¬`` silently would
|
|
40
|
+
turn "not derivable here" into "false everywhere", so it is REFUSED unless the
|
|
41
|
+
caller opts in with ``negation_as_failure="classical"`` and thereby states
|
|
42
|
+
that the assumption holds for their program.
|
|
43
|
+
|
|
44
|
+
**Naming is inverted on import**, exactly as in
|
|
45
|
+
:mod:`unicode_logic_kit.fol.tptp_input`: Prolog spells a predicate lower-case
|
|
46
|
+
and a variable upper-case, the kit does the opposite, so ``carbon(A)`` becomes
|
|
47
|
+
``Carbon(a)``. A quoted atom (``'1,2-diacyl'``) keeps its exact characters,
|
|
48
|
+
which may not be a legal kit token — run
|
|
49
|
+
:func:`unicode_logic_kit.fol.sanitize_names` over the result before rendering it
|
|
50
|
+
back to text, the same route the TPTP importer documents.
|
|
51
|
+
|
|
52
|
+
**The accepted fragment**, and nothing beyond it: facts, definite and normal
|
|
53
|
+
clauses, bodies joined by ``,`` and ``;``, ``\\+`` (opt-in), compound terms,
|
|
54
|
+
numbers, and the anonymous variable ``_`` (each occurrence a fresh variable).
|
|
55
|
+
Lists, operators (``is``, ``=..``, arithmetic comparison), the cut ``!``, and
|
|
56
|
+
if-then ``->`` are REFUSED with a message naming what was found — a parser
|
|
57
|
+
that quietly dropped a cut would change what the program means.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
import re
|
|
61
|
+
from typing import List, Optional, Tuple
|
|
62
|
+
|
|
63
|
+
from ._fol_nodes import _numeral_from_text
|
|
64
|
+
from .naming import ParsingError
|
|
65
|
+
from .nodes import (
|
|
66
|
+
Node, Variable, Constant, Number, Function, Atom, Not, And, Or, Implies,
|
|
67
|
+
Quantifier, free_variables,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
__all__ = [
|
|
71
|
+
"PrologParsingError", "parse_prolog_clause", "parse_prolog_program",
|
|
72
|
+
"load_prolog",
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class PrologParsingError(ParsingError):
|
|
77
|
+
"""A Prolog import failure carrying a plain message (subclasses
|
|
78
|
+
:class:`~unicode_logic_kit.fol.naming.ParsingError`, like every other
|
|
79
|
+
importer's error type).
|
|
80
|
+
|
|
81
|
+
The base class builds its message from a Lark exception; this importer has
|
|
82
|
+
no Lark parser to hand one, so the constructor is overridden to take the
|
|
83
|
+
message directly — the same shape
|
|
84
|
+
:class:`~unicode_logic_kit.fol.prover9_input.Prover9ParsingError` uses, for
|
|
85
|
+
the same reason.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
def __init__(self, message: str):
|
|
89
|
+
self.args = (message,)
|
|
90
|
+
|
|
91
|
+
def __str__(self) -> str:
|
|
92
|
+
return self.args[0]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
#: Constructs this module refuses rather than approximates, mapped to the
|
|
96
|
+
#: reason. Each would change the meaning of a clause if silently dropped or
|
|
97
|
+
#: guessed at, which is the one thing an importer must never do.
|
|
98
|
+
_REFUSED = {
|
|
99
|
+
"!": "the cut (!) — a control construct with no truth-conditional reading",
|
|
100
|
+
"->": "if-then (->) — Prolog's is a committed choice, not material "
|
|
101
|
+
"implication, and the two disagree when the condition fails",
|
|
102
|
+
"is": "arithmetic evaluation (is) — a computation, not a relation",
|
|
103
|
+
"=..": "the univ operator (=..) — term reflection has no first-order form",
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
_VAR_RE = re.compile(r"^(?:[A-Z_][A-Za-z0-9_]*)$")
|
|
107
|
+
_ATOM_RE = re.compile(r"^[a-z][A-Za-z0-9_]*$")
|
|
108
|
+
_NUMBER_RE = re.compile(r"^-?\d+(?:\.\d+)?$")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _cap(name: str) -> str:
|
|
112
|
+
"""Capitalise the first character — the kit's predicate spelling.
|
|
113
|
+
|
|
114
|
+
Mirrors :func:`unicode_logic_kit.fol.tptp_input._cap` rather than importing
|
|
115
|
+
it, so the two importers' conventions are visibly the same rule and a
|
|
116
|
+
change to either shows up as a failing round-trip test rather than as a
|
|
117
|
+
silent divergence.
|
|
118
|
+
"""
|
|
119
|
+
return name[:1].upper() + name[1:] if name else name
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _lower(name: str) -> str:
|
|
123
|
+
"""Lower-case the first character — the kit's variable spelling."""
|
|
124
|
+
return name[:1].lower() + name[1:] if name else name
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# ---------------------------------------------------------------------------
|
|
128
|
+
# Tokeniser / reader
|
|
129
|
+
# ---------------------------------------------------------------------------
|
|
130
|
+
#
|
|
131
|
+
# A hand-written reader rather than a Lark grammar, for one reason: Prolog's
|
|
132
|
+
# surface syntax is operator-driven and a grammar that accepted "enough of it"
|
|
133
|
+
# would accept operators this module has no reading for. Reading the exact
|
|
134
|
+
# fragment by hand makes every refusal explicit and located.
|
|
135
|
+
|
|
136
|
+
_TOKEN_RE = re.compile(r"""
|
|
137
|
+
(?P<quoted>'(?:\\.|[^'\\])*') # 'quoted atom'
|
|
138
|
+
| (?P<neg>\\\+) # \+ (negation as failure)
|
|
139
|
+
| (?P<neck>:-) # :-
|
|
140
|
+
| (?P<arrow>->) # -> (refused; tokenised so the
|
|
141
|
+
# refusal message can name it)
|
|
142
|
+
| (?P<univ>=\.\.) # =..
|
|
143
|
+
| (?P<punct>[(),.;\[\]|!]) # structural
|
|
144
|
+
| (?P<name>[A-Za-z_][A-Za-z0-9_]*) # atom or variable
|
|
145
|
+
| (?P<number>-?\d+(?:\.\d+)?) # number
|
|
146
|
+
| (?P<other>\S) # anything else -> refused, located
|
|
147
|
+
""", re.VERBOSE)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
_LINE_BREAK_RE = re.compile(r"\r\n?|\n")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _past_line_comment(text: str, index: int) -> int:
|
|
154
|
+
"""The index just past the '%' line comment starting at ``index``: past
|
|
155
|
+
the first line break of ANY convention (LF, CRLF or a bare CR -- text
|
|
156
|
+
handed over as a string, e.g. a learner's hypothesis, may use any), or
|
|
157
|
+
the end of ``text``."""
|
|
158
|
+
match = _LINE_BREAK_RE.search(text, index)
|
|
159
|
+
return len(text) if match is None else match.end()
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class _Reader:
|
|
163
|
+
"""Position-tracking token cursor over one clause's text."""
|
|
164
|
+
|
|
165
|
+
def __init__(self, text: str):
|
|
166
|
+
self.text = text
|
|
167
|
+
self.tokens: List[Tuple[str, str, int]] = []
|
|
168
|
+
index = 0
|
|
169
|
+
while index < len(text):
|
|
170
|
+
if text[index] in " \t\r\n":
|
|
171
|
+
index += 1
|
|
172
|
+
continue
|
|
173
|
+
if text[index] == "%": # line comment
|
|
174
|
+
index = _past_line_comment(text, index)
|
|
175
|
+
continue
|
|
176
|
+
match = _TOKEN_RE.match(text, index)
|
|
177
|
+
if match is None: # pragma: no cover
|
|
178
|
+
raise PrologParsingError(
|
|
179
|
+
f"SYNTAX_ERROR: unreadable character at position {index}")
|
|
180
|
+
kind = match.lastgroup
|
|
181
|
+
self.tokens.append((kind, match.group(), match.start()))
|
|
182
|
+
index = match.end()
|
|
183
|
+
self.position = 0
|
|
184
|
+
|
|
185
|
+
def peek(self) -> Optional[Tuple[str, str, int]]:
|
|
186
|
+
return (self.tokens[self.position]
|
|
187
|
+
if self.position < len(self.tokens) else None)
|
|
188
|
+
|
|
189
|
+
def next(self) -> Tuple[str, str, int]:
|
|
190
|
+
token = self.peek()
|
|
191
|
+
if token is None:
|
|
192
|
+
raise PrologParsingError(
|
|
193
|
+
"SYNTAX_ERROR: the clause ended unexpectedly")
|
|
194
|
+
self.position += 1
|
|
195
|
+
return token
|
|
196
|
+
|
|
197
|
+
def expect(self, value: str) -> Tuple[str, str, int]:
|
|
198
|
+
token = self.next()
|
|
199
|
+
if token[1] != value:
|
|
200
|
+
raise PrologParsingError(
|
|
201
|
+
f"SYNTAX_ERROR: expected {value!r} at position {token[2]}, "
|
|
202
|
+
f"found {token[1]!r}")
|
|
203
|
+
return token
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _unquote(text: str) -> str:
|
|
207
|
+
"""Strip a quoted atom's quotes and undo its escapes."""
|
|
208
|
+
return re.sub(r"\\(.)", r"\1", text[1:-1])
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class _Parser:
|
|
212
|
+
"""One clause, read with an explicit fresh-variable counter for ``_``."""
|
|
213
|
+
|
|
214
|
+
def __init__(self, reader: _Reader, negation_as_failure: str):
|
|
215
|
+
self.reader = reader
|
|
216
|
+
self.negation_as_failure = negation_as_failure
|
|
217
|
+
self._anonymous = 0
|
|
218
|
+
|
|
219
|
+
# -- terms ---------------------------------------------------------------
|
|
220
|
+
|
|
221
|
+
def term(self) -> Node:
|
|
222
|
+
kind, value, position = self.reader.next()
|
|
223
|
+
if value in _REFUSED:
|
|
224
|
+
raise PrologParsingError(
|
|
225
|
+
f"SYNTAX_ERROR: unsupported construct at position {position}: "
|
|
226
|
+
f"{_REFUSED[value]}")
|
|
227
|
+
if kind == "punct" and value == "[":
|
|
228
|
+
raise PrologParsingError(
|
|
229
|
+
f"SYNTAX_ERROR: list syntax at position {position} — this "
|
|
230
|
+
"importer has no first-order reading for '.'/2 list terms; "
|
|
231
|
+
"rewrite the clause without lists")
|
|
232
|
+
if kind == "number":
|
|
233
|
+
try:
|
|
234
|
+
return Number(_numeral_from_text(value))
|
|
235
|
+
except ValueError as exc:
|
|
236
|
+
raise PrologParsingError(
|
|
237
|
+
f"SYNTAX_ERROR: at position {position}: {exc}") from None
|
|
238
|
+
if kind == "quoted":
|
|
239
|
+
name = _unquote(value)
|
|
240
|
+
return self._compound_or_constant(name, quoted=True)
|
|
241
|
+
if kind != "name":
|
|
242
|
+
raise PrologParsingError(
|
|
243
|
+
f"SYNTAX_ERROR: expected a term at position {position}, found "
|
|
244
|
+
f"{value!r}")
|
|
245
|
+
if value == "_":
|
|
246
|
+
# Each anonymous variable is a DISTINCT fresh one; collapsing them
|
|
247
|
+
# into a single '_' would equate positions Prolog keeps apart.
|
|
248
|
+
self._anonymous += 1
|
|
249
|
+
return Variable(f"_g{self._anonymous}")
|
|
250
|
+
if _VAR_RE.match(value):
|
|
251
|
+
return Variable(_lower(value))
|
|
252
|
+
return self._compound_or_constant(value, quoted=False)
|
|
253
|
+
|
|
254
|
+
def _compound_or_constant(self, name: str, *, quoted: bool) -> Node:
|
|
255
|
+
token = self.reader.peek()
|
|
256
|
+
if token is not None and token[1] == "(":
|
|
257
|
+
self.reader.next()
|
|
258
|
+
arguments = self._arguments()
|
|
259
|
+
return Function(name, arguments)
|
|
260
|
+
return Constant(name)
|
|
261
|
+
|
|
262
|
+
def _arguments(self) -> List[Node]:
|
|
263
|
+
arguments = [self.term()]
|
|
264
|
+
while True:
|
|
265
|
+
token = self.reader.next()
|
|
266
|
+
if token[1] == ")":
|
|
267
|
+
return arguments
|
|
268
|
+
if token[1] != ",":
|
|
269
|
+
raise PrologParsingError(
|
|
270
|
+
f"SYNTAX_ERROR: expected ',' or ')' at position "
|
|
271
|
+
f"{token[2]}, found {token[1]!r}")
|
|
272
|
+
arguments.append(self.term())
|
|
273
|
+
|
|
274
|
+
# -- literals and goals --------------------------------------------------
|
|
275
|
+
|
|
276
|
+
def literal(self) -> Node:
|
|
277
|
+
"""One predicate application, as an :class:`Atom`."""
|
|
278
|
+
kind, value, position = self.reader.next()
|
|
279
|
+
if value in _REFUSED:
|
|
280
|
+
raise PrologParsingError(
|
|
281
|
+
f"SYNTAX_ERROR: unsupported construct at position {position}: "
|
|
282
|
+
f"{_REFUSED[value]}")
|
|
283
|
+
if kind == "quoted":
|
|
284
|
+
name = _unquote(value)
|
|
285
|
+
elif kind == "name" and not _VAR_RE.match(value):
|
|
286
|
+
name = value
|
|
287
|
+
else:
|
|
288
|
+
raise PrologParsingError(
|
|
289
|
+
f"SYNTAX_ERROR: expected a predicate name at position "
|
|
290
|
+
f"{position}, found {value!r}" +
|
|
291
|
+
(" — a variable cannot be a goal in this fragment"
|
|
292
|
+
if kind == "name" else ""))
|
|
293
|
+
token = self.reader.peek()
|
|
294
|
+
if token is None or token[1] != "(":
|
|
295
|
+
return Atom(_cap(name), [])
|
|
296
|
+
self.reader.next()
|
|
297
|
+
return Atom(_cap(name), self._arguments())
|
|
298
|
+
|
|
299
|
+
def goal(self) -> Node:
|
|
300
|
+
token = self.reader.peek()
|
|
301
|
+
if token is not None and token[0] == "neg":
|
|
302
|
+
self.reader.next()
|
|
303
|
+
if self.negation_as_failure != "classical":
|
|
304
|
+
raise PrologParsingError(
|
|
305
|
+
f"SYNTAX_ERROR: negation as failure (\\+) at position "
|
|
306
|
+
f"{token[2]} — it means 'not derivable', which coincides "
|
|
307
|
+
"with classical ¬ only under the closed world assumption "
|
|
308
|
+
"on a stratified program. Pass "
|
|
309
|
+
"negation_as_failure='classical' to read it as ¬ and "
|
|
310
|
+
"assert that assumption, or rewrite the clause.")
|
|
311
|
+
return Not(self.goal())
|
|
312
|
+
if token is not None and token[1] == "(":
|
|
313
|
+
self.reader.next()
|
|
314
|
+
inner = self.disjunction()
|
|
315
|
+
self.reader.expect(")")
|
|
316
|
+
return inner
|
|
317
|
+
return self.literal()
|
|
318
|
+
|
|
319
|
+
def conjunction(self) -> Node:
|
|
320
|
+
result = self.goal()
|
|
321
|
+
while True:
|
|
322
|
+
token = self.reader.peek()
|
|
323
|
+
if token is None or token[1] != ",":
|
|
324
|
+
return result
|
|
325
|
+
self.reader.next()
|
|
326
|
+
result = And(result, self.goal())
|
|
327
|
+
|
|
328
|
+
def disjunction(self) -> Node:
|
|
329
|
+
result = self.conjunction()
|
|
330
|
+
while True:
|
|
331
|
+
token = self.reader.peek()
|
|
332
|
+
if token is None or token[1] != ";":
|
|
333
|
+
return result
|
|
334
|
+
self.reader.next()
|
|
335
|
+
result = Or(result, self.conjunction())
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _close(formula: Node, variables, quantifier: str) -> Node:
|
|
339
|
+
"""Wrap ``formula`` in one quantifier per variable, alphabetically —
|
|
340
|
+
order among quantifiers of the same kind never changes truth, so this is
|
|
341
|
+
purely for deterministic output."""
|
|
342
|
+
result = formula
|
|
343
|
+
for variable in sorted(variables, key=lambda v: v.name, reverse=True):
|
|
344
|
+
result = Quantifier(quantifier, variable, result)
|
|
345
|
+
return result
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def parse_prolog_clause(text: str, *, mode: str = "clause",
|
|
349
|
+
negation_as_failure: str = "refuse") -> Node:
|
|
350
|
+
"""Parse ONE Prolog clause (with or without the trailing ``.``).
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
text: a fact (``p(a).``) or a rule (``h(A) :- b1(A), b2(A).``).
|
|
354
|
+
mode: ``"clause"`` for the standard logical reading (every variable
|
|
355
|
+
universally quantified over the implication) or ``"body"`` for
|
|
356
|
+
just the condition, with body-only variables existentially closed
|
|
357
|
+
and the head's variables left free — see the module docstring for
|
|
358
|
+
why this is the caller's choice and not a default.
|
|
359
|
+
negation_as_failure: ``"refuse"`` (default) or ``"classical"`` to read
|
|
360
|
+
``\\+`` as ``¬``, asserting the closed world assumption holds.
|
|
361
|
+
|
|
362
|
+
Returns:
|
|
363
|
+
A :class:`~unicode_logic_kit.fol.nodes.Node`. A FACT returns its head
|
|
364
|
+
atom, universally closed over its variables in ``"clause"`` mode and
|
|
365
|
+
returned bare in ``"body"`` mode (a fact has no condition, and
|
|
366
|
+
inventing ``⊤`` would be a formula the caller never wrote).
|
|
367
|
+
|
|
368
|
+
Raises:
|
|
369
|
+
~unicode_logic_kit.fol.prolog_input.PrologParsingError: the text is
|
|
370
|
+
outside the accepted fragment (see
|
|
371
|
+
the module docstring), or ``mode``/``negation_as_failure`` is not
|
|
372
|
+
one of the documented values.
|
|
373
|
+
"""
|
|
374
|
+
if mode not in ("clause", "body"):
|
|
375
|
+
raise PrologParsingError(
|
|
376
|
+
f"parse_prolog_clause: unknown mode={mode!r}, expected 'clause' "
|
|
377
|
+
"or 'body'")
|
|
378
|
+
if negation_as_failure not in ("refuse", "classical"):
|
|
379
|
+
raise PrologParsingError(
|
|
380
|
+
"parse_prolog_clause: unknown negation_as_failure="
|
|
381
|
+
f"{negation_as_failure!r}, expected 'refuse' or 'classical'")
|
|
382
|
+
|
|
383
|
+
stripped = text.strip()
|
|
384
|
+
if stripped.endswith("."):
|
|
385
|
+
stripped = stripped[:-1]
|
|
386
|
+
if not stripped.strip():
|
|
387
|
+
raise PrologParsingError("SYNTAX_ERROR: empty clause")
|
|
388
|
+
|
|
389
|
+
parser = _Parser(_Reader(stripped), negation_as_failure)
|
|
390
|
+
head = parser.literal()
|
|
391
|
+
|
|
392
|
+
token = parser.reader.peek()
|
|
393
|
+
body: Optional[Node] = None
|
|
394
|
+
if token is not None and token[0] == "neck":
|
|
395
|
+
parser.reader.next()
|
|
396
|
+
body = parser.disjunction()
|
|
397
|
+
leftover = parser.reader.peek()
|
|
398
|
+
if leftover is not None:
|
|
399
|
+
# A refused OPERATOR shows up here rather than inside a goal (it sits
|
|
400
|
+
# between two well-formed goals), so the documented reason has to be
|
|
401
|
+
# consulted at this point too — otherwise `p :- q -> r.` would be
|
|
402
|
+
# reported as a stray token instead of as if-then, and the reader
|
|
403
|
+
# would go looking for a typo.
|
|
404
|
+
if leftover[1] in _REFUSED:
|
|
405
|
+
raise PrologParsingError(
|
|
406
|
+
f"SYNTAX_ERROR: unsupported construct at position "
|
|
407
|
+
f"{leftover[2]}: {_REFUSED[leftover[1]]}")
|
|
408
|
+
raise PrologParsingError(
|
|
409
|
+
f"SYNTAX_ERROR: unexpected {leftover[1]!r} at position "
|
|
410
|
+
f"{leftover[2]} — one clause per call; use parse_prolog_program "
|
|
411
|
+
"for a file")
|
|
412
|
+
|
|
413
|
+
if body is None:
|
|
414
|
+
if mode == "body":
|
|
415
|
+
return head
|
|
416
|
+
return _close(head, free_variables(head), "∀")
|
|
417
|
+
|
|
418
|
+
if mode == "body":
|
|
419
|
+
head_variables = free_variables(head)
|
|
420
|
+
body_only = [v for v in free_variables(body) if v not in head_variables]
|
|
421
|
+
return _close(body, body_only, "∃")
|
|
422
|
+
return _close(Implies(body, head), free_variables(Implies(body, head)), "∀")
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def parse_prolog_program(text: str, *, mode: str = "clause",
|
|
426
|
+
negation_as_failure: str = "refuse") -> List[Node]:
|
|
427
|
+
"""Parse every clause in ``text``, in order.
|
|
428
|
+
|
|
429
|
+
Clauses are split on a ``.`` that ends a clause — a period followed by
|
|
430
|
+
whitespace or end of input — so a decimal point inside a number and a
|
|
431
|
+
period inside a quoted atom do not split anything.
|
|
432
|
+
|
|
433
|
+
Several clauses sharing one head are ALTERNATIVES (Prolog tries them in
|
|
434
|
+
turn), i.e. their bodies are implicitly disjoined. This function returns
|
|
435
|
+
them separately and does NOT combine them: the disjunction only holds
|
|
436
|
+
under the completion of the program, which is an assumption about the
|
|
437
|
+
whole program rather than a fact about these clauses, and asserting it
|
|
438
|
+
silently would strengthen what the caller wrote.
|
|
439
|
+
|
|
440
|
+
Raises:
|
|
441
|
+
~unicode_logic_kit.fol.prolog_input.PrologParsingError: as
|
|
442
|
+
:func:`parse_prolog_clause`, with the failing
|
|
443
|
+
clause's text in the message.
|
|
444
|
+
"""
|
|
445
|
+
clauses = []
|
|
446
|
+
for chunk in _split_clauses(text):
|
|
447
|
+
try:
|
|
448
|
+
clauses.append(parse_prolog_clause(
|
|
449
|
+
chunk, mode=mode, negation_as_failure=negation_as_failure))
|
|
450
|
+
except PrologParsingError as exc:
|
|
451
|
+
raise PrologParsingError(f"{exc} (in clause: {chunk.strip()!r})")
|
|
452
|
+
return clauses
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def _split_clauses(text: str) -> List[str]:
|
|
456
|
+
"""Clause-terminating periods only: not a decimal point, not inside a
|
|
457
|
+
quoted atom, not inside a line comment."""
|
|
458
|
+
chunks: List[str] = []
|
|
459
|
+
current: List[str] = []
|
|
460
|
+
index = 0
|
|
461
|
+
in_quote = False
|
|
462
|
+
while index < len(text):
|
|
463
|
+
character = text[index]
|
|
464
|
+
if in_quote:
|
|
465
|
+
current.append(character)
|
|
466
|
+
if character == "\\" and index + 1 < len(text):
|
|
467
|
+
current.append(text[index + 1])
|
|
468
|
+
index += 2
|
|
469
|
+
continue
|
|
470
|
+
if character == "'":
|
|
471
|
+
in_quote = False
|
|
472
|
+
index += 1
|
|
473
|
+
continue
|
|
474
|
+
if character == "'":
|
|
475
|
+
in_quote = True
|
|
476
|
+
current.append(character)
|
|
477
|
+
index += 1
|
|
478
|
+
continue
|
|
479
|
+
if character == "%":
|
|
480
|
+
index = _past_line_comment(text, index)
|
|
481
|
+
continue
|
|
482
|
+
if character == ".":
|
|
483
|
+
following = text[index + 1] if index + 1 < len(text) else " "
|
|
484
|
+
if following in " \t\r\n":
|
|
485
|
+
chunk = "".join(current).strip()
|
|
486
|
+
if chunk:
|
|
487
|
+
chunks.append(chunk)
|
|
488
|
+
current = []
|
|
489
|
+
index += 1
|
|
490
|
+
continue
|
|
491
|
+
current.append(character)
|
|
492
|
+
index += 1
|
|
493
|
+
tail = "".join(current).strip()
|
|
494
|
+
if tail:
|
|
495
|
+
chunks.append(tail)
|
|
496
|
+
return chunks
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def load_prolog(path: str, *, mode: str = "clause",
|
|
500
|
+
negation_as_failure: str = "refuse") -> List[Node]:
|
|
501
|
+
"""Read a ``.pl`` file and parse every clause in it (see
|
|
502
|
+
:func:`parse_prolog_program`)."""
|
|
503
|
+
with open(path, encoding="utf-8") as handle:
|
|
504
|
+
return parse_prolog_program(
|
|
505
|
+
handle.read(), mode=mode, negation_as_failure=negation_as_failure)
|