unicode-fol-kit 0.2.1__tar.gz → 0.3.0__tar.gz
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_fol_kit-0.2.1 → unicode_fol_kit-0.3.0}/PKG-INFO +25 -4
- {unicode_fol_kit-0.2.1 → unicode_fol_kit-0.3.0}/README.md +24 -3
- {unicode_fol_kit-0.2.1 → unicode_fol_kit-0.3.0}/pyproject.toml +1 -1
- {unicode_fol_kit-0.2.1 → unicode_fol_kit-0.3.0}/unicode_fol_kit/__init__.py +1 -1
- {unicode_fol_kit-0.2.1 → unicode_fol_kit-0.3.0}/unicode_fol_kit/fol/_fol_nodes.py +11 -0
- {unicode_fol_kit-0.2.1 → unicode_fol_kit-0.3.0}/unicode_fol_kit/fol/_msfl_nodes.py +176 -0
- {unicode_fol_kit-0.2.1 → unicode_fol_kit-0.3.0}/.gitignore +0 -0
- {unicode_fol_kit-0.2.1 → unicode_fol_kit-0.3.0}/LICENSE +0 -0
- {unicode_fol_kit-0.2.1 → unicode_fol_kit-0.3.0}/unicode_fol_kit/atp/__init__.py +0 -0
- {unicode_fol_kit-0.2.1 → unicode_fol_kit-0.3.0}/unicode_fol_kit/atp/prover9_entailment.py +0 -0
- {unicode_fol_kit-0.2.1 → unicode_fol_kit-0.3.0}/unicode_fol_kit/atp/z3_equivalence.py +0 -0
- {unicode_fol_kit-0.2.1 → unicode_fol_kit-0.3.0}/unicode_fol_kit/fol/__init__.py +0 -0
- {unicode_fol_kit-0.2.1 → unicode_fol_kit-0.3.0}/unicode_fol_kit/fol/grammars/fl.lark +0 -0
- {unicode_fol_kit-0.2.1 → unicode_fol_kit-0.3.0}/unicode_fol_kit/fol/grammars/fol.lark +0 -0
- {unicode_fol_kit-0.2.1 → unicode_fol_kit-0.3.0}/unicode_fol_kit/fol/grammars/msfl.lark +0 -0
- {unicode_fol_kit-0.2.1 → unicode_fol_kit-0.3.0}/unicode_fol_kit/fol/grammars/msfol.lark +0 -0
- {unicode_fol_kit-0.2.1 → unicode_fol_kit-0.3.0}/unicode_fol_kit/fol/grammars/terminals.lark +0 -0
- {unicode_fol_kit-0.2.1 → unicode_fol_kit-0.3.0}/unicode_fol_kit/fol/msflparser.py +0 -0
- {unicode_fol_kit-0.2.1 → unicode_fol_kit-0.3.0}/unicode_fol_kit/fol/naming.py +0 -0
- {unicode_fol_kit-0.2.1 → unicode_fol_kit-0.3.0}/unicode_fol_kit/fol/nodes.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: unicode-fol-kit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Parser and toolkit for first-order logic formulas using Unicode operators
|
|
5
5
|
Project-URL: Repository, https://github.com/fvossel/unicode-fol-kit
|
|
6
6
|
Project-URL: Issues, https://github.com/fvossel/unicode-fol-kit/issues
|
|
@@ -36,6 +36,7 @@ A Python toolkit for parsing and working with first-order logic (FOL) formulas w
|
|
|
36
36
|
- **Reductions** — `to_msfol()` lowers Łukasiewicz operators to classical nodes; `to_fol()` further eliminates sorts via relativisation
|
|
37
37
|
- **Serialisation** — convert formulas to/from JSON dictionaries; round-trip safe
|
|
38
38
|
- **Tree view** — render any formula as a readable ASCII tree
|
|
39
|
+
- **Unicode round-trip** — `to_unicode_str()` renders any node back to a parseable Unicode formula; re-parsing in the matching mode yields a structurally equal AST
|
|
39
40
|
- **Z3 export** — translate formulas to Z3 expressions for SMT solving
|
|
40
41
|
- **Prover9 export** — translate formulas to Prover9 syntax for automated theorem proving
|
|
41
42
|
- **TPTP export** — translate formulas to TPTP syntax
|
|
@@ -154,6 +155,26 @@ print(formula.tree_str())
|
|
|
154
155
|
# └── Variable: x
|
|
155
156
|
```
|
|
156
157
|
|
|
158
|
+
### Round-trip to Unicode
|
|
159
|
+
|
|
160
|
+
`to_unicode_str()` is the inverse of parsing: it renders any node back to a Unicode formula string. Re-parsing that string in the same mode reproduces a structurally equal AST. The renderer is precedence-aware and only inserts the parentheses the grammar requires — including the no-mixing rule for same-level connectives and the tight-binding rule for quantifiers.
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
parser = MSFLParser()
|
|
164
|
+
|
|
165
|
+
ast = parser.parse("∀x P(x) ∧ Q(x)")
|
|
166
|
+
ast.to_unicode_str() # '∀x P(x) ∧ Q(x)'
|
|
167
|
+
parser.parse(ast.to_unicode_str()) == ast # True
|
|
168
|
+
|
|
169
|
+
# Precedence-driven parentheses are reconstructed, not the original spelling:
|
|
170
|
+
parser.parse("((P(x) ∧ Q(x)))").to_unicode_str() # 'P(x) ∧ Q(x)'
|
|
171
|
+
parser.parse("P(x) ∧ (Q(x) ∨ R(x))").to_unicode_str() # 'P(x) ∧ (Q(x) ∨ R(x))'
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Available on every node, so subformulas render too. The output targets parseable
|
|
175
|
+
ASTs; alpha-renamed variables introduced by `beta_reduce` (e.g. `x_0`) are not
|
|
176
|
+
valid surface tokens and will not round-trip.
|
|
177
|
+
|
|
157
178
|
### Exporting to other formats
|
|
158
179
|
|
|
159
180
|
```python
|
|
@@ -560,15 +581,15 @@ parser.parse("(λP. P(x))(Q)")
|
|
|
560
581
|
### A complete MSFOL example
|
|
561
582
|
|
|
562
583
|
```text
|
|
563
|
-
∀x:Person ∀y:Person (Knows(x, y) ∧ Trusted(y
|
|
584
|
+
∀x:Person ∀y:Person (Knows(x, y) ∧ Trusted(y)) → Shares(x, y)
|
|
564
585
|
```
|
|
565
586
|
|
|
566
587
|
### A complete MSFL example
|
|
567
588
|
|
|
568
589
|
```text
|
|
569
590
|
∀x:Patient ∀y:Treatment
|
|
570
|
-
(Effective(y
|
|
571
|
-
→ Recommended(x
|
|
591
|
+
(Effective(y) ⊗ Tolerable(x, y))
|
|
592
|
+
→ Recommended(x, y)
|
|
572
593
|
```
|
|
573
594
|
|
|
574
595
|
### A complete FL example
|
|
@@ -12,6 +12,7 @@ A Python toolkit for parsing and working with first-order logic (FOL) formulas w
|
|
|
12
12
|
- **Reductions** — `to_msfol()` lowers Łukasiewicz operators to classical nodes; `to_fol()` further eliminates sorts via relativisation
|
|
13
13
|
- **Serialisation** — convert formulas to/from JSON dictionaries; round-trip safe
|
|
14
14
|
- **Tree view** — render any formula as a readable ASCII tree
|
|
15
|
+
- **Unicode round-trip** — `to_unicode_str()` renders any node back to a parseable Unicode formula; re-parsing in the matching mode yields a structurally equal AST
|
|
15
16
|
- **Z3 export** — translate formulas to Z3 expressions for SMT solving
|
|
16
17
|
- **Prover9 export** — translate formulas to Prover9 syntax for automated theorem proving
|
|
17
18
|
- **TPTP export** — translate formulas to TPTP syntax
|
|
@@ -130,6 +131,26 @@ print(formula.tree_str())
|
|
|
130
131
|
# └── Variable: x
|
|
131
132
|
```
|
|
132
133
|
|
|
134
|
+
### Round-trip to Unicode
|
|
135
|
+
|
|
136
|
+
`to_unicode_str()` is the inverse of parsing: it renders any node back to a Unicode formula string. Re-parsing that string in the same mode reproduces a structurally equal AST. The renderer is precedence-aware and only inserts the parentheses the grammar requires — including the no-mixing rule for same-level connectives and the tight-binding rule for quantifiers.
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
parser = MSFLParser()
|
|
140
|
+
|
|
141
|
+
ast = parser.parse("∀x P(x) ∧ Q(x)")
|
|
142
|
+
ast.to_unicode_str() # '∀x P(x) ∧ Q(x)'
|
|
143
|
+
parser.parse(ast.to_unicode_str()) == ast # True
|
|
144
|
+
|
|
145
|
+
# Precedence-driven parentheses are reconstructed, not the original spelling:
|
|
146
|
+
parser.parse("((P(x) ∧ Q(x)))").to_unicode_str() # 'P(x) ∧ Q(x)'
|
|
147
|
+
parser.parse("P(x) ∧ (Q(x) ∨ R(x))").to_unicode_str() # 'P(x) ∧ (Q(x) ∨ R(x))'
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Available on every node, so subformulas render too. The output targets parseable
|
|
151
|
+
ASTs; alpha-renamed variables introduced by `beta_reduce` (e.g. `x_0`) are not
|
|
152
|
+
valid surface tokens and will not round-trip.
|
|
153
|
+
|
|
133
154
|
### Exporting to other formats
|
|
134
155
|
|
|
135
156
|
```python
|
|
@@ -536,15 +557,15 @@ parser.parse("(λP. P(x))(Q)")
|
|
|
536
557
|
### A complete MSFOL example
|
|
537
558
|
|
|
538
559
|
```text
|
|
539
|
-
∀x:Person ∀y:Person (Knows(x, y) ∧ Trusted(y
|
|
560
|
+
∀x:Person ∀y:Person (Knows(x, y) ∧ Trusted(y)) → Shares(x, y)
|
|
540
561
|
```
|
|
541
562
|
|
|
542
563
|
### A complete MSFL example
|
|
543
564
|
|
|
544
565
|
```text
|
|
545
566
|
∀x:Patient ∀y:Treatment
|
|
546
|
-
(Effective(y
|
|
547
|
-
→ Recommended(x
|
|
567
|
+
(Effective(y) ⊗ Tolerable(x, y))
|
|
568
|
+
→ Recommended(x, y)
|
|
548
569
|
```
|
|
549
570
|
|
|
550
571
|
### A complete FL example
|
|
@@ -108,6 +108,17 @@ class Node:
|
|
|
108
108
|
children.extend(c for c in value if isinstance(c, Node))
|
|
109
109
|
return label, children
|
|
110
110
|
|
|
111
|
+
def to_unicode_str(self) -> str:
|
|
112
|
+
"""Render this node back to a parseable Unicode formula string.
|
|
113
|
+
|
|
114
|
+
The result, re-parsed in the matching MSFLParser mode, yields a
|
|
115
|
+
structurally equal AST (parser round-trip). The renderer lives in
|
|
116
|
+
_msfl_nodes.py (imported lazily to avoid a circular import) because it
|
|
117
|
+
dispatches over both the FOL nodes here and the MSFL/lambda nodes there.
|
|
118
|
+
"""
|
|
119
|
+
from ._msfl_nodes import _uni
|
|
120
|
+
return _uni(self)
|
|
121
|
+
|
|
111
122
|
def tree_str(self) -> str:
|
|
112
123
|
"""Render the AST as a multi-line ASCII tree using ├──/└── connectors."""
|
|
113
124
|
label, children = self._tree_parts()
|
|
@@ -893,6 +893,182 @@ def resolve_lambda_scope(node: Node) -> Node:
|
|
|
893
893
|
return _resolve(node, frozenset())
|
|
894
894
|
|
|
895
895
|
|
|
896
|
+
# =========================
|
|
897
|
+
# Unicode rendering (parser round-trip)
|
|
898
|
+
# =========================
|
|
899
|
+
#
|
|
900
|
+
# These functions render any node back to a Unicode formula string that, when
|
|
901
|
+
# re-parsed in the matching MSFLParser mode, yields a structurally equal AST.
|
|
902
|
+
# Dispatch is by class name so this single block covers both the FOL nodes
|
|
903
|
+
# (from _fol_nodes.py) and the MSFL/lambda nodes defined above.
|
|
904
|
+
#
|
|
905
|
+
# Formula precedence — higher binds tighter — mirrors the grammar layering
|
|
906
|
+
# (biimplication < implication < same-level binary < prefix < atomic):
|
|
907
|
+
_UNI_FORMULA_PREC = {
|
|
908
|
+
"Lambda": 0, "Application": 0,
|
|
909
|
+
"Iff": 1, "LukEquivalence": 1,
|
|
910
|
+
"Implies": 2, "LukImplication": 2,
|
|
911
|
+
"And": 3, "Or": 3, "Xor": 3,
|
|
912
|
+
"WeakConjunction": 3, "WeakDisjunction": 3,
|
|
913
|
+
"StrongConjunction": 3, "StrongDisjunction": 3,
|
|
914
|
+
"Not": 4, "LukNegation": 4,
|
|
915
|
+
"Quantifier": 4, "SortedQuantifier": 4,
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
# Binary connective glyphs. Xor and StrongDisjunction share ⊕ (disjoint modes);
|
|
919
|
+
# the weak/strong and classical operators reuse ∧ ∨ → ↔ by class identity.
|
|
920
|
+
_UNI_BINSYM = {
|
|
921
|
+
"And": "∧", "Or": "∨", "Xor": "⊕",
|
|
922
|
+
"Implies": "→", "Iff": "↔",
|
|
923
|
+
"WeakConjunction": "∧", "WeakDisjunction": "∨",
|
|
924
|
+
"StrongConjunction": "⊗", "StrongDisjunction": "⊕",
|
|
925
|
+
"LukImplication": "→", "LukEquivalence": "↔",
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
# The same-level binary group (grammar precedence 2): cannot be mixed without
|
|
929
|
+
# parentheses, and chains are left-folded.
|
|
930
|
+
_UNI_LEVEL2 = frozenset({
|
|
931
|
+
"And", "Or", "Xor",
|
|
932
|
+
"WeakConjunction", "WeakDisjunction",
|
|
933
|
+
"StrongConjunction", "StrongDisjunction",
|
|
934
|
+
})
|
|
935
|
+
|
|
936
|
+
_UNI_INFIX_COMPARE = frozenset({"=", "≠", "<", ">", "≤", "≥"})
|
|
937
|
+
_UNI_ARITH_OPS = frozenset({"+", "-", "*", "/"})
|
|
938
|
+
|
|
939
|
+
|
|
940
|
+
def _uni_prec(node) -> int:
|
|
941
|
+
"""Formula precedence of a node; atomic nodes (atoms, terms) default to 5."""
|
|
942
|
+
return _UNI_FORMULA_PREC.get(type(node).__name__, 5)
|
|
943
|
+
|
|
944
|
+
|
|
945
|
+
def _uni_wrap(node, min_prec: int) -> str:
|
|
946
|
+
"""Render node, parenthesising it when it binds looser than the slot allows."""
|
|
947
|
+
s = _uni(node)
|
|
948
|
+
return f"({s})" if _uni_prec(node) < min_prec else s
|
|
949
|
+
|
|
950
|
+
|
|
951
|
+
def _uni_level2_child(node, parent_cls: str, side: str) -> str:
|
|
952
|
+
"""Render a same-level (∧ ∨ ⊗ ⊕) operand with no-mixing / left-assoc parens.
|
|
953
|
+
|
|
954
|
+
Left operand: a same-class chain stays flat (a ∧ b ∧ c); a different
|
|
955
|
+
same-level operator is parenthesised (no silent mixing). Right operand:
|
|
956
|
+
any same-level node is parenthesised, since the parser left-folds chains.
|
|
957
|
+
"""
|
|
958
|
+
s = _uni(node)
|
|
959
|
+
p = _uni_prec(node)
|
|
960
|
+
if side == "left":
|
|
961
|
+
need = p < 3 or (p == 3 and type(node).__name__ != parent_cls)
|
|
962
|
+
else:
|
|
963
|
+
need = p < 4
|
|
964
|
+
return f"({s})" if need else s
|
|
965
|
+
|
|
966
|
+
|
|
967
|
+
def _uni_atom(node) -> str:
|
|
968
|
+
"""Render an Atom: infix comparison, nullary predicate, or applied predicate."""
|
|
969
|
+
if node.predicate in _UNI_INFIX_COMPARE and len(node.args) == 2:
|
|
970
|
+
return f"{_uni_term(node.args[0])} {node.predicate} {_uni_term(node.args[1])}"
|
|
971
|
+
if not node.args:
|
|
972
|
+
return node.predicate
|
|
973
|
+
return f"{node.predicate}(" + ", ".join(_uni_term(a) for a in node.args) + ")"
|
|
974
|
+
|
|
975
|
+
|
|
976
|
+
def _uni_term_prec(node) -> int:
|
|
977
|
+
"""Arithmetic term precedence: + - → 1, * / → 2, everything atomic → 3."""
|
|
978
|
+
if (type(node).__name__ == "Function"
|
|
979
|
+
and node.name in _UNI_ARITH_OPS and len(node.args) == 2):
|
|
980
|
+
return 2 if node.name in ("*", "/") else 1
|
|
981
|
+
return 3
|
|
982
|
+
|
|
983
|
+
|
|
984
|
+
def _uni_term_wrap(node, parent_prec: int, is_right: bool) -> str:
|
|
985
|
+
"""Render an arithmetic operand, parenthesising per left-associative precedence."""
|
|
986
|
+
s = _uni_term(node)
|
|
987
|
+
p = _uni_term_prec(node)
|
|
988
|
+
need = p < parent_prec or (p == parent_prec and is_right)
|
|
989
|
+
return f"({s})" if need else s
|
|
990
|
+
|
|
991
|
+
|
|
992
|
+
def _uni_spine(node):
|
|
993
|
+
"""Uncurry a left-nested Application into (head, [arg0, arg1, …])."""
|
|
994
|
+
args = []
|
|
995
|
+
n = node
|
|
996
|
+
while isinstance(n, Application):
|
|
997
|
+
args.append(n.arg)
|
|
998
|
+
n = n.func
|
|
999
|
+
args.reverse()
|
|
1000
|
+
return n, args
|
|
1001
|
+
|
|
1002
|
+
|
|
1003
|
+
def _uni_term(node) -> str:
|
|
1004
|
+
"""Render a node occurring in term (argument) position.
|
|
1005
|
+
|
|
1006
|
+
Higher-order applications produced by scope resolution (e.g. foo(x) under
|
|
1007
|
+
λfoo, parsed as a Function then rewritten to Application(LambdaVar, …)) are
|
|
1008
|
+
rendered back as function-call syntax so they re-parse and re-resolve to the
|
|
1009
|
+
same node.
|
|
1010
|
+
"""
|
|
1011
|
+
cls = type(node).__name__
|
|
1012
|
+
if cls in ("Variable", "LambdaVar", "Constant"):
|
|
1013
|
+
return node.name
|
|
1014
|
+
if cls == "Number":
|
|
1015
|
+
return str(node.value)
|
|
1016
|
+
if cls == "SortedConstant":
|
|
1017
|
+
return f"{node.name}:{node.sort}"
|
|
1018
|
+
if cls == "Function":
|
|
1019
|
+
if node.name in _UNI_ARITH_OPS and len(node.args) == 2:
|
|
1020
|
+
p = _uni_term_prec(node)
|
|
1021
|
+
left = _uni_term_wrap(node.args[0], p, is_right=False)
|
|
1022
|
+
right = _uni_term_wrap(node.args[1], p, is_right=True)
|
|
1023
|
+
return f"{left} {node.name} {right}"
|
|
1024
|
+
return f"{node.name}(" + ", ".join(_uni_term(a) for a in node.args) + ")"
|
|
1025
|
+
if cls == "Application":
|
|
1026
|
+
head, args = _uni_spine(node)
|
|
1027
|
+
if isinstance(head, (LambdaVar, Variable, Constant)) and args:
|
|
1028
|
+
return f"{head.name}(" + ", ".join(_uni_term(a) for a in args) + ")"
|
|
1029
|
+
return f"({_uni(node.func)})({_uni(node.arg)})"
|
|
1030
|
+
# Atoms / other formula nodes are not valid terms; best-effort fall-through.
|
|
1031
|
+
return _uni(node)
|
|
1032
|
+
|
|
1033
|
+
|
|
1034
|
+
def _uni(node) -> str:
|
|
1035
|
+
"""Render node as a formula-level Unicode string (no surrounding parens)."""
|
|
1036
|
+
cls = type(node).__name__
|
|
1037
|
+
|
|
1038
|
+
if cls in ("Variable", "LambdaVar", "Constant", "Number", "SortedConstant", "Function"):
|
|
1039
|
+
return _uni_term(node)
|
|
1040
|
+
if cls == "Atom":
|
|
1041
|
+
return _uni_atom(node)
|
|
1042
|
+
|
|
1043
|
+
if cls in ("Not", "LukNegation"):
|
|
1044
|
+
return "¬" + _uni_wrap(node.formula, 4)
|
|
1045
|
+
|
|
1046
|
+
if cls == "Quantifier":
|
|
1047
|
+
return f"{node.type}{node.variable.name} " + _uni_wrap(node.formula, 4)
|
|
1048
|
+
if cls == "SortedQuantifier":
|
|
1049
|
+
return f"{node.type}{node.variable.name}:{node.sort} " + _uni_wrap(node.formula, 4)
|
|
1050
|
+
|
|
1051
|
+
if cls in ("Iff", "LukEquivalence"):
|
|
1052
|
+
# ↔ right-assoc: left slot is implication (≥2), right slot biimplication (≥1)
|
|
1053
|
+
return f"{_uni_wrap(node.left, 2)} {_UNI_BINSYM[cls]} {_uni_wrap(node.right, 1)}"
|
|
1054
|
+
if cls in ("Implies", "LukImplication"):
|
|
1055
|
+
# → right-assoc: left slot same_level_ops (≥3), right slot implication (≥2)
|
|
1056
|
+
return f"{_uni_wrap(node.left, 3)} {_UNI_BINSYM[cls]} {_uni_wrap(node.right, 2)}"
|
|
1057
|
+
if cls in _UNI_LEVEL2:
|
|
1058
|
+
left = _uni_level2_child(node.left, cls, "left")
|
|
1059
|
+
right = _uni_level2_child(node.right, cls, "right")
|
|
1060
|
+
return f"{left} {_UNI_BINSYM[cls]} {right}"
|
|
1061
|
+
|
|
1062
|
+
if cls == "Lambda":
|
|
1063
|
+
# Body extends rightward through the whole formula; never wrapped here.
|
|
1064
|
+
return f"λ{node.param.name}. " + _uni(node.body)
|
|
1065
|
+
if cls == "Application":
|
|
1066
|
+
# (func)(arg): both sides are delimited by parens in the grammar.
|
|
1067
|
+
return f"({_uni(node.func)})({_uni(node.arg)})"
|
|
1068
|
+
|
|
1069
|
+
raise TypeError(f"to_unicode_str: unknown node type {cls}")
|
|
1070
|
+
|
|
1071
|
+
|
|
896
1072
|
# =========================
|
|
897
1073
|
# Registry extension
|
|
898
1074
|
# =========================
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|