unicode-fol-kit 0.1.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_fol_kit/__init__.py +17 -0
- unicode_fol_kit/atp/__init__.py +4 -0
- unicode_fol_kit/atp/prover9_entailment.py +70 -0
- unicode_fol_kit/atp/z3_equivalence.py +22 -0
- unicode_fol_kit/fol/__init__.py +15 -0
- unicode_fol_kit/fol/folparser.py +21 -0
- unicode_fol_kit/fol/naming.py +174 -0
- unicode_fol_kit/fol/nodes.py +779 -0
- unicode_fol_kit/fol/syntax.lark +76 -0
- unicode_fol_kit-0.1.0.dist-info/METADATA +302 -0
- unicode_fol_kit-0.1.0.dist-info/RECORD +13 -0
- unicode_fol_kit-0.1.0.dist-info/WHEEL +4 -0
- unicode_fol_kit-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from .fol import (
|
|
2
|
+
FOLParser,
|
|
3
|
+
Node, Variable, Constant, Number, Function,
|
|
4
|
+
Atom, Not, And, Or, Xor, Implies, Iff, Quantifier,
|
|
5
|
+
Z3Env,
|
|
6
|
+
NamingError, ParsingError,
|
|
7
|
+
)
|
|
8
|
+
from .atp import formulas_are_equivalent, check_logical_entailment
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"FOLParser",
|
|
12
|
+
"Node", "Variable", "Constant", "Number", "Function",
|
|
13
|
+
"Atom", "Not", "And", "Or", "Xor", "Implies", "Iff", "Quantifier",
|
|
14
|
+
"Z3Env",
|
|
15
|
+
"NamingError", "ParsingError",
|
|
16
|
+
"formulas_are_equivalent", "check_logical_entailment",
|
|
17
|
+
]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import tempfile
|
|
3
|
+
import os
|
|
4
|
+
from ..fol.nodes import Node
|
|
5
|
+
|
|
6
|
+
def _generate_prover9_input(premises: list[Node], conclusion: Node) -> str:
|
|
7
|
+
"""
|
|
8
|
+
Generates a Prover9 input string from given premises and conclusion.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
premises (list[Node]): List of premise formulas in FOL.
|
|
12
|
+
conclusion (Node): Conclusion formula in FOL.
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
str: Formatted Prover9 input string.
|
|
16
|
+
"""
|
|
17
|
+
prover9_input = []
|
|
18
|
+
|
|
19
|
+
#Header section
|
|
20
|
+
prover9_input.append("set(prolog_style_variables).")
|
|
21
|
+
prover9_input.append("set(auto_denials).")
|
|
22
|
+
prover9_input.append("clear(print_initial_clauses).")
|
|
23
|
+
prover9_input.append("clear(print_kept).")
|
|
24
|
+
prover9_input.append("clear(print_given).")
|
|
25
|
+
prover9_input.append("")
|
|
26
|
+
|
|
27
|
+
# Premises section
|
|
28
|
+
prover9_input.append("formulas(assumptions).")
|
|
29
|
+
for premise in premises:
|
|
30
|
+
prover9_formula = premise.to_prover9()
|
|
31
|
+
prover9_input.append(f" {prover9_formula}.")
|
|
32
|
+
prover9_input.append("end_of_list.")
|
|
33
|
+
prover9_input.append("")
|
|
34
|
+
|
|
35
|
+
# Goal section
|
|
36
|
+
prover9_input.append("formulas(goals).")
|
|
37
|
+
prover9_goal = conclusion.to_prover9()
|
|
38
|
+
prover9_input.append(f" {prover9_goal}.")
|
|
39
|
+
prover9_input.append("end_of_list.")
|
|
40
|
+
|
|
41
|
+
return "\n".join(prover9_input)
|
|
42
|
+
|
|
43
|
+
def _run_prover9(input: str, prover9_path: str, timeout: int=30) ->bool:
|
|
44
|
+
"""Run the prover9 command line tool."""
|
|
45
|
+
|
|
46
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.in', delete=False) as temp_file:
|
|
47
|
+
temp_file.write(input)
|
|
48
|
+
temp_filename = temp_file.name
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
result = subprocess.run(
|
|
52
|
+
[prover9_path, '-f', temp_filename],
|
|
53
|
+
capture_output=True,
|
|
54
|
+
text=True,
|
|
55
|
+
timeout=timeout
|
|
56
|
+
)
|
|
57
|
+
success = "THEOREM PROVED" in result.stdout
|
|
58
|
+
except subprocess.TimeoutExpired:
|
|
59
|
+
success = False
|
|
60
|
+
|
|
61
|
+
os.unlink(temp_filename)
|
|
62
|
+
return success
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def check_logical_entailment(premises: list[Node], conclusion: Node, prover9_path: str) ->bool:
|
|
66
|
+
"""Checks if a conclusion entails from the defined premises by using prover9."""
|
|
67
|
+
|
|
68
|
+
prover9_input = _generate_prover9_input(premises, conclusion)
|
|
69
|
+
success = _run_prover9(prover9_input, prover9_path)
|
|
70
|
+
return success
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from ..fol.nodes import Node
|
|
2
|
+
from z3 import Solver, unsat, set_param, Not
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def formulas_are_equivalent(formula1: Node, formula2: Node, timeout: int=10000) -> bool:
|
|
6
|
+
"""Checks if two formulas are equivalent by using z3 solver by verifying the unsatisfiability
|
|
7
|
+
of ¬(φ ↔ ψ), where φ is the formula produced by the LLM and ψ is the ground-truth formula.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
phi = formula1.to_z3()
|
|
11
|
+
psi = formula2.to_z3()
|
|
12
|
+
|
|
13
|
+
solver = Solver()
|
|
14
|
+
solver.set("timeout", timeout)
|
|
15
|
+
solver.set("random_seed", 42)
|
|
16
|
+
solver.add(Not(phi==psi))
|
|
17
|
+
|
|
18
|
+
result = solver.check()
|
|
19
|
+
if result == unsat:
|
|
20
|
+
return True
|
|
21
|
+
|
|
22
|
+
return False
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from .folparser import FOLParser
|
|
2
|
+
from .nodes import (
|
|
3
|
+
Node, Variable, Constant, Number, Function,
|
|
4
|
+
Atom, Not, And, Or, Xor, Implies, Iff, Quantifier,
|
|
5
|
+
Z3Env,
|
|
6
|
+
)
|
|
7
|
+
from .naming import NamingError, ParsingError
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"FOLParser",
|
|
11
|
+
"Node", "Variable", "Constant", "Number", "Function",
|
|
12
|
+
"Atom", "Not", "And", "Or", "Xor", "Implies", "Iff", "Quantifier",
|
|
13
|
+
"Z3Env",
|
|
14
|
+
"NamingError", "ParsingError",
|
|
15
|
+
]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
from lark import Lark, UnexpectedCharacters, UnexpectedToken, UnexpectedEOF
|
|
3
|
+
from .nodes import Node, FOLTransformer
|
|
4
|
+
from .naming import NamingError, ParsingError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class FOLParser:
|
|
8
|
+
def __init__(self):
|
|
9
|
+
grammar_path = pathlib.Path(__file__).parent / "syntax.lark"
|
|
10
|
+
with open(grammar_path, encoding="utf-8") as file:
|
|
11
|
+
grammar = file.read()
|
|
12
|
+
self.parser = Lark(grammar, parser='earley')
|
|
13
|
+
|
|
14
|
+
def parse(self, text: str) -> Node:
|
|
15
|
+
try:
|
|
16
|
+
tree = self.parser.parse(text)
|
|
17
|
+
return FOLTransformer().transform(tree)
|
|
18
|
+
except UnexpectedCharacters as e:
|
|
19
|
+
raise NamingError(self.parser, e, text)
|
|
20
|
+
except (UnexpectedToken, UnexpectedEOF) as e:
|
|
21
|
+
raise ParsingError(self.parser, e, text)
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
from lark import Lark, UnexpectedCharacters, UnexpectedToken, UnexpectedEOF
|
|
2
|
+
from lark.exceptions import ParseError
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
_SYMBOL_NAMES = {
|
|
6
|
+
"↔": "biconditional",
|
|
7
|
+
"→": "implication",
|
|
8
|
+
"∧": "conjunction",
|
|
9
|
+
"∨": "disjunction",
|
|
10
|
+
"⊕": "exclusive or",
|
|
11
|
+
"¬": "negation",
|
|
12
|
+
"∀": "universal quantifier",
|
|
13
|
+
"∃": "existential quantifier",
|
|
14
|
+
"≤": "less-than-or-equal",
|
|
15
|
+
"≥": "greater-than-or-equal",
|
|
16
|
+
"≠": "not-equal",
|
|
17
|
+
"=": "equality",
|
|
18
|
+
"<": "less-than",
|
|
19
|
+
">": "greater-than",
|
|
20
|
+
"+": "plus",
|
|
21
|
+
"-": "minus",
|
|
22
|
+
"*": "times",
|
|
23
|
+
"/": "division",
|
|
24
|
+
"(": "opening parenthesis",
|
|
25
|
+
")": "closing parenthesis",
|
|
26
|
+
"[": "opening bracket",
|
|
27
|
+
"]": "closing bracket",
|
|
28
|
+
",": "comma",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
_NAMED_TOKENS = {
|
|
32
|
+
"PREDICATE": "predicate",
|
|
33
|
+
"NAME": "name/constant",
|
|
34
|
+
"VARIABLE": "variable",
|
|
35
|
+
"CONSTANT": "constant",
|
|
36
|
+
"NUMBER": "number",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
_MIXING_SYMBOLS = {"∧", "∨", "⊕"}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _build_pattern_index(parser: Lark) -> dict:
|
|
43
|
+
"""Map terminal name -> raw pattern string for every terminal in the grammar."""
|
|
44
|
+
index = {}
|
|
45
|
+
for term in parser.terminals:
|
|
46
|
+
pattern = term.pattern
|
|
47
|
+
index[term.name] = pattern.value if hasattr(pattern, "value") else str(pattern)
|
|
48
|
+
return index
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _display_name(token_type: str, patterns: dict) -> str:
|
|
52
|
+
"""Resolve a terminal name to a human-readable label.
|
|
53
|
+
|
|
54
|
+
Named tokens use a fixed label; symbol tokens are resolved through their
|
|
55
|
+
pattern; anything unknown falls back to the raw terminal name.
|
|
56
|
+
"""
|
|
57
|
+
if token_type in _NAMED_TOKENS:
|
|
58
|
+
return _NAMED_TOKENS[token_type]
|
|
59
|
+
pattern = patterns.get(token_type)
|
|
60
|
+
if pattern in _SYMBOL_NAMES:
|
|
61
|
+
return _SYMBOL_NAMES[pattern]
|
|
62
|
+
return token_type
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _is_structural(token_type: str) -> bool:
|
|
66
|
+
"""True if the token is a symbol/operator rather than a name-like token."""
|
|
67
|
+
return token_type not in _NAMED_TOKENS
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _format_expected(expected, patterns: dict) -> str:
|
|
71
|
+
"""Render a set of expected terminal names as a sorted, deduplicated label list."""
|
|
72
|
+
labels = []
|
|
73
|
+
seen = set()
|
|
74
|
+
for token_type in expected or ():
|
|
75
|
+
label = _display_name(token_type, patterns)
|
|
76
|
+
if label not in seen:
|
|
77
|
+
seen.add(label)
|
|
78
|
+
labels.append(label)
|
|
79
|
+
return ", ".join(sorted(labels)) if labels else "a valid token"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class NamingError(UnexpectedCharacters):
|
|
83
|
+
"""Human-readable wrapper around a lexer-level UnexpectedCharacters failure.
|
|
84
|
+
|
|
85
|
+
Token names are resolved through the grammar's terminal patterns rather
|
|
86
|
+
than hard-coded anonymous-token numbers, so messages stay correct when the
|
|
87
|
+
grammar changes.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
def __init__(self, parser: Lark, original_exception: UnexpectedCharacters, formula: str):
|
|
91
|
+
self._patterns = _build_pattern_index(parser)
|
|
92
|
+
|
|
93
|
+
pos = original_exception.pos_in_stream
|
|
94
|
+
prefix = formula[:pos] if pos is not None and pos >= 0 else formula
|
|
95
|
+
tokens = list(parser.lex(prefix))
|
|
96
|
+
last_token = tokens[-1] if tokens else None
|
|
97
|
+
|
|
98
|
+
if last_token is None:
|
|
99
|
+
message = (
|
|
100
|
+
f"SYNTAX_ERROR: Unexpected character "
|
|
101
|
+
f"'{original_exception.char}' at position {original_exception.column}"
|
|
102
|
+
)
|
|
103
|
+
else:
|
|
104
|
+
message = self._build_message(last_token, original_exception)
|
|
105
|
+
|
|
106
|
+
self.__dict__.update(original_exception.__dict__)
|
|
107
|
+
self.args = (message,)
|
|
108
|
+
|
|
109
|
+
def _build_message(self, last_token, exc: UnexpectedCharacters) -> str:
|
|
110
|
+
"""Compose the final error message for a lexer-level failure."""
|
|
111
|
+
display = _display_name(last_token.type, self._patterns)
|
|
112
|
+
|
|
113
|
+
if _is_structural(last_token.type):
|
|
114
|
+
message = (
|
|
115
|
+
f"SYNTAX_ERROR: Unexpected character '{exc.char}' at position "
|
|
116
|
+
f"{exc.column} after {display} '{last_token.value}'"
|
|
117
|
+
)
|
|
118
|
+
if exc.char in _MIXING_SYMBOLS:
|
|
119
|
+
message += (
|
|
120
|
+
". Hint: Cannot mix conjunction (∧), disjunction (∨), and "
|
|
121
|
+
"exclusive or (⊕) without parentheses"
|
|
122
|
+
)
|
|
123
|
+
return message
|
|
124
|
+
|
|
125
|
+
message = (
|
|
126
|
+
f"SYNTAX_ERROR: Invalid {display} '{last_token.value}' - "
|
|
127
|
+
f"unexpected character '{exc.char}' at position {exc.column}"
|
|
128
|
+
)
|
|
129
|
+
pattern = self._patterns.get(last_token.type)
|
|
130
|
+
if pattern:
|
|
131
|
+
message += f". Expected pattern: {pattern}"
|
|
132
|
+
return message
|
|
133
|
+
|
|
134
|
+
def __str__(self):
|
|
135
|
+
return self.args[0]
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class ParsingError(ParseError):
|
|
139
|
+
"""Human-readable wrapper around parser-level failures.
|
|
140
|
+
|
|
141
|
+
Handles both UnexpectedToken (a valid token in an invalid position) and
|
|
142
|
+
UnexpectedEOF (the formula ended before it was complete). The expected-token
|
|
143
|
+
set is rendered through the same pattern-based resolution as NamingError.
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
def __init__(self, parser: Lark, original_exception, formula: str):
|
|
147
|
+
self._patterns = _build_pattern_index(parser)
|
|
148
|
+
expected_str = _format_expected(getattr(original_exception, "expected", None), self._patterns)
|
|
149
|
+
|
|
150
|
+
if isinstance(original_exception, UnexpectedEOF):
|
|
151
|
+
message = (
|
|
152
|
+
f"SYNTAX_ERROR: Incomplete formula - the input ended unexpectedly. "
|
|
153
|
+
f"Expected: {expected_str}"
|
|
154
|
+
)
|
|
155
|
+
else:
|
|
156
|
+
token = original_exception.token
|
|
157
|
+
display = _display_name(token.type, self._patterns)
|
|
158
|
+
column = getattr(original_exception, "column", None)
|
|
159
|
+
where = f" at position {column}" if column not in (None, -1) else ""
|
|
160
|
+
message = (
|
|
161
|
+
f"SYNTAX_ERROR: Unexpected {display} '{token.value}'{where}. "
|
|
162
|
+
f"Expected: {expected_str}"
|
|
163
|
+
)
|
|
164
|
+
if str(token.value) in _MIXING_SYMBOLS:
|
|
165
|
+
message += (
|
|
166
|
+
". Hint: Cannot mix conjunction (∧), disjunction (∨), and "
|
|
167
|
+
"exclusive or (⊕) without parentheses"
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
self.__dict__.update(original_exception.__dict__)
|
|
171
|
+
self.args = (message,)
|
|
172
|
+
|
|
173
|
+
def __str__(self):
|
|
174
|
+
return self.args[0]
|