lean-python 0.1.0__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lean_python/check.py +292 -0
- lean_python/cli.py +29 -6
- lean_python/embed.py +1 -1
- lean_python/lean/LeanPythonLib/Py.lean +52 -0
- lean_python/marshal.py +3 -1
- lean_python/translate.py +469 -0
- lean_python/verify.py +147 -0
- {lean_python-0.1.0.dist-info → lean_python-0.2.0.dist-info}/METADATA +84 -2
- lean_python-0.2.0.dist-info/RECORD +19 -0
- lean_python-0.1.0.dist-info/RECORD +0 -15
- {lean_python-0.1.0.dist-info → lean_python-0.2.0.dist-info}/WHEEL +0 -0
- {lean_python-0.1.0.dist-info → lean_python-0.2.0.dist-info}/entry_points.txt +0 -0
- {lean_python-0.1.0.dist-info → lean_python-0.2.0.dist-info}/licenses/LICENSE +0 -0
lean_python/check.py
ADDED
|
@@ -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
|
lean_python/cli.py
CHANGED
|
@@ -10,12 +10,21 @@ import sys
|
|
|
10
10
|
USAGE = """usage: lean-py <command> [parameters]
|
|
11
11
|
|
|
12
12
|
commands:
|
|
13
|
-
sync
|
|
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)
|
|
14
16
|
|
|
15
17
|
lean-py sync
|
|
16
18
|
--package <dir> The Python package (the directory with __init__.py) (required)
|
|
17
19
|
--lean-python <path> The Lean CLI (default: $LEAN_PYTHON_CLI, then `lean-python` on PATH)
|
|
18
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
|
+
|
|
19
28
|
exit codes: 0 success, 252 invalid parameters, 254 input that cannot be converted, 255 other errors
|
|
20
29
|
"""
|
|
21
30
|
|
|
@@ -25,24 +34,38 @@ def main(argv=None):
|
|
|
25
34
|
if not argv or argv[0] in ("help", "--help", "-h"):
|
|
26
35
|
print(USAGE, end="")
|
|
27
36
|
return 0
|
|
28
|
-
|
|
29
|
-
|
|
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)
|
|
30
40
|
return 252
|
|
31
|
-
p = argparse.ArgumentParser(prog="lean-py
|
|
41
|
+
p = argparse.ArgumentParser(prog=f"lean-py {command}", add_help=False, exit_on_error=False)
|
|
32
42
|
p.add_argument("--package", required=True)
|
|
33
43
|
p.add_argument("--lean-python")
|
|
44
|
+
p.add_argument("--require", choices=["proved"])
|
|
34
45
|
try:
|
|
35
46
|
args = p.parse_args(argv[1:])
|
|
36
47
|
except (argparse.ArgumentError, SystemExit) as e:
|
|
37
48
|
print(f"lean-py: {e}", file=sys.stderr)
|
|
38
49
|
return 252
|
|
39
|
-
from lean_python import sync
|
|
50
|
+
from lean_python import check, sync
|
|
40
51
|
|
|
52
|
+
cli = args.lean_python or os.environ.get("LEAN_PYTHON_CLI")
|
|
41
53
|
try:
|
|
42
|
-
|
|
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)
|
|
43
62
|
except sync.SyncError as e:
|
|
44
63
|
print(f"lean-py: {e}", file=sys.stderr)
|
|
45
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
|
|
46
69
|
return 0
|
|
47
70
|
|
|
48
71
|
|
lean_python/embed.py
CHANGED
|
@@ -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
|
lean_python/marshal.py
CHANGED
|
@@ -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
|
lean_python/translate.py
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
"""Translation of a typed, pure Python function into a Lean definition (``lean-py check``).
|
|
2
|
+
|
|
3
|
+
The body becomes Lean ``do`` notation, which has Python's statements: ``let mut`` for local
|
|
4
|
+
variables, ``for``/``while`` with ``break``/``continue``, ``if``, early ``return``. A function that
|
|
5
|
+
may fail (indexing, ``//``, ``%``, ``assert``, ``raise``, or a call of such a function) returns
|
|
6
|
+
``Py.M α`` (``Except Py.Exc α``); the others return ``α`` through ``Id.run do``. Python's ``int``
|
|
7
|
+
is ``Int``; a ``Nat`` parameter is converted to ``Int`` on entry.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import ast
|
|
11
|
+
import inspect
|
|
12
|
+
import textwrap
|
|
13
|
+
import typing
|
|
14
|
+
|
|
15
|
+
from pydantic import BaseModel
|
|
16
|
+
|
|
17
|
+
from lean_python import models
|
|
18
|
+
from lean_python.types import Nat
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TranslateError(Exception):
|
|
22
|
+
def __init__(self, msg, fn=None, node=None):
|
|
23
|
+
where = ""
|
|
24
|
+
if fn is not None:
|
|
25
|
+
line = fn.__code__.co_firstlineno + (node.lineno - 1 if node is not None and hasattr(node, "lineno") else 0)
|
|
26
|
+
where = f"{fn.__code__.co_filename}:{line}: "
|
|
27
|
+
super().__init__(f"{where}{msg}")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
INT, BOOL, STR, FLOAT, NONE = "int", "bool", "str", "float", "none"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def py_type(t, fn):
|
|
34
|
+
"""A small type descriptor of a Python annotation."""
|
|
35
|
+
if t is Nat or (typing.get_origin(t) is typing.Annotated and typing.get_args(t)[0] is int):
|
|
36
|
+
return INT
|
|
37
|
+
if t is int:
|
|
38
|
+
return INT
|
|
39
|
+
if t is bool:
|
|
40
|
+
return BOOL
|
|
41
|
+
if t is str:
|
|
42
|
+
return STR
|
|
43
|
+
if t is float:
|
|
44
|
+
return FLOAT
|
|
45
|
+
if t is type(None) or t is None:
|
|
46
|
+
return NONE
|
|
47
|
+
origin = typing.get_origin(t)
|
|
48
|
+
if origin is list:
|
|
49
|
+
return ("list", py_type(typing.get_args(t)[0], fn))
|
|
50
|
+
if origin is tuple:
|
|
51
|
+
return ("tuple", tuple(py_type(a, fn) for a in typing.get_args(t)))
|
|
52
|
+
args = typing.get_args(t)
|
|
53
|
+
if args and type(None) in args and len(args) == 2:
|
|
54
|
+
return ("opt", py_type(next(a for a in args if a is not type(None)), fn))
|
|
55
|
+
if isinstance(t, type) and issubclass(t, BaseModel):
|
|
56
|
+
return ("model", t)
|
|
57
|
+
raise TranslateError(f"unsupported type {t!r}", fn)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def lean_type(t):
|
|
61
|
+
if t == INT:
|
|
62
|
+
return "Int"
|
|
63
|
+
if t == BOOL:
|
|
64
|
+
return "Bool"
|
|
65
|
+
if t == STR:
|
|
66
|
+
return "String"
|
|
67
|
+
if t == FLOAT:
|
|
68
|
+
return "Float"
|
|
69
|
+
if t == NONE:
|
|
70
|
+
return "Unit"
|
|
71
|
+
kind = t[0]
|
|
72
|
+
if kind == "list":
|
|
73
|
+
return f"(List {lean_type(t[1])})"
|
|
74
|
+
if kind == "tuple":
|
|
75
|
+
return "(" + " × ".join(lean_type(a) for a in t[1]) + ")"
|
|
76
|
+
if kind == "opt":
|
|
77
|
+
return f"(Option {lean_type(t[1])})"
|
|
78
|
+
return models.class_name(t[1])
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def is_nat(annotation):
|
|
82
|
+
if annotation is Nat:
|
|
83
|
+
return True
|
|
84
|
+
if typing.get_origin(annotation) is typing.Annotated:
|
|
85
|
+
import annotated_types as at
|
|
86
|
+
|
|
87
|
+
base, *meta = typing.get_args(annotation)
|
|
88
|
+
return base is int and any(isinstance(m, at.Ge) and m.ge >= 0 for m in meta)
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def field_info(cls, name):
|
|
93
|
+
"""The Python type of a model field and whether its Lean field is a `Nat`."""
|
|
94
|
+
f = cls.model_fields.get(name)
|
|
95
|
+
if f is None:
|
|
96
|
+
raise TranslateError(f"{cls.__name__} has no field {name}")
|
|
97
|
+
t = py_type(f.annotation, None)
|
|
98
|
+
nat = False
|
|
99
|
+
if t == INT:
|
|
100
|
+
import annotated_types as at
|
|
101
|
+
|
|
102
|
+
lo = [m.ge for m in f.metadata if isinstance(m, at.Ge)] + [m.gt + 1 for m in f.metadata if isinstance(m, at.Gt)]
|
|
103
|
+
nat = is_nat(f.annotation) or (bool(lo) and max(lo) >= 0)
|
|
104
|
+
return t, nat
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class Fn:
|
|
108
|
+
"""A function to translate: its signature and the names Lean knows it by."""
|
|
109
|
+
|
|
110
|
+
def __init__(self, fn, lean_name):
|
|
111
|
+
self.fn = fn
|
|
112
|
+
self.lean_name = lean_name
|
|
113
|
+
self.hints = typing.get_type_hints(fn, include_extras=True)
|
|
114
|
+
sig = inspect.signature(fn)
|
|
115
|
+
self.params = []
|
|
116
|
+
for p in sig.parameters.values():
|
|
117
|
+
if p.name not in self.hints:
|
|
118
|
+
raise TranslateError(f"parameter {p.name} of {fn.__qualname__} has no type annotation", fn)
|
|
119
|
+
self.params.append((p.name, self.hints[p.name]))
|
|
120
|
+
if "return" not in self.hints:
|
|
121
|
+
raise TranslateError(f"{fn.__qualname__} has no return annotation", fn)
|
|
122
|
+
self.ret = py_type(self.hints["return"], fn)
|
|
123
|
+
self.ret_nat = is_nat(self.hints["return"])
|
|
124
|
+
src = textwrap.dedent(inspect.getsource(fn))
|
|
125
|
+
tree = ast.parse(src)
|
|
126
|
+
self.node = next(n for n in tree.body if isinstance(n, ast.FunctionDef))
|
|
127
|
+
self.fails = None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class Translator:
|
|
131
|
+
def __init__(self, fns):
|
|
132
|
+
self.fns = {f.fn.__name__: f for f in fns}
|
|
133
|
+
for f in fns:
|
|
134
|
+
f.fails = self._may_fail(f.node)
|
|
135
|
+
changed = True
|
|
136
|
+
while changed:
|
|
137
|
+
changed = False
|
|
138
|
+
for f in fns:
|
|
139
|
+
if not f.fails and any(
|
|
140
|
+
isinstance(n, ast.Call)
|
|
141
|
+
and isinstance(n.func, ast.Name)
|
|
142
|
+
and n.func.id in self.fns
|
|
143
|
+
and self.fns[n.func.id].fails
|
|
144
|
+
for stmt in f.node.body
|
|
145
|
+
for n in ast.walk(stmt)
|
|
146
|
+
):
|
|
147
|
+
f.fails = True
|
|
148
|
+
changed = True
|
|
149
|
+
|
|
150
|
+
def _may_fail(self, node):
|
|
151
|
+
for stmt in node.body:
|
|
152
|
+
for n in ast.walk(stmt):
|
|
153
|
+
if isinstance(n, (ast.Subscript, ast.Assert, ast.Raise)):
|
|
154
|
+
return True
|
|
155
|
+
if isinstance(n, ast.BinOp) and isinstance(n.op, (ast.FloorDiv, ast.Mod)):
|
|
156
|
+
return True
|
|
157
|
+
return False
|
|
158
|
+
|
|
159
|
+
# -- expressions -------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
def expr(self, e, env, fn):
|
|
162
|
+
"""Lean source and type of a Python expression."""
|
|
163
|
+
f = fn.fn
|
|
164
|
+
if isinstance(e, ast.Constant):
|
|
165
|
+
v = e.value
|
|
166
|
+
if isinstance(v, bool):
|
|
167
|
+
return ("true" if v else "false"), BOOL
|
|
168
|
+
if isinstance(v, int):
|
|
169
|
+
return (f"({v} : Int)"), INT
|
|
170
|
+
if isinstance(v, str):
|
|
171
|
+
return json_str(v), STR
|
|
172
|
+
if isinstance(v, float):
|
|
173
|
+
return repr(v), FLOAT
|
|
174
|
+
if v is None:
|
|
175
|
+
return "none", ("opt", None)
|
|
176
|
+
raise TranslateError(f"unsupported constant {v!r}", f, e)
|
|
177
|
+
if isinstance(e, ast.Name):
|
|
178
|
+
if e.id not in env:
|
|
179
|
+
raise TranslateError(f"unknown name {e.id}", f, e)
|
|
180
|
+
return lean_ident(e.id), env[e.id]
|
|
181
|
+
if isinstance(e, ast.BinOp):
|
|
182
|
+
a, ta = self.expr(e.left, env, fn)
|
|
183
|
+
b, _ = self.expr(e.right, env, fn)
|
|
184
|
+
op = e.op
|
|
185
|
+
if isinstance(op, ast.Add):
|
|
186
|
+
if ta in (STR,) or (isinstance(ta, tuple) and ta[0] == "list"):
|
|
187
|
+
return f"({a} ++ {b})", ta
|
|
188
|
+
return f"({a} + {b})", ta
|
|
189
|
+
if isinstance(op, ast.Sub):
|
|
190
|
+
return f"({a} - {b})", ta
|
|
191
|
+
if isinstance(op, ast.Mult):
|
|
192
|
+
return f"({a} * {b})", ta
|
|
193
|
+
if isinstance(op, ast.FloorDiv):
|
|
194
|
+
return f"(← Py.floordiv {a} {b})", INT
|
|
195
|
+
if isinstance(op, ast.Mod):
|
|
196
|
+
return f"(← Py.mod {a} {b})", INT
|
|
197
|
+
raise TranslateError(f"unsupported operator {type(op).__name__}", f, e)
|
|
198
|
+
if isinstance(e, ast.UnaryOp):
|
|
199
|
+
a, ta = self.expr(e.operand, env, fn)
|
|
200
|
+
if isinstance(e.op, ast.USub):
|
|
201
|
+
return f"(-{a})", ta
|
|
202
|
+
if isinstance(e.op, ast.Not):
|
|
203
|
+
return f"(!{a})", BOOL
|
|
204
|
+
raise TranslateError("unsupported unary operator", f, e)
|
|
205
|
+
if isinstance(e, ast.BoolOp):
|
|
206
|
+
parts = [self.expr(v, env, fn)[0] for v in e.values]
|
|
207
|
+
op = " && " if isinstance(e.op, ast.And) else " || "
|
|
208
|
+
return "(" + op.join(parts) + ")", BOOL
|
|
209
|
+
if isinstance(e, ast.Compare):
|
|
210
|
+
parts = []
|
|
211
|
+
left, _ = self.expr(e.left, env, fn)
|
|
212
|
+
for op, right in zip(e.ops, e.comparators, strict=True):
|
|
213
|
+
r, _ = self.expr(right, env, fn)
|
|
214
|
+
if isinstance(op, ast.In):
|
|
215
|
+
parts.append(f"({r}.contains {left})")
|
|
216
|
+
elif isinstance(op, ast.NotIn):
|
|
217
|
+
parts.append(f"(!{r}.contains {left})")
|
|
218
|
+
else:
|
|
219
|
+
sym = {ast.Eq: "==", ast.NotEq: "!=", ast.Lt: "<", ast.LtE: "≤", ast.Gt: ">", ast.GtE: "≥"}.get(
|
|
220
|
+
type(op)
|
|
221
|
+
)
|
|
222
|
+
if sym is None:
|
|
223
|
+
raise TranslateError("unsupported comparison", f, e)
|
|
224
|
+
parts.append(f"({left} {sym} {r})" if sym in ("==", "!=") else f"(decide ({left} {sym} {r}))")
|
|
225
|
+
left = r
|
|
226
|
+
return "(" + " && ".join(parts) + ")", BOOL
|
|
227
|
+
if isinstance(e, ast.IfExp):
|
|
228
|
+
c, _ = self.expr(e.test, env, fn)
|
|
229
|
+
a, ta = self.expr(e.body, env, fn)
|
|
230
|
+
b, _ = self.expr(e.orelse, env, fn)
|
|
231
|
+
return f"(if {c} then {a} else {b})", ta
|
|
232
|
+
if isinstance(e, ast.Attribute):
|
|
233
|
+
obj, to = self.expr(e.value, env, fn)
|
|
234
|
+
if isinstance(to, tuple) and to[0] == "model":
|
|
235
|
+
t, nat = field_info(to[1], e.attr)
|
|
236
|
+
s = f"{obj}.{lean_ident(e.attr)}"
|
|
237
|
+
return (f"({s} : Int)" if nat else s), t
|
|
238
|
+
raise TranslateError(f"attribute {e.attr} of a non-model value", f, e)
|
|
239
|
+
if isinstance(e, ast.Subscript):
|
|
240
|
+
obj, to = self.expr(e.value, env, fn)
|
|
241
|
+
if isinstance(to, tuple) and to[0] == "tuple" and isinstance(e.slice, ast.Constant):
|
|
242
|
+
i = e.slice.value
|
|
243
|
+
n = len(to[1])
|
|
244
|
+
proj = obj
|
|
245
|
+
for _ in range(i):
|
|
246
|
+
proj = f"{proj}.2"
|
|
247
|
+
if i < n - 1:
|
|
248
|
+
proj = f"{proj}.1"
|
|
249
|
+
return proj, to[1][i]
|
|
250
|
+
if isinstance(to, tuple) and to[0] == "list":
|
|
251
|
+
i, _ = self.expr(e.slice, env, fn)
|
|
252
|
+
return f"(← Py.getItem {obj} {i})", to[1]
|
|
253
|
+
raise TranslateError("unsupported subscript", f, e)
|
|
254
|
+
if isinstance(e, ast.Tuple):
|
|
255
|
+
parts = [self.expr(v, env, fn) for v in e.elts]
|
|
256
|
+
return "(" + ", ".join(p for p, _ in parts) + ")", ("tuple", tuple(t for _, t in parts))
|
|
257
|
+
if isinstance(e, ast.List):
|
|
258
|
+
parts = [self.expr(v, env, fn) for v in e.elts]
|
|
259
|
+
t = parts[0][1] if parts else INT
|
|
260
|
+
return "[" + ", ".join(p for p, _ in parts) + "]", ("list", t)
|
|
261
|
+
if isinstance(e, ast.ListComp):
|
|
262
|
+
return self.listcomp(e, env, fn)
|
|
263
|
+
if isinstance(e, ast.Call):
|
|
264
|
+
return self.call(e, env, fn)
|
|
265
|
+
raise TranslateError(f"unsupported expression {type(e).__name__}", f, e)
|
|
266
|
+
|
|
267
|
+
def listcomp(self, e, env, fn):
|
|
268
|
+
if len(e.generators) != 1 or e.generators[0].is_async or not isinstance(e.generators[0].target, ast.Name):
|
|
269
|
+
raise TranslateError("only list comprehensions with one `for x in ...` are supported", fn.fn, e)
|
|
270
|
+
g = e.generators[0]
|
|
271
|
+
src, ts = self.iterable(g.iter, env, fn)
|
|
272
|
+
x = g.target.id
|
|
273
|
+
inner = {**env, x: ts}
|
|
274
|
+
for cond in g.ifs:
|
|
275
|
+
c, _ = self.expr(cond, inner, fn)
|
|
276
|
+
src = f"({src}.filter (fun {lean_ident(x)} => {c}))"
|
|
277
|
+
body, t_body = self.expr(e.elt, inner, fn)
|
|
278
|
+
if "←" in body:
|
|
279
|
+
raise TranslateError("a list comprehension must not fail", fn.fn, e)
|
|
280
|
+
return f"({src}.map (fun {lean_ident(x)} => {body}))", ("list", t_body)
|
|
281
|
+
|
|
282
|
+
def iterable(self, e, env, fn):
|
|
283
|
+
if isinstance(e, ast.Call) and isinstance(e.func, ast.Name) and e.func.id == "range":
|
|
284
|
+
args = [self.expr(a, env, fn)[0] for a in e.args]
|
|
285
|
+
if len(args) == 1:
|
|
286
|
+
return f"(Py.range 0 {args[0]})", INT
|
|
287
|
+
if len(args) == 2:
|
|
288
|
+
return f"(Py.range {args[0]} {args[1]})", INT
|
|
289
|
+
raise TranslateError("range with a step is not supported", fn.fn, e)
|
|
290
|
+
src, t = self.expr(e, env, fn)
|
|
291
|
+
if isinstance(t, tuple) and t[0] == "list":
|
|
292
|
+
return src, t[1]
|
|
293
|
+
raise TranslateError("iteration over a non-list", fn.fn, e)
|
|
294
|
+
|
|
295
|
+
def call(self, e, env, fn):
|
|
296
|
+
f = fn.fn
|
|
297
|
+
if e.keywords and not (isinstance(e.func, ast.Name) and e.func.id not in ("len", "abs", "min", "max", "sum")):
|
|
298
|
+
raise TranslateError("keyword arguments are not supported", f, e)
|
|
299
|
+
if isinstance(e.func, ast.Name):
|
|
300
|
+
name = e.func.id
|
|
301
|
+
args = [self.expr(a, env, fn) for a in e.args]
|
|
302
|
+
if name == "len":
|
|
303
|
+
((a, ta),) = args
|
|
304
|
+
return (f"(Py.strLen {a})" if ta == STR else f"(Py.len {a})"), INT
|
|
305
|
+
if name == "abs":
|
|
306
|
+
return f"(Py.abs {args[0][0]})", INT
|
|
307
|
+
if name in ("min", "max") and len(args) == 2:
|
|
308
|
+
return f"({name} {args[0][0]} {args[1][0]})", args[0][1]
|
|
309
|
+
if name == "sum":
|
|
310
|
+
return f"(Py.sum {args[0][0]})", INT
|
|
311
|
+
if name in self.fns:
|
|
312
|
+
target = self.fns[name]
|
|
313
|
+
call = f"({target.lean_name} {' '.join(self.arg_for(target, i, a) for i, (a, _) in enumerate(args))})"
|
|
314
|
+
return (f"(← {call})" if target.fails else call), target.ret
|
|
315
|
+
raise TranslateError("unsupported call (only len, abs, min, max, sum and @model functions of the module)", f, e)
|
|
316
|
+
|
|
317
|
+
def arg_for(self, target, i, a):
|
|
318
|
+
if is_nat(target.params[i][1]):
|
|
319
|
+
return f"({a}).toNat"
|
|
320
|
+
return a
|
|
321
|
+
|
|
322
|
+
# -- statements --------------------------------------------------------
|
|
323
|
+
|
|
324
|
+
def body(self, stmts, env, fn, indent, declared):
|
|
325
|
+
out = []
|
|
326
|
+
pad = " " * indent
|
|
327
|
+
for s in stmts:
|
|
328
|
+
if isinstance(s, ast.Return):
|
|
329
|
+
if s.value is None:
|
|
330
|
+
raise TranslateError("a function must return a value", fn.fn, s)
|
|
331
|
+
v, _ = self.expr(s.value, env, fn)
|
|
332
|
+
out.append(f"{pad}return {v}")
|
|
333
|
+
elif isinstance(s, (ast.Assign, ast.AnnAssign, ast.AugAssign)):
|
|
334
|
+
out.extend(self.assign(s, env, fn, pad, declared))
|
|
335
|
+
elif isinstance(s, ast.If):
|
|
336
|
+
c, _ = self.expr(s.test, env, fn)
|
|
337
|
+
out.append(f"{pad}if {c} then")
|
|
338
|
+
out.extend(self.body(s.body, env, fn, indent + 1, declared) or [f"{pad} pure ()"])
|
|
339
|
+
if s.orelse:
|
|
340
|
+
out.append(f"{pad}else")
|
|
341
|
+
out.extend(self.body(s.orelse, env, fn, indent + 1, declared))
|
|
342
|
+
elif isinstance(s, ast.For):
|
|
343
|
+
if not isinstance(s.target, ast.Name):
|
|
344
|
+
raise TranslateError("only `for x in ...` is supported", fn.fn, s)
|
|
345
|
+
src, t = self.iterable(s.iter, env, fn)
|
|
346
|
+
inner = dict(env)
|
|
347
|
+
inner[s.target.id] = t
|
|
348
|
+
out.append(f"{pad}for {lean_ident(s.target.id)} in {src} do")
|
|
349
|
+
out.extend(self.body(s.body, inner, fn, indent + 1, declared) or [f"{pad} pure ()"])
|
|
350
|
+
elif isinstance(s, ast.While):
|
|
351
|
+
c, _ = self.expr(s.test, env, fn)
|
|
352
|
+
out.append(f"{pad}while {c} do")
|
|
353
|
+
out.extend(self.body(s.body, env, fn, indent + 1, declared) or [f"{pad} pure ()"])
|
|
354
|
+
elif isinstance(s, ast.Break):
|
|
355
|
+
out.append(f"{pad}break")
|
|
356
|
+
elif isinstance(s, ast.Continue):
|
|
357
|
+
out.append(f"{pad}continue")
|
|
358
|
+
elif isinstance(s, ast.Pass):
|
|
359
|
+
out.append(f"{pad}pure ()")
|
|
360
|
+
elif isinstance(s, ast.Assert):
|
|
361
|
+
c, _ = self.expr(s.test, env, fn)
|
|
362
|
+
out.append(f"{pad}Py.assert {c}")
|
|
363
|
+
elif isinstance(s, ast.Raise):
|
|
364
|
+
msg = '""'
|
|
365
|
+
if isinstance(s.exc, ast.Call) and s.exc.args and isinstance(s.exc.args[0], ast.Constant):
|
|
366
|
+
msg = json_str(str(s.exc.args[0].value))
|
|
367
|
+
out.append(f"{pad}throw (.valueError {msg})")
|
|
368
|
+
elif (
|
|
369
|
+
isinstance(s, ast.Expr)
|
|
370
|
+
and isinstance(s.value, ast.Call)
|
|
371
|
+
and isinstance(s.value.func, ast.Attribute)
|
|
372
|
+
and s.value.func.attr == "append"
|
|
373
|
+
):
|
|
374
|
+
target = s.value.func.value
|
|
375
|
+
if not (isinstance(target, ast.Name) and target.id in declared):
|
|
376
|
+
raise TranslateError("append only to a list created in the function", fn.fn, s)
|
|
377
|
+
v, _ = self.expr(s.value.args[0], env, fn)
|
|
378
|
+
out.append(f"{pad}{lean_ident(target.id)} := {lean_ident(target.id)} ++ [{v}]")
|
|
379
|
+
elif isinstance(s, ast.Expr) and isinstance(s.value, ast.Constant) and isinstance(s.value.value, str):
|
|
380
|
+
continue
|
|
381
|
+
else:
|
|
382
|
+
raise TranslateError(f"unsupported statement {type(s).__name__}", fn.fn, s)
|
|
383
|
+
return out
|
|
384
|
+
|
|
385
|
+
def assign(self, s, env, fn, pad, declared):
|
|
386
|
+
if isinstance(s, ast.AugAssign):
|
|
387
|
+
target = s.target
|
|
388
|
+
op = {ast.Add: "+", ast.Sub: "-", ast.Mult: "*"}.get(type(s.op))
|
|
389
|
+
if not isinstance(target, ast.Name) or target.id not in declared:
|
|
390
|
+
raise TranslateError("augmented assignment to an undefined local", fn.fn, s)
|
|
391
|
+
v, tv = self.expr(s.value, env, fn)
|
|
392
|
+
t = env[target.id]
|
|
393
|
+
if op == "+" and (t == STR or (isinstance(t, tuple) and t[0] == "list")):
|
|
394
|
+
op = "++"
|
|
395
|
+
if op is None:
|
|
396
|
+
if isinstance(s.op, ast.FloorDiv):
|
|
397
|
+
return [f"{pad}{lean_ident(target.id)} := (← Py.floordiv {lean_ident(target.id)} {v})"]
|
|
398
|
+
if isinstance(s.op, ast.Mod):
|
|
399
|
+
return [f"{pad}{lean_ident(target.id)} := (← Py.mod {lean_ident(target.id)} {v})"]
|
|
400
|
+
raise TranslateError("unsupported augmented assignment", fn.fn, s)
|
|
401
|
+
return [f"{pad}{lean_ident(target.id)} := {lean_ident(target.id)} {op} {v}"]
|
|
402
|
+
targets = [s.target] if isinstance(s, ast.AnnAssign) else s.targets
|
|
403
|
+
if len(targets) != 1:
|
|
404
|
+
raise TranslateError("chained assignment is not supported", fn.fn, s)
|
|
405
|
+
target = targets[0]
|
|
406
|
+
v, tv = self.expr(s.value, env, fn)
|
|
407
|
+
if isinstance(s, ast.AnnAssign):
|
|
408
|
+
tv = py_type(eval(compile(ast.Expression(s.annotation), "<annotation>", "eval"), fn.fn.__globals__), fn.fn)
|
|
409
|
+
names = [target] if isinstance(target, ast.Name) else (target.elts if isinstance(target, ast.Tuple) else None)
|
|
410
|
+
if names is None or not all(isinstance(n, ast.Name) for n in names):
|
|
411
|
+
raise TranslateError("only assignment to names is supported", fn.fn, s)
|
|
412
|
+
if isinstance(target, ast.Tuple):
|
|
413
|
+
types = tv[1] if isinstance(tv, tuple) and tv[0] == "tuple" else [None] * len(names)
|
|
414
|
+
out = []
|
|
415
|
+
tmp = "__t"
|
|
416
|
+
out.append(f"{pad}let {tmp} := {v}")
|
|
417
|
+
for i, (n, t) in enumerate(zip(names, types, strict=True)):
|
|
418
|
+
proj = tmp + ".2" * i + (".1" if i < len(names) - 1 else "")
|
|
419
|
+
out.extend(self._bind(n.id, proj, t, env, pad, declared))
|
|
420
|
+
return out
|
|
421
|
+
return self._bind(target.id, v, tv, env, pad, declared)
|
|
422
|
+
|
|
423
|
+
def _bind(self, name, v, t, env, pad, declared):
|
|
424
|
+
if name in declared:
|
|
425
|
+
return [f"{pad}{lean_ident(name)} := {v}"]
|
|
426
|
+
declared.add(name)
|
|
427
|
+
env[name] = t
|
|
428
|
+
ty = f" : {lean_type(t)}" if t is not None and not (isinstance(t, tuple) and t[1] is None) else ""
|
|
429
|
+
return [f"{pad}let mut {lean_ident(name)}{ty} := {v}"]
|
|
430
|
+
|
|
431
|
+
# -- definitions -------------------------------------------------------
|
|
432
|
+
|
|
433
|
+
def definition(self, fn):
|
|
434
|
+
env = {}
|
|
435
|
+
binders = []
|
|
436
|
+
prelude = []
|
|
437
|
+
for name, ann in fn.params:
|
|
438
|
+
t = py_type(ann, fn.fn)
|
|
439
|
+
env[name] = t
|
|
440
|
+
if is_nat(ann):
|
|
441
|
+
binders.append(f"({lean_ident(name)} : Nat)")
|
|
442
|
+
prelude.append(f" let {lean_ident(name)} : Int := {lean_ident(name)}")
|
|
443
|
+
else:
|
|
444
|
+
binders.append(f"({lean_ident(name)} : {lean_type(t)})")
|
|
445
|
+
declared = set()
|
|
446
|
+
ret = lean_type(fn.ret)
|
|
447
|
+
rty = f"Py.M {ret}" if fn.fails else ret
|
|
448
|
+
head = "do" if fn.fails else "Id.run do"
|
|
449
|
+
lines = self.body(fn.node.body, env, fn, 1, declared)
|
|
450
|
+
doc = f"/-- `{fn.fn.__module__}.{fn.fn.__qualname__}`, translated from Python. -/"
|
|
451
|
+
return "\n".join([doc, f"def {fn.lean_name} {' '.join(binders)} : {rty} := {head}", *prelude, *lines])
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def lean_ident(name):
|
|
455
|
+
return models.ident(name)
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def json_str(s):
|
|
459
|
+
out = '"'
|
|
460
|
+
for c in s:
|
|
461
|
+
if c == '"':
|
|
462
|
+
out += '\\"'
|
|
463
|
+
elif c == "\\":
|
|
464
|
+
out += "\\\\"
|
|
465
|
+
elif c == "\n":
|
|
466
|
+
out += "\\n"
|
|
467
|
+
else:
|
|
468
|
+
out += c
|
|
469
|
+
return out + '"'
|
lean_python/verify.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""``@model``: bind a Python function to Lean specifications.
|
|
2
|
+
|
|
3
|
+
``@model(refines=spec.f)`` says the function computes ``spec.f`` (a function of a ``lean(...)``
|
|
4
|
+
block); ``requires=`` and ``ensures=`` name ``Bool`` functions of blocks (the precondition takes
|
|
5
|
+
the arguments, the postcondition the arguments and the result). ``lean-py check`` translates the
|
|
6
|
+
function into Lean and reports whether the obligations are proved; ``check_model`` runs the same
|
|
7
|
+
specifications as a property test with Hypothesis. The decorator returns the function unchanged.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import inspect
|
|
11
|
+
import typing
|
|
12
|
+
|
|
13
|
+
REGISTRY = []
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ModelSpec:
|
|
17
|
+
def __init__(self, fn, refines, requires, ensures):
|
|
18
|
+
self.fn = fn
|
|
19
|
+
self.refines = refines
|
|
20
|
+
self.requires = requires
|
|
21
|
+
if ensures is None:
|
|
22
|
+
self.ensures = {}
|
|
23
|
+
elif isinstance(ensures, dict):
|
|
24
|
+
self.ensures = dict(ensures)
|
|
25
|
+
else:
|
|
26
|
+
self.ensures = {"ensures": ensures}
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def qualname(self):
|
|
30
|
+
return f"{self.fn.__module__}:{self.fn.__qualname__}"
|
|
31
|
+
|
|
32
|
+
def specs(self):
|
|
33
|
+
out = {}
|
|
34
|
+
if self.refines is not None:
|
|
35
|
+
out["refines"] = self.refines
|
|
36
|
+
if self.requires is not None:
|
|
37
|
+
out["requires"] = self.requires
|
|
38
|
+
out.update(self.ensures)
|
|
39
|
+
return out
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def model(*, refines=None, requires=None, ensures=None):
|
|
43
|
+
"""Binds the decorated function to Lean specifications (see the module docstring)."""
|
|
44
|
+
|
|
45
|
+
def deco(fn):
|
|
46
|
+
spec = ModelSpec(fn, refines, requires, ensures)
|
|
47
|
+
fn.__lean_model__ = spec
|
|
48
|
+
REGISTRY.append(spec)
|
|
49
|
+
return fn
|
|
50
|
+
|
|
51
|
+
return deco
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def models_of(module):
|
|
55
|
+
return [v.__lean_model__ for v in vars(module).values() if callable(v) and hasattr(v, "__lean_model__")]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
# Property tests
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def strategy(annotation, metadata=()):
|
|
63
|
+
"""A Hypothesis strategy of the values of a parameter or field type."""
|
|
64
|
+
import annotated_types as at
|
|
65
|
+
from hypothesis import strategies as st
|
|
66
|
+
from pydantic import BaseModel
|
|
67
|
+
|
|
68
|
+
metadata = list(metadata)
|
|
69
|
+
while typing.get_origin(annotation) is typing.Annotated:
|
|
70
|
+
annotation, *more = typing.get_args(annotation)
|
|
71
|
+
metadata.extend(more)
|
|
72
|
+
if annotation is int:
|
|
73
|
+
lo = hi = None
|
|
74
|
+
for m in metadata:
|
|
75
|
+
if isinstance(m, at.Ge):
|
|
76
|
+
lo = m.ge if lo is None else max(lo, m.ge)
|
|
77
|
+
elif isinstance(m, at.Gt):
|
|
78
|
+
lo = m.gt + 1 if lo is None else max(lo, m.gt + 1)
|
|
79
|
+
elif isinstance(m, at.Le):
|
|
80
|
+
hi = m.le if hi is None else min(hi, m.le)
|
|
81
|
+
elif isinstance(m, at.Lt):
|
|
82
|
+
hi = m.lt - 1 if hi is None else min(hi, m.lt - 1)
|
|
83
|
+
return st.integers(min_value=lo if lo is not None else -1000, max_value=hi if hi is not None else 1000)
|
|
84
|
+
if annotation is bool:
|
|
85
|
+
return st.booleans()
|
|
86
|
+
if annotation is str:
|
|
87
|
+
lo = max([m.min_length for m in metadata if isinstance(m, at.MinLen)], default=0)
|
|
88
|
+
hi = min([m.max_length for m in metadata if isinstance(m, at.MaxLen)], default=lo + 8)
|
|
89
|
+
return st.text(min_size=lo, max_size=hi)
|
|
90
|
+
if annotation is float:
|
|
91
|
+
return st.floats(allow_nan=False, allow_infinity=False, width=64)
|
|
92
|
+
origin = typing.get_origin(annotation)
|
|
93
|
+
if origin is list:
|
|
94
|
+
(a,) = typing.get_args(annotation)
|
|
95
|
+
return st.lists(strategy(a), max_size=8)
|
|
96
|
+
if origin is tuple:
|
|
97
|
+
return st.tuples(*(strategy(a) for a in typing.get_args(annotation)))
|
|
98
|
+
args = typing.get_args(annotation)
|
|
99
|
+
if args and type(None) in args:
|
|
100
|
+
rest = [a for a in args if a is not type(None)]
|
|
101
|
+
return st.none() | st.one_of(*(strategy(a) for a in rest))
|
|
102
|
+
if isinstance(annotation, type) and issubclass(annotation, BaseModel):
|
|
103
|
+
fields = {n: strategy(f.annotation, f.metadata) for n, f in annotation.model_fields.items()}
|
|
104
|
+
return st.builds(annotation, **fields)
|
|
105
|
+
raise TypeError(f"no strategy for {annotation!r}")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class PropertyFailure(AssertionError):
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def check_model(fn, max_examples=200):
|
|
113
|
+
"""Runs the function against its specifications on generated arguments; raises on a counterexample."""
|
|
114
|
+
from hypothesis import given, settings
|
|
115
|
+
from hypothesis import strategies as st
|
|
116
|
+
|
|
117
|
+
spec = fn.__lean_model__
|
|
118
|
+
hints = typing.get_type_hints(fn, include_extras=True)
|
|
119
|
+
params = list(inspect.signature(fn).parameters)
|
|
120
|
+
strategies = st.tuples(*(strategy(hints[p]) for p in params))
|
|
121
|
+
|
|
122
|
+
@settings(max_examples=max_examples, deadline=None, database=None)
|
|
123
|
+
@given(strategies)
|
|
124
|
+
def run(args):
|
|
125
|
+
from hypothesis import assume
|
|
126
|
+
|
|
127
|
+
if spec.requires is not None:
|
|
128
|
+
assume(spec.requires(*args))
|
|
129
|
+
try:
|
|
130
|
+
result = fn(*args)
|
|
131
|
+
except Exception as e: # noqa: BLE001
|
|
132
|
+
raise PropertyFailure(f"{fn.__qualname__}{args} raised {e!r}") from None
|
|
133
|
+
if spec.refines is not None:
|
|
134
|
+
expected = spec.refines(*args)
|
|
135
|
+
if result != expected:
|
|
136
|
+
raise PropertyFailure(f"{fn.__qualname__}{args} = {result!r}, the spec gives {expected!r}")
|
|
137
|
+
for label, e in spec.ensures.items():
|
|
138
|
+
if not e(*args, result):
|
|
139
|
+
raise PropertyFailure(f"{fn.__qualname__}{args} = {result!r} breaks {label}")
|
|
140
|
+
|
|
141
|
+
run()
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def check_module(module, max_examples=200):
|
|
145
|
+
"""``check_model`` for every ``@model`` function of the module."""
|
|
146
|
+
for spec in models_of(module):
|
|
147
|
+
check_model(spec.fn, max_examples)
|
|
@@ -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
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
lean_python/__init__.py,sha256=w09gaS4QGgFS0pM8Y3aSVMfQm-iHgEG_Sm_RqwKh--I,182
|
|
2
|
+
lean_python/check.py,sha256=uQkstmWq7mRSPxPW6Ac4yCCFqGacAZFBiCx19Y_XT6k,11129
|
|
3
|
+
lean_python/cli.py,sha256=r68dhTW7X1jUNx1GrJEsuXpx-kU9VGhCsbV0Xy5Ih9w,2575
|
|
4
|
+
lean_python/embed.py,sha256=4gKW2XM6EGJkJOQ8ghpnOAb_k_8GlJWscrAZVaVX2a8,3996
|
|
5
|
+
lean_python/io.py,sha256=l7LErXI_DhACZf9Jaef3r5Stqh4PdUk-bCNTo91BLE0,356
|
|
6
|
+
lean_python/io_externs.py,sha256=lI7oK7WTMjYV1JxaNzEnscVMunME_5SYoGDz3Qq1GSM,29807
|
|
7
|
+
lean_python/marshal.py,sha256=LBUL3bC9SoQFow6Kbj3kMyoCHfDvmQslrJnJW3SkqvQ,20877
|
|
8
|
+
lean_python/models.py,sha256=wunIZ9hOG-8H3shdRClLN7pR6Kmofo9zlPcL3Hr0rq0,10940
|
|
9
|
+
lean_python/runtime.py,sha256=Fr3R6x-kZUW2H1_NwbwI_LI97OUmMQ8Q0smnPKyyAAc,28439
|
|
10
|
+
lean_python/sync.py,sha256=JKJj3IWQlrqjTvK_-puPuF1Nt0LMumywUilUJANqhfc,9304
|
|
11
|
+
lean_python/translate.py,sha256=9K08bKrWt8yCYwdH8LutuuwMX9kLRqIGhUaI5BbKzYI,20596
|
|
12
|
+
lean_python/types.py,sha256=qOhcwe1lYGD17OHW_A4LzfGi2bcPovTANTggaJwG-FM,843
|
|
13
|
+
lean_python/verify.py,sha256=KmIdkIzNRF3AHvtdyw-XIGcnws2RhwObBF_AGycxDXY,5525
|
|
14
|
+
lean_python/lean/LeanPythonLib/Py.lean,sha256=NCx9mxRNnI7_wsK_FEh2eqkp6_EO9buXZrnZNiDrJmk,1658
|
|
15
|
+
lean_python-0.2.0.dist-info/METADATA,sha256=ghnqqVdM8sT4bk6cy0s8iKolBm39pDjdYjmfBJBCRQs,13387
|
|
16
|
+
lean_python-0.2.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
17
|
+
lean_python-0.2.0.dist-info/entry_points.txt,sha256=BD86jsJKikYcvsZse4ysbLvvN7TuGogwmiUZZwu7FBo,49
|
|
18
|
+
lean_python-0.2.0.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
|
19
|
+
lean_python-0.2.0.dist-info/RECORD,,
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
lean_python/__init__.py,sha256=w09gaS4QGgFS0pM8Y3aSVMfQm-iHgEG_Sm_RqwKh--I,182
|
|
2
|
-
lean_python/cli.py,sha256=ERwAkhhFJV32BqS3-lDHPYi0UiCbydyEFI0ZlHobmsk,1559
|
|
3
|
-
lean_python/embed.py,sha256=WBEWYN7r3xFbSNLgvRRfn-6qhcR5Q1h1ge8KwQ5yyfE,3996
|
|
4
|
-
lean_python/io.py,sha256=l7LErXI_DhACZf9Jaef3r5Stqh4PdUk-bCNTo91BLE0,356
|
|
5
|
-
lean_python/io_externs.py,sha256=lI7oK7WTMjYV1JxaNzEnscVMunME_5SYoGDz3Qq1GSM,29807
|
|
6
|
-
lean_python/marshal.py,sha256=WIY3yJtnIMvym8_jraY8F1tW8x1iYqRkcS4W_FGWwLM,20778
|
|
7
|
-
lean_python/models.py,sha256=wunIZ9hOG-8H3shdRClLN7pR6Kmofo9zlPcL3Hr0rq0,10940
|
|
8
|
-
lean_python/runtime.py,sha256=Fr3R6x-kZUW2H1_NwbwI_LI97OUmMQ8Q0smnPKyyAAc,28439
|
|
9
|
-
lean_python/sync.py,sha256=JKJj3IWQlrqjTvK_-puPuF1Nt0LMumywUilUJANqhfc,9304
|
|
10
|
-
lean_python/types.py,sha256=qOhcwe1lYGD17OHW_A4LzfGi2bcPovTANTggaJwG-FM,843
|
|
11
|
-
lean_python-0.1.0.dist-info/METADATA,sha256=7IosM2WL2AjIjMM0CYy015gHPmkViqxr_vT075QI1gw,9775
|
|
12
|
-
lean_python-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
13
|
-
lean_python-0.1.0.dist-info/entry_points.txt,sha256=BD86jsJKikYcvsZse4ysbLvvN7TuGogwmiUZZwu7FBo,49
|
|
14
|
-
lean_python-0.1.0.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
|
15
|
-
lean_python-0.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|