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.
@@ -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
+ ```
@@ -0,0 +1,14 @@
1
+ # poussins
2
+
3
+ ## Develop
4
+
5
+ ```bash
6
+ # Run
7
+ uv run -m poussins
8
+
9
+ # Unit Test
10
+ uv run pytest
11
+
12
+ # Lint
13
+ uv run ruff check .
14
+ ```
@@ -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,5 @@
1
+ """Public package exports for poussins."""
2
+
3
+ from .ast import *
4
+
5
+ __all__ = []
@@ -0,0 +1,6 @@
1
+ # Entrypoint for `python -m poussins`.
2
+ # Sets up logger and global error handler.
3
+ from .cli.main import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -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,14 @@
1
+ """Public CLIs."""
2
+
3
+
4
+ from .batch import run_batch
5
+ from .step import run_step
6
+ from .lean import run_lean2py, run_py2lean
7
+
8
+
9
+ __all__ = [
10
+ "run_batch",
11
+ "run_step",
12
+ "run_lean2py",
13
+ "run_py2lean",
14
+ ]
@@ -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,7 @@
1
+ """Internal kernel package (non-public).
2
+
3
+ Do not import from this package in user code.
4
+ Public APIs are re-exported from top-level packages only.
5
+ """
6
+
7
+ __all__: list[str] = []
@@ -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,11 @@
1
+ """Public tactic API."""
2
+
3
+
4
+ from .primitive import intro, exact, apply
5
+
6
+
7
+ __all__ = [
8
+ "intro",
9
+ "exact",
10
+ "apply"
11
+ ]
@@ -0,0 +1,2 @@
1
+ """
2
+ """
@@ -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}'.")