mikeljl commited on
Commit
ec4e190
Β·
1 Parent(s): e4faaaa

update to include tracks, remove scoring on hf

Browse files
This view is limited to 50 files because it contains too many changes. Β  See raw diff
Files changed (50) hide show
  1. .dockerignore +9 -11
  2. .gitattributes +2 -0
  3. .gitignore +0 -20
  4. Dockerfile +7 -48
  5. PyPantograph/.github/workflows/doc.yaml +0 -54
  6. PyPantograph/.github/workflows/test.yaml +0 -38
  7. PyPantograph/.gitignore +0 -11
  8. PyPantograph/LICENSE +0 -190
  9. PyPantograph/README.md +0 -77
  10. PyPantograph/build-pantograph.py +0 -25
  11. PyPantograph/doc/.gitignore +0 -1
  12. PyPantograph/doc/_config.yml +0 -71
  13. PyPantograph/doc/_toc.yml +0 -16
  14. PyPantograph/doc/agent-search.ipynb +0 -155
  15. PyPantograph/doc/api-data.rst +0 -5
  16. PyPantograph/doc/api-expr.rst +0 -8
  17. PyPantograph/doc/api-message.rst +0 -5
  18. PyPantograph/doc/api-search.rst +0 -5
  19. PyPantograph/doc/api-server.rst +0 -5
  20. PyPantograph/doc/frontend.ipynb +0 -401
  21. PyPantograph/doc/goal.ipynb +0 -662
  22. PyPantograph/doc/intro.md +0 -79
  23. PyPantograph/doc/setup.md +0 -85
  24. PyPantograph/examples/Example/.gitignore +0 -3
  25. PyPantograph/examples/Example/Example.lean +0 -14
  26. PyPantograph/examples/Example/lake-manifest.json +0 -25
  27. PyPantograph/examples/Example/lakefile.lean +0 -10
  28. PyPantograph/examples/Example/lean-toolchain +0 -1
  29. PyPantograph/examples/README.md +0 -24
  30. PyPantograph/examples/aesop.py +0 -14
  31. PyPantograph/examples/branch-sorry.py +0 -13
  32. PyPantograph/examples/simple.py +0 -7
  33. PyPantograph/examples/sketch.py +0 -41
  34. PyPantograph/pantograph/.gitignore +0 -2
  35. PyPantograph/pantograph/__init__.py +0 -1
  36. PyPantograph/pantograph/data.py +0 -113
  37. PyPantograph/pantograph/expr.py +0 -202
  38. PyPantograph/pantograph/message.py +0 -73
  39. PyPantograph/pantograph/search.py +0 -450
  40. PyPantograph/pantograph/server.py +0 -695
  41. PyPantograph/pantograph/test_message.py +0 -25
  42. PyPantograph/pantograph/test_search.py +0 -55
  43. PyPantograph/pantograph/test_server.py +0 -381
  44. PyPantograph/pantograph/utils.py +0 -48
  45. PyPantograph/pyproject.toml +0 -39
  46. PyPantograph/src/.gitignore +0 -6
  47. PyPantograph/src/.pre-commit-config.yaml +0 -30
  48. PyPantograph/src/LICENSE +0 -190
  49. PyPantograph/src/Main.lean +0 -80
  50. PyPantograph/src/Pantograph.lean +0 -9
.dockerignore CHANGED
@@ -1,12 +1,10 @@
1
- PyPantograph/.venv
2
- PyPantograph/dist
3
- PyPantograph/pantograph/pantograph-repl
4
- PyPantograph/src/.lake
5
- lean-project-copy
6
- lean-project/.lake
7
- cslib/.lake
8
- cslib/.git
9
- cslib_docker_test
10
- **/__pycache__
11
- **/*.pyc
12
  .git
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  .git
2
+ .gitignore
3
+ .gitattributes
4
+ .claude
5
+ CLAUDE.md
6
+ __pycache__
7
+ *.pyc
8
+ .DS_Store
9
+ README.md
10
+ leaderboard.json
.gitattributes CHANGED
@@ -36,3 +36,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
36
  assets/icon.png filter=lfs diff=lfs merge=lfs -text
37
  assets/sponsors/cslib.png filter=lfs diff=lfs merge=lfs -text
38
  assets/sponsors/PhysLibLogo.jpeg filter=lfs diff=lfs merge=lfs -text
 
 
 
36
  assets/icon.png filter=lfs diff=lfs merge=lfs -text
37
  assets/sponsors/cslib.png filter=lfs diff=lfs merge=lfs -text
38
  assets/sponsors/PhysLibLogo.jpeg filter=lfs diff=lfs merge=lfs -text
39
+ assets/benchmarks/cslib.png filter=lfs diff=lfs merge=lfs -text
40
+ assets/benchmarks/putnambench.png filter=lfs diff=lfs merge=lfs -text
.gitignore CHANGED
@@ -7,23 +7,3 @@ gradio_cached_examples/
7
  flagged/
8
  leaderboard.json
9
  .leaderboard.*.json
10
-
11
- # Lean build artifacts β€” rebuilt inside the Docker image (lake cache get / build),
12
- # never pushed to the Space.
13
- lean-project/.lake/
14
- lean-project-copy/
15
- cslib/.lake/
16
-
17
- # Local cslib compilation-test scaffolding (Docker build contexts + logs).
18
- cslib_docker_test/build_ctx/
19
- cslib_docker_test/build_ctx_submit/
20
- cslib_docker_test/*.log
21
-
22
- # cslib files not needed by the Space (kept on disk, excluded from the repo).
23
- cslib/.github/
24
- cslib/.vscode/
25
- cslib/scripts/
26
- cslib/docs/
27
- cslib/eval_cslib_long.jsonl
28
- cslib/eval_cslib_long_optimized.jsonl
29
- cslib/make_eval_jsonl.py
 
7
  flagged/
8
  leaderboard.json
9
  .leaderboard.*.json
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Dockerfile CHANGED
@@ -1,60 +1,19 @@
1
  FROM python:3.11-slim
2
 
3
- # System packages: curl for elan install, git for lake deps later, ca-certs for HTTPS
4
- RUN apt-get update && apt-get install -y --no-install-recommends \
5
- curl ca-certificates git \
6
- && rm -rf /var/lib/apt/lists/*
7
-
8
  RUN useradd -m -u 1000 user
9
  USER user
10
  ENV HOME=/home/user \
11
- PATH=/home/user/.elan/bin:/home/user/.local/bin:$PATH
12
 
13
  WORKDIR $HOME/app
14
 
15
- # Python deps first (cached layer, rarely changes). uv is needed to build
16
- # PyPantograph's wheel (which triggers the poetry-core build hook that
17
- # compiles the pantograph-repl Lean binary).
18
- RUN pip install --no-cache-dir --user "gradio>=5,<6" "uv>=0.4"
19
-
20
- # Install elan (Lean toolchain manager). No default toolchain; the
21
- # project's `lean-toolchain` file controls the version.
22
- RUN curl -sSfL https://raw.githubusercontent.com/leanprover/elan/master/elan-init.sh \
23
- | sh -s -- -y --default-toolchain none --no-modify-path
24
-
25
- # Copy the Lean project skeleton, materialize the toolchain, fetch Mathlib
26
- # source + transitive deps, and download pre-built oleans (~5 GB). All of
27
- # this is baked into a Docker layer so cold starts on HF are instant.
28
- COPY --chown=user lean-project /home/user/lean-project
29
- WORKDIR /home/user/lean-project
30
- RUN lake --version \
31
- && lake update \
32
- && lake exe cache get
33
 
34
- # Bake the CSLib project for path-bearing benchmark rows (compiled in place,
35
- # not in the empty lean-project). Fresh copy β€” `.lake` is excluded via
36
- # .dockerignore β€” so deps are materialized from the committed manifest, Mathlib
37
- # oleans are fetched, and the library is built here. app.py compiles
38
- # project-resident submissions against /home/user/cslib (CSLIB_PROJECT_ROOT).
39
- COPY --chown=user cslib /home/user/cslib
40
- WORKDIR /home/user/cslib
41
- RUN lake exe cache get \
42
- && lake build
43
-
44
- # Build PyPantograph: copy the source, invoke `uv build` which runs the
45
- # poetry-core build hook (build-pantograph.py β†’ `lake build repl` inside
46
- # PyPantograph/src/) and packages the resulting binary into a wheel. Then
47
- # `pip install` that wheel into the user site-packages alongside gradio.
48
- COPY --chown=user PyPantograph /home/user/PyPantograph
49
- WORKDIR /home/user/PyPantograph
50
- RUN uv build \
51
- && pip install --no-cache-dir --user /home/user/PyPantograph/dist/pantograph-*.whl
52
-
53
- # App
54
- WORKDIR $HOME/app
55
- COPY --chown=user app.py utils.py benchmark.py leaderboard.py versions.py \
56
- pantograph_server.py project_compile.py \
57
- benchmark_data_new.jsonl benchmark_heartbeats.jsonl .
58
  COPY --chown=user assets /home/user/app/assets
59
 
60
  EXPOSE 7860
 
1
  FROM python:3.11-slim
2
 
 
 
 
 
 
3
  RUN useradd -m -u 1000 user
4
  USER user
5
  ENV HOME=/home/user \
6
+ PATH=/home/user/.local/bin:$PATH
7
 
8
  WORKDIR $HOME/app
9
 
10
+ # UI-only image: all Lean compilation and scoring runs on a dedicated
11
+ # evaluation worker that exchanges jobs/results with this Space through the
12
+ # shared storage bucket mounted at /data. Nothing Lean-related is installed.
13
+ RUN pip install --no-cache-dir --user "gradio>=5,<6"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
+ COPY --chown=user app.py benchmark.py leaderboard.py contribution.md \
16
+ benchmark_data_warmup.jsonl benchmark_heartbeats.jsonl .
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  COPY --chown=user assets /home/user/app/assets
18
 
19
  EXPOSE 7860
PyPantograph/.github/workflows/doc.yaml DELETED
@@ -1,54 +0,0 @@
1
- name: deploy-doc
2
-
3
- on:
4
- push:
5
- branches:
6
- - main
7
-
8
- # This job installs dependencies, builds the book, and pushes it to `gh-pages`
9
- jobs:
10
- deploy-book:
11
- runs-on: ubuntu-latest
12
- permissions:
13
- pages: write
14
- id-token: write
15
- steps:
16
- - uses: actions/checkout@v4
17
- with:
18
- submodules: true
19
-
20
- - name: Install Python
21
- uses: actions/setup-python@v4
22
- with:
23
- python-version: 3.11
24
-
25
- - name: Install elan
26
- run: |
27
- set -o pipefail
28
- curl -sSfL https://github.com/leanprover/elan/releases/download/v3.1.1/elan-x86_64-unknown-linux-gnu.tar.gz | tar xz
29
- ./elan-init -y --default-toolchain none
30
- echo "$HOME/.elan/bin" >> "${GITHUB_PATH}"
31
-
32
- - name: Install Lean
33
- run: |
34
- elan toolchain install $(<src/lean-toolchain)
35
-
36
- - name: Install uv
37
- run: |
38
- curl -LsSf https://astral.sh/uv/install.sh | sh
39
- uv sync
40
-
41
- - name: Build documentations
42
- run: |
43
- uv run --group dev jupyter-book build doc
44
-
45
- # Upload the book's HTML as an artifact
46
- - name: Upload artifact
47
- uses: actions/upload-pages-artifact@v3
48
- with:
49
- path: "doc/_build/html"
50
-
51
- # Deploy the book's HTML to GitHub Pages
52
- - name: Deploy to GitHub Pages
53
- id: deployment
54
- uses: actions/deploy-pages@v4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
PyPantograph/.github/workflows/test.yaml DELETED
@@ -1,38 +0,0 @@
1
- name: Unit Tests
2
- on: [push]
3
-
4
- jobs:
5
- build:
6
- runs-on: ubuntu-latest
7
- permissions:
8
- pages: write
9
- id-token: write
10
- steps:
11
- - uses: actions/checkout@v4
12
- with:
13
- submodules: true
14
-
15
- - name: Install Python
16
- uses: actions/setup-python@v4
17
- with:
18
- python-version: 3.11
19
-
20
- - name: Install elan
21
- run: |
22
- set -o pipefail
23
- curl -sSfL https://github.com/leanprover/elan/releases/download/v3.1.1/elan-x86_64-unknown-linux-gnu.tar.gz | tar xz
24
- ./elan-init -y --default-toolchain none
25
- echo "$HOME/.elan/bin" >> "${GITHUB_PATH}"
26
-
27
- - name: Install Lean
28
- run: |
29
- elan toolchain install $(<src/lean-toolchain)
30
-
31
- - name: Install uv
32
- run: |
33
- curl -LsSf https://astral.sh/uv/install.sh | sh
34
- uv sync
35
-
36
- - name: Run tests
37
- run: |
38
- uv run pytest
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
PyPantograph/.gitignore DELETED
@@ -1,11 +0,0 @@
1
- .*
2
- !.gitignore
3
- !.github
4
-
5
- # Python
6
- *.py[cod]
7
- *.egg-info
8
-
9
- # Output
10
- /dist
11
- /venv
 
 
 
 
 
 
 
 
 
 
 
 
PyPantograph/LICENSE DELETED
@@ -1,190 +0,0 @@
1
- Apache License
2
- Version 2.0, January 2004
3
- http://www.apache.org/licenses/
4
-
5
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
-
7
- 1. Definitions.
8
-
9
- "License" shall mean the terms and conditions for use, reproduction,
10
- and distribution as defined by Sections 1 through 9 of this document.
11
-
12
- "Licensor" shall mean the copyright owner or entity authorized by
13
- the copyright owner that is granting the License.
14
-
15
- "Legal Entity" shall mean the union of the acting entity and all
16
- other entities that control, are controlled by, or are under common
17
- control with that entity. For the purposes of this definition,
18
- "control" means (i) the power, direct or indirect, to cause the
19
- direction or management of such entity, whether by contract or
20
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
- outstanding shares, or (iii) beneficial ownership of such entity.
22
-
23
- "You" (or "Your") shall mean an individual or Legal Entity
24
- exercising permissions granted by this License.
25
-
26
- "Source" form shall mean the preferred form for making modifications,
27
- including but not limited to software source code, documentation
28
- source, and configuration files.
29
-
30
- "Object" form shall mean any form resulting from mechanical
31
- transformation or translation of a Source form, including but
32
- not limited to compiled object code, generated documentation,
33
- and conversions to other media types.
34
-
35
- "Work" shall mean the work of authorship, whether in Source or
36
- Object form, made available under the License, as indicated by a
37
- copyright notice that is included in or attached to the work
38
- (an example is provided in the Appendix below).
39
-
40
- "Derivative Works" shall mean any work, whether in Source or Object
41
- form, that is based on (or derived from) the Work and for which the
42
- editorial revisions, annotations, elaborations, or other modifications
43
- represent, as a whole, an original work of authorship. For the purposes
44
- of this License, Derivative Works shall not include works that remain
45
- separable from, or merely link (or bind by name) to the interfaces of,
46
- the Work and Derivative Works thereof.
47
-
48
- "Contribution" shall mean any work of authorship, including
49
- the original version of the Work and any modifications or additions
50
- to that Work or Derivative Works thereof, that is intentionally
51
- submitted to Licensor for inclusion in the Work by the copyright owner
52
- or by an individual or Legal Entity authorized to submit on behalf of
53
- the copyright owner. For the purposes of this definition, "submitted"
54
- means any form of electronic, verbal, or written communication sent
55
- to the Licensor or its representatives, including but not limited to
56
- communication on electronic mailing lists, source code control systems,
57
- and issue tracking systems that are managed by, or on behalf of, the
58
- Licensor for the purpose of discussing and improving the Work, but
59
- excluding communication that is conspicuously marked or otherwise
60
- designated in writing by the copyright owner as "Not a Contribution."
61
-
62
- "Contributor" shall mean Licensor and any individual or Legal Entity
63
- on behalf of whom a Contribution has been received by Licensor and
64
- subsequently incorporated within the Work.
65
-
66
- 2. Grant of Copyright License. Subject to the terms and conditions of
67
- this License, each Contributor hereby grants to You a perpetual,
68
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
- copyright license to reproduce, prepare Derivative Works of,
70
- publicly display, publicly perform, sublicense, and distribute the
71
- Work and such Derivative Works in Source or Object form.
72
-
73
- 3. Grant of Patent License. Subject to the terms and conditions of
74
- this License, each Contributor hereby grants to You a perpetual,
75
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
- (except as stated in this section) patent license to make, have made,
77
- use, offer to sell, sell, import, and otherwise transfer the Work,
78
- where such license applies only to those patent claims licensable
79
- by such Contributor that are necessarily infringed by their
80
- Contribution(s) alone or by combination of their Contribution(s)
81
- with the Work to which such Contribution(s) was submitted. If You
82
- institute patent litigation against any entity (including a
83
- cross-claim or counterclaim in a lawsuit) alleging that the Work
84
- or a Contribution incorporated within the Work constitutes direct
85
- or contributory patent infringement, then any patent licenses
86
- granted to You under this License for that Work shall terminate
87
- as of the date such litigation is filed.
88
-
89
- 4. Redistribution. You may reproduce and distribute copies of the
90
- Work or Derivative Works thereof in any medium, with or without
91
- modifications, and in Source or Object form, provided that You
92
- meet the following conditions:
93
-
94
- (a) You must give any other recipients of the Work or
95
- Derivative Works a copy of this License; and
96
-
97
- (b) You must cause any modified files to carry prominent notices
98
- stating that You changed the files; and
99
-
100
- (c) You must retain, in the Source form of any Derivative Works
101
- that You distribute, all copyright, patent, trademark, and
102
- attribution notices from the Source form of the Work,
103
- excluding those notices that do not pertain to any part of
104
- the Derivative Works; and
105
-
106
- (d) If the Work includes a "NOTICE" text file as part of its
107
- distribution, then any Derivative Works that You distribute must
108
- include a readable copy of the attribution notices contained
109
- within such NOTICE file, excluding those notices that do not
110
- pertain to any part of the Derivative Works, in at least one
111
- of the following places: within a NOTICE text file distributed
112
- as part of the Derivative Works; within the Source form or
113
- documentation, if provided along with the Derivative Works; or,
114
- within a display generated by the Derivative Works, if and
115
- wherever such third-party notices normally appear. The contents
116
- of the NOTICE file are for informational purposes only and
117
- do not modify the License. You may add Your own attribution
118
- notices within Derivative Works that You distribute, alongside
119
- or as an addendum to the NOTICE text from the Work, provided
120
- that such additional attribution notices cannot be construed
121
- as modifying the License.
122
-
123
- You may add Your own copyright statement to Your modifications and
124
- may provide additional or different license terms and conditions
125
- for use, reproduction, or distribution of Your modifications, or
126
- for any such Derivative Works as a whole, provided Your use,
127
- reproduction, and distribution of the Work otherwise complies with
128
- the conditions stated in this License.
129
-
130
- 5. Submission of Contributions. Unless You explicitly state otherwise,
131
- any Contribution intentionally submitted for inclusion in the Work
132
- by You to the Licensor shall be under the terms and conditions of
133
- this License, without any additional terms or conditions.
134
- Notwithstanding the above, nothing herein shall supersede or modify
135
- the terms of any separate license agreement you may have executed
136
- with Licensor regarding such Contributions.
137
-
138
- 6. Trademarks. This License does not grant permission to use the trade
139
- names, trademarks, service marks, or product names of the Licensor,
140
- except as required for reasonable and customary use in describing the
141
- origin of the Work and reproducing the content of the NOTICE file.
142
-
143
- 7. Disclaimer of Warranty. Unless required by applicable law or
144
- agreed to in writing, Licensor provides the Work (and each
145
- Contributor provides its Contributions) on an "AS IS" BASIS,
146
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
- implied, including, without limitation, any warranties or conditions
148
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
- PARTICULAR PURPOSE. You are solely responsible for determining the
150
- appropriateness of using or redistributing the Work and assume any
151
- risks associated with Your exercise of permissions under this License.
152
-
153
- 8. Limitation of Liability. In no event and under no legal theory,
154
- whether in tort (including negligence), contract, or otherwise,
155
- unless required by applicable law (such as deliberate and grossly
156
- negligent acts) or agreed to in writing, shall any Contributor be
157
- liable to You for damages, including any direct, indirect, special,
158
- incidental, or consequential damages of any character arising as a
159
- result of this License or out of the use or inability to use the
160
- Work (including but not limited to damages for loss of goodwill,
161
- work stoppage, computer failure or malfunction, or any and all
162
- other commercial damages or losses), even if such Contributor
163
- has been advised of the possibility of such damages.
164
-
165
- 9. Accepting Warranty or Additional Liability. While redistributing
166
- the Work or Derivative Works thereof, You may choose to offer,
167
- and charge a fee for, acceptance of support, warranty, indemnity,
168
- or other liability obligations and/or rights consistent with this
169
- License. However, in accepting such obligations, You may act only
170
- on Your own behalf and on Your sole responsibility, not on behalf
171
- of any other Contributor, and only if You agree to indemnify,
172
- defend, and hold each Contributor harmless for any liability
173
- incurred by, or claims asserted against, such Contributor by reason
174
- of your accepting any such warranty or additional liability.
175
-
176
- END OF TERMS AND CONDITIONS
177
-
178
- Copyright 2024 Leni Aniva
179
-
180
- Licensed under the Apache License, Version 2.0 (the "License");
181
- you may not use this file except in compliance with the License.
182
- You may obtain a copy of the License at
183
-
184
- http://www.apache.org/licenses/LICENSE-2.0
185
-
186
- Unless required by applicable law or agreed to in writing, software
187
- distributed under the License is distributed on an "AS IS" BASIS,
188
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
189
- See the License for the specific language governing permissions and
190
- limitations under the License.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
PyPantograph/README.md DELETED
@@ -1,77 +0,0 @@
1
- # PyPantograph
2
-
3
- [![arXiv](https://img.shields.io/badge/cs.LO-2410.16429-b31b1b?logo=arxiv&logoColor=red)](https://arxiv.org/abs/2410.16429)
4
-
5
- A Machine-to-Machine Interaction System for Lean 4.
6
-
7
- Features:
8
- - Programmatically execute tactics (`goal_tactic`)
9
- - Handling of metavariable coupling
10
- - Whole file specification conformity check (`check_track`)
11
- - Extraction of tactic invocation data
12
- - Inspection of Lean constants
13
-
14
- ## Installation
15
-
16
- 1. Install `uv`
17
- 2. Install `elan`: See [Lean Manual](https://docs.lean-lang.org/lean4/doc/setup.html)
18
-
19
- ### Install as a project dependency
20
-
21
- 3. Add the package to your project:
22
- ```sh
23
- uv add git+https://github.com/stanford-centaur/PyPantograph
24
- uv sync
25
- ```
26
-
27
- ### Build wheels from source
28
-
29
- 3. Clone this repository with submodules:
30
- ```sh
31
- git clone --recurse-submodules <repo-path>
32
- ```
33
- 4. Execute
34
- ```sh
35
- cd <repo-path>
36
- uv build
37
- ```
38
- 5. Built wheels can be found at `dist/*.whl`
39
-
40
- ## Documentation
41
-
42
- Build the documentations by
43
- ```sh
44
- uv run --group dev jupyter-book build doc
45
- ```
46
- Then serve
47
- ```sh
48
- python3 -m http.server -d doc/_build/html
49
- ```
50
-
51
- ### Examples
52
-
53
- For API interaction examples, see `examples/README.md`.
54
-
55
- ### Contributing
56
-
57
- Execute unit tests with
58
-
59
- ```sh
60
- uv run pytest
61
- ```
62
-
63
- ## Reference
64
-
65
- [Paper Link](https://arxiv.org/abs/2410.16429)
66
-
67
- ```bib
68
- @misc{pantograph,
69
- title={Pantograph: A Machine-to-Machine Interaction Interface for Advanced Theorem Proving, High Level Reasoning, and Data Extraction in Lean 4},
70
- author={Leni Aniva and Chuyue Sun and Brando Miranda and Clark Barrett and Sanmi Koyejo},
71
- year={2024},
72
- eprint={2410.16429},
73
- archivePrefix={arXiv},
74
- primaryClass={cs.LO},
75
- url={https://arxiv.org/abs/2410.16429},
76
- }
77
- ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
PyPantograph/build-pantograph.py DELETED
@@ -1,25 +0,0 @@
1
- #!/usr/bin/env python3
2
-
3
- import subprocess, shutil, os, stat
4
- from pathlib import Path
5
-
6
- # -- Define paths for Pantograph source and Pantograph Python interface
7
- PATH_PANTOGRAPH = Path("./src")
8
- PATH_PY = Path("./pantograph")
9
-
10
- # -- Build the REPL
11
- with subprocess.Popen(["lake", "build", "repl"], cwd=PATH_PANTOGRAPH) as p:
12
- p.wait()
13
- if p.returncode != 0:
14
- raise Exception(f"Error: 'lake build repl' failed with exit code {p.returncode}.")
15
-
16
- # -- Copy the REPL executable to the specified path
17
- path_executable = PATH_PY / "pantograph-repl"
18
- repl_src = "repl.exe" if os.name == "nt" else "repl"
19
- shutil.copyfile(PATH_PANTOGRAPH / f".lake/build/bin/{repl_src}", path_executable)
20
-
21
- # -- Make the REPL executable executable
22
- os.chmod(path_executable, os.stat(path_executable).st_mode | stat.S_IEXEC)
23
-
24
- # -- Copy the Lean toolchain file to the specified path
25
- shutil.copyfile(PATH_PANTOGRAPH / "lean-toolchain", PATH_PY / "lean-toolchain")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
PyPantograph/doc/.gitignore DELETED
@@ -1 +0,0 @@
1
- /_build
 
 
PyPantograph/doc/_config.yml DELETED
@@ -1,71 +0,0 @@
1
- # Book settings
2
- # Learn more at https://jupyterbook.org/customize/config.html
3
- # Comprehensive example: https://github.com/executablebooks/jupyter-book/blob/master/docs/_config.yml
4
-
5
- title: PyPantograph
6
- author: Leni Aniva
7
- #logo: logo.png
8
-
9
- # Force re-execution of notebooks on each build.
10
- # See https://jupyterbook.org/content/execute.html
11
- execute:
12
- execute_notebooks: 'off'
13
-
14
- # Define the name of the latex output file for PDF builds
15
- latex:
16
- latex_documents:
17
- targetname: book.tex
18
-
19
- # Add a bibtex file so that we can create citations
20
- #bibtex_bibfiles:
21
- # - references.bib
22
-
23
- # Information about where the book exists on the web
24
- repository:
25
- url: https://github.com/lenianiva/PyPantograph # Online location of your book
26
- path_to_book: docs # Optional path to your book, relative to the repository root
27
- branch: main # Which branch of the repository should be used when creating links (optional)
28
-
29
- # Add GitHub buttons to your book
30
- # See https://jupyterbook.org/customize/config.html#add-a-link-to-your-repository
31
- html:
32
- use_issues_button: true
33
- use_repository_button: true
34
-
35
- sphinx:
36
- config:
37
- intersphinx_mapping:
38
- ebp:
39
- - "https://executablebooks.org/en/latest/"
40
- - null
41
- myst-parser:
42
- - "https://myst-parser.readthedocs.io/en/latest/"
43
- - null
44
- myst-nb:
45
- - "https://myst-nb.readthedocs.io/en/latest/"
46
- - null
47
- sphinx:
48
- - "https://www.sphinx-doc.org/en/master"
49
- - null
50
- nbformat:
51
- - "https://nbformat.readthedocs.io/en/latest"
52
- - null
53
- sd:
54
- - "https://sphinx-design.readthedocs.io/en/latest"
55
- - null
56
- sphinxproof:
57
- - "https://sphinx-proof.readthedocs.io/en/latest/"
58
- - null
59
- hoverxref_intersphinx:
60
- - "sphinxproof"
61
- mathjax3_config:
62
- tex:
63
- macros:
64
- "N": "\\mathbb{N}"
65
- "floor": ["\\lfloor#1\\rfloor", 1]
66
- "bmat": ["\\left[\\begin{array}"]
67
- "emat": ["\\end{array}\\right]"]
68
-
69
- extra_extensions:
70
- - sphinx.ext.intersphinx
71
- - sphinx.ext.autodoc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
PyPantograph/doc/_toc.yml DELETED
@@ -1,16 +0,0 @@
1
- format: jb-book
2
- root: intro
3
- parts:
4
- - caption: Features
5
- chapters:
6
- - file: setup
7
- - file: goal
8
- - file: agent-search
9
- - file: frontend
10
- - caption: API Documentation
11
- chapters:
12
- - file: api-message
13
- - file: api-server
14
- - file: api-expr
15
- - file: api-data
16
- - file: api-search
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
PyPantograph/doc/agent-search.ipynb DELETED
@@ -1,155 +0,0 @@
1
- {
2
- "cells": [
3
- {
4
- "cell_type": "markdown",
5
- "id": "ec3abb52-d7cd-471f-b3b7-2d9681c79360",
6
- "metadata": {},
7
- "source": [
8
- "# Search\n",
9
- "\n",
10
- "Pantograph supports basic proof search. In this case, Pantograph treats goals as nodes on an and-or tree. The user supplies an agent which should provide two functions:\n",
11
- "\n",
12
- "1. *Tactic*: Which tactic should be used on a goal?\n",
13
- "2. *Guidance*: What is the search priority on a goal?\n",
14
- "\n",
15
- "The user agent should inherit from `pantograph.search.Agent`. Here is a brute force agent example:"
16
- ]
17
- },
18
- {
19
- "cell_type": "code",
20
- "execution_count": 1,
21
- "id": "959458f5-02e4-4f73-ae28-16a756aebed9",
22
- "metadata": {},
23
- "outputs": [],
24
- "source": [
25
- "from typing import Optional\n",
26
- "import collections\n",
27
- "from pantograph import Server\n",
28
- "from pantograph.search import Agent\n",
29
- "from pantograph.expr import GoalState, Tactic"
30
- ]
31
- },
32
- {
33
- "cell_type": "code",
34
- "execution_count": 2,
35
- "id": "8b402602-3ae5-43e4-9a62-2fa9e2c039fa",
36
- "metadata": {},
37
- "outputs": [],
38
- "source": [
39
- "class DumbAgent(Agent):\n",
40
- "\n",
41
- " def __init__(self):\n",
42
- " super().__init__()\n",
43
- "\n",
44
- " self.goal_tactic_id_map = collections.defaultdict(lambda : 0)\n",
45
- " self.intros = [\n",
46
- " \"intro\",\n",
47
- " ]\n",
48
- " self.tactics = [\n",
49
- " \"intro h\",\n",
50
- " \"cases h\",\n",
51
- " \"apply Or.inl\",\n",
52
- " \"apply Or.inr\",\n",
53
- " ]\n",
54
- " self.no_space_tactics = [\n",
55
- " \"assumption\",\n",
56
- " ]\n",
57
- "\n",
58
- " def next_tactic(\n",
59
- " self,\n",
60
- " state: GoalState,\n",
61
- " goal_id: int,\n",
62
- " ) -> Optional[Tactic]:\n",
63
- " key = (state.state_id, goal_id)\n",
64
- " i = self.goal_tactic_id_map[key]\n",
65
- "\n",
66
- " target = state.goals[goal_id].target\n",
67
- " if target.startswith('βˆ€'):\n",
68
- " tactics = self.intros\n",
69
- " elif ' ' in target:\n",
70
- " tactics = self.tactics\n",
71
- " else:\n",
72
- " tactics = self.no_space_tactics\n",
73
- "\n",
74
- " if i >= len(tactics):\n",
75
- " return None\n",
76
- "\n",
77
- " self.goal_tactic_id_map[key] = i + 1\n",
78
- " return tactics[i]"
79
- ]
80
- },
81
- {
82
- "cell_type": "markdown",
83
- "id": "665db9d0-5fff-4b26-9cea-32d06a6e1e04",
84
- "metadata": {},
85
- "source": [
86
- "Execute the search with `agent.search`."
87
- ]
88
- },
89
- {
90
- "cell_type": "code",
91
- "execution_count": 3,
92
- "id": "1c7961d1-b1fa-498c-ab75-16feb784ca2c",
93
- "metadata": {},
94
- "outputs": [
95
- {
96
- "data": {
97
- "text/plain": [
98
- "SearchResult(n_goals_root=1, duration=0.7717759609222412, success=True, steps=16)"
99
- ]
100
- },
101
- "execution_count": 3,
102
- "metadata": {},
103
- "output_type": "execute_result"
104
- }
105
- ],
106
- "source": [
107
- "server = Server()\n",
108
- "agent = DumbAgent()\n",
109
- "goal_state = server.goal_start(\"βˆ€ (p q: Prop), Or p q -> Or q p\")\n",
110
- "agent.search(server=server, goal_state=goal_state, verbose=False)"
111
- ]
112
- },
113
- {
114
- "cell_type": "markdown",
115
- "id": "141e0116-cbb6-4957-aaea-2a1100f80ece",
116
- "metadata": {},
117
- "source": [
118
- "## Automatic and Manual Modes\n",
119
- "\n",
120
- "The agent chooses one goal and executes a tactic on this goal. What happens to the other goals that are not chosen? By default, the server runs in automatic mode. In automatic mode, all other goals are automatically inherited by a child state, so a user agent could declare a proof finished when there are no more goals remaining in the current goal state.\n",
121
- "\n",
122
- "Some users may wish to handle sibling goals manually. For example, Aesop's treatment of metavariable coupling is not automatic. To do this, pass the flag `options={ \"automaticMode\" : False }` to the `Server` constructor."
123
- ]
124
- },
125
- {
126
- "cell_type": "code",
127
- "execution_count": null,
128
- "id": "2090e538-d196-4923-937c-b83fedf1d9a2",
129
- "metadata": {},
130
- "outputs": [],
131
- "source": []
132
- }
133
- ],
134
- "metadata": {
135
- "kernelspec": {
136
- "display_name": "Python 3 (ipykernel)",
137
- "language": "python",
138
- "name": "python3"
139
- },
140
- "language_info": {
141
- "codemirror_mode": {
142
- "name": "ipython",
143
- "version": 3
144
- },
145
- "file_extension": ".py",
146
- "mimetype": "text/x-python",
147
- "name": "python",
148
- "nbconvert_exporter": "python",
149
- "pygments_lexer": "ipython3",
150
- "version": "3.12.6"
151
- }
152
- },
153
- "nbformat": 4,
154
- "nbformat_minor": 5
155
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
PyPantograph/doc/api-data.rst DELETED
@@ -1,5 +0,0 @@
1
- Data
2
- =============
3
-
4
- .. automodule:: pantograph.data
5
- :members:
 
 
 
 
 
 
PyPantograph/doc/api-expr.rst DELETED
@@ -1,8 +0,0 @@
1
- Expr
2
- =============
3
-
4
- .. automodule:: pantograph.expr
5
- :members:
6
-
7
- .. autodata:: pantograph.expr.Expr
8
- .. autodata:: pantograph.expr.Tactic
 
 
 
 
 
 
 
 
 
PyPantograph/doc/api-message.rst DELETED
@@ -1,5 +0,0 @@
1
- Message
2
- =============
3
-
4
- .. automodule:: pantograph.message
5
- :members:
 
 
 
 
 
 
PyPantograph/doc/api-search.rst DELETED
@@ -1,5 +0,0 @@
1
- Search
2
- =============
3
-
4
- .. automodule:: pantograph.search
5
- :members:
 
 
 
 
 
 
PyPantograph/doc/api-server.rst DELETED
@@ -1,5 +0,0 @@
1
- Server
2
- =============
3
-
4
- .. automodule:: pantograph.server
5
- :members:
 
 
 
 
 
 
PyPantograph/doc/frontend.ipynb DELETED
@@ -1,401 +0,0 @@
1
- {
2
- "cells": [
3
- {
4
- "cell_type": "markdown",
5
- "id": "fe7a3037-5c49-4097-9a5d-575b958cc7f8",
6
- "metadata": {},
7
- "source": [
8
- "# Data Extraction"
9
- ]
10
- },
11
- {
12
- "cell_type": "code",
13
- "execution_count": 1,
14
- "id": "fc68ad1d-e64c-48b7-9461-50d872d30473",
15
- "metadata": {},
16
- "outputs": [],
17
- "source": [
18
- "import os\n",
19
- "from pathlib import Path\n",
20
- "from pantograph.server import Server"
21
- ]
22
- },
23
- {
24
- "cell_type": "markdown",
25
- "id": "fd13c644-d731-4f81-964e-584bbd43e51c",
26
- "metadata": {},
27
- "source": [
28
- "## Tactic Invocation\n",
29
- "\n",
30
- "Pantograph can extract tactic invocation data from a Lean file. A **tactic\n",
31
- "invocation** is a tuple containing the before and after goal states, and the\n",
32
- "tactic which converts the \"before\" state to the \"after\" state.\n",
33
- "\n",
34
- "To extract tactic invocation data, use `server.tactic_invocations(file_name)`\n",
35
- "and supply the file name of the input Lean file."
36
- ]
37
- },
38
- {
39
- "cell_type": "code",
40
- "execution_count": 2,
41
- "id": "6282dc6f-4eac-4263-8277-9d54d19ad1a5",
42
- "metadata": {},
43
- "outputs": [
44
- {
45
- "name": "stdout",
46
- "output_type": "stream",
47
- "text": [
48
- "$PWD: /Users/aniva/Projects/matp/PyPantograph/examples/Example\n"
49
- ]
50
- }
51
- ],
52
- "source": [
53
- "project_path = Path(os.getcwd()).parent.resolve() / 'examples/Example'\n",
54
- "print(f\"$PWD: {project_path}\")\n",
55
- "server = await Server.create(imports=['Example'], project_path=project_path)\n",
56
- "units = await server.tactic_invocations_async(project_path / \"Example.lean\")"
57
- ]
58
- },
59
- {
60
- "cell_type": "markdown",
61
- "id": "c3c1be91-27a5-4481-b09d-a32dbb94b058",
62
- "metadata": {},
63
- "source": [
64
- "The function returns a list of `CompilationUnit` objects, corresponding to each compilation unit in the input Lean file. For performance reasons only the text boundaries are loaded into `CompilationUnit`s."
65
- ]
66
- },
67
- {
68
- "cell_type": "code",
69
- "execution_count": 3,
70
- "id": "e994aa2b-5d5e-4f86-af6c-40e0b3a032d2",
71
- "metadata": {},
72
- "outputs": [
73
- {
74
- "name": "stdout",
75
- "output_type": "stream",
76
- "text": [
77
- "#0: [14,85]\n",
78
- "/-- Ensure that Aesop is running -/\n",
79
- "example : Ξ± β†’ Ξ± :=\n",
80
- " by aesop\n",
81
- "\n",
82
- "\n",
83
- "#1: [85,254]\n",
84
- "example : βˆ€ (p q: Prop), p ∨ q β†’ q ∨ p := by\n",
85
- " intro p q h\n",
86
- " -- Here are some comments\n",
87
- " cases h\n",
88
- " . apply Or.inr\n",
89
- " assumption\n",
90
- " . apply Or.inl\n",
91
- " assumption\n",
92
- "\n"
93
- ]
94
- }
95
- ],
96
- "source": [
97
- "with open(project_path / \"Example.lean\", 'rb') as f:\n",
98
- " content = f.read()\n",
99
- " for i, unit in enumerate(units):\n",
100
- " print(f\"#{i}: [{unit.i_begin},{unit.i_end}]\")\n",
101
- " unit_text = content[unit.i_begin:unit.i_end].decode('utf-8')\n",
102
- " print(unit_text)"
103
- ]
104
- },
105
- {
106
- "cell_type": "markdown",
107
- "id": "52e650fc-4a87-445f-8aa8-707ed9e36c03",
108
- "metadata": {},
109
- "source": [
110
- "Each `CompilationUnit` includes a list of `TacticInvocation`s, which contains the `.before` (corresponding to the state before the tactic), `.after` (corresponding to the state after the tactic), and `.tactic` (tactic executed) fields. "
111
- ]
112
- },
113
- {
114
- "cell_type": "code",
115
- "execution_count": 4,
116
- "id": "8e0f0def-dd3c-4550-8a7c-b4aec6c7fd7f",
117
- "metadata": {},
118
- "outputs": [
119
- {
120
- "name": "stdout",
121
- "output_type": "stream",
122
- "text": [
123
- "[Before]\n",
124
- "Ξ± : Sort ?u.7\n",
125
- "⊒ Ξ± β†’ Ξ±\n",
126
- "[Tactic]\n",
127
- "aesop (using [])\n",
128
- "[After]\n",
129
- "\n"
130
- ]
131
- }
132
- ],
133
- "source": [
134
- "for i in units[0].invocations:\n",
135
- " print(f\"[Before]\\n{i.before}\")\n",
136
- " print(f\"[Tactic]\\n{i.tactic} (using {i.used_constants})\")\n",
137
- " print(f\"[After]\\n{i.after}\")"
138
- ]
139
- },
140
- {
141
- "cell_type": "code",
142
- "execution_count": 5,
143
- "id": "51f5398b-5416-4dc1-81cd-6d2514758232",
144
- "metadata": {
145
- "scrolled": true
146
- },
147
- "outputs": [
148
- {
149
- "name": "stdout",
150
- "output_type": "stream",
151
- "text": [
152
- "[Before]\n",
153
- "⊒ βˆ€ (p q : Prop), p ∨ q β†’ q ∨ p\n",
154
- "[Tactic]\n",
155
- "intro p q h (using [])\n",
156
- "[After]\n",
157
- "p q : Prop\n",
158
- "h : p ∨ q\n",
159
- "⊒ q ∨ p\n",
160
- "[Before]\n",
161
- "p q : Prop\n",
162
- "h : p ∨ q\n",
163
- "⊒ q ∨ p\n",
164
- "[Tactic]\n",
165
- "cases h (using ['Eq.refl', 'Or'])\n",
166
- "[After]\n",
167
- "case inl\n",
168
- "p q : Prop\n",
169
- "h✝ : p\n",
170
- "⊒ q ∨ p\n",
171
- "case inr\n",
172
- "p q : Prop\n",
173
- "h✝ : q\n",
174
- "⊒ q ∨ p\n",
175
- "[Before]\n",
176
- "case inl\n",
177
- "p q : Prop\n",
178
- "h✝ : p\n",
179
- "⊒ q ∨ p\n",
180
- "[Tactic]\n",
181
- "apply Or.inr (using ['Or.inr'])\n",
182
- "[After]\n",
183
- "case inl.h\n",
184
- "p q : Prop\n",
185
- "h✝ : p\n",
186
- "⊒ p\n",
187
- "[Before]\n",
188
- "case inl.h\n",
189
- "p q : Prop\n",
190
- "h✝ : p\n",
191
- "⊒ p\n",
192
- "[Tactic]\n",
193
- "assumption (using [])\n",
194
- "[After]\n",
195
- "\n",
196
- "[Before]\n",
197
- "case inr\n",
198
- "p q : Prop\n",
199
- "h✝ : q\n",
200
- "⊒ q ∨ p\n",
201
- "[Tactic]\n",
202
- "apply Or.inl (using ['Or.inl'])\n",
203
- "[After]\n",
204
- "case inr.h\n",
205
- "p q : Prop\n",
206
- "h✝ : q\n",
207
- "⊒ q\n",
208
- "[Before]\n",
209
- "case inr.h\n",
210
- "p q : Prop\n",
211
- "h✝ : q\n",
212
- "⊒ q\n",
213
- "[Tactic]\n",
214
- "assumption (using [])\n",
215
- "[After]\n",
216
- "\n"
217
- ]
218
- }
219
- ],
220
- "source": [
221
- "for i in units[1].invocations:\n",
222
- " print(f\"[Before]\\n{i.before}\")\n",
223
- " print(f\"[Tactic]\\n{i.tactic} (using {i.used_constants})\")\n",
224
- " print(f\"[After]\\n{i.after}\")"
225
- ]
226
- },
227
- {
228
- "cell_type": "markdown",
229
- "id": "3a8b5b28-a012-4d83-a6a9-ed017a8e1fc2",
230
- "metadata": {},
231
- "source": [
232
- "## Check Compilation\n",
233
- "\n",
234
- "Use `check_compile` to check if some Lean code compiles.\n",
235
- "\n",
236
- "Keep in mind that Lean compilation can execute arbitrary code."
237
- ]
238
- },
239
- {
240
- "cell_type": "code",
241
- "execution_count": 6,
242
- "id": "87e99da0-6e16-48c3-a462-1bfacb0d0cc7",
243
- "metadata": {},
244
- "outputs": [
245
- {
246
- "data": {
247
- "text/plain": [
248
- "[CompilationUnit(i_begin=0, i_end=31, messages=[], invocations=None, goal_state=None, goal_src_boundaries=None, new_constants=None)]"
249
- ]
250
- },
251
- "execution_count": 6,
252
- "metadata": {},
253
- "output_type": "execute_result"
254
- }
255
- ],
256
- "source": [
257
- "server = await Server.create()\n",
258
- "code = \"\"\"\n",
259
- "example : 1 + 1 = 2 := by rfl\n",
260
- "\"\"\"\n",
261
- "await server.check_compile_async(code)"
262
- ]
263
- },
264
- {
265
- "cell_type": "markdown",
266
- "id": "adf0dee9-33de-4640-b35a-3e093e590963",
267
- "metadata": {},
268
- "source": [
269
- "If there are no error messages, it means the unit compiles."
270
- ]
271
- },
272
- {
273
- "cell_type": "markdown",
274
- "id": "14e42793-e177-447b-89d3-edb16931f6e9",
275
- "metadata": {},
276
- "source": [
277
- "## Loading Definitions\n",
278
- "\n",
279
- "Pantograph keeps track of a global environment. `Server.load_definitions` adds new definitions to the environment."
280
- ]
281
- },
282
- {
283
- "cell_type": "code",
284
- "execution_count": 7,
285
- "id": "9d322474-6a6e-4c32-945b-9e7942b78826",
286
- "metadata": {},
287
- "outputs": [
288
- {
289
- "data": {
290
- "text/plain": [
291
- "{'type': {'pp': 'Nat β†’ Nat'},\n",
292
- " 'sourceStart': {'line': 2, 'column': 0},\n",
293
- " 'sourceEnd': {'line': 2, 'column': 42},\n",
294
- " 'isUnsafe': False}"
295
- ]
296
- },
297
- "execution_count": 7,
298
- "metadata": {},
299
- "output_type": "execute_result"
300
- }
301
- ],
302
- "source": [
303
- "code = \"\"\"\n",
304
- "def mystery : Nat -> Nat := fun x => x + 1\n",
305
- "\"\"\"\n",
306
- "await server.load_definitions_async(code)\n",
307
- "await server.env_inspect_async(\"mystery\")"
308
- ]
309
- },
310
- {
311
- "cell_type": "markdown",
312
- "id": "93536793-c7df-4caa-bd03-74fd3b97d987",
313
- "metadata": {},
314
- "source": [
315
- "## Track Checking\n",
316
- "\n",
317
- "We can check if one file conforms to the definition and theorems of another. If the result object has no `failure`s or error messages, the check has passed."
318
- ]
319
- },
320
- {
321
- "cell_type": "code",
322
- "execution_count": 8,
323
- "id": "67aa959c-3c7c-4844-9ffb-9b730ae07f8a",
324
- "metadata": {},
325
- "outputs": [
326
- {
327
- "data": {
328
- "text/plain": [
329
- "CheckTrackResult(src_messages=[], dst_messages=[], failure=None)"
330
- ]
331
- },
332
- "execution_count": 8,
333
- "metadata": {},
334
- "output_type": "execute_result"
335
- }
336
- ],
337
- "source": [
338
- "src = \"\"\"\n",
339
- "def f : Nat -> Nat := sorry\n",
340
- "theorem property (n : Nat) : f n = n + 1 := sorry\n",
341
- "\"\"\"\n",
342
- "dst = \"\"\"\n",
343
- "def f (x : Nat) := x + 1\n",
344
- "theorem property (n : Nat) : f n = n + 1 := rfl\n",
345
- "\"\"\"\n",
346
- "await server.check_track_async(src, dst)"
347
- ]
348
- },
349
- {
350
- "cell_type": "code",
351
- "execution_count": 9,
352
- "id": "5d56a22d-feb8-4d3b-bd5b-4f675991a550",
353
- "metadata": {},
354
- "outputs": [
355
- {
356
- "data": {
357
- "text/plain": [
358
- "CheckTrackResult(src_messages=[], dst_messages=[], failure='Type clash of property')"
359
- ]
360
- },
361
- "execution_count": 9,
362
- "metadata": {},
363
- "output_type": "execute_result"
364
- }
365
- ],
366
- "source": [
367
- "src = \"\"\"\n",
368
- "def f : Nat -> Nat := sorry\n",
369
- "theorem property (n : Nat) : f n = n + 1 := sorry\n",
370
- "\"\"\"\n",
371
- "# Tampering!\n",
372
- "dst = \"\"\"\n",
373
- "def f (x : Nat) := x + 1\n",
374
- "theorem property (n : Nat) : 0 = 0 := rfl\n",
375
- "\"\"\"\n",
376
- "await server.check_track_async(src, dst)"
377
- ]
378
- }
379
- ],
380
- "metadata": {
381
- "kernelspec": {
382
- "display_name": "Python 3 (ipykernel)",
383
- "language": "python",
384
- "name": "python3"
385
- },
386
- "language_info": {
387
- "codemirror_mode": {
388
- "name": "ipython",
389
- "version": 3
390
- },
391
- "file_extension": ".py",
392
- "mimetype": "text/x-python",
393
- "name": "python",
394
- "nbconvert_exporter": "python",
395
- "pygments_lexer": "ipython3",
396
- "version": "3.12.7"
397
- }
398
- },
399
- "nbformat": 4,
400
- "nbformat_minor": 5
401
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
PyPantograph/doc/goal.ipynb DELETED
@@ -1,662 +0,0 @@
1
- {
2
- "cells": [
3
- {
4
- "cell_type": "markdown",
5
- "id": "c5106980-4850-4bea-a333-5a1b2e4d1dc5",
6
- "metadata": {},
7
- "source": [
8
- "# Goals and Tactics\n",
9
- "\n",
10
- "Executing tactics in Pantograph is simple. To start a proof, call the\n",
11
- "`Server.goal_start` function and supply an expression."
12
- ]
13
- },
14
- {
15
- "cell_type": "code",
16
- "execution_count": 1,
17
- "id": "3257de2b-41ca-4cfe-b66c-1ef4781c98b0",
18
- "metadata": {},
19
- "outputs": [],
20
- "source": [
21
- "from pantograph import Server\n",
22
- "from pantograph.expr import Site, TacticHave, TacticExpr, TacticMode"
23
- ]
24
- },
25
- {
26
- "cell_type": "code",
27
- "execution_count": 2,
28
- "id": "6783d478-d8c7-4c4e-a56e-8170384297ef",
29
- "metadata": {},
30
- "outputs": [],
31
- "source": [
32
- "server = await Server.create()\n",
33
- "state0 = await server.goal_start_async(\"forall (p q: Prop), Or p q -> Or q p\")"
34
- ]
35
- },
36
- {
37
- "cell_type": "markdown",
38
- "id": "bfe5a9df-33c2-4538-a9ce-fc0e02c92ff2",
39
- "metadata": {},
40
- "source": [
41
- "This creates a *goal state*, which consists of some goals. In this\n",
42
- "case since it is the beginning of a state, it has only one goal."
43
- ]
44
- },
45
- {
46
- "cell_type": "code",
47
- "execution_count": 3,
48
- "id": "eefc9094-9574-4f92-9aa2-c39beb85389b",
49
- "metadata": {},
50
- "outputs": [
51
- {
52
- "name": "stdout",
53
- "output_type": "stream",
54
- "text": [
55
- "\n",
56
- "⊒ forall (p q: Prop), Or p q -> Or q p\n"
57
- ]
58
- }
59
- ],
60
- "source": [
61
- "print(state0)"
62
- ]
63
- },
64
- {
65
- "cell_type": "markdown",
66
- "id": "26dbe212-e09e-42dd-ab15-65ee2fba6234",
67
- "metadata": {},
68
- "source": [
69
- "To execute a tactic on a goal state, use `Server.goal_tactic`. This function\n",
70
- "takes a state, a tactic, and an optional site (see below). Most Lean tactics are strings."
71
- ]
72
- },
73
- {
74
- "cell_type": "code",
75
- "execution_count": 4,
76
- "id": "c907dbb6-4d6a-4aa7-b173-60220165ba9e",
77
- "metadata": {},
78
- "outputs": [
79
- {
80
- "name": "stdout",
81
- "output_type": "stream",
82
- "text": [
83
- "a : Prop\n",
84
- "⊒ βˆ€ (q : Prop), a ∨ q β†’ q ∨ a\n"
85
- ]
86
- }
87
- ],
88
- "source": [
89
- "state1 = await server.goal_tactic_async(state0, \"intro a\")\n",
90
- "print(state1)"
91
- ]
92
- },
93
- {
94
- "cell_type": "markdown",
95
- "id": "9978fdcf-a12b-4f22-9551-5e04c262e5e0",
96
- "metadata": {},
97
- "source": [
98
- "Executing a tactic produces a new goal state. If this goal state has no goals,\n",
99
- "the proof is complete. You can recover the usual form of a goal with `str()`"
100
- ]
101
- },
102
- {
103
- "cell_type": "code",
104
- "execution_count": 5,
105
- "id": "16595c5e-2285-49d5-8340-397ad1e6c9e7",
106
- "metadata": {},
107
- "outputs": [
108
- {
109
- "name": "stdout",
110
- "output_type": "stream",
111
- "text": [
112
- "a : Prop\n",
113
- "⊒ βˆ€ (q : Prop), a ∨ q β†’ q ∨ a\n"
114
- ]
115
- }
116
- ],
117
- "source": [
118
- "print(state1.goals[0])"
119
- ]
120
- },
121
- {
122
- "cell_type": "markdown",
123
- "id": "837dae74-b2d7-4e81-a318-86641dd016f9",
124
- "metadata": {},
125
- "source": [
126
- "Starting in v0.3.5, you can run multiple tactics in one shot. Use `?_` to mark goals to be solved later."
127
- ]
128
- },
129
- {
130
- "cell_type": "code",
131
- "execution_count": 6,
132
- "id": "6c70dbe0-e7ad-4bc2-bafe-0e59d78ccd3a",
133
- "metadata": {},
134
- "outputs": [
135
- {
136
- "name": "stdout",
137
- "output_type": "stream",
138
- "text": [
139
- "inl\n",
140
- "p : Prop\n",
141
- "q : Prop\n",
142
- "h✝ : p\n",
143
- "⊒ q ∨ p\n",
144
- "inr\n",
145
- "p : Prop\n",
146
- "q : Prop\n",
147
- "h✝ : q\n",
148
- "⊒ q ∨ p\n"
149
- ]
150
- }
151
- ],
152
- "source": [
153
- "state2 = await server.goal_tactic_async(state0, \"intro p q\\nintro h\\ncases h\")\n",
154
- "print(state2)"
155
- ]
156
- },
157
- {
158
- "cell_type": "code",
159
- "execution_count": 7,
160
- "id": "529edf1a-27bc-45a2-bbac-01e8756310a2",
161
- "metadata": {},
162
- "outputs": [
163
- {
164
- "name": "stdout",
165
- "output_type": "stream",
166
- "text": [
167
- "refine_2.inl\n",
168
- "p : Prop\n",
169
- "q : Prop\n",
170
- "random : 1 + 1 = 2\n",
171
- "h✝ : p\n",
172
- "⊒ q ∨ p\n",
173
- "refine_2.inr\n",
174
- "p : Prop\n",
175
- "q : Prop\n",
176
- "random : 1 + 1 = 2\n",
177
- "h✝ : q\n",
178
- "⊒ q ∨ p\n",
179
- "refine_1\n",
180
- "p : Prop\n",
181
- "q : Prop\n",
182
- "h : p ∨ q\n",
183
- "⊒ 1 + 1 = 2\n"
184
- ]
185
- }
186
- ],
187
- "source": [
188
- "state2 = await server.goal_tactic_async(state0, \"intro p q h\\nhave random : 1 + 1 = 2 := ?_\\ncases h\")\n",
189
- "print(state2)"
190
- ]
191
- },
192
- {
193
- "cell_type": "markdown",
194
- "id": "67f2a75d-6851-4393-bac9-a091400f1906",
195
- "metadata": {},
196
- "source": [
197
- "## Error Handling and GC\n",
198
- "\n",
199
- "When a tactic fails, it throws an exception (`TacticFailure`) which contains a list of either `str`s or `Message` objects in `e.args[0]`."
200
- ]
201
- },
202
- {
203
- "cell_type": "code",
204
- "execution_count": 8,
205
- "id": "c9784ba2-3810-4f80-a6c4-33d5eef3003e",
206
- "metadata": {
207
- "scrolled": true
208
- },
209
- "outputs": [
210
- {
211
- "name": "stdout",
212
- "output_type": "stream",
213
- "text": [
214
- "[Message(data=\"tactic 'assumption' failed\\na : Prop\\n⊒ βˆ€ (q : Prop), a ∨ q β†’ q ∨ a\", pos=Position(line=0, column=0), pos_end=None, severity=<Severity.ERROR: 3>, kind=None)]\n",
215
- "0:0: error: tactic 'assumption' failed\n",
216
- "a : Prop\n",
217
- "⊒ βˆ€ (q : Prop), a ∨ q β†’ q ∨ a\n"
218
- ]
219
- }
220
- ],
221
- "source": [
222
- "from pantograph.message import TacticFailure\n",
223
- "try:\n",
224
- " state2 = await server.goal_tactic_async(state1, \"assumption\")\n",
225
- " print(\"Should not reach this\")\n",
226
- "except TacticFailure as e:\n",
227
- " print(e)\n",
228
- " for msg in e.args[0]:\n",
229
- " print(msg)"
230
- ]
231
- },
232
- {
233
- "cell_type": "markdown",
234
- "id": "1ae60d9e-8656-4f26-b495-d04bced250fc",
235
- "metadata": {},
236
- "source": [
237
- "A state with no goals is considered solved"
238
- ]
239
- },
240
- {
241
- "cell_type": "code",
242
- "execution_count": 9,
243
- "id": "1cb96b19-d3bb-4533-abeb-a7dbc5bc8c3e",
244
- "metadata": {},
245
- "outputs": [
246
- {
247
- "data": {
248
- "text/plain": [
249
- "GoalState(#7, goals=[], _sentinel=#4"
250
- ]
251
- },
252
- "execution_count": 9,
253
- "metadata": {},
254
- "output_type": "execute_result"
255
- }
256
- ],
257
- "source": [
258
- "state0 = await server.goal_start_async(\"forall (p : Prop), p -> p\")\n",
259
- "state1 = await server.goal_tactic_async(state0, \"intro\")\n",
260
- "state2 = await server.goal_tactic_async(state1, \"intro h\")\n",
261
- "state3 = await server.goal_tactic_async(state2, \"exact h\")\n",
262
- "state3"
263
- ]
264
- },
265
- {
266
- "cell_type": "markdown",
267
- "id": "a2945e71-e583-4ae0-9c0f-83035f0492f2",
268
- "metadata": {},
269
- "source": [
270
- "Execute `server.gc()` once in a while to delete unused goals."
271
- ]
272
- },
273
- {
274
- "cell_type": "code",
275
- "execution_count": 10,
276
- "id": "d53624ff-c720-4847-98f7-28e109eb76e7",
277
- "metadata": {},
278
- "outputs": [],
279
- "source": [
280
- "await server.gc_async()"
281
- ]
282
- },
283
- {
284
- "cell_type": "markdown",
285
- "id": "0b59e05e-7d8c-4fad-b8ca-375ea995ea5b",
286
- "metadata": {},
287
- "source": [
288
- "## Special Tactics\n",
289
- "\n",
290
- "Lean has special provisions for some tactics. This includes `have`, `let`,\n",
291
- "`calc`. To execute one of these tactics, create a `TacticHave`, `TacticLet`,\n",
292
- "instance and feed it into `server.goal_tactic`.\n",
293
- "\n",
294
- "Technically speaking `have` and `let` are not tactics in Lean, so their execution requires special attention. In v0.3.5, they can be run under the normal tactic function as well (see above)."
295
- ]
296
- },
297
- {
298
- "cell_type": "code",
299
- "execution_count": 11,
300
- "id": "526d620b-064f-4ec0-a7b2-6a1ef3c6f6e7",
301
- "metadata": {},
302
- "outputs": [
303
- {
304
- "name": "stdout",
305
- "output_type": "stream",
306
- "text": [
307
- "\n",
308
- "⊒ 2 = 1 + 1\n",
309
- "h : 2 = 1 + 1\n",
310
- "⊒ 1 + 1 = 2\n"
311
- ]
312
- }
313
- ],
314
- "source": [
315
- "state0 = await server.goal_start_async(\"1 + 1 = 2\")\n",
316
- "state1 = await server.goal_tactic_async(state0, TacticHave(branch=\"2 = 1 + 1\", binder_name=\"h\"))\n",
317
- "print(state1)"
318
- ]
319
- },
320
- {
321
- "cell_type": "markdown",
322
- "id": "c415d436-ed0d-475f-bf5e-b8dc63954c7e",
323
- "metadata": {},
324
- "source": [
325
- "The `TacticExpr` \"tactic\" parses an expression and assigns it to the current\n",
326
- "goal. This leverages Lean's type unification system and is as expressive as\n",
327
- "Lean expressions. Many proofs in Mathlib4 are written in a mixture of expression\n",
328
- "and tactic forms."
329
- ]
330
- },
331
- {
332
- "cell_type": "code",
333
- "execution_count": 12,
334
- "id": "e1f06441-4d77-45a7-a1c3-b800b96a8105",
335
- "metadata": {},
336
- "outputs": [
337
- {
338
- "name": "stdout",
339
- "output_type": "stream",
340
- "text": [
341
- "\n"
342
- ]
343
- }
344
- ],
345
- "source": [
346
- "state0 = await server.goal_start_async(\"forall (p : Prop), p -> p\")\n",
347
- "state1 = await server.goal_tactic_async(state0, \"intro p\")\n",
348
- "state2 = await server.goal_tactic_async(state1, TacticExpr(\"fun h => h\"))\n",
349
- "print(state2)"
350
- ]
351
- },
352
- {
353
- "cell_type": "markdown",
354
- "id": "2ed8ea8f-708d-4d30-a7c4-fae20f07fd91",
355
- "metadata": {},
356
- "source": [
357
- "### Drafting\n",
358
- "\n",
359
- "Pantograph supports drafting (technically the sketch step) from\n",
360
- "[Draft-Sketch-Prove](https://github.com/wellecks/ntptutorial/tree/main/partII_dsp).\n",
361
- "Pantograph's drafting feature is more powerful. At any place in the proof, you\n",
362
- "can replace an expression with `sorry`, and the `sorry` will become a goal. Any type errors will also become goals. In order to detect whether type errors have occurred, the user can look at the messages from each compilation unit.\n",
363
- "\n",
364
- "At this point we must introduce the idea of compilation units. Each Lean\n",
365
- "definition, theorem, constant, etc., is a *compilation unit*. When Pantograph\n",
366
- "extracts data from Lean source code, it sections the data into these compilation\n",
367
- "units.\n",
368
- "\n",
369
- "For example, consider this sketch produced by a language model prover:\n",
370
- "```lean\n",
371
- "by\n",
372
- " intros n m\n",
373
- " induction n with\n",
374
- " | zero =>\n",
375
- " have h_base: 0 + m = m := sorry\n",
376
- " have h_symm: m + 0 = m := sorry\n",
377
- " sorry\n",
378
- " | succ n ih =>\n",
379
- " have h_inductive: n + m = m + n := sorry\n",
380
- " have h_pull_succ_out_from_right: m + Nat.succ n = Nat.succ (m + n) := sorry\n",
381
- " have h_flip_n_plus_m: Nat.succ (n + m) = Nat.succ (m + n) := sorry\n",
382
- " have h_pull_succ_out_from_left: Nat.succ n + m = Nat.succ (n + m) := sorry\n",
383
- " sorry\n",
384
- "```\n",
385
- "There are some `sorry`s that we want to solve automatically with hammer tactics. We can do this by drafting.\n",
386
- "\n",
387
- "Pantograph can also load `sorry`s from a code snippet, which provides an alternative way for proof initiation. Warning: `load_sorry` does not work with `example` declarations."
388
- ]
389
- },
390
- {
391
- "cell_type": "code",
392
- "execution_count": 13,
393
- "id": "e435456b-892a-42e7-a8a9-4968c8202c83",
394
- "metadata": {},
395
- "outputs": [
396
- {
397
- "name": "stdout",
398
- "output_type": "stream",
399
- "text": [
400
- "\n",
401
- "⊒ βˆ€ (n m : Nat), n + m = m + n\n"
402
- ]
403
- }
404
- ],
405
- "source": [
406
- "sketch = \"\"\"\n",
407
- "theorem add_comm_proved_formal_sketch : βˆ€ n m : Nat, n + m = m + n := sorry\n",
408
- "\"\"\"\n",
409
- "unit, = await server.load_sorry_async(sketch)\n",
410
- "print(unit.goal_state)"
411
- ]
412
- },
413
- {
414
- "cell_type": "code",
415
- "execution_count": 14,
416
- "id": "efce284c-35d0-453f-9857-aebe7b09bf3e",
417
- "metadata": {},
418
- "outputs": [
419
- {
420
- "name": "stdout",
421
- "output_type": "stream",
422
- "text": [
423
- "n : Nat\n",
424
- "m : Nat\n",
425
- "⊒ 0 + m = m\n",
426
- "n : Nat\n",
427
- "m : Nat\n",
428
- "h_base : 0 + m = m\n",
429
- "⊒ m + 0 = m\n",
430
- "n : Nat\n",
431
- "m : Nat\n",
432
- "h_base : 0 + m = m\n",
433
- "h_symm : m + 0 = m\n",
434
- "⊒ 0 + m = m + 0\n",
435
- "n✝ : Nat\n",
436
- "m : Nat\n",
437
- "n : Nat\n",
438
- "ih : n + m = m + n\n",
439
- "⊒ n + 1 + m = m + (n + 1)\n"
440
- ]
441
- }
442
- ],
443
- "source": [
444
- "step = \"\"\"\n",
445
- "by\n",
446
- " -- Consider some n and m in Nats.\n",
447
- " intros n m\n",
448
- " -- Perform induction on n.\n",
449
- " induction n with\n",
450
- " | zero =>\n",
451
- " -- Base case: When n = 0, we need to show 0 + m = m + 0.\n",
452
- " -- We have the fact 0 + m = m by the definition of addition.\n",
453
- " have h_base: 0 + m = m := sorry\n",
454
- " -- We also have the fact m + 0 = m by the definition of addition.\n",
455
- " have h_symm: m + 0 = m := sorry\n",
456
- " -- Combine facts to close goal\n",
457
- " sorry\n",
458
- " | succ n ih =>\n",
459
- " sorry\n",
460
- "\"\"\"\n",
461
- "from pantograph.expr import TacticDraft\n",
462
- "tactic = TacticDraft(step)\n",
463
- "state1 = await server.goal_tactic_async(unit.goal_state, tactic)\n",
464
- "print(state1)"
465
- ]
466
- },
467
- {
468
- "cell_type": "markdown",
469
- "id": "2cb11505-4076-481f-8736-e0d6d4e67ffc",
470
- "metadata": {},
471
- "source": [
472
- "### Search Target Distillation\n",
473
- "\n",
474
- "Sometimes, we want to search for an object (witness) along with proofs (companions) of properties about the object. This problem is known as **companion generation**. In Pantograph, `load_sorry` will automatically pair companions to create coupled search targets. Note that this is only available for flat dependency structures, where one object has a list of properties."
475
- ]
476
- },
477
- {
478
- "cell_type": "code",
479
- "execution_count": 15,
480
- "id": "d66afe22-541e-4464-86ce-b462272d76d2",
481
- "metadata": {},
482
- "outputs": [
483
- {
484
- "name": "stdout",
485
- "output_type": "stream",
486
- "text": [
487
- "\n",
488
- "⊒ { f // βˆ€ (n : Nat), f n = n + 1 }\n"
489
- ]
490
- }
491
- ],
492
- "source": [
493
- "sketch = \"\"\"\n",
494
- "def f : Nat -> Nat := sorry\n",
495
- "theorem property (n : Nat) : f n = n + 1 := sorry\n",
496
- "\"\"\"\n",
497
- "target, = await server.load_sorry_async(sketch, ignore_values=True)\n",
498
- "print(target.goal_state)"
499
- ]
500
- },
501
- {
502
- "cell_type": "markdown",
503
- "id": "efabd1ad-88aa-4bc1-90ac-2d7af58d9a07",
504
- "metadata": {},
505
- "source": [
506
- "## Sites\n",
507
- "\n",
508
- "The optional `site` argument to `goal_tactic` controls the area of effect of a tactic. Site controls what the tactic sees when it asks Lean for the current goal. Most tactics only act on a single goal, but tactics acting on multiple goals are plausible as well.\n",
509
- "\n",
510
- "The `auto_resume` field defaults to the server option's `automaticMode` (which defaults to `True`). When this field is true, Pantograph will not deliberately hide other goals away from the tactic. This is the usual modus operandi of tactic proofs in Lean. When `auto_resume` is set to `False`, Pantograph will set other goals to dormant. This can be useful in limiting the area of effect of a tactic. However, dormanting a goal comes with the extra burden that it has to be activated (\"resume\") later, via `goal_resume`."
511
- ]
512
- },
513
- {
514
- "cell_type": "code",
515
- "execution_count": 16,
516
- "id": "6806db59-bf2e-4c72-9903-200621ec1661",
517
- "metadata": {},
518
- "outputs": [
519
- {
520
- "name": "stdout",
521
- "output_type": "stream",
522
- "text": [
523
- "left\n",
524
- "p : Prop\n",
525
- "h : p\n",
526
- "⊒ p\n",
527
- "right\n",
528
- "p : Prop\n",
529
- "h : p\n",
530
- "⊒ p ∨ p\n"
531
- ]
532
- }
533
- ],
534
- "source": [
535
- "state = await server.goal_start_async(\"forall (p : Prop), p -> And p (Or p p)\")\n",
536
- "state = await server.goal_tactic_async(state, \"intro p h\")\n",
537
- "state = await server.goal_tactic_async(state, \"apply And.intro\")\n",
538
- "print(state)"
539
- ]
540
- },
541
- {
542
- "cell_type": "markdown",
543
- "id": "4e2f9539-bac0-4721-9f86-752090f3ed24",
544
- "metadata": {},
545
- "source": [
546
- "In the example below, we set `auto_resume` to `False`, and the sibling goal is dormanted."
547
- ]
548
- },
549
- {
550
- "cell_type": "code",
551
- "execution_count": 17,
552
- "id": "aebb6f3d-e2b2-4104-8d67-5adce5d2ea78",
553
- "metadata": {
554
- "scrolled": true
555
- },
556
- "outputs": [
557
- {
558
- "name": "stdout",
559
- "output_type": "stream",
560
- "text": [
561
- "\n"
562
- ]
563
- }
564
- ],
565
- "source": [
566
- "state1 = await server.goal_tactic_async(state, \"exact h\", site=Site(goal_id=0, auto_resume=False))\n",
567
- "print(state1)"
568
- ]
569
- },
570
- {
571
- "cell_type": "markdown",
572
- "id": "a1db5a24-1f98-47f6-ab26-d00e94d3b57f",
573
- "metadata": {},
574
- "source": [
575
- "In the example below, we preferentially operate on the second goal. Note that the first goal is still here."
576
- ]
577
- },
578
- {
579
- "cell_type": "code",
580
- "execution_count": 18,
581
- "id": "cb0e30db-f7fd-49df-82a9-09e2eeef2885",
582
- "metadata": {},
583
- "outputs": [
584
- {
585
- "name": "stdout",
586
- "output_type": "stream",
587
- "text": [
588
- "right.h\n",
589
- "p : Prop\n",
590
- "h : p\n",
591
- "⊒ p\n",
592
- "left\n",
593
- "p : Prop\n",
594
- "h : p\n",
595
- "⊒ p\n"
596
- ]
597
- }
598
- ],
599
- "source": [
600
- "state2 = await server.goal_tactic_async(state, \"apply Or.inl\", site=Site(goal_id=1))\n",
601
- "print(state2)"
602
- ]
603
- },
604
- {
605
- "cell_type": "markdown",
606
- "id": "acd74351-e9d5-497c-af6f-fc5709edb54d",
607
- "metadata": {},
608
- "source": [
609
- "## Tactic Modes\n",
610
- "\n",
611
- "Pantograph has special provisions for handling `conv` and `calc` tactics. The commonality of these tactics is incremental feedback: The tactic can run half way and produce some goal. Pantograph supports this via tactic modes. Every goal carries around with it a `TacticMode`, and the user is free to switch between modes. By default, the mode is `TacticMode.TACTIC`."
612
- ]
613
- },
614
- {
615
- "cell_type": "code",
616
- "execution_count": 19,
617
- "id": "30b85bc4-a4ff-4156-b0bb-5c3496bd9472",
618
- "metadata": {},
619
- "outputs": [
620
- {
621
- "data": {
622
- "text/plain": [
623
- "GoalState(#24, goals=[Goal(id='_uniq.381', variables=[Variable(t='Nat', v=None, name='a'), Variable(t='Nat', v=None, name='b'), Variable(t='b = 2', v=None, name='h')], target='1 + a + 1 = a + 1 + 1', sibling_dep=None, name='calc', mode=<TacticMode.TACTIC: 1>), Goal(id='_uniq.400', variables=[Variable(t='Nat', v=None, name='a'), Variable(t='Nat', v=None, name='b'), Variable(t='b = 2', v=None, name='h')], target='a + 1 + 1 = a + b', sibling_dep=None, name=None, mode=<TacticMode.CALC: 3>)], _sentinel=#14"
624
- ]
625
- },
626
- "execution_count": 19,
627
- "metadata": {},
628
- "output_type": "execute_result"
629
- }
630
- ],
631
- "source": [
632
- "state = await server.goal_start_async(\"βˆ€ (a b: Nat), (b = 2) -> 1 + a + 1 = a + b\")\n",
633
- "\n",
634
- "state = await server.goal_tactic_async(state, \"intro a b h\")\n",
635
- "state = await server.goal_tactic_async(state, TacticMode.CALC)\n",
636
- "state = await server.goal_tactic_async(state, \"1 + a + 1 = a + 1 + 1\")\n",
637
- "state"
638
- ]
639
- }
640
- ],
641
- "metadata": {
642
- "kernelspec": {
643
- "display_name": "Python 3 (ipykernel)",
644
- "language": "python",
645
- "name": "python3"
646
- },
647
- "language_info": {
648
- "codemirror_mode": {
649
- "name": "ipython",
650
- "version": 3
651
- },
652
- "file_extension": ".py",
653
- "mimetype": "text/x-python",
654
- "name": "python",
655
- "nbconvert_exporter": "python",
656
- "pygments_lexer": "ipython3",
657
- "version": "3.12.7"
658
- }
659
- },
660
- "nbformat": 4,
661
- "nbformat_minor": 5
662
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
PyPantograph/doc/intro.md DELETED
@@ -1,79 +0,0 @@
1
- # Introduction
2
-
3
- This is Pantograph, an machine-to-machine interaction interface for Lean 4.
4
- Its main purpose is to train and evaluate theorem proving agents. The main
5
- features of Pantograph are:
6
-
7
- 1. Writing mixed expression and tactic style proofs
8
- 2. Exposing the minimum amount of information for a search agent
9
- 3. Handling of metavariable coupling
10
- 4. Reading/Adding symbols from the environment
11
- 5. Extraction of tactic training data
12
- 6. Drafting incomplete proofs
13
-
14
- ## Name
15
-
16
- The name Pantograph is a pun. It means two things
17
- - A pantograph is an instrument for copying down writing. As an agent explores
18
- the vast proof search space, Pantograph records the current state to ensure
19
- the proof is sound.
20
- - A pantograph is also an equipment for an electric train. It supplies power to
21
- a locomotive. In comparison the (relatively) simple Pantograph software powers
22
- theorem proving projects.
23
-
24
- ## Design Rationale
25
-
26
- The Lean 4 interface is not conducive to search. Readers familiar with Coq may
27
- know that the Coq Serapi was superseded by CoqLSP. In the opinion of the
28
- authors, this is a mistake. An interface conducive for human operators to write
29
- proofs is often not an interface conductive to machine learning agents for
30
- searching.
31
-
32
- All of Pantograph's business logic is written in Lean, allowing coupling between
33
- the data extraction and proof search components.
34
-
35
- ## Caveats and Limitations
36
-
37
- Pantograph does not exactly mimic Lean LSP's behaviour. That would not grant the
38
- flexibility it offers. To support tree search means Pantograph has to act
39
- differently from Lean in some times, but never at the sacrifice of soundness.
40
-
41
- - When Lean LSP says "don't know how to synthesize placeholder", this indicates
42
- the human operator needs to manually move the cursor to the placeholder and
43
- type in the correct expression. This error therefore should not halt the proof
44
- process, and the placeholder should be turned into a goal.
45
- - When Lean LSP says "unresolved goals", that means a proof cannot finish where
46
- it is supposed to finish at the end of a `by` block. Pantograph will raise the
47
- error in this case, since it indicates the termination of a proof search branch.
48
-
49
- Pantograph cannot perform things that are inherently constrained by Lean. These
50
- include:
51
-
52
- - If a tactic loses track of metavariables, it will not be caught until the end
53
- of the proof search. This is a bug in the tactic itself.
54
- - Lean's concurrency model is coΓΆperative, which means a tactic is responsible
55
- for checking a cancellation flag if it runs for a long time. Pantograph's
56
- built-in timeout feature requires such behaviour. A tactic which hangs without
57
- checking the flag cannot be timeouted.
58
- - Interceptions of parsing errors generally cannot be turned into goals (e.g.
59
- `def mystery : Nat := :=`) due to Lean's parsing system.
60
-
61
- Each Pantograph version is anchored to a Lean version specified in
62
- `src/lean-toolchain`. Features can be backported to older Lean versions upon
63
- request.
64
-
65
- ## Referencing
66
-
67
- [Paper Link](https://arxiv.org/abs/2410.16429)
68
-
69
- ```bib
70
- @misc{pantograph,
71
- title={Pantograph: A Machine-to-Machine Interaction Interface for Advanced Theorem Proving, High Level Reasoning, and Data Extraction in Lean 4},
72
- author={Leni Aniva and Chuyue Sun and Brando Miranda and Clark Barrett and Sanmi Koyejo},
73
- year={2024},
74
- eprint={2410.16429},
75
- archivePrefix={arXiv},
76
- primaryClass={cs.LO},
77
- url={https://arxiv.org/abs/2410.16429},
78
- }
79
- ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
PyPantograph/doc/setup.md DELETED
@@ -1,85 +0,0 @@
1
- # Setup
2
-
3
- 1. Install `uv`
4
- 2. Clone this repository with submodules:
5
- ```sh
6
- git clone --recurse-submodules <repo-path>
7
- ```
8
- 3. Install `elan` and `lake`: See [Lean Manual](https://docs.lean-lang.org/lean4/doc/setup.html)
9
- 4. Execute
10
- ```sh
11
- cd <repo-path>
12
- uv sync
13
- ```
14
-
15
- `uv build` builds a wheel of Pantograph in `dist` which can then be installed. For
16
- example, a downstream project could have this line in its `pyproject.toml`
17
-
18
- ```toml
19
- pantograph = { file = "path/to/wheel/dist/pantograph-0.3.0-cp312-cp312-manylinux_2_40_x86_64.whl" }
20
- ```
21
-
22
- All interactions with Lean pass through the `Server` class. Create an instance of Pantograph using
23
- ```python
24
- from pantograph import Server
25
- server = Server()
26
- ```
27
-
28
- ## Lean Dependencies
29
-
30
- The server created from `Server()` is sufficient for basic theorem proving tasks
31
- reliant on Lean's `Init` library. Some users may find this insufficient and want
32
- to use non-builtin libraries such as Aesop or Mathlib4. In this case, feed in a
33
- list of module names via the `imports` parameter e.g. `imports=["Mathlib"]`. Due
34
- to inherent restrictions in Lean, importing a module that has not been imported
35
- before after the server has already started is not allowed and will trigger
36
- initializer exceptions. It may be possible to circumvent this if Lean relaxes
37
- this constraint.
38
-
39
- To use external Lean dependencies such as
40
- [Mathlib4](https://github.com/leanprover-community/mathlib4), Pantograph relies
41
- on an existing Lean repository. Instructions for creating this repository can be
42
- found [here](https://docs.lean-lang.org/lean4/doc/setup.html#lake).
43
-
44
- After creating this initial Lean repository, execute in the repository
45
- ```sh
46
- lake build
47
- ```
48
-
49
- to build all files from the repository. This step is necessary after any file in
50
- the repository is modified.
51
-
52
- Then, feed the repository's path to the server
53
- ```python
54
- server = Server(project_path="./path-to-lean-repo/")
55
- ```
56
-
57
- For a complete example, see `examples/`.
58
-
59
- ## Server Parameters
60
-
61
- The server has some additional options.
62
-
63
- - `core_options`: These options are passed to Lean's kernel. For example
64
- `set_option pp.all true` in Lean corresponds to passing `pp.all=true` to
65
- `core_options`.
66
- - `options`: These options are given to Pantograph itself. See below.
67
- - `timeout`: This timeout controls the maximum wait time for the server
68
- instance. If the server instance does not respond within this timeout limit,
69
- it gets terminated. In some cases it is necessary to increase this if loading
70
- a Lean project takes too long.
71
-
72
- A special note about running in Jupyter: Use the asynchronous version of each
73
- function.
74
-
75
- ```python
76
- server = await Server.create()
77
- unit, = await server.load_sorry_async(sketch)
78
- print(unit.goal_state)
79
- ```
80
-
81
- ### Options
82
-
83
- - `automaticMode`: Set to false to disable automatic goal continuation.
84
- - `timeout`: Set to a positive integer to set tactic execution timeout.
85
- - `printDependentMVars`: Set to true to explicitly store goal inter-dependencies
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
PyPantograph/examples/Example/.gitignore DELETED
@@ -1,3 +0,0 @@
1
- /build
2
- /lakefile.olean
3
- /lake-packages/*
 
 
 
 
PyPantograph/examples/Example/Example.lean DELETED
@@ -1,14 +0,0 @@
1
- import Aesop
2
-
3
- /-- Ensure that Aesop is running -/
4
- example : Ξ± β†’ Ξ± :=
5
- by aesop
6
-
7
- example : βˆ€ (p q: Prop), p ∨ q β†’ q ∨ p := by
8
- intro p q h
9
- -- Here are some comments
10
- cases h
11
- . apply Or.inr
12
- assumption
13
- . apply Or.inl
14
- assumption
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
PyPantograph/examples/Example/lake-manifest.json DELETED
@@ -1,25 +0,0 @@
1
- {"version": "1.1.0",
2
- "packagesDir": ".lake/packages",
3
- "packages":
4
- [{"url": "https://github.com/leanprover-community/aesop.git",
5
- "type": "git",
6
- "subDir": null,
7
- "scope": "",
8
- "rev": "1256a18522728c2eeed6109b02dd2b8f207a2a3c",
9
- "name": "aesop",
10
- "manifestFile": "lake-manifest.json",
11
- "inputRev": "v4.22.0",
12
- "inherited": false,
13
- "configFile": "lakefile.toml"},
14
- {"url": "https://github.com/leanprover-community/batteries",
15
- "type": "git",
16
- "subDir": null,
17
- "scope": "",
18
- "rev": "e96b5eca4fcfe2e0e96a1511a6cd5747515aba82",
19
- "name": "batteries",
20
- "manifestFile": "lake-manifest.json",
21
- "inputRev": "v4.22.0-rc4",
22
- "inherited": true,
23
- "configFile": "lakefile.toml"}],
24
- "name": "Example",
25
- "lakeDir": ".lake"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
PyPantograph/examples/Example/lakefile.lean DELETED
@@ -1,10 +0,0 @@
1
- import Lake
2
- open Lake DSL
3
-
4
- require aesop from git
5
- "https://github.com/leanprover-community/aesop.git" @ "v4.22.0"
6
-
7
- package Example
8
-
9
- @[default_target]
10
- lean_lib Example
 
 
 
 
 
 
 
 
 
 
 
PyPantograph/examples/Example/lean-toolchain DELETED
@@ -1 +0,0 @@
1
- ../../src/lean-toolchain
 
 
PyPantograph/examples/README.md DELETED
@@ -1,24 +0,0 @@
1
- # Examples
2
-
3
- This example showcases how to bind library dependencies and execute the `Aesop`
4
- tactic in Lean. First build the example project:
5
- ``` sh
6
- pushd Example
7
- lake build
8
- popd
9
- ```
10
- This would generate compiled `.olean` files. Then run one of the examples from the
11
- project root:
12
- ``` sh
13
- poetry run examples/aesop.py
14
- poetry run examples/sketch.py
15
- ```
16
-
17
- Warning: If you make modifications to any Lean files, you must re-run `lake
18
- build`! Moreover, the version of the Lean used in the example folder (including
19
- dependencies in `lakefile.lean` and `lean-toolchain`) **must match exactly**
20
- with the version in `src/`!
21
-
22
- * `aesop.py`: Example of how to use the `aesop` tactic
23
- * `sketch.py`: Example of loading a sketch
24
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
PyPantograph/examples/aesop.py DELETED
@@ -1,14 +0,0 @@
1
- #!/usr/bin/env python3
2
-
3
- from pathlib import Path
4
- from pantograph.server import Server
5
-
6
- # This example shows how to use project dependencies
7
-
8
- if __name__ == '__main__':
9
- project_path = Path(__file__).parent.resolve() / 'Example'
10
- print(f"$PWD: {project_path}")
11
- server = Server(imports=['Example'], project_path=project_path)
12
- state0 = server.goal_start("forall (p q: Prop), Or p q -> Or q p")
13
- state1 = server.goal_tactic(state0, tactic="aesop")
14
- assert state1.is_solved
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
PyPantograph/examples/branch-sorry.py DELETED
@@ -1,13 +0,0 @@
1
- #!/usr/bin/env python3
2
-
3
- from pantograph.server import Server
4
- from pantograph.expr import TacticHave
5
-
6
- # This example shows what happens when a tactic generates a sorry.
7
- if __name__ == '__main__':
8
- server = Server(imports=['Init'])
9
- state0 = server.goal_start("1 = 0")
10
- state1 = server.goal_tactic(state0, tactic=TacticHave("1 = 0"))
11
- print(state1)
12
- state1b = server.goal_tactic(state1, tactic="apply?")
13
- print(state1b)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
PyPantograph/examples/simple.py DELETED
@@ -1,7 +0,0 @@
1
- from pantograph.server import Server
2
-
3
- if __name__ == '__main__':
4
- server = Server(imports=['Init'])
5
- state0 = server.goal_start("forall (p q: Prop), Or p q -> Or q p")
6
- state1 = server.goal_tactic(state0, tactic="intro")
7
- print(state1)
 
 
 
 
 
 
 
 
PyPantograph/examples/sketch.py DELETED
@@ -1,41 +0,0 @@
1
- #!/usr/bin/env python3
2
-
3
- from pantograph.server import Server
4
- from pantograph.expr import TacticDraft
5
-
6
- root = """
7
- theorem add_comm_proved_formal_sketch : βˆ€ n m : Nat, n + m = m + n := sorry
8
- """
9
-
10
- sketch = """
11
- by
12
- -- Consider some n and m in Nats.
13
- intros n m
14
- -- Perform induction on n.
15
- induction n with
16
- | zero =>
17
- sorry
18
- | succ n ih =>
19
- -- Inductive step: Assume n + m = m + n, we need to show succ n + m = m + succ n.
20
- -- By the inductive hypothesis, we have n + m = m + n.
21
- have h_inductive: n + m = m + n := sorry
22
- -- 1. Note we start with: Nat.succ n + m = m + Nat.succ n, so, pull the succ out from m + Nat.succ n on the right side from the addition using addition facts Nat.add_succ.
23
- have h_pull_succ_out_from_right: m + Nat.succ n = Nat.succ (m + n) := sorry
24
- -- 2. then to flip m + S n to something like S (n + m) we need to use the IH.
25
- have h_flip_n_plus_m: Nat.succ (n + m) = Nat.succ (m + n) := sorry
26
- -- 3. Now the n & m are on the correct sides Nat.succ n + m = Nat.succ (n + m), so let's use the def of addition to pull out the succ from the addition on the left using Nat.succ_add.
27
- have h_pull_succ_out_from_left: Nat.succ n + m = Nat.succ (n + m) := sorry
28
- -- Combine facts to close goal
29
- sorry
30
- """
31
-
32
- if __name__ == '__main__':
33
- server = Server()
34
- unit, = server.load_sorry(root)
35
- print(unit.goal_state)
36
-
37
- # Send the draft payload using `TacticDraft`
38
- state1 = server.goal_tactic(
39
- unit.goal_state,
40
- tactic=TacticDraft(sketch))
41
- print(state1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
PyPantograph/pantograph/.gitignore DELETED
@@ -1,2 +0,0 @@
1
- pantograph-repl
2
- lean-toolchain
 
 
 
PyPantograph/pantograph/__init__.py DELETED
@@ -1 +0,0 @@
1
- from pantograph.server import Server
 
 
PyPantograph/pantograph/data.py DELETED
@@ -1,113 +0,0 @@
1
- from pantograph.message import Severity, Message
2
- from pantograph.expr import GoalState
3
-
4
- from typing import Optional, Tuple
5
- from dataclasses import dataclass, field
6
- from enum import Enum
7
-
8
- @dataclass(frozen=True)
9
- class TacticInvocation:
10
- """
11
- One tactic invocation with the before/after goals extracted from Lean source
12
- code.
13
- """
14
- before: str
15
- after: str
16
- tactic: str
17
- used_constants: list[str]
18
-
19
- @staticmethod
20
- def parse(payload: dict):
21
- return TacticInvocation(
22
- before=payload["goalBefore"],
23
- after=payload["goalAfter"],
24
- tactic=payload["tactic"],
25
- used_constants=payload.get('usedConstants', []),
26
- )
27
-
28
- @dataclass(frozen=True)
29
- class CompilationUnit:
30
-
31
- # Byte boundaries [begin, end[ of each compilation unit.
32
- i_begin: int
33
- i_end: int
34
-
35
- messages: list[Message] = field(default_factory=lambda: [])
36
-
37
- invocations: Optional[list[TacticInvocation]] = None
38
- # If `goal_state` is none, maybe error has occurred. See `messages`
39
- goal_state: Optional[GoalState] = None
40
- goal_src_boundaries: Optional[list[Tuple[int, int]]] = None
41
-
42
- new_constants: Optional[list[str]] = None
43
-
44
- @staticmethod
45
- def parse(payload: dict, goal_state_sentinel=None, invocations=None):
46
- i_begin = payload["boundary"][0]
47
- i_end = payload["boundary"][1]
48
- messages = [Message.parse(m) for m in payload["messages"]]
49
-
50
- if invocations:
51
- invocations = [
52
- TacticInvocation.parse(i) for i in invocations
53
- ]
54
- else:
55
- invocations = None
56
-
57
- if (state_id := payload.get("goalStateId")) is not None:
58
- goal_state = GoalState.parse_inner(int(state_id), payload["goals"], [], goal_state_sentinel)
59
- goal_src_boundaries = payload["goalSrcBoundaries"]
60
- else:
61
- goal_state = None
62
- goal_src_boundaries = None
63
-
64
- new_constants = payload.get("newConstants")
65
-
66
- return CompilationUnit(
67
- i_begin,
68
- i_end,
69
- messages,
70
- invocations,
71
- goal_state,
72
- goal_src_boundaries,
73
- new_constants
74
- )
75
-
76
- @dataclass
77
- class CheckTrackResult:
78
-
79
- src_messages : list[Message]
80
- dst_messages : list[Message]
81
- failure : Optional[str] = None
82
-
83
- @property
84
- def hasSrcError(self):
85
- return any(m.severity == Severity.ERROR for m in self.src_messages)
86
- @property
87
- def hasDstError(self):
88
- return any(m.severity == Severity.ERROR for m in self.dst_messages)
89
- @property
90
- def succeeded(self):
91
- return not self.hasSrcError and not self.hasDstError and self.failure is None
92
- @property
93
- def feedback(self):
94
- """
95
- Feedback based on the dst
96
- """
97
- result = [str(s) for s in self.dst_messages]
98
- if self.failure:
99
- result.append(self.failure)
100
- return result
101
-
102
- @dataclass(frozen=True)
103
- class SearchTarget:
104
- goal_state: GoalState
105
-
106
- @staticmethod
107
- def parse(payload: dict, goal_state_sentinel=None):
108
- state_id = payload.get("stateId")
109
- goal_state = GoalState.parse_inner(int(state_id), payload["goals"], [], goal_state_sentinel)
110
-
111
- return SearchTarget(
112
- goal_state,
113
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
PyPantograph/pantograph/expr.py DELETED
@@ -1,202 +0,0 @@
1
- """
2
- Data structuers for expressions and goals
3
- """
4
- from pantograph.message import Message
5
-
6
- from dataclasses import dataclass, field
7
- from enum import Enum
8
- from typing import Optional, TypeAlias
9
-
10
- Expr: TypeAlias = str
11
-
12
- def parse_expr(payload: dict) -> Expr:
13
- """
14
- :meta private:
15
- """
16
- return payload["pp"]
17
-
18
- class TacticMode(Enum):
19
- """
20
- Current execution mode
21
- """
22
- TACTIC = 1
23
- CONV = 2
24
- CALC = 3
25
-
26
- def serial(self):
27
- match self:
28
- case TacticMode.TACTIC: return "tactic"
29
- case TacticMode.CONV: return "conv"
30
- case TacticMode.CALC: return "calc"
31
-
32
- @dataclass(frozen=True)
33
- class Variable:
34
- t: Expr
35
- v: Optional[Expr] = None
36
- name: Optional[str] = None
37
-
38
- @staticmethod
39
- def parse(payload: dict):
40
- name = payload.get("userName")
41
- t = parse_expr(payload["type"])
42
- v = payload.get("value")
43
- if v:
44
- v = parse_expr(v)
45
- return Variable(t, v, name)
46
-
47
- def __str__(self):
48
- """
49
- :meta public:
50
- """
51
- result = self.name if self.name else "_"
52
- result += f" : {self.t}"
53
- if self.v:
54
- result += f" := {self.v}"
55
- return result
56
-
57
- @dataclass(frozen=True)
58
- class Goal:
59
- id: str
60
- variables: list[Variable]
61
- target: Expr
62
- sibling_dep: Optional[set[int]] = field(default_factory=lambda: None)
63
- name: Optional[str] = None
64
- mode: TacticMode = TacticMode.TACTIC
65
-
66
- @staticmethod
67
- def sentence(id: str, target: Expr):
68
- """
69
- :meta public:
70
- """
71
- return Goal(id=id, variables=[], target=target)
72
-
73
- @staticmethod
74
- def parse(payload: dict, sibling_map: dict[str, int]):
75
- id = payload["name"]
76
- name = payload.get("userName")
77
- variables = [Variable.parse(v) for v in payload["vars"]]
78
- target = parse_expr(payload["target"])
79
- mode = TacticMode[payload["fragment"].upper()]
80
-
81
- sibling_dep = None
82
- for e in [payload["target"]] \
83
- + [v["type"] for v in payload["vars"]] \
84
- + [v["value"] for v in payload["vars"] if "value" in v]:
85
- dependents = e.get("dependentMVars")
86
- if dependents is None:
87
- continue
88
- deps = [sibling_map[d] for d in dependents if d in sibling_map]
89
- if sibling_dep:
90
- sibling_dep = {*sibling_dep, *deps}
91
- else:
92
- sibling_dep = {*deps}
93
-
94
- return Goal(id, variables, target, sibling_dep, name, mode)
95
-
96
- def __str__(self):
97
- head = f"case {self.name}\n" if self.name else ""
98
- front = "|" if self.mode == TacticMode.CONV else "⊒"
99
- return head +\
100
- "\n".join(str(v) for v in self.variables) +\
101
- f"\n{front} {self.target}"
102
-
103
- @dataclass(frozen=True)
104
- class GoalState:
105
- state_id: int
106
- goals: list[Goal]
107
- messages: list[Message]
108
-
109
- # For tracking memory usage
110
- _sentinel: list[int]
111
-
112
- def __del__(self):
113
- self._sentinel.append(self.state_id)
114
- def __repr__(self):
115
- cls = self.__class__.__name__
116
- messages = f"messages={repr(self.messages)}, " if self.messages else ""
117
- return f"{cls}(#{self.state_id}, goals={repr(self.goals)}{messages}, _sentinel=#{len(self._sentinel)}"
118
-
119
- @property
120
- def is_solved(self) -> bool:
121
- """
122
- WARNING: Does not handle dormant goals.
123
-
124
- :meta public:
125
- """
126
- return not self.goals
127
-
128
- @staticmethod
129
- def parse_inner(state_id: int, goals: list, messages: list[dict], _sentinel: list[int]):
130
- assert _sentinel is not None
131
- goal_names = { g["name"]: i for i, g in enumerate(goals) }
132
- goals = [Goal.parse(g, goal_names) for g in goals]
133
- messages = [Message.parse(m) for m in messages]
134
- return GoalState(state_id, goals, messages, _sentinel)
135
- @staticmethod
136
- def parse(payload: dict, messages: list[dict], _sentinel: list[int]):
137
- return GoalState.parse_inner(payload["nextStateId"], payload["goals"], messages, _sentinel)
138
-
139
- def __str__(self):
140
- """
141
- :meta public:
142
- """
143
- return "\n".join([str(g) for g in self.goals])
144
-
145
- @dataclass(frozen=True)
146
- class Site:
147
- """
148
- Acting area of a tactic
149
- """
150
- goal_id: Optional[int] = None
151
- auto_resume: Optional[bool] = None
152
-
153
- def serial(self) -> dict:
154
- result = {}
155
- if self.goal_id is not None:
156
- result["goalId"] = self.goal_id
157
- if self.auto_resume is not None:
158
- result["autoResume"] = self.auto_resume
159
- return result
160
-
161
- class Subsumption(Enum):
162
- """
163
- Subsumption result
164
- """
165
- NONE = 1
166
- SUBSUMED = 2
167
- CYCLE = 3
168
-
169
- @dataclass(frozen=True)
170
- class TacticHave:
171
- """
172
- The `have` tactic, equivalent to
173
- ```lean
174
- have {binder_name} : {branch} := ...
175
- ```
176
- """
177
- branch: str
178
- binder_name: Optional[str] = None
179
- @dataclass(frozen=True)
180
- class TacticLet:
181
- """
182
- The `let` tactic, equivalent to
183
- ```lean
184
- let {binder_name} : {branch} := ...
185
- ```
186
- """
187
- branch: str
188
- binder_name: Optional[str] = None
189
- @dataclass(frozen=True)
190
- class TacticExpr:
191
- """
192
- Assigns an expression to the current goal
193
- """
194
- expr: str
195
- @dataclass(frozen=True)
196
- class TacticDraft:
197
- """
198
- Assigns an expression to the current goal
199
- """
200
- expr: str
201
-
202
- Tactic: TypeAlias = str | TacticHave | TacticLet | TacticExpr | TacticDraft | TacticMode
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
PyPantograph/pantograph/message.py DELETED
@@ -1,73 +0,0 @@
1
- from dataclasses import dataclass
2
- from enum import Enum
3
- from typing import Optional, Self
4
-
5
- class Severity(Enum):
6
- INFORMATION = 1
7
- WARNING = 2
8
- ERROR = 3
9
-
10
- def __str__(self):
11
- cls = self.__class__.__name__
12
- return super(Severity, self).__str__()[len(cls)+1:].lower()
13
-
14
- @dataclass(frozen=True)
15
- class Position:
16
- """
17
- Position in a file
18
- """
19
- line: int
20
- column: int
21
-
22
- @staticmethod
23
- def parse(d: Optional[dict]) -> Optional[Self]:
24
- if d is None:
25
- return None
26
- return Position(line=d["line"], column=d["column"])
27
-
28
- @dataclass(frozen=True)
29
- class Message:
30
- data: str
31
- pos: Position
32
- pos_end: Optional[Position] = None
33
- severity: Severity = Severity.ERROR
34
- kind: Optional[str] = None
35
-
36
- @staticmethod
37
- def parse(d: dict) -> Self:
38
- kind = None if d["kind"] == "[anonymous]" else d["kind"]
39
- return Message(
40
- severity=Severity[d["severity"].upper()],
41
- pos=Position.parse(d["pos"]),
42
- pos_end=Position.parse(d.get("endPos")),
43
- kind=kind,
44
- data=d["data"],
45
- )
46
-
47
- def __str__(self) -> str:
48
- if self.pos_end is not None:
49
- pos_end = f"-{self.pos_end.line}:{self.pos_end.column}"
50
- else:
51
- pos_end = ""
52
- match self.severity:
53
- case Severity.INFORMATION:
54
- prefix = ""
55
- case Severity.WARNING:
56
- prefix = "warning: "
57
- case Severity.ERROR:
58
- prefix = "error: "
59
- return f"{self.pos.line}:{self.pos.column}{pos_end}: {prefix}{self.data}"
60
-
61
-
62
- class TacticFailure(Exception):
63
- """
64
- Indicates a tactic failed to execute
65
- """
66
- class ParseError(Exception):
67
- """
68
- Indicates a logical error in the server.
69
- """
70
- class ServerError(Exception):
71
- """
72
- Indicates a logical error in the server.
73
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
PyPantograph/pantograph/search.py DELETED
@@ -1,450 +0,0 @@
1
- from .server import Server, TacticFailure, ServerError
2
- from .expr import Expr, Tactic, GoalState, Site
3
-
4
- import random
5
- from abc import abstractmethod
6
- import time
7
- from dataclasses import dataclass
8
- from typing import Optional, List
9
- import collections
10
- from math import log, sqrt
11
-
12
-
13
-
14
- @dataclass
15
- class SearchState:
16
-
17
- goal_state: GoalState
18
- parent: Optional["SearchState"]
19
- parent_goal_id: Optional[int]
20
- priorities: list[float]
21
- children: Optional[List["SearchState"]] = None
22
- tested_tactics: Optional[List[Tactic]] = None
23
- total_value: Optional[float] = None
24
- tactic_feedback: Optional[str] = None
25
-
26
- def __post_init__(self):
27
- assert len(self.priorities) == len(self.goal_state.goals)
28
- self.solved = [False for _ in self.goal_state.goals]
29
- self.trials = [0 for _ in self.goal_state.goals]
30
- self.tested_tactics = [] if self.tested_tactics is None else self.tested_tactics
31
- self.children = [] if self.children is None else self.children
32
- self.visit_count = 1
33
- self.exhausted = False
34
- self.subtree_exhausted = False
35
-
36
- @property
37
- def next_goal_id(self) -> int:
38
- goal_id, _ = max(
39
- ((i, prio) for i, prio in enumerate(self.priorities) if not self.solved[i]),
40
- key=lambda x: x[1])
41
- return goal_id
42
-
43
- @property
44
- def is_root(self) -> bool:
45
- return self.parent is None
46
-
47
- @property
48
- def is_solved(self) -> bool:
49
- return all(self.solved)
50
-
51
- @dataclass(frozen=True)
52
- class SearchResult:
53
-
54
- n_goals_root: int
55
- duration: float
56
- success: bool
57
- steps: int
58
-
59
- class Agent:
60
- """
61
- An agent interface for proof search
62
- """
63
-
64
- @abstractmethod
65
- def next_tactic(
66
- self,
67
- state: GoalState,
68
- goal_id: int,
69
- ) -> Optional[Tactic]:
70
- """
71
- Implement this function to generate the next tactic for a goal
72
- """
73
-
74
- @abstractmethod
75
- def guidance(self, state: GoalState) -> list[float]:
76
- """
77
- Return a list of priorities determining which goal should be searched
78
- first. This will not be called on states with one or zero goals.
79
- """
80
- return [0.0 for _ in state.goals]
81
- @abstractmethod
82
- def reset(self):
83
- """
84
- Called after search
85
- """
86
-
87
- def search(self,
88
- server: Server,
89
- goal_state: GoalState,
90
- max_steps: int = 100,
91
- max_trials_per_goal: int = 5,
92
- verbose: bool = False) -> SearchResult:
93
- """
94
- Executes proof search on this state
95
- """
96
-
97
- assert server.is_automatic(), "Search must be run in automatic mode"
98
-
99
- n_goals_root = len(goal_state.goals)
100
- time_start = time.time()
101
-
102
- initial_state = SearchState(
103
- goal_state,
104
- parent=None,
105
- parent_goal_id=None,
106
- priorities=[0.0 for _ in goal_state.goals]
107
- )
108
- search_stack = [initial_state]
109
- for i_step in range(max_steps):
110
- assert search_stack, "No states in search stack"
111
-
112
- if verbose:
113
- print(f"I={i_step}: len(S) = {len(search_stack)}")
114
- search_state = search_stack[-1]
115
-
116
- assert isinstance(search_state, SearchState)
117
-
118
- if search_state.is_solved:
119
- return SearchResult(
120
- n_goals_root=n_goals_root,
121
- duration=time.time() - time_start,
122
- success=True,
123
- steps=i_step,
124
- )
125
-
126
- # Find the unsolved goal with the highest priority
127
- goal_id = search_state.next_goal_id
128
-
129
- if search_state.trials[goal_id] > max_trials_per_goal:
130
- # force halt the search
131
- tactic = None
132
- else:
133
- # Generate tactic for this goal
134
- tactic = self.next_tactic(search_state.goal_state, goal_id)
135
-
136
- if verbose:
137
- print(f"Next tactic: {tactic}")
138
- if not tactic:
139
- # resets the feedback
140
- search_state.tactic_feedback = None
141
- # pop the current state and continue to the next
142
- search_stack.pop(-1)
143
- if not search_stack:
144
- if verbose:
145
- print("Search stack has been exhausted")
146
- self.reset()
147
- return SearchResult(
148
- n_goals_root=n_goals_root,
149
- duration=time.time() - time_start,
150
- success=False,
151
- steps=i_step,
152
- )
153
- continue
154
-
155
- try:
156
- search_state.trials[goal_id] += 1
157
- goal_state = search_state.goal_state
158
- if verbose:
159
- print(f"{goal_state.state_id}.{goal_id}: {tactic} on {goal_state.goals[goal_id]}")
160
- next_goal_state = server.goal_tactic(goal_state, tactic, site=Site(goal_id, auto_resume=False))
161
- # Generate priorities for the next goal state
162
- priorities = [0.0 for _ in next_goal_state.goals] \
163
- if len(next_goal_state.goals) <= 1 else \
164
- self.guidance(next_goal_state)
165
- parent = len(search_stack) - 1
166
- next_state = SearchState(
167
- goal_state=next_goal_state,
168
- parent=search_state,
169
- parent_goal_id=goal_id,
170
- priorities=priorities
171
- )
172
- search_stack.append(next_state)
173
-
174
- except TacticFailure as t:
175
- if verbose:
176
- print(f"Tactic failed: {t}")
177
- search_state.tactic_feedback = str(t)
178
- # try the next tactic. this one failed
179
- except ServerError as e:
180
- raise RuntimeError(f"While executing tactic: {tactic}") from e
181
-
182
- if verbose:
183
- print("Search iteration limit exhausted")
184
-
185
- self.reset()
186
- return SearchResult(
187
- n_goals_root=n_goals_root,
188
- duration=time.time() - time_start,
189
- success=False,
190
- steps=max_steps,
191
- )
192
-
193
-
194
- class MCTSAgent(Agent):
195
- """
196
- An agent interface for proof search using monte carlo tree search
197
- """
198
-
199
- @abstractmethod
200
- def next_tactic(
201
- self,
202
- state: GoalState,
203
- goal_id: int,
204
- tested: Optional[List[Tactic]] = None,
205
- ) -> Optional[Tactic]:
206
- """
207
- Implement this function to generate the next tactic for a goal given tactics already tested
208
- """
209
-
210
- @abstractmethod
211
- def reset(self):
212
- """
213
- Called after search
214
- """
215
-
216
- @abstractmethod
217
- def estimate(self, state: SearchState) -> SearchState:
218
- """
219
- Implement this function to estimate the value of a state
220
- """
221
-
222
- @abstractmethod
223
- def select(self, state: SearchState) -> list[SearchState]:
224
- """
225
- Implement this function to select the best node within the subtree of the state.
226
- Returns the path to the selected node from the given state.
227
- """
228
-
229
- def backup(self, states: list[SearchState], value: float):
230
- """
231
- Backup value of the state at the end of the states list.
232
- """
233
- for state in states:
234
- state.total_value += value
235
- state.visit_count += 1
236
- state.subtree_exhausted = all(child.subtree_exhausted for child in state.children) and state.exhausted
237
-
238
- def search(self,
239
- server: Server,
240
- goal_state: GoalState,
241
- max_steps: int = 100,
242
- max_trials_per_goal: int = 5,
243
- verbose: bool = False) -> SearchResult:
244
- """
245
- Executes proof search on this state
246
- """
247
-
248
- assert server.is_automatic(), "Search must be run in automatic mode"
249
-
250
- n_goals_root = len(goal_state.goals)
251
- time_start = time.time()
252
-
253
- initial_state = SearchState(
254
- goal_state=goal_state,
255
- parent=None,
256
- parent_goal_id=None,
257
- priorities=[0.0 for _ in goal_state.goals]
258
- )
259
- initial_state = self.estimate(initial_state)
260
- search_root = initial_state
261
-
262
- for i_step in range(max_steps):
263
- search_trajectory = self.select(search_root)
264
- search_state = search_trajectory[-1]
265
- assert isinstance(search_state, SearchState)
266
-
267
- if search_state.is_solved:
268
- return SearchResult(
269
- n_goals_root=n_goals_root,
270
- duration=time.time() - time_start,
271
- success=True,
272
- steps=i_step,
273
- )
274
-
275
- # Find the unsolved goal with the highest priority
276
- goal_id = search_state.next_goal_id
277
-
278
- if search_state.trials[goal_id] > max_trials_per_goal:
279
- # force halt the search
280
- tactic = None
281
- else:
282
- # Generate tactic for this goal
283
- tactic = self.next_tactic(search_state.goal_state, goal_id, search_state.tested_tactics)
284
-
285
- if verbose:
286
- print(f"Next tactic: {tactic}")
287
- if not tactic:
288
- # resets the feedback
289
- search_state.tactic_feedback = None
290
- search_state.exhausted = True
291
- search_state.subtree_exhausted = all(child.subtree_exhausted for child in search_state.children)
292
- continue
293
- assert tactic not in search_state.tested_tactics, "Tactic already seen!"
294
- search_state.tested_tactics.append(tactic)
295
-
296
- try:
297
- search_state.trials[goal_id] += 1
298
- state = search_state.goal_state
299
- if verbose:
300
- print(f"{state.state_id}.{goal_id}: {tactic} on {search_state.goal_state.goals[goal_id]}")
301
- next_goal_state = server.goal_tactic(goal_state, tactic, site=Site(goal_id, auto_resume=False))
302
- # Generate priorities for the next goal state
303
- priorities = [0.0 for _ in next_goal_state.goals] \
304
- if len(next_goal_state.goals) <= 1 else \
305
- self.guidance(next_goal_state)
306
- parent = -1
307
- next_state = SearchState(
308
- goal_state=next_goal_state,
309
- parent=parent,
310
- parent_goal_id=goal_id,
311
- priorities=priorities
312
- )
313
- next_state = self.estimate(next_state)
314
- search_state.children.append(next_state)
315
- self.backup(search_trajectory, next_state.total_value)
316
- except TacticFailure as t:
317
- if verbose:
318
- print(f"Tactic failed: {t}")
319
- search_state.tactic_feedback = str(t)
320
- # try the next tactic. this one failed
321
- except ServerError as e:
322
- raise RuntimeError(f"While executing tactic: {tactic}") from e
323
-
324
- if verbose:
325
- print("Search iteration limit exhausted")
326
-
327
- self.reset()
328
- return SearchResult(
329
- n_goals_root=n_goals_root,
330
- duration=time.time() - time_start,
331
- success=False,
332
- steps=max_steps,
333
- )
334
-
335
-
336
- class DumbAgent(Agent):
337
-
338
- def __init__(self):
339
- super().__init__()
340
-
341
- self.goal_tactic_id_map = collections.defaultdict(lambda : 0)
342
- self.intros = [
343
- "intro",
344
- ]
345
- self.tactics = [
346
- "intro h",
347
- "cases h",
348
- "apply Or.inl",
349
- "apply Or.inr",
350
- ]
351
- self.no_space_tactics = [
352
- "assumption",
353
- ]
354
-
355
- def next_tactic(
356
- self,
357
- state: GoalState,
358
- goal_id: int,
359
- ) -> Optional[Tactic]:
360
- key = (state.state_id, goal_id)
361
- i = self.goal_tactic_id_map[key]
362
-
363
- target = state.goals[goal_id].target
364
- if target.startswith('βˆ€'):
365
- tactics = self.intros
366
- elif ' ' in target:
367
- tactics = self.tactics
368
- else:
369
- tactics = self.no_space_tactics
370
-
371
- if i >= len(tactics):
372
- return None
373
-
374
- self.goal_tactic_id_map[key] = i + 1
375
- return tactics[i]
376
-
377
- class DumbMCTSAgent(MCTSAgent):
378
- def __init__(self):
379
- super().__init__()
380
-
381
- self.goal_tactic_id_map = collections.defaultdict(lambda : 0)
382
- self.intros = [
383
- "intro",
384
- ]
385
- self.tactics = [
386
- "intro h",
387
- "cases h",
388
- "apply Or.inl",
389
- "apply Or.inr",
390
- ]
391
- self.no_space_tactics = [
392
- "assumption",
393
- ]
394
- self.c = 0.6
395
-
396
- def estimate(self, state: SearchState) -> SearchState:
397
- state.total_value = random.random()
398
- return state
399
-
400
- def select(self, state: SearchState) -> list[SearchState]:
401
- """
402
- UCB scoring with taking the current state as one option, i.e. one child
403
- """
404
- state_trajectory = [state]
405
- current_state = state
406
- current_state_ucb = (state.total_value / state.visit_count) + self.c * sqrt((log(state.visit_count) / state.visit_count))
407
- while current_state.children:
408
- avg_val = [child.total_value / child.visit_count for child in current_state.children]
409
- visit_portions = [sqrt(log(current_state.visit_count) / child.visit_count) for child in current_state.children]
410
- ucbs = [avg + self.c * visit for avg, visit in zip(avg_val, visit_portions, strict=True)]
411
- child_idcs = [idx for idx in range(len(current_state.children)) if not current_state.children[idx].subtree_exhausted]
412
- if not child_idcs:
413
- return state_trajectory
414
- child_idx = child_idcs[0]
415
- for i in child_idcs:
416
- if ucbs[i] > ucbs[child_idx]:
417
- child_idx = i
418
- if ucbs[child_idx] < current_state_ucb and not current_state.exhausted:
419
- return state_trajectory
420
- current_state_ucb = ucbs[child_idx]
421
- current_state = current_state.children[child_idx]
422
- state_trajectory.append(current_state)
423
- return state_trajectory
424
-
425
- def next_tactic(
426
- self,
427
- state: GoalState,
428
- goal_id: int,
429
- tested: Optional[List[Tactic]] = None
430
- ) -> Optional[Tactic]:
431
- key = (state.state_id, goal_id)
432
- i = self.goal_tactic_id_map[key]
433
- target = state.goals[goal_id].target
434
- if target.startswith('βˆ€'):
435
- tactics = self.intros
436
- elif ' ' in target:
437
- tactics = self.tactics
438
- else:
439
- tactics = self.no_space_tactics
440
-
441
- if i >= len(tactics):
442
- return None
443
- self.goal_tactic_id_map[key] = i + 1
444
- while tactics[i] in tested:
445
- i += 1
446
- if i >= len(tactics):
447
- return None
448
- return tactics[i]
449
-
450
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
PyPantograph/pantograph/server.py DELETED
@@ -1,695 +0,0 @@
1
- """
2
- Class which manages a Pantograph instance. All calls to the kernel uses this
3
- interface.
4
- """
5
- from .message import (
6
- Position,
7
- Severity,
8
- Message,
9
- TacticFailure,
10
- ParseError,
11
- ServerError,
12
- )
13
- from .expr import (
14
- parse_expr,
15
- Expr,
16
- Variable,
17
- Goal,
18
- GoalState,
19
- Site,
20
- Subsumption,
21
- Tactic,
22
- TacticHave,
23
- TacticLet,
24
- TacticMode,
25
- TacticExpr,
26
- TacticDraft,
27
- )
28
- from .utils import (
29
- to_sync,
30
- _get_proc_cwd,
31
- _get_proc_path,
32
- get_lean_path_async,
33
- get_lean_path,
34
- )
35
- from .data import (
36
- CheckTrackResult,
37
- CompilationUnit,
38
- SearchTarget,
39
- )
40
-
41
- import json, os, asyncio, tempfile
42
- from typing import Union, List, Optional, Dict, List, Any, Tuple
43
- from pathlib import Path
44
-
45
- class Server:
46
- """
47
- Main interaction instance with Pantograph.
48
-
49
- Asynchronous and synchronous versions are provided for each function.
50
- """
51
-
52
- def __init__(
53
- self,
54
- imports: List[str]=["Init"],
55
- project_path: Optional[str]=None,
56
- lean_path: Optional[str]=None,
57
- # Options for executing the REPL.
58
- options: Dict[str, Any]={},
59
- # Options supplied to the Lean core
60
- core_options: List[str]=[],
61
- timeout: int=60,
62
- buffer_limit: Optional[int]=1000000,
63
- _sync_init: bool=True):
64
- """
65
- options: Given to Pantograph
66
- core_options: Given to Lean core
67
- timeout: Amount of time to wait for execution (in seconds)
68
- maxread: Maximum number of characters to read (especially important for large proofs and catalogs)
69
- """
70
- self.timeout = timeout
71
- self.imports = imports
72
- self.project_path = project_path if project_path else _get_proc_cwd()
73
- if _sync_init and project_path and not lean_path:
74
- lean_path = get_lean_path(project_path)
75
- self.lean_path = lean_path
76
- self.buffer_limit = buffer_limit
77
- self.proc_path = _get_proc_path()
78
-
79
- self.options = options
80
- self.core_options = core_options
81
- self.args = imports + [f'--{opt}' for opt in core_options]
82
- self.proc = None
83
- if _sync_init:
84
- self.restart()
85
-
86
- # List of goal states that should be garbage collected
87
- self.to_remove_goal_states = []
88
-
89
- @classmethod
90
- async def create(
91
- cls,
92
- imports: List[str]=["Init"],
93
- project_path: Optional[str]=None,
94
- lean_path: Optional[str]=None,
95
- # Options for executing the REPL.
96
- # Set `{ "automaticMode" : False }` to handle resumption by yourself.
97
- options: Dict[str, Any]={},
98
- core_options: List[str]=[],
99
- timeout: int=120,
100
- buffer_limit: Optional[int]=1000000,
101
- start: bool=True) -> 'Server':
102
- """
103
- timeout: Amount of time to wait for execution (in seconds)
104
- maxread: Maximum number of characters to read (especially important for large proofs and catalogs)
105
- """
106
- self = cls(
107
- imports,
108
- project_path,
109
- lean_path,
110
- options,
111
- core_options,
112
- timeout,
113
- buffer_limit,
114
- _sync_init=False
115
- )
116
- if project_path and not lean_path:
117
- lean_path = await get_lean_path_async(project_path)
118
- self.lean_path = lean_path
119
- if start:
120
- await self.restart_async()
121
- return self
122
-
123
- def __enter__(self) -> "Server":
124
- return self
125
-
126
- def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
127
- self._close()
128
-
129
- def __del__(self):
130
- pass #self._close()
131
-
132
- def _close(self):
133
- if not self.proc:
134
- return
135
-
136
- self.proc.terminate()
137
- self.proc = None
138
-
139
- def is_automatic(self):
140
- """
141
- Check if the server is running in automatic mode
142
- """
143
- return self.options.get("automaticMode", True)
144
-
145
- async def restart_async(self):
146
- """
147
- Restart the server
148
- """
149
- self._close()
150
- env = os.environ
151
- if self.lean_path:
152
- env = env | {'LEAN_PATH': self.lean_path}
153
-
154
- self.proc = await asyncio.create_subprocess_exec(
155
- self.proc_path,
156
- *self.args,
157
- stdin=asyncio.subprocess.PIPE,
158
- stdout=asyncio.subprocess.PIPE,
159
- #stderr=asyncio.subprocess.PIPE,
160
- cwd=self.project_path,
161
- env=env,
162
- limit=self.buffer_limit,
163
- )
164
- await self.proc.stdin.drain()
165
- try:
166
- ready = await asyncio.wait_for(self.proc.stdout.readline(), self.timeout)
167
- ready = ready.decode().strip()
168
- assert ready == "ready.", f"Server failed to emit ready signal: {ready}; This could be caused by Lean version mismatch between the project and Pantograph or insufficient timeout."
169
- except asyncio.TimeoutError as ex:
170
- raise RuntimeError("Server failed to emit ready signal in time") from ex
171
-
172
- if self.options:
173
- await self.run_async("options.set", self.options)
174
-
175
- restart = to_sync(restart_async)
176
-
177
- async def run_async(self, cmd, payload):
178
- """
179
- Runs a raw JSON command. Preferably use one of the commands below.
180
-
181
- :meta private:
182
- """
183
- assert self.proc, "Server not running."
184
-
185
- s = json.dumps(payload, ensure_ascii=False)
186
- command = f"{cmd} {s}\n"
187
- self.proc.stdin.write(command.encode())
188
- await self.proc.stdin.drain()
189
- line = ""
190
- try:
191
- line = await asyncio.wait_for(self.proc.stdout.readline(), self.timeout)
192
- except asyncio.TimeoutError as e:
193
- self._close()
194
- raise ServerError("Server reached timeout limit") from e
195
-
196
- try:
197
- line = line.decode().strip()
198
- return json.loads(line)
199
- except UnicodeDecodeError as e:
200
- self._close()
201
- raise ServerError(f"Could not decode process output: {line}") from e
202
- except json.JSONDecodeError as e:
203
- self._close()
204
- raise ServerError(f"Cannot decode Json object from: {line}") from e
205
-
206
- run = to_sync(run_async)
207
-
208
- async def gc_async(self):
209
- """
210
- Garbage collect deleted goal states to free up memory.
211
- """
212
- if not self.to_remove_goal_states:
213
- return
214
- result = await self.run_async('goal.delete', {'stateIds': self.to_remove_goal_states})
215
- self.to_remove_goal_states.clear()
216
- if "error" in result:
217
- raise ServerError(result)
218
-
219
- gc = to_sync(gc_async)
220
-
221
- async def expr_type_async(self, expr: Expr) -> Expr:
222
- """
223
- Evaluate the type of a given expression. This gives an error if the
224
- input `expr` is ill-formed.
225
- """
226
- result = await self.run_async('expr.echo', {"expr": expr})
227
- if "error" in result:
228
- raise ServerError(result)
229
- return parse_expr(result["type"])
230
-
231
- expr_type = to_sync(expr_type_async)
232
-
233
- async def goal_start_async(self, expr: Expr) -> GoalState:
234
- """
235
- Create a goal state with one root goal, whose target is `expr`
236
- """
237
- result = await self.run_async('goal.start', {"expr": str(expr)})
238
- if "error" in result:
239
- print(f"Cannot start goal: {expr}")
240
- raise ServerError(result)
241
- return GoalState(
242
- state_id=result["stateId"],
243
- goals=[Goal.sentence(result["root"], expr)],
244
- messages=[],
245
- _sentinel=self.to_remove_goal_states,
246
- )
247
-
248
- goal_start = to_sync(goal_start_async)
249
-
250
- async def goal_root_async(self, state: GoalState) -> Optional[Expr]:
251
- """
252
- Print the root expression of a goal state
253
- """
254
- args = {"stateId": state.state_id, "rootExpr": True}
255
- result = await self.run_async('goal.print', args)
256
- if "error" in result:
257
- raise ServerError(result)
258
- root = result.get('root')
259
- if root is None:
260
- return None
261
- return parse_expr(root)
262
-
263
- goal_root = to_sync(goal_root_async)
264
-
265
- async def goal_tactic_async(self, state: GoalState, tactic: Tactic, site: Site = Site()) -> GoalState:
266
- """
267
- Execute a tactic on `goal_id` of `state`
268
- """
269
- args = {"stateId": state.state_id, **site.serial()}
270
- match tactic:
271
- case str():
272
- args["tactic"] = tactic
273
- case TacticHave():
274
- args["have"] = tactic.branch
275
- if tactic.binder_name:
276
- args["binderName"] = tactic.binder_name
277
- case TacticLet():
278
- args["let"] = tactic.branch
279
- if tactic.binder_name:
280
- args["binderName"] = tactic.binder_name
281
- case TacticExpr():
282
- args["expr"] = tactic.expr
283
- case TacticDraft():
284
- args["draft"] = tactic.expr
285
- case TacticMode():
286
- args["mode"] = tactic.serial()
287
- case _:
288
- raise RuntimeError(f"Invalid tactic type: {type(tactic)}")
289
- result = await self.run_async('goal.tactic', args)
290
- next_state_id = result.get("nextStateId")
291
- if "error" in result:
292
- raise ServerError(result)
293
- if "parseError" in result:
294
- raise TacticFailure(result)
295
-
296
- messages = result.get("messages")
297
- if "goals" not in result:
298
- raise TacticFailure([Message.parse(m) for m in messages])
299
-
300
- if result["hasSorry"]:
301
- await self.run_async('goal.delete', {'stateIds': [next_state_id]})
302
- raise TacticFailure("Tactic generated sorry", messages)
303
- if result["hasUnsafe"]:
304
- await self.run_async('goal.delete', {'stateIds': [next_state_id]})
305
- raise TacticFailure("Tactic generated unsafe", messages)
306
-
307
- return GoalState.parse(result, messages, self.to_remove_goal_states)
308
-
309
- goal_tactic = to_sync(goal_tactic_async)
310
-
311
- async def goal_continue_async(self, target: GoalState, branch: GoalState) -> GoalState:
312
- """
313
- After finish searching `target`, resume search on `branch`
314
- """
315
- result = await self.run_async('goal.continue', {
316
- "target": target.state_id,
317
- "branch": branch.state_id,
318
- })
319
- if "error" in result:
320
- raise ServerError(result)
321
- if "tacticErrors" in result:
322
- raise ServerError(result)
323
- if "parseError" in result:
324
- raise ServerError(result)
325
- return GoalState.parse(result, [], self.to_remove_goal_states)
326
-
327
- goal_continue = to_sync(goal_continue_async)
328
-
329
- async def goal_resume_async(self, state: GoalState, goals: list[Goal]) -> GoalState:
330
- """
331
- Bring `goals` back into scope
332
- """
333
- result = await self.run_async('goal.continue', {
334
- "target": state.state_id,
335
- "goals": [goal.name for goal in goals],
336
- })
337
- if "error" in result:
338
- raise ServerError(result)
339
- if "tacticErrors" in result:
340
- raise ServerError(result)
341
- if "parseError" in result:
342
- raise ServerError(result)
343
- return GoalState.parse(result, [], self.to_remove_goal_states)
344
- goal_resume = to_sync(goal_resume_async)
345
-
346
- async def goal_subsume_async(
347
- self,
348
- state: GoalState,
349
- goal: Goal,
350
- candidates: list[Goal],
351
- src_state: Optional[GoalState]=None
352
- ) -> (Subsumption, Optional[GoalState], Optional[Goal]):
353
- """
354
- Detect subsumption by candidate goals
355
-
356
- The candidate goals must all exist in `src_state`. If `src_state` is not
357
- provided, they must exist in `state`. Returns a new goal state if a
358
- subsumption does not lead to a cycle, and the subsumptor if there is any
359
- subsumption happening.
360
- """
361
- args = {
362
- "stateId": state.state_id,
363
- "goal": goal.id,
364
- "candidates": [g.id for g in candidates],
365
- }
366
- if src_state:
367
- args["srcStateId"] = src_state.state_id
368
- result = await self.run_async('goal.subsume', args)
369
- if "error" in result:
370
- raise ServerError(result)
371
- nextState = None
372
- if state_id := result.get("stateId"):
373
- nextState = GoalState(
374
- state_id=state_id,
375
- goals=[g for g in state.goals if g.id != goal.id],
376
- messages=[],
377
- _sentinel=self.to_remove_goal_states,
378
- )
379
- sub = Subsumption[result["result"].upper()]
380
- subsumptor = None
381
- if subsumptor := result.get("subsumptor"):
382
- gen = (g for g in candidates if g.id == subsumptor)
383
- subsumptor = next(gen)
384
- if subsumptor is None:
385
- raise ServerError("Subsumptor should not be none")
386
- return (sub, nextState, subsumptor)
387
-
388
- goal_subsume = to_sync(goal_subsume_async)
389
-
390
- async def env_add_async(
391
- self, name: str, levels: list[str],
392
- t: Expr, v: Expr, is_theorem: bool = True):
393
- """
394
- Adds a definition to the environment.
395
-
396
- NOTE: May have to accept additional parameters if the definition
397
- contains universe mvars.
398
- """
399
- result = await self.run_async('env.add', {
400
- "name": name,
401
- "levels": levels,
402
- "type": t,
403
- "value": v,
404
- "isTheorem": is_theorem,
405
- "typeErrorsAsGoals": False,
406
- })
407
- if "error" in result:
408
- raise ServerError(result["desc"])
409
-
410
- env_add = to_sync(env_add_async)
411
-
412
- async def env_catalog_async(
413
- self,
414
- module_prefix: str | None = None,
415
- invert_filter: bool = False
416
- ) -> list[str]:
417
- """
418
- Print all symbols in environment.
419
- """
420
- with tempfile.NamedTemporaryFile('r') as tmp_file:
421
- result = await self.run_async('env.catalog', {
422
- 'filename': tmp_file.name,
423
- 'modulePrefix': module_prefix,
424
- 'invertFilter': invert_filter,
425
- })
426
- if "error" in result:
427
- raise ServerError(result["desc"])
428
- return [line.strip() for line in tmp_file.readlines()]
429
- env_catalog = to_sync(env_catalog_async)
430
-
431
- async def env_inspect_async(
432
- self,
433
- name: str,
434
- print_value: bool = False,
435
- print_dependency: bool = False) -> Dict:
436
- """
437
- Print the type and dependencies of a constant.
438
- """
439
- result = await self.run_async('env.inspect', {
440
- "name": name,
441
- "value": print_value,
442
- "dependency": print_dependency,
443
- "source": True,
444
- })
445
- if "error" in result:
446
- raise ServerError(result["desc"])
447
- return result
448
- env_inspect = to_sync(env_inspect_async)
449
-
450
- async def env_module_read_async(self, module: str) -> dict:
451
- """
452
- Reads the content from one Lean module including what constants are in
453
- it.
454
- """
455
- result = await self.run_async('env.module_read', {
456
- "module": module
457
- })
458
- if "error" in result:
459
- raise ServerError(result["desc"])
460
- return result
461
- env_module_read = to_sync(env_module_read_async)
462
-
463
- async def env_parse_async(self, src: str, category: str = "tactic") -> Tuple[str, str]:
464
- """
465
- Parse an input using a syntax category's parser. Returns the parsed
466
- component and the tail.
467
- """
468
- result = await self.run_async('env.parse', {
469
- "input": src,
470
- "category": category,
471
- })
472
- if "error" in result:
473
- if result['error'] == 'parse':
474
- raise ParseError(result["desc"])
475
- raise ServerError(result["desc"])
476
- pos = result["pos"]
477
- s = src.encode()
478
- return s[:pos].decode(), s[pos:].decode()
479
-
480
- env_parse = to_sync(env_parse_async)
481
-
482
- async def env_save_async(self, path: str):
483
- """
484
- Save the current environment to a file
485
- """
486
- result = await self.run_async('env.save', {
487
- "path": os.path.abspath(path),
488
- })
489
- if "error" in result:
490
- raise ServerError(result["desc"])
491
- env_save = to_sync(env_save_async)
492
-
493
- async def env_load_async(self, path: str):
494
- """
495
- Load the current environment from a file
496
- """
497
- result = await self.run_async('env.load', {
498
- "path": os.path.abspath(path),
499
- })
500
- if "error" in result:
501
- raise ServerError(result["desc"])
502
-
503
- env_load = to_sync(env_load_async)
504
-
505
- async def goal_save_async(self, goal_state: GoalState, path: str):
506
- """
507
- Save a goal state to a file
508
- """
509
- result = await self.run_async('goal.save', {
510
- "id": goal_state.state_id,
511
- "path": os.path.abspath(path),
512
- })
513
- if "error" in result:
514
- raise ServerError(result["desc"])
515
-
516
- goal_save = to_sync(goal_save_async)
517
-
518
- async def goal_load_async(self, path: str) -> GoalState:
519
- """
520
- Load a goal state from a file.
521
-
522
- User is responsible for keeping track of the environment.
523
- """
524
- result = await self.run_async('goal.load', {
525
- "path": os.path.abspath(path),
526
- })
527
- if "error" in result:
528
- raise ServerError(result["desc"])
529
- state_id = result['id']
530
- result = await self.run_async('goal.print', {
531
- 'stateId': state_id,
532
- 'goals': True,
533
- })
534
- if "error" in result:
535
- raise ServerError(result["desc"])
536
- return GoalState.parse_inner(
537
- state_id,
538
- result['goals'], [],
539
- self.to_remove_goal_states,
540
- )
541
-
542
- goal_load = to_sync(goal_load_async)
543
-
544
- async def tactic_invocations_async(self, file_name: Union[str, Path]) -> list[CompilationUnit]:
545
- """
546
- Collect tactic invocation points in file, and return them.
547
- """
548
- with tempfile.TemporaryDirectory() as tempdirname:
549
- invocation_file_name = f"{tempdirname}/invocations.json"
550
- result = await self.run_async('frontend.process', {
551
- 'fileName': str(file_name),
552
- 'invocations': invocation_file_name,
553
- "readHeader": True,
554
- "inheritEnv": False,
555
- "newConstants": False,
556
- })
557
- if "error" in result:
558
- raise ServerError(result)
559
-
560
- with open(invocation_file_name, "r") as f:
561
- data_units = json.load(f)
562
- units = [
563
- CompilationUnit.parse(payload, invocations=data_unit["invocations"])
564
- for payload, data_unit in zip(result['units'], data_units['units'])
565
- ]
566
- return units
567
-
568
- tactic_invocations = to_sync(tactic_invocations_async)
569
-
570
- async def load_header_async(self, header: str):
571
- """
572
- Loads the environment from a header. Set `imports` to `[]` during
573
- server creation to use this function.
574
- """
575
- result = await self.run_async('frontend.process', {
576
- 'file': header,
577
- "newConstants": False,
578
- "readHeader": True,
579
- "inheritEnv": True,
580
- })
581
- if "error" in result:
582
- raise ServerError(result)
583
-
584
- load_header = to_sync(load_header_async)
585
-
586
- async def load_definitions_async(self, snippet: str):
587
- """
588
- Loads definitions in some Lean code and update the environment.
589
-
590
- Existing goal states will not automatically inherit said definitions.
591
- """
592
- result = await self.run_async('frontend.process', {
593
- 'file': snippet,
594
- "newConstants": False,
595
- "readHeader": False,
596
- "inheritEnv": True,
597
- })
598
- if "error" in result:
599
- raise ServerError(result)
600
-
601
- load_definitions = to_sync(load_definitions_async)
602
-
603
- async def check_compile_async(
604
- self,
605
- code: str,
606
- new_constants: bool = False,
607
- read_header: bool = False):
608
- """
609
- Check if some Lean code compiles
610
- """
611
- result = await self.run_async('frontend.process', {
612
- 'file': code,
613
- "newConstants": new_constants,
614
- "readHeader": read_header,
615
- "inheritEnv": False,
616
- })
617
- if "error" in result:
618
- raise ServerError(result)
619
- units = [
620
- CompilationUnit.parse(payload, goal_state_sentinel=self.to_remove_goal_states)
621
- for payload in result['units']
622
- ]
623
- return units
624
-
625
- check_compile = to_sync(check_compile_async)
626
-
627
- async def load_sorry_async(
628
- self,
629
- src: str,
630
- binder_name: Optional[str] = None,
631
- ignore_values: bool = False) -> list[SearchTarget]:
632
- """
633
- Condense search target into goals
634
- """
635
- args = {"file": src, "ignoreValues": ignore_values}
636
- if binder_name is not None:
637
- args["binderName"] = binder_name
638
- result = await self.run_async('frontend.distil', args)
639
- if "error" in result:
640
- raise ServerError(result)
641
- units = [
642
- SearchTarget.parse(payload, goal_state_sentinel=self.to_remove_goal_states)
643
- for payload in result['targets']
644
- ]
645
- return units
646
-
647
- load_sorry = to_sync(load_sorry_async)
648
-
649
- async def check_track_async(self, src: str, dst: str) -> CheckTrackResult:
650
- """
651
- Checks if `dst` file conforms to the specifications in `src`
652
- """
653
- result = await self.run_async('frontend.track', {"src": src, "dst": dst})
654
- if "error" in result:
655
- raise ServerError(result)
656
- src_messages = [Message.parse(d) for d in result["srcMessages"]]
657
- dst_messages = [Message.parse(d) for d in result["dstMessages"]]
658
- return CheckTrackResult(
659
- src_messages,
660
- dst_messages,
661
- failure=result.get("failure"),
662
- )
663
-
664
- check_track = to_sync(check_track_async)
665
-
666
- async def refactor_search_target_async(
667
- self,
668
- code: str,
669
- core_options: list[str] = []) -> str:
670
- """
671
- Combine multiple `sorry`s into one `sorry` using subtyping. It only
672
- supports flat dependency structures.
673
-
674
- This feature is experimental and depends on the round-trip capabilities
675
- of the delaborator.
676
- """
677
- result = await self.run_async('frontend.refactor', {
678
- 'file': code,
679
- 'coreOptions': core_options,
680
- })
681
- if "error" in result:
682
- raise ServerError(result)
683
- return result["file"]
684
-
685
- refactor_search_target = to_sync(refactor_search_target_async)
686
-
687
- def get_version() -> str:
688
- """
689
- Returns the current Pantograph version for diagnostics purposes.
690
- """
691
- import subprocess
692
- with subprocess.Popen([_get_proc_path(), "--version"],
693
- stdout=subprocess.PIPE,
694
- cwd=_get_proc_cwd()) as p:
695
- return p.communicate()[0].decode('utf-8').strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
PyPantograph/pantograph/test_message.py DELETED
@@ -1,25 +0,0 @@
1
- from .message import *
2
- import unittest
3
-
4
- class TestMessage(unittest.TestCase):
5
-
6
- def test_severity(self):
7
- self.assertEqual(str(Severity.ERROR), "error")
8
- def test_message(self):
9
- m = Message(
10
- data="hi",
11
- pos=Position(12, 34),
12
- pos_end=Position(56, 78),
13
- severity=Severity.WARNING,
14
- )
15
- self.assertEqual(str(m), "12:34-56:78: warning: hi")
16
-
17
- m = Message(
18
- data="hi",
19
- pos=Position(0, 0),
20
- severity=Severity.ERROR,
21
- )
22
- self.assertEqual(str(m), "0:0: error: hi")
23
-
24
- if __name__ == '__main__':
25
- unittest.main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
PyPantograph/pantograph/test_search.py DELETED
@@ -1,55 +0,0 @@
1
- from .search import *
2
- import unittest
3
-
4
- class TestSearch(unittest.TestCase):
5
-
6
- def test_solve(self):
7
-
8
- server = Server()
9
- agent = DumbAgent()
10
- goal_state = server.goal_start("βˆ€ (p q: Prop), p -> p")
11
- flag = agent.search(
12
- server=server,
13
- goal_state=goal_state,
14
- verbose=False)
15
- #flag = agent.search(server=server, target="βˆ€ (p q: Prop), Or p q -> Or q p", verbose=True)
16
- self.assertTrue(flag)
17
- def test_solve_big(self):
18
-
19
- server = Server()
20
- agent = DumbAgent()
21
- goal_state = server.goal_start("βˆ€ (p q: Prop), Or p q -> Or q p")
22
- flag = agent.search(
23
- server=server,
24
- goal_state=goal_state,
25
- verbose=False)
26
- self.assertTrue(flag)
27
-
28
- class TestMCTSSearch(unittest.TestCase):
29
-
30
- def test_solve(self):
31
-
32
- server = Server()
33
- agent = DumbMCTSAgent()
34
- goal_state = server.goal_start("βˆ€ (p q: Prop), p -> p")
35
- flag = agent.search(
36
- server=server,
37
- goal_state=goal_state,
38
- verbose=False)
39
- #flag = agent.search(server=server, target="βˆ€ (p q: Prop), Or p q -> Or q p", verbose=True)
40
- self.assertTrue(flag)
41
- def test_solve_big(self):
42
-
43
- server = Server()
44
- agent = DumbMCTSAgent()
45
- goal_state = server.goal_start("βˆ€ (p q: Prop), Or p q -> Or q p")
46
- flag = agent.search(
47
- server=server,
48
- goal_state=goal_state,
49
- max_steps=200,
50
- verbose=False)
51
- self.assertTrue(flag)
52
-
53
-
54
- if __name__ == '__main__':
55
- unittest.main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
PyPantograph/pantograph/test_server.py DELETED
@@ -1,381 +0,0 @@
1
- from .server import *
2
- import unittest
3
-
4
- class TestServer(unittest.TestCase):
5
-
6
- def test_version(self):
7
- """
8
- NOTE: Update this after upstream updates.
9
- """
10
- self.assertEqual(get_version(), "0.3.10")
11
-
12
- def test_server_init_del(self):
13
- import warnings
14
- with warnings.catch_warnings():
15
- warnings.simplefilter("error", ResourceWarning)
16
- server = Server()
17
- server.expr_type("forall (n m: Nat), n + m = m + n")
18
- del server
19
- server = Server()
20
- server.expr_type("forall (n m: Nat), n + m = m + n")
21
- del server
22
- server = Server()
23
- server.expr_type("forall (n m: Nat), n + m = m + n")
24
- del server
25
-
26
- def test_expr_type(self):
27
- server = Server()
28
- t = server.expr_type("forall (n m: Nat), n + m = m + n")
29
- self.assertEqual(t, "Prop")
30
-
31
- def test_goal_start(self):
32
- server = Server()
33
- state0 = server.goal_start("forall (p q: Prop), Or p q -> Or q p")
34
- self.assertEqual(len(server.to_remove_goal_states), 0)
35
- self.assertEqual(state0.state_id, 0)
36
- state1 = server.goal_tactic(state0, tactic="intro a")
37
- self.assertEqual(state1.state_id, 1)
38
- self.assertEqual(state1.goals, [Goal(
39
- "_uniq.11",
40
- variables=[Variable(name="a", t="Prop")],
41
- target="βˆ€ (q : Prop), a ∨ q β†’ q ∨ a",
42
- name=None,
43
- )])
44
- self.assertEqual(str(state1.goals[0]),"a : Prop\n⊒ βˆ€ (q : Prop), a ∨ q β†’ q ∨ a")
45
-
46
- del state0
47
- self.assertEqual(len(server.to_remove_goal_states), 1)
48
- server.gc()
49
- self.assertEqual(len(server.to_remove_goal_states), 0)
50
-
51
- state0b = server.goal_start("forall (p: Prop), p -> p")
52
- del state0b
53
- self.assertEqual(len(server.to_remove_goal_states), 1)
54
- server.gc()
55
- self.assertEqual(len(server.to_remove_goal_states), 0)
56
-
57
- def test_goal_root(self):
58
- server = Server()
59
- state0 = server.goal_start("forall (p: Prop), p -> p")
60
- e = server.goal_root(state0)
61
- self.assertEqual(e, None)
62
- state1 = server.goal_tactic(state0, tactic="exact fun z p => p")
63
- e = server.goal_root(state1)
64
- self.assertEqual(e, "fun z p => p")
65
-
66
- def test_automatic_mode(self):
67
- server = Server()
68
- state0 = server.goal_start("forall (p q: Prop), Or p q -> Or q p")
69
- self.assertEqual(len(server.to_remove_goal_states), 0)
70
- self.assertEqual(state0.state_id, 0)
71
- state1 = server.goal_tactic(state0, tactic="intro a b h")
72
- self.assertEqual(state1.state_id, 1)
73
- self.assertEqual(state1.goals, [Goal(
74
- "_uniq.17",
75
- variables=[
76
- Variable(name="a", t="Prop"),
77
- Variable(name="b", t="Prop"),
78
- Variable(name="h", t="a ∨ b"),
79
- ],
80
- target="b ∨ a",
81
- name=None,
82
- )])
83
- state2 = server.goal_tactic(state1, tactic="cases h")
84
- self.assertEqual(state2.goals, [
85
- Goal(
86
- "_uniq.57",
87
- variables=[
88
- Variable(name="a", t="Prop"),
89
- Variable(name="b", t="Prop"),
90
- Variable(name="h✝", t="a"),
91
- ],
92
- target="b ∨ a",
93
- name="inl",
94
- ),
95
- Goal(
96
- "_uniq.70",
97
- variables=[
98
- Variable(name="a", t="Prop"),
99
- Variable(name="b", t="Prop"),
100
- Variable(name="h✝", t="b"),
101
- ],
102
- target="b ∨ a",
103
- name="inr",
104
- ),
105
- ])
106
- state3 = server.goal_tactic(state2, tactic="apply Or.inl", site=Site(goal_id=1))
107
- state4 = server.goal_tactic(state3, tactic="assumption")
108
- self.assertEqual(state4.goals, [
109
- Goal(
110
- "_uniq.57",
111
- variables=[
112
- Variable(name="a", t="Prop"),
113
- Variable(name="b", t="Prop"),
114
- Variable(name="h✝", t="a"),
115
- ],
116
- target="b ∨ a",
117
- name="inl",
118
- )
119
- ])
120
-
121
- def test_have(self):
122
- server = Server()
123
- state0 = server.goal_start("1 + 1 = 2")
124
- state1 = server.goal_tactic(state0, tactic=TacticHave(branch="2 = 1 + 1", binder_name="h"))
125
- self.assertEqual(state1.goals, [
126
- Goal(
127
- "_uniq.251",
128
- variables=[],
129
- target="2 = 1 + 1",
130
- ),
131
- Goal(
132
- "_uniq.253",
133
- variables=[Variable(name="h", t="2 = 1 + 1")],
134
- target="1 + 1 = 2",
135
- ),
136
- ])
137
- def test_let(self):
138
- server = Server()
139
- state0 = server.goal_start("1 + 1 = 2")
140
- state1 = server.goal_tactic(
141
- state0, tactic=TacticLet(branch="2 = 1 + 1", binder_name="h"))
142
- self.assertEqual(state1.goals, [
143
- Goal(
144
- "_uniq.251",
145
- variables=[],
146
- name="h",
147
- target="2 = 1 + 1",
148
- ),
149
- Goal(
150
- "_uniq.253",
151
- variables=[Variable(name="h", t="2 = 1 + 1", v="?h")],
152
- target="1 + 1 = 2",
153
- ),
154
- ])
155
-
156
- def test_conv_calc(self):
157
- server = Server(options={"automaticMode": False})
158
- state0 = server.goal_start("βˆ€ (a b: Nat), (b = 2) -> 1 + a + 1 = a + b")
159
-
160
- variables = [
161
- Variable(name="a", t="Nat"),
162
- Variable(name="b", t="Nat"),
163
- Variable(name="h", t="b = 2"),
164
- ]
165
- state1 = server.goal_tactic(state0, "intro a b h")
166
- state1b = server.goal_tactic(state1, TacticMode.CALC)
167
- state2 = server.goal_tactic(state1b, "1 + a + 1 = a + 1 + 1")
168
- self.assertEqual(state2.goals, [
169
- Goal(
170
- "_uniq.342",
171
- variables,
172
- target="1 + a + 1 = a + 1 + 1",
173
- name='calc',
174
- ),
175
- Goal(
176
- "_uniq.356",
177
- variables,
178
- target="a + 1 + 1 = a + b",
179
- mode=TacticMode.CALC,
180
- ),
181
- ])
182
- state_c1 = server.goal_tactic(state2, TacticMode.CONV)
183
- state_c2 = server.goal_tactic(state_c1, "rhs")
184
- state_c3 = server.goal_tactic(state_c2, "rw [Nat.add_comm]")
185
- state_c4 = server.goal_tactic(state_c3, TacticMode.TACTIC)
186
- #state_c4b = server.goal_resume(state_c4, [state2.goals[0]])
187
- state_c5 = server.goal_tactic(state_c4, "rfl")
188
- self.assertTrue(state_c5.is_solved)
189
-
190
- state3 = server.goal_tactic(state2, "_ = a + 2", site=Site(1))
191
- state4 = server.goal_tactic(state3, "rw [Nat.add_assoc]")
192
- self.assertTrue(state4.is_solved)
193
-
194
- def test_dependent_mvars(self):
195
- server = Server(options={"printDependentMVars": True})
196
- state = server.goal_start("βˆƒ (x : Nat), x + 1 = 0")
197
- state = server.goal_tactic(state, "apply Exists.intro")
198
- self.assertEqual(state.goals[0].sibling_dep, {1})
199
- self.assertEqual(state.goals[1].sibling_dep, set())
200
-
201
- def test_subsume(self):
202
- server = Server()
203
- state0 = server.goal_start("forall (p : Prop), p -> p")
204
- state1 = server.goal_tactic(state0, "intro p")
205
- state2 = server.goal_tactic(state1, "intro h")
206
- state3 = server.goal_tactic(state2, "revert h")
207
- src = state1.goals[0]
208
- (sub, state, subsumptor) = server.goal_subsume(
209
- state3,
210
- state3.goals[0],
211
- [state1.goals[0], state2.goals[0]],
212
- )
213
- self.assertEqual(sub, Subsumption.CYCLE)
214
- self.assertEqual(state, None)
215
- self.assertEqual(subsumptor, src)
216
-
217
- def test_env_add_inspect(self):
218
- server = Server()
219
- server.env_add(
220
- name="mystery",
221
- levels=[],
222
- t="forall (n: Nat), Nat",
223
- v="fun (n: Nat) => n + 1",
224
- is_theorem=False,
225
- )
226
- inspect_result = server.env_inspect(name="mystery")
227
- self.assertEqual(inspect_result['type'], {'pp': 'Nat β†’ Nat'})
228
-
229
- def test_env_catalog(self):
230
- server = Server()
231
- server.load_definitions("def foo: Nat -> Nat | 0 => 1 | n + 1 => foo n")
232
- definitions = server.env_catalog(module_prefix="Init", invert_filter=True)
233
- self.assertEqual(
234
- set(definitions),
235
- {'dfoo._sunfold', 'dfoo._unsafe_rec', 'dfoo', 'dfoo.match_1'}
236
- )
237
-
238
- def test_env_parse(self):
239
- server = Server()
240
- head, tail = server.env_parse("intro x; apply a", category="tactic")
241
- self.assertEqual(head, "intro x")
242
- self.assertEqual(tail, "; apply a")
243
-
244
- def test_goal_state_pickling(self):
245
- import tempfile
246
- server = Server()
247
- state0 = server.goal_start("forall (p q: Prop), Or p q -> Or q p")
248
- with tempfile.TemporaryDirectory() as td:
249
- path = td + "/goal-state.pickle"
250
- server.goal_save(state0, path)
251
- state0b = server.goal_load(path)
252
- self.assertEqual(state0b.goals, [
253
- Goal(
254
- "_uniq.9",
255
- variables=[
256
- ],
257
- target="βˆ€ (p q : Prop), p ∨ q β†’ q ∨ p",
258
- )
259
- ])
260
-
261
- def test_load_header(self):
262
- server = Server(imports=[])
263
- server.load_header("import Init\nopen Nat")
264
- state0 = server.goal_start("forall (n : Nat), n + 1 = n.succ")
265
- state1 = server.goal_tactic(state0, "intro")
266
- state2 = server.goal_tactic(state1, "apply add_one")
267
- self.assertTrue(state2.is_solved)
268
-
269
- def test_check_compile(self):
270
- server = Server()
271
- unit, = server.check_compile("example (p: Prop) : p -> p := id")
272
- self.assertEqual(unit.messages, [])
273
- unit, = server.check_compile("example (p: Prop) : p -> p := 1")
274
- self.assertEqual(unit.messages, [Message(
275
- pos=Position(1, 30),
276
- pos_end=Position(1, 31),
277
- data=
278
- "numerals are data in Lean, but the expected type is "
279
- "a proposition\n"
280
- " p β†’ p : Prop"
281
- )
282
- ])
283
- unit, = server.check_compile("import Lean\nexample (p: Prop) : p -> p := id", read_header=True)
284
- self.assertEqual(unit.messages, [])
285
-
286
- def test_load_definitions(self):
287
- server = Server()
288
- server.load_definitions(
289
- "def mystery (x : Nat) := x + 123"
290
- )
291
- inspect_result = server.env_inspect(name="mystery")
292
- self.assertEqual(inspect_result['type'], {'pp': 'Nat β†’ Nat'})
293
-
294
- def test_load_sorry(self):
295
- server = Server()
296
- unit, = server.load_sorry("theorem mystery (p: Prop) : p β†’ p := sorry")
297
- #self.assertIsNotNone(unit.goal_state, f"{unit.messages}")
298
- state0 = unit.goal_state
299
- self.assertEqual(state0.goals, [
300
- Goal(
301
- "_uniq.5",
302
- [Variable(name="p", t="Prop")],
303
- target="p β†’ p",
304
- ),
305
- ])
306
- state1 = server.goal_tactic(state0, tactic="intro h")
307
- state2 = server.goal_tactic(state1, tactic="exact h")
308
- self.assertTrue(state2.is_solved)
309
-
310
- state1b = server.goal_tactic(state0, tactic=TacticDraft("by\nhave h1 : Or p p := sorry\nsorry"))
311
- self.assertEqual(state1b.goals, [
312
- Goal(
313
- "_uniq.19",
314
- [Variable(name="p", t="Prop")],
315
- target="p ∨ p",
316
- ),
317
- Goal(
318
- "_uniq.21",
319
- [
320
- Variable(name="p", t="Prop"),
321
- Variable(name="h1", t="p ∨ p", v="?m.7"),
322
- ],
323
- target="p β†’ p",
324
- ),
325
- ])
326
-
327
- def test_distil_search_target(self):
328
- server = Server()
329
- unit, = server.load_sorry("theorem mystery (p: Prop) : p β†’ p := sorry", ignore_values = True)
330
- state0 = unit.goal_state
331
- self.assertEqual(state0.goals, [
332
- Goal(
333
- "_uniq.3",
334
- [],
335
- target="βˆ€ (p : Prop), p β†’ p",
336
- ),
337
- ])
338
- state1 = server.goal_tactic(state0, tactic="intro p h")
339
- state2 = server.goal_tactic(state1, tactic="exact h")
340
- self.assertTrue(state2.is_solved)
341
-
342
- def test_distil_coupled(self):
343
- server = Server()
344
- code = """
345
- def f : Nat -> Nat := sorry
346
- theorem property (n : Nat) : f n = n := sorry"""
347
- unit, = server.load_sorry(code, ignore_values=False)
348
- state0 = unit.goal_state
349
- self.assertEqual(state0.goals, [
350
- Goal(
351
- "_uniq.7",
352
- [],
353
- name='f',
354
- target="Nat β†’ Nat",
355
- ),
356
- Goal(
357
- "_uniq.10",
358
- [Variable(name='n', t='Nat')],
359
- target="?f n = n",
360
- ),
361
- ])
362
-
363
- def test_check_track(self):
364
- server = Server()
365
- src = "def f : Nat -> Nat := sorry"
366
- dst = "def f : Nat -> Nat := fun y => y + y"
367
- self.assertTrue(server.check_track(src, dst).succeeded)
368
-
369
- def test_refactor_search_target(self):
370
- code = """
371
- def f : Nat -> Nat := sorry
372
- theorem property (n : Nat) : f n = n := sorry"""
373
- target = """
374
- def f_composite : { f : Nat β†’ Nat // βˆ€ (n : Nat), f n = n } :=
375
- sorry"""
376
- server = Server()
377
- result = server.refactor_search_target(code)
378
- self.assertEqual(result, target)
379
-
380
- if __name__ == '__main__':
381
- unittest.main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
PyPantograph/pantograph/utils.py DELETED
@@ -1,48 +0,0 @@
1
- import asyncio
2
- from pathlib import Path
3
- import functools as F
4
-
5
- DEFAULT_EVENT_LOOP = asyncio.new_event_loop()
6
-
7
- def get_event_loop():
8
- try:
9
- return asyncio.get_event_loop()
10
- except RuntimeError:
11
- asyncio.set_event_loop(DEFAULT_EVENT_LOOP)
12
- return loop
13
-
14
- def to_sync(func):
15
- loop = get_event_loop()
16
- @F.wraps(func)
17
- def wrapper(*args, **kwargs):
18
- return loop.run_until_complete(func(*args, **kwargs))
19
- return wrapper
20
-
21
- async def check_output(*args, **kwargs):
22
- p = await asyncio.create_subprocess_exec(
23
- *args,
24
- stdout=asyncio.subprocess.PIPE,
25
- stderr=asyncio.subprocess.PIPE,
26
- **kwargs,
27
- )
28
- stdout_data, stderr_data = await p.communicate()
29
- if p.returncode == 0:
30
- return stdout_data
31
-
32
- def _get_proc_cwd():
33
- return Path(__file__).parent
34
-
35
- def _get_proc_path():
36
- return _get_proc_cwd() / "pantograph-repl"
37
-
38
- async def get_lean_path_async(project_path):
39
- """
40
- Extracts the `LEAN_PATH` variable from a project path.
41
- """
42
- p = await check_output(
43
- 'lake', 'env', 'printenv', 'LEAN_PATH',
44
- cwd=project_path,
45
- )
46
- return p
47
-
48
- get_lean_path = to_sync(get_lean_path_async)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
PyPantograph/pyproject.toml DELETED
@@ -1,39 +0,0 @@
1
- [project]
2
- name = "pantograph"
3
- version = "0.3.9"
4
- description = "A machine-to-machine interaction system for Lean"
5
- license = "Apache-2.0"
6
- readme = "README.md"
7
- authors = [
8
- {name = "Leni Aniva", email = "aniva@stanford.edu"},
9
- {name = "contributors"}
10
- ]
11
- requires-python = ">=3.11.0"
12
- dependencies = []
13
- [dependency-groups]
14
- dev = [
15
- "jupyter-book>=1.0.4.post1",
16
- "notebook",
17
- "pytest>=8.4.1",
18
- ]
19
-
20
- [build-system]
21
- requires = ["poetry-core>=2,<3"]
22
- build-backend = "poetry.core.masonry.api"
23
-
24
- [tool.poetry]
25
- include = [
26
- { path = "pantograph/pantograph-repl", format = ["sdist", "wheel"] },
27
- { path = "pantograph/lean-toolchain", format = ["sdist", "wheel"] },
28
- { path = "src", format = ["sdist", "wheel"] },
29
- ]
30
-
31
- [tool.poetry.build]
32
- generate-setup-file = false
33
- script = "build-pantograph.py"
34
-
35
- [tool.poetry.group.doc]
36
- optional = true
37
- [tool.poetry.group.doc.dependencies]
38
- notebook = "^7.2.1"
39
- jupyter-book = "^1.0.3"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
PyPantograph/src/.gitignore DELETED
@@ -1,6 +0,0 @@
1
- .*
2
- !.gitignore
3
- !.pre-commit-config.yaml
4
-
5
- *.[io]lean
6
- /result
 
 
 
 
 
 
 
PyPantograph/src/.pre-commit-config.yaml DELETED
@@ -1,30 +0,0 @@
1
- default_install_hook_types:
2
- - pre-commit
3
- - commit-msg
4
- repos:
5
- - repo: https://github.com/pre-commit/pre-commit-hooks
6
- rev: v6.0.0
7
- hooks:
8
- - id: check-yaml
9
- - id: end-of-file-fixer
10
- - id: trailing-whitespace
11
- - id: check-symlinks
12
- - id: check-added-large-files
13
- - id: check-merge-conflict
14
- - id: detect-private-key
15
- - id: forbid-submodules
16
- - id: no-commit-to-branch
17
- args: [ --branch, main, --branch, dev ]
18
- - repo: https://github.com/compilerla/conventional-pre-commit
19
- rev: v4.3.0
20
- hooks:
21
- - id: conventional-pre-commit
22
- stages: [commit-msg]
23
- args: [feat, fix, doc, chore, test, build, merge, refactor]
24
- - repo: local
25
- hooks:
26
- - id: format nix
27
- name: Format Nix Code
28
- language: system
29
- entry: nix fmt
30
- files: .*\.nix$
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
PyPantograph/src/LICENSE DELETED
@@ -1,190 +0,0 @@
1
- Apache License
2
- Version 2.0, January 2004
3
- http://www.apache.org/licenses/
4
-
5
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
-
7
- 1. Definitions.
8
-
9
- "License" shall mean the terms and conditions for use, reproduction,
10
- and distribution as defined by Sections 1 through 9 of this document.
11
-
12
- "Licensor" shall mean the copyright owner or entity authorized by
13
- the copyright owner that is granting the License.
14
-
15
- "Legal Entity" shall mean the union of the acting entity and all
16
- other entities that control, are controlled by, or are under common
17
- control with that entity. For the purposes of this definition,
18
- "control" means (i) the power, direct or indirect, to cause the
19
- direction or management of such entity, whether by contract or
20
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
- outstanding shares, or (iii) beneficial ownership of such entity.
22
-
23
- "You" (or "Your") shall mean an individual or Legal Entity
24
- exercising permissions granted by this License.
25
-
26
- "Source" form shall mean the preferred form for making modifications,
27
- including but not limited to software source code, documentation
28
- source, and configuration files.
29
-
30
- "Object" form shall mean any form resulting from mechanical
31
- transformation or translation of a Source form, including but
32
- not limited to compiled object code, generated documentation,
33
- and conversions to other media types.
34
-
35
- "Work" shall mean the work of authorship, whether in Source or
36
- Object form, made available under the License, as indicated by a
37
- copyright notice that is included in or attached to the work
38
- (an example is provided in the Appendix below).
39
-
40
- "Derivative Works" shall mean any work, whether in Source or Object
41
- form, that is based on (or derived from) the Work and for which the
42
- editorial revisions, annotations, elaborations, or other modifications
43
- represent, as a whole, an original work of authorship. For the purposes
44
- of this License, Derivative Works shall not include works that remain
45
- separable from, or merely link (or bind by name) to the interfaces of,
46
- the Work and Derivative Works thereof.
47
-
48
- "Contribution" shall mean any work of authorship, including
49
- the original version of the Work and any modifications or additions
50
- to that Work or Derivative Works thereof, that is intentionally
51
- submitted to Licensor for inclusion in the Work by the copyright owner
52
- or by an individual or Legal Entity authorized to submit on behalf of
53
- the copyright owner. For the purposes of this definition, "submitted"
54
- means any form of electronic, verbal, or written communication sent
55
- to the Licensor or its representatives, including but not limited to
56
- communication on electronic mailing lists, source code control systems,
57
- and issue tracking systems that are managed by, or on behalf of, the
58
- Licensor for the purpose of discussing and improving the Work, but
59
- excluding communication that is conspicuously marked or otherwise
60
- designated in writing by the copyright owner as "Not a Contribution."
61
-
62
- "Contributor" shall mean Licensor and any individual or Legal Entity
63
- on behalf of whom a Contribution has been received by Licensor and
64
- subsequently incorporated within the Work.
65
-
66
- 2. Grant of Copyright License. Subject to the terms and conditions of
67
- this License, each Contributor hereby grants to You a perpetual,
68
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
- copyright license to reproduce, prepare Derivative Works of,
70
- publicly display, publicly perform, sublicense, and distribute the
71
- Work and such Derivative Works in Source or Object form.
72
-
73
- 3. Grant of Patent License. Subject to the terms and conditions of
74
- this License, each Contributor hereby grants to You a perpetual,
75
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
- (except as stated in this section) patent license to make, have made,
77
- use, offer to sell, sell, import, and otherwise transfer the Work,
78
- where such license applies only to those patent claims licensable
79
- by such Contributor that are necessarily infringed by their
80
- Contribution(s) alone or by combination of their Contribution(s)
81
- with the Work to which such Contribution(s) was submitted. If You
82
- institute patent litigation against any entity (including a
83
- cross-claim or counterclaim in a lawsuit) alleging that the Work
84
- or a Contribution incorporated within the Work constitutes direct
85
- or contributory patent infringement, then any patent licenses
86
- granted to You under this License for that Work shall terminate
87
- as of the date such litigation is filed.
88
-
89
- 4. Redistribution. You may reproduce and distribute copies of the
90
- Work or Derivative Works thereof in any medium, with or without
91
- modifications, and in Source or Object form, provided that You
92
- meet the following conditions:
93
-
94
- (a) You must give any other recipients of the Work or
95
- Derivative Works a copy of this License; and
96
-
97
- (b) You must cause any modified files to carry prominent notices
98
- stating that You changed the files; and
99
-
100
- (c) You must retain, in the Source form of any Derivative Works
101
- that You distribute, all copyright, patent, trademark, and
102
- attribution notices from the Source form of the Work,
103
- excluding those notices that do not pertain to any part of
104
- the Derivative Works; and
105
-
106
- (d) If the Work includes a "NOTICE" text file as part of its
107
- distribution, then any Derivative Works that You distribute must
108
- include a readable copy of the attribution notices contained
109
- within such NOTICE file, excluding those notices that do not
110
- pertain to any part of the Derivative Works, in at least one
111
- of the following places: within a NOTICE text file distributed
112
- as part of the Derivative Works; within the Source form or
113
- documentation, if provided along with the Derivative Works; or,
114
- within a display generated by the Derivative Works, if and
115
- wherever such third-party notices normally appear. The contents
116
- of the NOTICE file are for informational purposes only and
117
- do not modify the License. You may add Your own attribution
118
- notices within Derivative Works that You distribute, alongside
119
- or as an addendum to the NOTICE text from the Work, provided
120
- that such additional attribution notices cannot be construed
121
- as modifying the License.
122
-
123
- You may add Your own copyright statement to Your modifications and
124
- may provide additional or different license terms and conditions
125
- for use, reproduction, or distribution of Your modifications, or
126
- for any such Derivative Works as a whole, provided Your use,
127
- reproduction, and distribution of the Work otherwise complies with
128
- the conditions stated in this License.
129
-
130
- 5. Submission of Contributions. Unless You explicitly state otherwise,
131
- any Contribution intentionally submitted for inclusion in the Work
132
- by You to the Licensor shall be under the terms and conditions of
133
- this License, without any additional terms or conditions.
134
- Notwithstanding the above, nothing herein shall supersede or modify
135
- the terms of any separate license agreement you may have executed
136
- with Licensor regarding such Contributions.
137
-
138
- 6. Trademarks. This License does not grant permission to use the trade
139
- names, trademarks, service marks, or product names of the Licensor,
140
- except as required for reasonable and customary use in describing the
141
- origin of the Work and reproducing the content of the NOTICE file.
142
-
143
- 7. Disclaimer of Warranty. Unless required by applicable law or
144
- agreed to in writing, Licensor provides the Work (and each
145
- Contributor provides its Contributions) on an "AS IS" BASIS,
146
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
- implied, including, without limitation, any warranties or conditions
148
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
- PARTICULAR PURPOSE. You are solely responsible for determining the
150
- appropriateness of using or redistributing the Work and assume any
151
- risks associated with Your exercise of permissions under this License.
152
-
153
- 8. Limitation of Liability. In no event and under no legal theory,
154
- whether in tort (including negligence), contract, or otherwise,
155
- unless required by applicable law (such as deliberate and grossly
156
- negligent acts) or agreed to in writing, shall any Contributor be
157
- liable to You for damages, including any direct, indirect, special,
158
- incidental, or consequential damages of any character arising as a
159
- result of this License or out of the use or inability to use the
160
- Work (including but not limited to damages for loss of goodwill,
161
- work stoppage, computer failure or malfunction, or any and all
162
- other commercial damages or losses), even if such Contributor
163
- has been advised of the possibility of such damages.
164
-
165
- 9. Accepting Warranty or Additional Liability. While redistributing
166
- the Work or Derivative Works thereof, You may choose to offer,
167
- and charge a fee for, acceptance of support, warranty, indemnity,
168
- or other liability obligations and/or rights consistent with this
169
- License. However, in accepting such obligations, You may act only
170
- on Your own behalf and on Your sole responsibility, not on behalf
171
- of any other Contributor, and only if You agree to indemnify,
172
- defend, and hold each Contributor harmless for any liability
173
- incurred by, or claims asserted against, such Contributor by reason
174
- of your accepting any such warranty or additional liability.
175
-
176
- END OF TERMS AND CONDITIONS
177
-
178
- Copyright 2024 Leni Aniva
179
-
180
- Licensed under the Apache License, Version 2.0 (the "License");
181
- you may not use this file except in compliance with the License.
182
- You may obtain a copy of the License at
183
-
184
- http://www.apache.org/licenses/LICENSE-2.0
185
-
186
- Unless required by applicable law or agreed to in writing, software
187
- distributed under the License is distributed on an "AS IS" BASIS,
188
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
189
- See the License for the specific language governing permissions and
190
- limitations under the License.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
PyPantograph/src/Main.lean DELETED
@@ -1,80 +0,0 @@
1
- import Lean.Data.Json
2
- import Lean.Environment
3
-
4
- import Pantograph
5
- import Repl
6
-
7
- -- Main IO functions
8
- open Pantograph.Repl
9
- open Pantograph.Protocol
10
-
11
- /-- Print a string to stdout without buffering -/
12
- def printImmediate (s : String) : IO Unit := do
13
- let stdout ← IO.getStdout
14
- stdout.putStr (s ++ "\n")
15
- stdout.flush
16
-
17
- /-- Parse a command either in `{ "cmd": ..., "payload": ... }` form or `cmd { ... }` form. -/
18
- def parseCommand (s: String): Except String Command := do
19
- match s.trim.get? 0 with
20
- | .some '{' =>
21
- -- Parse in Json mode
22
- Lean.fromJson? (← Lean.Json.parse s)
23
- | .some _ =>
24
- -- Parse in line mode
25
- let offset := s.posOf ' ' |> s.offsetOfPos
26
- if offset = s.length then
27
- return { cmd := s.take offset, payload := Lean.Json.null }
28
- else
29
- let payload ← s.drop offset |> Lean.Json.parse
30
- return { cmd := s.take offset, payload := payload }
31
- | .none =>
32
- throw "Command is empty"
33
-
34
- partial def loop : MainM Unit := do repeat do
35
- let state ← get
36
- let command ← (← IO.getStdin).getLine
37
- -- Halt the program if empty line is given
38
- if command.trim.length = 0 then break
39
- match parseCommand command with
40
- | .error error =>
41
- let error := Lean.toJson ({ error := "command", desc := error }: InteractionError)
42
- -- Using `Lean.Json.compress` here to prevent newline
43
- printImmediate error.compress
44
- | .ok command =>
45
- try
46
- let ret ← execute command
47
- let str := match state.options.printJsonPretty with
48
- | true => ret.pretty
49
- | false => ret.compress
50
- printImmediate str
51
- catch e =>
52
- let message := e.toString
53
- let error := Lean.toJson ({ error := "main", desc := message }: InteractionError)
54
- printImmediate error.compress
55
-
56
- def main (args: List String): IO Unit := do
57
- -- NOTE: A more sophisticated scheme of command line argument handling is needed.
58
- if args == ["--version"] then do
59
- IO.println s!"{Pantograph.version}"
60
- return
61
-
62
- unsafe do
63
- Pantograph.initSearch
64
-
65
- -- Separate imports and options
66
- let (options, imports) := args.partition (Β·.startsWith "--")
67
- let coreContext ← options.map (Β·.drop 2) |>.toArray |> Pantograph.createCoreContext
68
- let env ← Lean.importModules
69
- (imports := imports.toArray.map ({ module := Β·.toName }))
70
- (opts := {})
71
- (trustLevel := 1)
72
- (loadExts := true)
73
- try
74
- let mainM := loop.run { coreContext } |>.run' { env }
75
- printImmediate "ready."
76
- mainM
77
- catch ex =>
78
- let message := ex.toString
79
- let error := Lean.toJson ({ error := "io", desc := message }: InteractionError)
80
- IO.println error.compress
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
PyPantograph/src/Pantograph.lean DELETED
@@ -1,9 +0,0 @@
1
- import Pantograph.Delate
2
- import Pantograph.Elab
3
- import Pantograph.Environment
4
- import Pantograph.Frontend
5
- import Pantograph.Goal
6
- import Pantograph.Library
7
- import Pantograph.Protocol
8
- import Pantograph.Serial
9
- import Pantograph.Version