22from __future__ import annotations
33
44import hashlib
5+ import os
56import re
7+ import signal
68import subprocess
79import tempfile
10+ import time
811from dataclasses import dataclass
912from pathlib import Path
1013
@@ -28,7 +31,19 @@ class LeanSignatureResult:
2831 source : str
2932 signature_hash : str
3033 ok : bool
34+ status : str = "FORMALIZED"
3135 error : str = ""
36+ attempts : int = 1
37+ elapsed_s : float = 0.0
38+ output : str = ""
39+
40+
41+ @dataclass (frozen = True )
42+ class _LeanRun :
43+ returncode : int | None
44+ timed_out : bool
45+ elapsed_s : float
46+ output : str
3247
3348
3449def extract_lean_signature_blocks (text : str ) -> list [tuple [str , str ]]:
@@ -43,38 +58,159 @@ def _signature_only(source: str) -> str:
4358 return source [:match .start ()].strip () if match else source .strip ()
4459
4560
61+ def _run_lean (
62+ content : str ,
63+ * ,
64+ project_root : Path ,
65+ timeout_s : float ,
66+ ) -> _LeanRun :
67+ started = time .monotonic ()
68+ with tempfile .NamedTemporaryFile (
69+ mode = "w" ,
70+ suffix = ".lean" ,
71+ encoding = "utf-8" ,
72+ delete = False ,
73+ ) as handle :
74+ handle .write (content )
75+ path = Path (handle .name )
76+ process = None
77+ try :
78+ process = subprocess .Popen (
79+ ["lake" , "env" , "lean" , str (path )],
80+ cwd = project_root ,
81+ stdout = subprocess .PIPE ,
82+ stderr = subprocess .STDOUT ,
83+ text = True ,
84+ start_new_session = True ,
85+ )
86+ try :
87+ output , _ = process .communicate (timeout = timeout_s )
88+ return _LeanRun (
89+ process .returncode ,
90+ False ,
91+ time .monotonic () - started ,
92+ output or "" ,
93+ )
94+ except subprocess .TimeoutExpired as exc :
95+ partial = (
96+ exc .stdout .decode (errors = "replace" )
97+ if isinstance (exc .stdout , bytes )
98+ else (exc .stdout or "" )
99+ )
100+ try :
101+ os .killpg (process .pid , signal .SIGKILL )
102+ except (OSError , ProcessLookupError ):
103+ process .kill ()
104+ remainder , _ = process .communicate ()
105+ return _LeanRun (
106+ None ,
107+ True ,
108+ time .monotonic () - started ,
109+ partial + (remainder or "" ),
110+ )
111+ except OSError as exc :
112+ return _LeanRun (
113+ None ,
114+ False ,
115+ time .monotonic () - started ,
116+ f"{ type (exc ).__name__ } : { exc } " ,
117+ )
118+ finally :
119+ if process is not None and process .poll () is None :
120+ process .kill ()
121+ process .wait ()
122+ path .unlink (missing_ok = True )
123+
124+
125+ def warm_lean_environment (
126+ project_root : Path ,
127+ * ,
128+ timeout_s : float = 120.0 ,
129+ ) -> LeanSignatureResult :
130+ source = "theorem kakeyaLeanWarmup : True := by trivial"
131+ content = (
132+ "import KakeyaLeanGate\n \n "
133+ "set_option autoImplicit false\n \n "
134+ + source
135+ + "\n "
136+ )
137+ run = _run_lean (
138+ content ,
139+ project_root = project_root ,
140+ timeout_s = timeout_s ,
141+ )
142+ if run .timed_out :
143+ return LeanSignatureResult (
144+ source ,
145+ "" ,
146+ False ,
147+ status = "TYPECHECK_TIMEOUT" ,
148+ error = f"Lean warmup timed out after { timeout_s :.1f} s" ,
149+ elapsed_s = run .elapsed_s ,
150+ output = run .output ,
151+ )
152+ if run .returncode != 0 :
153+ return LeanSignatureResult (
154+ source ,
155+ "" ,
156+ False ,
157+ status = "ENVIRONMENT_FAILED" ,
158+ error = f"Lean warmup failed: { run .output [- 2000 :]} " ,
159+ elapsed_s = run .elapsed_s ,
160+ output = run .output ,
161+ )
162+ return LeanSignatureResult (
163+ source ,
164+ "" ,
165+ True ,
166+ status = "ENVIRONMENT_READY" ,
167+ elapsed_s = run .elapsed_s ,
168+ output = run .output ,
169+ )
170+
171+
46172def validate_lean_signature (
47173 source : str ,
48174 * ,
49175 project_root : Path ,
50- timeout_s : float = 30.0 ,
176+ timeout_s : float = 45.0 ,
177+ retry_timeout_s : float = 120.0 ,
51178) -> LeanSignatureResult :
52179 source = source .strip ()
53180 if not source :
54- return LeanSignatureResult ("" , "" , False , "empty Lean signature" )
181+ return LeanSignatureResult (
182+ "" , "" , False , status = "TYPECHECK_FAILED" ,
183+ error = "empty Lean signature" ,
184+ )
55185 if len (source ) > 12_000 :
56- return LeanSignatureResult ("" , "" , False , "Lean signature too large" )
186+ return LeanSignatureResult (
187+ "" , "" , False , status = "TYPECHECK_FAILED" ,
188+ error = "Lean signature too large" ,
189+ )
57190 if _FORBIDDEN .search (source ):
58191 return LeanSignatureResult (
59192 source ,
60193 "" ,
61194 False ,
62- "forbidden Lean command in generated signature" ,
195+ status = "UNSAFE_REJECTED" ,
196+ error = "forbidden Lean command in generated signature" ,
63197 )
64198 declarations = re .findall (r"^\s*theorem\s+([A-Za-z_][\w']*)" , source , re .MULTILINE )
65199 if len (declarations ) != 1 :
66200 return LeanSignatureResult (
67201 source ,
68202 "" ,
69203 False ,
70- "expected exactly one theorem declaration" ,
204+ status = "TYPECHECK_FAILED" ,
205+ error = "expected exactly one theorem declaration" ,
71206 )
72207 if not re .search (r"\s*:=\s*by\b" , source ):
73208 return LeanSignatureResult (
74209 source ,
75210 "" ,
76211 False ,
77- "theorem signature must end with `:= by` proof scaffold" ,
212+ status = "TYPECHECK_FAILED" ,
213+ error = "theorem signature must end with `:= by` proof scaffold" ,
78214 )
79215 signature = " " .join (_signature_only (source ).split ())
80216 signature_hash = hashlib .sha256 (signature .encode ()).hexdigest ()
@@ -84,39 +220,72 @@ def validate_lean_signature(
84220 + source
85221 + "\n "
86222 )
87- try :
88- with tempfile .NamedTemporaryFile (
89- mode = "w" ,
90- suffix = ".lean" ,
91- encoding = "utf-8" ,
92- delete = False ,
93- ) as handle :
94- handle .write (content )
95- path = Path (handle .name )
96- completed = subprocess .run (
97- ["lake" , "env" , "lean" , str (path )],
98- cwd = project_root ,
99- capture_output = True ,
100- text = True ,
101- timeout = timeout_s ,
102- check = False ,
223+ first = _run_lean (
224+ content ,
225+ project_root = project_root ,
226+ timeout_s = timeout_s ,
227+ )
228+ attempts = 1
229+ total_elapsed = first .elapsed_s
230+ output = first .output
231+ run = first
232+ if first .timed_out :
233+ warmup = warm_lean_environment (
234+ project_root ,
235+ timeout_s = retry_timeout_s ,
103236 )
104- except (OSError , subprocess .TimeoutExpired ) as exc :
237+ total_elapsed += warmup .elapsed_s
238+ output += warmup .output
239+ if not warmup .ok :
240+ return LeanSignatureResult (
241+ source ,
242+ signature_hash ,
243+ False ,
244+ status = warmup .status ,
245+ error = warmup .error ,
246+ attempts = 1 ,
247+ elapsed_s = total_elapsed ,
248+ output = output ,
249+ )
250+ run = _run_lean (
251+ content ,
252+ project_root = project_root ,
253+ timeout_s = retry_timeout_s ,
254+ )
255+ attempts = 2
256+ total_elapsed += run .elapsed_s
257+ output += run .output
258+ if run .timed_out :
105259 return LeanSignatureResult (
106260 source ,
107261 signature_hash ,
108262 False ,
109- f"Lean invocation failed: { type (exc ).__name__ } : { exc } " ,
263+ status = "TYPECHECK_TIMEOUT" ,
264+ error = (
265+ f"Lean typecheck timed out after { attempts } attempts "
266+ f"({ timeout_s :.1f} s/{ retry_timeout_s :.1f} s)"
267+ ),
268+ attempts = attempts ,
269+ elapsed_s = total_elapsed ,
270+ output = output ,
110271 )
111- finally :
112- if "path" in locals ():
113- path .unlink (missing_ok = True )
114- if completed .returncode != 0 :
115- error = (completed .stderr or completed .stdout ).strip ()
272+ if run .returncode != 0 :
116273 return LeanSignatureResult (
117274 source ,
118275 signature_hash ,
119276 False ,
120- f"Lean typecheck failed: { error [- 2000 :]} " ,
277+ status = "TYPECHECK_FAILED" ,
278+ error = f"Lean typecheck failed: { run .output [- 2000 :]} " ,
279+ attempts = attempts ,
280+ elapsed_s = total_elapsed ,
281+ output = output ,
121282 )
122- return LeanSignatureResult (source , signature_hash , True )
283+ return LeanSignatureResult (
284+ source ,
285+ signature_hash ,
286+ True ,
287+ status = "FORMALIZED" ,
288+ attempts = attempts ,
289+ elapsed_s = total_elapsed ,
290+ output = output ,
291+ )
0 commit comments