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,1114 @@
|
|
|
1
|
+
"""Independent first-order resolution *proof checker* (not a searcher).
|
|
2
|
+
|
|
3
|
+
Where :mod:`unicode_logic_kit.atp.resolution` *searches* for a refutation (given
|
|
4
|
+
clause saturation, unification-driven), this module *certifies* one that was
|
|
5
|
+
produced elsewhere: an external searcher — a research engine comparing
|
|
6
|
+
calculi, a human, another prover — hands over a :class:`ResolutionDerivation`
|
|
7
|
+
(a flat list of clauses each justified as an input clause, a binary resolvent,
|
|
8
|
+
or a factor of an earlier clause) and :func:`verify_resolution_proof` decides,
|
|
9
|
+
independently, whether every step genuinely follows. This is the same
|
|
10
|
+
division of labour as :mod:`unicode_logic_kit.atp.fitch` (``check_proof``) and
|
|
11
|
+
:mod:`unicode_logic_kit.atp.sequent` (``check_sequent_proof``): a derivation is
|
|
12
|
+
only as trustworthy as its checker, so the checker must never share code with
|
|
13
|
+
whatever produced the derivation.
|
|
14
|
+
|
|
15
|
+
Clauses are represented exactly as :func:`unicode_logic_kit.atp.resolution.to_clauses`
|
|
16
|
+
produces them: a clause is a ``frozenset`` of literals, each literal a kit
|
|
17
|
+
:class:`~unicode_logic_kit.fol.nodes.Atom` (positive) or
|
|
18
|
+
:class:`~unicode_logic_kit.fol.nodes.Not` wrapping one (negative); variables are
|
|
19
|
+
implicitly universally quantified over the whole clause. Steps are 1-indexed in
|
|
20
|
+
derivation order — ``step.index`` must equal its 1-based position in
|
|
21
|
+
``derivation.steps`` — mirroring :mod:`atp.fitch`'s ``1..N`` line-numbering
|
|
22
|
+
convention.
|
|
23
|
+
|
|
24
|
+
Checking semantics (soundness is the only job; the searcher's search *strategy*
|
|
25
|
+
— which pair it picked, in what order — is irrelevant and not reconstructed):
|
|
26
|
+
|
|
27
|
+
- ``"input"`` (0 parents): the step's clause must be a *variant* (see below) of
|
|
28
|
+
one of ``derivation.inputs``.
|
|
29
|
+
- ``"resolve"`` (2 parents ``i, j``, both ``< step.index``): the two parent
|
|
30
|
+
clauses are standardized apart (deterministic disjoint renaming — required
|
|
31
|
+
for soundness, since two clauses may share a variable name that names
|
|
32
|
+
logically unrelated individuals), and the step is accepted iff *some* pair
|
|
33
|
+
of complementary-polarity literals has unifiable atoms whose mgu, applied to
|
|
34
|
+
the union of the two remainder clauses, is a variant of the stated clause.
|
|
35
|
+
- ``"factor"`` (1 parent ``i``): accepted iff *some* pair of same-polarity
|
|
36
|
+
literals in the parent unifies, and the mgu applied to the whole clause is a
|
|
37
|
+
variant of the stated clause.
|
|
38
|
+
- ``"paramodulate"`` (2 parents ``i, j`` — ``i`` the equation, ``j`` the
|
|
39
|
+
rewrite target): unlike ``"resolve"``/``"factor"``, which search over EVERY
|
|
40
|
+
candidate literal pair, this rule and the two below require the
|
|
41
|
+
derivation to name its literals EXPLICITLY (``step.eq_literal``,
|
|
42
|
+
``step.target_literal``, as they appear in the cited parents' OWN clauses —
|
|
43
|
+
not in some checker-internal renamed form) plus ``step.direction`` (``"lr"``
|
|
44
|
+
uses the equation's first argument as the rewrite source, ``"rl"`` the
|
|
45
|
+
second) and ``step.position`` (a tuple of argument indices addressing the
|
|
46
|
+
rewritten subterm of ``target_literal``'s atom). Nothing about the
|
|
47
|
+
UNIFIER is trusted or stored: the checker re-derives it from scratch by
|
|
48
|
+
unifying the equation's stated side against the subterm it extracts at
|
|
49
|
+
``step.position``, and only accepts the step if that mgu, applied
|
|
50
|
+
everywhere, reproduces a variant of the stated clause. The two parents are
|
|
51
|
+
standardized apart first, exactly as for ``"resolve"``.
|
|
52
|
+
There is deliberately NO ``"self_paramodulate"`` rule: a shared-instance
|
|
53
|
+
shortcut (equation and target from ONE clause instantiation, both dropped)
|
|
54
|
+
is unsound — {u ≈ v, L[u]} would certify {L[v]}, false in a model that
|
|
55
|
+
satisfies the clause via L[u] alone with u ≠ v. A clause paramodulating
|
|
56
|
+
into itself must be certified as ``"paramodulate"`` with the clause cited
|
|
57
|
+
as BOTH parents (the checker standardizes the two citations apart).
|
|
58
|
+
- ``"reflexivity"`` (1 parent ``i``): ``step.eq_literal`` must be a NEGATIVE
|
|
59
|
+
equality literal ``¬(u=v)`` of the cited clause whose two sides unify (own
|
|
60
|
+
``_unify``); the mgu applied to the clause minus that literal must be a
|
|
61
|
+
variant of the stated clause.
|
|
62
|
+
- ``"demodulate"`` (2 parents ``i, j`` — ``i`` the target, ``j`` a UNIT
|
|
63
|
+
equation clause): ``step.eq_literal`` must be the sole literal of the
|
|
64
|
+
cited unit clause ``j`` (a non-unit clause can never license an
|
|
65
|
+
unconditional rewrite — it only asserts a disjunction). Unlike
|
|
66
|
+
``"paramodulate"``, the rewrite uses one-sided MATCHING, not unification
|
|
67
|
+
(the equation's stated side must literally instantiate, via its own free
|
|
68
|
+
variables only, the subterm at ``step.position`` — the target's variables
|
|
69
|
+
are never bound; the matcher is applied to the other side in ONE
|
|
70
|
+
simultaneous step, :func:`_apply_matcher`, because its images are terms of
|
|
71
|
+
the target and may be spelled like the equation's own variables), and the
|
|
72
|
+
orientation is re-checked with this module's own
|
|
73
|
+
term order (``_term_gt`` — see :mod:`atp.resolution`'s module docstring for
|
|
74
|
+
the order's definition): a direction that does not strictly decrease under
|
|
75
|
+
it is rejected, independent of what the derivation claims. No standardizing
|
|
76
|
+
apart is needed here (see the module's own note on that, near ``_match_term``).
|
|
77
|
+
- ``"truth_constants"`` (1 parent ``i``): the stated clause is the cited clause
|
|
78
|
+
with some of its literals removed, and every removed literal is ``$false`` or
|
|
79
|
+
``¬$true`` (false in every interpretation), so ``{$false}`` gives the empty
|
|
80
|
+
clause and ``{P, $false}`` gives ``{P}``; removing any other literal is
|
|
81
|
+
rejected.
|
|
82
|
+
- The empty step list is ``ok=True, refuted=False`` (a derivation may verify
|
|
83
|
+
ok without proving anything); ``refuted`` is True iff some *successfully
|
|
84
|
+
verified* step's clause is empty, tracked independently of whether a later
|
|
85
|
+
step fails (so a bogus tail does not retroactively hide a genuine
|
|
86
|
+
refutation reached earlier).
|
|
87
|
+
|
|
88
|
+
Two deliberate independence requirements (the checker's raison d'être — an
|
|
89
|
+
external searcher must be certifiable *without trusting it*, and in particular
|
|
90
|
+
without trusting any inference machinery it might also use):
|
|
91
|
+
|
|
92
|
+
1. **Unification is reimplemented from scratch here** (:func:`_unify` /
|
|
93
|
+
:func:`_apply`, Robinson's algorithm with an occurs-check) instead of
|
|
94
|
+
importing :mod:`unicode_logic_kit.fol.unification`. A searcher built on top of
|
|
95
|
+
the kit's own ``unify`` must not be checked by the very same unification
|
|
96
|
+
code — a bug shared between searcher and checker would be invisible to
|
|
97
|
+
both. This is checker independence in the sense of Sicherheitsanforderungen
|
|
98
|
+
wie FA-5.2 des Zielprojekts: der Prüfer teilt keinen Code mit dem Sucher
|
|
99
|
+
("the checker shares no code with the searcher"). ``tests/test_resolution_check.py``
|
|
100
|
+
holds a differential battery: on unifiable/non-unifiable atom pairs
|
|
101
|
+
(occurs-check both directions, function nesting, repeated variables),
|
|
102
|
+
:func:`_unify` and :func:`unicode_logic_kit.fol.unification.unify` must agree
|
|
103
|
+
on unifiability, and where both succeed each substitution must equalize its
|
|
104
|
+
own two atoms.
|
|
105
|
+
2. **Variant (alpha-equivalence) checking is exact, not an approximation.**
|
|
106
|
+
Two clauses are *variants* iff there is a bijection between their literals
|
|
107
|
+
together with a bijection on variable names under which corresponding
|
|
108
|
+
literals become syntactically identical — this is implemented as an
|
|
109
|
+
explicit backtracking search over literal correspondences
|
|
110
|
+
(:func:`_is_variant`), not a canonical/sorted-form shortcut (which would be
|
|
111
|
+
wrong: e.g. sorting literals by a variable-name-sensitive key does not
|
|
112
|
+
commute with the variable renaming a real variant requires). This matters
|
|
113
|
+
because both ``"input"`` and the resolvent/factor of ``"resolve"``/``"factor"``
|
|
114
|
+
are checked *up to variant*, not up to literal `==`: a searcher's exact
|
|
115
|
+
choice of variable spelling must never cause a genuinely correct step to be
|
|
116
|
+
rejected, while a real structural mismatch (different arity, a constant
|
|
117
|
+
where a variable was needed, an unmatched literal) must still be caught.
|
|
118
|
+
|
|
119
|
+
Public API: :class:`ResolutionStep`, :class:`ResolutionDerivation`,
|
|
120
|
+
:class:`ResolutionCheckResult`, the checkers :func:`verify_resolution_proof` /
|
|
121
|
+
:func:`check_resolution_proof`, and :func:`render_resolution_proof`.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
from dataclasses import dataclass
|
|
125
|
+
from typing import Dict, FrozenSet, Optional, Tuple
|
|
126
|
+
|
|
127
|
+
from ..fol._msfl_nodes import key_text
|
|
128
|
+
from ..fol.nodes import Node, Atom, Not, Variable, Constant, Number, Function
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# ---------------------------------------------------------------------------
|
|
132
|
+
# Derivation objects
|
|
133
|
+
# ---------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
@dataclass(frozen=True)
|
|
136
|
+
class ResolutionStep:
|
|
137
|
+
"""One line of a resolution derivation: a clause and how it was obtained.
|
|
138
|
+
|
|
139
|
+
``rule`` is one of ``"input"`` (0 parents — the clause must be a variant
|
|
140
|
+
of one of the derivation's inputs), ``"resolve"`` (2 parents — a binary
|
|
141
|
+
resolvent of the two cited earlier clauses), ``"factor"`` (1 parent — a
|
|
142
|
+
factor of the one cited earlier clause), the three equality rules
|
|
143
|
+
``"paramodulate"``/``"reflexivity"``/``"demodulate"``
|
|
144
|
+
(see the module docstring for each — including why a
|
|
145
|
+
``"self_paramodulate"`` rule deliberately does NOT exist), or
|
|
146
|
+
``"truth_constants"`` (1 parent — the cited clause with some of its literals
|
|
147
|
+
that are false in every interpretation, ``$false`` and ``¬$true``, removed).
|
|
148
|
+
``parents``
|
|
149
|
+
holds the 1-based
|
|
150
|
+
``index`` values of the cited earlier steps, in citation order (so for
|
|
151
|
+
``"resolve"`` the pair is *not* order-sensitive — both orderings of the
|
|
152
|
+
complementary literal are tried by the checker; the equality rules
|
|
153
|
+
ARE order-sensitive — see below and the module docstring).
|
|
154
|
+
|
|
155
|
+
The four extra fields below are used only by the equality rules (and left
|
|
156
|
+
at their defaults — ``None``/``None``/``None``/``()`` — for
|
|
157
|
+
``"input"``/``"resolve"``/``"factor"``, keeping old derivations valid
|
|
158
|
+
unchanged):
|
|
159
|
+
|
|
160
|
+
- ``eq_literal``: the equality-atom literal the step consumes, exactly as
|
|
161
|
+
it appears in its owning parent clause (parents[0] for
|
|
162
|
+
``"paramodulate"``/``"reflexivity"``,
|
|
163
|
+
parents[1] for ``"demodulate"``) — POSITIVE for
|
|
164
|
+
``"paramodulate"``/``"demodulate"``, NEGATIVE
|
|
165
|
+
for ``"reflexivity"``.
|
|
166
|
+
- ``target_literal``: the literal being rewritten (unused by
|
|
167
|
+
``"reflexivity"``, which only ever removes ``eq_literal`` itself) —
|
|
168
|
+
for ``"paramodulate"``/``"demodulate"`` this is a literal of the OTHER
|
|
169
|
+
parent (parents[1]/parents[0] respectively).
|
|
170
|
+
- ``direction``: ``"lr"`` uses ``eq_literal``'s first argument as the
|
|
171
|
+
rewrite source and its second as the replacement, ``"rl"`` the reverse.
|
|
172
|
+
- ``position``: a tuple of argument indices addressing the rewritten
|
|
173
|
+
subterm within ``target_literal``'s atom (``()`` would address the
|
|
174
|
+
atom itself, never valid — a position always descends into at least
|
|
175
|
+
one argument first).
|
|
176
|
+
|
|
177
|
+
``clause`` is coerced to a ``frozenset``, ``parents``/``position`` to
|
|
178
|
+
``tuple``\\ s, so the step stays hashable.
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
index: int
|
|
182
|
+
clause: FrozenSet[Node]
|
|
183
|
+
rule: str
|
|
184
|
+
parents: Tuple[int, ...] = ()
|
|
185
|
+
eq_literal: Optional[Node] = None
|
|
186
|
+
target_literal: Optional[Node] = None
|
|
187
|
+
direction: Optional[str] = None
|
|
188
|
+
position: Tuple[int, ...] = ()
|
|
189
|
+
|
|
190
|
+
def __post_init__(self):
|
|
191
|
+
"""Coerce ``clause``/``parents``/``position`` for hashability."""
|
|
192
|
+
object.__setattr__(self, "clause", frozenset(self.clause))
|
|
193
|
+
object.__setattr__(self, "parents", tuple(self.parents))
|
|
194
|
+
object.__setattr__(self, "position", tuple(self.position))
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@dataclass(frozen=True)
|
|
198
|
+
class ResolutionDerivation:
|
|
199
|
+
"""A full resolution derivation: the original clauses plus a proof of steps.
|
|
200
|
+
|
|
201
|
+
``inputs`` is the set of clauses the ``"input"`` steps are licensed to
|
|
202
|
+
restate (up to variant) — typically the clausal form of the premises and
|
|
203
|
+
the negated conclusion, as :func:`unicode_logic_kit.atp.resolution.to_clauses`
|
|
204
|
+
would produce them, though the checker does not require or re-derive that
|
|
205
|
+
provenance; it only checks that every ``"input"`` step matches one of
|
|
206
|
+
them. ``steps`` is the ordered derivation body (see :class:`ResolutionStep`).
|
|
207
|
+
"""
|
|
208
|
+
|
|
209
|
+
inputs: Tuple[FrozenSet[Node], ...] = ()
|
|
210
|
+
steps: Tuple[ResolutionStep, ...] = ()
|
|
211
|
+
|
|
212
|
+
def __post_init__(self):
|
|
213
|
+
"""Coerce ``inputs`` to a tuple of frozensets and ``steps`` to a tuple."""
|
|
214
|
+
object.__setattr__(self, "inputs", tuple(frozenset(c) for c in self.inputs))
|
|
215
|
+
object.__setattr__(self, "steps", tuple(self.steps))
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
@dataclass(frozen=True)
|
|
219
|
+
class ResolutionCheckResult:
|
|
220
|
+
"""The outcome of checking a derivation.
|
|
221
|
+
|
|
222
|
+
``ok`` is True iff every step up to (and including) the point of failure —
|
|
223
|
+
i.e. every step, if there was no failure — is licensed by its rule.
|
|
224
|
+
``error_index``/``error`` name the first offending step (``error`` in the
|
|
225
|
+
style ``"step N: reason"``) or are ``None`` on success. ``refuted`` is True
|
|
226
|
+
iff some *successfully verified* step derives the empty clause — computed
|
|
227
|
+
independently of ``ok``, so a refutation reached before a later malformed
|
|
228
|
+
step is still reported.
|
|
229
|
+
"""
|
|
230
|
+
|
|
231
|
+
ok: bool
|
|
232
|
+
error_index: Optional[int]
|
|
233
|
+
error: Optional[str]
|
|
234
|
+
refuted: bool
|
|
235
|
+
|
|
236
|
+
def __bool__(self) -> bool:
|
|
237
|
+
"""A ResolutionCheckResult is truthy iff the derivation checked out."""
|
|
238
|
+
return self.ok
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
# ---------------------------------------------------------------------------
|
|
242
|
+
# Independent unification (Robinson's algorithm, occurs-check) — see module
|
|
243
|
+
# docstring, independence requirement 1: this must NOT import or call
|
|
244
|
+
# unicode_logic_kit.fol.unification.unify. A substitution is a plain dict
|
|
245
|
+
# mapping a variable NAME (str) to a term Node, exactly as in that module,
|
|
246
|
+
# but every function below is a from-scratch reimplementation.
|
|
247
|
+
# ---------------------------------------------------------------------------
|
|
248
|
+
|
|
249
|
+
def _apply(node: Node, subst: Dict[str, Node]) -> Node:
|
|
250
|
+
"""Apply ``subst`` to ``node``, following chained bindings, without mutation.
|
|
251
|
+
|
|
252
|
+
This reads a UNIFIER's output, whose bindings are triangular (``{x: y, y: a}`` says
|
|
253
|
+
``x`` is ``a``), so the chain has to be followed. It is NOT how a one-sided matcher is
|
|
254
|
+
applied: the images of a matcher are terms of the target, and following them reads a
|
|
255
|
+
variable of the target as a variable of the pattern that happens to be spelled the
|
|
256
|
+
same (see :func:`_apply_matcher`).
|
|
257
|
+
"""
|
|
258
|
+
if isinstance(node, Variable):
|
|
259
|
+
if node.name in subst:
|
|
260
|
+
return _apply(subst[node.name], subst)
|
|
261
|
+
return node
|
|
262
|
+
if isinstance(node, (Constant, Number)):
|
|
263
|
+
return node
|
|
264
|
+
if isinstance(node, Function):
|
|
265
|
+
return Function(node.name, [_apply(a, subst) for a in node.args])
|
|
266
|
+
if isinstance(node, Atom):
|
|
267
|
+
return Atom(node.predicate, [_apply(a, subst) for a in node.args])
|
|
268
|
+
raise TypeError(f"_apply: unsupported node type {type(node).__name__}")
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _apply_literal(literal: Node, subst: Dict[str, Node]) -> Node:
|
|
272
|
+
"""Apply ``subst`` to a literal (``Atom`` or ``Not(Atom)``), preserving polarity."""
|
|
273
|
+
if isinstance(literal, Not):
|
|
274
|
+
return Not(_apply(literal.formula, subst))
|
|
275
|
+
return _apply(literal, subst)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _apply_matcher(node: Node, matcher: Dict[str, Node]) -> Node:
|
|
279
|
+
"""Apply the one-sided matcher ``matcher`` to ``node`` in ONE simultaneous step.
|
|
280
|
+
|
|
281
|
+
A matcher (:func:`_match_term`) maps each variable of the PATTERN to a subterm of the
|
|
282
|
+
TARGET. The target's own variables are held fixed, not bound, and nothing is standardized
|
|
283
|
+
apart, so an image may mention a variable spelled like one the matcher binds: the
|
|
284
|
+
pattern ``f(x, y)`` against ``f(y, z)`` gives ``{x: y, y: z}``, and the instance of
|
|
285
|
+
``g(x)`` is ``g(y)``, with that ``y`` the target's. An image is a finished term and is
|
|
286
|
+
never looked up again; :func:`_apply` would read it as the pattern's ``y`` and answer
|
|
287
|
+
``g(z)``, and it would not end on ``{x: y, y: x}`` or ``{y: y}`` (a matcher that
|
|
288
|
+
is cyclic, or that binds a variable to itself, both arise from a rule matched against a
|
|
289
|
+
clause that shares its variable names).
|
|
290
|
+
|
|
291
|
+
A variable the matcher does not bind is left as it is, and so is every other leaf.
|
|
292
|
+
"""
|
|
293
|
+
if isinstance(node, Variable):
|
|
294
|
+
return matcher.get(node.name, node)
|
|
295
|
+
if isinstance(node, (Constant, Number)):
|
|
296
|
+
return node
|
|
297
|
+
if isinstance(node, Function):
|
|
298
|
+
return Function(node.name, tuple(_apply_matcher(a, matcher) for a in node.args))
|
|
299
|
+
if isinstance(node, Atom):
|
|
300
|
+
return Atom(node.predicate, tuple(_apply_matcher(a, matcher) for a in node.args))
|
|
301
|
+
raise TypeError(f"_apply_matcher: unsupported node type {type(node).__name__}")
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _occurs(name: str, term: Node, subst: Dict[str, Node]) -> bool:
|
|
305
|
+
"""Return True iff variable ``name`` occurs in ``term`` after applying ``subst``.
|
|
306
|
+
|
|
307
|
+
The occurs-check: rejects a binding such as ``x ↦ f(x)`` that would build
|
|
308
|
+
an infinite (cyclic) term.
|
|
309
|
+
"""
|
|
310
|
+
term = _apply(term, subst)
|
|
311
|
+
if isinstance(term, Variable):
|
|
312
|
+
return term.name == name
|
|
313
|
+
if isinstance(term, Function):
|
|
314
|
+
return any(_occurs(name, a, subst) for a in term.args)
|
|
315
|
+
return False
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _bind(name: str, term: Node, subst: Dict[str, Node]) -> Optional[Dict[str, Node]]:
|
|
319
|
+
"""Extend ``subst`` with ``name ↦ term`` (occurs-checked); returns a new dict or None.
|
|
320
|
+
|
|
321
|
+
Binding a variable to itself is a no-op that returns ``subst`` unchanged.
|
|
322
|
+
"""
|
|
323
|
+
if isinstance(term, Variable) and term.name == name:
|
|
324
|
+
return subst
|
|
325
|
+
if _occurs(name, term, subst):
|
|
326
|
+
return None
|
|
327
|
+
new_subst = dict(subst)
|
|
328
|
+
new_subst[name] = _apply(term, subst)
|
|
329
|
+
return new_subst
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _leaf_value(node: Node):
|
|
333
|
+
"""Return the comparable payload of a leaf node (Constant name / Number value)."""
|
|
334
|
+
if isinstance(node, Constant):
|
|
335
|
+
return node.name
|
|
336
|
+
if isinstance(node, Number):
|
|
337
|
+
return node.value
|
|
338
|
+
return None
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _unify_args(args1, args2, subst: Dict[str, Node]) -> Optional[Dict[str, Node]]:
|
|
342
|
+
"""Unify two equal-length argument lists left-to-right, threading ``subst``."""
|
|
343
|
+
for a, b in zip(args1, args2):
|
|
344
|
+
subst = _unify(a, b, subst)
|
|
345
|
+
if subst is None:
|
|
346
|
+
return None
|
|
347
|
+
return subst
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _unify(t1: Node, t2: Node, subst: Optional[Dict[str, Node]] = None) -> Optional[Dict[str, Node]]:
|
|
351
|
+
"""Most general unifier of ``t1`` and ``t2``, or None (Robinson, occurs-checked).
|
|
352
|
+
|
|
353
|
+
Independent reimplementation of :func:`unicode_logic_kit.fol.unification.unify`
|
|
354
|
+
(see module docstring, independence requirement 1) — same term language
|
|
355
|
+
(``Variable``/``Constant``/``Number``/``Function``, plus ``Atom`` for the
|
|
356
|
+
literal case) and the same algorithm, but sharing no code with it.
|
|
357
|
+
"""
|
|
358
|
+
if subst is None:
|
|
359
|
+
subst = {}
|
|
360
|
+
|
|
361
|
+
t1 = _apply(t1, subst)
|
|
362
|
+
t2 = _apply(t2, subst)
|
|
363
|
+
|
|
364
|
+
if isinstance(t1, Variable):
|
|
365
|
+
return _bind(t1.name, t2, subst)
|
|
366
|
+
if isinstance(t2, Variable):
|
|
367
|
+
return _bind(t2.name, t1, subst)
|
|
368
|
+
|
|
369
|
+
if isinstance(t1, (Constant, Number)) or isinstance(t2, (Constant, Number)):
|
|
370
|
+
if type(t1) is type(t2) and _leaf_value(t1) == _leaf_value(t2):
|
|
371
|
+
return subst
|
|
372
|
+
return None
|
|
373
|
+
|
|
374
|
+
if isinstance(t1, Function) and isinstance(t2, Function):
|
|
375
|
+
if t1.name != t2.name or len(t1.args) != len(t2.args):
|
|
376
|
+
return None
|
|
377
|
+
return _unify_args(t1.args, t2.args, subst)
|
|
378
|
+
|
|
379
|
+
if isinstance(t1, Atom) and isinstance(t2, Atom):
|
|
380
|
+
if t1.predicate != t2.predicate or len(t1.args) != len(t2.args):
|
|
381
|
+
return None
|
|
382
|
+
return _unify_args(t1.args, t2.args, subst)
|
|
383
|
+
|
|
384
|
+
return None
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
# ---------------------------------------------------------------------------
|
|
388
|
+
# Exact variant (alpha-equivalence) checking — see module docstring,
|
|
389
|
+
# independence requirement 2: a backtracking search for a literal
|
|
390
|
+
# correspondence plus a variable-name BIJECTION, not a canonical-form
|
|
391
|
+
# shortcut.
|
|
392
|
+
# ---------------------------------------------------------------------------
|
|
393
|
+
|
|
394
|
+
def _term_alpha_match(t1: Node, t2: Node, fwd: Dict[str, str], bwd: Dict[str, str]):
|
|
395
|
+
"""Try to extend the variable bijection ``(fwd, bwd)`` matching ``t1`` (A-side)
|
|
396
|
+
onto ``t2`` (B-side); returns the extended ``(fwd, bwd)`` pair or None.
|
|
397
|
+
|
|
398
|
+
``fwd``/``bwd`` map A-variable-name -> B-variable-name and back; neither
|
|
399
|
+
input dict is mutated (a matched extension is a fresh copy), so a failed
|
|
400
|
+
branch never corrupts an earlier partial match a backtracking caller may
|
|
401
|
+
still want to try again on a different literal.
|
|
402
|
+
"""
|
|
403
|
+
if isinstance(t1, Variable) and isinstance(t2, Variable):
|
|
404
|
+
mapped = fwd.get(t1.name)
|
|
405
|
+
if mapped is not None:
|
|
406
|
+
return (fwd, bwd) if mapped == t2.name else None
|
|
407
|
+
if t2.name in bwd:
|
|
408
|
+
return None # t2 already claimed by a different A-variable: not injective
|
|
409
|
+
new_fwd = dict(fwd)
|
|
410
|
+
new_fwd[t1.name] = t2.name
|
|
411
|
+
new_bwd = dict(bwd)
|
|
412
|
+
new_bwd[t2.name] = t1.name
|
|
413
|
+
return new_fwd, new_bwd
|
|
414
|
+
if isinstance(t1, Variable) or isinstance(t2, Variable):
|
|
415
|
+
return None # a variable can only match a variable — constants are not variables
|
|
416
|
+
if isinstance(t1, Constant) and isinstance(t2, Constant):
|
|
417
|
+
return (fwd, bwd) if t1.name == t2.name else None
|
|
418
|
+
if isinstance(t1, Number) and isinstance(t2, Number):
|
|
419
|
+
return (fwd, bwd) if t1.value == t2.value else None
|
|
420
|
+
if isinstance(t1, Function) and isinstance(t2, Function):
|
|
421
|
+
if t1.name != t2.name or len(t1.args) != len(t2.args):
|
|
422
|
+
return None
|
|
423
|
+
for a1, a2 in zip(t1.args, t2.args):
|
|
424
|
+
res = _term_alpha_match(a1, a2, fwd, bwd)
|
|
425
|
+
if res is None:
|
|
426
|
+
return None
|
|
427
|
+
fwd, bwd = res
|
|
428
|
+
return fwd, bwd
|
|
429
|
+
return None # mismatched node kinds (e.g. Constant vs Number)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _lit_alpha_match(l1: Node, l2: Node, fwd: Dict[str, str], bwd: Dict[str, str]):
|
|
433
|
+
"""As :func:`_term_alpha_match`, but for a whole literal (``Atom``/``Not(Atom)``).
|
|
434
|
+
|
|
435
|
+
Polarity must match, and the underlying atoms' predicate and arity must
|
|
436
|
+
match, before any variable matching is attempted.
|
|
437
|
+
"""
|
|
438
|
+
neg1, neg2 = isinstance(l1, Not), isinstance(l2, Not)
|
|
439
|
+
if neg1 != neg2:
|
|
440
|
+
return None
|
|
441
|
+
a1 = l1.formula if neg1 else l1
|
|
442
|
+
a2 = l2.formula if neg2 else l2
|
|
443
|
+
if not (isinstance(a1, Atom) and isinstance(a2, Atom)):
|
|
444
|
+
return None
|
|
445
|
+
if a1.predicate != a2.predicate or len(a1.args) != len(a2.args):
|
|
446
|
+
return None
|
|
447
|
+
for x, y in zip(a1.args, a2.args):
|
|
448
|
+
res = _term_alpha_match(x, y, fwd, bwd)
|
|
449
|
+
if res is None:
|
|
450
|
+
return None
|
|
451
|
+
fwd, bwd = res
|
|
452
|
+
return fwd, bwd
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def _is_variant(a: FrozenSet[Node], b: FrozenSet[Node]) -> bool:
|
|
456
|
+
"""Return True iff clause ``a`` is a variant (alpha-equivalent copy) of clause ``b``.
|
|
457
|
+
|
|
458
|
+
Backtracking search for a bijection between the literals of ``a`` and
|
|
459
|
+
``b`` together with a bijection on variable names under which every
|
|
460
|
+
matched literal pair becomes syntactically identical. Deliberately exact
|
|
461
|
+
(not a sorted/canonical-form shortcut — see the module docstring): e.g.
|
|
462
|
+
``{P(x,y), P(y,x)}`` IS a variant of ``{P(u,v), P(v,u)}`` (a 2-cycle
|
|
463
|
+
literal correspondence with a 2-variable bijection), ``{P(x,x)}`` is NOT a
|
|
464
|
+
variant of ``{P(x,y)}`` (no single variable can equal two distinct ones),
|
|
465
|
+
and ``{P(x)}`` is NOT a variant of ``{P(a)}`` (a constant is never matched
|
|
466
|
+
by a variable). Worst case is factorial in clause size, which is fine for
|
|
467
|
+
the small clauses resolution steps actually produce.
|
|
468
|
+
"""
|
|
469
|
+
la, lb = list(a), list(b)
|
|
470
|
+
if len(la) != len(lb):
|
|
471
|
+
return False
|
|
472
|
+
used = [False] * len(lb)
|
|
473
|
+
|
|
474
|
+
def backtrack(i: int, fwd: Dict[str, str], bwd: Dict[str, str]) -> bool:
|
|
475
|
+
if i == len(la):
|
|
476
|
+
return True
|
|
477
|
+
for j in range(len(lb)):
|
|
478
|
+
if used[j]:
|
|
479
|
+
continue
|
|
480
|
+
res = _lit_alpha_match(la[i], lb[j], fwd, bwd)
|
|
481
|
+
if res is None:
|
|
482
|
+
continue
|
|
483
|
+
used[j] = True
|
|
484
|
+
if backtrack(i + 1, res[0], res[1]):
|
|
485
|
+
return True
|
|
486
|
+
used[j] = False
|
|
487
|
+
return False
|
|
488
|
+
|
|
489
|
+
return backtrack(0, {}, {})
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
# ---------------------------------------------------------------------------
|
|
493
|
+
# Standardizing apart (deterministic disjoint renaming of two clauses)
|
|
494
|
+
# ---------------------------------------------------------------------------
|
|
495
|
+
|
|
496
|
+
def _clause_var_names(clause: FrozenSet[Node]) -> set:
|
|
497
|
+
"""Return the set of variable names occurring anywhere in ``clause``."""
|
|
498
|
+
names = set()
|
|
499
|
+
for literal in clause:
|
|
500
|
+
for n in literal.walk():
|
|
501
|
+
if isinstance(n, Variable):
|
|
502
|
+
names.add(n.name)
|
|
503
|
+
return names
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def _rename_term(node: Node, mapping: Dict[str, Variable]) -> Node:
|
|
507
|
+
"""Return ``node`` with each Variable renamed via the one-level ``mapping``."""
|
|
508
|
+
if isinstance(node, Variable):
|
|
509
|
+
return mapping.get(node.name, node)
|
|
510
|
+
if isinstance(node, (Constant, Number)):
|
|
511
|
+
return node
|
|
512
|
+
if isinstance(node, Function):
|
|
513
|
+
return Function(node.name, [_rename_term(a, mapping) for a in node.args])
|
|
514
|
+
if isinstance(node, Atom):
|
|
515
|
+
return Atom(node.predicate, [_rename_term(a, mapping) for a in node.args])
|
|
516
|
+
raise TypeError(f"_rename_term: unsupported node type {type(node).__name__}")
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def _rename_literal(literal: Node, mapping: Dict[str, Variable]) -> Node:
|
|
520
|
+
"""Return ``literal`` with each Variable renamed via the one-level ``mapping``."""
|
|
521
|
+
if isinstance(literal, Not):
|
|
522
|
+
return Not(_rename_term(literal.formula, mapping))
|
|
523
|
+
return _rename_term(literal, mapping)
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def _standardize_apart_maps(ca: FrozenSet[Node], cb: FrozenSet[Node]):
|
|
527
|
+
"""As :func:`_standardize_apart`, but also returns the two rename maps
|
|
528
|
+
(original variable name -> fresh :class:`Variable`) used to build the
|
|
529
|
+
standardized-apart clauses.
|
|
530
|
+
|
|
531
|
+
Needed by ``"paramodulate"`` (not
|
|
532
|
+
``"demodulate"``, which needs no standardizing apart — see its checker):
|
|
533
|
+
a derivation names ``eq_literal``/``target_literal`` exactly as they
|
|
534
|
+
appear in the ORIGINAL (pre-rename) parent clauses, so the checker must
|
|
535
|
+
replay the SAME rename to find their counterparts in the standardized
|
|
536
|
+
clauses it works with.
|
|
537
|
+
"""
|
|
538
|
+
names_a = sorted(_clause_var_names(ca))
|
|
539
|
+
names_b = sorted(_clause_var_names(cb))
|
|
540
|
+
map_a = {name: Variable(f"_L{i}") for i, name in enumerate(names_a)}
|
|
541
|
+
map_b = {name: Variable(f"_R{i}") for i, name in enumerate(names_b)}
|
|
542
|
+
new_a = frozenset(_rename_literal(lit, map_a) for lit in ca)
|
|
543
|
+
new_b = frozenset(_rename_literal(lit, map_b) for lit in cb)
|
|
544
|
+
return new_a, new_b, map_a, map_b
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def _standardize_apart(ca: FrozenSet[Node], cb: FrozenSet[Node]):
|
|
548
|
+
"""Rename the variables of ``ca`` and ``cb`` to disjoint fresh names.
|
|
549
|
+
|
|
550
|
+
A soundness requirement before resolving two clauses: each clause's
|
|
551
|
+
variables are implicitly universally quantified *over that clause alone*,
|
|
552
|
+
so a name they happen to share (e.g. both using ``x``) names logically
|
|
553
|
+
unrelated individuals and must not be conflated. Renaming is deterministic
|
|
554
|
+
(sorted by original name, prefixed ``_L``/``_R``) so the check is
|
|
555
|
+
reproducible; the *choice* of fresh names is otherwise immaterial since
|
|
556
|
+
every candidate resolvent is compared up to variant, not up to `==`. A
|
|
557
|
+
thin wrapper over :func:`_standardize_apart_maps` that drops the maps,
|
|
558
|
+
for callers (``"resolve"``) that don't need to translate a
|
|
559
|
+
pre-standardization literal reference.
|
|
560
|
+
"""
|
|
561
|
+
new_a, new_b, _map_a, _map_b = _standardize_apart_maps(ca, cb)
|
|
562
|
+
return new_a, new_b
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
# ---------------------------------------------------------------------------
|
|
566
|
+
# Literal polarity
|
|
567
|
+
# ---------------------------------------------------------------------------
|
|
568
|
+
|
|
569
|
+
def _lit_atom_polarity(literal: Node) -> Optional[Tuple[Atom, bool]]:
|
|
570
|
+
"""Return ``(atom, is_positive)`` for a well-formed literal, or None.
|
|
571
|
+
|
|
572
|
+
A well-formed literal is a bare ``Atom`` (positive) or a ``Not`` wrapping
|
|
573
|
+
exactly one (negative); anything else (e.g. a doubly-negated or non-atomic
|
|
574
|
+
literal) is not a legal clause literal and is returned as None so callers
|
|
575
|
+
can skip it rather than crash — a malformed clause simply matches nothing.
|
|
576
|
+
"""
|
|
577
|
+
if isinstance(literal, Not) and isinstance(literal.formula, Atom):
|
|
578
|
+
return literal.formula, False
|
|
579
|
+
if isinstance(literal, Atom):
|
|
580
|
+
return literal, True
|
|
581
|
+
return None
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def _lit_key(literal: Node) -> str:
|
|
585
|
+
"""Canonical sort key for a literal (its surface form with every constant written by its
|
|
586
|
+
bare name, ``key_text``): search order over a
|
|
587
|
+
clause's literals must be a function of CONTENT, not of frozenset iteration
|
|
588
|
+
order (hash-randomised across processes), so the checker's behaviour and
|
|
589
|
+
:func:`render_resolution_proof`'s output are reproducible run to run. The quotes that the
|
|
590
|
+
text of a formula puts around a constant play no part in the order."""
|
|
591
|
+
return key_text(literal)
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
# ---------------------------------------------------------------------------
|
|
595
|
+
# Equality: an INDEPENDENT reimplementation of the term order, one-sided term
|
|
596
|
+
# matching, and subterm positions/replacement used by the four equality
|
|
597
|
+
# rules below. Independence requirement 1 (see the module docstring) applies
|
|
598
|
+
# here exactly as it does to _unify/_apply: this module must reach the SAME
|
|
599
|
+
# verdicts as unicode_logic_kit.atp.resolution's own _term_gt/_match_term/
|
|
600
|
+
# _term_at/_replace_at on any given input, but shares no code with them.
|
|
601
|
+
# ---------------------------------------------------------------------------
|
|
602
|
+
|
|
603
|
+
def _is_equality_atom(atom: Atom) -> bool:
|
|
604
|
+
"""True iff atom is a binary ``=`` atom."""
|
|
605
|
+
return atom.predicate == "=" and len(atom.args) == 2
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
def _term_weight(term: Node) -> int:
|
|
609
|
+
"""Term order, part 1 — WEIGHT (term size): 1 for a leaf
|
|
610
|
+
(Variable/Constant/Number), 1 + the sum of its arguments' weights for a
|
|
611
|
+
Function. Must be the SAME definition as
|
|
612
|
+
:func:`unicode_logic_kit.atp.resolution._term_weight` (both sides need to
|
|
613
|
+
reach the same orientation verdict on any given equation), reimplemented
|
|
614
|
+
independently here."""
|
|
615
|
+
if isinstance(term, Function):
|
|
616
|
+
return 1 + sum(_term_weight(a) for a in term.args)
|
|
617
|
+
return 1
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def _term_order_key(term: Node):
|
|
621
|
+
"""Term order, part 2 — ties in weight broken lexicographically by the
|
|
622
|
+
term's rendering with every constant written by its bare name (``key_text``)."""
|
|
623
|
+
return (_term_weight(term), key_text(term))
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
def _term_gt(s: Node, t: Node) -> bool:
|
|
627
|
+
"""True iff ``s`` is STRICTLY greater than ``t`` under the module's term
|
|
628
|
+
order (weight, then lexicographic tie-break). See
|
|
629
|
+
:mod:`unicode_logic_kit.atp.resolution`'s module docstring ("Equality"
|
|
630
|
+
section) for the full definition and the soundness argument for why
|
|
631
|
+
checking this on concretely SUBSTITUTED terms (as ``"demodulate"``'s
|
|
632
|
+
checker does) needs no substitution-compatibility closure."""
|
|
633
|
+
return _term_order_key(s) > _term_order_key(t)
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
def _match_term(pattern: Node, target: Node, subst: Dict[str, Node]) -> Optional[Dict[str, Node]]:
|
|
637
|
+
"""One-sided structural match of ``pattern`` against ``target``: PATTERN's
|
|
638
|
+
variables may bind, TARGET is held fixed (its own variables, if any, are
|
|
639
|
+
opaque — never instantiated). A repeated pattern variable must match the
|
|
640
|
+
same target subterm everywhere (checked by structural equality against
|
|
641
|
+
the existing binding). Returns the extended substitution, or None on
|
|
642
|
+
failure. The result is applied with :func:`_apply_matcher`, never with
|
|
643
|
+
:func:`_apply`. Used only by ``"demodulate"``'s checker, to re-derive whether
|
|
644
|
+
the cited equation's stated side genuinely matches the target subterm —
|
|
645
|
+
an independent reimplementation of the same one-sided-matching idea
|
|
646
|
+
:mod:`unicode_logic_kit.atp.resolution` uses for both subsumption and its
|
|
647
|
+
own demodulation (:func:`~unicode_logic_kit.atp.resolution._match_term`),
|
|
648
|
+
sharing no code with it.
|
|
649
|
+
"""
|
|
650
|
+
if isinstance(pattern, Variable):
|
|
651
|
+
if pattern.name in subst:
|
|
652
|
+
return subst if subst[pattern.name] == target else None
|
|
653
|
+
extended = dict(subst)
|
|
654
|
+
extended[pattern.name] = target
|
|
655
|
+
return extended
|
|
656
|
+
if isinstance(pattern, Function):
|
|
657
|
+
if (not isinstance(target, Function)
|
|
658
|
+
or pattern.name != target.name
|
|
659
|
+
or len(pattern.args) != len(target.args)):
|
|
660
|
+
return None
|
|
661
|
+
for p_arg, t_arg in zip(pattern.args, target.args):
|
|
662
|
+
subst = _match_term(p_arg, t_arg, subst)
|
|
663
|
+
if subst is None:
|
|
664
|
+
return None
|
|
665
|
+
return subst
|
|
666
|
+
return subst if pattern == target else None
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
def _term_at(node: Node, position: Tuple[int, ...]) -> Node:
|
|
670
|
+
"""Walk ``position`` (a tuple of argument indices) from ``node`` down to
|
|
671
|
+
the addressed subterm. Raises ``IndexError``/``AttributeError``/
|
|
672
|
+
``TypeError`` on an invalid path (out-of-range index, or descending past
|
|
673
|
+
a leaf) — callers catch these and turn them into a proper
|
|
674
|
+
``"step N: ..."`` error, exactly the outcome a TAMPERED position (one of
|
|
675
|
+
the derivation manipulations these checkers must catch) should produce.
|
|
676
|
+
"""
|
|
677
|
+
for i in position:
|
|
678
|
+
node = node.args[i]
|
|
679
|
+
return node
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
def _replace_at(node: Node, position: Tuple[int, ...], replacement: Node) -> Node:
|
|
683
|
+
"""Return a copy of ``node`` (an :class:`Atom` or :class:`Function`) with
|
|
684
|
+
the subterm at ``position`` replaced by ``replacement``. Raises the same
|
|
685
|
+
exceptions as :func:`_term_at` on an invalid path."""
|
|
686
|
+
if not position:
|
|
687
|
+
return replacement
|
|
688
|
+
i, rest = position[0], position[1:]
|
|
689
|
+
new_args = list(node.args)
|
|
690
|
+
new_args[i] = _replace_at(node.args[i], rest, replacement)
|
|
691
|
+
if isinstance(node, Atom):
|
|
692
|
+
return Atom(node.predicate, new_args)
|
|
693
|
+
return Function(node.name, new_args)
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
# ---------------------------------------------------------------------------
|
|
697
|
+
# Per-rule step checkers: each returns None (licensed) or an error string
|
|
698
|
+
# (WITHOUT the "step N: " prefix, added by the caller).
|
|
699
|
+
# ---------------------------------------------------------------------------
|
|
700
|
+
|
|
701
|
+
def _check_input_step(step: ResolutionStep, inputs: Tuple[FrozenSet[Node], ...]) -> Optional[str]:
|
|
702
|
+
"""``"input"``: the clause must be a variant of one of ``inputs``."""
|
|
703
|
+
for inp in inputs:
|
|
704
|
+
if _is_variant(step.clause, inp):
|
|
705
|
+
return None
|
|
706
|
+
return "'input' clause is not a variant (alpha-equivalent copy) of any derivation input"
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
def _check_resolve_step(step: ResolutionStep, ci: FrozenSet[Node], cj: FrozenSet[Node]) -> Optional[str]:
|
|
710
|
+
"""``"resolve"``: some complementary-polarity literal pair's mgu must give the clause."""
|
|
711
|
+
ci2, cj2 = _standardize_apart(ci, cj)
|
|
712
|
+
found_complementary_pair = False
|
|
713
|
+
for lit1 in sorted(ci2, key=_lit_key):
|
|
714
|
+
parsed1 = _lit_atom_polarity(lit1)
|
|
715
|
+
if parsed1 is None:
|
|
716
|
+
continue
|
|
717
|
+
atom1, pos1 = parsed1
|
|
718
|
+
for lit2 in sorted(cj2, key=_lit_key):
|
|
719
|
+
parsed2 = _lit_atom_polarity(lit2)
|
|
720
|
+
if parsed2 is None:
|
|
721
|
+
continue
|
|
722
|
+
atom2, pos2 = parsed2
|
|
723
|
+
if pos1 == pos2:
|
|
724
|
+
continue
|
|
725
|
+
found_complementary_pair = True
|
|
726
|
+
sigma = _unify(atom1, atom2)
|
|
727
|
+
if sigma is None:
|
|
728
|
+
continue
|
|
729
|
+
resolvent = frozenset(
|
|
730
|
+
[_apply_literal(l, sigma) for l in ci2 if l != lit1]
|
|
731
|
+
+ [_apply_literal(l, sigma) for l in cj2 if l != lit2]
|
|
732
|
+
)
|
|
733
|
+
if _is_variant(resolvent, step.clause):
|
|
734
|
+
return None
|
|
735
|
+
if not found_complementary_pair:
|
|
736
|
+
return "'resolve': the parent clauses contain no complementary-polarity literal pair"
|
|
737
|
+
return ("'resolve': no complementary-polarity literal pair's mgu produces "
|
|
738
|
+
"the stated resolvent clause")
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
def _check_factor_step(step: ResolutionStep, ci: FrozenSet[Node]) -> Optional[str]:
|
|
742
|
+
"""``"factor"``: some same-polarity literal pair's mgu must give the clause."""
|
|
743
|
+
literals = sorted(ci, key=_lit_key)
|
|
744
|
+
for a in range(len(literals)):
|
|
745
|
+
parsed_a = _lit_atom_polarity(literals[a])
|
|
746
|
+
if parsed_a is None:
|
|
747
|
+
continue
|
|
748
|
+
atom_a, pos_a = parsed_a
|
|
749
|
+
for b in range(a + 1, len(literals)):
|
|
750
|
+
parsed_b = _lit_atom_polarity(literals[b])
|
|
751
|
+
if parsed_b is None:
|
|
752
|
+
continue
|
|
753
|
+
atom_b, pos_b = parsed_b
|
|
754
|
+
if pos_a != pos_b:
|
|
755
|
+
continue
|
|
756
|
+
sigma = _unify(atom_a, atom_b)
|
|
757
|
+
if sigma is None:
|
|
758
|
+
continue
|
|
759
|
+
factored = frozenset(_apply_literal(l, sigma) for l in ci)
|
|
760
|
+
if _is_variant(factored, step.clause):
|
|
761
|
+
return None
|
|
762
|
+
return ("'factor': no same-polarity literal pair's mgu produces the "
|
|
763
|
+
"stated factored clause")
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
def _direction_sides(eq_atom: Atom, direction: Optional[str]):
|
|
767
|
+
"""Return ``(from, to)`` for a ``"lr"``/``"rl"`` direction over
|
|
768
|
+
``eq_atom``'s two arguments, or ``None`` for any other value (including
|
|
769
|
+
a missing direction) -- shared by all four equality-rule checkers below."""
|
|
770
|
+
if direction == "lr":
|
|
771
|
+
return eq_atom.args[0], eq_atom.args[1]
|
|
772
|
+
if direction == "rl":
|
|
773
|
+
return eq_atom.args[1], eq_atom.args[0]
|
|
774
|
+
return None
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
def _check_paramodulate_step(step: ResolutionStep, ci: FrozenSet[Node], cj: FrozenSet[Node]) -> Optional[str]:
|
|
778
|
+
"""``"paramodulate"``: parents ``(i, j)`` -- ``i`` supplies a positive
|
|
779
|
+
equality literal (``step.eq_literal``, oriented by ``step.direction``),
|
|
780
|
+
``j`` supplies the target literal (``step.target_literal``) rewritten at
|
|
781
|
+
``step.position``. Re-derived from scratch: the two parents are
|
|
782
|
+
standardized apart (as for ``"resolve"``); the declared eq_literal/
|
|
783
|
+
target_literal (named as they appear in the ORIGINAL parents) are
|
|
784
|
+
located in the standardized clauses via the SAME rename maps; the
|
|
785
|
+
subterm at ``step.position`` is extracted from the target atom and
|
|
786
|
+
unified (own ``_unify``) against the equation's stated side; the
|
|
787
|
+
resulting mgu, applied to both remainders plus the rewritten literal, is
|
|
788
|
+
compared up to variant against the stated clause -- resolve's recipe,
|
|
789
|
+
generalized to a rewrite instead of a literal cancellation.
|
|
790
|
+
"""
|
|
791
|
+
if step.eq_literal is None or step.target_literal is None:
|
|
792
|
+
return "'paramodulate' requires eq_literal and target_literal"
|
|
793
|
+
ci2, cj2, map_i, map_j = _standardize_apart_maps(ci, cj)
|
|
794
|
+
eq_lit = _rename_literal(step.eq_literal, map_i)
|
|
795
|
+
if eq_lit not in ci2:
|
|
796
|
+
return "'paramodulate': eq_literal is not present in the cited equation parent"
|
|
797
|
+
parsed_eq = _lit_atom_polarity(eq_lit)
|
|
798
|
+
if parsed_eq is None or not parsed_eq[1]:
|
|
799
|
+
return "'paramodulate': eq_literal must be a POSITIVE equality literal"
|
|
800
|
+
eq_atom, _pos = parsed_eq
|
|
801
|
+
if not _is_equality_atom(eq_atom):
|
|
802
|
+
return "'paramodulate': eq_literal is not a binary equality atom"
|
|
803
|
+
sides = _direction_sides(eq_atom, step.direction)
|
|
804
|
+
if sides is None:
|
|
805
|
+
return "'paramodulate' requires direction 'lr' or 'rl'"
|
|
806
|
+
frm, to = sides
|
|
807
|
+
|
|
808
|
+
tgt_lit = _rename_literal(step.target_literal, map_j)
|
|
809
|
+
if tgt_lit not in cj2:
|
|
810
|
+
return "'paramodulate': target_literal is not present in the cited target parent"
|
|
811
|
+
parsed_tgt = _lit_atom_polarity(tgt_lit)
|
|
812
|
+
if parsed_tgt is None:
|
|
813
|
+
return "'paramodulate': target_literal is not a well-formed literal"
|
|
814
|
+
tgt_atom, tgt_is_positive = parsed_tgt
|
|
815
|
+
|
|
816
|
+
try:
|
|
817
|
+
subterm = _term_at(tgt_atom, step.position)
|
|
818
|
+
except (IndexError, TypeError, AttributeError):
|
|
819
|
+
return "'paramodulate': position does not address a subterm of target_literal"
|
|
820
|
+
|
|
821
|
+
sigma = _unify(frm, subterm)
|
|
822
|
+
if sigma is None:
|
|
823
|
+
return "'paramodulate': the equation's stated side does not unify with the subterm at position"
|
|
824
|
+
|
|
825
|
+
try:
|
|
826
|
+
rewritten_atom = _replace_at(tgt_atom, step.position, to)
|
|
827
|
+
except (IndexError, TypeError, AttributeError):
|
|
828
|
+
return "'paramodulate': position does not address a replaceable subterm"
|
|
829
|
+
rewritten_lit = rewritten_atom if tgt_is_positive else Not(rewritten_atom)
|
|
830
|
+
|
|
831
|
+
expected = frozenset(
|
|
832
|
+
[_apply_literal(l, sigma) for l in ci2 if l != eq_lit]
|
|
833
|
+
+ [_apply_literal(l, sigma) for l in cj2 if l != tgt_lit]
|
|
834
|
+
+ [_apply_literal(rewritten_lit, sigma)]
|
|
835
|
+
)
|
|
836
|
+
if _is_variant(expected, step.clause):
|
|
837
|
+
return None
|
|
838
|
+
return "'paramodulate': the recomputed rewrite is not a variant of the stated clause"
|
|
839
|
+
|
|
840
|
+
|
|
841
|
+
def _check_reflexivity_step(step: ResolutionStep, ci: FrozenSet[Node]) -> Optional[str]:
|
|
842
|
+
"""``"reflexivity"``: drops a NEGATIVE equality literal ``¬(u=v)`` from
|
|
843
|
+
the cited clause once its own two sides unify (own ``_unify``,
|
|
844
|
+
occurs-checked); the resulting mgu is applied to what remains. Covers
|
|
845
|
+
reflexivity goals such as ``⊢ c=c``, whose negation clausifies to
|
|
846
|
+
``{¬(c=c)}`` (unified by the trivial mgu ``{}``).
|
|
847
|
+
"""
|
|
848
|
+
if step.eq_literal is None:
|
|
849
|
+
return "'reflexivity' requires eq_literal"
|
|
850
|
+
if step.eq_literal not in ci:
|
|
851
|
+
return "'reflexivity': eq_literal is not present in the cited parent"
|
|
852
|
+
parsed = _lit_atom_polarity(step.eq_literal)
|
|
853
|
+
if parsed is None or parsed[1]:
|
|
854
|
+
return "'reflexivity': eq_literal must be a NEGATIVE equality literal"
|
|
855
|
+
atom, _pos = parsed
|
|
856
|
+
if not _is_equality_atom(atom):
|
|
857
|
+
return "'reflexivity': eq_literal is not a binary equality atom"
|
|
858
|
+
u, v = atom.args
|
|
859
|
+
sigma = _unify(u, v)
|
|
860
|
+
if sigma is None:
|
|
861
|
+
return "'reflexivity': the two sides of eq_literal do not unify"
|
|
862
|
+
expected = frozenset(_apply_literal(l, sigma) for l in ci if l != step.eq_literal)
|
|
863
|
+
if _is_variant(expected, step.clause):
|
|
864
|
+
return None
|
|
865
|
+
return "'reflexivity': the recomputed clause is not a variant of the stated clause"
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
def _check_demodulate_step(step: ResolutionStep, ci: FrozenSet[Node], cj: FrozenSet[Node]) -> Optional[str]:
|
|
869
|
+
"""``"demodulate"``: parents ``(target i, unit-equation j)``. ``cj`` must
|
|
870
|
+
be a UNIT clause (a single literal) whose sole literal is
|
|
871
|
+
``step.eq_literal``, a positive equality literal used as a REWRITE RULE
|
|
872
|
+
via one-sided MATCHING (:func:`_match_term` -- not unification: the
|
|
873
|
+
rule's variables bind, the target clause's variables are held fixed)
|
|
874
|
+
against the subterm of ``step.target_literal`` in ``ci`` at
|
|
875
|
+
``step.position``. The orientation is re-checked on the CONCRETE,
|
|
876
|
+
substituted terms with this module's own term order (:func:`_term_gt`):
|
|
877
|
+
only a genuinely simplifying rewrite (subterm ≻ rσ) is accepted -- a
|
|
878
|
+
tampered direction against the order is rejected here, independent of
|
|
879
|
+
what the derivation claims. No standardizing apart is needed: one-sided
|
|
880
|
+
matching never binds a target-side variable, so a coincidental variable
|
|
881
|
+
name shared between ``ci`` and ``cj`` cannot cause capture (the same
|
|
882
|
+
reasoning :mod:`unicode_logic_kit.atp.resolution` relies on for its own
|
|
883
|
+
demodulation and for clause subsumption, neither of which standardizes
|
|
884
|
+
apart either). The matcher is applied to the right-hand side in ONE
|
|
885
|
+
simultaneous step (:func:`_apply_matcher`): its images are terms of the
|
|
886
|
+
target, which may be spelled like the equation's variables, so they are
|
|
887
|
+
not looked up again.
|
|
888
|
+
|
|
889
|
+
The non-unit restriction on ``cj`` is a soundness requirement, not a
|
|
890
|
+
convenience: a clause ``{u≈v, Q}`` only asserts ``u≈v ∨ Q``, not the
|
|
891
|
+
unconditional fact ``u≈v`` -- rewriting with it while discarding ``Q``
|
|
892
|
+
would be unsound.
|
|
893
|
+
"""
|
|
894
|
+
if step.eq_literal is None or step.target_literal is None:
|
|
895
|
+
return "'demodulate' requires eq_literal and target_literal"
|
|
896
|
+
if len(cj) != 1:
|
|
897
|
+
return "'demodulate': the equation parent must be a UNIT clause (a single positive equality literal)"
|
|
898
|
+
if step.eq_literal not in cj:
|
|
899
|
+
return "'demodulate': eq_literal is not present in the cited equation parent"
|
|
900
|
+
parsed_eq = _lit_atom_polarity(step.eq_literal)
|
|
901
|
+
if parsed_eq is None or not parsed_eq[1]:
|
|
902
|
+
return "'demodulate': eq_literal must be a POSITIVE equality literal"
|
|
903
|
+
eq_atom, _pos = parsed_eq
|
|
904
|
+
if not _is_equality_atom(eq_atom):
|
|
905
|
+
return "'demodulate': eq_literal is not a binary equality atom"
|
|
906
|
+
sides = _direction_sides(eq_atom, step.direction)
|
|
907
|
+
if sides is None:
|
|
908
|
+
return "'demodulate' requires direction 'lr' or 'rl'"
|
|
909
|
+
l, r = sides
|
|
910
|
+
|
|
911
|
+
if step.target_literal not in ci:
|
|
912
|
+
return "'demodulate': target_literal is not present in the cited target parent"
|
|
913
|
+
parsed_tgt = _lit_atom_polarity(step.target_literal)
|
|
914
|
+
if parsed_tgt is None:
|
|
915
|
+
return "'demodulate': target_literal is not a well-formed literal"
|
|
916
|
+
tgt_atom, tgt_is_positive = parsed_tgt
|
|
917
|
+
|
|
918
|
+
try:
|
|
919
|
+
subterm = _term_at(tgt_atom, step.position)
|
|
920
|
+
except (IndexError, TypeError, AttributeError):
|
|
921
|
+
return "'demodulate': position does not address a subterm of target_literal"
|
|
922
|
+
|
|
923
|
+
sigma = _match_term(l, subterm, {})
|
|
924
|
+
if sigma is None:
|
|
925
|
+
return "'demodulate': the equation's stated left side does not MATCH the subterm at position"
|
|
926
|
+
|
|
927
|
+
r_sigma = _apply_matcher(r, sigma)
|
|
928
|
+
if not _term_gt(subterm, r_sigma):
|
|
929
|
+
return "'demodulate': the orientation does not strictly decrease under the documented term order"
|
|
930
|
+
|
|
931
|
+
try:
|
|
932
|
+
rewritten_atom = _replace_at(tgt_atom, step.position, r_sigma)
|
|
933
|
+
except (IndexError, TypeError, AttributeError):
|
|
934
|
+
return "'demodulate': position does not address a replaceable subterm"
|
|
935
|
+
rewritten_lit = rewritten_atom if tgt_is_positive else Not(rewritten_atom)
|
|
936
|
+
|
|
937
|
+
rest = [l2 for l2 in ci if l2 != step.target_literal]
|
|
938
|
+
expected = frozenset(rest + [rewritten_lit])
|
|
939
|
+
if _is_variant(expected, step.clause):
|
|
940
|
+
return None
|
|
941
|
+
return "'demodulate': the recomputed clause is not a variant of the stated clause"
|
|
942
|
+
|
|
943
|
+
|
|
944
|
+
def _is_false_constant_literal(literal: Node) -> bool:
|
|
945
|
+
"""True iff ``literal`` holds in no interpretation: the truth constant ``$false``
|
|
946
|
+
(or the atom ``⊥``) or the negation of the truth constant ``$true`` (or ``⊤``)
|
|
947
|
+
(re-derived here from the names, like the rest of this checker's vocabulary).
|
|
948
|
+
``$true`` and ``¬$false`` are NOT false: nothing may be removed because of them."""
|
|
949
|
+
if isinstance(literal, Not):
|
|
950
|
+
inner = literal.formula
|
|
951
|
+
return isinstance(inner, Atom) and not inner.args and inner.predicate in ("$true", "⊤")
|
|
952
|
+
return isinstance(literal, Atom) and not literal.args and literal.predicate in ("$false", "⊥")
|
|
953
|
+
|
|
954
|
+
|
|
955
|
+
def _check_truth_constants_step(step: ResolutionStep, ci: FrozenSet[Node]) -> Optional[str]:
|
|
956
|
+
"""``"truth_constants"``: from the cited clause, drop literals that are false in
|
|
957
|
+
every interpretation (``$false`` and ``¬$true``).
|
|
958
|
+
|
|
959
|
+
The stated clause must be a SUBSET of the cited one and every literal missing
|
|
960
|
+
from it must be such a constant, so ``C ∨ $false`` gives ``C`` and nothing
|
|
961
|
+
else is licensed: a literal of any other atom, ``$true`` and ``¬$false`` stay
|
|
962
|
+
(a clause that has those is simply true, which no step has to derive).
|
|
963
|
+
"""
|
|
964
|
+
if not step.clause <= ci:
|
|
965
|
+
return "'truth_constants': the stated clause must be a subset of the cited clause"
|
|
966
|
+
for literal in ci - step.clause:
|
|
967
|
+
if not _is_false_constant_literal(literal):
|
|
968
|
+
return (f"'truth_constants': {literal.to_unicode_str()!r} is not a literal "
|
|
969
|
+
"that is false in every interpretation ($false or ¬$true)")
|
|
970
|
+
return None
|
|
971
|
+
|
|
972
|
+
|
|
973
|
+
_RULE_ARITY = {
|
|
974
|
+
"input": 0, "resolve": 2, "factor": 1,
|
|
975
|
+
"paramodulate": 2, "reflexivity": 1, "demodulate": 2,
|
|
976
|
+
"truth_constants": 1,
|
|
977
|
+
}
|
|
978
|
+
# NO "self_paramodulate": the shared-instance shortcut (equation and target
|
|
979
|
+
# from ONE instantiation, both dropped) is UNSOUND — for {u ≈ v, L[u]} it
|
|
980
|
+
# would certify {L[v]}, false in a model satisfying the clause via L[u] with
|
|
981
|
+
# u ≠ v. The generator no longer emits it (adversarial review, Tier 3), and
|
|
982
|
+
# an external derivation claiming it must be REJECTED, not re-derived.
|
|
983
|
+
|
|
984
|
+
|
|
985
|
+
# ---------------------------------------------------------------------------
|
|
986
|
+
# The checker
|
|
987
|
+
# ---------------------------------------------------------------------------
|
|
988
|
+
|
|
989
|
+
def verify_resolution_proof(derivation: "ResolutionDerivation") -> ResolutionCheckResult:
|
|
990
|
+
"""Check ``derivation`` and return a :class:`ResolutionCheckResult`.
|
|
991
|
+
|
|
992
|
+
Walks ``derivation.steps`` in order, verifying each step's structural
|
|
993
|
+
shape (index == position, parents strictly earlier, parent count matches
|
|
994
|
+
the rule) and then its rule-specific licensing (see the module docstring).
|
|
995
|
+
Stops at the first failure and reports it as ``"step N: reason"``.
|
|
996
|
+
``refuted`` tracks, independently of ``ok``, whether some step verified
|
|
997
|
+
*before* any failure derives the empty clause.
|
|
998
|
+
"""
|
|
999
|
+
clause_by_index: Dict[int, FrozenSet[Node]] = {}
|
|
1000
|
+
refuted = False
|
|
1001
|
+
|
|
1002
|
+
for pos, step in enumerate(derivation.steps):
|
|
1003
|
+
expected_index = pos + 1
|
|
1004
|
+
if not isinstance(step, ResolutionStep):
|
|
1005
|
+
return ResolutionCheckResult(
|
|
1006
|
+
False, None,
|
|
1007
|
+
f"step {expected_index}: expected a ResolutionStep, got "
|
|
1008
|
+
f"{type(step).__name__}",
|
|
1009
|
+
refuted)
|
|
1010
|
+
if step.index != expected_index:
|
|
1011
|
+
return ResolutionCheckResult(
|
|
1012
|
+
False, step.index,
|
|
1013
|
+
f"step {step.index}: index must equal its position in the "
|
|
1014
|
+
f"derivation (expected {expected_index})",
|
|
1015
|
+
refuted)
|
|
1016
|
+
|
|
1017
|
+
arity = _RULE_ARITY.get(step.rule)
|
|
1018
|
+
if arity is None:
|
|
1019
|
+
expected = ", ".join(repr(r) for r in _RULE_ARITY)
|
|
1020
|
+
return ResolutionCheckResult(
|
|
1021
|
+
False, step.index,
|
|
1022
|
+
f"step {step.index}: unknown rule {step.rule!r} (expected "
|
|
1023
|
+
f"one of {expected})",
|
|
1024
|
+
refuted)
|
|
1025
|
+
if len(step.parents) != arity:
|
|
1026
|
+
return ResolutionCheckResult(
|
|
1027
|
+
False, step.index,
|
|
1028
|
+
f"step {step.index}: rule {step.rule!r} takes {arity} "
|
|
1029
|
+
f"parent(s), got {len(step.parents)}",
|
|
1030
|
+
refuted)
|
|
1031
|
+
|
|
1032
|
+
for p in step.parents:
|
|
1033
|
+
if p not in clause_by_index or p >= step.index:
|
|
1034
|
+
return ResolutionCheckResult(
|
|
1035
|
+
False, step.index,
|
|
1036
|
+
f"step {step.index}: parent {p} does not refer to a "
|
|
1037
|
+
f"strictly earlier verified step (parents must cite "
|
|
1038
|
+
f"smaller indices)",
|
|
1039
|
+
refuted)
|
|
1040
|
+
|
|
1041
|
+
if step.rule == "input":
|
|
1042
|
+
err = _check_input_step(step, derivation.inputs)
|
|
1043
|
+
elif step.rule == "resolve":
|
|
1044
|
+
i, j = step.parents
|
|
1045
|
+
err = _check_resolve_step(step, clause_by_index[i], clause_by_index[j])
|
|
1046
|
+
elif step.rule == "factor":
|
|
1047
|
+
(i,) = step.parents
|
|
1048
|
+
err = _check_factor_step(step, clause_by_index[i])
|
|
1049
|
+
elif step.rule == "paramodulate":
|
|
1050
|
+
i, j = step.parents
|
|
1051
|
+
err = _check_paramodulate_step(step, clause_by_index[i], clause_by_index[j])
|
|
1052
|
+
elif step.rule == "reflexivity":
|
|
1053
|
+
(i,) = step.parents
|
|
1054
|
+
err = _check_reflexivity_step(step, clause_by_index[i])
|
|
1055
|
+
elif step.rule == "truth_constants":
|
|
1056
|
+
(i,) = step.parents
|
|
1057
|
+
err = _check_truth_constants_step(step, clause_by_index[i])
|
|
1058
|
+
else: # "demodulate"
|
|
1059
|
+
i, j = step.parents
|
|
1060
|
+
err = _check_demodulate_step(step, clause_by_index[i], clause_by_index[j])
|
|
1061
|
+
|
|
1062
|
+
if err is not None:
|
|
1063
|
+
return ResolutionCheckResult(False, step.index, f"step {step.index}: {err}", refuted)
|
|
1064
|
+
|
|
1065
|
+
clause_by_index[step.index] = step.clause
|
|
1066
|
+
if not step.clause:
|
|
1067
|
+
refuted = True
|
|
1068
|
+
|
|
1069
|
+
return ResolutionCheckResult(True, None, None, refuted)
|
|
1070
|
+
|
|
1071
|
+
|
|
1072
|
+
def check_resolution_proof(derivation: "ResolutionDerivation") -> bool:
|
|
1073
|
+
"""Return True iff ``derivation`` is a valid resolution derivation (sound).
|
|
1074
|
+
|
|
1075
|
+
A thin bool wrapper over :func:`verify_resolution_proof`; use that for the
|
|
1076
|
+
failing step index and reason, or to read ``refuted``.
|
|
1077
|
+
"""
|
|
1078
|
+
return verify_resolution_proof(derivation).ok
|
|
1079
|
+
|
|
1080
|
+
|
|
1081
|
+
# ---------------------------------------------------------------------------
|
|
1082
|
+
# Rendering
|
|
1083
|
+
# ---------------------------------------------------------------------------
|
|
1084
|
+
|
|
1085
|
+
def _render_clause(clause: FrozenSet[Node]) -> str:
|
|
1086
|
+
"""Render a clause as its literals joined by ``∨``, or ``□`` if empty.
|
|
1087
|
+
|
|
1088
|
+
Literals are sorted by their Unicode rendering for a deterministic,
|
|
1089
|
+
frozenset-iteration-order-independent string.
|
|
1090
|
+
"""
|
|
1091
|
+
if not clause:
|
|
1092
|
+
return "□"
|
|
1093
|
+
return " ∨ ".join(lit.to_unicode_str() for lit in sorted(clause, key=_lit_key))
|
|
1094
|
+
|
|
1095
|
+
|
|
1096
|
+
def render_resolution_proof(derivation: "ResolutionDerivation") -> str:
|
|
1097
|
+
"""Render ``derivation`` as one numbered line per step.
|
|
1098
|
+
|
|
1099
|
+
Each line is ``N. <clause> [rule parents]`` — the number, the clause, and
|
|
1100
|
+
the justification in brackets. (The format is spelled out here rather than
|
|
1101
|
+
in the summary line above because autosummary cuts that line at the first
|
|
1102
|
+
sentence end, and a ``.`` inside an inline literal reads as one.)
|
|
1103
|
+
|
|
1104
|
+
The empty clause renders as ``□``. Parent indices are comma-joined in
|
|
1105
|
+
citation order; a rule with no parents (``"input"``) renders with no
|
|
1106
|
+
trailing citation list, e.g. ``"1. P(a) [input]"``.
|
|
1107
|
+
"""
|
|
1108
|
+
lines = []
|
|
1109
|
+
for step in derivation.steps:
|
|
1110
|
+
bracket = step.rule
|
|
1111
|
+
if step.parents:
|
|
1112
|
+
bracket += " " + ",".join(str(p) for p in step.parents)
|
|
1113
|
+
lines.append(f"{step.index}. {_render_clause(step.clause)} [{bracket}]")
|
|
1114
|
+
return "\n".join(lines)
|