lean-lsp-mcp 0.19.2__tar.gz → 0.20.0__tar.gz
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.
- {lean_lsp_mcp-0.19.2 → lean_lsp_mcp-0.20.0}/PKG-INFO +49 -4
- lean_lsp_mcp-0.19.2/src/lean_lsp_mcp.egg-info/PKG-INFO → lean_lsp_mcp-0.20.0/README.md +46 -27
- {lean_lsp_mcp-0.19.2 → lean_lsp_mcp-0.20.0}/pyproject.toml +8 -1
- {lean_lsp_mcp-0.19.2 → lean_lsp_mcp-0.20.0}/src/lean_lsp_mcp/__init__.py +14 -0
- {lean_lsp_mcp-0.19.2 → lean_lsp_mcp-0.20.0}/src/lean_lsp_mcp/instructions.py +11 -10
- lean_lsp_mcp-0.20.0/src/lean_lsp_mcp/repl.py +260 -0
- {lean_lsp_mcp-0.19.2 → lean_lsp_mcp-0.20.0}/src/lean_lsp_mcp/server.py +161 -28
- lean_lsp_mcp-0.19.2/README.md → lean_lsp_mcp-0.20.0/src/lean_lsp_mcp.egg-info/PKG-INFO +72 -3
- {lean_lsp_mcp-0.19.2 → lean_lsp_mcp-0.20.0}/src/lean_lsp_mcp.egg-info/SOURCES.txt +2 -0
- {lean_lsp_mcp-0.19.2 → lean_lsp_mcp-0.20.0}/src/lean_lsp_mcp.egg-info/requires.txt +3 -0
- lean_lsp_mcp-0.20.0/tests/test_logging.py +230 -0
- lean_lsp_mcp-0.20.0/tests/test_repl.py +217 -0
- lean_lsp_mcp-0.19.2/tests/test_logging.py +0 -90
- {lean_lsp_mcp-0.19.2 → lean_lsp_mcp-0.20.0}/LICENSE +0 -0
- {lean_lsp_mcp-0.19.2 → lean_lsp_mcp-0.20.0}/setup.cfg +0 -0
- {lean_lsp_mcp-0.19.2 → lean_lsp_mcp-0.20.0}/src/lean_lsp_mcp/__main__.py +0 -0
- {lean_lsp_mcp-0.19.2 → lean_lsp_mcp-0.20.0}/src/lean_lsp_mcp/client_utils.py +0 -0
- {lean_lsp_mcp-0.19.2 → lean_lsp_mcp-0.20.0}/src/lean_lsp_mcp/file_utils.py +0 -0
- {lean_lsp_mcp-0.19.2 → lean_lsp_mcp-0.20.0}/src/lean_lsp_mcp/loogle.py +0 -0
- {lean_lsp_mcp-0.19.2 → lean_lsp_mcp-0.20.0}/src/lean_lsp_mcp/models.py +0 -0
- {lean_lsp_mcp-0.19.2 → lean_lsp_mcp-0.20.0}/src/lean_lsp_mcp/outline_utils.py +0 -0
- {lean_lsp_mcp-0.19.2 → lean_lsp_mcp-0.20.0}/src/lean_lsp_mcp/profile_utils.py +0 -0
- {lean_lsp_mcp-0.19.2 → lean_lsp_mcp-0.20.0}/src/lean_lsp_mcp/search_utils.py +0 -0
- {lean_lsp_mcp-0.19.2 → lean_lsp_mcp-0.20.0}/src/lean_lsp_mcp/utils.py +0 -0
- {lean_lsp_mcp-0.19.2 → lean_lsp_mcp-0.20.0}/src/lean_lsp_mcp.egg-info/dependency_links.txt +0 -0
- {lean_lsp_mcp-0.19.2 → lean_lsp_mcp-0.20.0}/src/lean_lsp_mcp.egg-info/entry_points.txt +0 -0
- {lean_lsp_mcp-0.19.2 → lean_lsp_mcp-0.20.0}/src/lean_lsp_mcp.egg-info/top_level.txt +0 -0
- {lean_lsp_mcp-0.19.2 → lean_lsp_mcp-0.20.0}/tests/test_diagnostic_line_range.py +0 -0
- {lean_lsp_mcp-0.19.2 → lean_lsp_mcp-0.20.0}/tests/test_editor_tools.py +0 -0
- {lean_lsp_mcp-0.19.2 → lean_lsp_mcp-0.20.0}/tests/test_error_handling.py +0 -0
- {lean_lsp_mcp-0.19.2 → lean_lsp_mcp-0.20.0}/tests/test_file_caching.py +0 -0
- {lean_lsp_mcp-0.19.2 → lean_lsp_mcp-0.20.0}/tests/test_misc_tools.py +0 -0
- {lean_lsp_mcp-0.19.2 → lean_lsp_mcp-0.20.0}/tests/test_outline.py +0 -0
- {lean_lsp_mcp-0.19.2 → lean_lsp_mcp-0.20.0}/tests/test_profile.py +0 -0
- {lean_lsp_mcp-0.19.2 → lean_lsp_mcp-0.20.0}/tests/test_project_tools.py +0 -0
- {lean_lsp_mcp-0.19.2 → lean_lsp_mcp-0.20.0}/tests/test_search_tools.py +0 -0
- {lean_lsp_mcp-0.19.2 → lean_lsp_mcp-0.20.0}/tests/test_structured_output.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lean-lsp-mcp
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.20.0
|
|
4
4
|
Summary: Lean Theorem Prover MCP
|
|
5
5
|
Author-email: Oliver Dressler <hey@oli.show>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -12,6 +12,8 @@ Requires-Dist: leanclient==0.9.2
|
|
|
12
12
|
Requires-Dist: mcp[cli]==1.25.0
|
|
13
13
|
Requires-Dist: orjson>=3.11.1
|
|
14
14
|
Requires-Dist: certifi>=2024.0.0
|
|
15
|
+
Provides-Extra: yaml
|
|
16
|
+
Requires-Dist: PyYAML>=6.0; extra == "yaml"
|
|
15
17
|
Provides-Extra: lint
|
|
16
18
|
Requires-Dist: ruff>=0.2.0; extra == "lint"
|
|
17
19
|
Provides-Extra: dev
|
|
@@ -267,8 +269,10 @@ l1c1-l1c6, severity: 3
|
|
|
267
269
|
|
|
268
270
|
#### lean_multi_attempt
|
|
269
271
|
|
|
270
|
-
Attempt multiple
|
|
271
|
-
|
|
272
|
+
Attempt multiple tactics on a line and return goal state and diagnostics for each.
|
|
273
|
+
Useful to screen different proof attempts before committing to one.
|
|
274
|
+
|
|
275
|
+
When `LEAN_REPL=true`, uses the REPL tactic mode for up to 5x faster execution (see [Environment Variables](#environment-variables)).
|
|
272
276
|
|
|
273
277
|
<details>
|
|
274
278
|
<summary>Example output (attempting `rw [Nat.pow_sub (Fintype.card_pos_of_nonempty S)]` and `by_contra h_neq`)</summary>
|
|
@@ -484,7 +488,12 @@ This MCP server works out-of-the-box without any configuration. However, a few o
|
|
|
484
488
|
### Environment Variables
|
|
485
489
|
|
|
486
490
|
- `LEAN_LOG_LEVEL`: Log level for the server. Options are "INFO", "WARNING", "ERROR", "NONE". Defaults to "INFO".
|
|
491
|
+
- `LEAN_LOG_FILE_CONFIG`: Config file path for logging, with priority over `LEAN_LOG_LEVEL`. If not set, logs are printed to stdout.
|
|
487
492
|
- `LEAN_PROJECT_PATH`: Path to your Lean project root. Set this if the server cannot automatically detect your project.
|
|
493
|
+
- `LEAN_REPL`: Set to `true`, `1`, or `yes` to enable fast REPL-based `lean_multi_attempt` (~5x faster, see [REPL Setup](#repl-setup)).
|
|
494
|
+
- `LEAN_REPL_PATH`: Path to the `repl` binary. Auto-detected from `.lake/packages/repl/` if not set.
|
|
495
|
+
- `LEAN_REPL_TIMEOUT`: Per-command timeout in seconds (default: 60).
|
|
496
|
+
- `LEAN_REPL_MEM_MB`: Max memory per REPL in MB (default: 8192). Only enforced on Linux/macOS.
|
|
488
497
|
- `LEAN_LSP_MCP_TOKEN`: Secret token for bearer authentication when using `streamable-http` or `sse` transport.
|
|
489
498
|
- `LEAN_STATE_SEARCH_URL`: URL for a self-hosted [premise-search.com](https://premise-search.com) instance.
|
|
490
499
|
- `LEAN_HAMMER_URL`: URL for a self-hosted [Lean Hammer Premise Search](https://github.com/hanwenzhu/lean-premise-server) instance.
|
|
@@ -545,6 +554,36 @@ uvx lean-lsp-mcp --transport streamable-http
|
|
|
545
554
|
|
|
546
555
|
Clients should then include the token in the `Authorization` header.
|
|
547
556
|
|
|
557
|
+
### REPL Setup
|
|
558
|
+
|
|
559
|
+
Enable fast REPL-based `lean_multi_attempt` (~5x faster). Uses [leanprover-community/repl](https://github.com/leanprover-community/repl) tactic mode.
|
|
560
|
+
|
|
561
|
+
**1. Add REPL to your Lean project's `lakefile.toml`:**
|
|
562
|
+
|
|
563
|
+
```toml
|
|
564
|
+
[[require]]
|
|
565
|
+
name = "repl"
|
|
566
|
+
git = "https://github.com/leanprover-community/repl"
|
|
567
|
+
rev = "v4.25.0" # Match your Lean version
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
**2. Build it:**
|
|
571
|
+
|
|
572
|
+
```bash
|
|
573
|
+
lake build repl
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
**3. Enable via CLI or environment variable:**
|
|
577
|
+
|
|
578
|
+
```bash
|
|
579
|
+
uvx lean-lsp-mcp --repl
|
|
580
|
+
|
|
581
|
+
# Or via environment variable
|
|
582
|
+
export LEAN_REPL=true
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
The REPL binary is auto-detected from `.lake/packages/repl/`. Falls back to LSP if not found.
|
|
586
|
+
|
|
548
587
|
### Local Loogle
|
|
549
588
|
|
|
550
589
|
Run loogle locally to avoid the remote API's rate limit (3 req/30s). First run takes ~5-10 minutes to build; subsequent runs start in seconds.
|
|
@@ -591,9 +630,15 @@ uv sync --all-extras
|
|
|
591
630
|
uv run pytest tests
|
|
592
631
|
```
|
|
593
632
|
|
|
594
|
-
## Publications using lean-lsp-mcp
|
|
633
|
+
## Publications and Formalization Projects using lean-lsp-mcp
|
|
595
634
|
|
|
596
635
|
- Ax-Prover: A Deep Reasoning Agentic Framework for Theorem Proving in Mathematics and Quantum Physics [arxiv](https://arxiv.org/abs/2510.12787)
|
|
636
|
+
- Numina-Lean-Agent: An Open and General Agentic Reasoning System for Formal Mathematics [arxiv](https://arxiv.org/abs/2601.14027) [github](https://github.com/project-numina/numina-lean-agent)
|
|
637
|
+
- A Group-Theoretic Approach to Shannon Capacity of Graphs and a Limit Theorem from Lattice Packings [github](https://github.com/jzuiddam/GroupTheoreticShannonCapacity/)
|
|
638
|
+
|
|
639
|
+
## Talks
|
|
640
|
+
|
|
641
|
+
lean-lsp-mcp: Tools for agentic interaction with Lean (Lean Together 2026) [youtube](https://www.youtube.com/watch?v=uttbYaTaF-E)
|
|
597
642
|
|
|
598
643
|
## Related Projects
|
|
599
644
|
|
|
@@ -1,27 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: lean-lsp-mcp
|
|
3
|
-
Version: 0.19.2
|
|
4
|
-
Summary: Lean Theorem Prover MCP
|
|
5
|
-
Author-email: Oliver Dressler <hey@oli.show>
|
|
6
|
-
License-Expression: MIT
|
|
7
|
-
Project-URL: Repository, https://github.com/oOo0oOo/lean-lsp-mcp
|
|
8
|
-
Requires-Python: >=3.10
|
|
9
|
-
Description-Content-Type: text/markdown
|
|
10
|
-
License-File: LICENSE
|
|
11
|
-
Requires-Dist: leanclient==0.9.2
|
|
12
|
-
Requires-Dist: mcp[cli]==1.25.0
|
|
13
|
-
Requires-Dist: orjson>=3.11.1
|
|
14
|
-
Requires-Dist: certifi>=2024.0.0
|
|
15
|
-
Provides-Extra: lint
|
|
16
|
-
Requires-Dist: ruff>=0.2.0; extra == "lint"
|
|
17
|
-
Provides-Extra: dev
|
|
18
|
-
Requires-Dist: ruff>=0.2.0; extra == "dev"
|
|
19
|
-
Requires-Dist: pytest>=8.3; extra == "dev"
|
|
20
|
-
Requires-Dist: anyio>=4.4; extra == "dev"
|
|
21
|
-
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
22
|
-
Requires-Dist: pytest-timeout>=2.3; extra == "dev"
|
|
23
|
-
Dynamic: license-file
|
|
24
|
-
|
|
25
1
|
<h1 align="center">
|
|
26
2
|
lean-lsp-mcp
|
|
27
3
|
</h1>
|
|
@@ -267,8 +243,10 @@ l1c1-l1c6, severity: 3
|
|
|
267
243
|
|
|
268
244
|
#### lean_multi_attempt
|
|
269
245
|
|
|
270
|
-
Attempt multiple
|
|
271
|
-
|
|
246
|
+
Attempt multiple tactics on a line and return goal state and diagnostics for each.
|
|
247
|
+
Useful to screen different proof attempts before committing to one.
|
|
248
|
+
|
|
249
|
+
When `LEAN_REPL=true`, uses the REPL tactic mode for up to 5x faster execution (see [Environment Variables](#environment-variables)).
|
|
272
250
|
|
|
273
251
|
<details>
|
|
274
252
|
<summary>Example output (attempting `rw [Nat.pow_sub (Fintype.card_pos_of_nonempty S)]` and `by_contra h_neq`)</summary>
|
|
@@ -484,7 +462,12 @@ This MCP server works out-of-the-box without any configuration. However, a few o
|
|
|
484
462
|
### Environment Variables
|
|
485
463
|
|
|
486
464
|
- `LEAN_LOG_LEVEL`: Log level for the server. Options are "INFO", "WARNING", "ERROR", "NONE". Defaults to "INFO".
|
|
465
|
+
- `LEAN_LOG_FILE_CONFIG`: Config file path for logging, with priority over `LEAN_LOG_LEVEL`. If not set, logs are printed to stdout.
|
|
487
466
|
- `LEAN_PROJECT_PATH`: Path to your Lean project root. Set this if the server cannot automatically detect your project.
|
|
467
|
+
- `LEAN_REPL`: Set to `true`, `1`, or `yes` to enable fast REPL-based `lean_multi_attempt` (~5x faster, see [REPL Setup](#repl-setup)).
|
|
468
|
+
- `LEAN_REPL_PATH`: Path to the `repl` binary. Auto-detected from `.lake/packages/repl/` if not set.
|
|
469
|
+
- `LEAN_REPL_TIMEOUT`: Per-command timeout in seconds (default: 60).
|
|
470
|
+
- `LEAN_REPL_MEM_MB`: Max memory per REPL in MB (default: 8192). Only enforced on Linux/macOS.
|
|
488
471
|
- `LEAN_LSP_MCP_TOKEN`: Secret token for bearer authentication when using `streamable-http` or `sse` transport.
|
|
489
472
|
- `LEAN_STATE_SEARCH_URL`: URL for a self-hosted [premise-search.com](https://premise-search.com) instance.
|
|
490
473
|
- `LEAN_HAMMER_URL`: URL for a self-hosted [Lean Hammer Premise Search](https://github.com/hanwenzhu/lean-premise-server) instance.
|
|
@@ -545,6 +528,36 @@ uvx lean-lsp-mcp --transport streamable-http
|
|
|
545
528
|
|
|
546
529
|
Clients should then include the token in the `Authorization` header.
|
|
547
530
|
|
|
531
|
+
### REPL Setup
|
|
532
|
+
|
|
533
|
+
Enable fast REPL-based `lean_multi_attempt` (~5x faster). Uses [leanprover-community/repl](https://github.com/leanprover-community/repl) tactic mode.
|
|
534
|
+
|
|
535
|
+
**1. Add REPL to your Lean project's `lakefile.toml`:**
|
|
536
|
+
|
|
537
|
+
```toml
|
|
538
|
+
[[require]]
|
|
539
|
+
name = "repl"
|
|
540
|
+
git = "https://github.com/leanprover-community/repl"
|
|
541
|
+
rev = "v4.25.0" # Match your Lean version
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
**2. Build it:**
|
|
545
|
+
|
|
546
|
+
```bash
|
|
547
|
+
lake build repl
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
**3. Enable via CLI or environment variable:**
|
|
551
|
+
|
|
552
|
+
```bash
|
|
553
|
+
uvx lean-lsp-mcp --repl
|
|
554
|
+
|
|
555
|
+
# Or via environment variable
|
|
556
|
+
export LEAN_REPL=true
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
The REPL binary is auto-detected from `.lake/packages/repl/`. Falls back to LSP if not found.
|
|
560
|
+
|
|
548
561
|
### Local Loogle
|
|
549
562
|
|
|
550
563
|
Run loogle locally to avoid the remote API's rate limit (3 req/30s). First run takes ~5-10 minutes to build; subsequent runs start in seconds.
|
|
@@ -591,9 +604,15 @@ uv sync --all-extras
|
|
|
591
604
|
uv run pytest tests
|
|
592
605
|
```
|
|
593
606
|
|
|
594
|
-
## Publications using lean-lsp-mcp
|
|
607
|
+
## Publications and Formalization Projects using lean-lsp-mcp
|
|
595
608
|
|
|
596
609
|
- Ax-Prover: A Deep Reasoning Agentic Framework for Theorem Proving in Mathematics and Quantum Physics [arxiv](https://arxiv.org/abs/2510.12787)
|
|
610
|
+
- Numina-Lean-Agent: An Open and General Agentic Reasoning System for Formal Mathematics [arxiv](https://arxiv.org/abs/2601.14027) [github](https://github.com/project-numina/numina-lean-agent)
|
|
611
|
+
- A Group-Theoretic Approach to Shannon Capacity of Graphs and a Limit Theorem from Lattice Packings [github](https://github.com/jzuiddam/GroupTheoreticShannonCapacity/)
|
|
612
|
+
|
|
613
|
+
## Talks
|
|
614
|
+
|
|
615
|
+
lean-lsp-mcp: Tools for agentic interaction with Lean (Lean Together 2026) [youtube](https://www.youtube.com/watch?v=uttbYaTaF-E)
|
|
597
616
|
|
|
598
617
|
## Related Projects
|
|
599
618
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "lean-lsp-mcp"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.20.0"
|
|
4
4
|
description = "Lean Theorem Prover MCP"
|
|
5
5
|
authors = [{ name = "Oliver Dressler", email = "hey@oli.show" }]
|
|
6
6
|
readme = "README.md"
|
|
@@ -12,9 +12,16 @@ dependencies = ["leanclient==0.9.2", "mcp[cli]==1.25.0", "orjson>=3.11.1", "cert
|
|
|
12
12
|
Repository = "https://github.com/oOo0oOo/lean-lsp-mcp"
|
|
13
13
|
|
|
14
14
|
[project.optional-dependencies]
|
|
15
|
+
yaml = ["PyYAML>=6.0"]
|
|
15
16
|
lint = ["ruff>=0.2.0"]
|
|
16
17
|
dev = ["ruff>=0.2.0", "pytest>=8.3", "anyio>=4.4", "pytest-asyncio>=0.23", "pytest-timeout>=2.3"]
|
|
17
18
|
|
|
19
|
+
[tool.ruff]
|
|
20
|
+
exclude = [
|
|
21
|
+
"tests/test_project/.lake",
|
|
22
|
+
"tests/mathlib_project/.lake",
|
|
23
|
+
]
|
|
24
|
+
|
|
18
25
|
[tool.pytest.ini_options]
|
|
19
26
|
asyncio_mode = "auto"
|
|
20
27
|
markers = ["slow: marks tests as slow (deselect with '-m \"not slow\"')"]
|
|
@@ -36,6 +36,16 @@ def main():
|
|
|
36
36
|
type=str,
|
|
37
37
|
help="Override loogle cache location (default: ~/.cache/lean-lsp-mcp/loogle)",
|
|
38
38
|
)
|
|
39
|
+
parser.add_argument(
|
|
40
|
+
"--repl",
|
|
41
|
+
action="store_true",
|
|
42
|
+
help="Enable fast REPL-based multi-attempt (~5x faster). Requires Lean REPL.",
|
|
43
|
+
)
|
|
44
|
+
parser.add_argument(
|
|
45
|
+
"--repl-timeout",
|
|
46
|
+
type=int,
|
|
47
|
+
help="REPL command timeout in seconds (default: 60)",
|
|
48
|
+
)
|
|
39
49
|
args = parser.parse_args()
|
|
40
50
|
|
|
41
51
|
# Set env vars from CLI args (CLI takes precedence over env vars)
|
|
@@ -43,6 +53,10 @@ def main():
|
|
|
43
53
|
os.environ["LEAN_LOOGLE_LOCAL"] = "true"
|
|
44
54
|
if args.loogle_cache_dir:
|
|
45
55
|
os.environ["LEAN_LOOGLE_CACHE_DIR"] = args.loogle_cache_dir
|
|
56
|
+
if args.repl:
|
|
57
|
+
os.environ["LEAN_REPL"] = "true"
|
|
58
|
+
if args.repl_timeout:
|
|
59
|
+
os.environ["LEAN_REPL_TIMEOUT"] = str(args.repl_timeout)
|
|
46
60
|
|
|
47
61
|
mcp.settings.host = args.host
|
|
48
62
|
mcp.settings.port = args.port
|
|
@@ -16,19 +16,19 @@ INSTRUCTIONS = """## General Rules
|
|
|
16
16
|
- **lean_profile_proof**: Profile a theorem for performance. Shows tactic hotspots. SLOW!
|
|
17
17
|
|
|
18
18
|
## Search Tools (rate limited)
|
|
19
|
-
- **lean_leansearch** (3/30s): Natural language
|
|
20
|
-
- **lean_loogle** (3/30s): Type pattern
|
|
19
|
+
- **lean_leansearch** (3/30s): Natural language -> mathlib
|
|
20
|
+
- **lean_loogle** (3/30s): Type pattern -> mathlib
|
|
21
21
|
- **lean_leanfinder** (10/30s): Semantic/conceptual search
|
|
22
|
-
- **lean_state_search** (3/30s): Goal
|
|
23
|
-
- **lean_hammer_premise** (3/30s): Goal
|
|
22
|
+
- **lean_state_search** (3/30s): Goal -> closing lemmas
|
|
23
|
+
- **lean_hammer_premise** (3/30s): Goal -> premises for simp/aesop
|
|
24
24
|
|
|
25
25
|
## Search Decision Tree
|
|
26
|
-
1. "Does X exist locally?"
|
|
27
|
-
2. "I need a lemma that says X"
|
|
28
|
-
3. "Find lemma with type pattern"
|
|
29
|
-
4. "What's the Lean name for concept X?"
|
|
30
|
-
5. "What closes this goal?"
|
|
31
|
-
6. "What to feed simp?"
|
|
26
|
+
1. "Does X exist locally?" -> lean_local_search
|
|
27
|
+
2. "I need a lemma that says X" -> lean_leansearch
|
|
28
|
+
3. "Find lemma with type pattern" -> lean_loogle
|
|
29
|
+
4. "What's the Lean name for concept X?" -> lean_leanfinder
|
|
30
|
+
5. "What closes this goal?" -> lean_state_search
|
|
31
|
+
6. "What to feed simp?" -> lean_hammer_premise
|
|
32
32
|
|
|
33
33
|
After finding a name: lean_local_search to verify, lean_hover_info for signature.
|
|
34
34
|
|
|
@@ -37,4 +37,5 @@ List tools return JSON arrays. Empty = `[]`.
|
|
|
37
37
|
|
|
38
38
|
## Error Handling
|
|
39
39
|
Check `isError` in responses: `true` means failure (timeout/LSP error), while `[]` with `isError: false` means no results found.
|
|
40
|
+
|
|
40
41
|
"""
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"""Lean REPL for fast multi-attempt tactic execution using tactic mode."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import platform
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
if platform.system() != "Windows":
|
|
13
|
+
import resource
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ReplError(Exception):
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class SnippetResult:
|
|
22
|
+
goals: list[str] = field(default_factory=list)
|
|
23
|
+
messages: list[dict[str, Any]] = field(default_factory=list)
|
|
24
|
+
proof_status: str | None = None
|
|
25
|
+
error: str | None = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def repl_enabled() -> bool:
|
|
29
|
+
return os.environ.get("LEAN_REPL", "").lower() in ("1", "true", "yes")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def find_repl_binary(project_dir: str | None = None) -> str | None:
|
|
33
|
+
"""Find REPL binary: env var > .lake/packages > PATH."""
|
|
34
|
+
import shutil
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
|
|
37
|
+
# 1. Explicit env var
|
|
38
|
+
if path := os.environ.get("LEAN_REPL_PATH"):
|
|
39
|
+
return path if Path(path).exists() or shutil.which(path) else None
|
|
40
|
+
|
|
41
|
+
# 2. Auto-detect from .lake/packages (common location after `lake build`)
|
|
42
|
+
if project_dir:
|
|
43
|
+
candidates = [
|
|
44
|
+
Path(project_dir)
|
|
45
|
+
/ ".lake"
|
|
46
|
+
/ "packages"
|
|
47
|
+
/ "repl"
|
|
48
|
+
/ ".lake"
|
|
49
|
+
/ "build"
|
|
50
|
+
/ "bin"
|
|
51
|
+
/ "repl",
|
|
52
|
+
Path(project_dir) / ".lake" / "build" / "bin" / "repl",
|
|
53
|
+
]
|
|
54
|
+
for p in candidates:
|
|
55
|
+
if p.exists():
|
|
56
|
+
return str(p)
|
|
57
|
+
|
|
58
|
+
# 3. Fall back to PATH
|
|
59
|
+
if found := shutil.which("repl"):
|
|
60
|
+
return found
|
|
61
|
+
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _split_imports(code: str) -> tuple[str, str]:
|
|
66
|
+
"""Split code into (header with imports, body)."""
|
|
67
|
+
lines = code.splitlines()
|
|
68
|
+
i = 0
|
|
69
|
+
while i < len(lines) and (not lines[i].strip() or lines[i].startswith("import ")):
|
|
70
|
+
i += 1
|
|
71
|
+
|
|
72
|
+
# Deduplicate imports while preserving order
|
|
73
|
+
imports = [ln.strip() for ln in lines[:i] if ln.startswith("import ")]
|
|
74
|
+
header = "\n".join(dict.fromkeys(imports))
|
|
75
|
+
return header, "\n".join(lines[i:])
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class Repl:
|
|
79
|
+
"""Lean REPL using tactic mode for fast multi-attempt."""
|
|
80
|
+
|
|
81
|
+
def __init__(self, project_dir: str, repl_path: str | None = None):
|
|
82
|
+
self.project_dir = project_dir
|
|
83
|
+
self.repl_path = repl_path or find_repl_binary(project_dir) or "repl"
|
|
84
|
+
self.timeout = int(os.environ.get("LEAN_REPL_TIMEOUT", "60"))
|
|
85
|
+
self.mem_mb = int(os.environ.get("LEAN_REPL_MEM_MB", "8192"))
|
|
86
|
+
self._proc: asyncio.subprocess.Process | None = None
|
|
87
|
+
self._header: str | None = None
|
|
88
|
+
self._header_env: int | None = None
|
|
89
|
+
self._lock = asyncio.Lock()
|
|
90
|
+
|
|
91
|
+
async def _start(self) -> None:
|
|
92
|
+
"""Start REPL subprocess."""
|
|
93
|
+
kwargs: dict[str, Any] = {
|
|
94
|
+
"cwd": self.project_dir,
|
|
95
|
+
"stdin": asyncio.subprocess.PIPE,
|
|
96
|
+
"stdout": asyncio.subprocess.PIPE,
|
|
97
|
+
"stderr": asyncio.subprocess.PIPE,
|
|
98
|
+
}
|
|
99
|
+
if platform.system() != "Windows":
|
|
100
|
+
kwargs["start_new_session"] = True
|
|
101
|
+
# Memory limit on Unix systems
|
|
102
|
+
if platform.system() == "Linux":
|
|
103
|
+
limit = resource.RLIMIT_AS # Virtual memory
|
|
104
|
+
else: # macOS
|
|
105
|
+
limit = resource.RLIMIT_RSS # Resident set size
|
|
106
|
+
mem = self.mem_mb * 1024 * 1024
|
|
107
|
+
kwargs["preexec_fn"] = lambda: resource.setrlimit(limit, (mem, mem))
|
|
108
|
+
self._proc = await asyncio.create_subprocess_exec(
|
|
109
|
+
"lake", "env", self.repl_path, **kwargs
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
async def _send(self, cmd: dict[str, Any]) -> dict[str, Any]:
|
|
113
|
+
"""Send command and return response."""
|
|
114
|
+
if not self._proc or not self._proc.stdin or not self._proc.stdout:
|
|
115
|
+
raise ReplError("REPL not running")
|
|
116
|
+
|
|
117
|
+
self._proc.stdin.write((json.dumps(cmd) + "\n\n").encode())
|
|
118
|
+
await self._proc.stdin.drain()
|
|
119
|
+
|
|
120
|
+
lines = []
|
|
121
|
+
while True:
|
|
122
|
+
line = await self._proc.stdout.readline()
|
|
123
|
+
if not line or not line.strip():
|
|
124
|
+
break
|
|
125
|
+
lines.append(line)
|
|
126
|
+
|
|
127
|
+
if not lines:
|
|
128
|
+
raise ReplError("No response from REPL")
|
|
129
|
+
return json.loads(b"".join(lines))
|
|
130
|
+
|
|
131
|
+
async def _send_cmd(self, code: str, env: int | None = None) -> dict[str, Any]:
|
|
132
|
+
"""Send a command (code) to the REPL."""
|
|
133
|
+
cmd: dict[str, Any] = {"cmd": code}
|
|
134
|
+
if env is not None:
|
|
135
|
+
cmd["env"] = env
|
|
136
|
+
return await self._send(cmd)
|
|
137
|
+
|
|
138
|
+
async def _send_tactic(self, tactic: str, proof_state: int) -> dict[str, Any]:
|
|
139
|
+
"""Send a tactic to run in a proof state."""
|
|
140
|
+
return await self._send({"tactic": tactic, "proofState": proof_state})
|
|
141
|
+
|
|
142
|
+
async def _ensure_header(self, header: str) -> int | None:
|
|
143
|
+
"""Ensure REPL is running with given header, return header env."""
|
|
144
|
+
if self._header != header:
|
|
145
|
+
await self.close()
|
|
146
|
+
self._header = header
|
|
147
|
+
self._header_env = None
|
|
148
|
+
|
|
149
|
+
if not self._proc or self._proc.returncode is not None:
|
|
150
|
+
await self._start()
|
|
151
|
+
if header:
|
|
152
|
+
resp = await self._send_cmd(header, env=None)
|
|
153
|
+
if "error" in resp:
|
|
154
|
+
raise ReplError(f"Failed to load imports: {resp['error']}")
|
|
155
|
+
self._header_env = resp.get("env")
|
|
156
|
+
|
|
157
|
+
return self._header_env
|
|
158
|
+
|
|
159
|
+
async def run_snippets(
|
|
160
|
+
self, base_code: str, snippets: list[str]
|
|
161
|
+
) -> list[SnippetResult]:
|
|
162
|
+
"""Run multiple tactic snippets using tactic mode.
|
|
163
|
+
|
|
164
|
+
1. Load header (imports) - cached across calls
|
|
165
|
+
2. Send body + sorry to get proofState
|
|
166
|
+
3. Run each tactic via tactic mode (very fast)
|
|
167
|
+
"""
|
|
168
|
+
header, body = _split_imports(base_code)
|
|
169
|
+
|
|
170
|
+
async with self._lock:
|
|
171
|
+
try:
|
|
172
|
+
# Load imports (cached)
|
|
173
|
+
header_env = await asyncio.wait_for(
|
|
174
|
+
self._ensure_header(header), timeout=self.timeout
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# Send body with sorry to get proof state
|
|
178
|
+
if not body.strip():
|
|
179
|
+
return [SnippetResult(error="No proof body") for _ in snippets]
|
|
180
|
+
|
|
181
|
+
# Ensure proper whitespace before sorry
|
|
182
|
+
body_with_sorry = body.rstrip() + "\n sorry"
|
|
183
|
+
resp = await asyncio.wait_for(
|
|
184
|
+
self._send_cmd(body_with_sorry, env=header_env),
|
|
185
|
+
timeout=self.timeout,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
if "error" in resp:
|
|
189
|
+
return [SnippetResult(error=resp["error"]) for _ in snippets]
|
|
190
|
+
|
|
191
|
+
# Get proof state from the sorry
|
|
192
|
+
sorries = resp.get("sorries", [])
|
|
193
|
+
if not sorries:
|
|
194
|
+
# No sorry = no proof goal, check messages for errors
|
|
195
|
+
msgs = resp.get("messages", [])
|
|
196
|
+
err = "; ".join(
|
|
197
|
+
m.get("data", "") for m in msgs if m.get("severity") == "error"
|
|
198
|
+
)
|
|
199
|
+
return [
|
|
200
|
+
SnippetResult(error=err or "No proof goal found")
|
|
201
|
+
for _ in snippets
|
|
202
|
+
]
|
|
203
|
+
|
|
204
|
+
proof_state = sorries[0].get("proofState")
|
|
205
|
+
if proof_state is None:
|
|
206
|
+
return [
|
|
207
|
+
SnippetResult(error="No proofState returned") for _ in snippets
|
|
208
|
+
]
|
|
209
|
+
|
|
210
|
+
# Run each tactic in tactic mode
|
|
211
|
+
results = []
|
|
212
|
+
for snippet in snippets:
|
|
213
|
+
try:
|
|
214
|
+
resp = await asyncio.wait_for(
|
|
215
|
+
self._send_tactic(snippet.strip(), proof_state),
|
|
216
|
+
timeout=self.timeout,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
if "error" in resp:
|
|
220
|
+
# Lean error (tactic failed)
|
|
221
|
+
results.append(SnippetResult(error=resp["error"]))
|
|
222
|
+
else:
|
|
223
|
+
goals = resp.get("goals", [])
|
|
224
|
+
messages = resp.get("messages", [])
|
|
225
|
+
proof_status = resp.get("proofStatus")
|
|
226
|
+
results.append(
|
|
227
|
+
SnippetResult(
|
|
228
|
+
goals=goals,
|
|
229
|
+
messages=messages,
|
|
230
|
+
proof_status=proof_status,
|
|
231
|
+
)
|
|
232
|
+
)
|
|
233
|
+
except Exception as e:
|
|
234
|
+
results.append(SnippetResult(error=str(e)))
|
|
235
|
+
|
|
236
|
+
return results
|
|
237
|
+
|
|
238
|
+
except Exception as e:
|
|
239
|
+
await self.close()
|
|
240
|
+
raise ReplError(str(e)) from e
|
|
241
|
+
|
|
242
|
+
async def close(self) -> None:
|
|
243
|
+
if not self._proc:
|
|
244
|
+
return
|
|
245
|
+
proc, self._proc = self._proc, None
|
|
246
|
+
self._header = None
|
|
247
|
+
self._header_env = None
|
|
248
|
+
try:
|
|
249
|
+
if platform.system() != "Windows":
|
|
250
|
+
os.killpg(os.getpgid(proc.pid), 9)
|
|
251
|
+
else:
|
|
252
|
+
proc.kill()
|
|
253
|
+
except (ProcessLookupError, OSError):
|
|
254
|
+
pass
|
|
255
|
+
try:
|
|
256
|
+
await asyncio.wait_for(proc.wait(), timeout=1.0)
|
|
257
|
+
except asyncio.TimeoutError:
|
|
258
|
+
pass
|
|
259
|
+
if hasattr(proc, "_transport") and proc._transport:
|
|
260
|
+
proc._transport.close()
|