11import re
22import os
3- import sys
43import json
54import time
6- import shlex
7- import signal
85import psutil
6+ import pexpect
97import tempfile
10- import subprocess
118from pathlib import Path
129from loguru import logger
1310from dataclasses import dataclass , field
2118from ..data_extraction .traced_data import TracedFile , get_code_without_comments
2219
2320
24- _REPL_PROMPT = "REPL>"
25-
26-
2721@dataclass (frozen = True )
2822class 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
9488class 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+
9899def _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 ()
0 commit comments