lean-interact 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
|
|
3
|
+
from lean_interact.server import AutoLeanServer, LeanREPLConfig, LeanRequire, LeanServer
|
|
4
|
+
from lean_interact.utils import clear_cache
|
|
5
|
+
|
|
6
|
+
__all__ = ["LeanREPLConfig", "LeanServer", "AutoLeanServer", "LeanRequire", "clear_cache"]
|
|
7
|
+
|
|
8
|
+
# check if lake is installed
|
|
9
|
+
if shutil.which("lake") is None:
|
|
10
|
+
raise RuntimeError(
|
|
11
|
+
"Lean 4 build system (`lake`) is not installed. You can find installation instructions here: https://leanprover-community.github.io/get_started.html"
|
|
12
|
+
)
|
lean_interact/server.py
ADDED
|
@@ -0,0 +1,842 @@
|
|
|
1
|
+
import gc
|
|
2
|
+
import hashlib
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
from copy import deepcopy
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from time import sleep
|
|
10
|
+
from typing import Literal
|
|
11
|
+
|
|
12
|
+
import pexpect
|
|
13
|
+
import psutil
|
|
14
|
+
|
|
15
|
+
from lean_interact.utils import (
|
|
16
|
+
DEFAULT_CACHE_DIR,
|
|
17
|
+
DEFAULT_REPL_GIT_URL,
|
|
18
|
+
_limit_memory,
|
|
19
|
+
fetch_lean_versions,
|
|
20
|
+
get_project_lean_version,
|
|
21
|
+
logger,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
DEFAULT_TIMEOUT: int | None = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class LeanRequire:
|
|
29
|
+
name: str
|
|
30
|
+
git: str
|
|
31
|
+
rev: str | None = None
|
|
32
|
+
|
|
33
|
+
def __hash__(self):
|
|
34
|
+
return hash((self.name, self.git, self.rev))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class LeanREPLConfig:
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
lean_version: str | None = None,
|
|
41
|
+
project_dir: str | None = None,
|
|
42
|
+
require: Literal["mathlib"] | list[LeanRequire] | None = None,
|
|
43
|
+
repl_rev: str | None = None,
|
|
44
|
+
repl_git: str = DEFAULT_REPL_GIT_URL,
|
|
45
|
+
cache_dir: str = DEFAULT_CACHE_DIR,
|
|
46
|
+
max_memory: int = 32 * 1024,
|
|
47
|
+
verbose: bool = True,
|
|
48
|
+
):
|
|
49
|
+
"""
|
|
50
|
+
Configuration class used to specify the Lean environment we wish to use.
|
|
51
|
+
|
|
52
|
+
\u26a0 Multiprocessing: instantiate one `LeanServerConfig` in the main process before starting multiprocessing.
|
|
53
|
+
This will ensure that the REPL is correctly set up and will avoid potential concurrency issues.
|
|
54
|
+
You can then instantiate one `LeanServer` per process by passing the config as an argument to the constructor.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
lean_version:
|
|
58
|
+
Lean version you want to use (listed in the `versions` folder of the REPL git repo).
|
|
59
|
+
If set to `None`, the latest version compatible with the Lean REPL will be used.
|
|
60
|
+
project_dir:
|
|
61
|
+
A directory where a local Lean project is stored. We recommend not setting `lean_version` parameter when
|
|
62
|
+
using `project_dir` as it the lean version can be automatically inferred from the project.
|
|
63
|
+
This option cannot be used simultaneously with `require`.
|
|
64
|
+
require:
|
|
65
|
+
Define dependencies to use if no `project_dir` is specified. This will automatically set up a
|
|
66
|
+
temporary Lean project with the required dependencies. A list of `LeanRequire` objects can be provided.
|
|
67
|
+
If `None`, no dependencies will be required.
|
|
68
|
+
As Mathlib is a common dependency, you can just set `require="mathlib"` and a compatible version of mathlib
|
|
69
|
+
will be used.
|
|
70
|
+
This feature has been developed mostly to be able to run benchmarks using Mathlib as a dependency
|
|
71
|
+
(such as [ProofNet](https://github.com/zhangir-azerbayev/ProofNet) or
|
|
72
|
+
[MiniF2F](https://github.com/yangky11/miniF2F-lean4)) without having to manually set up a Lean project.
|
|
73
|
+
repl_rev:
|
|
74
|
+
The REPL version you want to use (valid versions correspond to tags, commit hashes, or branches in the repo).
|
|
75
|
+
While we recommend to always specify a version, if `repl_rev` is set to `None`, the latest tagged local version will be used.
|
|
76
|
+
If the REPL has not been cloned yet, the latest version will be cloned.
|
|
77
|
+
If you want to force the update of the local version, just set `repl_rev` to the latest version and it will pull the latest changes.
|
|
78
|
+
This default behavior aims to avoid unexpected updates to the local version and to minimize calls to the git repository.
|
|
79
|
+
repl_git:
|
|
80
|
+
The git repository of the Lean REPL. It is not recommended to change this value unless you know what you are doing.
|
|
81
|
+
cache_dir:
|
|
82
|
+
The directory where the Lean REPL and temporary Lean projects with dependencies will be cached.
|
|
83
|
+
Default is inside the package directory.
|
|
84
|
+
max_memory:
|
|
85
|
+
The maximum memory usage in MB for the Lean server. Setting this value too low may lead to timeouts.
|
|
86
|
+
Default is 32GB.
|
|
87
|
+
"""
|
|
88
|
+
self.lean_version = lean_version
|
|
89
|
+
self.require = require
|
|
90
|
+
self.project_dir = project_dir
|
|
91
|
+
self.repl_git = repl_git
|
|
92
|
+
self.repl_rev = repl_rev
|
|
93
|
+
self.cache_dir = cache_dir
|
|
94
|
+
self.max_memory = max_memory
|
|
95
|
+
self.verbose = verbose
|
|
96
|
+
|
|
97
|
+
if self.require and self.project_dir:
|
|
98
|
+
raise ValueError("`require` and `project_dir` can not be set simultaneously.")
|
|
99
|
+
|
|
100
|
+
self._setup_repl()
|
|
101
|
+
self._setup_environment()
|
|
102
|
+
|
|
103
|
+
def _setup_repl(self) -> None:
|
|
104
|
+
# check if the repl is cloned
|
|
105
|
+
repo_name = "/".join(self.repl_git.split("/")[-2:]).replace(".git", "")
|
|
106
|
+
cache_clean_repl_dir = os.path.join(self.cache_dir, repo_name, "repl_clean_copy")
|
|
107
|
+
stdout_setting = None if self.verbose else subprocess.DEVNULL
|
|
108
|
+
stderr_setting = None if self.verbose else subprocess.DEVNULL
|
|
109
|
+
if not os.path.exists(cache_clean_repl_dir):
|
|
110
|
+
os.makedirs(cache_clean_repl_dir, exist_ok=True)
|
|
111
|
+
subprocess.run(
|
|
112
|
+
["git", "clone", self.repl_git, "."],
|
|
113
|
+
cwd=cache_clean_repl_dir,
|
|
114
|
+
check=True,
|
|
115
|
+
stdout=stdout_setting,
|
|
116
|
+
stderr=stderr_setting,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
if self.repl_rev is None:
|
|
120
|
+
# get the latest tagged local revision
|
|
121
|
+
result = subprocess.run(
|
|
122
|
+
["git", "describe", "--tags", "--abbrev=0"],
|
|
123
|
+
cwd=cache_clean_repl_dir,
|
|
124
|
+
capture_output=True,
|
|
125
|
+
text=True,
|
|
126
|
+
check=True,
|
|
127
|
+
stdout=stdout_setting,
|
|
128
|
+
stderr=stderr_setting,
|
|
129
|
+
)
|
|
130
|
+
self.repl_rev = result.stdout.strip()
|
|
131
|
+
|
|
132
|
+
# check if the repl revision (tag, commit, or branch) is available
|
|
133
|
+
try:
|
|
134
|
+
# checkouts the required revision
|
|
135
|
+
subprocess.run(
|
|
136
|
+
["git", "checkout", self.repl_rev],
|
|
137
|
+
cwd=cache_clean_repl_dir,
|
|
138
|
+
check=True,
|
|
139
|
+
stdout=stdout_setting,
|
|
140
|
+
stderr=stderr_setting,
|
|
141
|
+
)
|
|
142
|
+
except subprocess.CalledProcessError:
|
|
143
|
+
# let's try to pull the latest changes
|
|
144
|
+
subprocess.run(
|
|
145
|
+
["git", "pull"],
|
|
146
|
+
cwd=cache_clean_repl_dir,
|
|
147
|
+
check=True,
|
|
148
|
+
stdout=stdout_setting,
|
|
149
|
+
stderr=stderr_setting,
|
|
150
|
+
)
|
|
151
|
+
try:
|
|
152
|
+
# checkouts the required revision
|
|
153
|
+
subprocess.run(
|
|
154
|
+
["git", "checkout", self.repl_rev],
|
|
155
|
+
cwd=cache_clean_repl_dir,
|
|
156
|
+
check=True,
|
|
157
|
+
stdout=stdout_setting,
|
|
158
|
+
stderr=stderr_setting,
|
|
159
|
+
)
|
|
160
|
+
except subprocess.CalledProcessError:
|
|
161
|
+
raise ValueError(f"Revision `{self.repl_rev}` is not available in the Lean REPL repository.")
|
|
162
|
+
|
|
163
|
+
# check if the Lean version is available in the repository
|
|
164
|
+
lean_versions = fetch_lean_versions(cache_clean_repl_dir)
|
|
165
|
+
if not lean_versions:
|
|
166
|
+
raise ValueError("No Lean versions are available in the Lean REPL repository.")
|
|
167
|
+
if self.lean_version is None:
|
|
168
|
+
if self.project_dir is not None:
|
|
169
|
+
inferred_ver = get_project_lean_version(self.project_dir)
|
|
170
|
+
self.lean_version = inferred_ver if inferred_ver else lean_versions[-1]
|
|
171
|
+
else:
|
|
172
|
+
self.lean_version = lean_versions[-1]
|
|
173
|
+
if self.lean_version not in lean_versions:
|
|
174
|
+
raise ValueError(
|
|
175
|
+
f"Lean version `{self.lean_version}` is required but not available in the Lean REPL repository."
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# check if the repl revision is already in the cache
|
|
179
|
+
self._cache_repl_dir = os.path.join(self.cache_dir, repo_name, f"repl_{self.repl_rev}_{self.lean_version}")
|
|
180
|
+
if not os.path.exists(self._cache_repl_dir):
|
|
181
|
+
# copy the repository to the version directory and checkout the required revision
|
|
182
|
+
os.makedirs(self._cache_repl_dir, exist_ok=True)
|
|
183
|
+
shutil.copytree(cache_clean_repl_dir, self._cache_repl_dir, dirs_exist_ok=True)
|
|
184
|
+
subprocess.run(
|
|
185
|
+
["git", "checkout", self.repl_rev],
|
|
186
|
+
cwd=self._cache_repl_dir,
|
|
187
|
+
check=True,
|
|
188
|
+
stdout=stdout_setting,
|
|
189
|
+
stderr=stderr_setting,
|
|
190
|
+
)
|
|
191
|
+
# apply the diff for the required version
|
|
192
|
+
diff_file = os.path.join(self._cache_repl_dir, "versions", f"{self.lean_version}.diff")
|
|
193
|
+
# check if the file is not empty
|
|
194
|
+
if os.path.getsize(diff_file) > 0:
|
|
195
|
+
subprocess.run(["git", "apply", diff_file], cwd=self._cache_repl_dir, check=True)
|
|
196
|
+
|
|
197
|
+
# build the repl
|
|
198
|
+
subprocess.run(
|
|
199
|
+
["lake", "build"],
|
|
200
|
+
cwd=self._cache_repl_dir,
|
|
201
|
+
check=True,
|
|
202
|
+
stdout=stdout_setting,
|
|
203
|
+
stderr=stderr_setting,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
def _setup_environment(self) -> None:
|
|
207
|
+
assert isinstance(self.lean_version, str)
|
|
208
|
+
|
|
209
|
+
stdout_setting = None if self.verbose else subprocess.DEVNULL
|
|
210
|
+
stderr_setting = None if self.verbose else subprocess.DEVNULL
|
|
211
|
+
|
|
212
|
+
# require Lean libraries
|
|
213
|
+
if self.require:
|
|
214
|
+
if self.require == "mathlib":
|
|
215
|
+
self.require = [
|
|
216
|
+
LeanRequire("mathlib", "https://github.com/leanprover-community/mathlib4.git", self.lean_version)
|
|
217
|
+
]
|
|
218
|
+
assert isinstance(self.require, list)
|
|
219
|
+
self.require = sorted(self.require, key=lambda x: x.name)
|
|
220
|
+
|
|
221
|
+
# create a unique hash to allow for caching
|
|
222
|
+
require_hash = hashlib.sha256(str(self.require).encode()).hexdigest()
|
|
223
|
+
tmp_project_dir = os.path.join(self.cache_dir, "tmp_projects", self.lean_version, require_hash)
|
|
224
|
+
os.makedirs(tmp_project_dir, exist_ok=True)
|
|
225
|
+
|
|
226
|
+
# check if the Lean project is already built
|
|
227
|
+
if not os.path.exists(os.path.join(tmp_project_dir, "lakefile.lean")):
|
|
228
|
+
# clean the content of the folder in case of a previous failed build
|
|
229
|
+
shutil.rmtree(tmp_project_dir, ignore_errors=True)
|
|
230
|
+
os.makedirs(tmp_project_dir, exist_ok=True)
|
|
231
|
+
|
|
232
|
+
# initialize the Lean project
|
|
233
|
+
cmd_init = ["lake", f"+{self.lean_version}", "init", "dummy", "exe.lean"]
|
|
234
|
+
if self.lean_version.startswith("v4") and int(self.lean_version.split(".")[1]) <= 7:
|
|
235
|
+
cmd_init = ["lake", f"+{self.lean_version}", "init", "dummy", "exe"]
|
|
236
|
+
subprocess.run(cmd_init, cwd=tmp_project_dir, check=True)
|
|
237
|
+
|
|
238
|
+
with open(os.path.join(tmp_project_dir, "lakefile.lean"), "a") as f:
|
|
239
|
+
for req in self.require:
|
|
240
|
+
f.write(
|
|
241
|
+
f'\n\nrequire {req.name} from git\n "{req.git}"' + (f' @ "{req.rev}"' if req.rev else "")
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
logger.info("Preparing Lean environment with dependencies (may take a while the first time)...")
|
|
245
|
+
subprocess.run(
|
|
246
|
+
["lake", "update"],
|
|
247
|
+
cwd=tmp_project_dir,
|
|
248
|
+
check=True,
|
|
249
|
+
stdout=stdout_setting,
|
|
250
|
+
stderr=stderr_setting,
|
|
251
|
+
)
|
|
252
|
+
subprocess.run(
|
|
253
|
+
["lake", "exe", "cache", "get"],
|
|
254
|
+
cwd=tmp_project_dir,
|
|
255
|
+
stdout=stdout_setting,
|
|
256
|
+
stderr=stderr_setting,
|
|
257
|
+
)
|
|
258
|
+
subprocess.run(
|
|
259
|
+
["lake", "build"],
|
|
260
|
+
cwd=tmp_project_dir,
|
|
261
|
+
check=True,
|
|
262
|
+
stdout=stdout_setting,
|
|
263
|
+
stderr=stderr_setting,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
self._working_dir = tmp_project_dir
|
|
267
|
+
|
|
268
|
+
elif self.project_dir:
|
|
269
|
+
# check that the project is built
|
|
270
|
+
subprocess.run(
|
|
271
|
+
["lake", "build"],
|
|
272
|
+
cwd=self.project_dir,
|
|
273
|
+
check=True,
|
|
274
|
+
stdout=stdout_setting,
|
|
275
|
+
stderr=stderr_setting,
|
|
276
|
+
)
|
|
277
|
+
self._working_dir = self.project_dir
|
|
278
|
+
else:
|
|
279
|
+
self._working_dir = self._cache_repl_dir
|
|
280
|
+
|
|
281
|
+
def get_available_lean_versions(self) -> list[str]:
|
|
282
|
+
"""
|
|
283
|
+
Get the available Lean versions for the selected REPL.
|
|
284
|
+
"""
|
|
285
|
+
return fetch_lean_versions(self._cache_repl_dir)
|
|
286
|
+
|
|
287
|
+
def is_setup(self) -> bool:
|
|
288
|
+
return hasattr(self, "_working_dir")
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
class LeanServer:
|
|
292
|
+
config: LeanREPLConfig
|
|
293
|
+
_proc: pexpect.spawn | None
|
|
294
|
+
|
|
295
|
+
def __init__(self, config: LeanREPLConfig):
|
|
296
|
+
"""
|
|
297
|
+
This class is a Python wrapper for the Lean REPL. Please refer to the \
|
|
298
|
+
[Lean REPL documentation](https://github.com/leanprover-community/repl) to learn more about the Lean REPL commands.
|
|
299
|
+
|
|
300
|
+
\u26a0 Multiprocessing: instantiate one config before starting multiprocessing. Then instantiate one `LeanServer`
|
|
301
|
+
per process by passing the config instance to the constructor. This will ensure that the REPL is already set up
|
|
302
|
+
for your specific environment and avoid concurrency conflicts.
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
config: The configuration for the Lean server.
|
|
306
|
+
"""
|
|
307
|
+
self.config = config
|
|
308
|
+
assert self.config.is_setup(), "The Lean environment has not been set up properly."
|
|
309
|
+
self._proc = None
|
|
310
|
+
self.start()
|
|
311
|
+
|
|
312
|
+
@property
|
|
313
|
+
def lean_version(self) -> str | None:
|
|
314
|
+
return self.config.lean_version
|
|
315
|
+
|
|
316
|
+
def start(self) -> None:
|
|
317
|
+
self._proc = pexpect.spawn(
|
|
318
|
+
"/bin/bash",
|
|
319
|
+
cwd=self.config._working_dir,
|
|
320
|
+
encoding="utf-8",
|
|
321
|
+
codec_errors="replace",
|
|
322
|
+
echo=False,
|
|
323
|
+
preexec_fn=lambda: _limit_memory(self.config.max_memory),
|
|
324
|
+
)
|
|
325
|
+
# `stty -icanon` is required to handle arbitrary long inputs in the Lean REPL
|
|
326
|
+
self._proc.sendline("stty -icanon")
|
|
327
|
+
self._proc.sendline(f"lake env {self.config._cache_repl_dir}/.lake/build/bin/repl")
|
|
328
|
+
|
|
329
|
+
def is_alive(self) -> bool:
|
|
330
|
+
return hasattr(self, "_proc") and self._proc is not None and self._proc.isalive()
|
|
331
|
+
|
|
332
|
+
def kill(self) -> None:
|
|
333
|
+
if hasattr(self, "_proc") and self._proc:
|
|
334
|
+
self._proc.terminate(force=True)
|
|
335
|
+
del self._proc
|
|
336
|
+
gc.collect()
|
|
337
|
+
|
|
338
|
+
def restart(self) -> None:
|
|
339
|
+
self.kill()
|
|
340
|
+
self.start()
|
|
341
|
+
|
|
342
|
+
def __del__(self):
|
|
343
|
+
self.kill()
|
|
344
|
+
|
|
345
|
+
def _execute_cmd_in_repl(self, json_query: str, verbose: bool, timeout: float | None) -> str:
|
|
346
|
+
"""Send JSON queries to the Lean REPL and wait for the standard delimiter."""
|
|
347
|
+
assert self._proc is not None
|
|
348
|
+
if verbose:
|
|
349
|
+
logger.info(f"Sending query: {json_query}")
|
|
350
|
+
self._proc.sendline(json_query)
|
|
351
|
+
self._proc.sendline()
|
|
352
|
+
_ = self._proc.expect_exact("\r\n\r\n", timeout=timeout)
|
|
353
|
+
return self._proc.before or ""
|
|
354
|
+
|
|
355
|
+
def _parse_repl_output(self, raw_output: str, verbose: bool) -> dict:
|
|
356
|
+
"""Clean up raw REPL output and parse JSON response."""
|
|
357
|
+
output = raw_output.replace("\r\n", "\n")
|
|
358
|
+
output = output[output.find('{"') :] if '{"' in output else ""
|
|
359
|
+
if verbose:
|
|
360
|
+
logger.info(f"Server raw output: `{raw_output}")
|
|
361
|
+
logger.info(f"Server cleaned output: `{output}`")
|
|
362
|
+
try:
|
|
363
|
+
return json.loads(output)
|
|
364
|
+
except json.JSONDecodeError as e:
|
|
365
|
+
raise json.JSONDecodeError(
|
|
366
|
+
msg=f"Could not parse the Lean server output: `{repr(output)}`.",
|
|
367
|
+
doc=e.doc,
|
|
368
|
+
pos=e.pos,
|
|
369
|
+
) from e
|
|
370
|
+
|
|
371
|
+
def _process_request(
|
|
372
|
+
self, dict_query: dict, verbose: bool = False, timeout: float | None = DEFAULT_TIMEOUT
|
|
373
|
+
) -> dict:
|
|
374
|
+
if not self.is_alive():
|
|
375
|
+
raise ChildProcessError("The Lean server is not running.")
|
|
376
|
+
|
|
377
|
+
json_query = json.dumps(dict_query, ensure_ascii=False)
|
|
378
|
+
try:
|
|
379
|
+
raw_output = self._execute_cmd_in_repl(json_query, verbose, timeout)
|
|
380
|
+
except pexpect.exceptions.TIMEOUT as e:
|
|
381
|
+
self.kill()
|
|
382
|
+
raise TimeoutError(f"The Lean server did not respond in time ({timeout=}) and is now killed.") from e
|
|
383
|
+
except pexpect.exceptions.EOF as e:
|
|
384
|
+
self.kill()
|
|
385
|
+
raise ConnectionAbortedError(
|
|
386
|
+
"The Lean server closed unexpectedly. Possible reasons (not exhaustive):\n"
|
|
387
|
+
"- Not enough memory and/or compute available\n"
|
|
388
|
+
"- Your cached Lean REPL is corrupted. In this case, clear the cache"
|
|
389
|
+
" using the `clear_cache` (`from pyleanrepl import clear_cache`) method."
|
|
390
|
+
) from e
|
|
391
|
+
|
|
392
|
+
return self._parse_repl_output(raw_output, verbose)
|
|
393
|
+
|
|
394
|
+
def run_file(
|
|
395
|
+
self,
|
|
396
|
+
path: str,
|
|
397
|
+
verbose: bool = False,
|
|
398
|
+
timeout: float | None = DEFAULT_TIMEOUT,
|
|
399
|
+
extra_repl_args: dict | None = None,
|
|
400
|
+
) -> dict:
|
|
401
|
+
if not path:
|
|
402
|
+
raise ValueError("`path` cannot be `None` or empty")
|
|
403
|
+
if not isinstance(path, str):
|
|
404
|
+
raise ValueError("`path` must be a string")
|
|
405
|
+
return self._process_request(
|
|
406
|
+
dict_query=dict(path=path) | (extra_repl_args or {}), timeout=timeout, verbose=verbose
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
def run_code(
|
|
410
|
+
self,
|
|
411
|
+
code: str,
|
|
412
|
+
env: int | None = None,
|
|
413
|
+
verbose: bool = False,
|
|
414
|
+
timeout: float | None = DEFAULT_TIMEOUT,
|
|
415
|
+
extra_repl_args: dict | None = None,
|
|
416
|
+
) -> dict:
|
|
417
|
+
"""
|
|
418
|
+
Run a short Lean code snippet and return the Lean REPL output.
|
|
419
|
+
|
|
420
|
+
Args:
|
|
421
|
+
code: The Lean code to run.
|
|
422
|
+
env: The environment to use.
|
|
423
|
+
verbose: Whether to print additional information during the verification process.
|
|
424
|
+
timeout: The timeout for the request.
|
|
425
|
+
optimize_prefix_env_reuse: Whether to optimize the environment reuse by checking if the current\
|
|
426
|
+
code is a prefix of a cached code.
|
|
427
|
+
Used for performance optimization purposes and only if `env` parameter is `None`.
|
|
428
|
+
Returns:
|
|
429
|
+
The output of the Lean server.
|
|
430
|
+
"""
|
|
431
|
+
|
|
432
|
+
if not code:
|
|
433
|
+
raise ValueError("`code` cannot be `None` or empty")
|
|
434
|
+
|
|
435
|
+
if not isinstance(code, str):
|
|
436
|
+
raise ValueError("`code` must be a string")
|
|
437
|
+
|
|
438
|
+
command = dict(cmd=code) | (dict(env=env) if env is not None else {}) | (extra_repl_args or {})
|
|
439
|
+
return self._process_request(dict_query=command, timeout=timeout, verbose=verbose)
|
|
440
|
+
|
|
441
|
+
def run_tactic(
|
|
442
|
+
self,
|
|
443
|
+
tactic: str,
|
|
444
|
+
proof_state: int,
|
|
445
|
+
verbose: bool = False,
|
|
446
|
+
timeout: float | None = DEFAULT_TIMEOUT,
|
|
447
|
+
extra_repl_args: dict | None = None,
|
|
448
|
+
) -> dict:
|
|
449
|
+
"""
|
|
450
|
+
Run a tactic in the Lean REPL.
|
|
451
|
+
|
|
452
|
+
Args:
|
|
453
|
+
tactic: The tactic to run.
|
|
454
|
+
proof_state: The proof state to apply the tactic to.
|
|
455
|
+
verbose: Whether to print additional information during the verification process.
|
|
456
|
+
timeout: The timeout for the request.
|
|
457
|
+
Returns:
|
|
458
|
+
The output of the Lean server.
|
|
459
|
+
"""
|
|
460
|
+
if not tactic:
|
|
461
|
+
raise ValueError("`tactic` cannot be `None` or empty")
|
|
462
|
+
|
|
463
|
+
if not isinstance(tactic, str):
|
|
464
|
+
raise ValueError("`tactic` must be a string")
|
|
465
|
+
|
|
466
|
+
if proof_state is None:
|
|
467
|
+
raise ValueError("`proof_state` cannot be `None`")
|
|
468
|
+
|
|
469
|
+
command = dict(tactic=tactic, proofState=proof_state) | (extra_repl_args or {})
|
|
470
|
+
return self._process_request(dict_query=command, timeout=timeout, verbose=verbose)
|
|
471
|
+
|
|
472
|
+
def run_proof(
|
|
473
|
+
self,
|
|
474
|
+
proof: str,
|
|
475
|
+
proof_state: int,
|
|
476
|
+
verbose: bool = False,
|
|
477
|
+
timeout: float | None = DEFAULT_TIMEOUT,
|
|
478
|
+
extra_repl_args: dict | None = None,
|
|
479
|
+
):
|
|
480
|
+
"""
|
|
481
|
+
EXPERIMENTAL
|
|
482
|
+
|
|
483
|
+
Run a (partial) proof in the Lean REPL.
|
|
484
|
+
|
|
485
|
+
Args:
|
|
486
|
+
proof: The (partial) proof to run.
|
|
487
|
+
proof_state: The proof state to apply the proof to.
|
|
488
|
+
verbose: Whether to print additional information during the verification process.
|
|
489
|
+
timeout: The timeout for the request.
|
|
490
|
+
Returns:
|
|
491
|
+
The output of the Lean server.
|
|
492
|
+
"""
|
|
493
|
+
if not proof:
|
|
494
|
+
raise ValueError("`proof` cannot be `None` or empty")
|
|
495
|
+
|
|
496
|
+
if not isinstance(proof, str):
|
|
497
|
+
raise ValueError("`proof` must be a string")
|
|
498
|
+
|
|
499
|
+
if proof_state is None:
|
|
500
|
+
raise ValueError("`proof_state` cannot be `None`")
|
|
501
|
+
|
|
502
|
+
return self.run_tactic(
|
|
503
|
+
tactic=f"(\n{proof}\n)",
|
|
504
|
+
proof_state=proof_state,
|
|
505
|
+
verbose=verbose,
|
|
506
|
+
timeout=timeout,
|
|
507
|
+
extra_repl_args=extra_repl_args,
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
def pickle(
|
|
511
|
+
self,
|
|
512
|
+
path: str,
|
|
513
|
+
env: int | None = None,
|
|
514
|
+
proof_state: int | None = None,
|
|
515
|
+
timeout: float | None = DEFAULT_TIMEOUT,
|
|
516
|
+
verbose: bool = False,
|
|
517
|
+
) -> None:
|
|
518
|
+
"""
|
|
519
|
+
Pickle the environment or proof state to a file.
|
|
520
|
+
Only one of `env` or `proof_state` can be provided.
|
|
521
|
+
|
|
522
|
+
Args:
|
|
523
|
+
path: The path to the .olean file to save the environment or proof state.
|
|
524
|
+
env: The environment to pickle.
|
|
525
|
+
proof_state: The proof state to pickle.
|
|
526
|
+
"""
|
|
527
|
+
if not path:
|
|
528
|
+
raise ValueError("`path` cannot be `None` or empty")
|
|
529
|
+
if not isinstance(path, str):
|
|
530
|
+
raise ValueError("`path` must be a string")
|
|
531
|
+
|
|
532
|
+
if env is None and proof_state is None:
|
|
533
|
+
raise ValueError("Either `env` or `proof_state` must be provided")
|
|
534
|
+
if env is not None and proof_state is not None:
|
|
535
|
+
raise ValueError("Only one of `env` or `proof_state` can be provided")
|
|
536
|
+
|
|
537
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
538
|
+
|
|
539
|
+
if env is not None:
|
|
540
|
+
self._process_request(dict_query=dict(pickleTo=path, env=env), verbose=verbose, timeout=timeout)
|
|
541
|
+
else:
|
|
542
|
+
self._process_request(
|
|
543
|
+
dict_query=dict(pickleTo=path, proofState=proof_state), verbose=verbose, timeout=timeout
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
def unpickle(
|
|
547
|
+
self,
|
|
548
|
+
path: str,
|
|
549
|
+
is_proof_state: bool = False,
|
|
550
|
+
timeout: float | None = DEFAULT_TIMEOUT,
|
|
551
|
+
verbose: bool = False,
|
|
552
|
+
) -> int:
|
|
553
|
+
"""
|
|
554
|
+
Unpickle an environment or proof state from a file.
|
|
555
|
+
|
|
556
|
+
Args:
|
|
557
|
+
path: The path to the .olean file to load the environment or proof state from.
|
|
558
|
+
is_proof_state: Whether to unpickle a proof state instead of an environment.
|
|
559
|
+
Returns:
|
|
560
|
+
The environment or proof state index.
|
|
561
|
+
"""
|
|
562
|
+
if not path:
|
|
563
|
+
raise ValueError("`path` cannot be `None` or empty")
|
|
564
|
+
if not isinstance(path, str):
|
|
565
|
+
raise ValueError("`path` must be a string")
|
|
566
|
+
|
|
567
|
+
if is_proof_state:
|
|
568
|
+
return self._process_request(
|
|
569
|
+
dict_query=dict(unpickleProofStateFrom=path), verbose=verbose, timeout=timeout
|
|
570
|
+
)["proofState"]
|
|
571
|
+
else:
|
|
572
|
+
return self._process_request(dict_query=dict(unpickleEnvFrom=path), verbose=verbose, timeout=timeout)["env"]
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
@dataclass
|
|
576
|
+
class _SessionState:
|
|
577
|
+
session_id: int
|
|
578
|
+
repl_id: int
|
|
579
|
+
pickle_file: str
|
|
580
|
+
is_proof_state: bool
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
class AutoLeanServer(LeanServer):
|
|
584
|
+
def __init__(
|
|
585
|
+
self,
|
|
586
|
+
config: LeanREPLConfig,
|
|
587
|
+
max_total_memory: float = 0.8,
|
|
588
|
+
max_restart_attempts: int = 5,
|
|
589
|
+
):
|
|
590
|
+
"""
|
|
591
|
+
This class is a Python wrapper for the Lean REPL. `AutoLeanServer` differs from `LeanServer` by automatically \
|
|
592
|
+
restarting when it runs out of memory to clear Lean environment states. \
|
|
593
|
+
It also automatically recovers from timeouts (). \
|
|
594
|
+
An exponential backoff strategy is used to restart the server, making this class slightly more friendly for multiprocessing
|
|
595
|
+
than `LeanServer` when multiple instances are competing for resources. \
|
|
596
|
+
Please refer to the original [Lean REPL documentation](https://github.com/leanprover-community/repl) to learn more about the \
|
|
597
|
+
Lean REPL commands.
|
|
598
|
+
|
|
599
|
+
A session cache is implemented to keep user-selected environment / proof states across these automatic restarts. \
|
|
600
|
+
Use the `add_to_session_cache` parameter in the different class methods to add the command to \
|
|
601
|
+
the session cache. `AutoLeanServer` works best when only a few states are cached simultaneously. \
|
|
602
|
+
You can use `remove_from_session_cache` and `clear_session_cache` to clear the session cache. \
|
|
603
|
+
Cached state indices are negative integers starting from -1 to not conflict with the positive integers used by the Lean REPL.
|
|
604
|
+
|
|
605
|
+
**Note:** the session cache is specific to each `AutoLeanServer` instance and is cleared when the instance is deleted. \
|
|
606
|
+
If you want truly persistent states, you can use the `pickle` and `unpickle` methods to save and load states to disk.
|
|
607
|
+
|
|
608
|
+
\u26a0 Multiprocessing: instantiate the config before starting multiprocessing. Then instantiate one `LeanServer`
|
|
609
|
+
per process by passing the config instance to the constructor. This will ensure that the REPL is already set up
|
|
610
|
+
for your specific environment and avoid concurrency conflicts.
|
|
611
|
+
|
|
612
|
+
Args:
|
|
613
|
+
config: The configuration for the Lean server.
|
|
614
|
+
max_total_memory: The maximum proportion of total memory usage before restarting the Lean server. Default is 0.8 (80%).
|
|
615
|
+
max_restart_attempts: The maximum number of restart attempts before raising a `MemoryError`. Default is 5.
|
|
616
|
+
"""
|
|
617
|
+
self._state_counter = 0
|
|
618
|
+
self._restart_persistent_session_cache: dict[int, _SessionState] = {}
|
|
619
|
+
self._max_total_memory = max_total_memory
|
|
620
|
+
self._max_restart_attempts = max_restart_attempts
|
|
621
|
+
self._restart_counter = 0
|
|
622
|
+
super().__init__(config=config)
|
|
623
|
+
|
|
624
|
+
def _get_repl_state_id(self, state_id: int | None) -> int | None:
|
|
625
|
+
if state_id is None:
|
|
626
|
+
return None
|
|
627
|
+
if state_id >= 0:
|
|
628
|
+
return state_id
|
|
629
|
+
return self._restart_persistent_session_cache[state_id].repl_id
|
|
630
|
+
|
|
631
|
+
def _reload_session_cache(self, verbose: bool = False) -> None:
|
|
632
|
+
"""
|
|
633
|
+
Reload the session cache. This method should be called only after a restart of the Lean REPL.
|
|
634
|
+
"""
|
|
635
|
+
for state_data in self._restart_persistent_session_cache.values():
|
|
636
|
+
state_data.repl_id = self.unpickle(
|
|
637
|
+
path=state_data.pickle_file,
|
|
638
|
+
is_proof_state=state_data.is_proof_state,
|
|
639
|
+
verbose=verbose,
|
|
640
|
+
add_to_session_cache=False,
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
def restart(self, verbose: bool = False) -> None:
|
|
644
|
+
super().restart()
|
|
645
|
+
self._reload_session_cache(verbose=verbose)
|
|
646
|
+
|
|
647
|
+
def remove_from_session_cache(self, session_state_id: int) -> None:
|
|
648
|
+
"""
|
|
649
|
+
Remove an environment from the session cache.
|
|
650
|
+
|
|
651
|
+
Args:
|
|
652
|
+
env_id: The environment id to remove.
|
|
653
|
+
"""
|
|
654
|
+
if (state_cache := self._restart_persistent_session_cache.pop(session_state_id, None)) is not None:
|
|
655
|
+
os.remove(state_cache.pickle_file)
|
|
656
|
+
|
|
657
|
+
def clear_session_cache(self, force: bool = False) -> None:
|
|
658
|
+
"""
|
|
659
|
+
Clear the session cache.
|
|
660
|
+
|
|
661
|
+
Args:
|
|
662
|
+
force: Whether to directly clear the session cache. \
|
|
663
|
+
`force=False` will only clear the session cache the next time the server runs out of memory while \
|
|
664
|
+
still allowing you to add new content in the session cache in the meantime.
|
|
665
|
+
"""
|
|
666
|
+
states_data = list(self._restart_persistent_session_cache.values())
|
|
667
|
+
for state_data in states_data:
|
|
668
|
+
self.remove_from_session_cache(session_state_id=state_data.session_id)
|
|
669
|
+
self._restart_persistent_session_cache = {}
|
|
670
|
+
if force:
|
|
671
|
+
self.restart()
|
|
672
|
+
|
|
673
|
+
def __del__(self):
|
|
674
|
+
# delete the session cache
|
|
675
|
+
for state_data in self._restart_persistent_session_cache.values():
|
|
676
|
+
try:
|
|
677
|
+
os.remove(state_data.pickle_file)
|
|
678
|
+
except FileNotFoundError:
|
|
679
|
+
pass
|
|
680
|
+
|
|
681
|
+
super().__del__()
|
|
682
|
+
|
|
683
|
+
def _store_session_cache(
|
|
684
|
+
self, hash_key: str, repl_id: int, is_proof_state: bool = False, verbose: bool = False
|
|
685
|
+
) -> int:
|
|
686
|
+
self._state_counter -= 1
|
|
687
|
+
process_id = os.getpid() # use process id to avoid conflicts in multiprocessing
|
|
688
|
+
pickle_file = os.path.join(
|
|
689
|
+
self.config._working_dir,
|
|
690
|
+
f"session_cache/{hashlib.sha256(hash_key.encode()).hexdigest()}_{process_id}.olean",
|
|
691
|
+
)
|
|
692
|
+
if is_proof_state:
|
|
693
|
+
self.pickle(path=pickle_file, proof_state=repl_id, verbose=verbose)
|
|
694
|
+
else:
|
|
695
|
+
self.pickle(path=pickle_file, env=repl_id, verbose=verbose)
|
|
696
|
+
|
|
697
|
+
self._restart_persistent_session_cache[self._state_counter] = _SessionState(
|
|
698
|
+
session_id=self._state_counter,
|
|
699
|
+
repl_id=repl_id,
|
|
700
|
+
pickle_file=pickle_file,
|
|
701
|
+
is_proof_state=is_proof_state,
|
|
702
|
+
)
|
|
703
|
+
return self._state_counter
|
|
704
|
+
|
|
705
|
+
def run_file(
|
|
706
|
+
self,
|
|
707
|
+
path: str,
|
|
708
|
+
verbose: bool = False,
|
|
709
|
+
timeout: float | None = DEFAULT_TIMEOUT,
|
|
710
|
+
extra_repl_args: dict | None = None,
|
|
711
|
+
add_to_session_cache: bool = False,
|
|
712
|
+
) -> dict:
|
|
713
|
+
res = super().run_file(path=path, verbose=verbose, timeout=timeout, extra_repl_args=extra_repl_args)
|
|
714
|
+
if add_to_session_cache:
|
|
715
|
+
try:
|
|
716
|
+
res["env"] = self._store_session_cache(
|
|
717
|
+
hash_key=f"path_{extra_repl_args}", repl_id=res["env"], is_proof_state=False, verbose=verbose
|
|
718
|
+
)
|
|
719
|
+
except (ValueError, KeyError) as e:
|
|
720
|
+
raise ValueError("Failed to add the environment to the session cache.") from e
|
|
721
|
+
return res
|
|
722
|
+
|
|
723
|
+
def run_code(
|
|
724
|
+
self,
|
|
725
|
+
code: str,
|
|
726
|
+
env: int | None = None,
|
|
727
|
+
verbose: bool = False,
|
|
728
|
+
timeout: float | None = DEFAULT_TIMEOUT,
|
|
729
|
+
extra_repl_args: dict | None = None,
|
|
730
|
+
add_to_session_cache: bool = False,
|
|
731
|
+
) -> dict:
|
|
732
|
+
res = super().run_code(code=code, env=env, verbose=verbose, timeout=timeout, extra_repl_args=extra_repl_args)
|
|
733
|
+
if add_to_session_cache:
|
|
734
|
+
try:
|
|
735
|
+
res["env"] = self._store_session_cache(
|
|
736
|
+
hash_key=f"code_{extra_repl_args}_env_{env}",
|
|
737
|
+
repl_id=res["env"],
|
|
738
|
+
is_proof_state=False,
|
|
739
|
+
verbose=verbose,
|
|
740
|
+
)
|
|
741
|
+
except (ValueError, KeyError) as e:
|
|
742
|
+
raise ValueError("Failed to add the environment to the session cache.") from e
|
|
743
|
+
return res
|
|
744
|
+
|
|
745
|
+
def run_tactic(
|
|
746
|
+
self,
|
|
747
|
+
tactic: str,
|
|
748
|
+
proof_state: int,
|
|
749
|
+
verbose: bool = False,
|
|
750
|
+
timeout: float | None = DEFAULT_TIMEOUT,
|
|
751
|
+
extra_repl_args: dict | None = None,
|
|
752
|
+
add_to_session_cache: bool = False,
|
|
753
|
+
) -> dict:
|
|
754
|
+
res = super().run_tactic(
|
|
755
|
+
tactic=tactic, proof_state=proof_state, verbose=verbose, timeout=timeout, extra_repl_args=extra_repl_args
|
|
756
|
+
)
|
|
757
|
+
if add_to_session_cache:
|
|
758
|
+
try:
|
|
759
|
+
res["proofState"] = self._store_session_cache(
|
|
760
|
+
hash_key=f"tactic_{extra_repl_args}_proofstate_{proof_state}",
|
|
761
|
+
repl_id=res["proofState"],
|
|
762
|
+
is_proof_state=True,
|
|
763
|
+
verbose=verbose,
|
|
764
|
+
)
|
|
765
|
+
except (ValueError, KeyError) as e:
|
|
766
|
+
raise ValueError("Failed to add the proof state to the session cache.") from e
|
|
767
|
+
return res
|
|
768
|
+
|
|
769
|
+
def run_proof(
|
|
770
|
+
self,
|
|
771
|
+
proof: str,
|
|
772
|
+
proof_state: int,
|
|
773
|
+
verbose: bool = False,
|
|
774
|
+
timeout: float | None = DEFAULT_TIMEOUT,
|
|
775
|
+
extra_repl_args: dict | None = None,
|
|
776
|
+
add_to_session_cache: bool = False,
|
|
777
|
+
) -> dict:
|
|
778
|
+
res = super().run_proof(
|
|
779
|
+
proof=proof, proof_state=proof_state, verbose=verbose, timeout=timeout, extra_repl_args=extra_repl_args
|
|
780
|
+
)
|
|
781
|
+
if add_to_session_cache:
|
|
782
|
+
try:
|
|
783
|
+
res["proofState"] = self._store_session_cache(
|
|
784
|
+
hash_key=f"proof_{extra_repl_args}_proofstate_{proof_state}",
|
|
785
|
+
repl_id=res["proofState"],
|
|
786
|
+
is_proof_state=True,
|
|
787
|
+
verbose=verbose,
|
|
788
|
+
)
|
|
789
|
+
except (ValueError, KeyError) as e:
|
|
790
|
+
raise ValueError("Failed to add the proof state to the session cache.") from e
|
|
791
|
+
return res
|
|
792
|
+
|
|
793
|
+
def _process_request(
|
|
794
|
+
self,
|
|
795
|
+
dict_query: dict,
|
|
796
|
+
verbose: bool = False,
|
|
797
|
+
timeout: float | None = DEFAULT_TIMEOUT,
|
|
798
|
+
) -> dict:
|
|
799
|
+
if psutil.virtual_memory().percent >= 100 * self._max_total_memory:
|
|
800
|
+
self.kill()
|
|
801
|
+
if self._restart_counter >= self._max_restart_attempts:
|
|
802
|
+
raise MemoryError(
|
|
803
|
+
f"Memory usage is too high. We attempted to restart the Lean server {self._max_restart_attempts} times without success."
|
|
804
|
+
)
|
|
805
|
+
if verbose:
|
|
806
|
+
logger.info("Memory usage is too high. Reloading the Lean server...")
|
|
807
|
+
sleep(2**self._restart_counter)
|
|
808
|
+
self._restart_counter += 1
|
|
809
|
+
return self._process_request(dict_query=dict_query, verbose=verbose, timeout=timeout)
|
|
810
|
+
|
|
811
|
+
if not self.is_alive():
|
|
812
|
+
self.start()
|
|
813
|
+
self._reload_session_cache(verbose=verbose)
|
|
814
|
+
|
|
815
|
+
# Replace the negative environment / proof state ids with the corresponding REPL ids
|
|
816
|
+
if dict_query.get("env", 0) < 0:
|
|
817
|
+
dict_query = deepcopy(dict_query)
|
|
818
|
+
dict_query["env"] = self._get_repl_state_id(dict_query["env"])
|
|
819
|
+
if dict_query.get("proofState", 0) < 0:
|
|
820
|
+
dict_query = deepcopy(dict_query)
|
|
821
|
+
dict_query["proofState"] = self._get_repl_state_id(dict_query["proofState"])
|
|
822
|
+
|
|
823
|
+
res = super()._process_request(dict_query=dict_query, verbose=verbose, timeout=timeout)
|
|
824
|
+
|
|
825
|
+
self._restart_counter = 0
|
|
826
|
+
return res
|
|
827
|
+
|
|
828
|
+
def unpickle(
|
|
829
|
+
self,
|
|
830
|
+
path: str,
|
|
831
|
+
is_proof_state: bool = False,
|
|
832
|
+
timeout: float | None = DEFAULT_TIMEOUT,
|
|
833
|
+
verbose: bool = False,
|
|
834
|
+
add_to_session_cache: bool = False,
|
|
835
|
+
) -> int:
|
|
836
|
+
res = super().unpickle(path=path, is_proof_state=is_proof_state, timeout=timeout, verbose=verbose)
|
|
837
|
+
if add_to_session_cache:
|
|
838
|
+
try:
|
|
839
|
+
res = self._store_session_cache(path, repl_id=res, is_proof_state=is_proof_state, verbose=verbose)
|
|
840
|
+
except (ValueError, KeyError) as e:
|
|
841
|
+
raise ValueError("Failed to add the environment to the session cache.") from e
|
|
842
|
+
return res
|
lean_interact/utils.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import resource
|
|
4
|
+
import shutil
|
|
5
|
+
|
|
6
|
+
from packaging.version import Version
|
|
7
|
+
from rich.logging import RichHandler
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger("document_autoformalization")
|
|
10
|
+
logger.setLevel("INFO")
|
|
11
|
+
handler = RichHandler(rich_tracebacks=True)
|
|
12
|
+
handler.setLevel("NOTSET")
|
|
13
|
+
handler.setFormatter(logging.Formatter("%(message)s", datefmt="[%X]"))
|
|
14
|
+
logger.handlers = []
|
|
15
|
+
logger.addHandler(handler)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
19
|
+
DEFAULT_CACHE_DIR = os.path.join(ROOT_DIR, "cache")
|
|
20
|
+
DEFAULT_REPL_GIT_URL = "https://github.com/augustepoiroux/repl"
|
|
21
|
+
|
|
22
|
+
os.makedirs(DEFAULT_CACHE_DIR, exist_ok=True)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _limit_memory(max_mb: int):
|
|
26
|
+
"""Limit the memory usage of the current process."""
|
|
27
|
+
try:
|
|
28
|
+
resource.setrlimit(resource.RLIMIT_AS, (max_mb * 1024 * 1024, max_mb * 1024 * 1024))
|
|
29
|
+
except ValueError:
|
|
30
|
+
logger.warning(f"Failed to set memory limit to {max_mb} MB.")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def clear_cache():
|
|
34
|
+
shutil.rmtree(DEFAULT_CACHE_DIR, ignore_errors=True)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def fetch_lean_versions(repl_repo_dir: str) -> list[str]:
|
|
38
|
+
"""
|
|
39
|
+
Fetch the list of Lean versions available in the `versions` folder of the local REPL repository.
|
|
40
|
+
"""
|
|
41
|
+
versions_dir = os.path.join(repl_repo_dir, "versions")
|
|
42
|
+
if not os.path.isdir(versions_dir):
|
|
43
|
+
return []
|
|
44
|
+
return sorted(
|
|
45
|
+
[filename[:-5] for filename in os.listdir(versions_dir) if filename.endswith(".diff")],
|
|
46
|
+
key=lambda v: Version(v),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_project_lean_version(project_dir: str) -> str | None:
|
|
51
|
+
"""
|
|
52
|
+
Get the Lean version used in a project.
|
|
53
|
+
"""
|
|
54
|
+
toolchain_file = os.path.join(project_dir, "lean-toolchain")
|
|
55
|
+
if os.path.isfile(toolchain_file):
|
|
56
|
+
with open(toolchain_file, "r", encoding="utf-8") as f:
|
|
57
|
+
content = f.read().strip()
|
|
58
|
+
if content:
|
|
59
|
+
try:
|
|
60
|
+
return content.split(":")[-1]
|
|
61
|
+
except Exception:
|
|
62
|
+
pass
|
|
63
|
+
return None
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lean-interact
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: LeanInteract is a Python package that allows you to interact with the Lean theorem prover.
|
|
5
|
+
Author-email: Auguste Poiroux <auguste.poiroux@epfl.ch>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2023 LeanDojo Team
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Keywords: Lean,REPL,autoformalization,theorem proving
|
|
29
|
+
Requires-Python: >=3.10
|
|
30
|
+
Requires-Dist: packaging>=24.2
|
|
31
|
+
Requires-Dist: pexpect>=4.9.0
|
|
32
|
+
Requires-Dist: psutil>=6.1.0
|
|
33
|
+
Requires-Dist: requests>=2.32.3
|
|
34
|
+
Requires-Dist: rich>=13.9.4
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
|
|
37
|
+
# LeanInteract
|
|
38
|
+
|
|
39
|
+
**LeanInteract** is a Python package designed to seamlessly interact with Lean 4 through the [Lean REPL](https://github.com/leanprover-community/repl).
|
|
40
|
+
|
|
41
|
+
> [!NOTE]
|
|
42
|
+
> This tool is still experimental and has been primarily tested on Linux. Compatibility with macOS is not guaranteed. Windows is not supported at the moment. Please report any issues you encounter.
|
|
43
|
+
|
|
44
|
+
## Key Features
|
|
45
|
+
|
|
46
|
+
- **🚀 Ease of Use**: LeanInteract abstracts the complexities of Lean setup and interaction, enabling quick experimentation.
|
|
47
|
+
- **🔗 Interactivity**: Execute Lean code, files, tactics, and (partial) proofs directly from Python. Easily iterate through environment and proof states.
|
|
48
|
+
- **🔧 Compatibility**: Supports Lean versions from `v4.7.0-rc1` to `v4.16.0-rc2`.
|
|
49
|
+
- Ensures compatibility with various Lean projects and machine learning benchmarks.
|
|
50
|
+
- Need older versions? Open an issue [here](https://github.com/augustepoiroux/repl).
|
|
51
|
+
- **🔄 Up-to-date**: LeanInteract is a lightweight wrapper around [Lean REPL](https://github.com/leanprover-community/repl), ensuring it stays current with the latest Lean versions and features.
|
|
52
|
+
- **📥 Automatic Setup**: Automatically downloads and builds Lean REPL versions for you. Versions are cached for fast reuse.
|
|
53
|
+
- **📦 Temporary Projects**: Easily instantiate temporary Lean environments with dependencies.
|
|
54
|
+
- Useful for experimenting and interacting with benchmarks like [ProofNet](https://github.com/zhangir-azerbayev/ProofNet) and [MiniF2F](https://github.com/yangky11/miniF2F-lean4) without manual setup.
|
|
55
|
+
|
|
56
|
+
## Similar tools
|
|
57
|
+
|
|
58
|
+
We recommend checking out these tools:
|
|
59
|
+
|
|
60
|
+
- **[PyPantograph](https://github.com/lenianiva/PyPantograph)**: Based on Pantograph, offering more options for proof interactions than Lean REPL.
|
|
61
|
+
- **[LeanDojo](https://github.com/lean-dojo/LeanDojo)**: Parses Lean projects to create datasets and interact with theorems to prove them.
|
|
62
|
+
- **[leanclient](https://github.com/oOo0oOo/leanclient)**: Interact with the Lean LSP server.
|
|
63
|
+
|
|
64
|
+
LeanInteract is inspired by:
|
|
65
|
+
|
|
66
|
+
- **[pylean](https://github.com/zhangir-azerbayev/repl)**
|
|
67
|
+
- **[lean4_jupyter](https://github.com/utensil/lean4_jupyter)**
|
|
68
|
+
|
|
69
|
+
## Installation and Setup
|
|
70
|
+
|
|
71
|
+
Requirements:
|
|
72
|
+
|
|
73
|
+
- Python >= 3.10
|
|
74
|
+
- git
|
|
75
|
+
- [Lean 4](https://leanprover-community.github.io/get_started.html)
|
|
76
|
+
|
|
77
|
+
If the requirements are met, install the LeanInteract package:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
pip install lean-interact
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Script examples
|
|
84
|
+
|
|
85
|
+
In the `examples` directory, you will find various scripts demonstrating how to use LeanInteract:
|
|
86
|
+
|
|
87
|
+
- `minif2f_deepseek_prover_v1_5.py`: use [DeepSeek-Prover-V1.5](https://arxiv.org/abs/2408.08152) to prove theorems from the [MiniF2F](https://github.com/yangky11/miniF2F-lean4) benchmark.
|
|
88
|
+
- `proofnetsharp_type_check.py`: type check theorems from the [ProofNet#](https://github.com/zhangir-azerbayev/ProofNet) benchmark.
|
|
89
|
+
- `proofnetsharp_false_theorems.py`: find theorems that can be proven false from the [ProofNet#](https://github.com/zhangir-azerbayev/ProofNet) benchmark.
|
|
90
|
+
- `proofnet_metric_beq_plus.py`: run a simple [BEq](https://openreview.net/forum?id=hUb2At2DsQ) checker on the ProofNet-Metric benchmark.
|
|
91
|
+
- `improving_autoformalization_using_type_checking.py`: an implementation of the sampling method used in [Improving Autoformalization using Type Checking](https://arxiv.org/abs/2406.07222).
|
|
92
|
+
|
|
93
|
+
## Usage
|
|
94
|
+
|
|
95
|
+
### Default Lean version (latest available)
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from lean_interact import LeanREPLConfig, LeanServer
|
|
99
|
+
|
|
100
|
+
config = LeanREPLConfig() # download and build Lean REPL
|
|
101
|
+
server = LeanServer(config) # start Lean REPL
|
|
102
|
+
server.run_code("theorem ex (n : Nat) : n = 5 → n = 5 := sorry")
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
<details>
|
|
106
|
+
<summary>Output</summary>
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{"sorries": [{"proofState": 0,
|
|
110
|
+
"pos": {"line": 1, "column": 40},
|
|
111
|
+
"goal": "n : Nat\n⊢ n = 5 → n = 5",
|
|
112
|
+
"endPos": {"line": 1, "column": 45}}],
|
|
113
|
+
"messages": [{"severity": "warning",
|
|
114
|
+
"pos": {"line": 1, "column": 8},
|
|
115
|
+
"endPos": {"line": 1, "column": 10},
|
|
116
|
+
"data": "declaration uses 'sorry'"}],
|
|
117
|
+
"env": 0}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
</details>
|
|
121
|
+
|
|
122
|
+
You can then iterate on the proof state by executing tactics:
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
server.run_tactic("intro h", proof_state=0)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
<details>
|
|
129
|
+
<summary>Output</summary>
|
|
130
|
+
|
|
131
|
+
```json
|
|
132
|
+
{"proofState": 1, "goals": ["n : Nat\nh : n = 5\n⊢ n = 5"]}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
</details>
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
server.run_tactic("exact h", proof_state=1)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
<details>
|
|
142
|
+
<summary>Output</summary>
|
|
143
|
+
|
|
144
|
+
```json
|
|
145
|
+
{"proofState": 2, "goals": []}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
</details>
|
|
149
|
+
|
|
150
|
+
Or by running the entire proof:
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
server.run_proof("intro h\nexact h", proof_state=0)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
<details>
|
|
157
|
+
<summary>Output</summary>
|
|
158
|
+
|
|
159
|
+
```json
|
|
160
|
+
{"proofState": 3, "goals": []}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
</details>
|
|
164
|
+
|
|
165
|
+
You can also iterate on the environment:
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
server.run_code("theorem ex2 (x : Nat) : x = 5 → x = 5 := by\n exact ex x", env=0)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
<details>
|
|
172
|
+
<summary>Output</summary>
|
|
173
|
+
|
|
174
|
+
```json
|
|
175
|
+
{"env": 1}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
</details>
|
|
179
|
+
|
|
180
|
+
> [!NOTE]
|
|
181
|
+
> The initial invocation of `LeanREPLConfig` might take some time as it downloads and builds Lean REPL. Future executions with identical parameters will be significantly quicker due to caching.
|
|
182
|
+
|
|
183
|
+
### Using a specific Lean version
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
config = LeanREPLConfig(lean_version="v4.7.0")
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Using an existing Lean project directory
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
config = LeanREPLConfig(project_dir="path/to/your/project")
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
You can then use `run_code`, `run_tactic` and `run_file` as usual:
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
server = LeanServer(config)
|
|
199
|
+
server.run_file("file.lean")
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
> [!IMPORTANT]
|
|
203
|
+
> Ensure the project in `project_dir` has been *successfully* built with `lake build` before using the REPL.
|
|
204
|
+
|
|
205
|
+
### Using a temporary project with dependencies
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
config = LeanREPLConfig(lean_version="v4.7.0", require=[LeanRequire(
|
|
209
|
+
name="mathlib",
|
|
210
|
+
git="https://github.com/leanprover-community/mathlib4.git",
|
|
211
|
+
rev="v4.7.0"
|
|
212
|
+
)])
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Mathlib being a frequent requirement, a shortcut is available:
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
config = LeanREPLConfig(lean_version="v4.7.0", require="mathlib")
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
You can then use Mathlib:
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
server = LeanServer(config_readme_mathlib)
|
|
225
|
+
server.run_code("""import Mathlib
|
|
226
|
+
theorem ex_mathlib (x : ℝ) (y : ℚ) :\n ( Irrational x ) -> Irrational ( x + y ) := sorry""")
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
<details>
|
|
230
|
+
<summary>Output</summary>
|
|
231
|
+
|
|
232
|
+
```json
|
|
233
|
+
{"sorries": [{"proofState": 0,
|
|
234
|
+
"pos": {"line": 4, "column": 26},
|
|
235
|
+
"goal": "x : ℝ\ny : ℚ\n⊢ Irrational (x + ↑y)",
|
|
236
|
+
"endPos": {"line": 4, "column": 31}}],
|
|
237
|
+
"messages": [{"severity": "warning",
|
|
238
|
+
"pos": {"line": 3, "column": 8},
|
|
239
|
+
"endPos": {"line": 3, "column": 18},
|
|
240
|
+
"data": "declaration uses 'sorry'"}],
|
|
241
|
+
"env": 0}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
</details>
|
|
245
|
+
|
|
246
|
+
> [!NOTE]
|
|
247
|
+
>
|
|
248
|
+
> - Mathlib is a large library and may take some time to download and build.
|
|
249
|
+
> - Mathlib, and other libraries, are not available for all Lean versions. An error will be raised if the Lean version you are using does not support Mathlib. You can always specify a different version with the `lean_version` parameter if you know a compatible version.
|
|
250
|
+
> - A separate cache is used for each unique set of dependencies.
|
|
251
|
+
|
|
252
|
+
## Advanced options
|
|
253
|
+
|
|
254
|
+
### LeanServer
|
|
255
|
+
|
|
256
|
+
Two versions of Lean servers are available:
|
|
257
|
+
|
|
258
|
+
- **`LeanServer`**: A wrapper around Lean REPL. Interact with it using `run_code`, `run_file`, and `run_tactic` methods.
|
|
259
|
+
- **`AutoLeanServer`**: An experimental subclass of `LeanServer` monitoring memory usage to limit *out of memory* crashes in multiprocessing contexts. Additionally, since Lean REPL retains all environment and proof states, `AutoLeanServer` regularly clears the REPL's memory to prevent crashes. Use the `add_to_session_cache` attribute in various methods to prevent selected environment/proof states to be cleared.
|
|
260
|
+
|
|
261
|
+
> [!TIP]
|
|
262
|
+
>
|
|
263
|
+
> - To run multiple requests in parallel, we recommend using multiprocessing with one `AutoLeanServer` instance per process.
|
|
264
|
+
> - Make sure to instantiate `LeanREPLConfig` before starting the processes to avoid conflicts during Lean REPL's download and build.
|
|
265
|
+
> - While `AutoLeanServer` can help prevent crashes, it is not a complete solution. If you encounter crashes, consider reducing the number of parallel processes or increasing the memory available to your system.
|
|
266
|
+
|
|
267
|
+
### Helper Methods and Variables
|
|
268
|
+
|
|
269
|
+
- `clear_cache()`: Removes all Lean REPL versions and temporary projects in the package cache. This can help resolve some issues. If it does, please open an issue.
|
|
270
|
+
|
|
271
|
+
### Custom Lean REPL
|
|
272
|
+
|
|
273
|
+
To use a forked Lean REPL project, specify the git repository using the `repl_git` parameter in the `LeanREPLConfig`. Your fork should have a similar format to <https://github.com/augustepoiroux/repl>. For assistance, feel free to contact [us](mailto:auguste.poiroux@epfl.ch).
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
lean_interact/__init__.py,sha256=uz3M7IXSQR6yQkDGMev0KzEDVUqKqDk6CQDBillXLIc,491
|
|
2
|
+
lean_interact/server.py,sha256=SswYgExtyRSHsegGnu8Oo9QpMtu11nIPRwg7y9JAAlo,34665
|
|
3
|
+
lean_interact/utils.py,sha256=kch-uyqwD_VgIs7h5PwBjkI5qrk0QXLe_RdstXvxi2E,1935
|
|
4
|
+
lean_interact-0.1.0.dist-info/METADATA,sha256=bPqJPh-QOzOb7I2WW-y3HyRQdccuJS-NzIdnndlA4tw,10251
|
|
5
|
+
lean_interact-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
6
|
+
lean_interact-0.1.0.dist-info/licenses/LICENSE,sha256=iGKZ60MHRLJE-TXF6EuIE8jSdcQqTcVY0RBI0gRo9jA,1070
|
|
7
|
+
lean_interact-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 LeanDojo Team
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|