lean-python 0.1.0__tar.gz → 0.2.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.
- {lean_python-0.1.0 → lean_python-0.2.0}/PKG-INFO +84 -2
- {lean_python-0.1.0 → lean_python-0.2.0}/README.md +83 -1
- {lean_python-0.1.0 → lean_python-0.2.0}/pyproject.toml +1 -1
- lean_python-0.2.0/src/lean_python/check.py +292 -0
- lean_python-0.2.0/src/lean_python/cli.py +73 -0
- {lean_python-0.1.0 → lean_python-0.2.0}/src/lean_python/embed.py +1 -1
- lean_python-0.2.0/src/lean_python/lean/LeanPythonLib/Py.lean +52 -0
- {lean_python-0.1.0 → lean_python-0.2.0}/src/lean_python/marshal.py +3 -1
- lean_python-0.2.0/src/lean_python/translate.py +469 -0
- lean_python-0.2.0/src/lean_python/verify.py +147 -0
- lean_python-0.1.0/src/lean_python/cli.py +0 -50
- {lean_python-0.1.0 → lean_python-0.2.0}/.gitignore +0 -0
- {lean_python-0.1.0 → lean_python-0.2.0}/LICENSE +0 -0
- {lean_python-0.1.0 → lean_python-0.2.0}/src/lean_python/__init__.py +0 -0
- {lean_python-0.1.0 → lean_python-0.2.0}/src/lean_python/io.py +0 -0
- {lean_python-0.1.0 → lean_python-0.2.0}/src/lean_python/io_externs.py +0 -0
- {lean_python-0.1.0 → lean_python-0.2.0}/src/lean_python/models.py +0 -0
- {lean_python-0.1.0 → lean_python-0.2.0}/src/lean_python/runtime.py +0 -0
- {lean_python-0.1.0 → lean_python-0.2.0}/src/lean_python/sync.py +0 -0
- {lean_python-0.1.0 → lean_python-0.2.0}/src/lean_python/types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: lean-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Run Lean 4 code as Python: a runtime for code generated from Lean's IR, Lean blocks in Python files, and pydantic models for Lean types
|
|
5
5
|
Project-URL: Repository, https://github.com/conao3/lean-py
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -170,12 +170,94 @@ member), `Literal["a", "b"]` (`LeanPythonLiterals.<module>.<Model>_<field>`), an
|
|
|
170
170
|
docstring lists them. Generated functions take and return your classes; values that did not go
|
|
171
171
|
through validation (`model_construct`) are checked again on the way to Lean.
|
|
172
172
|
|
|
173
|
+
## Verifying Python functions
|
|
174
|
+
|
|
175
|
+
`@model` binds a typed, pure Python function to specifications written in a Lean block. The function
|
|
176
|
+
stays ordinary Python; `lean-py check` translates it to Lean and checks your proofs, and
|
|
177
|
+
`check_model` runs the same specifications as a Hypothesis property test.
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
from lean_python import lean
|
|
181
|
+
from lean_python.verify import model
|
|
182
|
+
|
|
183
|
+
spec = lean("spec", r"""
|
|
184
|
+
def clampSpec (x lo hi : Int) : Int := max lo (min hi x)
|
|
185
|
+
def ordered (_x lo hi : Int) : Bool := lo ≤ hi
|
|
186
|
+
def inRange (_x lo hi : Int) (r : Int) : Bool := lo ≤ r && r ≤ hi
|
|
187
|
+
""")
|
|
188
|
+
|
|
189
|
+
@model(refines=spec.clampSpec, requires=spec.ordered, ensures={"in_range": spec.inRange})
|
|
190
|
+
def clamp(x: int, lo: int, hi: int) -> int:
|
|
191
|
+
if x < lo:
|
|
192
|
+
return lo
|
|
193
|
+
if x > hi:
|
|
194
|
+
return hi
|
|
195
|
+
return x
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
`lean-py check --package calc` writes the translation and one obligation per specification:
|
|
199
|
+
|
|
200
|
+
```lean
|
|
201
|
+
def LeanPythonModel.calc.calc.clamp (x : Int) (lo : Int) (hi : Int) : Int := Id.run do
|
|
202
|
+
if ((decide (x < lo))) then
|
|
203
|
+
return lo
|
|
204
|
+
if ((decide (x > hi))) then
|
|
205
|
+
return hi
|
|
206
|
+
return x
|
|
207
|
+
|
|
208
|
+
abbrev LeanPythonModel.calc.calc.clamp.obligation.refines : Prop :=
|
|
209
|
+
∀ (x : Int) (lo : Int) (hi : Int), ordered x lo hi = true → clamp x lo hi = clampSpec x lo hi
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
and judges the theorems `<function>.proof.<label>` of `lean/Proofs/*.lean` next to `pyproject.toml`:
|
|
213
|
+
|
|
214
|
+
```lean
|
|
215
|
+
import LeanPythonModel.calc.calc
|
|
216
|
+
open LeanPythonEmbed.calc.spec.spec
|
|
217
|
+
namespace LeanPythonModel.«calc».«calc»
|
|
218
|
+
|
|
219
|
+
theorem clamp.proof.refines : clamp.obligation.refines := by
|
|
220
|
+
intro x lo hi h
|
|
221
|
+
simp only [ordered, decide_eq_true_eq] at h
|
|
222
|
+
simp only [clamp, clampSpec]
|
|
223
|
+
split <;> (try split) <;> simp_all <;> omega
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
```
|
|
227
|
+
proved calc.calc:clamp refines
|
|
228
|
+
proved calc.calc:clamp in_range
|
|
229
|
+
sorry calc.calc:sum_to refines
|
|
230
|
+
missing calc.calc:sum_abs nonneg
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
A `for` loop over a list becomes `List.foldl` under `simp` (`forIn_pure_yield_eq_foldl`), so a loop
|
|
234
|
+
is proved by induction on the list (`tests/verify/lean/Proofs/Calc.lean` proves `sum_abs`).
|
|
235
|
+
|
|
236
|
+
The states are proved, sorry, axioms (non-standard axioms), failed (the proof file does not build),
|
|
237
|
+
missing, mismatch (the theorem states something else). `--require proved` fails unless all are
|
|
238
|
+
proved. The result goes to `lean-python.lock` with a hash of each function's source;
|
|
239
|
+
`lean-py status` reads it without Lean and marks changed functions `stale`.
|
|
240
|
+
|
|
241
|
+
- `refines=s`: `f x = s x` (`Except.ok (s x)` for a function that may fail); `requires=p`: `p x = true →`
|
|
242
|
+
before each obligation; `ensures={"label": q}`: `q x (f x) = true`. A function annotated `-> Nat`
|
|
243
|
+
also gets `returns_nat` (`0 ≤ f x`).
|
|
244
|
+
- The translation: `int` is `Int` (a `Nat` parameter is a `Nat` converted to `Int` on entry);
|
|
245
|
+
statements become Lean `do` notation (`let mut`, `for`, `while`, `break`, `continue`, early
|
|
246
|
+
`return`); indexing, `//`, `%`, `assert` and `raise` make the function return
|
|
247
|
+
`Py.M α = Except Py.Exc α`, with Python's negative indices and floor division (`LeanPythonLib.Py`).
|
|
248
|
+
Supported: `int`, `bool`, `str`, `float`, `list`, `tuple`, `T | None`, pydantic models (as the
|
|
249
|
+
Lean structures of `models=`), `len`, `abs`, `min`, `max`, `sum`, list comprehensions, `append` on
|
|
250
|
+
local lists, and calls of other `@model` functions of the module.
|
|
251
|
+
- `check_model(f)` / `check_module(m)` generate arguments from the annotations (pydantic constraints
|
|
252
|
+
included), skip those failing `requires`, and compare with `refines` and `ensures`.
|
|
253
|
+
|
|
173
254
|
## The CLI
|
|
174
255
|
|
|
175
256
|
```
|
|
176
257
|
lean-python module generate --module-name <Name> [--roots <Name> ...] [--namespace <Name>]
|
|
177
258
|
[--search-path <dir> ...] [--output-file <path>] [--internal-only] [--output json|text]
|
|
178
259
|
[--type-module <Name>=<module> ...] [--model-module <Name>=<module> ...] [--batch <file>]
|
|
260
|
+
lean-python obligations check --modules <Name> ... --obligations <file> [--search-path <dir> ...]
|
|
179
261
|
lean-python --version | --generator-version | help
|
|
180
262
|
```
|
|
181
263
|
|
|
@@ -224,7 +306,7 @@ nix develop -c make bench
|
|
|
224
306
|
- `Task`s run synchronously.
|
|
225
307
|
- Building a `String` with `String.push` copies the string each time (Python `bytes`), so it grows
|
|
226
308
|
quadratically.
|
|
227
|
-
- Python
|
|
309
|
+
- The translation of Python functions covers the subset above; the guarantee is partial correctness.
|
|
228
310
|
|
|
229
311
|
## License
|
|
230
312
|
|
|
@@ -156,12 +156,94 @@ member), `Literal["a", "b"]` (`LeanPythonLiterals.<module>.<Model>_<field>`), an
|
|
|
156
156
|
docstring lists them. Generated functions take and return your classes; values that did not go
|
|
157
157
|
through validation (`model_construct`) are checked again on the way to Lean.
|
|
158
158
|
|
|
159
|
+
## Verifying Python functions
|
|
160
|
+
|
|
161
|
+
`@model` binds a typed, pure Python function to specifications written in a Lean block. The function
|
|
162
|
+
stays ordinary Python; `lean-py check` translates it to Lean and checks your proofs, and
|
|
163
|
+
`check_model` runs the same specifications as a Hypothesis property test.
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
from lean_python import lean
|
|
167
|
+
from lean_python.verify import model
|
|
168
|
+
|
|
169
|
+
spec = lean("spec", r"""
|
|
170
|
+
def clampSpec (x lo hi : Int) : Int := max lo (min hi x)
|
|
171
|
+
def ordered (_x lo hi : Int) : Bool := lo ≤ hi
|
|
172
|
+
def inRange (_x lo hi : Int) (r : Int) : Bool := lo ≤ r && r ≤ hi
|
|
173
|
+
""")
|
|
174
|
+
|
|
175
|
+
@model(refines=spec.clampSpec, requires=spec.ordered, ensures={"in_range": spec.inRange})
|
|
176
|
+
def clamp(x: int, lo: int, hi: int) -> int:
|
|
177
|
+
if x < lo:
|
|
178
|
+
return lo
|
|
179
|
+
if x > hi:
|
|
180
|
+
return hi
|
|
181
|
+
return x
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
`lean-py check --package calc` writes the translation and one obligation per specification:
|
|
185
|
+
|
|
186
|
+
```lean
|
|
187
|
+
def LeanPythonModel.calc.calc.clamp (x : Int) (lo : Int) (hi : Int) : Int := Id.run do
|
|
188
|
+
if ((decide (x < lo))) then
|
|
189
|
+
return lo
|
|
190
|
+
if ((decide (x > hi))) then
|
|
191
|
+
return hi
|
|
192
|
+
return x
|
|
193
|
+
|
|
194
|
+
abbrev LeanPythonModel.calc.calc.clamp.obligation.refines : Prop :=
|
|
195
|
+
∀ (x : Int) (lo : Int) (hi : Int), ordered x lo hi = true → clamp x lo hi = clampSpec x lo hi
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
and judges the theorems `<function>.proof.<label>` of `lean/Proofs/*.lean` next to `pyproject.toml`:
|
|
199
|
+
|
|
200
|
+
```lean
|
|
201
|
+
import LeanPythonModel.calc.calc
|
|
202
|
+
open LeanPythonEmbed.calc.spec.spec
|
|
203
|
+
namespace LeanPythonModel.«calc».«calc»
|
|
204
|
+
|
|
205
|
+
theorem clamp.proof.refines : clamp.obligation.refines := by
|
|
206
|
+
intro x lo hi h
|
|
207
|
+
simp only [ordered, decide_eq_true_eq] at h
|
|
208
|
+
simp only [clamp, clampSpec]
|
|
209
|
+
split <;> (try split) <;> simp_all <;> omega
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
```
|
|
213
|
+
proved calc.calc:clamp refines
|
|
214
|
+
proved calc.calc:clamp in_range
|
|
215
|
+
sorry calc.calc:sum_to refines
|
|
216
|
+
missing calc.calc:sum_abs nonneg
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
A `for` loop over a list becomes `List.foldl` under `simp` (`forIn_pure_yield_eq_foldl`), so a loop
|
|
220
|
+
is proved by induction on the list (`tests/verify/lean/Proofs/Calc.lean` proves `sum_abs`).
|
|
221
|
+
|
|
222
|
+
The states are proved, sorry, axioms (non-standard axioms), failed (the proof file does not build),
|
|
223
|
+
missing, mismatch (the theorem states something else). `--require proved` fails unless all are
|
|
224
|
+
proved. The result goes to `lean-python.lock` with a hash of each function's source;
|
|
225
|
+
`lean-py status` reads it without Lean and marks changed functions `stale`.
|
|
226
|
+
|
|
227
|
+
- `refines=s`: `f x = s x` (`Except.ok (s x)` for a function that may fail); `requires=p`: `p x = true →`
|
|
228
|
+
before each obligation; `ensures={"label": q}`: `q x (f x) = true`. A function annotated `-> Nat`
|
|
229
|
+
also gets `returns_nat` (`0 ≤ f x`).
|
|
230
|
+
- The translation: `int` is `Int` (a `Nat` parameter is a `Nat` converted to `Int` on entry);
|
|
231
|
+
statements become Lean `do` notation (`let mut`, `for`, `while`, `break`, `continue`, early
|
|
232
|
+
`return`); indexing, `//`, `%`, `assert` and `raise` make the function return
|
|
233
|
+
`Py.M α = Except Py.Exc α`, with Python's negative indices and floor division (`LeanPythonLib.Py`).
|
|
234
|
+
Supported: `int`, `bool`, `str`, `float`, `list`, `tuple`, `T | None`, pydantic models (as the
|
|
235
|
+
Lean structures of `models=`), `len`, `abs`, `min`, `max`, `sum`, list comprehensions, `append` on
|
|
236
|
+
local lists, and calls of other `@model` functions of the module.
|
|
237
|
+
- `check_model(f)` / `check_module(m)` generate arguments from the annotations (pydantic constraints
|
|
238
|
+
included), skip those failing `requires`, and compare with `refines` and `ensures`.
|
|
239
|
+
|
|
159
240
|
## The CLI
|
|
160
241
|
|
|
161
242
|
```
|
|
162
243
|
lean-python module generate --module-name <Name> [--roots <Name> ...] [--namespace <Name>]
|
|
163
244
|
[--search-path <dir> ...] [--output-file <path>] [--internal-only] [--output json|text]
|
|
164
245
|
[--type-module <Name>=<module> ...] [--model-module <Name>=<module> ...] [--batch <file>]
|
|
246
|
+
lean-python obligations check --modules <Name> ... --obligations <file> [--search-path <dir> ...]
|
|
165
247
|
lean-python --version | --generator-version | help
|
|
166
248
|
```
|
|
167
249
|
|
|
@@ -210,7 +292,7 @@ nix develop -c make bench
|
|
|
210
292
|
- `Task`s run synchronously.
|
|
211
293
|
- Building a `String` with `String.push` copies the string each time (Python `bytes`), so it grows
|
|
212
294
|
quadratically.
|
|
213
|
-
- Python
|
|
295
|
+
- The translation of Python functions covers the subset above; the guarantee is partial correctness.
|
|
214
296
|
|
|
215
297
|
## License
|
|
216
298
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "lean-python"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0"
|
|
4
4
|
description = "Run Lean 4 code as Python: a runtime for code generated from Lean's IR, Lean blocks in Python files, and pydantic models for Lean types"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "Apache-2.0"
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"""``lean-py check`` and ``lean-py status``: the proofs of the ``@model`` functions of a package.
|
|
2
|
+
|
|
3
|
+
``check`` syncs the package's Lean blocks, translates every ``@model`` function into
|
|
4
|
+
``LeanPythonModel.<module>`` with one obligation per specification (``<function>.obligation.<label>``),
|
|
5
|
+
builds them with the proofs in ``<project>/lean/Proofs/`` (theorems ``<function>.proof.<label>``),
|
|
6
|
+
and asks the Lean CLI for the state of each obligation: proved, sorry, axioms, missing,
|
|
7
|
+
mismatch. The result and a hash of each function's source go to ``<project>/lean-python.lock``.
|
|
8
|
+
``status`` reads the lock without Lean and marks a function stale when its source changed.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import ast
|
|
12
|
+
import hashlib
|
|
13
|
+
import importlib
|
|
14
|
+
import inspect
|
|
15
|
+
import json
|
|
16
|
+
import pathlib
|
|
17
|
+
import shutil
|
|
18
|
+
import subprocess
|
|
19
|
+
import sys
|
|
20
|
+
import textwrap
|
|
21
|
+
|
|
22
|
+
from lean_python import models, sync, translate, verify
|
|
23
|
+
|
|
24
|
+
LAKEFILE_EXTRA = """
|
|
25
|
+
[[lean_lib]]
|
|
26
|
+
name = "LeanPythonLib"
|
|
27
|
+
|
|
28
|
+
[[lean_lib]]
|
|
29
|
+
name = "LeanPythonModel"
|
|
30
|
+
|
|
31
|
+
[[lean_lib]]
|
|
32
|
+
name = "Proofs"
|
|
33
|
+
globs = ["Proofs.+"]
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
STATUSES = ("proved", "sorry", "axioms", "failed", "missing", "mismatch", "unextracted", "stale")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def model_module(module):
|
|
40
|
+
return ["LeanPythonModel", *module.split(".")]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def function_name(fn):
|
|
44
|
+
return models.dotted([*model_module(fn.__module__), fn.__qualname__])
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def source_hash(fn):
|
|
48
|
+
src = textwrap.dedent(inspect.getsource(fn))
|
|
49
|
+
return hashlib.sha256(ast.dump(ast.parse(src)).encode()).hexdigest()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def lean_name_of(spec_fn, label, fn):
|
|
53
|
+
name = getattr(spec_fn, "__lean_name__", None)
|
|
54
|
+
if name is None:
|
|
55
|
+
raise sync.SyncError(
|
|
56
|
+
f"{fn.__qualname__}: the {label} specification is not a function of a lean(...) block", 252
|
|
57
|
+
)
|
|
58
|
+
return name, getattr(spec_fn, "__lean_module__", None)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def obligations(spec, tfn):
|
|
62
|
+
"""The Lean statements of the obligations of one function, by label."""
|
|
63
|
+
fn = spec.fn
|
|
64
|
+
binders = []
|
|
65
|
+
args = []
|
|
66
|
+
for name, ann in tfn.params:
|
|
67
|
+
t = translate.py_type(ann, fn)
|
|
68
|
+
ty = "Nat" if translate.is_nat(ann) else translate.lean_type(t)
|
|
69
|
+
binders.append(f"({translate.lean_ident(name)} : {ty})")
|
|
70
|
+
args.append(translate.lean_ident(name))
|
|
71
|
+
call = f"({tfn.lean_name} {' '.join(args)})"
|
|
72
|
+
a = " ".join(args)
|
|
73
|
+
imports = set()
|
|
74
|
+
pre = ""
|
|
75
|
+
if spec.requires is not None:
|
|
76
|
+
r, m = lean_name_of(spec.requires, "requires", fn)
|
|
77
|
+
imports.add(m)
|
|
78
|
+
pre = f"{r} {a} = true → "
|
|
79
|
+
out = {}
|
|
80
|
+
|
|
81
|
+
def wrap(body):
|
|
82
|
+
return f"∀ {' '.join(binders)}, {pre}{body}" if binders else f"{pre}{body}"
|
|
83
|
+
|
|
84
|
+
if spec.refines is not None:
|
|
85
|
+
s, m = lean_name_of(spec.refines, "refines", fn)
|
|
86
|
+
imports.add(m)
|
|
87
|
+
ret = getattr(spec.refines, "__annotations__", {}).get("return")
|
|
88
|
+
expected = f"(({s} {a} : Nat) : Int)" if ret == "Nat" else f"({s} {a})"
|
|
89
|
+
out["refines"] = wrap(f"{call} = Except.ok {expected}" if tfn.fails else f"{call} = {expected}")
|
|
90
|
+
for label, e in spec.ensures.items():
|
|
91
|
+
en, m = lean_name_of(e, label, fn)
|
|
92
|
+
imports.add(m)
|
|
93
|
+
out[label] = wrap(f"∀ r, {call} = Except.ok r → {en} {a} r = true" if tfn.fails else f"{en} {a} {call} = true")
|
|
94
|
+
if tfn.ret_nat:
|
|
95
|
+
out["returns_nat"] = wrap(f"∀ r, {call} = Except.ok r → 0 ≤ r" if tfn.fails else f"0 ≤ {call}")
|
|
96
|
+
return out, {i for i in imports if i}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def reimport(pkg):
|
|
100
|
+
for name in [m for m in sys.modules if m == pkg or m.startswith(pkg + ".")]:
|
|
101
|
+
del sys.modules[name]
|
|
102
|
+
importlib.invalidate_caches()
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def check_package(pkg_dir, cli=None, out=sys.stderr):
|
|
106
|
+
pkg_dir = pathlib.Path(pkg_dir).resolve()
|
|
107
|
+
cli = cli or shutil.which("lean-python")
|
|
108
|
+
sync.sync_package(pkg_dir, cli=cli, out=out)
|
|
109
|
+
root = sync.project_root(pkg_dir)
|
|
110
|
+
ws = root / ".lean-python" / "workspace"
|
|
111
|
+
|
|
112
|
+
reimport(pkg_dir.name)
|
|
113
|
+
verify.REGISTRY.clear()
|
|
114
|
+
for path in sorted(pkg_dir.rglob("*.py")):
|
|
115
|
+
rel = path.relative_to(pkg_dir)
|
|
116
|
+
if rel.parts[0] == "_lean" or rel.name == "__main__.py":
|
|
117
|
+
continue
|
|
118
|
+
parts = [pkg_dir.name, *rel.with_suffix("").parts]
|
|
119
|
+
if parts[-1] == "__init__":
|
|
120
|
+
parts = parts[:-1]
|
|
121
|
+
importlib.import_module(".".join(parts))
|
|
122
|
+
specs = list(verify.REGISTRY)
|
|
123
|
+
|
|
124
|
+
lib = ws / "LeanPythonLib"
|
|
125
|
+
shutil.rmtree(lib, ignore_errors=True)
|
|
126
|
+
shutil.copytree(pathlib.Path(__file__).parent / "lean" / "LeanPythonLib", lib)
|
|
127
|
+
(ws / "LeanPythonLib.lean").write_text("import LeanPythonLib.Py\n")
|
|
128
|
+
shutil.rmtree(ws / "LeanPythonModel", ignore_errors=True)
|
|
129
|
+
shutil.rmtree(ws / "Proofs", ignore_errors=True)
|
|
130
|
+
proofs_src = root / "lean" / "Proofs"
|
|
131
|
+
if proofs_src.is_dir():
|
|
132
|
+
shutil.copytree(proofs_src, ws / "Proofs")
|
|
133
|
+
else:
|
|
134
|
+
(ws / "Proofs").mkdir()
|
|
135
|
+
(ws / "Proofs" / "Empty.lean").write_text("")
|
|
136
|
+
lakefile = (ws / "lakefile.toml").read_text()
|
|
137
|
+
(ws / "lakefile.toml").write_text(lakefile + LAKEFILE_EXTRA)
|
|
138
|
+
|
|
139
|
+
by_module = {}
|
|
140
|
+
for s in specs:
|
|
141
|
+
by_module.setdefault(s.fn.__module__, []).append(s)
|
|
142
|
+
model_modules = []
|
|
143
|
+
obligation_refs = []
|
|
144
|
+
for module, ss in by_module.items():
|
|
145
|
+
tfns = [translate.Fn(s.fn, function_name(s.fn)) for s in ss]
|
|
146
|
+
tr = translate.Translator(tfns)
|
|
147
|
+
model_classes = set()
|
|
148
|
+
for t in tfns:
|
|
149
|
+
for _, ann in t.params:
|
|
150
|
+
_collect_models(ann, model_classes)
|
|
151
|
+
_collect_models(t.hints["return"], model_classes)
|
|
152
|
+
imports = {"LeanPythonLib.Py"}
|
|
153
|
+
imports |= (
|
|
154
|
+
{models.dotted(models.models_namespace(m)) for m in models.modules_source(list(model_classes))}
|
|
155
|
+
if model_classes
|
|
156
|
+
else set()
|
|
157
|
+
)
|
|
158
|
+
defs = []
|
|
159
|
+
for s, t in zip(ss, tfns, strict=True):
|
|
160
|
+
try:
|
|
161
|
+
defs.append(tr.definition(t))
|
|
162
|
+
except translate.TranslateError as e:
|
|
163
|
+
raise sync.SyncError(str(e), 254) from None
|
|
164
|
+
obls, extra = obligations(s, t)
|
|
165
|
+
imports |= extra
|
|
166
|
+
for label, prop in obls.items():
|
|
167
|
+
defs.append(f"abbrev {t.lean_name}.obligation.{label} : Prop :=\n {prop}")
|
|
168
|
+
obligation_refs.append((s, label))
|
|
169
|
+
missing_models = [m for m in models.modules_source(list(model_classes))] if model_classes else []
|
|
170
|
+
for m in missing_models:
|
|
171
|
+
p = ws / pathlib.Path(*models.models_namespace(m)).with_suffix(".lean")
|
|
172
|
+
if not p.exists():
|
|
173
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
174
|
+
p.write_text(models.modules_source(list(model_classes))[m])
|
|
175
|
+
path = ws / pathlib.Path(*model_module(module)).with_suffix(".lean")
|
|
176
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
177
|
+
head = "".join(f"import {i}\n" for i in sorted(imports))
|
|
178
|
+
path.write_text(head + "\n" + "\n\n".join(defs) + "\n")
|
|
179
|
+
model_modules.append(".".join(model_module(module)))
|
|
180
|
+
(ws / "LeanPythonModel.lean").write_text(
|
|
181
|
+
"".join(f"import {models.dotted(m.split('.'))}\n" for m in model_modules) or "\n"
|
|
182
|
+
)
|
|
183
|
+
(ws / "LeanPythonModels.lean").write_text(
|
|
184
|
+
"".join(
|
|
185
|
+
f"import {models.dotted(p.relative_to(ws).with_suffix('').parts)}\n"
|
|
186
|
+
for p in sorted((ws / "LeanPythonModels").rglob("*.lean"))
|
|
187
|
+
)
|
|
188
|
+
if (ws / "LeanPythonModels").is_dir()
|
|
189
|
+
else "\n"
|
|
190
|
+
)
|
|
191
|
+
proof_modules = [".".join(p.relative_to(ws).with_suffix("").parts) for p in sorted((ws / "Proofs").rglob("*.lean"))]
|
|
192
|
+
(ws / "Proofs.lean").write_text("".join(f"import {models.dotted(m.split('.'))}\n" for m in proof_modules))
|
|
193
|
+
|
|
194
|
+
r = subprocess.run(
|
|
195
|
+
["lake", "build", "LeanPythonLib", "LeanPythonModels", "LeanPythonEmbed", "LeanPythonModel"],
|
|
196
|
+
cwd=ws,
|
|
197
|
+
capture_output=True,
|
|
198
|
+
text=True,
|
|
199
|
+
check=False,
|
|
200
|
+
)
|
|
201
|
+
if r.returncode != 0:
|
|
202
|
+
print(r.stdout + r.stderr, file=out)
|
|
203
|
+
raise sync.SyncError("lake build of the translated functions failed", 254)
|
|
204
|
+
failed_files = {}
|
|
205
|
+
built = []
|
|
206
|
+
for m in proof_modules:
|
|
207
|
+
r = subprocess.run(["lake", "build", m], cwd=ws, capture_output=True, text=True, check=False)
|
|
208
|
+
if r.returncode == 0:
|
|
209
|
+
built.append(m)
|
|
210
|
+
continue
|
|
211
|
+
src = (ws / pathlib.Path(*m.split("."))).with_suffix(".lean")
|
|
212
|
+
failed_files[m] = src.read_text()
|
|
213
|
+
for line in (r.stdout + r.stderr).splitlines():
|
|
214
|
+
if line.startswith(("error:", " ", "⊢", "case ")) or ": error" in line:
|
|
215
|
+
print(f"lean-py: {line}", file=out)
|
|
216
|
+
proof_modules = built
|
|
217
|
+
|
|
218
|
+
obl_file = ws / "obligations.json"
|
|
219
|
+
obl_file.write_text(
|
|
220
|
+
json.dumps([{"model": _plain(function_name(s.fn)), "label": label} for s, label in obligation_refs])
|
|
221
|
+
)
|
|
222
|
+
r = subprocess.run(
|
|
223
|
+
[
|
|
224
|
+
cli,
|
|
225
|
+
"obligations",
|
|
226
|
+
"check",
|
|
227
|
+
"--search-path",
|
|
228
|
+
str(ws / ".lake/build/lib/lean"),
|
|
229
|
+
"--obligations",
|
|
230
|
+
str(obl_file),
|
|
231
|
+
"--modules",
|
|
232
|
+
*model_modules,
|
|
233
|
+
*proof_modules,
|
|
234
|
+
],
|
|
235
|
+
capture_output=True,
|
|
236
|
+
text=True,
|
|
237
|
+
check=False,
|
|
238
|
+
)
|
|
239
|
+
if r.returncode != 0:
|
|
240
|
+
print(r.stderr, file=out)
|
|
241
|
+
raise sync.SyncError("lean-python obligations check failed", r.returncode)
|
|
242
|
+
results = json.loads(r.stdout)
|
|
243
|
+
for (s, label), res in zip(obligation_refs, results, strict=True):
|
|
244
|
+
theorem = f"{s.fn.__qualname__}.proof.{label}"
|
|
245
|
+
if res["status"] == "missing" and any(theorem in text for text in failed_files.values()):
|
|
246
|
+
res["status"] = "failed"
|
|
247
|
+
lock = {}
|
|
248
|
+
for (s, label), res in zip(obligation_refs, results, strict=True):
|
|
249
|
+
entry = lock.setdefault(s.qualname, {"hash": source_hash(s.fn), "lean": function_name(s.fn), "obligations": {}})
|
|
250
|
+
entry["obligations"][label] = res["status"]
|
|
251
|
+
(root / "lean-python.lock").write_text(json.dumps(lock, indent=2, sort_keys=True) + "\n")
|
|
252
|
+
return lock, results
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _plain(name):
|
|
256
|
+
return name.replace("«", "").replace("»", "")
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _collect_models(ann, acc):
|
|
260
|
+
import typing
|
|
261
|
+
|
|
262
|
+
from pydantic import BaseModel
|
|
263
|
+
|
|
264
|
+
while typing.get_origin(ann) is typing.Annotated:
|
|
265
|
+
ann = typing.get_args(ann)[0]
|
|
266
|
+
if isinstance(ann, type) and issubclass(ann, BaseModel):
|
|
267
|
+
acc.add(ann)
|
|
268
|
+
return
|
|
269
|
+
for a in typing.get_args(ann):
|
|
270
|
+
_collect_models(a, acc)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def status(pkg_dir):
|
|
274
|
+
"""The lock's states, with ``stale`` for functions whose source changed (no Lean needed)."""
|
|
275
|
+
pkg_dir = pathlib.Path(pkg_dir).resolve()
|
|
276
|
+
root = sync.project_root(pkg_dir)
|
|
277
|
+
lock_path = root / "lean-python.lock"
|
|
278
|
+
lock = json.loads(lock_path.read_text()) if lock_path.exists() else {}
|
|
279
|
+
sys.path.insert(0, str(pkg_dir.parent))
|
|
280
|
+
verify.REGISTRY.clear()
|
|
281
|
+
sync.record_blocks(pkg_dir)
|
|
282
|
+
rows = []
|
|
283
|
+
for s in verify.REGISTRY:
|
|
284
|
+
entry = lock.get(s.qualname)
|
|
285
|
+
labels = list(s.specs())
|
|
286
|
+
if entry is None:
|
|
287
|
+
rows.extend((s.qualname, label, "stale") for label in labels if label != "requires")
|
|
288
|
+
continue
|
|
289
|
+
stale = entry["hash"] != source_hash(s.fn)
|
|
290
|
+
for label, st in entry["obligations"].items():
|
|
291
|
+
rows.append((s.qualname, label, "stale" if stale else st))
|
|
292
|
+
return rows
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""``lean-py``: the command line of the Python package.
|
|
2
|
+
|
|
3
|
+
usage: lean-py sync --package <dir> [--lean-python <path>]
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
USAGE = """usage: lean-py <command> [parameters]
|
|
11
|
+
|
|
12
|
+
commands:
|
|
13
|
+
sync Check the Lean blocks of a package and generate their Python modules
|
|
14
|
+
check Translate the @model functions to Lean and judge the proofs of their obligations
|
|
15
|
+
status Show the states recorded by `check`, marking functions changed since then (no Lean)
|
|
16
|
+
|
|
17
|
+
lean-py sync
|
|
18
|
+
--package <dir> The Python package (the directory with __init__.py) (required)
|
|
19
|
+
--lean-python <path> The Lean CLI (default: $LEAN_PYTHON_CLI, then `lean-python` on PATH)
|
|
20
|
+
|
|
21
|
+
lean-py check
|
|
22
|
+
--package <dir>, --lean-python <path> as for sync
|
|
23
|
+
--require proved Exit with 254 unless every obligation is proved
|
|
24
|
+
|
|
25
|
+
lean-py status
|
|
26
|
+
--package <dir> The Python package (required)
|
|
27
|
+
|
|
28
|
+
exit codes: 0 success, 252 invalid parameters, 254 input that cannot be converted, 255 other errors
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def main(argv=None):
|
|
33
|
+
argv = sys.argv[1:] if argv is None else argv
|
|
34
|
+
if not argv or argv[0] in ("help", "--help", "-h"):
|
|
35
|
+
print(USAGE, end="")
|
|
36
|
+
return 0
|
|
37
|
+
command = argv[0]
|
|
38
|
+
if command not in ("sync", "check", "status"):
|
|
39
|
+
print(f"lean-py: unknown command {command!r} (see `lean-py help`)", file=sys.stderr)
|
|
40
|
+
return 252
|
|
41
|
+
p = argparse.ArgumentParser(prog=f"lean-py {command}", add_help=False, exit_on_error=False)
|
|
42
|
+
p.add_argument("--package", required=True)
|
|
43
|
+
p.add_argument("--lean-python")
|
|
44
|
+
p.add_argument("--require", choices=["proved"])
|
|
45
|
+
try:
|
|
46
|
+
args = p.parse_args(argv[1:])
|
|
47
|
+
except (argparse.ArgumentError, SystemExit) as e:
|
|
48
|
+
print(f"lean-py: {e}", file=sys.stderr)
|
|
49
|
+
return 252
|
|
50
|
+
from lean_python import check, sync
|
|
51
|
+
|
|
52
|
+
cli = args.lean_python or os.environ.get("LEAN_PYTHON_CLI")
|
|
53
|
+
try:
|
|
54
|
+
if command == "sync":
|
|
55
|
+
sync.sync_package(args.package, cli=cli)
|
|
56
|
+
return 0
|
|
57
|
+
if command == "check":
|
|
58
|
+
lock, _ = check.check_package(args.package, cli=cli)
|
|
59
|
+
rows = [(q, label, st) for q, e in sorted(lock.items()) for label, st in e["obligations"].items()]
|
|
60
|
+
else:
|
|
61
|
+
rows = check.status(args.package)
|
|
62
|
+
except sync.SyncError as e:
|
|
63
|
+
print(f"lean-py: {e}", file=sys.stderr)
|
|
64
|
+
return e.code
|
|
65
|
+
for q, label, st in rows:
|
|
66
|
+
print(f"{st:<10} {q} {label}")
|
|
67
|
+
if args.require == "proved" and any(st != "proved" for _, _, st in rows):
|
|
68
|
+
return 254
|
|
69
|
+
return 0
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
if __name__ == "__main__":
|
|
73
|
+
sys.exit(main())
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/-! The meaning of the Python operations the translated functions use (`lean-py check`).
|
|
2
|
+
|
|
3
|
+
Failing operations (indexing, `//`, `%`, `assert`, `raise`) return `Py.M α = Except Py.Exc α`.
|
|
4
|
+
Python's `int` is `Int`; `//` and `%` round towards negative infinity (`Int.fdiv`, `Int.fmod`). -/
|
|
5
|
+
|
|
6
|
+
namespace Py
|
|
7
|
+
|
|
8
|
+
/-- The exceptions of the translated functions. -/
|
|
9
|
+
inductive Exc where
|
|
10
|
+
| indexError
|
|
11
|
+
| zeroDivisionError
|
|
12
|
+
| assertionError
|
|
13
|
+
| valueError (msg : String)
|
|
14
|
+
deriving Repr, DecidableEq
|
|
15
|
+
|
|
16
|
+
abbrev M := Except Exc
|
|
17
|
+
|
|
18
|
+
/-- `xs[i]`, with Python's negative indices. -/
|
|
19
|
+
def getItem (xs : List α) (i : Int) : M α :=
|
|
20
|
+
let j := if i < 0 then i + xs.length else i
|
|
21
|
+
if 0 ≤ j then
|
|
22
|
+
match xs[j.toNat]? with
|
|
23
|
+
| some v => pure v
|
|
24
|
+
| none => throw .indexError
|
|
25
|
+
else throw .indexError
|
|
26
|
+
|
|
27
|
+
/-- `a // b`. -/
|
|
28
|
+
def floordiv (a b : Int) : M Int := if b = 0 then throw .zeroDivisionError else pure (Int.fdiv a b)
|
|
29
|
+
|
|
30
|
+
/-- `a % b`. -/
|
|
31
|
+
def mod (a b : Int) : M Int := if b = 0 then throw .zeroDivisionError else pure (Int.fmod a b)
|
|
32
|
+
|
|
33
|
+
/-- `range(a, b)`. -/
|
|
34
|
+
def range (a b : Int) : List Int := (List.range (b - a).toNat).map fun (k : Nat) => a + (k : Int)
|
|
35
|
+
|
|
36
|
+
def abs (a : Int) : Int := if a < 0 then -a else a
|
|
37
|
+
|
|
38
|
+
def len (xs : List α) : Int := xs.length
|
|
39
|
+
|
|
40
|
+
def strLen (s : String) : Int := s.length
|
|
41
|
+
|
|
42
|
+
def sum (xs : List Int) : Int := xs.foldl (· + ·) 0
|
|
43
|
+
|
|
44
|
+
def assert (c : Bool) : M Unit := if c then pure () else throw .assertionError
|
|
45
|
+
|
|
46
|
+
@[simp] theorem range_mem (a b i : Int) : i ∈ range a b ↔ a ≤ i ∧ i < b := by
|
|
47
|
+
simp only [range, List.mem_map, List.mem_range]
|
|
48
|
+
constructor
|
|
49
|
+
· rintro ⟨k, hk, rfl⟩; omega
|
|
50
|
+
· intro h; exact ⟨(i - a).toNat, by omega, by omega⟩
|
|
51
|
+
|
|
52
|
+
end Py
|
|
@@ -701,9 +701,11 @@ def register_inductive(base, pairs):
|
|
|
701
701
|
# Module assembly
|
|
702
702
|
|
|
703
703
|
|
|
704
|
-
def attach(g, path, fn):
|
|
704
|
+
def attach(g, path, fn, lean_name=None):
|
|
705
705
|
fn.__name__ = path[-1]
|
|
706
706
|
fn.__qualname__ = ".".join(path)
|
|
707
|
+
fn.__lean_name__ = lean_name
|
|
708
|
+
fn.__lean_module__ = g.get("__lean_module__")
|
|
707
709
|
if len(path) == 1:
|
|
708
710
|
g[path[0]] = fn
|
|
709
711
|
return
|