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/README.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# LeanBack Implementation
|
|
2
|
+
|
|
3
|
+
MCP server exposing Lean 4 development tools via Model Context Protocol.
|
|
4
|
+
|
|
5
|
+
## Module Structure
|
|
6
|
+
|
|
7
|
+
- `mcp_server.py`: MCP server — defines project_info, check, prove, goals, eval, search tools
|
|
8
|
+
- `check.py`: Check tool implementation
|
|
9
|
+
- `prove.py`: Prove tool implementation
|
|
10
|
+
- `goals.py`: Goals tool implementation (proof state inspection)
|
|
11
|
+
- `mathlib_search.py`: Search tool (Mathlib declaration search)
|
|
12
|
+
- `mathlib_search_simple.py`: Regex-based Mathlib search backend
|
|
13
|
+
- `mcp_test.py`: E2E test client
|
|
14
|
+
- `common/`: Shared utilities
|
|
15
|
+
- `env.py`: Lean environment handling (LeanRunner)
|
|
16
|
+
- `errors.py`: Error parsing and formatting
|
|
17
|
+
- `project_context.py`: Project validation and context
|
|
18
|
+
- `path_manager.py`: Path resolution and management
|
|
19
|
+
- `util.py`: Misc utilities (temp file creation)
|
leanback/__init__.py
ADDED
leanback/check.py
ADDED
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Implementation of the leanback-check tool.
|
|
3
|
+
|
|
4
|
+
This module provides functionality for checking Lean files and projects.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import subprocess
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
|
|
13
|
+
from leanback.common.env import LeanRunner
|
|
14
|
+
from leanback.common.errors import LeanError, parse_lean_output
|
|
15
|
+
from leanback.common.path_manager import PathManager
|
|
16
|
+
from leanback.common.project_context import ProjectContext
|
|
17
|
+
from leanback.common.util import create_temp_file
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def check_file(
|
|
21
|
+
file_path: str | Path,
|
|
22
|
+
project_path: str | Path | None = None,
|
|
23
|
+
capture_json: bool = True,
|
|
24
|
+
console: Console | None = None,
|
|
25
|
+
preserve_relative: bool = True,
|
|
26
|
+
) -> tuple[bool, list[LeanError]]:
|
|
27
|
+
"""
|
|
28
|
+
Check a single Lean file for errors.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
file_path: Path to the Lean file to check
|
|
32
|
+
project_path: Path to the Lean project (if None, tries to find it)
|
|
33
|
+
capture_json: Whether to capture output as JSON for better error parsing
|
|
34
|
+
console: Rich console for output (creates a new one if None)
|
|
35
|
+
preserve_relative: Whether to preserve relative paths (True by default)
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Tuple of (success, errors)
|
|
39
|
+
"""
|
|
40
|
+
if console is None:
|
|
41
|
+
console = Console()
|
|
42
|
+
|
|
43
|
+
# Convert to Path
|
|
44
|
+
if isinstance(file_path, str):
|
|
45
|
+
file_path = Path(file_path)
|
|
46
|
+
|
|
47
|
+
# Store original path format for consistent return values
|
|
48
|
+
original_file_path = file_path
|
|
49
|
+
|
|
50
|
+
# For file existence check, we need the absolute path
|
|
51
|
+
abs_file_path = file_path.absolute() if preserve_relative else file_path
|
|
52
|
+
|
|
53
|
+
# Ensure the file exists
|
|
54
|
+
if not abs_file_path.exists():
|
|
55
|
+
return False, [
|
|
56
|
+
LeanError(
|
|
57
|
+
file_name=str(
|
|
58
|
+
original_file_path
|
|
59
|
+
), # Use original path in error messages
|
|
60
|
+
line=0,
|
|
61
|
+
column=0,
|
|
62
|
+
severity="error",
|
|
63
|
+
message=f"File '{original_file_path}' does not exist",
|
|
64
|
+
)
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
# Get path manager
|
|
68
|
+
path_manager = PathManager()
|
|
69
|
+
|
|
70
|
+
# Find project root if not provided
|
|
71
|
+
if project_path is None:
|
|
72
|
+
project_path = path_manager.find_project_root(file_path)
|
|
73
|
+
else:
|
|
74
|
+
# Use ProjectContext to resolve project path (handles project names)
|
|
75
|
+
project_context = ProjectContext(project_path, console)
|
|
76
|
+
if project_context.project_path:
|
|
77
|
+
project_path = project_context.project_path
|
|
78
|
+
else:
|
|
79
|
+
# If ProjectContext couldn't resolve it, try as a regular path
|
|
80
|
+
if isinstance(project_path, str):
|
|
81
|
+
project_path = Path(project_path)
|
|
82
|
+
if not project_path.exists():
|
|
83
|
+
return False, [
|
|
84
|
+
LeanError(
|
|
85
|
+
file_name=str(original_file_path),
|
|
86
|
+
line=0,
|
|
87
|
+
column=0,
|
|
88
|
+
severity="error",
|
|
89
|
+
message=f"Project '{project_path}' not found",
|
|
90
|
+
)
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
# Check if this file imports mathlib
|
|
94
|
+
has_mathlib_import = path_manager.file_has_mathlib_import(file_path)
|
|
95
|
+
|
|
96
|
+
# Always enable mathlib integration with all dependencies for consistent behavior
|
|
97
|
+
# Run Lean on the file
|
|
98
|
+
exit_code, stdout, stderr = LeanRunner.run_lean(
|
|
99
|
+
file_path=file_path,
|
|
100
|
+
project_path=project_path,
|
|
101
|
+
capture_json=capture_json,
|
|
102
|
+
check=False,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# Parse the output for errors
|
|
106
|
+
errors = parse_lean_output(
|
|
107
|
+
stdout if capture_json else stderr, json_format=capture_json
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# If no detailed errors were parsed but the check failed, provide more information
|
|
111
|
+
if exit_code != 0 and not errors:
|
|
112
|
+
# First, check if it might be a mathlib import issue
|
|
113
|
+
if has_mathlib_import:
|
|
114
|
+
errors.append(
|
|
115
|
+
LeanError(
|
|
116
|
+
file_name=str(file_path),
|
|
117
|
+
line=0,
|
|
118
|
+
column=0,
|
|
119
|
+
severity="error",
|
|
120
|
+
message="Mathlib import issue detected.",
|
|
121
|
+
suggestion="\nTo fix mathlib import issues:\n"
|
|
122
|
+
"1. Make sure your file is part of a properly created project\n"
|
|
123
|
+
"2. Create a project with Mathlib: lake +leanprover-community/mathlib4:lean-toolchain new <name> math\n"
|
|
124
|
+
"3. Download cache: cd <name> && lake exe cache get\n\n"
|
|
125
|
+
"Raw output from Lean:\n"
|
|
126
|
+
+ (
|
|
127
|
+
f"stdout: {stdout}\n\nstderr: {stderr}"
|
|
128
|
+
if stdout or stderr
|
|
129
|
+
else "No output captured"
|
|
130
|
+
),
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
else:
|
|
134
|
+
# General error with no specific details
|
|
135
|
+
errors.append(
|
|
136
|
+
LeanError(
|
|
137
|
+
file_name=str(file_path),
|
|
138
|
+
line=0,
|
|
139
|
+
column=0,
|
|
140
|
+
severity="error",
|
|
141
|
+
message="Lean check failed without detailed error information",
|
|
142
|
+
suggestion="\nRaw output from Lean:\n"
|
|
143
|
+
+ (
|
|
144
|
+
f"stdout: {stdout}\n\nstderr: {stderr}"
|
|
145
|
+
if stdout or stderr
|
|
146
|
+
else "No output captured"
|
|
147
|
+
),
|
|
148
|
+
)
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# Return success (no errors) and the list of errors
|
|
152
|
+
return exit_code == 0, errors
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def check_project(
|
|
156
|
+
project_path: str | Path,
|
|
157
|
+
check_all_files: bool = False,
|
|
158
|
+
capture_json: bool = True,
|
|
159
|
+
console: Console | None = None,
|
|
160
|
+
) -> tuple[bool, list[LeanError]]:
|
|
161
|
+
"""
|
|
162
|
+
Check an entire Lean project for errors.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
project_path: Path to the Lean project
|
|
166
|
+
check_all_files: Whether to check all .lean files or just run lake build
|
|
167
|
+
capture_json: Whether to capture output as JSON for better error parsing
|
|
168
|
+
console: Rich console for output (creates a new one if None)
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
Tuple of (success, errors)
|
|
172
|
+
"""
|
|
173
|
+
if console is None:
|
|
174
|
+
console = Console()
|
|
175
|
+
|
|
176
|
+
# Create project context for validation
|
|
177
|
+
project_context = ProjectContext(project_path, console)
|
|
178
|
+
|
|
179
|
+
# Ensure the project is valid
|
|
180
|
+
if not project_context.project_path:
|
|
181
|
+
return False, [
|
|
182
|
+
LeanError(
|
|
183
|
+
file_name=str(project_path),
|
|
184
|
+
line=0,
|
|
185
|
+
column=0,
|
|
186
|
+
severity="error",
|
|
187
|
+
message=f"Project directory '{project_path}' does not exist",
|
|
188
|
+
)
|
|
189
|
+
]
|
|
190
|
+
|
|
191
|
+
# Use the validated project path
|
|
192
|
+
project_path = project_context.project_path
|
|
193
|
+
|
|
194
|
+
# If we're not checking individual files, just run lake build
|
|
195
|
+
if not check_all_files:
|
|
196
|
+
console.print(f"Building project at {project_path}...")
|
|
197
|
+
try:
|
|
198
|
+
process = subprocess.run(
|
|
199
|
+
["lake", "build"],
|
|
200
|
+
cwd=project_path,
|
|
201
|
+
capture_output=True,
|
|
202
|
+
text=True,
|
|
203
|
+
check=False,
|
|
204
|
+
timeout=300, # 5 minutes for mathlib projects
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# Parse output for actual compilation errors
|
|
208
|
+
errors = []
|
|
209
|
+
has_compilation_errors = False
|
|
210
|
+
|
|
211
|
+
# Combine stdout and stderr for comprehensive parsing
|
|
212
|
+
all_output = (process.stdout or "") + "\n" + (process.stderr or "")
|
|
213
|
+
|
|
214
|
+
for line in all_output.splitlines():
|
|
215
|
+
# Look for Lean compilation errors (more specific patterns)
|
|
216
|
+
if "error:" in line and any(
|
|
217
|
+
indicator in line
|
|
218
|
+
for indicator in [
|
|
219
|
+
".lean:", # Lean file error
|
|
220
|
+
"failed to", # Compilation failure
|
|
221
|
+
"unknown identifier", # Common Lean error
|
|
222
|
+
"type mismatch", # Type error
|
|
223
|
+
"tactic failed", # Proof error
|
|
224
|
+
"declaration", # Declaration errors
|
|
225
|
+
]
|
|
226
|
+
):
|
|
227
|
+
has_compilation_errors = True
|
|
228
|
+
errors.append(
|
|
229
|
+
LeanError(
|
|
230
|
+
file_name="unknown",
|
|
231
|
+
line=0,
|
|
232
|
+
column=0,
|
|
233
|
+
severity="error",
|
|
234
|
+
message=line.strip(),
|
|
235
|
+
)
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
if process.returncode == 0:
|
|
239
|
+
return True, []
|
|
240
|
+
|
|
241
|
+
# Non-zero exit code — always a failure
|
|
242
|
+
if has_compilation_errors:
|
|
243
|
+
return False, errors
|
|
244
|
+
|
|
245
|
+
# No specific compilation errors parsed — provide context
|
|
246
|
+
raw_output = all_output[:1000]
|
|
247
|
+
return False, [
|
|
248
|
+
LeanError(
|
|
249
|
+
file_name="lake",
|
|
250
|
+
line=0,
|
|
251
|
+
column=0,
|
|
252
|
+
severity="error",
|
|
253
|
+
message="Lake build failed.",
|
|
254
|
+
suggestion=(
|
|
255
|
+
"\nThis might be due to:\n"
|
|
256
|
+
"- Dependency or network issues\n"
|
|
257
|
+
"- Corrupted build cache\n\n"
|
|
258
|
+
"Try running 'lake clean' and rebuilding.\n\n"
|
|
259
|
+
f"Raw output:\n{raw_output}"
|
|
260
|
+
),
|
|
261
|
+
)
|
|
262
|
+
]
|
|
263
|
+
|
|
264
|
+
except subprocess.TimeoutExpired:
|
|
265
|
+
return False, [
|
|
266
|
+
LeanError(
|
|
267
|
+
file_name="lake",
|
|
268
|
+
line=0,
|
|
269
|
+
column=0,
|
|
270
|
+
severity="error",
|
|
271
|
+
message="Lake build timed out after 5 minutes",
|
|
272
|
+
suggestion="This might be due to:\n"
|
|
273
|
+
"- First-time mathlib download (can take 10-30 minutes)\n"
|
|
274
|
+
"- Large project compilation\n"
|
|
275
|
+
"- System resource constraints\n\n"
|
|
276
|
+
"Try running 'lake build' manually in the project directory.",
|
|
277
|
+
)
|
|
278
|
+
]
|
|
279
|
+
except Exception as e:
|
|
280
|
+
return False, [
|
|
281
|
+
LeanError(
|
|
282
|
+
file_name="lake",
|
|
283
|
+
line=0,
|
|
284
|
+
column=0,
|
|
285
|
+
severity="error",
|
|
286
|
+
message=f"Failed to run 'lake build': {e}",
|
|
287
|
+
)
|
|
288
|
+
]
|
|
289
|
+
|
|
290
|
+
# If we're checking individual files, find all .lean files and check them
|
|
291
|
+
all_errors = []
|
|
292
|
+
success = True
|
|
293
|
+
|
|
294
|
+
# Find all .lean files in the project
|
|
295
|
+
lean_files = list(project_path.glob("**/*.lean"))
|
|
296
|
+
|
|
297
|
+
for file_path in lean_files:
|
|
298
|
+
# Skip files in .lake directory
|
|
299
|
+
if ".lake" in file_path.parts:
|
|
300
|
+
continue
|
|
301
|
+
|
|
302
|
+
console.print(f"Checking {file_path.relative_to(project_path)}...")
|
|
303
|
+
file_success, file_errors = check_file(
|
|
304
|
+
file_path=file_path,
|
|
305
|
+
project_path=project_path,
|
|
306
|
+
capture_json=capture_json,
|
|
307
|
+
console=console,
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
if not file_success:
|
|
311
|
+
success = False
|
|
312
|
+
all_errors.extend(file_errors)
|
|
313
|
+
|
|
314
|
+
return success, all_errors
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def check_lean_expr(
|
|
318
|
+
expr: str,
|
|
319
|
+
imports: list[str] | None = None,
|
|
320
|
+
project_path: str | Path | None = None,
|
|
321
|
+
capture_json: bool = True,
|
|
322
|
+
console: Console | None = None,
|
|
323
|
+
) -> tuple[bool, list[LeanError]]:
|
|
324
|
+
"""
|
|
325
|
+
Check a Lean expression for errors.
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
expr: Lean expression or code to check
|
|
329
|
+
imports: List of imports to include
|
|
330
|
+
project_path: Path to the Lean project (if None, tries to find it)
|
|
331
|
+
capture_json: Whether to capture output as JSON for better error parsing
|
|
332
|
+
console: Rich console for output (creates a new one if None)
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
Tuple of (success, errors)
|
|
336
|
+
"""
|
|
337
|
+
if console is None:
|
|
338
|
+
console = Console()
|
|
339
|
+
|
|
340
|
+
# Set default imports if none provided
|
|
341
|
+
if imports is None:
|
|
342
|
+
imports = []
|
|
343
|
+
|
|
344
|
+
# Create a temporary file with the expression
|
|
345
|
+
content = "\n".join(f"import {imp}" for imp in imports)
|
|
346
|
+
if content:
|
|
347
|
+
content += "\n\n"
|
|
348
|
+
content += expr
|
|
349
|
+
|
|
350
|
+
temp_file = create_temp_file(content)
|
|
351
|
+
|
|
352
|
+
try:
|
|
353
|
+
# Check the temporary file
|
|
354
|
+
return check_file(
|
|
355
|
+
file_path=temp_file,
|
|
356
|
+
project_path=project_path,
|
|
357
|
+
capture_json=capture_json,
|
|
358
|
+
console=console,
|
|
359
|
+
)
|
|
360
|
+
finally:
|
|
361
|
+
# Clean up the temporary file
|
|
362
|
+
try:
|
|
363
|
+
os.unlink(temp_file)
|
|
364
|
+
except OSError:
|
|
365
|
+
pass
|
|
File without changes
|
leanback/common/env.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Lean environment handling utilities.
|
|
3
|
+
|
|
4
|
+
This module provides functionality for executing Lean commands in the correct environment.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import subprocess
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class LeanRunner:
|
|
13
|
+
"""
|
|
14
|
+
Utility for running Lean commands in the appropriate environment.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
@staticmethod
|
|
18
|
+
def run_lean(
|
|
19
|
+
file_path: str | Path,
|
|
20
|
+
project_path: str | Path | None = None,
|
|
21
|
+
capture_json: bool = False,
|
|
22
|
+
check: bool = True,
|
|
23
|
+
env_vars: dict[str, str] | None = None,
|
|
24
|
+
timeout: float | None = None,
|
|
25
|
+
) -> tuple[int, str, str]:
|
|
26
|
+
"""
|
|
27
|
+
Run Lean on a file in the appropriate environment.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
file_path: Path to the Lean file to run
|
|
31
|
+
project_path: Path to a Lean project (for lake env)
|
|
32
|
+
capture_json: Whether to request JSON output
|
|
33
|
+
check: Whether to raise an exception on non-zero exit codes
|
|
34
|
+
env_vars: Additional environment variables
|
|
35
|
+
timeout: Timeout in seconds
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Tuple of (exit_code, stdout, stderr)
|
|
39
|
+
"""
|
|
40
|
+
# Convert to Path
|
|
41
|
+
if isinstance(file_path, str):
|
|
42
|
+
file_path = Path(file_path)
|
|
43
|
+
|
|
44
|
+
# We need absolute paths for checking existence internally
|
|
45
|
+
abs_file_path = file_path.absolute()
|
|
46
|
+
|
|
47
|
+
# Determine which path and command to use
|
|
48
|
+
if project_path:
|
|
49
|
+
# If a project is specified, use that project's environment
|
|
50
|
+
project_path = (
|
|
51
|
+
Path(project_path) if isinstance(project_path, str) else project_path
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Always use lake env lean for projects - this ensures proper dependency management
|
|
55
|
+
command = ["lake", "env", "lean"]
|
|
56
|
+
if capture_json:
|
|
57
|
+
command.append("--json")
|
|
58
|
+
command.append(str(abs_file_path))
|
|
59
|
+
cwd = project_path
|
|
60
|
+
|
|
61
|
+
# Lake automatically handles all dependency paths - no manual LEAN_PATH setup needed
|
|
62
|
+
|
|
63
|
+
else:
|
|
64
|
+
# Default: run directly with lean for files without project context
|
|
65
|
+
command = ["lean"]
|
|
66
|
+
if capture_json:
|
|
67
|
+
command.append("--json")
|
|
68
|
+
command.append(str(abs_file_path))
|
|
69
|
+
|
|
70
|
+
cwd = file_path.parent
|
|
71
|
+
|
|
72
|
+
# Set up environment
|
|
73
|
+
proc_env = os.environ.copy()
|
|
74
|
+
if env_vars:
|
|
75
|
+
proc_env.update(env_vars)
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
result = subprocess.run(
|
|
79
|
+
command,
|
|
80
|
+
cwd=cwd,
|
|
81
|
+
check=check,
|
|
82
|
+
capture_output=True,
|
|
83
|
+
text=True,
|
|
84
|
+
env=proc_env,
|
|
85
|
+
timeout=timeout,
|
|
86
|
+
)
|
|
87
|
+
return result.returncode, result.stdout, result.stderr
|
|
88
|
+
except subprocess.CalledProcessError as e:
|
|
89
|
+
return e.returncode, e.stdout, e.stderr
|
|
90
|
+
except subprocess.TimeoutExpired:
|
|
91
|
+
return 124, "", "Lean execution timed out"
|