leanback 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.
- leanback/README.md +19 -0
- leanback/__init__.py +3 -0
- leanback/check.py +365 -0
- leanback/common/__init__.py +0 -0
- leanback/common/env.py +91 -0
- leanback/common/errors.py +517 -0
- leanback/common/path_manager.py +191 -0
- leanback/common/project_context.py +276 -0
- leanback/common/util.py +72 -0
- leanback/goals.py +167 -0
- leanback/mathlib_search.py +190 -0
- leanback/mathlib_search_simple.py +221 -0
- leanback/mcp_server.py +1179 -0
- leanback/mcp_test.py +496 -0
- leanback/prove.py +87 -0
- leanback-0.1.0.dist-info/METADATA +154 -0
- leanback-0.1.0.dist-info/RECORD +20 -0
- leanback-0.1.0.dist-info/WHEEL +4 -0
- leanback-0.1.0.dist-info/entry_points.txt +3 -0
- leanback-0.1.0.dist-info/licenses/LICENSE +191 -0
leanback/mcp_test.py
ADDED
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""E2E test client for LeanBack MCP server."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import sys
|
|
9
|
+
import traceback
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from mcp.client.session import ClientSession
|
|
13
|
+
from mcp.client.stdio import StdioServerParameters, stdio_client
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# Configure logging
|
|
17
|
+
logging.basicConfig(
|
|
18
|
+
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
|
|
19
|
+
)
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
async def test_check_tool(session: ClientSession, project: str):
|
|
24
|
+
"""Test the check tool."""
|
|
25
|
+
logger.info("Testing check tool...")
|
|
26
|
+
|
|
27
|
+
# Test 1: Check entire project
|
|
28
|
+
logger.info(f" Test 1: Checking entire project '{project}'")
|
|
29
|
+
try:
|
|
30
|
+
result = await session.call_tool("check", {"project": project})
|
|
31
|
+
content = result.content[0] if result.content else None
|
|
32
|
+
if content and hasattr(content, "text"):
|
|
33
|
+
data = json.loads(content.text)
|
|
34
|
+
logger.info(f" Result: {json.dumps(data, indent=2)}")
|
|
35
|
+
else:
|
|
36
|
+
logger.info(f" Raw result: {result}")
|
|
37
|
+
except Exception as e:
|
|
38
|
+
logger.error(f" Failed: {e}")
|
|
39
|
+
traceback.print_exc()
|
|
40
|
+
|
|
41
|
+
# Test 2: Check specific file
|
|
42
|
+
logger.info(" Test 2: Checking specific file 'test_simple_proof.lean'")
|
|
43
|
+
try:
|
|
44
|
+
result = await session.call_tool(
|
|
45
|
+
"check", {"project": project, "file": "test_simple_proof.lean"}
|
|
46
|
+
)
|
|
47
|
+
content = result.content[0] if result.content else None
|
|
48
|
+
if content and hasattr(content, "text"):
|
|
49
|
+
data = json.loads(content.text)
|
|
50
|
+
logger.info(f" Result: {json.dumps(data, indent=2)}")
|
|
51
|
+
except Exception as e:
|
|
52
|
+
logger.error(f" Failed: {e}")
|
|
53
|
+
|
|
54
|
+
# Test 3: Check expression
|
|
55
|
+
logger.info(" Test 3: Checking expression '#check 2 + 2 = 4'")
|
|
56
|
+
try:
|
|
57
|
+
result = await session.call_tool(
|
|
58
|
+
"check", {"project": project, "expr": "#check 2 + 2 = 4"}
|
|
59
|
+
)
|
|
60
|
+
content = result.content[0] if result.content else None
|
|
61
|
+
if content and hasattr(content, "text"):
|
|
62
|
+
data = json.loads(content.text)
|
|
63
|
+
logger.info(f" Result: {json.dumps(data, indent=2)}")
|
|
64
|
+
except Exception as e:
|
|
65
|
+
logger.error(f" Failed: {e}")
|
|
66
|
+
|
|
67
|
+
logger.info("✓ Check tool tests completed")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
async def test_prove_tool(session: ClientSession, project: str):
|
|
71
|
+
"""Test the prove tool."""
|
|
72
|
+
logger.info("Testing prove tool...")
|
|
73
|
+
|
|
74
|
+
# Test 1: Simple theorem with tactic
|
|
75
|
+
logger.info(" Test 1: Proving simple theorem '2 + 2 = 4'")
|
|
76
|
+
try:
|
|
77
|
+
result = await session.call_tool(
|
|
78
|
+
"prove", {"project": project, "theorem": "2 + 2 = 4", "tactics": ["rfl"]}
|
|
79
|
+
)
|
|
80
|
+
content = result.content[0] if result.content else None
|
|
81
|
+
if content and hasattr(content, "text"):
|
|
82
|
+
data = json.loads(content.text)
|
|
83
|
+
logger.info(f" Result: {json.dumps(data, indent=2)}")
|
|
84
|
+
except Exception as e:
|
|
85
|
+
logger.error(f" Failed: {e}")
|
|
86
|
+
|
|
87
|
+
# Test 2: Verify proof file
|
|
88
|
+
logger.info(" Test 2: Verifying proof file 'test_simple_proof.lean'")
|
|
89
|
+
try:
|
|
90
|
+
result = await session.call_tool(
|
|
91
|
+
"prove", {"project": project, "file": "test_simple_proof.lean"}
|
|
92
|
+
)
|
|
93
|
+
content = result.content[0] if result.content else None
|
|
94
|
+
if content and hasattr(content, "text"):
|
|
95
|
+
data = json.loads(content.text)
|
|
96
|
+
logger.info(f" Result: {json.dumps(data, indent=2)}")
|
|
97
|
+
except Exception as e:
|
|
98
|
+
logger.error(f" Failed: {e}")
|
|
99
|
+
|
|
100
|
+
# Test 3: Theorem with mathlib
|
|
101
|
+
logger.info(" Test 3: Proving theorem with mathlib")
|
|
102
|
+
try:
|
|
103
|
+
result = await session.call_tool(
|
|
104
|
+
"prove",
|
|
105
|
+
{
|
|
106
|
+
"project": project,
|
|
107
|
+
"theorem": "∀ n : Nat, n + 0 = n",
|
|
108
|
+
"tactics": ["intro n", "rfl"],
|
|
109
|
+
"mathlib": True,
|
|
110
|
+
},
|
|
111
|
+
)
|
|
112
|
+
content = result.content[0] if result.content else None
|
|
113
|
+
if content and hasattr(content, "text"):
|
|
114
|
+
data = json.loads(content.text)
|
|
115
|
+
logger.info(f" Result: {json.dumps(data, indent=2)}")
|
|
116
|
+
except Exception as e:
|
|
117
|
+
logger.error(f" Failed: {e}")
|
|
118
|
+
|
|
119
|
+
logger.info("✓ Prove tool tests completed")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
async def test_eval_tool(session: ClientSession, project: str):
|
|
123
|
+
"""Test the eval tool."""
|
|
124
|
+
logger.info("Testing eval tool...")
|
|
125
|
+
|
|
126
|
+
# Test 1: Evaluate expression
|
|
127
|
+
logger.info(" Test 1: Evaluating expression '#eval 2 + 2'")
|
|
128
|
+
try:
|
|
129
|
+
result = await session.call_tool(
|
|
130
|
+
"eval", {"project": project, "code": "#eval 2 + 2"}
|
|
131
|
+
)
|
|
132
|
+
content = result.content[0] if result.content else None
|
|
133
|
+
if content and hasattr(content, "text"):
|
|
134
|
+
data = json.loads(content.text)
|
|
135
|
+
logger.info(f" Result: {json.dumps(data, indent=2)}")
|
|
136
|
+
except Exception as e:
|
|
137
|
+
logger.error(f" Failed: {e}")
|
|
138
|
+
|
|
139
|
+
# Test 2: Check type
|
|
140
|
+
logger.info(" Test 2: Checking type '#check Nat.add'")
|
|
141
|
+
try:
|
|
142
|
+
result = await session.call_tool(
|
|
143
|
+
"eval", {"project": project, "code": "#check Nat.add"}
|
|
144
|
+
)
|
|
145
|
+
content = result.content[0] if result.content else None
|
|
146
|
+
if content and hasattr(content, "text"):
|
|
147
|
+
data = json.loads(content.text)
|
|
148
|
+
logger.info(f" Result: {json.dumps(data, indent=2)}")
|
|
149
|
+
except Exception as e:
|
|
150
|
+
logger.error(f" Failed: {e}")
|
|
151
|
+
|
|
152
|
+
# Test 3: Evaluate with mathlib
|
|
153
|
+
logger.info(" Test 3: Evaluating with mathlib")
|
|
154
|
+
try:
|
|
155
|
+
result = await session.call_tool(
|
|
156
|
+
"eval",
|
|
157
|
+
{
|
|
158
|
+
"project": project,
|
|
159
|
+
"code": "example : ∃ x : Nat, x > 0 := by use 1; simp",
|
|
160
|
+
"mathlib": True,
|
|
161
|
+
},
|
|
162
|
+
)
|
|
163
|
+
content = result.content[0] if result.content else None
|
|
164
|
+
if content and hasattr(content, "text"):
|
|
165
|
+
data = json.loads(content.text)
|
|
166
|
+
logger.info(f" Result: {json.dumps(data, indent=2)}")
|
|
167
|
+
except Exception as e:
|
|
168
|
+
logger.error(f" Failed: {e}")
|
|
169
|
+
|
|
170
|
+
logger.info("✓ Eval tool tests completed")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
async def test_search_tool(session: ClientSession, project: str):
|
|
174
|
+
"""Test the search tool."""
|
|
175
|
+
logger.info("Testing search tool...")
|
|
176
|
+
|
|
177
|
+
# Test 1: Search for a pattern
|
|
178
|
+
logger.info(" Test 1: Searching for 'add_comm'")
|
|
179
|
+
try:
|
|
180
|
+
result = await session.call_tool(
|
|
181
|
+
"search", {"project": project, "pattern": "add_comm", "max_results": 5}
|
|
182
|
+
)
|
|
183
|
+
content = result.content[0] if result.content else None
|
|
184
|
+
if content and hasattr(content, "text"):
|
|
185
|
+
data = json.loads(content.text)
|
|
186
|
+
logger.info(f" Result: Found {len(data.get('results', []))} matches")
|
|
187
|
+
if data.get("results"):
|
|
188
|
+
logger.info(f" First match: {data['results'][0]['name']}")
|
|
189
|
+
except Exception as e:
|
|
190
|
+
logger.error(f" Failed: {e}")
|
|
191
|
+
|
|
192
|
+
# Test 2: Batch search
|
|
193
|
+
logger.info(" Test 2: Batch searching for 'add_comm,mul_comm,sub_eq_add_neg'")
|
|
194
|
+
try:
|
|
195
|
+
result = await session.call_tool(
|
|
196
|
+
"search",
|
|
197
|
+
{
|
|
198
|
+
"project": project,
|
|
199
|
+
"batch": "add_comm,mul_comm,sub_eq_add_neg",
|
|
200
|
+
"max_results": 3,
|
|
201
|
+
},
|
|
202
|
+
)
|
|
203
|
+
content = result.content[0] if result.content else None
|
|
204
|
+
if content and hasattr(content, "text"):
|
|
205
|
+
data = json.loads(content.text)
|
|
206
|
+
logger.info(f" Result: {json.dumps(data, indent=2)}")
|
|
207
|
+
except Exception as e:
|
|
208
|
+
logger.error(f" Failed: {e}")
|
|
209
|
+
|
|
210
|
+
# Test 3: Filtered search
|
|
211
|
+
logger.info(" Test 3: Searching for 'reverse' in List namespace")
|
|
212
|
+
try:
|
|
213
|
+
result = await session.call_tool(
|
|
214
|
+
"search",
|
|
215
|
+
{
|
|
216
|
+
"project": project,
|
|
217
|
+
"pattern": "reverse",
|
|
218
|
+
"filter_namespace": "List",
|
|
219
|
+
"max_results": 5,
|
|
220
|
+
},
|
|
221
|
+
)
|
|
222
|
+
content = result.content[0] if result.content else None
|
|
223
|
+
if content and hasattr(content, "text"):
|
|
224
|
+
data = json.loads(content.text)
|
|
225
|
+
logger.info(
|
|
226
|
+
f" Result: Found {len(data.get('results', []))} matches in List namespace"
|
|
227
|
+
)
|
|
228
|
+
except Exception as e:
|
|
229
|
+
logger.error(f" Failed: {e}")
|
|
230
|
+
|
|
231
|
+
logger.info("✓ Search tool tests completed")
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
async def test_goals_tool(session: ClientSession, project: str):
|
|
235
|
+
"""Test the goals tool."""
|
|
236
|
+
logger.info("Testing goals tool...")
|
|
237
|
+
|
|
238
|
+
# Test 1: Initial goal (no tactics)
|
|
239
|
+
logger.info(" Test 1: Initial goal for '∀ n : Nat, n + 0 = n'")
|
|
240
|
+
try:
|
|
241
|
+
result = await session.call_tool(
|
|
242
|
+
"goals",
|
|
243
|
+
{"project": project, "theorem": "∀ n : Nat, n + 0 = n"},
|
|
244
|
+
)
|
|
245
|
+
content = result.content[0] if result.content else None
|
|
246
|
+
if content and hasattr(content, "text"):
|
|
247
|
+
data = json.loads(content.text)
|
|
248
|
+
logger.info(f" Result: {json.dumps(data, indent=2)}")
|
|
249
|
+
except Exception as e:
|
|
250
|
+
logger.error(f" Failed: {e}")
|
|
251
|
+
|
|
252
|
+
# Test 2: Partial tactics (goals remain)
|
|
253
|
+
logger.info(" Test 2: After 'intro n'")
|
|
254
|
+
try:
|
|
255
|
+
result = await session.call_tool(
|
|
256
|
+
"goals",
|
|
257
|
+
{
|
|
258
|
+
"project": project,
|
|
259
|
+
"theorem": "∀ n : Nat, n + 0 = n",
|
|
260
|
+
"tactics": ["intro n"],
|
|
261
|
+
},
|
|
262
|
+
)
|
|
263
|
+
content = result.content[0] if result.content else None
|
|
264
|
+
if content and hasattr(content, "text"):
|
|
265
|
+
data = json.loads(content.text)
|
|
266
|
+
logger.info(f" Result: {json.dumps(data, indent=2)}")
|
|
267
|
+
except Exception as e:
|
|
268
|
+
logger.error(f" Failed: {e}")
|
|
269
|
+
|
|
270
|
+
# Test 3: Complete proof
|
|
271
|
+
logger.info(" Test 3: Complete proof with simp")
|
|
272
|
+
try:
|
|
273
|
+
result = await session.call_tool(
|
|
274
|
+
"goals",
|
|
275
|
+
{
|
|
276
|
+
"project": project,
|
|
277
|
+
"theorem": "∀ n : Nat, n + 0 = n",
|
|
278
|
+
"tactics": ["intro n", "simp"],
|
|
279
|
+
"mathlib": True,
|
|
280
|
+
},
|
|
281
|
+
)
|
|
282
|
+
content = result.content[0] if result.content else None
|
|
283
|
+
if content and hasattr(content, "text"):
|
|
284
|
+
data = json.loads(content.text)
|
|
285
|
+
logger.info(f" Result: {json.dumps(data, indent=2)}")
|
|
286
|
+
except Exception as e:
|
|
287
|
+
logger.error(f" Failed: {e}")
|
|
288
|
+
|
|
289
|
+
# Test 4: Tactic failure
|
|
290
|
+
logger.info(" Test 4: Failing tactic (rfl on non-reflexive goal)")
|
|
291
|
+
try:
|
|
292
|
+
result = await session.call_tool(
|
|
293
|
+
"goals",
|
|
294
|
+
{
|
|
295
|
+
"project": project,
|
|
296
|
+
"theorem": "∀ n m : Nat, n + m = m + n",
|
|
297
|
+
"tactics": ["intro n", "intro m", "rfl"],
|
|
298
|
+
},
|
|
299
|
+
)
|
|
300
|
+
content = result.content[0] if result.content else None
|
|
301
|
+
if content and hasattr(content, "text"):
|
|
302
|
+
data = json.loads(content.text)
|
|
303
|
+
logger.info(f" Result: {json.dumps(data, indent=2)}")
|
|
304
|
+
except Exception as e:
|
|
305
|
+
logger.error(f" Failed: {e}")
|
|
306
|
+
|
|
307
|
+
# Test 5: Multiple goals (constructor)
|
|
308
|
+
logger.info(" Test 5: Multiple goals with constructor")
|
|
309
|
+
try:
|
|
310
|
+
result = await session.call_tool(
|
|
311
|
+
"goals",
|
|
312
|
+
{
|
|
313
|
+
"project": project,
|
|
314
|
+
"theorem": "True ∧ True",
|
|
315
|
+
"tactics": ["constructor"],
|
|
316
|
+
},
|
|
317
|
+
)
|
|
318
|
+
content = result.content[0] if result.content else None
|
|
319
|
+
if content and hasattr(content, "text"):
|
|
320
|
+
data = json.loads(content.text)
|
|
321
|
+
logger.info(f" Result: {json.dumps(data, indent=2)}")
|
|
322
|
+
except Exception as e:
|
|
323
|
+
logger.error(f" Failed: {e}")
|
|
324
|
+
|
|
325
|
+
# Test 6: Induction (case labels)
|
|
326
|
+
logger.info(" Test 6: Induction goals")
|
|
327
|
+
try:
|
|
328
|
+
result = await session.call_tool(
|
|
329
|
+
"goals",
|
|
330
|
+
{
|
|
331
|
+
"project": project,
|
|
332
|
+
"theorem": "∀ n : Nat, 0 + n = n",
|
|
333
|
+
"tactics": ["intro n", "induction n"],
|
|
334
|
+
},
|
|
335
|
+
)
|
|
336
|
+
content = result.content[0] if result.content else None
|
|
337
|
+
if content and hasattr(content, "text"):
|
|
338
|
+
data = json.loads(content.text)
|
|
339
|
+
logger.info(f" Result: {json.dumps(data, indent=2)}")
|
|
340
|
+
except Exception as e:
|
|
341
|
+
logger.error(f" Failed: {e}")
|
|
342
|
+
|
|
343
|
+
# Test 7: Sorry
|
|
344
|
+
logger.info(" Test 7: Sorry usage")
|
|
345
|
+
try:
|
|
346
|
+
result = await session.call_tool(
|
|
347
|
+
"goals",
|
|
348
|
+
{
|
|
349
|
+
"project": project,
|
|
350
|
+
"theorem": "∀ n : Nat, n + 0 = n",
|
|
351
|
+
"tactics": ["intro n", "sorry"],
|
|
352
|
+
},
|
|
353
|
+
)
|
|
354
|
+
content = result.content[0] if result.content else None
|
|
355
|
+
if content and hasattr(content, "text"):
|
|
356
|
+
data = json.loads(content.text)
|
|
357
|
+
logger.info(f" Result: {json.dumps(data, indent=2)}")
|
|
358
|
+
except Exception as e:
|
|
359
|
+
logger.error(f" Failed: {e}")
|
|
360
|
+
|
|
361
|
+
logger.info("✓ Goals tool tests completed")
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
async def show_tool_descriptions():
|
|
365
|
+
"""Connect to the MCP server and display tool descriptions."""
|
|
366
|
+
server_params = StdioServerParameters(
|
|
367
|
+
command="uv",
|
|
368
|
+
args=["run", "python", "-m", "leanback.mcp_server"],
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
try:
|
|
372
|
+
logger.info("Connecting to LeanBack MCP server...")
|
|
373
|
+
async with stdio_client(server_params) as (read, write):
|
|
374
|
+
async with ClientSession(read, write) as session:
|
|
375
|
+
await session.initialize()
|
|
376
|
+
logger.info("✓ Connected to server\n")
|
|
377
|
+
|
|
378
|
+
try:
|
|
379
|
+
tools_result = await session.list_tools()
|
|
380
|
+
for tool in tools_result.tools:
|
|
381
|
+
print(f"=== Tool: {tool.name} ===")
|
|
382
|
+
print(f"\n{tool.description}")
|
|
383
|
+
print("\n" + "=" * 50 + "\n")
|
|
384
|
+
except Exception as e:
|
|
385
|
+
logger.error(f"Failed to list tools: {e}")
|
|
386
|
+
raise
|
|
387
|
+
|
|
388
|
+
except Exception as e:
|
|
389
|
+
logger.error(f"Failed to connect: {e}")
|
|
390
|
+
raise
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
async def run_tests(project: str, tool: str | None):
|
|
394
|
+
"""Run tests against the MCP server."""
|
|
395
|
+
server_params = StdioServerParameters(
|
|
396
|
+
command="uv",
|
|
397
|
+
args=["run", "python", "-m", "leanback.mcp_server"],
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
try:
|
|
401
|
+
logger.info("Connecting to LeanBack MCP server...")
|
|
402
|
+
async with stdio_client(server_params) as (read, write):
|
|
403
|
+
async with ClientSession(read, write) as session:
|
|
404
|
+
await session.initialize()
|
|
405
|
+
logger.info("✓ Connected to server")
|
|
406
|
+
|
|
407
|
+
# List available tools
|
|
408
|
+
try:
|
|
409
|
+
tools_result = await session.list_tools()
|
|
410
|
+
tool_names = [t.name for t in tools_result.tools]
|
|
411
|
+
logger.info(f"Available tools: {tool_names}")
|
|
412
|
+
except Exception as e:
|
|
413
|
+
logger.error(f"Failed to list tools: {e}")
|
|
414
|
+
raise
|
|
415
|
+
|
|
416
|
+
# Run tests based on specified tool
|
|
417
|
+
if tool is None or tool == "check":
|
|
418
|
+
await test_check_tool(session, project)
|
|
419
|
+
if tool is None or tool == "prove":
|
|
420
|
+
await test_prove_tool(session, project)
|
|
421
|
+
if tool is None or tool == "goals":
|
|
422
|
+
await test_goals_tool(session, project)
|
|
423
|
+
if tool is None or tool == "eval":
|
|
424
|
+
await test_eval_tool(session, project)
|
|
425
|
+
if tool is None or tool == "search":
|
|
426
|
+
await test_search_tool(session, project)
|
|
427
|
+
|
|
428
|
+
except Exception as e:
|
|
429
|
+
logger.error(f"Test failed: {e}")
|
|
430
|
+
raise
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def main():
|
|
434
|
+
"""Main entry point for the test client."""
|
|
435
|
+
parser = argparse.ArgumentParser(description="LeanBack MCP Server Test Client")
|
|
436
|
+
parser.add_argument(
|
|
437
|
+
"--project",
|
|
438
|
+
"-p",
|
|
439
|
+
type=str,
|
|
440
|
+
required=False,
|
|
441
|
+
help="Absolute path to a Lean project for testing (required for tool tests)",
|
|
442
|
+
)
|
|
443
|
+
parser.add_argument(
|
|
444
|
+
"--tool",
|
|
445
|
+
type=str,
|
|
446
|
+
choices=["check", "prove", "goals", "eval", "search"],
|
|
447
|
+
help="Test specific tool only",
|
|
448
|
+
)
|
|
449
|
+
parser.add_argument(
|
|
450
|
+
"--debug",
|
|
451
|
+
action="store_true",
|
|
452
|
+
help="Enable debug logging",
|
|
453
|
+
)
|
|
454
|
+
parser.add_argument(
|
|
455
|
+
"--tool-descriptions",
|
|
456
|
+
action="store_true",
|
|
457
|
+
help="Display tool descriptions and exit",
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
args = parser.parse_args()
|
|
461
|
+
|
|
462
|
+
if args.debug:
|
|
463
|
+
logging.getLogger().setLevel(logging.DEBUG)
|
|
464
|
+
|
|
465
|
+
# Show tool descriptions (no project needed)
|
|
466
|
+
if args.tool_descriptions:
|
|
467
|
+
try:
|
|
468
|
+
asyncio.run(show_tool_descriptions())
|
|
469
|
+
except Exception:
|
|
470
|
+
logger.error("Failed to show tool descriptions!")
|
|
471
|
+
sys.exit(1)
|
|
472
|
+
return
|
|
473
|
+
|
|
474
|
+
# For tool tests, project is required
|
|
475
|
+
if not args.project:
|
|
476
|
+
logger.error("--project is required for tool tests")
|
|
477
|
+
parser.print_usage()
|
|
478
|
+
sys.exit(1)
|
|
479
|
+
|
|
480
|
+
# Validate project path
|
|
481
|
+
project_path = Path(args.project).resolve()
|
|
482
|
+
if not project_path.exists():
|
|
483
|
+
logger.error(f"Project directory does not exist: {project_path}")
|
|
484
|
+
sys.exit(1)
|
|
485
|
+
|
|
486
|
+
# Run tests
|
|
487
|
+
try:
|
|
488
|
+
asyncio.run(run_tests(str(project_path), args.tool))
|
|
489
|
+
logger.info("All tests passed!")
|
|
490
|
+
except Exception:
|
|
491
|
+
logger.error("Tests failed!")
|
|
492
|
+
sys.exit(1)
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
if __name__ == "__main__":
|
|
496
|
+
main()
|
leanback/prove.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Implementation of the leanback-prove tool.
|
|
3
|
+
|
|
4
|
+
This module provides functionality for verifying Lean proofs.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
|
|
12
|
+
from leanback.common.errors import LeanError
|
|
13
|
+
from leanback.common.util import build_proof_content, create_temp_file
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def verify_proof(
|
|
17
|
+
theorem: str,
|
|
18
|
+
tactics: list[str] | None = None,
|
|
19
|
+
proof_file: str | Path | None = None,
|
|
20
|
+
imports: list[str] | None = None,
|
|
21
|
+
project_path: str | Path | None = None,
|
|
22
|
+
console: Console | None = None,
|
|
23
|
+
) -> tuple[bool, list[LeanError]]:
|
|
24
|
+
"""
|
|
25
|
+
Verify a proof without interactive mode.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
theorem: The theorem statement to prove
|
|
29
|
+
tactics: List of tactics to apply
|
|
30
|
+
proof_file: Path to a file containing the proof
|
|
31
|
+
imports: List of modules to import
|
|
32
|
+
project_path: Path to the Lean project
|
|
33
|
+
console: Rich console for output
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Tuple of (success, errors)
|
|
37
|
+
"""
|
|
38
|
+
if console is None:
|
|
39
|
+
console = Console()
|
|
40
|
+
|
|
41
|
+
if project_path and isinstance(project_path, str):
|
|
42
|
+
project_path = Path(project_path)
|
|
43
|
+
|
|
44
|
+
# If proof_file is provided, read the proof from file
|
|
45
|
+
if proof_file:
|
|
46
|
+
proof_file = Path(proof_file) if isinstance(proof_file, str) else proof_file
|
|
47
|
+
if not proof_file.exists():
|
|
48
|
+
return False, [
|
|
49
|
+
LeanError(
|
|
50
|
+
file_name=str(proof_file),
|
|
51
|
+
line=0,
|
|
52
|
+
column=0,
|
|
53
|
+
severity="error",
|
|
54
|
+
message=f"Proof file '{proof_file}' does not exist",
|
|
55
|
+
)
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
# Run Lean on the file directly
|
|
59
|
+
from leanback.check import check_file
|
|
60
|
+
|
|
61
|
+
return check_file(
|
|
62
|
+
file_path=proof_file, project_path=project_path, console=console
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Otherwise, construct a proof from theorem and tactics
|
|
66
|
+
content, _ = build_proof_content(
|
|
67
|
+
theorem=theorem,
|
|
68
|
+
tactics=tactics or [],
|
|
69
|
+
imports=imports or [],
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Create a temporary file
|
|
73
|
+
temp_file = create_temp_file(content)
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
# Check the temporary file
|
|
77
|
+
from leanback.check import check_file
|
|
78
|
+
|
|
79
|
+
return check_file(
|
|
80
|
+
file_path=temp_file, project_path=project_path, console=console
|
|
81
|
+
)
|
|
82
|
+
finally:
|
|
83
|
+
# Clean up
|
|
84
|
+
try:
|
|
85
|
+
os.unlink(temp_file)
|
|
86
|
+
except OSError:
|
|
87
|
+
pass
|