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,1760 @@
|
|
|
1
|
+
"""Quantified modal logic via a first-order shallow embedding (Benzmüller-style, FO).
|
|
2
|
+
|
|
3
|
+
This is the *first-order fragment* of the shallow semantical embedding (SSE): a modal
|
|
4
|
+
formula over an explicit "current world" is translated into classical first-order
|
|
5
|
+
logic, so the existing back-ends (Z3, the resolution prover) can decide its validity
|
|
6
|
+
or the equivalence of two modal formulas. Unlike the propositional
|
|
7
|
+
:func:`unicode_logic_kit.fol.modal_translation.standard_translation`, this embedding
|
|
8
|
+
handles **object quantifiers** (`∀x` / `∃x`) under a chosen *domain regime*, so the
|
|
9
|
+
**Barcan** formula and its converse come out valid or invalid exactly as the regime
|
|
10
|
+
dictates.
|
|
11
|
+
|
|
12
|
+
Single-sorted embedding with guard predicates (worlds and individuals share one FO
|
|
13
|
+
sort): ``World(t)`` / ``Object(t)`` carve the two kinds apart; accessibility ``R`` is
|
|
14
|
+
typed World×World; existence ``E(x, w)`` ("object ``x`` exists at world ``w``") is
|
|
15
|
+
typed Object×World. The translation:
|
|
16
|
+
|
|
17
|
+
- ``P(t̄)`` → ``P(t̄, w)`` (the world is appended as the last argument) — for every
|
|
18
|
+
predicate EXCEPT identity: ``t₁ = t₂`` → ``t₁ = t₂`` and ``t₁ ≠ t₂`` → ``¬(t₁ = t₂)``,
|
|
19
|
+
with no world argument (see "Equality is rigid" below);
|
|
20
|
+
- ``□φ`` → ``∀v (World(v) ∧ R(w,v) → ST(φ,v))``; ``◇φ`` → ``∃v (World(v) ∧ R(w,v) ∧ ST(φ,v))``;
|
|
21
|
+
- **actualist** ``∀x φ`` → ``∀x (Object(x) ∧ E(x,w) → ST(φ,w))`` and dually ``∃x``;
|
|
22
|
+
- **constant / possibilist** ``∀x φ`` → ``∀x (Object(x) → ST(φ,w))`` (``E`` unused).
|
|
23
|
+
|
|
24
|
+
Domain regimes (the existence-axiom correspondence, verified against the Kripke
|
|
25
|
+
evaluator): **decreasing** ``∀x∀w∀v(E(x,v)∧R(w,v)→E(x,w))`` validates BF; **increasing**
|
|
26
|
+
(cumulative) ``∀x∀w∀v(E(x,w)∧R(w,v)→E(x,v))`` validates CBF; **constant** validates both;
|
|
27
|
+
**varying** neither.
|
|
28
|
+
|
|
29
|
+
**Equality is rigid.** An identity atom (``Atom("=", (t₁, t₂))``, what ``a = b`` parses
|
|
30
|
+
to; ``≠`` is its negation) is translated to the SAME binary identity over the object
|
|
31
|
+
terms, inside the relativised formula, with **no world argument**. So identity does not
|
|
32
|
+
vary by world: the necessity of identity ``a = b → □(a = b)`` and of distinctness
|
|
33
|
+
``a ≠ b → □(a ≠ b)`` are valid in every frame and under every domain regime, as is
|
|
34
|
+
``◇(a = b) → a = b``; constants and function symbols were already rigid here (``ST``
|
|
35
|
+
leaves terms alone), and now so is the relation between them. It is Z3's own binary
|
|
36
|
+
identity, so reflexivity, symmetry, transitivity and congruence (hence Leibniz's law,
|
|
37
|
+
``a = b → (P(a) ↔ P(b))`` and ``a = b → (□P(a) ↔ □P(b))``) come from the solver, not from
|
|
38
|
+
an axiom: :func:`qml_axioms` has none for equality — checked, not assumed (see
|
|
39
|
+
``tests/test_qml.py``). Two consequences worth stating, because they are easy to misread:
|
|
40
|
+
|
|
41
|
+
- ``□(a = b) → a = b`` is **not** valid in K — a dead-end world makes the box vacuously
|
|
42
|
+
true while ``a`` and ``b`` differ — and is valid in every frame that guarantees a
|
|
43
|
+
successor-or-self (T, S4, S5, KD, KD45 …). Rigidity makes the converse direction
|
|
44
|
+
``a = b → □(a = b)`` free; the direction back needs the frame.
|
|
45
|
+
- ``a = b`` and ``¬(a = b)`` are not valid: two terms may denote the same object or two.
|
|
46
|
+
|
|
47
|
+
*Varying domains — the choice made here.* Identity ranges over the whole **object
|
|
48
|
+
domain** (everything typed ``Object``), not over the local domain ``D_w`` of the
|
|
49
|
+
world ``ST`` is at; an equality is **not** existence-guarded. So ``a = a``,
|
|
50
|
+
``a = b → b = a`` and ``a = b → □(a = b)`` are valid under every mode, *including* at a
|
|
51
|
+
world where ``a`` does not exist (a constant outside ``D_w`` still denotes, and is
|
|
52
|
+
still itself). That is the reading the rest of this embedding already has for atoms on
|
|
53
|
+
a non-existent constant — ``P(c, w)`` is left open, neither forced true (positive free
|
|
54
|
+
logic) nor forced false (negative free logic) — and the rigid-designator reading of
|
|
55
|
+
constants (``_signature_typing_facts`` types a constant ``Object``, never ``E(c, w)``).
|
|
56
|
+
Existence is therefore *expressed*, not presupposed, by ``∃x (x = c)``: valid under
|
|
57
|
+
``constant`` / ``possibilist`` (every object exists everywhere), **not** valid under
|
|
58
|
+
``varying`` (nor ``increasing`` / ``decreasing``), where the constant may lie outside
|
|
59
|
+
``D_w`` — and ``∃x (x = c) → □∃x (x = c)`` is valid exactly under the cumulative regime
|
|
60
|
+
(``increasing``) and ``constant``. (Here ``c`` is a CONSTANT, a multi-letter name such as
|
|
61
|
+
``alice`` in the kit's syntax; a single letter is a variable, and a free variable is a
|
|
62
|
+
parameter, which exists at the world of evaluation: see "A free variable is a
|
|
63
|
+
parameter" below.) The quantifier clauses, the domain axioms and the
|
|
64
|
+
modes themselves are unchanged. A free-logic variant in which ``c = c`` fails for a
|
|
65
|
+
non-existent ``c`` would be a different (negative) logic and is deliberately not
|
|
66
|
+
offered; guard explicitly with ``∃x (x = c) → …`` where that reading is wanted.
|
|
67
|
+
The propositional evaluator :mod:`unicode_logic_kit.semantics.kripke` has no term
|
|
68
|
+
semantics and refuses ``=`` / ``≠`` by name instead, as do the propositional modal
|
|
69
|
+
tableau (:mod:`unicode_logic_kit.atp.modal_tableau`), the propositional standard
|
|
70
|
+
translation (:mod:`unicode_logic_kit.fol.modal_translation`) and the intuitionistic
|
|
71
|
+
GMT embedding (:mod:`unicode_logic_kit.hol.intuitionistic`) — one shared refusal, in
|
|
72
|
+
:func:`unicode_logic_kit.semantics._modal_reject.reject_equality`. The modal HOL
|
|
73
|
+
exports AGREE with this route: :func:`to_thf_modal`,
|
|
74
|
+
:mod:`unicode_logic_kit.hol.thf_modal` and :mod:`unicode_logic_kit.hol.isabelle_modal`
|
|
75
|
+
emit identity as the host logic's own ``=`` over the individual sort with no world
|
|
76
|
+
argument. So on equality a formula has exactly two fates in this kit: rigid
|
|
77
|
+
identity, or a refusal that names the atom.
|
|
78
|
+
|
|
79
|
+
**A free variable is a parameter.** A variable that is free in a formula names ONE unknown
|
|
80
|
+
individual of the object domain, the same everywhere in the formula (the assignment-wise
|
|
81
|
+
consequence relation of the textbooks), and it is rigid, like a constant: the same
|
|
82
|
+
individual at every world. Which individuals it may be depends on the domain regime. Under
|
|
83
|
+
``constant`` / ``possibilist`` every individual exists at every world, so the parameter is
|
|
84
|
+
typed ``Object(y)`` like a constant and ``∀x P(x) → P(y)`` and ``P(y) → ∃x P(x)`` are
|
|
85
|
+
valid. Under ``varying`` / ``increasing`` / ``decreasing`` the parameter exists at the world
|
|
86
|
+
of evaluation, which the validity query states as ``E(y, w)`` (the existence guard the
|
|
87
|
+
free-logic route gives its parameters), and nowhere else is it required to exist. So
|
|
88
|
+
``∀x P(x) → P(y)``, ``P(y) → ∃x P(x)`` and ``∃x (x = y)`` are valid in every regime, while
|
|
89
|
+
``□∀x P(x) → □P(y)`` and ``□∃x (x = y)`` are valid under ``constant`` / ``possibilist`` and
|
|
90
|
+
``increasing`` only: the individual may be missing from a later world of a ``varying`` or a
|
|
91
|
+
``decreasing`` model, and then the later world's quantifier does not reach it. A CONSTANT
|
|
92
|
+
is different (see "Equality is rigid"): it may lie outside the domain of the world it is
|
|
93
|
+
read at, so ``∃x (x = alice)`` is valid only under ``constant`` / ``possibilist``. Use a
|
|
94
|
+
constant for that reading, or state the existence of the individual in the formula. For a
|
|
95
|
+
formula with no premise the parameter reading is the universal closure of the formula
|
|
96
|
+
(under a varying regime: of the formula guarded by the existence of its parameters).
|
|
97
|
+
``P(y) → P(alice)`` and ``(P(y) ∧ Q(z)) → ∀x (P(x) ∧ Q(x))`` are not valid in any regime.
|
|
98
|
+
:func:`qml_translate` leaves a free variable alone; the typing and the guard are part of
|
|
99
|
+
the validity query (:func:`qml_validity_formula`), which :func:`qml_is_valid` and
|
|
100
|
+
:func:`qml_equivalent` decide.
|
|
101
|
+
|
|
102
|
+
**One relation per modal family.** ``R`` is alethic (``□`` / ``◇``, configured by
|
|
103
|
+
``frame=``), ``T`` is the temporal *henceforth* relation (``Always`` / ``Eventually``
|
|
104
|
+
read it forward, ``Historically`` / ``Once`` read its converse), ``N`` is the *one-step
|
|
105
|
+
successor* (``Next``, and ``Previous`` over its converse) and ``D`` is deontic
|
|
106
|
+
(``Obligatory`` / ``Permitted``); the agent-indexed families use the ternary ``Rk`` /
|
|
107
|
+
``Rb`` / ``Rs`` / ``Rw`` (configured by ``systems=``). :func:`qml_axioms` types every
|
|
108
|
+
relation the formula actually uses and, for ``T`` / ``N`` / ``D``, asserts these
|
|
109
|
+
**default-on** frame conditions — chosen so this route agrees with
|
|
110
|
+
:func:`unicode_logic_kit.semantics.kripke.satisfies_modal` and with the HOL routes, whose
|
|
111
|
+
``t_refl`` / ``t_trans`` / ``n_in_t`` / ``d_serial`` are the same conditions
|
|
112
|
+
(:mod:`unicode_logic_kit.hol.isabelle_modal`, :mod:`unicode_logic_kit.hol.thf_modal`):
|
|
113
|
+
|
|
114
|
+
- ``T`` reflexive + transitive, ``N ⊆ T``, and — when both relations occur — the
|
|
115
|
+
``first_step`` axiom below, the first-order shadow of the HOL routes' ``t_in_nstar``.
|
|
116
|
+
So ``Gφ → φ``, ``Gφ → GGφ``, ``Gφ → Fφ``, ``Gφ → Xφ``, ``φ → Fφ``, the fixpoint
|
|
117
|
+
unfolding ``(φ ∧ XGφ) → Gφ`` and the past mirrors ``Hφ → φ``, ``Hφ → HHφ``,
|
|
118
|
+
``Hφ → Yφ``, ``φ → Once φ`` are valid, while ``Xφ → φ``, ``φ → Gφ``, ``Fφ → Gφ``,
|
|
119
|
+
``Gφ → Hφ`` and ``Xφ → Fφ`` correctly stay invalid.
|
|
120
|
+
- ``D`` serial (Standard Deontic Logic, KD), the reading the ``Obligatory`` / ``Permitted``
|
|
121
|
+
node docstrings already declare. ``Oφ → Pφ`` and ``Oφ → ¬O¬φ`` become valid; ``Oφ → φ``
|
|
122
|
+
stays invalid. Honest contract: a ``True`` verdict for a deontic formula therefore means
|
|
123
|
+
"valid over every **serial-deontic** model". ``satisfies_modal`` enforces no seriality of
|
|
124
|
+
its own, so a hand-built dead-end deontic model still refutes ``Oφ → Pφ`` *there*.
|
|
125
|
+
|
|
126
|
+
**The transitive-closure limit — what stays out of reach.** ``satisfies_modal`` evaluates
|
|
127
|
+
``Always`` / ``Eventually`` over the reflexive-transitive CLOSURE ``N*`` of the one-step
|
|
128
|
+
relation, i.e. the *intended* constraint is ``T = N*``. Reflexivity, transitivity and
|
|
129
|
+
``N ⊆ T`` axiomatise the half ``T ⊇ N*`` (``N*`` is by definition the least refl-trans
|
|
130
|
+
relation containing ``N``), which is what makes the list above sound. The converse half
|
|
131
|
+
``T ⊆ N*`` is **not** first-order definable — the same obstacle this module already
|
|
132
|
+
reports for ``Until`` / ``Since`` — because it demands that the witnessing path be
|
|
133
|
+
*finite*. A first-order CONSEQUENCE of it is definable, however, and this route asserts it
|
|
134
|
+
whenever ``T`` and ``N`` both occur (``first_step``, see
|
|
135
|
+
:func:`_temporal_first_step_axiom`)::
|
|
136
|
+
|
|
137
|
+
∀w ∀v (World(w) ∧ World(v) ∧ T(w,v) → w = v ∨ ∃u (World(u) ∧ N(w,u) ∧ T(u,v)))
|
|
138
|
+
|
|
139
|
+
— "a henceforth-step is either standing still or one step followed by a henceforth-step".
|
|
140
|
+
Every ``T = N*`` model satisfies it, so it over-validates nothing, and with it the ``←``
|
|
141
|
+
direction of the fixpoint unfolding ``(φ ∧ XGφ) → Gφ`` becomes provable here (the ``→``
|
|
142
|
+
direction needs only ``T ⊇ N*``). Temporal induction ``(φ ∧ G(φ → Xφ)) → Gφ`` genuinely
|
|
143
|
+
does stay out of reach — measured: ``first_step`` does not close it — because reaching an
|
|
144
|
+
arbitrary ``T``-successor from the first step needs induction over the closure, which no
|
|
145
|
+
first-order theory states. For that one use the higher-order routes
|
|
146
|
+
:func:`unicode_logic_kit.hol.isabelle_modal.to_isabelle_modal` /
|
|
147
|
+
:func:`unicode_logic_kit.hol.isabelle_runner.isabelle_decide_modal`, whose ``t_in_nstar``
|
|
148
|
+
axiom pins ``t`` to ``rtranclp n``. The honest summary of this route's temporal strength
|
|
149
|
+
is therefore "refl + trans + ``N ⊆ T`` + ``first_step``" — a chosen axiom set, not "the
|
|
150
|
+
limit of first-order logic".
|
|
151
|
+
|
|
152
|
+
**Cross-family bridges** relate two families' relations and are **opt-in**: none is
|
|
153
|
+
emitted by default, because none of them is forced by ``satisfies_modal``. Pass
|
|
154
|
+
``bridges=["knowledge_implies_belief", ...]``; the table is :data:`QML_BRIDGES`, an
|
|
155
|
+
unknown name raises ``ValueError`` listing the known ones, and so does a bridge whose
|
|
156
|
+
partner family the formula never mentions. Each condition is the exact frame correspondent
|
|
157
|
+
of its schema and is **the same condition the HOL routes emit under the same name** —
|
|
158
|
+
inclusions ``Rb ⊆ Rk`` / ``Rb ⊆ Rs`` for the two agent-indexed bridges, and the meet
|
|
159
|
+
condition ``∀w ∃v (D(w,v) ∧ R(w,v))`` (``d_meets_r``) for ``ought_implies_can``, which is
|
|
160
|
+
deliberately NOT the folklore ``D ⊆ R``: see :func:`_bridge_axiom` for the measured
|
|
161
|
+
witnesses showing that the inclusion validates the unrequested ``□φ → Oφ`` and
|
|
162
|
+
``Pφ → ◇φ`` while the meet condition validates ``Oφ → ◇φ`` and neither artefact.
|
|
163
|
+
|
|
164
|
+
Validity is ``AX → ∀w (World(w) → ST(φ, w))``, checked with Z3. First-order modal logic
|
|
165
|
+
is undecidable, so this is **sound but bounded-incomplete** (Z3 may not close every
|
|
166
|
+
valid instance) — the model-theoretic partner is
|
|
167
|
+
:func:`unicode_logic_kit.semantics.kripke.satisfies_modal` with per-world ``domains``.
|
|
168
|
+
|
|
169
|
+
**Many-sorted formulas** (``SortedQuantifier``, ``∀x:S φ`` / ``∃x:S φ``) are handled by
|
|
170
|
+
relativizing the WHOLE input formula once, at :func:`qml_translate`'s entry, into the
|
|
171
|
+
guarded plain FOL ``fol.to_fol`` also builds (``∀x:S φ`` → ``∀x (S(x) → φ)``, ``∃x:S φ``
|
|
172
|
+
→ ``∃x (S(x) ∧ φ)``) — before anything scans or translates it, so a ``SortedConstant``
|
|
173
|
+
anywhere in the formula (not only directly under a ``SortedQuantifier``) is relativized
|
|
174
|
+
too, and :func:`_signature_typing_facts` sees the resulting plain ``Constant`` like any
|
|
175
|
+
other. ``ST``'s existing ``P(t̄) → P(t̄, w)`` rule then appends the world argument to the
|
|
176
|
+
sort guard exactly as it does to every other atom — so a sort's guard predicate
|
|
177
|
+
``S(x, w)`` comes out WORLD-RELATIVE (not rigid): an object can be ``S`` at one world and
|
|
178
|
+
not at another, the same "actualist" reading the ``E``xistence guard already gives the
|
|
179
|
+
object domain. :func:`qml_axioms` additionally emits, per sort the formula uses, the same
|
|
180
|
+
"every sort is non-empty" assumption the classical routes make
|
|
181
|
+
(:func:`~unicode_logic_kit.fol._msfl_nodes.nonempty_sort_axioms`) — extended PER WORLD here
|
|
182
|
+
(``∀w (World(w) → ∃x (Object(x) [∧ E(x,w)] ∧ S(x,w)))``) to match the world-relative guard,
|
|
183
|
+
mirroring the existing ``nonempty_dom`` axiom's shape — so e.g. ``∀x:S P(x) → ∃x:S P(x)``
|
|
184
|
+
agrees with the classical Z3 verdict at every world, not just incidentally at one.
|
|
185
|
+
|
|
186
|
+
A SORTED CONSTANT ``c:S`` denotes an element of ``S``, so :func:`qml_axioms` also emits
|
|
187
|
+
``∀w (World(w) → S(c, w))`` for it (read from the formula as given, where the
|
|
188
|
+
annotation is still there; :func:`~unicode_logic_kit.fol._msfl_nodes.sort_membership_axioms`
|
|
189
|
+
names the constants). A constant is a rigid designator, so the fact holds at EVERY
|
|
190
|
+
world, and it is not guarded by ``E(c, w)``: ``c`` may lie outside the domain of a
|
|
191
|
+
world, which is the reading of constants this module already has. With it
|
|
192
|
+
``∀x:S P(x) → P(c:S)`` is valid exactly where ``∀x P(x) → P(c)`` is (every constant
|
|
193
|
+
domain; no actualist mode, where ``c`` may be outside ``D_w`` and the instance is not
|
|
194
|
+
available), and it stays invalid with a plain ``c``, which no formula places in ``S``.
|
|
195
|
+
|
|
196
|
+
Public API: :func:`qml_translate`, :func:`qml_axioms`, :func:`qml_is_valid`,
|
|
197
|
+
:func:`qml_equivalent`, :func:`qml_validity_formula` (the closed classical-FOL
|
|
198
|
+
validity query itself, e.g. for :func:`unicode_logic_kit.hets.dol.to_dol_library_from_modal`
|
|
199
|
+
to hand to the CASL/DOL/Hets route), and the constants :data:`BARCAN`,
|
|
200
|
+
:data:`CONVERSE_BARCAN`, :data:`QML_BRIDGES`.
|
|
201
|
+
"""
|
|
202
|
+
|
|
203
|
+
from functools import reduce
|
|
204
|
+
from typing import Dict, List, Optional
|
|
205
|
+
|
|
206
|
+
from .nodes import (
|
|
207
|
+
Node, Variable, Atom, Not, And, Or, Xor, Implies, Iff, Quantifier,
|
|
208
|
+
Box, Diamond, Knows, Believes, Says, Wants,
|
|
209
|
+
Always, Eventually, Next, Until,
|
|
210
|
+
Historically, Once, Previous, Since,
|
|
211
|
+
Obligatory, Permitted, SortedQuantifier,
|
|
212
|
+
)
|
|
213
|
+
from ._hybrid_nodes import Down
|
|
214
|
+
from ._fol_nodes import constant_name_to_ascii
|
|
215
|
+
from ._free_parameters import free_parameter_names
|
|
216
|
+
from ._identifiers import fresh_variables
|
|
217
|
+
from ._msfl_nodes import key_text, nonempty_sort_axioms, sort_membership_axioms
|
|
218
|
+
from ._truth_constants import truth_value
|
|
219
|
+
from .frames import (
|
|
220
|
+
FRAME_CONDITIONS, FRAMES as _SHARED_FRAMES, UnsupportedFrameCondition,
|
|
221
|
+
resolve_frame, parse_geach,
|
|
222
|
+
)
|
|
223
|
+
from ._numeral_symbols import numerals_as_constants, prefixed_numeral_name
|
|
224
|
+
from ._symbol_names import SymbolNames, dedupe
|
|
225
|
+
|
|
226
|
+
# Guard / typing predicate names (the contract with the axiom set).
|
|
227
|
+
_WORLD = "World"
|
|
228
|
+
_OBJECT = "Object"
|
|
229
|
+
_E = "E" # existence: E(x, w) ≙ x ∈ D_w
|
|
230
|
+
|
|
231
|
+
# Accessibility-relation names per modality (all typed World×World).
|
|
232
|
+
_R_ALETHIC = "R"
|
|
233
|
+
_R_TEMPORAL = "T"
|
|
234
|
+
_R_NEXT = "N"
|
|
235
|
+
_R_DEONTIC = "D"
|
|
236
|
+
# Epistemic / doxastic / assertive / bouletic accessibility relations are
|
|
237
|
+
# AGENT-INDEXED ternary predicates Rk(agent, w, v) / Rb(agent, w, v) /
|
|
238
|
+
# Rs(agent, w, v) / Rw(agent, w, v), so the agent can be a quantified object
|
|
239
|
+
# variable (``∀x (Student(x) → K_x φ)``) rather than baked into the relation name.
|
|
240
|
+
_R_KNOWS = "Rk"
|
|
241
|
+
_R_BELIEVES = "Rb"
|
|
242
|
+
_R_SAYS = "Rs"
|
|
243
|
+
_R_WANTS = "Rw"
|
|
244
|
+
|
|
245
|
+
# Every predicate name the translation itself emits. A USER predicate (or sort
|
|
246
|
+
# guard) with one of these names used to be read as the translation's own
|
|
247
|
+
# predicate once ST appended its world argument: a unary user ``R`` became
|
|
248
|
+
# ``R(x, w)`` — the alethic accessibility relation — so ``R(alice) →
|
|
249
|
+
# ◇R(alice)`` came out VALID in K, and a binary ``R`` crashed Z3 on the arity
|
|
250
|
+
# clash. :func:`_user_predicate` keeps the two namespaces apart.
|
|
251
|
+
_RESERVED_PREDICATES = frozenset({
|
|
252
|
+
_WORLD, _OBJECT, _E,
|
|
253
|
+
_R_ALETHIC, _R_TEMPORAL, _R_NEXT, _R_DEONTIC,
|
|
254
|
+
_R_KNOWS, _R_BELIEVES, _R_SAYS, _R_WANTS,
|
|
255
|
+
})
|
|
256
|
+
# U+00B7 MIDDLE DOT: punctuation, never part of an identifier the parsers
|
|
257
|
+
# produce, and no reserved name ends in it.
|
|
258
|
+
_USER_MARK = "·"
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _user_predicate(name: str) -> str:
|
|
262
|
+
"""The name a USER predicate (or sort guard) gets in the translation.
|
|
263
|
+
|
|
264
|
+
Unchanged unless the name, with any trailing ``·`` stripped, is one of
|
|
265
|
+
:data:`_RESERVED_PREDICATES`; then one ``·`` is appended. The map is
|
|
266
|
+
injective (a changed name ends in ``·`` and strips to a reserved name,
|
|
267
|
+
which no unchanged name does) and never yields a reserved name, so a user
|
|
268
|
+
predicate can no longer be confused with ``World``/``Object``/``E`` or an
|
|
269
|
+
accessibility relation — and every formula that does not use those names
|
|
270
|
+
translates byte-for-byte as before.
|
|
271
|
+
"""
|
|
272
|
+
if name.rstrip(_USER_MARK) in _RESERVED_PREDICATES:
|
|
273
|
+
return name + _USER_MARK
|
|
274
|
+
return name
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
_FORALL = "∀"
|
|
278
|
+
_EXISTS = "∃"
|
|
279
|
+
_ACTUALIST_MODES = frozenset({"varying", "increasing", "cumulative", "decreasing"})
|
|
280
|
+
_CONSTANT_MODES = frozenset({"constant", "possibilist"})
|
|
281
|
+
#: The named modal systems, shared with every other route (see
|
|
282
|
+
#: :mod:`unicode_logic_kit.fol.frames`). A ``G(m,n,r,s)`` Scott–Lemmon spec is
|
|
283
|
+
#: accepted as a frame name too; ``resolve_frame`` resolves both.
|
|
284
|
+
_FRAMES = _SHARED_FRAMES
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _resolve_frame(frame: str, *, route: str = "qml") -> tuple:
|
|
288
|
+
"""The conditions of ``frame``, refusing what a FIRST-ORDER route cannot
|
|
289
|
+
express: the three non-first-order conditions (Löb, McKinsey, Grz) are
|
|
290
|
+
named as the scope boundary they are, with the higher-order routes that
|
|
291
|
+
DO carry them pointed at — never silently dropped, which would answer
|
|
292
|
+
about a larger frame class than the caller asked for."""
|
|
293
|
+
try:
|
|
294
|
+
conds = resolve_frame(frame)
|
|
295
|
+
except ValueError as exc:
|
|
296
|
+
raise ValueError(f"{route}: {exc}") from None
|
|
297
|
+
for cond in conds:
|
|
298
|
+
entry = FRAME_CONDITIONS.get(cond)
|
|
299
|
+
if entry is not None and not entry.first_order:
|
|
300
|
+
raise NotImplementedError(
|
|
301
|
+
f"{route}: the frame {frame!r} needs the condition {cond!r} "
|
|
302
|
+
f"({entry.description}), which the first-order embedding "
|
|
303
|
+
"cannot express. Use the higher-order embeddings "
|
|
304
|
+
"hol.thf_modal.to_thf_modal_full / "
|
|
305
|
+
"hol.isabelle_modal.to_isabelle_modal with the same frame — "
|
|
306
|
+
"they assert the schema itself.")
|
|
307
|
+
return conds
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _geach_axiom(spec, W, R):
|
|
311
|
+
"""The Scott–Lemmon condition of ``G(m,n,r,s)`` as a closed FO axiom::
|
|
312
|
+
|
|
313
|
+
∀w,u,v (R^m(w,u) ∧ R^r(w,v) → ∃t (R^n(u,t) ∧ R^s(v,t)))
|
|
314
|
+
|
|
315
|
+
``R^0(a,b)`` is ``a = b`` and ``R^k`` for ``k > 1`` chains ``k-1``
|
|
316
|
+
existentially bound intermediate worlds, each World-guarded like every
|
|
317
|
+
other world variable here. The axiom is closed over its own variables,
|
|
318
|
+
so it cannot capture anything in a translated formula.
|
|
319
|
+
|
|
320
|
+
Those variables are minted by
|
|
321
|
+
:func:`~unicode_logic_kit.fol._identifiers.fresh_variables` (``v0``, ``v1``,
|
|
322
|
+
…). Until 0.30.0 they were ``_gw`` / ``_gu`` / ``_gv`` / ``_gt`` /
|
|
323
|
+
``_gz0``, which the kit's own VARIABLE terminal rejects — one letter plus
|
|
324
|
+
digits, no underscore — so an axiom this function built could be printed
|
|
325
|
+
but not read back by :func:`unicode_logic_kit.api.parse_any`.
|
|
326
|
+
"""
|
|
327
|
+
mint = _world_minter()
|
|
328
|
+
|
|
329
|
+
def path(a, b, k):
|
|
330
|
+
if k == 0:
|
|
331
|
+
return Atom("=", (a, b))
|
|
332
|
+
previous, mids, conj = a, [], None
|
|
333
|
+
for _ in range(k - 1):
|
|
334
|
+
z = mint()
|
|
335
|
+
mids.append(z)
|
|
336
|
+
step = And(W(z), R(previous, z))
|
|
337
|
+
conj = step if conj is None else And(conj, step)
|
|
338
|
+
previous = z
|
|
339
|
+
body = R(previous, b)
|
|
340
|
+
if conj is not None:
|
|
341
|
+
body = And(conj, body)
|
|
342
|
+
for z in reversed(mids):
|
|
343
|
+
body = Quantifier(_EXISTS, z, body)
|
|
344
|
+
return body
|
|
345
|
+
|
|
346
|
+
w, u, v, s = mint(), mint(), mint(), mint()
|
|
347
|
+
antecedent = And(And(W(w), And(W(u), W(v))),
|
|
348
|
+
And(path(w, u, spec.m), path(w, v, spec.r)))
|
|
349
|
+
consequent = Quantifier(_EXISTS, s, And(
|
|
350
|
+
W(s), And(path(u, s, spec.n), path(v, s, spec.s))))
|
|
351
|
+
body = Implies(antecedent, consequent)
|
|
352
|
+
for var in (v, u, w):
|
|
353
|
+
body = Quantifier(_FORALL, var, body)
|
|
354
|
+
return body
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
#: The letter every world variable this module mints is built from (plus
|
|
358
|
+
#: digits: ``w0``, ``w1``, …). It matches the propositional standard
|
|
359
|
+
#: translation's own world names, so the two routes' images read alike.
|
|
360
|
+
_WORLD_LETTER = "w"
|
|
361
|
+
|
|
362
|
+
#: The letter the Geach axiom's own bound worlds are built from. A separate
|
|
363
|
+
#: letter only for readability — the axiom is closed, so it cannot capture.
|
|
364
|
+
_GEACH_LETTER = "v"
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _world_minter(reserved=(), letter: str = _GEACH_LETTER):
|
|
368
|
+
"""Return a callable minting never-repeating world Variables.
|
|
369
|
+
|
|
370
|
+
The names are :func:`~unicode_logic_kit.fol._identifiers.fresh_variables`'
|
|
371
|
+
— ``letter`` plus digits — because the translation's output has to be text
|
|
372
|
+
this kit's own parser reads back. Until 0.30.0 the shapes were ``_w0`` and
|
|
373
|
+
``_gz0``, both of which :func:`unicode_logic_kit.api.parse_any` rejects.
|
|
374
|
+
"""
|
|
375
|
+
used = set(reserved)
|
|
376
|
+
|
|
377
|
+
def mint() -> Variable:
|
|
378
|
+
name = fresh_variables(1, letter=letter, avoid=used)[0]
|
|
379
|
+
used.add(name)
|
|
380
|
+
return Variable(name)
|
|
381
|
+
|
|
382
|
+
return mint
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
class _Fresh:
|
|
386
|
+
"""Fresh world-variable generator that avoids a reserved set of names.
|
|
387
|
+
|
|
388
|
+
``reserved`` is the object-variable names of the formula being translated
|
|
389
|
+
(plus the current-world name): a world variable that reused one of them
|
|
390
|
+
would be captured by the object quantifier that binds it. The names are
|
|
391
|
+
``w0``, ``w1``, … — see :func:`_world_minter`.
|
|
392
|
+
"""
|
|
393
|
+
|
|
394
|
+
def __init__(self, reserved):
|
|
395
|
+
self._mint = _world_minter(reserved, _WORLD_LETTER)
|
|
396
|
+
|
|
397
|
+
def next(self) -> Variable:
|
|
398
|
+
return self._mint()
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _object_var_names(node: Node) -> set:
|
|
402
|
+
"""All Variable names occurring in ``node`` (object variables, pre-translation)."""
|
|
403
|
+
names = set()
|
|
404
|
+
for n in node.walk():
|
|
405
|
+
if isinstance(n, Variable):
|
|
406
|
+
names.add(n.name)
|
|
407
|
+
return names
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _pick_world_name(formula: Node, preferred: str) -> str:
|
|
411
|
+
"""Return a world-variable name not clashing with any object variable in ``formula``.
|
|
412
|
+
|
|
413
|
+
The translation appends the current-world variable as the last argument of every
|
|
414
|
+
atom (``A(x)`` → ``A(x, w)``); if the caller's world name coincides with an object
|
|
415
|
+
variable the formula already binds (e.g. ``∃w A(w)`` with the default world ``w``),
|
|
416
|
+
that object quantifier would *capture* the world parameter and corrupt the
|
|
417
|
+
translation. Keep the preferred name when it is free, otherwise pick a fresh one.
|
|
418
|
+
"""
|
|
419
|
+
reserved = _object_var_names(formula)
|
|
420
|
+
if preferred not in reserved:
|
|
421
|
+
return preferred
|
|
422
|
+
# The fallback is a minted name, not ``_world0``: an identifier starting
|
|
423
|
+
# with an underscore is not a VARIABLE the kit's own parser accepts.
|
|
424
|
+
return fresh_variables(1, letter=_WORLD_LETTER, avoid=reserved)[0]
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def _box(rel: str, w: Variable, body: Node, fresh: _Fresh, mode: str) -> Node:
|
|
428
|
+
v = fresh.next()
|
|
429
|
+
guard = And(Atom(_WORLD, (v,)), Atom(rel, (w, v)))
|
|
430
|
+
return Quantifier(_FORALL, v, Implies(guard, _st(body, v, fresh, mode)))
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def _diamond(rel: str, w: Variable, body: Node, fresh: _Fresh, mode: str) -> Node:
|
|
434
|
+
v = fresh.next()
|
|
435
|
+
guard = And(Atom(_WORLD, (v,)), Atom(rel, (w, v)))
|
|
436
|
+
return Quantifier(_EXISTS, v, And(guard, _st(body, v, fresh, mode)))
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def _box_conv(rel: str, w: Variable, body: Node, fresh: _Fresh, mode: str) -> Node:
|
|
440
|
+
"""``∀v (World(v) ∧ rel(v, w) → ST(body, v))`` — a box over the CONVERSE relation."""
|
|
441
|
+
v = fresh.next()
|
|
442
|
+
guard = And(Atom(_WORLD, (v,)), Atom(rel, (v, w)))
|
|
443
|
+
return Quantifier(_FORALL, v, Implies(guard, _st(body, v, fresh, mode)))
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def _diamond_conv(rel: str, w: Variable, body: Node, fresh: _Fresh, mode: str) -> Node:
|
|
447
|
+
"""``∃v (World(v) ∧ rel(v, w) ∧ ST(body, v))`` — a diamond over the CONVERSE relation."""
|
|
448
|
+
v = fresh.next()
|
|
449
|
+
guard = And(Atom(_WORLD, (v,)), Atom(rel, (v, w)))
|
|
450
|
+
return Quantifier(_EXISTS, v, And(guard, _st(body, v, fresh, mode)))
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def _box_agent(rel: str, agent: Node, w: Variable, body: Node, fresh: _Fresh, mode: str) -> Node:
|
|
454
|
+
"""Agent-indexed □: ``∀v (World(v) ∧ rel(agent, w, v) → ST(body, v))``.
|
|
455
|
+
|
|
456
|
+
``agent`` is carried into the relation as a real term argument, so a bound object
|
|
457
|
+
variable in agent position quantifies over agents.
|
|
458
|
+
"""
|
|
459
|
+
v = fresh.next()
|
|
460
|
+
guard = And(Atom(_WORLD, (v,)), Atom(rel, (agent, w, v)))
|
|
461
|
+
return Quantifier(_FORALL, v, Implies(guard, _st(body, v, fresh, mode)))
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
_EQUALITY = "="
|
|
465
|
+
_DISEQUALITY = "≠"
|
|
466
|
+
#: The two spellings the kit gives identity atoms (``a = b`` parses to
|
|
467
|
+
#: ``Atom("=", (a, b))``, ``a ≠ b`` to ``Atom("≠", (a, b))``). Neither may ever
|
|
468
|
+
#: reach the generic ``P(t̄) → P(t̄, w)`` rule: see :func:`_st_equality`.
|
|
469
|
+
_EQUALITY_PREDICATES = frozenset({_EQUALITY, _DISEQUALITY})
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def _st_equality(atom: Atom) -> Node:
|
|
473
|
+
"""ST of an identity atom: the SAME binary identity, with NO world argument.
|
|
474
|
+
|
|
475
|
+
``t₁ = t₂`` stays ``t₁ = t₂`` and ``t₁ ≠ t₂`` becomes ``¬(t₁ = t₂)``, whatever
|
|
476
|
+
world ``ST`` is currently at. That is what makes identity **rigid** (the module
|
|
477
|
+
docstring's "Equality is rigid" section states the contract and the choice made
|
|
478
|
+
for varying domains): the translated atom does not mention the world, so it
|
|
479
|
+
cannot vary with it. It is Z3's own binary identity, so reflexivity, symmetry,
|
|
480
|
+
transitivity and congruence over function symbols and predicates (Leibniz's law)
|
|
481
|
+
come from the solver, not from any axiom of ours — :func:`qml_axioms` adds none.
|
|
482
|
+
|
|
483
|
+
``≠`` is lowered to ``¬(=)`` rather than kept as a binary ``≠`` atom because the
|
|
484
|
+
two are the same relation by definition and ``¬(=)`` is the form the downstream
|
|
485
|
+
consumers already read (CASL has no disequality connective at all, see
|
|
486
|
+
:mod:`unicode_logic_kit.hets.dol`).
|
|
487
|
+
|
|
488
|
+
The previous behaviour appended the world like for any other atom, turning
|
|
489
|
+
``a = b`` into a TERNARY uninterpreted predicate ``=(a, b, w)`` — so ``a = a`` was
|
|
490
|
+
not valid and ``□(a = b) → a = b`` was "valid" only by reflexivity of the frame,
|
|
491
|
+
never by equality. A silently re-interpreted connective is the approximation this
|
|
492
|
+
package refuses; the arity check below is the same rule for a malformed atom.
|
|
493
|
+
"""
|
|
494
|
+
if len(atom.args) != 2:
|
|
495
|
+
raise ValueError(
|
|
496
|
+
f"qml: equality atom {atom.predicate!r} needs exactly two terms, got "
|
|
497
|
+
f"{len(atom.args)} ({atom.to_unicode_str()}). '=' / '≠' are reserved for "
|
|
498
|
+
"identity and are never read as a world-relative predicate here; rename "
|
|
499
|
+
"the predicate if a different relation was meant.")
|
|
500
|
+
if atom.predicate == _DISEQUALITY:
|
|
501
|
+
return Not(Atom(_EQUALITY, tuple(atom.args)))
|
|
502
|
+
return atom
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def _st(formula: Node, w: Variable, fresh: _Fresh, mode: str) -> Node:
|
|
506
|
+
"""The shallow-embedding translation ST(formula, w)."""
|
|
507
|
+
if isinstance(formula, Down):
|
|
508
|
+
# N1: this route already has no rule for plain hybrid nominals/@
|
|
509
|
+
# (they fall through to the generic "unsupported node type" below,
|
|
510
|
+
# naming themselves that way) — ↓ gets its OWN, named check ahead of
|
|
511
|
+
# that generic one because it additionally makes validity
|
|
512
|
+
# undecidable, which is worth saying explicitly rather than leaving
|
|
513
|
+
# a reader to infer it from "unsupported node type Down".
|
|
514
|
+
raise NotImplementedError(
|
|
515
|
+
"qml: the ↓ binder is outside this first-order-modal shallow "
|
|
516
|
+
"embedding (which does not cover hybrid logic at all — no "
|
|
517
|
+
"nominals/@ either — and, separately, ↓ makes validity "
|
|
518
|
+
"undecidable). Use "
|
|
519
|
+
"unicode_logic_kit.fol.modal_translation.down_is_valid "
|
|
520
|
+
"(propositional H(@,↓), Z3, PROVED-only) or "
|
|
521
|
+
"unicode_logic_kit.atp.kripke_enum.KripkeEnumBackend / "
|
|
522
|
+
"modal_enum_search (bounded search, REFUTED-only), or evaluate "
|
|
523
|
+
"directly with unicode_logic_kit.semantics.kripke.satisfies_modal.")
|
|
524
|
+
if isinstance(formula, Atom):
|
|
525
|
+
if truth_value(formula) is not None:
|
|
526
|
+
return formula # `$true` / `$false`: the same at every world, no world argument
|
|
527
|
+
if formula.predicate in _EQUALITY_PREDICATES:
|
|
528
|
+
return _st_equality(formula)
|
|
529
|
+
return Atom(_user_predicate(formula.predicate), (*formula.args, w))
|
|
530
|
+
if isinstance(formula, Not):
|
|
531
|
+
return Not(_st(formula.formula, w, fresh, mode))
|
|
532
|
+
if isinstance(formula, And):
|
|
533
|
+
return And(_st(formula.left, w, fresh, mode), _st(formula.right, w, fresh, mode))
|
|
534
|
+
if isinstance(formula, Or):
|
|
535
|
+
return Or(_st(formula.left, w, fresh, mode), _st(formula.right, w, fresh, mode))
|
|
536
|
+
if isinstance(formula, Xor):
|
|
537
|
+
return Xor(_st(formula.left, w, fresh, mode), _st(formula.right, w, fresh, mode))
|
|
538
|
+
if isinstance(formula, Implies):
|
|
539
|
+
return Implies(_st(formula.left, w, fresh, mode), _st(formula.right, w, fresh, mode))
|
|
540
|
+
if isinstance(formula, Iff):
|
|
541
|
+
return Iff(_st(formula.left, w, fresh, mode), _st(formula.right, w, fresh, mode))
|
|
542
|
+
|
|
543
|
+
if isinstance(formula, Box):
|
|
544
|
+
return _box(_R_ALETHIC, w, formula.formula, fresh, mode)
|
|
545
|
+
if isinstance(formula, Diamond):
|
|
546
|
+
return _diamond(_R_ALETHIC, w, formula.formula, fresh, mode)
|
|
547
|
+
if isinstance(formula, Knows):
|
|
548
|
+
return _box_agent(_R_KNOWS, formula.agent, w, formula.formula, fresh, mode)
|
|
549
|
+
if isinstance(formula, Believes):
|
|
550
|
+
return _box_agent(_R_BELIEVES, formula.agent, w, formula.formula, fresh, mode)
|
|
551
|
+
if isinstance(formula, Says):
|
|
552
|
+
return _box_agent(_R_SAYS, formula.agent, w, formula.formula, fresh, mode)
|
|
553
|
+
if isinstance(formula, Wants):
|
|
554
|
+
return _box_agent(_R_WANTS, formula.agent, w, formula.formula, fresh, mode)
|
|
555
|
+
if isinstance(formula, Obligatory):
|
|
556
|
+
return _box(_R_DEONTIC, w, formula.formula, fresh, mode)
|
|
557
|
+
if isinstance(formula, Permitted):
|
|
558
|
+
return _diamond(_R_DEONTIC, w, formula.formula, fresh, mode)
|
|
559
|
+
if isinstance(formula, Always):
|
|
560
|
+
return _box(_R_TEMPORAL, w, formula.formula, fresh, mode)
|
|
561
|
+
if isinstance(formula, Eventually):
|
|
562
|
+
return _diamond(_R_TEMPORAL, w, formula.formula, fresh, mode)
|
|
563
|
+
if isinstance(formula, Next):
|
|
564
|
+
return _box(_R_NEXT, w, formula.formula, fresh, mode)
|
|
565
|
+
if isinstance(formula, Historically):
|
|
566
|
+
return _box_conv(_R_TEMPORAL, w, formula.formula, fresh, mode)
|
|
567
|
+
if isinstance(formula, Once):
|
|
568
|
+
return _diamond_conv(_R_TEMPORAL, w, formula.formula, fresh, mode)
|
|
569
|
+
if isinstance(formula, Previous):
|
|
570
|
+
return _box_conv(_R_NEXT, w, formula.formula, fresh, mode)
|
|
571
|
+
|
|
572
|
+
if isinstance(formula, Quantifier):
|
|
573
|
+
x = formula.variable
|
|
574
|
+
body = _st(formula.formula, w, fresh, mode)
|
|
575
|
+
obj = Atom(_OBJECT, (x,))
|
|
576
|
+
guard: Node
|
|
577
|
+
if mode in _ACTUALIST_MODES:
|
|
578
|
+
guard = And(obj, Atom(_E, (x, w))) # actualist: x exists at w
|
|
579
|
+
else:
|
|
580
|
+
guard = obj # constant / possibilist
|
|
581
|
+
if formula.type in (_FORALL, "forall"):
|
|
582
|
+
return Quantifier(_FORALL, x, Implies(guard, body))
|
|
583
|
+
if formula.type in (_EXISTS, "exists"):
|
|
584
|
+
return Quantifier(_EXISTS, x, And(guard, body))
|
|
585
|
+
raise ValueError(f"qml: unknown quantifier type {formula.type!r}")
|
|
586
|
+
|
|
587
|
+
if isinstance(formula, Until):
|
|
588
|
+
raise NotImplementedError(
|
|
589
|
+
"qml: Until is not first-order definable (needs transitive closure); "
|
|
590
|
+
"evaluate it with satisfies_modal, or emit the HOL embedding with "
|
|
591
|
+
"to_isabelle_modal (inductive least-fixpoint muntil)."
|
|
592
|
+
)
|
|
593
|
+
if isinstance(formula, Since):
|
|
594
|
+
raise NotImplementedError(
|
|
595
|
+
"qml: Since is not first-order definable (the backward mirror of "
|
|
596
|
+
"Until — it needs a transitive-closure fixpoint); evaluate it with "
|
|
597
|
+
"satisfies_modal, or emit the HOL embedding with to_isabelle_modal "
|
|
598
|
+
"(inductive least-fixpoint msince) / to_thf_modal_full."
|
|
599
|
+
)
|
|
600
|
+
if isinstance(formula, SortedQuantifier):
|
|
601
|
+
# Delegate to the guarded plain Quantifier _relativize builds, then
|
|
602
|
+
# translate THAT: _st's own Quantifier case adds the usual
|
|
603
|
+
# Object/E guard, and its Atom case appends the world argument to
|
|
604
|
+
# the sort guard atom exactly as it does to every other atom, so the
|
|
605
|
+
# two guards compose correctly with no special-casing here (see the
|
|
606
|
+
# module docstring's "Many-sorted formulas" section). Normally
|
|
607
|
+
# unreachable through the public entry points, which already
|
|
608
|
+
# relativize the WHOLE formula once up front (qml_translate) — kept
|
|
609
|
+
# as a defensive fallback for a direct/recursive _st call on an
|
|
610
|
+
# unrelativized sub-formula.
|
|
611
|
+
return _st(formula._relativize([]), w, fresh, mode)
|
|
612
|
+
raise NotImplementedError(f"qml: unsupported node type {type(formula).__name__}.")
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
def qml_translate(formula: Node, mode: str = "constant", world: str = "w") -> Node:
|
|
616
|
+
"""Return the shallow-embedding translation ``ST(formula, world)`` (a classical FO Node).
|
|
617
|
+
|
|
618
|
+
``mode`` selects the domain regime for object quantifiers — ``"constant"`` /
|
|
619
|
+
``"possibilist"`` (unrelativised) or ``"varying"`` / ``"increasing"`` /
|
|
620
|
+
``"decreasing"`` (actualist, guarded by the existence predicate ``E``).
|
|
621
|
+
|
|
622
|
+
If ``world`` clashes with an object variable the formula binds, a fresh world name
|
|
623
|
+
is substituted to prevent that quantifier from capturing the world parameter.
|
|
624
|
+
|
|
625
|
+
A variable that is free in ``formula`` stays free in the image, as the parameter it
|
|
626
|
+
is. What the translation does not state is its typing (an element of the object
|
|
627
|
+
domain) or, under a varying regime, its existence at ``world``: those are facts of the
|
|
628
|
+
validity query, :func:`qml_validity_formula`. See the module docstring's "A free
|
|
629
|
+
variable is a parameter".
|
|
630
|
+
|
|
631
|
+
An identity atom is the one atom that does NOT get the world appended: ``t₁ = t₂``
|
|
632
|
+
stays ``t₁ = t₂`` and ``t₁ ≠ t₂`` becomes ``¬(t₁ = t₂)``, so identity is rigid (see
|
|
633
|
+
the module docstring's "Equality is rigid", which also states the varying-domain
|
|
634
|
+
choice). A non-binary ``=`` / ``≠`` atom raises ``ValueError`` rather than being
|
|
635
|
+
read as a world-relative predicate.
|
|
636
|
+
|
|
637
|
+
A many-sorted ``formula`` (``SortedQuantifier`` / ``SortedConstant``) is relativized
|
|
638
|
+
ONCE, here, before anything else runs — see the module docstring's "Many-sorted
|
|
639
|
+
formulas" section for what that does and does not assume. The relativisation forgets
|
|
640
|
+
which constants were annotated, so the membership of a sorted constant in its sort is
|
|
641
|
+
NOT part of this translation: it is an axiom, which :func:`qml_axioms` emits from the
|
|
642
|
+
formula as given.
|
|
643
|
+
"""
|
|
644
|
+
if mode not in _ACTUALIST_MODES and mode not in _CONSTANT_MODES:
|
|
645
|
+
raise ValueError(
|
|
646
|
+
f"qml: unknown mode {mode!r} (use one of "
|
|
647
|
+
f"{sorted(_ACTUALIST_MODES | _CONSTANT_MODES)}).")
|
|
648
|
+
formula = formula._relativize([])
|
|
649
|
+
world = _pick_world_name(formula, world)
|
|
650
|
+
fresh = _Fresh(_object_var_names(formula) | {world})
|
|
651
|
+
return _st(formula, Variable(world), fresh, mode)
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
def _v(*names):
|
|
655
|
+
return [Variable(n) for n in names]
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
_AGENT_FAMILIES = {"epistemic": _R_KNOWS, "doxastic": _R_BELIEVES,
|
|
659
|
+
"assertive": _R_SAYS, "bouletic": _R_WANTS}
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
def _agent_frame_axioms(rel_name: str, conds) -> List[Node]:
|
|
663
|
+
"""Frame axioms for an AGENT-INDEXED relation ``Rel(a, w, v)`` (per agent ``a``).
|
|
664
|
+
|
|
665
|
+
Mirrors the alethic frame conditions but quantifies the agent too, so a chosen
|
|
666
|
+
epistemic/doxastic system (e.g. S5 for knowledge, KD45 for belief) constrains every
|
|
667
|
+
agent's accessibility — making e.g. factivity ``K_a φ → φ`` valid under a reflexive
|
|
668
|
+
(T/S4/S5) epistemic system.
|
|
669
|
+
"""
|
|
670
|
+
a, w, v, u = _v("a", "w", "v", "u")
|
|
671
|
+
W = lambda z: Atom(_WORLD, (z,))
|
|
672
|
+
O = lambda z: Atom(_OBJECT, (z,))
|
|
673
|
+
Rel = lambda *args: Atom(rel_name, tuple(args))
|
|
674
|
+
fa = lambda var, body: Quantifier(_FORALL, var, body)
|
|
675
|
+
fa4 = lambda body: fa(a, fa(w, fa(v, fa(u, body))))
|
|
676
|
+
out: List[Node] = [
|
|
677
|
+
# typing: Rel(a, w, v) → Object(a) ∧ World(w) ∧ World(v).
|
|
678
|
+
fa(a, fa(w, fa(v, Implies(Rel(a, w, v), And(O(a), And(W(w), W(v))))))),
|
|
679
|
+
]
|
|
680
|
+
if "refl" in conds:
|
|
681
|
+
out.append(fa(a, fa(w, Implies(And(O(a), W(w)), Rel(a, w, w)))))
|
|
682
|
+
if "trans" in conds:
|
|
683
|
+
out.append(fa4(Implies(
|
|
684
|
+
And(And(O(a), W(w)), And(And(W(v), W(u)), And(Rel(a, w, v), Rel(a, v, u)))),
|
|
685
|
+
Rel(a, w, u))))
|
|
686
|
+
if "sym" in conds:
|
|
687
|
+
out.append(fa(a, fa(w, fa(v, Implies(
|
|
688
|
+
And(And(O(a), W(w)), And(W(v), Rel(a, w, v))), Rel(a, v, w))))))
|
|
689
|
+
if "eucl" in conds:
|
|
690
|
+
out.append(fa4(Implies(
|
|
691
|
+
And(And(O(a), W(w)), And(And(W(v), W(u)), And(Rel(a, w, v), Rel(a, w, u)))),
|
|
692
|
+
Rel(a, v, u))))
|
|
693
|
+
if "serial" in conds:
|
|
694
|
+
out.append(fa(a, fa(w, Implies(And(O(a), W(w)),
|
|
695
|
+
Quantifier(_EXISTS, v, And(W(v), Rel(a, w, v)))))))
|
|
696
|
+
if "directed" in conds:
|
|
697
|
+
out.append(fa(a, fa(w, fa(v, fa(u, Implies(
|
|
698
|
+
And(And(O(a), W(w)), And(And(W(v), W(u)), And(Rel(a, w, v), Rel(a, w, u)))),
|
|
699
|
+
Quantifier(_EXISTS, Variable("z"), And(W(Variable("z")),
|
|
700
|
+
And(Rel(a, v, Variable("z")), Rel(a, u, Variable("z")))))))))))
|
|
701
|
+
if "connected" in conds:
|
|
702
|
+
out.append(fa(a, fa(w, fa(v, fa(u, Implies(
|
|
703
|
+
And(And(O(a), W(w)), And(And(W(v), W(u)), And(Rel(a, w, v), Rel(a, w, u)))),
|
|
704
|
+
Or(Rel(a, v, u), Rel(a, u, v))))))))
|
|
705
|
+
return out
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
# Every accessibility relation this embedding can introduce. ``qml_axioms`` called
|
|
709
|
+
# WITHOUT a formula ("give me the background theory") emits axioms for all of them.
|
|
710
|
+
QML_RELATIONS = (_R_ALETHIC, _R_TEMPORAL, _R_NEXT, _R_DEONTIC,
|
|
711
|
+
_R_KNOWS, _R_BELIEVES, _R_SAYS, _R_WANTS)
|
|
712
|
+
|
|
713
|
+
# Which relation each modal node reads. ``Until`` / ``Since`` are deliberately ABSENT:
|
|
714
|
+
# ``_st`` rejects them outright (not first-order definable), so they can never reach a
|
|
715
|
+
# validity query, and listing them here would suggest this route handles them.
|
|
716
|
+
_REL_OF_NODE = (
|
|
717
|
+
((Box, Diamond), _R_ALETHIC),
|
|
718
|
+
((Obligatory, Permitted), _R_DEONTIC),
|
|
719
|
+
((Always, Eventually, Historically, Once), _R_TEMPORAL),
|
|
720
|
+
((Next, Previous), _R_NEXT),
|
|
721
|
+
((Knows,), _R_KNOWS),
|
|
722
|
+
((Believes,), _R_BELIEVES),
|
|
723
|
+
((Says,), _R_SAYS),
|
|
724
|
+
((Wants,), _R_WANTS),
|
|
725
|
+
)
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
def _relations_used(formula: Node) -> set:
|
|
729
|
+
"""The accessibility relations ``formula`` actually mentions.
|
|
730
|
+
|
|
731
|
+
Gating the frame axioms on this is **not** cosmetic. The deontic seriality axiom is
|
|
732
|
+
the only ∃-quantified frame condition in the set, and Z3's model finder chokes on it:
|
|
733
|
+
measured, asserting ``d_serial`` unconditionally turned ``∃x □A(x) → □∃x A(x)`` under
|
|
734
|
+
varying domains — a formula with no deontic operator at all — from a 0.017 s
|
|
735
|
+
countermodel into a 10 s timeout, i.e. flipped a *reported* verdict. The temporal
|
|
736
|
+
axioms are near-free by comparison, but the rule is uniform: a relation that does not
|
|
737
|
+
occur contributes nothing, so for a formula that uses none of ``T`` / ``N`` / ``D``
|
|
738
|
+
(and no configured ``systems=`` family) :func:`qml_axioms` returns the exact same
|
|
739
|
+
list — same members, same order — as it did before those axioms existed. That
|
|
740
|
+
identity is load-bearing: several suite queries sit on a Z3 knife edge under a short
|
|
741
|
+
timeout, where *any* perturbation of the axiom list can flip a verdict.
|
|
742
|
+
"""
|
|
743
|
+
used = set()
|
|
744
|
+
for n in formula.walk():
|
|
745
|
+
for classes, rel in _REL_OF_NODE:
|
|
746
|
+
if isinstance(n, classes):
|
|
747
|
+
used.add(rel)
|
|
748
|
+
break
|
|
749
|
+
return used
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
def _relation_typing(rel: str) -> Node:
|
|
753
|
+
"""``∀w ∀v (rel(w,v) → World(w) ∧ World(v))`` — a binary relation is World×World.
|
|
754
|
+
|
|
755
|
+
Mirrors the ``R`` typing axiom exactly. Without it a model could relate an *object*
|
|
756
|
+
to a world and the guard predicates would no longer carve the universe in two.
|
|
757
|
+
"""
|
|
758
|
+
w, v = _v("w", "v")
|
|
759
|
+
return Quantifier(_FORALL, w, Quantifier(_FORALL, v, Implies(
|
|
760
|
+
Atom(rel, (w, v)), And(Atom(_WORLD, (w,)), Atom(_WORLD, (v,))))))
|
|
761
|
+
|
|
762
|
+
|
|
763
|
+
def _temporal_frame_axioms() -> List[Node]:
|
|
764
|
+
"""``T`` is reflexive and transitive — the *henceforth* relation.
|
|
765
|
+
|
|
766
|
+
:func:`unicode_logic_kit.semantics.kripke.satisfies_modal` evaluates ``Always`` /
|
|
767
|
+
``Eventually`` over the reflexive-transitive CLOSURE of the one-step ``"temporal"``
|
|
768
|
+
edges (and ``Historically`` / ``Once`` over the closure of the reversed edges), so
|
|
769
|
+
the current world is always in view and reachability composes. Asserting exactly that
|
|
770
|
+
makes ``Gφ → φ``, ``Gφ → GGφ``, ``Gφ → Fφ``, ``φ → Fφ`` and the past mirrors valid
|
|
771
|
+
here, matching the oracle and the Isabelle route's ``t_refl`` / ``t_trans``.
|
|
772
|
+
|
|
773
|
+
These two axioms plus ``n_in_t`` say ``T ⊇ N*`` and no more. The converse ``T ⊆ N*``
|
|
774
|
+
is not first-order definable, but a first-order CONSEQUENCE of it is, and
|
|
775
|
+
:func:`_temporal_first_step_axiom` asserts it; temporal induction stays out of reach
|
|
776
|
+
even so — see the module docstring and use ``isabelle_decide_modal`` for that one.
|
|
777
|
+
"""
|
|
778
|
+
w, v, u = _v("w", "v", "u")
|
|
779
|
+
W = lambda a: Atom(_WORLD, (a,))
|
|
780
|
+
T = lambda a, b: Atom(_R_TEMPORAL, (a, b))
|
|
781
|
+
fa = lambda var, body: Quantifier(_FORALL, var, body)
|
|
782
|
+
return [
|
|
783
|
+
fa(w, Implies(W(w), T(w, w))),
|
|
784
|
+
fa(w, fa(v, fa(u, Implies(
|
|
785
|
+
And(And(W(w), W(v)), And(W(u), And(T(w, v), T(v, u)))), T(w, u))))),
|
|
786
|
+
]
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
def _next_in_temporal_axiom() -> Node:
|
|
790
|
+
"""``∀w ∀v (World(w) ∧ World(v) ∧ N(w,v) → T(w,v))`` — one step is a step.
|
|
791
|
+
|
|
792
|
+
``_st`` splits the oracle's single ``"temporal"`` relation into two symbols: ``N``
|
|
793
|
+
carries the raw one-step edges that ``Next`` / ``Previous`` read, ``T`` carries the
|
|
794
|
+
closure that ``Always`` / ``Eventually`` read. Decoupled, the embedding would refute
|
|
795
|
+
``Gφ → Xφ``, which the oracle validates. This inclusion restores it, and is the FO
|
|
796
|
+
counterpart of ``isabelle_modal``'s ``n_in_t``.
|
|
797
|
+
"""
|
|
798
|
+
w, v = _v("w", "v")
|
|
799
|
+
return Quantifier(_FORALL, w, Quantifier(_FORALL, v, Implies(
|
|
800
|
+
And(And(Atom(_WORLD, (w,)), Atom(_WORLD, (v,))), Atom(_R_NEXT, (w, v))),
|
|
801
|
+
Atom(_R_TEMPORAL, (w, v)))))
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
def _temporal_first_step_axiom() -> Node:
|
|
805
|
+
"""``∀w ∀v (World(w) ∧ World(v) ∧ T(w,v) → w = v ∨ ∃u (World(u) ∧ N(w,u) ∧ T(u,v)))``.
|
|
806
|
+
|
|
807
|
+
"A henceforth-step is either standing still or one step followed by a henceforth-step"
|
|
808
|
+
— the FIRST STEP of the path that witnesses ``T``, made explicit. This is the
|
|
809
|
+
first-order half of ``T ⊆ N*`` that *is* expressible: ``T ⊆ N*`` itself demands the
|
|
810
|
+
witnessing path be finite and so is not first-order definable, but every model with
|
|
811
|
+
``T = N*`` satisfies the implication above, which is what makes asserting it sound.
|
|
812
|
+
``t_refl`` / ``t_trans`` / ``n_in_t`` alone do not entail it — they hold of any
|
|
813
|
+
refl-trans superset of ``N``, including one with edges no ``N``-path underwrites.
|
|
814
|
+
|
|
815
|
+
What it buys, measured: the ``←`` direction of the fixpoint unfolding
|
|
816
|
+
``(φ ∧ XGφ) → Gφ`` becomes provable (Z3 closes it in well under a second), where the
|
|
817
|
+
shipped axioms alone left it underivable. What it does NOT buy: temporal induction
|
|
818
|
+
``(φ ∧ G(φ → Xφ)) → Gφ`` stays underivable, because unrolling the first step finitely
|
|
819
|
+
many times never reaches an arbitrary ``T``-successor — that needs induction over the
|
|
820
|
+
closure, which no first-order theory states. So the honest summary of this route's
|
|
821
|
+
temporal strength is "refl + trans + ``N ⊆ T`` + ``first_step``", not "everything
|
|
822
|
+
first-order logic can reach" and not "everything the oracle validates".
|
|
823
|
+
|
|
824
|
+
The ``w = v`` disjunct is genuine identity: at this level ``Atom("=", [w, v])``
|
|
825
|
+
lowers to Z3's own equality (``Node.to_z3`` maps binary ``=`` natively). An
|
|
826
|
+
object-language ``=`` inside a modal formula is translated to that SAME binary
|
|
827
|
+
identity (rigid, no world argument — see the module docstring), so the two share
|
|
828
|
+
a symbol and differ in what they relate: this axiom's operands are
|
|
829
|
+
``World``-guarded worlds, a user equality's are objects, and ``World`` / ``Object``
|
|
830
|
+
are disjoint, so neither equates operands of the other kind.
|
|
831
|
+
|
|
832
|
+
Emitted only when ``T`` and ``N`` BOTH occur and ``temporal_closure`` is on, mirroring
|
|
833
|
+
``isabelle_modal``'s ``t_in_nstar`` (which pins ``t = rtranclp n`` outright, HOL being
|
|
834
|
+
able to say what first-order logic cannot) and for the same reason: the axiom relates
|
|
835
|
+
the two relations, so it is vacuous — and misleading — unless both are in play.
|
|
836
|
+
"""
|
|
837
|
+
w, v, u = _v("w", "v", "u")
|
|
838
|
+
W = lambda a: Atom(_WORLD, (a,))
|
|
839
|
+
T = lambda a, b: Atom(_R_TEMPORAL, (a, b))
|
|
840
|
+
fa = lambda var, body: Quantifier(_FORALL, var, body)
|
|
841
|
+
return fa(w, fa(v, Implies(
|
|
842
|
+
And(And(W(w), W(v)), T(w, v)),
|
|
843
|
+
Or(Atom("=", (w, v)),
|
|
844
|
+
Quantifier(_EXISTS, u, And(And(W(u), Atom(_R_NEXT, (w, u))), T(u, v)))))))
|
|
845
|
+
|
|
846
|
+
|
|
847
|
+
def _deontic_frame_axioms() -> List[Node]:
|
|
848
|
+
"""``∀w (World(w) → ∃v (World(v) ∧ D(w,v)))`` — the deontic relation is serial (KD).
|
|
849
|
+
|
|
850
|
+
Standard Deontic Logic is exactly this condition, and it is the reading the
|
|
851
|
+
``Obligatory`` / ``Permitted`` node docstrings and the :mod:`kripke` module docstring
|
|
852
|
+
already declare; both HOL routes commit to it unconditionally
|
|
853
|
+
(``isabelle_modal._deontic_axioms``, ``thf_modal._THF_DEONTIC_AXIOM``). It validates
|
|
854
|
+
``Oφ → Pφ`` and ``Oφ → ¬O¬φ`` and leaves ``Oφ → φ`` invalid.
|
|
855
|
+
|
|
856
|
+
Consequence for the contract: ``True`` for a deontic formula means "valid over every
|
|
857
|
+
SERIAL-deontic model". ``satisfies_modal`` itself enforces nothing, so a hand-built
|
|
858
|
+
dead-end deontic model refutes ``Oφ → Pφ`` there — that is the honest reading of the
|
|
859
|
+
disagreement, not a bug on either side.
|
|
860
|
+
"""
|
|
861
|
+
w, v = _v("w", "v")
|
|
862
|
+
return [Quantifier(_FORALL, w, Implies(
|
|
863
|
+
Atom(_WORLD, (w,)),
|
|
864
|
+
Quantifier(_EXISTS, v, And(Atom(_WORLD, (v,)), Atom(_R_DEONTIC, (w, v))))))]
|
|
865
|
+
|
|
866
|
+
|
|
867
|
+
# Cross-family bridges. Each entry names the two relations the bridge relates (with the
|
|
868
|
+
# operator label to quote in an error message), the schema it is named after, and the
|
|
869
|
+
# frame condition emitted for it.
|
|
870
|
+
#
|
|
871
|
+
# EVERY CONDITION IS THE EXACT FRAME CORRESPONDENT OF ITS SCHEMA — necessary as well as
|
|
872
|
+
# sufficient — and is the SAME condition ``hol.isabelle_modal`` / ``hol.thf_modal`` emit
|
|
873
|
+
# under the same option name, down to the fact name in ``"fact"``. One option name must
|
|
874
|
+
# denote one logic on every route that offers it; a route-vs-route split is exactly the
|
|
875
|
+
# disagreement ``bridges=`` was introduced to remove (``atp.modal_tableau`` refuses the
|
|
876
|
+
# option outright for the same reason).
|
|
877
|
+
#
|
|
878
|
+
# For the two agent-indexed bridges the correspondent is a relation INCLUSION: a box over
|
|
879
|
+
# the super-relation entails the box over the sub-relation, so the relation of the ENTAILED
|
|
880
|
+
# modality is the subset, and the reversed inclusion does not validate the principle.
|
|
881
|
+
#
|
|
882
|
+
# THE ``D ⊆ R`` TRAP — do NOT "simplify" ought_implies_can into an inclusion. Measured
|
|
883
|
+
# (:func:`_bridge_axiom` documents the witnesses): ``D ⊆ R`` on its own does not validate
|
|
884
|
+
# ``Oφ → ◇φ`` at all — a world with no deontic successor makes ``Oφ`` vacuously true while
|
|
885
|
+
# ``◇φ`` is false — and ``D ⊆ R`` TOGETHER WITH the default-on ``d_serial`` validates the
|
|
886
|
+
# strictly stronger ``□φ → Oφ`` ("whatever is necessary is obligatory") plus ``Pφ → ◇φ``,
|
|
887
|
+
# neither of which the caller asked for. The existential "meet" condition below is exactly
|
|
888
|
+
# right: it subsumes deontic seriality and validates the schema and nothing else.
|
|
889
|
+
#: The cross-family bridge schemas :func:`qml_is_valid` accepts in
|
|
890
|
+
#: ``bridges=[...]``, keyed by name. Each entry names the accessibility
|
|
891
|
+
#: relations it needs, the schema it validates, the frame condition that does
|
|
892
|
+
#: so, and the fact name the exporters emit. The same three names are accepted
|
|
893
|
+
#: by the HOL routes via :data:`unicode_logic_kit.hol.BRIDGES`.
|
|
894
|
+
QML_BRIDGES = {
|
|
895
|
+
"knowledge_implies_belief": {
|
|
896
|
+
"needs": ((_R_KNOWS, "Knows"), (_R_BELIEVES, "Believes")),
|
|
897
|
+
"schema": "K_a φ → B_a φ",
|
|
898
|
+
"condition": "Rb ⊆ Rk",
|
|
899
|
+
"fact": "rb_in_rk",
|
|
900
|
+
},
|
|
901
|
+
"sincerity": {
|
|
902
|
+
"needs": ((_R_SAYS, "Says"), (_R_BELIEVES, "Believes")),
|
|
903
|
+
"schema": "Say_a φ → B_a φ",
|
|
904
|
+
"condition": "Rb ⊆ Rs",
|
|
905
|
+
"fact": "rb_in_rs",
|
|
906
|
+
},
|
|
907
|
+
"ought_implies_can": {
|
|
908
|
+
"needs": ((_R_DEONTIC, "Obligatory/Permitted"), (_R_ALETHIC, "□/◇")),
|
|
909
|
+
"schema": "Oφ → ◇φ",
|
|
910
|
+
"condition": "∀w (World(w) → ∃v (World(v) ∧ D(w,v) ∧ R(w,v)))",
|
|
911
|
+
"fact": "d_meets_r",
|
|
912
|
+
},
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
# The two agent-indexed bridges are inclusions between ternary relations; the deontic one
|
|
916
|
+
# is the binary "meet" condition. Kept next to the table so the shape of each axiom is
|
|
917
|
+
# read off one place.
|
|
918
|
+
_BRIDGE_INCLUSIONS = {
|
|
919
|
+
"knowledge_implies_belief": (_R_BELIEVES, _R_KNOWS), # Rb ⊆ Rk
|
|
920
|
+
"sincerity": (_R_BELIEVES, _R_SAYS), # Rb ⊆ Rs
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
|
|
924
|
+
def _bridge_axiom(name: str) -> Node:
|
|
925
|
+
"""The frame condition realising the cross-family bridge ``name``.
|
|
926
|
+
|
|
927
|
+
The two INCLUSION bridges are emitted **unguarded** — ``∀a ∀w ∀v (Rb(a,w,v) →
|
|
928
|
+
Rk(a,w,v))``, not the ``Object(a) ∧ World(w) ∧ …`` guarded form. That is deliberate
|
|
929
|
+
and load-bearing: the axiom constrains only the relations, every *use* site is already
|
|
930
|
+
``World``-guarded by ``_box`` / ``_diamond`` / ``_box_agent``, and a guarded version
|
|
931
|
+
would silently fail to fire for a FREE agent variable, since nothing types a free
|
|
932
|
+
variable as an ``Object`` — so ``∀x (K_x φ → B_x φ)`` would come out invalid.
|
|
933
|
+
Soundness is untouched: any oracle model in which the inclusion holds between world
|
|
934
|
+
pairs expands to a first-order model of the unguarded axiom.
|
|
935
|
+
|
|
936
|
+
``ought_implies_can`` is the ∃-quantified MEET condition ``∀w (World(w) → ∃v (World(v)
|
|
937
|
+
∧ D(w,v) ∧ R(w,v)))`` — every world has an alternative that is *both* deontically
|
|
938
|
+
ideal and alethically possible — which is the exact correspondent of ``Oφ → ◇φ`` and
|
|
939
|
+
the same condition the HOL routes emit as ``d_meets_r``. Two details:
|
|
940
|
+
|
|
941
|
+
* Unlike the inclusions this one **must** be ``World``-guarded. Unguarded it would
|
|
942
|
+
demand a ``D``-successor for every element of the universe, including objects; with
|
|
943
|
+
the ``D`` typing axiom that forces every object to be a world, contradicting the
|
|
944
|
+
sort-disjointness axiom — an inconsistent hypothesis, under which *everything*
|
|
945
|
+
comes out valid. The guard costs nothing, since ``_box`` / ``_diamond`` only ever
|
|
946
|
+
instantiate the axiom at worlds.
|
|
947
|
+
* It does **not** validate the ``D ⊆ R`` artefacts ``□φ → Oφ`` and ``Pφ → ◇φ``, and
|
|
948
|
+
that is a measured difference rather than a stylistic one. The frame
|
|
949
|
+
``W = {0,1,2}``, ``D = {(0,1),(0,2),(1,1),(2,2)}``, ``R = {(0,1),(1,1),(2,2)}``
|
|
950
|
+
satisfies the meet condition at every world, yet refutes ``□P → OP`` at 0 (with
|
|
951
|
+
``P`` true only at 1) and ``PP → ◇P`` at 0 (with ``P`` true only at 2), while
|
|
952
|
+
``OP → ◇P`` holds — checked against ``semantics.kripke.satisfies_modal``.
|
|
953
|
+
"""
|
|
954
|
+
fa = lambda var, body: Quantifier(_FORALL, var, body)
|
|
955
|
+
if name in _BRIDGE_INCLUSIONS:
|
|
956
|
+
sub, sup = _BRIDGE_INCLUSIONS[name]
|
|
957
|
+
a, w, v = _v("a", "w", "v")
|
|
958
|
+
return fa(a, fa(w, fa(v, Implies(Atom(sub, (a, w, v)), Atom(sup, (a, w, v))))))
|
|
959
|
+
if name != "ought_implies_can":
|
|
960
|
+
raise ValueError(f"qml: no axiom shape for bridge {name!r}.")
|
|
961
|
+
w, v = _v("w", "v")
|
|
962
|
+
return fa(w, Implies(
|
|
963
|
+
Atom(_WORLD, (w,)),
|
|
964
|
+
Quantifier(_EXISTS, v, And(Atom(_WORLD, (v,)),
|
|
965
|
+
And(Atom(_R_DEONTIC, (w, v)), Atom(_R_ALETHIC, (w, v)))))))
|
|
966
|
+
|
|
967
|
+
|
|
968
|
+
def _validate_bridges(bridges) -> List[str]:
|
|
969
|
+
"""Normalise and check ``bridges=``; an unknown name must never be silently ignored."""
|
|
970
|
+
if not bridges:
|
|
971
|
+
return []
|
|
972
|
+
if isinstance(bridges, str):
|
|
973
|
+
bridges = [bridges]
|
|
974
|
+
names = list(bridges)
|
|
975
|
+
for name in names:
|
|
976
|
+
if name not in QML_BRIDGES:
|
|
977
|
+
raise ValueError(
|
|
978
|
+
f"qml: unknown bridge {name!r} (use one of {sorted(QML_BRIDGES)}).")
|
|
979
|
+
return names
|
|
980
|
+
|
|
981
|
+
|
|
982
|
+
def _check_bridge_families(names: List[str], used: set) -> None:
|
|
983
|
+
"""Reject a bridge whose partner family does not occur in the formula.
|
|
984
|
+
|
|
985
|
+
A bridge is a claim about how two relations sit relative to each other, so asking for
|
|
986
|
+
one while the formula mentions only one of the two families is asking for something
|
|
987
|
+
the query cannot express. The same three policies the HOL routes weigh apply here, and
|
|
988
|
+
the same one wins (see ``hol.isabelle_modal._bridge_axioms``):
|
|
989
|
+
|
|
990
|
+
- *skip it silently*: the caller asked for a logic and would get a strictly weaker one
|
|
991
|
+
with no signal — the "silently weakened logic" failure this package refuses;
|
|
992
|
+
- *emit it anyway*: not conservative. ``d_meets_r`` entails ``∃v R(w,v)``, i.e.
|
|
993
|
+
seriality of the ALETHIC relation, so emitting ``ought_implies_can`` for a ``□``-only
|
|
994
|
+
formula would turn ``□P → ◇P`` from invalid into valid under ``frame="K"`` — a
|
|
995
|
+
silent change to the alethic logic the caller *did* select;
|
|
996
|
+
- *raise*: one uniform rule for every bridge, present and future, and the same rule the
|
|
997
|
+
HOL routes apply, so a ``bridges=`` call means the same thing on every route.
|
|
998
|
+
|
|
999
|
+
``ValueError`` — not ``NotImplementedError`` — because the route *can* express the
|
|
1000
|
+
bridge; the caller's formula simply does not mention one of the families.
|
|
1001
|
+
``qml_axioms`` called **without** a formula asks for the whole background theory, in
|
|
1002
|
+
which every relation is in scope, so nothing is rejected there.
|
|
1003
|
+
"""
|
|
1004
|
+
for name in names:
|
|
1005
|
+
spec = QML_BRIDGES[name]
|
|
1006
|
+
missing = [op for rel, op in spec["needs"] if rel not in used]
|
|
1007
|
+
if missing:
|
|
1008
|
+
raise ValueError(
|
|
1009
|
+
f"qml: the bridge {name!r} relates two modal families "
|
|
1010
|
+
f"({spec['condition']}), but the formula contains no "
|
|
1011
|
+
f"{' / '.join(missing)} operator, so that relation is unconstrained by "
|
|
1012
|
+
"the query. Emitting the condition anyway is not conservative — "
|
|
1013
|
+
"d_meets_r entails seriality of the alethic R, which would silently make "
|
|
1014
|
+
"□P → ◇P valid under frame='K' — and skipping it would answer for a "
|
|
1015
|
+
"weaker logic than requested. Drop the bridge, state the formula in both "
|
|
1016
|
+
"families, or call qml_axioms() without formula= for the whole "
|
|
1017
|
+
"background theory. (Same rule as to_isabelle_modal / to_thf_modal_full.)")
|
|
1018
|
+
|
|
1019
|
+
|
|
1020
|
+
def qml_axioms(mode: str = "constant", frame: str = "K", systems=None,
|
|
1021
|
+
formula: Optional[Node] = None, bridges=None,
|
|
1022
|
+
temporal_closure: bool = True) -> List[Node]:
|
|
1023
|
+
"""Return the background axioms (sort typing, frame conditions, domain regime).
|
|
1024
|
+
|
|
1025
|
+
The conjunction of these is the hypothesis under which a translated formula's
|
|
1026
|
+
validity is checked. ``frame`` ∈ {K, T, S4, S5, KD, KD45} sets the ALETHIC system;
|
|
1027
|
+
``mode`` selects the domain regime (see the module docstring for the existence-axiom
|
|
1028
|
+
correspondence). ``systems`` optionally sets the frame system for the AGENT-INDEXED
|
|
1029
|
+
epistemic / doxastic relations, e.g. ``systems={"epistemic": "S5", "doxastic": "KD45"}``
|
|
1030
|
+
— so knowledge can be made factive (T/S4/S5) and belief consistent (KD45), symmetric
|
|
1031
|
+
to the THF exporter.
|
|
1032
|
+
|
|
1033
|
+
The temporal (``T`` reflexive + transitive, ``N ⊆ T``) and deontic (``D`` serial)
|
|
1034
|
+
conditions are **on by default**, which is what brings this route into agreement with
|
|
1035
|
+
``satisfies_modal`` and with the Isabelle / THF exporters; see the module docstring
|
|
1036
|
+
for the resulting validities and for the transitive-closure limit that keeps temporal
|
|
1037
|
+
induction out of reach.
|
|
1038
|
+
|
|
1039
|
+
``formula`` gates the output: only the relations occurring in it are typed and
|
|
1040
|
+
constrained (see :func:`_relations_used` for why that matters — it is a real
|
|
1041
|
+
performance and *verdict* concern, not tidiness). Called **without** a formula this
|
|
1042
|
+
stays the "give me the whole background theory" call and emits axioms for every
|
|
1043
|
+
relation in :data:`QML_RELATIONS`. The same ``formula`` gates the many-sorted
|
|
1044
|
+
facts: per sort it uses, that the sort is non-empty at every world, and per sorted
|
|
1045
|
+
constant ``c:S``, that ``c`` is in ``S`` at every world (give the formula AS WRITTEN,
|
|
1046
|
+
not :func:`qml_translate`'s relativised form, which has forgotten the annotations).
|
|
1047
|
+
|
|
1048
|
+
``bridges`` is an opt-in list of cross-family frame conditions from
|
|
1049
|
+
:data:`QML_BRIDGES`; an unknown name raises ``ValueError`` listing the known ones, and
|
|
1050
|
+
so does a bridge whose partner family the ``formula`` never mentions (see
|
|
1051
|
+
:func:`_check_bridge_families` — the same rule the HOL routes apply, so one option
|
|
1052
|
+
name means one thing on every route). A requested bridge is never gated away by
|
|
1053
|
+
:func:`_relations_used`, but it also does not drag its relations' default frame blocks
|
|
1054
|
+
into scope.
|
|
1055
|
+
|
|
1056
|
+
``temporal_closure=False`` drops ``T``'s reflexivity and transitivity and the
|
|
1057
|
+
``first_step`` axiom, keeping only ``N ⊆ T``, for parity with
|
|
1058
|
+
``isabelle_modal_theory`` / ``to_thf_modal_full``. Be aware
|
|
1059
|
+
of what that means on a *decision* route: the answers are then those of a strictly
|
|
1060
|
+
weaker temporal logic than the one ``satisfies_modal`` implements (``Gφ → φ`` and
|
|
1061
|
+
``Gφ → Fφ`` become underivable), so a ``False`` under it is not evidence about the
|
|
1062
|
+
oracle's temporal logic.
|
|
1063
|
+
"""
|
|
1064
|
+
conds = _resolve_frame(frame)
|
|
1065
|
+
if mode not in _ACTUALIST_MODES and mode not in _CONSTANT_MODES:
|
|
1066
|
+
raise ValueError(
|
|
1067
|
+
f"qml: unknown mode {mode!r} (use one of "
|
|
1068
|
+
f"{sorted(_ACTUALIST_MODES | _CONSTANT_MODES)}).")
|
|
1069
|
+
bridge_names = _validate_bridges(bridges)
|
|
1070
|
+
used = set(QML_RELATIONS) if formula is None else _relations_used(formula)
|
|
1071
|
+
_check_bridge_families(bridge_names, used)
|
|
1072
|
+
x, w, v, u = _v("x", "w", "v", "u")
|
|
1073
|
+
t = Variable("t")
|
|
1074
|
+
W = lambda a: Atom(_WORLD, (a,))
|
|
1075
|
+
O = lambda a: Atom(_OBJECT, (a,))
|
|
1076
|
+
R = lambda a, b: Atom(_R_ALETHIC, (a, b))
|
|
1077
|
+
E = lambda a, b: Atom(_E, (a, b))
|
|
1078
|
+
fa = lambda var, body: Quantifier(_FORALL, var, body)
|
|
1079
|
+
|
|
1080
|
+
axioms: List[Node] = [
|
|
1081
|
+
# sort discipline: worlds and objects are disjoint; both kinds are non-empty.
|
|
1082
|
+
fa(t, Not(And(W(t), O(t)))),
|
|
1083
|
+
Quantifier(_EXISTS, w, W(w)),
|
|
1084
|
+
Quantifier(_EXISTS, x, O(x)),
|
|
1085
|
+
# typing of the relations.
|
|
1086
|
+
fa(w, fa(v, Implies(R(w, v), And(W(w), W(v))))),
|
|
1087
|
+
fa(x, fa(w, Implies(E(x, w), And(O(x), W(w))))),
|
|
1088
|
+
]
|
|
1089
|
+
# typing for the other world×world relations, only where they occur (see
|
|
1090
|
+
# _relations_used). The agent-indexed ones are typed by _agent_frame_axioms.
|
|
1091
|
+
axioms += [_relation_typing(rel)
|
|
1092
|
+
for rel in (_R_TEMPORAL, _R_NEXT, _R_DEONTIC) if rel in used]
|
|
1093
|
+
|
|
1094
|
+
if "refl" in conds:
|
|
1095
|
+
axioms.append(fa(w, Implies(W(w), R(w, w))))
|
|
1096
|
+
if "trans" in conds:
|
|
1097
|
+
axioms.append(fa(w, fa(v, fa(u, Implies(
|
|
1098
|
+
And(And(W(w), W(v)), And(W(u), And(R(w, v), R(v, u)))), R(w, u))))))
|
|
1099
|
+
if "sym" in conds:
|
|
1100
|
+
axioms.append(fa(w, fa(v, Implies(And(And(W(w), W(v)), R(w, v)), R(v, w)))))
|
|
1101
|
+
if "eucl" in conds:
|
|
1102
|
+
axioms.append(fa(w, fa(v, fa(u, Implies(
|
|
1103
|
+
And(And(W(w), W(v)), And(W(u), And(R(w, v), R(w, u)))), R(v, u))))))
|
|
1104
|
+
if "serial" in conds:
|
|
1105
|
+
axioms.append(fa(w, Implies(W(w), Quantifier(_EXISTS, v, And(W(v), R(w, v))))))
|
|
1106
|
+
if "directed" in conds:
|
|
1107
|
+
# .2 convergence: ∀w,v,u (Rwv ∧ Rwu → ∃z (Rvz ∧ Ruz)).
|
|
1108
|
+
axioms.append(fa(w, fa(v, fa(u, Implies(
|
|
1109
|
+
And(And(W(w), W(v)), And(W(u), And(R(w, v), R(w, u)))),
|
|
1110
|
+
Quantifier(_EXISTS, t, And(W(t), And(R(v, t), R(u, t)))))))))
|
|
1111
|
+
if "connected" in conds:
|
|
1112
|
+
# .3 no-branching: ∀w,v,u (Rwv ∧ Rwu → Rvu ∨ Ruv). With reflexivity the
|
|
1113
|
+
# v=u case is covered (Rvv), so no world-equality is needed.
|
|
1114
|
+
axioms.append(fa(w, fa(v, fa(u, Implies(
|
|
1115
|
+
And(And(W(w), W(v)), And(W(u), And(R(w, v), R(w, u)))),
|
|
1116
|
+
Or(R(v, u), R(u, v)))))))
|
|
1117
|
+
if "functional" in conds:
|
|
1118
|
+
# CD: at most one successor. ∀w,v,u (Rwv ∧ Rwu → v = u).
|
|
1119
|
+
axioms.append(fa(w, fa(v, fa(u, Implies(
|
|
1120
|
+
And(And(W(w), W(v)), And(W(u), And(R(w, v), R(w, u)))),
|
|
1121
|
+
Atom("=", (v, u)))))))
|
|
1122
|
+
if "dense" in conds:
|
|
1123
|
+
# C4: ∀w,v (Rwv → ∃u (Rwu ∧ Ruv)).
|
|
1124
|
+
axioms.append(fa(w, fa(v, Implies(
|
|
1125
|
+
And(And(W(w), W(v)), R(w, v)),
|
|
1126
|
+
Quantifier(_EXISTS, u, And(W(u), And(R(w, u), R(u, v))))))))
|
|
1127
|
+
if "shift_refl" in conds:
|
|
1128
|
+
# Ṁ: ∀w,v (Rwv → Rvv) — every accessible world is reflexive.
|
|
1129
|
+
axioms.append(fa(w, fa(v, Implies(
|
|
1130
|
+
And(And(W(w), W(v)), R(w, v)), R(v, v)))))
|
|
1131
|
+
if "empty" in conds:
|
|
1132
|
+
# Ver: no accessible worlds at all, so □φ holds vacuously everywhere.
|
|
1133
|
+
axioms.append(fa(w, fa(v, Not(R(w, v)))))
|
|
1134
|
+
for cond in conds:
|
|
1135
|
+
spec = parse_geach(cond)
|
|
1136
|
+
if spec is not None:
|
|
1137
|
+
axioms.append(_geach_axiom(spec, W, R))
|
|
1138
|
+
|
|
1139
|
+
# inter-modality frame conditions (default-on; see the module docstring). Each block
|
|
1140
|
+
# fires only when its relation occurs, so a formula in the alethic fragment gets the
|
|
1141
|
+
# exact axiom list it got before these existed.
|
|
1142
|
+
if _R_TEMPORAL in used and temporal_closure:
|
|
1143
|
+
axioms += _temporal_frame_axioms()
|
|
1144
|
+
if _R_TEMPORAL in used and _R_NEXT in used:
|
|
1145
|
+
# the link is vacuous — and misleading — unless BOTH relations occur.
|
|
1146
|
+
axioms.append(_next_in_temporal_axiom())
|
|
1147
|
+
if temporal_closure:
|
|
1148
|
+
# ... and, when T is meant to be the closure, say that each T-step starts
|
|
1149
|
+
# with an N-step. Mirrors isabelle_modal's t_in_nstar, which pins t = n**.
|
|
1150
|
+
axioms.append(_temporal_first_step_axiom())
|
|
1151
|
+
if _R_DEONTIC in used:
|
|
1152
|
+
axioms += _deontic_frame_axioms()
|
|
1153
|
+
|
|
1154
|
+
# non-empty local domains (standard classical QML: every world has an existing
|
|
1155
|
+
# individual). The Barcan counter-models all use non-empty domains, so this does
|
|
1156
|
+
# not affect BF/CBF; it makes ∀x φ → ∃x φ valid, and keeps (A) and the THF export
|
|
1157
|
+
# (B), which carries the same nonempty_dom axiom, in agreement.
|
|
1158
|
+
if mode in _ACTUALIST_MODES:
|
|
1159
|
+
axioms.append(fa(w, Implies(W(w), Quantifier(_EXISTS, x, And(O(x), E(x, w))))))
|
|
1160
|
+
|
|
1161
|
+
# non-empty SORTS (many-sorted formulas only): the same MSFOL convention
|
|
1162
|
+
# fol._msfl_nodes.nonempty_sort_axioms enforces for the classical routes
|
|
1163
|
+
# (every sort is non-empty), extended PER WORLD here because a sort guard
|
|
1164
|
+
# is an ordinary, WORLD-RELATIVE atom once qml_translate's up-front
|
|
1165
|
+
# relativisation feeds it through ST (see the module docstring's
|
|
1166
|
+
# "Many-sorted formulas" section) — so ``∀x:S P(x) → ∃x:S P(x)`` agrees
|
|
1167
|
+
# with the classical Z3-via-api.prove verdict at every world, not just
|
|
1168
|
+
# incidentally at one. Gated on ``formula`` the same way
|
|
1169
|
+
# _signature_typing_facts is: there is nothing to scan for sorts without one.
|
|
1170
|
+
if formula is not None:
|
|
1171
|
+
for name in _sort_names_used(formula):
|
|
1172
|
+
s_guard = Atom(_user_predicate(name), (x, w))
|
|
1173
|
+
witness = And(O(x), s_guard)
|
|
1174
|
+
if mode in _ACTUALIST_MODES:
|
|
1175
|
+
witness = And(And(O(x), E(x, w)), s_guard)
|
|
1176
|
+
axioms.append(fa(w, Implies(W(w), Quantifier(_EXISTS, x, witness))))
|
|
1177
|
+
# membership of a SORTED CONSTANT: ``c:S`` denotes an element of ``S``. A
|
|
1178
|
+
# constant is a rigid designator, so the fact is rigid too -- ``S(c, w)`` at
|
|
1179
|
+
# EVERY world -- and it is NOT guarded by ``E(c, w)``: the guarded form makes
|
|
1180
|
+
# no query's verdict differ, because in the constant-domain modes ``E`` is
|
|
1181
|
+
# not forced and in the actualist modes a constant may lie outside ``D_w``
|
|
1182
|
+
# (see the module docstring), so the guard would never fire. Read from the
|
|
1183
|
+
# ORIGINAL formula: the relativised one has already forgotten which
|
|
1184
|
+
# constants were annotated. The atoms ``sort_membership_axioms`` returns
|
|
1185
|
+
# carry no world, so each is lifted here, through the same predicate naming
|
|
1186
|
+
# the guard of a sorted quantifier gets.
|
|
1187
|
+
for member in sort_membership_axioms(formula):
|
|
1188
|
+
assert isinstance(member, Atom) # sort_membership_axioms yields atoms ``S(c)`` only
|
|
1189
|
+
axioms.append(fa(w, Implies(
|
|
1190
|
+
W(w), Atom(_user_predicate(member.predicate),
|
|
1191
|
+
(member.args[0], w)))))
|
|
1192
|
+
|
|
1193
|
+
# domain-regime existence axioms.
|
|
1194
|
+
typed = lambda body: And(And(O(x), W(w)), And(W(v), body))
|
|
1195
|
+
if mode in ("increasing", "cumulative", "constant"):
|
|
1196
|
+
axioms.append(fa(x, fa(w, fa(v, Implies(typed(And(E(x, w), R(w, v))), E(x, v))))))
|
|
1197
|
+
if mode in ("decreasing", "constant"):
|
|
1198
|
+
axioms.append(fa(x, fa(w, fa(v, Implies(typed(And(E(x, v), R(w, v))), E(x, w))))))
|
|
1199
|
+
|
|
1200
|
+
# agent-indexed epistemic / doxastic frame systems (optional).
|
|
1201
|
+
if systems:
|
|
1202
|
+
for fam, sys in systems.items():
|
|
1203
|
+
if fam not in _AGENT_FAMILIES:
|
|
1204
|
+
raise ValueError(
|
|
1205
|
+
f"qml: unknown modal family {fam!r} for systems= "
|
|
1206
|
+
f"(use one of {sorted(_AGENT_FAMILIES)}).")
|
|
1207
|
+
if sys not in _FRAMES:
|
|
1208
|
+
raise ValueError(
|
|
1209
|
+
f"qml: unknown system {sys!r} for {fam} (use one of {sorted(_FRAMES)}).")
|
|
1210
|
+
# both validators stay UNCONDITIONAL — a typo in systems= must be reported
|
|
1211
|
+
# even when the configured family does not occur in the formula.
|
|
1212
|
+
if _AGENT_FAMILIES[fam] in used:
|
|
1213
|
+
axioms += _agent_frame_axioms(_AGENT_FAMILIES[fam], _FRAMES[sys])
|
|
1214
|
+
|
|
1215
|
+
# explicitly requested bridges are never gated away, but they also do not pull their
|
|
1216
|
+
# relations' default frame blocks into scope (see the qml_axioms docstring).
|
|
1217
|
+
axioms += [_bridge_axiom(name) for name in bridge_names]
|
|
1218
|
+
return axioms
|
|
1219
|
+
|
|
1220
|
+
|
|
1221
|
+
def _sort_names_used(formula: Node) -> List[str]:
|
|
1222
|
+
"""Distinct sort names ``formula`` mentions, in first-occurrence order.
|
|
1223
|
+
|
|
1224
|
+
Reuses :func:`~unicode_logic_kit.fol._msfl_nodes.nonempty_sort_axioms`'s own
|
|
1225
|
+
scan (each of its returned ``∃x (S(x))`` sentences names its sort as the
|
|
1226
|
+
predicate of its body atom) rather than re-implementing "which of the four
|
|
1227
|
+
many-sorted node types occur" here — so this route and the classical one
|
|
1228
|
+
can never disagree about which sorts a formula uses.
|
|
1229
|
+
"""
|
|
1230
|
+
sorts: List[str] = []
|
|
1231
|
+
for axiom in nonempty_sort_axioms(formula):
|
|
1232
|
+
assert isinstance(axiom, Quantifier) and isinstance(axiom.formula, Atom) # ∃x S(x)
|
|
1233
|
+
sorts.append(axiom.formula.predicate)
|
|
1234
|
+
return sorts
|
|
1235
|
+
|
|
1236
|
+
|
|
1237
|
+
def _signature_typing_facts(formula: Node) -> List[Node]:
|
|
1238
|
+
"""Object-typing facts for the constants and functions of ``formula``.
|
|
1239
|
+
|
|
1240
|
+
The guarded embedding sorts the universe into Worlds and Objects. A bare
|
|
1241
|
+
constant is otherwise UNTYPED: no model is forced to put it in Object, so
|
|
1242
|
+
an Object-guarded quantifier could never instantiate it — ``∀x P(x) →
|
|
1243
|
+
P(c)`` came out spuriously invalid, and an Object-guarded agent frame
|
|
1244
|
+
axiom (``systems=``) never fired for a NAMED agent. Constants (including
|
|
1245
|
+
numbers and agent names) denote objects rigidly, matching
|
|
1246
|
+
``satisfies_modal``; a function maps objects to objects.
|
|
1247
|
+
"""
|
|
1248
|
+
from .nodes import Constant as _C, Number as _N, Function as _F
|
|
1249
|
+
consts, funcs = {}, {}
|
|
1250
|
+
for n in formula.walk():
|
|
1251
|
+
if isinstance(n, (_C, _N)):
|
|
1252
|
+
# keyed (and so ordered) by the bare name, as ever: the key is not shown anywhere
|
|
1253
|
+
consts[key_text(n)] = n
|
|
1254
|
+
elif isinstance(n, _F):
|
|
1255
|
+
funcs[(n.name, len(n.args))] = n
|
|
1256
|
+
facts: List[Node] = [Atom(_OBJECT, (t,))
|
|
1257
|
+
for _, t in sorted(consts.items())]
|
|
1258
|
+
for (name, arity), fn in sorted(funcs.items()):
|
|
1259
|
+
xs = [Variable(f"_a{i}") for i in range(arity)]
|
|
1260
|
+
guards: List[Node] = [Atom(_OBJECT, (x,)) for x in xs]
|
|
1261
|
+
guard = reduce(And, guards)
|
|
1262
|
+
body: Node = Implies(guard, Atom(_OBJECT, (type(fn)(name, tuple(xs)),)))
|
|
1263
|
+
for x in reversed(xs):
|
|
1264
|
+
body = Quantifier(_FORALL, x, body)
|
|
1265
|
+
facts.append(body)
|
|
1266
|
+
return facts
|
|
1267
|
+
|
|
1268
|
+
|
|
1269
|
+
def _validity_formula(formula: Node, mode: str, frame: str, systems=None,
|
|
1270
|
+
bridges=None, temporal_closure: bool = True) -> Node:
|
|
1271
|
+
"""Build ``⋀axioms ∧ ⋀typing-facts → ∀w (World(w) → ST(formula, w))``.
|
|
1272
|
+
|
|
1273
|
+
The axioms are gated on ``formula``'s signature, so a query only carries the frame
|
|
1274
|
+
conditions of the relations it actually mentions. New parameters are APPENDED, so the
|
|
1275
|
+
positional call in :mod:`unicode_logic_kit.atp.resolution` keeps working unchanged and
|
|
1276
|
+
inherits the gating.
|
|
1277
|
+
|
|
1278
|
+
A free variable of ``formula`` is a parameter (the module docstring's "A free variable
|
|
1279
|
+
is a parameter"), and it stays free in the query: it is one unknown individual, so a
|
|
1280
|
+
solver that reads it as an uninterpreted constant and one that closes the whole
|
|
1281
|
+
query universally decide the same validity. Under ``constant`` / ``possibilist`` the
|
|
1282
|
+
individual is typed ``Object(y)``, among the typing facts, as every constant is. Under
|
|
1283
|
+
an actualist regime it is guarded where the formula is evaluated: the consequent is
|
|
1284
|
+
``∀w (World(w) ∧ E(y, w) → ST(formula, w))``.
|
|
1285
|
+
"""
|
|
1286
|
+
# The sort-name / relation-usage scan in qml_axioms needs the ORIGINAL,
|
|
1287
|
+
# un-relativized ``formula`` (a SortedQuantifier/SortedConstant is what it
|
|
1288
|
+
# looks for) — so that scan runs on ``formula`` itself, while everything
|
|
1289
|
+
# downstream of it (typing facts, translation) runs on ONE relativized
|
|
1290
|
+
# copy, so a SortedConstant gets typed as an Object exactly like a plain
|
|
1291
|
+
# Constant would (see qml_translate's own up-front relativisation, which
|
|
1292
|
+
# this pre-empts so _signature_typing_facts sees the same plain
|
|
1293
|
+
# Constant qml_translate's translation will).
|
|
1294
|
+
axioms = qml_axioms(mode, frame, systems, formula=formula, bridges=bridges,
|
|
1295
|
+
temporal_closure=temporal_closure)
|
|
1296
|
+
relativized = formula._relativize([])
|
|
1297
|
+
axioms += _signature_typing_facts(relativized)
|
|
1298
|
+
parameters = [Variable(name) for name in free_parameter_names([relativized])]
|
|
1299
|
+
if mode in _CONSTANT_MODES:
|
|
1300
|
+
axioms += [Atom(_OBJECT, (parameter,)) for parameter in parameters]
|
|
1301
|
+
w = _pick_world_name(relativized, "w")
|
|
1302
|
+
where: Node = Atom(_WORLD, (Variable(w),))
|
|
1303
|
+
if mode in _ACTUALIST_MODES:
|
|
1304
|
+
for parameter in parameters:
|
|
1305
|
+
where = And(where, Atom(_E, (parameter, Variable(w))))
|
|
1306
|
+
body = Implies(where, qml_translate(relativized, mode, world=w))
|
|
1307
|
+
closed = Quantifier(_FORALL, Variable(w), body)
|
|
1308
|
+
hyp = reduce(And, axioms)
|
|
1309
|
+
return Implies(hyp, closed)
|
|
1310
|
+
|
|
1311
|
+
|
|
1312
|
+
def qml_is_valid(formula: Node, mode: str = "constant", frame: str = "K",
|
|
1313
|
+
systems=None, timeout: int = 10000, bridges=None,
|
|
1314
|
+
temporal_closure: bool = True) -> bool:
|
|
1315
|
+
"""Return True iff ``formula`` is QML-valid under ``mode`` / ``frame`` (via Z3).
|
|
1316
|
+
|
|
1317
|
+
``systems`` optionally sets the agent-indexed epistemic / doxastic frame systems,
|
|
1318
|
+
e.g. ``systems={"epistemic": "S5"}`` makes knowledge factive so ``∀x (K_x φ → φ)``
|
|
1319
|
+
comes out valid. ``bridges`` opts into the cross-family relation inclusions of
|
|
1320
|
+
:data:`QML_BRIDGES` (none by default).
|
|
1321
|
+
|
|
1322
|
+
Sound but bounded-incomplete: ``True`` means Z3 proved validity; ``False`` means it
|
|
1323
|
+
did not (a genuine countermodel, or — since first-order modal logic is undecidable
|
|
1324
|
+
— an instance Z3 could not close). For a definite countermodel use
|
|
1325
|
+
:func:`unicode_logic_kit.semantics.kripke.satisfies_modal` over an explicit model.
|
|
1326
|
+
|
|
1327
|
+
Two contract details worth knowing before reading a verdict:
|
|
1328
|
+
|
|
1329
|
+
- a temporal or deontic formula is judged under the default-on frame conditions, so
|
|
1330
|
+
``True`` for a deontic formula means "valid over every SERIAL-deontic model", and
|
|
1331
|
+
temporal induction ``(φ ∧ G(φ → Xφ)) → Gφ`` comes back ``False`` here despite
|
|
1332
|
+
holding in every intended model — decide that one with
|
|
1333
|
+
:func:`unicode_logic_kit.hol.isabelle_runner.isabelle_decide_modal`;
|
|
1334
|
+
- an identity atom ``=`` / ``≠`` is rigid (no world argument) and existence-
|
|
1335
|
+
independent under every ``mode``; see the module docstring's "Equality is rigid"
|
|
1336
|
+
for the varying-domain choice and for why ``□(a = b) → a = b`` needs a frame
|
|
1337
|
+
with a successor-or-self (T, S4, S5, KD …) while ``a = b → □(a = b)`` holds in K;
|
|
1338
|
+
- a variable that is free in ``formula`` is a PARAMETER: one unknown individual, the
|
|
1339
|
+
same everywhere in the formula. Under ``mode='constant'`` / ``'possibilist'`` it is an
|
|
1340
|
+
element of the object domain, so ``∀x P(x) → P(y)`` and ``P(y) → ∃x P(x)`` are valid;
|
|
1341
|
+
under a varying regime it exists at the world of evaluation (and need not exist at any
|
|
1342
|
+
other), so those two are valid there too while ``□∀x P(x) → □P(y)`` is valid under
|
|
1343
|
+
``'constant'`` / ``'possibilist'`` / ``'increasing'`` only. A constant is not read
|
|
1344
|
+
that way (it may lie outside the world's domain); see the module docstring's "A free
|
|
1345
|
+
variable is a parameter";
|
|
1346
|
+
- a deontic ``False`` is typically Z3 returning *unknown* rather than a countermodel:
|
|
1347
|
+
the ∃-quantified seriality axiom defeats its model finder, measured identical at
|
|
1348
|
+
1 s / 2 s / 10 s budgets (the same behaviour ``frame="KD"`` has always had). A short
|
|
1349
|
+
``timeout`` therefore costs no reliability on deontic non-theorems.
|
|
1350
|
+
"""
|
|
1351
|
+
from ..atp.z3_models import is_valid
|
|
1352
|
+
return is_valid(_validity_formula(formula, mode, frame, systems, bridges=bridges,
|
|
1353
|
+
temporal_closure=temporal_closure), timeout=timeout)
|
|
1354
|
+
|
|
1355
|
+
|
|
1356
|
+
def qml_validity_formula(formula: Node, mode: str = "constant", frame: str = "K",
|
|
1357
|
+
systems=None, bridges=None,
|
|
1358
|
+
temporal_closure: bool = True) -> Node:
|
|
1359
|
+
"""Return the classical-FOL validity query :func:`qml_is_valid` decides,
|
|
1360
|
+
as a :class:`Node` — closed unless ``formula`` has a free variable, which stays
|
|
1361
|
+
free in it as the parameter it is (closing the query universally gives the same
|
|
1362
|
+
validity) — the documented, public counterpart of the private
|
|
1363
|
+
:func:`_validity_formula` this function simply delegates to (kept private and
|
|
1364
|
+
unchanged, including its positional-argument shape, since
|
|
1365
|
+
:mod:`unicode_logic_kit.atp.resolution` already calls it positionally).
|
|
1366
|
+
|
|
1367
|
+
``⋀axioms ∧ ⋀typing-facts → ∀w (World(w) → ST(formula, w))`` — see
|
|
1368
|
+
:func:`_validity_formula`'s own docstring for exactly what ``axioms`` and
|
|
1369
|
+
``typing-facts`` contain. The Node this returns is built ONLY from
|
|
1370
|
+
:class:`Atom`/:class:`Not`/:class:`And`/:class:`Or`/:class:`Xor`/
|
|
1371
|
+
:class:`Implies`/:class:`Iff`/:class:`Quantifier` and the term classes
|
|
1372
|
+
:class:`Variable`/:class:`Constant`/:class:`Function` — i.e. exactly the
|
|
1373
|
+
classical FOL fragment :mod:`unicode_logic_kit.fol.casl_export` accepts — so it
|
|
1374
|
+
is ready to hand to :func:`~unicode_logic_kit.fol.casl_export.to_casl_spec`
|
|
1375
|
+
directly. The fresh world / Geach variables this route mints are ``w0`` / ``v0``
|
|
1376
|
+
style names (:func:`unicode_logic_kit.fol._identifiers.fresh_variables`), which are
|
|
1377
|
+
legal CASL identifiers as well as legal names for this kit's own parser — before
|
|
1378
|
+
0.30.0 they were ``_w0`` / ``_gz0``, and :mod:`unicode_logic_kit.hets.dol` had to
|
|
1379
|
+
rename every one. An object-language ``=`` needs no handling either: it is
|
|
1380
|
+
translated to the SAME binary, rigid identity (no world argument — see the module
|
|
1381
|
+
docstring's "Equality is rigid"), which is exactly CASL's own fixed built-in
|
|
1382
|
+
``=``, and ``≠`` to ``¬(=)``, which ``casl_export`` renders as it renders any
|
|
1383
|
+
negation. What ``hets.dol`` still has to rename is the ``·`` mark this module
|
|
1384
|
+
appends to a user predicate named like one of its own relations (``R`` → ``R·``):
|
|
1385
|
+
U+00B7 is punctuation no CASL identifier may contain. Use
|
|
1386
|
+
:func:`unicode_logic_kit.hets.dol.to_dol_library_from_modal` — it calls this
|
|
1387
|
+
function, sanitises the names injectively, and renders the result as a complete
|
|
1388
|
+
DOL library over :func:`~unicode_logic_kit.fol.casl_export.to_casl_spec` (see that
|
|
1389
|
+
module's own docstring for the sanitisation contract) — rather than feeding this
|
|
1390
|
+
function's raw output to ``to_casl_spec`` yourself.
|
|
1391
|
+
"""
|
|
1392
|
+
return _validity_formula(formula, mode, frame, systems, bridges=bridges,
|
|
1393
|
+
temporal_closure=temporal_closure)
|
|
1394
|
+
|
|
1395
|
+
|
|
1396
|
+
def qml_equivalent(left: Node, right: Node, mode: str = "constant", frame: str = "K",
|
|
1397
|
+
systems=None, timeout: int = 10000, bridges=None,
|
|
1398
|
+
temporal_closure: bool = True) -> bool:
|
|
1399
|
+
"""Return True iff two modal formulas are QML-equivalent under ``mode`` / ``frame``.
|
|
1400
|
+
|
|
1401
|
+
A variable free in either formula is one parameter shared by both (see
|
|
1402
|
+
:func:`qml_is_valid`): the formulas are equivalent when ``left ↔ right`` is valid for
|
|
1403
|
+
every individual it may denote.
|
|
1404
|
+
"""
|
|
1405
|
+
return qml_is_valid(Iff(left, right), mode=mode, frame=frame, systems=systems,
|
|
1406
|
+
timeout=timeout, bridges=bridges,
|
|
1407
|
+
temporal_closure=temporal_closure)
|
|
1408
|
+
|
|
1409
|
+
|
|
1410
|
+
# The Barcan formula and its converse, over a unary predicate A — the standard
|
|
1411
|
+
# litmus tests for the domain regime.
|
|
1412
|
+
def _barcan_pair():
|
|
1413
|
+
x = Variable("x")
|
|
1414
|
+
A = lambda t: Atom("A", [t])
|
|
1415
|
+
bf = Implies(Diamond(Quantifier(_EXISTS, x, A(x))),
|
|
1416
|
+
Quantifier(_EXISTS, x, Diamond(A(x))))
|
|
1417
|
+
cbf = Implies(Quantifier(_EXISTS, x, Diamond(A(x))),
|
|
1418
|
+
Diamond(Quantifier(_EXISTS, x, A(x))))
|
|
1419
|
+
return bf, cbf
|
|
1420
|
+
|
|
1421
|
+
|
|
1422
|
+
BARCAN, CONVERSE_BARCAN = _barcan_pair()
|
|
1423
|
+
|
|
1424
|
+
|
|
1425
|
+
# ===========================================================================
|
|
1426
|
+
# (B) Higher-order shallow embedding — TPTP THF export (Benzmüller-style)
|
|
1427
|
+
# ===========================================================================
|
|
1428
|
+
#
|
|
1429
|
+
# A genuine higher-order shallow embedding: modal propositions are functions
|
|
1430
|
+
# ``mu > $o`` (world → bool), the modalities are λ-lifted quantifiers over the
|
|
1431
|
+
# accessibility relation ``r``, and object quantifiers are ``existsAt``-guarded
|
|
1432
|
+
# (actualist). The emitted THF problem is decidable by a higher-order ATP
|
|
1433
|
+
# (Leo-III, Satallax) — the toolkit emits it the way it emits TPTP/Prover9, it
|
|
1434
|
+
# does not run it. Covers the alethic □/◇ fragment.
|
|
1435
|
+
|
|
1436
|
+
_THF_DEFS = """\
|
|
1437
|
+
thf(mnot, definition, ( mnot = ( ^ [Phi: mu>$o, W: mu] : ~ ( Phi @ W ) ) )).
|
|
1438
|
+
thf(mand, definition, ( mand = ( ^ [Phi: mu>$o, Psi: mu>$o, W: mu] : ( ( Phi @ W ) & ( Psi @ W ) ) ) )).
|
|
1439
|
+
thf(mor, definition, ( mor = ( ^ [Phi: mu>$o, Psi: mu>$o, W: mu] : ( ( Phi @ W ) | ( Psi @ W ) ) ) )).
|
|
1440
|
+
thf(mimplies, definition, ( mimplies = ( ^ [Phi: mu>$o, Psi: mu>$o, W: mu] : ( ( Phi @ W ) => ( Psi @ W ) ) ) )).
|
|
1441
|
+
thf(mequiv, definition, ( mequiv = ( ^ [Phi: mu>$o, Psi: mu>$o, W: mu] : ( ( Phi @ W ) <=> ( Psi @ W ) ) ) )).
|
|
1442
|
+
thf(mbox, definition, ( mbox = ( ^ [Phi: mu>$o, W: mu] : ! [V: mu] : ( ( r @ W @ V ) => ( Phi @ V ) ) ) )).
|
|
1443
|
+
thf(mdia, definition, ( mdia = ( ^ [Phi: mu>$o, W: mu] : ? [V: mu] : ( ( r @ W @ V ) & ( Phi @ V ) ) ) )).
|
|
1444
|
+
thf(mforall, definition, ( mforall = ( ^ [Phi: $i>(mu>$o), W: mu] : ! [X: $i] : ( ( existsAt @ X @ W ) => ( Phi @ X @ W ) ) ) )).
|
|
1445
|
+
thf(mexists, definition, ( mexists = ( ^ [Phi: $i>(mu>$o), W: mu] : ? [X: $i] : ( ( existsAt @ X @ W ) & ( Phi @ X @ W ) ) ) )).
|
|
1446
|
+
thf(mvalid, definition, ( mvalid = ( ^ [Phi: mu>$o] : ! [W: mu] : ( Phi @ W ) ) )).\
|
|
1447
|
+
"""
|
|
1448
|
+
|
|
1449
|
+
_THF_FRAME = {
|
|
1450
|
+
"refl": "thf(refl, axiom, ( ! [W: mu] : ( r @ W @ W ) )).",
|
|
1451
|
+
"trans": "thf(trans, axiom, ( ! [W: mu, V: mu, U: mu] : ( ( ( r @ W @ V ) & ( r @ V @ U ) ) => ( r @ W @ U ) ) )).",
|
|
1452
|
+
"sym": "thf(symm, axiom, ( ! [W: mu, V: mu] : ( ( r @ W @ V ) => ( r @ V @ W ) ) )).",
|
|
1453
|
+
"serial": "thf(serial, axiom, ( ! [W: mu] : ? [V: mu] : ( r @ W @ V ) )).",
|
|
1454
|
+
"eucl": "thf(euclid, axiom, ( ! [W: mu, V: mu, U: mu] : ( ( ( r @ W @ V ) & ( r @ W @ U ) ) => ( r @ V @ U ) ) )).",
|
|
1455
|
+
# .2 convergence: any two successors of a world have a common successor.
|
|
1456
|
+
"directed": "thf(directed, axiom, ( ! [W: mu, V: mu, U: mu] : ( ( ( r @ W @ V ) & ( r @ W @ U ) ) => ? [Z: mu] : ( ( r @ V @ Z ) & ( r @ U @ Z ) ) ) )).",
|
|
1457
|
+
# .3 no-branching: the successors of a world are linearly r-ordered.
|
|
1458
|
+
"connected": "thf(connected, axiom, ( ! [W: mu, V: mu, U: mu] : ( ( ( r @ W @ V ) & ( r @ W @ U ) ) => ( ( r @ V @ U ) | ( r @ U @ V ) ) ) )).",
|
|
1459
|
+
# CD: at most one successor.
|
|
1460
|
+
"functional": "thf(functional, axiom, ( ! [W: mu, V: mu, U: mu] : ( ( ( r @ W @ V ) & ( r @ W @ U ) ) => ( V = U ) ) )).",
|
|
1461
|
+
# C4: dense.
|
|
1462
|
+
"dense": "thf(dense, axiom, ( ! [W: mu, V: mu] : ( ( r @ W @ V ) => ? [U: mu] : ( ( r @ W @ U ) & ( r @ U @ V ) ) ) )).",
|
|
1463
|
+
# Ṁ: every accessible world is reflexive.
|
|
1464
|
+
"shift_refl": "thf(shift_refl, axiom, ( ! [W: mu, V: mu] : ( ( r @ W @ V ) => ( r @ V @ V ) ) )).",
|
|
1465
|
+
# Ver: no accessible worlds at all.
|
|
1466
|
+
"empty": "thf(empty_r, axiom, ( ! [W: mu, V: mu] : ~ ( r @ W @ V ) )).",
|
|
1467
|
+
# The three schemas with NO first-order frame condition are asserted as
|
|
1468
|
+
# schemas, quantified over propositions — which is exactly what the
|
|
1469
|
+
# higher-order route can do and every first-order route cannot.
|
|
1470
|
+
"loeb": "thf(loeb, axiom, ( ! [Phi: mu > $o, W: mu] : ( ( mbox @ ( mimplies @ ( mbox @ Phi ) @ Phi ) @ W ) => ( mbox @ Phi @ W ) ) )).",
|
|
1471
|
+
"mckinsey": "thf(mckinsey, axiom, ( ! [Phi: mu > $o, W: mu] : ( ( mbox @ ( mdia @ Phi ) @ W ) => ( mdia @ ( mbox @ Phi ) @ W ) ) )).",
|
|
1472
|
+
"grz": "thf(grz, axiom, ( ! [Phi: mu > $o, W: mu] : ( ( mbox @ ( mimplies @ ( mbox @ ( mimplies @ Phi @ ( mbox @ Phi ) ) ) @ Phi ) @ W ) => ( Phi @ W ) ) )).",
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
_THF_DOMAIN = {
|
|
1476
|
+
"constant": "thf(const_dom, axiom, ( ! [X: $i, W: mu] : ( existsAt @ X @ W ) )).",
|
|
1477
|
+
"increasing": "thf(cumulative_dom, axiom, ( ! [X: $i, W: mu, V: mu] : ( ( ( existsAt @ X @ W ) & ( r @ W @ V ) ) => ( existsAt @ X @ V ) ) )).",
|
|
1478
|
+
"decreasing": "thf(decreasing_dom, axiom, ( ! [X: $i, W: mu, V: mu] : ( ( ( existsAt @ X @ V ) & ( r @ W @ V ) ) => ( existsAt @ X @ W ) ) )).",
|
|
1479
|
+
}
|
|
1480
|
+
_THF_DOMAIN["cumulative"] = _THF_DOMAIN["increasing"]
|
|
1481
|
+
# possibilist ≡ constant domain (every individual exists at every world): the FO
|
|
1482
|
+
# embedding treats them identically, so the THF export must emit const_dom too —
|
|
1483
|
+
# otherwise its actualist mforall/mexists macros would model a varying domain.
|
|
1484
|
+
_THF_DOMAIN["possibilist"] = _THF_DOMAIN["constant"]
|
|
1485
|
+
|
|
1486
|
+
|
|
1487
|
+
# `feq` / `fneq` are the functors equality USED to get in the THF / Isabelle exports of
|
|
1488
|
+
# the modal layer, where it was an ordinary uninterpreted, world-relativized predicate
|
|
1489
|
+
# and so answered a different question than qml_is_valid. It is now rigid identity on
|
|
1490
|
+
# every one of those routes (`_RigidNames` in hol.thf_modal overrides the two entries,
|
|
1491
|
+
# and `_lower_identity` rewrites `≠` to `¬(=)` before any name is looked up), so the two
|
|
1492
|
+
# equality entries below are unreachable from to_thf_modal / to_isabelle_modal /
|
|
1493
|
+
# hol.thf_modal / hol.isabelle_modal. They are kept because this table also names `⊥`
|
|
1494
|
+
# and `⊤` and is shared with routes that do not lower identity; a route that reads one
|
|
1495
|
+
# of the two gets a valid, distinct functor rather than a crash — never a SILENT
|
|
1496
|
+
# uninterpreted reading, because reaching them at all now takes a caller that opted out
|
|
1497
|
+
# of `_RigidNames`.
|
|
1498
|
+
_THF_PRED_ALIAS = {"=": "feq", "≠": "fneq", "⊥": "bottom", "⊤": "top"}
|
|
1499
|
+
|
|
1500
|
+
# The THF export's own fixed functors. A user symbol that sanitises onto one of
|
|
1501
|
+
# these (a predicate literally named ``r`` or ``mbox``) must NOT claim it — it
|
|
1502
|
+
# would re-declare a built-in at a conflicting type, so the emitted problem would
|
|
1503
|
+
# be rejected by a strict THF parser (or, worse, silently change meaning).
|
|
1504
|
+
# :class:`_ThfNames` pre-claims them, pushing such symbols to ``r_2`` etc.
|
|
1505
|
+
_THF_RESERVED = frozenset({
|
|
1506
|
+
"mu", "r", "existsAt",
|
|
1507
|
+
"mnot", "mand", "mor", "mimplies", "mequiv", "mbox", "mdia",
|
|
1508
|
+
"mforall", "mexists", "mvalid",
|
|
1509
|
+
})
|
|
1510
|
+
|
|
1511
|
+
|
|
1512
|
+
def _thf_name(name: str) -> str:
|
|
1513
|
+
"""Lower-case ASCII predicate/constant/function stem for a THF functor (NOT injective).
|
|
1514
|
+
|
|
1515
|
+
Non-ASCII characters are transliterated FIRST via
|
|
1516
|
+
:func:`~unicode_logic_kit.fol._fol_nodes.constant_name_to_ascii` (ASCII passthrough;
|
|
1517
|
+
Greek -> conventional name; anything else -> a reversible ``uXXXX`` escape) — THF
|
|
1518
|
+
functors are ASCII-only (TPTP ``lower_word``), so a raw Unicode letter reaching this
|
|
1519
|
+
far would be silent corruption, not sanitisation. A leading digit (which can also
|
|
1520
|
+
now arise from a transliterated escape, though those always start with the ASCII
|
|
1521
|
+
letter ``u``) is prefixed with ``p`` so the result is a legal lower identifier — TPTP
|
|
1522
|
+
functors can never start with a digit, and unlike the predicate/constant/function
|
|
1523
|
+
case this class had no digit guard before, since a THF functor name in this exporter
|
|
1524
|
+
is always built from a Constant/Function/Atom name, and only Constant names (via the
|
|
1525
|
+
kit's NAME terminal) can be digit-leading in the first place. A leading underscore
|
|
1526
|
+
gets the same ``p``: the constant ``'_sk0'``, the constant ``'-3'`` and the function
|
|
1527
|
+
``+`` would otherwise be written ``_sk0``, ``_3`` and ``_``, none of which is a lower
|
|
1528
|
+
word (nor, upper-cased for a variable, an upper word).
|
|
1529
|
+
|
|
1530
|
+
Use :class:`_ThfNames` to get a per-formula *unique* functor — ``_thf_name`` alone
|
|
1531
|
+
can map distinct symbols (``Ab`` / ``ab``, or ``theta`` / the Greek ``θ``) to the
|
|
1532
|
+
same functor.
|
|
1533
|
+
"""
|
|
1534
|
+
if name in _THF_PRED_ALIAS:
|
|
1535
|
+
return _THF_PRED_ALIAS[name]
|
|
1536
|
+
safe = "".join(c if (c.isalnum() or c == "_") else "_"
|
|
1537
|
+
for c in constant_name_to_ascii(name))
|
|
1538
|
+
if not safe:
|
|
1539
|
+
return "p"
|
|
1540
|
+
if safe[0].isdigit() or safe[0] == "_":
|
|
1541
|
+
safe = "p" + safe
|
|
1542
|
+
return safe[:1].lower() + safe[1:]
|
|
1543
|
+
|
|
1544
|
+
|
|
1545
|
+
class _ThfNames(SymbolNames):
|
|
1546
|
+
"""Per-formula THF functor resolver (the shared :class:`SymbolNames` over
|
|
1547
|
+
``_thf_name`` + the equality/inequality aliases), so distinct source symbols that
|
|
1548
|
+
sanitise alike — ``Ab`` / ``ab`` — or a predicate used at two arities get DISTINCT
|
|
1549
|
+
functors. Without it ``□Ab → □ab`` could collapse to the tautology ``□ab → □ab``.
|
|
1550
|
+
The export's own built-in functors (``reserved``, default :data:`_THF_RESERVED`)
|
|
1551
|
+
are pre-claimed so no user symbol can shadow them.
|
|
1552
|
+
"""
|
|
1553
|
+
|
|
1554
|
+
def __init__(self, formula: Node, reserved=_THF_RESERVED):
|
|
1555
|
+
super().__init__(formula, _thf_name, _THF_PRED_ALIAS, reserved=reserved)
|
|
1556
|
+
self._var_map: Dict[str, str] = {}
|
|
1557
|
+
self._var_used: set = set()
|
|
1558
|
+
|
|
1559
|
+
def variable(self, name: str) -> str:
|
|
1560
|
+
"""Unique upper-case THF variable token for object-variable ``name``.
|
|
1561
|
+
|
|
1562
|
+
THF variable tokens are the whole (transliterated) name upper-cased — the
|
|
1563
|
+
pre-existing convention this method preserves, including its one known
|
|
1564
|
+
simplification: two source names differing only by case (``x``/``X``) still
|
|
1565
|
+
fold onto the same token, since THF variables have no separate case-preserving
|
|
1566
|
+
slot the way predicate/function/constant identifiers do. What this method adds
|
|
1567
|
+
is (a) ASCII purity — a raw non-ASCII variable name would otherwise reach the
|
|
1568
|
+
THF text verbatim (just upper-cased), which is exactly the same "isalnum() lets
|
|
1569
|
+
Unicode letters through" gap :func:`_thf_name` had — and (b) de-collision via
|
|
1570
|
+
:func:`~unicode_logic_kit.fol._symbol_names.dedupe`, so two DISTINCT source names
|
|
1571
|
+
that only coincide AFTER transliteration (not the pre-existing case-fold above,
|
|
1572
|
+
which is unaffected by this fix) do not collapse into one bound variable.
|
|
1573
|
+
"""
|
|
1574
|
+
if name in self._var_map:
|
|
1575
|
+
return self._var_map[name]
|
|
1576
|
+
cand = dedupe(_thf_name(name).upper(), self._var_used)
|
|
1577
|
+
self._var_map[name] = cand
|
|
1578
|
+
return cand
|
|
1579
|
+
|
|
1580
|
+
|
|
1581
|
+
def _thf_term(node: Node, names: "_ThfNames") -> str:
|
|
1582
|
+
"""Render an individual term in THF (Variable → unique upper-case token, else a
|
|
1583
|
+
unique functor)."""
|
|
1584
|
+
if isinstance(node, Variable):
|
|
1585
|
+
return names.variable(node.name)
|
|
1586
|
+
from .nodes import Constant, Number, Function
|
|
1587
|
+
if isinstance(node, Constant):
|
|
1588
|
+
return names.constant(node.name)
|
|
1589
|
+
if isinstance(node, Number):
|
|
1590
|
+
return names.constant(prefixed_numeral_name(node.value))
|
|
1591
|
+
if isinstance(node, Function):
|
|
1592
|
+
head = names.function(node)
|
|
1593
|
+
return "( " + " @ ".join([head] + [_thf_term(a, names) for a in node.args]) + " )"
|
|
1594
|
+
raise NotImplementedError(f"to_thf_modal: unsupported term {type(node).__name__}.")
|
|
1595
|
+
|
|
1596
|
+
|
|
1597
|
+
def _thf_lift(node: Node, names: "_ThfNames") -> str:
|
|
1598
|
+
"""Render a modal formula as a THF term of type ``mu > $o``."""
|
|
1599
|
+
if isinstance(node, Down):
|
|
1600
|
+
# See the identical, more fully-explained guard in _st above — this
|
|
1601
|
+
# basic THF export never covered hybrid logic either; hol.thf_modal
|
|
1602
|
+
# (the full shallow embedding) gets its OWN ↓ guard, see that module.
|
|
1603
|
+
raise NotImplementedError(
|
|
1604
|
+
"to_thf_modal: the ↓ binder is outside the alethic □/◇ fragment "
|
|
1605
|
+
"supported by this THF export (which does not cover hybrid logic "
|
|
1606
|
+
"at all — no nominals/@ either); use "
|
|
1607
|
+
"unicode_logic_kit.fol.modal_translation.down_is_valid or "
|
|
1608
|
+
"unicode_logic_kit.atp.kripke_enum.KripkeEnumBackend instead.")
|
|
1609
|
+
if isinstance(node, Atom):
|
|
1610
|
+
# An identity atom never reaches here with its own predicate name: the
|
|
1611
|
+
# callers lower `≠` to `¬(=)` and hand `names` a `_RigidNames`, whose
|
|
1612
|
+
# `atom` maps `=` onto the `meq` macro — THF's own `=` over `$i` with the
|
|
1613
|
+
# world dropped, the reading qml_is_valid has ("Equality is rigid").
|
|
1614
|
+
constant = truth_value(node)
|
|
1615
|
+
if constant is not None:
|
|
1616
|
+
# `$true` / `$false`: the proposition true (false) at every world.
|
|
1617
|
+
return "( ^ [W: mu] : $true )" if constant else "( ^ [W: mu] : $false )"
|
|
1618
|
+
head = names.atom(node)
|
|
1619
|
+
if not node.args:
|
|
1620
|
+
return head
|
|
1621
|
+
return "( " + " @ ".join([head] + [_thf_term(a, names) for a in node.args]) + " )"
|
|
1622
|
+
if isinstance(node, Not):
|
|
1623
|
+
return f"( mnot @ {_thf_lift(node.formula, names)} )"
|
|
1624
|
+
if isinstance(node, And):
|
|
1625
|
+
return f"( mand @ {_thf_lift(node.left, names)} @ {_thf_lift(node.right, names)} )"
|
|
1626
|
+
if isinstance(node, Or):
|
|
1627
|
+
return f"( mor @ {_thf_lift(node.left, names)} @ {_thf_lift(node.right, names)} )"
|
|
1628
|
+
if isinstance(node, Implies):
|
|
1629
|
+
return f"( mimplies @ {_thf_lift(node.left, names)} @ {_thf_lift(node.right, names)} )"
|
|
1630
|
+
if isinstance(node, Iff):
|
|
1631
|
+
return f"( mequiv @ {_thf_lift(node.left, names)} @ {_thf_lift(node.right, names)} )"
|
|
1632
|
+
if isinstance(node, Box):
|
|
1633
|
+
return f"( mbox @ {_thf_lift(node.formula, names)} )"
|
|
1634
|
+
if isinstance(node, Diamond):
|
|
1635
|
+
return f"( mdia @ {_thf_lift(node.formula, names)} )"
|
|
1636
|
+
if isinstance(node, Quantifier):
|
|
1637
|
+
x = names.variable(node.variable.name)
|
|
1638
|
+
binder = "mforall" if node.type in (_FORALL, "forall") else "mexists"
|
|
1639
|
+
return f"( {binder} @ ( ^ [{x}: $i] : {_thf_lift(node.formula, names)} ) )"
|
|
1640
|
+
raise NotImplementedError(
|
|
1641
|
+
f"to_thf_modal: {type(node).__name__} is outside the alethic □/◇ fragment "
|
|
1642
|
+
"supported by this THF export — use "
|
|
1643
|
+
"unicode_logic_kit.hol.thf_modal.to_thf_modal_full, which covers the full "
|
|
1644
|
+
"modal family (epistemic/doxastic/assertive/bouletic/deontic/temporal "
|
|
1645
|
+
"incl. Until/Since, and hybrid nominals/@).")
|
|
1646
|
+
|
|
1647
|
+
|
|
1648
|
+
def _thf_signature(formula: Node, names: Optional["_ThfNames"] = None) -> List[str]:
|
|
1649
|
+
"""Type declarations for every predicate / constant / function in ``formula``.
|
|
1650
|
+
|
|
1651
|
+
Uses the de-colliding :class:`_ThfNames` resolver (built from ``formula`` if not
|
|
1652
|
+
supplied) so each distinct symbol gets a unique functor and a unique declaration.
|
|
1653
|
+
"""
|
|
1654
|
+
if names is None:
|
|
1655
|
+
names = _ThfNames(formula)
|
|
1656
|
+
decls = []
|
|
1657
|
+
for (name, arity), functor in sorted(names.pred.items(), key=lambda kv: kv[1]):
|
|
1658
|
+
typ = " > ".join(["$i"] * arity + ["mu > $o"]) if arity else "mu > $o"
|
|
1659
|
+
decls.append(f"thf({functor}_decl, type, ( {functor} : ( {typ} ) )).")
|
|
1660
|
+
for name, functor in sorted(names.const.items(), key=lambda kv: kv[1]):
|
|
1661
|
+
decls.append(f"thf({functor}_decl, type, ( {functor} : $i )).")
|
|
1662
|
+
for (name, arity), functor in sorted(names.func.items(), key=lambda kv: kv[1]):
|
|
1663
|
+
typ = " > ".join(["$i"] * (arity + 1))
|
|
1664
|
+
decls.append(f"thf({functor}_decl, type, ( {functor} : ( {typ} ) )).")
|
|
1665
|
+
return decls
|
|
1666
|
+
|
|
1667
|
+
|
|
1668
|
+
def to_thf_modal(formula: Node, mode: str = "constant", frame: str = "K") -> str:
|
|
1669
|
+
"""Emit a Benzmüller-style TPTP **THF** shallow embedding of ``formula``.
|
|
1670
|
+
|
|
1671
|
+
Produces a complete, self-contained THF problem — type declarations, the lifted
|
|
1672
|
+
modal operators, the frame axioms for ``frame`` (K/T/S4/S5/KD/KD45), the
|
|
1673
|
+
``existsAt`` domain axioms for ``mode`` (constant / increasing / decreasing /
|
|
1674
|
+
varying), and the conjecture ``mvalid @ ⟨formula⟩`` — ready for a higher-order
|
|
1675
|
+
ATP (Leo-III, Satallax). Covers the alethic □/◇ fragment.
|
|
1676
|
+
|
|
1677
|
+
Equality ``=`` / ``≠`` is **rigid identity**, the reading of :func:`qml_is_valid` /
|
|
1678
|
+
:func:`qml_translate` (module docstring, "Equality is rigid"): THF's own ``=`` over
|
|
1679
|
+
the individual sort ``$i`` with no world argument, through one extra macro
|
|
1680
|
+
``meq = ^ [A: $i, B: $i, W: mu] : ( A = B )`` that is emitted only when the formula
|
|
1681
|
+
contains identity (so an equality-free problem is unchanged). ``t₁ ≠ t₂`` is lowered
|
|
1682
|
+
to ``¬(t₁ = t₂)`` as ``qml_translate`` does, a non-binary ``=`` / ``≠`` atom raises
|
|
1683
|
+
``ValueError`` as there, and no ``feq`` / ``fneq`` functor is declared. So a prover
|
|
1684
|
+
on this problem answers the question :func:`qml_is_valid` answers: ``∀x (x = x)``,
|
|
1685
|
+
``a = b → □(a = b)`` and ``◇(a = b) → a = b`` are theorems under ``frame='K'``,
|
|
1686
|
+
``□(a = b) → a = b`` only under a reflexive / serial frame. Identity is not
|
|
1687
|
+
existence-guarded (the module docstring's varying-domain choice). The macro and the
|
|
1688
|
+
lowering live in :mod:`unicode_logic_kit.hol.thf_modal`, which emits the same line.
|
|
1689
|
+
|
|
1690
|
+
THF has no free variable, so a variable that is free in ``formula`` (a parameter, see
|
|
1691
|
+
the module docstring's "A free variable is a parameter") is bound in the conjecture:
|
|
1692
|
+
``! [Y: $i] : mvalid @ …``, and under a varying regime ``existsAt @ Y`` guards the
|
|
1693
|
+
formula (``! [Y: $i] : mvalid @ (mimplies @ (existsAt @ Y) @ …)``), which is the
|
|
1694
|
+
reading :func:`qml_is_valid` has. For a single formula with no premise this binding
|
|
1695
|
+
is the parameter reading itself.
|
|
1696
|
+
"""
|
|
1697
|
+
# Lazy: hol.thf_modal imports this module, so the shared rigid-identity helpers
|
|
1698
|
+
# are reached from inside the function (as to_isabelle_modal below reaches hol).
|
|
1699
|
+
from ..hol.thf_modal import (
|
|
1700
|
+
_RigidNames, _THF_RIGID_EQ_DEF, _has_identity, _lower_identity,
|
|
1701
|
+
)
|
|
1702
|
+
if frame not in _FRAMES:
|
|
1703
|
+
raise ValueError(f"to_thf_modal: unknown frame {frame!r}.")
|
|
1704
|
+
# A numeral is a constant identified by its value (1 and 1.0 are one), named ``n1``:
|
|
1705
|
+
# a user constant spelled like it is refused, not merged with it.
|
|
1706
|
+
[formula], _ = numerals_as_constants([formula], where="to_thf_modal",
|
|
1707
|
+
spell=prefixed_numeral_name)
|
|
1708
|
+
formula = _lower_identity(formula, "to_thf_modal")
|
|
1709
|
+
lines = [
|
|
1710
|
+
f"% Shallow embedding of a quantified modal formula (mode={mode}, frame={frame}).",
|
|
1711
|
+
"% Conjecture is 'Theorem' iff the formula is QML-valid under this regime.",
|
|
1712
|
+
"thf(mu_type, type, ( mu : $tType )).",
|
|
1713
|
+
"thf(r_decl, type, ( r : ( mu > mu > $o ) )).",
|
|
1714
|
+
"thf(existsAt_decl, type, ( existsAt : ( $i > mu > $o ) )).",
|
|
1715
|
+
]
|
|
1716
|
+
names = _RigidNames(formula)
|
|
1717
|
+
lines += _thf_signature(formula, names)
|
|
1718
|
+
lines.append(_THF_DEFS)
|
|
1719
|
+
if _has_identity(formula):
|
|
1720
|
+
lines.append(_THF_RIGID_EQ_DEF)
|
|
1721
|
+
lines.append("thf(nonempty_dom, axiom, ( ! [W: mu] : ? [X: $i] : ( existsAt @ X @ W ) )).")
|
|
1722
|
+
for cond in _FRAMES[frame]:
|
|
1723
|
+
lines.append(_THF_FRAME[cond])
|
|
1724
|
+
if mode in _THF_DOMAIN:
|
|
1725
|
+
lines.append(_THF_DOMAIN[mode])
|
|
1726
|
+
elif mode not in ("varying",) and mode not in _CONSTANT_MODES:
|
|
1727
|
+
raise ValueError(f"to_thf_modal: unknown mode {mode!r}.")
|
|
1728
|
+
goal = _thf_lift(formula, names)
|
|
1729
|
+
parameters = free_parameter_names([formula])
|
|
1730
|
+
if mode in _ACTUALIST_MODES:
|
|
1731
|
+
for name in reversed(parameters):
|
|
1732
|
+
goal = f"( mimplies @ ( existsAt @ {names.variable(name)} ) @ {goal} )"
|
|
1733
|
+
conjecture = f"mvalid @ {goal}"
|
|
1734
|
+
for name in reversed(parameters):
|
|
1735
|
+
conjecture = f"! [{names.variable(name)}: $i] : ( {conjecture} )"
|
|
1736
|
+
lines.append(f"thf(goal, conjecture, ( {conjecture} )).")
|
|
1737
|
+
return "\n".join(lines) + "\n"
|
|
1738
|
+
|
|
1739
|
+
|
|
1740
|
+
def to_isabelle_modal(formula: Node, mode: str = "constant", frame: str = "K") -> str:
|
|
1741
|
+
"""Emit a complete, loadable Isabelle/HOL theory shallow-embedding ``formula``.
|
|
1742
|
+
|
|
1743
|
+
This delegates to :func:`unicode_logic_kit.hol.isabelle_modal.to_isabelle_modal` — the
|
|
1744
|
+
real, full-modal-family exporter that emits a loadable ``theory … begin … end`` with
|
|
1745
|
+
every lifted operator defined and a genuine ``lemma`` (it replaced the earlier
|
|
1746
|
+
alethic-only skeleton). Use that module directly for the additional options
|
|
1747
|
+
(epistemic/doxastic/deontic/temporal coverage, the proof ``tactic``,
|
|
1748
|
+
``temporal_closure``).
|
|
1749
|
+
|
|
1750
|
+
A variable that is free in ``formula`` stays a free variable of the lemma, which then
|
|
1751
|
+
holds for EVERY individual of the type ``i``. Under ``constant`` / ``possibilist`` that
|
|
1752
|
+
is the parameter reading of :func:`qml_is_valid` (one formula, no premise). Under a
|
|
1753
|
+
varying regime it is stronger than that reading, which asks the individual to exist
|
|
1754
|
+
at the world of evaluation only: a proof of the lemma proves the parameter instance,
|
|
1755
|
+
and a counterexample to the lemma in which the individual does not exist at the world
|
|
1756
|
+
of evaluation does not refute it. Use :func:`qml_is_valid` for the verdict under such
|
|
1757
|
+
a regime.
|
|
1758
|
+
"""
|
|
1759
|
+
from ..hol.isabelle_modal import to_isabelle_modal as _real
|
|
1760
|
+
return _real(formula, mode=mode, frame=frame)
|