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,1290 @@
|
|
|
1
|
+
"""A first-class, explicit ``Signature`` object: the kit's declared vocabulary.
|
|
2
|
+
|
|
3
|
+
A signature names what a formula is allowed to talk about — which predicate
|
|
4
|
+
symbols exist and at what arity, which functions, which constants, and (for
|
|
5
|
+
many-sorted formulas) which sorts and which argument/result positions are
|
|
6
|
+
typed. Today that vocabulary is checked in at least three independent, ad hoc
|
|
7
|
+
places:
|
|
8
|
+
|
|
9
|
+
* ``unicode_logic_kit.api.check(formula, signature={...})`` takes a LOOSE DICT
|
|
10
|
+
(see "The api.check loose dict convention" below) and reports unknown
|
|
11
|
+
symbols / arity mismatches with did-you-mean suggestions.
|
|
12
|
+
* ``unicode_logic_kit.eval.predicate_match`` extracts a per-namespace symbol
|
|
13
|
+
INVENTORY (``(name, arity)`` sets for predicates/functions, a name set for
|
|
14
|
+
constants) purely to realign two formulas' vocabularies against each other.
|
|
15
|
+
* ``unicode_logic_kit.fol.casl_export`` INFERS a signature (arities, and a
|
|
16
|
+
per-argument-position sort via union-find) from a batch of formulas, purely
|
|
17
|
+
as an internal step of emitting a CASL ``spec`` block.
|
|
18
|
+
|
|
19
|
+
Each of the three is correct for its own narrow purpose, but none of them is a
|
|
20
|
+
reusable, inspectable, first-class object — you cannot ask any of them "here
|
|
21
|
+
is a signature, does this OTHER formula conform to it", or serialise one to
|
|
22
|
+
disk, or merge two signatures from different sources. :class:`Signature` is
|
|
23
|
+
that missing object: predicates, functions, constants, and sorts as one
|
|
24
|
+
explicit, immutable value, with two ways to build one (from a dict, or by
|
|
25
|
+
inferring it from a batch of ASTs), a way to check an arbitrary formula
|
|
26
|
+
against it (:meth:`Signature.validate`), a way to combine two of them
|
|
27
|
+
(:meth:`Signature.merge`), and a stable JSON-compatible round-trip
|
|
28
|
+
(:meth:`Signature.to_dict` / :meth:`Signature.from_dict`).
|
|
29
|
+
|
|
30
|
+
DESIGN NOTE — the shared carrier, partially wired
|
|
31
|
+
---------------------------------------------------------
|
|
32
|
+
``api.check(formula, signature=Signature(...))`` IS wired: the facade
|
|
33
|
+
projects a :class:`Signature` onto its loose dict convention for the
|
|
34
|
+
classic unknown-symbol/arity diagnostics (keeping their did-you-mean
|
|
35
|
+
suggestions) and ADDITIONALLY reports this module's sort-mismatch messages
|
|
36
|
+
(``kind="sort_mismatch"`` entries) — see ``api.check``.
|
|
37
|
+
|
|
38
|
+
``predicate_match`` and ``casl_export`` keep their own independently-tested
|
|
39
|
+
walks — a full rewrite onto :meth:`from_formulas` remains out of scope,
|
|
40
|
+
because each has a genuine, load-bearing reason to differ (see below) — but
|
|
41
|
+
they no longer duplicate this module's low-level bookkeeping:
|
|
42
|
+
|
|
43
|
+
* :func:`inventory_of` is the LENIENT counterpart of :meth:`from_formulas`'s
|
|
44
|
+
own classification walk, factored out so
|
|
45
|
+
``unicode_logic_kit.eval.predicate_match``'s ``_symbol_inventory`` (which
|
|
46
|
+
must never raise — it scores possibly-malformed model output, see that
|
|
47
|
+
module's docstring) can share it instead of maintaining a second,
|
|
48
|
+
independently-written ``node.walk()`` classification pass. Where
|
|
49
|
+
:meth:`from_formulas` REFUSES a vocabulary conflict, :func:`inventory_of`
|
|
50
|
+
keeps every conflicting entry side by side — see its own docstring.
|
|
51
|
+
* the ARITY-conflict and constant/function name-clash REFUSAL itself (the
|
|
52
|
+
actual comparison-and-raise, as opposed to how each caller accumulates
|
|
53
|
+
the values being compared) is factored into the module-private
|
|
54
|
+
:func:`_check_single_valued` / :func:`_check_not_dual_use`, called by
|
|
55
|
+
both :meth:`from_formulas` and
|
|
56
|
+
:mod:`unicode_logic_kit.fol.casl_export`'s ``_analyze``. Each caller keeps
|
|
57
|
+
its OWN accumulation strategy and OWN message wording:
|
|
58
|
+
:meth:`from_formulas` collects the full set of arities/sorts seen for a
|
|
59
|
+
symbol across the whole batch before checking it once (so a three-way
|
|
60
|
+
conflict names every arity involved); ``casl_export._analyze`` checks
|
|
61
|
+
incrementally, per occurrence, against a running single recorded value
|
|
62
|
+
(so an arity conflict is reported — and the whole batch's sort-inference
|
|
63
|
+
union-find work is short-circuited — as soon as it is first seen, before
|
|
64
|
+
any later, unrelated formula in the batch is even walked). These are two
|
|
65
|
+
different, independently-justified accumulation policies (see
|
|
66
|
+
``casl_export``'s own module docstring), not two implementations of one
|
|
67
|
+
policy — only the underlying "more than one distinct value is a
|
|
68
|
+
conflict" / "this name is claimed in the other namespace" predicates are
|
|
69
|
+
shared.
|
|
70
|
+
* ``casl_export``'s per-argument-position sort inference (the
|
|
71
|
+
``_UnionFind`` / ``_Signature`` union-find, and its ``default_sort``
|
|
72
|
+
total-resolution policy) stays exactly where it is, unshared — see the
|
|
73
|
+
"Signature.from_formulas — scope" section above for why.
|
|
74
|
+
|
|
75
|
+
The module stays self-contained and importable on its own
|
|
76
|
+
(``from unicode_logic_kit.fol.signature import Signature``).
|
|
77
|
+
|
|
78
|
+
The api.check loose dict convention (verified against unicode_logic_kit/api.py)
|
|
79
|
+
-------------------------------------------------------------------------------
|
|
80
|
+
``api.check``'s ``_signature_errors`` (api.py) reads a plain dict with up to
|
|
81
|
+
three OPTIONAL top-level keys — a key that is absent means that namespace is
|
|
82
|
+
UNCONSTRAINED (nothing is flagged unknown in it), not empty:
|
|
83
|
+
|
|
84
|
+
{
|
|
85
|
+
"predicates": {"Human": 1, "Mortal": 1}, # name -> arity ...
|
|
86
|
+
"functions": {"double": 1}, # ... or an iterable of
|
|
87
|
+
"constants": ["a", "b"], # allowed arities
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
``predicates`` / ``functions`` map a name to EITHER a single ``int`` arity or
|
|
91
|
+
an iterable of allowed arities (``_signature_errors``'s ``allowed_arities``
|
|
92
|
+
helper does ``{value}`` for an int, ``set(value)`` otherwise); ``constants``
|
|
93
|
+
is a plain iterable of bare names. This is confirmed against
|
|
94
|
+
``tests/test_api.py`` (``test_check_signature_wrong_arity_and_suggestion``,
|
|
95
|
+
``test_check_passes_a_clean_sentence``), which construct exactly
|
|
96
|
+
``{"predicates": {"Human": 1, "Mortal": 1}, "constants": ["a", "b"]}``.
|
|
97
|
+
:meth:`Signature.from_dict` accepts this convention 1:1, EXCEPT for the
|
|
98
|
+
multi-arity-iterable case: ``api.check``'s dict describes a permissive
|
|
99
|
+
CONSTRAINT ("P may be used at arity 1 or 2"), whereas a ``Signature``
|
|
100
|
+
DECLARES what a symbol IS — it has no notion of "one of several arities". A
|
|
101
|
+
single-element iterable (``{"P": [1]}`` or ``{"P": {1}}``) is accepted as
|
|
102
|
+
just arity 1 (no information lost); an iterable naming more than one distinct
|
|
103
|
+
arity raises :class:`ValueError` naming the symbol and the arities, per the
|
|
104
|
+
kit's loud-refusal convention (see ``unicode_logic_kit.fol.casl_export``'s
|
|
105
|
+
arity-conflict checks for the same idea applied elsewhere).
|
|
106
|
+
|
|
107
|
+
The richer explicit dict form (what :meth:`Signature.to_dict` emits)
|
|
108
|
+
------------------------------------------------------------------------
|
|
109
|
+
:meth:`Signature.to_dict` always emits full-fidelity, self-describing JSON
|
|
110
|
+
(``json.dumps`` works directly on it) with a STABLE key order (sections in
|
|
111
|
+
declaration order, entries within a section sorted alphabetically by name —
|
|
112
|
+
independent of the ``Mapping``'s own internal order, so two signatures with
|
|
113
|
+
the same content always serialise byte-identically):
|
|
114
|
+
|
|
115
|
+
{
|
|
116
|
+
"predicates": {
|
|
117
|
+
"Loves": {"arity": 2, "arg_sorts": ["Human", "Human"]},
|
|
118
|
+
"Rain": {"arity": 0, "arg_sorts": None},
|
|
119
|
+
},
|
|
120
|
+
"functions": {
|
|
121
|
+
"fatherOf": {"arity": 1, "arg_sorts": ["Human"],
|
|
122
|
+
"result_sort": "Human"},
|
|
123
|
+
},
|
|
124
|
+
"constants": {"alice": "Human", "unsorted_thing": None},
|
|
125
|
+
"sorts": ["Human"],
|
|
126
|
+
"subsorts": {"Human": ["Animal"]},
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
``"subsorts"`` (present only when at least one edge is declared) is the
|
|
130
|
+
CHILD-sort-to-DIRECT-parent-sorts mapping, JSON-rendered as name -> a
|
|
131
|
+
sorted list of parent names (never the transitive closure — see the
|
|
132
|
+
"Subsorting" section below for why only direct edges are the canonical
|
|
133
|
+
form). :meth:`Signature.from_dict` reads it back the same shape, and every
|
|
134
|
+
sort it mentions (child or parent) is unioned into :attr:`Signature.sorts`
|
|
135
|
+
exactly like an ``arg_sorts``/``result_sort``/constant ``sort`` mention.
|
|
136
|
+
|
|
137
|
+
:meth:`Signature.from_dict` recognises this rich per-entry shape (a ``dict``
|
|
138
|
+
with the required key ``"arity"`` for predicates/functions, an optional
|
|
139
|
+
``"arg_sorts"`` list, and — functions only — an optional ``"result_sort"``;
|
|
140
|
+
an unrecognised key inside such an entry, or a ``"result_sort"`` on a
|
|
141
|
+
PREDICATE entry, is refused with :class:`ValueError`) alongside the loose
|
|
142
|
+
convention above, distinguishing the two purely by the shape of each value
|
|
143
|
+
(``int``/iterable → loose; ``dict`` → rich). ``constants`` is rich when the
|
|
144
|
+
section itself is a ``dict`` (name -> sort-or-``None``) and loose when it is
|
|
145
|
+
a bare iterable of names (sort defaults to ``None`` for all of them). The
|
|
146
|
+
top-level ``"sorts"`` key (loose convention has none) supplies sort names not
|
|
147
|
+
otherwise implied by any declaration — e.g. a sort with no symbol
|
|
148
|
+
referencing it yet. Regardless of what ``"sorts"`` says, :attr:`Signature.sorts`
|
|
149
|
+
always also contains every sort mentioned by any ``arg_sorts`` / ``result_sort``
|
|
150
|
+
/ constant ``sort`` / ``subsorts`` in the same dict — the two are unioned,
|
|
151
|
+
never one silently overriding the other. An unrecognised TOP-LEVEL key (a
|
|
152
|
+
typo like ``"predicate"`` for ``"predicates"``) is refused rather than
|
|
153
|
+
silently ignored.
|
|
154
|
+
|
|
155
|
+
``Signature`` constructed directly (the dataclass constructor, not
|
|
156
|
+
``from_dict``/``from_formulas``) does NOT perform this implied-sorts
|
|
157
|
+
derivation — ``sorts`` is then exactly what was passed in, verbatim
|
|
158
|
+
(frozenset-coerced). This keeps the plain constructor simple/predictable; the
|
|
159
|
+
two smart constructors below are where the derivation happens.
|
|
160
|
+
|
|
161
|
+
Signature.from_formulas — scope and what it deliberately does NOT infer
|
|
162
|
+
-----------------------------------------------------------------------
|
|
163
|
+
:meth:`Signature.from_formulas` walks a batch of ASTs and infers:
|
|
164
|
+
|
|
165
|
+
* every predicate's and every function's arity from how it is actually
|
|
166
|
+
applied (an arity conflict ACROSS the batch — the same symbol used with two
|
|
167
|
+
different argument counts somewhere in the batch — is refused with
|
|
168
|
+
:class:`ValueError` naming the symbol and every arity seen);
|
|
169
|
+
* every constant's sort from its :class:`~unicode_logic_kit.fol.nodes.SortedConstant`
|
|
170
|
+
occurrences (a plain, unsorted :class:`~unicode_logic_kit.fol.nodes.Constant`
|
|
171
|
+
occurrence of the same name contributes no sort; a genuine conflict — the
|
|
172
|
+
SAME constant name annotated with two different concrete sorts somewhere in
|
|
173
|
+
the batch — is refused with :class:`ValueError`, the same spirit as the
|
|
174
|
+
arity conflict above);
|
|
175
|
+
* the top-level :attr:`Signature.sorts` set, from every
|
|
176
|
+
:class:`~unicode_logic_kit.fol.nodes.SortedQuantifier`'s and
|
|
177
|
+
:class:`~unicode_logic_kit.fol.nodes.SortedConstant`'s own sort name (a plain,
|
|
178
|
+
unsorted :class:`~unicode_logic_kit.fol.nodes.Quantifier` contributes nothing
|
|
179
|
+
here — there is no ``default_sort`` concept in this module, unlike
|
|
180
|
+
``casl_export``);
|
|
181
|
+
* a name used BOTH as a bare constant (:class:`Constant` or
|
|
182
|
+
:class:`SortedConstant`) and as a function application
|
|
183
|
+
(:class:`~unicode_logic_kit.fol.nodes.Function`) anywhere in the batch is
|
|
184
|
+
refused with :class:`ValueError` — mirroring
|
|
185
|
+
``unicode_logic_kit.fol.casl_export``'s identical refusal, for the identical
|
|
186
|
+
reason: a signature declares at most one meaning per name per namespace,
|
|
187
|
+
and "sometimes a ground term, sometimes an applied function" cannot be
|
|
188
|
+
reconciled into one declaration.
|
|
189
|
+
|
|
190
|
+
It deliberately does **not** attempt ``casl_export``'s full per-ARGUMENT-POSITION
|
|
191
|
+
sort inference (the union-find over ``("pred"|"func", name, i)`` slots
|
|
192
|
+
propagating sort annotations through shared variables and equalities) — every
|
|
193
|
+
:class:`~unicode_logic_kit.fol.signature.PredicateDecl` / :class:`FunctionDecl`
|
|
194
|
+
built by ``from_formulas`` has ``arg_sorts=None`` (unsorted argument
|
|
195
|
+
positions), even when the formulas passed in would, under that fuller
|
|
196
|
+
algorithm, pin down concrete argument sorts. Duplicating that union-find here
|
|
197
|
+
would either diverge from ``casl_export``'s (already correct, already tested)
|
|
198
|
+
implementation or need to import its private internals; the task this module
|
|
199
|
+
was built for scopes ``from_formulas`` to arity + constant-sort + sort-name
|
|
200
|
+
inference only, leaving richer per-argument-position inference as a possible
|
|
201
|
+
FUTURE addition once/if a shared implementation is worth extracting. A
|
|
202
|
+
:class:`Signature` with unsorted argument positions is still fully usable —
|
|
203
|
+
``validate()`` simply never reports a sort mismatch on a position neither
|
|
204
|
+
side has pinned down (see below).
|
|
205
|
+
|
|
206
|
+
Signature.validate — violation vocabulary
|
|
207
|
+
------------------------------------------
|
|
208
|
+
:meth:`Signature.validate` walks a formula (recognising
|
|
209
|
+
:class:`~unicode_logic_kit.fol.nodes.Atom`,
|
|
210
|
+
:class:`~unicode_logic_kit.fol.nodes.Function`,
|
|
211
|
+
:class:`~unicode_logic_kit.fol.nodes.Constant`,
|
|
212
|
+
:class:`~unicode_logic_kit.fol.nodes.SortedConstant`,
|
|
213
|
+
:class:`~unicode_logic_kit.fol.nodes.Variable`,
|
|
214
|
+
:class:`~unicode_logic_kit.fol.nodes.Quantifier`, and
|
|
215
|
+
:class:`~unicode_logic_kit.fol.nodes.SortedQuantifier`, plus the binders that
|
|
216
|
+
introduce a variable the way a quantifier does
|
|
217
|
+
(:class:`~unicode_logic_kit.fol.nodes.Count`,
|
|
218
|
+
:class:`~unicode_logic_kit.fol.nodes.SortedCount`,
|
|
219
|
+
:class:`~unicode_logic_kit.fol.nodes.Cardinality` and
|
|
220
|
+
:class:`~unicode_logic_kit.fol.nodes.SortedCardinality`, whose bound variable
|
|
221
|
+
carries the counting sort, or none for the unsorted forms); every OTHER node
|
|
222
|
+
type — modal/temporal operators, second-order quantifiers,
|
|
223
|
+
Lambda/Application, Measure, the linear/Lambek/team-semantic connectives, …—
|
|
224
|
+
is transparently recursed into via the generic
|
|
225
|
+
:meth:`~unicode_logic_kit.fol.nodes.Node._child_nodes` traversal rather than
|
|
226
|
+
rejected, so an :class:`Atom` buried inside e.g. a modal box or a counting
|
|
227
|
+
quantifier is still checked; there is no ``casl_export``-style fragment
|
|
228
|
+
gate here) and returns a list of precise, human-readable violation strings —
|
|
229
|
+
one per OFFENDING OCCURRENCE (an undeclared symbol used twice in the formula
|
|
230
|
+
produces two separate messages, one per use site; this module never
|
|
231
|
+
deduplicates, so the length of the list is a genuine occurrence count, not
|
|
232
|
+
just a distinct-problem count). An empty list means the formula conforms.
|
|
233
|
+
Five violation shapes are produced, all following the pattern
|
|
234
|
+
``"<kind> '<symbol>' <detail>"``:
|
|
235
|
+
|
|
236
|
+
* ``undeclared predicate 'Foo' (arity 2)`` / ``undeclared function 'bar'
|
|
237
|
+
(arity 1)`` / ``undeclared constant 'alice'`` — the symbol is not a key of
|
|
238
|
+
the corresponding :attr:`Signature.predicates` / :attr:`functions` /
|
|
239
|
+
:attr:`constants` mapping. The six comparison/equality predicates (``=``,
|
|
240
|
+
``≠``, ``<``, ``>``, ``≤``, ``≥``) and the four arithmetic functions (``+``,
|
|
241
|
+
``-``, ``*``, ``/``) are the kit's BUILT-IN operators, never user
|
|
242
|
+
vocabulary (mirroring ``unicode_logic_kit.eval.validate``'s identical
|
|
243
|
+
``_BUILTIN_PREDS`` / ``_BUILTIN_FUNCS`` split — duplicated here as a
|
|
244
|
+
small, independent, literal copy rather than an import, since this module
|
|
245
|
+
is meant to become something ``eval.validate`` itself could eventually
|
|
246
|
+
depend on, and depending the other way around would be backwards) — they
|
|
247
|
+
are never flagged as undeclared, though their OWN arguments are still
|
|
248
|
+
recursed into and checked. The two truth constants are not user vocabulary
|
|
249
|
+
either: the nullary atoms ``$true`` and ``$false`` (and the atoms named
|
|
250
|
+
``⊤`` and ``⊥``, which are the same constants) are never flagged as
|
|
251
|
+
undeclared predicates, and neither :meth:`Signature.from_formulas` nor
|
|
252
|
+
:func:`inventory_of` lists them.
|
|
253
|
+
* ``predicate 'Human' expects arity 1, used with arity 2`` / the ``function``
|
|
254
|
+
equivalent — the symbol IS declared, but this occurrence's argument count
|
|
255
|
+
does not match :attr:`PredicateDecl.arity` / :attr:`FunctionDecl.arity`.
|
|
256
|
+
* ``predicate 'Loves' argument 1 expects sort 'Human', got sort 'Animal'`` /
|
|
257
|
+
the ``function`` equivalent — argument position ``i`` (1-based in the
|
|
258
|
+
message) has a declared sort (``PredicateDecl.arg_sorts[i]`` /
|
|
259
|
+
``FunctionDecl.arg_sorts[i]``) that is not ``None``, AND the term actually
|
|
260
|
+
passed there resolves to a DIFFERENT concrete sort (also not ``None``). A
|
|
261
|
+
term's own concrete sort comes from: a :class:`Variable` bound by an
|
|
262
|
+
enclosing :class:`SortedQuantifier` or sorted counting binder (that
|
|
263
|
+
binder's sort; a plain :class:`Quantifier`, an unsorted counting binder or
|
|
264
|
+
a free variable gives ``None`` — unknown, not a conflict); a :class:`SortedConstant`'s own inline sort annotation; a
|
|
265
|
+
:class:`Constant`'s signature-declared :attr:`ConstantDecl.sort` (``None``
|
|
266
|
+
if undeclared, or if declared unsorted); or a :class:`Function`
|
|
267
|
+
application's declared :attr:`FunctionDecl.result_sort`. Per the task's
|
|
268
|
+
own rule — repeated here because it is the crux of the whole check — **a
|
|
269
|
+
``None`` sort on EITHER side never conflicts**; only two BOTH-concrete,
|
|
270
|
+
DIFFERENT sorts are a violation. This is a local, single-pass check (no
|
|
271
|
+
union-find / propagation across the formula the way ``casl_export``'s sort
|
|
272
|
+
inference does): it only ever compares a declared argument-position sort
|
|
273
|
+
against whatever concrete sort the term passed there ALREADY carries by
|
|
274
|
+
itself (from its own binder or its own signature entry), never propagates
|
|
275
|
+
a sort backward onto an otherwise-unsorted variable.
|
|
276
|
+
* ``constant 'alice' is annotated sort 'Animal' here but declared sort
|
|
277
|
+
'Human' in the signature`` — a :class:`SortedConstant` occurrence's own
|
|
278
|
+
inline sort disagrees with that same name's :attr:`ConstantDecl.sort` in
|
|
279
|
+
the signature (both concrete, both known, and different — the same "both
|
|
280
|
+
sides concrete" rule as above, applied to a constant instead of an
|
|
281
|
+
argument position).
|
|
282
|
+
|
|
283
|
+
Variables never need declaring (per the task's own framing) — a bare
|
|
284
|
+
:class:`Variable` is only ever looked up in the quantifier-scope environment
|
|
285
|
+
built while walking, never checked against :attr:`Signature.constants`; an
|
|
286
|
+
unbound (free) variable simply resolves to sort ``None`` and is silently
|
|
287
|
+
skipped, exactly like an unbound one under a plain :class:`Quantifier`.
|
|
288
|
+
|
|
289
|
+
Subsorting (``S < T``) — a subset-semantics reading, not full order-sorted algebra
|
|
290
|
+
-----------------------------------------------------------------------------------
|
|
291
|
+
:attr:`Signature.subsorts` declares a CHILD sort's DIRECT parent sorts —
|
|
292
|
+
``{"Human": frozenset({"Animal"})}`` reads "Human is declared a subsort of
|
|
293
|
+
Animal". The semantics is the plain SUBSET reading MSFOL relativisation
|
|
294
|
+
already gives a single sort's own universe (see
|
|
295
|
+
:mod:`unicode_logic_kit.semantics.modelfinder`'s "many-sorted" section): a
|
|
296
|
+
subsort edge ``S < T`` means every structure's universe for ``S`` is a
|
|
297
|
+
SUBSET of its universe for ``T`` — ``ext(S) ⊆ ext(T)`` — nothing more. This
|
|
298
|
+
is deliberately NOT full CASL order-sorted algebra: there are no injection/
|
|
299
|
+
retract functions, no ``x as S`` / ``x in S`` casts, and no operation/
|
|
300
|
+
predicate overloading across the hierarchy — a symbol still has exactly one
|
|
301
|
+
declared arity/argument-sort profile, subsort or not (see
|
|
302
|
+
:mod:`unicode_logic_kit.fol.casl_import`'s identical scoping decision on the
|
|
303
|
+
CASL side).
|
|
304
|
+
|
|
305
|
+
``__post_init__`` computes the REFLEXIVE-TRANSITIVE closure of the declared
|
|
306
|
+
DIRECT edges purely to answer :meth:`is_subsort` and to detect a cycle (a
|
|
307
|
+
sort that is, directly or transitively, its own ancestor) — refused with
|
|
308
|
+
:class:`ValueError` naming the cycle path. The closure is a private,
|
|
309
|
+
derived cache (not a dataclass field, so it never participates in equality/
|
|
310
|
+
repr/hashing); :attr:`Signature.subsorts` itself always stays exactly what
|
|
311
|
+
was declared — the DIRECT edges only — which is what
|
|
312
|
+
:func:`unicode_logic_kit.fol.to_fol` needs to emit the minimal axiom set (a
|
|
313
|
+
transitive edge follows from chaining two direct ones' implications, so
|
|
314
|
+
re-emitting it would be redundant, not wrong, but is avoided).
|
|
315
|
+
|
|
316
|
+
:meth:`Signature.validate`'s two "declared vs. actual argument sort"
|
|
317
|
+
violations (the ``predicate``/``function`` "argument ``i`` expects sort"
|
|
318
|
+
messages above) accept a SUBSORT of the declared sort as well as an exact
|
|
319
|
+
match — passing a ``Human``-sorted term where ``Animal`` is declared now
|
|
320
|
+
validates clean once ``Human < Animal`` is declared, but NOT the reverse (an
|
|
321
|
+
``Animal``-sorted term where ``Human`` is declared is still reported):
|
|
322
|
+
subsort substitutability is one-directional, exactly like Liskov
|
|
323
|
+
substitution. The two constant-sort-annotation checks (a
|
|
324
|
+
:class:`SortedConstant` occurrence's own inline sort vs. its declared
|
|
325
|
+
:attr:`ConstantDecl.sort`) stay EXACT-match — those compare one name's own
|
|
326
|
+
two annotations for self-consistency, not a substitutability question.
|
|
327
|
+
|
|
328
|
+
:meth:`Signature.merge` unions ``subsorts`` the same way it unions ``sorts``
|
|
329
|
+
— per CHILD sort, the two sides' direct-parent sets are unioned (never
|
|
330
|
+
flagged as conflicting merely for differing, since two DIFFERENT declared
|
|
331
|
+
parent sets for the same child are both true constraints, not competing
|
|
332
|
+
claims about a single fact the way two different arities would be). The one
|
|
333
|
+
way a merge can still fail is if the UNION of both sides' edges creates a
|
|
334
|
+
cycle that did not exist in either signature alone — caught by the same
|
|
335
|
+
cycle check :meth:`__post_init__` runs on the merged result, so it is named
|
|
336
|
+
exactly like any other cycle.
|
|
337
|
+
|
|
338
|
+
Equality, immutability, and hashability
|
|
339
|
+
------------------------------------------
|
|
340
|
+
:class:`PredicateDecl` / :class:`FunctionDecl` / :class:`ConstantDecl` are
|
|
341
|
+
plain frozen dataclasses over only hashable fields (``str`` / ``int`` /
|
|
342
|
+
``Optional[str]`` / a coerced ``Tuple[Optional[str], ...]``), so they are
|
|
343
|
+
fully immutable AND hashable, and compare equal structurally.
|
|
344
|
+
:class:`Signature` itself is a frozen dataclass too (no attribute can be
|
|
345
|
+
reassigned after construction) whose three symbol-table fields are coerced
|
|
346
|
+
in ``__post_init__`` to :class:`types.MappingProxyType` (a read-only VIEW —
|
|
347
|
+
mutating the dict a caller originally passed in after construction does NOT
|
|
348
|
+
retroactively change the ``Signature``, since a fresh internal ``dict`` is
|
|
349
|
+
built and wrapped) and whose ``sorts`` field is coerced to a genuine
|
|
350
|
+
``frozenset``. Two ``Signature`` values compare equal iff every predicate,
|
|
351
|
+
function, constant, and sort matches — regardless of which constructor built
|
|
352
|
+
them (``from_dict``, ``from_formulas``, ``merge``, or the plain constructor
|
|
353
|
+
all funnel through the same ``__post_init__`` coercion, so construction path
|
|
354
|
+
never affects equality). ``Signature`` is NOT hashable, however (a
|
|
355
|
+
``MappingProxyType`` wrapping a ``dict`` is itself unhashable) — "frozen"
|
|
356
|
+
here means immutable-by-construction, not usable as a ``dict``/``set`` key.
|
|
357
|
+
"""
|
|
358
|
+
|
|
359
|
+
from dataclasses import dataclass, field, replace
|
|
360
|
+
from types import MappingProxyType
|
|
361
|
+
from typing import Dict, FrozenSet, Iterable, List, Mapping, Optional, Set, Tuple
|
|
362
|
+
|
|
363
|
+
from .nodes import (
|
|
364
|
+
Node, Atom, Function, Constant, SortedConstant, Variable,
|
|
365
|
+
Quantifier, SortedQuantifier, SortedCount, SortedCardinality,
|
|
366
|
+
Count, Cardinality,
|
|
367
|
+
)
|
|
368
|
+
from ._truth_constants import is_truth_constant
|
|
369
|
+
|
|
370
|
+
__all__ = ["Signature", "PredicateDecl", "FunctionDecl", "ConstantDecl", "inventory_of"]
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
# Comparison/equality predicates and arithmetic functions are the kit's
|
|
374
|
+
# BUILT-IN operators, not user vocabulary that a Signature declares — see the
|
|
375
|
+
# module docstring's "violation vocabulary" section for why this is a small
|
|
376
|
+
# independent literal copy of unicode_logic_kit.eval.validate's identical sets
|
|
377
|
+
# rather than an import.
|
|
378
|
+
_BUILTIN_PREDS = frozenset({"=", "≠", "<", ">", "≤", "≥"})
|
|
379
|
+
_BUILTIN_FUNCS = frozenset({"+", "-", "*", "/"})
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
# =============================================================================
|
|
383
|
+
# inventory_of — the LENIENT, non-raising counterpart of from_formulas
|
|
384
|
+
# =============================================================================
|
|
385
|
+
|
|
386
|
+
def inventory_of(node: Node):
|
|
387
|
+
"""Return the lenient symbol inventory of ``node``: every user predicate/
|
|
388
|
+
function ``(name, arity)`` pair and every constant name reachable
|
|
389
|
+
anywhere in the tree — NEVER raising, unlike :meth:`Signature.from_formulas`.
|
|
390
|
+
|
|
391
|
+
This is the classification :meth:`Signature.from_formulas` would also
|
|
392
|
+
need to do, minus its refusals: a predicate or function used at two
|
|
393
|
+
different arities somewhere under ``node`` contributes TWO separate
|
|
394
|
+
``(name, arity)`` entries to the returned set (rather than being
|
|
395
|
+
refused as a conflict), and a name used both as a bare constant and as
|
|
396
|
+
an applied function contributes to BOTH the constants set and the
|
|
397
|
+
functions set (rather than being refused as a namespace clash). It
|
|
398
|
+
exists for :mod:`unicode_logic_kit.eval.predicate_match`, whose whole
|
|
399
|
+
purpose is scoring possibly-malformed model output gracefully — a
|
|
400
|
+
genuine vocabulary conflict is exactly the kind of input it must still
|
|
401
|
+
align rather than reject (see that module's docstring).
|
|
402
|
+
|
|
403
|
+
Returns ``(predicates, functions, constants)`` — ``predicates`` /
|
|
404
|
+
``functions`` are sets of ``(name, arity)`` pairs (the kit's built-in
|
|
405
|
+
operators, :data:`_BUILTIN_PREDS` / :data:`_BUILTIN_FUNCS`, and the two truth
|
|
406
|
+
constants ``$true`` / ``$false`` excluded, exactly like
|
|
407
|
+
:meth:`from_formulas`'s classification); ``constants`` is
|
|
408
|
+
a set of names (:class:`Constant` and :class:`SortedConstant`
|
|
409
|
+
occurrences alike — a sorted constant's own inline sort annotation is
|
|
410
|
+
not part of this vocabulary-shape inventory).
|
|
411
|
+
|
|
412
|
+
The walk is :meth:`~unicode_logic_kit.fol.nodes.Node.walk` — every node in
|
|
413
|
+
the tree, pre-order, via the generic ``_child_nodes()`` recursion — so a
|
|
414
|
+
symbol nested under a modal operator, inside a counting quantifier's set
|
|
415
|
+
builder, or under any other construct this module does not itself know
|
|
416
|
+
about is still found. This flat "classify every node, regardless of
|
|
417
|
+
whether it sits in formula or term position" pass needs no
|
|
418
|
+
formula/term dispatch to be complete (unlike :meth:`from_formulas`'s own
|
|
419
|
+
walk, which routes a nested FORMULA field back through its
|
|
420
|
+
formula-walker for the same reason) — every ``Node``-valued field,
|
|
421
|
+
wherever it lives structurally, is a ``walk()`` stop in its own right.
|
|
422
|
+
"""
|
|
423
|
+
predicates: Set[Tuple[str, int]] = set()
|
|
424
|
+
functions: Set[Tuple[str, int]] = set()
|
|
425
|
+
constants: Set[str] = set()
|
|
426
|
+
for n in node.walk():
|
|
427
|
+
if isinstance(n, Atom):
|
|
428
|
+
if n.predicate not in _BUILTIN_PREDS and not is_truth_constant(n):
|
|
429
|
+
predicates.add((n.predicate, len(n.args)))
|
|
430
|
+
elif isinstance(n, Function):
|
|
431
|
+
if n.name not in _BUILTIN_FUNCS:
|
|
432
|
+
functions.add((n.name, len(n.args)))
|
|
433
|
+
elif isinstance(n, (Constant, SortedConstant)):
|
|
434
|
+
constants.add(n.name)
|
|
435
|
+
return predicates, functions, constants
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
# =============================================================================
|
|
439
|
+
# Shared refusal bookkeeping — used by this module's own from_formulas AND by
|
|
440
|
+
# unicode_logic_kit.fol.casl_export's independently-tested _analyze pass (see
|
|
441
|
+
# the module docstring's DESIGN NOTE for why only the comparison-and-raise
|
|
442
|
+
# itself is shared, not each caller's accumulation strategy or wording).
|
|
443
|
+
# =============================================================================
|
|
444
|
+
|
|
445
|
+
def _check_single_valued(values, message: str):
|
|
446
|
+
"""Return the sole element of ``values`` if every member is the same
|
|
447
|
+
value; raise ``ValueError(message)`` if ``values`` names more than one
|
|
448
|
+
distinct value.
|
|
449
|
+
|
|
450
|
+
``values`` is never empty at either call site (both only call this once
|
|
451
|
+
a symbol has been recorded at least once). The shared "this symbol was
|
|
452
|
+
declared inconsistently" predicate: :meth:`Signature.from_formulas`
|
|
453
|
+
calls it once per symbol, over the FULL set of arities/sorts collected
|
|
454
|
+
across an entire formula batch; ``casl_export._analyze`` calls it
|
|
455
|
+
incrementally, per occurrence, over just ``{previously recorded value,
|
|
456
|
+
this occurrence's value}`` — so it can raise as soon as a conflict
|
|
457
|
+
first appears, before any later formula in the batch is even walked.
|
|
458
|
+
"""
|
|
459
|
+
if len(values) > 1:
|
|
460
|
+
raise ValueError(message)
|
|
461
|
+
return next(iter(values))
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def _check_not_dual_use(name: str, other_namespace_names, message: str) -> None:
|
|
465
|
+
"""Raise ``ValueError(message)`` iff ``name`` is a member of
|
|
466
|
+
``other_namespace_names`` — the shared "used as both a constant and a
|
|
467
|
+
function" clash check behind both :meth:`Signature.from_formulas` and
|
|
468
|
+
``casl_export._analyze``'s independent bookkeeping. Each caller keeps
|
|
469
|
+
its own message wording (the two modules describe the same underlying
|
|
470
|
+
fact in their own terms), so only the comparison is shared here.
|
|
471
|
+
"""
|
|
472
|
+
if name in other_namespace_names:
|
|
473
|
+
raise ValueError(message)
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
# =============================================================================
|
|
477
|
+
# Declaration dataclasses
|
|
478
|
+
# =============================================================================
|
|
479
|
+
|
|
480
|
+
def _require_arity(value, name: str, kind: str) -> int:
|
|
481
|
+
"""Return ``value`` coerced to a validated non-negative arity, or refuse.
|
|
482
|
+
|
|
483
|
+
``bool`` is explicitly rejected even though it is a ``int`` subclass in
|
|
484
|
+
Python — an accidental ``True``/``False`` silently read as arity 1/0
|
|
485
|
+
would be exactly the kind of silent-acceptance bug this kit's loud-
|
|
486
|
+
refusal convention exists to catch.
|
|
487
|
+
"""
|
|
488
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
489
|
+
raise TypeError(
|
|
490
|
+
f"{kind} {name!r}: arity must be an int, got {type(value).__name__}."
|
|
491
|
+
)
|
|
492
|
+
if value < 0:
|
|
493
|
+
raise ValueError(f"{kind} {name!r}: arity must be >= 0, got {value}.")
|
|
494
|
+
return value
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
@dataclass(frozen=True)
|
|
498
|
+
class PredicateDecl:
|
|
499
|
+
"""A predicate symbol's declared shape.
|
|
500
|
+
|
|
501
|
+
``arg_sorts``, when given, must have exactly ``arity`` entries, each a
|
|
502
|
+
sort name or ``None`` (that argument position's sort is left
|
|
503
|
+
unconstrained — see the module docstring's "both sides concrete" rule).
|
|
504
|
+
``arg_sorts=None`` (the default) is the canonical "nothing declared"
|
|
505
|
+
form for a fully unsorted predicate — NOT a tuple of ``arity`` ``None``
|
|
506
|
+
entries;
|
|
507
|
+
the two are equivalent to :meth:`Signature.validate` (neither ever
|
|
508
|
+
conflicts with anything), but only the former is what
|
|
509
|
+
:meth:`Signature.from_dict` / :meth:`Signature.from_formulas` produce.
|
|
510
|
+
"""
|
|
511
|
+
|
|
512
|
+
name: str
|
|
513
|
+
arity: int
|
|
514
|
+
arg_sorts: Optional[Tuple[Optional[str], ...]] = None
|
|
515
|
+
|
|
516
|
+
def __post_init__(self):
|
|
517
|
+
object.__setattr__(
|
|
518
|
+
self, "arity", _require_arity(self.arity, self.name, "PredicateDecl"))
|
|
519
|
+
if self.arg_sorts is not None:
|
|
520
|
+
coerced = tuple(self.arg_sorts)
|
|
521
|
+
if len(coerced) != self.arity:
|
|
522
|
+
raise ValueError(
|
|
523
|
+
f"PredicateDecl {self.name!r}: arg_sorts has {len(coerced)} "
|
|
524
|
+
f"entries but arity is {self.arity}."
|
|
525
|
+
)
|
|
526
|
+
object.__setattr__(self, "arg_sorts", coerced)
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
@dataclass(frozen=True)
|
|
530
|
+
class FunctionDecl:
|
|
531
|
+
"""A function symbol's declared shape: like :class:`PredicateDecl`, plus
|
|
532
|
+
an optional ``result_sort`` for the sort of ``name(...)`` itself as a
|
|
533
|
+
term (``None`` — the default — means the result sort is unconstrained)."""
|
|
534
|
+
|
|
535
|
+
name: str
|
|
536
|
+
arity: int
|
|
537
|
+
arg_sorts: Optional[Tuple[Optional[str], ...]] = None
|
|
538
|
+
result_sort: Optional[str] = None
|
|
539
|
+
|
|
540
|
+
def __post_init__(self):
|
|
541
|
+
object.__setattr__(
|
|
542
|
+
self, "arity", _require_arity(self.arity, self.name, "FunctionDecl"))
|
|
543
|
+
if self.arg_sorts is not None:
|
|
544
|
+
coerced = tuple(self.arg_sorts)
|
|
545
|
+
if len(coerced) != self.arity:
|
|
546
|
+
raise ValueError(
|
|
547
|
+
f"FunctionDecl {self.name!r}: arg_sorts has {len(coerced)} "
|
|
548
|
+
f"entries but arity is {self.arity}."
|
|
549
|
+
)
|
|
550
|
+
object.__setattr__(self, "arg_sorts", coerced)
|
|
551
|
+
if self.result_sort is not None and not isinstance(self.result_sort, str):
|
|
552
|
+
raise TypeError(
|
|
553
|
+
f"FunctionDecl {self.name!r}: result_sort must be a str or "
|
|
554
|
+
f"None, got {type(self.result_sort).__name__}."
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
@dataclass(frozen=True)
|
|
559
|
+
class ConstantDecl:
|
|
560
|
+
"""A constant symbol's declared shape: just a name and an optional sort
|
|
561
|
+
(arity is implicitly 0 — a constant is never applied to arguments)."""
|
|
562
|
+
|
|
563
|
+
name: str
|
|
564
|
+
sort: Optional[str] = None
|
|
565
|
+
|
|
566
|
+
def __post_init__(self):
|
|
567
|
+
if self.sort is not None and not isinstance(self.sort, str):
|
|
568
|
+
raise TypeError(
|
|
569
|
+
f"ConstantDecl {self.name!r}: sort must be a str or None, "
|
|
570
|
+
f"got {type(self.sort).__name__}."
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
# =============================================================================
|
|
575
|
+
# Signature: the aggregate carrier
|
|
576
|
+
# =============================================================================
|
|
577
|
+
|
|
578
|
+
def _freeze_section(mapping, decl_cls, kind: str) -> Mapping:
|
|
579
|
+
"""Validate and wrap one symbol-table field as a read-only ``MappingProxyType``.
|
|
580
|
+
|
|
581
|
+
Refuses (``TypeError``) a non-mapping input, a value of the wrong decl
|
|
582
|
+
class, or (``ValueError``) a dict key that does not match that decl's own
|
|
583
|
+
``.name`` — the map key and the declaration's self-reported name must
|
|
584
|
+
agree, or a lookup by key would silently return a decl that describes a
|
|
585
|
+
DIFFERENT symbol.
|
|
586
|
+
"""
|
|
587
|
+
if not isinstance(mapping, Mapping):
|
|
588
|
+
raise TypeError(
|
|
589
|
+
f"Signature: {kind}s must be a dict, got {type(mapping).__name__}."
|
|
590
|
+
)
|
|
591
|
+
out = {}
|
|
592
|
+
for key, decl in mapping.items():
|
|
593
|
+
if not isinstance(decl, decl_cls):
|
|
594
|
+
raise TypeError(
|
|
595
|
+
f"Signature: {kind} entry {key!r} must be a {decl_cls.__name__}, "
|
|
596
|
+
f"got {type(decl).__name__}."
|
|
597
|
+
)
|
|
598
|
+
if decl.name != key:
|
|
599
|
+
raise ValueError(
|
|
600
|
+
f"Signature: {kind} dict key {key!r} does not match "
|
|
601
|
+
f"{decl_cls.__name__}.name {decl.name!r}."
|
|
602
|
+
)
|
|
603
|
+
out[key] = decl
|
|
604
|
+
return MappingProxyType(out)
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
def _freeze_subsorts(mapping) -> Mapping[str, FrozenSet[str]]:
|
|
608
|
+
"""Validate and wrap the ``subsorts`` field as a read-only mapping of
|
|
609
|
+
child sort name -> a ``frozenset`` of its DIRECT parent sort names.
|
|
610
|
+
|
|
611
|
+
Refuses (``TypeError``) a non-mapping input, a non-``str`` child key, or
|
|
612
|
+
a per-child value that is not an iterable of ``str`` parent names.
|
|
613
|
+
Cycle detection happens separately, in :func:`_subsort_closure` (needs
|
|
614
|
+
the whole mapping built first).
|
|
615
|
+
"""
|
|
616
|
+
if not isinstance(mapping, Mapping):
|
|
617
|
+
raise TypeError(
|
|
618
|
+
f"Signature: subsorts must be a dict, got {type(mapping).__name__}."
|
|
619
|
+
)
|
|
620
|
+
out = {}
|
|
621
|
+
for child, parents in mapping.items():
|
|
622
|
+
if not isinstance(child, str):
|
|
623
|
+
raise TypeError(
|
|
624
|
+
f"Signature: subsorts key must be a sort name (str), got "
|
|
625
|
+
f"{type(child).__name__}."
|
|
626
|
+
)
|
|
627
|
+
if isinstance(parents, str) or not isinstance(parents, Iterable):
|
|
628
|
+
raise TypeError(
|
|
629
|
+
f"Signature: subsorts[{child!r}] must be an iterable of "
|
|
630
|
+
f"parent sort names, got {type(parents).__name__}."
|
|
631
|
+
)
|
|
632
|
+
parent_set = frozenset(parents)
|
|
633
|
+
for p in parent_set:
|
|
634
|
+
if not isinstance(p, str):
|
|
635
|
+
raise TypeError(
|
|
636
|
+
f"Signature: subsorts[{child!r}] entries must be sort "
|
|
637
|
+
f"names (str), got {type(p).__name__}."
|
|
638
|
+
)
|
|
639
|
+
out[child] = parent_set
|
|
640
|
+
return MappingProxyType(out)
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def _subsort_closure(direct: Mapping[str, FrozenSet[str]]) -> Mapping[str, FrozenSet[str]]:
|
|
644
|
+
"""Return the transitive (non-reflexive) closure of ``direct`` — child
|
|
645
|
+
sort name -> every ancestor reachable by chaining one or more edges.
|
|
646
|
+
|
|
647
|
+
Raises :class:`ValueError` naming the cycle (e.g. ``"A -> B -> A"``) if
|
|
648
|
+
``direct`` is not a DAG — a sort declared, directly or transitively, as
|
|
649
|
+
its own ancestor is not a coherent subset relation (``ext(A) ⊆ ext(A)``
|
|
650
|
+
trivially, but a genuine cycle among two-or-more DISTINCT declared sorts
|
|
651
|
+
would force them to denote the exact same set, which this module refuses
|
|
652
|
+
loudly rather than silently accepting as "fine, just redundant").
|
|
653
|
+
"""
|
|
654
|
+
memo: Dict[str, FrozenSet[str]] = {}
|
|
655
|
+
|
|
656
|
+
def closure_of(child: str, stack: Tuple[str, ...]) -> FrozenSet[str]:
|
|
657
|
+
if child in memo:
|
|
658
|
+
return memo[child]
|
|
659
|
+
if child in stack:
|
|
660
|
+
cycle = stack[stack.index(child):] + (child,)
|
|
661
|
+
raise ValueError(
|
|
662
|
+
f"Signature: subsorts has a cycle: {' -> '.join(cycle)}."
|
|
663
|
+
)
|
|
664
|
+
ancestors: Set[str] = set()
|
|
665
|
+
for parent in direct.get(child, frozenset()):
|
|
666
|
+
ancestors.add(parent)
|
|
667
|
+
ancestors |= closure_of(parent, stack + (child,))
|
|
668
|
+
memo[child] = frozenset(ancestors)
|
|
669
|
+
return memo[child]
|
|
670
|
+
|
|
671
|
+
for child in direct:
|
|
672
|
+
closure_of(child, ())
|
|
673
|
+
return MappingProxyType(memo)
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
def _merge_subsorts(a: Mapping[str, FrozenSet[str]],
|
|
677
|
+
b: Mapping[str, FrozenSet[str]]) -> Dict[str, FrozenSet[str]]:
|
|
678
|
+
"""Union ``a`` and ``b``'s direct edges, per child sort — see
|
|
679
|
+
:meth:`Signature.merge`'s docstring for why two differing parent sets
|
|
680
|
+
for the same child are unioned rather than flagged as conflicting."""
|
|
681
|
+
out: Dict[str, Set[str]] = {child: set(parents) for child, parents in a.items()}
|
|
682
|
+
for child, parents in b.items():
|
|
683
|
+
out.setdefault(child, set()).update(parents)
|
|
684
|
+
return {child: frozenset(parents) for child, parents in out.items()}
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
@dataclass(frozen=True)
|
|
688
|
+
class Signature:
|
|
689
|
+
"""The kit's declared vocabulary: predicates, functions, constants, sorts.
|
|
690
|
+
|
|
691
|
+
See the module docstring for the full account of every constructor and
|
|
692
|
+
method below. All four fields default to empty (an "empty" signature
|
|
693
|
+
declares NOTHING — every predicate/function/constant use in any formula
|
|
694
|
+
is then reported as undeclared by :meth:`validate`; this is the strict,
|
|
695
|
+
loud-refusal-by-default reading, not a permissive "anything goes" one).
|
|
696
|
+
"""
|
|
697
|
+
|
|
698
|
+
predicates: Mapping[str, PredicateDecl] = field(default_factory=dict)
|
|
699
|
+
functions: Mapping[str, FunctionDecl] = field(default_factory=dict)
|
|
700
|
+
constants: Mapping[str, ConstantDecl] = field(default_factory=dict)
|
|
701
|
+
sorts: FrozenSet[str] = field(default_factory=frozenset)
|
|
702
|
+
#: child sort name -> its DIRECT declared parent sorts (never the
|
|
703
|
+
#: transitive closure — see the module docstring's "Subsorting" section).
|
|
704
|
+
subsorts: Mapping[str, FrozenSet[str]] = field(default_factory=dict)
|
|
705
|
+
|
|
706
|
+
def __post_init__(self):
|
|
707
|
+
object.__setattr__(
|
|
708
|
+
self, "predicates", _freeze_section(self.predicates, PredicateDecl, "predicate"))
|
|
709
|
+
object.__setattr__(
|
|
710
|
+
self, "functions", _freeze_section(self.functions, FunctionDecl, "function"))
|
|
711
|
+
object.__setattr__(
|
|
712
|
+
self, "constants", _freeze_section(self.constants, ConstantDecl, "constant"))
|
|
713
|
+
object.__setattr__(self, "sorts", frozenset(self.sorts))
|
|
714
|
+
direct = _freeze_subsorts(self.subsorts)
|
|
715
|
+
object.__setattr__(self, "subsorts", direct)
|
|
716
|
+
object.__setattr__(self, "_subsort_closure", _subsort_closure(direct))
|
|
717
|
+
|
|
718
|
+
# -------------------------------------------------------------------
|
|
719
|
+
# Constructors
|
|
720
|
+
# -------------------------------------------------------------------
|
|
721
|
+
|
|
722
|
+
@staticmethod
|
|
723
|
+
def from_dict(d: Mapping) -> "Signature":
|
|
724
|
+
"""Build a :class:`Signature` from a plain dict.
|
|
725
|
+
|
|
726
|
+
Accepts both the ``api.check`` loose convention and the richer
|
|
727
|
+
explicit form :meth:`to_dict` emits, per-section and per-entry — see
|
|
728
|
+
the module docstring's "The api.check loose dict convention" and
|
|
729
|
+
"The richer explicit dict form" sections for the exact shapes and
|
|
730
|
+
the refusals (unknown top-level key, unknown key inside a rich
|
|
731
|
+
entry, a multi-arity iterable naming more than one distinct arity,
|
|
732
|
+
a wrong-typed value) this raises on.
|
|
733
|
+
"""
|
|
734
|
+
if not isinstance(d, Mapping):
|
|
735
|
+
raise TypeError(f"Signature.from_dict expects a dict, got {type(d).__name__}.")
|
|
736
|
+
extra = set(d) - _ALLOWED_TOP_KEYS
|
|
737
|
+
if extra:
|
|
738
|
+
raise ValueError(
|
|
739
|
+
f"Signature.from_dict: unexpected top-level key(s) {sorted(extra)} "
|
|
740
|
+
f"(expected a subset of {sorted(_ALLOWED_TOP_KEYS)})."
|
|
741
|
+
)
|
|
742
|
+
predicates = _parse_symbol_section(
|
|
743
|
+
d.get("predicates", {}), PredicateDecl, "predicate", has_result_sort=False)
|
|
744
|
+
functions = _parse_symbol_section(
|
|
745
|
+
d.get("functions", {}), FunctionDecl, "function", has_result_sort=True)
|
|
746
|
+
constants = _parse_constants_section(d.get("constants", {}))
|
|
747
|
+
subsorts = _parse_subsorts_section(d.get("subsorts", {}))
|
|
748
|
+
|
|
749
|
+
declared_sorts = d.get("sorts", ())
|
|
750
|
+
if not isinstance(declared_sorts, (list, tuple, set, frozenset)):
|
|
751
|
+
raise TypeError(
|
|
752
|
+
f"Signature.from_dict: 'sorts' must be an iterable of names, "
|
|
753
|
+
f"got {type(declared_sorts).__name__}."
|
|
754
|
+
)
|
|
755
|
+
sorts = (set(declared_sorts) | _implied_sorts(predicates, functions, constants)
|
|
756
|
+
| set(subsorts) | {p for parents in subsorts.values() for p in parents})
|
|
757
|
+
return Signature(predicates=predicates, functions=functions,
|
|
758
|
+
constants=constants, sorts=frozenset(sorts),
|
|
759
|
+
subsorts=subsorts)
|
|
760
|
+
|
|
761
|
+
@staticmethod
|
|
762
|
+
def from_formulas(formulas: Iterable[Node]) -> "Signature":
|
|
763
|
+
"""Infer a :class:`Signature` from a batch of ASTs.
|
|
764
|
+
|
|
765
|
+
See the module docstring's "Signature.from_formulas — scope and what
|
|
766
|
+
it deliberately does NOT infer" section for exactly what this does
|
|
767
|
+
(arities, constant sorts, the ``sorts`` set) and does not (per-
|
|
768
|
+
argument-position sort inference) attempt, and for the two refusals
|
|
769
|
+
(a cross-formula arity or constant-sort conflict; a name used both
|
|
770
|
+
as a constant and as a function) it raises :class:`ValueError` on.
|
|
771
|
+
|
|
772
|
+
A constant written with TWO sorts (``c:A`` here, ``c:B`` there) is
|
|
773
|
+
one of those refusals, on purpose: a :class:`Signature` is a typed
|
|
774
|
+
DECLARATION, and a declaration gives a constant ONE sort. A formula
|
|
775
|
+
is not a declaration. The routes that decide formulas read such a
|
|
776
|
+
constant as lying in BOTH sorts (the finite model finder draws it
|
|
777
|
+
from the intersection of their universes), so the same input is
|
|
778
|
+
refused here and answered there; declare the constant under one
|
|
779
|
+
sort, or leave it out of the signature and let the formulas speak.
|
|
780
|
+
"""
|
|
781
|
+
pred_arities: Dict[str, set] = {}
|
|
782
|
+
func_arities: Dict[str, set] = {}
|
|
783
|
+
const_sorts: Dict[str, set] = {}
|
|
784
|
+
const_names: set = set()
|
|
785
|
+
func_names: set = set()
|
|
786
|
+
literal_sorts: set = set()
|
|
787
|
+
|
|
788
|
+
def walk_formula(node: Node) -> None:
|
|
789
|
+
if isinstance(node, Atom):
|
|
790
|
+
if node.predicate not in _BUILTIN_PREDS and not is_truth_constant(node):
|
|
791
|
+
pred_arities.setdefault(node.predicate, set()).add(len(node.args))
|
|
792
|
+
for a in node.args:
|
|
793
|
+
walk_term(a)
|
|
794
|
+
return
|
|
795
|
+
if isinstance(node, SortedQuantifier):
|
|
796
|
+
literal_sorts.add(node.sort)
|
|
797
|
+
walk_formula(node.formula)
|
|
798
|
+
return
|
|
799
|
+
if isinstance(node, SortedCount):
|
|
800
|
+
# a sort that occurs only in a sorted counting quantifier is a sort of the signature too
|
|
801
|
+
literal_sorts.add(node.sort)
|
|
802
|
+
for child in node._child_nodes():
|
|
803
|
+
walk_formula(child)
|
|
804
|
+
|
|
805
|
+
def walk_term(node: Node) -> None:
|
|
806
|
+
if isinstance(node, SortedConstant):
|
|
807
|
+
const_names.add(node.name)
|
|
808
|
+
const_sorts.setdefault(node.name, set()).add(node.sort)
|
|
809
|
+
literal_sorts.add(node.sort)
|
|
810
|
+
return
|
|
811
|
+
if isinstance(node, Constant):
|
|
812
|
+
const_names.add(node.name)
|
|
813
|
+
return
|
|
814
|
+
if isinstance(node, Function):
|
|
815
|
+
if node.name not in _BUILTIN_FUNCS:
|
|
816
|
+
func_arities.setdefault(node.name, set()).add(len(node.args))
|
|
817
|
+
func_names.add(node.name)
|
|
818
|
+
for a in node.args:
|
|
819
|
+
walk_term(a)
|
|
820
|
+
return
|
|
821
|
+
# TERM nodes that carry a genuine FORMULA field (the counting
|
|
822
|
+
# comparisons' set builders): route it back through the formula
|
|
823
|
+
# walker — otherwise every predicate used only inside a
|
|
824
|
+
# |{v : …}| body would be invisible (review-confirmed gap).
|
|
825
|
+
formula_field = getattr(node, "formula", None)
|
|
826
|
+
if formula_field is not None:
|
|
827
|
+
if isinstance(node, SortedCardinality):
|
|
828
|
+
literal_sorts.add(node.sort)
|
|
829
|
+
walk_formula(formula_field)
|
|
830
|
+
for child in node._child_nodes():
|
|
831
|
+
if child is not formula_field:
|
|
832
|
+
walk_term(child)
|
|
833
|
+
return
|
|
834
|
+
for child in node._child_nodes():
|
|
835
|
+
walk_term(child)
|
|
836
|
+
|
|
837
|
+
for f in formulas:
|
|
838
|
+
walk_formula(f)
|
|
839
|
+
|
|
840
|
+
clash = const_names & func_names
|
|
841
|
+
if clash:
|
|
842
|
+
name = sorted(clash)[0]
|
|
843
|
+
_check_not_dual_use(
|
|
844
|
+
name, func_names,
|
|
845
|
+
f"Signature.from_formulas: {name!r} is used both as a constant "
|
|
846
|
+
"and as a function across the given formulas — a Signature "
|
|
847
|
+
"cannot declare one name in both namespaces from usage alone."
|
|
848
|
+
)
|
|
849
|
+
|
|
850
|
+
predicates = {}
|
|
851
|
+
for name, arities in pred_arities.items():
|
|
852
|
+
arity = _check_single_valued(
|
|
853
|
+
arities,
|
|
854
|
+
f"Signature.from_formulas: predicate {name!r} used with "
|
|
855
|
+
f"conflicting arities {tuple(sorted(arities))}."
|
|
856
|
+
)
|
|
857
|
+
predicates[name] = PredicateDecl(name, arity)
|
|
858
|
+
|
|
859
|
+
functions = {}
|
|
860
|
+
for name, arities in func_arities.items():
|
|
861
|
+
arity = _check_single_valued(
|
|
862
|
+
arities,
|
|
863
|
+
f"Signature.from_formulas: function {name!r} used with "
|
|
864
|
+
f"conflicting arities {tuple(sorted(arities))}."
|
|
865
|
+
)
|
|
866
|
+
functions[name] = FunctionDecl(name, arity)
|
|
867
|
+
|
|
868
|
+
constants = {}
|
|
869
|
+
for name in const_names:
|
|
870
|
+
seen = const_sorts.get(name, set())
|
|
871
|
+
sort = _check_single_valued(
|
|
872
|
+
seen,
|
|
873
|
+
f"Signature.from_formulas: constant {name!r} used with "
|
|
874
|
+
f"conflicting sorts {tuple(sorted(seen))}."
|
|
875
|
+
) if seen else None
|
|
876
|
+
constants[name] = ConstantDecl(name, sort)
|
|
877
|
+
|
|
878
|
+
return Signature(predicates=predicates, functions=functions,
|
|
879
|
+
constants=constants, sorts=frozenset(literal_sorts))
|
|
880
|
+
|
|
881
|
+
# -------------------------------------------------------------------
|
|
882
|
+
# Serialisation
|
|
883
|
+
# -------------------------------------------------------------------
|
|
884
|
+
|
|
885
|
+
def to_dict(self) -> dict:
|
|
886
|
+
"""Render this signature as the rich, JSON-compatible, key-order-stable dict.
|
|
887
|
+
|
|
888
|
+
See the module docstring's "The richer explicit dict form" section
|
|
889
|
+
for the exact shape. ``Signature.from_dict(sig.to_dict())`` always
|
|
890
|
+
reconstructs a :class:`Signature` equal to ``sig``.
|
|
891
|
+
"""
|
|
892
|
+
predicates = {
|
|
893
|
+
name: {
|
|
894
|
+
"arity": decl.arity,
|
|
895
|
+
"arg_sorts": list(decl.arg_sorts) if decl.arg_sorts is not None else None,
|
|
896
|
+
}
|
|
897
|
+
for name, decl in sorted(self.predicates.items())
|
|
898
|
+
}
|
|
899
|
+
functions = {
|
|
900
|
+
name: {
|
|
901
|
+
"arity": decl.arity,
|
|
902
|
+
"arg_sorts": list(decl.arg_sorts) if decl.arg_sorts is not None else None,
|
|
903
|
+
"result_sort": decl.result_sort,
|
|
904
|
+
}
|
|
905
|
+
for name, decl in sorted(self.functions.items())
|
|
906
|
+
}
|
|
907
|
+
constants = {name: decl.sort for name, decl in sorted(self.constants.items())}
|
|
908
|
+
subsorts = {
|
|
909
|
+
child: sorted(parents) for child, parents in sorted(self.subsorts.items())
|
|
910
|
+
}
|
|
911
|
+
return {
|
|
912
|
+
"predicates": predicates,
|
|
913
|
+
"functions": functions,
|
|
914
|
+
"constants": constants,
|
|
915
|
+
"sorts": sorted(self.sorts),
|
|
916
|
+
"subsorts": subsorts,
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
# -------------------------------------------------------------------
|
|
920
|
+
# Checking
|
|
921
|
+
# -------------------------------------------------------------------
|
|
922
|
+
|
|
923
|
+
def validate(self, formula: Node) -> List[str]:
|
|
924
|
+
"""Return the list of vocabulary violations ``formula`` commits against
|
|
925
|
+
this signature; an empty list means ``formula`` conforms.
|
|
926
|
+
|
|
927
|
+
See the module docstring's "Signature.validate — violation
|
|
928
|
+
vocabulary" section for the exact message shapes and the walk's
|
|
929
|
+
scope (which node types are specially recognised, and how every
|
|
930
|
+
other node type is transparently recursed through rather than
|
|
931
|
+
rejected).
|
|
932
|
+
"""
|
|
933
|
+
violations: List[str] = []
|
|
934
|
+
_walk_formula(formula, {}, self, violations)
|
|
935
|
+
return violations
|
|
936
|
+
|
|
937
|
+
# -------------------------------------------------------------------
|
|
938
|
+
# Subsorting
|
|
939
|
+
# -------------------------------------------------------------------
|
|
940
|
+
|
|
941
|
+
def is_subsort(self, s: str, t: str) -> bool:
|
|
942
|
+
"""True iff ``s`` is ``t`` itself, or a direct-or-transitive subsort
|
|
943
|
+
of ``t`` (reflexive-transitive lookup against the closure computed
|
|
944
|
+
in :meth:`__post_init__` from the declared direct edges).
|
|
945
|
+
|
|
946
|
+
Never symmetric: ``is_subsort("Human", "Animal")`` and
|
|
947
|
+
``is_subsort("Animal", "Human")`` differ once only ``Human <
|
|
948
|
+
Animal`` is declared (see the module docstring's "Subsorting"
|
|
949
|
+
section).
|
|
950
|
+
"""
|
|
951
|
+
if s == t:
|
|
952
|
+
return True
|
|
953
|
+
return t in self._subsort_closure.get(s, frozenset())
|
|
954
|
+
|
|
955
|
+
# -------------------------------------------------------------------
|
|
956
|
+
# Combining
|
|
957
|
+
# -------------------------------------------------------------------
|
|
958
|
+
|
|
959
|
+
def merge(self, other: "Signature") -> "Signature":
|
|
960
|
+
"""Return the union of ``self`` and ``other``.
|
|
961
|
+
|
|
962
|
+
Every predicate/function/constant/sort present in either input is
|
|
963
|
+
present in the result. A symbol declared by BOTH sides with
|
|
964
|
+
DIFFERING declarations (different arity, ``arg_sorts``,
|
|
965
|
+
``result_sort``, or ``sort``) is a genuine conflict and raises
|
|
966
|
+
:class:`ValueError` naming the symbol and both declarations; a
|
|
967
|
+
symbol declared identically by both sides merges without complaint.
|
|
968
|
+
Sorts merge with a plain set union (a bare sort NAME can never
|
|
969
|
+
itself conflict with another). ``subsorts`` merges the same way,
|
|
970
|
+
per-child-sort union of direct parents (see the module docstring's
|
|
971
|
+
"Subsorting" section for why two differing parent sets are never
|
|
972
|
+
themselves a conflict, and the one way a merge can still fail: the
|
|
973
|
+
union creating a cycle neither side had alone).
|
|
974
|
+
"""
|
|
975
|
+
predicates = _merge_section(self.predicates, other.predicates, "predicate")
|
|
976
|
+
functions = _merge_section(self.functions, other.functions, "function")
|
|
977
|
+
constants = _merge_section(self.constants, other.constants, "constant")
|
|
978
|
+
subsorts = _merge_subsorts(self.subsorts, other.subsorts)
|
|
979
|
+
return Signature(predicates=predicates, functions=functions,
|
|
980
|
+
constants=constants, sorts=self.sorts | other.sorts,
|
|
981
|
+
subsorts=subsorts)
|
|
982
|
+
|
|
983
|
+
|
|
984
|
+
# =============================================================================
|
|
985
|
+
# from_dict helpers
|
|
986
|
+
# =============================================================================
|
|
987
|
+
|
|
988
|
+
_ALLOWED_TOP_KEYS = frozenset(
|
|
989
|
+
{"predicates", "functions", "constants", "sorts", "subsorts"})
|
|
990
|
+
|
|
991
|
+
|
|
992
|
+
def _parse_subsorts_section(section) -> dict:
|
|
993
|
+
"""Parse the ``subsorts`` section: name -> an iterable of direct parent
|
|
994
|
+
sort names. Cycle detection is left to :class:`Signature`'s own
|
|
995
|
+
``__post_init__`` (needs the fully-assembled mapping)."""
|
|
996
|
+
if not isinstance(section, Mapping):
|
|
997
|
+
raise TypeError(
|
|
998
|
+
f"Signature.from_dict: 'subsorts' must be a dict, got "
|
|
999
|
+
f"{type(section).__name__}."
|
|
1000
|
+
)
|
|
1001
|
+
out = {}
|
|
1002
|
+
for child, parents in section.items():
|
|
1003
|
+
if not isinstance(parents, (list, tuple, set, frozenset)):
|
|
1004
|
+
raise TypeError(
|
|
1005
|
+
f"Signature.from_dict: subsorts[{child!r}] must be an "
|
|
1006
|
+
f"iterable of parent sort names, got {type(parents).__name__}."
|
|
1007
|
+
)
|
|
1008
|
+
out[child] = frozenset(parents)
|
|
1009
|
+
return out
|
|
1010
|
+
|
|
1011
|
+
|
|
1012
|
+
def _parse_symbol_section(section, decl_cls, kind: str, has_result_sort: bool) -> dict:
|
|
1013
|
+
"""Parse one ``predicates``/``functions`` section, loose OR rich per entry."""
|
|
1014
|
+
if not isinstance(section, Mapping):
|
|
1015
|
+
raise TypeError(
|
|
1016
|
+
f"Signature.from_dict: {kind}s section must be a dict, got "
|
|
1017
|
+
f"{type(section).__name__}."
|
|
1018
|
+
)
|
|
1019
|
+
allowed_keys = {"arity", "arg_sorts", "result_sort"} if has_result_sort \
|
|
1020
|
+
else {"arity", "arg_sorts"}
|
|
1021
|
+
out = {}
|
|
1022
|
+
for name, value in section.items():
|
|
1023
|
+
if isinstance(value, dict):
|
|
1024
|
+
if "arity" not in value:
|
|
1025
|
+
raise ValueError(
|
|
1026
|
+
f"Signature.from_dict: {kind} {name!r} rich entry is "
|
|
1027
|
+
"missing the required key 'arity'."
|
|
1028
|
+
)
|
|
1029
|
+
extra = set(value) - allowed_keys
|
|
1030
|
+
if extra:
|
|
1031
|
+
raise ValueError(
|
|
1032
|
+
f"Signature.from_dict: {kind} {name!r} entry has "
|
|
1033
|
+
f"unexpected key(s) {sorted(extra)}."
|
|
1034
|
+
)
|
|
1035
|
+
arity = _require_arity(value["arity"], name, kind)
|
|
1036
|
+
raw_arg_sorts = value.get("arg_sorts")
|
|
1037
|
+
arg_sorts = tuple(raw_arg_sorts) if raw_arg_sorts is not None else None
|
|
1038
|
+
result_sort = value.get("result_sort") if has_result_sort else None
|
|
1039
|
+
elif isinstance(value, (list, tuple, set, frozenset)):
|
|
1040
|
+
arities = sorted({_require_arity(v, name, kind) for v in value})
|
|
1041
|
+
if not arities:
|
|
1042
|
+
raise ValueError(
|
|
1043
|
+
f"Signature.from_dict: {kind} {name!r} has an empty arity set."
|
|
1044
|
+
)
|
|
1045
|
+
if len(arities) > 1:
|
|
1046
|
+
raise ValueError(
|
|
1047
|
+
f"Signature.from_dict: {kind} {name!r} lists multiple "
|
|
1048
|
+
f"allowed arities {tuple(arities)} — a Signature declares "
|
|
1049
|
+
"exactly one arity per symbol (api.check's multi-arity "
|
|
1050
|
+
"CONSTRAINT has no Signature DECLARATION equivalent)."
|
|
1051
|
+
)
|
|
1052
|
+
arity = arities[0]
|
|
1053
|
+
arg_sorts = None
|
|
1054
|
+
result_sort = None
|
|
1055
|
+
else:
|
|
1056
|
+
arity = _require_arity(value, name, kind)
|
|
1057
|
+
arg_sorts = None
|
|
1058
|
+
result_sort = None
|
|
1059
|
+
kwargs = {"name": name, "arity": arity, "arg_sorts": arg_sorts}
|
|
1060
|
+
if has_result_sort:
|
|
1061
|
+
kwargs["result_sort"] = result_sort
|
|
1062
|
+
out[name] = decl_cls(**kwargs)
|
|
1063
|
+
return out
|
|
1064
|
+
|
|
1065
|
+
|
|
1066
|
+
def _parse_constants_section(section) -> dict:
|
|
1067
|
+
"""Parse the ``constants`` section: a dict (rich, name -> sort-or-None)
|
|
1068
|
+
or a bare iterable of names (loose, every constant unsorted)."""
|
|
1069
|
+
if isinstance(section, Mapping):
|
|
1070
|
+
out = {}
|
|
1071
|
+
for name, sort in section.items():
|
|
1072
|
+
out[name] = ConstantDecl(name, sort)
|
|
1073
|
+
return out
|
|
1074
|
+
if isinstance(section, (list, tuple, set, frozenset)):
|
|
1075
|
+
out = {}
|
|
1076
|
+
for name in section:
|
|
1077
|
+
if not isinstance(name, str):
|
|
1078
|
+
raise TypeError(
|
|
1079
|
+
f"Signature.from_dict: constants entries must be strings, "
|
|
1080
|
+
f"got {type(name).__name__}."
|
|
1081
|
+
)
|
|
1082
|
+
out[name] = ConstantDecl(name, None)
|
|
1083
|
+
return out
|
|
1084
|
+
raise TypeError(
|
|
1085
|
+
f"Signature.from_dict: constants section must be a dict or an "
|
|
1086
|
+
f"iterable of names, got {type(section).__name__}."
|
|
1087
|
+
)
|
|
1088
|
+
|
|
1089
|
+
|
|
1090
|
+
def _implied_sorts(predicates: dict, functions: dict, constants: dict) -> set:
|
|
1091
|
+
"""Every sort name mentioned by any parsed decl's arg_sorts/result_sort/sort."""
|
|
1092
|
+
sorts = set()
|
|
1093
|
+
for decl in predicates.values():
|
|
1094
|
+
if decl.arg_sorts:
|
|
1095
|
+
sorts.update(s for s in decl.arg_sorts if s is not None)
|
|
1096
|
+
for decl in functions.values():
|
|
1097
|
+
if decl.arg_sorts:
|
|
1098
|
+
sorts.update(s for s in decl.arg_sorts if s is not None)
|
|
1099
|
+
if decl.result_sort is not None:
|
|
1100
|
+
sorts.add(decl.result_sort)
|
|
1101
|
+
for decl in constants.values():
|
|
1102
|
+
if decl.sort is not None:
|
|
1103
|
+
sorts.add(decl.sort)
|
|
1104
|
+
return sorts
|
|
1105
|
+
|
|
1106
|
+
|
|
1107
|
+
# =============================================================================
|
|
1108
|
+
# merge helper
|
|
1109
|
+
# =============================================================================
|
|
1110
|
+
|
|
1111
|
+
def _normalized_decl(decl):
|
|
1112
|
+
"""A decl with vacuous sort annotations folded away, for comparison.
|
|
1113
|
+
|
|
1114
|
+
``arg_sorts=None`` and ``arg_sorts=(None,) * arity`` are behaviourally
|
|
1115
|
+
identical to :meth:`Signature.validate` (neither ever conflicts with
|
|
1116
|
+
anything), so :func:`_merge_section` must treat them as the SAME
|
|
1117
|
+
declaration — plain dataclass equality does not (review-confirmed:
|
|
1118
|
+
``from_dict({'arg_sorts': [None]})`` vs a bare decl raised a spurious
|
|
1119
|
+
merge conflict).
|
|
1120
|
+
"""
|
|
1121
|
+
arg_sorts = getattr(decl, "arg_sorts", None)
|
|
1122
|
+
if arg_sorts is not None and all(s is None for s in arg_sorts):
|
|
1123
|
+
decl = replace(decl, arg_sorts=None)
|
|
1124
|
+
return decl
|
|
1125
|
+
|
|
1126
|
+
|
|
1127
|
+
def _merge_section(a: Mapping, b: Mapping, kind: str) -> dict:
|
|
1128
|
+
out = dict(a)
|
|
1129
|
+
for name, decl in b.items():
|
|
1130
|
+
if name in out and _normalized_decl(out[name]) != _normalized_decl(decl):
|
|
1131
|
+
raise ValueError(
|
|
1132
|
+
f"Signature.merge: conflicting {kind} declaration for "
|
|
1133
|
+
f"{name!r}: {out[name]!r} vs {decl!r}."
|
|
1134
|
+
)
|
|
1135
|
+
out.setdefault(name, decl)
|
|
1136
|
+
return out
|
|
1137
|
+
|
|
1138
|
+
|
|
1139
|
+
# =============================================================================
|
|
1140
|
+
# validate() helpers — a single-pass, generically-recursive walk
|
|
1141
|
+
# =============================================================================
|
|
1142
|
+
|
|
1143
|
+
def _walk_formula(node: Node, env: Dict[str, Optional[str]], sig: Signature,
|
|
1144
|
+
violations: List[str]) -> None:
|
|
1145
|
+
"""Walk a formula node, threading the sorted-variable environment.
|
|
1146
|
+
|
|
1147
|
+
Recognises :class:`Atom` (checked via :func:`_check_atom`),
|
|
1148
|
+
:class:`SortedQuantifier` and :class:`Quantifier` (both extend ``env``
|
|
1149
|
+
for their body — sorted with a concrete sort, plain with ``None``,
|
|
1150
|
+
correctly shadowing an outer binding of the same variable name), and the
|
|
1151
|
+
counting binders :class:`Count` and :class:`SortedCount`, which extend
|
|
1152
|
+
``env`` the same way (the set-builder terms are handled in
|
|
1153
|
+
:func:`_term_sort`); every
|
|
1154
|
+
other node type is transparently recursed into via
|
|
1155
|
+
``node._child_nodes()`` with the SAME ``env`` — this is what lets an
|
|
1156
|
+
``Atom`` nested under a modal/temporal/counting/… node still get
|
|
1157
|
+
checked, without this module needing to know about every node class in
|
|
1158
|
+
the kit.
|
|
1159
|
+
"""
|
|
1160
|
+
if isinstance(node, Atom):
|
|
1161
|
+
_check_atom(node, env, sig, violations)
|
|
1162
|
+
return
|
|
1163
|
+
if isinstance(node, SortedQuantifier):
|
|
1164
|
+
new_env = dict(env)
|
|
1165
|
+
new_env[node.variable.name] = node.sort
|
|
1166
|
+
_walk_formula(node.formula, new_env, sig, violations)
|
|
1167
|
+
return
|
|
1168
|
+
if isinstance(node, Quantifier):
|
|
1169
|
+
new_env = dict(env)
|
|
1170
|
+
new_env[node.variable.name] = None
|
|
1171
|
+
_walk_formula(node.formula, new_env, sig, violations)
|
|
1172
|
+
return
|
|
1173
|
+
if isinstance(node, (SortedCount, Count)):
|
|
1174
|
+
# A counting quantifier binds its variable exactly as a quantifier
|
|
1175
|
+
# does, so an atom in its matrix sees the variable at the counting
|
|
1176
|
+
# sort (``None`` for the unsorted form).
|
|
1177
|
+
new_env = dict(env)
|
|
1178
|
+
new_env[node.variable.name] = getattr(node, "sort", None)
|
|
1179
|
+
_walk_formula(node.formula, new_env, sig, violations)
|
|
1180
|
+
return
|
|
1181
|
+
for child in node._child_nodes():
|
|
1182
|
+
_walk_formula(child, env, sig, violations)
|
|
1183
|
+
|
|
1184
|
+
|
|
1185
|
+
def _check_atom(node: Atom, env: Dict[str, Optional[str]], sig: Signature,
|
|
1186
|
+
violations: List[str]) -> None:
|
|
1187
|
+
"""Check one Atom occurrence: declaredness, arity, and per-argument sort."""
|
|
1188
|
+
arity = len(node.args)
|
|
1189
|
+
if node.predicate in _BUILTIN_PREDS:
|
|
1190
|
+
for arg in node.args:
|
|
1191
|
+
_term_sort(arg, env, sig, violations)
|
|
1192
|
+
return
|
|
1193
|
+
if is_truth_constant(node):
|
|
1194
|
+
return # `$true` / `$false` (and ``⊤`` / ``⊥``): no argument, no vocabulary to declare
|
|
1195
|
+
decl = sig.predicates.get(node.predicate)
|
|
1196
|
+
if decl is None:
|
|
1197
|
+
violations.append(f"undeclared predicate '{node.predicate}' (arity {arity})")
|
|
1198
|
+
for arg in node.args:
|
|
1199
|
+
_term_sort(arg, env, sig, violations)
|
|
1200
|
+
return
|
|
1201
|
+
if decl.arity != arity:
|
|
1202
|
+
violations.append(
|
|
1203
|
+
f"predicate '{node.predicate}' expects arity {decl.arity}, used "
|
|
1204
|
+
f"with arity {arity}"
|
|
1205
|
+
)
|
|
1206
|
+
for i, arg in enumerate(node.args):
|
|
1207
|
+
arg_sort = _term_sort(arg, env, sig, violations)
|
|
1208
|
+
if decl.arg_sorts is not None and i < len(decl.arg_sorts):
|
|
1209
|
+
declared = decl.arg_sorts[i]
|
|
1210
|
+
if declared is not None and arg_sort is not None and not (
|
|
1211
|
+
arg_sort == declared or sig.is_subsort(arg_sort, declared)
|
|
1212
|
+
):
|
|
1213
|
+
violations.append(
|
|
1214
|
+
f"predicate '{node.predicate}' argument {i + 1} expects "
|
|
1215
|
+
f"sort '{declared}', got sort '{arg_sort}'"
|
|
1216
|
+
)
|
|
1217
|
+
|
|
1218
|
+
|
|
1219
|
+
def _term_sort(node: Node, env: Dict[str, Optional[str]], sig: Signature,
|
|
1220
|
+
violations: List[str]) -> Optional[str]:
|
|
1221
|
+
"""Check one term occurrence and return its resolved concrete sort (or
|
|
1222
|
+
``None`` if unknown/unsorted) — see the module docstring for the rules
|
|
1223
|
+
determining a term's concrete sort per node kind."""
|
|
1224
|
+
if isinstance(node, Variable):
|
|
1225
|
+
return env.get(node.name)
|
|
1226
|
+
if isinstance(node, SortedConstant):
|
|
1227
|
+
decl = sig.constants.get(node.name)
|
|
1228
|
+
if decl is None:
|
|
1229
|
+
violations.append(f"undeclared constant '{node.name}'")
|
|
1230
|
+
elif decl.sort is not None and decl.sort != node.sort:
|
|
1231
|
+
violations.append(
|
|
1232
|
+
f"constant '{node.name}' is annotated sort '{node.sort}' "
|
|
1233
|
+
f"here but declared sort '{decl.sort}' in the signature"
|
|
1234
|
+
)
|
|
1235
|
+
return node.sort
|
|
1236
|
+
if isinstance(node, Constant):
|
|
1237
|
+
decl = sig.constants.get(node.name)
|
|
1238
|
+
if decl is None:
|
|
1239
|
+
violations.append(f"undeclared constant '{node.name}'")
|
|
1240
|
+
return None
|
|
1241
|
+
return decl.sort
|
|
1242
|
+
if isinstance(node, Function):
|
|
1243
|
+
arity = len(node.args)
|
|
1244
|
+
if node.name in _BUILTIN_FUNCS:
|
|
1245
|
+
for arg in node.args:
|
|
1246
|
+
_term_sort(arg, env, sig, violations)
|
|
1247
|
+
return None
|
|
1248
|
+
decl = sig.functions.get(node.name)
|
|
1249
|
+
if decl is None:
|
|
1250
|
+
violations.append(f"undeclared function '{node.name}' (arity {arity})")
|
|
1251
|
+
for arg in node.args:
|
|
1252
|
+
_term_sort(arg, env, sig, violations)
|
|
1253
|
+
return None
|
|
1254
|
+
if decl.arity != arity:
|
|
1255
|
+
violations.append(
|
|
1256
|
+
f"function '{node.name}' expects arity {decl.arity}, used "
|
|
1257
|
+
f"with arity {arity}"
|
|
1258
|
+
)
|
|
1259
|
+
for i, arg in enumerate(node.args):
|
|
1260
|
+
arg_sort = _term_sort(arg, env, sig, violations)
|
|
1261
|
+
if decl.arg_sorts is not None and i < len(decl.arg_sorts):
|
|
1262
|
+
declared = decl.arg_sorts[i]
|
|
1263
|
+
if declared is not None and arg_sort is not None and not (
|
|
1264
|
+
arg_sort == declared or sig.is_subsort(arg_sort, declared)
|
|
1265
|
+
):
|
|
1266
|
+
violations.append(
|
|
1267
|
+
f"function '{node.name}' argument {i + 1} expects "
|
|
1268
|
+
f"sort '{declared}', got sort '{arg_sort}'"
|
|
1269
|
+
)
|
|
1270
|
+
return decl.result_sort
|
|
1271
|
+
# A TERM node with a genuine FORMULA field (Cardinality/SortedCardinality
|
|
1272
|
+
# set builders): the buried Atoms must go back through the FORMULA
|
|
1273
|
+
# walker or they escape declaredness/arity/sort checking entirely
|
|
1274
|
+
# (review-confirmed: '|{v : Votes(x, v)}| > …' validated as clean
|
|
1275
|
+
# against a Signature declaring Votes at the wrong arity).
|
|
1276
|
+
formula_field = getattr(node, "formula", None)
|
|
1277
|
+
if formula_field is not None:
|
|
1278
|
+
inner_env = env
|
|
1279
|
+
if isinstance(node, (SortedCardinality, Cardinality)):
|
|
1280
|
+
# The set builder binds its variable over the matrix.
|
|
1281
|
+
inner_env = dict(env)
|
|
1282
|
+
inner_env[node.variable.name] = getattr(node, "sort", None)
|
|
1283
|
+
_walk_formula(formula_field, inner_env, sig, violations)
|
|
1284
|
+
for child in node._child_nodes():
|
|
1285
|
+
if child is not formula_field:
|
|
1286
|
+
_term_sort(child, env, sig, violations)
|
|
1287
|
+
return None
|
|
1288
|
+
for child in node._child_nodes():
|
|
1289
|
+
_term_sort(child, env, sig, violations)
|
|
1290
|
+
return None
|