poussins 0.0.1__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.
- poussins-0.0.1/PKG-INFO +21 -0
- poussins-0.0.1/README.md +14 -0
- poussins-0.0.1/pyproject.toml +26 -0
- poussins-0.0.1/src/poussins/__init__.py +5 -0
- poussins-0.0.1/src/poussins/__main__.py +6 -0
- poussins-0.0.1/src/poussins/ast/__init__.py +57 -0
- poussins-0.0.1/src/poussins/ast/formulas.py +79 -0
- poussins-0.0.1/src/poussins/ast/proof_terms.py +242 -0
- poussins-0.0.1/src/poussins/cli/__init__.py +14 -0
- poussins-0.0.1/src/poussins/cli/batch.py +24 -0
- poussins-0.0.1/src/poussins/cli/lean.py +13 -0
- poussins-0.0.1/src/poussins/cli/main.py +42 -0
- poussins-0.0.1/src/poussins/cli/step.py +8 -0
- poussins-0.0.1/src/poussins/dsl/__init__.py +27 -0
- poussins-0.0.1/src/poussins/dsl/axiom.py +23 -0
- poussins-0.0.1/src/poussins/dsl/proof_base.py +66 -0
- poussins-0.0.1/src/poussins/dsl/prop.py +104 -0
- poussins-0.0.1/src/poussins/dsl/theorem.py +102 -0
- poussins-0.0.1/src/poussins/environment/__init__.py +1 -0
- poussins-0.0.1/src/poussins/errors/__init__.py +0 -0
- poussins-0.0.1/src/poussins/kernel/__init__.py +7 -0
- poussins-0.0.1/src/poussins/kernel/goal.py +54 -0
- poussins-0.0.1/src/poussins/kernel/proof_engine.py +98 -0
- poussins-0.0.1/src/poussins/kernel/proof_state.py +20 -0
- poussins-0.0.1/src/poussins/tactics/__init__.py +11 -0
- poussins-0.0.1/src/poussins/tactics/derived.py +2 -0
- poussins-0.0.1/src/poussins/tactics/primitive.py +90 -0
poussins-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: poussins
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
|
|
8
|
+
# poussins
|
|
9
|
+
|
|
10
|
+
## Develop
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
# Run
|
|
14
|
+
uv run -m poussins
|
|
15
|
+
|
|
16
|
+
# Unit Test
|
|
17
|
+
uv run pytest
|
|
18
|
+
|
|
19
|
+
# Lint
|
|
20
|
+
uv run ruff check .
|
|
21
|
+
```
|
poussins-0.0.1/README.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "poussins"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
description = "Add your description here"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
dependencies = []
|
|
8
|
+
|
|
9
|
+
[tool.setuptools]
|
|
10
|
+
package-dir = {"" = "src"}
|
|
11
|
+
|
|
12
|
+
[tool.setuptools.packages.find]
|
|
13
|
+
where = ["src"]
|
|
14
|
+
|
|
15
|
+
[build-system]
|
|
16
|
+
requires = ["uv_build>=0.11.9,<0.12.0"]
|
|
17
|
+
build-backend = "uv_build"
|
|
18
|
+
|
|
19
|
+
[dependency-groups]
|
|
20
|
+
dev = [
|
|
21
|
+
"build>=1.5.0",
|
|
22
|
+
"pytest>=9.0.3",
|
|
23
|
+
"pytest-cov>=7.1.0",
|
|
24
|
+
"ruff>=0.15.12",
|
|
25
|
+
"twine>=6.2.0",
|
|
26
|
+
]
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Formula kernel: AST nodes and proof terms for propositional logic."""
|
|
2
|
+
|
|
3
|
+
from .formulas import (
|
|
4
|
+
Formula,
|
|
5
|
+
FVar,
|
|
6
|
+
FImpl,
|
|
7
|
+
FAnd,
|
|
8
|
+
FOr,
|
|
9
|
+
FTrue,
|
|
10
|
+
FFalse,
|
|
11
|
+
FExists,
|
|
12
|
+
)
|
|
13
|
+
from .proof_terms import (
|
|
14
|
+
ProofTerm,
|
|
15
|
+
PMetaVar,
|
|
16
|
+
PVar,
|
|
17
|
+
PLam,
|
|
18
|
+
PApp,
|
|
19
|
+
PAndI,
|
|
20
|
+
PAndE1,
|
|
21
|
+
PAndE2,
|
|
22
|
+
POrIL,
|
|
23
|
+
POrIR,
|
|
24
|
+
POrE,
|
|
25
|
+
PTrueI,
|
|
26
|
+
PFalseE,
|
|
27
|
+
PExI,
|
|
28
|
+
PExE,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
# Formula AST
|
|
33
|
+
"Formula",
|
|
34
|
+
"FVar",
|
|
35
|
+
"FImpl",
|
|
36
|
+
"FAnd",
|
|
37
|
+
"FOr",
|
|
38
|
+
"FTrue",
|
|
39
|
+
"FFalse",
|
|
40
|
+
"FExists",
|
|
41
|
+
# ProofTerm AST
|
|
42
|
+
"ProofTerm",
|
|
43
|
+
"PMetaVar",
|
|
44
|
+
"PVar",
|
|
45
|
+
"PLam",
|
|
46
|
+
"PApp",
|
|
47
|
+
"PAndI",
|
|
48
|
+
"PAndE1",
|
|
49
|
+
"PAndE2",
|
|
50
|
+
"POrIL",
|
|
51
|
+
"POrIR",
|
|
52
|
+
"POrE",
|
|
53
|
+
"PTrueI",
|
|
54
|
+
"PFalseE",
|
|
55
|
+
"PExI",
|
|
56
|
+
"PExE",
|
|
57
|
+
]
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Formula AST nodes for propositional logic.
|
|
2
|
+
|
|
3
|
+
Defined in this module:
|
|
4
|
+
- FVar, FImpl, FAnd, FOr, FTrue, FFalse, FExists
|
|
5
|
+
|
|
6
|
+
Not defined here (by design):
|
|
7
|
+
- FNot is syntax sugar and is expanded during formula parsing
|
|
8
|
+
from formula text to Formula AST:
|
|
9
|
+
~A = FImpl(A, FFalse)
|
|
10
|
+
- FIff is syntax sugar and is expanded during formula parsing
|
|
11
|
+
from formula text to Formula AST:
|
|
12
|
+
A <-> B = FAnd(FImpl(A, B), FImpl(B, A))
|
|
13
|
+
|
|
14
|
+
TODO:
|
|
15
|
+
- Add FAll/FExists over a proper term language (arithmetic milestone).
|
|
16
|
+
The current FExists is second-order (quantifies over propositional variables).
|
|
17
|
+
- Add FEq with a dedicated Term AST (arithmetic milestone).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
from abc import ABC
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Formula(ABC):
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class FVar(Formula):
|
|
31
|
+
"""Propositional variable, e.g. P, Q."""
|
|
32
|
+
|
|
33
|
+
name: str
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class FImpl(Formula):
|
|
38
|
+
"""Implication: antecedent -> consequent."""
|
|
39
|
+
|
|
40
|
+
antecedent: Formula
|
|
41
|
+
consequent: Formula
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class FAnd(Formula):
|
|
46
|
+
"""Conjunction: left /\\ right."""
|
|
47
|
+
|
|
48
|
+
left: Formula
|
|
49
|
+
right: Formula
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True)
|
|
53
|
+
class FOr(Formula):
|
|
54
|
+
"""Disjunction: left \\/ right."""
|
|
55
|
+
|
|
56
|
+
left: Formula
|
|
57
|
+
right: Formula
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True)
|
|
61
|
+
class FTrue(Formula):
|
|
62
|
+
"""Logical true (top)."""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True)
|
|
66
|
+
class FFalse(Formula):
|
|
67
|
+
"""Logical false (bottom)."""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(frozen=True)
|
|
71
|
+
class FExists(Formula):
|
|
72
|
+
"""Existential quantification over a propositional variable: ∃var. body.
|
|
73
|
+
|
|
74
|
+
``var`` is the name of a bound propositional variable (an FVar name).
|
|
75
|
+
``body`` is the formula with ``var`` potentially free.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
var: str
|
|
79
|
+
body: Formula
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"""ProofTerm AST nodes for propositional logic (natural deduction).
|
|
2
|
+
|
|
3
|
+
Each node corresponds to one natural deduction inference rule.
|
|
4
|
+
|
|
5
|
+
Inference rules covered:
|
|
6
|
+
PVar Var h : A in ctx => ctx |- A
|
|
7
|
+
PLam ->-I ctx, x:A |- B => ctx |- A -> B
|
|
8
|
+
PApp ->-E ctx |- A -> B, ctx |- A => ctx |- B
|
|
9
|
+
PAndI /\\-I ctx |- A, ctx |- B => ctx |- A /\\ B
|
|
10
|
+
PAndE1 /\\-E1 ctx |- A /\\ B => ctx |- A
|
|
11
|
+
PAndE2 /\\-E2 ctx |- A /\\ B => ctx |- B
|
|
12
|
+
POrIL \\/-I1 ctx |- A => ctx |- A \\/ B
|
|
13
|
+
POrIR \\/-I2 ctx |- B => ctx |- A \\/ B
|
|
14
|
+
POrE \\/-E ctx |- A \\/ B, ctx,h:A |- C, ctx,h:B |- C => ctx |- C
|
|
15
|
+
PTrueI T-I ctx |- True
|
|
16
|
+
PFalseE F-E ctx |- False => ctx |- A (ex falso)
|
|
17
|
+
PExI ∃-I ctx |- P[A/x] => ctx |- ∃x. P
|
|
18
|
+
PExE ∃-E ctx |- ∃x. P, ctx, h:P[α/x] |- C => ctx |- C
|
|
19
|
+
|
|
20
|
+
Note on negation: FNot is not a primitive node.
|
|
21
|
+
~A is sugar for FImpl(A, FFalse), expanded during formula parsing
|
|
22
|
+
from formula text to Formula AST.
|
|
23
|
+
PNotI = PLam, PNotE = PApp (no extra nodes needed).
|
|
24
|
+
|
|
25
|
+
TODO:
|
|
26
|
+
- Add PRefl once FEq and a Term AST are introduced.
|
|
27
|
+
- Add oracle proof terms (PRing, PSimp, POmega) via reflection.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
from abc import ABC, abstractmethod
|
|
33
|
+
from dataclasses import dataclass
|
|
34
|
+
|
|
35
|
+
from .formulas import Formula
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ProofTerm(ABC):
|
|
39
|
+
@property
|
|
40
|
+
@abstractmethod
|
|
41
|
+
def has_meta_var(self) -> bool:
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass()
|
|
46
|
+
class PMetaVar(ProofTerm):
|
|
47
|
+
"""Meta-variable (proof hole): represents an unresolved subgoal in the proof tree."""
|
|
48
|
+
|
|
49
|
+
goal_id: str
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def has_meta_var(self) -> bool:
|
|
53
|
+
return True
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass()
|
|
57
|
+
class PVar(ProofTerm):
|
|
58
|
+
"""Hypothesis variable: proves A when h : A is in context."""
|
|
59
|
+
|
|
60
|
+
name: str
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def has_meta_var(self) -> bool:
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass()
|
|
68
|
+
class PLam(ProofTerm):
|
|
69
|
+
"""Implication introduction: abstracts over a hypothesis.
|
|
70
|
+
|
|
71
|
+
Proves antecedent -> consequent when body proves consequent
|
|
72
|
+
under the assumption var : dom (= antecedent).
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
var: str
|
|
76
|
+
dom: Formula
|
|
77
|
+
body: ProofTerm
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def has_meta_var(self) -> bool:
|
|
81
|
+
return self.body.has_meta_var
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass()
|
|
85
|
+
class PApp(ProofTerm):
|
|
86
|
+
"""Implication elimination (modus ponens)."""
|
|
87
|
+
|
|
88
|
+
fn: ProofTerm
|
|
89
|
+
arg: ProofTerm
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def has_meta_var(self) -> bool:
|
|
93
|
+
return self.fn.has_meta_var or self.arg.has_meta_var
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass()
|
|
97
|
+
class PAndI(ProofTerm):
|
|
98
|
+
"""Conjunction introduction."""
|
|
99
|
+
|
|
100
|
+
left: ProofTerm
|
|
101
|
+
right: ProofTerm
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def has_meta_var(self) -> bool:
|
|
105
|
+
return self.left.has_meta_var or self.right.has_meta_var
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclass()
|
|
109
|
+
class PAndE1(ProofTerm):
|
|
110
|
+
"""Conjunction elimination, left projection."""
|
|
111
|
+
|
|
112
|
+
inner: ProofTerm
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def has_meta_var(self) -> bool:
|
|
116
|
+
return self.inner.has_meta_var
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass()
|
|
120
|
+
class PAndE2(ProofTerm):
|
|
121
|
+
"""Conjunction elimination, right projection."""
|
|
122
|
+
|
|
123
|
+
inner: ProofTerm
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def has_meta_var(self) -> bool:
|
|
127
|
+
return self.inner.has_meta_var
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dataclass()
|
|
131
|
+
class POrIL(ProofTerm):
|
|
132
|
+
"""Disjunction introduction, left.
|
|
133
|
+
|
|
134
|
+
right_type must be provided explicitly because type_check cannot
|
|
135
|
+
infer the right disjunct from the proof term alone.
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
pf: ProofTerm
|
|
139
|
+
right_type: Formula
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def has_meta_var(self) -> bool:
|
|
143
|
+
return self.pf.has_meta_var
|
|
144
|
+
|
|
145
|
+
@dataclass()
|
|
146
|
+
class POrIR(ProofTerm):
|
|
147
|
+
"""Disjunction introduction, right."""
|
|
148
|
+
|
|
149
|
+
left_type: Formula
|
|
150
|
+
pf: ProofTerm
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def has_meta_var(self) -> bool:
|
|
154
|
+
return self.pf.has_meta_var
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@dataclass()
|
|
158
|
+
class PTrueI(ProofTerm):
|
|
159
|
+
"""True introduction: proves FTrue unconditionally."""
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def has_meta_var(self) -> bool:
|
|
163
|
+
return False
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@dataclass()
|
|
167
|
+
class POrE(ProofTerm):
|
|
168
|
+
"""Disjunction elimination (case split).
|
|
169
|
+
|
|
170
|
+
Given a proof of A \\/ B, and a proof of C assuming A (left branch),
|
|
171
|
+
and a proof of C assuming B (right branch), concludes C.
|
|
172
|
+
|
|
173
|
+
left_var : name bound in left_branch (h : A)
|
|
174
|
+
right_var : name bound in right_branch (h : B)
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
disj: ProofTerm
|
|
178
|
+
left_var: str
|
|
179
|
+
left_branch: ProofTerm
|
|
180
|
+
right_var: str
|
|
181
|
+
right_branch: ProofTerm
|
|
182
|
+
|
|
183
|
+
@property
|
|
184
|
+
def has_meta_var(self) -> bool:
|
|
185
|
+
return (
|
|
186
|
+
self.disj.has_meta_var or
|
|
187
|
+
self.left_branch.has_meta_var or
|
|
188
|
+
self.right_branch.has_meta_var
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@dataclass()
|
|
193
|
+
class PFalseE(ProofTerm):
|
|
194
|
+
"""False elimination (ex falso quodlibet): proves any formula from FFalse."""
|
|
195
|
+
|
|
196
|
+
inner: ProofTerm
|
|
197
|
+
conclusion: Formula
|
|
198
|
+
|
|
199
|
+
@property
|
|
200
|
+
def has_meta_var(self) -> bool:
|
|
201
|
+
return self.inner.has_meta_var
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@dataclass()
|
|
205
|
+
class PExI(ProofTerm):
|
|
206
|
+
"""Existential introduction (∃-I).
|
|
207
|
+
|
|
208
|
+
Proves FExists(exists_var, body) when pf proves body[witness/exists_var].
|
|
209
|
+
Both exists_var and body must be provided because the type checker cannot
|
|
210
|
+
infer them from the proof term alone.
|
|
211
|
+
"""
|
|
212
|
+
|
|
213
|
+
exists_var: str
|
|
214
|
+
body: Formula
|
|
215
|
+
witness: Formula
|
|
216
|
+
pf: ProofTerm
|
|
217
|
+
|
|
218
|
+
@property
|
|
219
|
+
def has_meta_var(self) -> bool:
|
|
220
|
+
return self.pf.has_meta_var
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
@dataclass()
|
|
224
|
+
class PExE(ProofTerm):
|
|
225
|
+
"""Existential elimination (∃-E).
|
|
226
|
+
|
|
227
|
+
Given pf : ∃x. P(x), introduces a fresh propositional variable prop_var
|
|
228
|
+
and a hypothesis hyp_var : P[prop_var/x], then proves the conclusion C
|
|
229
|
+
(which must not mention prop_var).
|
|
230
|
+
|
|
231
|
+
prop_var : name of the fresh propositional variable (appears in hyp type)
|
|
232
|
+
hyp_var : proof-context name bound to P[prop_var/x]
|
|
233
|
+
"""
|
|
234
|
+
|
|
235
|
+
pf: ProofTerm
|
|
236
|
+
prop_var: str
|
|
237
|
+
hyp_var: str
|
|
238
|
+
body: ProofTerm
|
|
239
|
+
|
|
240
|
+
@property
|
|
241
|
+
def has_meta_var(self) -> bool:
|
|
242
|
+
return self.pf.has_meta_var or self.body.has_meta_var
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
|
|
2
|
+
"""Batch proof execution subcommand for poussins CLI."""
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def run_batch(filepath: str):
|
|
6
|
+
"""
|
|
7
|
+
Run batch proof execution: import the file, collect all theorems/lemmas, and log their status.
|
|
8
|
+
"""
|
|
9
|
+
import importlib.util
|
|
10
|
+
import sys
|
|
11
|
+
import os
|
|
12
|
+
|
|
13
|
+
file_abspath = os.path.abspath(filepath)
|
|
14
|
+
file_dir = os.path.dirname(file_abspath)
|
|
15
|
+
sys.path.insert(0, file_dir)
|
|
16
|
+
sys.path.insert(0, os.getcwd())
|
|
17
|
+
module_name = os.path.splitext(os.path.basename(filepath))[0]
|
|
18
|
+
spec = importlib.util.spec_from_file_location(module_name, file_abspath)
|
|
19
|
+
module = importlib.util.module_from_spec(spec)
|
|
20
|
+
try:
|
|
21
|
+
spec.loader.exec_module(module)
|
|
22
|
+
except Exception as e:
|
|
23
|
+
print(f"[poussins] Error executing {filepath}: {e}")
|
|
24
|
+
return
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Lean conversion subcommand."""
|
|
2
|
+
|
|
3
|
+
def run_lean2py(filepath: str, output: str | None = None):
|
|
4
|
+
"""
|
|
5
|
+
Convert Lean file to Python DSL.
|
|
6
|
+
"""
|
|
7
|
+
print(f"[poussins] Lean to Python conversion (stub): {filepath} -> {output}")
|
|
8
|
+
|
|
9
|
+
def run_py2lean(filepath: str, output: str | None = None):
|
|
10
|
+
"""
|
|
11
|
+
Convert Python DSL file to Lean format.
|
|
12
|
+
"""
|
|
13
|
+
print(f"[poussins] Python to Lean conversion (stub): {filepath} -> {output}")
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Command-line entrypoint wiring for poussins."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
from .batch import run_batch
|
|
6
|
+
from .step import run_step
|
|
7
|
+
from .lean import run_lean2py, run_py2lean
|
|
8
|
+
|
|
9
|
+
def main():
|
|
10
|
+
parser = argparse.ArgumentParser(description="poussins command line")
|
|
11
|
+
subparsers = parser.add_subparsers(dest="subcmd", required=True)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# batch
|
|
15
|
+
p_batch = subparsers.add_parser("batch", help="Batch proof execution (.py)")
|
|
16
|
+
p_batch.add_argument("filepath", help=".py file with theorems/lemmas")
|
|
17
|
+
|
|
18
|
+
# step
|
|
19
|
+
p_step = subparsers.add_parser("step", help="Step-by-step proof execution")
|
|
20
|
+
p_step.add_argument("filepath", help=".py file with theorems/lemmas")
|
|
21
|
+
p_step.add_argument("--theorem", help="Theorem name to step through", default=None)
|
|
22
|
+
|
|
23
|
+
# lean2py
|
|
24
|
+
p_lean2py = subparsers.add_parser("lean2py", help="Convert Lean file to Python DSL")
|
|
25
|
+
p_lean2py.add_argument("filepath", help=".lean file to convert")
|
|
26
|
+
p_lean2py.add_argument("--output", help="Output file path", default=None)
|
|
27
|
+
|
|
28
|
+
# py2lean
|
|
29
|
+
p_py2lean = subparsers.add_parser("py2lean", help="Convert Python DSL to Lean format")
|
|
30
|
+
p_py2lean.add_argument("filepath", help=".py file to convert")
|
|
31
|
+
p_py2lean.add_argument("--output", help="Output file path", default=None)
|
|
32
|
+
|
|
33
|
+
args = parser.parse_args()
|
|
34
|
+
|
|
35
|
+
if args.subcmd == "batch":
|
|
36
|
+
run_batch(args.filepath)
|
|
37
|
+
elif args.subcmd == "step":
|
|
38
|
+
run_step(args.filepath, args.theorem)
|
|
39
|
+
elif args.subcmd == "lean2py":
|
|
40
|
+
run_lean2py(args.filepath, args.output)
|
|
41
|
+
elif args.subcmd == "py2lean":
|
|
42
|
+
run_py2lean(args.filepath, args.output)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
|
|
2
|
+
"""Step-by-step proof execution subcommand for poussins CLI."""
|
|
3
|
+
|
|
4
|
+
def run_step(filepath: str, theorem: str | None = None):
|
|
5
|
+
"""
|
|
6
|
+
Run step-by-step proof execution for a given file and theorem (stub).
|
|
7
|
+
"""
|
|
8
|
+
print(f"[poussins] Step-by-step execution (stub): {filepath}, theorem={theorem}")
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Public DSL layer: Prop, Axiom, Theorem, Example, and aliases."""
|
|
2
|
+
|
|
3
|
+
from .prop import Prop
|
|
4
|
+
from .axiom import Axiom
|
|
5
|
+
from .theorem import (
|
|
6
|
+
Theorem,
|
|
7
|
+
Lemma,
|
|
8
|
+
Proposition,
|
|
9
|
+
Corollary,
|
|
10
|
+
Fact,
|
|
11
|
+
Remark,
|
|
12
|
+
Property,
|
|
13
|
+
Example,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"Prop",
|
|
18
|
+
"Axiom",
|
|
19
|
+
"Theorem",
|
|
20
|
+
"Lemma",
|
|
21
|
+
"Proposition",
|
|
22
|
+
"Corollary",
|
|
23
|
+
"Fact",
|
|
24
|
+
"Remark",
|
|
25
|
+
"Property",
|
|
26
|
+
"Example",
|
|
27
|
+
]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Axiom: a named proposition accepted without proof."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
class Axiom:
|
|
6
|
+
"""A named proposition accepted without proof.
|
|
7
|
+
|
|
8
|
+
Axioms always have assurance TRUSTED.
|
|
9
|
+
|
|
10
|
+
If *env* is provided the axiom is registered immediately, mirroring
|
|
11
|
+
Coq's ``Axiom foo : T.`` which registers into the global environment
|
|
12
|
+
on the same line.
|
|
13
|
+
|
|
14
|
+
Usage::
|
|
15
|
+
|
|
16
|
+
# declare and register in one step (Coq-style)
|
|
17
|
+
ax = Axiom("excluded_middle", p | ~p, env)
|
|
18
|
+
|
|
19
|
+
# declare only — useful for tests or composing declarations
|
|
20
|
+
ax = Axiom("excluded_middle", p | ~p)
|
|
21
|
+
env.register(ax.to_declaration())
|
|
22
|
+
"""
|
|
23
|
+
pass
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""ProofBase: tactic methods for proof-carrying DSL objects (Theorem, Example).
|
|
2
|
+
|
|
3
|
+
This abstract base class provides method-style tactic access on top of the underlying
|
|
4
|
+
ProofEngine. The logic of each tactic lives in ``tactics/primitive.py``
|
|
5
|
+
and ``tactics/derived.py``; this module only delegates to those functions.
|
|
6
|
+
|
|
7
|
+
Classes that inherit ProofBase must implement the ``engine`` property.
|
|
8
|
+
|
|
9
|
+
Usage::
|
|
10
|
+
|
|
11
|
+
th = Theorem("identity", p >> p)
|
|
12
|
+
th.intro("h")
|
|
13
|
+
th.exact("h")
|
|
14
|
+
th.qed(env)
|
|
15
|
+
|
|
16
|
+
Alternatively, the same tactics are still available as standalone
|
|
17
|
+
functions for use in combinators or batch execution::
|
|
18
|
+
|
|
19
|
+
from poussins import intro, exact
|
|
20
|
+
intro(th.engine, "h")
|
|
21
|
+
exact(th.engine, "h")
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
from abc import ABC
|
|
26
|
+
|
|
27
|
+
from ..ast.formulas import Formula
|
|
28
|
+
from ..ast.proof_terms import ProofTerm
|
|
29
|
+
from ..kernel.proof_engine import ProofEngine
|
|
30
|
+
from .prop import Prop
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ProofBase(ABC):
|
|
34
|
+
def __init__(self, statement: Prop | Formula):
|
|
35
|
+
self.__setattr__("engine", ProofEngine(Prop.to_formula(statement)))
|
|
36
|
+
|
|
37
|
+
def is_closed(self) -> bool:
|
|
38
|
+
return self.engine.is_closed()
|
|
39
|
+
|
|
40
|
+
def qed(self):
|
|
41
|
+
# TODO: Add to Envirnonment and closed status should be checked in engine.
|
|
42
|
+
if not self.is_closed():
|
|
43
|
+
raise ValueError("Not proofed.")
|
|
44
|
+
print(f"Assignment: {self.engine.goal.assignment}")
|
|
45
|
+
|
|
46
|
+
# ------------------------------------------------------------------
|
|
47
|
+
# Primitive tactics
|
|
48
|
+
# ------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
def intro(self, name: str) -> None:
|
|
51
|
+
from ..tactics.primitive import intro
|
|
52
|
+
intro(self.engine, name)
|
|
53
|
+
|
|
54
|
+
def exact(self, term_or_hyp: ProofTerm | str) -> None:
|
|
55
|
+
from ..tactics.primitive import exact
|
|
56
|
+
exact(self.engine, term_or_hyp)
|
|
57
|
+
|
|
58
|
+
def apply(self, term_or_hyp: ProofTerm | str) -> None:
|
|
59
|
+
from ..tactics.primitive import apply
|
|
60
|
+
apply(self.engine, term_or_hyp)
|
|
61
|
+
|
|
62
|
+
# ------------------------------------------------------------------
|
|
63
|
+
# Derived tactics
|
|
64
|
+
# ------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
# TODO: add derived tactics here
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Prop: public-facing propositional formula DSL.
|
|
2
|
+
|
|
3
|
+
Wraps the internal Formula AST with Python operator overloads so that
|
|
4
|
+
propositions can be written naturally in Python code:
|
|
5
|
+
|
|
6
|
+
p, q = Prop("P"), Prop("Q")
|
|
7
|
+
p >> q # P → Q (implication)
|
|
8
|
+
p & q # P ∧ Q (conjunction)
|
|
9
|
+
p | q # P ∨ Q (disjunction)
|
|
10
|
+
~p # ¬P (negation, sugar for P → ⊥)
|
|
11
|
+
Prop.top() # ⊤
|
|
12
|
+
Prop.bot() # ⊥
|
|
13
|
+
Prop.exists("x", p) # ∃x. P
|
|
14
|
+
|
|
15
|
+
Prop is immutable. The underlying Formula is accessible via .formula.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
|
|
21
|
+
from ..ast.formulas import (
|
|
22
|
+
Formula,
|
|
23
|
+
FVar,
|
|
24
|
+
FTrue,
|
|
25
|
+
FFalse,
|
|
26
|
+
FExists,
|
|
27
|
+
FAnd, FImpl, FOr)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class Prop:
|
|
32
|
+
def __init__(self, formula: Formula | str) -> None:
|
|
33
|
+
if isinstance(formula, Formula):
|
|
34
|
+
object.__setattr__(self, "formula", formula)
|
|
35
|
+
else:
|
|
36
|
+
object.__setattr__(self, "formula", FVar(formula))
|
|
37
|
+
|
|
38
|
+
# ------------------------------------------------------------------
|
|
39
|
+
# Factory helpers
|
|
40
|
+
# ------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def top(cls) -> Prop:
|
|
44
|
+
"""⊤ (True)."""
|
|
45
|
+
return cls(FTrue())
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def bot(cls) -> Prop:
|
|
49
|
+
"""⊥ (False)."""
|
|
50
|
+
return cls(FFalse())
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def exists(cls, var: str, body: Formula) -> Prop:
|
|
54
|
+
"""∃var. body."""
|
|
55
|
+
return cls(FExists(var, body))
|
|
56
|
+
|
|
57
|
+
# ------------------------------------------------------------------
|
|
58
|
+
# Coercion
|
|
59
|
+
# ------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
@staticmethod
|
|
62
|
+
def to_formula(prop_or_formula: Prop | Formula) -> Formula:
|
|
63
|
+
return prop_or_formula.formula if isinstance(prop_or_formula, Prop) else prop_or_formula
|
|
64
|
+
|
|
65
|
+
@staticmethod
|
|
66
|
+
def to_prop(prop_or_formula: Prop | Formula) -> Prop:
|
|
67
|
+
return prop_or_formula if isinstance(prop_or_formula, Prop) else Prop(prop_or_formula)
|
|
68
|
+
|
|
69
|
+
# ------------------------------------------------------------------
|
|
70
|
+
# Operator overloads
|
|
71
|
+
# ------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
def __rshift__(self, other: Prop | Formula) -> Prop:
|
|
74
|
+
"""P >> Q → P → Q (implication)."""
|
|
75
|
+
return Prop(FImpl(self.to_formula(self), self.to_formula(other)))
|
|
76
|
+
|
|
77
|
+
def __and__(self, other: Prop | Formula) -> Prop:
|
|
78
|
+
"""P & Q → P ∧ Q (conjunction)."""
|
|
79
|
+
return Prop(FAnd(self.to_formula(self), self.to_formula(other)))
|
|
80
|
+
|
|
81
|
+
def __or__(self, other: Prop | Formula) -> Prop:
|
|
82
|
+
"""P | Q → P ∨ Q (disjunction)."""
|
|
83
|
+
return Prop(FOr(self.to_formula(self), self.to_formula(other)))
|
|
84
|
+
|
|
85
|
+
def __invert__(self) -> Prop:
|
|
86
|
+
"""~P → P → ⊥ (negation)."""
|
|
87
|
+
return Prop(FImpl(self.to_formula(self), FFalse()))
|
|
88
|
+
|
|
89
|
+
# ------------------------------------------------------------------
|
|
90
|
+
# Equality / hashing — delegate to Formula
|
|
91
|
+
# ------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
def __eq__(self, other: object) -> bool:
|
|
94
|
+
if isinstance(other, Prop):
|
|
95
|
+
return self.formula == other.formula
|
|
96
|
+
elif isinstance(other, Formula):
|
|
97
|
+
return self.formula == other
|
|
98
|
+
return NotImplemented
|
|
99
|
+
|
|
100
|
+
def __hash__(self) -> int:
|
|
101
|
+
return hash(self.formula)
|
|
102
|
+
|
|
103
|
+
def __repr__(self) -> str:
|
|
104
|
+
return f"Prop({self.formula!r})"
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Theorem, Lemma, Example: proof-carrying DSL objects.
|
|
2
|
+
|
|
3
|
+
Usage pattern::
|
|
4
|
+
|
|
5
|
+
from poussins import Prop, Theorem, Lemma, Axiom, Example, Environment
|
|
6
|
+
from poussins import intro, exact
|
|
7
|
+
|
|
8
|
+
p, q = Prop("P"), Prop("Q")
|
|
9
|
+
env = Environment()
|
|
10
|
+
|
|
11
|
+
# Theorem: named, registered in Environment
|
|
12
|
+
th = Theorem("identity", p >> p)
|
|
13
|
+
th.intro("h") # method-style via ProofBase
|
|
14
|
+
th.exact("h")
|
|
15
|
+
th.qed(env)
|
|
16
|
+
|
|
17
|
+
# Standalone functions are also available (useful in combinators)
|
|
18
|
+
intro(th.engine, "h")
|
|
19
|
+
exact(th.engine, "h")
|
|
20
|
+
|
|
21
|
+
# Lemma: alias for Theorem (stylistic distinction only)
|
|
22
|
+
lem = Lemma("and_comm", (p & q) >> (q & p))
|
|
23
|
+
...
|
|
24
|
+
|
|
25
|
+
# Example: anonymous proof, never registered
|
|
26
|
+
ex = Example(p >> p)
|
|
27
|
+
intro(ex, "h")
|
|
28
|
+
exact(ex, "h")
|
|
29
|
+
assert ex.closed
|
|
30
|
+
|
|
31
|
+
Design:
|
|
32
|
+
Theorem wraps ProofSession and produces a Declaration on success.
|
|
33
|
+
Inherits ProofBase for method-style tactic access.
|
|
34
|
+
Example is like Theorem but anonymous and does not produce a Declaration.
|
|
35
|
+
|
|
36
|
+
None of the above hold a reference to an Environment; that is the
|
|
37
|
+
caller's concern.
|
|
38
|
+
|
|
39
|
+
See also:
|
|
40
|
+
dsl/proof_base.py for ProofBase (method delegation layer).
|
|
41
|
+
dsl/axiom.py for Axiom (no-proof declarations).
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
46
|
+
from ..ast.formulas import Formula
|
|
47
|
+
from .prop import Prop
|
|
48
|
+
from .proof_base import ProofBase
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class Theorem(ProofBase):
|
|
52
|
+
"""A named proposition together with an interactive proof session.
|
|
53
|
+
|
|
54
|
+
Tactics can be applied as methods (via ProofBase) or as standalone
|
|
55
|
+
functions — both styles are equivalent::
|
|
56
|
+
|
|
57
|
+
th = Theorem("mp", (p >> q) >> p >> q)
|
|
58
|
+
th.intro("hpq") # method style
|
|
59
|
+
intro(th.engine, "hpq") # function style — also valid
|
|
60
|
+
|
|
61
|
+
th.intro("hp")
|
|
62
|
+
th.apply("hpq")
|
|
63
|
+
th.exact("hp")
|
|
64
|
+
th.qed(env) # seals the proof and registers into env
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def __init__(
|
|
69
|
+
self,
|
|
70
|
+
name: str,
|
|
71
|
+
statement: Prop
|
|
72
|
+
) -> None:
|
|
73
|
+
self.name = name
|
|
74
|
+
super().__init__(statement)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# Alias for Theorem.
|
|
78
|
+
Lemma = Theorem
|
|
79
|
+
Proposition = Theorem
|
|
80
|
+
Corollary = Theorem
|
|
81
|
+
Fact = Theorem
|
|
82
|
+
Remark = Theorem
|
|
83
|
+
Property = Theorem
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class Example(ProofBase):
|
|
87
|
+
"""An anonymous proof for exploration or testing.
|
|
88
|
+
|
|
89
|
+
Like Theorem but without a name and without to_declaration().
|
|
90
|
+
Tactics can be applied as methods (via ProofBase) or as standalone
|
|
91
|
+
functions — both styles are equivalent::
|
|
92
|
+
|
|
93
|
+
ex = Example(p >> p)
|
|
94
|
+
ex.intro("h") # method style
|
|
95
|
+
ex.exact("h")
|
|
96
|
+
assert ex.closed
|
|
97
|
+
"""
|
|
98
|
+
def __init__(
|
|
99
|
+
self,
|
|
100
|
+
statement: Prop | Formula,
|
|
101
|
+
) -> None:
|
|
102
|
+
super().__init__(statement)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# TODO: implementation
|
|
File without changes
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""
|
|
2
|
+
"""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Dict, Optional
|
|
8
|
+
from uuid import uuid4
|
|
9
|
+
|
|
10
|
+
from ..ast.proof_terms import ProofTerm, Formula
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ProofAssurance(str, Enum):
|
|
14
|
+
"""Verification assurance axis."""
|
|
15
|
+
|
|
16
|
+
UNKNOWN = "unknown"
|
|
17
|
+
VERIFIED = "verified"
|
|
18
|
+
TRUSTED = "trusted"
|
|
19
|
+
ADMITTED = "admitted"
|
|
20
|
+
INVALID = "invalid"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class Context:
|
|
25
|
+
hyps: Dict[str, Formula]
|
|
26
|
+
|
|
27
|
+
def get(self, name: str) -> Optional[Formula]:
|
|
28
|
+
return self.hyps.get(name)
|
|
29
|
+
|
|
30
|
+
def add(self, additional_hyps: Dict[str, Formula]) -> Context:
|
|
31
|
+
new_hyps = dict(self.hyps)
|
|
32
|
+
new_hyps.update(additional_hyps)
|
|
33
|
+
return Context(hyps=new_hyps)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class Goal:
|
|
38
|
+
id: str = field(init=False)
|
|
39
|
+
formula: Formula
|
|
40
|
+
context: Context
|
|
41
|
+
assignment: Optional[ProofTerm] = None
|
|
42
|
+
assurance: ProofAssurance = ProofAssurance.UNKNOWN
|
|
43
|
+
|
|
44
|
+
def __post_init__(self):
|
|
45
|
+
self.id = str(uuid4())
|
|
46
|
+
|
|
47
|
+
def is_closed(self) -> bool:
|
|
48
|
+
if self.assignment is None:
|
|
49
|
+
return False
|
|
50
|
+
else:
|
|
51
|
+
return not self.assignment.has_meta_var
|
|
52
|
+
|
|
53
|
+
def __eq__(self, other: Goal) -> bool:
|
|
54
|
+
return self.id == other.id
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""
|
|
2
|
+
"""
|
|
3
|
+
|
|
4
|
+
from collections import deque
|
|
5
|
+
from copy import deepcopy
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from poussins.ast import *
|
|
9
|
+
|
|
10
|
+
from .proof_state import ProofState
|
|
11
|
+
from .goal import Goal, Context, ProofAssurance
|
|
12
|
+
from ..ast import Formula, ProofTerm, PMetaVar
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ProofEngine:
|
|
16
|
+
"""Proof engine: manages the proof state and applies inference rules to manipulate goals."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, root_formula: Formula):
|
|
19
|
+
self.goal = Goal(formula=root_formula, context=Context(hyps={}))
|
|
20
|
+
self.state = ProofState(goals=deque([self.goal]))
|
|
21
|
+
|
|
22
|
+
@staticmethod
|
|
23
|
+
def _substitute_meta_var(closed_goal: Goal, term: ProofTerm) -> ProofTerm:
|
|
24
|
+
if not closed_goal.is_closed():
|
|
25
|
+
return term
|
|
26
|
+
elif isinstance(term, PMetaVar) and term.goal_id == closed_goal.id:
|
|
27
|
+
return deepcopy(closed_goal.assignment)
|
|
28
|
+
else:
|
|
29
|
+
match term:
|
|
30
|
+
case PLam():
|
|
31
|
+
term.body = ProofEngine._substitute_meta_var(closed_goal, term.body)
|
|
32
|
+
case PApp():
|
|
33
|
+
term.fn = ProofEngine._substitute_meta_var(closed_goal, term.fn)
|
|
34
|
+
term.arg = ProofEngine._substitute_meta_var(closed_goal, term.arg)
|
|
35
|
+
case PAndI():
|
|
36
|
+
term.left = ProofEngine._substitute_meta_var(closed_goal, term.left)
|
|
37
|
+
term.right = ProofEngine._substitute_meta_var(closed_goal, term.right)
|
|
38
|
+
case PAndE1():
|
|
39
|
+
term.inner = ProofEngine._substitute_meta_var(closed_goal, term.inner)
|
|
40
|
+
case PAndE2():
|
|
41
|
+
term.inner = ProofEngine._substitute_meta_var(closed_goal, term.inner)
|
|
42
|
+
case POrIL():
|
|
43
|
+
term.pf = ProofEngine._substitute_meta_var(closed_goal, term.pf)
|
|
44
|
+
case POrIR():
|
|
45
|
+
term.pf = ProofEngine._substitute_meta_var(closed_goal, term.pf)
|
|
46
|
+
case POrE():
|
|
47
|
+
term.disj = ProofEngine._substitute_meta_var(closed_goal, term.disj)
|
|
48
|
+
term.left_branch = ProofEngine._substitute_meta_var(closed_goal, term.left_branch)
|
|
49
|
+
term.right_branch = ProofEngine._substitute_meta_var(closed_goal, term.right_branch)
|
|
50
|
+
case PFalseE():
|
|
51
|
+
term.inner = ProofEngine._substitute_meta_var(closed_goal, term.inner)
|
|
52
|
+
case PExE():
|
|
53
|
+
term.pf = ProofEngine._substitute_meta_var(closed_goal, term.pf)
|
|
54
|
+
term.body = ProofEngine._substitute_meta_var(closed_goal, term.body)
|
|
55
|
+
|
|
56
|
+
return term
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _close_sub_goals(self, closed_goal: Goal):
|
|
60
|
+
if not closed_goal.is_closed():
|
|
61
|
+
raise ValueError("Cannot close sub-goals of an open goal.")
|
|
62
|
+
else:
|
|
63
|
+
self.state.goals.remove(closed_goal)
|
|
64
|
+
|
|
65
|
+
closed_goals: list[Goal] = []
|
|
66
|
+
for goal in list(self.state.goals):
|
|
67
|
+
goal.assignment = ProofEngine._substitute_meta_var(closed_goal, goal.assignment)
|
|
68
|
+
if goal.is_closed() and goal not in closed_goals:
|
|
69
|
+
closed_goals.append(goal)
|
|
70
|
+
|
|
71
|
+
for closed_goal in closed_goals:
|
|
72
|
+
self._close_sub_goals(closed_goal)
|
|
73
|
+
|
|
74
|
+
def close_goal(self, assignment: ProofTerm):
|
|
75
|
+
current_goal = self.state.current_goal()
|
|
76
|
+
if current_goal is None:
|
|
77
|
+
raise ValueError("No active goal to close.")
|
|
78
|
+
elif current_goal.assignment is not None:
|
|
79
|
+
raise ValueError("Current goal is already assigned a proof term.")
|
|
80
|
+
elif assignment.has_meta_var:
|
|
81
|
+
raise ValueError("Cannot close goal with a proof term containing meta-variables.")
|
|
82
|
+
else:
|
|
83
|
+
current_goal.assignment = assignment
|
|
84
|
+
self._close_sub_goals(current_goal)
|
|
85
|
+
|
|
86
|
+
def refine_goal(self, sub_goals: list[Goal], assignment: Optional[ProofTerm] = None):
|
|
87
|
+
if assignment is not None:
|
|
88
|
+
self.state.current_goal().assignment = assignment
|
|
89
|
+
self.state.goals.extendleft(sub_goals[::-1])
|
|
90
|
+
|
|
91
|
+
def rotate_left(self):
|
|
92
|
+
self.state.goals.rotate(-1)
|
|
93
|
+
|
|
94
|
+
def rotate_right(self):
|
|
95
|
+
self.state.goals.rotate(1)
|
|
96
|
+
|
|
97
|
+
def is_closed(self) -> bool:
|
|
98
|
+
return self.state.is_closed() and self.goal.is_closed()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""
|
|
2
|
+
"""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
from collections import deque
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from .goal import Goal
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class ProofState:
|
|
14
|
+
goals: deque[Goal] = field(default_factory=deque)
|
|
15
|
+
|
|
16
|
+
def current_goal(self) -> Optional[Goal]:
|
|
17
|
+
return self.goals[0] if self.goals else None
|
|
18
|
+
|
|
19
|
+
def is_closed(self) -> bool:
|
|
20
|
+
return not self.goals
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""
|
|
2
|
+
"""
|
|
3
|
+
|
|
4
|
+
from copy import deepcopy
|
|
5
|
+
|
|
6
|
+
from poussins.ast.proof_terms import PLam, ProofTerm
|
|
7
|
+
|
|
8
|
+
from ..kernel.proof_engine import ProofEngine
|
|
9
|
+
from ..kernel.goal import Goal
|
|
10
|
+
from ..ast import Formula, FImpl, PMetaVar, PVar, PApp
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def intro(proof_engine: ProofEngine, hyp_name: str):
|
|
14
|
+
"""Introduce a new hypothesis."""
|
|
15
|
+
current_goal = proof_engine.state.current_goal()
|
|
16
|
+
if current_goal is None:
|
|
17
|
+
raise ValueError("No active goal to apply intro tactic.")
|
|
18
|
+
elif not isinstance(current_goal.formula, FImpl):
|
|
19
|
+
raise ValueError("Intro tactic can only be applied to implications.")
|
|
20
|
+
|
|
21
|
+
sub_goal = Goal(
|
|
22
|
+
formula=deepcopy(current_goal.formula.consequent),
|
|
23
|
+
context=current_goal.context.add(
|
|
24
|
+
{ hyp_name: deepcopy(current_goal.formula.antecedent) }
|
|
25
|
+
),
|
|
26
|
+
assignment=PMetaVar(goal_id=current_goal.id)
|
|
27
|
+
)
|
|
28
|
+
proof_engine.refine_goal(
|
|
29
|
+
[sub_goal],
|
|
30
|
+
assignment=PLam(
|
|
31
|
+
var=hyp_name,
|
|
32
|
+
dom=deepcopy(current_goal.formula.antecedent),
|
|
33
|
+
body=PMetaVar(goal_id=sub_goal.id)
|
|
34
|
+
)
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def exact(proof_engine: ProofEngine, hyp_name: str):
|
|
39
|
+
"""Close the current goal with a hypothesis."""
|
|
40
|
+
current_goal = proof_engine.state.current_goal()
|
|
41
|
+
if current_goal is None:
|
|
42
|
+
raise ValueError("No active goal to apply exact tactic.")
|
|
43
|
+
|
|
44
|
+
hyp = current_goal.context.get(hyp_name)
|
|
45
|
+
if hyp is None:
|
|
46
|
+
raise ValueError(f"Hypothesis '{hyp_name}' not found in the current context.")
|
|
47
|
+
elif hyp != current_goal.formula:
|
|
48
|
+
raise ValueError(f"Hypothesis '{hyp_name}' does not match the current goal formula.")
|
|
49
|
+
|
|
50
|
+
proof_engine.close_goal(PVar(name=hyp_name))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def apply(proof_engine: ProofEngine, hyp_name: str):
|
|
54
|
+
current_goal = proof_engine.state.current_goal()
|
|
55
|
+
if current_goal is None:
|
|
56
|
+
raise ValueError("No active goal to apply apply tactic.")
|
|
57
|
+
|
|
58
|
+
hyp = current_goal.context.get(hyp_name)
|
|
59
|
+
if hyp is None:
|
|
60
|
+
raise ValueError(f"Hypothesis '{hyp_name}' not found in the current context.")
|
|
61
|
+
elif hyp == current_goal.formula:
|
|
62
|
+
exact(proof_engine, hyp_name)
|
|
63
|
+
elif not isinstance(hyp, FImpl):
|
|
64
|
+
raise ValueError(f"Hypothesis '{hyp_name}' is not an implication and cannot be applied.")
|
|
65
|
+
|
|
66
|
+
sub_goals, assignment = _apply(current_goal, hyp_name, hyp)
|
|
67
|
+
proof_engine.refine_goal(sub_goals=sub_goals, assignment=assignment)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _apply(
|
|
71
|
+
current_goal: Goal,
|
|
72
|
+
hyp_name: str,
|
|
73
|
+
hyp: Formula,
|
|
74
|
+
goals: list[Goal] = None,
|
|
75
|
+
idx: int = 0
|
|
76
|
+
) -> tuple[list[Goal], ProofTerm]:
|
|
77
|
+
if goals is None:
|
|
78
|
+
goals = []
|
|
79
|
+
|
|
80
|
+
if current_goal.formula == hyp:
|
|
81
|
+
return goals, PVar(name=hyp_name)
|
|
82
|
+
elif isinstance(hyp, FImpl):
|
|
83
|
+
sub_goal = Goal(
|
|
84
|
+
formula=deepcopy(hyp.antecedent),
|
|
85
|
+
context=current_goal.context
|
|
86
|
+
)
|
|
87
|
+
sub_goals, assignment = _apply(current_goal, hyp_name, hyp.consequent, goals, idx + 1)
|
|
88
|
+
return [sub_goal] + sub_goals, PApp(fn=assignment, arg=PMetaVar(goal_id=sub_goal.id))
|
|
89
|
+
else:
|
|
90
|
+
raise ValueError(f"Hypothesis '{hyp}' cannot be applied to the current goal '{current_goal.formula}'.")
|