├── .github └── workflows │ ├── ci-tests.yml │ ├── package.yml │ └── release.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── coq_jupyter ├── __init__.py ├── __main__.py ├── coqtop.py ├── install.py ├── kernel.js ├── kernel.py └── renderer.py ├── logo.svg ├── main.py ├── process_indexes.py ├── screenshot.png ├── setup.py └── test ├── container_test_entrypoint.sh └── kernel_test.py /.github/workflows/ci-tests.yml: -------------------------------------------------------------------------------- 1 | name: CI Tests 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_call: 7 | workflow_dispatch: 8 | schedule: 9 | - cron: '0 12 * * 1' # At 12:00 on Monday. 10 | 11 | jobs: 12 | package: 13 | uses: ./.github/workflows/package.yml 14 | 15 | test-docker: 16 | name: Test against Coq release (Docker) 17 | needs: package 18 | runs-on: ubuntu-latest 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | coq-version: 23 | - "8.6" 24 | - "8.7" 25 | - "8.8" 26 | - "8.13" 27 | - "8.14" 28 | - "8.15" 29 | - "8.16" 30 | - "8.17" 31 | - "8.18" 32 | - "latest" 33 | steps: 34 | - uses: actions/checkout@v2 35 | 36 | - uses: actions/download-artifact@v2 37 | with: 38 | name: packages 39 | path: dist 40 | 41 | - uses: addnab/docker-run-action@v3 42 | with: 43 | image: coqorg/coq:${{ matrix.coq-version }} 44 | options: -v ${{ github.workspace }}:/github/workspace 45 | run: /github/workspace/test/container_test_entrypoint.sh 46 | 47 | -------------------------------------------------------------------------------- /.github/workflows/package.yml: -------------------------------------------------------------------------------- 1 | name: Package kernel 2 | 3 | on: [workflow_call, workflow_dispatch] 4 | 5 | jobs: 6 | package: 7 | name: Build kernel packages 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | 12 | - uses: conda-incubator/setup-miniconda@v2 13 | with: 14 | auto-update-conda: true 15 | python-version: "3.10" 16 | 17 | - name: Install build dependencies 18 | shell: bash -l {0} 19 | run: | 20 | pip install --upgrade pip setuptools wheel 21 | 22 | - name: Build kernel packages 23 | shell: bash -l {0} 24 | run: | 25 | python setup.py sdist bdist_wheel 26 | 27 | - uses: actions/upload-artifact@v2 28 | with: 29 | name: packages 30 | path: dist/* 31 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | ci-tests: 10 | uses: ./.github/workflows/ci-tests.yml 11 | 12 | release-github: 13 | name: Create Github release 14 | needs: ci-tests 15 | runs-on: ubuntu-latest 16 | permissions: 17 | contents: write 18 | steps: 19 | - uses: actions/download-artifact@v2 20 | with: 21 | name: packages 22 | path: dist 23 | 24 | - uses: ncipollo/release-action@v1 25 | with: 26 | artifacts: "dist/*" 27 | omitBody: true 28 | token: ${{ secrets.GITHUB_TOKEN }} 29 | prerelease: false 30 | draft: false 31 | 32 | release-pypi: 33 | name: Create PyPI release 34 | needs: ci-tests 35 | runs-on: ubuntu-latest 36 | steps: 37 | - uses: actions/download-artifact@v2 38 | with: 39 | name: packages 40 | path: dist 41 | 42 | - uses: pypa/gh-action-pypi-publish@master 43 | with: 44 | password: ${{ secrets.PYPI_API_TOKEN }} 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | # IDE 107 | .idea -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Please use `development` branch to send pull requests. 2 | 3 | --- 4 | 5 | This document assumes that you have `cd`ed into root of the repo and installed the following: 6 | 7 | pip install --upgrade jupyter-console setuptools wheel twine "jupyter_kernel_test>=0.3" 8 | 9 | --- 10 | 11 | To run tests (this also installs kernel from source): 12 | 13 | python setup.py install 14 | python -m coq_jupyter.install 15 | python test/kernel_test.py 16 | 17 | --- 18 | 19 | To run kernel from source (with DEBUG logging level): 20 | 21 | python main.py --ConnectionFileMixin.connection_file=coq_kernel.json --Application.log_level=DEBUG 22 | 23 | ... and then connect to kernel using: 24 | 25 | jupyter console --existing coq_kernel.json 26 | 27 | --- 28 | 29 | To build: 30 | 31 | python setup.py sdist bdist_wheel 32 | 33 | --- 34 | 35 | To publish (to TestPyPI): 36 | 37 | python -m twine upload --repository-url https://test.pypi.org/legacy/ dist/* 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 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 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include coq_jupyter/kernel.js 2 | include LICENSE 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | install: 2 | pip install coq-jupyter 3 | python -m coq_jupyter.install 4 | 5 | install-local: 6 | pip install . 7 | python -m coq_jupyter.install 8 | 9 | uninstall: 10 | jupyter kernelspec uninstall coq 11 | pip uninstall coq_jupyter 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Logo](logo.svg) 2 | 3 | [![StandWithUkraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/badges/StandWithUkraine.svg)](https://github.com/vshymanskyy/StandWithUkraine/blob/main/docs/README.md) [![PyPI version](https://badge.fury.io/py/coq-jupyter.svg)](https://badge.fury.io/py/coq-jupyter) [![CI Tests](https://github.com/EugeneLoy/coq_jupyter/actions/workflows/ci-tests.yml/badge.svg)](https://github.com/EugeneLoy/coq_jupyter/actions/workflows/ci-tests.yml) [![Join the chat at https://gitter.im/coq_jupyter/community](https://badges.gitter.im/coq_jupyter/community.svg)](https://gitter.im/coq_jupyter/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![badge](https://img.shields.io/badge/launch%20demo-binder-579ACA.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFkAAABZCAMAAABi1XidAAAB8lBMVEX///9XmsrmZYH1olJXmsr1olJXmsrmZYH1olJXmsr1olJXmsrmZYH1olL1olJXmsr1olJXmsrmZYH1olL1olJXmsrmZYH1olJXmsr1olL1olJXmsrmZYH1olL1olJXmsrmZYH1olL1olL0nFf1olJXmsrmZYH1olJXmsq8dZb1olJXmsrmZYH1olJXmspXmspXmsr1olL1olJXmsrmZYH1olJXmsr1olL1olJXmsrmZYH1olL1olLeaIVXmsrmZYH1olL1olL1olJXmsrmZYH1olLna31Xmsr1olJXmsr1olJXmsrmZYH1olLqoVr1olJXmsr1olJXmsrmZYH1olL1olKkfaPobXvviGabgadXmsqThKuofKHmZ4Dobnr1olJXmsr1olJXmspXmsr1olJXmsrfZ4TuhWn1olL1olJXmsqBi7X1olJXmspZmslbmMhbmsdemsVfl8ZgmsNim8Jpk8F0m7R4m7F5nLB6jbh7jbiDirOEibOGnKaMhq+PnaCVg6qWg6qegKaff6WhnpKofKGtnomxeZy3noG6dZi+n3vCcpPDcpPGn3bLb4/Mb47UbIrVa4rYoGjdaIbeaIXhoWHmZYHobXvpcHjqdHXreHLroVrsfG/uhGnuh2bwj2Hxk17yl1vzmljzm1j0nlX1olL3AJXWAAAAbXRSTlMAEBAQHx8gICAuLjAwMDw9PUBAQEpQUFBXV1hgYGBkcHBwcXl8gICAgoiIkJCQlJicnJ2goKCmqK+wsLC4usDAwMjP0NDQ1NbW3Nzg4ODi5+3v8PDw8/T09PX29vb39/f5+fr7+/z8/Pz9/v7+zczCxgAABC5JREFUeAHN1ul3k0UUBvCb1CTVpmpaitAGSLSpSuKCLWpbTKNJFGlcSMAFF63iUmRccNG6gLbuxkXU66JAUef/9LSpmXnyLr3T5AO/rzl5zj137p136BISy44fKJXuGN/d19PUfYeO67Znqtf2KH33Id1psXoFdW30sPZ1sMvs2D060AHqws4FHeJojLZqnw53cmfvg+XR8mC0OEjuxrXEkX5ydeVJLVIlV0e10PXk5k7dYeHu7Cj1j+49uKg7uLU61tGLw1lq27ugQYlclHC4bgv7VQ+TAyj5Zc/UjsPvs1sd5cWryWObtvWT2EPa4rtnWW3JkpjggEpbOsPr7F7EyNewtpBIslA7p43HCsnwooXTEc3UmPmCNn5lrqTJxy6nRmcavGZVt/3Da2pD5NHvsOHJCrdc1G2r3DITpU7yic7w/7Rxnjc0kt5GC4djiv2Sz3Fb2iEZg41/ddsFDoyuYrIkmFehz0HR2thPgQqMyQYb2OtB0WxsZ3BeG3+wpRb1vzl2UYBog8FfGhttFKjtAclnZYrRo9ryG9uG/FZQU4AEg8ZE9LjGMzTmqKXPLnlWVnIlQQTvxJf8ip7VgjZjyVPrjw1te5otM7RmP7xm+sK2Gv9I8Gi++BRbEkR9EBw8zRUcKxwp73xkaLiqQb+kGduJTNHG72zcW9LoJgqQxpP3/Tj//c3yB0tqzaml05/+orHLksVO+95kX7/7qgJvnjlrfr2Ggsyx0eoy9uPzN5SPd86aXggOsEKW2Prz7du3VID3/tzs/sSRs2w7ovVHKtjrX2pd7ZMlTxAYfBAL9jiDwfLkq55Tm7ifhMlTGPyCAs7RFRhn47JnlcB9RM5T97ASuZXIcVNuUDIndpDbdsfrqsOppeXl5Y+XVKdjFCTh+zGaVuj0d9zy05PPK3QzBamxdwtTCrzyg/2Rvf2EstUjordGwa/kx9mSJLr8mLLtCW8HHGJc2R5hS219IiF6PnTusOqcMl57gm0Z8kanKMAQg0qSyuZfn7zItsbGyO9QlnxY0eCuD1XL2ys/MsrQhltE7Ug0uFOzufJFE2PxBo/YAx8XPPdDwWN0MrDRYIZF0mSMKCNHgaIVFoBbNoLJ7tEQDKxGF0kcLQimojCZopv0OkNOyWCCg9XMVAi7ARJzQdM2QUh0gmBozjc3Skg6dSBRqDGYSUOu66Zg+I2fNZs/M3/f/Grl/XnyF1Gw3VKCez0PN5IUfFLqvgUN4C0qNqYs5YhPL+aVZYDE4IpUk57oSFnJm4FyCqqOE0jhY2SMyLFoo56zyo6becOS5UVDdj7Vih0zp+tcMhwRpBeLyqtIjlJKAIZSbI8SGSF3k0pA3mR5tHuwPFoa7N7reoq2bqCsAk1HqCu5uvI1n6JuRXI+S1Mco54YmYTwcn6Aeic+kssXi8XpXC4V3t7/ADuTNKaQJdScAAAAAElFTkSuQmCC)](https://mybinder.org/v2/gh/EugeneLoy/coq_jupyter_demo/master?filepath=demo.ipynb) 4 | 5 | A [Jupyter](https://jupyter.org/) kernel for [Coq](https://coq.inria.fr/). 6 | 7 | You can try it [online in Binder](https://mybinder.org/v2/gh/EugeneLoy/coq_jupyter_demo/master?filepath=demo.ipynb). 8 | 9 | ## Installation 10 | 11 | ### Prerequisites 12 | 13 | Make sure that CoqIDE (8.6 or newer) is installed and `coqidetop` or `coqidetop.opt` (`coqtop` for Coq versions before 8.9.0) is in your `PATH`. Also, make sure the `python` command is recognized on your machine. If not, you can set up an alias for it e.g. python-is-python3 on Ubuntu. 14 | 15 | ### Install using pip 16 | 17 | Install with `pip`: 18 | 19 | pip install coq-jupyter 20 | python -m coq_jupyter.install 21 | 22 | Uninstall with `pip`: 23 | 24 | jupyter kernelspec uninstall coq 25 | pip uninstall coq-jupyter 26 | 27 | ### Install using MAKE 28 | 29 | All commands are run from the top level folder of this repo (where `Makefile` is located). 30 | 31 | Install from PyPi: 32 | 33 | make 34 | 35 | Install from locally checked out source code: 36 | 37 | make install-local 38 | 39 | Uninstall: 40 | 41 | make uninstall 42 | 43 | ## Backtracking 44 | 45 | There are number of convenience improvements over standard Jupyter notebook behaviour that are implemented to support Coq-specific use cases. 46 | 47 | By default, running cell will rollback any code that was executed in that cell before. If needed, this can be disabled on a per-cell basis (using `Auto rollback` checkbox). 48 | 49 | Manual cell rollback is also available using `Rollback cell` button (at the bottom of executed cell) or shortcut (`Ctrl+Backspace`). 50 | 51 | ![Backtracking screenshot](screenshot.png) 52 | 53 | ## coqtop arguments 54 | 55 | Use `--coqtop-args` to supply additional arguments to `coqidetop`/`coqidetop.opt`/`coqtop` when installing kernel. In this case you might also want to set custom kernel name/display name using `--kernel-name`/`--kernel-display-name`. 56 | 57 | For example, to add kernel that instructs `coqidetop` to load `/workspace/init.v` on startup: 58 | 59 | python -m coq_jupyter.install --kernel-name=coq_with_init --kernel-display-name="Coq (with init.v)" --coqtop-args="-l /workspace/init.v" 60 | 61 | ## Contributing 62 | 63 | Give feedback with [issues](https://github.com/EugeneLoy/coq_jupyter/issues) or [gitter](https://gitter.im/coq_jupyter/community), send pull requests. Also check out [CONTRIBUTING.md](CONTRIBUTING.md) for instructions. 64 | -------------------------------------------------------------------------------- /coq_jupyter/__init__.py: -------------------------------------------------------------------------------- 1 | """A Coq kernel for Jupyter""" 2 | 3 | from .kernel import __version__ 4 | -------------------------------------------------------------------------------- /coq_jupyter/__main__.py: -------------------------------------------------------------------------------- 1 | from ipykernel.kernelapp import IPKernelApp 2 | from sys import argv 3 | from .kernel import CoqKernel 4 | 5 | IPKernelApp.launch_instance(kernel_class=CoqKernel, args=argv) 6 | -------------------------------------------------------------------------------- /coq_jupyter/coqtop.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import pexpect 4 | import re 5 | import xml.etree.ElementTree as ET 6 | 7 | from future.utils import raise_with_traceback 8 | from collections import deque 9 | from subprocess import check_output 10 | 11 | LANGUAGE_VERSION_PATTERN = re.compile(r'version (\d+(\.\d+)+)') 12 | 13 | SEPARATOR_PATTERN = r"((?:\[\s*[a-zA-Z][a-zA-Z0-9_']*\s*\]\s*\:\s*\{)|(?:\d+\s*\:\s*\{)|(?:\{)|(?:\})|(?:\++)|(?:\-+)|(?:\*+)|(?:\.))" 14 | 15 | REPLY_PATTERNS = [ 16 | re.compile(r'\<{0}.*?\>.+?\<\/{0}\>'.format(t), re.DOTALL) 17 | for t in [ 18 | "feedback", 19 | "value", 20 | "message" # older versions of coqtop wont wrap 'message' inside 'feedback' 21 | ] 22 | ] 23 | 24 | 25 | class CoqtopError(Exception): 26 | pass 27 | 28 | 29 | class Coqtop: 30 | 31 | def __init__(self, kernel, coqtop_executable, coqtop_args): 32 | try: 33 | self.log = kernel.log 34 | 35 | locate_coqtop = coqtop_executable == "" 36 | cmd_candidates = ["coqidetop.opt", "coqidetop", "coqtop"] if locate_coqtop else [coqtop_executable] 37 | for cmd in cmd_candidates: 38 | try: 39 | banner = check_output([cmd, '--version']).decode('utf-8') 40 | break 41 | except OSError: 42 | cmd = None 43 | 44 | if cmd is None: 45 | raise CoqtopError("Failed to run coqtop executable (tried the following: {}).".format(cmd_candidates)) 46 | 47 | self.cmd = cmd 48 | self.banner = banner 49 | self.version = self._parse_version(self.banner) 50 | 51 | if self.cmd.endswith("coqtop") and self.version >= (8, 9, 0): 52 | if locate_coqtop: 53 | raise CoqtopError("Failed to locate 'coqidetop.opt'/'coqidetop' executable ('coqtop' has been found but is insufficient since v8.9)") 54 | else: 55 | raise CoqtopError("'coqidetop.opt'/'coqidetop' executable is required since Coq v8.9") 56 | 57 | # run coqtop executable 58 | spawn_args = { 59 | "echo": False, 60 | "encoding": "utf-8", 61 | "codec_errors": "replace" 62 | } 63 | if self.cmd.endswith("coqidetop.opt") or self.cmd.endswith("coqidetop"): 64 | self._coqtop = pexpect.spawn("{} -main-channel stdfds {}".format(self.cmd, coqtop_args), **spawn_args) 65 | else: 66 | self._coqtop = pexpect.spawn("{} -toploop coqidetop -main-channel stdfds {}".format(self.cmd, coqtop_args), **spawn_args) 67 | 68 | # perform init 69 | (reply, _) = self._execute_command(self._build_init_command()) 70 | self.tip = reply.find("./state_id").get("val") 71 | 72 | except Exception as e: 73 | raise_with_traceback(CoqtopError("Cause: {}".format(repr(e)))) 74 | 75 | def eval(self, code): 76 | try: 77 | tip_before = self.tip 78 | 79 | sentences = deque(self._get_sentences(code)) 80 | 81 | # Attempt to evaluate sentences in code 82 | code_evaluated = True 83 | outputs = [] 84 | while len(sentences) > 0: 85 | sentence = sentences.popleft() 86 | 87 | (add_reply, _) = self._execute_command(self._build_add_command(sentence, self.tip), allow_fail=True) 88 | (status_reply, out_of_band_status_replies) = self._execute_command(self._build_status_command(), allow_fail=True) 89 | 90 | sentence_evaluated = self._is_good(add_reply) and self._is_good(status_reply) 91 | errors = [ 92 | self._get_error_content(r) 93 | for r in [add_reply, status_reply] 94 | if self._has_error(r) 95 | ] 96 | out_of_band_status_messages = [ 97 | self._get_message_content(r) 98 | for r in out_of_band_status_replies 99 | if self._is_message(r) 100 | ] 101 | 102 | if not sentence_evaluated: 103 | # In some cases (for example if there is invalid reference) erroneous command 104 | # can be accepted by parser, increase coqtop tip and fail late. 105 | # To ensure consistent state it is better to roll back to predictable state 106 | self.roll_back_to(self.tip) 107 | 108 | if not sentence_evaluated and len(sentences) > 0: 109 | # Attempt to fix error by joining erroneous sentence with next one. 110 | # This should fix any errors caused by headless splitting 111 | # of code into sentences 112 | sentences.appendleft(sentence + sentences.popleft()) 113 | continue 114 | 115 | if not sentence_evaluated and len(sentences) == 0 and self._is_end_of_input_error(add_reply): 116 | # It is ok to ignore failure and output generated by evaluating 117 | # 'effectively empty' leftover sentence 118 | break 119 | 120 | if not sentence_evaluated and len(sentences) == 0: 121 | # Upon reaching this state it we can definitely say that there is 122 | # an error in cell code 123 | code_evaluated = False 124 | outputs.extend(errors) 125 | break 126 | 127 | self.tip = self._get_next_tip(add_reply) 128 | outputs.extend(out_of_band_status_messages) 129 | 130 | if code_evaluated: 131 | # Get data about theorem being proven 132 | if self._is_proving(status_reply): 133 | outputs.append("Proving: {}".format(self._get_proof_name(status_reply))) 134 | 135 | # Get goal state 136 | (goal_reply, _) = self._execute_command(self._build_goal_command()) 137 | if self._has_goals(goal_reply): 138 | outputs.append(self._get_goals_content(goal_reply)) 139 | else: 140 | # roll back any side effects of code 141 | self.roll_back_to(tip_before) 142 | 143 | return code_evaluated, outputs 144 | 145 | except Exception as e: 146 | raise_with_traceback(CoqtopError("Cause: {}".format(repr(e)))) 147 | 148 | def roll_back_to(self, state_id): 149 | self._execute_command(self._build_edit_at_command(state_id)) 150 | self.tip = state_id 151 | 152 | def _get_sentences(self, code): 153 | parts = re.split(SEPARATOR_PATTERN, code) 154 | pi = iter(parts) 155 | sentences = [p + next(pi, '') for p in pi] 156 | if sentences[-1].strip(" \t\n\r") != "": 157 | return sentences 158 | else: 159 | return sentences[:-1] 160 | 161 | def _execute_command(self, command, allow_fail=False): 162 | self.log.debug("Executing coqtop command: {}".format(repr(command))) 163 | self._coqtop.send(command + "\n") 164 | out_of_band_replies = [] 165 | while True: 166 | self._coqtop.expect(REPLY_PATTERNS) 167 | 168 | if self._coqtop.before.strip(" \t\n\r") != "": 169 | self.log.warning("Skipping unexpected coqtop output: {}".format(repr(self._coqtop.before))) 170 | 171 | reply = self._parse(self._coqtop.match.group(0)) 172 | self.log.debug("Received coqtop reply: {}".format(ET.tostring(reply))) 173 | 174 | if reply.tag == "value" and not allow_fail and not self._is_good(reply): 175 | raise CoqtopError("Unexpected reply: {}".format(ET.tostring(reply))) 176 | elif reply.tag == "value": 177 | return reply, out_of_band_replies 178 | else: 179 | out_of_band_replies.append(reply) 180 | 181 | def _parse_version(self, banner): 182 | version_string = LANGUAGE_VERSION_PATTERN.search(banner).group(1) 183 | version = tuple(map(int, version_string.split("."))) 184 | return tuple(version + (0,) * (3 - len(version))) 185 | 186 | def _parse(self, reply): 187 | return ET.fromstring(reply.replace(" ", " ")) 188 | 189 | def _get_next_tip(self, reply): 190 | state_id = reply.find("[@val='good']/pair/pair/union/state_id") 191 | if state_id is None: 192 | state_id = reply.find("[@val='good']/pair/state_id") 193 | return state_id.get("val") 194 | 195 | def _is_good(self, reply): 196 | return reply.get("val") == "good" 197 | 198 | def _unwrap_error(self, reply): 199 | return reply.find(".[@val='fail']/richpp/..") 200 | 201 | def _has_error(self, reply): 202 | return self._unwrap_error(reply) is not None 203 | 204 | def _get_error_content(self, reply): 205 | # TODO add error context using loc_s, loc_e 206 | error_content = self._format_richpp(self._unwrap_error(reply).find("./richpp")) 207 | error_prefix = "Error: " 208 | if error_content.startswith(error_prefix): 209 | return error_content 210 | else: 211 | return error_prefix + error_content 212 | 213 | def _unwrap_message(self, reply): 214 | return reply if reply.tag == "message" else reply.find(".//message") 215 | 216 | def _is_message(self, reply): 217 | return self._unwrap_message(reply) is not None 218 | 219 | def _get_message_content(self, reply): 220 | message = self._unwrap_message(reply) 221 | message_content = self._format_richpp(message.find("./richpp")) 222 | message_level = message.find("./message_level").get("val") 223 | message_level_prefix = "{}: ".format(message_level.capitalize()) 224 | 225 | if message_level == "notice" or message_content.startswith(message_level_prefix): 226 | return message_content 227 | else: 228 | return message_level_prefix + message_content 229 | 230 | def _unwrap_goals(self, reply): 231 | return reply.find("./option/goals") 232 | 233 | def _has_goals(self, reply): 234 | return self._unwrap_goals(reply) is not None 235 | 236 | def _get_goals_content(self, reply): 237 | goals = self._unwrap_goals(reply) 238 | current_goals = list(goals.find("./list").findall("./goal")) 239 | background_before_goals = list(goals.find("./list[2]").findall("./pair/list[1]/goal")) 240 | background_after_goals = list(goals.find("./list[2]").findall("./pair/list[2]/goal")) 241 | background_goals = background_before_goals + background_after_goals 242 | 243 | if len(current_goals) == 0 and len(background_goals) == 0: 244 | return "No more subgoals" 245 | elif len(current_goals) == 0 and len(background_goals) > 0: 246 | header_content = "This subproof is complete, but there are some unfocused goals:" 247 | hypotheses_content = "" 248 | display_goals = background_goals 249 | else: 250 | header_content = "1 subgoal" if len(current_goals) == 1 else "{} subgoals".format(len(current_goals)) 251 | hypotheses_content = "\n".join(map(self._format_richpp, current_goals[0].findall("./list/richpp"))) 252 | display_goals = current_goals 253 | 254 | goals_content = "\n".join(map( 255 | lambda d: "{}\n{}".format( 256 | "-------------- ({}/{})".format(d[0] + 1, len(display_goals))[-20:], 257 | self._format_richpp(d[1].find("./richpp")) 258 | ), 259 | enumerate(display_goals) 260 | )) 261 | 262 | return "\n\n".join(filter(lambda c: c != "", [header_content, hypotheses_content, goals_content])) 263 | 264 | def _is_proving(self, reply): 265 | return reply.find(".status/option/string") is not None 266 | 267 | def _get_proof_name(self, reply): 268 | return reply.find(".status/option/string").text 269 | 270 | def _format_richpp(self, richpp): 271 | return ET.tostring(richpp, encoding='utf8', method='text').decode('utf8').strip("\n\r") 272 | 273 | def _is_end_of_input_error(self, reply): 274 | if self._is_good(reply): 275 | return False 276 | else: 277 | error_content = self._get_error_content(reply) 278 | if "Anomaly" in error_content and "Stm.End_of_input" in error_content: 279 | return True 280 | elif "Anomaly" in error_content and 'Invalid_argument("vernac_parse")' in error_content: # for older coqtop versions 281 | return True 282 | else: 283 | return False 284 | 285 | def _build_init_command(self): 286 | return ' ' 287 | 288 | def _build_status_command(self): 289 | return ' ' 290 | 291 | def _build_goal_command(self): 292 | return ' ' 293 | 294 | def _build_edit_at_command(self, state_id): 295 | return ' '.format(state_id) 296 | 297 | def _build_add_command(self, sentence, tip): 298 | if self.version >= (8, 15, 0): 299 | command = ET.fromstring(""" 300 | 301 | 302 | 303 | 304 | 305 | 306 | 0 307 | 308 | 309 | 310 | 311 | 312 | 313 | 0 314 | 315 | 316 | 0 317 | 0 318 | 319 | 320 | 321 | """) 322 | command.find("./pair/pair/pair/pair/string").text = sentence 323 | command.find("./pair/pair/pair/pair[2]/state_id").set("val", tip) 324 | else: 325 | command = ET.fromstring(""" 326 | 327 | 328 | 0 329 | 330 | 331 | 332 | """) 333 | command.find("./pair/pair/string").text = sentence 334 | command.find("./pair/pair[2]/state_id").set("val", tip) 335 | 336 | return ET.tostring(command, encoding='utf8').decode('utf8') 337 | -------------------------------------------------------------------------------- /coq_jupyter/install.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import json 3 | import os 4 | import sys 5 | import shutil 6 | 7 | from jupyter_client.kernelspec import KernelSpecManager 8 | from IPython.utils.tempdir import TemporaryDirectory 9 | 10 | def kernel_json(display_name, coqtop_executable, coqtop_args): 11 | argv = [sys.executable, "-m", "coq_jupyter", "-f", "{connection_file}"] 12 | if coqtop_executable is not None: 13 | argv.append('--CoqKernel.coqtop_executable="{}"'.format(coqtop_executable)) 14 | if coqtop_args is not None: 15 | argv.append('--CoqKernel.coqtop_args="{}"'.format(coqtop_args)) 16 | 17 | return { 18 | "argv": argv, 19 | "display_name": display_name, 20 | "language": "coq", 21 | } 22 | 23 | def install_kernel_spec(user, prefix, kernel_name, kernel_display_name, coqtop_executable, coqtop_args): 24 | with TemporaryDirectory() as td: 25 | os.chmod(td, 0o755) # Starts off as 700, not user readable 26 | 27 | with open(os.path.join(td, 'kernel.json'), 'w') as f: 28 | json.dump(kernel_json(kernel_display_name, coqtop_executable, coqtop_args), f, sort_keys=True) 29 | 30 | shutil.copyfile( 31 | os.path.join(os.path.dirname(__file__), "kernel.js"), 32 | os.path.join(td, 'kernel.js') 33 | ) 34 | 35 | print('Installing Jupyter kernel spec') 36 | KernelSpecManager().install_kernel_spec(td, kernel_name, user=user, replace=True, prefix=prefix) 37 | 38 | def _is_root(): 39 | try: 40 | return os.geteuid() == 0 41 | except AttributeError: 42 | return False # assume not an admin on non-Unix platforms 43 | 44 | def main(argv=None): 45 | ap = argparse.ArgumentParser() 46 | ap.add_argument('--user', action='store_true', help="Install to the per-user kernels registry. Default if not root.") 47 | ap.add_argument('--sys-prefix', action='store_true', help="Install to sys.prefix (e.g. a virtualenv or conda env)") 48 | ap.add_argument('--prefix', help="Install to the given prefix. Kernelspec will be installed in {PREFIX}/share/jupyter/kernels/") 49 | ap.add_argument('--kernel-name', help="Kernelspec name. Default is 'coq'.") 50 | ap.add_argument('--kernel-display-name', help="Kernelspec name that will be used in UI. Default is 'Coq'.") 51 | ap.add_argument('--coqtop-executable', help="coqidetop executable (coqtop for coq versions before 8.9.0).") 52 | ap.add_argument('--coqtop-args', help="Arguments to add when launching coqtop.") 53 | 54 | args = ap.parse_args(argv) 55 | 56 | if args.sys_prefix: 57 | args.prefix = sys.prefix 58 | if not args.prefix and not _is_root(): 59 | args.user = True 60 | if not args.kernel_name: 61 | args.kernel_name = 'coq' 62 | if not args.kernel_display_name: 63 | args.kernel_display_name = 'Coq' 64 | 65 | install_kernel_spec( 66 | args.user, 67 | args.prefix, 68 | args.kernel_name, 69 | args.kernel_display_name, 70 | args.coqtop_executable, 71 | args.coqtop_args 72 | ) 73 | 74 | if __name__ == '__main__': 75 | main() 76 | -------------------------------------------------------------------------------- /coq_jupyter/kernel.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'base/js/namespace', 3 | 'notebook/js/codecell', 4 | 'notebook/js/outputarea', 5 | 'codemirror/lib/codemirror' 6 | ], function ( 7 | Jupyter, 8 | CodeCell, 9 | OutputArea, 10 | CodeMirror 11 | ) { 12 | "use strict"; 13 | 14 | var self = { 15 | 16 | version: '1.6.2', 17 | 18 | onload: function() { 19 | console.info('Loading Coq kernel script, version: ' + self.version); 20 | 21 | // TODO find better way to expose coq kernel 22 | window.CoqKernel = self; 23 | 24 | self.init_CodeMirror(); 25 | self.patch(); 26 | self.init_shortcuts(); 27 | self.init_kernel_comm(); 28 | 29 | console.info('Coq kernel script loaded.'); 30 | }, 31 | 32 | init_CodeMirror: function() { 33 | console.info('Coq kernel script: adding CodeMirror mode.'); 34 | 35 | // Coq mode based on source taken from: https://github.com/ejgallego/CodeMirror/blob/9a1be1c5f716301245c27d4c541358835c1694fe/mode/coq/coq.js 36 | // Vernacular and tactics updated for 8.9.0 37 | // Also see: https://github.com/EugeneLoy/coq_jupyter/issues/19 38 | 39 | CodeMirror.defineMode('coq', function(_config, _parserConfig) { 40 | 41 | var vernacular = [ 42 | "Abort", 43 | "About", 44 | "Add", 45 | "Admit", 46 | "Admitted", 47 | "Arguments", 48 | "Axiom", 49 | "Axioms", 50 | "Back", 51 | "BackTo", 52 | "Bind", 53 | "Canonical", 54 | "Cd", 55 | "Check", 56 | "Class", 57 | "Close", 58 | "CoFixpoint", 59 | "CoInductive", 60 | "Coercion", 61 | "Collection", 62 | "Combined", 63 | "Comments", 64 | "Compute", 65 | "Conjecture", 66 | "Conjectures", 67 | "Constraint", 68 | "Context", 69 | "Corollary", 70 | "Create", 71 | "Debug", 72 | "Declare", 73 | "Defined", 74 | "Definition", 75 | "Delimit", 76 | "Derive", 77 | "Disable", 78 | "Drop", 79 | "Enable", 80 | "End", 81 | "Eval", 82 | "Example", 83 | "Existing", 84 | "Export", 85 | "Extract", 86 | "Extraction", 87 | "Fact", 88 | "Fail", 89 | "Final", 90 | "Fixpoint", 91 | "Focus", 92 | "From", 93 | "Function", 94 | "Functional", 95 | "Generalizable", 96 | "Generate", 97 | "Goal", 98 | "Guarded", 99 | "Hint", 100 | "Hypotheses", 101 | "Hypothesis", 102 | "Identity", 103 | "Implicit", 104 | "Import", 105 | "Include", 106 | "Inductive", 107 | "Infix", 108 | "Info", 109 | "Inspect", 110 | "Instance", 111 | "Lemma", 112 | "Let", 113 | "Load", 114 | "Locate", 115 | "Ltac", 116 | "Ltac2", 117 | "Module", 118 | "Next", 119 | "Notation", 120 | "Number", 121 | "Obligation", 122 | "Obligations", 123 | "Opaque", 124 | "Open", 125 | "Optimize", 126 | "Parameter", 127 | "Parameters", 128 | "Prenex", 129 | "Preterm", 130 | "Primitive", 131 | "Print", 132 | "Proof", 133 | "Property", 134 | "Proposition", 135 | "Pwd", 136 | "Qed", 137 | "Quit", 138 | "Record", 139 | "Recursive", 140 | "Redirect", 141 | "Register", 142 | "Remark", 143 | "Remove", 144 | "Require", 145 | "Reserved", 146 | "Reset", 147 | "Restart", 148 | "Save", 149 | "Scheme", 150 | "Search", 151 | "SearchPattern", 152 | "SearchRewrite", 153 | "Section", 154 | "Separate", 155 | "Set", 156 | "Show", 157 | "Solve", 158 | "Strategy", 159 | "String", 160 | "Structure", 161 | "SubClass", 162 | "Succeed", 163 | "Tactic", 164 | "Test", 165 | "Theorem", 166 | "Time", 167 | "Timeout", 168 | "Transparent", 169 | "Type", 170 | "Typeclasses", 171 | "Undelimit", 172 | "Undo", 173 | "Unfocus", 174 | "Unfocused", 175 | "Universe", 176 | "Universes", 177 | "Unset", 178 | "Unshelve", 179 | "Validate", 180 | "Variable", 181 | "Variables", 182 | "Variant", 183 | "infoH" 184 | ]; 185 | 186 | var gallina = [ 187 | 'as', 188 | 'at', 189 | 'cofix', 'crush', 190 | 'else', 'end', 191 | 'False', 'fix', 'for', 'forall', 'fun', 192 | 'if', 'in', 'is', 193 | 'let', 194 | 'match', 195 | 'of', 196 | 'Prop', 197 | 'return', 198 | 'struct', 199 | 'then', 'True', 'Type', 200 | 'when', 'with' 201 | ]; 202 | 203 | var tactics = [ 204 | "abstract", 205 | "absurd", 206 | "admit", 207 | "apply", 208 | "assert", 209 | "assert_fails", 210 | "assert_succeeds", 211 | "assumption", 212 | "auto", 213 | "autoapply", 214 | "autorewrite", 215 | "autounfold", 216 | "autounfold_one", 217 | "btauto", 218 | "bullet", 219 | "by", 220 | "case", 221 | "case_eq", 222 | "casetype", 223 | "cbn", 224 | "cbv", 225 | "change", 226 | "change_no_check", 227 | "classical_left", 228 | "classical_right", 229 | "clear", 230 | "clearbody", 231 | "cofix", 232 | "compare", 233 | "compute", 234 | "congr", 235 | "congruence", 236 | "constr_eq", 237 | "constr_eq_nounivs", 238 | "constr_eq_strict", 239 | "constructor", 240 | "context", 241 | "contradict", 242 | "contradiction", 243 | "cut", 244 | "cutrewrite", 245 | "cycle", 246 | "debug", 247 | "decide", 248 | "decompose", 249 | "dependent", 250 | "destauto", 251 | "destruct", 252 | "dfs", 253 | "dintuition", 254 | "discrR", 255 | "discriminate", 256 | "do", 257 | "done", 258 | "dtauto", 259 | "eapply", 260 | "eassert", 261 | "eassumption", 262 | "easy", 263 | "eauto", 264 | "ecase", 265 | "econstructor", 266 | "edestruct", 267 | "ediscriminate", 268 | "eelim", 269 | "eenough", 270 | "eexact", 271 | "eexists", 272 | "einduction", 273 | "einjection", 274 | "eintros", 275 | "eleft", 276 | "elim", 277 | "elimtype", 278 | "enough", 279 | "epose", 280 | "eremember", 281 | "erewrite", 282 | "eright", 283 | "eset", 284 | "esimplify_eq", 285 | "esplit", 286 | "etransitivity", 287 | "eval", 288 | "evar", 289 | "exact", 290 | "exact_no_check", 291 | "exactly_once", 292 | "exfalso", 293 | "exists", 294 | "f_equal", 295 | "fail", 296 | "field", 297 | "field_lookup", 298 | "field_simplify", 299 | "field_simplify_eq", 300 | "finish_timing", 301 | "first", 302 | "firstorder", 303 | "fix", 304 | "fold", 305 | "fresh", 306 | "fun", 307 | "functional", 308 | "generalize", 309 | "generalize_eqs", 310 | "generalize_eqs_vars", 311 | "generally", 312 | "gfail", 313 | "gintuition", 314 | "give_up", 315 | "guard", 316 | "has_evar", 317 | "have", 318 | "head_of_constr", 319 | "hnf", 320 | "idtac", 321 | "if-then-else", 322 | "in", 323 | "induction", 324 | "info_auto", 325 | "info_eauto", 326 | "info_trivial", 327 | "injection", 328 | "instantiate", 329 | "intro", 330 | "intros", 331 | "intuition", 332 | "inversion", 333 | "inversion_clear", 334 | "inversion_sigma", 335 | "is_cofix", 336 | "is_const", 337 | "is_constructor", 338 | "is_evar", 339 | "is_fix", 340 | "is_ground", 341 | "is_ind", 342 | "is_proj", 343 | "is_var", 344 | "lapply", 345 | "last", 346 | "lazy", 347 | "lazy_match!", 348 | "lazymatch", 349 | "left", 350 | "let", 351 | "lia", 352 | "lra", 353 | "ltac-seq", 354 | "match", 355 | "match!", 356 | "move", 357 | "multi_match!", 358 | "multimatch", 359 | "native_cast_no_check", 360 | "native_compute", 361 | "nia", 362 | "not_evar", 363 | "now", 364 | "now_show", 365 | "nra", 366 | "nsatz", 367 | "nsatz_compute", 368 | "numgoals", 369 | "once", 370 | "only", 371 | "optimize_heap", 372 | "over", 373 | "pattern", 374 | "pose", 375 | "progress", 376 | "protect_fv", 377 | "psatz", 378 | "rapply", 379 | "red", 380 | "refine", 381 | "reflexivity", 382 | "remember", 383 | "rename", 384 | "repeat", 385 | "replace", 386 | "reset", 387 | "restart_timer", 388 | "revert", 389 | "revgoals", 390 | "rewrite", 391 | "rewrite_db", 392 | "rewrite_strat", 393 | "right", 394 | "ring", 395 | "ring_lookup", 396 | "ring_simplify", 397 | "rtauto", 398 | "set", 399 | "setoid_etransitivity", 400 | "setoid_reflexivity", 401 | "setoid_replace", 402 | "setoid_rewrite", 403 | "setoid_symmetry", 404 | "setoid_transitivity", 405 | "shelve", 406 | "shelve_unifiable", 407 | "show", 408 | "simpl", 409 | "simple", 410 | "simplify_eq", 411 | "soft", 412 | "solve", 413 | "solve_constraints", 414 | "specialize", 415 | "specialize_eqs", 416 | "split", 417 | "split_Rabs", 418 | "split_Rmult", 419 | "start", 420 | "stepl", 421 | "stepr", 422 | "stop", 423 | "subst", 424 | "substitute", 425 | "suff", 426 | "suffices", 427 | "swap", 428 | "symmetry", 429 | "tauto", 430 | "time", 431 | "time_constr", 432 | "timeout", 433 | "transitivity", 434 | "transparent_abstract", 435 | "trivial", 436 | "try", 437 | "tryif", 438 | "type", 439 | "type_term", 440 | "typeclasses", 441 | "under", 442 | "unfold", 443 | "unify", 444 | "unlock", 445 | "unshelve", 446 | "vm_cast_no_check", 447 | "vm_compute", 448 | "with_strategy", 449 | "without", 450 | "wlia", 451 | "wlog", 452 | "wlra_Q", 453 | "wnia", 454 | "wnra_Q", 455 | "wpsatz_Q", 456 | "wpsatz_Z", 457 | "wsos_Q", 458 | "wsos_Z", 459 | "xlia", 460 | "xlra_Q", 461 | "xlra_R", 462 | "xnia", 463 | "xnra_Q", 464 | "xnra_R", 465 | "xpsatz_Q", 466 | "xpsatz_R", 467 | "xpsatz_Z", 468 | "xsos_Q", 469 | "xsos_R", 470 | "xsos_Z", 471 | "zify", 472 | "zify_elim_let", 473 | "zify_iter_let", 474 | "zify_iter_specs", 475 | "zify_op", 476 | "zify_saturate" 477 | ]; 478 | 479 | var terminators = [ 480 | 'assumption', 481 | 'by', 482 | 'contradiction', 483 | 'discriminate', 484 | 'exact', 485 | 'now', 486 | 'omega', 487 | 'reflexivity', 488 | 'tauto' 489 | ]; 490 | 491 | var admitters = [ 492 | 'admit', 493 | 'Admitted' 494 | ]; 495 | 496 | // Map assigning each keyword a category. 497 | var words = {}; 498 | 499 | // TODO the following mappings are temporary modified. 500 | // TODO should change these again as part of https://github.com/EugeneLoy/coq_jupyter/issues/19 501 | // We map 502 | // - gallina keywords -> CM keywords 503 | // - vernaculars -> CM builtins 504 | // - admitters -> CM keywords XXX 505 | gallina .map(function(word){words[word] = 'builtin';}); 506 | admitters .map(function(word){words[word] = 'builtin';}); 507 | vernacular .map(function(word){words[word] = 'keyword';}); 508 | 509 | tactics .map(function(word){words[word] = 'builtin';}); 510 | terminators.map(function(word){words[word] = 'builtin';}); 511 | 512 | /* 513 | Coq mode defines the following state variables: 514 | 515 | - begin_sentence: only \s caracters seen from the last sentence. 516 | 517 | - commentLevel [:int] = Level of nested comments. 518 | 519 | - tokenize [:func] = current active tokenizer. We provide 4 main ones: 520 | 521 | + tokenBase: Main parser, it reads the next character and 522 | setups the next tokenizer. In particular it takes care of 523 | braces. It doesn't properly analyze the sentences and 524 | bracketing. 525 | 526 | + tokenStatementEnd: Called when a dot is found in tokenBase, 527 | it looks ahead on the string and returns statement end. 528 | 529 | + tokenString: Takes care of escaping. 530 | 531 | + tokenComment: Takes care of nested comments. 532 | 533 | */ 534 | 535 | /* 536 | Codemirror lexing functions: 537 | 538 | - eat(s) = eat next char if s 539 | - eatWhile(s) = eat s until fails 540 | - match(regexp) => return array of matches and optionally eat 541 | 542 | */ 543 | function tokenBase(stream, state) { 544 | 545 | // If any space in the input, return null. 546 | if(stream.eatSpace()) 547 | return null; 548 | 549 | var ch = stream.next(); 550 | 551 | if(state.begin_sentence && (/[-*+{}]/.test(ch))) 552 | return 'coq-bullet'; 553 | 554 | // Preserve begin sentence after comment. 555 | if (ch === '(') { 556 | if (stream.peek() === '*') { 557 | stream.next(); 558 | state.commentLevel++; 559 | state.tokenize = tokenComment; 560 | return state.tokenize(stream, state); 561 | } 562 | state.begin_sentence = false; 563 | return 'parenthesis'; 564 | } 565 | 566 | if( ! (/\s/.test(ch)) ) { 567 | state.begin_sentence = false; 568 | } 569 | 570 | if(ch === '.') { 571 | // Parse .. specially. 572 | if(stream.peek() !== '.') { 573 | state.tokenize = tokenStatementEnd; 574 | return state.tokenize(stream, state); 575 | } else { 576 | stream.next(); 577 | return 'operator'; 578 | } 579 | 580 | } 581 | 582 | if (ch === '"') { 583 | state.tokenize = tokenString; 584 | return state.tokenize(stream, state); 585 | } 586 | 587 | if(ch === ')') 588 | return 'parenthesis'; 589 | 590 | if (ch === '~') { 591 | stream.eatWhile(/\w/); 592 | return 'variable-2'; 593 | } 594 | 595 | if (ch === '`') { 596 | stream.eatWhile(/\w/); 597 | return 'quote'; 598 | } 599 | 600 | if (/\d/.test(ch)) { 601 | stream.eatWhile(/[\d]/); 602 | /* 603 | if (stream.eat('.')) { 604 | stream.eatWhile(/[\d]/); 605 | } 606 | */ 607 | return 'number'; 608 | } 609 | 610 | if ( /[+\-*&%=<>!?|]/.test(ch)) { 611 | return 'operator'; 612 | } 613 | 614 | if(/[\[\]]/.test(ch)) { 615 | return 'bracket'; 616 | } 617 | 618 | stream.eatWhile(/\w/); 619 | var cur = stream.current(); 620 | return words.hasOwnProperty(cur) ? words[cur] : 'variable'; 621 | 622 | } 623 | 624 | function tokenString(stream, state) { 625 | var next, end = false, escaped = false; 626 | while ((next = stream.next()) != null) { 627 | if (next === '"' && !escaped) { 628 | end = true; 629 | break; 630 | } 631 | escaped = !escaped && next === '\\'; 632 | } 633 | if (end && !escaped) { 634 | state.tokenize = tokenBase; 635 | } 636 | return 'string'; 637 | } 638 | 639 | function tokenComment(stream, state) { 640 | var ch; 641 | while(state.commentLevel && (ch = stream.next())) { 642 | if(ch === '(' && stream.peek() === '*') { 643 | stream.next(); 644 | state.commentLevel++; 645 | } 646 | 647 | if(ch === '*' && stream.peek() === ')') { 648 | stream.next(); 649 | state.commentLevel--; 650 | } 651 | } 652 | 653 | if(!state.commentLevel) 654 | state.tokenize = tokenBase; 655 | 656 | return 'comment'; 657 | } 658 | 659 | function tokenStatementEnd(stream, state) { 660 | state.tokenize = tokenBase; 661 | 662 | if(stream.eol() || stream.match(/\s/, false)) { 663 | state.begin_sentence = true; 664 | return 'statementend'; 665 | } 666 | } 667 | 668 | return { 669 | startState: function() { 670 | return {begin_sentence: true, tokenize: tokenBase, commentLevel: 0}; 671 | }, 672 | 673 | token: function(stream, state) { 674 | return state.tokenize(stream, state); 675 | }, 676 | 677 | blockCommentStart: "(*", 678 | blockCommentEnd : "*)", 679 | lineComment: null 680 | }; 681 | }); 682 | 683 | CodeMirror.defineMIME('text/x-coq', { 684 | name: 'coq' 685 | }); 686 | }, 687 | 688 | patch: function() { 689 | console.info('Coq kernel script: patching CodeCell.execute.'); 690 | 691 | // based on: https://gist.github.com/quinot/e3801b09f754efb0f39ccfbf0b50eb40 692 | 693 | var original_execute = CodeCell.CodeCell.prototype.execute; 694 | CodeCell.CodeCell.prototype.execute = function(stop_on_error) { 695 | var cell = this; 696 | 697 | if (!this.coq_kernel_kernel_patched) { 698 | this.coq_kernel_kernel_patched = true; 699 | 700 | this.coq_kernel_original_kernel = this.kernel; 701 | this.kernel = new Proxy( 702 | this.coq_kernel_original_kernel, 703 | { 704 | "get": function(target, prop, receiver) { 705 | if (prop == "execute") { 706 | return function(code, callbacks, metadata) { 707 | return self.execute_cell(cell, code, callbacks, metadata); 708 | }; 709 | } else { 710 | return target[prop]; 711 | } 712 | } 713 | } 714 | ); 715 | } 716 | 717 | original_execute.call(this, stop_on_error) 718 | }; 719 | 720 | console.info('Coq kernel script: patching CodeCell.create_element.'); 721 | var original_create_element = CodeCell.CodeCell.prototype.create_element; 722 | CodeCell.CodeCell.prototype.create_element = function() { 723 | var cell = this; 724 | setTimeout(function() { self.on_create_element(cell); }, 0); 725 | return original_create_element.call(this); 726 | }; 727 | 728 | console.info('Coq kernel script: patching OutputArea.append_execute_result.'); 729 | 730 | var original_append_execute_result = OutputArea.OutputArea.prototype.append_execute_result; 731 | OutputArea.OutputArea.prototype.append_execute_result = function(json) { 732 | var result = original_append_execute_result.call(this, json); 733 | self.on_append_execute_result(this, json); 734 | return result; 735 | }; 736 | }, 737 | 738 | init_shortcuts: function() { 739 | console.info('Coq kernel script: adding actions/shortcuts.'); 740 | 741 | var action = { 742 | icon: 'fa-step-backward', 743 | cmd: 'Rollback cell', 744 | help: 'Rollback cell', 745 | help_index: 'zz', // TODO not sure what to set here 746 | handler: function () { 747 | var cells = Jupyter.notebook.get_cells(); 748 | for (var c = 0; c < cells.length; c++) { 749 | if (cells[c].selected || cells[c].element.hasClass('jupyter-soft-selected')) { 750 | self.roll_back_cell(cells[c]); 751 | } 752 | } 753 | } 754 | }; 755 | var prefix = 'coq_jupyter'; 756 | var action_name = 'rollback-cell'; 757 | 758 | var full_action_name = Jupyter.actions.register(action, action_name, prefix); 759 | Jupyter.toolbar.add_buttons_group([full_action_name]); 760 | Jupyter.keyboard_manager.command_shortcuts.add_shortcut('Ctrl-Backspace', full_action_name); 761 | }, 762 | 763 | init_kernel_comm: function() { 764 | if (Jupyter.notebook.kernel) { 765 | console.info('Coq kernel script: initializing kernel comm.'); 766 | Jupyter.notebook.kernel.events.on('kernel_ready.Kernel', function (evt, info) { 767 | self.open_kernel_comm(); 768 | }); 769 | self.open_kernel_comm(); 770 | } else { 771 | console.info('Coq kernel script: kernel is not ready - postponing kernel comm initialization.'); 772 | setTimeout(self.init_kernel_comm, 100); 773 | } 774 | }, 775 | 776 | open_kernel_comm: function() { 777 | console.info('Coq kernel script: opening kernel comm.'); 778 | if (self.kernel_comm !== undefined) { 779 | self.close_kernel_comm(); 780 | } 781 | self.kernel_comm = Jupyter.notebook.kernel.comm_manager.new_comm('coq_kernel.kernel_comm'); 782 | self.kernel_comm.on_msg(function(message) { 783 | self.handle_kernel_comm_message(message); 784 | }); 785 | console.info('Coq kernel script: kernel comm opened.'); 786 | }, 787 | 788 | close_kernel_comm: function() { 789 | console.info('Coq kernel script: closing kernel comm : ' + self.kernel_comm.comm_id); 790 | try { 791 | self.kernel_comm.close(); 792 | Jupyter.notebook.kernel.comm_manager.unregister_comm(self.kernel_comm); 793 | } catch(e) { 794 | console.error(e); 795 | } 796 | }, 797 | 798 | handle_kernel_comm_message: function(message) { 799 | if (message.content.data.comm_msg_type === "kernel_comm_opened") { 800 | console.info('Kernel comm opened. comm_id: ' + message.content.comm_id); 801 | self.on_history_received(message.content.data.history); 802 | } else if (message.content.data.comm_msg_type === "cell_state") { 803 | console.info('Cell state updated. execution_id: ' + message.content.data.execution_id); 804 | self.on_cell_state_received(message.content.data.execution_id, message.content.data.evaluated, message.content.data.rolled_back); 805 | } else { 806 | console.error('Unexpected comm message: ' + JSON.stringify(message)); 807 | } 808 | }, 809 | 810 | execute_cell: function(cell, code, callbacks, metadata) { 811 | var previous_execution_id = self.get_metadata(cell, "execution_id"); 812 | 813 | self.reset_metadata(cell); 814 | 815 | if (self.get_metadata(cell, "auto_roll_back") && previous_execution_id !== undefined) { 816 | metadata.coq_kernel_roll_back_cell = previous_execution_id; 817 | } 818 | 819 | // reuse kernel message id as execution id for this cell 820 | var execution_id = cell.coq_kernel_original_kernel.execute(code, callbacks, metadata); 821 | 822 | self.bind_execution_id(cell, execution_id) 823 | 824 | return execution_id; 825 | }, 826 | 827 | on_append_execute_result: function(outputarea, json) { 828 | var cell = self.get_cell_by_element(outputarea.element[0]) 829 | self.set_metadata(cell, "evaluated", json.metadata.coq_kernel_evaluated); 830 | self.set_metadata(cell, "rolled_back", json.metadata.coq_kernel_rolled_back); 831 | self.update_rich_cell_output(cell); 832 | }, 833 | 834 | on_create_element: function(cell) { 835 | if (!self.has_valid_metadata(cell)) { 836 | self.reset_metadata(cell); 837 | } 838 | self.update_rich_cell_output(cell); 839 | }, 840 | 841 | on_cell_state_received: function(execution_id, evaluated, rolled_back) { 842 | var cells = Jupyter.notebook.get_cells(); 843 | for (var c = 0; c < cells.length; c++) { 844 | if (self.has_valid_metadata(cells[c]) && self.get_metadata(cells[c], "execution_id") === execution_id) { 845 | self.update_cell_state_metadata(cells[c], evaluated, rolled_back); 846 | self.update_rich_cell_output(cells[c]); 847 | break; 848 | } 849 | } 850 | }, 851 | 852 | on_history_received: function(history) { 853 | var cells = Jupyter.notebook.get_cells(); 854 | for (var c = 0; c < cells.length; c++) { 855 | var execution_id = self.get_metadata(cells[c], "execution_id"); 856 | 857 | self.reset_metadata(cells[c]); 858 | if (execution_id !== undefined) { 859 | // rebind execution ids to cells. This is typically needed after loading 860 | // since cell ids are not persisted. 861 | self.bind_execution_id(cells[c], execution_id); 862 | } 863 | 864 | for (var h = 0; h < history.length; h++) { 865 | if (self.get_metadata(cells[c], "execution_id") === history[h].execution_id) { 866 | self.update_cell_state_metadata(cells[c], history[h].evaluated, history[h].rolled_back); 867 | break; 868 | } 869 | } 870 | 871 | self.update_rich_cell_output(cells[c]); 872 | } 873 | }, 874 | 875 | reset_metadata: function(cell) { 876 | if (cell.metadata.coq_kernel_metadata === undefined) { 877 | cell.metadata.coq_kernel_metadata = { 878 | "auto_roll_back": true 879 | }; 880 | } 881 | 882 | cell.metadata.coq_kernel_metadata.execution_id = undefined; 883 | cell.metadata.coq_kernel_metadata.cell_id = undefined; 884 | cell.metadata.coq_kernel_metadata.evaluated = undefined; 885 | cell.metadata.coq_kernel_metadata.rolled_back = undefined; 886 | }, 887 | 888 | has_valid_metadata: function(cell) { 889 | return ( 890 | cell.metadata.coq_kernel_metadata !== undefined && 891 | cell.metadata.coq_kernel_metadata.cell_id === cell.cell_id && 892 | cell.metadata.coq_kernel_metadata.execution_id !== undefined && 893 | cell.metadata.coq_kernel_metadata.evaluated !== undefined && 894 | cell.metadata.coq_kernel_metadata.rolled_back !== undefined && 895 | cell.metadata.coq_kernel_metadata.auto_roll_back !== undefined 896 | ); 897 | }, 898 | 899 | get_metadata: function(cell, name) { 900 | if (cell.metadata.coq_kernel_metadata === undefined) { 901 | self.reset_metadata(cell); 902 | } 903 | return cell.metadata.coq_kernel_metadata[name]; 904 | }, 905 | 906 | set_metadata: function(cell, name, value) { 907 | if (cell.metadata.coq_kernel_metadata === undefined) { 908 | self.reset_metadata(cell); 909 | } 910 | cell.metadata.coq_kernel_metadata[name] = value; 911 | }, 912 | 913 | bind_execution_id: function(cell, execution_id) { 914 | self.set_metadata(cell, "execution_id", execution_id); 915 | self.set_metadata(cell, "cell_id", cell.cell_id); 916 | }, 917 | 918 | update_cell_state_metadata: function(cell, evaluated, rolled_back) { 919 | self.set_metadata(cell, "evaluated", evaluated); 920 | self.set_metadata(cell, "rolled_back", rolled_back); 921 | }, 922 | 923 | roll_back_cell: function(cell) { 924 | $(cell.element[0]).find(".coq_kernel_roll_back_button").prop('disabled', true); 925 | 926 | self.kernel_comm.send({ 927 | 'comm_msg_type': 'roll_back', 928 | "execution_id": self.get_metadata(cell, "execution_id") 929 | }); 930 | }, 931 | 932 | roll_back: function(button) { 933 | self.roll_back_cell(self.get_cell_by_element(button)); 934 | }, 935 | 936 | toggle_auto_roll_back: function(input) { 937 | self.set_metadata(self.get_cell_by_element(input), "auto_roll_back", input.checked); 938 | }, 939 | 940 | get_cell_by_element: function(element) { 941 | var cells = Jupyter.notebook.get_cells(); 942 | for (var c = 0; c < cells.length; c++) { 943 | if ($.contains(cells[c].element[0], element)) { 944 | return cells[c]; 945 | } 946 | } 947 | }, 948 | 949 | update_rich_cell_output: function(cell) { 950 | self.hide_rich_cell_output(cell); 951 | 952 | if (self.has_valid_metadata(cell)) { 953 | var evaluated = self.get_metadata(cell, "evaluated"); 954 | var rolled_back = self.get_metadata(cell, "rolled_back"); 955 | var auto_roll_back = self.get_metadata(cell, "auto_roll_back"); 956 | 957 | $(cell.element[0]).find(".coq_kernel_output_area").toggle(!rolled_back); 958 | 959 | $(cell.element[0]).find(".coq_kernel_status_message_area").toggle(!rolled_back) 960 | $(cell.element[0]).find(".coq_kernel_rolled_back_status_message").toggle(rolled_back); 961 | 962 | $(cell.element[0]).find(".coq_kernel_roll_back_controls_area").toggle(evaluated && !rolled_back); 963 | 964 | $(cell.element[0]).find(".coq_kernel_auto_roll_back_checkbox").prop('checked', auto_roll_back); 965 | } 966 | }, 967 | 968 | hide_rich_cell_output: function(cell) { 969 | $(cell.element[0]).find(".coq_kernel_rich_cell_output").toggle(false); 970 | } 971 | 972 | }; 973 | 974 | return self; 975 | 976 | }); 977 | -------------------------------------------------------------------------------- /coq_jupyter/kernel.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import sys 4 | import traceback 5 | import time 6 | 7 | from traitlets import Unicode, Undefined 8 | from uuid import uuid4 9 | from ipykernel.kernelbase import Kernel 10 | from .coqtop import Coqtop, CoqtopError 11 | from .renderer import Renderer, HTML_ROLLED_BACK_STATUS_MESSAGE, TEXT_ROLLED_BACK_STATUS_MESSAGE 12 | 13 | 14 | __version__ = '1.6.2' 15 | 16 | 17 | CELL_COMM_TARGET_NAME = "coq_kernel.kernel_comm" 18 | 19 | 20 | class CellRecord: 21 | 22 | def __init__(self, state_label_before, state_label_after, evaluated, rolled_back, execution_id, parent_header): 23 | self.state_label_after = state_label_after 24 | self.state_label_before = state_label_before 25 | self.evaluated = evaluated 26 | self.rolled_back = rolled_back 27 | self.execution_id = execution_id 28 | self.parent_header = parent_header 29 | 30 | def __repr__(self): 31 | return "".format(repr((self.state_label_before, self.state_label_after, self.evaluated, self.rolled_back, self.execution_id, self.parent_header))) 32 | 33 | 34 | class CellJournal: 35 | 36 | def __init__(self, kernel): 37 | self.log = kernel.log # TODO 38 | self.history = [] 39 | 40 | def add(self, state_label_before, state_label_after, evaluated, rolled_back, execution_id, parent_header): 41 | record = CellRecord(state_label_before, state_label_after, evaluated, rolled_back, execution_id, parent_header) 42 | self.history.append(record) 43 | return record 44 | 45 | def find_by_execution_id(self, execution_id): 46 | result = list(filter(lambda r: r.execution_id == execution_id, self.history)) 47 | if len(result) == 1: 48 | return result[0] 49 | else: 50 | return None 51 | 52 | def find_rolled_back_transitively(self, state_label_before): 53 | return list(filter(lambda r: int(r.state_label_before) > int(state_label_before) and not r.rolled_back, self.history)) 54 | 55 | 56 | def shutdown_on_coqtop_error(function): 57 | def wrapper(self, *args, **kwargs): 58 | try: 59 | return function(self, *args, **kwargs) 60 | except CoqtopError: 61 | self.log.exception("CoqtopError has occured. Scheduling shutdown.") 62 | from tornado import ioloop 63 | loop = ioloop.IOLoop.current() 64 | loop.add_timeout(time.time() + 0.1, loop.stop) 65 | raise 66 | 67 | return wrapper 68 | 69 | 70 | class CoqKernel(Kernel): 71 | implementation = 'coq' 72 | implementation_version = __version__ 73 | language = 'coq' 74 | 75 | @property 76 | def language_info(self): 77 | return { 78 | 'name': 'coq', 79 | 'mimetype': 'text/x-coq', 80 | 'file_extension': '.v', 81 | 'version': self.language_version 82 | } 83 | 84 | @property 85 | def banner(self): 86 | return self._coqtop.banner 87 | 88 | @property 89 | def language_version(self): 90 | return ".".join(map(str, self._coqtop.version)) 91 | 92 | 93 | coqtop_executable = Unicode().tag(config=True) 94 | coqtop_args = Unicode().tag(config=True) 95 | 96 | 97 | @shutdown_on_coqtop_error 98 | def __init__(self, **kwargs): 99 | Kernel.__init__(self, **kwargs) 100 | self._coqtop = Coqtop(self, self.coqtop_executable, self.coqtop_args) 101 | self._journal = CellJournal(self) 102 | self._renderer = Renderer() 103 | self._kernel_comms = [] 104 | for msg_type in ['comm_open', 'comm_msg', 'comm_close']: 105 | self.shell_handlers[msg_type] = getattr(self, msg_type) 106 | 107 | def do_execute(self, code, silent, store_history=True, user_expressions=None, allow_stdin=False): 108 | try: 109 | self.log.info("Processing 'execute_request', code: \n{}\n".format(repr(code))) 110 | 111 | if "coq_kernel_roll_back_cell" in self._parent_header["content"]: 112 | self._roll_back(self._parent_header["content"]["coq_kernel_roll_back_cell"]) 113 | 114 | if code.strip("\n\r\t ") != "": 115 | state_label_before = self._coqtop.tip 116 | (evaluated, outputs) = shutdown_on_coqtop_error(lambda self: self._coqtop.eval(code))(self) 117 | state_label_after = self._coqtop.tip 118 | execution_id = self._parent_header["msg_id"] 119 | 120 | record = self._journal.add(state_label_before, state_label_after, evaluated, False, execution_id, self._parent_header) 121 | 122 | if not silent: 123 | self.log.info("Sending 'execute_result', cell record:\n{}\n".format(repr(record))) 124 | self._send_execute_result(outputs, execution_id, evaluated, False, state_label_after) 125 | 126 | return self._build_ok_content(state_label_before) 127 | else: 128 | self.log.info("code is empty - skipping evaluation and sending results.") 129 | return self._build_ok_content(self._coqtop.tip) 130 | 131 | except Exception as e: 132 | self.log.exception("Error during evaluating code: \n'{}'\n".format(repr(code))) 133 | return self._build_error_content(*sys.exc_info()) 134 | 135 | def comm_open(self, stream, ident, msg): 136 | content = msg["content"] 137 | if content["target_name"] == CELL_COMM_TARGET_NAME: 138 | self._init_kernel_comm(content["comm_id"]) 139 | else: 140 | self.log.error("Unexpected comm_open, msg: {}".format(repr(msg))) 141 | 142 | def comm_close(self, stream, ident, msg): 143 | self._kernel_comms.remove(msg["content"]["comm_id"]) 144 | self.log.info("Kernel comm closed, msg: {}".format(repr(msg))) 145 | 146 | @shutdown_on_coqtop_error 147 | def comm_msg(self, stream, ident, msg): 148 | content = msg["content"] 149 | if content["comm_id"] in self._kernel_comms: 150 | if content["data"]["comm_msg_type"] == "roll_back": 151 | self._roll_back(msg["content"]["data"]["execution_id"]) 152 | else: 153 | self.log.error("Unexpected comm_msg, msg: {}".format(repr(msg))) 154 | else: 155 | self.log.info("Unexpected (possibly leftover) comm_msg, msg: {}, opened comms: {}".format(repr(msg)), repr(self._kernel_comms)) 156 | 157 | def _init_kernel_comm(self, comm_id): 158 | self._send_kernel_comm_opened_comm_msg(comm_id, self._journal.history) 159 | self._kernel_comms.append(comm_id) 160 | self.log.info("Kernel comm opened, comm_id: {}".format(comm_id)) 161 | 162 | def _roll_back(self, execution_id): 163 | self.log.info("roll back, execution_id: {}".format(execution_id)) 164 | cell_record = self._journal.find_by_execution_id(execution_id) 165 | 166 | if cell_record is not None and cell_record.evaluated and not cell_record.rolled_back: 167 | self._coqtop.roll_back_to(cell_record.state_label_before) 168 | 169 | for record in [cell_record] + self._journal.find_rolled_back_transitively(cell_record.state_label_before): 170 | # mark cell as rolled back 171 | record.rolled_back= True 172 | 173 | # update content of rolled back cell 174 | self._send_roll_back_update_display_data(record.parent_header, record.execution_id, record.evaluated, record.rolled_back) 175 | 176 | # update cell state via kernel comms 177 | for comm_id in self._kernel_comms: 178 | self._send_cell_state_comm_msg(comm_id, record.execution_id, record.evaluated, record.rolled_back) 179 | 180 | else: 181 | self.log.info("Unexpected (possibly leftover) roll back request for execution_id: {}".format(execution_id)) 182 | 183 | def _build_ok_content(self, state_label_before): 184 | return { 185 | 'status': 'ok', 186 | 'execution_count': int(state_label_before), 187 | 'payload': [], 188 | 'user_expressions': {}, 189 | } 190 | 191 | def _build_error_content(self, ex_type, ex, tb): 192 | return { 193 | 'status': 'error', 194 | 'ename' : ex_type.__name__, 195 | 'evalue' : repr(ex), 196 | 'traceback' : traceback.format_list(traceback.extract_tb(tb)) 197 | } 198 | 199 | def _build_display_data_content(self, text, html, execution_id, evaluated, rolled_back): 200 | return { 201 | 'data': { 202 | 'text/plain': text, 203 | 'text/html': html 204 | }, 205 | 'metadata': { 206 | 'coq_kernel_execution_id': execution_id, 207 | 'coq_kernel_evaluated': evaluated, 208 | 'coq_kernel_rolled_back': rolled_back 209 | }, 210 | 'transient': { 'display_id': execution_id } 211 | } 212 | 213 | def _send_kernel_comm_opened_comm_msg(self, comm_id, history): 214 | content = { 215 | "comm_id": comm_id, 216 | "data": { 217 | "comm_msg_type": "kernel_comm_opened", 218 | "history": [ 219 | { 220 | "execution_id": record.execution_id, 221 | "evaluated": record.evaluated, 222 | "rolled_back": record.rolled_back 223 | } 224 | for record in history 225 | ] 226 | } 227 | } 228 | self.session.send(self.iopub_socket, "comm_msg", content, None, None, None, None, None, None) 229 | 230 | def _send_cell_state_comm_msg(self, comm_id, execution_id, evaluated, rolled_back): 231 | content = { 232 | "comm_id": comm_id, 233 | "data": { 234 | "comm_msg_type": "cell_state", 235 | "execution_id": execution_id, 236 | "evaluated": evaluated, 237 | "rolled_back": rolled_back 238 | } 239 | } 240 | self.session.send(self.iopub_socket, "comm_msg", content, None, None, None, None, None, None) 241 | 242 | def _send_roll_back_update_display_data(self, parent_header, execution_id, evaluated, rolled_back): 243 | content = self._build_display_data_content(TEXT_ROLLED_BACK_STATUS_MESSAGE, HTML_ROLLED_BACK_STATUS_MESSAGE, execution_id, evaluated, rolled_back) 244 | self.session.send(self.iopub_socket, "update_display_data", content, parent_header, None, None, None, None, None) 245 | 246 | def _send_execute_result(self, outputs, execution_id, evaluated, rolled_back, state_label_after): 247 | text = self._renderer.render_text_result(outputs) 248 | html = self._renderer.render_html_result(outputs, execution_id, evaluated) 249 | content = self._build_display_data_content(text, html, execution_id, evaluated, rolled_back) 250 | content['execution_count'] = int(state_label_after) 251 | self.send_response(self.iopub_socket, 'execute_result', content) 252 | -------------------------------------------------------------------------------- /coq_jupyter/renderer.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import os 4 | 5 | 6 | HTML_SUCCESS_STATUS_MESSAGE = """ 7 |
8 | 9 | Cell evaluated. 10 |
11 | """ 12 | 13 | HTML_ERROR_STATUS_MESSAGE = """ 14 |
15 | 16 | Error while evaluating cell. Cell rolled back. 17 |
18 | """ 19 | 20 | HTML_ROLLED_BACK_STATUS_MESSAGE = """ 21 |
22 | 23 | Cell rolled back. 24 |
25 | """ 26 | 27 | HTML_ROLLED_BACK_STATUS_MESSAGE_HIDDEN = """ 28 | 32 | """ 33 | 34 | TEXT_ROLLED_BACK_STATUS_MESSAGE = "Cell rolled back." 35 | 36 | HTML_OUTPUT_TEMPLATE = """ 37 |
38 |
{0}
39 |
40 | """ 41 | 42 | HTML_ROLL_BACK_CONTROLS = """ 43 | 54 | """ 55 | 56 | 57 | class Renderer: 58 | 59 | def render_text_result(self, outputs): 60 | cell_output = "\n\n".join(outputs) 61 | return cell_output 62 | 63 | def render_html_result(self, outputs, execution_id, success_output): 64 | html = HTML_OUTPUT_TEMPLATE.format(self.render_text_result(outputs)) 65 | if success_output: 66 | html += HTML_SUCCESS_STATUS_MESSAGE 67 | html += HTML_ROLLED_BACK_STATUS_MESSAGE_HIDDEN 68 | html += HTML_ROLL_BACK_CONTROLS 69 | else: 70 | html += HTML_ERROR_STATUS_MESSAGE 71 | 72 | return html 73 | -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | 2 | # This entry point is used for debug only: 3 | if __name__ == '__main__': 4 | from sys import argv 5 | from ipykernel.kernelapp import IPKernelApp 6 | from coq_jupyter.kernel import CoqKernel 7 | IPKernelApp.launch_instance(kernel_class=CoqKernel, args=argv) 8 | -------------------------------------------------------------------------------- /process_indexes.py: -------------------------------------------------------------------------------- 1 | 2 | # TODO find a better way to do this 3 | # This script parses coq tactics\commands index pages and outputs relevant 4 | # keywords to be used in CodeMirror coq mode 5 | # X_data variables are taken from Coq 8.18.0 documentation (Command/Tactic index) 6 | 7 | commands_data = """ 8 | a 9 | Abort 10 | About 11 | Add 12 | Add Field 13 | Add Morphism 14 | Add Parametric Morphism 15 | Add Parametric Relation 16 | Add Parametric Setoid 17 | Add Relation 18 | Add Ring 19 | Add Setoid 20 | Add Zify 21 | Admit Obligations 22 | Admitted 23 | Arguments 24 | Axiom 25 | Axioms 26 | 27 | b 28 | Back 29 | BackTo 30 | Bind Scope 31 | 32 | c 33 | Canonical Structure 34 | Cd 35 | Check 36 | Class 37 | Close Scope 38 | Coercion 39 | CoFixpoint 40 | CoInductive 41 | Collection 42 | Combined Scheme 43 | Comments 44 | Compute 45 | Conjecture 46 | Conjectures 47 | Constraint 48 | Context 49 | Corollary 50 | Create HintDb 51 | 52 | d 53 | Debug 54 | Declare Custom Entry 55 | Declare Equivalent Keys 56 | Declare Instance 57 | Declare Left Step 58 | Declare ML Module 59 | Declare Module 60 | Declare Morphism 61 | Declare Reduction 62 | Declare Right Step 63 | Declare Scope 64 | Defined 65 | Definition 66 | Delimit Scope 67 | Derive 68 | Derive Dependent Inversion 69 | Derive Dependent Inversion_clear 70 | Derive Inversion 71 | Derive Inversion_clear 72 | Disable Notation 73 | Drop 74 | 75 | e 76 | Enable Notation 77 | End 78 | Eval 79 | Example 80 | Existing Class 81 | Existing Instance 82 | Existing Instances 83 | Export 84 | Extract Constant 85 | Extract Inductive 86 | Extract Inlined Constant 87 | Extraction 88 | Extraction Blacklist 89 | Extraction Implicit 90 | Extraction Inline 91 | Extraction Language 92 | Extraction Library 93 | Extraction NoInline 94 | Extraction TestCompile 95 | 96 | f 97 | Fact 98 | Fail 99 | Final Obligation 100 | Fixpoint 101 | Focus 102 | From … Dependency 103 | From … Require 104 | Function 105 | Functional Case 106 | Functional Scheme 107 | 108 | g 109 | Generalizable 110 | Generate graph for 111 | Goal 112 | Guarded 113 | 114 | h 115 | Hint Constants 116 | Hint Constructors 117 | Hint Cut 118 | Hint Extern 119 | Hint Immediate 120 | Hint Mode 121 | Hint Opaque 122 | Hint Resolve 123 | Hint Rewrite 124 | Hint Transparent 125 | Hint Unfold 126 | Hint Variables 127 | Hint View for 128 | Hint View for apply 129 | Hint View for move 130 | Hypotheses 131 | Hypothesis 132 | 133 | i 134 | Identity Coercion 135 | Implicit Type 136 | Implicit Types 137 | Import 138 | Include 139 | Include Type 140 | Inductive 141 | Infix 142 | Info 143 | infoH 144 | Inspect 145 | Instance 146 | 147 | l 148 | Lemma 149 | Let 150 | Let CoFixpoint 151 | Let Fixpoint 152 | Load 153 | Locate 154 | Locate File 155 | Locate Library 156 | Locate Ltac 157 | Locate Ltac2 158 | Locate Module 159 | Locate Term 160 | Ltac 161 | Ltac2 162 | Ltac2 Eval 163 | Ltac2 external 164 | Ltac2 Notation 165 | Ltac2 Notation (abbreviation) 166 | Ltac2 Set 167 | Ltac2 Type 168 | 169 | m 170 | Module 171 | Module Type 172 | 173 | n 174 | Next Obligation 175 | Notation 176 | Notation (abbreviation) 177 | Number Notation 178 | 179 | o 180 | Obligation 181 | Obligation Tactic 182 | Obligations 183 | Opaque 184 | Open Scope 185 | Optimize Heap 186 | Optimize Proof 187 | 188 | p 189 | Parameter 190 | Parameters 191 | Prenex Implicits 192 | Preterm 193 | Primitive 194 | Print 195 | Print All 196 | Print All Dependencies 197 | Print Assumptions 198 | Print Canonical Projections 199 | Print Classes 200 | Print Coercion Paths 201 | Print Coercions 202 | Print Custom Grammar 203 | Print Debug GC 204 | Print Equivalent Keys 205 | Print Extraction Blacklist 206 | Print Extraction Inline 207 | Print Fields 208 | Print Firstorder Solver 209 | Print Grammar 210 | Print Graph 211 | Print Hint 212 | Print HintDb 213 | Print Implicit 214 | Print Instances 215 | Print Keywords 216 | Print Libraries 217 | Print LoadPath 218 | Print Ltac 219 | Print Ltac Signatures 220 | Print Ltac2 221 | Print Ltac2 Signatures 222 | Print ML Modules 223 | Print ML Path 224 | Print Module 225 | Print Module Type 226 | Print Namespace 227 | Print Notation 228 | Print Opaque Dependencies 229 | Print Options 230 | Print Registered 231 | Print Rewrite HintDb 232 | Print Rings 233 | Print Scope 234 | Print Scopes 235 | Print Section 236 | Print Strategies 237 | Print Strategy 238 | Print Table 239 | Print Tables 240 | Print Transparent Dependencies 241 | Print Typeclasses 242 | Print Typing Flags 243 | Print Universes 244 | Print Visibility 245 | Proof 246 | Proof `term` 247 | Proof Mode 248 | Proof using 249 | Proof with 250 | Property 251 | Proposition 252 | Pwd 253 | 254 | q 255 | Qed 256 | Quit 257 | 258 | r 259 | Record 260 | Recursive Extraction 261 | Recursive Extraction Library 262 | Redirect 263 | Register 264 | Register Inline 265 | Remark 266 | Remove 267 | Remove Hints 268 | Require 269 | Require Export 270 | Require Import 271 | Reserved Infix 272 | Reserved Notation 273 | Reset 274 | Reset Extraction Blacklist 275 | Reset Extraction Inline 276 | Reset Initial 277 | Reset Ltac Profile 278 | Restart 279 | 280 | s 281 | Save 282 | Scheme 283 | Scheme Boolean Equality 284 | Scheme Equality 285 | Search 286 | SearchPattern 287 | SearchRewrite 288 | Section 289 | Separate Extraction 290 | Set 291 | Show 292 | Show Conjectures 293 | Show Existentials 294 | Show Extraction 295 | Show Goal 296 | Show Intro 297 | Show Intros 298 | Show Lia Profile 299 | Show Ltac Profile 300 | Show Match 301 | Show Obligation Tactic 302 | Show Proof 303 | Show Universes 304 | Show Zify 305 | Solve All Obligations 306 | Solve Obligations 307 | Strategy 308 | String Notation 309 | Structure 310 | SubClass 311 | Succeed 312 | 313 | t 314 | Tactic Notation 315 | Test 316 | Theorem 317 | Time 318 | Timeout 319 | Transparent 320 | Type 321 | Typeclasses eauto 322 | Typeclasses Opaque 323 | Typeclasses Transparent 324 | 325 | u 326 | Undelimit Scope 327 | Undo 328 | Unfocus 329 | Unfocused 330 | Universe 331 | Universes 332 | Unset 333 | Unshelve 334 | 335 | v 336 | Validate Proof 337 | Variable 338 | Variables 339 | Variant 340 | """ 341 | 342 | tactics_data = """ 343 | + 344 | + (backtracking branching) 345 | 346 | = 347 | => 348 | 349 | [ 350 | [ … | … | … ] (dispatch) 351 | [> … | … | … ] (dispatch) 352 | 353 | a 354 | abstract 355 | abstract (ssreflect) 356 | absurd 357 | admit 358 | apply 359 | apply (ssreflect) 360 | assert 361 | assert_fails 362 | assert_succeeds 363 | assumption 364 | auto 365 | autoapply 366 | autorewrite 367 | autounfold 368 | autounfold_one 369 | 370 | b 371 | btauto 372 | bullet (- + *) 373 | by 374 | 375 | c 376 | case 377 | case (ssreflect) 378 | case_eq 379 | casetype 380 | cbn 381 | cbv 382 | change 383 | change_no_check 384 | classical_left 385 | classical_right 386 | clear 387 | clear dependent 388 | clearbody 389 | cofix 390 | compare 391 | compute 392 | congr 393 | congruence 394 | constr_eq 395 | constr_eq_nounivs 396 | constr_eq_strict 397 | constructor 398 | context 399 | contradict 400 | contradiction 401 | cut 402 | cutrewrite 403 | cycle 404 | 405 | d 406 | debug auto 407 | debug eauto 408 | debug trivial 409 | decide 410 | decide equality 411 | decompose 412 | decompose record 413 | decompose sum 414 | dependent destruction 415 | dependent generalize_eqs 416 | dependent generalize_eqs_vars 417 | dependent induction 418 | dependent inversion 419 | dependent inversion_clear 420 | dependent rewrite 421 | dependent simple inversion 422 | destauto 423 | destruct 424 | dfs eauto 425 | dintuition 426 | discriminate 427 | discrR 428 | do 429 | do (ssreflect) 430 | done 431 | dtauto 432 | 433 | e 434 | eapply 435 | eassert 436 | eassumption 437 | easy 438 | eauto 439 | ecase 440 | econstructor 441 | edestruct 442 | ediscriminate 443 | eelim 444 | eenough 445 | eexact 446 | eexists 447 | einduction 448 | einjection 449 | eintros 450 | eleft 451 | elim 452 | elim (ssreflect) 453 | elimtype 454 | enough 455 | epose 456 | epose proof 457 | eremember 458 | erewrite 459 | eright 460 | eset 461 | esimplify_eq 462 | esplit 463 | etransitivity 464 | eval 465 | evar 466 | exact 467 | exact (ssreflect) 468 | exact_no_check 469 | exactly_once 470 | exfalso 471 | exists 472 | 473 | f 474 | f_equal 475 | fail 476 | field 477 | field_lookup 478 | field_simplify 479 | field_simplify_eq 480 | finish_timing 481 | first 482 | first (ssreflect) 483 | first last 484 | firstorder 485 | fix 486 | fold 487 | fresh 488 | fun 489 | functional induction 490 | functional inversion 491 | 492 | g 493 | generalize 494 | generalize dependent 495 | generalize_eqs 496 | generalize_eqs_vars 497 | generally have 498 | gfail 499 | gintuition 500 | give_up 501 | guard 502 | 503 | h 504 | has_evar 505 | have 506 | head_of_constr 507 | hnf 508 | 509 | i 510 | idtac 511 | if-then-else (Ltac2) 512 | in 513 | induction 514 | info_auto 515 | info_eauto 516 | info_trivial 517 | injection 518 | instantiate 519 | intro 520 | intros 521 | intros until 522 | intuition 523 | inversion 524 | inversion_clear 525 | inversion_sigma 526 | is_cofix 527 | is_const 528 | is_constructor 529 | is_evar 530 | is_fix 531 | is_ground 532 | is_ind 533 | is_proj 534 | is_var 535 | 536 | l 537 | lapply 538 | last 539 | last first 540 | lazy 541 | lazy_match! 542 | lazy_match! goal 543 | lazymatch 544 | lazymatch goal 545 | left 546 | let 547 | lia 548 | lra 549 | ltac-seq 550 | 551 | m 552 | match 553 | match (Ltac2) 554 | match goal 555 | match! 556 | match! goal 557 | move 558 | move (ssreflect) 559 | multi_match! 560 | multi_match! goal 561 | multimatch 562 | multimatch goal 563 | 564 | n 565 | native_cast_no_check 566 | native_compute 567 | nia 568 | not_evar 569 | now 570 | now_show 571 | nra 572 | nsatz 573 | nsatz_compute 574 | numgoals 575 | 576 | o 577 | once 578 | only 579 | optimize_heap 580 | over 581 | 582 | p 583 | pattern 584 | pose 585 | pose (ssreflect) 586 | pose proof 587 | progress 588 | protect_fv 589 | psatz 590 | 591 | r 592 | rapply 593 | red 594 | refine 595 | reflexivity 596 | remember 597 | rename 598 | repeat 599 | replace 600 | reset ltac profile 601 | restart_timer 602 | revert 603 | revert dependent 604 | revgoals 605 | rewrite 606 | rewrite (ssreflect) 607 | rewrite * 608 | rewrite_db 609 | rewrite_strat 610 | right 611 | ring 612 | ring_lookup 613 | ring_simplify 614 | rtauto 615 | 616 | s 617 | set 618 | set (ssreflect) 619 | setoid_etransitivity 620 | setoid_reflexivity 621 | setoid_replace 622 | setoid_rewrite 623 | setoid_symmetry 624 | setoid_transitivity 625 | shelve 626 | shelve_unifiable 627 | show ltac profile 628 | simpl 629 | simple apply 630 | simple congruence 631 | simple destruct 632 | simple eapply 633 | simple induction 634 | simple injection 635 | simple inversion 636 | simple subst 637 | simplify_eq 638 | soft functional induction 639 | solve 640 | solve_constraints 641 | specialize 642 | specialize_eqs 643 | split 644 | split_Rabs 645 | split_Rmult 646 | start ltac profiling 647 | stepl 648 | stepr 649 | stop ltac profiling 650 | subst 651 | substitute 652 | suff 653 | suffices 654 | swap 655 | symmetry 656 | 657 | t 658 | tauto 659 | time 660 | time_constr 661 | timeout 662 | transitivity 663 | transparent_abstract 664 | trivial 665 | try 666 | tryif 667 | type of 668 | type_term 669 | typeclasses eauto 670 | 671 | u 672 | under 673 | unfold 674 | unify 675 | unlock 676 | unshelve 677 | 678 | v 679 | vm_cast_no_check 680 | vm_compute 681 | 682 | w 683 | with_strategy 684 | without loss 685 | wlia 686 | wlog 687 | wlra_Q 688 | wnia 689 | wnra_Q 690 | wpsatz_Q 691 | wpsatz_Z 692 | wsos_Q 693 | wsos_Z 694 | 695 | x 696 | xlia 697 | xlra_Q 698 | xlra_R 699 | xnia 700 | xnra_Q 701 | xnra_R 702 | xpsatz_Q 703 | xpsatz_R 704 | xpsatz_Z 705 | xsos_Q 706 | xsos_R 707 | xsos_Z 708 | 709 | z 710 | zify 711 | zify_elim_let 712 | zify_iter_let 713 | zify_iter_specs 714 | zify_op 715 | zify_saturate 716 | """ 717 | 718 | def extract(data): 719 | result = map(lambda l: l.strip("\t "), data.splitlines()) 720 | result = filter(lambda l: len(l) > 1, result) 721 | result = map(lambda l: l.split(" ")[0], result) 722 | result = filter(lambda l: l[0].isalpha(), result) 723 | result = map(lambda l: l.rstrip(":"), result) 724 | result = sorted(set(result)) 725 | return result 726 | 727 | print("Commands:") 728 | for command in extract(commands_data): 729 | print('"{}",'.format(command)) 730 | 731 | print("Tactics:") 732 | for tactic in extract(tactics_data): 733 | print('"{}",'.format(tactic)) 734 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EugeneLoy/coq_jupyter/ca58efef3fc315c16b339a6b6e609e49efa2a636/screenshot.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | readme = """Jupyter kernel for Coq. 4 | See: https://github.com/EugeneLoy/coq_jupyter 5 | """ 6 | 7 | setup( 8 | name='coq_jupyter', 9 | version='1.6.2', 10 | packages=['coq_jupyter'], 11 | description='Coq kernel for Jupyter', 12 | long_description=readme, 13 | author='Eugene Loy', 14 | author_email='eugeny.loy@gmail.com', 15 | url='https://github.com/EugeneLoy/coq_jupyter', 16 | include_package_data=True, 17 | install_requires=[ 18 | 'jupyter_client', 19 | 'IPython', 20 | 'ipykernel', 21 | 'future', 22 | 'pexpect>=4.0' 23 | ], 24 | classifiers=[ 25 | 'Development Status :: 4 - Beta', 26 | 'License :: OSI Approved :: Apache Software License', 27 | 'Programming Language :: Python :: 3', 28 | 'Operating System :: POSIX :: Linux', 29 | 'Framework :: Jupyter', 30 | 'Intended Audience :: Education', 31 | 'Intended Audience :: Developers', 32 | 'Intended Audience :: Science/Research', 33 | 'Topic :: Software Development' 34 | ], 35 | ) 36 | -------------------------------------------------------------------------------- /test/container_test_entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export OPAMROOT=/home/coq/.opam 4 | eval $(opam env) 5 | 6 | sudo apt-get update 7 | sudo apt-get install -y python3-pip 8 | 9 | python3 --version 10 | coqtop --version 11 | 12 | sudo pip3 install --upgrade --force-reinstall /github/workspace/dist/coq_jupyter-*.tar.gz 'jupyter_client<=6.1.12' 'jupyter_kernel_test<=0.3' 13 | sudo python3 -m coq_jupyter.install 14 | 15 | python3 /github/workspace/test/kernel_test.py -v 16 | -------------------------------------------------------------------------------- /test/kernel_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import jupyter_kernel_test 3 | import os 4 | 5 | class KernelTests(jupyter_kernel_test.KernelTests): 6 | 7 | # Required by jupyter_kernel_test.KernelTests: 8 | kernel_name = "coq" 9 | language_name = "coq" 10 | 11 | _coq_version = tuple([int(i) for i in os.environ.get("COQ_VERSION", "1000.0.0").split(".")]) 12 | _counter = iter(range(1, 100)) 13 | 14 | def _build_sum_command(self): 15 | lhs = 100 16 | rhs = next(self._counter) 17 | result = str(lhs + rhs) 18 | command = "Compute {} + {}.".format(lhs, rhs) 19 | return (result, command) 20 | 21 | def _execute_cell(self, code): 22 | self.flush_channels() 23 | reply, output_msgs = self.execute_helper(code=code) 24 | 25 | self.assertEqual(reply['content']['status'], 'ok') 26 | self.assertEqual(len(output_msgs), 1) 27 | self.assertEqual(output_msgs[0]['msg_type'], 'execute_result') 28 | self.assertIn('text/plain', output_msgs[0]['content']['data']) 29 | 30 | return output_msgs[0]['content']['data']['text/plain'] 31 | 32 | def setUp(self): 33 | self._execute_cell("Reset Initial.") 34 | 35 | def test_coq_jupyter____executing_empty_code____should_not_print_anything(self): 36 | self.flush_channels() 37 | reply, output_msgs = self.execute_helper(code="") 38 | 39 | self.assertEqual(reply['content']['status'], 'ok') 40 | self.assertEqual(len(output_msgs), 0) 41 | 42 | def test_coq_jupyter____executing_code_without_coq_content____should_not_print_anything(self): 43 | self.flush_channels() 44 | reply, output_msgs = self.execute_helper(code="\n\r\t \n\r\t ") 45 | 46 | self.assertEqual(reply['content']['status'], 'ok') 47 | self.assertEqual(len(output_msgs), 0) 48 | 49 | def test_coq_jupyter____executing_one_command____prints_computed_command_result(self): 50 | (expected_result, command) = self._build_sum_command() 51 | result = self._execute_cell(command) 52 | self.assertIn(expected_result, result) 53 | 54 | def test_coq_jupyter____executing_one_command____does_not_print_command(self): 55 | (expected_result, command) = self._build_sum_command() 56 | result = self._execute_cell(command) 57 | self.assertNotIn(command, result) 58 | 59 | def test_coq_jupyter____executing_compute_command_when_not_proving____does_not_print_proving_context(self): 60 | (expected_result, command) = self._build_sum_command() 61 | result = self._execute_cell(command) 62 | self.assertNotIn("proving:", result) 63 | self.assertNotIn("subgoal", result) 64 | 65 | def test_coq_jupyter____executing_multiple_commands____prints_computed_command_results(self): 66 | (expected_result1, command1) = self._build_sum_command() 67 | (expected_result2, command2) = self._build_sum_command() 68 | result = self._execute_cell(command1 + " " + command2) 69 | self.assertIn(expected_result1, result) 70 | self.assertIn(expected_result2, result) 71 | 72 | def test_coq_jupyter____executing_commands_when_proving____prints_proving_context(self): 73 | result = self._execute_cell("Theorem t1 : True.") 74 | self.assertIn("1 subgoal", result) 75 | self.assertIn("Proving: t1", result) 76 | 77 | result = self._execute_cell(self._build_sum_command()[1]) 78 | self.assertIn("1 subgoal", result) 79 | self.assertIn("Proving: t1", result) 80 | 81 | result = self._execute_cell("Proof. pose (i1 := I).") 82 | self.assertIn("1 subgoal", result) 83 | self.assertIn("i1 := I : True", result) 84 | self.assertIn("Proving: t1", result) 85 | 86 | result = self._execute_cell(self._build_sum_command()[1]) 87 | self.assertIn("1 subgoal", result) 88 | self.assertIn("i1 := I : True", result) 89 | self.assertIn("Proving: t1", result) 90 | 91 | result = self._execute_cell("exact i1. Qed.") 92 | self.assertNotIn("1 subgoal", result) 93 | self.assertNotIn("No more subgoals", result) 94 | self.assertNotIn("i1 := I : True", result) 95 | self.assertNotIn("Proving:", result) 96 | 97 | result = self._execute_cell(self._build_sum_command()[1]) 98 | self.assertNotIn("1 subgoal", result) 99 | self.assertNotIn("No more subgoals", result) 100 | self.assertNotIn("i1 := I : True", result) 101 | self.assertNotIn("proving:", result) 102 | 103 | def test_coq_jupyter____when_proving____prints_most_recent_proving_context_once(self): 104 | result = self._execute_cell("Theorem t3 : bool. Proof. pose (b1 := true). pose (b2 := false).") 105 | self.assertEqual(result.count("Proving: t3"), 1, "result: " + repr(result)) 106 | self.assertEqual(result.count("1 subgoal"), 1, "result: " + repr(result)) 107 | self.assertEqual(result.count("No more subgoals"), 0, "result: " + repr(result)) 108 | self.assertEqual(result.count("b1 := true : bool"), 1, "result: " + repr(result)) 109 | self.assertEqual(result.count("b2 := false : bool"), 1, "result: " + repr(result)) 110 | 111 | result = self._execute_cell("exact b2.") 112 | self.assertEqual(result.count("Proving: t3"), 1, "result: " + repr(result)) 113 | self.assertEqual(result.count("1 subgoal"), 0, "result: " + repr(result)) 114 | self.assertEqual(result.count("No more subgoals"), 1, "result: " + repr(result)) 115 | self.assertEqual(result.count("b1 := true : bool"), 0, "result: " + repr(result)) 116 | self.assertEqual(result.count("b2 := false : bool"), 0, "result: " + repr(result)) 117 | 118 | def _build_commands_with_error_fixture(self, t_base, t, valid_command_template, invalid_command_template, expected_error_message): 119 | return ( 120 | "t{}_{}".format(t_base + t, t), 121 | ["t{}_{}".format(t_base + t, i) for i in (1, 2, 3) if i != t], 122 | ("t{}_0".format(t_base + t), valid_command_template.format(t_base + t, 0)), 123 | "\n".join([ 124 | valid_command_template.format(t_base + t, i) if i != t else invalid_command_template.format(t_base + t, i) 125 | for i in (1, 2, 3) 126 | ]), 127 | expected_error_message 128 | ) 129 | 130 | def test_coq_jupyter____executing_commands_with_error____prints_error_and_rolls_back(self): 131 | cases_with_reference_error = [ 132 | self._build_commands_with_error_fixture( 133 | 3, 134 | t, 135 | "Definition t{}_{} := I.", 136 | "Definition t{}_{} := INVALID_REFERENCE.", 137 | "Error: The reference INVALID_REFERENCE was not found" 138 | ) 139 | for t in (1,2,3) 140 | ] 141 | cases_with_syntax_error = [ 142 | self._build_commands_with_error_fixture( 143 | 6, 144 | t, 145 | "Definition t{}_{} := I.", 146 | "Definition t{}_{} := (I.", 147 | "Syntax error: ',' or ')' expected after" 148 | ) 149 | for t in (1,2,3) 150 | ] 151 | cases_with_incomplete_command_error = [ 152 | self._build_commands_with_error_fixture( 153 | 9, 154 | t, 155 | "Definition t{}_{} := I.", 156 | "Definition t{}_{} := I", 157 | "Syntax error: '.' expected after" 158 | ) 159 | for t in (1,2,3) 160 | ] 161 | fixture = cases_with_reference_error + cases_with_syntax_error + cases_with_incomplete_command_error 162 | 163 | for f in range(len(fixture)): 164 | (invalid_definition, valid_definitions, commited_definition, code, expected_error_message) = fixture[f] 165 | 166 | self._execute_cell(commited_definition[1]) 167 | 168 | result = self._execute_cell(code) 169 | self.assertIn(expected_error_message, result, msg="fixture: {}".format(repr(fixture[f]))) 170 | 171 | # verify roll back 172 | result = self._execute_cell("Print All.") 173 | self.assertIn(commited_definition[0], result, msg="fixture: {}".format(repr(fixture[f]))) 174 | self.assertNotIn(invalid_definition, result, msg="fixture: {}".format(repr(fixture[f]))) 175 | self.assertNotIn(valid_definitions[0], result, msg="fixture: {}".format(repr(fixture[f]))) 176 | self.assertNotIn(valid_definitions[1], result, msg="fixture: {}".format(repr(fixture[f]))) 177 | 178 | def test_coq_jupyter____when_executing_command_that_results_in_warning____prints_warning(self): 179 | # this test ensures fix of the following: 180 | # https://github.com/EugeneLoy/coq_jupyter/issues/21 181 | # https://github.com/EugeneLoy/coq_jupyter/issues/23 182 | 183 | result = self._execute_cell("Compute 5001.") 184 | 185 | self.assertIn("Warning: ", result) 186 | self.assertNotIn("", result) 187 | 188 | def test_coq_jupyter____when_executing_command_that_results_in_error____prints_error_once(self): 189 | result = self._execute_cell("Compute INVALID_REFERENCE.") 190 | 191 | self.assertEqual(result.count("Error: The reference INVALID_REFERENCE was not found"), 1, "result: " + repr(result)) 192 | self.assertNotIn("", result) 193 | 194 | def test_coq_jupyter____when_executing_command_that_results_in_notice_message____does_not_print_notice_message_level(self): 195 | (_, command) = self._build_sum_command() 196 | result = self._execute_cell(command) 197 | 198 | self.assertNotIn("notice", result.lower()) 199 | 200 | def test_coq_jupyter____executing_code_with_unclosed_comment____prints_error(self): 201 | (_, command) = self._build_sum_command() 202 | result = self._execute_cell(command + " (* ") 203 | 204 | self.assertIn("Unterminated comment", result) 205 | 206 | def test_coq_jupyter____executing_code_surrounded_by_unclosed_comments____prints_evaluation_result(self): 207 | if self._coq_version >= (8,10,0): 208 | self.skipTest("skipping due to coq version: {}".format(self._coq_version)) # TODO skipping might not be the best solution here 209 | 210 | (expected_result, command) = self._build_sum_command() 211 | result = self._execute_cell("(* comment *)" + command + "(* comment *)") 212 | 213 | self.assertIn(expected_result, result) 214 | self.assertNotIn("error", result.lower()) 215 | 216 | def test_coq_jupyter____executing_code_with_comments_woven_in____prints_evaluation_result(self): 217 | result = self._execute_cell("Check (* some comment with '.' in the middle *) I.") 218 | 219 | self.assertIn("True", result) 220 | self.assertNotIn("error", result.lower()) 221 | 222 | def test_coq_jupyter____executing_code_comments_only____does_not_result_in_error(self): 223 | if self._coq_version >= (8,10,0): 224 | self.skipTest("skipping due to coq version: {}".format(self._coq_version)) # TODO skipping might not be the best solution here 225 | 226 | result = self._execute_cell("(* comment *)") 227 | 228 | self.assertNotIn("error", result.lower()) 229 | 230 | def test_coq_jupyter____executing_code_with_non_xml_symbols____prints_evaluation_result(self): 231 | code = """ 232 | Compute 233 | match 0 with 234 | | 0 => 1 + 1 235 | | S n' => n' 236 | end. 237 | """ 238 | result = self._execute_cell(code) 239 | 240 | self.assertIn("2", result, msg="Code:\n{}".format(code)) 241 | self.assertNotIn("error", result.lower(), msg="Code:\n{}".format(code)) 242 | 243 | def test_coq_jupyter____executing_long_running_code_____prints_evaluation_result(self): 244 | code = "Goal True. timeout 10 (repeat eapply proj1)." 245 | result = self._execute_cell(code) 246 | 247 | self.assertIn("Tactic timeout", result) 248 | 249 | def test_coq_jupyter____executing_code_with_undotted_separators____prints_evaluation_result_for_every_statement(self): 250 | fixture = [ 251 | ("-", "-", ""), 252 | ("*", "*", ""), 253 | ("+", "+", ""), 254 | ("---", "---", ""), 255 | ] 256 | if self._coq_version >= (8,9,0): 257 | fixture += [ 258 | ("{", "{", "}"), 259 | ("1:{", "1 : {", "}"), 260 | ("[G1]:{", "[ g2_' ] : {", "}") 261 | ] 262 | 263 | for (opening_separator1, opening_separator2, closing_separator) in fixture: 264 | (expected_results, commands) = zip(*[self._build_sum_command() for _ in range(4)]) 265 | code = """ 266 | Goal True /\ True. 267 | split ; [ refine ?[G1] | refine ?[g2_'] ]. 268 | {0} {3} 269 | {4} 270 | exact I. 271 | {2} 272 | {1} {5} 273 | {6} 274 | exact I. 275 | {2} 276 | Qed. 277 | """.format(opening_separator1, opening_separator2, closing_separator, *commands) 278 | 279 | result = self._execute_cell(code) 280 | 281 | for expected_result in expected_results: 282 | self.assertIn(expected_result, result, msg="Code:\n{}".format(code)) 283 | self.assertNotIn("error", result.lower(), msg="Code:\n{}".format(code)) 284 | 285 | def test_coq_jupyter____executing_code_that_completes_subproof_while_having_unfocused_goals____prints_info_about_unfocused_goals(self): 286 | marker1 = next(self._counter) 287 | marker2 = next(self._counter) 288 | marker3 = next(self._counter) 289 | code = """ 290 | Goal {0}={0} /\ {1}={1} /\ {2}={2}. 291 | split ; [ | split]. 292 | - easy. 293 | """.format(marker1, marker2, marker3) 294 | 295 | result = self._execute_cell(code) 296 | 297 | self.assertIn("This subproof is complete, but there are some unfocused goals:", result, msg="Code:\n{}".format(code)) 298 | self.assertIn("{0} = {0}".format(marker2), result, msg="Code:\n{}".format(code)) 299 | self.assertIn("{0} = {0}".format(marker3), result, msg="Code:\n{}".format(code)) 300 | self.assertNotIn("error", result.lower(), msg="Code:\n{}".format(code)) 301 | 302 | if __name__ == '__main__': 303 | unittest.main() 304 | --------------------------------------------------------------------------------