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
+ )
@@ -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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.