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,1135 @@
|
|
|
1
|
+
"""CASL text emission for classical FOL and many-sorted FOL (MSFOL).
|
|
2
|
+
|
|
3
|
+
`CASL <https://en.wikipedia.org/wiki/Common_Algebraic_Specification_Language>`_
|
|
4
|
+
(the Common Algebraic Specification Language) is the CoFI family's central
|
|
5
|
+
ASCII, keyword-based specification language; ``libraries`` of CASL ``spec``s
|
|
6
|
+
are what `Hets <https://github.com/spechub/Hets>`_ (the Heterogeneous Tool
|
|
7
|
+
Set) parses, structures, and hands off to first-order/HOL back-ends. This
|
|
8
|
+
module is the EXPORTER (kit AST -> CASL text): it turns a batch of
|
|
9
|
+
the kit's classical FOL / MSFOL formulas into a complete, Hets-parsable
|
|
10
|
+
``spec ... end`` block, or a bare formula fragment for embedding in
|
|
11
|
+
hand-written CASL. The other direction, for the text this module writes, is
|
|
12
|
+
:func:`unicode_logic_kit.fol.casl_import.parse_casl_spec`.
|
|
13
|
+
|
|
14
|
+
Scope: classical, two-valued FOL and many-sorted FOL only
|
|
15
|
+
-----------------------------------------------------------
|
|
16
|
+
CASL's basic first-order layer has no counting quantifiers, no modal/temporal
|
|
17
|
+
operators, no second-order quantification, and no substructural connectives —
|
|
18
|
+
so this exporter accepts exactly the classical fragment those constructs sit
|
|
19
|
+
outside of: :class:`~unicode_logic_kit.fol.nodes.Atom`, :class:`Not`,
|
|
20
|
+
:class:`And`, :class:`Or`, :class:`Xor`, :class:`Implies`, :class:`Iff`,
|
|
21
|
+
:class:`Quantifier`, :class:`SortedQuantifier` on the formula side, and
|
|
22
|
+
:class:`Variable`, :class:`Constant`, :class:`SortedConstant`,
|
|
23
|
+
:class:`Function` on the term side. Every OTHER node class the kit knows about
|
|
24
|
+
(modal/temporal/epistemic operators, :class:`Count`/:class:`Measure`/
|
|
25
|
+
:class:`Cardinality`/:class:`Contrast`/:class:`SortedCount`/
|
|
26
|
+
:class:`SortedCardinality`, :class:`Number`, second-order quantifiers,
|
|
27
|
+
Lambda/Application, and the linear/Lambek/team-semantic node families) is
|
|
28
|
+
REFUSED with a loud :class:`NotImplementedError` naming the offending node
|
|
29
|
+
class, per the kit's honesty convention (an unsupported construct is a
|
|
30
|
+
precise, actionable error — never a silent mistranslation or a weaker result
|
|
31
|
+
than what the input actually says). :func:`_check_fragment` implements this
|
|
32
|
+
gate by class-NAME membership in :data:`_ALLOWED_CASL_NODES` rather than an
|
|
33
|
+
``isinstance`` chain over dozens of unrelated classes scattered across the
|
|
34
|
+
kit's node modules — the gate is meant to reject everything not explicitly
|
|
35
|
+
allow-listed, and a name-set is the form that stays correct (rejects) when a
|
|
36
|
+
new node class is added elsewhere in the kit and nobody updates this module.
|
|
37
|
+
A richer bridge for one non-classical logic — quantified modal logic — DOES
|
|
38
|
+
exist, and goes through DOL (the Distributed Ontology, Modelling and
|
|
39
|
+
Specification Language, CoFI's heterogeneous layer over CASL/OWL/etc.)
|
|
40
|
+
rather than a native "logic Modal" CASL institution:
|
|
41
|
+
:func:`unicode_logic_kit.fol.qml.qml_validity_formula` already lowers a modal
|
|
42
|
+
formula to exactly this module's classical fragment (the shallow, first-order
|
|
43
|
+
"standard translation" — see that module's own docstring), and
|
|
44
|
+
:func:`unicode_logic_kit.hets.dol.to_dol_library_from_modal` composes that
|
|
45
|
+
translation with THIS module's :func:`to_casl_spec` (sanitising ``qml``'s
|
|
46
|
+
auto-generated identifiers first, since those are not always legal CASL) to
|
|
47
|
+
hand the result to Hets as a DOL library. Identity needs no translation on
|
|
48
|
+
that path any more: ``qml`` keeps an ``=`` atom RIGID and exactly 2-ary — no
|
|
49
|
+
world argument — which is precisely CASL's own built-in ``=``, and lowers
|
|
50
|
+
``≠`` to ``¬(=)``. The alias that used to rename a world-relativized,
|
|
51
|
+
ternary ``=``/``≠`` to a fresh uninterpreted predicate is gone with the
|
|
52
|
+
behaviour that produced it; a hand-built non-binary ``=`` is passed through
|
|
53
|
+
and refused by this module's own arity check, and a ``≠`` atom is refused by
|
|
54
|
+
``hets.dol``'s sanitiser. This module's OWN
|
|
55
|
+
fragment gate — and its own exactly-2-ary ``=`` check — is unaffected: it
|
|
56
|
+
still refuses every non-classical node, and every malformed equality atom,
|
|
57
|
+
it always refused (see :func:`_check_fragment` and :func:`_infer_formula`);
|
|
58
|
+
the bridge works by translating the modal formula down to this fragment
|
|
59
|
+
BEFORE it ever reaches here, not by widening what this exporter itself
|
|
60
|
+
accepts.
|
|
61
|
+
|
|
62
|
+
``Number`` is deliberately EXCLUDED even though it is a term the classical
|
|
63
|
+
grammar can trivially produce (``NUMBER`` is a term-layer alternative), because
|
|
64
|
+
CASL's basic library has no built-in numeric sort — a bare literal such as
|
|
65
|
+
``3`` would need an ad hoc ``Nat``/``Int`` structuring this module does not
|
|
66
|
+
attempt, so it is refused rather than silently emitted as an undeclared term.
|
|
67
|
+
|
|
68
|
+
Sort inference: a union-find over symbol-position "slots"
|
|
69
|
+
-------------------------------------------------------------
|
|
70
|
+
A CASL ``ops``/``preds`` block declares the ARGUMENT SORTS of every operation
|
|
71
|
+
and predicate symbol — but the kit's classical (unsorted) :class:`Atom` /
|
|
72
|
+
:class:`Function` / :class:`Constant` carry no sort annotations at all, and
|
|
73
|
+
even :class:`SortedQuantifier` only annotates the BOUND VARIABLE, not every
|
|
74
|
+
symbol that variable is later passed to. So before any declaration can be
|
|
75
|
+
written, this module infers a single concrete sort for every symbol POSITION
|
|
76
|
+
that appears anywhere across the axioms and conjectures being exported, by
|
|
77
|
+
propagating the sort annotations that ARE present (:class:`SortedQuantifier`,
|
|
78
|
+
:class:`SortedConstant`) through every place a term is passed as an argument.
|
|
79
|
+
|
|
80
|
+
The inference is a classic union-find (disjoint-set) over a universe of
|
|
81
|
+
members of two kinds:
|
|
82
|
+
|
|
83
|
+
* a SLOT — one of ``("pred", name, i)`` (argument ``i`` of predicate
|
|
84
|
+
``name``), ``("func", name, i)`` (argument ``i`` of function ``name``),
|
|
85
|
+
``("func_result", name)`` (the sort of what ``name(...)`` itself denotes as
|
|
86
|
+
a term), or ``("const", name)`` (a constant symbol's own sort);
|
|
87
|
+
* a concrete SORT NAME (a plain string) — every string used as a sort
|
|
88
|
+
anywhere (a :class:`SortedQuantifier` sort, a :class:`SortedConstant` sort,
|
|
89
|
+
or ``default_sort``) acts as its own permanently-rooted anchor.
|
|
90
|
+
|
|
91
|
+
Each formula is walked with a variable environment (``name -> concrete sort``)
|
|
92
|
+
built up as quantifiers are entered — :class:`SortedQuantifier` binds its
|
|
93
|
+
variable to its own (concrete) sort; a plain (unsorted) :class:`Quantifier`
|
|
94
|
+
binds its variable to ``default_sort``; shadowing is scope-correct because the
|
|
95
|
+
environment is threaded functionally (a fresh dict per binder, never mutated
|
|
96
|
+
in place). Every place two term positions must agree in sort contributes ONE
|
|
97
|
+
union: a non-equality :class:`Atom`'s argument ``i`` unifies
|
|
98
|
+
``("pred", name, i)`` with that argument's own slot; an equality
|
|
99
|
+
:class:`Atom` unifies its two arguments' slots directly with each other; a
|
|
100
|
+
:class:`Function`'s argument ``i`` unifies ``("func", name, i)`` with that
|
|
101
|
+
argument's slot, and the :class:`Function` application ITSELF (when it occurs
|
|
102
|
+
as someone else's argument) contributes the slot ``("func_result", name)``; a
|
|
103
|
+
:class:`Variable`'s slot is simply its environment sort (already concrete); a
|
|
104
|
+
:class:`Constant`'s slot is ``("const", name)``; a :class:`SortedConstant`
|
|
105
|
+
unifies ``("const", name)`` with its own concrete sort immediately.
|
|
106
|
+
|
|
107
|
+
After every formula has been walked, each union-find equivalence class is
|
|
108
|
+
resolved to a single sort: a class containing a concrete sort name uses it (a
|
|
109
|
+
class can never legitimately contain TWO DIFFERENT concrete names — that
|
|
110
|
+
situation is a genuine sort conflict and is refused immediately, at the
|
|
111
|
+
``union`` call that would create it, with a :class:`ValueError` naming both
|
|
112
|
+
conflicting sorts and the symbol/position being unified); a class with no
|
|
113
|
+
concrete member at all (a symbol never connected, directly or transitively,
|
|
114
|
+
to any sort annotation) resolves to ``default_sort``. :class:`_UnionFind`
|
|
115
|
+
keeps concrete-sort roots as an INVARIANT (a string member is always its
|
|
116
|
+
class's root, never reparented under a slot-tuple root), which is what makes
|
|
117
|
+
conflict detection a plain root-equality check rather than a separate
|
|
118
|
+
per-class bookkeeping pass.
|
|
119
|
+
|
|
120
|
+
This is the TYPED reading, and it is stronger than the kit's own. The kit has ONE
|
|
121
|
+
universe, a sort is a (non-empty) subset of it, a constant written ``c:S`` is in
|
|
122
|
+
``S``, and an unannotated constant, an unsorted variable and the value of a function
|
|
123
|
+
are ANY element of the universe: a function has no declared result sort. The
|
|
124
|
+
inference above declares an unannotated constant or a function value at the sort of
|
|
125
|
+
the position it is used in (``∀x:Human Mortal(x), Mortal(socrates)`` declares
|
|
126
|
+
``socrates : Human``), and CASL's sorts are disjoint, so an unsorted quantifier
|
|
127
|
+
(typed with ``default_sort``) bounds a sort of its own rather than the universe the
|
|
128
|
+
other sorts are subsets of. An export into a typed language keeps writing that
|
|
129
|
+
reading, as a designed feature (the round trip through
|
|
130
|
+
:mod:`~unicode_logic_kit.fol.casl_import` and the typed worked examples rest on it); a
|
|
131
|
+
caller that DECIDES with the text must not take its answer for the kit's. The check that says when the
|
|
132
|
+
two readings differ is :func:`~unicode_logic_kit.atp.tptp_tff.check_typed_reading`
|
|
133
|
+
(shared with the TF0 and NXF writers), and
|
|
134
|
+
:class:`~unicode_logic_kit.atp.hets_backend.HetsBackend` applies it before it asks
|
|
135
|
+
Hets, answering ``unknown`` / ``unsupported`` for such a problem.
|
|
136
|
+
|
|
137
|
+
**The default sort is a sort of its own.** ``default_sort`` (``Thing``) is the sort of every
|
|
138
|
+
unsorted quantifier and of every position no annotation reaches. A sort of the SAME name
|
|
139
|
+
that the formulas write (``∀x:Thing``, ``c:Thing``) or ``subsorts`` declares would be one
|
|
140
|
+
sort with it, so an unsorted position would silently be a position of the user's sort:
|
|
141
|
+
``∀x:Thing P(x) ⊢ ∀y P(y)`` would be proved, which the kit's reading (a sort is a part of
|
|
142
|
+
the universe) does not say. When the default sort is used at all (an unsorted quantifier,
|
|
143
|
+
or a symbol connected to no annotation) and a sort of that name is also written, the
|
|
144
|
+
export is refused by name, and the refusal names the keyword that picks another default
|
|
145
|
+
sort: ``default_sort=`` of :func:`to_casl_spec` and :func:`formula_to_casl`
|
|
146
|
+
(``DolSpec.default_sort`` in :mod:`~unicode_logic_kit.hets.dol`).
|
|
147
|
+
:class:`~unicode_logic_kit.atp.hets_backend.HetsBackend` picks a default sort that no sort
|
|
148
|
+
of the problem has, so it never meets the refusal.
|
|
149
|
+
|
|
150
|
+
Two further checks ride along the same walk, both refusals the kit's honesty
|
|
151
|
+
convention requires rather than a mistranslation: a predicate or function
|
|
152
|
+
name used at two DIFFERENT arities across the exported formulas (CASL has no
|
|
153
|
+
notion of a single symbol overloaded across arities in this module's simple
|
|
154
|
+
model) raises :class:`ValueError`; and a name used both as a bare
|
|
155
|
+
:class:`Constant`/:class:`SortedConstant` and as a :class:`Function`
|
|
156
|
+
application raises :class:`ValueError` (this module declares at most one
|
|
157
|
+
``ops`` entry per name, so the two uses cannot be reconciled). A free
|
|
158
|
+
:class:`Variable` — one never bound by any enclosing quantifier — also
|
|
159
|
+
raises :class:`ValueError`: CASL ``. <formula>`` axioms must be closed, so
|
|
160
|
+
there is no implicit universal closure or variable declaration to fall back
|
|
161
|
+
on here.
|
|
162
|
+
|
|
163
|
+
Bound variables and the symbols of the spec
|
|
164
|
+
----------------------------------------------
|
|
165
|
+
CASL text writes a bound variable, a constant (a 0-ary operation) and a predicate as
|
|
166
|
+
one identifier, compared exactly (``w`` and ``W`` are two names), and inside a
|
|
167
|
+
quantifier the name of its variable is the variable: ``forall w : Thing . (P(w) =>
|
|
168
|
+
Q(w))`` next to ``ops w : Thing`` says nothing about a constant ``w``, whatever the
|
|
169
|
+
formula was. So a bound variable that is spelled like ANY symbol of the whole spec (a
|
|
170
|
+
constant, a function, a predicate, a sort, the default sort) is renamed to a fresh
|
|
171
|
+
variable name, fresh against every name the formulas hold, in its quantifier and in
|
|
172
|
+
every occurrence it binds (``forall w0 : Thing . (P(w0) => Q(w))``). Text without such
|
|
173
|
+
a clash is byte-identical to what it was before. The renaming is an alpha-conversion
|
|
174
|
+
of a closed sentence, so the text means the formula; read back through
|
|
175
|
+
:func:`~unicode_logic_kit.fol.casl_import.parse_casl_spec` it is the same formula up to
|
|
176
|
+
the names of the variables that had to be renamed. :func:`to_casl_spec` and
|
|
177
|
+
:func:`formula_to_casl` take ``visible_symbols`` for the symbols the text meets
|
|
178
|
+
without declaring them.
|
|
179
|
+
|
|
180
|
+
Reserved words and identifier hygiene
|
|
181
|
+
----------------------------------------
|
|
182
|
+
CASL keywords (``sort``, ``pred``, ``forall``, ``not``, …; the exact list is
|
|
183
|
+
:data:`_CASL_KEYWORDS`, taken from the CASL Reference Manual's lexical
|
|
184
|
+
grammar) may not be used as ordinary identifiers. This module checks every
|
|
185
|
+
CONSTANT, FUNCTION, and VARIABLE name it emits against that list (variables
|
|
186
|
+
are checked too, even though the kit's grammar restricts a variable to a
|
|
187
|
+
single lowercase letter optionally followed by digits — ``[a-z][0-9]*`` — so
|
|
188
|
+
in practice a variable can never collide with any of the (all
|
|
189
|
+
two-or-more-character) CASL keywords; the check runs anyway, both for
|
|
190
|
+
uniformity with the constant/function path and as a safety net if the kit's
|
|
191
|
+
variable grammar or the keyword list ever changes) and refuses with
|
|
192
|
+
:class:`ValueError` on any collision. ``spec_name`` (the ``spec <NAME> = ...``
|
|
193
|
+
header) is validated separately: it must match the simple-identifier pattern
|
|
194
|
+
``[A-Za-z][A-Za-z0-9_]*`` and must not itself be a keyword.
|
|
195
|
+
|
|
196
|
+
Formula emission rules (exact, classical two-valued reading)
|
|
197
|
+
------------------------------------------------------------
|
|
198
|
+
* Connectives render as CASL's ASCII operators: ``/\\`` (and), ``\\/`` (or),
|
|
199
|
+
``=>`` (implies), ``<=>`` (iff), ``not <f>`` (negation), ``t = t'``
|
|
200
|
+
(equality). :class:`Xor` has no direct CASL operator, so it is expanded to
|
|
201
|
+
its exact classical reading, ``not (<a> <=> <b>)`` — i.e. XOR-as-negated-IFF,
|
|
202
|
+
the standard truth-functional identity (P XOR Q is true exactly when P and Q
|
|
203
|
+
disagree, i.e. exactly when P IFF Q is false).
|
|
204
|
+
* A quantifier renders as ``forall x : Sort . <body>`` / ``exists x : Sort .
|
|
205
|
+
<body>``; a plain (unsorted) :class:`Quantifier` uses ``default_sort`` for
|
|
206
|
+
its bound variable's declared sort.
|
|
207
|
+
* Parenthesisation follows one rule, applied uniformly at every non-root
|
|
208
|
+
position: an :class:`Atom` renders bare; a :class:`Not` **of an atom** is
|
|
209
|
+
rendered bare as a connective operand (``not P /\\ Q`` — CASL's ``not``
|
|
210
|
+
binds tightest, so this is unambiguous), while a Not of a COMPOUND gets
|
|
211
|
+
the operand-level parens like any other compound child — yielding e.g.
|
|
212
|
+
``(not (P /\\ Q)) \\/ R``, where the inner pair is Not self-delimiting
|
|
213
|
+
its own operand and the outer pair is the ordinary child wrap. The outer
|
|
214
|
+
pair is technically redundant (the inner parens already delimit the
|
|
215
|
+
scope) but harmless, and keeping the child rule uniform beats a special
|
|
216
|
+
case (adversarial-review adjudicated: docstring corrected to the actual,
|
|
217
|
+
semantically exact behaviour). As Not's OWN operand the rule is
|
|
218
|
+
narrower: an atom stays bare (``not P``), anything else is wrapped in
|
|
219
|
+
one pair (``not (P /\\ Q)``); every OTHER node
|
|
220
|
+
(And/Or/Xor/Implies/Iff, and — with one exception — Quantifier/
|
|
221
|
+
SortedQuantifier) is wrapped in parens whenever it is not the formula's own
|
|
222
|
+
root. The one exception: a quantifier used as the BODY of an enclosing
|
|
223
|
+
quantifier stays bare (``forall x:S . exists y:T . phi``, no parens) — CASL
|
|
224
|
+
quantifier scope already extends as far right as syntactically possible, so
|
|
225
|
+
chained binders are unambiguous without help, exactly the same reason
|
|
226
|
+
nested parens are never needed between a quantifier and a following
|
|
227
|
+
``not``. A quantifier used as the operand of a BINARY connective, or as
|
|
228
|
+
Not's operand, IS wrapped (a bare ``P => exists x:S . Q`` would let the
|
|
229
|
+
quantifier's scope run past where the writer intended, so CASL text must
|
|
230
|
+
delimit it explicitly there) — this is also why a binary connective used as
|
|
231
|
+
a quantifier's body is wrapped (``forall x:S . (P => Q)``, never ``forall
|
|
232
|
+
x:S . P => Q``): the worked example in this module's docstring and test
|
|
233
|
+
suite is exactly this shape.
|
|
234
|
+
* A 0-ary :class:`Atom` renders as its bare predicate name, both in a formula
|
|
235
|
+
and as a ``preds`` declaration (``Rain : ()``).
|
|
236
|
+
* The truth constants ``$true`` / ``$false`` render as CASL's own formulas
|
|
237
|
+
``true`` / ``false`` and declare no predicate.
|
|
238
|
+
|
|
239
|
+
``to_casl_spec`` output shape (see the module's tests for the byte-exact
|
|
240
|
+
golden cases): a ``spec <name> =`` header; an optional ``sorts`` line (only
|
|
241
|
+
if at least one sort is used) with every distinct sort name, alphabetically,
|
|
242
|
+
comma-separated on one line; when ``subsorts`` is given (see below), one
|
|
243
|
+
further ``sort <child> < <parent>`` line per DIRECT edge, in
|
|
244
|
+
``(child, parent)`` order — one line per edge even when a child has several
|
|
245
|
+
parents or several children share one parent, never CASL's list-sharing form
|
|
246
|
+
(``sort S1, S2 < T``), which keeps the emitted grammar a strict subset of
|
|
247
|
+
what :mod:`unicode_logic_kit.fol.casl_import` already parses one edge at a
|
|
248
|
+
time; an optional ``ops`` block (constants AND functions merged into ONE
|
|
249
|
+
alphabetically-sorted list — a constant's ``ops`` entry is ``name : Sort``,
|
|
250
|
+
a function's is ``name : Sort1 * … -> Result``);
|
|
251
|
+
an optional ``preds`` block (``name : Sort1 * … * Sortn``, or ``name : ()``
|
|
252
|
+
for a nullary predicate), alphabetically; each axiom as its own ``. <formula>``
|
|
253
|
+
line, each conjecture as its own ``. <formula> %implied`` line (the ``%implied``
|
|
254
|
+
annotation is what Hets turns into a proof obligation rather than an assumed
|
|
255
|
+
axiom); and a closing ``end``. A section header's entries after the first are
|
|
256
|
+
indented to align under the first entry's own column, each non-final entry
|
|
257
|
+
suffixed with ``;``. Output is a pure function of the input AST (no
|
|
258
|
+
timestamps, no incidental set-iteration-order nondeterminism — every
|
|
259
|
+
collection is explicitly sorted before being joined into text), so the same
|
|
260
|
+
input always produces byte-identical output.
|
|
261
|
+
"""
|
|
262
|
+
|
|
263
|
+
from dataclasses import dataclass, field, replace
|
|
264
|
+
from typing import Dict, FrozenSet, Iterable, List, Mapping, Optional, Sequence, Set, Tuple, Union
|
|
265
|
+
import re
|
|
266
|
+
|
|
267
|
+
from .nodes import Node, SortedConstant, SortedQuantifier, Variable
|
|
268
|
+
from ._identifiers import fresh_variable_like, symbol_names
|
|
269
|
+
from ._truth_constants import truth_value
|
|
270
|
+
# Shared arity-conflict / constant-vs-function name-clash refusal, factored
|
|
271
|
+
# out into unicode_logic_kit.fol.signature so this module's independently-
|
|
272
|
+
# accumulated bookkeeping (see _Signature's docstring) and Signature.
|
|
273
|
+
# from_formulas's own arity/name-clash refusals come from one tested
|
|
274
|
+
# comparison-and-raise, even though each keeps its own accumulation
|
|
275
|
+
# strategy and message wording — see signature.py's module docstring's
|
|
276
|
+
# DESIGN NOTE.
|
|
277
|
+
from .signature import _check_single_valued, _check_not_dual_use
|
|
278
|
+
|
|
279
|
+
__all__ = ["to_casl_spec", "formula_to_casl"]
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
# =============================================================================
|
|
283
|
+
# Fragment gate
|
|
284
|
+
# =============================================================================
|
|
285
|
+
|
|
286
|
+
# Exactly the classical FOL / many-sorted-FOL node classes this exporter
|
|
287
|
+
# understands. See the module docstring's "Scope" section for why detection
|
|
288
|
+
# is by class-NAME membership here rather than an isinstance chain, and why
|
|
289
|
+
# Number is excluded despite being a plain classical term.
|
|
290
|
+
_ALLOWED_CASL_NODES = frozenset({
|
|
291
|
+
"Variable", "Constant", "SortedConstant", "Function",
|
|
292
|
+
"Atom", "Not", "And", "Or", "Xor", "Implies", "Iff",
|
|
293
|
+
"Quantifier", "SortedQuantifier",
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _check_fragment(formula: Node) -> None:
|
|
298
|
+
"""Walk ``formula`` and refuse (loudly) the first node outside the
|
|
299
|
+
classical FOL / MSFOL fragment this exporter covers.
|
|
300
|
+
|
|
301
|
+
Raises :class:`NotImplementedError` naming the specific offending node
|
|
302
|
+
class. ``Node.walk()`` yields in pre-order, so for a disallowed
|
|
303
|
+
CONSTRUCT (e.g. a :class:`Count` wrapping an otherwise-classical
|
|
304
|
+
formula) the outer, disallowed node itself is what gets named — not
|
|
305
|
+
some allowed node buried inside it.
|
|
306
|
+
"""
|
|
307
|
+
for n in formula.walk():
|
|
308
|
+
cls = type(n).__name__
|
|
309
|
+
if cls not in _ALLOWED_CASL_NODES:
|
|
310
|
+
raise NotImplementedError(
|
|
311
|
+
f"CASL export: {cls!r} is outside the classical FOL / "
|
|
312
|
+
"many-sorted FOL fragment this exporter covers (only Atom, "
|
|
313
|
+
"Not, And, Or, Xor, Implies, Iff, Quantifier, SortedQuantifier, "
|
|
314
|
+
"and the term classes Variable, Constant, SortedConstant, "
|
|
315
|
+
"Function are CASL-expressible here). Quantified modal logic "
|
|
316
|
+
"already has a route to CASL/DOL/Hets: translate the RAW modal "
|
|
317
|
+
"formula first with unicode_logic_kit.fol.qml.qml_validity_formula "
|
|
318
|
+
"(the standard translation to this classical fragment), then "
|
|
319
|
+
"hand it to unicode_logic_kit.hets.dol.to_dol_library_from_modal — "
|
|
320
|
+
"do not pass a modal node (Box/Diamond/…) to this exporter "
|
|
321
|
+
"directly. Every other non-classical logic (counting/measure/"
|
|
322
|
+
"cardinality, second-order quantification, linear/Lambek/"
|
|
323
|
+
"team-semantic connectives, …) has no such route yet and may "
|
|
324
|
+
"later be routed through a DOL (Distributed Ontology Language) "
|
|
325
|
+
"heterogeneous translation instead of a direct CASL emitter."
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
# =============================================================================
|
|
330
|
+
# CASL reserved words and identifier validation
|
|
331
|
+
# =============================================================================
|
|
332
|
+
|
|
333
|
+
# The CASL lexical keyword set (case-sensitive; all lowercase), per the CASL
|
|
334
|
+
# Reference Manual. None of these may be used as an ordinary sort/op/pred/
|
|
335
|
+
# variable identifier.
|
|
336
|
+
_CASL_KEYWORDS = frozenset({
|
|
337
|
+
"and", "arch", "as", "assoc", "axiom", "axioms", "closed", "comm", "def",
|
|
338
|
+
"else", "end", "exists", "false", "fit", "forall", "free", "from",
|
|
339
|
+
"generated", "get", "given", "hide", "idem", "if", "in", "lambda",
|
|
340
|
+
"library", "local", "logic", "not", "op", "ops", "pred", "preds",
|
|
341
|
+
"result", "reveal", "sort", "sorts", "spec", "then", "to", "true",
|
|
342
|
+
"type", "types", "unit", "units", "var", "vars", "version", "view",
|
|
343
|
+
"when", "with", "within",
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
_SIMPLE_ID_RE = re.compile(r"[A-Za-z][A-Za-z0-9_]*")
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _check_reserved(name: str, kind: str) -> None:
|
|
350
|
+
"""Refuse ``name`` (a ``kind`` identifier — 'constant' / 'function' /
|
|
351
|
+
'variable' / 'predicate' / 'sort') if it collides with a CASL keyword
|
|
352
|
+
or is not a simple CASL word at all.
|
|
353
|
+
|
|
354
|
+
Predicates and sorts are checked since the adversarial review: the
|
|
355
|
+
parser's casing rules never apply to directly-constructed AST nodes
|
|
356
|
+
(``Atom("axiom", ())``) or to raw sort strings (``SortedQuantifier``
|
|
357
|
+
annotations, ``default_sort``), and a keyword or malformed identifier
|
|
358
|
+
in ANY emitted position renders a spec HETS cannot parse (verified
|
|
359
|
+
live: HTTP 500 ``unexpected keyword``). See the module docstring's
|
|
360
|
+
"Reserved words" section for why the 'variable' case is checked even
|
|
361
|
+
though it can never fire under the kit's own variable grammar.
|
|
362
|
+
"""
|
|
363
|
+
if name in _CASL_KEYWORDS:
|
|
364
|
+
raise ValueError(
|
|
365
|
+
f"CASL export: {kind} name '{name}' collides with the reserved "
|
|
366
|
+
f"CASL keyword '{name}' and cannot be emitted as an identifier."
|
|
367
|
+
)
|
|
368
|
+
if not _SIMPLE_ID_RE.fullmatch(name):
|
|
369
|
+
raise ValueError(
|
|
370
|
+
f"CASL export: {kind} name {name!r} is not a simple CASL "
|
|
371
|
+
"identifier matching [A-Za-z][A-Za-z0-9_]*."
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _validate_spec_name(name: str) -> None:
|
|
376
|
+
"""Refuse a ``spec_name`` that is not a simple CASL identifier, or that
|
|
377
|
+
is itself a CASL keyword."""
|
|
378
|
+
if not _SIMPLE_ID_RE.fullmatch(name):
|
|
379
|
+
raise ValueError(
|
|
380
|
+
f"CASL export: spec_name {name!r} is not a simple CASL "
|
|
381
|
+
"identifier matching [A-Za-z][A-Za-z0-9_]*."
|
|
382
|
+
)
|
|
383
|
+
if name in _CASL_KEYWORDS:
|
|
384
|
+
raise ValueError(
|
|
385
|
+
f"CASL export: spec_name {name!r} collides with the reserved "
|
|
386
|
+
f"CASL keyword '{name}'."
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
# =============================================================================
|
|
391
|
+
# Sort inference: union-find over slots and concrete sort names
|
|
392
|
+
# =============================================================================
|
|
393
|
+
|
|
394
|
+
# A "slot" is a tuple key identifying one symbol POSITION whose sort is being
|
|
395
|
+
# inferred; a plain str member is a concrete, already-known sort name. See the
|
|
396
|
+
# module docstring's "Sort inference" section for the full account.
|
|
397
|
+
_Slot = Union[Tuple[str, str, int], Tuple[str, str], str]
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
class _UnionFind:
|
|
401
|
+
"""Weighted union-find over sort-inference slots and concrete sort names.
|
|
402
|
+
|
|
403
|
+
Invariant: whenever a class contains a concrete sort name (a plain str
|
|
404
|
+
member), that string is ALWAYS the class's root — union() only ever
|
|
405
|
+
reparents a slot-tuple root onto a string root, never the reverse. That
|
|
406
|
+
invariant is what makes conflict detection O(1) once both roots are
|
|
407
|
+
known: if find(a) and find(b) are both strings and different, the merge
|
|
408
|
+
would put two incompatible concrete sorts in one class.
|
|
409
|
+
"""
|
|
410
|
+
|
|
411
|
+
def __init__(self) -> None:
|
|
412
|
+
self._parent: Dict[_Slot, _Slot] = {}
|
|
413
|
+
self._rank: Dict[_Slot, int] = {}
|
|
414
|
+
|
|
415
|
+
def find(self, x: _Slot) -> _Slot:
|
|
416
|
+
"""Return the root of x's class, path-compressing along the way."""
|
|
417
|
+
self._parent.setdefault(x, x)
|
|
418
|
+
path = []
|
|
419
|
+
while self._parent[x] != x:
|
|
420
|
+
path.append(x)
|
|
421
|
+
x = self._parent[x]
|
|
422
|
+
for n in path:
|
|
423
|
+
self._parent[n] = x
|
|
424
|
+
return x
|
|
425
|
+
|
|
426
|
+
def union(self, a: _Slot, b: _Slot, context: str) -> None:
|
|
427
|
+
"""Merge the classes of ``a`` and ``b``.
|
|
428
|
+
|
|
429
|
+
``context`` names what triggered this merge (a predicate/function
|
|
430
|
+
argument position, an equality, or a sort annotation) and is used
|
|
431
|
+
only to build the error message below.
|
|
432
|
+
|
|
433
|
+
Raises :class:`ValueError` if ``a`` and ``b``'s classes each already
|
|
434
|
+
carry a DIFFERENT concrete sort name — an unsatisfiable constraint —
|
|
435
|
+
naming both conflicting sorts and ``context``.
|
|
436
|
+
"""
|
|
437
|
+
ra, rb = self.find(a), self.find(b)
|
|
438
|
+
if ra == rb:
|
|
439
|
+
return
|
|
440
|
+
a_concrete, b_concrete = isinstance(ra, str), isinstance(rb, str)
|
|
441
|
+
if a_concrete and b_concrete:
|
|
442
|
+
raise ValueError(
|
|
443
|
+
f"CASL export: sort conflict while resolving {context} — "
|
|
444
|
+
f"inferred both '{ra}' and '{rb}'."
|
|
445
|
+
)
|
|
446
|
+
if a_concrete:
|
|
447
|
+
self._parent[rb] = ra
|
|
448
|
+
elif b_concrete:
|
|
449
|
+
self._parent[ra] = rb
|
|
450
|
+
else:
|
|
451
|
+
rank_a, rank_b = self._rank.get(ra, 0), self._rank.get(rb, 0)
|
|
452
|
+
if rank_a < rank_b:
|
|
453
|
+
ra, rb = rb, ra
|
|
454
|
+
self._parent[rb] = ra
|
|
455
|
+
if rank_a == rank_b:
|
|
456
|
+
self._rank[ra] = rank_a + 1
|
|
457
|
+
|
|
458
|
+
def sort_of(self, slot: _Slot, default_sort: str) -> str:
|
|
459
|
+
"""Return the concrete sort resolved for ``slot``, or ``default_sort``
|
|
460
|
+
if that slot's class has no concrete member at all."""
|
|
461
|
+
root = self.find(slot)
|
|
462
|
+
return root if isinstance(root, str) else default_sort
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
@dataclass
|
|
466
|
+
class _Signature:
|
|
467
|
+
"""Mutable accumulator built by one :func:`_analyze` pass.
|
|
468
|
+
|
|
469
|
+
uf — the union-find carrying every sort-inference constraint.
|
|
470
|
+
pred_arity — predicate name -> the single arity it is checked against.
|
|
471
|
+
func_arity — function name -> the single arity it is checked against.
|
|
472
|
+
const_names — every constant symbol seen (Constant or SortedConstant).
|
|
473
|
+
func_names — every function symbol seen.
|
|
474
|
+
literal_sorts — every sort name written explicitly in the source (a
|
|
475
|
+
SortedQuantifier's or SortedConstant's own sort) or
|
|
476
|
+
implied by a plain Quantifier (default_sort) —
|
|
477
|
+
independent of whether union-find ever connects that sort
|
|
478
|
+
to any declared op/pred, so a vacuously-used sort (e.g. a
|
|
479
|
+
quantified variable that is never passed to anything) is
|
|
480
|
+
still declared in the spec's ``sorts`` line.
|
|
481
|
+
default_sort — threaded through so the walk functions don't need it as
|
|
482
|
+
a separate parameter.
|
|
483
|
+
"""
|
|
484
|
+
|
|
485
|
+
uf: _UnionFind = field(default_factory=_UnionFind)
|
|
486
|
+
pred_arity: Dict[str, int] = field(default_factory=dict)
|
|
487
|
+
func_arity: Dict[str, int] = field(default_factory=dict)
|
|
488
|
+
const_names: Set[str] = field(default_factory=set)
|
|
489
|
+
func_names: Set[str] = field(default_factory=set)
|
|
490
|
+
literal_sorts: Set[str] = field(default_factory=set)
|
|
491
|
+
default_sort: str = "Thing"
|
|
492
|
+
#: the sorts the caller wrote (a SortedQuantifier's or a SortedConstant's), never the
|
|
493
|
+
#: default sort that a plain Quantifier implies
|
|
494
|
+
user_sorts: Set[str] = field(default_factory=set)
|
|
495
|
+
#: whether a plain Quantifier typed a variable with the default sort
|
|
496
|
+
default_quantified: bool = False
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
_CONST_VS_FUNCTION = (
|
|
500
|
+
"CASL export: '{name}' is used both as a constant and as a function — "
|
|
501
|
+
"this exporter declares at most one 'ops' entry per name, so the two "
|
|
502
|
+
"uses cannot be reconciled into a single CASL operation symbol."
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def _infer_term(node: Node, env: Dict[str, str], sig: _Signature) -> _Slot:
|
|
507
|
+
"""Process a term node: register its symbol, recurse into any
|
|
508
|
+
sub-terms, and return the union-find slot that represents its sort.
|
|
509
|
+
|
|
510
|
+
Raises ValueError on a free variable, an arity conflict, a constant/
|
|
511
|
+
function name clash, or (via ``sig.uf.union``) a sort conflict.
|
|
512
|
+
"""
|
|
513
|
+
cls = type(node).__name__
|
|
514
|
+
|
|
515
|
+
if cls == "Variable":
|
|
516
|
+
if node.name not in env:
|
|
517
|
+
raise ValueError(
|
|
518
|
+
f"CASL export: free variable '{node.name}' — CASL axioms "
|
|
519
|
+
"must be closed formulas; every variable must be bound by "
|
|
520
|
+
"an enclosing quantifier."
|
|
521
|
+
)
|
|
522
|
+
return env[node.name]
|
|
523
|
+
|
|
524
|
+
if cls in ("Constant", "SortedConstant"):
|
|
525
|
+
name = node.name
|
|
526
|
+
_check_not_dual_use(name, sig.func_names, _CONST_VS_FUNCTION.format(name=name))
|
|
527
|
+
sig.const_names.add(name)
|
|
528
|
+
slot: _Slot = ("const", name)
|
|
529
|
+
if cls == "SortedConstant":
|
|
530
|
+
# _check_fragment admits a node only by class name, and SortedConstant is the
|
|
531
|
+
# one class of that name in the node hierarchy.
|
|
532
|
+
assert isinstance(node, SortedConstant)
|
|
533
|
+
sig.literal_sorts.add(node.sort)
|
|
534
|
+
sig.user_sorts.add(node.sort)
|
|
535
|
+
sig.uf.union(slot, node.sort, f"constant '{name}' sort annotation")
|
|
536
|
+
return slot
|
|
537
|
+
|
|
538
|
+
if cls == "Function":
|
|
539
|
+
name = node.name
|
|
540
|
+
_check_not_dual_use(name, sig.const_names, _CONST_VS_FUNCTION.format(name=name))
|
|
541
|
+
arity = len(node.args)
|
|
542
|
+
prev = sig.func_arity.get(name)
|
|
543
|
+
if prev is not None:
|
|
544
|
+
_check_single_valued(
|
|
545
|
+
{prev, arity},
|
|
546
|
+
f"CASL export: function '{name}' used with conflicting "
|
|
547
|
+
f"arities {prev} and {arity}."
|
|
548
|
+
)
|
|
549
|
+
sig.func_arity[name] = arity
|
|
550
|
+
sig.func_names.add(name)
|
|
551
|
+
for i, a in enumerate(node.args):
|
|
552
|
+
arg_slot = _infer_term(a, env, sig)
|
|
553
|
+
sig.uf.union(("func", name, i), arg_slot,
|
|
554
|
+
f"function '{name}' argument {i + 1}")
|
|
555
|
+
return ("func_result", name)
|
|
556
|
+
|
|
557
|
+
raise AssertionError(
|
|
558
|
+
f"CASL export: unreachable — term node {cls!r} passed the fragment "
|
|
559
|
+
"check but is not handled by _infer_term."
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def _infer_formula(node: Node, env: Dict[str, str], sig: _Signature) -> None:
|
|
564
|
+
"""Walk a formula node, threading the bound-variable environment,
|
|
565
|
+
registering every predicate/connective/quantifier constraint into
|
|
566
|
+
``sig``. Raises ValueError per :func:`_infer_term` and on a predicate
|
|
567
|
+
arity conflict or a malformed equality atom.
|
|
568
|
+
"""
|
|
569
|
+
cls = type(node).__name__
|
|
570
|
+
|
|
571
|
+
if cls == "Atom":
|
|
572
|
+
if truth_value(node) is not None:
|
|
573
|
+
return # CASL's own `true` / `false`: no predicate
|
|
574
|
+
if node.predicate == "=":
|
|
575
|
+
if len(node.args) != 2:
|
|
576
|
+
raise ValueError(
|
|
577
|
+
"CASL export: equality atom must have exactly 2 "
|
|
578
|
+
f"arguments, got {len(node.args)}."
|
|
579
|
+
)
|
|
580
|
+
s0 = _infer_term(node.args[0], env, sig)
|
|
581
|
+
s1 = _infer_term(node.args[1], env, sig)
|
|
582
|
+
sig.uf.union(s0, s1, "an equality (=) atom")
|
|
583
|
+
return
|
|
584
|
+
arity = len(node.args)
|
|
585
|
+
prev = sig.pred_arity.get(node.predicate)
|
|
586
|
+
if prev is not None:
|
|
587
|
+
_check_single_valued(
|
|
588
|
+
{prev, arity},
|
|
589
|
+
f"CASL export: predicate '{node.predicate}' used with "
|
|
590
|
+
f"conflicting arities {prev} and {arity}."
|
|
591
|
+
)
|
|
592
|
+
sig.pred_arity[node.predicate] = arity
|
|
593
|
+
for i, a in enumerate(node.args):
|
|
594
|
+
arg_slot = _infer_term(a, env, sig)
|
|
595
|
+
sig.uf.union(("pred", node.predicate, i), arg_slot,
|
|
596
|
+
f"predicate '{node.predicate}' argument {i + 1}")
|
|
597
|
+
return
|
|
598
|
+
|
|
599
|
+
if cls == "Not":
|
|
600
|
+
_infer_formula(node.formula, env, sig)
|
|
601
|
+
return
|
|
602
|
+
|
|
603
|
+
if cls in ("And", "Or", "Xor", "Implies", "Iff"):
|
|
604
|
+
_infer_formula(node.left, env, sig)
|
|
605
|
+
_infer_formula(node.right, env, sig)
|
|
606
|
+
return
|
|
607
|
+
|
|
608
|
+
if cls == "Quantifier":
|
|
609
|
+
_check_reserved(node.variable.name, "variable")
|
|
610
|
+
sig.literal_sorts.add(sig.default_sort)
|
|
611
|
+
sig.default_quantified = True
|
|
612
|
+
new_env = dict(env)
|
|
613
|
+
new_env[node.variable.name] = sig.default_sort
|
|
614
|
+
_infer_formula(node.formula, new_env, sig)
|
|
615
|
+
return
|
|
616
|
+
|
|
617
|
+
if cls == "SortedQuantifier":
|
|
618
|
+
# Same invariant as for SortedConstant in _infer_term.
|
|
619
|
+
assert isinstance(node, SortedQuantifier)
|
|
620
|
+
_check_reserved(node.variable.name, "variable")
|
|
621
|
+
sig.literal_sorts.add(node.sort)
|
|
622
|
+
sig.user_sorts.add(node.sort)
|
|
623
|
+
new_env = dict(env)
|
|
624
|
+
new_env[node.variable.name] = node.sort
|
|
625
|
+
_infer_formula(node.formula, new_env, sig)
|
|
626
|
+
return
|
|
627
|
+
|
|
628
|
+
raise AssertionError(
|
|
629
|
+
f"CASL export: unreachable — formula node {cls!r} passed the "
|
|
630
|
+
"fragment check but is not handled by _infer_formula."
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
def _analyze(formulas: Sequence[Node], default_sort: str,
|
|
635
|
+
declared_sorts: Iterable[str] = ()) -> _Signature:
|
|
636
|
+
"""Run the full validation + sort-inference pipeline over a batch of
|
|
637
|
+
formulas that will share one CASL signature (all the axioms and
|
|
638
|
+
conjectures of a single :func:`to_casl_spec` call, or the single formula
|
|
639
|
+
passed to :func:`formula_to_casl`).
|
|
640
|
+
|
|
641
|
+
Every formula is fragment-checked FIRST, across the whole batch, so an
|
|
642
|
+
out-of-fragment node is always reported before any sort-inference error
|
|
643
|
+
that a DIFFERENT, in-fragment formula in the same batch might otherwise
|
|
644
|
+
trigger. Each formula gets its own FRESH variable environment (formulas
|
|
645
|
+
are independent closed statements; a variable named ``x`` in one axiom
|
|
646
|
+
is unrelated to ``x`` in another), but all formulas share one
|
|
647
|
+
:class:`_Signature` (one union-find, one arity/name-clash bookkeeping),
|
|
648
|
+
because they share one CASL ``ops``/``preds`` vocabulary.
|
|
649
|
+
|
|
650
|
+
``declared_sorts`` are sorts the caller declares without a formula (the names of the
|
|
651
|
+
``subsorts`` edges); like the sorts the formulas write they must not be the default
|
|
652
|
+
sort when that is used (:func:`_check_default_sort_is_free`).
|
|
653
|
+
"""
|
|
654
|
+
for f in formulas:
|
|
655
|
+
_check_fragment(f)
|
|
656
|
+
# default_sort is validated UNCONDITIONALLY, not only when a quantifier
|
|
657
|
+
# binds it into sig.literal_sorts: the union-find's sort_of() fallback
|
|
658
|
+
# emits it into the `sorts` line for any argument slot never connected
|
|
659
|
+
# to a concrete annotation (e.g. a quantifier-free axiom batch), so a
|
|
660
|
+
# keyword or malformed default_sort would otherwise slip straight into
|
|
661
|
+
# the spec text (adversarial review, Tier 3 — the DolSpec.default_sort
|
|
662
|
+
# route reaches here with an arbitrary caller string).
|
|
663
|
+
_check_reserved(default_sort, "sort")
|
|
664
|
+
sig = _Signature(default_sort=default_sort)
|
|
665
|
+
for f in formulas:
|
|
666
|
+
_infer_formula(f, {}, sig)
|
|
667
|
+
for name in sig.const_names:
|
|
668
|
+
_check_reserved(name, "constant")
|
|
669
|
+
for name in sig.func_names:
|
|
670
|
+
_check_reserved(name, "function")
|
|
671
|
+
# Predicates and sorts are emitted identifiers exactly like constants
|
|
672
|
+
# and functions: a keyword-named predicate (Atom("axiom", ()) built
|
|
673
|
+
# directly on the AST — the parser's casing rule never applies there)
|
|
674
|
+
# or a keyword sort (SortedQuantifier/SortedConstant annotations, or a
|
|
675
|
+
# caller-supplied default_sort, all raw strings) would render a spec
|
|
676
|
+
# HETS cannot parse at all. Review-confirmed live; refuse loudly.
|
|
677
|
+
for name in sig.pred_arity:
|
|
678
|
+
if name != "=":
|
|
679
|
+
_check_reserved(name, "predicate")
|
|
680
|
+
for name in sig.literal_sorts:
|
|
681
|
+
_check_reserved(name, "sort")
|
|
682
|
+
_check_default_sort_is_free(sig, declared_sorts)
|
|
683
|
+
return sig
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
#: A sort name no caller can write (it is not an identifier): marks a slot no annotation reaches.
|
|
687
|
+
_NO_SORT = "\0"
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
def _default_sort_is_used(sig: _Signature) -> bool:
|
|
691
|
+
"""Whether the default sort types anything: a variable of a plain quantifier, or a
|
|
692
|
+
symbol position that no sort annotation reaches (:meth:`_UnionFind.sort_of` falls back
|
|
693
|
+
to the default sort for it)."""
|
|
694
|
+
if sig.default_quantified:
|
|
695
|
+
return True
|
|
696
|
+
slots: List[_Slot] = [("const", name) for name in sig.const_names]
|
|
697
|
+
for name, arity in sig.func_arity.items():
|
|
698
|
+
slots.append(("func_result", name))
|
|
699
|
+
slots.extend(("func", name, i) for i in range(arity))
|
|
700
|
+
for name, arity in sig.pred_arity.items():
|
|
701
|
+
if name != "=":
|
|
702
|
+
slots.extend(("pred", name, i) for i in range(arity))
|
|
703
|
+
return any(sig.uf.sort_of(slot, _NO_SORT) == _NO_SORT for slot in slots)
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
def _check_default_sort_is_free(sig: _Signature, declared_sorts: Iterable[str]) -> None:
|
|
707
|
+
"""Refuse a sort the caller wrote that is spelled like the default sort, when the
|
|
708
|
+
default sort is used (see the module docstring, "The default sort is a sort of its own").
|
|
709
|
+
|
|
710
|
+
Raises:
|
|
711
|
+
ValueError: such a sort exists; the message names it and the keyword that picks
|
|
712
|
+
another default sort.
|
|
713
|
+
"""
|
|
714
|
+
written = sig.user_sorts | set(declared_sorts)
|
|
715
|
+
if sig.default_sort in written and _default_sort_is_used(sig):
|
|
716
|
+
raise ValueError(
|
|
717
|
+
f"CASL export: the sort {sig.default_sort!r} is written in the formulas (or "
|
|
718
|
+
f"declared in subsorts) and is also the default sort, which this exporter gives "
|
|
719
|
+
"every unsorted quantifier and every position no sort annotation reaches, so the "
|
|
720
|
+
"two would be ONE sort and an unsorted position would silently become a position "
|
|
721
|
+
f"of your sort {sig.default_sort!r} (∀x:{sig.default_sort} P(x) ⊢ ∀y P(y) would "
|
|
722
|
+
"be proved). Pick another default sort with the keyword default_sort= of "
|
|
723
|
+
"to_casl_spec / formula_to_casl (DolSpec.default_sort in hets.dol), for "
|
|
724
|
+
f"example default_sort={sig.default_sort + '1'!r}.")
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
# =============================================================================
|
|
728
|
+
# Bound variables against the symbols of the specification
|
|
729
|
+
# =============================================================================
|
|
730
|
+
#
|
|
731
|
+
# In CASL text a bound variable, a constant (a 0-ary operation) and a predicate are
|
|
732
|
+
# all written as one identifier, and identifiers are compared exactly (``w`` and ``W``
|
|
733
|
+
# are two names, as :func:`~unicode_logic_kit.fol.casl_import.parse_casl_spec` reads
|
|
734
|
+
# them). ``forall w : Thing . (P(w) => Q(w))`` therefore cannot say that ``Q`` is
|
|
735
|
+
# applied to a constant ``w``: inside the quantifier the name is the variable. A
|
|
736
|
+
# binder is renamed when its name is the name of any symbol of the specification, and
|
|
737
|
+
# only then, so text without such a clash is unchanged.
|
|
738
|
+
|
|
739
|
+
def _specification_symbols(sig: _Signature, extra: Iterable[str] = ()) -> FrozenSet[str]:
|
|
740
|
+
"""Every name of the specification that a bound variable must not share: the constants,
|
|
741
|
+
functions and predicates it declares, every sort it names, the default sort, and the
|
|
742
|
+
names in ``extra`` (sorts declared by ``subsorts``, symbols visible from outside)."""
|
|
743
|
+
names: Set[str] = set(sig.const_names) | set(sig.func_names)
|
|
744
|
+
names.update(name for name in sig.pred_arity if name != "=")
|
|
745
|
+
names.update(sig.literal_sorts)
|
|
746
|
+
names.update(sig.user_sorts)
|
|
747
|
+
names.update(extra)
|
|
748
|
+
names.add(sig.default_sort)
|
|
749
|
+
return frozenset(names)
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
def _bind_apart(formulas: Sequence[Node], symbols: FrozenSet[str]) -> List[Node]:
|
|
753
|
+
"""``formulas`` with every bound variable that is spelled like one of ``symbols`` renamed.
|
|
754
|
+
|
|
755
|
+
A binder whose name is in ``symbols`` is renamed to a fresh variable name (the same
|
|
756
|
+
one the occurrences it binds are rewritten to), fresh against every name any of the
|
|
757
|
+
formulas holds, against ``symbols``, and against every name minted before it. Every
|
|
758
|
+
other binder and every other node is kept as it is, and when no binder clashes the
|
|
759
|
+
formulas come back unchanged. A formula is a closed sentence here (a free variable
|
|
760
|
+
has been refused), so renaming a binder changes nothing the formula says.
|
|
761
|
+
"""
|
|
762
|
+
clashing = {node.variable.name for f in formulas for node in f.walk()
|
|
763
|
+
if type(node).__name__ in ("Quantifier", "SortedQuantifier")
|
|
764
|
+
and node.variable.name in symbols}
|
|
765
|
+
if not clashing:
|
|
766
|
+
return list(formulas)
|
|
767
|
+
names = set(symbol_names(*formulas)) | set(symbols)
|
|
768
|
+
|
|
769
|
+
def rename(node: Node, scope: Dict[str, str]) -> Node:
|
|
770
|
+
kind = type(node).__name__
|
|
771
|
+
if kind == "Variable":
|
|
772
|
+
new = scope.get(node.name)
|
|
773
|
+
return node if new is None else Variable(new)
|
|
774
|
+
if kind in ("Quantifier", "SortedQuantifier"):
|
|
775
|
+
old = node.variable.name
|
|
776
|
+
if old in clashing:
|
|
777
|
+
new = fresh_variable_like(old, names)
|
|
778
|
+
names.add(new)
|
|
779
|
+
inner = {**scope, old: new}
|
|
780
|
+
else:
|
|
781
|
+
new = old
|
|
782
|
+
inner = {name: image for name, image in scope.items() if name != old}
|
|
783
|
+
return replace(node, variable=Variable(new), formula=rename(node.formula, inner))
|
|
784
|
+
return node.map_children(lambda child: rename(child, scope))
|
|
785
|
+
|
|
786
|
+
return [rename(f, {}) for f in formulas]
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
# =============================================================================
|
|
790
|
+
# Formula rendering
|
|
791
|
+
# =============================================================================
|
|
792
|
+
#
|
|
793
|
+
# Three separate "child" contexts, per the module docstring's parenthesisation
|
|
794
|
+
# rule — each decides, for the specific node it is about to render, whether
|
|
795
|
+
# that node needs an extra pair of parens in THAT position:
|
|
796
|
+
#
|
|
797
|
+
# _render_conn_operand — an operand of a binary connective (And/Or/Xor/
|
|
798
|
+
# Implies/Iff): bare only for an Atom or a Not-of-Atom; everything else
|
|
799
|
+
# (another binary connective, a Quantifier/SortedQuantifier, or a
|
|
800
|
+
# Not-of-compound) is wrapped.
|
|
801
|
+
# _render_not_operand — Not's own operand: bare for an Atom, wrapped for
|
|
802
|
+
# anything else (including another Not, a connective, or a quantifier).
|
|
803
|
+
# _render_quant_body — a quantifier's body: a binary connective is
|
|
804
|
+
# wrapped (matches _render_conn_operand's blanket rule — this is the
|
|
805
|
+
# 'forall x:S . (P => Q)' shape); a nested Quantifier/SortedQuantifier,
|
|
806
|
+
# Atom, or Not stays bare, since quantifier scope (and Not's prefix
|
|
807
|
+
# scope) already extends maximally to the right.
|
|
808
|
+
#
|
|
809
|
+
# _render(node, default_sort) itself is always called BARE — at the root of
|
|
810
|
+
# formula_to_casl's argument, at the root of each to_casl_spec axiom/
|
|
811
|
+
# conjecture line, and internally wherever one of the three context helpers
|
|
812
|
+
# above has already decided not to wrap. No function here re-wraps its own
|
|
813
|
+
# return value; wrapping is applied exactly once, by whichever caller placed
|
|
814
|
+
# the node in a non-root position.
|
|
815
|
+
|
|
816
|
+
_CONN_SYMBOLS = {"And": "/\\", "Or": "\\/", "Implies": "=>", "Iff": "<=>"}
|
|
817
|
+
|
|
818
|
+
|
|
819
|
+
def _quant_keyword(qtype: str) -> str:
|
|
820
|
+
"""Map a Quantifier/SortedQuantifier ``type`` field to its CASL keyword."""
|
|
821
|
+
if qtype in ("∀", "forall"):
|
|
822
|
+
return "forall"
|
|
823
|
+
if qtype in ("∃", "exists"):
|
|
824
|
+
return "exists"
|
|
825
|
+
raise ValueError(f"CASL export: unknown quantifier type {qtype!r}.")
|
|
826
|
+
|
|
827
|
+
|
|
828
|
+
def _render_term(node: Node) -> str:
|
|
829
|
+
"""Render a term node (Variable / Constant / SortedConstant / Function)."""
|
|
830
|
+
cls = type(node).__name__
|
|
831
|
+
if cls in ("Variable", "Constant", "SortedConstant"):
|
|
832
|
+
return node.name
|
|
833
|
+
if cls == "Function":
|
|
834
|
+
if not node.args:
|
|
835
|
+
# A 0-ary operation is declared like a constant (ops f : S) and
|
|
836
|
+
# must be REFERENCED bare too — 'f()' is not valid CASL term
|
|
837
|
+
# syntax (review-confirmed; mirrors _render_atom's 0-ary rule).
|
|
838
|
+
return node.name
|
|
839
|
+
args = ", ".join(_render_term(a) for a in node.args)
|
|
840
|
+
return f"{node.name}({args})"
|
|
841
|
+
raise NotImplementedError(
|
|
842
|
+
f"CASL export: term node {cls!r} is outside the classical FOL/MSFOL "
|
|
843
|
+
"fragment this exporter supports; non-classical logics may later go "
|
|
844
|
+
"through DOL."
|
|
845
|
+
)
|
|
846
|
+
|
|
847
|
+
|
|
848
|
+
def _render_atom(node: Node) -> str:
|
|
849
|
+
"""Render an Atom: infix equality, a 0-ary bare predicate name, or an
|
|
850
|
+
applied predicate (the truth constants render as ``true`` / ``false``)."""
|
|
851
|
+
if truth_value(node) is not None:
|
|
852
|
+
return "true" if truth_value(node) else "false"
|
|
853
|
+
if node.predicate == "=":
|
|
854
|
+
if len(node.args) != 2:
|
|
855
|
+
raise ValueError(
|
|
856
|
+
"CASL export: equality atom must have exactly 2 arguments, "
|
|
857
|
+
f"got {len(node.args)}."
|
|
858
|
+
)
|
|
859
|
+
return f"{_render_term(node.args[0])} = {_render_term(node.args[1])}"
|
|
860
|
+
if not node.args:
|
|
861
|
+
return node.predicate
|
|
862
|
+
args = ", ".join(_render_term(a) for a in node.args)
|
|
863
|
+
return f"{node.predicate}({args})"
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
def _render(node: Node, default_sort: str) -> str:
|
|
867
|
+
"""Render ``node`` bare (no self-wrapping) as CASL formula text."""
|
|
868
|
+
cls = type(node).__name__
|
|
869
|
+
|
|
870
|
+
if cls == "Atom":
|
|
871
|
+
return _render_atom(node)
|
|
872
|
+
|
|
873
|
+
if cls == "Not":
|
|
874
|
+
return f"not {_render_not_operand(node.formula, default_sort)}"
|
|
875
|
+
|
|
876
|
+
if cls in _CONN_SYMBOLS:
|
|
877
|
+
left = _render_conn_operand(node.left, default_sort)
|
|
878
|
+
right = _render_conn_operand(node.right, default_sort)
|
|
879
|
+
return f"{left} {_CONN_SYMBOLS[cls]} {right}"
|
|
880
|
+
|
|
881
|
+
if cls == "Xor":
|
|
882
|
+
# Exact classical reading: P XOR Q == not (P <=> Q). See the module
|
|
883
|
+
# docstring's "Formula emission rules".
|
|
884
|
+
left = _render_conn_operand(node.left, default_sort)
|
|
885
|
+
right = _render_conn_operand(node.right, default_sort)
|
|
886
|
+
return f"not ({left} <=> {right})"
|
|
887
|
+
|
|
888
|
+
if cls == "Quantifier":
|
|
889
|
+
body = _render_quant_body(node.formula, default_sort)
|
|
890
|
+
return (f"{_quant_keyword(node.type)} {node.variable.name} : "
|
|
891
|
+
f"{default_sort} . {body}")
|
|
892
|
+
|
|
893
|
+
if cls == "SortedQuantifier":
|
|
894
|
+
body = _render_quant_body(node.formula, default_sort)
|
|
895
|
+
return (f"{_quant_keyword(node.type)} {node.variable.name} : "
|
|
896
|
+
f"{node.sort} . {body}")
|
|
897
|
+
|
|
898
|
+
raise NotImplementedError(
|
|
899
|
+
f"CASL export: formula node {cls!r} is outside the classical "
|
|
900
|
+
"FOL/MSFOL fragment this exporter supports; non-classical logics "
|
|
901
|
+
"may later go through DOL."
|
|
902
|
+
)
|
|
903
|
+
|
|
904
|
+
|
|
905
|
+
def _render_conn_operand(node: Node, default_sort: str) -> str:
|
|
906
|
+
"""Render ``node`` as the operand of a binary connective (And/Or/Xor/
|
|
907
|
+
Implies/Iff): bare for an Atom or a Not-of-Atom, else wrapped."""
|
|
908
|
+
cls = type(node).__name__
|
|
909
|
+
if cls == "Atom" or (cls == "Not" and type(node.formula).__name__ == "Atom"):
|
|
910
|
+
return _render(node, default_sort)
|
|
911
|
+
return f"({_render(node, default_sort)})"
|
|
912
|
+
|
|
913
|
+
|
|
914
|
+
def _render_not_operand(node: Node, default_sort: str) -> str:
|
|
915
|
+
"""Render ``node`` as Not's own operand: bare for an Atom, else wrapped."""
|
|
916
|
+
if type(node).__name__ == "Atom":
|
|
917
|
+
return _render(node, default_sort)
|
|
918
|
+
return f"({_render(node, default_sort)})"
|
|
919
|
+
|
|
920
|
+
|
|
921
|
+
def _render_quant_body(node: Node, default_sort: str) -> str:
|
|
922
|
+
"""Render ``node`` as a quantifier's body: a binary connective (And/Or/
|
|
923
|
+
Xor/Implies/Iff) is wrapped; everything else (Atom, Not, a nested
|
|
924
|
+
Quantifier/SortedQuantifier) stays bare."""
|
|
925
|
+
if type(node).__name__ in _CONN_SYMBOLS or type(node).__name__ == "Xor":
|
|
926
|
+
return f"({_render(node, default_sort)})"
|
|
927
|
+
return _render(node, default_sort)
|
|
928
|
+
|
|
929
|
+
|
|
930
|
+
# =============================================================================
|
|
931
|
+
# Declaration synthesis (to_casl_spec only)
|
|
932
|
+
# =============================================================================
|
|
933
|
+
|
|
934
|
+
def _render_op_type(arg_sorts: List[str], result_sort: str) -> str:
|
|
935
|
+
"""Render an ``ops`` entry's type: ``Result`` for a 0-ary function
|
|
936
|
+
(indistinguishable in shape from a constant's own type), else
|
|
937
|
+
``Sort1 * … * Sortn -> Result``."""
|
|
938
|
+
if not arg_sorts:
|
|
939
|
+
return result_sort
|
|
940
|
+
return " * ".join(arg_sorts) + " -> " + result_sort
|
|
941
|
+
|
|
942
|
+
|
|
943
|
+
def _render_pred_type(arg_sorts: List[str]) -> str:
|
|
944
|
+
"""Render a ``preds`` entry's type: ``()`` for a 0-ary predicate, else
|
|
945
|
+
``Sort1 * … * Sortn``."""
|
|
946
|
+
if not arg_sorts:
|
|
947
|
+
return "()"
|
|
948
|
+
return " * ".join(arg_sorts)
|
|
949
|
+
|
|
950
|
+
|
|
951
|
+
def _render_block(keyword: str, entries: List[Tuple[str, str]]) -> str:
|
|
952
|
+
"""Render one ``ops``/``preds`` block: `` <keyword> name : type;`` for
|
|
953
|
+
the first entry, each further entry on its own line indented to align
|
|
954
|
+
under the first entry's own column, every non-final entry ending in
|
|
955
|
+
``;``. ``entries`` must already be sorted by name."""
|
|
956
|
+
prefix = f" {keyword} "
|
|
957
|
+
cont_indent = " " * len(prefix)
|
|
958
|
+
last = len(entries) - 1
|
|
959
|
+
lines = []
|
|
960
|
+
for i, (name, type_str) in enumerate(entries):
|
|
961
|
+
text = f"{name} : {type_str}"
|
|
962
|
+
if i != last:
|
|
963
|
+
text += ";"
|
|
964
|
+
lines.append((prefix if i == 0 else cont_indent) + text)
|
|
965
|
+
return "\n".join(lines)
|
|
966
|
+
|
|
967
|
+
|
|
968
|
+
# =============================================================================
|
|
969
|
+
# Public API
|
|
970
|
+
# =============================================================================
|
|
971
|
+
|
|
972
|
+
def formula_to_casl(formula: Node, *, default_sort: str = "Thing",
|
|
973
|
+
visible_symbols: Iterable[str] = ()) -> str:
|
|
974
|
+
"""Render a single closed formula as bare CASL formula text, without a
|
|
975
|
+
``spec`` wrapper.
|
|
976
|
+
|
|
977
|
+
This is the same rendering :func:`to_casl_spec` uses for each of its
|
|
978
|
+
``. <formula>`` lines, for embedding into hand-written CASL.
|
|
979
|
+
|
|
980
|
+
Runs the full validation pipeline first (see the module docstring's
|
|
981
|
+
"Sort inference" and "Reserved words" sections): fragment check, free-
|
|
982
|
+
variable check, arity/name-clash checks, and sort-conflict detection —
|
|
983
|
+
even though the resolved sorts themselves are not needed to render a
|
|
984
|
+
bare formula (only :class:`SortedQuantifier`'s own literal sort and
|
|
985
|
+
``default_sort`` ever appear in the text), the checks still apply,
|
|
986
|
+
because CASL text with a free variable, a reserved-word identifier, or
|
|
987
|
+
an unsatisfiable sort constraint is not valid CASL either way.
|
|
988
|
+
|
|
989
|
+
A bound variable that is spelled like a symbol of the formula (a constant, a
|
|
990
|
+
function, a predicate or a sort it names, or ``default_sort``) is renamed, as in
|
|
991
|
+
:func:`to_casl_spec`. ``visible_symbols`` names the symbols of the hand-written CASL
|
|
992
|
+
the text is embedded into, which this function cannot see: a bound variable is
|
|
993
|
+
never given the spelling of one of them either.
|
|
994
|
+
|
|
995
|
+
Raises:
|
|
996
|
+
NotImplementedError: ``formula`` contains a node outside the
|
|
997
|
+
classical FOL/MSFOL fragment (see the module docstring's
|
|
998
|
+
"Scope" section).
|
|
999
|
+
ValueError: a free variable, a reserved-word identifier, an arity
|
|
1000
|
+
or constant/function-vs-function conflict, an unsatisfiable
|
|
1001
|
+
sort constraint, or a sort written in the formula that is spelled like
|
|
1002
|
+
``default_sort`` while the default sort is used (the message names the
|
|
1003
|
+
keyword that picks another one).
|
|
1004
|
+
"""
|
|
1005
|
+
sig = _analyze([formula], default_sort)
|
|
1006
|
+
[formula] = _bind_apart([formula], _specification_symbols(sig, visible_symbols))
|
|
1007
|
+
return _render(formula, default_sort)
|
|
1008
|
+
|
|
1009
|
+
|
|
1010
|
+
def to_casl_spec(
|
|
1011
|
+
axioms: Iterable[Node],
|
|
1012
|
+
*,
|
|
1013
|
+
conjectures: Iterable[Node] = (),
|
|
1014
|
+
spec_name: str = "KitExport",
|
|
1015
|
+
default_sort: str = "Thing",
|
|
1016
|
+
subsorts: Optional[Mapping[str, FrozenSet[str]]] = None,
|
|
1017
|
+
visible_symbols: Iterable[str] = (),
|
|
1018
|
+
) -> str:
|
|
1019
|
+
"""Render ``axioms`` and ``conjectures`` as one complete CASL basic spec:
|
|
1020
|
+
``spec <spec_name> = … end``, Hets-parsable ASCII text.
|
|
1021
|
+
|
|
1022
|
+
``axioms`` and ``conjectures`` share ONE signature: every predicate,
|
|
1023
|
+
function, and constant that appears anywhere across both is declared
|
|
1024
|
+
exactly once (sort inference — see the module docstring — runs over the
|
|
1025
|
+
whole combined batch). Each axiom becomes its own ``. <formula>`` line;
|
|
1026
|
+
each conjecture becomes its own ``. <formula> %implied`` line (the
|
|
1027
|
+
``%implied`` annotation is what turns a formula into a Hets proof
|
|
1028
|
+
obligation rather than an assumed axiom). ``sorts``/``ops``/``preds``
|
|
1029
|
+
sections are emitted only when non-empty, sorted alphabetically by
|
|
1030
|
+
symbol name (``ops`` merges constants and functions into one list).
|
|
1031
|
+
|
|
1032
|
+
``subsorts`` (default ``None``, no subsort declarations emitted) is the
|
|
1033
|
+
child-sort-to-DIRECT-parent-sorts mapping — the same shape as
|
|
1034
|
+
:attr:`~unicode_logic_kit.fol.signature.Signature.subsorts`, and typically
|
|
1035
|
+
passed as exactly that attribute — completing the round trip with
|
|
1036
|
+
:func:`unicode_logic_kit.fol.casl_import.parse_casl_spec`, which reads
|
|
1037
|
+
``sort <child> < <parent>`` back into the identical mapping. Unlike
|
|
1038
|
+
every other declaration here, a subsort edge is NOT inferred from the
|
|
1039
|
+
formulas (nothing about ``S < T`` is derivable from how ``S``/``T`` are
|
|
1040
|
+
USED in an axiom), so both sort names in every edge are added to the
|
|
1041
|
+
``sorts`` line even if neither appears in any formula, and both are
|
|
1042
|
+
checked against the CASL reserved-word list exactly like every other
|
|
1043
|
+
emitted sort name (see the module docstring's "Reserved words" section).
|
|
1044
|
+
|
|
1045
|
+
**Bound variables and the symbols of the spec.** CASL writes a bound variable and a
|
|
1046
|
+
constant as the same identifier, and the variable wins inside its quantifier, so a
|
|
1047
|
+
constant ``w`` under a quantifier that binds ``w`` would be read as the variable. A
|
|
1048
|
+
bound variable that is spelled like ANY symbol of the whole spec (a constant, function
|
|
1049
|
+
or predicate of any axiom or conjecture, a sort, ``default_sort``, a name in
|
|
1050
|
+
``visible_symbols``) is therefore renamed to a fresh variable name (``w0``, ``w1``, …,
|
|
1051
|
+
fresh against every name the formulas hold), in its quantifier and in every
|
|
1052
|
+
occurrence it binds; names are compared exactly, as CASL does (``W`` and ``w`` do not
|
|
1053
|
+
clash). Text without such a clash is unchanged. The result reads back through
|
|
1054
|
+
:func:`~unicode_logic_kit.fol.casl_import.parse_casl_spec` as the same formula up to
|
|
1055
|
+
the names of the bound variables that had to be renamed (and is the same formula
|
|
1056
|
+
where none was). ``visible_symbols`` are the names of symbols the spec sees without
|
|
1057
|
+
declaring them (those of a spec it extends with ``then``); no formula is read for
|
|
1058
|
+
them, they only keep the binders away.
|
|
1059
|
+
|
|
1060
|
+
Raises:
|
|
1061
|
+
ValueError: neither ``axioms`` nor ``conjectures`` contains any
|
|
1062
|
+
formula; ``spec_name`` is not a simple identifier or collides
|
|
1063
|
+
with a CASL keyword; a sort name in ``subsorts`` is reserved; a sort
|
|
1064
|
+
written in the formulas or named in ``subsorts`` is spelled like
|
|
1065
|
+
``default_sort`` while the default sort is used (the message names the
|
|
1066
|
+
keyword ``default_sort=`` that picks another one); or
|
|
1067
|
+
any of the sort-inference / identifier refusals documented on
|
|
1068
|
+
:func:`formula_to_casl` above.
|
|
1069
|
+
NotImplementedError: as :func:`formula_to_casl`.
|
|
1070
|
+
"""
|
|
1071
|
+
_validate_spec_name(spec_name)
|
|
1072
|
+
axioms = list(axioms)
|
|
1073
|
+
conjectures = list(conjectures)
|
|
1074
|
+
all_formulas = axioms + conjectures
|
|
1075
|
+
if not all_formulas:
|
|
1076
|
+
raise ValueError(
|
|
1077
|
+
"CASL export: to_casl_spec needs at least one axiom or "
|
|
1078
|
+
"conjecture; an empty spec has no formulas to export."
|
|
1079
|
+
)
|
|
1080
|
+
subsorts = subsorts or {}
|
|
1081
|
+
subsort_edges: List[Tuple[str, str]] = sorted(
|
|
1082
|
+
(child, parent) for child, parents in subsorts.items() for parent in parents
|
|
1083
|
+
)
|
|
1084
|
+
for child, parent in subsort_edges:
|
|
1085
|
+
_check_reserved(child, "sort")
|
|
1086
|
+
_check_reserved(parent, "sort")
|
|
1087
|
+
|
|
1088
|
+
edge_sorts = [s for edge in subsort_edges for s in edge]
|
|
1089
|
+
sig = _analyze(all_formulas, default_sort, declared_sorts=edge_sorts)
|
|
1090
|
+
bound_apart = _bind_apart(all_formulas,
|
|
1091
|
+
_specification_symbols(sig, [*edge_sorts, *visible_symbols]))
|
|
1092
|
+
axioms, conjectures = bound_apart[:len(axioms)], bound_apart[len(axioms):]
|
|
1093
|
+
declared_sorts: Set[str] = set(sig.literal_sorts)
|
|
1094
|
+
for child, parent in subsort_edges:
|
|
1095
|
+
declared_sorts.add(child)
|
|
1096
|
+
declared_sorts.add(parent)
|
|
1097
|
+
|
|
1098
|
+
op_entries: List[Tuple[str, str]] = []
|
|
1099
|
+
for name in sorted(sig.func_names):
|
|
1100
|
+
arity = sig.func_arity[name]
|
|
1101
|
+
arg_sorts = [sig.uf.sort_of(("func", name, i), default_sort)
|
|
1102
|
+
for i in range(arity)]
|
|
1103
|
+
result_sort = sig.uf.sort_of(("func_result", name), default_sort)
|
|
1104
|
+
declared_sorts.update(arg_sorts)
|
|
1105
|
+
declared_sorts.add(result_sort)
|
|
1106
|
+
op_entries.append((name, _render_op_type(arg_sorts, result_sort)))
|
|
1107
|
+
for name in sorted(sig.const_names):
|
|
1108
|
+
const_sort = sig.uf.sort_of(("const", name), default_sort)
|
|
1109
|
+
declared_sorts.add(const_sort)
|
|
1110
|
+
op_entries.append((name, const_sort))
|
|
1111
|
+
op_entries.sort(key=lambda e: e[0])
|
|
1112
|
+
|
|
1113
|
+
pred_entries: List[Tuple[str, str]] = []
|
|
1114
|
+
for name in sorted(sig.pred_arity):
|
|
1115
|
+
arity = sig.pred_arity[name]
|
|
1116
|
+
arg_sorts = [sig.uf.sort_of(("pred", name, i), default_sort)
|
|
1117
|
+
for i in range(arity)]
|
|
1118
|
+
declared_sorts.update(arg_sorts)
|
|
1119
|
+
pred_entries.append((name, _render_pred_type(arg_sorts)))
|
|
1120
|
+
|
|
1121
|
+
lines = [f"spec {spec_name} ="]
|
|
1122
|
+
if declared_sorts:
|
|
1123
|
+
lines.append(" sorts " + ", ".join(sorted(declared_sorts)))
|
|
1124
|
+
for child, parent in subsort_edges:
|
|
1125
|
+
lines.append(f" sort {child} < {parent}")
|
|
1126
|
+
if op_entries:
|
|
1127
|
+
lines.append(_render_block("ops", op_entries))
|
|
1128
|
+
if pred_entries:
|
|
1129
|
+
lines.append(_render_block("preds", pred_entries))
|
|
1130
|
+
for f in axioms:
|
|
1131
|
+
lines.append(f" . {_render(f, default_sort)}")
|
|
1132
|
+
for f in conjectures:
|
|
1133
|
+
lines.append(f" . {_render(f, default_sort)} %implied")
|
|
1134
|
+
lines.append("end")
|
|
1135
|
+
return "\n".join(lines)
|