unicode-fol-kit 0.2.0__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.0 → unicode_fol_kit-0.3.0}/PKG-INFO +89 -19
- {unicode_fol_kit-0.2.0 → unicode_fol_kit-0.3.0}/README.md +88 -18
- {unicode_fol_kit-0.2.0 → unicode_fol_kit-0.3.0}/pyproject.toml +1 -1
- {unicode_fol_kit-0.2.0 → unicode_fol_kit-0.3.0}/unicode_fol_kit/__init__.py +1 -1
- {unicode_fol_kit-0.2.0 → unicode_fol_kit-0.3.0}/unicode_fol_kit/fol/_fol_nodes.py +11 -0
- {unicode_fol_kit-0.2.0 → unicode_fol_kit-0.3.0}/unicode_fol_kit/fol/_msfl_nodes.py +176 -0
- {unicode_fol_kit-0.2.0 → unicode_fol_kit-0.3.0}/.gitignore +0 -0
- {unicode_fol_kit-0.2.0 → unicode_fol_kit-0.3.0}/LICENSE +0 -0
- {unicode_fol_kit-0.2.0 → unicode_fol_kit-0.3.0}/unicode_fol_kit/atp/__init__.py +0 -0
- {unicode_fol_kit-0.2.0 → unicode_fol_kit-0.3.0}/unicode_fol_kit/atp/prover9_entailment.py +0 -0
- {unicode_fol_kit-0.2.0 → unicode_fol_kit-0.3.0}/unicode_fol_kit/atp/z3_equivalence.py +0 -0
- {unicode_fol_kit-0.2.0 → unicode_fol_kit-0.3.0}/unicode_fol_kit/fol/__init__.py +0 -0
- {unicode_fol_kit-0.2.0 → unicode_fol_kit-0.3.0}/unicode_fol_kit/fol/grammars/fl.lark +0 -0
- {unicode_fol_kit-0.2.0 → unicode_fol_kit-0.3.0}/unicode_fol_kit/fol/grammars/fol.lark +0 -0
- {unicode_fol_kit-0.2.0 → unicode_fol_kit-0.3.0}/unicode_fol_kit/fol/grammars/msfl.lark +0 -0
- {unicode_fol_kit-0.2.0 → unicode_fol_kit-0.3.0}/unicode_fol_kit/fol/grammars/msfol.lark +0 -0
- {unicode_fol_kit-0.2.0 → unicode_fol_kit-0.3.0}/unicode_fol_kit/fol/grammars/terminals.lark +0 -0
- {unicode_fol_kit-0.2.0 → unicode_fol_kit-0.3.0}/unicode_fol_kit/fol/msflparser.py +0 -0
- {unicode_fol_kit-0.2.0 → unicode_fol_kit-0.3.0}/unicode_fol_kit/fol/naming.py +0 -0
- {unicode_fol_kit-0.2.0 → 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
|
|
@@ -24,11 +24,11 @@ Description-Content-Type: text/markdown
|
|
|
24
24
|
|
|
25
25
|
# unicode-fol-kit
|
|
26
26
|
|
|
27
|
-
A Python toolkit for parsing and working with first-order logic (FOL) formulas written with Unicode operators. The single parser class `MSFLParser` supports
|
|
27
|
+
A Python toolkit for parsing and working with first-order logic (FOL) formulas written with Unicode operators. The single parser class `MSFLParser` supports four modes — classical FOL, many-sorted FOL (MSFOL), many-sorted fuzzy logic (MSFL), and single-sorted fuzzy logic (FL, Łukasiewicz) — selected by constructor flags.
|
|
28
28
|
|
|
29
29
|
## Features
|
|
30
30
|
|
|
31
|
-
- **
|
|
31
|
+
- **Four parser modes** — FOL, many-sorted FOL (MSFOL), many-sorted fuzzy/Łukasiewicz logic (MSFL), and single-sorted fuzzy/Łukasiewicz logic (FL), all from one class
|
|
32
32
|
- **Unicode surface syntax** — natural symbols (∀ ∃ ∧ ∨ ¬ → ↔ ⊕ ⊗ = ≠ ≤ ≥) with no ASCII fallbacks needed
|
|
33
33
|
- **Sorted quantifiers and constants** — `∀x:Human P(x)`, `P(alice:Human)` in MSFOL and MSFL modes
|
|
34
34
|
- **Łukasiewicz operators** — weak ∧ / ∨ (min/max), strong ⊗ / ⊕ (t-norm/t-conorm), and Łukasiewicz ¬ → ↔ in MSFL mode
|
|
@@ -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
|
|
@@ -69,7 +70,7 @@ pip install .
|
|
|
69
70
|
MSFLParser(many_sorted=False, fuzzy=False) # FOL (default)
|
|
70
71
|
MSFLParser(many_sorted=True, fuzzy=False) # MSFOL
|
|
71
72
|
MSFLParser(many_sorted=True, fuzzy=True) # MSFL
|
|
72
|
-
MSFLParser(many_sorted=False, fuzzy=True) #
|
|
73
|
+
MSFLParser(many_sorted=False, fuzzy=True) # FL
|
|
73
74
|
```
|
|
74
75
|
|
|
75
76
|
| `many_sorted` | `fuzzy` | Mode | Quantifiers | Constants | Connectives |
|
|
@@ -77,6 +78,7 @@ MSFLParser(many_sorted=False, fuzzy=True) # → raises ValueError
|
|
|
77
78
|
| `False` | `False` | **FOL** | unsorted `∀x` | unsorted | classical ∧ ∨ ⊕ ¬ → ↔ |
|
|
78
79
|
| `True` | `False` | **MSFOL** | sorted `∀x:Sort` | sorted `alice:Sort` | classical ∧ ∨ ¬ → ↔ (no ⊕) |
|
|
79
80
|
| `True` | `True` | **MSFL** | sorted `∀x:Sort` | sorted `alice:Sort` | weak ∧ ∨, strong ⊗ ⊕, Łuk ¬ → ↔ |
|
|
81
|
+
| `False` | `True` | **FL** | unsorted `∀x` | unsorted | weak ∧ ∨, strong ⊗ ⊕, Łuk ¬ → ↔ |
|
|
80
82
|
|
|
81
83
|
## Usage
|
|
82
84
|
|
|
@@ -120,6 +122,26 @@ parser.parse("P(x) → Q(x)") # LukImplication (min{1, 1−x+y})
|
|
|
120
122
|
parser.parse("∀x:Human P(x)") # SortedQuantifier
|
|
121
123
|
```
|
|
122
124
|
|
|
125
|
+
### FL mode
|
|
126
|
+
|
|
127
|
+
Łukasiewicz operators with **unsorted** quantifiers and plain constants — same connectives as MSFL, same quantifier/constant syntax as FOL:
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
parser = MSFLParser(many_sorted=False, fuzzy=True)
|
|
131
|
+
|
|
132
|
+
parser.parse("P(x) ∧ Q(x)") # WeakConjunction (min)
|
|
133
|
+
parser.parse("P(x) ⊗ Q(x)") # StrongConjunction (t-norm: max{0, x+y−1})
|
|
134
|
+
parser.parse("P(x) ⊕ Q(x)") # StrongDisjunction (t-conorm: min{1, x+y})
|
|
135
|
+
parser.parse("¬P(x)") # LukNegation (1−x)
|
|
136
|
+
parser.parse("P(x) → Q(x)") # LukImplication (min{1, 1−x+y})
|
|
137
|
+
parser.parse("∀x P(x)") # unsorted Quantifier (no sort annotation)
|
|
138
|
+
parser.parse("P(alice)") # plain Constant (no sort annotation)
|
|
139
|
+
|
|
140
|
+
# Lambda works in FL mode too
|
|
141
|
+
parser.parse("λx. P(x) ⊗ Q(x)")
|
|
142
|
+
# Lambda(LambdaVar("x"), StrongConjunction(Atom("P",[LambdaVar("x")]), Atom("Q",[LambdaVar("x")])))
|
|
143
|
+
```
|
|
144
|
+
|
|
123
145
|
### ASCII tree view
|
|
124
146
|
|
|
125
147
|
```python
|
|
@@ -133,6 +155,26 @@ print(formula.tree_str())
|
|
|
133
155
|
# └── Variable: x
|
|
134
156
|
```
|
|
135
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
|
+
|
|
136
178
|
### Exporting to other formats
|
|
137
179
|
|
|
138
180
|
```python
|
|
@@ -365,6 +407,21 @@ Connectives are reinterpreted as Łukasiewicz operators:
|
|
|
365
407
|
| `φ ↔ ψ` | Łuk. equivalence | 1 − \|φ − ψ\| |
|
|
366
408
|
| `∀x:Sort φ`, `∃x:Sort φ` | sorted quantifiers | |
|
|
367
409
|
|
|
410
|
+
#### FL mode
|
|
411
|
+
|
|
412
|
+
Same Łukasiewicz connectives as MSFL, but with **unsorted** quantifiers and plain constants (no `:Sort` required):
|
|
413
|
+
|
|
414
|
+
| Syntax | Operator | Semantics |
|
|
415
|
+
|---|---|---|
|
|
416
|
+
| `¬φ` | Łuk. negation | 1 − φ |
|
|
417
|
+
| `φ ∧ ψ` | weak conjunction | min(φ, ψ) |
|
|
418
|
+
| `φ ∨ ψ` | weak disjunction | max(φ, ψ) |
|
|
419
|
+
| `φ ⊗ ψ` | strong conjunction | max(0, φ + ψ − 1) |
|
|
420
|
+
| `φ ⊕ ψ` | strong disjunction | min(1, φ + ψ) |
|
|
421
|
+
| `φ → ψ` | Łuk. implication | min(1, 1 − φ + ψ) |
|
|
422
|
+
| `φ ↔ ψ` | Łuk. equivalence | 1 − \|φ − ψ\| |
|
|
423
|
+
| `∀x φ`, `∃x φ` | unsorted quantifiers | |
|
|
424
|
+
|
|
368
425
|
A formula may be wrapped in parentheses `( … )` or square brackets `[ … ]`; the two are interchangeable for grouping.
|
|
369
426
|
|
|
370
427
|
### Operator precedence
|
|
@@ -374,7 +431,7 @@ The precedence levels are the same across all three modes (MSFL uses the same sy
|
|
|
374
431
|
| Precedence | Operators | Associativity |
|
|
375
432
|
|---|---|---|
|
|
376
433
|
| 1 (highest) | `¬`, quantifiers `∀` / `∃` | prefix |
|
|
377
|
-
| 2 | `∧` `∨` `⊕` (FOL) / `∧` `∨` (MSFOL) / `∧` `∨` `⊗` `⊕` (MSFL) | left |
|
|
434
|
+
| 2 | `∧` `∨` `⊕` (FOL) / `∧` `∨` (MSFOL) / `∧` `∨` `⊗` `⊕` (MSFL / FL) | left |
|
|
378
435
|
| 3 | `→` | right |
|
|
379
436
|
| 4 (lowest) | `↔` | right |
|
|
380
437
|
|
|
@@ -404,7 +461,7 @@ P(x) ∧ Q(x) ∨ R(x) # rejected
|
|
|
404
461
|
(P(x) ∧ Q(x)) ∨ R(x) # accepted
|
|
405
462
|
```
|
|
406
463
|
|
|
407
|
-
**MSFL mode** — `∧`, `∨`, `⊗`, `⊕` cannot be mixed:
|
|
464
|
+
**MSFL / FL mode** — `∧`, `∨`, `⊗`, `⊕` cannot be mixed:
|
|
408
465
|
|
|
409
466
|
```text
|
|
410
467
|
P(x) ∧ Q(x) ⊗ R(x) # rejected
|
|
@@ -433,16 +490,16 @@ Quantifiers can be stacked directly: `∀x:H ∀y:H ∃z:A φ`.
|
|
|
433
490
|
|
|
434
491
|
### Supported symbols
|
|
435
492
|
|
|
436
|
-
| Category | FOL | MSFOL | MSFL |
|
|
437
|
-
|
|
438
|
-
| Quantifiers | `∀` `∃` (unsorted) | `∀` `∃` (sorted `:Sort`) | `∀` `∃` (sorted `:Sort`) |
|
|
439
|
-
| Connectives | `∧` `∨` `⊕` `¬` `→` `↔` | `∧` `∨` `¬` `→` `↔` | `∧` `∨` `⊗` `⊕` `¬` `→` `↔` |
|
|
440
|
-
| Lambda | `λ` | `λ` | `λ` |
|
|
441
|
-
| Sort annotations | — | `:Sort` | `:Sort` |
|
|
442
|
-
| Equality / comparison | `=` `≠` `<` `>` `≤` `≥` | same | same |
|
|
443
|
-
| Arithmetic | `+` `-` `*` `/` | same | same |
|
|
444
|
-
| Grouping | `(` `)` `[` `]` | same | same |
|
|
445
|
-
| Argument separator | `,` | same | same |
|
|
493
|
+
| Category | FOL | MSFOL | MSFL | FL |
|
|
494
|
+
|---|---|---|---|---|
|
|
495
|
+
| Quantifiers | `∀` `∃` (unsorted) | `∀` `∃` (sorted `:Sort`) | `∀` `∃` (sorted `:Sort`) | `∀` `∃` (unsorted) |
|
|
496
|
+
| Connectives | `∧` `∨` `⊕` `¬` `→` `↔` | `∧` `∨` `¬` `→` `↔` | `∧` `∨` `⊗` `⊕` `¬` `→` `↔` | `∧` `∨` `⊗` `⊕` `¬` `→` `↔` |
|
|
497
|
+
| Lambda | `λ` | `λ` | `λ` | `λ` |
|
|
498
|
+
| Sort annotations | — | `:Sort` | `:Sort` | — |
|
|
499
|
+
| Equality / comparison | `=` `≠` `<` `>` `≤` `≥` | same | same | same |
|
|
500
|
+
| Arithmetic | `+` `-` `*` `/` | same | same | same |
|
|
501
|
+
| Grouping | `(` `)` `[` `]` | same | same | same |
|
|
502
|
+
| Argument separator | `,` | same | same | same |
|
|
446
503
|
|
|
447
504
|
Whitespace is insignificant and may be used freely between tokens — including before sort annotation colons.
|
|
448
505
|
|
|
@@ -524,15 +581,23 @@ parser.parse("(λP. P(x))(Q)")
|
|
|
524
581
|
### A complete MSFOL example
|
|
525
582
|
|
|
526
583
|
```text
|
|
527
|
-
∀x:Person ∀y:Person (Knows(x, y) ∧ Trusted(y
|
|
584
|
+
∀x:Person ∀y:Person (Knows(x, y) ∧ Trusted(y)) → Shares(x, y)
|
|
528
585
|
```
|
|
529
586
|
|
|
530
587
|
### A complete MSFL example
|
|
531
588
|
|
|
532
589
|
```text
|
|
533
590
|
∀x:Patient ∀y:Treatment
|
|
534
|
-
(Effective(y
|
|
535
|
-
→ Recommended(x
|
|
591
|
+
(Effective(y) ⊗ Tolerable(x, y))
|
|
592
|
+
→ Recommended(x, y)
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
### A complete FL example
|
|
596
|
+
|
|
597
|
+
```text
|
|
598
|
+
∀x ∀y
|
|
599
|
+
(Effective(y) ⊗ Tolerable(x, y))
|
|
600
|
+
→ Recommended(x, y)
|
|
536
601
|
```
|
|
537
602
|
|
|
538
603
|
## AST nodes
|
|
@@ -618,6 +683,11 @@ MSFLParser(many_sorted=True).parse("P(x) ∧ Q(x) ∨ R(x)")
|
|
|
618
683
|
MSFLParser(many_sorted=True, fuzzy=True).parse("P(x) ∧ Q(x) ⊗ R(x)")
|
|
619
684
|
# SYNTAX_ERROR: … Hint: Cannot mix weak conjunction (∧), weak disjunction (∨),
|
|
620
685
|
# strong conjunction (⊗), and strong disjunction (⊕) without parentheses
|
|
686
|
+
|
|
687
|
+
# FL mode — same hint as MSFL (Łukasiewicz connectives, unsorted)
|
|
688
|
+
MSFLParser(many_sorted=False, fuzzy=True).parse("P(x) ∧ Q(x) ⊗ R(x)")
|
|
689
|
+
# SYNTAX_ERROR: … Hint: Cannot mix weak conjunction (∧), weak disjunction (∨),
|
|
690
|
+
# strong conjunction (⊗), and strong disjunction (⊕) without parentheses
|
|
621
691
|
```
|
|
622
692
|
|
|
623
693
|
## Citation
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# unicode-fol-kit
|
|
2
2
|
|
|
3
|
-
A Python toolkit for parsing and working with first-order logic (FOL) formulas written with Unicode operators. The single parser class `MSFLParser` supports
|
|
3
|
+
A Python toolkit for parsing and working with first-order logic (FOL) formulas written with Unicode operators. The single parser class `MSFLParser` supports four modes — classical FOL, many-sorted FOL (MSFOL), many-sorted fuzzy logic (MSFL), and single-sorted fuzzy logic (FL, Łukasiewicz) — selected by constructor flags.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
- **
|
|
7
|
+
- **Four parser modes** — FOL, many-sorted FOL (MSFOL), many-sorted fuzzy/Łukasiewicz logic (MSFL), and single-sorted fuzzy/Łukasiewicz logic (FL), all from one class
|
|
8
8
|
- **Unicode surface syntax** — natural symbols (∀ ∃ ∧ ∨ ¬ → ↔ ⊕ ⊗ = ≠ ≤ ≥) with no ASCII fallbacks needed
|
|
9
9
|
- **Sorted quantifiers and constants** — `∀x:Human P(x)`, `P(alice:Human)` in MSFOL and MSFL modes
|
|
10
10
|
- **Łukasiewicz operators** — weak ∧ / ∨ (min/max), strong ⊗ / ⊕ (t-norm/t-conorm), and Łukasiewicz ¬ → ↔ in MSFL mode
|
|
@@ -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
|
|
@@ -45,7 +46,7 @@ pip install .
|
|
|
45
46
|
MSFLParser(many_sorted=False, fuzzy=False) # FOL (default)
|
|
46
47
|
MSFLParser(many_sorted=True, fuzzy=False) # MSFOL
|
|
47
48
|
MSFLParser(many_sorted=True, fuzzy=True) # MSFL
|
|
48
|
-
MSFLParser(many_sorted=False, fuzzy=True) #
|
|
49
|
+
MSFLParser(many_sorted=False, fuzzy=True) # FL
|
|
49
50
|
```
|
|
50
51
|
|
|
51
52
|
| `many_sorted` | `fuzzy` | Mode | Quantifiers | Constants | Connectives |
|
|
@@ -53,6 +54,7 @@ MSFLParser(many_sorted=False, fuzzy=True) # → raises ValueError
|
|
|
53
54
|
| `False` | `False` | **FOL** | unsorted `∀x` | unsorted | classical ∧ ∨ ⊕ ¬ → ↔ |
|
|
54
55
|
| `True` | `False` | **MSFOL** | sorted `∀x:Sort` | sorted `alice:Sort` | classical ∧ ∨ ¬ → ↔ (no ⊕) |
|
|
55
56
|
| `True` | `True` | **MSFL** | sorted `∀x:Sort` | sorted `alice:Sort` | weak ∧ ∨, strong ⊗ ⊕, Łuk ¬ → ↔ |
|
|
57
|
+
| `False` | `True` | **FL** | unsorted `∀x` | unsorted | weak ∧ ∨, strong ⊗ ⊕, Łuk ¬ → ↔ |
|
|
56
58
|
|
|
57
59
|
## Usage
|
|
58
60
|
|
|
@@ -96,6 +98,26 @@ parser.parse("P(x) → Q(x)") # LukImplication (min{1, 1−x+y})
|
|
|
96
98
|
parser.parse("∀x:Human P(x)") # SortedQuantifier
|
|
97
99
|
```
|
|
98
100
|
|
|
101
|
+
### FL mode
|
|
102
|
+
|
|
103
|
+
Łukasiewicz operators with **unsorted** quantifiers and plain constants — same connectives as MSFL, same quantifier/constant syntax as FOL:
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
parser = MSFLParser(many_sorted=False, fuzzy=True)
|
|
107
|
+
|
|
108
|
+
parser.parse("P(x) ∧ Q(x)") # WeakConjunction (min)
|
|
109
|
+
parser.parse("P(x) ⊗ Q(x)") # StrongConjunction (t-norm: max{0, x+y−1})
|
|
110
|
+
parser.parse("P(x) ⊕ Q(x)") # StrongDisjunction (t-conorm: min{1, x+y})
|
|
111
|
+
parser.parse("¬P(x)") # LukNegation (1−x)
|
|
112
|
+
parser.parse("P(x) → Q(x)") # LukImplication (min{1, 1−x+y})
|
|
113
|
+
parser.parse("∀x P(x)") # unsorted Quantifier (no sort annotation)
|
|
114
|
+
parser.parse("P(alice)") # plain Constant (no sort annotation)
|
|
115
|
+
|
|
116
|
+
# Lambda works in FL mode too
|
|
117
|
+
parser.parse("λx. P(x) ⊗ Q(x)")
|
|
118
|
+
# Lambda(LambdaVar("x"), StrongConjunction(Atom("P",[LambdaVar("x")]), Atom("Q",[LambdaVar("x")])))
|
|
119
|
+
```
|
|
120
|
+
|
|
99
121
|
### ASCII tree view
|
|
100
122
|
|
|
101
123
|
```python
|
|
@@ -109,6 +131,26 @@ print(formula.tree_str())
|
|
|
109
131
|
# └── Variable: x
|
|
110
132
|
```
|
|
111
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
|
+
|
|
112
154
|
### Exporting to other formats
|
|
113
155
|
|
|
114
156
|
```python
|
|
@@ -341,6 +383,21 @@ Connectives are reinterpreted as Łukasiewicz operators:
|
|
|
341
383
|
| `φ ↔ ψ` | Łuk. equivalence | 1 − \|φ − ψ\| |
|
|
342
384
|
| `∀x:Sort φ`, `∃x:Sort φ` | sorted quantifiers | |
|
|
343
385
|
|
|
386
|
+
#### FL mode
|
|
387
|
+
|
|
388
|
+
Same Łukasiewicz connectives as MSFL, but with **unsorted** quantifiers and plain constants (no `:Sort` required):
|
|
389
|
+
|
|
390
|
+
| Syntax | Operator | Semantics |
|
|
391
|
+
|---|---|---|
|
|
392
|
+
| `¬φ` | Łuk. negation | 1 − φ |
|
|
393
|
+
| `φ ∧ ψ` | weak conjunction | min(φ, ψ) |
|
|
394
|
+
| `φ ∨ ψ` | weak disjunction | max(φ, ψ) |
|
|
395
|
+
| `φ ⊗ ψ` | strong conjunction | max(0, φ + ψ − 1) |
|
|
396
|
+
| `φ ⊕ ψ` | strong disjunction | min(1, φ + ψ) |
|
|
397
|
+
| `φ → ψ` | Łuk. implication | min(1, 1 − φ + ψ) |
|
|
398
|
+
| `φ ↔ ψ` | Łuk. equivalence | 1 − \|φ − ψ\| |
|
|
399
|
+
| `∀x φ`, `∃x φ` | unsorted quantifiers | |
|
|
400
|
+
|
|
344
401
|
A formula may be wrapped in parentheses `( … )` or square brackets `[ … ]`; the two are interchangeable for grouping.
|
|
345
402
|
|
|
346
403
|
### Operator precedence
|
|
@@ -350,7 +407,7 @@ The precedence levels are the same across all three modes (MSFL uses the same sy
|
|
|
350
407
|
| Precedence | Operators | Associativity |
|
|
351
408
|
|---|---|---|
|
|
352
409
|
| 1 (highest) | `¬`, quantifiers `∀` / `∃` | prefix |
|
|
353
|
-
| 2 | `∧` `∨` `⊕` (FOL) / `∧` `∨` (MSFOL) / `∧` `∨` `⊗` `⊕` (MSFL) | left |
|
|
410
|
+
| 2 | `∧` `∨` `⊕` (FOL) / `∧` `∨` (MSFOL) / `∧` `∨` `⊗` `⊕` (MSFL / FL) | left |
|
|
354
411
|
| 3 | `→` | right |
|
|
355
412
|
| 4 (lowest) | `↔` | right |
|
|
356
413
|
|
|
@@ -380,7 +437,7 @@ P(x) ∧ Q(x) ∨ R(x) # rejected
|
|
|
380
437
|
(P(x) ∧ Q(x)) ∨ R(x) # accepted
|
|
381
438
|
```
|
|
382
439
|
|
|
383
|
-
**MSFL mode** — `∧`, `∨`, `⊗`, `⊕` cannot be mixed:
|
|
440
|
+
**MSFL / FL mode** — `∧`, `∨`, `⊗`, `⊕` cannot be mixed:
|
|
384
441
|
|
|
385
442
|
```text
|
|
386
443
|
P(x) ∧ Q(x) ⊗ R(x) # rejected
|
|
@@ -409,16 +466,16 @@ Quantifiers can be stacked directly: `∀x:H ∀y:H ∃z:A φ`.
|
|
|
409
466
|
|
|
410
467
|
### Supported symbols
|
|
411
468
|
|
|
412
|
-
| Category | FOL | MSFOL | MSFL |
|
|
413
|
-
|
|
414
|
-
| Quantifiers | `∀` `∃` (unsorted) | `∀` `∃` (sorted `:Sort`) | `∀` `∃` (sorted `:Sort`) |
|
|
415
|
-
| Connectives | `∧` `∨` `⊕` `¬` `→` `↔` | `∧` `∨` `¬` `→` `↔` | `∧` `∨` `⊗` `⊕` `¬` `→` `↔` |
|
|
416
|
-
| Lambda | `λ` | `λ` | `λ` |
|
|
417
|
-
| Sort annotations | — | `:Sort` | `:Sort` |
|
|
418
|
-
| Equality / comparison | `=` `≠` `<` `>` `≤` `≥` | same | same |
|
|
419
|
-
| Arithmetic | `+` `-` `*` `/` | same | same |
|
|
420
|
-
| Grouping | `(` `)` `[` `]` | same | same |
|
|
421
|
-
| Argument separator | `,` | same | same |
|
|
469
|
+
| Category | FOL | MSFOL | MSFL | FL |
|
|
470
|
+
|---|---|---|---|---|
|
|
471
|
+
| Quantifiers | `∀` `∃` (unsorted) | `∀` `∃` (sorted `:Sort`) | `∀` `∃` (sorted `:Sort`) | `∀` `∃` (unsorted) |
|
|
472
|
+
| Connectives | `∧` `∨` `⊕` `¬` `→` `↔` | `∧` `∨` `¬` `→` `↔` | `∧` `∨` `⊗` `⊕` `¬` `→` `↔` | `∧` `∨` `⊗` `⊕` `¬` `→` `↔` |
|
|
473
|
+
| Lambda | `λ` | `λ` | `λ` | `λ` |
|
|
474
|
+
| Sort annotations | — | `:Sort` | `:Sort` | — |
|
|
475
|
+
| Equality / comparison | `=` `≠` `<` `>` `≤` `≥` | same | same | same |
|
|
476
|
+
| Arithmetic | `+` `-` `*` `/` | same | same | same |
|
|
477
|
+
| Grouping | `(` `)` `[` `]` | same | same | same |
|
|
478
|
+
| Argument separator | `,` | same | same | same |
|
|
422
479
|
|
|
423
480
|
Whitespace is insignificant and may be used freely between tokens — including before sort annotation colons.
|
|
424
481
|
|
|
@@ -500,15 +557,23 @@ parser.parse("(λP. P(x))(Q)")
|
|
|
500
557
|
### A complete MSFOL example
|
|
501
558
|
|
|
502
559
|
```text
|
|
503
|
-
∀x:Person ∀y:Person (Knows(x, y) ∧ Trusted(y
|
|
560
|
+
∀x:Person ∀y:Person (Knows(x, y) ∧ Trusted(y)) → Shares(x, y)
|
|
504
561
|
```
|
|
505
562
|
|
|
506
563
|
### A complete MSFL example
|
|
507
564
|
|
|
508
565
|
```text
|
|
509
566
|
∀x:Patient ∀y:Treatment
|
|
510
|
-
(Effective(y
|
|
511
|
-
→ Recommended(x
|
|
567
|
+
(Effective(y) ⊗ Tolerable(x, y))
|
|
568
|
+
→ Recommended(x, y)
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
### A complete FL example
|
|
572
|
+
|
|
573
|
+
```text
|
|
574
|
+
∀x ∀y
|
|
575
|
+
(Effective(y) ⊗ Tolerable(x, y))
|
|
576
|
+
→ Recommended(x, y)
|
|
512
577
|
```
|
|
513
578
|
|
|
514
579
|
## AST nodes
|
|
@@ -594,6 +659,11 @@ MSFLParser(many_sorted=True).parse("P(x) ∧ Q(x) ∨ R(x)")
|
|
|
594
659
|
MSFLParser(many_sorted=True, fuzzy=True).parse("P(x) ∧ Q(x) ⊗ R(x)")
|
|
595
660
|
# SYNTAX_ERROR: … Hint: Cannot mix weak conjunction (∧), weak disjunction (∨),
|
|
596
661
|
# strong conjunction (⊗), and strong disjunction (⊕) without parentheses
|
|
662
|
+
|
|
663
|
+
# FL mode — same hint as MSFL (Łukasiewicz connectives, unsorted)
|
|
664
|
+
MSFLParser(many_sorted=False, fuzzy=True).parse("P(x) ∧ Q(x) ⊗ R(x)")
|
|
665
|
+
# SYNTAX_ERROR: … Hint: Cannot mix weak conjunction (∧), weak disjunction (∨),
|
|
666
|
+
# strong conjunction (⊗), and strong disjunction (⊕) without parentheses
|
|
597
667
|
```
|
|
598
668
|
|
|
599
669
|
## Citation
|
|
@@ -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
|