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,566 @@
|
|
|
1
|
+
"""Kit AST -> Prolog / Datalog clauses.
|
|
2
|
+
|
|
3
|
+
The missing return leg of :mod:`unicode_logic_kit.fol.prolog_input`: that module
|
|
4
|
+
reads a fact or a definite/normal clause into a kit formula; this module goes
|
|
5
|
+
back the other way, for a formula that was BUILT to look like one — a rule
|
|
6
|
+
mined from a structure, a hand-written class definition, or the classical
|
|
7
|
+
reading of a clause that came from somewhere else and needs to travel back out
|
|
8
|
+
as text a Prolog engine (or a rule learner such as Popper) can consume.
|
|
9
|
+
|
|
10
|
+
**Not a ``Node.to_prolog()`` method.** Every other exporter in this package
|
|
11
|
+
(``to_tptp``, ``to_prover9``, ``to_latex``, ``to_z3``) is a total syntactic
|
|
12
|
+
recursion: every node type has *some* rendering in the target syntax, so
|
|
13
|
+
spreading the method across every AST class is the natural shape. Prolog is
|
|
14
|
+
different — it can only express a narrow shape (a fact, or a single-headed
|
|
15
|
+
implication whose body combines facts with ``,``/``;``/opt-in ``\\+``), and
|
|
16
|
+
most formulas have **no** Prolog reading at all. A method on every node class
|
|
17
|
+
would need to raise from nearly every one of them; a single function that
|
|
18
|
+
inspects the *whole* clause shape up front and refuses by name is both more
|
|
19
|
+
honest about what this format can hold and mirrors how
|
|
20
|
+
:func:`~unicode_logic_kit.fol.prolog_input.parse_prolog_clause` reads it back in
|
|
21
|
+
one place rather than one grammar rule per node type.
|
|
22
|
+
|
|
23
|
+
**Equivalence-preserving by construction, not by clausification.** The
|
|
24
|
+
accepted fragment is the EXACT syntactic mirror of
|
|
25
|
+
:func:`~unicode_logic_kit.fol.prolog_input.parse_prolog_clause`'s ``mode="clause"``
|
|
26
|
+
reading:
|
|
27
|
+
|
|
28
|
+
- a bare (or ``∀``-closed) :class:`~unicode_logic_kit.fol.nodes.Atom` — a fact,
|
|
29
|
+
``pred(args).``;
|
|
30
|
+
- a (possibly ``∀``-prefixed)
|
|
31
|
+
:class:`~unicode_logic_kit.fol.nodes.Implies`\\ ``(body, head)`` where ``head``
|
|
32
|
+
is a single atom and ``body`` is built only from
|
|
33
|
+
:class:`~unicode_logic_kit.fol.nodes.And`/:class:`~unicode_logic_kit.fol.nodes.Or`/
|
|
34
|
+
:class:`~unicode_logic_kit.fol.nodes.Atom`/``Not(Atom)`` — ``Head :- Body.``.
|
|
35
|
+
|
|
36
|
+
Nothing else is rendered — it is REFUSED, by name, the same way
|
|
37
|
+
:func:`unicode_logic_kit.fol.normalforms._unsupported_hint` points a caller at
|
|
38
|
+
the right tool for a node the classical normal forms cannot take either. The
|
|
39
|
+
direct shape check ALONE is enough to refuse everything outside the fragment
|
|
40
|
+
soundly; :func:`~unicode_logic_kit.fol.normalforms.is_horn` is not used as a
|
|
41
|
+
gate on top of it (a formula using ``\\+`` classically is typically not even
|
|
42
|
+
a classical Horn clause — ``Q(x) ∧ ¬R(x) → P(x)`` clausifies to
|
|
43
|
+
``¬Q(x) ∨ R(x) ∨ P(x)``, two positive literals — so gating acceptance on
|
|
44
|
+
``is_horn`` would refuse exactly the clauses the negation-as-failure opt-in
|
|
45
|
+
below exists to allow). It is used for exactly one thing: when the direct
|
|
46
|
+
shape check has ALREADY failed (the formula is not directly a fact or a
|
|
47
|
+
``body → head`` implication), ``is_horn`` checks whether the formula's
|
|
48
|
+
skolemised/CNF clausal form would nonetheless be Horn, and if so the refusal
|
|
49
|
+
says so explicitly — because that is the dangerous NEAR MISS the original
|
|
50
|
+
version of this feature would have silently mis-rendered: skolemize is
|
|
51
|
+
satisfiability-, not equivalence-preserving (see
|
|
52
|
+
:mod:`unicode_logic_kit.fol.normalforms`'s own docstring), so a naive
|
|
53
|
+
"``is_horn(node)`` is True, so clausify and emit" exporter would have swapped
|
|
54
|
+
equivalence for mere equisatisfiability. That check can only ever ADD a more
|
|
55
|
+
specific reason to a refusal that was already happening — it never turns a
|
|
56
|
+
refusal into an acceptance.
|
|
57
|
+
|
|
58
|
+
**Negation as failure is opt-in, and is NOT classical negation** — the same
|
|
59
|
+
discipline the importer applies, inverted. ``Not(Atom)`` in the body only
|
|
60
|
+
renders as ``\\+ Atom`` when the caller passes
|
|
61
|
+
``negation_as_failure="classical"``; otherwise it is refused, naming the same
|
|
62
|
+
"closed world assumption" reason
|
|
63
|
+
:func:`~unicode_logic_kit.fol.prolog_input.parse_prolog_clause` gives on the way
|
|
64
|
+
in. Passing the opt-in makes the round trip syntactically faithful (the
|
|
65
|
+
emitted ``\\+`` reads back as the same ``Not(Atom)``), but it does **not**
|
|
66
|
+
make ``\\+`` and ``¬`` mean the same thing when the emitted program is
|
|
67
|
+
actually *run*: ``\\+ G`` succeeds whenever Prolog FAILS TO PROVE ``G``, which
|
|
68
|
+
agrees with ``¬G`` only when the program is COMPLETE for ``G`` — every ground
|
|
69
|
+
instance that is true (in whatever structure the program is meant to model)
|
|
70
|
+
is also derivable from the program. When the program is incomplete — some
|
|
71
|
+
true fact about ``G`` was never asserted — ``\\+ G`` succeeds where ``¬G`` is
|
|
72
|
+
in fact false. That disagreement (not a hypothetical: see
|
|
73
|
+
``tests/test_prolog_export.py::test_naf_disagrees_with_classical_negation_on_an_incomplete_program``,
|
|
74
|
+
which exhibits it with the kit's own resolution prover as the independent
|
|
75
|
+
oracle) is exactly why the opt-in exists rather than a silent default: passing
|
|
76
|
+
``negation_as_failure="classical"`` is the caller asserting that their program
|
|
77
|
+
is, or will be run as, complete for every negated predicate.
|
|
78
|
+
|
|
79
|
+
**Naming inverts the importer's fold exactly.** A predicate is folded to
|
|
80
|
+
Prolog's lower-case-initial spelling (only the FIRST character — mirroring
|
|
81
|
+
:func:`unicode_logic_kit.ilp.task.to_prolog_atom` and
|
|
82
|
+
:func:`unicode_logic_kit.fol.prolog_input._cap`/``_lower`` rather than importing
|
|
83
|
+
either, so a divergence between the three shows up as a failing round-trip
|
|
84
|
+
test instead of leaking silently); a name that does not fold back to the
|
|
85
|
+
EXACT original spelling (i.e. does not start with an upper-case letter — the
|
|
86
|
+
kit's own signal for predicate-hood, see
|
|
87
|
+
:mod:`unicode_logic_kit.fol._identifiers`) is refused rather than exported under
|
|
88
|
+
a spelling that would not read back to itself. A constant or function name is
|
|
89
|
+
emitted VERBATIM (the importer never folds one), quoted with Prolog's
|
|
90
|
+
``'...'`` syntax whenever it is not already a legal bare atom. Every variable
|
|
91
|
+
in a clause is renamed to a fresh, upper-case-initial Prolog spelling
|
|
92
|
+
(``V0``, ``V1``, ...) — the same convention
|
|
93
|
+
:meth:`~unicode_logic_kit.fol.nodes.Variable.to_prover9` uses under
|
|
94
|
+
``set(prolog_style_variables)`` — assigned in the SAME alphabetical order
|
|
95
|
+
:func:`~unicode_logic_kit.fol.prolog_input.parse_prolog_clause` itself closes
|
|
96
|
+
variables in, so the round trip lands on the identical quantifier nesting,
|
|
97
|
+
not merely an alpha-equivalent one. Prolog variable spelling is scoped to one
|
|
98
|
+
clause and carries no meaning beyond identity, so the original kit name is
|
|
99
|
+
never preserved (nor does it need to be).
|
|
100
|
+
|
|
101
|
+
**Numbers and zero-arity function terms are refused, not approximated,
|
|
102
|
+
when Prolog has no exact reading for them.** A :class:`Number` whose value
|
|
103
|
+
would print outside :func:`~unicode_logic_kit.fol.prolog_input.parse_prolog_clause`'s
|
|
104
|
+
numeral grammar — scientific notation for a very large/small float, or the
|
|
105
|
+
non-finite ``nan``/``inf``/``-inf`` — is refused rather than emitted as text
|
|
106
|
+
the importer cannot read back (``nan``/``inf`` are worse than a parse error:
|
|
107
|
+
they would silently reparse as a *different* node type, :class:`Constant`,
|
|
108
|
+
with no error at all). A zero-argument :class:`Function` is refused for the
|
|
109
|
+
same reason: Prolog syntax cannot write ``f()`` distinct from the bare atom
|
|
110
|
+
``f``, so it would reparse as a :class:`Constant`, not a :class:`Function` —
|
|
111
|
+
confirmed with the kit's own resolution prover that the two are not
|
|
112
|
+
logically equivalent.
|
|
113
|
+
|
|
114
|
+
**One numeral per value.** ``Number(1)`` and ``Number(1.0)`` are equal nodes, one constant,
|
|
115
|
+
but Prolog keeps the integer ``1`` and the float ``1.0`` apart (they do not unify), so a
|
|
116
|
+
float with a whole value is written as the integer it equals: ``p(1.0)`` is exported as
|
|
117
|
+
``p(1)``, and ``p(1) :- p(1.0)`` as ``p(1) :- p(1)``. A :class:`Constant` named ``'1'``
|
|
118
|
+
is the quoted atom ``'1'``, another Prolog term than the number ``1``, and is read back as
|
|
119
|
+
the constant.
|
|
120
|
+
|
|
121
|
+
Public API: :func:`formula_to_prolog_clause` (one clause) and
|
|
122
|
+
:func:`formula_to_prolog_program` (several, one per line, splitting a
|
|
123
|
+
top-level conjunction the way
|
|
124
|
+
:func:`~unicode_logic_kit.fol.prolog_input.parse_prolog_program` returns several
|
|
125
|
+
clauses rather than one).
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
import re
|
|
129
|
+
from typing import Iterable, List, Union
|
|
130
|
+
|
|
131
|
+
from .nodes import (
|
|
132
|
+
Node, Variable, Constant, Number, Function, Atom, Not, And, Or, Implies,
|
|
133
|
+
Quantifier, free_variables,
|
|
134
|
+
)
|
|
135
|
+
from .normalforms import is_horn, _unsupported_hint
|
|
136
|
+
from ._numeral_symbols import numeral_name
|
|
137
|
+
from ._truth_constants import refuse_truth_constants
|
|
138
|
+
|
|
139
|
+
__all__ = [
|
|
140
|
+
"PrologExportError", "formula_to_prolog_clause", "formula_to_prolog_program",
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
_FORALL = ("∀", "forall")
|
|
144
|
+
|
|
145
|
+
#: Mirrors :data:`unicode_logic_kit.fol.prolog_input._ATOM_RE` exactly (a legal
|
|
146
|
+
#: BARE Prolog atom) rather than importing it — the same "duplicate a tiny
|
|
147
|
+
#: naming rule so a divergence fails a test instead of leaking silently"
|
|
148
|
+
#: convention every importer/exporter pair in this package already follows
|
|
149
|
+
#: (see :func:`unicode_logic_kit.fol.prolog_input._cap`'s docstring).
|
|
150
|
+
_ATOM_RE = re.compile(r"^[a-z][A-Za-z0-9_]*$")
|
|
151
|
+
|
|
152
|
+
#: Mirrors :data:`unicode_logic_kit.fol.prolog_input._NUMBER_RE` exactly (the
|
|
153
|
+
#: only numeral shape ``parse_prolog_clause`` reads back — a plain, optionally
|
|
154
|
+
#: signed, optionally decimal literal; no exponent, no ``nan``/``inf``), same
|
|
155
|
+
#: duplicate-rather-than-import convention as ``_ATOM_RE`` above. Python's
|
|
156
|
+
#: ``str()`` of a :class:`~unicode_logic_kit.fol.nodes.Number`'s ``value`` falls
|
|
157
|
+
#: outside this grammar for very large/small floats (``str(1e20) ==
|
|
158
|
+
#: '1e+20'``) and for the non-finite floats ``nan``/``inf``/``-inf`` — those
|
|
159
|
+
#: are refused in :func:`_render_term` rather than emitted as text the kit's
|
|
160
|
+
#: own importer could not read back (or, worse for ``nan``/``inf``, would
|
|
161
|
+
#: silently read back as a *different* node type, :class:`Constant`, with no
|
|
162
|
+
#: error at all).
|
|
163
|
+
_NUMBER_RE = re.compile(r"^-?\d+(?:\.\d+)?$")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class PrologExportError(ValueError):
|
|
167
|
+
"""Raised when a formula has no sound Prolog clause reading.
|
|
168
|
+
|
|
169
|
+
Not a :class:`~unicode_logic_kit.fol.naming.ParsingError` subclass — this
|
|
170
|
+
module never parses anything; it is the exporter's own refusal, in the
|
|
171
|
+
same spirit as :mod:`unicode_logic_kit.fol.casl_export`'s ``ValueError``
|
|
172
|
+
exceptions and :class:`~unicode_logic_kit.fol.tptp_repair.TptpRepairError`.
|
|
173
|
+
Every message names the offending construct or reason, never a bare "invalid
|
|
174
|
+
formula".
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
# ---------------------------------------------------------------------------
|
|
179
|
+
# Naming
|
|
180
|
+
# ---------------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
def _quote_if_needed(text: str) -> str:
|
|
183
|
+
"""Return ``text`` bare if it is already a legal Prolog atom, else quoted.
|
|
184
|
+
|
|
185
|
+
The escaping is the exact inverse of
|
|
186
|
+
:func:`unicode_logic_kit.fol.prolog_input._unquote`: a backslash or a single
|
|
187
|
+
quote inside the content is backslash-escaped, nothing else is touched.
|
|
188
|
+
"""
|
|
189
|
+
if _ATOM_RE.fullmatch(text):
|
|
190
|
+
return text
|
|
191
|
+
escaped = text.replace("\\", "\\\\").replace("'", "\\'")
|
|
192
|
+
return f"'{escaped}'"
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _predicate_text(name: str) -> str:
|
|
196
|
+
"""The Prolog spelling of a kit PREDICATE name — fold the first character
|
|
197
|
+
down, then quote if the result is not a bare atom.
|
|
198
|
+
|
|
199
|
+
:func:`~unicode_logic_kit.fol.prolog_input.parse_prolog_clause` folds a
|
|
200
|
+
Prolog functor's first character UP to build a predicate name (``_cap``),
|
|
201
|
+
on both the bare and the quoted route (see that module's docstring for
|
|
202
|
+
why a quoted ``'1,2-diacyl'`` still "keeps its exact characters" despite
|
|
203
|
+
the unconditional fold: ``_cap`` is a no-op on a non-letter first
|
|
204
|
+
character). Inverting that fold is only sound when the kit's own name
|
|
205
|
+
starts with the upper-case half of the SAME fold — i.e. when it is
|
|
206
|
+
already a legal kit predicate spelling in the first place (the
|
|
207
|
+
first-letter-is-upper-case convention every kit predicate is parsed
|
|
208
|
+
under; see :mod:`unicode_logic_kit.fol._identifiers`). A name that does not
|
|
209
|
+
satisfy this is refused rather than exported under a spelling that would
|
|
210
|
+
not read back to itself.
|
|
211
|
+
"""
|
|
212
|
+
folded = name[:1].lower() + name[1:] if name else name
|
|
213
|
+
refolded = folded[:1].upper() + folded[1:] if folded else folded
|
|
214
|
+
if refolded != name:
|
|
215
|
+
raise PrologExportError(
|
|
216
|
+
f"formula_to_prolog_clause: the predicate name {name!r} does not "
|
|
217
|
+
"start with an upper-case letter, so it is not a legal kit "
|
|
218
|
+
"predicate spelling in the first place (every kit predicate is "
|
|
219
|
+
"parsed as starting upper-case — see "
|
|
220
|
+
"unicode_logic_kit.fol._identifiers) and folding it to a Prolog "
|
|
221
|
+
"functor would not read back to this exact name.")
|
|
222
|
+
return _quote_if_needed(folded)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _term_text(name: str) -> str:
|
|
226
|
+
"""The Prolog spelling of a kit CONSTANT or FUNCTION name — verbatim,
|
|
227
|
+
quoted if it is not already a legal bare atom.
|
|
228
|
+
|
|
229
|
+
Unlike a predicate, :func:`~unicode_logic_kit.fol.prolog_input.parse_prolog_clause`
|
|
230
|
+
never folds a constant or function name's case (see its module
|
|
231
|
+
docstring's naming section and ``tests/test_prolog_input.py``'s
|
|
232
|
+
``test_a_compound_term_becomes_a_function_not_a_predicate``), so no
|
|
233
|
+
inversion is needed here — only quoting.
|
|
234
|
+
"""
|
|
235
|
+
return _quote_if_needed(name)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _variable_names(variables) -> dict:
|
|
239
|
+
"""``{kit Variable name -> fresh Prolog spelling}`` for one clause.
|
|
240
|
+
|
|
241
|
+
Every variable becomes ``V0``, ``V1``, ... — upper-case-initial, the
|
|
242
|
+
convention :meth:`~unicode_logic_kit.fol.nodes.Variable.to_prover9` also
|
|
243
|
+
uses under ``set(prolog_style_variables)`` — assigned in the SAME
|
|
244
|
+
alphabetical order (by the ORIGINAL kit name) that
|
|
245
|
+
:func:`~unicode_logic_kit.fol.prolog_input.parse_prolog_clause`'s own
|
|
246
|
+
``_close`` helper uses to re-quantify them on the way back in. Because
|
|
247
|
+
the fresh names are zero-padded to a common width, sorting them
|
|
248
|
+
LEXICALLY (which is what ``_close`` does on re-import) reproduces this
|
|
249
|
+
exact assignment order regardless of how many variables the clause has —
|
|
250
|
+
so the round trip lands on the identical quantifier nesting, not merely
|
|
251
|
+
an alpha-equivalent one.
|
|
252
|
+
"""
|
|
253
|
+
ordered = sorted(variables, key=lambda v: v.name)
|
|
254
|
+
width = max(1, len(str(max(len(ordered) - 1, 0))))
|
|
255
|
+
return {v.name: f"V{i:0{width}d}" for i, v in enumerate(ordered)}
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# ---------------------------------------------------------------------------
|
|
259
|
+
# Terms
|
|
260
|
+
# ---------------------------------------------------------------------------
|
|
261
|
+
|
|
262
|
+
def _render_term(node: Node, names: dict) -> str:
|
|
263
|
+
if isinstance(node, Variable):
|
|
264
|
+
try:
|
|
265
|
+
return names[node.name]
|
|
266
|
+
except KeyError: # pragma: no cover — every free variable of the
|
|
267
|
+
# clause is registered by the caller before rendering starts.
|
|
268
|
+
raise PrologExportError(
|
|
269
|
+
f"formula_to_prolog_clause: internal error — variable "
|
|
270
|
+
f"{node.name!r} was not in the clause's own free-variable set")
|
|
271
|
+
if isinstance(node, Constant):
|
|
272
|
+
return _term_text(node.name)
|
|
273
|
+
if isinstance(node, Number):
|
|
274
|
+
text = str(node.value)
|
|
275
|
+
if not _NUMBER_RE.fullmatch(text):
|
|
276
|
+
raise PrologExportError(
|
|
277
|
+
f"formula_to_prolog_clause: the number {node.value!r} would "
|
|
278
|
+
f"render as {text!r}, which is outside "
|
|
279
|
+
"parse_prolog_clause's numeral grammar (-?\\d+(\\.\\d+)?, no "
|
|
280
|
+
"exponent, no nan/inf) and would not read back to this exact "
|
|
281
|
+
"value — refused rather than emitted as text the kit's own "
|
|
282
|
+
"importer could not parse (or, for nan/inf, would silently "
|
|
283
|
+
"misread as a Constant instead of a Number)")
|
|
284
|
+
# A numeral is identified by its VALUE: Prolog keeps the integer 1 and the float 1.0 apart
|
|
285
|
+
# (they do not unify), the kit's Number(1) == Number(1.0) does not, so a float with a
|
|
286
|
+
# whole value is written as the integer it equals.
|
|
287
|
+
return numeral_name(node.value)
|
|
288
|
+
if isinstance(node, Function):
|
|
289
|
+
if not node.args:
|
|
290
|
+
raise PrologExportError(
|
|
291
|
+
f"formula_to_prolog_clause: {node.name!r} is a 0-arity "
|
|
292
|
+
"Function term — Prolog syntax has no way to write 'f()' "
|
|
293
|
+
"distinct from the bare atom 'f', so parse_prolog_clause "
|
|
294
|
+
"would read the emitted text back as a Constant, not a "
|
|
295
|
+
"Function (a genuine change of node type, not merely of "
|
|
296
|
+
"spelling — the kit's own resolution prover confirms "
|
|
297
|
+
"Atom('p', [Function('f', [])]) and Atom('p', "
|
|
298
|
+
"[Constant('f')]) are not logically equivalent in either "
|
|
299
|
+
"direction). Use Constant instead if a bare atom is what is "
|
|
300
|
+
"meant.")
|
|
301
|
+
args = ", ".join(_render_term(a, names) for a in node.args)
|
|
302
|
+
return f"{_term_text(node.name)}({args})"
|
|
303
|
+
raise PrologExportError(
|
|
304
|
+
f"formula_to_prolog_clause: {type(node).__name__} has no Prolog term "
|
|
305
|
+
f"reading{_unsupported_hint(node)}")
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
# ---------------------------------------------------------------------------
|
|
309
|
+
# Atoms (predicate applications)
|
|
310
|
+
# ---------------------------------------------------------------------------
|
|
311
|
+
|
|
312
|
+
def _check_not_comparison(atom: Atom) -> None:
|
|
313
|
+
"""Refuse ``=``/``≠``/``<``/``>``/``≤``/``≥`` — Prolog reads none of
|
|
314
|
+
them (see :mod:`unicode_logic_kit.fol.prolog_input`'s module docstring:
|
|
315
|
+
arithmetic comparison is one of the constructs it refuses on the way
|
|
316
|
+
in), so there is no sound way back out either.
|
|
317
|
+
"""
|
|
318
|
+
if atom.predicate in Atom.INFIX_PREDS_P9:
|
|
319
|
+
raise PrologExportError(
|
|
320
|
+
f"formula_to_prolog_clause: {atom.predicate!r} is a comparison/"
|
|
321
|
+
"equality predicate — parse_prolog_clause has no reading for "
|
|
322
|
+
"'=', '≠', '<', '>', '≤', or '≥' (they are not part of the "
|
|
323
|
+
"accepted Prolog fragment on the way in either), so exporting it "
|
|
324
|
+
"would not round trip")
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _render_atom(atom: Atom, names: dict) -> str:
|
|
328
|
+
refuse_truth_constants(
|
|
329
|
+
[atom], "formula_to_prolog_clause",
|
|
330
|
+
"Prolog's own true / fail are control goals that cannot head a clause, "
|
|
331
|
+
"and parse_prolog_clause reads them back as the ordinary predicates "
|
|
332
|
+
"True / Fail, so the clause would not round trip",
|
|
333
|
+
error=PrologExportError)
|
|
334
|
+
_check_not_comparison(atom)
|
|
335
|
+
predicate = _predicate_text(atom.predicate)
|
|
336
|
+
if not atom.args:
|
|
337
|
+
return predicate
|
|
338
|
+
args = ", ".join(_render_term(a, names) for a in atom.args)
|
|
339
|
+
return f"{predicate}({args})"
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
# ---------------------------------------------------------------------------
|
|
343
|
+
# Bodies: And / Or / Atom / Not(Atom)
|
|
344
|
+
# ---------------------------------------------------------------------------
|
|
345
|
+
|
|
346
|
+
def _render_body(node: Node, names: dict, negation_as_failure: str) -> str:
|
|
347
|
+
if isinstance(node, Atom):
|
|
348
|
+
return _render_atom(node, names)
|
|
349
|
+
if isinstance(node, Not):
|
|
350
|
+
if negation_as_failure != "classical":
|
|
351
|
+
raise PrologExportError(
|
|
352
|
+
"formula_to_prolog_clause: the body contains classical "
|
|
353
|
+
f"negation ({node.to_unicode_str()!r}), which has no sound "
|
|
354
|
+
"'\\+' reading unless the caller asserts the closed world "
|
|
355
|
+
"assumption holds for the program the clause will run in — "
|
|
356
|
+
"pass negation_as_failure='classical' to opt in (see the "
|
|
357
|
+
"module docstring for exactly when '\\+' and classical ¬ "
|
|
358
|
+
"agree, and when they do not)")
|
|
359
|
+
if not isinstance(node.formula, Atom):
|
|
360
|
+
raise PrologExportError(
|
|
361
|
+
"formula_to_prolog_clause: only '\\+' applied to a single "
|
|
362
|
+
f"atom is exported; {type(node.formula).__name__} inside "
|
|
363
|
+
f"'\\+' ({node.to_unicode_str()!r}) is outside the accepted "
|
|
364
|
+
"fragment (parse_prolog_clause's own body grammar allows "
|
|
365
|
+
"'\\+' to nest further, but this exporter deliberately "
|
|
366
|
+
"narrows to '\\+ Atom' only)")
|
|
367
|
+
return f"\\+ {_render_atom(node.formula, names)}"
|
|
368
|
+
if isinstance(node, And):
|
|
369
|
+
left = _render_body_operand(node.left, names, negation_as_failure, "and")
|
|
370
|
+
right = _render_body_operand(node.right, names, negation_as_failure, "and")
|
|
371
|
+
return f"{left}, {right}"
|
|
372
|
+
if isinstance(node, Or):
|
|
373
|
+
left = _render_body_operand(node.left, names, negation_as_failure, "or")
|
|
374
|
+
right = _render_body_operand(node.right, names, negation_as_failure, "or")
|
|
375
|
+
return f"{left} ; {right}"
|
|
376
|
+
raise PrologExportError(
|
|
377
|
+
f"formula_to_prolog_clause: {type(node).__name__} has no Prolog body "
|
|
378
|
+
f"reading{_unsupported_hint(node)} — a body may only combine facts "
|
|
379
|
+
"with ',' (∧), ';' (∨), and opt-in '\\+' (see the module docstring "
|
|
380
|
+
"for the accepted fragment)")
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _render_body_operand(node: Node, names: dict, negation_as_failure: str,
|
|
384
|
+
parent: str) -> str:
|
|
385
|
+
"""Render one operand of And/Or, parenthesising only where Prolog's
|
|
386
|
+
precedence (',' binds tighter than ';') would otherwise change the
|
|
387
|
+
parse: an Or nested inside an And's operand position.
|
|
388
|
+
|
|
389
|
+
Every other nesting (And-in-And, Or-in-Or, either under a parent Or, a
|
|
390
|
+
bare Atom or '\\+ Atom' anywhere) already parses back to the intended
|
|
391
|
+
shape without parentheses — and even where the exact associativity
|
|
392
|
+
differs from the input tree, that difference is one
|
|
393
|
+
:func:`unicode_logic_kit.eval.canonical.canonicalize` already quotients
|
|
394
|
+
out (And/Or are flattened and re-sorted there), so it is never a
|
|
395
|
+
round-trip risk.
|
|
396
|
+
"""
|
|
397
|
+
text = _render_body(node, names, negation_as_failure)
|
|
398
|
+
if parent == "and" and isinstance(node, Or):
|
|
399
|
+
return f"({text})"
|
|
400
|
+
return text
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
# ---------------------------------------------------------------------------
|
|
404
|
+
# Clauses
|
|
405
|
+
# ---------------------------------------------------------------------------
|
|
406
|
+
|
|
407
|
+
def _strip_foralls(node: Node) -> Node:
|
|
408
|
+
"""Peel off every leading ``∀`` and return the matrix underneath.
|
|
409
|
+
|
|
410
|
+
The peeled variables are not inspected: whatever free variables remain
|
|
411
|
+
in the matrix (whether they were bound here, were free in the input
|
|
412
|
+
all along, or a caller's vacuous ``∀`` bound nothing at all) are exactly
|
|
413
|
+
what :func:`~unicode_logic_kit.fol.prolog_input.parse_prolog_clause`
|
|
414
|
+
re-quantifies on the way back in, so re-deriving them from the matrix
|
|
415
|
+
itself — rather than trusting the caller's prefix — is what makes both
|
|
416
|
+
a bare formula (``Implies(body, head)``, free variables and all) and an
|
|
417
|
+
already ``∀``-closed one valid input.
|
|
418
|
+
"""
|
|
419
|
+
matrix = node
|
|
420
|
+
while isinstance(matrix, Quantifier) and matrix.type in _FORALL:
|
|
421
|
+
matrix = matrix.formula
|
|
422
|
+
return matrix
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def formula_to_prolog_clause(node: Node, *, negation_as_failure: str = "refuse") -> str:
|
|
426
|
+
"""Render ``node`` as ONE Prolog clause: a fact or a definite/normal rule.
|
|
427
|
+
|
|
428
|
+
The accepted fragment is the exact mirror of
|
|
429
|
+
:func:`~unicode_logic_kit.fol.prolog_input.parse_prolog_clause`'s
|
|
430
|
+
``mode="clause"`` reading — see the module docstring. Anything else is
|
|
431
|
+
refused, naming the construct.
|
|
432
|
+
|
|
433
|
+
Args:
|
|
434
|
+
node: a bare or ``∀``-closed :class:`~unicode_logic_kit.fol.nodes.Atom`
|
|
435
|
+
(a fact), or a bare or ``∀``-closed
|
|
436
|
+
:class:`~unicode_logic_kit.fol.nodes.Implies`\\ ``(body, head)``
|
|
437
|
+
(a rule) whose ``head`` is a single atom and whose ``body`` is
|
|
438
|
+
built from And/Or/Atom/``Not(Atom)`` only.
|
|
439
|
+
negation_as_failure: ``"refuse"`` (default) — any ``Not`` in the body
|
|
440
|
+
is refused — or ``"classical"`` to render ``Not(Atom)`` as
|
|
441
|
+
``\\+ Atom``, asserting that ``\\+`` and classical ¬ agree for
|
|
442
|
+
this clause's program (see the module docstring for exactly
|
|
443
|
+
when that holds).
|
|
444
|
+
|
|
445
|
+
Returns:
|
|
446
|
+
The clause text, ending in ``"."``.
|
|
447
|
+
|
|
448
|
+
Raises:
|
|
449
|
+
PrologExportError: ``node`` is outside the accepted fragment, its
|
|
450
|
+
head has a variable not bound by its body (not range-restricted),
|
|
451
|
+
a predicate/constant/function name cannot be rendered soundly, or
|
|
452
|
+
``negation_as_failure`` is not one of the documented values.
|
|
453
|
+
|
|
454
|
+
Example:
|
|
455
|
+
>>> from unicode_logic_kit.fol.nodes import Atom, Variable, Implies, Quantifier
|
|
456
|
+
>>> from unicode_logic_kit.fol.prolog_export import formula_to_prolog_clause
|
|
457
|
+
>>> x = Variable("x")
|
|
458
|
+
>>> clause = Quantifier("∀", x, Implies(Atom("Human", [x]), Atom("Mortal", [x])))
|
|
459
|
+
>>> formula_to_prolog_clause(clause)
|
|
460
|
+
'mortal(V0) :- human(V0).'
|
|
461
|
+
"""
|
|
462
|
+
if negation_as_failure not in ("refuse", "classical"):
|
|
463
|
+
raise PrologExportError(
|
|
464
|
+
"formula_to_prolog_clause: unknown negation_as_failure="
|
|
465
|
+
f"{negation_as_failure!r}, expected 'refuse' or 'classical'")
|
|
466
|
+
|
|
467
|
+
matrix = _strip_foralls(node)
|
|
468
|
+
|
|
469
|
+
if isinstance(matrix, Atom):
|
|
470
|
+
names = _variable_names(free_variables(matrix))
|
|
471
|
+
return _render_atom(matrix, names) + "."
|
|
472
|
+
|
|
473
|
+
if isinstance(matrix, Implies):
|
|
474
|
+
body, head = matrix.left, matrix.right
|
|
475
|
+
if not isinstance(head, Atom):
|
|
476
|
+
raise PrologExportError(
|
|
477
|
+
"formula_to_prolog_clause: the head of a rule must be a "
|
|
478
|
+
f"single atom, got {type(head).__name__} "
|
|
479
|
+
f"({head.to_unicode_str()!r}) — a disjunctive or otherwise "
|
|
480
|
+
"compound head has no Prolog reading (a Prolog rule has "
|
|
481
|
+
"exactly one head literal)")
|
|
482
|
+
unbound = free_variables(head) - free_variables(body)
|
|
483
|
+
if unbound:
|
|
484
|
+
unbound_str = ", ".join(sorted(v.name for v in unbound))
|
|
485
|
+
raise PrologExportError(
|
|
486
|
+
f"formula_to_prolog_clause: the head variable(s) {unbound_str} "
|
|
487
|
+
"do not occur in the body — a Prolog rule with an unbound "
|
|
488
|
+
"head variable does not mean what the universally-quantified "
|
|
489
|
+
"formula meant (Prolog would leave it ranging over every "
|
|
490
|
+
"value at that argument position instead)")
|
|
491
|
+
names = _variable_names(free_variables(matrix))
|
|
492
|
+
body_text = _render_body(body, names, negation_as_failure)
|
|
493
|
+
head_text = _render_atom(head, names)
|
|
494
|
+
return f"{head_text} :- {body_text}."
|
|
495
|
+
|
|
496
|
+
hint = _unsupported_hint(matrix)
|
|
497
|
+
horn_note = ""
|
|
498
|
+
try:
|
|
499
|
+
if is_horn(node):
|
|
500
|
+
horn_note = (
|
|
501
|
+
" Note: is_horn(node) is True — the SKOLEMISED/CNF clausal "
|
|
502
|
+
"form happens to be Horn — but skolemize() is "
|
|
503
|
+
"satisfiability-preserving, not equivalence-preserving (see "
|
|
504
|
+
"unicode_logic_kit.fol.normalforms's docstring), so emitting "
|
|
505
|
+
"those clauses instead of this formula would silently swap "
|
|
506
|
+
"equivalence for mere equisatisfiability. "
|
|
507
|
+
"formula_to_prolog_clause only exports a formula that is "
|
|
508
|
+
"DIRECTLY a fact or a possibly ∀-prefixed body → head "
|
|
509
|
+
"implication; rewrite the formula into that shape by hand "
|
|
510
|
+
"if it should be exported.")
|
|
511
|
+
except Exception:
|
|
512
|
+
pass
|
|
513
|
+
raise PrologExportError(
|
|
514
|
+
f"formula_to_prolog_clause: {type(matrix).__name__} "
|
|
515
|
+
f"({node.to_unicode_str()!r}) has no Prolog clause reading{hint} — "
|
|
516
|
+
"only a fact (a bare atom) or a definite/normal clause (a possibly "
|
|
517
|
+
"∀-prefixed body → head implication) is exported; see the module "
|
|
518
|
+
f"docstring for the accepted fragment.{horn_note}")
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def _conjuncts(node: Node) -> List[Node]:
|
|
522
|
+
"""Flatten a top-level ``∧`` tree, left to right — the same shallow
|
|
523
|
+
split :mod:`unicode_logic_kit.ilp.readback` and
|
|
524
|
+
:mod:`unicode_logic_kit.fol.normalforms` each keep a private copy of
|
|
525
|
+
rather than importing, so a change to the convention shows up as a
|
|
526
|
+
failing test in each user instead of leaking silently."""
|
|
527
|
+
if isinstance(node, And):
|
|
528
|
+
return _conjuncts(node.left) + _conjuncts(node.right)
|
|
529
|
+
return [node]
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def formula_to_prolog_program(nodes_or_conjunction: Union[Node, Iterable[Node]],
|
|
533
|
+
*, negation_as_failure: str = "refuse") -> str:
|
|
534
|
+
"""Render several clauses as one Prolog program, one clause per line.
|
|
535
|
+
|
|
536
|
+
Args:
|
|
537
|
+
nodes_or_conjunction: either a single formula whose top level is an
|
|
538
|
+
``∧`` of several fact/rule-shaped conjuncts (split the same way
|
|
539
|
+
:func:`~unicode_logic_kit.fol.prolog_input.parse_prolog_program`'s
|
|
540
|
+
multi-clause reading returns them separately rather than
|
|
541
|
+
disjoined), or any iterable of such formulas directly.
|
|
542
|
+
negation_as_failure: forwarded to :func:`formula_to_prolog_clause`
|
|
543
|
+
for every clause.
|
|
544
|
+
|
|
545
|
+
Returns:
|
|
546
|
+
The clauses' text, one per line, each ending in ``"."``.
|
|
547
|
+
|
|
548
|
+
Raises:
|
|
549
|
+
PrologExportError: as :func:`formula_to_prolog_clause`, for whichever
|
|
550
|
+
clause fails first, with that clause's position and text appended
|
|
551
|
+
— mirroring :func:`~unicode_logic_kit.fol.prolog_input.parse_prolog_program`'s
|
|
552
|
+
own ``"(in clause: ...)"`` suffix on the way in, so a failure in a
|
|
553
|
+
program of several clauses names which one, not just why.
|
|
554
|
+
"""
|
|
555
|
+
clauses = (_conjuncts(nodes_or_conjunction) if isinstance(nodes_or_conjunction, Node)
|
|
556
|
+
else list(nodes_or_conjunction))
|
|
557
|
+
rendered = []
|
|
558
|
+
for index, clause in enumerate(clauses):
|
|
559
|
+
try:
|
|
560
|
+
rendered.append(formula_to_prolog_clause(
|
|
561
|
+
clause, negation_as_failure=negation_as_failure))
|
|
562
|
+
except PrologExportError as exc:
|
|
563
|
+
raise PrologExportError(
|
|
564
|
+
f"{exc} (in clause {index + 1} of {len(clauses)}: "
|
|
565
|
+
f"{clause.to_unicode_str()!r})")
|
|
566
|
+
return "\n".join(rendered)
|