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,1018 @@
|
|
|
1
|
+
r"""Classical FOL/MSFOL and propositional modal-K → **Lean 4** exporter, plus an
|
|
2
|
+
optional LIVE elaboration tier through a local Lean 4 toolchain.
|
|
3
|
+
|
|
4
|
+
This is the Lean counterpart of :mod:`unicode_logic_kit.hol.classical` (THF /
|
|
5
|
+
Isabelle) — a first vertical slice, structured the same way, and deliberately
|
|
6
|
+
**not** parity with the full Isabelle/THF pair (no relevant/substructural/
|
|
7
|
+
many-valued/second-third-order/deepshallow Lean routes yet; those are separate
|
|
8
|
+
follow-on items once this pattern is proven). It covers exactly two fragments:
|
|
9
|
+
|
|
10
|
+
* classical **FOL / MSFOL** (:func:`to_lean_fol`, :func:`to_lean_msfol`), and
|
|
11
|
+
* the propositional **modal-K** fragment — Box/Diamond + connectives over
|
|
12
|
+
ground atoms, no frame conditions on the accessibility relation
|
|
13
|
+
(:func:`to_lean_modal_k`).
|
|
14
|
+
|
|
15
|
+
**Emit-only is the honest scope.** Like :mod:`~unicode_logic_kit.hol.classical`,
|
|
16
|
+
the functions above only *emit* a self-contained ``.lean`` source that always
|
|
17
|
+
elaborates (the goal is ``sorry``-closed by default): "here is a well-typed
|
|
18
|
+
statement", never "proved". The optional live tier below is the other half —
|
|
19
|
+
if a local Lean 4 toolchain is installed, :func:`check_theory` actually
|
|
20
|
+
*elaborates* the emitted file (catching syntax/typing bugs no string test
|
|
21
|
+
can), and :func:`lean_decide_fol` / :func:`lean_decide_modal_k` additionally
|
|
22
|
+
try a small tactic battery for a genuine, kernel-checked proof. A file this
|
|
23
|
+
module PRESENTS as checked never contains ``sorry``: :class:`LeanBuildResult`
|
|
24
|
+
tracks ``uses_sorry`` separately from ``ok`` (a ``sorry``-closed file still
|
|
25
|
+
*elaborates*, exit 0), and every ``*_decide_*`` verdict is **VALID only when
|
|
26
|
+
the proof is genuinely sorry-free** — never a false "proved". There is no
|
|
27
|
+
``INVALID`` verdict here (unlike :mod:`~unicode_logic_kit.hol.isabelle_runner`'s
|
|
28
|
+
nitpick-backed refutation): only ``VALID`` / ``UNKNOWN``, exactly the two
|
|
29
|
+
outcomes the tactic battery can honestly support without a model finder.
|
|
30
|
+
|
|
31
|
+
Faithfulness of the encoding
|
|
32
|
+
-----------------------------
|
|
33
|
+
**The uninterpreted individual type is non-empty by construction, exactly
|
|
34
|
+
like Isabelle's ``typedecl``.** A bare Lean ``axiom Ind : Type`` is *not*
|
|
35
|
+
non-empty by fiat (unlike Isabelle/HOL's ``typedecl``, which always denotes a
|
|
36
|
+
non-empty type) — omitting a witness would silently invalidate classical
|
|
37
|
+
schemas that depend on a non-empty domain (e.g. ``(∀x, P x) → ∃x, P x``),
|
|
38
|
+
exactly the "approximate silently" failure this project refuses to ship. Every
|
|
39
|
+
emitted theory therefore declares
|
|
40
|
+
|
|
41
|
+
.. code-block:: lean
|
|
42
|
+
|
|
43
|
+
axiom Ind : Type
|
|
44
|
+
axiom Ind_nonempty : Nonempty Ind
|
|
45
|
+
instance : Nonempty Ind := Ind_nonempty
|
|
46
|
+
|
|
47
|
+
— an explicit witness *axiom*, registered as a type-class ``instance`` too so
|
|
48
|
+
downstream tactics that need ``Nonempty``/``Inhabited`` search find it. The
|
|
49
|
+
propositional modal-K embedding does the same for its Kripke world type
|
|
50
|
+
(``World`` / ``World_nonempty``).
|
|
51
|
+
|
|
52
|
+
**Many-sorted input.** :func:`to_lean_msfol` reduces a many-sorted formula
|
|
53
|
+
with :func:`~unicode_logic_kit.fol.nodes.to_fol` (each sort becomes a unary
|
|
54
|
+
guard predicate over the single flat ``Ind``, each sorted quantifier is
|
|
55
|
+
relativised — ``∀x:S φ ↦ ∀x (S(x) → φ)``, ``∃x:S φ ↦ ∃x (S(x) ∧ φ)``) and
|
|
56
|
+
then emits the resulting plain-FOL formula as :func:`to_lean_fol` does,
|
|
57
|
+
**exactly like** :func:`~unicode_logic_kit.hol.classical.to_isabelle_msfol` /
|
|
58
|
+
:func:`~unicode_logic_kit.hol.classical.to_thf_msfol`. That reduction forgets the
|
|
59
|
+
two facts of the many-sorted reading (:func:`~unicode_logic_kit.fol.nodes.sort_axioms`):
|
|
60
|
+
no sort is empty, and a sorted constant ``c:S`` is an element of ``S``. For a
|
|
61
|
+
theorem they are stated as HYPOTHESES of it — ``theorem goal (sort_nonempty_0 :
|
|
62
|
+
∃ x : Ind, human x) (sort_member_0 : human socrates) : φ`` — never conjoined to
|
|
63
|
+
the goal (``human socrates ∧ φ`` could not be proved even for a tautology ``φ``)
|
|
64
|
+
and never folded into the per-node translation, which is polarity-blind (an
|
|
65
|
+
existential baked in there would land under the wrong polarity whenever the
|
|
66
|
+
sorted node occurs negated). Being local hypotheses they are visible to
|
|
67
|
+
``tauto`` and ``simp``, which a global ``axiom`` is not. ``Ind`` itself is
|
|
68
|
+
guaranteed non-empty (above); ``include_sort_facts=False`` is the bare
|
|
69
|
+
relativisation with no sort facts at all.
|
|
70
|
+
|
|
71
|
+
**Equality.** By default ``=`` / ``≠`` are the *uninterpreted* predicates
|
|
72
|
+
``feq`` / ``fneq`` (not Lean's own ``=``), matching the toolkit-wide HOL
|
|
73
|
+
convention (:mod:`~unicode_logic_kit.hol.classical`, ``qml.to_thf_modal``). Pass
|
|
74
|
+
``native_equality=True`` to emit Lean's own built-in, congruence-free ``=`` /
|
|
75
|
+
``≠`` instead — genuine identity at ``Ind``, so no axioms are needed for it.
|
|
76
|
+
|
|
77
|
+
**Propositional modal-K.** :func:`to_lean_modal_k` is a Benzmüller-style
|
|
78
|
+
shallow embedding, faithful to
|
|
79
|
+
:func:`~unicode_logic_kit.semantics.kripke.satisfies_modal` restricted to the
|
|
80
|
+
propositional fragment (``Box``/``Diamond`` + connectives over ground atoms
|
|
81
|
+
only — no quantifiers, agent-indexed modalities, deontic, or temporal
|
|
82
|
+
operators; those raise ``NotImplementedError`` naming the construct) over an
|
|
83
|
+
**unconstrained** accessibility relation ``R`` — frame **K**, no ``refl`` /
|
|
84
|
+
``trans`` / ... conditions, since this vertical slice covers only K. Box/
|
|
85
|
+
Diamond translate the standard clauses ``⟦□φ⟧w = ∀v, R w v → ⟦φ⟧v`` /
|
|
86
|
+
``⟦◇φ⟧w = ∃v, R w v ∧ ⟦φ⟧v``, and every distinct ground atom becomes its own
|
|
87
|
+
``World → Prop`` valuation axiom. ``tests/test_lean.py`` validates this
|
|
88
|
+
encoding two ways: (a) the modal **K axiom** ``□(p→q) → (□p → □q)`` elaborates
|
|
89
|
+
*and* has a hand-written, kernel-checked proof (no ``sorry``) — live-checked
|
|
90
|
+
against a real Lean 4 install; (b) a known **non**-theorem of K (``□p → p``,
|
|
91
|
+
the ``T`` axiom, which needs reflexivity K does not have) is refuted: a
|
|
92
|
+
concrete, decidable, two-world instantiation makes Lean's own ``decide``
|
|
93
|
+
tactic *prove the goal is false* (not merely "fails to prove it") — so the
|
|
94
|
+
encoding is never one Isabelle-style ``INVALID`` verdict shy of a false
|
|
95
|
+
positive.
|
|
96
|
+
|
|
97
|
+
**Lean 4 core only — no Mathlib.** Every fragment above (classical FOL/MSFOL
|
|
98
|
+
over ``Classical`` reasoning, propositional modal K) is expressible in bare
|
|
99
|
+
Lean 4 core + the ``Classical`` namespace it ships with (``Classical.em`` and
|
|
100
|
+
friends — confirmed by hand: this module's own live tests run against a real,
|
|
101
|
+
Mathlib-free Lean 4.34 toolchain). Emitted theories always ``open Classical``
|
|
102
|
+
so a later, more elaborate hand proof can reach for it. The optional
|
|
103
|
+
:data:`DEFAULT_METHODS` battery (``decide`` / ``tauto`` / ``aesop``) reflects
|
|
104
|
+
this honestly: ``tauto`` and ``aesop`` are *Mathlib* tactics, so without it
|
|
105
|
+
they simply fail to parse ("unknown tactic") and the battery moves on to the
|
|
106
|
+
next method — never a crash, never a false ``VALID``. ``decide`` only closes
|
|
107
|
+
a goal Lean can find a computable ``Decidable`` instance for; the axiom-based
|
|
108
|
+
shallow embeddings above are intentionally *not* decidable (``Ind`` / ``World``
|
|
109
|
+
are opaque, uninterpreted types), so in a Mathlib-free environment
|
|
110
|
+
:func:`lean_decide_fol` / :func:`lean_decide_modal_k` will typically report
|
|
111
|
+
``UNKNOWN`` for a nontrivial goal even when it IS valid — exactly the honest,
|
|
112
|
+
incomplete-but-sound behaviour :mod:`~unicode_logic_kit.hol.isabelle_runner`
|
|
113
|
+
already documents for its own battery, never a false "proved".
|
|
114
|
+
|
|
115
|
+
Locating Lean
|
|
116
|
+
--------------
|
|
117
|
+
:func:`find_lean` looks, in order, at an explicit path, the env var
|
|
118
|
+
``UFK_LEAN_HOME``, ``lean`` on ``PATH``, and the standard ``elan`` install
|
|
119
|
+
location ``~/.elan/bin`` — the same discovery shape
|
|
120
|
+
:func:`~unicode_logic_kit.hol.isabelle_runner.find_isabelle` uses for Isabelle.
|
|
121
|
+
:func:`lean_available` is the cheap predicate tests gate on. Nothing in this
|
|
122
|
+
module modifies ``PATH`` or a shell profile; a toolchain installed via
|
|
123
|
+
``elan-init --no-modify-path`` is still found through ``~/.elan/bin`` or
|
|
124
|
+
``UFK_LEAN_HOME``.
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
from __future__ import annotations
|
|
128
|
+
|
|
129
|
+
import os
|
|
130
|
+
import re
|
|
131
|
+
import shutil
|
|
132
|
+
import subprocess
|
|
133
|
+
import tempfile
|
|
134
|
+
import time
|
|
135
|
+
import uuid
|
|
136
|
+
from dataclasses import dataclass
|
|
137
|
+
from typing import Dict, List, Optional, Sequence, Tuple
|
|
138
|
+
|
|
139
|
+
from ..fol._msfl_nodes import _reduce_nl_nodes, key_text
|
|
140
|
+
from ..fol._numeral_symbols import numerals_as_constants, prefixed_numeral_name
|
|
141
|
+
from ..fol._symbol_names import dedupe
|
|
142
|
+
from ..fol._truth_constants import truth_value
|
|
143
|
+
from ..fol.nodes import (
|
|
144
|
+
Node, Variable, Constant, Number, Function, Measure,
|
|
145
|
+
Atom, Not, And, Or, Xor, Implies, Iff, Quantifier,
|
|
146
|
+
Box, Diamond,
|
|
147
|
+
)
|
|
148
|
+
from .classical import (
|
|
149
|
+
_signature, _sanitize, _SymbolResolver, _VarResolver, _free_variables,
|
|
150
|
+
_CAT_PRED, _CAT_FUNC, _CAT_CONST, _is_native_eq, _FORALL,
|
|
151
|
+
_scope, _msfol_split, _refuse_open_assertion,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
__all__ = [
|
|
155
|
+
"to_lean_fol", "to_lean_msfol", "to_lean_modal_k",
|
|
156
|
+
"LeanInstall", "LeanNotAvailable", "LeanBuildResult", "LeanVerdict",
|
|
157
|
+
"find_lean", "lean_available", "check_theory",
|
|
158
|
+
"lean_decide_fol", "lean_decide_modal_k",
|
|
159
|
+
"DEFAULT_METHODS", "VALID", "UNKNOWN",
|
|
160
|
+
]
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# ===========================================================================
|
|
164
|
+
# Lean 4 identifier safety: reserved words + this module's own scaffold names
|
|
165
|
+
# ===========================================================================
|
|
166
|
+
|
|
167
|
+
# Lean 4 keywords / predeclared core identifiers a sanitised source-symbol
|
|
168
|
+
# name (e.g. a natural-language predicate literally called "in" or "at")
|
|
169
|
+
# could otherwise collide with. Not claimed to be exhaustive of every Lean
|
|
170
|
+
# parser token, but covers every keyword class (binders, declarations, macro/
|
|
171
|
+
# syntax machinery, control flow, placeholders) plus the handful of lowercase
|
|
172
|
+
# core names (`true`/`false`/...) most likely to appear in a translated
|
|
173
|
+
# natural-language formula.
|
|
174
|
+
_LEAN_RESERVED = frozenset({
|
|
175
|
+
"fun", "let", "in", "do", "at", "with", "where", "from", "have", "show",
|
|
176
|
+
"suffices", "calc", "this", "match", "if", "then", "else", "by",
|
|
177
|
+
"forall", "exists", "nomatch", "rec", "generalizing", "deriving",
|
|
178
|
+
"def", "theorem", "lemma", "axiom", "opaque", "constant", "abbrev",
|
|
179
|
+
"instance", "class", "structure", "inductive", "mutual", "partial",
|
|
180
|
+
"unsafe", "noncomputable", "private", "protected", "scoped", "local",
|
|
181
|
+
"variable", "variables", "universe", "universes", "namespace", "section",
|
|
182
|
+
"end", "open", "import", "extends", "attribute",
|
|
183
|
+
"macro", "macro_rules", "syntax", "elab", "elab_rules", "notation",
|
|
184
|
+
"infix", "infixl", "infixr", "prefix", "postfix",
|
|
185
|
+
"set_option", "run_cmd",
|
|
186
|
+
"sorry", "admit", "native_decide", "stop",
|
|
187
|
+
"for", "while", "return", "try", "catch", "finally", "break", "continue",
|
|
188
|
+
"mut",
|
|
189
|
+
"true", "false", "and", "or", "not", "iff", "eq", "ne",
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class _LeanNames:
|
|
194
|
+
"""Wrap an already-de-collided identifier (from :class:`_SymbolResolver`,
|
|
195
|
+
:class:`_VarResolver`, or a locally synthesised name) into one that ALSO
|
|
196
|
+
avoids Lean 4 reserved words and this emission's own scaffold identifiers
|
|
197
|
+
(``Ind`` / ``World`` / ``R`` / ``goal`` / ...), while staying globally
|
|
198
|
+
injective: two distinct LOGICAL symbols never map to the same output.
|
|
199
|
+
|
|
200
|
+
Memoisation is keyed on an explicit, caller-supplied ``key`` that
|
|
201
|
+
identifies the *logical* symbol requesting a name — e.g. ``(category,
|
|
202
|
+
raw_name, arity)`` for a predicate/function/constant (the same key
|
|
203
|
+
:class:`_SymbolResolver` itself uses), ``("var", raw_name)`` for a bound
|
|
204
|
+
variable, or ``("atom", atom_key)`` / ``("world", depth)`` for the modal
|
|
205
|
+
encoding — NEVER the candidate string itself. This distinguishes "the
|
|
206
|
+
same logical symbol requested twice" (which must be idempotent: the
|
|
207
|
+
second request returns the identifier already assigned) from "two
|
|
208
|
+
DIFFERENT logical symbols whose independently-sanitised candidate
|
|
209
|
+
strings just happen to coincide" (which must still be de-collided against
|
|
210
|
+
each other). Caching on the candidate string alone cannot tell these
|
|
211
|
+
apart — a predicate resolved by :class:`_SymbolResolver` and a bound
|
|
212
|
+
variable resolved by the entirely separate :class:`_VarResolver` (or the
|
|
213
|
+
``w{n}`` Kripke-world counter in :func:`to_lean_modal_k`) run their OWN
|
|
214
|
+
de-collision within their own namespace, so they can and do produce the
|
|
215
|
+
identical candidate string for two distinct symbols (e.g. a predicate
|
|
216
|
+
named ``Foo`` and a bound variable also named ``Foo`` both sanitise to
|
|
217
|
+
``"foo"``; an atom literally named ``w1`` and the Kripke-world binder at
|
|
218
|
+
depth 1 both produce ``"w1"``) — exactly the scenario this class exists
|
|
219
|
+
to rule out.
|
|
220
|
+
|
|
221
|
+
One instance is threaded through a whole emission (declarations first,
|
|
222
|
+
then the formula body), so a synthesised bound-variable name is checked
|
|
223
|
+
against every symbol identifier already declared, not just against other
|
|
224
|
+
bound-variable names — Lean itself tolerates a local binder shadowing a
|
|
225
|
+
top-level ``axiom`` (verified by hand: it elaborates, just silently reads
|
|
226
|
+
the wrong one inside the shadowed scope, or in the modal case fails to
|
|
227
|
+
elaborate at all because the shadowed ``axiom``'s function type is gone),
|
|
228
|
+
so keying only by logical identity, never by the rendered string, is what
|
|
229
|
+
keeps that from being a silent semantic bug.
|
|
230
|
+
"""
|
|
231
|
+
|
|
232
|
+
def __init__(self, reserved=frozenset()):
|
|
233
|
+
self._used = set(_LEAN_RESERVED) | set(reserved)
|
|
234
|
+
self._map: Dict[object, str] = {}
|
|
235
|
+
|
|
236
|
+
def safe(self, key: object, ident: str) -> str:
|
|
237
|
+
"""Return the unique Lean identifier for the logical symbol ``key``.
|
|
238
|
+
|
|
239
|
+
``ident`` is that symbol's already-sanitised candidate string (from
|
|
240
|
+
the appropriate resolver). The SAME ``key`` always returns the SAME
|
|
241
|
+
identifier (idempotent); a DIFFERENT ``key`` whose ``ident`` collides
|
|
242
|
+
with one already assigned is de-collided via :func:`dedupe` instead
|
|
243
|
+
of silently reusing it.
|
|
244
|
+
"""
|
|
245
|
+
if key in self._map:
|
|
246
|
+
return self._map[key]
|
|
247
|
+
cand = dedupe(ident, self._used)
|
|
248
|
+
self._map[key] = cand
|
|
249
|
+
return cand
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
# ===========================================================================
|
|
253
|
+
# (A) Classical FOL / MSFOL -> Lean 4
|
|
254
|
+
# ===========================================================================
|
|
255
|
+
|
|
256
|
+
_LEAN_BINOP = {And: "∧", Or: "∨", Implies: "→", Iff: "↔"}
|
|
257
|
+
|
|
258
|
+
_FOL_RESERVED = frozenset({"Ind", "Ind_nonempty", "goal"})
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _lean_term(node: Node, syms: "_SymbolResolver", vars_: "_VarResolver",
|
|
262
|
+
names: "_LeanNames") -> str:
|
|
263
|
+
"""Render an individual (``Ind``) term in Lean prefix-application syntax."""
|
|
264
|
+
if isinstance(node, Variable):
|
|
265
|
+
return names.safe(("var", node.name), vars_.token(node.name))
|
|
266
|
+
if isinstance(node, Constant):
|
|
267
|
+
return names.safe((_CAT_CONST, node.name, 0), syms.name(_CAT_CONST, node.name, 0))
|
|
268
|
+
if isinstance(node, Number):
|
|
269
|
+
raw = prefixed_numeral_name(node.value) # one constant per VALUE: 1 and 1.0 are ``n1``
|
|
270
|
+
return names.safe((_CAT_CONST, raw, 0), syms.name(_CAT_CONST, raw, 0))
|
|
271
|
+
if isinstance(node, Function):
|
|
272
|
+
key = (_CAT_FUNC, node.name, len(node.args))
|
|
273
|
+
head = names.safe(key, syms.name(_CAT_FUNC, node.name, len(node.args)))
|
|
274
|
+
args = [_lean_term(a, syms, vars_, names) for a in node.args]
|
|
275
|
+
return "(" + " ".join([head] + args) + ")"
|
|
276
|
+
if isinstance(node, Measure):
|
|
277
|
+
key = (_CAT_FUNC, "measure", 2)
|
|
278
|
+
head = names.safe(key, syms.name(_CAT_FUNC, "measure", 2))
|
|
279
|
+
return ("(" + " ".join([head, _lean_term(node.entity, syms, vars_, names),
|
|
280
|
+
_lean_term(node.dimension, syms, vars_, names)]) + ")")
|
|
281
|
+
raise NotImplementedError(
|
|
282
|
+
f"to_lean_fol: unsupported term {type(node).__name__} (classical FOL terms only)."
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _lean_formula(node: Node, syms: "_SymbolResolver", vars_: "_VarResolver",
|
|
287
|
+
names: "_LeanNames", native_equality: bool = False) -> str:
|
|
288
|
+
"""Render a classical FOL formula as a Lean ``Prop`` term.
|
|
289
|
+
|
|
290
|
+
Connectives use Lean 4's own Unicode notation (``¬ ∧ ∨ → ↔``, all part of
|
|
291
|
+
core, no import needed); quantifiers bind ``Ind`` variables (``∀ x : Ind,
|
|
292
|
+
…`` / ``∃ x : Ind, …``); atoms apply their declared predicate by plain
|
|
293
|
+
juxtaposition. Every predicate / function / constant / variable name is
|
|
294
|
+
routed through the resolvers AND :class:`_LeanNames`, so distinct source
|
|
295
|
+
symbols stay distinct and none collides with a Lean keyword or this
|
|
296
|
+
emission's own scaffold names. Anything outside the classical fragment
|
|
297
|
+
(modal, second-order, many-valued, substructural, lambda) raises
|
|
298
|
+
``NotImplementedError``.
|
|
299
|
+
|
|
300
|
+
With ``native_equality=True``, a binary ``=``/``≠`` atom renders as
|
|
301
|
+
Lean's own infix ``(a = b)`` / ``(a ≠ b)`` instead of the ``feq``/``fneq``
|
|
302
|
+
functor — genuine, axiom-free Lean identity.
|
|
303
|
+
"""
|
|
304
|
+
def f(n):
|
|
305
|
+
return _lean_formula(n, syms, vars_, names, native_equality)
|
|
306
|
+
if isinstance(node, Atom):
|
|
307
|
+
if truth_value(node) is not None:
|
|
308
|
+
return "True" if truth_value(node) else "False"
|
|
309
|
+
if _is_native_eq(node.predicate, len(node.args), native_equality):
|
|
310
|
+
op = "=" if node.predicate == "=" else "≠"
|
|
311
|
+
left = _lean_term(node.args[0], syms, vars_, names)
|
|
312
|
+
right = _lean_term(node.args[1], syms, vars_, names)
|
|
313
|
+
return f"({left} {op} {right})"
|
|
314
|
+
key = (_CAT_PRED, node.predicate, len(node.args))
|
|
315
|
+
head = names.safe(key, syms.name(_CAT_PRED, node.predicate, len(node.args)))
|
|
316
|
+
if not node.args:
|
|
317
|
+
return head
|
|
318
|
+
return "(" + " ".join([head] + [_lean_term(a, syms, vars_, names)
|
|
319
|
+
for a in node.args]) + ")"
|
|
320
|
+
if isinstance(node, Not):
|
|
321
|
+
return f"(¬ {f(node.formula)})"
|
|
322
|
+
if type(node) in _LEAN_BINOP:
|
|
323
|
+
op = _LEAN_BINOP[type(node)]
|
|
324
|
+
return f"({f(node.left)} {op} {f(node.right)})"
|
|
325
|
+
if isinstance(node, Xor):
|
|
326
|
+
return f"(¬ ({f(node.left)} ↔ {f(node.right)}))"
|
|
327
|
+
if isinstance(node, Quantifier):
|
|
328
|
+
x = names.safe(("var", node.variable.name), vars_.token(node.variable.name))
|
|
329
|
+
binder = "∀" if node.type in (_FORALL, "forall") else "∃"
|
|
330
|
+
return f"({binder} {x} : Ind, {f(node.formula)})"
|
|
331
|
+
raise NotImplementedError(
|
|
332
|
+
f"to_lean_fol: {type(node).__name__} is outside the classical FOL fragment "
|
|
333
|
+
"supported by the Lean export (no modal / second-order / many-valued / "
|
|
334
|
+
"substructural / lambda). Propositional modal K -> hol.lean.to_lean_modal_k; "
|
|
335
|
+
"every other non-classical logic has no Lean route yet (see the module "
|
|
336
|
+
"docstring: this is a first vertical slice)."
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _lean_signature_decls(formula: Node, syms: "_SymbolResolver", names: "_LeanNames",
|
|
341
|
+
native_equality: bool = False) -> List[str]:
|
|
342
|
+
"""``axiom`` declarations for every predicate / function / constant.
|
|
343
|
+
|
|
344
|
+
Individuals live in the single uninterpreted, explicitly-``Nonempty`` type
|
|
345
|
+
``Ind``; a k-ary predicate has type ``Ind → … → Prop`` and a k-ary
|
|
346
|
+
function ``Ind → … → Ind``. Each declaration uses the resolver-assigned,
|
|
347
|
+
then :class:`_LeanNames`-wrapped identifier, so no two declarations share
|
|
348
|
+
a name and every name matches its usages. A binary ``=``/``≠`` skips its
|
|
349
|
+
declaration when ``native_equality=True`` — Lean's own polymorphic ``=``
|
|
350
|
+
needs no ``axiom``.
|
|
351
|
+
"""
|
|
352
|
+
preds, funcs, consts = _signature(formula)
|
|
353
|
+
decls: List[str] = []
|
|
354
|
+
for name, arity in sorted(preds):
|
|
355
|
+
if _is_native_eq(name, arity, native_equality):
|
|
356
|
+
continue
|
|
357
|
+
key = (_CAT_PRED, name, arity)
|
|
358
|
+
ident = names.safe(key, syms.name(_CAT_PRED, name, arity))
|
|
359
|
+
typ = " → ".join(["Ind"] * arity + ["Prop"]) if arity else "Prop"
|
|
360
|
+
decls.append(f"axiom {ident} : {typ}")
|
|
361
|
+
for name, arity in sorted(funcs):
|
|
362
|
+
key = (_CAT_FUNC, name, arity)
|
|
363
|
+
ident = names.safe(key, syms.name(_CAT_FUNC, name, arity))
|
|
364
|
+
typ = " → ".join(["Ind"] * (arity + 1))
|
|
365
|
+
decls.append(f"axiom {ident} : {typ}")
|
|
366
|
+
for name in sorted(consts):
|
|
367
|
+
key = (_CAT_CONST, name, 0)
|
|
368
|
+
ident = names.safe(key, syms.name(_CAT_CONST, name, 0))
|
|
369
|
+
decls.append(f"axiom {ident} : Ind")
|
|
370
|
+
return decls
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def to_lean_fol(formula: Node, conjecture: bool = True, native_equality: bool = False,
|
|
374
|
+
proof: str = "sorry") -> str:
|
|
375
|
+
"""Emit a complete, self-contained **Lean 4** source for a classical FOL ``formula``.
|
|
376
|
+
|
|
377
|
+
Declares the uninterpreted individual type ``Ind`` (with an explicit
|
|
378
|
+
``Nonempty`` witness — see the module docstring on why that is the one
|
|
379
|
+
real semantic trap here), a typed ``axiom`` for every predicate / function
|
|
380
|
+
/ constant in the signature, ``open Classical`` (for a later hand proof
|
|
381
|
+
that needs ``Classical.em`` / ``Classical.byContradiction``), and the
|
|
382
|
+
formula itself as ``theorem goal : … := by`` / `` <proof>`` (default
|
|
383
|
+
``proof="sorry"`` — the file always elaborates without claiming a proof,
|
|
384
|
+
Lean's analogue of Isabelle's ``oops``). ``proof`` is the TACTIC BLOCK
|
|
385
|
+
BODY (no leading ``by`` — this function supplies it), one or more
|
|
386
|
+
newline-separated tactics, each re-indented two spaces so a multi-line
|
|
387
|
+
hand proof parses regardless of how the caller indented it. With
|
|
388
|
+
``conjecture=False`` the formula is instead emitted as ``axiom goal : …``
|
|
389
|
+
— no proof needed, useful for asserting the formula as a hypothesis in a
|
|
390
|
+
larger hand-written problem.
|
|
391
|
+
|
|
392
|
+
A free variable of a theorem is closed universally before emission (matching
|
|
393
|
+
:func:`~unicode_logic_kit.hol.classical.to_thf_fol` /
|
|
394
|
+
:func:`~unicode_logic_kit.hol.classical.to_isabelle_fol`). A free variable is a
|
|
395
|
+
parameter of the problem, one unknown individual, and for a single formula with no
|
|
396
|
+
premise the two readings coincide: the formula is valid for the parameter iff it is
|
|
397
|
+
valid for every individual. An ASSERTED formula (``conjecture=False``) is a premise
|
|
398
|
+
of a larger problem, and there the closure would say more than the formula says
|
|
399
|
+
(``∀x P(x)`` entails ``P(a)``, the premise ``P(x)`` does not), so an asserted
|
|
400
|
+
formula with a free variable is refused by name: state the parameter with a
|
|
401
|
+
constant, or bind the variable with a quantifier.
|
|
402
|
+
|
|
403
|
+
By default, equality ``=`` / ``≠`` becomes the uninterpreted
|
|
404
|
+
predicate ``feq`` / ``fneq`` (see module docstring); pass
|
|
405
|
+
``native_equality=True`` to instead emit Lean's own built-in ``=`` / ``≠``.
|
|
406
|
+
|
|
407
|
+
Classical FOL is *semi-decidable only*: no tactic is guaranteed to close
|
|
408
|
+
every valid goal. This function only emits the file; it does not run
|
|
409
|
+
Lean — see :func:`check_theory` / :func:`lean_decide_fol` for the optional
|
|
410
|
+
live tier.
|
|
411
|
+
|
|
412
|
+
Raises:
|
|
413
|
+
NotImplementedError: ``conjecture`` is false and ``formula`` has a free variable;
|
|
414
|
+
or a numeral and a constant are spelled alike.
|
|
415
|
+
"""
|
|
416
|
+
return _lean_problem(formula, conjecture, native_equality, proof)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _lean_problem(formula: Node, conjecture: bool, native_equality: bool, proof: str,
|
|
420
|
+
background: Sequence[Node] = (), where: str = "to_lean_fol") -> str:
|
|
421
|
+
"""The Lean source of ``formula``, with ``background`` as hypotheses of its theorem.
|
|
422
|
+
|
|
423
|
+
``background`` are closed sentences (the many-sorted reading's sort facts, see
|
|
424
|
+
:func:`~unicode_logic_kit.hol.classical._msfol_split`). For a theorem each becomes
|
|
425
|
+
a named hypothesis (``sort_nonempty_<i>`` for an ``∃``, ``sort_member_<i>`` for
|
|
426
|
+
an atom), so they are in the local context a tactic searches; for an asserted
|
|
427
|
+
formula each is an ``axiom`` line of its own.
|
|
428
|
+
|
|
429
|
+
A numeral is a constant identified by its value (``1`` and ``1.0`` are the one constant
|
|
430
|
+
``n1``); a constant spelled like it, in the formula or in ``background``, is refused by
|
|
431
|
+
name (``where`` is the function that is writing, which the refusal names).
|
|
432
|
+
"""
|
|
433
|
+
formulas, _ = numerals_as_constants([formula, *background], where=where,
|
|
434
|
+
spell=prefixed_numeral_name)
|
|
435
|
+
formula, background = formulas[0], formulas[1:]
|
|
436
|
+
formula = _reduce_nl_nodes(formula) # Contrast -> And, Count -> witnesses
|
|
437
|
+
_refuse_open_assertion(formula, conjecture, where)
|
|
438
|
+
closed = formula
|
|
439
|
+
for name in reversed(_free_variables(formula)):
|
|
440
|
+
closed = Quantifier(_FORALL, Variable(name), closed)
|
|
441
|
+
scope = _scope(closed, background)
|
|
442
|
+
syms = _SymbolResolver(scope, native_equality=native_equality)
|
|
443
|
+
vars_ = _VarResolver(_sanitize)
|
|
444
|
+
names = _LeanNames(_FOL_RESERVED)
|
|
445
|
+
|
|
446
|
+
lines = [
|
|
447
|
+
"-- Classical FOL embedded into Lean 4 core (no Mathlib) over an",
|
|
448
|
+
"-- uninterpreted, EXPLICITLY NONEMPTY individual type `Ind` and",
|
|
449
|
+
"-- uninterpreted predicates/functions/constants declared as `axiom`s.",
|
|
450
|
+
]
|
|
451
|
+
preds, _, _ = _signature(scope)
|
|
452
|
+
if any(_is_native_eq(n, a, native_equality) for n, a in preds):
|
|
453
|
+
lines.append("-- '=' / '≠' are Lean's own built-in identity (no axioms needed).")
|
|
454
|
+
else:
|
|
455
|
+
lines.append("-- '=' / '≠' are the uninterpreted predicates feq / fneq, NOT Lean's `=`.")
|
|
456
|
+
lines += [
|
|
457
|
+
"",
|
|
458
|
+
"axiom Ind : Type",
|
|
459
|
+
"axiom Ind_nonempty : Nonempty Ind",
|
|
460
|
+
"instance : Nonempty Ind := Ind_nonempty",
|
|
461
|
+
"",
|
|
462
|
+
"open Classical",
|
|
463
|
+
"",
|
|
464
|
+
]
|
|
465
|
+
decls = _lean_signature_decls(scope, syms, names, native_equality=native_equality)
|
|
466
|
+
if decls:
|
|
467
|
+
lines += decls
|
|
468
|
+
lines.append("")
|
|
469
|
+
body = _lean_formula(closed, syms, vars_, names, native_equality=native_equality)
|
|
470
|
+
facts = []
|
|
471
|
+
nonempty = member = 0
|
|
472
|
+
for fact in background:
|
|
473
|
+
if isinstance(fact, Quantifier):
|
|
474
|
+
label, nonempty = f"sort_nonempty_{nonempty}", nonempty + 1
|
|
475
|
+
else:
|
|
476
|
+
label, member = f"sort_member_{member}", member + 1
|
|
477
|
+
facts.append((names.safe(("sort_fact", label), label),
|
|
478
|
+
_lean_formula(fact, syms, vars_, names, native_equality=native_equality)))
|
|
479
|
+
if conjecture:
|
|
480
|
+
hypotheses = "".join(f" ({label} : {text})" for label, text in facts)
|
|
481
|
+
lines.append(f"theorem goal{hypotheses} : {body} := by")
|
|
482
|
+
lines.extend(f" {pl}" for pl in proof.split("\n"))
|
|
483
|
+
else:
|
|
484
|
+
lines.extend(f"axiom {label} : {text}" for label, text in facts)
|
|
485
|
+
lines.append(f"axiom goal : {body}")
|
|
486
|
+
return "\n".join(lines) + "\n"
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def to_lean_msfol(formula: Node, conjecture: bool = True, include_sort_facts: bool = True,
|
|
490
|
+
native_equality: bool = False, proof: str = "sorry") -> str:
|
|
491
|
+
"""Emit a **Lean 4** source for a *many-sorted* FOL ``formula`` via guard relativization.
|
|
492
|
+
|
|
493
|
+
Each sort becomes a unary guard predicate and each sorted quantifier is
|
|
494
|
+
relativized (``∀x:S φ ↦ ∀x (S(x) → φ)``, ``∃x:S φ ↦ ∃x (S(x) ∧ φ)``) by
|
|
495
|
+
the toolkit's :func:`~unicode_logic_kit.fol.nodes.to_fol`; the resulting
|
|
496
|
+
plain-FOL formula is then emitted with :func:`to_lean_fol` — exactly the
|
|
497
|
+
same reduction :func:`~unicode_logic_kit.hol.classical.to_isabelle_msfol` /
|
|
498
|
+
:func:`~unicode_logic_kit.hol.classical.to_thf_msfol` already use, so this
|
|
499
|
+
adds no new semantics. With ``include_sort_facts=True`` (default) the two
|
|
500
|
+
facts that reduction forgets are stated too
|
|
501
|
+
(:func:`~unicode_logic_kit.fol.nodes.sort_axioms`): every sort is non-empty
|
|
502
|
+
(``∃ x : Ind, S x``) and a sorted constant is in its sort (``Human socrates``
|
|
503
|
+
for ``Mortal(socrates:Human)``). For a theorem each is a named HYPOTHESIS of
|
|
504
|
+
it — ``theorem goal (sort_nonempty_0 : …) (sort_member_0 : …) : φ`` — never a
|
|
505
|
+
conjunct of the goal, which could not be proved even for a tautology ``φ``.
|
|
506
|
+
An asserted formula (``conjecture=False``) keeps the membership atoms as a
|
|
507
|
+
conjunct, since they are part of what is asserted, and gets the non-emptiness
|
|
508
|
+
facts as ``axiom`` lines. ``include_sort_facts=False`` is the bare
|
|
509
|
+
relativisation, no sort facts. All sorts share the single Lean type ``Ind`` —
|
|
510
|
+
the relativisation, not the type system, keeps the sorts apart. See
|
|
511
|
+
:func:`to_lean_fol` for ``native_equality`` / ``proof`` and for the free variable of an
|
|
512
|
+
asserted formula, which is refused by name.
|
|
513
|
+
|
|
514
|
+
Raises:
|
|
515
|
+
NotImplementedError: ``conjecture`` is false and ``formula`` has a free variable.
|
|
516
|
+
"""
|
|
517
|
+
plain, background = _msfol_split(formula, conjecture, include_sort_facts)
|
|
518
|
+
return _lean_problem(plain, conjecture, native_equality, proof, background,
|
|
519
|
+
where="to_lean_msfol")
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
# ===========================================================================
|
|
523
|
+
# (B) Propositional modal K -> Lean 4
|
|
524
|
+
# ===========================================================================
|
|
525
|
+
|
|
526
|
+
_MODAL_K_ALLOWED = (Atom, Not, And, Or, Xor, Implies, Iff, Box, Diamond)
|
|
527
|
+
_MODAL_GROUND_TERM = (Constant, Number, Function)
|
|
528
|
+
_MODAL_RESERVED = frozenset({"World", "World_nonempty", "R", "goal"})
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def _reject_non_propositional_modal(formula: Node) -> None:
|
|
532
|
+
"""Raise ``NotImplementedError`` naming the first node outside the
|
|
533
|
+
propositional modal-K fragment: ``Box``/``Diamond`` + classical
|
|
534
|
+
connectives over GROUND atoms only. A free/bound ``Variable`` (hence any
|
|
535
|
+
``Quantifier``), an agent-indexed modality (``Knows``/``Believes``/…), or
|
|
536
|
+
a deontic/temporal operator is rejected — the same node-type gate
|
|
537
|
+
:func:`~unicode_logic_kit.hol.isabelle_runner._is_alethic_propositional`
|
|
538
|
+
uses for the analogous Isabelle-side check.
|
|
539
|
+
"""
|
|
540
|
+
for n in formula.walk():
|
|
541
|
+
if isinstance(n, _MODAL_K_ALLOWED) or isinstance(n, _MODAL_GROUND_TERM):
|
|
542
|
+
continue
|
|
543
|
+
raise NotImplementedError(
|
|
544
|
+
f"to_lean_modal_k: {type(n).__name__} is outside the propositional "
|
|
545
|
+
"modal-K fragment (Box/Diamond + not/and/or/xor/implies/iff over "
|
|
546
|
+
"GROUND atoms only -- no quantifiers, free variables, agent-indexed "
|
|
547
|
+
"modalities Knows/Believes, or deontic/temporal operators)."
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
class _ModalAtomNames:
|
|
552
|
+
"""Map each DISTINCT ground atom (by its rendered Unicode key,
|
|
553
|
+
``key_text(atom)`` — the text with every constant written by its bare name, the same
|
|
554
|
+
key :func:`~unicode_logic_kit.semantics.kripke.satisfies_modal` uses) to a
|
|
555
|
+
unique Lean identifier stem, exactly the way :class:`_SymbolResolver`
|
|
556
|
+
de-collides FOL symbols: keyed on the ORIGINAL atom key, not the
|
|
557
|
+
sanitised string, so two semantically distinct atoms that happen to
|
|
558
|
+
sanitise to the same stem (``_sanitize`` is not injective — see its
|
|
559
|
+
docstring) still get distinct identifiers instead of silently colliding.
|
|
560
|
+
"""
|
|
561
|
+
|
|
562
|
+
def __init__(self):
|
|
563
|
+
self._used: set = set()
|
|
564
|
+
self._map: Dict[str, str] = {}
|
|
565
|
+
|
|
566
|
+
def ident(self, atom_key: str) -> str:
|
|
567
|
+
if atom_key in self._map:
|
|
568
|
+
return self._map[atom_key]
|
|
569
|
+
cand = dedupe(_sanitize(atom_key), self._used)
|
|
570
|
+
self._map[atom_key] = cand
|
|
571
|
+
return cand
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
def _lean_modal_body(node: Node, world: str, counter: List[int],
|
|
575
|
+
names: "_LeanNames", atoms: "_ModalAtomNames") -> str:
|
|
576
|
+
"""Render ``node``'s Kripke truth condition at the Lean world-variable ``world``.
|
|
577
|
+
|
|
578
|
+
``Box``/``Diamond`` allocate a FRESH bound-world name from ``counter``
|
|
579
|
+
(a single, monotonically increasing, whole-formula counter — never reused
|
|
580
|
+
across sibling branches either, so no nested scope can ever need to
|
|
581
|
+
reason about whether reuse was safe), rendering the standard clauses
|
|
582
|
+
``∀ v, R w v → ⟦φ⟧v`` / ``∃ v, R w v ∧ ⟦φ⟧v``.
|
|
583
|
+
"""
|
|
584
|
+
if isinstance(node, Atom):
|
|
585
|
+
if truth_value(node) is not None:
|
|
586
|
+
return "True" if truth_value(node) else "False"
|
|
587
|
+
atom_key = key_text(node)
|
|
588
|
+
ident = names.safe(("atom", atom_key), atoms.ident(atom_key))
|
|
589
|
+
return f"({ident} {world})"
|
|
590
|
+
if isinstance(node, Not):
|
|
591
|
+
return f"(¬ {_lean_modal_body(node.formula, world, counter, names, atoms)})"
|
|
592
|
+
if type(node) in _LEAN_BINOP:
|
|
593
|
+
op = _LEAN_BINOP[type(node)]
|
|
594
|
+
left = _lean_modal_body(node.left, world, counter, names, atoms)
|
|
595
|
+
right = _lean_modal_body(node.right, world, counter, names, atoms)
|
|
596
|
+
return f"({left} {op} {right})"
|
|
597
|
+
if isinstance(node, Xor):
|
|
598
|
+
left = _lean_modal_body(node.left, world, counter, names, atoms)
|
|
599
|
+
right = _lean_modal_body(node.right, world, counter, names, atoms)
|
|
600
|
+
return f"(¬ ({left} ↔ {right}))"
|
|
601
|
+
if isinstance(node, Box):
|
|
602
|
+
depth = counter[0]
|
|
603
|
+
counter[0] += 1
|
|
604
|
+
v = names.safe(("world", depth), f"w{depth}")
|
|
605
|
+
inner = _lean_modal_body(node.formula, v, counter, names, atoms)
|
|
606
|
+
return f"(∀ {v} : World, R {world} {v} → {inner})"
|
|
607
|
+
if isinstance(node, Diamond):
|
|
608
|
+
depth = counter[0]
|
|
609
|
+
counter[0] += 1
|
|
610
|
+
v = names.safe(("world", depth), f"w{depth}")
|
|
611
|
+
inner = _lean_modal_body(node.formula, v, counter, names, atoms)
|
|
612
|
+
return f"(∃ {v} : World, R {world} {v} ∧ {inner})"
|
|
613
|
+
raise NotImplementedError(
|
|
614
|
+
f"to_lean_modal_k: unsupported node {type(node).__name__} "
|
|
615
|
+
"(unreachable if _reject_non_propositional_modal ran first)."
|
|
616
|
+
)
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def to_lean_modal_k(formula: Node, conjecture: bool = True, proof: str = "sorry") -> str:
|
|
620
|
+
"""Emit a **Lean 4** shallow embedding of a propositional modal-**K** ``formula``.
|
|
621
|
+
|
|
622
|
+
Declares an uninterpreted, explicitly ``Nonempty`` Kripke world type
|
|
623
|
+
``World``, an UNCONSTRAINED accessibility relation ``R : World → World →
|
|
624
|
+
Prop`` (frame K — no ``refl``/``trans``/... axioms), and a ``World → Prop``
|
|
625
|
+
valuation ``axiom`` for every distinct ground atom in ``formula``. The
|
|
626
|
+
goal is the formula's validity — true at every world, over any ``R`` and
|
|
627
|
+
any valuation satisfying just those types — ``theorem goal : ∀ w, ⟦formula⟧w
|
|
628
|
+
:= by`` / `` <proof>`` (default ``proof="sorry"``, so the file always
|
|
629
|
+
elaborates without claiming a proof). ``proof`` is the tactic block body
|
|
630
|
+
(no leading ``by``), see :func:`to_lean_fol`. With ``conjecture=False``,
|
|
631
|
+
emits ``axiom goal : ∀ w, ⟦formula⟧w`` instead (no proof needed).
|
|
632
|
+
|
|
633
|
+
Only the propositional fragment is supported: ``Box``/``Diamond`` plus the
|
|
634
|
+
classical connectives over GROUND atoms (no quantifiers, agent-indexed
|
|
635
|
+
modalities, or deontic/temporal operators) — anything else raises
|
|
636
|
+
``NotImplementedError`` naming the construct (see module docstring for
|
|
637
|
+
the two-way validation of this encoding against
|
|
638
|
+
:func:`~unicode_logic_kit.semantics.kripke.satisfies_modal`). A numeral inside an atom is
|
|
639
|
+
the constant of its value (``P(1)`` and ``P(1.0)`` are one letter); a constant spelled
|
|
640
|
+
like it (``n1`` next to the number ``1``) is refused by name.
|
|
641
|
+
|
|
642
|
+
This function only emits the file; it does not run Lean — see
|
|
643
|
+
:func:`check_theory` / :func:`lean_decide_modal_k` for the optional live tier.
|
|
644
|
+
"""
|
|
645
|
+
_reject_non_propositional_modal(formula)
|
|
646
|
+
# An atom is a propositional letter keyed by its printed text, so ``P(1)`` and ``P(1.0)`` --
|
|
647
|
+
# one atom, a numeral is identified by its value -- would be two letters. Writing each numeral
|
|
648
|
+
# as the constant of its value gives them one text (and refuses a constant spelled like it).
|
|
649
|
+
[formula], _ = numerals_as_constants([formula], where="to_lean_modal_k",
|
|
650
|
+
spell=prefixed_numeral_name)
|
|
651
|
+
names = _LeanNames(_MODAL_RESERVED)
|
|
652
|
+
atoms = _ModalAtomNames()
|
|
653
|
+
|
|
654
|
+
seen: List[str] = []
|
|
655
|
+
seen_set = set()
|
|
656
|
+
for n in formula.walk():
|
|
657
|
+
if isinstance(n, Atom) and truth_value(n) is None:
|
|
658
|
+
key = key_text(n)
|
|
659
|
+
if key not in seen_set:
|
|
660
|
+
seen_set.add(key)
|
|
661
|
+
seen.append(key)
|
|
662
|
+
|
|
663
|
+
lines = [
|
|
664
|
+
"-- Propositional modal K embedded into Lean 4 core (no Mathlib): a shallow",
|
|
665
|
+
"-- Kripke embedding faithful to",
|
|
666
|
+
"-- unicode_logic_kit.semantics.kripke.satisfies_modal restricted to the",
|
|
667
|
+
"-- propositional fragment, over frame K (R is UNCONSTRAINED -- no frame",
|
|
668
|
+
"-- conditions). World is EXPLICITLY NONEMPTY, same reasoning as `Ind` in",
|
|
669
|
+
"-- the classical FOL export (see hol.lean's module docstring).",
|
|
670
|
+
"",
|
|
671
|
+
"axiom World : Type",
|
|
672
|
+
"axiom World_nonempty : Nonempty World",
|
|
673
|
+
"instance : Nonempty World := World_nonempty",
|
|
674
|
+
"",
|
|
675
|
+
"open Classical",
|
|
676
|
+
"",
|
|
677
|
+
"axiom R : World → World → Prop",
|
|
678
|
+
"",
|
|
679
|
+
]
|
|
680
|
+
decls = [f"axiom {names.safe(('atom', key), atoms.ident(key))} : World → Prop"
|
|
681
|
+
for key in seen]
|
|
682
|
+
if decls:
|
|
683
|
+
lines += decls
|
|
684
|
+
lines.append("")
|
|
685
|
+
|
|
686
|
+
counter = [1]
|
|
687
|
+
w0 = names.safe(("world", 0), "w0")
|
|
688
|
+
body = _lean_modal_body(formula, w0, counter, names, atoms)
|
|
689
|
+
if conjecture:
|
|
690
|
+
lines.append(f"theorem goal : ∀ {w0} : World, {body} := by")
|
|
691
|
+
lines.extend(f" {pl}" for pl in proof.split("\n"))
|
|
692
|
+
else:
|
|
693
|
+
lines.append(f"axiom goal : ∀ {w0} : World, {body}")
|
|
694
|
+
return "\n".join(lines) + "\n"
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
# ===========================================================================
|
|
698
|
+
# (C) Optional LIVE tier: elaborate / prove through a local Lean 4 toolchain
|
|
699
|
+
# ===========================================================================
|
|
700
|
+
|
|
701
|
+
class LeanNotAvailable(RuntimeError):
|
|
702
|
+
"""Raised when a Lean 4 install is required but none could be located."""
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
@dataclass(frozen=True)
|
|
706
|
+
class LeanInstall:
|
|
707
|
+
"""A located Lean 4 installation."""
|
|
708
|
+
|
|
709
|
+
lean_exe: str # path to the `lean` binary itself
|
|
710
|
+
home: str # its containing toolchain/bin directory
|
|
711
|
+
version: Optional[str] = None
|
|
712
|
+
|
|
713
|
+
def __str__(self) -> str:
|
|
714
|
+
return f"Lean({self.version or '?'} at {self.lean_exe})"
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
def _lean_version(exe: str) -> Optional[str]:
|
|
718
|
+
"""Return ``lean --version``'s version number, or ``None`` if it fails."""
|
|
719
|
+
try:
|
|
720
|
+
proc = subprocess.run(
|
|
721
|
+
[exe, "--version"], capture_output=True, text=True,
|
|
722
|
+
encoding="utf-8", errors="replace", timeout=10,
|
|
723
|
+
)
|
|
724
|
+
except Exception:
|
|
725
|
+
return None
|
|
726
|
+
if proc.returncode != 0:
|
|
727
|
+
return None
|
|
728
|
+
m = re.search(r"\d+\.\d+\.\d+", proc.stdout or "")
|
|
729
|
+
return m.group(0) if m else (proc.stdout or "").strip() or None
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
def _validate_lean(path: Optional[str]) -> Optional[LeanInstall]:
|
|
733
|
+
"""Turn a candidate path (a `lean` executable, or a dir containing one,
|
|
734
|
+
directly or under `bin/`) into a :class:`LeanInstall`, or ``None``."""
|
|
735
|
+
if not path:
|
|
736
|
+
return None
|
|
737
|
+
ext = ".exe" if os.name == "nt" else ""
|
|
738
|
+
candidates = [path] if os.path.isfile(path) else [
|
|
739
|
+
os.path.join(path, "lean" + ext),
|
|
740
|
+
os.path.join(path, "bin", "lean" + ext),
|
|
741
|
+
]
|
|
742
|
+
for exe in candidates:
|
|
743
|
+
if os.path.isfile(exe):
|
|
744
|
+
exe = os.path.abspath(exe)
|
|
745
|
+
home = os.path.dirname(exe)
|
|
746
|
+
return LeanInstall(lean_exe=exe, home=home, version=_lean_version(exe))
|
|
747
|
+
return None
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
_FIND_CACHE: dict = {}
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
def find_lean(lean_home: Optional[str] = None, *, use_cache: bool = True) -> Optional[LeanInstall]:
|
|
754
|
+
"""Locate a Lean 4 installation, or return ``None`` if none is found.
|
|
755
|
+
|
|
756
|
+
Search order: ``lean_home`` argument (a `lean` executable, or a directory
|
|
757
|
+
containing one directly or under ``bin/``) -> env ``UFK_LEAN_HOME`` ->
|
|
758
|
+
``lean`` on ``PATH`` -> the standard ``elan`` bin directory
|
|
759
|
+
``~/.elan/bin`` (found even when ``elan-init`` was run with
|
|
760
|
+
``--no-modify-path``, i.e. nothing was put on ``PATH``). The result is
|
|
761
|
+
cached (keyed by the explicit argument); pass ``use_cache=False`` to
|
|
762
|
+
force a fresh lookup.
|
|
763
|
+
"""
|
|
764
|
+
key = lean_home or ""
|
|
765
|
+
if use_cache and key in _FIND_CACHE:
|
|
766
|
+
return _FIND_CACHE[key]
|
|
767
|
+
|
|
768
|
+
candidates: List[str] = []
|
|
769
|
+
if lean_home:
|
|
770
|
+
candidates.append(lean_home)
|
|
771
|
+
v = os.environ.get("UFK_LEAN_HOME")
|
|
772
|
+
if v:
|
|
773
|
+
candidates.append(v)
|
|
774
|
+
exe = shutil.which("lean")
|
|
775
|
+
if exe:
|
|
776
|
+
candidates.append(exe)
|
|
777
|
+
ext = ".exe" if os.name == "nt" else ""
|
|
778
|
+
candidates.append(os.path.join(os.path.expanduser("~"), ".elan", "bin", "lean" + ext))
|
|
779
|
+
|
|
780
|
+
found: Optional[LeanInstall] = None
|
|
781
|
+
seen = set()
|
|
782
|
+
for c in candidates:
|
|
783
|
+
ac = os.path.abspath(c) if c else c
|
|
784
|
+
if ac in seen:
|
|
785
|
+
continue
|
|
786
|
+
seen.add(ac)
|
|
787
|
+
inst = _validate_lean(c)
|
|
788
|
+
if inst:
|
|
789
|
+
found = inst
|
|
790
|
+
break
|
|
791
|
+
if use_cache:
|
|
792
|
+
_FIND_CACHE[key] = found
|
|
793
|
+
return found
|
|
794
|
+
|
|
795
|
+
|
|
796
|
+
def lean_available(lean_home: Optional[str] = None) -> bool:
|
|
797
|
+
"""``True`` iff a Lean 4 installation can be located (cheap; cached)."""
|
|
798
|
+
return find_lean(lean_home) is not None
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
_SORRY_RE = re.compile(r"declaration uses[^\n]*sorry")
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
@dataclass
|
|
805
|
+
class LeanBuildResult:
|
|
806
|
+
"""Outcome of elaborating one self-contained ``.lean`` file.
|
|
807
|
+
|
|
808
|
+
``ok`` is ``True`` iff ``lean`` exits 0 — the file **elaborates** (every
|
|
809
|
+
declaration type-checks), which is true for a ``sorry``-closed goal too.
|
|
810
|
+
``uses_sorry`` is set when the (successful) elaboration warned that a
|
|
811
|
+
declaration used ``sorry`` — Lean's own signal that a "proof" is not one.
|
|
812
|
+
:attr:`proved` (and truthiness) is the honest, stronger claim: elaborates
|
|
813
|
+
AND is genuinely sorry-free.
|
|
814
|
+
"""
|
|
815
|
+
|
|
816
|
+
ok: bool
|
|
817
|
+
exit_code: int
|
|
818
|
+
output: str
|
|
819
|
+
theory_name: str
|
|
820
|
+
elapsed: float
|
|
821
|
+
uses_sorry: bool = False
|
|
822
|
+
|
|
823
|
+
@property
|
|
824
|
+
def proved(self) -> bool:
|
|
825
|
+
return self.ok and not self.uses_sorry
|
|
826
|
+
|
|
827
|
+
def __bool__(self) -> bool:
|
|
828
|
+
return self.proved
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
def check_theory(lean_source: str, theory_name: str, *,
|
|
832
|
+
install: Optional[LeanInstall] = None,
|
|
833
|
+
timeout: float = 120.0,
|
|
834
|
+
keep: bool = False) -> LeanBuildResult:
|
|
835
|
+
"""Elaborate one self-contained Lean 4 source file and report the outcome.
|
|
836
|
+
|
|
837
|
+
Writes ``lean_source`` to ``<theory_name>.lean`` in a scratch directory
|
|
838
|
+
and runs bare ``lean <file>.lean`` on it (no ``lakefile``/project needed:
|
|
839
|
+
the classical-FOL and modal-K fragments this module emits use only Lean 4
|
|
840
|
+
core + ``Classical``, confirmed by hand against a real toolchain — see
|
|
841
|
+
module docstring). ``ok`` is ``True`` iff the process exits 0.
|
|
842
|
+
|
|
843
|
+
Args:
|
|
844
|
+
lean_source: the full file text (as :func:`to_lean_fol` et al. emit).
|
|
845
|
+
theory_name: the file's base name (without ``.lean``) — also used as
|
|
846
|
+
a scratch-directory-local label.
|
|
847
|
+
install: a :class:`LeanInstall`; located via :func:`find_lean` when
|
|
848
|
+
``None``.
|
|
849
|
+
timeout: wall-clock subprocess timeout, in seconds.
|
|
850
|
+
keep: keep the scratch directory (for debugging) instead of deleting it.
|
|
851
|
+
|
|
852
|
+
Raises:
|
|
853
|
+
LeanNotAvailable: if no Lean 4 installation can be located.
|
|
854
|
+
"""
|
|
855
|
+
install = install or find_lean()
|
|
856
|
+
if install is None:
|
|
857
|
+
raise LeanNotAvailable(
|
|
858
|
+
"No Lean 4 installation found. Set UFK_LEAN_HOME, put `lean` on PATH, "
|
|
859
|
+
"or install elan (https://leanprover-community.github.io/get_started.html) "
|
|
860
|
+
"with `elan-init --no-modify-path -y --default-toolchain stable`.")
|
|
861
|
+
|
|
862
|
+
work = tempfile.mkdtemp(prefix="ufk_lean_")
|
|
863
|
+
try:
|
|
864
|
+
path = os.path.join(work, theory_name + ".lean")
|
|
865
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
866
|
+
f.write(lean_source)
|
|
867
|
+
t0 = time.perf_counter()
|
|
868
|
+
try:
|
|
869
|
+
proc = subprocess.run(
|
|
870
|
+
[install.lean_exe, path], capture_output=True, text=True,
|
|
871
|
+
encoding="utf-8", errors="replace", timeout=timeout, cwd=work,
|
|
872
|
+
)
|
|
873
|
+
code = proc.returncode
|
|
874
|
+
output = (proc.stdout or "") + (proc.stderr or "")
|
|
875
|
+
except subprocess.TimeoutExpired as e:
|
|
876
|
+
out = (e.stdout or "") + (e.stderr or "")
|
|
877
|
+
if isinstance(out, bytes):
|
|
878
|
+
out = out.decode("utf-8", "replace")
|
|
879
|
+
code, output = 124, out + f"\n[runner] wall-clock timeout after {timeout}s\n"
|
|
880
|
+
elapsed = time.perf_counter() - t0
|
|
881
|
+
return LeanBuildResult(
|
|
882
|
+
ok=(code == 0), exit_code=code, output=output, theory_name=theory_name,
|
|
883
|
+
elapsed=elapsed, uses_sorry=bool(_SORRY_RE.search(output)),
|
|
884
|
+
)
|
|
885
|
+
finally:
|
|
886
|
+
if not keep:
|
|
887
|
+
shutil.rmtree(work, ignore_errors=True)
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
# --------------------------------------------------------------------------- #
|
|
891
|
+
# Deciding validity through Lean: a small, honest tactic battery.
|
|
892
|
+
# --------------------------------------------------------------------------- #
|
|
893
|
+
|
|
894
|
+
VALID = "valid"
|
|
895
|
+
UNKNOWN = "unknown"
|
|
896
|
+
|
|
897
|
+
# `decide` is Lean 4 core (only closes a goal with a synthesizable Decidable
|
|
898
|
+
# instance -- the axiom-based embeddings above are intentionally NOT
|
|
899
|
+
# decidable, see module docstring). `tauto` / `aesop` are MATHLIB tactics:
|
|
900
|
+
# without Mathlib installed they fail to PARSE ("unknown tactic"), which
|
|
901
|
+
# check_theory reports as ok=False like any other failed attempt -- the
|
|
902
|
+
# battery just moves on, never a crash, never a false VALID.
|
|
903
|
+
DEFAULT_METHODS: Tuple[str, ...] = ("decide", "tauto", "aesop")
|
|
904
|
+
|
|
905
|
+
|
|
906
|
+
@dataclass
|
|
907
|
+
class LeanVerdict:
|
|
908
|
+
"""Result of trying to prove a formula's validity through a Lean tactic battery.
|
|
909
|
+
|
|
910
|
+
``status`` is ``"valid"`` (some method closed the goal with a genuinely
|
|
911
|
+
sorry-free proof) or ``"unknown"`` (no method in the battery did, within
|
|
912
|
+
budget) — there is no ``"invalid"`` here (see module docstring): this
|
|
913
|
+
battery can certify a proof but not construct a countermodel, so
|
|
914
|
+
"unknown" is the only honest outcome for a formula it cannot close,
|
|
915
|
+
valid or not.
|
|
916
|
+
"""
|
|
917
|
+
|
|
918
|
+
status: str
|
|
919
|
+
method: Optional[str] = None
|
|
920
|
+
output: str = ""
|
|
921
|
+
elapsed: float = 0.0
|
|
922
|
+
|
|
923
|
+
@property
|
|
924
|
+
def is_valid(self) -> bool:
|
|
925
|
+
return self.status == VALID
|
|
926
|
+
|
|
927
|
+
@property
|
|
928
|
+
def is_unknown(self) -> bool:
|
|
929
|
+
return self.status == UNKNOWN
|
|
930
|
+
|
|
931
|
+
def __bool__(self) -> bool:
|
|
932
|
+
return self.status == VALID
|
|
933
|
+
|
|
934
|
+
def __str__(self) -> str:
|
|
935
|
+
extra = f" (by {self.method})" if self.status == VALID and self.method else ""
|
|
936
|
+
return f"LeanVerdict[{self.status}{extra}]"
|
|
937
|
+
|
|
938
|
+
|
|
939
|
+
def _run_battery(emit, methods: Sequence[str], timeout: float,
|
|
940
|
+
install: Optional[LeanInstall]) -> LeanVerdict:
|
|
941
|
+
install = install or find_lean()
|
|
942
|
+
if install is None:
|
|
943
|
+
raise LeanNotAvailable(
|
|
944
|
+
"No Lean 4 installation found. Set UFK_LEAN_HOME, put `lean` on PATH, "
|
|
945
|
+
"or install elan (https://leanprover-community.github.io/get_started.html).")
|
|
946
|
+
for m in methods:
|
|
947
|
+
tok = "G" + uuid.uuid4().hex[:8]
|
|
948
|
+
src = emit(proof=m)
|
|
949
|
+
r = check_theory(src, tok, install=install, timeout=timeout)
|
|
950
|
+
if r.proved:
|
|
951
|
+
return LeanVerdict(status=VALID, method=m, output=r.output, elapsed=r.elapsed)
|
|
952
|
+
return LeanVerdict(status=UNKNOWN)
|
|
953
|
+
|
|
954
|
+
|
|
955
|
+
def lean_decide_fol(
|
|
956
|
+
formula: Node, *,
|
|
957
|
+
msfol: bool = False,
|
|
958
|
+
native_equality: bool = False,
|
|
959
|
+
methods: Sequence[str] = DEFAULT_METHODS,
|
|
960
|
+
timeout: float = 60.0,
|
|
961
|
+
install: Optional[LeanInstall] = None,
|
|
962
|
+
) -> LeanVerdict:
|
|
963
|
+
"""Try to prove a classical FOL (or MSFOL) formula's validity via a Lean tactic battery.
|
|
964
|
+
|
|
965
|
+
Emits :func:`to_lean_fol` (or :func:`to_lean_msfol` when ``msfol=True``)
|
|
966
|
+
once per method in ``methods``, each time with that method as the tactic
|
|
967
|
+
(``proof=method``), and returns the first genuinely sorry-free success as
|
|
968
|
+
:data:`VALID` — or :data:`UNKNOWN` if none of them close it
|
|
969
|
+
(see :class:`LeanVerdict` and the module docstring for why there is no
|
|
970
|
+
``INVALID`` outcome, and why ``UNKNOWN`` is the expected result in a
|
|
971
|
+
Mathlib-free environment for most nontrivial formulas).
|
|
972
|
+
|
|
973
|
+
Args:
|
|
974
|
+
formula: the FOL AST node.
|
|
975
|
+
msfol: emit the many-sorted embedding instead of plain FOL.
|
|
976
|
+
native_equality: render ``=`` / ``≠`` as Lean identity instead of the
|
|
977
|
+
uninterpreted ``feq`` / ``fneq``.
|
|
978
|
+
methods / timeout / install: as for :func:`check_theory`; ``methods``
|
|
979
|
+
defaults to :data:`DEFAULT_METHODS`.
|
|
980
|
+
|
|
981
|
+
Raises:
|
|
982
|
+
LeanNotAvailable: if no Lean 4 installation can be located.
|
|
983
|
+
"""
|
|
984
|
+
emit_fn = to_lean_msfol if msfol else to_lean_fol
|
|
985
|
+
|
|
986
|
+
def emit(proof: str) -> str:
|
|
987
|
+
return emit_fn(formula, native_equality=native_equality, proof=proof)
|
|
988
|
+
|
|
989
|
+
return _run_battery(emit, methods, timeout, install)
|
|
990
|
+
|
|
991
|
+
|
|
992
|
+
def lean_decide_modal_k(
|
|
993
|
+
formula: Node, *,
|
|
994
|
+
methods: Sequence[str] = DEFAULT_METHODS,
|
|
995
|
+
timeout: float = 60.0,
|
|
996
|
+
install: Optional[LeanInstall] = None,
|
|
997
|
+
) -> LeanVerdict:
|
|
998
|
+
"""Try to prove a propositional modal-K formula's validity via a Lean tactic battery.
|
|
999
|
+
|
|
1000
|
+
Emits :func:`to_lean_modal_k` once per method in ``methods`` and returns
|
|
1001
|
+
the first genuinely sorry-free success as :data:`VALID`, else
|
|
1002
|
+
:data:`UNKNOWN` — exactly like :func:`lean_decide_fol` (see there, and
|
|
1003
|
+
the module docstring, for why there is no ``INVALID`` outcome).
|
|
1004
|
+
|
|
1005
|
+
Args:
|
|
1006
|
+
formula: the modal AST node (propositional fragment only — see
|
|
1007
|
+
:func:`to_lean_modal_k`).
|
|
1008
|
+
methods / timeout / install: as for :func:`lean_decide_fol`.
|
|
1009
|
+
|
|
1010
|
+
Raises:
|
|
1011
|
+
LeanNotAvailable: if no Lean 4 installation can be located.
|
|
1012
|
+
NotImplementedError: propagated from :func:`to_lean_modal_k` for a
|
|
1013
|
+
construct outside the propositional modal-K fragment.
|
|
1014
|
+
"""
|
|
1015
|
+
def emit(proof: str) -> str:
|
|
1016
|
+
return to_lean_modal_k(formula, proof=proof)
|
|
1017
|
+
|
|
1018
|
+
return _run_battery(emit, methods, timeout, install)
|