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,663 @@
|
|
|
1
|
+
"""Canonical form and canonical exact match for NL→FOL evaluation.
|
|
2
|
+
|
|
3
|
+
``canonicalize`` maps a formula to a normal form that quotients out exactly the
|
|
4
|
+
"free" syntactic differences that should NOT count as a mismatch when comparing
|
|
5
|
+
a predicted FOL formula against a reference one, while staying LOGICALLY
|
|
6
|
+
EQUIVALENT to the input. It normalizes four — and only four — kinds of
|
|
7
|
+
difference:
|
|
8
|
+
|
|
9
|
+
(a) ALPHA-renaming of bound variables (Quantifier / SortedQuantifier bound
|
|
10
|
+
Variable, Lambda param, and the counting-quantifier / set-cardinality
|
|
11
|
+
binders Count and Cardinality, which likewise bind a Variable over their
|
|
12
|
+
matrix): two formulas that differ only in the names of their bound
|
|
13
|
+
variables canonicalize identically. Bound variables are
|
|
14
|
+
renamed to a deterministic scheme (``q0``, ``q1``, … — a single lowercase
|
|
15
|
+
letter plus digits, which is a valid VARIABLE token and therefore
|
|
16
|
+
round-trips through the parser; underscores are deliberately avoided
|
|
17
|
+
because they do not round-trip). The rename is capture-safe.
|
|
18
|
+
|
|
19
|
+
(b) COMMUTATIVITY + ASSOCIATIVITY of the commutative connectives: classical
|
|
20
|
+
And, Or, Iff, Xor and fuzzy WeakConjunction, WeakDisjunction,
|
|
21
|
+
StrongConjunction, StrongDisjunction. Nested same-class chains are
|
|
22
|
+
flattened, each operand is canonicalized, the operands are sorted by a
|
|
23
|
+
deterministic alpha-invariant key, and the group is rebuilt left-folded.
|
|
24
|
+
Identical operands — the same formula up to the names of bound variables,
|
|
25
|
+
so ``P(alice) ∧ P(bob)`` keeps both — are de-duplicated (``P ∧ P`` → ``P``)
|
|
26
|
+
ONLY for the IDEMPOTENT connectives — classical And/Or and fuzzy WeakConjunction (min)
|
|
27
|
+
/ WeakDisjunction (max), where ``a ⋆ a ≡ a`` makes removal
|
|
28
|
+
equivalence-preserving. The remaining commutative connectives are NOT
|
|
29
|
+
idempotent (``P ⊕ P ≡ ⊥``, ``P ↔ P ≡ ⊤``, and the Łukasiewicz strong
|
|
30
|
+
conjunction/disjunction t-norm/t-conorm), so their repeated operands are
|
|
31
|
+
kept intact; only flatten + sort applies to them. Implies and
|
|
32
|
+
LukImplication are NOT commutative, so their operand order is preserved.
|
|
33
|
+
|
|
34
|
+
(c) DOUBLE NEGATION: ``Not(Not(x))`` → ``x`` and ``LukNegation(LukNegation(x))``
|
|
35
|
+
→ ``x`` (Łukasiewicz negation is involutive: 1−(1−x)=x).
|
|
36
|
+
|
|
37
|
+
It does NOT perform distributivity, CNF/DNF conversion, or any full
|
|
38
|
+
logical-equivalence decision. ``canonicalize`` is a normal form for EXACTLY the
|
|
39
|
+
set {alpha, commutativity, associativity, operand-dedup, double-negation}; it
|
|
40
|
+
sits strictly between raw structural equality (``==`` on frozen nodes) and full
|
|
41
|
+
logical equivalence. Two formulas with the same canonical form are logically
|
|
42
|
+
equivalent, but logically-equivalent formulas need not share a canonical form
|
|
43
|
+
(e.g. ``P → Q`` and ``¬P ∨ Q`` canonicalize differently).
|
|
44
|
+
|
|
45
|
+
REQUIRED INVARIANTS (each is exercised by tests/test_canonical.py):
|
|
46
|
+
P1 equivalence-preserving: ``canonicalize(f)`` is logically equivalent to f.
|
|
47
|
+
P2 idempotent: ``canonicalize(canonicalize(f)) == canonicalize(f)``.
|
|
48
|
+
P3 alpha-invariance: renaming f's bound variables does not change
|
|
49
|
+
``canonicalize(f)``.
|
|
50
|
+
P4 comm/assoc-invariance: reordering or reassociating the operands of the
|
|
51
|
+
commutative connectives does not change the result.
|
|
52
|
+
P5 double-negation: ``canonicalize(¬¬f) == canonicalize(f)``.
|
|
53
|
+
|
|
54
|
+
Implementation note on the alpha-vs-sort ordering interplay: the operand sort
|
|
55
|
+
key must be invariant under bound-variable renaming, otherwise sorting and
|
|
56
|
+
alpha-renaming would race. The key is therefore built from the operand's tree
|
|
57
|
+
directly (see ``_sort_key``), not from its printed text: every variable occurrence
|
|
58
|
+
is encoded by POSITION (the binder it belongs to), never by its name, and a final
|
|
59
|
+
deterministic alpha-normalization pass then renames the bound variables of the
|
|
60
|
+
fully structured tree. Because the sort key is alpha-invariant and the final pass
|
|
61
|
+
assigns names by binder-encounter order on a now-stable structure, the whole
|
|
62
|
+
pipeline reaches a fixpoint — idempotency (P2) guards against any residual
|
|
63
|
+
ordering bug.
|
|
64
|
+
|
|
65
|
+
The same key decides which operands are DUPLICATES, so it has to tell apart every
|
|
66
|
+
two operands that are not the same formula up to the names of bound variables:
|
|
67
|
+
a constant, a numeral, a sorted constant, a nominal, a predicate term, an agent,
|
|
68
|
+
a group of agents, the type or bound name of a second-order quantifier — every
|
|
69
|
+
field of a node that is not itself a child node goes into the key, by name and
|
|
70
|
+
value. Two operands that differ in any of them are two operands, never one.
|
|
71
|
+
|
|
72
|
+
Which binders are encoded by position (and so renamed by the final pass, P3):
|
|
73
|
+
Quantifier, SortedQuantifier, Lambda, Count, Cardinality, SortedCount,
|
|
74
|
+
SortedCardinality and SlashedExists (with the names of its slash set). Which are
|
|
75
|
+
encoded by NAME: every other binder — SecondOrderQuantifier (its bound predicate
|
|
76
|
+
name is an ordinary field) and the hybrid binder Down (its bound state variable is
|
|
77
|
+
a Nominal, recorded by its name) — and any binder added to the kit later until it
|
|
78
|
+
is taught to ``_alpha``. That is the safe direction: two operands that differ only
|
|
79
|
+
by the renaming of such a binder are kept as two operands, which can cost a match
|
|
80
|
+
but never produces a wrong one. Their bound names are not renamed, so P3 does not
|
|
81
|
+
extend to them.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
import dataclasses
|
|
85
|
+
from typing import Any, List
|
|
86
|
+
|
|
87
|
+
from unicode_logic_kit.fol.nodes import (
|
|
88
|
+
Node, Variable,
|
|
89
|
+
And, Or, Xor, Iff, Implies,
|
|
90
|
+
Not, Quantifier, SortedQuantifier,
|
|
91
|
+
Count, Cardinality, SortedCount, SortedCardinality,
|
|
92
|
+
SlashedExists,
|
|
93
|
+
WeakConjunction, WeakDisjunction, StrongConjunction, StrongDisjunction,
|
|
94
|
+
LukNegation, LukImplication, LukEquivalence,
|
|
95
|
+
Lambda, LambdaVar,
|
|
96
|
+
free_variables,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# Commutative connective classes: associative + commutative, so their operand
|
|
101
|
+
# groups may be flattened, sorted, and rebuilt left-folded. Implies /
|
|
102
|
+
# LukImplication and the (non-commutative) negations are deliberately excluded.
|
|
103
|
+
_COMMUTATIVE = (
|
|
104
|
+
And, Or, Xor, Iff,
|
|
105
|
+
WeakConjunction, WeakDisjunction, StrongConjunction, StrongDisjunction,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# IDEMPOTENT commutative connectives — exactly those for which ``a ⋆ a ≡ a``, so
|
|
109
|
+
# that de-duplicating repeated operands is equivalence-preserving:
|
|
110
|
+
# And / Or (classical ∧ ∨ are idempotent)
|
|
111
|
+
# WeakConjunction / WeakDisjunction (fuzzy min / max are idempotent)
|
|
112
|
+
# The remaining commutative connectives are NOT idempotent and MUST NOT be
|
|
113
|
+
# de-duplicated:
|
|
114
|
+
# Xor / StrongDisjunction (⊕): a ⊕ a ≡ ⊥ (classical) / x⊕x = min{1,2x} (fuzzy)
|
|
115
|
+
# Iff (↔): a ↔ a ≡ ⊤
|
|
116
|
+
# StrongConjunction (⊗): x⊗x = max{0,2x−1} ≢ x
|
|
117
|
+
# Removing a duplicate from any of these would change the truth value, breaking
|
|
118
|
+
# the equivalence-preservation invariant P1.
|
|
119
|
+
_IDEMPOTENT = (And, Or, WeakConjunction, WeakDisjunction)
|
|
120
|
+
|
|
121
|
+
# Involutive negations: ``op(op(x)) == x``.
|
|
122
|
+
_INVOLUTIVE_NEG = (Not, LukNegation)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# ---------------------------------------------------------------------------
|
|
126
|
+
# Capture-safe alpha-normalization
|
|
127
|
+
# ---------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
def _alpha_normalize(node: Node) -> Node:
|
|
130
|
+
"""Rename every bound variable to a canonical ``q0``, ``q1``, … name.
|
|
131
|
+
|
|
132
|
+
Performs a deterministic pre-order traversal: each binder encountered
|
|
133
|
+
(Quantifier / SortedQuantifier / Count / Cardinality over a Variable, Lambda
|
|
134
|
+
over a LambdaVar) consumes the next FREE-name-avoiding ``q``-index and its
|
|
135
|
+
bound occurrences are rewritten to a fresh canonical name of the matching kind
|
|
136
|
+
(Variable for quantifiers/counts/cardinalities, LambdaVar for lambdas). An
|
|
137
|
+
environment maps each currently-in-scope original bound name to its canonical
|
|
138
|
+
replacement; inner binders shadow outer ones of the same name, exactly as
|
|
139
|
+
scope demands.
|
|
140
|
+
|
|
141
|
+
The scheme is capture-safe in BOTH directions. (i) An inner binder never
|
|
142
|
+
reuses an enclosing canonical name, because names are minted from a single
|
|
143
|
+
monotonic counter. (ii) A bound variable is never renamed onto a FREE variable
|
|
144
|
+
of the matrix: ``avoid`` holds every free name in ``node`` (a ``q``-name is a
|
|
145
|
+
legal VARIABLE token, so a formula may legitimately contain a free ``q0``), and
|
|
146
|
+
the mint skips any ``q``-index already in ``avoid``. Without (ii) the disjoint
|
|
147
|
+
formulas ``∃x P(x)`` and ``∃x P(q0)`` would both collapse to ``∃q0 P(q0)`` and
|
|
148
|
+
wrongly compare equal — a false positive that breaks equivalence-preservation
|
|
149
|
+
(P1). Free variables themselves are left untouched. The result is invariant
|
|
150
|
+
under any alpha-renaming of ``node`` (P3; renaming bound variables cannot change
|
|
151
|
+
the free-name set) and is idempotent (P2; a second pass sees the same free
|
|
152
|
+
names and the same binder-encounter order, so it assigns identical names).
|
|
153
|
+
"""
|
|
154
|
+
counter = [0]
|
|
155
|
+
avoid = frozenset(v.name for v in free_variables(node))
|
|
156
|
+
return _alpha(node, {}, {}, counter, avoid)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _fresh_canonical_name(counter: list, avoid) -> str:
|
|
160
|
+
"""Return the next ``q``-index name that does NOT collide with a free name.
|
|
161
|
+
|
|
162
|
+
Advances the monotonic ``counter`` past any ``q``-index present in ``avoid``
|
|
163
|
+
(the set of names free in the whole formula), so a minted bound name is always
|
|
164
|
+
disjoint from every free variable — the capture-safety guarantee (ii) in
|
|
165
|
+
:func:`_alpha_normalize`. Distinctness among minted names is preserved because
|
|
166
|
+
every successful mint consumes a strictly larger counter value.
|
|
167
|
+
"""
|
|
168
|
+
name = f"q{counter[0]}"
|
|
169
|
+
counter[0] += 1
|
|
170
|
+
while name in avoid:
|
|
171
|
+
name = f"q{counter[0]}"
|
|
172
|
+
counter[0] += 1
|
|
173
|
+
return name
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _alpha(node: Node, var_env: dict, lam_env: dict, counter: list, avoid) -> Node:
|
|
177
|
+
"""Recurse, rewriting bound occurrences per the two environments.
|
|
178
|
+
|
|
179
|
+
``var_env`` maps an in-scope original logical-variable name to its canonical
|
|
180
|
+
Variable; ``lam_env`` does the same for lambda-bound names → canonical
|
|
181
|
+
LambdaVar. The environments are copied (never mutated) when entering a
|
|
182
|
+
binder, so siblings never see each other's bindings. ``avoid`` is the frozen
|
|
183
|
+
set of names free in the whole formula, threaded unchanged so every fresh
|
|
184
|
+
canonical name skips them (see :func:`_fresh_canonical_name`).
|
|
185
|
+
"""
|
|
186
|
+
if isinstance(node, Variable):
|
|
187
|
+
return var_env.get(node.name, node)
|
|
188
|
+
if isinstance(node, LambdaVar):
|
|
189
|
+
return lam_env.get(node.name, node)
|
|
190
|
+
|
|
191
|
+
if isinstance(node, Quantifier):
|
|
192
|
+
fresh = Variable(_fresh_canonical_name(counter, avoid))
|
|
193
|
+
inner = dict(var_env)
|
|
194
|
+
inner[node.variable.name] = fresh
|
|
195
|
+
return Quantifier(node.type, fresh, _alpha(node.formula, inner, lam_env, counter, avoid))
|
|
196
|
+
|
|
197
|
+
if isinstance(node, SortedQuantifier):
|
|
198
|
+
fresh = Variable(_fresh_canonical_name(counter, avoid))
|
|
199
|
+
inner = dict(var_env)
|
|
200
|
+
inner[node.variable.name] = fresh
|
|
201
|
+
return SortedQuantifier(
|
|
202
|
+
node.type, fresh, node.sort,
|
|
203
|
+
_alpha(node.formula, inner, lam_env, counter, avoid),
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
if isinstance(node, Lambda):
|
|
207
|
+
fresh = LambdaVar(_fresh_canonical_name(counter, avoid))
|
|
208
|
+
inner = dict(lam_env)
|
|
209
|
+
inner[node.param.name] = fresh
|
|
210
|
+
return Lambda(fresh, _alpha(node.body, var_env, inner, counter, avoid))
|
|
211
|
+
|
|
212
|
+
# Count / Cardinality also bind a logical Variable over their matrix (Count
|
|
213
|
+
# additionally carries the op code and the symbolic bound n, both copied
|
|
214
|
+
# verbatim — neither contains a variable). They rename like a quantifier.
|
|
215
|
+
if isinstance(node, Count):
|
|
216
|
+
fresh = Variable(_fresh_canonical_name(counter, avoid))
|
|
217
|
+
inner = dict(var_env)
|
|
218
|
+
inner[node.variable.name] = fresh
|
|
219
|
+
return Count(node.op, node.n, fresh,
|
|
220
|
+
_alpha(node.formula, inner, lam_env, counter, avoid))
|
|
221
|
+
|
|
222
|
+
if isinstance(node, Cardinality):
|
|
223
|
+
fresh = Variable(_fresh_canonical_name(counter, avoid))
|
|
224
|
+
inner = dict(var_env)
|
|
225
|
+
inner[node.variable.name] = fresh
|
|
226
|
+
return Cardinality(fresh, _alpha(node.formula, inner, lam_env, counter, avoid))
|
|
227
|
+
|
|
228
|
+
# The sorted counterparts bind a Variable too; the sort string is copied verbatim.
|
|
229
|
+
if isinstance(node, SortedCount):
|
|
230
|
+
fresh = Variable(_fresh_canonical_name(counter, avoid))
|
|
231
|
+
inner = dict(var_env)
|
|
232
|
+
inner[node.variable.name] = fresh
|
|
233
|
+
return SortedCount(node.op, node.n, fresh, node.sort,
|
|
234
|
+
_alpha(node.formula, inner, lam_env, counter, avoid))
|
|
235
|
+
|
|
236
|
+
if isinstance(node, SortedCardinality):
|
|
237
|
+
fresh = Variable(_fresh_canonical_name(counter, avoid))
|
|
238
|
+
inner = dict(var_env)
|
|
239
|
+
inner[node.variable.name] = fresh
|
|
240
|
+
return SortedCardinality(fresh, node.sort,
|
|
241
|
+
_alpha(node.formula, inner, lam_env, counter, avoid))
|
|
242
|
+
|
|
243
|
+
# The IF slashed existential binds its variable AND carries a slash set of
|
|
244
|
+
# NAMES referring to enclosing binders: those names follow the enclosing
|
|
245
|
+
# binders' canonical renames (looked up in var_env), so alpha-variant slash
|
|
246
|
+
# annotations canonicalize identically.
|
|
247
|
+
if isinstance(node, SlashedExists):
|
|
248
|
+
fresh = Variable(_fresh_canonical_name(counter, avoid))
|
|
249
|
+
slashed = tuple(var_env[n].name if n in var_env else n
|
|
250
|
+
for n in node.slashed)
|
|
251
|
+
inner = dict(var_env)
|
|
252
|
+
inner[node.variable.name] = fresh
|
|
253
|
+
return SlashedExists(fresh, slashed,
|
|
254
|
+
_alpha(node.formula, inner, lam_env, counter, avoid))
|
|
255
|
+
|
|
256
|
+
# Non-binder structural node (Atom, Function, Not, binary connectives,
|
|
257
|
+
# Measure, Contrast, Application, …): recurse into every child with the same
|
|
258
|
+
# environments.
|
|
259
|
+
return node.map_children(lambda c: _alpha(c, var_env, lam_env, counter, avoid))
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
# ---------------------------------------------------------------------------
|
|
263
|
+
# Structural canonicalization (comm/assoc/dedupe/double-negation)
|
|
264
|
+
# ---------------------------------------------------------------------------
|
|
265
|
+
|
|
266
|
+
def _flatten(node: Node, cls: type) -> List[Node]:
|
|
267
|
+
"""Collect the operands of a maximal same-class commutative chain.
|
|
268
|
+
|
|
269
|
+
Walks the left/right spine of ``node`` (assumed an instance of ``cls``),
|
|
270
|
+
descending into any nested node of the same class so that, e.g.,
|
|
271
|
+
``(P ∧ Q) ∧ R`` and ``P ∧ (Q ∧ R)`` both yield ``[P, Q, R]``. Operands of a
|
|
272
|
+
different class terminate the chain and are returned as-is (not yet
|
|
273
|
+
canonicalized — the caller canonicalizes each).
|
|
274
|
+
"""
|
|
275
|
+
out: List[Node] = []
|
|
276
|
+
for side in (node.left, node.right):
|
|
277
|
+
if isinstance(side, cls):
|
|
278
|
+
out.extend(_flatten(side, cls))
|
|
279
|
+
else:
|
|
280
|
+
out.append(side)
|
|
281
|
+
return out
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _canon_operands(node: Node, cls: type, levels: dict) -> List[Node]:
|
|
285
|
+
"""Return the canonicalized operands of a commutative ``cls`` group.
|
|
286
|
+
|
|
287
|
+
Flattens the raw chain, canonicalizes each operand (threading the enclosing
|
|
288
|
+
binder ``levels`` map), then RE-FLATTENS any operand that itself canonicalized
|
|
289
|
+
into the same class. The re-flatten step is essential: a double-negation
|
|
290
|
+
collapse can turn an operand such as ``¬¬(P ∨ Q)`` into a bare ``P ∨ Q`` (the
|
|
291
|
+
same class as its parent ``∨``), and that newly-exposed nested group must be
|
|
292
|
+
absorbed into the parent chain — otherwise the duplicate ``Q`` in
|
|
293
|
+
``¬¬(P ∨ Q) ∨ Q`` would survive a single pass (breaking idempotency P2 and
|
|
294
|
+
associativity-invariance P4).
|
|
295
|
+
"""
|
|
296
|
+
out: List[Node] = []
|
|
297
|
+
for raw in _flatten(node, cls):
|
|
298
|
+
canon = _structural(raw, levels)
|
|
299
|
+
if isinstance(canon, cls):
|
|
300
|
+
out.extend(_flatten(canon, cls))
|
|
301
|
+
else:
|
|
302
|
+
out.append(canon)
|
|
303
|
+
return out
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _scope_name(node: Any):
|
|
307
|
+
"""Key of a variable occurrence or of a lambda parameter in the scope maps.
|
|
308
|
+
|
|
309
|
+
A logical variable and a lambda variable live in two name spaces (``_alpha``
|
|
310
|
+
keeps one environment for each): ``∀x`` binds the variable ``x`` and leaves a
|
|
311
|
+
lambda variable ``x`` alone, so the two must not share a scope entry. A plain
|
|
312
|
+
name is a logical variable.
|
|
313
|
+
"""
|
|
314
|
+
return ("lambda", node.name) if isinstance(node, LambdaVar) else node.name
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _plain(value) -> str:
|
|
318
|
+
"""Text of a field value that is the same for equal values in every run.
|
|
319
|
+
|
|
320
|
+
``repr`` already is, except for a set, whose element order depends on the
|
|
321
|
+
hash seed of the process and on how the set was built; its elements are sorted.
|
|
322
|
+
"""
|
|
323
|
+
if isinstance(value, (set, frozenset)):
|
|
324
|
+
return type(value).__name__ + "{" + ", ".join(sorted(_plain(v) for v in value)) + "}"
|
|
325
|
+
return repr(value)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _loose_fields(node: Node, skip: tuple = ()) -> List[str]:
|
|
329
|
+
"""One token per part of ``node`` that is NOT a child node, except the fields in ``skip``.
|
|
330
|
+
|
|
331
|
+
A scalar field gives ``name=value``; a sequence field gives ``name#length`` and,
|
|
332
|
+
for every item that is not a node, ``name[position]=value`` (the nodes among
|
|
333
|
+
the items are the node's children, which the caller visits). The fields are
|
|
334
|
+
read from the node itself, so a node class added to the kit later is covered
|
|
335
|
+
without being listed here: the name of a ``Constant``, the value of a
|
|
336
|
+
``Number``, name and sort of a ``SortedConstant``, a string agent, the type and
|
|
337
|
+
bound name of a second-order quantifier, and so on. Every node class of the kit
|
|
338
|
+
is a dataclass — ``Node._child_nodes`` and ``Node.map_children``, which the
|
|
339
|
+
canonical form is built on, read ``dataclasses.fields`` as well.
|
|
340
|
+
"""
|
|
341
|
+
tokens: List[str] = []
|
|
342
|
+
for f in dataclasses.fields(node): # type: ignore[arg-type]
|
|
343
|
+
if f.name in skip:
|
|
344
|
+
continue
|
|
345
|
+
value = getattr(node, f.name)
|
|
346
|
+
if isinstance(value, Node):
|
|
347
|
+
continue
|
|
348
|
+
if isinstance(value, (list, tuple)):
|
|
349
|
+
tokens.append(f"{f.name}#{len(value)}")
|
|
350
|
+
tokens.extend(f"{f.name}[{i}]={_plain(item)}"
|
|
351
|
+
for i, item in enumerate(value) if not isinstance(item, Node))
|
|
352
|
+
else:
|
|
353
|
+
tokens.append(f"{f.name}={_plain(value)}")
|
|
354
|
+
return tokens
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _sort_key(operand: Node, levels: dict) -> tuple:
|
|
358
|
+
"""Deterministic sort key, invariant under bound-variable renaming AND under
|
|
359
|
+
commutative reordering of sibling operands, and equal for two operands ONLY
|
|
360
|
+
when they are the same formula up to the names of their bound variables.
|
|
361
|
+
|
|
362
|
+
The commutative sort runs bottom-up *before* the final whole-tree
|
|
363
|
+
``_alpha_normalize`` pass renames variables bound by ENCLOSING binders. A
|
|
364
|
+
name-sensitive key would therefore order operands differently on the first
|
|
365
|
+
pass (enclosing binders still named ``x``, ``w``) than on the second (renamed
|
|
366
|
+
``q0``, ``q1``) — the classic alpha-vs-sort non-idempotence trap (P2). We
|
|
367
|
+
avoid it by encoding every variable occurrence by POSITION, not name:
|
|
368
|
+
|
|
369
|
+
* a variable bound by an ENCLOSING binder is encoded by that binder's LEVEL
|
|
370
|
+
(its depth from the root, supplied in ``levels``). The level is invariant
|
|
371
|
+
under commutative reordering of siblings — reordering operands never
|
|
372
|
+
changes any binder's depth — so two operands that reference *different*
|
|
373
|
+
enclosing binders (e.g. ``P(x)`` vs ``P(y)`` under ``∀y ∀x``) get
|
|
374
|
+
DIFFERENT keys and a stable order, fixing comm/assoc-invariance (P4);
|
|
375
|
+
* a variable bound INSIDE the operand is encoded by a local de-Bruijn-style
|
|
376
|
+
index assigned in binder-encounter order;
|
|
377
|
+
* a genuinely free variable is encoded by its name (such variables are not
|
|
378
|
+
renamed by canonicalize, so the name is already stable).
|
|
379
|
+
|
|
380
|
+
Because the key depends on no renamable name, the ordering is identical
|
|
381
|
+
across passes (P2) and identical for any alpha-variant of the input (P3).
|
|
382
|
+
|
|
383
|
+
The key is a pair. Its first part, the SKELETON, is a string that holds the
|
|
384
|
+
class of every node, the predicate of an ``Atom``, the name of a ``Function``,
|
|
385
|
+
and the binder and variable encodings above. Its second part, the DETAIL, is a
|
|
386
|
+
tuple of tokens that holds all of that again as separate items, and in addition
|
|
387
|
+
every field of every node that is no child node (see :func:`_loose_fields`):
|
|
388
|
+
without them ``P(alice)`` and ``P(bob)`` would have one key, and so would
|
|
389
|
+
``P(1)`` and ``P(2)``, or ``K_alice φ`` and ``K_bob φ``, and one of two such
|
|
390
|
+
conjuncts would be removed as a duplicate. The skeleton comes first so that
|
|
391
|
+
operands it tells apart sort exactly as they always did; the detail only
|
|
392
|
+
decides between operands whose skeletons are equal, and being a tuple of
|
|
393
|
+
items (not a joined string) it stays exact for a name that contains the
|
|
394
|
+
separator.
|
|
395
|
+
|
|
396
|
+
A binder this function does not know (a second-order quantifier, the hybrid
|
|
397
|
+
``Down``, a binder added later) is read like any other node: its bound name is
|
|
398
|
+
one of the fields that go into the detail (for ``Down`` it is the ``Nominal``
|
|
399
|
+
child, which is recorded by its name). Two operands that differ only by the
|
|
400
|
+
renaming of such a binder therefore have different keys: a match that
|
|
401
|
+
``canonicalize`` does not make, never a wrong one.
|
|
402
|
+
"""
|
|
403
|
+
skeleton: List[str] = []
|
|
404
|
+
detail: List[str] = []
|
|
405
|
+
local_depth = [0] # monotonic binder counter; immune to shadowing
|
|
406
|
+
|
|
407
|
+
def rec(n: Node, local: dict) -> None:
|
|
408
|
+
cls_name = type(n).__name__
|
|
409
|
+
if isinstance(n, (Variable, LambdaVar)):
|
|
410
|
+
scope = _scope_name(n)
|
|
411
|
+
if scope in local:
|
|
412
|
+
where = f"b{local[scope]}" # operand-bound
|
|
413
|
+
elif scope in levels:
|
|
414
|
+
where = f"L{levels[scope]}" # enclosing-bound
|
|
415
|
+
else:
|
|
416
|
+
where = f"v:{n.name}" # genuinely free
|
|
417
|
+
skeleton.append(where)
|
|
418
|
+
# The class keeps a free variable apart from a free lambda variable of
|
|
419
|
+
# the same spelling.
|
|
420
|
+
detail.extend((cls_name, where))
|
|
421
|
+
return
|
|
422
|
+
if isinstance(n, (Quantifier, SortedQuantifier)):
|
|
423
|
+
inner = dict(local)
|
|
424
|
+
inner[n.variable.name] = local_depth[0]
|
|
425
|
+
local_depth[0] += 1
|
|
426
|
+
sort = getattr(n, "sort", "")
|
|
427
|
+
skeleton.append(f"Q{n.type}:{sort}")
|
|
428
|
+
detail.append(cls_name)
|
|
429
|
+
detail.extend(_loose_fields(n, ("variable", "formula")))
|
|
430
|
+
rec(n.formula, inner)
|
|
431
|
+
return
|
|
432
|
+
if isinstance(n, Lambda):
|
|
433
|
+
inner = dict(local)
|
|
434
|
+
inner[_scope_name(n.param)] = local_depth[0]
|
|
435
|
+
local_depth[0] += 1
|
|
436
|
+
skeleton.append("LAM")
|
|
437
|
+
detail.append(cls_name)
|
|
438
|
+
detail.extend(_loose_fields(n, ("param", "body")))
|
|
439
|
+
rec(n.body, inner)
|
|
440
|
+
return
|
|
441
|
+
if isinstance(n, Count):
|
|
442
|
+
inner = dict(local)
|
|
443
|
+
inner[n.variable.name] = local_depth[0]
|
|
444
|
+
local_depth[0] += 1
|
|
445
|
+
# op and n distinguish ∃≥3 from ∃≤3 / ∃≥5; neither is renamable, so
|
|
446
|
+
# the key stays invariant under bound-variable renaming (P3).
|
|
447
|
+
skeleton.append(f"CNT{n.op}:{n.n.value}")
|
|
448
|
+
detail.append(cls_name)
|
|
449
|
+
detail.extend(_loose_fields(n, ("variable", "formula")))
|
|
450
|
+
detail.append(f"n={_plain(n.n.value)}")
|
|
451
|
+
rec(n.formula, inner)
|
|
452
|
+
return
|
|
453
|
+
if isinstance(n, Cardinality):
|
|
454
|
+
inner = dict(local)
|
|
455
|
+
inner[n.variable.name] = local_depth[0]
|
|
456
|
+
local_depth[0] += 1
|
|
457
|
+
skeleton.append("CARD")
|
|
458
|
+
detail.append(cls_name)
|
|
459
|
+
detail.extend(_loose_fields(n, ("variable", "formula")))
|
|
460
|
+
rec(n.formula, inner)
|
|
461
|
+
return
|
|
462
|
+
if isinstance(n, SortedCount):
|
|
463
|
+
inner = dict(local)
|
|
464
|
+
inner[n.variable.name] = local_depth[0]
|
|
465
|
+
local_depth[0] += 1
|
|
466
|
+
# op, n AND sort are all significant and non-renamable.
|
|
467
|
+
skeleton.append(f"SCNT{n.op}:{n.n.value}:{n.sort}")
|
|
468
|
+
detail.append(cls_name)
|
|
469
|
+
detail.extend(_loose_fields(n, ("variable", "formula")))
|
|
470
|
+
detail.append(f"n={_plain(n.n.value)}")
|
|
471
|
+
rec(n.formula, inner)
|
|
472
|
+
return
|
|
473
|
+
if isinstance(n, SortedCardinality):
|
|
474
|
+
inner = dict(local)
|
|
475
|
+
inner[n.variable.name] = local_depth[0]
|
|
476
|
+
local_depth[0] += 1
|
|
477
|
+
skeleton.append(f"SCARD:{n.sort}")
|
|
478
|
+
detail.append(cls_name)
|
|
479
|
+
detail.extend(_loose_fields(n, ("variable", "formula")))
|
|
480
|
+
rec(n.formula, inner)
|
|
481
|
+
return
|
|
482
|
+
if isinstance(n, SlashedExists):
|
|
483
|
+
# Slash names are encoded positionally like variable occurrences
|
|
484
|
+
# (operand-bound by local index, enclosing-bound by level), so the
|
|
485
|
+
# key stays invariant under bound-variable renaming (P3).
|
|
486
|
+
enc = []
|
|
487
|
+
for s in n.slashed:
|
|
488
|
+
if s in local:
|
|
489
|
+
enc.append(f"b{local[s]}")
|
|
490
|
+
elif s in levels:
|
|
491
|
+
enc.append(f"L{levels[s]}")
|
|
492
|
+
else:
|
|
493
|
+
enc.append(f"v:{s}")
|
|
494
|
+
inner = dict(local)
|
|
495
|
+
inner[n.variable.name] = local_depth[0]
|
|
496
|
+
local_depth[0] += 1
|
|
497
|
+
skeleton.append("SLEX:" + ",".join(enc))
|
|
498
|
+
detail.append(cls_name)
|
|
499
|
+
detail.extend(_loose_fields(n, ("variable", "formula", "slashed")))
|
|
500
|
+
detail.append(f"slashed#{len(enc)}")
|
|
501
|
+
detail.extend(enc)
|
|
502
|
+
rec(n.formula, inner)
|
|
503
|
+
return
|
|
504
|
+
skeleton.append(cls_name)
|
|
505
|
+
if cls_name == "Atom":
|
|
506
|
+
skeleton.append(n.predicate)
|
|
507
|
+
elif cls_name == "Function":
|
|
508
|
+
skeleton.append(n.name)
|
|
509
|
+
detail.append(cls_name)
|
|
510
|
+
detail.extend(_loose_fields(n))
|
|
511
|
+
for child in n._child_nodes():
|
|
512
|
+
rec(child, local)
|
|
513
|
+
skeleton.append("/")
|
|
514
|
+
|
|
515
|
+
rec(operand, {})
|
|
516
|
+
return "|".join(skeleton), tuple(detail)
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
# NOTE on de-duplication: the dedup identity key is ``_sort_key`` itself. Two
|
|
520
|
+
# operands sharing the same enclosing scope are logically identical iff they are
|
|
521
|
+
# alpha-equivalent with the variables bound by ENCLOSING binders held fixed —
|
|
522
|
+
# which is EXACTLY what ``_sort_key`` encodes (enclosing-bound variables by
|
|
523
|
+
# level, operand-bound by local index, genuinely-free by name), together with
|
|
524
|
+
# every other field of every node (constant names, numeral values, sorts, agents,
|
|
525
|
+
# …) compared as it is, so two operands that differ in any of them are never taken
|
|
526
|
+
# for duplicates. Crucially this is capture-proof: a string built from
|
|
527
|
+
# ``_alpha_normalize(operand)`` would, on a second pass, capture a free variable
|
|
528
|
+
# that the first pass had renamed to ``q0`` under a freshly-minted bound ``q0``
|
|
529
|
+
# (e.g. the shadowing case ``∃x R(x) ∧ ∃z R(x)``), wrongly collapsing two
|
|
530
|
+
# NON-equivalent operands and breaking both idempotency (P2) and
|
|
531
|
+
# equivalence-preservation (P1). Keying dedup on ``_sort_key`` avoids any
|
|
532
|
+
# name-based capture entirely.
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def _structural(node: Node, levels: dict) -> Node:
|
|
536
|
+
"""Apply comm/assoc/dedupe and double-negation, bottom-up.
|
|
537
|
+
|
|
538
|
+
``levels`` maps each variable bound by an ENCLOSING binder to that binder's
|
|
539
|
+
level (depth from the root); it is extended when recursing through a
|
|
540
|
+
quantifier or lambda and consulted by ``_sort_key`` so the commutative sort
|
|
541
|
+
is stable under bound-variable renaming (see ``_sort_key``).
|
|
542
|
+
|
|
543
|
+
Children are canonicalized first; then double negations collapse and
|
|
544
|
+
commutative groups are flattened, their operands sorted by ``_sort_key`` and
|
|
545
|
+
(for the idempotent connectives only) de-duplicated, and the group is rebuilt
|
|
546
|
+
left-folded. Bound-variable renaming is deferred to a single final pass (see
|
|
547
|
+
``canonicalize``).
|
|
548
|
+
"""
|
|
549
|
+
# Double negation: collapse op(op(x)) for the involutive negations.
|
|
550
|
+
for neg_cls in _INVOLUTIVE_NEG:
|
|
551
|
+
if isinstance(node, neg_cls) and isinstance(node.formula, neg_cls):
|
|
552
|
+
return _structural(node.formula.formula, levels)
|
|
553
|
+
|
|
554
|
+
if isinstance(node, _COMMUTATIVE):
|
|
555
|
+
cls = type(node)
|
|
556
|
+
operands = _canon_operands(node, cls, levels)
|
|
557
|
+
operands.sort(key=lambda op: _sort_key(op, levels))
|
|
558
|
+
if isinstance(node, _IDEMPOTENT):
|
|
559
|
+
# Dedupe operands ONLY for the idempotent connectives (∧ ∨ and fuzzy
|
|
560
|
+
# min/max), where P ⋆ P ≡ P makes removal equivalence-preserving.
|
|
561
|
+
# Dedup keys on ``_sort_key`` (level-encoded, capture-proof — see the
|
|
562
|
+
# note above), so alpha-equivalent operands like (∀x P(x)) and
|
|
563
|
+
# (∀y P(y)) collapse while genuinely distinct operands like R(x) and
|
|
564
|
+
# R(w) do NOT. Xor / Iff / StrongConjunction / StrongDisjunction are
|
|
565
|
+
# NOT idempotent and are left intact (deduping them would break P1).
|
|
566
|
+
seen: set = set()
|
|
567
|
+
deduped: List[Node] = []
|
|
568
|
+
for op in operands:
|
|
569
|
+
key = _sort_key(op, levels)
|
|
570
|
+
if key not in seen:
|
|
571
|
+
seen.add(key)
|
|
572
|
+
deduped.append(op)
|
|
573
|
+
operands = deduped
|
|
574
|
+
result = operands[0]
|
|
575
|
+
for op in operands[1:]:
|
|
576
|
+
result = cls(result, op)
|
|
577
|
+
return result
|
|
578
|
+
|
|
579
|
+
# Binders extend the enclosing-level map for their body, then keep their bound
|
|
580
|
+
# variable verbatim (it is renamed by the final _alpha_normalize pass). The
|
|
581
|
+
# new level is one past the deepest existing one — NOT ``len(levels)`` — so a
|
|
582
|
+
# shadowing binder (e.g. the inner ``∀z`` in ``∀z ∀z ∀w …``) overwrites its
|
|
583
|
+
# name's entry yet still advances the depth, keeping every level distinct.
|
|
584
|
+
next_level = (max(levels.values()) + 1) if levels else 0
|
|
585
|
+
if isinstance(node, Quantifier):
|
|
586
|
+
inner = dict(levels)
|
|
587
|
+
inner[node.variable.name] = next_level
|
|
588
|
+
return Quantifier(node.type, node.variable, _structural(node.formula, inner))
|
|
589
|
+
if isinstance(node, SortedQuantifier):
|
|
590
|
+
inner = dict(levels)
|
|
591
|
+
inner[node.variable.name] = next_level
|
|
592
|
+
return SortedQuantifier(node.type, node.variable, node.sort,
|
|
593
|
+
_structural(node.formula, inner))
|
|
594
|
+
if isinstance(node, Lambda):
|
|
595
|
+
inner = dict(levels)
|
|
596
|
+
inner[_scope_name(node.param)] = next_level
|
|
597
|
+
return Lambda(node.param, _structural(node.body, inner))
|
|
598
|
+
if isinstance(node, Count):
|
|
599
|
+
inner = dict(levels)
|
|
600
|
+
inner[node.variable.name] = next_level
|
|
601
|
+
return Count(node.op, node.n, node.variable,
|
|
602
|
+
_structural(node.formula, inner))
|
|
603
|
+
if isinstance(node, Cardinality):
|
|
604
|
+
inner = dict(levels)
|
|
605
|
+
inner[node.variable.name] = next_level
|
|
606
|
+
return Cardinality(node.variable, _structural(node.formula, inner))
|
|
607
|
+
if isinstance(node, SortedCount):
|
|
608
|
+
inner = dict(levels)
|
|
609
|
+
inner[node.variable.name] = next_level
|
|
610
|
+
return SortedCount(node.op, node.n, node.variable, node.sort,
|
|
611
|
+
_structural(node.formula, inner))
|
|
612
|
+
if isinstance(node, SortedCardinality):
|
|
613
|
+
inner = dict(levels)
|
|
614
|
+
inner[node.variable.name] = next_level
|
|
615
|
+
return SortedCardinality(node.variable, node.sort,
|
|
616
|
+
_structural(node.formula, inner))
|
|
617
|
+
if isinstance(node, SlashedExists):
|
|
618
|
+
inner = dict(levels)
|
|
619
|
+
inner[node.variable.name] = next_level
|
|
620
|
+
return SlashedExists(node.variable, node.slashed,
|
|
621
|
+
_structural(node.formula, inner))
|
|
622
|
+
|
|
623
|
+
# Any other node (Implies, LukImplication, Not/LukNegation without a nested
|
|
624
|
+
# negation, Contrast, Measure, atoms, terms, Application): keep its shape,
|
|
625
|
+
# recursing structurally into the children. Operand order of the
|
|
626
|
+
# non-commutative connectives is thereby preserved.
|
|
627
|
+
return node.map_children(lambda c: _structural(c, levels))
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
# ---------------------------------------------------------------------------
|
|
631
|
+
# Public API
|
|
632
|
+
# ---------------------------------------------------------------------------
|
|
633
|
+
|
|
634
|
+
def canonicalize(node: Node) -> Node:
|
|
635
|
+
"""Return the canonical form of ``node`` (see the module docstring).
|
|
636
|
+
|
|
637
|
+
The result is logically equivalent to ``node`` (P1) and quotients out
|
|
638
|
+
exactly alpha-renaming (P3), commutativity/associativity (P4), operand
|
|
639
|
+
duplication, and double negation (P5). It is idempotent (P2).
|
|
640
|
+
|
|
641
|
+
Pipeline: first ``_structural`` rewrites the tree under comm/assoc/dedupe and
|
|
642
|
+
double-negation (sorting commutative operands by a key invariant under
|
|
643
|
+
bound-variable renaming and sibling reordering — see ``_sort_key``), then a
|
|
644
|
+
single ``_alpha_normalize`` pass assigns canonical bound-variable names over
|
|
645
|
+
the now-stable structure.
|
|
646
|
+
"""
|
|
647
|
+
return _alpha_normalize(_structural(node, {}))
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
def exact_match(pred: Node, ref: Node, canonical: bool = True) -> bool:
|
|
651
|
+
"""Return whether ``pred`` matches ``ref``.
|
|
652
|
+
|
|
653
|
+
When ``canonical`` is True (the default), the comparison is up to canonical
|
|
654
|
+
form: ``canonicalize(pred) == canonicalize(ref)``, so differences in
|
|
655
|
+
bound-variable names, commutative-operand order/association, duplicated
|
|
656
|
+
operands, and double negation do not cause a mismatch. When ``canonical`` is
|
|
657
|
+
False, the comparison is raw structural equality (``pred == ref``); because
|
|
658
|
+
the nodes are frozen and hashable with tuple-normalized argument lists, this
|
|
659
|
+
already ignores list-vs-tuple construction but nothing else.
|
|
660
|
+
"""
|
|
661
|
+
if canonical:
|
|
662
|
+
return canonicalize(pred) == canonicalize(ref)
|
|
663
|
+
return pred == ref
|