Skip to content

Commit 501a573

Browse files
authored
Merge pull request #180 from lean-dojo/kaiyu
Use `pexpect` instead of `signal` in dojo.py
2 parents 3742322 + 6ce9e9c commit 501a573

7 files changed

Lines changed: 48 additions & 97 deletions

File tree

docs/source/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
project = "LeanDojo"
1414
copyright = "2023, LeanDojo Team"
1515
author = "Kaiyu Yang"
16-
release = "2.0.0"
16+
release = "2.0.1"
1717

1818
# -- General configuration ---------------------------------------------------
1919
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration

mypy.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,6 @@ pretty = True
66
implicit_reexport = True
77
disallow_untyped_calls = False
88
follow_imports = skip
9+
10+
[mypy-pexpect.*]
11+
ignore_missing_imports = True

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ exclude = [
1212

1313
[project]
1414
name = "lean-dojo"
15-
version = "2.0.0"
15+
version = "2.0.1"
1616
authors = [
1717
{ name="Kaiyu Yang", email="kaiyuy@meta.com" },
1818
]
@@ -32,6 +32,7 @@ dependencies = [
3232
"loguru",
3333
"filelock",
3434
"psutil",
35+
"pexpect",
3536
"types-psutil",
3637
"tqdm",
3738
"toml",

src/lean_dojo/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
TimeoutError,
2020
TacticResult,
2121
DojoCrashError,
22-
DojoHardTimeoutError,
22+
DojoTacticTimeoutError,
2323
DojoInitError,
2424
Dojo,
2525
ProofFinished,

src/lean_dojo/constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
load_dotenv()
1616

17-
__version__ = "2.0.0"
17+
__version__ = "2.0.1"
1818

1919
logger.remove()
2020
if "VERBOSE" in os.environ or "DEBUG" in os.environ:

src/lean_dojo/interaction/dojo.py

Lines changed: 38 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
import re
22
import os
3-
import sys
43
import json
54
import time
6-
import shlex
7-
import signal
85
import psutil
6+
import pexpect
97
import tempfile
10-
import subprocess
118
from pathlib import Path
129
from loguru import logger
1310
from dataclasses import dataclass, field
@@ -21,9 +18,6 @@
2118
from ..data_extraction.traced_data import TracedFile, get_code_without_comments
2219

2320

24-
_REPL_PROMPT = "REPL>"
25-
26-
2721
@dataclass(frozen=True)
2822
class CommandState:
2923
id: int = field(compare=False)
@@ -87,14 +81,21 @@ def is_out_of_memory(self) -> bool:
8781
return str(self) == "OOM"
8882

8983

90-
class DojoHardTimeoutError(Exception):
84+
class DojoTacticTimeoutError(Exception):
9185
pass
9286

9387

9488
class DojoInitError(Exception):
9589
pass
9690

9791

92+
def kill_descendants(pid: int) -> None:
93+
try:
94+
_kill_descendants(psutil.Process(pid))
95+
except psutil.NoSuchProcess:
96+
pass
97+
98+
9899
def _kill_descendants(proc: psutil.Process) -> None:
99100
for child in proc.children():
100101
_kill_descendants(child)
@@ -108,7 +109,6 @@ class Dojo:
108109
"""Gym-like environment for programmatic interaction with Lean through tactics or commands."""
109110

110111
entry: Union[Theorem, Tuple[LeanGitRepo, Path, int]]
111-
hard_timeout: Optional[float]
112112
additional_imports: List[str]
113113
repo: LeanGitRepo
114114
file_path: Path
@@ -120,7 +120,7 @@ class Dojo:
120120
def __init__(
121121
self,
122122
entry: Union[Theorem, Tuple[LeanGitRepo, Path, int]],
123-
hard_timeout: Optional[float] = None,
123+
timeout: int = 600,
124124
additional_imports: List[str] = [],
125125
):
126126
"""Initialize Dojo.
@@ -130,10 +130,10 @@ def __init__(
130130
the :class:`Dojo` object enables interaction with the theorem through tactics.
131131
When a tuple of (repo, file_path, line_nb) is given (only supported in Lean 4),
132132
the :class:`Dojo` object enables interaction with Lean through commands (similar to a REPL).
133-
hard_timeout (Optional[float], optional): Hard timeout in seconds. Defaults to None.
133+
timeout (int): The maximum number of seconds for a single interaction (e.g., tactic).
134134
"""
135135
self.entry = entry
136-
self.hard_timeout = hard_timeout
136+
self.timeout = timeout
137137
self.additional_imports = additional_imports
138138

139139
if self.uses_tactics:
@@ -146,11 +146,6 @@ def __init__(
146146
self.repo, self.file_path, _ = entry
147147
self.file_path = Path(self.file_path)
148148

149-
if self.hard_timeout is None:
150-
logger.warning(
151-
"Running tactics without a hard timeout may hang indefinitely."
152-
)
153-
154149
@property
155150
def uses_tactics(self) -> bool:
156151
return isinstance(self.entry, Theorem)
@@ -162,7 +157,6 @@ def uses_commands(self) -> bool:
162157
def __enter__(self) -> Tuple["Dojo", State]:
163158
"""Initialize Dojo."""
164159
logger.debug(f"Initializing Dojo for {self.entry}")
165-
self._install_handlers()
166160

167161
# Replace the human-written proof with a `repl` tactic.
168162
traced_repo_path = get_traced_repo_path(self.repo)
@@ -180,14 +174,8 @@ def __enter__(self) -> Tuple["Dojo", State]:
180174
memory_limit = 1024 * int(TACTIC_MEMORY_LIMIT[:-1])
181175
modified_path = Path(self.modified_file.name).relative_to(traced_repo_path)
182176
cmd = f"lake env lean --threads={TACTIC_CPU_LIMIT} --memory={memory_limit} {modified_path}"
183-
self.proc = subprocess.Popen(
184-
shlex.split(cmd),
185-
stdin=subprocess.PIPE,
186-
stdout=subprocess.PIPE,
187-
stderr=subprocess.STDOUT,
188-
universal_newlines=True,
189-
encoding="utf-8",
190-
bufsize=1,
177+
self.proc = pexpect.spawn(
178+
cmd, timeout=self.timeout, maxread=1, encoding="utf-8", echo=False
191179
)
192180

193181
# Get the initial tactic state.
@@ -217,41 +205,12 @@ def __enter__(self) -> Tuple["Dojo", State]:
217205
init_state = CommandState(int(res["sid"]))
218206

219207
self.start_time = time.monotonic()
220-
self._set_timer()
221-
222208
return self, init_state
223209

224210
def _locate_traced_file(self, traced_repo_path: Path) -> TracedFile:
225211
json_path = to_json_path(traced_repo_path, self.file_path, self.repo)
226212
return TracedFile.from_traced_file(traced_repo_path, json_path, self.repo)
227213

228-
def _set_timer(self) -> None:
229-
if self.hard_timeout is not None:
230-
signal.signal(signal.SIGALRM, self._handle_hard_timeout)
231-
signal.alarm(int(self.hard_timeout))
232-
233-
def _cancel_timer(self) -> None:
234-
if self.hard_timeout is not None:
235-
signal.alarm(0)
236-
signal.signal(signal.SIGALRM, signal.SIG_DFL)
237-
238-
def _handle_hard_timeout(self, signum: Any, frame: Any) -> None:
239-
logger.debug(f"Hard timeout in {self}")
240-
self.has_timedout = True
241-
raise DojoHardTimeoutError()
242-
243-
def _install_handlers(self) -> None:
244-
self.old_sigint = signal.signal(signal.SIGINT, self._exit_gracefully)
245-
self.old_sigterm = signal.signal(signal.SIGTERM, self._exit_gracefully)
246-
247-
def _uninstall_handlers(self) -> None:
248-
signal.signal(signal.SIGINT, self.old_sigint)
249-
signal.signal(signal.SIGTERM, self.old_sigterm)
250-
251-
def _exit_gracefully(self, signum: Any, frame: Any) -> None:
252-
logger.debug("Exiting gracefully.")
253-
sys.exit(-1)
254-
255214
def __exit__(self, exc_type: None, exc_val: None, exc_tb: None) -> None:
256215
"""Exit Dojo.
257216
@@ -261,12 +220,8 @@ def __exit__(self, exc_type: None, exc_val: None, exc_tb: None) -> None:
261220
exc_tb (None): _description_
262221
"""
263222
logger.debug("Cleaning up.")
264-
self._cancel_timer()
265-
try:
266-
_kill_descendants(psutil.Process(self.proc.pid))
267-
self.modified_file.__exit__(exc_type, exc_val, exc_tb)
268-
finally:
269-
self._uninstall_handlers()
223+
kill_descendants(self.proc.pid)
224+
self.modified_file.__exit__(exc_type, exc_val, exc_tb)
270225

271226
def _post_process(self, tactic_state: str) -> str:
272227
"""Post-process the pretty-printed tactic state.
@@ -372,8 +327,6 @@ def run_tac(self, state: TacticState, tactic: str) -> TacticResult:
372327
if res["error"] is not None:
373328
if "proof contains `sorry`" in res["error"]:
374329
return ProofGivenUp()
375-
elif "try_for_time tactic failed, timeout" in res["error"]:
376-
return TimeoutError(res["error"].strip())
377330
else:
378331
return LeanError(res["error"].strip())
379332
elif res["tacticState"] == "no goals":
@@ -415,11 +368,9 @@ def _submit_request(self, req: str) -> Dict[str, Any]:
415368
Returns:
416369
Dict[str, Any]: _description_
417370
"""
418-
if self.proc.stdin is None:
419-
raise RuntimeError("self.proc.stdin is not initialized")
420371
self._check_alive()
421372
logger.debug(req)
422-
self.proc.stdin.write(req + "\n")
373+
self.proc.sendline(req)
423374
try:
424375
res, msg = self._read_next_line()
425376
except EOFError:
@@ -433,10 +384,11 @@ def _submit_request(self, req: str) -> Dict[str, Any]:
433384
return result
434385

435386
def _check_alive(self) -> None:
436-
exit_code = self.proc.poll()
437-
if exit_code is None:
387+
if self.proc.isalive():
438388
return
439-
elif exit_code == 137:
389+
exit_code = self.proc.exitstatus
390+
assert exit_code is not None
391+
if exit_code == 137:
440392
raise DojoCrashError("OOM")
441393
else:
442394
raise DojoCrashError(f"Unknown exit code: {exit_code}")
@@ -452,28 +404,23 @@ def _read_next_line(self) -> Tuple[str, str]:
452404
Returns:
453405
str: _description_
454406
"""
455-
if self.proc.stdout is None:
456-
raise RuntimeError("self.proc.stout is not initialized")
407+
_REPL_PROMPT = "REPL>"
457408
msg: List[str] = []
458409
while True:
459-
line = self.proc.stdout.readline().strip()
460-
logger.debug(line)
461-
if line == "":
462-
raise EOFError
463-
if line.startswith(_REPL_PROMPT):
410+
try:
411+
index = self.proc.expect(["\n", f"{_REPL_PROMPT}.*?\n"])
412+
if index == 0:
413+
if self.proc.before == "":
414+
raise EOFError
415+
else:
416+
msg.append(self.proc.before.strip())
417+
continue
464418
self._check_alive()
465-
return line[len(_REPL_PROMPT) :].strip(), "\n".join(msg)
466-
elif "error: " in line:
467-
if (
468-
"error: deep recursion was detected" in line
469-
or "error: [fatal] not_a_theorem" in line
470-
):
471-
self.is_crashed = True
472-
raise DojoCrashError(line)
473-
elif "error: unknown package" in line:
474-
self.is_crashed = True
475-
raise DojoInitError(line)
476-
else:
477-
pass
478-
else:
479-
msg.append(line)
419+
res = self.proc.match.string[len(_REPL_PROMPT) :].strip()
420+
return res, "\n".join(msg) + self.proc.before
421+
except pexpect.EOF:
422+
raise EOFError
423+
except pexpect.TIMEOUT:
424+
logger.debug(f"Tactic timed out")
425+
self.has_timedout = True
426+
raise DojoTacticTimeoutError()

tests/interaction/test_timeout.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,6 @@ def test_timeout_1(lean4_example_repo: LeanGitRepo) -> None:
99
"Lean4Example.lean",
1010
"hello_world",
1111
)
12-
with Dojo(thm, hard_timeout=10) as (dojo, init_state):
13-
with pytest.raises(DojoHardTimeoutError):
12+
with Dojo(thm) as (dojo, init_state):
13+
with pytest.raises(DojoTacticTimeoutError):
1414
dojo.run_tac(init_state, "sleep 99999999999999")

0 commit comments

Comments
 (0)