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,537 @@
|
|
|
1
|
+
"""Repair layer for LLM-generated formulas in the kit's OWN surface syntax.
|
|
2
|
+
|
|
3
|
+
Sibling of :mod:`unicode_logic_kit.fol.tptp_repair`, which repairs in TPTP the
|
|
4
|
+
three shapes LLM-generated formulas fail on for pure SYNTAX — worth repairing
|
|
5
|
+
rather than retrying, since a syntax rejection costs a whole generation
|
|
6
|
+
attempt. Carrying those three over to the kit's unicode dialect gives three
|
|
7
|
+
DIFFERENT answers, and which is which is this module's entire content:
|
|
8
|
+
|
|
9
|
+
1. **Biimplication structure error** — **does not exist here.** A strict
|
|
10
|
+
TPTP parser (gavel, for one) rejects ``p(X) <=> a(X) & b(X)`` for lacking
|
|
11
|
+
brackets around the right side. The kit's unicode grammar accepts it, and
|
|
12
|
+
``Node.to_unicode_str`` renders it back in exactly that form, so there is
|
|
13
|
+
nothing to normalise in either direction. It is unambiguous because the
|
|
14
|
+
grammar's ladder puts ``↔``/``→`` below ``∧``/``∨`` — and because the one
|
|
15
|
+
shape that WOULD be ambiguous, mixing ``∧`` and ``∨`` at the same level,
|
|
16
|
+
is refused outright rather than silently resolved by precedence.
|
|
17
|
+
|
|
18
|
+
That refusal is why this module never inserts a bracket. ``A ∧ B ∨ C`` has
|
|
19
|
+
two readings, they are different formulas, and picking one would throw
|
|
20
|
+
away the exact property that makes this dialect worth generating in: the
|
|
21
|
+
parser says "cannot mix" *before* a wrong reading reaches a model checker.
|
|
22
|
+
Reported as a ``"mixed_connectives"`` issue with the grammar's own hint;
|
|
23
|
+
never repaired.
|
|
24
|
+
|
|
25
|
+
2. **Invalid predicate name** — **repaired differently.** A chemical name
|
|
26
|
+
used as a predicate (``1,2-diacyl-sn-glycero-3-phospho‐
|
|
27
|
+
choline``, ``(2S)Flavan4One``) is not a legal token in any FOL surface
|
|
28
|
+
syntax. TPTP has an escape hatch, single quoting, which preserves the
|
|
29
|
+
name exactly; the unicode grammar quotes a CONSTANT (``'1,2-diacyl'`` as
|
|
30
|
+
a term) but has no quoted form for a PREDICATE or a FUNCTION name
|
|
31
|
+
(``'1,2-diacyl'(x)`` fails at the bracket after the closing quote), so the
|
|
32
|
+
fix here is a sanitising RENAME through
|
|
33
|
+
:class:`~unicode_logic_kit.fol.sanitize.NameMapping`. What stands between a
|
|
34
|
+
pair of single quotes is a constant's name, exactly as written, and is
|
|
35
|
+
never a candidate for this rename (see the limits below).
|
|
36
|
+
|
|
37
|
+
Renaming such a name to a plain camelCase identifier is the obvious
|
|
38
|
+
fallback, and it is a loss: the mapping back to the original chemical
|
|
39
|
+
class name is gone. Here the mapping IS the return value (``names``, and
|
|
40
|
+
the caller's own :class:`NameMapping`), so the original spelling is
|
|
41
|
+
recoverable and the same original renames identically in every formula of
|
|
42
|
+
a run when the caller passes one mapping through. Nothing is lost; it just
|
|
43
|
+
lives beside the formula instead of inside it.
|
|
44
|
+
|
|
45
|
+
3. **Unbound variable error** — **repaired identically.** Free variables are
|
|
46
|
+
read off the AST, which no longer knows what dialect it was written in, so
|
|
47
|
+
this module calls tptp_repair's own two fixes
|
|
48
|
+
(:func:`~unicode_logic_kit.fol.tptp_repair._drop_argument_fix`,
|
|
49
|
+
:func:`~unicode_logic_kit.fol.tptp_repair._close_universally`) rather than
|
|
50
|
+
reimplementing them — the two paths cannot drift apart if there is only
|
|
51
|
+
one implementation.
|
|
52
|
+
|
|
53
|
+
4. **A truth glyph beside another dialect's connectives** — **repaired,
|
|
54
|
+
and reported.** ``P -> ⊥`` is the Prover9 arrow with the unicode glyph for
|
|
55
|
+
falsity, and no grammar reads it: the glyph is not ASCII, and the
|
|
56
|
+
detection of the two ASCII-only dialects (Prover9, TPTP) keys on text that
|
|
57
|
+
is ASCII, while the unicode ladder does not read ``->``. ``P -> Q`` parses
|
|
58
|
+
(as Prover9) and ``P → ⊥`` parses (as the unicode ladder), so the mix is
|
|
59
|
+
repaired the way ``P -> Q`` is: the glyphs ``⊤`` / ``⊥`` are read as the
|
|
60
|
+
truth constants of the dialect the rest of the text is written in, first
|
|
61
|
+
as Prover9's ``$T`` / ``$F``, then as TPTP's ``$true`` / ``$false``, and
|
|
62
|
+
the first rewrite that parses wins. Either way the result holds the truth
|
|
63
|
+
constants ``$true`` / ``$false``, and an ``"truth_glyph"`` issue says what
|
|
64
|
+
was read as what. A rewrite that does not parse is discarded whole.
|
|
65
|
+
|
|
66
|
+
Public API: :func:`repair_formula`, returning a :class:`DialectRepairResult`
|
|
67
|
+
(a :class:`~unicode_logic_kit.fol.tptp_repair.RepairResult` plus the dialect
|
|
68
|
+
that parsed and the renamings applied), so a caller that already handles TPTP
|
|
69
|
+
repairs handles these with the same code.
|
|
70
|
+
|
|
71
|
+
Hard limits (refuse rather than silently reinterpret)
|
|
72
|
+
-------------------------------------------------------
|
|
73
|
+
* Mixed same-level connectives are reported, never bracketed (above).
|
|
74
|
+
* A rename is only ever attempted on text that FAILED to parse, and only for
|
|
75
|
+
a name-shaped span in functor position that is legal in no symbol class —
|
|
76
|
+
and it is kept only if the rewritten text then parses. A rewrite that does
|
|
77
|
+
not make the input parseable is discarded whole: ``ok=False``, the original
|
|
78
|
+
text returned untouched, and the parser's own farthest diagnosis reported.
|
|
79
|
+
* Every text between a pair of single quotes is left byte for byte as it was,
|
|
80
|
+
in every step of this module (the scan for names, the rename, the reading of
|
|
81
|
+
a truth glyph): a quoted constant such as ``'Foo-bar(x)'`` or ``'⊤'`` is a
|
|
82
|
+
name, not a functor-position span and not a glyph. A pair is what the
|
|
83
|
+
quoted-name terminal reads, and the escapes ``\\'`` and ``\\\\`` inside it
|
|
84
|
+
do not end it; a quote without a partner is no pair and is left to the
|
|
85
|
+
parser's message.
|
|
86
|
+
* The rename always picks the PREDICATE spelling (``[A-Z][a-zA-Z0-9]*``).
|
|
87
|
+
A functor-position name is a predicate or a function depending only on its
|
|
88
|
+
spelling, so this is a choice, not a reading — stated in the issue message
|
|
89
|
+
each time. A case-2 name of this shape is a class predicate in practice; a
|
|
90
|
+
name meant as a function symbol must be written legally by hand.
|
|
91
|
+
* A legalised name never collides with a symbol already in the text: the
|
|
92
|
+
mapping's reserved set is preseeded with every identifier the text
|
|
93
|
+
contains, so ``1,2-diacyl`` beside an existing ``P12diacyl`` yields
|
|
94
|
+
``P12diacyl2`` instead of merging the two predicates.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
import re
|
|
98
|
+
from dataclasses import dataclass
|
|
99
|
+
from functools import lru_cache
|
|
100
|
+
from typing import List, Optional, Tuple
|
|
101
|
+
|
|
102
|
+
from .nodes import Node, free_variables
|
|
103
|
+
# The three symbol-class patterns come from fol._identifiers — the SAME
|
|
104
|
+
# generated character classes the grammar itself lexes PREDICATE/NAME/
|
|
105
|
+
# VARIABLE with — rather than from sanitize.py. sanitize.py's patterns are a
|
|
106
|
+
# DIFFERENT, deliberately narrower thing: the strictly-ASCII subset safe to
|
|
107
|
+
# emit into TPTP/Prover9/SMT-LIB/Isabelle/CASL (see its module docstring), not
|
|
108
|
+
# "every name this dialect's parser accepts". Using sanitize's patterns here
|
|
109
|
+
# used to be harmless because they happened to coincide with the grammar's
|
|
110
|
+
# own (then-ASCII) terminals; once the grammar widened to Unicode letters,
|
|
111
|
+
# underscores, and digit-leading names, that coincidence broke — a legal name
|
|
112
|
+
# like `family_History` would fail sanitize's ASCII-only _NAME_RE and get
|
|
113
|
+
# needlessly (and destructively) renamed, even though it already parses fine.
|
|
114
|
+
# _is_legal_name has to ask what THIS dialect's grammar accepts, which is
|
|
115
|
+
# _identifiers, not what sanitize.py is willing to export.
|
|
116
|
+
from ._identifiers import (
|
|
117
|
+
_UNSPELLABLE_CLASS,
|
|
118
|
+
predicate_pattern as _predicate_pattern,
|
|
119
|
+
name_pattern as _name_pattern,
|
|
120
|
+
variable_pattern as _variable_pattern,
|
|
121
|
+
uppercase_class as _uppercase_class,
|
|
122
|
+
lowercase_class as _lowercase_class,
|
|
123
|
+
combining_class as _combining_class,
|
|
124
|
+
)
|
|
125
|
+
from .sanitize import NameMapping
|
|
126
|
+
from .tptp_repair import (
|
|
127
|
+
Issue, RepairResult, _close_universally, _drop_argument_fix, _squash,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
__all__ = ["DialectRepairResult", "repair_formula"]
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@dataclass(frozen=True)
|
|
134
|
+
class DialectRepairResult(RepairResult):
|
|
135
|
+
"""A :class:`~unicode_logic_kit.fol.tptp_repair.RepairResult` for
|
|
136
|
+
:func:`repair_formula`.
|
|
137
|
+
|
|
138
|
+
``repaired_text`` is the kit's unicode rendering of the repaired AST
|
|
139
|
+
(``Node.to_unicode_str``), not TPTP — it re-parses to ``formula``.
|
|
140
|
+
|
|
141
|
+
``dialect`` is what actually parsed (``parse_any``'s report), ``None``
|
|
142
|
+
when nothing did. ``names`` lists the ``(original, legal)`` renamings
|
|
143
|
+
case 2 applied, in application order — empty when nothing was renamed.
|
|
144
|
+
The originals are recoverable from this alone; pass a shared
|
|
145
|
+
:class:`~unicode_logic_kit.fol.sanitize.NameMapping` to
|
|
146
|
+
:func:`repair_formula` to keep them consistent across a whole run.
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
dialect: Optional[str] = None
|
|
150
|
+
names: Tuple[Tuple[str, str], ...] = ()
|
|
151
|
+
|
|
152
|
+
def to_dict(self) -> dict:
|
|
153
|
+
out = super().to_dict()
|
|
154
|
+
out["dialect"] = self.dialect
|
|
155
|
+
out["names"] = [{"original": o, "legal": l} for o, l in self.names]
|
|
156
|
+
return out
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# ---------------------------------------------------------------------------
|
|
160
|
+
# Quoted constants: what the scans below must not look into
|
|
161
|
+
# ---------------------------------------------------------------------------
|
|
162
|
+
#
|
|
163
|
+
# A constant may be written in single quotes, and the text between the quotes
|
|
164
|
+
# is its name exactly (``'1,2-diacyl'``, ``'Foo_bar(x)'``, ``'⊤'``). The scans
|
|
165
|
+
# of this module are regular expressions over raw text and know nothing of
|
|
166
|
+
# that, so each of them runs only on the stretches OUTSIDE the quoted spans.
|
|
167
|
+
#
|
|
168
|
+
# A quoted span is read the way the QUOTED_NAME terminal reads one — a quote,
|
|
169
|
+
# characters that are no quote, backslash or unspellable character, the escapes
|
|
170
|
+
# of a quote and of a backslash, a quote — with one deliberate widening: a
|
|
171
|
+
# backslash in front of ANY spellable character is taken as an escape. The
|
|
172
|
+
# grammar refuses ``'a\b'``, but a text that is being repaired may hold one, and
|
|
173
|
+
# its span still has to end at the quote the writer meant, not at the quote
|
|
174
|
+
# before it. The empty ``''`` is a span too (it has nothing to protect, and
|
|
175
|
+
# counting it keeps the quotes after it paired as the writer paired them).
|
|
176
|
+
# A quote with no partner is no span.
|
|
177
|
+
_QUOTED_SPAN = re.compile(
|
|
178
|
+
rf"'(?:[^'\\{_UNSPELLABLE_CLASS}]|\\[^{_UNSPELLABLE_CLASS}])*'")
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _unquoted_stretches(text: str) -> List[str]:
|
|
182
|
+
"""The pieces of ``text`` between its quoted spans, in order.
|
|
183
|
+
|
|
184
|
+
``len(result)`` is one more than the number of spans; an empty piece stands
|
|
185
|
+
where a span begins or ends the text. A piece never holds a quote that has a
|
|
186
|
+
partner.
|
|
187
|
+
"""
|
|
188
|
+
pieces: List[str] = []
|
|
189
|
+
last = 0
|
|
190
|
+
for span in _QUOTED_SPAN.finditer(text):
|
|
191
|
+
pieces.append(text[last:span.start()])
|
|
192
|
+
last = span.end()
|
|
193
|
+
pieces.append(text[last:])
|
|
194
|
+
return pieces
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _map_unquoted(text: str, rewrite) -> str:
|
|
198
|
+
"""``text`` with ``rewrite`` applied to each piece outside its quoted spans;
|
|
199
|
+
every quoted span stays exactly as it was."""
|
|
200
|
+
out: List[str] = []
|
|
201
|
+
last = 0
|
|
202
|
+
for span in _QUOTED_SPAN.finditer(text):
|
|
203
|
+
out.append(rewrite(text[last:span.start()]))
|
|
204
|
+
out.append(span.group(0))
|
|
205
|
+
last = span.end()
|
|
206
|
+
out.append(rewrite(text[last:]))
|
|
207
|
+
return "".join(out)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
# ---------------------------------------------------------------------------
|
|
211
|
+
# Case 2 — invalid names: raw-text detection
|
|
212
|
+
# ---------------------------------------------------------------------------
|
|
213
|
+
#
|
|
214
|
+
# Mirrors tptp_repair._CANDIDATE_RE (a name-shaped span immediately before an
|
|
215
|
+
# argument list's "(", optionally preceded by one non-nested parenthesised
|
|
216
|
+
# fragment, with a left-boundary lookbehind so scanning is non-overlapping)
|
|
217
|
+
# with one deliberate difference: "_" is a candidate character here, so a
|
|
218
|
+
# name-shaped span that holds an underscore is found whole and not from the
|
|
219
|
+
# underscore on. An underscore is legal inside a predicate of this dialect
|
|
220
|
+
# (`Foo_bar(x)` reads as it is), so it does not by itself make a name a case-2
|
|
221
|
+
# name; a hyphen does (`Foo-bar(x)`).
|
|
222
|
+
_CANDIDATE_RE = re.compile(
|
|
223
|
+
r"(?<![A-Za-z0-9_,\-)])"
|
|
224
|
+
r"((?:\([^()\n]{1,40}\))?[A-Za-z0-9][A-Za-z0-9_,\-]*)"
|
|
225
|
+
r"(?=\s*\()"
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
# The three symbol-class patterns (and the broader IDENT scan below) are
|
|
229
|
+
# built from fol._identifiers lazily, behind lru_cache, rather than at this
|
|
230
|
+
# module's import time: fol/__init__.py imports this module eagerly, so a
|
|
231
|
+
# module-level ``re.compile(_predicate_pattern())`` here would force
|
|
232
|
+
# _identifiers' once-per-process codepoint scan (~0.1-1s) onto every
|
|
233
|
+
# ``import unicode_logic_kit`` — even one that never calls repair_formula. The
|
|
234
|
+
# lru_cache still means the scan runs at most once per process; it just runs
|
|
235
|
+
# on first actual USE instead of on import.
|
|
236
|
+
|
|
237
|
+
@lru_cache(maxsize=1)
|
|
238
|
+
def _legal_name_patterns():
|
|
239
|
+
return (
|
|
240
|
+
re.compile(_predicate_pattern()),
|
|
241
|
+
re.compile(_name_pattern()),
|
|
242
|
+
re.compile(_variable_pattern()),
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@lru_cache(maxsize=1)
|
|
247
|
+
def _ident_re():
|
|
248
|
+
"""Every identifier-shaped span in the dialect's widened alphabet: a
|
|
249
|
+
letter or digit, then letters/digits/underscores/combining marks — a
|
|
250
|
+
superset of PREDICATE/NAME/VARIABLE/CONSTANT (permissive on purpose,
|
|
251
|
+
since this only feeds the collision-avoidance preseed below, not a
|
|
252
|
+
legality check)."""
|
|
253
|
+
letters = f"{_uppercase_class()}{_lowercase_class()}"
|
|
254
|
+
cont = f"{letters}0-9_{_combining_class()}"
|
|
255
|
+
return re.compile(f"[{letters}0-9][{cont}]*")
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
#: Substring of a parse-error message that means the grammar refused a mix of
|
|
259
|
+
#: same-level connectives. Matching on the message text is the same small,
|
|
260
|
+
#: documented heuristic ``unicode_logic_kit.mcp.server._SPEC_HINTS`` already
|
|
261
|
+
#: uses to route a diagnosis; the hint itself is built in
|
|
262
|
+
#: ``unicode_logic_kit.fol.naming._MIXING_INFO``.
|
|
263
|
+
_MIXING_MARKER = "cannot mix"
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _is_legal_name(name: str) -> bool:
|
|
267
|
+
"""Is ``name`` already a legal token in SOME symbol class of THIS
|
|
268
|
+
dialect's grammar?
|
|
269
|
+
|
|
270
|
+
Predicate, function/bare constant (NAME, including its underscored and
|
|
271
|
+
digit-leading forms), or variable — see :mod:`unicode_logic_kit.fol.
|
|
272
|
+
_identifiers` for the exact shapes. The variable class matters even
|
|
273
|
+
though a variable is never a functor: ``∀x (…)`` puts ``x`` immediately
|
|
274
|
+
before a "(" and so matches :data:`_CANDIDATE_RE` — renaming a bound
|
|
275
|
+
variable to a predicate would destroy the formula, and the one thing
|
|
276
|
+
keeping it out of the candidate list is that ``x`` is legal here.
|
|
277
|
+
"""
|
|
278
|
+
pred_re, name_re, var_re = _legal_name_patterns()
|
|
279
|
+
return bool(pred_re.fullmatch(name) or name_re.fullmatch(name)
|
|
280
|
+
or var_re.fullmatch(name))
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _find_name_candidates(text: str) -> List[str]:
|
|
284
|
+
"""Distinct, order-preserving functor-position names in ``text`` that are
|
|
285
|
+
legal in no symbol class. Nothing between a pair of single quotes is a
|
|
286
|
+
candidate: that is a constant's name, not a name in functor position."""
|
|
287
|
+
seen = set()
|
|
288
|
+
out: List[str] = []
|
|
289
|
+
for stretch in _unquoted_stretches(text):
|
|
290
|
+
for match in _CANDIDATE_RE.finditer(stretch):
|
|
291
|
+
name = match.group(1)
|
|
292
|
+
if _is_legal_name(name) or name in seen:
|
|
293
|
+
continue
|
|
294
|
+
seen.add(name)
|
|
295
|
+
out.append(name)
|
|
296
|
+
return out
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _apply_renames(text: str, renames: Tuple[Tuple[str, str], ...]) -> str:
|
|
300
|
+
"""Replace every functor-position occurrence of each original name.
|
|
301
|
+
|
|
302
|
+
By name rather than by span (the same mis-written predicate legitimately
|
|
303
|
+
occurs more than once, and substituting by name catches every occurrence
|
|
304
|
+
without tracking shifting offsets), LONGEST FIRST: one invalid name can
|
|
305
|
+
end with another (``3-oxo-steroid`` and ``oxo-steroid``), and rewriting
|
|
306
|
+
the shorter one first would eat the tail of the longer one's occurrences
|
|
307
|
+
and leave a half-renamed name behind.
|
|
308
|
+
|
|
309
|
+
Only the text outside single quotes is rewritten: the spelling of a quoted
|
|
310
|
+
constant is the constant, so a quoted span that holds a candidate's
|
|
311
|
+
spelling (``'1,2-diacyl(x)'``) stays as it is.
|
|
312
|
+
"""
|
|
313
|
+
def rewrite(stretch: str) -> str:
|
|
314
|
+
for name, legal in sorted(renames, key=lambda pair: -len(pair[0])):
|
|
315
|
+
stretch = re.sub(re.escape(name) + r"(?=\s*\()", legal, stretch)
|
|
316
|
+
return stretch
|
|
317
|
+
|
|
318
|
+
return _map_unquoted(text, rewrite)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _farthest(errors) -> str:
|
|
322
|
+
"""The most informative of ``parse_any``'s per-dialect messages.
|
|
323
|
+
|
|
324
|
+
The FARTHEST failure, not the last listed — the dialects at the end of
|
|
325
|
+
the detection order are the specialised ones that give up earliest on
|
|
326
|
+
ordinary input. Same rule, same implementation, as
|
|
327
|
+
:func:`unicode_logic_kit.api._suggest`'s.
|
|
328
|
+
"""
|
|
329
|
+
from ..api import _message_progress # deferred: api imports fol
|
|
330
|
+
|
|
331
|
+
if not errors:
|
|
332
|
+
return "no parser accepted the text"
|
|
333
|
+
best = max(errors, key=lambda e: _message_progress(e.get("message", "")))
|
|
334
|
+
return best.get("message", "")
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _failed(text: str, kind: str, message: str,
|
|
338
|
+
suggestion: Optional[str] = None) -> DialectRepairResult:
|
|
339
|
+
"""An honest failure: nothing rewritten, the diagnosis handed back."""
|
|
340
|
+
return DialectRepairResult(
|
|
341
|
+
ok=False, formula=None, repaired_text=text,
|
|
342
|
+
issues=(Issue(kind, message, suggestion=suggestion),), changed=False)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
# ---------------------------------------------------------------------------
|
|
346
|
+
# Case 4 — a truth glyph beside another dialect's connectives
|
|
347
|
+
# ---------------------------------------------------------------------------
|
|
348
|
+
|
|
349
|
+
#: The unicode glyph of each truth constant and, for the two ASCII dialects, the
|
|
350
|
+
#: constant that dialect spells it with: ``(Prover9, TPTP)``.
|
|
351
|
+
_GLYPH_SPELLINGS = {"⊤": ("$T", "$true"), "⊥": ("$F", "$false")}
|
|
352
|
+
_GLYPH_RE = re.compile("[⊤⊥]")
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _glyph_rewrites(text: str):
|
|
356
|
+
"""The texts that spell each truth glyph of ``text`` as an ASCII dialect's own
|
|
357
|
+
constant, Prover9's first and TPTP's second; nothing when ``text`` has no glyph.
|
|
358
|
+
|
|
359
|
+
A glyph between single quotes is part of a constant's name (``'⊤'``) and is
|
|
360
|
+
neither found nor rewritten."""
|
|
361
|
+
if not any(_GLYPH_RE.search(stretch) for stretch in _unquoted_stretches(text)):
|
|
362
|
+
return
|
|
363
|
+
for column in (0, 1):
|
|
364
|
+
yield _map_unquoted(
|
|
365
|
+
text,
|
|
366
|
+
lambda stretch, column=column: _GLYPH_RE.sub(
|
|
367
|
+
lambda m: _GLYPH_SPELLINGS[m.group(0)][column], stretch)), column
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _parse_reading_glyphs(text: str, dialect: Optional[str]):
|
|
371
|
+
"""``(parse result, rewritten text)``: ``text`` parsed as it stands, else, when it
|
|
372
|
+
does not parse and holds a truth glyph, parsed with the glyphs spelled as an ASCII
|
|
373
|
+
dialect's constants (see the module docstring, case 4). The rewritten text is
|
|
374
|
+
``None`` when the text parsed as it stands or no rewrite parsed."""
|
|
375
|
+
from ..api import parse_any # deferred: api imports fol
|
|
376
|
+
|
|
377
|
+
parsed = parse_any(text, hint=dialect)
|
|
378
|
+
if parsed.ok:
|
|
379
|
+
return parsed, None
|
|
380
|
+
for rewritten, _column in _glyph_rewrites(text):
|
|
381
|
+
retried = parse_any(rewritten, hint=dialect)
|
|
382
|
+
if retried.ok:
|
|
383
|
+
return retried, rewritten
|
|
384
|
+
return parsed, None
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _glyph_issue(rewritten: str, dialect_name: Optional[str]) -> Issue:
|
|
388
|
+
return Issue(
|
|
389
|
+
"truth_glyph",
|
|
390
|
+
"the text writes the truth glyph ⊤ / ⊥ beside the connectives of another "
|
|
391
|
+
f"dialect, which no single grammar reads; the glyphs were read as that "
|
|
392
|
+
f"dialect's own truth constants ({rewritten!r}), and the formula parsed "
|
|
393
|
+
f"as {dialect_name!r}. The result holds the truth constants $true / $false.",
|
|
394
|
+
suggestion=rewritten)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
# ---------------------------------------------------------------------------
|
|
398
|
+
# Public API
|
|
399
|
+
# ---------------------------------------------------------------------------
|
|
400
|
+
|
|
401
|
+
def repair_formula(text: str, *, dialect: Optional[str] = None,
|
|
402
|
+
sanitize_invalid_names: bool = True,
|
|
403
|
+
close_free_variables: bool = False,
|
|
404
|
+
mapping: Optional[NameMapping] = None
|
|
405
|
+
) -> DialectRepairResult:
|
|
406
|
+
"""Repair one formula written in the kit's own surface syntax.
|
|
407
|
+
|
|
408
|
+
Pipeline:
|
|
409
|
+
|
|
410
|
+
1. Parse via :func:`unicode_logic_kit.api.parse_any`. A text that does not
|
|
411
|
+
parse but holds the truth glyph ``⊤`` / ``⊥`` beside another dialect's
|
|
412
|
+
connectives (``P -> ⊥``) is read with the glyph spelled as that
|
|
413
|
+
dialect's constant (case 4), and says so in an issue. On failure, if the
|
|
414
|
+
grammar refused a mix of same-level connectives, report it and stop
|
|
415
|
+
(case 1' — never bracketed, see the module docstring). Otherwise, if
|
|
416
|
+
``sanitize_invalid_names``, rename every functor-position name that is
|
|
417
|
+
legal in no symbol class (case 2) and re-parse; a rewrite that still
|
|
418
|
+
does not parse is discarded entirely.
|
|
419
|
+
2. Free variables (case 3) are read off the AST: always reported, and
|
|
420
|
+
additionally closed — argument-drop where that exact shape holds, else
|
|
421
|
+
universal closure — only when ``close_free_variables=True``.
|
|
422
|
+
3. ``repaired_text`` is the unicode rendering of the resulting AST.
|
|
423
|
+
|
|
424
|
+
Args:
|
|
425
|
+
text: one formula in the kit's unicode syntax.
|
|
426
|
+
dialect: pin the dialect (``parse_any``'s ``hint``) instead of
|
|
427
|
+
detecting it. ``"unicode"`` for the whole mode ladder, a mode
|
|
428
|
+
name (``"fol"``, ``"modal"``, …) for one mode.
|
|
429
|
+
sanitize_invalid_names: attempt case 2's rename when parsing fails.
|
|
430
|
+
``False`` reports the syntax error unchanged.
|
|
431
|
+
close_free_variables: apply case 3's fix instead of only reporting
|
|
432
|
+
it. Default ``False``: report, never touch — an unclosed free
|
|
433
|
+
variable never blocks ``ok=True``, because the AST is valid; the
|
|
434
|
+
kit just will not guess whether you meant universal closure or an
|
|
435
|
+
argument that should be dropped.
|
|
436
|
+
mapping: an existing :class:`NameMapping` to extend, so the same
|
|
437
|
+
original name renames to the same token across a whole run. A
|
|
438
|
+
fresh one is used when omitted.
|
|
439
|
+
|
|
440
|
+
Returns:
|
|
441
|
+
A :class:`DialectRepairResult`.
|
|
442
|
+
"""
|
|
443
|
+
issues: List[Issue] = []
|
|
444
|
+
renames: Tuple[Tuple[str, str], ...] = ()
|
|
445
|
+
|
|
446
|
+
parsed, glyph_text = _parse_reading_glyphs(text, dialect)
|
|
447
|
+
if glyph_text is not None:
|
|
448
|
+
issues.append(_glyph_issue(glyph_text, parsed.dialect))
|
|
449
|
+
if not parsed.ok:
|
|
450
|
+
message = _farthest(parsed.errors)
|
|
451
|
+
if _MIXING_MARKER in message.lower():
|
|
452
|
+
return _failed(
|
|
453
|
+
text, "mixed_connectives",
|
|
454
|
+
f"{message} The kit does not bracket this for you: the two "
|
|
455
|
+
"readings are different formulas and choosing one would be a "
|
|
456
|
+
"guess. Write the brackets you mean.")
|
|
457
|
+
if not sanitize_invalid_names:
|
|
458
|
+
return _failed(text, "syntax_error", message)
|
|
459
|
+
|
|
460
|
+
candidates = _find_name_candidates(text)
|
|
461
|
+
if not candidates:
|
|
462
|
+
return _failed(text, "syntax_error", message)
|
|
463
|
+
|
|
464
|
+
if mapping is None:
|
|
465
|
+
mapping = NameMapping()
|
|
466
|
+
# Preseed with the identifiers already in the text so a legalised
|
|
467
|
+
# name can never collide with a symbol that is already there (see
|
|
468
|
+
# the module docstring's collision limit). The words of a quoted
|
|
469
|
+
# constant are in the preseed too, on purpose: that reads the text and
|
|
470
|
+
# rewrites nothing, and a legal name that a constant already spells is
|
|
471
|
+
# one more name to keep the renamed predicate away from.
|
|
472
|
+
mapping.used.update(match.group(0) for match in _ident_re().finditer(text))
|
|
473
|
+
renames = tuple((name, mapping.for_predicate(name)) for name in candidates)
|
|
474
|
+
|
|
475
|
+
retried = _apply_renames(text, renames)
|
|
476
|
+
parsed, glyph_text = _parse_reading_glyphs(retried, dialect)
|
|
477
|
+
if glyph_text is not None:
|
|
478
|
+
issues.append(_glyph_issue(glyph_text, parsed.dialect))
|
|
479
|
+
if not parsed.ok:
|
|
480
|
+
names = sorted(name for name, _legal in renames)
|
|
481
|
+
return _failed(
|
|
482
|
+
text, "syntax_error",
|
|
483
|
+
f"renaming {names!r} did not make the formula parseable: "
|
|
484
|
+
f"{_farthest(parsed.errors)}")
|
|
485
|
+
for name, legal in renames:
|
|
486
|
+
issues.append(Issue(
|
|
487
|
+
"invalid_predicate_name",
|
|
488
|
+
f"{name!r} is a legal token in no symbol class of this "
|
|
489
|
+
f"dialect; renamed to the predicate {legal!r}. The original "
|
|
490
|
+
"spelling is preserved in this result's `names` (and in the "
|
|
491
|
+
"NameMapping, if you passed one) — nothing about the name is "
|
|
492
|
+
"lost, but it now lives beside the formula rather than in it.",
|
|
493
|
+
suggestion=legal))
|
|
494
|
+
|
|
495
|
+
formula: Node = parsed.formula
|
|
496
|
+
final_formula = formula
|
|
497
|
+
free = free_variables(formula)
|
|
498
|
+
if free:
|
|
499
|
+
names_text = ", ".join(sorted(v.name for v in free))
|
|
500
|
+
if close_free_variables:
|
|
501
|
+
dropped = (_drop_argument_fix(formula, next(iter(free)))
|
|
502
|
+
if len(free) == 1 else None)
|
|
503
|
+
if dropped is not None:
|
|
504
|
+
final_formula = dropped
|
|
505
|
+
issues.append(Issue(
|
|
506
|
+
"free_variable",
|
|
507
|
+
f"unbound variable {names_text!r} is the sole argument of "
|
|
508
|
+
"the left-hand definiendum and does not recur on the "
|
|
509
|
+
"right — dropped it: the definition is, in substance, a "
|
|
510
|
+
"0-ary property that only accidentally carries an "
|
|
511
|
+
"argument.",
|
|
512
|
+
suggestion=dropped.to_unicode_str()))
|
|
513
|
+
else:
|
|
514
|
+
final_formula = _close_universally(formula, free)
|
|
515
|
+
issues.append(Issue(
|
|
516
|
+
"free_variable",
|
|
517
|
+
f"unbound variable(s) {names_text} closed via universal "
|
|
518
|
+
"quantification (standard universal-closure convention; "
|
|
519
|
+
"the argument-drop fix does not apply — the formula is "
|
|
520
|
+
"not a `P(x) ↔ …`/`P(x) → …` definition with x absent "
|
|
521
|
+
"from the right-hand side).",
|
|
522
|
+
suggestion=final_formula.to_unicode_str()))
|
|
523
|
+
else:
|
|
524
|
+
issues.append(Issue(
|
|
525
|
+
"free_variable",
|
|
526
|
+
f"unbound variable(s) {names_text} — not closed (default "
|
|
527
|
+
"close_free_variables=False). The kit will not guess whether "
|
|
528
|
+
"you meant universal closure or an argument that should be "
|
|
529
|
+
"dropped; pass close_free_variables=True to apply the "
|
|
530
|
+
"documented conservative fix, or rewrite by hand."))
|
|
531
|
+
|
|
532
|
+
repaired_text = final_formula.to_unicode_str()
|
|
533
|
+
return DialectRepairResult(
|
|
534
|
+
ok=True, formula=final_formula, repaired_text=repaired_text,
|
|
535
|
+
issues=tuple(issues),
|
|
536
|
+
changed=_squash(text) != _squash(repaired_text),
|
|
537
|
+
dialect=parsed.dialect, names=renames)
|