lean-runtime 2.0.8__tar.gz → 2.1.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.
Files changed (150) hide show
  1. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/CHANGELOG.md +45 -1
  2. {lean_runtime-2.0.8/lean_runtime.egg-info → lean_runtime-2.1.0}/PKG-INFO +19 -1
  3. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/README.md +18 -0
  4. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/docs/cli.md +36 -3
  5. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/docs/getting-started.md +12 -2
  6. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/docs/python-api.md +23 -2
  7. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/docs/standalone-files.md +25 -4
  8. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/docs/trust-and-limitations.md +1 -1
  9. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/cli.py +37 -2
  10. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/diagnostics.py +22 -0
  11. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/discovery/__init__.py +2 -1
  12. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/discovery/engine.py +102 -84
  13. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/discovery/policy.py +11 -1
  14. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/discovery/probe.py +46 -6
  15. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/environments.py +21 -4
  16. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/facade.py +8 -3
  17. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/models.py +48 -0
  18. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/run_cli.py +45 -8
  19. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/runtime.py +43 -7
  20. lean_runtime-2.1.0/lean_runtime/toolchain_slim.py +209 -0
  21. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/toolchains.py +127 -3
  22. {lean_runtime-2.0.8 → lean_runtime-2.1.0/lean_runtime.egg-info}/PKG-INFO +19 -1
  23. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime.egg-info/SOURCES.txt +3 -0
  24. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/pyproject.toml +1 -1
  25. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/tests/discovery/test_engine.py +143 -8
  26. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/tests/test_cli.py +15 -0
  27. lean_runtime-2.1.0/tests/test_diagnostics.py +36 -0
  28. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/tests/test_facade.py +17 -0
  29. lean_runtime-2.1.0/tests/test_models.py +61 -0
  30. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/tests/test_prebuilt_policy.py +16 -0
  31. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/tests/test_run_cli.py +36 -0
  32. lean_runtime-2.1.0/tests/test_toolchain_slim.py +125 -0
  33. lean_runtime-2.0.8/tests/test_diagnostics.py +0 -18
  34. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/LICENSE +0 -0
  35. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/MANIFEST.in +0 -0
  36. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/catalog/environments.toml +0 -0
  37. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/catalog/locks/core-v4.32.2.lock.json +0 -0
  38. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/catalog/locks/mathlib-v4.30.0.lock.json +0 -0
  39. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/catalog/locks/mathlib-v4.31.0.lock.json +0 -0
  40. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/catalog/locks/mathlib-v4.32.2.lock.json +0 -0
  41. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/compatibility/README.md +0 -0
  42. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/compatibility/mathlib-4.32.2.json +0 -0
  43. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/compatibility/mathlib-4.32.2.toml +0 -0
  44. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/docs/architecture.md +0 -0
  45. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/docs/captures.md +0 -0
  46. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/docs/case-study-v1.md +0 -0
  47. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/docs/development.md +0 -0
  48. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/docs/environments.md +0 -0
  49. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/docs/index.md +0 -0
  50. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/docs/local-projects.md +0 -0
  51. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/docs/portable-copies.md +0 -0
  52. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/docs/ready-programs.md +0 -0
  53. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/docs/v1-precision.md +0 -0
  54. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/examples/mathlib.toml +0 -0
  55. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/__init__.py +0 -0
  56. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/__main__.py +0 -0
  57. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/_git.py +0 -0
  58. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/_paths.py +0 -0
  59. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/backends.py +0 -0
  60. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/bundles.py +0 -0
  61. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/comparison.py +0 -0
  62. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/decisions.py +0 -0
  63. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/discovery/analyzer.py +0 -0
  64. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/discovery/api.py +0 -0
  65. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/discovery/candidate.py +0 -0
  66. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/discovery/catalog.py +0 -0
  67. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/discovery/catalog_build.py +0 -0
  68. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/discovery/catalog_cli.py +0 -0
  69. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/discovery/catalog_manifest.py +0 -0
  70. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/discovery/data/catalog.json +0 -0
  71. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/discovery/defaults.py +0 -0
  72. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/discovery/errors.py +0 -0
  73. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/discovery/module_inventory.py +0 -0
  74. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/discovery/planner.py +0 -0
  75. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/discovery/py.typed +0 -0
  76. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/discovery/result.py +0 -0
  77. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/discovery/schema_resources.py +0 -0
  78. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/discovery/schemas/catalog-v1.schema.json +0 -0
  79. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/discovery/schemas/plan-v1.schema.json +0 -0
  80. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/discovery/schemas/result-v1.schema.json +0 -0
  81. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/errors.py +0 -0
  82. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/events.py +0 -0
  83. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/frontmatter.py +0 -0
  84. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/health.py +0 -0
  85. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/lake.py +0 -0
  86. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/lockfiles.py +0 -0
  87. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/locking.py +0 -0
  88. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/matrix.py +0 -0
  89. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/oci.py +0 -0
  90. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/policies.py +0 -0
  91. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/profiling.py +0 -0
  92. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/programs.py +0 -0
  93. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/projects.py +0 -0
  94. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/publisher_verification.py +0 -0
  95. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/py.typed +0 -0
  96. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/references.py +0 -0
  97. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/resolver.py +0 -0
  98. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/schema_resources.py +0 -0
  99. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/serialization.py +0 -0
  100. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/specs.py +0 -0
  101. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/store.py +0 -0
  102. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/timings.py +0 -0
  103. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/verification.py +0 -0
  104. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime/wire.py +0 -0
  105. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime.egg-info/dependency_links.txt +0 -0
  106. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime.egg-info/entry_points.txt +0 -0
  107. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime.egg-info/requires.txt +0 -0
  108. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/lean_runtime.egg-info/top_level.txt +0 -0
  109. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/mkdocs.yml +0 -0
  110. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/schemas/cleanup-v1.schema.json +0 -0
  111. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/schemas/comparison-v1.schema.json +0 -0
  112. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/schemas/execution-v1.schema.json +0 -0
  113. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/schemas/inspect-v1.schema.json +0 -0
  114. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/schemas/matrix-v1.schema.json +0 -0
  115. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/schemas/profile-v1.schema.json +0 -0
  116. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/schemas/verify-v1.schema.json +0 -0
  117. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/scripts/run_compatibility.py +0 -0
  118. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/scripts/run_v1_case_study.py +0 -0
  119. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/scripts/smoke_wheel.py +0 -0
  120. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/setup.cfg +0 -0
  121. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/tests/conftest.py +0 -0
  122. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/tests/discovery/e2e/test_real_discovery.py +0 -0
  123. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/tests/discovery/test_analyzer.py +0 -0
  124. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/tests/discovery/test_catalog.py +0 -0
  125. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/tests/discovery/test_catalog_build.py +0 -0
  126. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/tests/discovery/test_discovery_schema_resources.py +0 -0
  127. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/tests/discovery/test_planner.py +0 -0
  128. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/tests/discovery/test_policy.py +0 -0
  129. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/tests/test_backend.py +0 -0
  130. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/tests/test_bundles.py +0 -0
  131. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/tests/test_environment_integration.py +0 -0
  132. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/tests/test_events_health.py +0 -0
  133. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/tests/test_frontmatter.py +0 -0
  134. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/tests/test_git.py +0 -0
  135. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/tests/test_interactive.py +0 -0
  136. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/tests/test_lockfiles.py +0 -0
  137. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/tests/test_multifile.py +0 -0
  138. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/tests/test_paths.py +0 -0
  139. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/tests/test_programs.py +0 -0
  140. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/tests/test_projects.py +0 -0
  141. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/tests/test_references.py +0 -0
  142. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/tests/test_runtime.py +0 -0
  143. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/tests/test_schema_resources.py +0 -0
  144. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/tests/test_schemas.py +0 -0
  145. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/tests/test_signatures.py +0 -0
  146. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/tests/test_specs.py +0 -0
  147. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/tests/test_store.py +0 -0
  148. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/tests/test_toolchains.py +0 -0
  149. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/tests/test_v1_precision.py +0 -0
  150. {lean_runtime-2.0.8 → lean_runtime-2.1.0}/tests/test_verification_inventory.py +0 -0
@@ -1,6 +1,50 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 2.1.0 - 2026-08-11
4
+
5
+ - Add `lean-runtime toolchain-slim` and
6
+ `ToolchainManager.materialize_slim()`: a verified check-profile toolchain
7
+ copy that drops editor indexes, static libraries, bundled LLVM/clang, and
8
+ sources (about 2.6 GB → 2.1 GB for v4.32.2 once `--prune-original` removes
9
+ the full Elan copy). Materialization hardlinks files, verification runs a
10
+ capability corpus with the slim copy's own `lean`, and checking transparently
11
+ routes through the slim copy when the full toolchain is absent. Lean v4.32
12
+ requires all `.olean` facets and per-module IR for ordinary elaboration, so
13
+ those remain; source builds of new environments still need the full
14
+ toolchain.
15
+
16
+ - Exempt environment acquisition (downloads, toolchain installs, source
17
+ builds) from the discovery search budget: a slow first-time download can no
18
+ longer expire an otherwise healthy search. Acquisition is bounded separately
19
+ by `DiscoveryPolicy.acquisition_timeout_seconds` and `lean-run
20
+ --acquire-timeout`; `--search-timeout` and `--check-timeout` name the
21
+ remaining budgets, with `--discovery-timeout` and `--timeout` kept as
22
+ deprecated aliases.
23
+ - Split the candidate probe into `acquire` and `check` phases and announce
24
+ first-time toolchain installation through a `toolchain.install_started`
25
+ event and a `lean-run` progress line. `CandidateProbe` implementers must now
26
+ provide both methods; `acquire` returns the newly exported
27
+ `AcquiredCandidate`, and its acquisition budget is forwarded to
28
+ `open_exact(build_timeout=...)` so `--acquire-timeout` governs source
29
+ builds.
30
+ - Add `ExecutionResult.errors`, `.warnings`, and `.first_error` severity views,
31
+ a `Diagnostic.location` property, and compact `repr` output for both types.
32
+ `to_dict()` payloads are unchanged.
33
+ - Report structured diagnostics against the caller's logical input names
34
+ (for example `Main.lean`) instead of staged sandbox paths, and rewrite the
35
+ staged entrypoint path back to the user's file in `lean-run` text output.
36
+ Raw `stdout`/`stderr` remain authoritative and unmodified in results.
37
+ - Add `lean.setup(toolchain="v4.32.2")` and `Runtime.open_toolchain()` for
38
+ reusable core-only environments; the empty-`deps` error now teaches the
39
+ toolchain form.
40
+ - Accept an exact lock path anywhere `lean-runtime profile` previously
41
+ required an environment name (`Runtime.subject_environment`); a path that
42
+ exists on disk always wins over an environment name.
43
+
44
+ ## 2.0.9 - 2026-08-10
45
+
46
+ - Allow callers and `build-and-publish --timeout` to raise the per-step limit
47
+ for artifact hydration and environment builds on slower platforms.
4
48
 
5
49
  ## 2.0.8 - 2026-08-10
6
50
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lean-runtime
3
- Version: 2.0.8
3
+ Version: 2.1.0
4
4
  Summary: Run Lean 4 proofs from Python or standalone files
5
5
  Author: Alejandro Radisic
6
6
  License-Expression: Apache-2.0
@@ -131,6 +131,24 @@ result = env.check(
131
131
  result.raise_for_error()
132
132
  ```
133
133
 
134
+ Rejected proofs carry parsed diagnostics:
135
+
136
+ ```python
137
+ result = env.check(broken_proof)
138
+
139
+ for error in result.errors:
140
+ print(error.file, error.line, error.message)
141
+
142
+ result.raise_for_error() # raises LeanCheckError with the same detail
143
+ ```
144
+
145
+ Core-only work does not need a dependency:
146
+
147
+ ```python
148
+ core = lean.setup(toolchain="v4.32.2")
149
+ core.check("example : 2 + 2 = 4 := rfl").raise_for_error()
150
+ ```
151
+
134
152
  Batch and asyncio APIs reuse that prepared environment:
135
153
 
136
154
  ```python
@@ -96,6 +96,24 @@ result = env.check(
96
96
  result.raise_for_error()
97
97
  ```
98
98
 
99
+ Rejected proofs carry parsed diagnostics:
100
+
101
+ ```python
102
+ result = env.check(broken_proof)
103
+
104
+ for error in result.errors:
105
+ print(error.file, error.line, error.message)
106
+
107
+ result.raise_for_error() # raises LeanCheckError with the same detail
108
+ ```
109
+
110
+ Core-only work does not need a dependency:
111
+
112
+ ```python
113
+ core = lean.setup(toolchain="v4.32.2")
114
+ core.check("example : 2 + 2 = 4 := rfl").raise_for_error()
115
+ ```
116
+
99
117
  Batch and asyncio APIs reuse that prepared environment:
100
118
 
101
119
  ```python
@@ -18,9 +18,14 @@ lean-run Main.lean --offline
18
18
 
19
19
  Without explicit context or a pinned Lake project, `lean-run` automatically
20
20
  searches its bundled exact-environment catalog. Use `--lock-out` to retain the
21
- successful lock, `--no-discover` to require explicit context,
22
- `--catalog PATH` to override the catalog, and `--max-candidates` plus
23
- `--discovery-timeout` to bound search. See
21
+ successful lock, `--no-discover` to require explicit context, and
22
+ `--catalog PATH` to override the catalog. Three budgets bound the work:
23
+ `--search-timeout` covers ranking and compiler probes, `--check-timeout`
24
+ covers each Lean invocation, and `--acquire-timeout` independently bounds
25
+ downloading, installing, or building one candidate environment, so a slow
26
+ first-time download cannot expire the search. `--discovery-timeout` and
27
+ `--timeout` remain as deprecated aliases of `--search-timeout` and
28
+ `--check-timeout`. See
24
29
  [Standalone Lean files](standalone-files.md) for routing and policy details.
25
30
 
26
31
  ## `lean-runtime`
@@ -79,6 +84,34 @@ identity, package Git trees, computer compatibility, and Lean probe before
79
84
  making the environment available. See [Portable copies and environment
80
85
  libraries](portable-copies.md) for its trust boundary.
81
86
 
87
+ ## Slim toolchains
88
+
89
+ The official Lean toolchain is roughly 2.6 GB installed. Proof checking does
90
+ not need all of it: editor indexes, static libraries, the bundled LLVM/clang,
91
+ and toolchain sources are only used by editors and native compilation.
92
+
93
+ ```bash
94
+ lean-runtime toolchain-slim v4.32.2
95
+ lean-runtime toolchain-slim v4.32.2 --prune-original
96
+ ```
97
+
98
+ `toolchain-slim` materializes a separate check-profile copy by hardlinking the
99
+ kept files (near-zero extra disk), then verifies it against a capability
100
+ corpus — elaboration, core tactics, `decide`, `#eval`, metaprogramming, and
101
+ `Std` imports — using the slim copy's own `lean`. A copy that fails any probe
102
+ is removed and reported.
103
+
104
+ `--prune-original` uninstalls the full Elan toolchain after verification,
105
+ reducing the v4.32.2 footprint from about 2.6 GB to about 2.1 GB. All checking
106
+ continues through the slim copy. Lean v4.32 loads every `.olean` facet and
107
+ per-module IR during ordinary elaboration, so those artifact classes must
108
+ stay; larger reductions require upstream facet-loading changes.
109
+
110
+ After pruning, source builds of *new* environments and native compilation
111
+ need the full toolchain again; reinstall it with `lean-runtime install`.
112
+ Downloads are unaffected: first-time installation still transfers the full
113
+ official release before a slim copy can be derived from it.
114
+
82
115
  ## Replay
83
116
 
84
117
  ```bash
@@ -75,17 +75,27 @@ result.raise_for_error()
75
75
  `setup()` resolves and ensures the environment once. Further checks reuse that
76
76
  handle, including batch and asynchronous calls.
77
77
 
78
- The same entry point opens local projects, exact locks, and previously named
79
- environments:
78
+ The same entry point opens local projects, exact locks, previously named
79
+ environments, and core-only toolchains:
80
80
 
81
81
  ```python
82
82
  project = lean.setup(project="./my-project")
83
83
  locked = lean.setup(lock="environment.lock.json")
84
84
  existing = lean.setup(environment="research-stack")
85
+ core = lean.setup(toolchain="v4.32.2")
85
86
  ```
86
87
 
87
88
  Exactly one context must be supplied.
88
89
 
90
+ Rejected checks expose parsed diagnostics directly, with `file` naming your
91
+ logical input:
92
+
93
+ ```python
94
+ result = environment.check(broken_proof)
95
+ for error in result.errors:
96
+ print(error.file, error.line, error.message)
97
+ ```
98
+
89
99
  ## Lock for CI
90
100
 
91
101
  Resolve a friendly dependency declaration and retain its exact graph:
@@ -12,9 +12,13 @@ environment = lean.setup(["mathlib@v4.32.2"])
12
12
  project = lean.setup(project="./my-project")
13
13
  locked = lean.setup(lock="environment.lock.json")
14
14
  existing = lean.setup(environment="research-stack")
15
+ core = lean.setup(toolchain="v4.32.2")
15
16
  ```
16
17
 
17
- Exactly one of `deps`, `project`, `lock`, or `environment` is required. The
18
+ Exactly one of `deps`, `project`, `lock`, `environment`, or a bare
19
+ `toolchain` is required. A bare `toolchain` prepares the core-only
20
+ environment for that Lean release; combined with `deps` or `project` it
21
+ remains an override. The
18
22
  default `Runtime` is created lazily on the first operation; importing
19
23
  `lean_runtime` has no filesystem or network side effects. Supply
20
24
  `runtime=Runtime(...)` to any façade function for explicit configuration.
@@ -53,7 +57,24 @@ runs. Planning metadata narrows candidates but never asserts compatibility.
53
57
 
54
58
  ## Results
55
59
 
56
- Results remain inspectable values. Scripts that prefer exceptions can use:
60
+ Results remain inspectable values. Failed checks expose their parsed
61
+ diagnostics directly:
62
+
63
+ ```python
64
+ result = environment.check(source)
65
+
66
+ for error in result.errors:
67
+ print(error.file, error.line, error.message)
68
+
69
+ if result.first_error is not None:
70
+ print(result.first_error.location)
71
+ ```
72
+
73
+ `errors` and `warnings` are severity-filtered views of `diagnostics`;
74
+ `first_error` returns the first error or `None`. Diagnostic `file` values name
75
+ the caller's logical input (for example `Main.lean`); raw compiler output
76
+ remains authoritative on `stdout`/`stderr` and may contain physical sandbox
77
+ paths. Scripts that prefer exceptions can use:
57
78
 
58
79
  ```python
59
80
  result.raise_for_error()
@@ -15,9 +15,21 @@ environment is compatible. Runtime materializes each bounded candidate and
15
15
  Lean authoritatively checks the source before discovery can succeed.
16
16
 
17
17
  The bundled bootstrap catalog contains core Lean v4.32.2 plus exact Mathlib
18
- v4.32.2, v4.31.0, and v4.30.0 locks. `--catalog PATH` selects another validated catalog,
19
- `--max-candidates` and `--discovery-timeout` bound the search, and
20
- `--no-discover` restores strict explicit-context behavior.
18
+ v4.32.2, v4.31.0, and v4.30.0 locks. `--catalog PATH` selects another validated
19
+ catalog and `--no-discover` restores strict explicit-context behavior.
20
+
21
+ Three budgets bound the work independently:
22
+
23
+ - `--max-candidates` and `--search-timeout` bound the search itself: ranking
24
+ plus compiler probes.
25
+ - `--check-timeout` bounds each Lean invocation.
26
+ - `--acquire-timeout` bounds downloading, installing, or building one
27
+ candidate environment, including its source build. Acquisition never
28
+ consumes the search budget, so a slow first-time toolchain download cannot
29
+ expire an otherwise healthy search.
30
+
31
+ `--discovery-timeout` and `--timeout` remain as deprecated aliases of
32
+ `--search-timeout` and `--check-timeout`.
21
33
 
22
34
  ## Frontmatter format
23
35
 
@@ -53,12 +65,21 @@ environments only.
53
65
 
54
66
  ## Output
55
67
 
56
- Human output is concise:
68
+ Human output is concise, and error locations name the file you passed rather
69
+ than an internal staging path:
57
70
 
58
71
  ```text
59
72
  ✓ Main.lean accepted in 0.42s
60
73
  ```
61
74
 
75
+ ```text
76
+ proofs/Bad.lean:1:23: error: Type mismatch
77
+ ✗ proofs/Bad.lean rejected in 0.35s
78
+ ```
79
+
80
+ Only the checked file's own path is rewritten; dependency paths in compiler
81
+ output are shown as Lean printed them.
82
+
62
83
  Cold operations report meaningful dependency, cache, and build phases on
63
84
  stderr. `--quiet` suppresses progress and `--json` emits the complete structured
64
85
  `ExecutionResult` without progress messages.
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Trusted local execution
4
4
 
5
- Lean Runtime 0.5 orchestrates trusted code. It is not a sandbox.
5
+ Lean Runtime orchestrates trusted code. It is not a sandbox.
6
6
 
7
7
  Lake configurations are executable, dependencies may contain native code and
8
8
  custom build targets, and explicit artifact commands run subprocesses. Lockfile
@@ -142,6 +142,12 @@ def parser() -> argparse.ArgumentParser:
142
142
  push.add_argument("--publish-to", required=True)
143
143
  push.add_argument("--tag", action="append", default=[])
144
144
  push.add_argument("--name")
145
+ push.add_argument(
146
+ "--timeout",
147
+ type=float,
148
+ default=1800,
149
+ help="maximum seconds for each artifact hydration and the environment build",
150
+ )
145
151
  push.add_argument(
146
152
  "--platform-only",
147
153
  action="store_true",
@@ -265,8 +271,21 @@ def parser() -> argparse.ArgumentParser:
265
271
  diff.add_argument("right")
266
272
  diff.add_argument("--json", action="store_true")
267
273
 
274
+ toolchain_slim = commands.add_parser(
275
+ "toolchain-slim",
276
+ help="materialize a verified slim check-profile copy of a toolchain",
277
+ )
278
+ toolchain_slim.add_argument("toolchain", help="Lean toolchain, e.g. v4.32.2")
279
+ toolchain_slim.add_argument(
280
+ "--prune-original",
281
+ action="store_true",
282
+ help="uninstall the full Elan toolchain after verification; checking keeps "
283
+ "working through the slim copy, but source builds of new environments and "
284
+ "native compilation need the full toolchain again",
285
+ )
286
+
268
287
  profile = commands.add_parser("profile", help="measure repeated checks in one environment")
269
- profile.add_argument("environment")
288
+ profile.add_argument("environment", help="environment name or exact lock path")
270
289
  profile.add_argument("file", type=Path)
271
290
  profile.add_argument("--warmup", type=int, default=1)
272
291
  profile.add_argument("--repeat", type=int, default=5)
@@ -368,7 +387,11 @@ def main(argv: list[str] | None = None) -> int:
368
387
  _json(environment.inspect().to_dict())
369
388
  return 0
370
389
  if args.command == "build-and-publish":
371
- environment = runtime.open_exact(EnvironmentLock.load(args.lock), name=args.name)
390
+ environment = runtime.open_exact(
391
+ EnvironmentLock.load(args.lock),
392
+ name=args.name,
393
+ build_timeout=args.timeout,
394
+ )
372
395
  _json(
373
396
  runtime.publish_environment(
374
397
  environment.id,
@@ -509,6 +532,18 @@ def main(argv: list[str] | None = None) -> int:
509
532
  if args.command == "storage":
510
533
  _json(runtime.store_status().to_dict())
511
534
  return 0
535
+ if args.command == "toolchain-slim":
536
+ manifest = runtime.toolchains.materialize_slim(args.toolchain)
537
+ if args.prune_original:
538
+ runtime.toolchains.prune_original(args.toolchain)
539
+ _json(
540
+ {
541
+ **manifest.to_dict(),
542
+ "path": str(runtime.toolchains.slim_path(args.toolchain)),
543
+ "pruned_original": args.prune_original,
544
+ }
545
+ )
546
+ return 0
512
547
  if args.command == "doctor":
513
548
  doctor_report = runtime.doctor()
514
549
  _json(doctor_report.to_dict())
@@ -3,6 +3,8 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import re
6
+ from collections.abc import Mapping
7
+ from dataclasses import replace
6
8
 
7
9
  from .models import Diagnostic, Severity
8
10
 
@@ -46,6 +48,26 @@ def parse_diagnostics(output: str) -> tuple[Diagnostic, ...]:
46
48
  return tuple(diagnostics)
47
49
 
48
50
 
51
+ def map_diagnostic_paths(
52
+ diagnostics: tuple[Diagnostic, ...],
53
+ path_map: Mapping[str, str] | None,
54
+ ) -> tuple[Diagnostic, ...]:
55
+ """Rewrite staged physical paths to the caller's logical input names.
56
+
57
+ Only exact staged-file matches are rewritten; package files may
58
+ legitimately share basenames, and raw compiler output remains authoritative
59
+ on the execution result.
60
+ """
61
+ if not path_map:
62
+ return diagnostics
63
+ return tuple(
64
+ replace(item, file=path_map[item.file])
65
+ if item.file is not None and item.file in path_map
66
+ else item
67
+ for item in diagnostics
68
+ )
69
+
70
+
49
71
  def error_diagnostic(message: str) -> Diagnostic:
50
72
  """Create an infrastructure diagnostic."""
51
73
  severity: Severity = "error"
@@ -21,7 +21,7 @@ from .defaults import DEFAULT_CATALOG_PATH, default_catalog
21
21
  from .errors import CatalogError, DiscoveryError, PolicyError
22
22
  from .planner import Planner
23
23
  from .policy import DiscoveryPolicy
24
- from .probe import CandidateProbe, LeanRuntimeProbe, ProbeOutcome
24
+ from .probe import AcquiredCandidate, CandidateProbe, LeanRuntimeProbe, ProbeOutcome
25
25
  from .result import (
26
26
  RESULT_SCHEMA,
27
27
  CandidateAttempt,
@@ -39,6 +39,7 @@ __all__ = [
39
39
  "AvailabilityObservation",
40
40
  "Candidate",
41
41
  "CandidateAttempt",
42
+ "AcquiredCandidate",
42
43
  "CandidateProbe",
43
44
  "CandidateReason",
44
45
  "Catalog",
@@ -68,6 +68,61 @@ def _diagnostic(code: str, detail: str) -> tuple[ResultDiagnostic, ...]:
68
68
  return (ResultDiagnostic(code=code, detail=detail),)
69
69
 
70
70
 
71
+ def _failed_attempt(
72
+ exc: Exception,
73
+ *,
74
+ candidate_id: str,
75
+ lock_id: str,
76
+ attempt_started: float,
77
+ cancelled_externally: bool,
78
+ timed_out: bool,
79
+ ) -> tuple[CandidateAttempt, str]:
80
+ """Map one probe exception to an attempt and an action: continue, failed, or cancelled."""
81
+ duration = time.monotonic() - attempt_started
82
+ if isinstance(exc, ProbeIntegrityFailure):
83
+ status: AttemptStatus = "environment_integrity_failure"
84
+ code = CANDIDATE_INTEGRITY_FAILURE
85
+ action = "failed"
86
+ elif isinstance(exc, ProbeUnavailable):
87
+ if cancelled_externally:
88
+ status = "cancelled"
89
+ code = CANDIDATE_CANCELLED
90
+ action = "cancelled"
91
+ elif timed_out:
92
+ status = "timeout"
93
+ code = CANDIDATE_TIMEOUT
94
+ action = "continue"
95
+ else:
96
+ status = "environment_unavailable"
97
+ code = CANDIDATE_UNAVAILABLE
98
+ action = "continue"
99
+ elif isinstance(exc, LeanRuntimeError):
100
+ if cancelled_externally:
101
+ status = "cancelled"
102
+ code = CANDIDATE_CANCELLED
103
+ action = "cancelled"
104
+ elif timed_out:
105
+ status = "timeout"
106
+ code = CANDIDATE_TIMEOUT
107
+ action = "continue"
108
+ else:
109
+ status = "runtime_error"
110
+ code = CANDIDATE_RUNTIME_ERROR
111
+ action = "failed"
112
+ else: # defensive adapter boundary
113
+ status = "runtime_error"
114
+ code = CANDIDATE_RUNTIME_ERROR
115
+ action = "generic_failed"
116
+ attempt = CandidateAttempt(
117
+ candidate_id=candidate_id,
118
+ lock_id=lock_id,
119
+ status=status,
120
+ duration_seconds=duration,
121
+ diagnostics=_diagnostic(code, str(exc)),
122
+ )
123
+ return attempt, action
124
+
125
+
71
126
  def _rejection_detail(stderr: str, stdout: str) -> str:
72
127
  return stderr.strip() or stdout.strip() or "Lean rejected the source"
73
128
 
@@ -130,114 +185,77 @@ def discover(
130
185
  duration_seconds=time.monotonic() - started,
131
186
  diagnostics=_diagnostic(DISCOVERY_TIME_LIMIT, "total search budget expired"),
132
187
  )
133
- timeout = min(
134
- remaining,
135
- plan.policy.candidate_timeout_seconds or remaining,
136
- )
137
188
  attempt_started = time.monotonic()
138
- with _AttemptCancellation(timeout, cancel) as attempt_cancel:
189
+ acquisition_timeout = plan.policy.acquisition_timeout_seconds
190
+ with _AttemptCancellation(acquisition_timeout, cancel) as acquisition_cancel:
139
191
  try:
140
- outcome = probe.check(
141
- candidate,
142
- source,
143
- timeout_seconds=timeout,
144
- cancel=attempt_cancel.cancel,
145
- )
146
- except ProbeIntegrityFailure as exc:
147
- attempt = CandidateAttempt(
148
- candidate_id=candidate.entry.id,
149
- lock_id=candidate.entry.lock.lock_id,
150
- status="environment_integrity_failure",
151
- duration_seconds=time.monotonic() - attempt_started,
152
- diagnostics=_diagnostic(CANDIDATE_INTEGRITY_FAILURE, str(exc)),
153
- )
154
- attempts.append(attempt)
155
- return DiscoveryResult(
156
- status="failed",
157
- confidence="exhausted",
158
- plan=plan,
159
- attempts=tuple(attempts),
160
- duration_seconds=time.monotonic() - started,
161
- diagnostics=attempt.diagnostics,
162
- )
163
- except ProbeUnavailable as exc:
164
- if cancel is not None and cancel.is_set():
165
- attempt = CandidateAttempt(
166
- candidate_id=candidate.entry.id,
167
- lock_id=candidate.entry.lock.lock_id,
168
- status="cancelled",
169
- duration_seconds=time.monotonic() - attempt_started,
170
- diagnostics=_diagnostic(CANDIDATE_CANCELLED, str(exc)),
171
- )
172
- attempts.append(attempt)
173
- return DiscoveryResult(
174
- status="cancelled",
175
- confidence="exhausted",
176
- plan=plan,
177
- attempts=tuple(attempts),
178
- duration_seconds=time.monotonic() - started,
179
- diagnostics=attempt.diagnostics,
192
+ # Acquisition (downloads, toolchain installs, builds) is
193
+ # budgeted separately; every acquisition outcome, including
194
+ # failures, credits its elapsed time back to the search budget.
195
+ try:
196
+ acquired = probe.acquire(
197
+ candidate,
198
+ timeout_seconds=acquisition_timeout,
199
+ cancel=acquisition_cancel.cancel,
180
200
  )
181
- unavailable_status: AttemptStatus = (
182
- "timeout" if attempt_cancel.timed_out.is_set() else "environment_unavailable"
183
- )
184
- code = (
185
- CANDIDATE_TIMEOUT if unavailable_status == "timeout" else CANDIDATE_UNAVAILABLE
186
- )
187
- attempts.append(
188
- CandidateAttempt(
189
- candidate_id=candidate.entry.id,
190
- lock_id=candidate.entry.lock.lock_id,
191
- status=unavailable_status,
192
- duration_seconds=time.monotonic() - attempt_started,
193
- diagnostics=_diagnostic(code, str(exc)),
194
- )
195
- )
196
- continue
197
- except LeanRuntimeError as exc:
198
- if cancel is not None and cancel.is_set():
199
- error_status: AttemptStatus = "cancelled"
200
- code = CANDIDATE_CANCELLED
201
- elif attempt_cancel.timed_out.is_set():
202
- error_status = "timeout"
203
- code = CANDIDATE_TIMEOUT
204
- else:
205
- error_status = "runtime_error"
206
- code = CANDIDATE_RUNTIME_ERROR
207
- attempt = CandidateAttempt(
201
+ finally:
202
+ deadline += time.monotonic() - attempt_started
203
+ except Exception as exc: # defensive adapter boundary
204
+ attempt, action = _failed_attempt(
205
+ exc,
208
206
  candidate_id=candidate.entry.id,
209
207
  lock_id=candidate.entry.lock.lock_id,
210
- status=error_status,
211
- duration_seconds=time.monotonic() - attempt_started,
212
- diagnostics=_diagnostic(code, str(exc)),
208
+ attempt_started=attempt_started,
209
+ cancelled_externally=cancel is not None and cancel.is_set(),
210
+ timed_out=acquisition_cancel.timed_out.is_set(),
213
211
  )
214
212
  attempts.append(attempt)
215
- if error_status == "timeout":
213
+ if action == "continue":
216
214
  continue
217
215
  return DiscoveryResult(
218
- status="cancelled" if error_status == "cancelled" else "failed",
216
+ status="cancelled" if action == "cancelled" else "failed",
219
217
  confidence="exhausted",
220
218
  plan=plan,
221
219
  attempts=tuple(attempts),
222
220
  duration_seconds=time.monotonic() - started,
223
- diagnostics=attempt.diagnostics,
221
+ diagnostics=_diagnostic(DISCOVERY_RUNTIME_ERROR, str(exc))
222
+ if action == "generic_failed"
223
+ else attempt.diagnostics,
224
+ )
225
+ remaining = deadline - time.monotonic()
226
+ timeout = min(
227
+ remaining,
228
+ plan.policy.candidate_timeout_seconds or remaining,
229
+ )
230
+ with _AttemptCancellation(timeout, cancel) as attempt_cancel:
231
+ try:
232
+ outcome = probe.check(
233
+ acquired,
234
+ source,
235
+ timeout_seconds=timeout,
236
+ cancel=attempt_cancel.cancel,
224
237
  )
225
238
  except Exception as exc: # defensive adapter boundary
226
- attempt = CandidateAttempt(
239
+ attempt, action = _failed_attempt(
240
+ exc,
227
241
  candidate_id=candidate.entry.id,
228
242
  lock_id=candidate.entry.lock.lock_id,
229
- status="runtime_error",
230
- duration_seconds=time.monotonic() - attempt_started,
231
- diagnostics=_diagnostic(CANDIDATE_RUNTIME_ERROR, str(exc)),
243
+ attempt_started=attempt_started,
244
+ cancelled_externally=cancel is not None and cancel.is_set(),
245
+ timed_out=attempt_cancel.timed_out.is_set(),
232
246
  )
233
247
  attempts.append(attempt)
248
+ if action == "continue":
249
+ continue
234
250
  return DiscoveryResult(
235
- status="failed",
251
+ status="cancelled" if action == "cancelled" else "failed",
236
252
  confidence="exhausted",
237
253
  plan=plan,
238
254
  attempts=tuple(attempts),
239
255
  duration_seconds=time.monotonic() - started,
240
- diagnostics=_diagnostic(DISCOVERY_RUNTIME_ERROR, str(exc)),
256
+ diagnostics=_diagnostic(DISCOVERY_RUNTIME_ERROR, str(exc))
257
+ if action == "generic_failed"
258
+ else attempt.diagnostics,
241
259
  )
242
260
 
243
261
  result = outcome.execution_result