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.
Files changed (20) hide show
  1. {unicode_fol_kit-0.2.0 → unicode_fol_kit-0.3.0}/PKG-INFO +89 -19
  2. {unicode_fol_kit-0.2.0 → unicode_fol_kit-0.3.0}/README.md +88 -18
  3. {unicode_fol_kit-0.2.0 → unicode_fol_kit-0.3.0}/pyproject.toml +1 -1
  4. {unicode_fol_kit-0.2.0 → unicode_fol_kit-0.3.0}/unicode_fol_kit/__init__.py +1 -1
  5. {unicode_fol_kit-0.2.0 → unicode_fol_kit-0.3.0}/unicode_fol_kit/fol/_fol_nodes.py +11 -0
  6. {unicode_fol_kit-0.2.0 → unicode_fol_kit-0.3.0}/unicode_fol_kit/fol/_msfl_nodes.py +176 -0
  7. {unicode_fol_kit-0.2.0 → unicode_fol_kit-0.3.0}/.gitignore +0 -0
  8. {unicode_fol_kit-0.2.0 → unicode_fol_kit-0.3.0}/LICENSE +0 -0
  9. {unicode_fol_kit-0.2.0 → unicode_fol_kit-0.3.0}/unicode_fol_kit/atp/__init__.py +0 -0
  10. {unicode_fol_kit-0.2.0 → unicode_fol_kit-0.3.0}/unicode_fol_kit/atp/prover9_entailment.py +0 -0
  11. {unicode_fol_kit-0.2.0 → unicode_fol_kit-0.3.0}/unicode_fol_kit/atp/z3_equivalence.py +0 -0
  12. {unicode_fol_kit-0.2.0 → unicode_fol_kit-0.3.0}/unicode_fol_kit/fol/__init__.py +0 -0
  13. {unicode_fol_kit-0.2.0 → unicode_fol_kit-0.3.0}/unicode_fol_kit/fol/grammars/fl.lark +0 -0
  14. {unicode_fol_kit-0.2.0 → unicode_fol_kit-0.3.0}/unicode_fol_kit/fol/grammars/fol.lark +0 -0
  15. {unicode_fol_kit-0.2.0 → unicode_fol_kit-0.3.0}/unicode_fol_kit/fol/grammars/msfl.lark +0 -0
  16. {unicode_fol_kit-0.2.0 → unicode_fol_kit-0.3.0}/unicode_fol_kit/fol/grammars/msfol.lark +0 -0
  17. {unicode_fol_kit-0.2.0 → unicode_fol_kit-0.3.0}/unicode_fol_kit/fol/grammars/terminals.lark +0 -0
  18. {unicode_fol_kit-0.2.0 → unicode_fol_kit-0.3.0}/unicode_fol_kit/fol/msflparser.py +0 -0
  19. {unicode_fol_kit-0.2.0 → unicode_fol_kit-0.3.0}/unicode_fol_kit/fol/naming.py +0 -0
  20. {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.2.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 three modes — classical FOL, many-sorted FOL (MSFOL), and many-sorted fuzzy logic (MSFL, Łukasiewicz) — selected by constructor flags.
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
- - **Three parser modes** — FOL, many-sorted FOL (MSFOL), and many-sorted fuzzy/Łukasiewicz logic (MSFL), all from one class
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) # → raises ValueError
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:Person)) → Shares(x, 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:Treatment) ⊗ Tolerable(x:Patient, y:Treatment))
535
- → Recommended(x:Patient, y:Treatment)
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 three modes — classical FOL, many-sorted FOL (MSFOL), and many-sorted fuzzy logic (MSFL, Łukasiewicz) — selected by constructor flags.
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
- - **Three parser modes** — FOL, many-sorted FOL (MSFOL), and many-sorted fuzzy/Łukasiewicz logic (MSFL), all from one class
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) # → raises ValueError
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:Person)) → Shares(x, 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:Treatment) ⊗ Tolerable(x:Patient, y:Treatment))
511
- → Recommended(x:Patient, y:Treatment)
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "unicode-fol-kit"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  description = "Parser and toolkit for first-order logic formulas using Unicode operators"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -17,7 +17,7 @@ from .fol import (
17
17
  )
18
18
  from .atp import formulas_are_equivalent, check_logical_entailment
19
19
 
20
- __version__ = "0.2.0"
20
+ __version__ = "0.3.0"
21
21
 
22
22
  __all__ = [
23
23
  "MSFLParser",
@@ -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